diff --git a/package-lock.json b/package-lock.json index d235ae069..58b473dcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tari-universe", - "version": "0.8.45", + "version": "0.8.49", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tari-universe", - "version": "0.8.45", + "version": "0.8.49", "dependencies": { "@floating-ui/react": "^0.26.28", "@lottiefiles/dotlottie-react": "^0.12.0", diff --git a/package.json b/package.json index 770cb93f5..4f12b03b9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tari-universe", "private": true, - "version": "0.8.48", + "version": "0.8.49", "type": "module", "scripts": { "dev": "vite dev --mode development", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index db5067678..8cce71e90 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6981,7 +6981,7 @@ dependencies = [ [[package]] name = "tari-universe" -version = "0.8.48" +version = "0.8.49" dependencies = [ "anyhow", "async-trait", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 942d9092b..694798d73 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -4,7 +4,7 @@ description = "Tari Universe" edition = "2021" name = "tari-universe" repository = "https://github.com/tari-project/universe" -version = "0.8.48" +version = "0.8.49" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src-tauri/src/airdrop.rs b/src-tauri/src/airdrop.rs new file mode 100644 index 000000000..a349c714d --- /dev/null +++ b/src-tauri/src/airdrop.rs @@ -0,0 +1,118 @@ +// Copyright 2024. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use log::{info, warn}; +use serde::{Deserialize, Serialize}; + +use crate::{mm_proxy_adapter::MergeMiningProxyConfig, UniverseAppState}; + +const LOG_TARGET: &str = "tari::universe::airdrop"; + +#[derive(Debug, Deserialize, Serialize)] +pub struct AirdropAccessToken { + pub exp: u64, + pub iat: i32, + pub id: String, + pub provider: String, + pub role: String, + pub scope: String, +} + +pub fn decode_jwt_claims(t: &str) -> Option { + let key = DecodingKey::from_secret(&[]); + let mut validation = Validation::new(Algorithm::HS256); + validation.insecure_disable_signature_validation(); + + match decode::(t, &key, &validation) { + Ok(data) => Some(data.claims), + Err(e) => { + warn!(target: LOG_TARGET,"Error decoding access token: {:?}", e); + None + } + } +} + +pub fn decode_jwt_claims_without_exp(t: &str) -> Option { + let key = DecodingKey::from_secret(&[]); + let mut validation = Validation::new(Algorithm::HS256); + validation.insecure_disable_signature_validation(); + validation.validate_exp = false; + + match decode::(t, &key, &validation) { + Ok(data) => Some(data.claims), + Err(e) => { + warn!(target: LOG_TARGET,"Error decoding access token without exp: {:?}", e); + None + } + } +} + +pub async fn validate_jwt(airdrop_access_token: Option) -> Option { + airdrop_access_token.and_then(|t| { + let claims = decode_jwt_claims(&t); + + let now = std::time::SystemTime::now(); + let now_unix = now + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + if let Some(claims) = claims { + if claims.exp < now_unix { + warn!(target: LOG_TARGET,"Access token has expired"); + None + } else { + Some(t) + } + } else { + None + } + }) +} + +pub async fn restart_mm_proxy_with_new_telemetry_id( + state: tauri::State<'_, UniverseAppState>, +) -> Result<(), String> { + info!(target: LOG_TARGET, "Restarting mm_proxy"); + let telemetry_id = state + .telemetry_manager + .read() + .await + .get_unique_string() + .await; + let mm_proxy_manager_config = state + .mm_proxy_manager + .config() + .await + .ok_or("mm proxy config could not be found")?; + let _unused = state + .mm_proxy_manager + .change_config(MergeMiningProxyConfig { + coinbase_extra: telemetry_id.clone(), + ..mm_proxy_manager_config + }) + .await + .map_err(|e| e.to_string()); + info!(target: LOG_TARGET, "mm_proxy restarted"); + Ok(()) +} diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index 34c614ef0..f33242f78 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -112,6 +112,8 @@ pub struct AppConfigFromFile { p2pool_stats_server_port: Option, #[serde(default = "default_false")] pre_release: bool, + #[serde(default)] + airdrop_tokens: Option, } impl Default for AppConfigFromFile { @@ -154,6 +156,7 @@ impl Default for AppConfigFromFile { show_experimental_settings: false, p2pool_stats_server_port: default_p2pool_stats_server_port(), pre_release: false, + airdrop_tokens: None, } } } @@ -165,6 +168,12 @@ pub enum DisplayMode { Light, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AirdropTokens { + pub token: String, + pub refresh_token: String, +} + impl DisplayMode { pub fn from_str(s: &str) -> Option { match s { @@ -266,6 +275,7 @@ pub(crate) struct AppConfig { show_experimental_settings: bool, p2pool_stats_server_port: Option, pre_release: bool, + airdrop_tokens: Option, } impl AppConfig { @@ -310,6 +320,7 @@ impl AppConfig { keyring_accessed: false, p2pool_stats_server_port: default_p2pool_stats_server_port(), pre_release: false, + airdrop_tokens: None, } } @@ -387,6 +398,7 @@ impl AppConfig { self.show_experimental_settings = config.show_experimental_settings; self.p2pool_stats_server_port = config.p2pool_stats_server_port; self.pre_release = config.pre_release; + self.airdrop_tokens = config.airdrop_tokens; KEYRING_ACCESSED.store( config.keyring_accessed, @@ -506,6 +518,19 @@ impl AppConfig { self.custom_max_gpu_usage.clone() } + pub fn airdrop_tokens(&self) -> Option { + self.airdrop_tokens.clone() + } + + pub async fn set_airdrop_tokens( + &mut self, + airdrop_tokens: Option, + ) -> Result<(), anyhow::Error> { + self.airdrop_tokens = airdrop_tokens; + self.update_config_file().await?; + Ok(()) + } + pub async fn set_max_gpu_usage( &mut self, custom_max_gpu_usage: Vec, @@ -786,6 +811,7 @@ impl AppConfig { show_experimental_settings: self.show_experimental_settings, p2pool_stats_server_port: self.p2pool_stats_server_port, pre_release: self.pre_release, + airdrop_tokens: self.airdrop_tokens.clone(), }; let config = serde_json::to_string(config)?; debug!(target: LOG_TARGET, "Updating config file: {:?} {:?}", file, self.clone()); diff --git a/src-tauri/src/app_in_memory_config.rs b/src-tauri/src/app_in_memory_config.rs index 28152798a..5e799c406 100644 --- a/src-tauri/src/app_in_memory_config.rs +++ b/src-tauri/src/app_in_memory_config.rs @@ -42,7 +42,6 @@ const TELEMETRY_API_URL: &str = pub struct AppInMemoryConfig { pub airdrop_url: String, pub airdrop_api_url: String, - pub airdrop_access_token: Option, pub telemetry_api_url: String, } @@ -67,7 +66,6 @@ impl Default for AppInMemoryConfig { AppInMemoryConfig { airdrop_url: "https://airdrop.tari.com".into(), airdrop_api_url: "https://ut.tari.com".into(), - airdrop_access_token: None, telemetry_api_url: "https://ut.tari.com/push".into(), } } @@ -112,7 +110,6 @@ impl AppInMemoryConfig { return AppInMemoryConfig { airdrop_url: AIRDROP_BASE_URL.into(), airdrop_api_url: AIRDROP_API_BASE_URL.into(), - airdrop_access_token: None, telemetry_api_url: TELEMETRY_API_URL.into(), }; @@ -120,7 +117,6 @@ impl AppInMemoryConfig { return AppInMemoryConfig { airdrop_url: "http://localhost:4000".into(), airdrop_api_url: "http://localhost:3004".into(), - airdrop_access_token: None, telemetry_api_url: "http://localhost:3004".into(), }; diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 9e94e2550..9ff3a40d2 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -20,7 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use crate::app_config::{AppConfig, GpuThreads}; +use crate::app_config::{AirdropTokens, AppConfig, GpuThreads}; use crate::app_in_memory_config::{ get_der_encode_pub_key, get_websocket_key, AirdropInMemoryConfig, }; @@ -40,7 +40,7 @@ use crate::tor_adapter::TorConfig; use crate::utils::shutdown_utils::stop_all_processes; use crate::wallet_adapter::{TransactionInfo, WalletBalance}; use crate::wallet_manager::WalletManagerError; -use crate::{node_adapter, UniverseAppState, APPLICATION_FOLDER_ID}; +use crate::{airdrop, node_adapter, UniverseAppState, APPLICATION_FOLDER_ID}; use base64::prelude::*; use keyring::Entry; @@ -704,6 +704,20 @@ pub async fn get_tor_entry_guards( Ok(res) } +#[tauri::command] +pub async fn get_airdrop_tokens( + _window: tauri::Window, + state: tauri::State<'_, UniverseAppState>, + _app: tauri::AppHandle, +) -> Result, String> { + let timer = Instant::now(); + let airdrop_access_token = state.config.read().await.airdrop_tokens(); + if timer.elapsed() > MAX_ACCEPTABLE_COMMAND_TIME { + warn!(target: LOG_TARGET, "get_airdrop_tokens took too long: {:?}", timer.elapsed()); + } + Ok(airdrop_access_token) +} + #[tauri::command] pub async fn get_coinbase_transactions( state: tauri::State<'_, UniverseAppState>, @@ -1406,6 +1420,73 @@ pub async fn set_visual_mode<'r>( Ok(()) } +#[allow(clippy::too_many_lines)] +#[tauri::command] +pub async fn set_airdrop_tokens<'r>( + airdrop_tokens: Option, + state: tauri::State<'_, UniverseAppState>, + app: tauri::AppHandle, +) -> Result<(), String> { + let old_tokens = state.config.clone().read().await.airdrop_tokens(); + let old_id = old_tokens.clone().and_then(|tokens| { + airdrop::decode_jwt_claims_without_exp(&tokens.token).map(|claim| claim.id) + }); + let new_id = airdrop_tokens.clone().and_then(|tokens| { + airdrop::decode_jwt_claims_without_exp(&tokens.token).map(|claim| claim.id) + }); + + let user_id_changed = old_id != new_id; + + let mut app_config_lock = state.config.write().await; + app_config_lock + .set_airdrop_tokens(airdrop_tokens) + .await + .map_err(|e| e.to_string())?; + drop(app_config_lock); + + info!(target: LOG_TARGET, "New Airdrop tokens saved, user id changed:{:?}", user_id_changed); + if user_id_changed { + let currently_mining = { + let node_status = state.base_node_latest_status.borrow().clone(); + let cpu_miner = state.cpu_miner.read().await; + let cpu_mining_status = match cpu_miner + .status( + node_status.randomx_network_hashrate, + node_status.block_reward, + ) + .await + .map_err(|e| e.to_string()) + { + Ok(cpu) => cpu, + Err(e) => { + warn!(target: LOG_TARGET, "Error getting cpu miner status: {:?}", e); + state + .is_getting_miner_metrics + .store(false, Ordering::SeqCst); + return Err(e); + } + }; + let gpu_mining_status = state.gpu_latest_status.borrow().clone(); + cpu_mining_status.is_mining || gpu_mining_status.is_mining + }; + + if currently_mining { + stop_mining(state.clone()) + .await + .map_err(|e| e.to_string())?; + + airdrop::restart_mm_proxy_with_new_telemetry_id(state.clone()).await?; + + start_mining(state.clone(), app.clone()) + .await + .map_err(|e| e.to_string())?; + } else { + airdrop::restart_mm_proxy_with_new_telemetry_id(state.clone()).await?; + } + } + Ok(()) +} + #[allow(clippy::too_many_lines)] #[tauri::command] pub async fn start_mining<'r>( @@ -1420,6 +1501,8 @@ pub async fn start_mining<'r>( let mode = config.mode(); let custom_cpu_usage = config.custom_cpu_usage(); let custom_gpu_usage = config.custom_gpu_usage(); + let cpu_miner_running = state.cpu_miner.read().await.is_running().await; + let gpu_miner_running = state.gpu_miner.read().await.is_running().await; let cpu_miner_config = state.cpu_miner_config.read().await; let tari_address = cpu_miner_config.tari_address.clone(); @@ -1431,7 +1514,8 @@ pub async fn start_mining<'r>( .await .get_unique_string() .await; - if cpu_mining_enabled { + + if cpu_mining_enabled && !cpu_miner_running { let mm_proxy_port = state .mm_proxy_manager .get_monero_port() @@ -1476,7 +1560,7 @@ pub async fn start_mining<'r>( let gpu_available = state.gpu_miner.read().await.is_gpu_mining_available(); info!(target: LOG_TARGET, "Gpu availability {:?} gpu_mining_enabled {}", gpu_available.clone(), gpu_mining_enabled); - if gpu_mining_enabled && gpu_available { + if gpu_mining_enabled && gpu_available && !gpu_miner_running { info!(target: LOG_TARGET, "1. Starting gpu miner"); // let tari_address = state.cpu_miner_config.read().await.tari_address.clone(); // let p2pool_enabled = state.config.read().await.p2pool_enabled(); @@ -1549,6 +1633,7 @@ pub async fn stop_mining<'r>(state: tauri::State<'_, UniverseAppState>) -> Resul .stop() .await .map_err(|e| e.to_string())?; + info!(target:LOG_TARGET, "cpu miner stopped"); state .gpu_miner @@ -1557,6 +1642,8 @@ pub async fn stop_mining<'r>(state: tauri::State<'_, UniverseAppState>) -> Resul .stop() .await .map_err(|e| e.to_string())?; + info!(target:LOG_TARGET, "gpu miner stopped"); + if timer.elapsed() > MAX_ACCEPTABLE_COMMAND_TIME { warn!(target: LOG_TARGET, "stop_mining took too long: {:?}", timer.elapsed()); } diff --git a/src-tauri/src/feedback.rs b/src-tauri/src/feedback.rs index f55130fad..b1330f4b0 100644 --- a/src-tauri/src/feedback.rs +++ b/src-tauri/src/feedback.rs @@ -158,11 +158,11 @@ impl Feedback { }; let jwt = self - .in_memory_config + .config .read() .await - .airdrop_access_token - .clone(); + .airdrop_tokens() + .map(|tokens| tokens.token); // Send the POST request let mut req = reqwest::Client::new().post(feedback_url).multipart(form); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3c8f854ab..e76c784e6 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -51,7 +51,7 @@ use tari_common::configuration::Network; use tari_common_types::tari_address::TariAddress; use tari_shutdown::Shutdown; use tauri::async_runtime::{block_on, JoinHandle}; -use tauri::{Emitter, Listener, Manager, RunEvent}; +use tauri::{Emitter, Manager, RunEvent}; use tauri_plugin_sentry::{minidump, sentry}; use tokio::select; use tokio::sync::{Mutex, RwLock}; @@ -86,6 +86,7 @@ use crate::wallet_manager::WalletManager; use utils::macos_utils::is_app_in_applications_folder; use utils::shutdown_utils::stop_all_processes; +mod airdrop; mod app_config; mod app_in_memory_config; mod auto_launcher; @@ -250,7 +251,7 @@ async fn setup_inner( .telemetry_manager .write() .await - .initialize(state.airdrop_access_token.clone(), app.clone()) + .initialize(app.clone()) .await?; let mut telemetry_id = state @@ -785,7 +786,6 @@ struct UniverseAppState { telemetry_manager: Arc>, telemetry_service: Arc>, feedback: Arc>, - airdrop_access_token: Arc>>, p2pool_manager: P2poolManager, tor_manager: TorManager, updates_manager: UpdatesManager, @@ -802,7 +802,7 @@ struct Payload { #[derive(Clone, serde::Serialize, serde::Deserialize)] struct FEPayload { - token: String, + token: Option, } #[derive(Clone, serde::Serialize)] @@ -904,7 +904,6 @@ fn main() { telemetry_manager: Arc::new(RwLock::new(telemetry_manager)), telemetry_service: Arc::new(RwLock::new(telemetry_service)), feedback: Arc::new(RwLock::new(feedback)), - airdrop_access_token: Arc::new(RwLock::new(None)), tor_manager, updates_manager, cached_p2pool_connections: Arc::new(RwLock::new(None)), @@ -985,26 +984,6 @@ fn main() { return Err(Box::new(e)); } }; - - - let token_state_clone = app.state::().airdrop_access_token.clone(); - let memory_state_clone = app.state::().in_memory_config.clone(); - app.listen("airdrop_token", move |event| { - let token_value = token_state_clone.clone(); - let memory_value = memory_state_clone.clone(); - tauri::async_runtime::spawn(async move { - info!(target: LOG_TARGET, "Getting token from Frontend"); - let payload = event.payload(); - let res = serde_json::from_str::(payload).expect("No token"); - - let token = res.token; - let mut lock = token_value.write().await; - *lock = Some(token.clone()); - - let mut in_memory_app_config = memory_value.write().await; - in_memory_app_config.airdrop_access_token = Some(token); - }); - }); // The start of needed restart operations. Break this out into a module if we need n+1 let tcp_tor_toggled_file = config_path.join("tcp_tor_toggled"); if tcp_tor_toggled_file.exists() { @@ -1181,6 +1160,8 @@ fn main() { commands::try_update, commands::get_network, commands::sign_ws_data, + commands::set_airdrop_tokens, + commands::get_airdrop_tokens ]) .build(tauri::generate_context!()) .inspect_err( diff --git a/src-tauri/src/mm_proxy_manager.rs b/src-tauri/src/mm_proxy_manager.rs index 1171d3d28..52c63b7a1 100644 --- a/src-tauri/src/mm_proxy_manager.rs +++ b/src-tauri/src/mm_proxy_manager.rs @@ -52,6 +52,22 @@ pub(crate) struct StartConfig { pub use_monero_fail: bool, } +impl StartConfig { + fn override_by(&self, override_by: MergeMiningProxyConfig) -> Self { + let cloned = self.clone(); + Self { + p2pool_enabled: override_by.p2pool_enabled, + base_node_grpc_port: override_by.base_node_grpc_port, + p2pool_port: override_by.p2pool_grpc_port, + coinbase_extra: override_by.coinbase_extra, + tari_address: override_by.tari_address, + use_monero_fail: override_by.use_monero_fail, + monero_nodes: override_by.monero_nodes, + ..cloned + } + } +} + pub struct MmProxyManager { watcher: Arc>>, start_config: Arc>>, @@ -86,16 +102,17 @@ impl MmProxyManager { } pub async fn change_config(&self, config: MergeMiningProxyConfig) -> Result<(), anyhow::Error> { - let mut lock = self.watcher.write().await; - lock.stop().await?; - lock.adapter.config = Some(config); + if self.watcher.read().await.is_running() { + let mut lock = self.watcher.write().await; + lock.stop().await?; + drop(lock); + } let start_config_read = self.start_config.read().await; match start_config_read.as_ref() { Some(start_config) => { - drop(lock); - let config = start_config.clone(); + let config_with_override = start_config.override_by(config); drop(start_config_read); - self.start(config).await?; + self.start(config_with_override).await?; self.wait_ready().await?; } None => { diff --git a/src-tauri/src/telemetry_manager.rs b/src-tauri/src/telemetry_manager.rs index be83a46c4..e19428f2e 100644 --- a/src-tauri/src/telemetry_manager.rs +++ b/src-tauri/src/telemetry_manager.rs @@ -20,6 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use crate::airdrop; use crate::app_in_memory_config::AppInMemoryConfig; use crate::gpu_miner_adapter::GpuMinerStatus; use crate::hardware::hardware_status_monitor::HardwareStatusMonitor; @@ -37,7 +38,6 @@ use blake2::digest::Update; use blake2::digest::VariableOutput; use blake2::Blake2bVar; use jsonwebtoken::errors::Error as JwtError; -use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -69,16 +69,6 @@ impl Default for TelemetryFrequency { } } -#[derive(Debug, Deserialize, Serialize)] -struct AirdropAccessToken { - exp: u64, - iat: i32, - id: String, - provider: String, - role: String, - scope: String, -} - #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "lowercase")] pub enum TelemetryResource { @@ -205,7 +195,6 @@ pub struct TelemetryManager { in_memory_config: Arc>, pub cancellation_token: CancellationToken, node_network: Option, - airdrop_access_token: Arc>>, gpu_status: watch::Receiver, node_status: watch::Receiver, p2pool_status: watch::Receiver>, @@ -230,7 +219,6 @@ impl TelemetryManager { cancellation_token, node_network: network, in_memory_config, - airdrop_access_token: Arc::new(RwLock::new(None)), gpu_status, node_status, p2pool_status, @@ -255,11 +243,10 @@ impl TelemetryManager { let version = env!("CARGO_PKG_VERSION"); // let mode = MiningMode::to_str(config.mode()); - let token_guard = self.airdrop_access_token.read().await; - let id: Option = token_guard.clone().and_then(|jwt| { - let claims = decode_jwt_claims(&jwt); - claims.map(|claim| claim.id) - }); + let airdrop_access_token = config.airdrop_tokens().map(|tokens| tokens.token); + let id: Option = airdrop_access_token + .and_then(|token| airdrop::decode_jwt_claims(&token).map(|claim| claim.id)); + let id_or_empty = id.unwrap_or("".to_string()); let id_as_bytes = id_or_empty.as_bytes(); @@ -283,13 +270,8 @@ impl TelemetryManager { self.node_network = network; } - pub async fn initialize( - &mut self, - airdrop_access_token: Arc>>, - app_handle: tauri::AppHandle, - ) -> Result<()> { + pub async fn initialize(&mut self, app_handle: tauri::AppHandle) -> Result<()> { info!(target: LOG_TARGET, "Starting telemetry manager"); - self.airdrop_access_token = airdrop_access_token.clone(); self.start_telemetry_process(TelemetryFrequency::default().into(), app_handle) .await?; Ok(()) @@ -310,7 +292,6 @@ impl TelemetryManager { let network = self.node_network; let config_cloned = self.config.clone(); let in_memory_config_cloned = self.in_memory_config.clone(); - let airdrop_access_token = self.airdrop_access_token.clone(); let stats_collector = self.process_stats_collector.clone(); tokio::spawn(async move { tokio::select! { @@ -318,8 +299,9 @@ impl TelemetryManager { debug!(target: LOG_TARGET, "TelemetryManager::start_telemetry_process has been started"); loop { let telemetry_collection_enabled = config_cloned.read().await.allow_telemetry(); + let airdrop_access_token = config_cloned.read().await.airdrop_tokens().map(|tokens| tokens.token); if telemetry_collection_enabled { - let airdrop_access_token_validated = validate_jwt(airdrop_access_token.clone()).await; + let airdrop_access_token_validated = airdrop::validate_jwt(airdrop_access_token).await; let telemetry_data = get_telemetry_data(&cpu_miner, &gpu_status, &node_status, &p2pool_status, &config, network, uptime, &stats_collector).await; let airdrop_api_url = in_memory_config_cloned.read().await.airdrop_api_url.clone(); handle_telemetry_data(telemetry_data, airdrop_api_url, airdrop_access_token_validated, app_handle.clone()).await; @@ -336,44 +318,6 @@ impl TelemetryManager { } } -async fn validate_jwt(airdrop_access_token: Arc>>) -> Option { - let airdrop_access_token_internal = airdrop_access_token.read().await.clone(); - airdrop_access_token_internal.clone().and_then(|t| { - let claims = decode_jwt_claims(&t); - - let now = std::time::SystemTime::now(); - let now_unix = now - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - - if let Some(claims) = claims { - if claims.exp < now_unix { - warn!(target: LOG_TARGET,"Access token has expired"); - None - } else { - Some(t) - } - } else { - None - } - }) -} - -fn decode_jwt_claims(t: &str) -> Option { - let key = DecodingKey::from_secret(&[]); - let mut validation = Validation::new(Algorithm::HS256); - validation.insecure_disable_signature_validation(); - - match decode::(t, &key, &validation) { - Ok(data) => Some(data.claims), - Err(e) => { - warn!(target: LOG_TARGET,"Error decoding access token: {:?}", e); - None - } - } -} - #[allow(clippy::too_many_lines)] async fn get_telemetry_data( cpu_miner: &RwLock, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ac5efc520..15ab6db3e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,5 +1,5 @@ { - "version": "0.8.48", + "version": "0.8.49", "productName": "Tari Universe (Alpha)", "mainBinaryName": "Tari Universe (Alpha)", "identifier": "com.tari.universe.alpha", diff --git a/src/containers/main/Airdrop/AirdropGiftTracker/sections/LoggedOut/LoggedOut.tsx b/src/containers/main/Airdrop/AirdropGiftTracker/sections/LoggedOut/LoggedOut.tsx index ee865a810..ed592bd1c 100644 --- a/src/containers/main/Airdrop/AirdropGiftTracker/sections/LoggedOut/LoggedOut.tsx +++ b/src/containers/main/Airdrop/AirdropGiftTracker/sections/LoggedOut/LoggedOut.tsx @@ -5,11 +5,9 @@ import { open } from '@tauri-apps/plugin-shell'; import { v4 as uuidv4 } from 'uuid'; import { useTranslation } from 'react-i18next'; import gemImage from '../../images/gem.png'; -import { useMiningStore } from '@app/store/useMiningStore'; export default function LoggedOut() { const { t } = useTranslation(['airdrop'], { useSuspense: false }); - const restartMining = useMiningStore((s) => s.restartMining); const { referralQuestPoints, authUuid, setAuthUuid, backendInMemoryConfig } = useAirdropStore(); const handleAuth = useCallback( @@ -45,7 +43,6 @@ export default function LoggedOut() { if (!data.error) { clearInterval(interval); setAirdropTokens(data); - restartMining(); } }); } diff --git a/src/hooks/airdrop/stateHelpers/useAirdropTokensRefresh.ts b/src/hooks/airdrop/stateHelpers/useAirdropTokensRefresh.ts index bfc1e7edf..d365bc05e 100644 --- a/src/hooks/airdrop/stateHelpers/useAirdropTokensRefresh.ts +++ b/src/hooks/airdrop/stateHelpers/useAirdropTokensRefresh.ts @@ -12,7 +12,8 @@ export async function fetchAirdropTokens(airdropApiUrl: string, airdropTokens: A }), }); if (!response.ok) { - throw new Error('Failed to refresh token'); + console.error('Fetching airdrop tokens was not successful'); + return undefined; } const data: AirdropTokens = await response.json(); @@ -22,6 +23,9 @@ export async function fetchAirdropTokens(airdropApiUrl: string, airdropTokens: A export async function handleRefreshAirdropTokens(airdropApiUrl: string) { const airdropTokens = useAirdropStore.getState().airdropTokens; let tokens: AirdropTokens | undefined = airdropTokens; + if (!tokens) { + return; + } // 5 hours from now const expirationLimit = new Date(new Date().getTime() + 1000 * 60 * 60 * 5); const tokenExpirationTime = airdropTokens?.expiresAt && new Date(airdropTokens?.expiresAt * 1000); @@ -34,7 +38,6 @@ export async function handleRefreshAirdropTokens(airdropApiUrl: string) { console.error('Error refreshing airdrop tokens:', error); } } - await setAirdropTokens(tokens); } export function useAirdropTokensRefresh() { diff --git a/src/store/useAirdropStore.ts b/src/store/useAirdropStore.ts index 56ec7bdb5..769424102 100644 --- a/src/store/useAirdropStore.ts +++ b/src/store/useAirdropStore.ts @@ -1,8 +1,5 @@ import { createWithEqualityFn as create } from 'zustand/traditional'; -import { persist } from 'zustand/middleware'; import { invoke } from '@tauri-apps/api/core'; -import { useMiningStore } from './useMiningStore'; -import { getCurrentWindow } from '@tauri-apps/api/window'; export const GIFT_GEMS = 5000; export const REFERRAL_GEMS = 5000; @@ -166,69 +163,114 @@ const clearState: Partial = { miningRewardPoints: undefined, userDetails: undefined, userPoints: undefined, + referralQuestPoints: undefined, + bonusTiers: undefined, + flareAnimationType: undefined, + seenPermissions: false, }; -export const useAirdropStore = create()( - persist( - (set) => ({ - ...initialState, - setReferralQuestPoints: (referralQuestPoints) => set({ referralQuestPoints }), - setFlareAnimationType: (flareAnimationType) => set({ flareAnimationType }), - setBonusTiers: (bonusTiers) => set({ bonusTiers }), - setUserDetails: (userDetails) => set({ userDetails }), - setAuthUuid: (authUuid) => set({ authUuid }), - setReferralCount: (referralCount) => set({ referralCount }), - setUserPoints: (userPoints) => set({ userPoints, referralCount: userPoints?.referralCount }), - setUserGems: (userGems: number) => - set((state) => { - const userPointsFormatted = { - ...state.userPoints, - base: { ...state.userPoints?.base, gems: userGems }, - } as UserPoints; - - return { - userPoints: userPointsFormatted, - }; - }), - setMiningRewardPoints: (miningRewardPoints) => set({ miningRewardPoints, flareAnimationType: 'BonusGems' }), - logout: async () => { - set(clearState); - await useMiningStore.getState().restartMining(); - }, +export const useAirdropStore = create()((set) => ({ + ...initialState, + setReferralQuestPoints: (referralQuestPoints) => set({ referralQuestPoints }), + setFlareAnimationType: (flareAnimationType) => set({ flareAnimationType }), + setBonusTiers: (bonusTiers) => set({ bonusTiers }), + setUserDetails: (userDetails) => set({ userDetails }), + setAuthUuid: (authUuid) => set({ authUuid }), + setReferralCount: (referralCount) => set({ referralCount }), + setUserPoints: (userPoints) => set({ userPoints, referralCount: userPoints?.referralCount }), + setUserGems: (userGems: number) => + set((state) => { + const userPointsFormatted = { + ...state.userPoints, + base: { ...state.userPoints?.base, gems: userGems }, + } as UserPoints; + + return { + userPoints: userPointsFormatted, + }; }), - { - name: 'airdrop-store', - partialize: (s) => ({ - airdropTokens: s.airdropTokens, - miningRewardPoints: s.miningRewardPoints, - referralQuestPoints: s.referralQuestPoints, - }), - } - ) -); + setMiningRewardPoints: (miningRewardPoints) => set({ miningRewardPoints, flareAnimationType: 'BonusGems' }), + logout: async () => { + await setAirdropTokens(undefined); + }, +})); export const setAirdropTokens = async (airdropTokens?: AirdropTokens) => { - const currentWindow = getCurrentWindow(); + const currentState = useAirdropStore.getState(); if (airdropTokens) { - await currentWindow?.emit('airdrop_token', { token: airdropTokens.token }); useAirdropStore.setState({ syncedWithBackend: true, airdropTokens: { + ...currentState, ...airdropTokens, expiresAt: parseJwt(airdropTokens.token).exp, }, }); + + await invoke('set_airdrop_tokens', { + airdropTokens: { token: airdropTokens.token, refresh_token: airdropTokens.refreshToken }, + }); } else { // User not connected - useAirdropStore.setState({ syncedWithBackend: true }); + const clearedState = { ...currentState, ...clearState, syncedWithBackend: true, airdropTokens: undefined }; + useAirdropStore.setState(clearedState); + try { + await invoke('set_airdrop_tokens', { airdropTokens: undefined }); + } catch (e) { + console.error('Error clearing airdrop access token:', e); + } } }; export const fetchBackendInMemoryConfig = async () => { + const currentState = useAirdropStore.getState(); + + // Checks for old persisted tokens + const existingTokensStore = localStorage.getItem('airdrop-store'); + let existingTokens: AirdropTokens | undefined = undefined; + if (existingTokensStore) { + try { + existingTokens = (JSON.parse(existingTokensStore).state as AirdropState).airdropTokens; + } catch (e) { + console.error('Failed to parse existing tokens:', e); + } + } + //////////////////////////// + let backendInMemoryConfig: BackendInMemoryConfig | undefined = undefined; + try { backendInMemoryConfig = await invoke('get_app_in_memory_config', {}); - useAirdropStore.setState({ backendInMemoryConfig }); + const airdropTokens = (await invoke('get_airdrop_tokens')) || {}; + const newState = { + ...currentState, + backendInMemoryConfig, + }; + + if (airdropTokens?.token) { + newState.airdropTokens = { + ...airdropTokens, + expiresAt: parseJwt(airdropTokens.token).exp, + }; + } else if (existingTokens?.token) { + try { + await invoke('set_airdrop_tokens', { + airdropTokens: { token: existingTokens.token, refresh_token: existingTokens.refreshToken }, + }); + + newState.airdropTokens = { + ...existingTokens, + expiresAt: parseJwt(existingTokens.token).exp, + }; + + // Remove old tokens + localStorage.removeItem('airdrop-store'); + } catch (e) { + console.error('get_app_in_memory_config error:', e); + } + } + + useAirdropStore.setState(newState); } catch (e) { console.error('get_app_in_memory_config error:', e); } diff --git a/src/types/invoke.ts b/src/types/invoke.ts index 5076e866d..e712d9a5f 100644 --- a/src/types/invoke.ts +++ b/src/types/invoke.ts @@ -14,6 +14,7 @@ import { import { Language } from '@app/i18initializer'; import { PaperWalletDetails } from '@app/types/app-status.ts'; import { displayMode, modeType } from '@app/store/types.ts'; +import { AirdropTokens } from '@app/store/useAirdropStore'; declare module '@tauri-apps/api/core' { function invoke( @@ -78,6 +79,11 @@ declare module '@tauri-apps/api/core' { function invoke(param: 'set_pre_release', payload: { preRelease: boolean }): Promise; function invoke(param: 'proceed_with_update'): Promise; function invoke(param: 'check_for_updates'): Promise; + function invoke( + param: 'set_airdrop_tokens', + airdropTokens: Pick + ): Promise; + function invoke(param: 'get_airdrop_tokens'): Promise>; function invoke(param: 'try_update', payload?: { force?: boolean }): Promise; function invoke( param: 'set_show_experimental_settings',