Files
khadhroony-solana-project/crates/ksp-app-wallet-desk/src/app_state.rs

1347 lines
76 KiB
Rust

// 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<LoggingRuntimeState>,
offchain_runtime: std::option::Option<crate::OffchainTransportRuntime>,
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<std::option::Option<crate::PendingWalletImport>>,
wallet_session: std::sync::Mutex<crate::WalletSession>,
}
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<Self> {
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<crate::RuntimeStatusDto> {
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<crate::LockedWalletDto> {
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<crate::WalletSessionDto> {
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<crate::WalletAuthorizedDto> {
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::<std::vec::Vec<_>>();
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<crate::WalletTransferInspectionDto> {
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<crate::WalletAuthorizedDto> {
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::<std::vec::Vec<_>>();
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<crate::LockedWalletDto> {
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<WalletUnlockContext> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<String> {
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<crate::WalletExportResultDto> {
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<crate::WalletViewSecurityStatusDto> {
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<crate::WalletViewSecurityStatusDto> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<ViewOperationContext> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<OwnerOperationContext> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletAuthorizedDto> {
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<crate::WalletBalanceDto> {
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<String> {
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::<std::vec::Vec<_>>();
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<WalletBalanceContext> {
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<String>) -> std::option::Option<String> {
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<ksp_wallet_lib::WalletView>,
wallet_id: String,
}
struct OwnerOperationContext {
operation: &'static str,
owner: std::boxed::Box<ksp_wallet_lib::WalletOwner>,
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<u32> {
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<String>,
fallback_active: bool,
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}