v0.2.6-pre.005
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/app_state.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Shared backend state owned by the Wallet Desk Tauri application.
|
||||
|
||||
@@ -10,6 +10,7 @@ pub(crate) struct AppState {
|
||||
splash_settings: crate::SplashSettings,
|
||||
splash_sequence_started: std::sync::atomic::AtomicBool,
|
||||
wallet_config_startup: crate::WalletConfigStartup,
|
||||
wallet_session: std::sync::Mutex<crate::WalletSession>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -58,6 +59,7 @@ impl AppState {
|
||||
splash_settings,
|
||||
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
|
||||
wallet_config_startup,
|
||||
wallet_session: std::sync::Mutex::new(crate::WalletSession::no_selection()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,7 +102,7 @@ impl AppState {
|
||||
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.004-wallet-inventory".to_owned(),
|
||||
shell_phase: "pre.005-wallet-session-create".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
wallets_directory: resolved.wallets_directory().to_string_lossy().into_owned(),
|
||||
wallets_subdirectory,
|
||||
@@ -113,6 +115,123 @@ impl AppState {
|
||||
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, dto) = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
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_v1(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 dto = crate::owner_projection(filename.as_str(), view_enabled, &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, wallet: 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);
|
||||
}
|
||||
|
||||
/// 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::Owner { wallet_id, path, wallet } => {
|
||||
drop(wallet);
|
||||
(wallet_id, path)
|
||||
},
|
||||
};
|
||||
let locked = ksp_wallet_lib::inspect_locked_wallet_file_v1(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 dto = crate::LockedWalletDto {
|
||||
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);
|
||||
}
|
||||
|
||||
/// Returns the resolved common splash timings captured during bootstrap.
|
||||
#[must_use]
|
||||
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
|
||||
@@ -128,6 +247,19 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
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 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 LoggingRuntimeState {
|
||||
guard: ksp_logging_lib::LoggingGuard,
|
||||
active_profile_id: std::option::Option<String>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/constants.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Logging targets, domains and composite component identifiers owned by Wallet Desk.
|
||||
|
||||
@@ -19,6 +19,8 @@ pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.shell";
|
||||
pub(crate) const TRACING_DOMAIN_WALLET_CONFIG: &str = "wallet.config";
|
||||
/// Structured domain used while enumerating and inspecting locked Wallet files.
|
||||
pub(crate) const TRACING_DOMAIN_WALLET_INVENTORY: &str = "wallet.inventory";
|
||||
/// Structured domain used by durable Wallet session and creation operations.
|
||||
pub(crate) const TRACING_DOMAIN_WALLET_SESSION: &str = "wallet.session";
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
|
||||
/// Owning target for backend events emitted by Wallet Desk.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/errors.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Application-local error codes for Wallet Desk composition and desktop runtime surfaces.
|
||||
|
||||
@@ -35,3 +35,5 @@ pub(crate) const ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED: ksp_core_lib::Error
|
||||
pub(crate) const ERROR_CODE_WALLET_INVENTORY_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_inventory_failed");
|
||||
/// A requested Wallet inventory identifier is unsafe or does not resolve to an eligible regular `.kspwallet` file.
|
||||
pub(crate) const ERROR_CODE_WALLET_SELECTION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_selection_invalid");
|
||||
/// A Wallet session lifecycle operation is incompatible with the current state.
|
||||
pub(crate) const ERROR_CODE_WALLET_SESSION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_session_invalid");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Tauri desktop application shell for KSP Wallet management and inspection.
|
||||
|
||||
@@ -20,6 +20,7 @@ mod tw_main;
|
||||
mod tw_splash;
|
||||
mod wallet_config;
|
||||
mod wallet_inventory;
|
||||
mod wallet_session;
|
||||
|
||||
/// Runs the KSP wallet desktop application.
|
||||
pub use self::tauri::run;
|
||||
@@ -52,6 +53,8 @@ pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_CONFIG;
|
||||
/// Structured domain used while enumerating and inspecting locked Wallet files.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_INVENTORY;
|
||||
/// Structured domain used by durable Wallet session and creation operations.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_SESSION;
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
|
||||
/// Owning target for backend events emitted by Wallet Desk.
|
||||
@@ -98,6 +101,8 @@ pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED;
|
||||
pub(crate) use self::errors::ERROR_CODE_WALLET_INVENTORY_FAILED;
|
||||
/// Wallet selection identifier is invalid or no longer eligible.
|
||||
pub(crate) use self::errors::ERROR_CODE_WALLET_SELECTION_INVALID;
|
||||
/// Wallet session lifecycle operation is incompatible with the current state.
|
||||
pub(crate) use self::errors::ERROR_CODE_WALLET_SESSION_INVALID;
|
||||
/// Log payload sent by Wallet Desk frontend scripts.
|
||||
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
|
||||
/// Emits one validated frontend event through the KSP Logging facade.
|
||||
@@ -134,5 +139,23 @@ pub(crate) use self::wallet_inventory::WalletInventoryStateDto;
|
||||
pub(crate) use self::wallet_inventory::WalletSelectionRequestDto;
|
||||
/// Enumerates native Wallet files under the effective Config-managed root.
|
||||
pub(crate) use self::wallet_inventory::list_wallet_inventory;
|
||||
/// Re-inspects one root-scoped Wallet selection.
|
||||
/// Builds a validated destination path for one new root-scoped Wallet.
|
||||
pub(crate) use self::wallet_inventory::new_wallet_destination;
|
||||
/// Re-resolves one locked Wallet into Rust-only path state plus safe DTO.
|
||||
pub(crate) use self::wallet_inventory::resolve_locked_wallet;
|
||||
/// Re-inspects one root-scoped Wallet selection in crate unit tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::wallet_inventory::select_locked_wallet;
|
||||
/// Authorized OWNER projection exposed only after successful creation.
|
||||
pub(crate) use self::wallet_session::WalletAuthorizedDto;
|
||||
/// Create request whose password strings move frontend -> Rust only.
|
||||
pub(crate) use self::wallet_session::WalletCreateRequestDto;
|
||||
/// Durable backend Wallet session retaining full paths and handles only in Rust.
|
||||
pub(crate) use self::wallet_session::WalletSession;
|
||||
/// Minimal safe projection of the durable Wallet session.
|
||||
pub(crate) use self::wallet_session::WalletSessionDto;
|
||||
/// Safe state code for the durable Wallet session in crate unit tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::wallet_session::WalletSessionStateDto;
|
||||
/// Builds an authorized OWNER DTO from a Rust-only Wallet handle.
|
||||
pub(crate) use self::wallet_session::owner_projection;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/tauri.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Tauri runtime assembly for the KSP wallet desktop application.
|
||||
|
||||
@@ -38,12 +38,15 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
|
||||
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
|
||||
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.invoke_handler(tauri::generate_handler![
|
||||
create_wallet,
|
||||
deselect_wallet,
|
||||
emit_frontend_log,
|
||||
get_runtime_status,
|
||||
list_wallets,
|
||||
lock_wallet,
|
||||
refresh_wallets,
|
||||
select_wallet,
|
||||
splash_frontend_ready
|
||||
splash_frontend_ready,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -62,6 +65,27 @@ fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri:
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn create_wallet(
|
||||
request: crate::WalletCreateRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
|
||||
let result = state.create_wallet(request).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn deselect_wallet(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletSessionDto, crate::CommandErrorDto> {
|
||||
let result = state.deselect_wallet();
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||
let result = crate::emit_frontend_log_event(payload);
|
||||
@@ -90,10 +114,23 @@ async fn list_wallets(state: tauri::State<'_, crate::AppState>) -> std::result::
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn lock_wallet(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LockedWalletDto, crate::CommandErrorDto> {
|
||||
let result = state.lock_wallet().await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn refresh_wallets(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<std::vec::Vec<crate::WalletInventoryEntryDto>, crate::CommandErrorDto> {
|
||||
let deselected = state.deselect_wallet();
|
||||
if let std::result::Result::Err(error) = deselected {
|
||||
return std::result::Result::Err(crate::CommandErrorDto::from_error(&error));
|
||||
}
|
||||
let root = state.wallet_inventory_root().to_path_buf();
|
||||
let result = crate::list_wallet_inventory(root.as_path()).await;
|
||||
return match result {
|
||||
@@ -107,8 +144,7 @@ async fn select_wallet(
|
||||
request: crate::WalletSelectionRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::LockedWalletDto, crate::CommandErrorDto> {
|
||||
let root = state.wallet_inventory_root().to_path_buf();
|
||||
let result = crate::select_locked_wallet(root.as_path(), request).await;
|
||||
let result = state.select_wallet(request).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/wallet_inventory.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Root-scoped Wallet inventory and locked-file selection for Wallet Desk.
|
||||
|
||||
@@ -134,8 +134,21 @@ pub(crate) async fn list_wallet_inventory(root: &std::path::Path) -> ksp_core_li
|
||||
}
|
||||
|
||||
/// Re-resolves and re-inspects one selected inventory identifier without exposing its full path.
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn select_locked_wallet(root: &std::path::Path, request: WalletSelectionRequestDto) -> ksp_core_lib::Result<LockedWalletDto> {
|
||||
let path = resolve_wallet_path(root, request.wallet_id.as_str()).await;
|
||||
let resolved = resolve_locked_wallet(root, request.wallet_id).await;
|
||||
return match resolved {
|
||||
std::result::Result::Ok((_, _, dto)) => std::result::Result::Ok(dto),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Resolves one locked Wallet into its Rust-only path, locked info and safe DTO.
|
||||
pub(crate) async fn resolve_locked_wallet(
|
||||
root: &std::path::Path,
|
||||
wallet_id: String,
|
||||
) -> ksp_core_lib::Result<(std::path::PathBuf, ksp_wallet_lib::LockedWalletInfo, LockedWalletDto)> {
|
||||
let path = resolve_wallet_path(root, wallet_id.as_str()).await;
|
||||
let path = match path {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -145,13 +158,22 @@ pub(crate) async fn select_locked_wallet(root: &std::path::Path, request: Wallet
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, wallet_id = request.wallet_id.as_str(), format_version = locked.format_version(), view_enabled = locked.view_enabled(), "Locked Wallet selected from inventory");
|
||||
return std::result::Result::Ok(LockedWalletDto {
|
||||
filename: request.wallet_id.clone(),
|
||||
let dto = LockedWalletDto {
|
||||
filename: wallet_id.clone(),
|
||||
format_version: locked.format_version(),
|
||||
wallet_id: request.wallet_id,
|
||||
wallet_id: wallet_id.clone(),
|
||||
view_enabled: locked.view_enabled(),
|
||||
});
|
||||
};
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, wallet_id = wallet_id.as_str(), format_version = locked.format_version(), view_enabled = locked.view_enabled(), "Locked Wallet selected from inventory");
|
||||
return std::result::Result::Ok((path, locked, dto));
|
||||
}
|
||||
|
||||
/// Validates a root-scoped destination filename for a new native Wallet.
|
||||
pub(crate) fn new_wallet_destination(root: &std::path::Path, filename: &str) -> ksp_core_lib::Result<std::path::PathBuf> {
|
||||
if !is_wallet_filename(filename) {
|
||||
return selection_invalid(filename, "Wallet destination must be one UTF-8 filename ending in .kspwallet");
|
||||
}
|
||||
return std::result::Result::Ok(root.join(filename));
|
||||
}
|
||||
|
||||
fn valid_inventory_entry(filename: String, locked: ksp_wallet_lib::LockedWalletInfo) -> WalletInventoryEntryDto {
|
||||
|
||||
166
crates/ksp-app-wallet-desk/src/wallet_session.rs
Normal file
166
crates/ksp-app-wallet-desk/src/wallet_session.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/wallet_session.rs
|
||||
// version: 1
|
||||
|
||||
//! Durable root-scoped Wallet session lifecycle for Wallet Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Safe state code projected for the current Wallet session.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletSessionStateDto.ts")]
|
||||
pub(crate) enum WalletSessionStateDto {
|
||||
/// No Wallet is selected.
|
||||
NoSelection,
|
||||
/// One Wallet is selected but remains locked.
|
||||
Locked,
|
||||
/// One Wallet is open with OWNER capability.
|
||||
OwnerOpen,
|
||||
}
|
||||
|
||||
/// Minimal non-secret projection of the backend Wallet session.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletSessionDto.ts")]
|
||||
pub(crate) struct WalletSessionDto {
|
||||
/// Filename when a Wallet is selected.
|
||||
pub(crate) filename: std::option::Option<String>,
|
||||
/// Current session state.
|
||||
pub(crate) state: WalletSessionStateDto,
|
||||
/// Root-scoped identifier when a Wallet is selected.
|
||||
pub(crate) wallet_id: std::option::Option<String>,
|
||||
}
|
||||
|
||||
/// One protected Wallet note exposed only from an authorized session.
|
||||
#[derive(serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletNoteDto.ts")]
|
||||
pub(crate) struct WalletNoteDto {
|
||||
/// Stable note identifier generated by Wallet.
|
||||
pub(crate) id: String,
|
||||
/// Protected note text available only after authorization.
|
||||
pub(crate) text: String,
|
||||
}
|
||||
|
||||
/// Authorized Wallet projection returned after creation with OWNER capability.
|
||||
#[derive(serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletAuthorizedDto.ts")]
|
||||
pub(crate) struct WalletAuthorizedDto {
|
||||
/// Protected internal alias when configured.
|
||||
pub(crate) alias: std::option::Option<String>,
|
||||
/// Capability that produced this projection.
|
||||
pub(crate) capability: String,
|
||||
/// Native Wallet filename without its parent path.
|
||||
pub(crate) filename: String,
|
||||
/// Native Wallet format version.
|
||||
pub(crate) format_version: u32,
|
||||
/// Protected notes available to the OWNER session.
|
||||
pub(crate) notes: std::vec::Vec<WalletNoteDto>,
|
||||
/// Authorized Solana public key.
|
||||
pub(crate) pubkey: String,
|
||||
/// Root-scoped identifier accepted by later Wallet Desk operations.
|
||||
pub(crate) wallet_id: String,
|
||||
/// Whether the created Wallet has an enabled VIEW slot.
|
||||
pub(crate) view_enabled: bool,
|
||||
}
|
||||
|
||||
/// Create request moved from the frontend directly into Wallet password wrappers.
|
||||
#[derive(serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletCreateRequestDto.ts")]
|
||||
pub(crate) struct WalletCreateRequestDto {
|
||||
/// Optional protected internal alias stored inside the Wallet.
|
||||
pub(crate) alias: std::option::Option<String>,
|
||||
/// Root-scoped destination filename.
|
||||
pub(crate) filename: String,
|
||||
/// Optional first protected note stored inside the Wallet.
|
||||
pub(crate) initial_note: std::option::Option<String>,
|
||||
/// OWNER password transported only frontend -> Rust.
|
||||
pub(crate) owner_password: String,
|
||||
/// Optional VIEW password; `None` keeps VIEW disabled.
|
||||
pub(crate) view_password: std::option::Option<String>,
|
||||
}
|
||||
|
||||
/// Backend Wallet session. Full paths and authorized handles never cross IPC.
|
||||
pub(crate) enum WalletSession {
|
||||
/// No current Wallet selection.
|
||||
NoSelection,
|
||||
/// One re-inspected locked Wallet.
|
||||
Locked {
|
||||
/// Native Wallet filename / root-scoped identifier.
|
||||
wallet_id: String,
|
||||
/// Full root-scoped filesystem path retained only in Rust.
|
||||
path: std::path::PathBuf,
|
||||
/// Locked information re-inspected from the file.
|
||||
locked_info: ksp_wallet_lib::LockedWalletInfo,
|
||||
},
|
||||
/// One Wallet kept open with OWNER capability.
|
||||
Owner {
|
||||
/// Native Wallet filename / root-scoped identifier.
|
||||
wallet_id: String,
|
||||
/// Full root-scoped filesystem path retained only in Rust.
|
||||
path: std::path::PathBuf,
|
||||
/// Authorized OWNER handle retaining secret material only in Rust.
|
||||
wallet: ksp_wallet_lib::WalletOwner,
|
||||
},
|
||||
}
|
||||
|
||||
impl WalletSession {
|
||||
/// Creates an empty application session.
|
||||
#[must_use]
|
||||
pub(crate) const fn no_selection() -> Self {
|
||||
return Self::NoSelection;
|
||||
}
|
||||
|
||||
/// Returns a minimal safe session projection.
|
||||
#[must_use]
|
||||
pub(crate) fn safe_projection(&self) -> WalletSessionDto {
|
||||
return match self {
|
||||
Self::NoSelection => WalletSessionDto {
|
||||
filename: std::option::Option::None,
|
||||
state: WalletSessionStateDto::NoSelection,
|
||||
wallet_id: std::option::Option::None,
|
||||
},
|
||||
Self::Locked { wallet_id, locked_info, .. } => {
|
||||
let _ = locked_info.format_version();
|
||||
WalletSessionDto {
|
||||
filename: std::option::Option::Some(wallet_id.clone()),
|
||||
state: WalletSessionStateDto::Locked,
|
||||
wallet_id: std::option::Option::Some(wallet_id.clone()),
|
||||
}
|
||||
},
|
||||
Self::Owner { wallet_id, .. } => WalletSessionDto {
|
||||
filename: std::option::Option::Some(wallet_id.clone()),
|
||||
state: WalletSessionStateDto::OwnerOpen,
|
||||
wallet_id: std::option::Option::Some(wallet_id.clone()),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an OWNER handle into the authorized response DTO without exposing secret key material.
|
||||
pub(crate) fn owner_projection(wallet_id: &str, view_enabled: bool, owner: &ksp_wallet_lib::WalletOwner) -> WalletAuthorizedDto {
|
||||
let info = owner.info();
|
||||
let notes = info
|
||||
.notes()
|
||||
.iter()
|
||||
.map(|note| {
|
||||
return WalletNoteDto { id: note.id().to_owned(), text: note.text().to_owned() };
|
||||
})
|
||||
.collect();
|
||||
return WalletAuthorizedDto {
|
||||
alias: info.alias().map(|value| return value.to_owned()),
|
||||
capability: "owner".to_owned(),
|
||||
filename: wallet_id.to_owned(),
|
||||
format_version: info.format_version(),
|
||||
notes,
|
||||
pubkey: info.pubkey().to_string(),
|
||||
wallet_id: wallet_id.to_owned(),
|
||||
view_enabled,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/wallet_session.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user