// file: crates/ksp-app-wallet-desk/src/app_state.rs // version: 21 //! Shared backend state owned by the Wallet Desk Tauri application. /// Shared Wallet Desk application state managed by Tauri. pub(crate) struct AppState { config_management: ksp_config_lib::ConfigManagement, logging_runtime: std::sync::Mutex, offchain_runtime: std::option::Option, splash_settings: crate::SplashSettings, splash_sequence_started: std::sync::atomic::AtomicBool, transport_runtime: crate::TransportRuntime, wallet_config_startup: crate::WalletConfigStartup, wallet_import_source: std::sync::Mutex>, wallet_session: std::sync::Mutex, } impl AppState { /// Initializes Config composition, Logging, Wallet directory preparation and the common desktop splash state. pub(crate) fn initialize(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result { let config_management = crate::config_management(arguments); let config_management = match config_management { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let runtime_identity = crate::launch_identity(); let runtime_identity = match runtime_identity { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let logging_startup = crate::initialize_logging(&config_management, &runtime_identity); let logging_startup = match logging_startup { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let transport_runtime = crate::initialize_transport(&config_management); let transport_runtime = match transport_runtime { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { ksp_logging_lib::error!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_BOOTSTRAP, error_domain = error.code().domain(), error_code = error.code().code(), "Wallet Desk Config/Transport bootstrap failed"); return std::result::Result::Err(error); }, }; let offchain_runtime = crate::initialize_offchain_transport(&config_management); let offchain_runtime = match offchain_runtime { std::result::Result::Ok(value) => std::option::Option::Some(value), std::result::Result::Err(error) => { ksp_logging_lib::warn!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, error_domain = error.code().domain(), error_code = error.code().code(), "Wallet Desk Off-chain Transport is unavailable; SOL/USD average will be N.A." ); std::option::Option::None }, }; let wallet_config_startup = crate::initialize_wallet_config(&config_management); let wallet_config_startup = match wallet_config_startup { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { ksp_logging_lib::error!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_BOOTSTRAP, error_domain = error.code().domain(), error_code = error.code().code(), "Wallet Desk Config/Wallet directory bootstrap failed"); return std::result::Result::Err(error); }, }; let splash_settings = crate::SplashSettings::load(); let splash_settings = match splash_settings { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, error_domain = error.code().domain(), error_code = error.code().code(), "managed splash timings are invalid; using transient in-memory defaults"); crate::SplashSettings::fallback() }, }; ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, minimum_ms = splash_settings.minimum_ms(), minimum_source = splash_settings.minimum_source(), fade_in_ms = splash_settings.fade_in_ms(), fade_in_source = splash_settings.fade_in_source(), fade_out_ms = splash_settings.fade_out_ms(), fade_out_source = splash_settings.fade_out_source(), expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(), "resolved Wallet Desk splash timings"); return std::result::Result::Ok(Self { config_management, logging_runtime: std::sync::Mutex::new(LoggingRuntimeState { guard: logging_startup.guard, active_profile_id: logging_startup.active_profile_id, fallback_active: logging_startup.fallback_active, startup_diagnostic: logging_startup.startup_diagnostic, }), offchain_runtime, splash_settings, splash_sequence_started: std::sync::atomic::AtomicBool::new(false), transport_runtime, wallet_config_startup, wallet_import_source: std::sync::Mutex::new(std::option::Option::None), wallet_session: std::sync::Mutex::new(crate::WalletSession::no_selection()), }); } /// Builds the safe runtime status DTO exposed by the Wallet Desk shell. pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result { let document_count = self.config_management.engine().registry().descriptors().count(); let document_count = u32::try_from(document_count); let document_count = match document_count { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_APP_STATE_INVALID, "Config registry contains too many descriptors for the Wallet Desk runtime DTO", ) .with_source(error), ); }, }; let runtime = self.logging_runtime.lock(); let runtime = match runtime { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_APP_STATE_LOCK_FAILED, "Wallet Desk Logging runtime state lock is poisoned", )); }, }; let _keep_guard_alive = &runtime.guard; let transport_snapshot = self.transport_runtime.pool().snapshot(); let transport_available_endpoint_count = count_to_u32(transport_snapshot.available_endpoint_count(), "available Transport endpoints"); let transport_available_endpoint_count = match transport_available_endpoint_count { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let transport_endpoint_count = count_to_u32(transport_snapshot.endpoint_count(), "Transport endpoints"); let transport_endpoint_count = match transport_endpoint_count { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let resolved = self.wallet_config_startup.resolved(); let wallets_subdirectory = resolved.wallets_subdirectory().map(|value| return value.to_string_lossy().into_owned()); return std::result::Result::Ok(crate::RuntimeStatusDto { application_version: env!("CARGO_PKG_VERSION").to_owned(), active_composite_profile: self.wallet_config_startup.composite_profile_id().to_owned(), active_logging_profile: runtime.active_profile_id.clone(), active_transport_profile: self.transport_runtime.profile_id().to_owned(), active_wallet_profile: resolved.profile_id().to_owned(), config_document_count: document_count, effective_wallets_directory: resolved.effective_wallets_directory().to_string_lossy().into_owned(), effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(), fallback_logging_active: runtime.fallback_active, root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(), shell_phase: "pre.014-desktop-polish".to_owned(), startup_diagnostic: runtime.startup_diagnostic.clone(), transport_available_endpoint_count, transport_clusters: self.transport_runtime.clusters(), transport_endpoint_count, transport_providers: self.transport_runtime.providers(), transport_role: self.transport_runtime.role().as_str().to_owned(), wallets_directory: resolved.wallets_directory().to_string_lossy().into_owned(), wallets_subdirectory, }); } /// Returns the effective Config-managed Wallet directory used by inventory operations. #[must_use] pub(crate) fn wallet_inventory_root(&self) -> &std::path::Path { return self.wallet_config_startup.resolved().effective_wallets_directory(); } /// Selects one locked Wallet and installs it as the durable backend session. pub(crate) async fn select_wallet(&self, request: crate::WalletSelectionRequestDto) -> ksp_core_lib::Result { let deselected = self.deselect_wallet(); if let std::result::Result::Err(error) = deselected { return std::result::Result::Err(error); } let root = self.wallet_inventory_root().to_path_buf(); let resolved = crate::resolve_locked_wallet(root.as_path(), request.wallet_id).await; let (path, locked_info, mut dto) = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; dto.configured_secret_candidate_count = self.secret_candidate_count_or_zero(dto.wallet_id.as_str()); let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; *session = crate::WalletSession::Locked { wallet_id: dto.wallet_id.clone(), path, locked_info }; return std::result::Result::Ok(dto); } /// Drops any selected or authorized Wallet state and returns to `NoSelection`. pub(crate) fn deselect_wallet(&self) -> ksp_core_lib::Result { let previous = { let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; std::mem::replace(&mut *session, crate::WalletSession::no_selection()) }; drop(previous); ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, "Wallet session deselected and protected handle state purged"); return std::result::Result::Ok(crate::WalletSession::no_selection().safe_projection()); } /// Creates a new Wallet under the effective root and retains the returned OWNER handle only in Rust. pub(crate) async fn create_wallet(&self, request: crate::WalletCreateRequestDto) -> ksp_core_lib::Result { let deselected = self.deselect_wallet(); if let std::result::Result::Err(error) = deselected { return std::result::Result::Err(error); } let crate::WalletCreateRequestDto { alias, filename, initial_note, owner_password, view_password } = request; let root = self.wallet_inventory_root().to_path_buf(); let destination = crate::new_wallet_destination(root.as_path(), filename.as_str()); let destination = match destination { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let view_enabled = view_password.is_some(); let owner_password = ksp_wallet_lib::OwnerPassword::new(owner_password); let view_password = view_password.map(ksp_wallet_lib::ViewPassword::new); let alias = normalize_optional_text(alias); let note = normalize_optional_text(initial_note); let note_texts = note.into_iter().collect::>(); let metadata = ksp_wallet_lib::WalletCreateMetadataV1::new(alias, note_texts); ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = filename.as_str(), view_enabled, "Wallet creation requested under effective Config root"); let created = ksp_wallet_lib::create_wallet_file(destination.as_path(), owner_password, view_password, metadata).await; let owner = match created { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = filename.as_str(), error_domain = error.code().domain(), error_code = error.code().code(), "Wallet creation failed"); return std::result::Result::Err(error); }, }; let configured_secret_candidate_count = self.secret_candidate_count_or_zero(filename.as_str()); let dto = crate::owner_projection(filename.as_str(), view_enabled, configured_secret_candidate_count, &owner); let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; *session = crate::WalletSession::Owner { wallet_id: filename.clone(), path: destination, view_enabled, wallet: std::boxed::Box::new(owner) }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = filename.as_str(), view_enabled, "Wallet created and OWNER session opened"); return std::result::Result::Ok(dto); } /// Replaces any previous native-picker transfer source with one newly validated bounded Rust-only source. pub(crate) async fn stage_import_source( &self, source: std::path::PathBuf, format: crate::WalletTransferFormatDto, ) -> ksp_core_lib::Result { let staged = crate::stage_wallet_import_source(source, format).await; let staged = match staged { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let dto = staged.safe_projection(); let slot = self.wallet_import_source.lock(); let mut slot = match slot { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(import_source_lock_error()), }; let previous = (*slot).replace(staged); drop(previous); ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_IMPORT, transfer_format = dto.format.code(), "Wallet transfer source inspected and staged in Rust-only memory"); return std::result::Result::Ok(dto); } /// Clears any staged external transfer source and zeroizes its bounded secret bytes. pub(crate) fn clear_import_source(&self) -> ksp_core_lib::Result<()> { let slot = self.wallet_import_source.lock(); let mut slot = match slot { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(import_source_lock_error()), }; let previous = slot.take(); drop(previous); return std::result::Result::Ok(()); } /// Imports the staged Solana transfer bytes into a new root-scoped native Wallet and retains the returned OWNER handle only in Rust. pub(crate) async fn import_wallet(&self, request: crate::WalletImportRequestDto) -> ksp_core_lib::Result { let pending = { let slot = self.wallet_import_source.lock(); let mut slot = match slot { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(import_source_lock_error()), }; slot.take() }; let pending = match pending { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_IMPORT_SOURCE_MISSING, "Wallet import requires one inspected native-picker transfer source", )); }, }; let deselected = self.deselect_wallet(); if let std::result::Result::Err(error) = deselected { return std::result::Result::Err(error); } let crate::WalletImportRequestDto { alias, filename, initial_note, owner_password, view_password } = request; let root = self.wallet_inventory_root().to_path_buf(); let destination = crate::new_wallet_destination(root.as_path(), filename.as_str()); let destination = match destination { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let view_enabled = view_password.is_some(); let owner_password = ksp_wallet_lib::OwnerPassword::new(owner_password); let view_password = view_password.map(ksp_wallet_lib::ViewPassword::new); let alias = normalize_optional_text(alias); let note = normalize_optional_text(initial_note); let note_texts = note.into_iter().collect::>(); let metadata = ksp_wallet_lib::WalletCreateMetadataV1::new(alias, note_texts); let transfer_format = pending.wallet_format(); ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_IMPORT, wallet_id = filename.as_str(), transfer_format = transfer_format.code(), view_enabled, "Wallet transfer import requested under effective Config root"); let imported = ksp_wallet_lib::import_wallet_transfer(destination.as_path(), pending.source_bytes(), transfer_format, owner_password, view_password, metadata) .await; let owner = match imported { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_IMPORT, wallet_id = filename.as_str(), transfer_format = transfer_format.code(), error_domain = error.code().domain(), error_code = error.code().code(), "Wallet transfer import failed"); return std::result::Result::Err(error); }, }; let configured_secret_candidate_count = self.secret_candidate_count_or_zero(filename.as_str()); let dto = crate::owner_projection(filename.as_str(), view_enabled, configured_secret_candidate_count, &owner); let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; *session = crate::WalletSession::Owner { wallet_id: filename.clone(), path: destination, view_enabled, wallet: std::boxed::Box::new(owner) }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_IMPORT, wallet_id = filename.as_str(), transfer_format = transfer_format.code(), view_enabled, "Wallet transfer imported and OWNER session opened"); return std::result::Result::Ok(dto); } /// Opens the selected locked Wallet with one manual VIEW password supplied frontend -> Rust. pub(crate) async fn unlock_wallet_view_manual(&self, request: crate::WalletUnlockRequestDto) -> ksp_core_lib::Result { let context = self.begin_unlock_operation(crate::WalletUnlockCapability::View); let context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let password = ksp_wallet_lib::ViewPassword::new(request.password); let opened = ksp_wallet_lib::open_wallet_view_file(context.path.as_path(), password).await; let view = match opened { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { self.restore_locked_after_unlock_failure(&context); ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), error_domain = error.code().domain(), error_code = error.code().code(), "Manual Wallet unlock failed"); return std::result::Result::Err(error); }, }; return self.finish_view_unlock(context, view); } /// Opens the selected locked Wallet with one manual OWNER password supplied frontend -> Rust. pub(crate) async fn unlock_wallet_owner_manual(&self, request: crate::WalletUnlockRequestDto) -> ksp_core_lib::Result { let context = self.begin_unlock_operation(crate::WalletUnlockCapability::Owner); let context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let password = ksp_wallet_lib::OwnerPassword::new(request.password); let opened = ksp_wallet_lib::open_wallet_owner_file(context.path.as_path(), password).await; let owner = match opened { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { self.restore_locked_after_unlock_failure(&context); ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), error_domain = error.code().domain(), error_code = error.code().code(), "Manual Wallet unlock failed"); return std::result::Result::Err(error); }, }; return self.finish_owner_unlock(context, owner); } /// Explicitly attempts Config-owned Wallet password candidates until VIEW unlock succeeds or candidates are exhausted. pub(crate) async fn unlock_wallet_view_configured(&self) -> ksp_core_lib::Result { let context = self.begin_unlock_operation(crate::WalletUnlockCapability::View); let context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let candidates = crate::discover_wallet_secret_candidates(&self.config_management, context.wallet_id.as_str()); let candidates = match candidates { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { self.restore_locked_after_unlock_failure(&context); return std::result::Result::Err(error); }, }; let candidate_count = candidates.len(); if candidate_count == 0 { self.restore_locked_after_unlock_failure(&context); return std::result::Result::Err(configured_secret_missing_error()); } ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Explicit configured-secret Wallet unlock started"); let mut last_error = std::option::Option::None; for candidate in candidates { let secret = candidate.reveal(&self.config_management); let secret = match secret { std::result::Result::Ok(std::option::Option::Some(value)) => value, std::result::Result::Ok(std::option::Option::None) => continue, std::result::Result::Err(error) => { self.restore_locked_after_unlock_failure(&context); return std::result::Result::Err(error); }, }; let opened = ksp_wallet_lib::open_wallet_view_file(context.path.as_path(), ksp_wallet_lib::ViewPassword::new(secret)).await; match opened { std::result::Result::Ok(view) => { ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock succeeded"); return self.finish_view_unlock_with_count(context, view, candidate_count); }, std::result::Result::Err(error) => last_error = std::option::Option::Some(error), } } self.restore_locked_after_unlock_failure(&context); ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock exhausted all candidates"); return match last_error { std::option::Option::Some(error) => std::result::Result::Err(error), std::option::Option::None => std::result::Result::Err(configured_secret_missing_error()), }; } /// Explicitly attempts Config-owned Wallet password candidates until OWNER unlock succeeds or candidates are exhausted. pub(crate) async fn unlock_wallet_owner_configured(&self) -> ksp_core_lib::Result { let context = self.begin_unlock_operation(crate::WalletUnlockCapability::Owner); let context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let candidates = crate::discover_wallet_secret_candidates(&self.config_management, context.wallet_id.as_str()); let candidates = match candidates { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { self.restore_locked_after_unlock_failure(&context); return std::result::Result::Err(error); }, }; let candidate_count = candidates.len(); if candidate_count == 0 { self.restore_locked_after_unlock_failure(&context); return std::result::Result::Err(configured_secret_missing_error()); } ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Explicit configured-secret Wallet unlock started"); let mut last_error = std::option::Option::None; for candidate in candidates { let secret = candidate.reveal(&self.config_management); let secret = match secret { std::result::Result::Ok(std::option::Option::Some(value)) => value, std::result::Result::Ok(std::option::Option::None) => continue, std::result::Result::Err(error) => { self.restore_locked_after_unlock_failure(&context); return std::result::Result::Err(error); }, }; let opened = ksp_wallet_lib::open_wallet_owner_file(context.path.as_path(), ksp_wallet_lib::OwnerPassword::new(secret)).await; match opened { std::result::Result::Ok(owner) => { ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock succeeded"); return self.finish_owner_unlock_with_count(context, owner, candidate_count); }, std::result::Result::Err(error) => last_error = std::option::Option::Some(error), } } self.restore_locked_after_unlock_failure(&context); ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock exhausted all candidates"); return match last_error { std::option::Option::Some(error) => std::result::Result::Err(error), std::option::Option::None => std::result::Result::Err(configured_secret_missing_error()), }; } /// Drops an authorized handle, re-inspects the selected file and returns the session to `Locked`. pub(crate) async fn lock_wallet(&self) -> ksp_core_lib::Result { let previous = { let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; std::mem::replace(&mut *session, crate::WalletSession::no_selection()) }; let (wallet_id, path) = match previous { crate::WalletSession::NoSelection => { return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_SESSION_INVALID, "No Wallet is selected to lock")); }, crate::WalletSession::Locked { wallet_id, path, .. } => (wallet_id, path), crate::WalletSession::PrivilegedOperation { wallet_id, path, locked_info, capability } => { let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; *session = crate::WalletSession::PrivilegedOperation { wallet_id, path, locked_info, capability }; return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet unlock operation is already in progress", )); }, crate::WalletSession::ViewOperation { wallet_id, path, pubkey } => { let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; *session = crate::WalletSession::ViewOperation { wallet_id, path, pubkey }; return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet VIEW privileged operation is already in progress", )); }, crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, view_enabled } => { let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; *session = crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, view_enabled }; return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet OWNER privileged operation is already in progress", )); }, crate::WalletSession::View { wallet_id, path, wallet, .. } => { drop(wallet); (wallet_id, path) }, crate::WalletSession::Owner { wallet_id, path, wallet, .. } => { drop(wallet); (wallet_id, path) }, }; let locked = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await; let locked = match locked { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let configured_secret_candidate_count = self.secret_candidate_count_or_zero(wallet_id.as_str()); let dto = crate::LockedWalletDto { configured_secret_candidate_count, filename: wallet_id.clone(), format_version: locked.format_version(), wallet_id: wallet_id.clone(), view_enabled: locked.view_enabled(), }; let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; *session = crate::WalletSession::Locked { wallet_id: wallet_id.clone(), path, locked_info: locked }; ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), "Wallet session locked and authorized handle state purged"); return std::result::Result::Ok(dto); } fn begin_unlock_operation(&self, capability: crate::WalletUnlockCapability) -> ksp_core_lib::Result { let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); let (wallet_id, path, locked_info) = match previous { crate::WalletSession::Locked { wallet_id, path, locked_info } => (wallet_id, path, locked_info), other => { *session = other; return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet must be selected and locked before unlock", )); }, }; if capability == crate::WalletUnlockCapability::View && !locked_info.view_enabled() { *session = crate::WalletSession::Locked { wallet_id, path, locked_info }; return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_VIEW_DISABLED, "Selected Wallet does not expose an enabled VIEW capability", )); } let context = WalletUnlockContext { capability, path: path.clone(), wallet_id: wallet_id.clone() }; *session = crate::WalletSession::PrivilegedOperation { capability, wallet_id, path, locked_info }; ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), "Explicit Wallet unlock operation started"); return std::result::Result::Ok(context); } fn restore_locked_after_unlock_failure(&self, context: &WalletUnlockContext) { let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); match previous { crate::WalletSession::PrivilegedOperation { capability, wallet_id, path, locked_info } if capability == context.capability && wallet_id == context.wallet_id && path == context.path => { *session = crate::WalletSession::Locked { wallet_id, path, locked_info }; }, other => *session = other, } } fn finish_view_unlock(&self, context: WalletUnlockContext, view: ksp_wallet_lib::WalletView) -> ksp_core_lib::Result { let candidate_count = self.secret_candidate_count_or_zero(context.wallet_id.as_str()); return self.finish_view_unlock_with_count(context, view, candidate_count); } fn finish_view_unlock_with_count( &self, context: WalletUnlockContext, view: ksp_wallet_lib::WalletView, configured_secret_candidate_count: usize, ) -> ksp_core_lib::Result { let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); let (wallet_id, path, view_enabled) = match previous { crate::WalletSession::PrivilegedOperation { capability, wallet_id, path, locked_info } if capability == context.capability && wallet_id == context.wallet_id && path == context.path => { (wallet_id, path, locked_info.view_enabled()) }, other => { *session = other; drop(view); return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet unlock completion no longer owns the selected session", )); }, }; let dto = crate::view_projection(wallet_id.as_str(), view_enabled, configured_secret_candidate_count, &view); *session = crate::WalletSession::View { wallet_id: wallet_id.clone(), path, wallet: std::boxed::Box::new(view) }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), capability = "view", "Wallet VIEW session opened"); return std::result::Result::Ok(dto); } fn finish_owner_unlock(&self, context: WalletUnlockContext, owner: ksp_wallet_lib::WalletOwner) -> ksp_core_lib::Result { let candidate_count = self.secret_candidate_count_or_zero(context.wallet_id.as_str()); return self.finish_owner_unlock_with_count(context, owner, candidate_count); } fn finish_owner_unlock_with_count( &self, context: WalletUnlockContext, owner: ksp_wallet_lib::WalletOwner, configured_secret_candidate_count: usize, ) -> ksp_core_lib::Result { let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); let (wallet_id, path, view_enabled) = match previous { crate::WalletSession::PrivilegedOperation { capability, wallet_id, path, locked_info } if capability == context.capability && wallet_id == context.wallet_id && path == context.path => { (wallet_id, path, locked_info.view_enabled()) }, other => { *session = other; drop(owner); return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet unlock completion no longer owns the selected session", )); }, }; let dto = crate::owner_projection(wallet_id.as_str(), view_enabled, configured_secret_candidate_count, &owner); *session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled, wallet: std::boxed::Box::new(owner) }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), capability = "owner", "Wallet OWNER session opened"); return std::result::Result::Ok(dto); } fn secret_candidate_count_or_zero(&self, wallet_id: &str) -> usize { let count = crate::wallet_secret_candidate_count(&self.config_management, wallet_id); return match count { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id, error_domain = error.code().domain(), error_code = error.code().code(), "Wallet configured-secret candidate count is unavailable"); 0 }, }; } /// Updates or clears the protected alias through the current OWNER handle. pub(crate) async fn update_wallet_alias(&self, request: crate::WalletAliasUpdateRequestDto) -> ksp_core_lib::Result { let context = self.begin_owner_operation("alias_update"); let mut context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let alias = normalize_optional_text(request.alias); let result = context.owner.update_alias(context.path.as_path(), alias).await; return self.finish_owner_operation(context, result).await; } /// Appends one protected note through the current OWNER handle. pub(crate) async fn add_wallet_note(&self, request: crate::WalletNoteAddRequestDto) -> ksp_core_lib::Result { let context = self.begin_owner_operation("note_add"); let mut context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let result = context.owner.add_note(context.path.as_path(), request.text).await; let result = match result { std::result::Result::Ok(_) => std::result::Result::Ok(()), std::result::Result::Err(error) => std::result::Result::Err(error), }; return self.finish_owner_operation(context, result).await; } /// Updates one protected note through the current OWNER handle. pub(crate) async fn update_wallet_note(&self, request: crate::WalletNoteUpdateRequestDto) -> ksp_core_lib::Result { let context = self.begin_owner_operation("note_update"); let mut context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let result = context.owner.update_note(context.path.as_path(), request.note_id.as_str(), request.text).await; return self.finish_owner_operation(context, result).await; } /// Deletes one protected note through the current OWNER handle. pub(crate) async fn delete_wallet_note(&self, request: crate::WalletNoteDeleteRequestDto) -> ksp_core_lib::Result { let context = self.begin_owner_operation("note_delete"); let mut context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let result = context.owner.delete_note(context.path.as_path(), request.note_id.as_str()).await; return self.finish_owner_operation(context, result).await; } /// Returns the native save-picker default filename when an OWNER session is currently authorized. pub(crate) fn wallet_export_picker_filename(&self, format: crate::WalletTransferFormatDto) -> ksp_core_lib::Result { let session = self.wallet_session.lock(); let session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; return match &*session { crate::WalletSession::Owner { wallet_id, .. } => std::result::Result::Ok(crate::wallet_export_default_filename(wallet_id.as_str(), format)), _ => std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED, "Wallet transfer export requires an authorized OWNER session", )), }; } /// Exports the immutable Solana keypair to one Rust-owned native save-picker destination. pub(crate) async fn export_wallet_transfer( &self, destination: std::path::PathBuf, request: crate::WalletExportRequestDto, ) -> ksp_core_lib::Result { let destination_name = crate::wallet_export_destination_name(destination.as_path()); let destination_name = match destination_name { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let format = request.format; let context = self.begin_owner_operation("keypair_export"); let context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let wallet_id = context.wallet_id.clone(); ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_EXPORT, wallet_id = wallet_id.as_str(), transfer_format = format.code(), "Wallet OWNER transfer export started" ); let result = context.owner.export_transfer_file(destination.as_path(), format.wallet_format()).await; let restored = self.finish_owner_operation(context, result).await; if let std::result::Result::Err(error) = restored { ksp_logging_lib::warn!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_EXPORT, wallet_id = wallet_id.as_str(), transfer_format = format.code(), error_domain = error.code().domain(), error_code = error.code().code(), "Wallet OWNER transfer export failed" ); return std::result::Result::Err(error); } ksp_logging_lib::info!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_EXPORT, wallet_id = wallet_id.as_str(), transfer_format = format.code(), "Wallet OWNER transfer export completed" ); return std::result::Result::Ok(crate::WalletExportResultDto { destination_name, format, wallet_id }); } /// Strongly disables VIEW from the current OWNER session while preserving OWNER authorization. pub(crate) async fn disable_wallet_view(&self) -> ksp_core_lib::Result { let context = self.begin_owner_operation("view_disable"); let mut context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let result = context.owner.disable_view(context.path.as_path()).await; if result.is_ok() { context.result_view_enabled = false; } let wallet = self.finish_owner_operation(context, result).await; return match wallet { std::result::Result::Ok(value) => std::result::Result::Ok(crate::view_security_status_from_authorized(&value)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Strongly recreates VIEW from the current OWNER session with a fresh metadata key, slot and password. pub(crate) async fn recreate_wallet_view( &self, request: crate::WalletPasswordRotationRequestDto, ) -> ksp_core_lib::Result { let context = self.begin_owner_operation("view_recreate"); let mut context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let password = ksp_wallet_lib::ViewPassword::new(request.password); let result = context.owner.recreate_view(context.path.as_path(), password).await; if result.is_ok() { context.result_view_enabled = true; } let wallet = self.finish_owner_operation(context, result).await; return match wallet { std::result::Result::Ok(value) => std::result::Result::Ok(crate::view_security_status_from_authorized(&value)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Rotates the OWNER password while preserving the current authorized identity and metadata projection. pub(crate) async fn rotate_owner_password(&self, request: crate::WalletPasswordRotationRequestDto) -> ksp_core_lib::Result { let context = self.begin_owner_operation("owner_password_rotate"); let mut context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let password = ksp_wallet_lib::OwnerPassword::new(request.password); let result = context.owner.rotate_owner_password(context.path.as_path(), password).await; return self.finish_owner_operation(context, result).await; } /// Rotates the enabled VIEW password from either VIEW self-service or OWNER administration. pub(crate) async fn rotate_view_password(&self, request: crate::WalletPasswordRotationRequestDto) -> ksp_core_lib::Result { let owner_context = self.begin_owner_operation("view_password_rotate"); if let std::result::Result::Ok(mut context) = owner_context { if !context.view_enabled { self.restore_owner_after_operation_failure(context); return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_VIEW_DISABLED, "Wallet VIEW capability is disabled and cannot rotate a VIEW password", )); } let password = ksp_wallet_lib::ViewPassword::new(request.password); let result = context.owner.rotate_view_password(context.path.as_path(), password).await; return self.finish_owner_operation(context, result).await; } let context = self.begin_view_operation("view_password_self_rotate"); let mut context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let password = ksp_wallet_lib::ViewPassword::new(request.password); let result = context.view.rotate_view_password(context.path.as_path(), password).await; return self.finish_view_operation(context, result).await; } fn begin_view_operation(&self, operation: &'static str) -> ksp_core_lib::Result { let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); let (wallet_id, path, view) = match previous { crate::WalletSession::View { wallet_id, path, wallet } => (wallet_id, path, wallet), other => { *session = other; return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED, "Wallet VIEW self-service operation requires an authorized VIEW session", )); }, }; let pubkey = view.pubkey().to_owned(); let context = ViewOperationContext { operation, path: path.clone(), pubkey, view, wallet_id: wallet_id.clone() }; *session = crate::WalletSession::ViewOperation { wallet_id, path, pubkey }; ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), operation, "Wallet VIEW privileged operation started"); return std::result::Result::Ok(context); } async fn finish_view_operation( &self, context: ViewOperationContext, operation_result: ksp_core_lib::Result<()>, ) -> ksp_core_lib::Result { if let std::result::Result::Err(error) = operation_result { if error.code() == ksp_wallet_lib::ERROR_CODE_STATE_CONFLICT { self.recover_view_state_conflict(context).await; return std::result::Result::Err(error); } self.restore_view_after_operation_failure(context); return std::result::Result::Err(error); } return self.install_view_after_operation_success(context); } fn install_view_after_operation_success(&self, context: ViewOperationContext) -> ksp_core_lib::Result { let ViewOperationContext { operation, path, pubkey, view, wallet_id } = context; let configured_secret_candidate_count = self.secret_candidate_count_or_zero(wallet_id.as_str()); let dto = crate::view_projection(wallet_id.as_str(), true, configured_secret_candidate_count, &view); let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); return match previous { crate::WalletSession::ViewOperation { wallet_id: reserved_wallet_id, path: reserved_path, pubkey: reserved_pubkey } if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey => { *session = crate::WalletSession::View { wallet_id: wallet_id.clone(), path, wallet: view }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet VIEW privileged operation completed"); std::result::Result::Ok(dto) }, other => { *session = other; drop(view); std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet VIEW operation completion no longer owns the selected session", )) }, }; } fn restore_view_after_operation_failure(&self, context: ViewOperationContext) { let ViewOperationContext { operation, path, pubkey, view, wallet_id } = context; let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); match previous { crate::WalletSession::ViewOperation { wallet_id: reserved_wallet_id, path: reserved_path, pubkey: reserved_pubkey } if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey => { *session = crate::WalletSession::View { wallet_id: wallet_id.clone(), path, wallet: view }; ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet VIEW privileged operation failed without invalidating the authenticated handle"); }, other => { *session = other; drop(view); }, } } async fn recover_view_state_conflict(&self, context: ViewOperationContext) { let ViewOperationContext { operation, path, pubkey, view, wallet_id } = context; drop(view); let inspected = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await; let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); match previous { crate::WalletSession::ViewOperation { wallet_id: reserved_wallet_id, path: reserved_path, pubkey: reserved_pubkey } if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey => { if let std::result::Result::Ok(locked_info) = inspected { *session = crate::WalletSession::Locked { wallet_id: wallet_id.clone(), path, locked_info }; } ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet VIEW state conflict purged the stale handle and forced reauthorization"); }, other => { *session = other; return; }, } } fn begin_owner_operation(&self, operation: &'static str) -> ksp_core_lib::Result { let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); let (wallet_id, path, view_enabled, owner) = match previous { crate::WalletSession::Owner { wallet_id, path, view_enabled, wallet } => (wallet_id, path, view_enabled, wallet), other => { *session = other; return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED, "Wallet OWNER operation requires an authorized OWNER session", )); }, }; let pubkey = owner.pubkey().to_owned(); let context = OwnerOperationContext { operation, owner, path: path.clone(), pubkey, result_view_enabled: view_enabled, view_enabled, wallet_id: wallet_id.clone(), }; *session = crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, view_enabled }; ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), operation, "Wallet OWNER privileged operation started"); return std::result::Result::Ok(context); } async fn finish_owner_operation( &self, context: OwnerOperationContext, operation_result: ksp_core_lib::Result<()>, ) -> ksp_core_lib::Result { if let std::result::Result::Err(error) = operation_result { if error.code() == ksp_wallet_lib::ERROR_CODE_STATE_CONFLICT { self.recover_owner_state_conflict(context).await; return std::result::Result::Err(error); } self.restore_owner_after_operation_failure(context); return std::result::Result::Err(error); } return self.install_owner_after_operation_success(context); } fn install_owner_after_operation_success(&self, context: OwnerOperationContext) -> ksp_core_lib::Result { let OwnerOperationContext { operation, owner, path, pubkey, result_view_enabled, view_enabled, wallet_id } = context; let configured_secret_candidate_count = self.secret_candidate_count_or_zero(wallet_id.as_str()); let dto = crate::owner_projection(wallet_id.as_str(), result_view_enabled, configured_secret_candidate_count, &owner); let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); return match previous { crate::WalletSession::OwnerOperation { wallet_id: reserved_wallet_id, path: reserved_path, pubkey: reserved_pubkey, view_enabled: reserved_view_enabled, } if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey && reserved_view_enabled == view_enabled => { *session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled: result_view_enabled, wallet: owner }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER privileged operation completed"); std::result::Result::Ok(dto) }, other => { *session = other; drop(owner); std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet OWNER operation completion no longer owns the selected session", )) }, }; } fn restore_owner_after_operation_failure(&self, context: OwnerOperationContext) { let OwnerOperationContext { operation, owner, path, pubkey, result_view_enabled: _, view_enabled, wallet_id } = context; let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); match previous { crate::WalletSession::OwnerOperation { wallet_id: reserved_wallet_id, path: reserved_path, pubkey: reserved_pubkey, view_enabled: reserved_view_enabled, } if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey && reserved_view_enabled == view_enabled => { *session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled, wallet: owner }; ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER privileged operation failed without invalidating the authenticated handle"); }, other => { *session = other; drop(owner); }, } } async fn recover_owner_state_conflict(&self, context: OwnerOperationContext) { let OwnerOperationContext { operation, owner, path, pubkey, result_view_enabled: _, view_enabled, wallet_id } = context; drop(owner); let inspected = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await; let session = self.wallet_session.lock(); let mut session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); match previous { crate::WalletSession::OwnerOperation { wallet_id: reserved_wallet_id, path: reserved_path, pubkey: reserved_pubkey, view_enabled: reserved_view_enabled, } if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey && reserved_view_enabled == view_enabled => { if let std::result::Result::Ok(locked_info) = inspected { *session = crate::WalletSession::Locked { wallet_id: wallet_id.clone(), path, locked_info }; } ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER state conflict purged the stale handle and forced reauthorization"); }, other => { *session = other; return; }, } } /// Refreshes the native SOL balance for the currently authorized VIEW/OWNER Wallet. pub(crate) async fn refresh_wallet_balance(&self) -> ksp_core_lib::Result { let context = self.authorized_balance_context(); let context = match context { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let config = ksp_onchain_transport_lib::GetBalanceConfig::new( std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed), std::option::Option::None, ); ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_TRANSPORT, wallet_id = context.wallet_id.as_str(), capability = context.capability, transport_profile = self.transport_runtime.profile_id(), transport_role = self.transport_runtime.role().as_str(), "Wallet balance and auxiliary SOL/USD refresh started"); let balance_future = self.transport_runtime.pool().get_balance(self.transport_runtime.role(), &context.pubkey, std::option::Option::Some(&config)); let price_future = self.refresh_sol_usd_average(); let (result, sol_usd_average) = tokio::join!(balance_future, price_future); let result = match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_TRANSPORT, wallet_id = context.wallet_id.as_str(), capability = context.capability, error_domain = error.code().domain(), error_code = error.code().code(), "Wallet balance refresh failed"); return std::result::Result::Err(error); }, }; if !self.balance_context_is_current(&context) { return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet balance refresh completed after the authorized session changed", )); } let lamports = result.value(); let sol_usd_equivalent = crate::sol_usd_equivalent(lamports, sol_usd_average.as_deref()); let dto = crate::WalletBalanceDto { api_version: result.context().api_version().map(str::to_owned), lamports: lamports.to_string(), slot: result.context().slot(), sol: crate::format_lamports_as_sol(lamports), sol_usd_average, sol_usd_equivalent, transport_profile: self.transport_runtime.profile_id().to_owned(), transport_role: self.transport_runtime.role().as_str().to_owned(), wallet_id: context.wallet_id.clone(), }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_TRANSPORT, wallet_id = context.wallet_id.as_str(), capability = context.capability, lamports, slot = dto.slot, sol_usd_average_available = dto.sol_usd_average.is_some(), sol_usd_equivalent_available = dto.sol_usd_equivalent.is_some(), "Wallet balance refresh succeeded"); return std::result::Result::Ok(dto); } async fn refresh_sol_usd_average(&self) -> std::option::Option { let runtime = match &self.offchain_runtime { std::option::Option::Some(value) => value, std::option::Option::None => return std::option::Option::None, }; let outcomes = runtime.service().refresh_all().await; let outcomes = match outcomes { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { ksp_logging_lib::warn!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, offchain_profile = runtime.profile_id(), error_domain = error.code().domain(), error_code = error.code().code(), "Wallet Desk SOL/USD provider refresh failed; average is unavailable" ); return std::option::Option::None; }, }; let prices = outcomes .iter() .filter_map(|outcome| return outcome.observation().map(|observation| return observation.price())) .collect::>(); let average = crate::average_sol_usd_prices(prices.as_slice()); ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, offchain_profile = runtime.profile_id(), provider_count = outcomes.len(), observed_provider_count = prices.len(), average_available = average.is_some(), "Wallet Desk SOL/USD refresh completed" ); return average; } fn authorized_balance_context(&self) -> ksp_core_lib::Result { let session = self.wallet_session.lock(); let session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), }; return match &*session { crate::WalletSession::View { wallet_id, wallet, .. } => { std::result::Result::Ok(WalletBalanceContext { capability: "view", pubkey: wallet.pubkey().to_owned(), wallet_id: wallet_id.clone() }) }, crate::WalletSession::Owner { wallet_id, wallet, .. } => { std::result::Result::Ok(WalletBalanceContext { capability: "owner", pubkey: wallet.pubkey().to_owned(), wallet_id: wallet_id.clone() }) }, crate::WalletSession::ViewOperation { .. } | crate::WalletSession::OwnerOperation { .. } | crate::WalletSession::PrivilegedOperation { .. } => { std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_SESSION_INVALID, "Wallet balance is unavailable while a privileged Wallet operation is running", )) }, crate::WalletSession::Locked { .. } | crate::WalletSession::NoSelection => std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED, "Wallet balance requires an authorized VIEW or OWNER session", )), }; } fn balance_context_is_current(&self, context: &WalletBalanceContext) -> bool { let session = self.wallet_session.lock(); let session = match session { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return false, }; return match &*session { crate::WalletSession::View { wallet_id, wallet, .. } => { context.capability == "view" && wallet_id == &context.wallet_id && wallet.pubkey() == &context.pubkey }, crate::WalletSession::Owner { wallet_id, wallet, .. } => { context.capability == "owner" && wallet_id == &context.wallet_id && wallet.pubkey() == &context.pubkey }, crate::WalletSession::NoSelection | crate::WalletSession::Locked { .. } | crate::WalletSession::ViewOperation { .. } | crate::WalletSession::OwnerOperation { .. } | crate::WalletSession::PrivilegedOperation { .. } => false, }; } /// Returns the resolved common splash timings captured during bootstrap. #[must_use] pub(crate) const fn splash_settings(&self) -> crate::SplashSettings { return self.splash_settings; } /// Marks the one-shot splash lifecycle as started and reports whether this caller won the transition. pub(crate) fn begin_splash_sequence(&self) -> bool { return self .splash_sequence_started .compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire) .is_ok(); } } fn normalize_optional_text(value: std::option::Option) -> std::option::Option { return value.and_then(|text| { if text.trim().is_empty() { return std::option::Option::None; } return std::option::Option::Some(text); }); } fn import_source_lock_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_LOCK_FAILED, "Wallet Desk staged import source lock is poisoned"); } fn session_lock_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_LOCK_FAILED, "Wallet Desk Wallet session state lock is poisoned"); } struct WalletUnlockContext { capability: crate::WalletUnlockCapability, path: std::path::PathBuf, wallet_id: String, } fn configured_secret_missing_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_SECRET_CANDIDATES_MISSING, "No effective Config-owned Wallet password candidate is available"); } struct ViewOperationContext { operation: &'static str, path: std::path::PathBuf, pubkey: ksp_core_lib::Pubkey, view: std::boxed::Box, wallet_id: String, } struct OwnerOperationContext { operation: &'static str, owner: std::boxed::Box, path: std::path::PathBuf, pubkey: ksp_core_lib::Pubkey, result_view_enabled: bool, view_enabled: bool, wallet_id: String, } struct WalletBalanceContext { capability: &'static str, pubkey: ksp_core_lib::Pubkey, wallet_id: String, } fn count_to_u32(value: usize, label: &'static str) -> ksp_core_lib::Result { let converted = u32::try_from(value); return match converted { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(error) => std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Wallet Desk runtime count exceeds DTO range") .with_context("count_kind", label) .with_source(error), ), }; } struct LoggingRuntimeState { guard: ksp_logging_lib::LoggingGuard, active_profile_id: std::option::Option, fallback_active: bool, startup_diagnostic: std::option::Option, }