v0.2.6-pre.008

This commit is contained in:
2026-08-21 11:36:28 +02:00
parent 787df2861f
commit 1a9edd14e1
19 changed files with 902 additions and 45 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 174
# version: 175
[workspace]
resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package]
version = "0.2.6-pre.7.fix.2"
version = "0.2.6-pre.8"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
@@ -34,6 +34,7 @@ tempfile = { version = "^3.27" }
chrono = { version = "^0.4", default-features = false }
tauri = { version = "^2.11" }
tauri-build = { version = "^2.6" }
tauri-plugin-dialog = { version = "^2.7" }
tauri-plugin-tracing = { version = "^0.3" }
ts-rs = { version = "^12.0" }
zeroize = { version = "^1.9" }

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-app-wallet-desk/Cargo.toml
# version: 3
# version: 4
[package]
name = "ksp-app-wallet-desk"
@@ -31,9 +31,11 @@ ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
ksp-wallet-lib = { path = "../ksp-wallet-lib" }
serde = { workspace = true, features = ["derive"] }
tauri.workspace = true
tauri-plugin-dialog.workspace = true
tauri-plugin-tracing.workspace = true
tokio = { workspace = true, features = ["fs", "rt", "time"] }
tokio = { workspace = true, features = ["fs", "io-util", "rt", "time"] }
ts-rs.workspace = true
zeroize.workspace = true
[dev-dependencies]
serde_json.workspace = true

View File

@@ -42,7 +42,7 @@
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1>
<p class="text-body-secondary mb-0">Identité autorisée et balance SOL via le Transport HTTP composite sont actives.</p>
</div>
<span class="badge text-bg-secondary">0.2.6-pre.007</span>
<span class="badge text-bg-secondary">0.2.6-pre.008</span>
</div>
<section data-view-panel="dashboard">
<div class="row g-3">
@@ -163,6 +163,71 @@
Les passwords sont envoyés uniquement du frontend vers Rust pour l'opération courante. Ils ne sont jamais renvoyés par Rust ni journalisés.
</div>
</div>
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Importer un keypair Solana vers un wallet KSP</div>
<div class="card-body">
<div class="row g-3 mb-3">
<div class="col-md-5">
<label class="form-label" for="importTransferFormat">Format source</label>
<select id="importTransferFormat" class="form-select">
<option value="solana_cli_json">Solana CLI JSON</option>
<option value="solana_keypair_base58">Keypair Base58 complet</option>
</select>
</div>
<div class="col-md-7 d-flex align-items-end">
<button id="inspectImportSource" class="btn btn-outline-primary" type="button">
<i class="fa-solid fa-folder-open me-2" aria-hidden="true"></i>Choisir et inspecter la source
</button>
</div>
<div class="col-12">
<div class="alert alert-secondary mb-0">
<div><strong>Source :</strong> <span id="importSourceName"></span></div>
<div><strong>Format :</strong> <span id="importSourceFormat"></span></div>
<div><strong>Pubkey inspectée :</strong> <span id="importSourcePubkey" class="text-break"></span></div>
<div class="small text-body-secondary mt-2">Le chemin et le contenu secret restent exclusivement côté Rust. Le frontend ne reçoit que cette projection publique.</div>
</div>
</div>
</div>
<form id="importWalletForm" autocomplete="off">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="importWalletFilename">Destination .kspwallet</label>
<input id="importWalletFilename" class="form-control" type="text" value="imported-01.kspwallet" required spellcheck="false">
</div>
<div class="col-md-6">
<label class="form-label" for="importWalletAlias">Alias interne optionnel</label>
<input id="importWalletAlias" class="form-control" type="text" autocomplete="off">
</div>
<div class="col-md-6">
<label class="form-label" for="importWalletOwnerPassword">Nouveau OWNER password</label>
<input id="importWalletOwnerPassword" class="form-control" type="password" autocomplete="new-password" required>
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="form-check mb-2">
<input id="importWalletEnableView" class="form-check-input" type="checkbox">
<label class="form-check-label" for="importWalletEnableView">Créer aussi une capability VIEW</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label" for="importWalletViewPassword">Nouveau VIEW password</label>
<input id="importWalletViewPassword" class="form-control" type="password" autocomplete="new-password" disabled>
</div>
<div class="col-12">
<label class="form-label" for="importWalletInitialNote">Note initiale optionnelle</label>
<textarea id="importWalletInitialNote" class="form-control" rows="3" autocomplete="off"></textarea>
</div>
<div class="col-12 d-flex align-items-center gap-3">
<button id="importWalletSubmit" class="btn btn-primary" type="submit" disabled>
<i class="fa-solid fa-file-import me-2" aria-hidden="true"></i>Import wallet
</button>
<span id="importWalletStatus" class="text-body-secondary" role="status">Choisir une source.</span>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
<section data-view-panel="details" hidden>
@@ -246,7 +311,7 @@
</div>
</main>
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
<span>KSP Wallet Desk · authorized identity + getBalance pre.007</span>
<span>KSP Wallet Desk · native transfer import pre.008</span>
</footer>
<script type="module" src="./ts/main.ts"></script>
</body>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 7
// version: 8
import "bootstrap";
import DataTable from "datatables.net-bs5";
@@ -9,6 +9,10 @@ import "simplebar";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { WalletBalanceDto } from "./bindings/ksp_app_wallet_desk/wallet_balance/WalletBalanceDto.ts";
import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts";
import type { WalletImportRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletImportRequestDto.ts";
import type { WalletImportSourceRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletImportSourceRequestDto.ts";
import type { WalletTransferFormatDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletTransferFormatDto.ts";
import type { WalletTransferInspectionDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletTransferInspectionDto.ts";
import type { LockedWalletDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/LockedWalletDto.ts";
import type { WalletInventoryEntryDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletInventoryEntryDto.ts";
import type { WalletSelectionRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletSelectionRequestDto.ts";
@@ -40,6 +44,7 @@ let activeWalletId: string | null = null;
let activeSessionState: ActiveSessionState = "no_selection";
let activeViewEnabled = false;
let configuredSecretCandidateCount = 0;
let stagedImportSource: WalletTransferInspectionDto | null = null;
function isViewId(value: string): value is ViewId {
return value in viewTitles;
@@ -170,7 +175,7 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.",
},
});
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.007-wallet-balance" });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.008-wallet-import" });
}
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
@@ -230,6 +235,44 @@ function updateUnlockActions(): void {
setText("#unlockSecretCandidateCount", configuredSecretCandidateCount.toString());
}
function importFormatLabel(format: WalletTransferFormatDto): string {
return format === "solana_cli_json" ? "Solana CLI JSON" : "Keypair Base58 complet";
}
function updateImportActions(): void {
const submit = document.querySelector<HTMLButtonElement>("#importWalletSubmit");
if (submit) {
submit.disabled = stagedImportSource === null;
}
}
function clearImportSourceProjection(): void {
stagedImportSource = null;
setText("#importSourceName", "—");
setText("#importSourceFormat", "—");
setText("#importSourcePubkey", "—");
updateImportActions();
}
function clearImportSensitiveInputs(): void {
const ownerPassword = document.querySelector<HTMLInputElement>("#importWalletOwnerPassword");
const viewPassword = document.querySelector<HTMLInputElement>("#importWalletViewPassword");
const alias = document.querySelector<HTMLInputElement>("#importWalletAlias");
const note = document.querySelector<HTMLTextAreaElement>("#importWalletInitialNote");
if (ownerPassword) {
ownerPassword.value = "";
}
if (viewPassword) {
viewPassword.value = "";
}
if (alias) {
alias.value = "";
}
if (note) {
note.value = "";
}
}
function clearCreateFormSensitiveInputs(): void {
const ownerPassword = document.querySelector<HTMLInputElement>("#createWalletOwnerPassword");
const viewPassword = document.querySelector<HTMLInputElement>("#createWalletViewPassword");
@@ -483,6 +526,129 @@ function bindCreateWalletForm(): void {
frontendTrace("main", "Create Wallet form handlers installed");
}
function importRequestFromForm(): WalletImportRequestDto | null {
const filename = document.querySelector<HTMLInputElement>("#importWalletFilename");
const ownerPassword = document.querySelector<HTMLInputElement>("#importWalletOwnerPassword");
const enableView = document.querySelector<HTMLInputElement>("#importWalletEnableView");
const viewPassword = document.querySelector<HTMLInputElement>("#importWalletViewPassword");
const alias = document.querySelector<HTMLInputElement>("#importWalletAlias");
const note = document.querySelector<HTMLTextAreaElement>("#importWalletInitialNote");
if (!stagedImportSource || !filename || !ownerPassword || !enableView || !viewPassword || !alias || !note) {
return null;
}
const normalizedFilename = filename.value.trim();
if (!normalizedFilename.endsWith(".kspwallet") || normalizedFilename.length <= ".kspwallet".length || ownerPassword.value.length === 0) {
return null;
}
if (enableView.checked && viewPassword.value.length === 0) {
return null;
}
return {
alias: alias.value.length === 0 ? null : alias.value,
filename: normalizedFilename,
initialNote: note.value.length === 0 ? null : note.value,
ownerPassword: ownerPassword.value,
viewPassword: enableView.checked ? viewPassword.value : null,
};
}
async function inspectImportSource(): Promise<void> {
const formatControl = document.querySelector<HTMLSelectElement>("#importTransferFormat");
if (!formatControl) {
return;
}
const format = formatControl.value as WalletTransferFormatDto;
const request: WalletImportSourceRequestDto = { format };
clearImportSourceProjection();
setText("#importWalletStatus", "Sélection native de la source…");
frontendDebug("main", "Wallet import native picker requested", { format });
try {
const inspection = await invokeKsp<WalletTransferInspectionDto | null>("main", "inspect_import_source", { request });
if (!inspection) {
setText("#importWalletStatus", "Sélection annulée.");
frontendDebug("main", "Wallet import native picker cancelled", { format });
return;
}
stagedImportSource = inspection;
setText("#importSourceName", inspection.sourceName);
setText("#importSourceFormat", importFormatLabel(inspection.format));
setText("#importSourcePubkey", inspection.pubkey);
setText("#importWalletStatus", "Source validée. Saisir les nouveaux credentials KSP.");
updateImportActions();
frontendInfo("main", "Wallet transfer source inspected", { format: inspection.format });
} catch {
clearImportSourceProjection();
setText("#importWalletStatus", "Source invalide ou illisible. Consulter le diagnostic et les logs.");
frontendWarn("main", "Wallet transfer source inspection failed", { format });
}
}
function bindImportWalletForm(): void {
const form = document.querySelector<HTMLFormElement>("#importWalletForm");
const format = document.querySelector<HTMLSelectElement>("#importTransferFormat");
const pick = document.querySelector<HTMLButtonElement>("#inspectImportSource");
const enableView = document.querySelector<HTMLInputElement>("#importWalletEnableView");
const viewPassword = document.querySelector<HTMLInputElement>("#importWalletViewPassword");
if (format) {
format.addEventListener("change", () => {
clearImportSourceProjection();
setText("#importWalletStatus", "Format modifié ; choisir une nouvelle source.");
void invokeKsp<void>("main", "clear_import_source").catch(() => {
frontendWarn("main", "Backend staged Wallet import source clear failed");
});
frontendTrace("main", "Wallet import format changed", { format: format.value });
});
}
if (pick) {
pick.addEventListener("click", () => {
void inspectImportSource();
});
}
if (enableView && viewPassword) {
enableView.addEventListener("change", () => {
viewPassword.disabled = !enableView.checked;
if (!enableView.checked) {
viewPassword.value = "";
}
frontendTrace("main", "Import Wallet VIEW option changed", { enabled: enableView.checked });
});
}
if (!form) {
return;
}
form.addEventListener("submit", event => {
event.preventDefault();
const request = importRequestFromForm();
if (!request || !stagedImportSource) {
setText("#importWalletStatus", "Source/filename/passwords invalides ou incomplets.");
frontendWarn("main", "Import Wallet form validation rejected request");
return;
}
const requestedWalletId = request.filename;
const transferFormat = stagedImportSource.format;
const requestedViewEnabled = request.viewPassword !== null;
setText("#importWalletStatus", "Import en cours…");
frontendDebug("main", "Import Wallet request submitted", { transferFormat, viewEnabled: requestedViewEnabled, walletId: requestedWalletId });
clearSelectedWallet();
void invokeKsp<WalletAuthorizedDto>("main", "import_wallet", { request })
.then(async wallet => {
clearImportSensitiveInputs();
clearImportSourceProjection();
renderAuthorizedWallet(wallet);
setText("#importWalletStatus", "Wallet importé ; session OWNER ouverte.");
await loadWalletInventory("list_wallets", false);
activateView("dashboard", "user");
})
.catch(() => {
clearImportSensitiveInputs();
clearImportSourceProjection();
setText("#importWalletStatus", "Import impossible ; choisir à nouveau la source. Consulter le diagnostic et les logs.");
frontendWarn("main", "Import Wallet operation failed", { transferFormat, walletId: requestedWalletId });
});
});
frontendTrace("main", "Import Wallet form handlers installed");
}
function beginFrontendUnlock(capability: UnlockCapability, provider: "manual" | "configured"): string | null {
if (activeSessionState !== "locked" || !activeWalletId) {
return null;
@@ -658,7 +824,7 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory);
setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent");
setText("#runtimeShellPhase", status.shellPhase);
setText("#shellStatus", "Config/Transport résolus ; VIEW/OWNER et getBalance prêts.");
setText("#shellStatus", "Config/Transport résolus ; import natif, VIEW/OWNER et getBalance prêts.");
frontendTrace("main", "Wallet Desk runtime status rendered", {
compositeProfile: status.activeCompositeProfile,
fallbackLoggingActive: status.fallbackLoggingActive,
@@ -706,6 +872,7 @@ async function initializeMain(): Promise<void> {
bindNavigation();
bindWalletTableSelection();
bindCreateWalletForm();
bindImportWalletForm();
bindUnlockActions();
bindBalanceActions();
bindShellActions();

View File

@@ -1,7 +1,7 @@
{
"name": "ksp-app-wallet-desk",
"private": true,
"version": "0.2.6-pre.7.fix.2",
"version": "0.2.6-pre.8",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/app_state.rs
// version: 8
// version: 9
//! Shared backend state owned by the Wallet Desk Tauri application.
@@ -11,6 +11,7 @@ pub(crate) struct AppState {
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>,
}
@@ -69,6 +70,7 @@ impl AppState {
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()),
});
}
@@ -124,7 +126,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.007-wallet-balance".to_owned(),
shell_phase: "pre.008-wallet-import".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
transport_available_endpoint_count,
transport_clusters: self.transport_runtime.clusters(),
@@ -220,6 +222,102 @@ impl AppState {
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 = std::mem::replace(&mut *slot, std::option::Option::Some(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_v1(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, 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);
@@ -656,6 +754,10 @@ fn normalize_optional_text(value: std::option::Option<String>) -> std::option::O
});
}
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");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/constants.rs
// version: 6
// version: 7
//! 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_TRANSPORT: &str = "wallet.transport";
/// Structured domain used while preparing Wallet filesystem roots.
pub(crate) const TRACING_DOMAIN_WALLET_CONFIG: &str = "wallet.config";
/// Structured domain used by native-picker Wallet transfer import operations.
pub(crate) const TRACING_DOMAIN_WALLET_IMPORT: &str = "wallet.import";
/// Structured domain used while enumerating and inspecting locked Wallet files.
pub(crate) const TRACING_DOMAIN_WALLET_INVENTORY: &str = "wallet.inventory";
/// Structured domain used by Config-owned Wallet password candidate operations.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/errors.rs
// version: 6
// version: 7
//! Application-local error codes for Wallet Desk composition and desktop runtime surfaces.
@@ -34,6 +34,10 @@ pub(crate) const ERROR_CODE_WALLET_DIRECTORY_INVALID: ksp_core_lib::ErrorCode =
/// Wallet Desk could not inspect or create the configured Wallet directory tree.
pub(crate) const ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_directory_prepare_failed");
/// A native-picker Wallet transfer source is not a supported regular file or cannot be staged safely.
pub(crate) const ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_import_source_invalid");
/// Wallet import was requested without one previously inspected native-picker source.
pub(crate) const ERROR_CODE_WALLET_IMPORT_SOURCE_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_import_source_missing");
/// Wallet Desk cannot enumerate or inspect its effective Wallet inventory directory.
pub(crate) const ERROR_CODE_WALLET_INVENTORY_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_inventory_failed");
/// No effective Config-owned Wallet password candidate is available for the explicit configured-secret unlock action.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/lib.rs
// version: 7
// version: 8
//! Tauri desktop application shell for KSP Wallet management and inspection.
@@ -21,6 +21,7 @@ mod tw_main;
mod tw_splash;
mod wallet_balance;
mod wallet_config;
mod wallet_import;
mod wallet_inventory;
mod wallet_secrets;
mod wallet_session;
@@ -56,6 +57,8 @@ pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
pub(crate) use self::constants::TRACING_DOMAIN_TRANSPORT;
/// Structured domain used while preparing Wallet filesystem roots.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_CONFIG;
/// Structured domain used by native-picker Wallet transfer import operations.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_IMPORT;
/// Structured domain used while enumerating and inspecting locked Wallet files.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_INVENTORY;
/// Structured domain used by Config-owned Wallet password candidate operations.
@@ -106,6 +109,10 @@ pub(crate) use self::errors::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED;
pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_INVALID;
/// Wallet Desk could not inspect or create the configured Wallet directory tree.
pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED;
/// Native-picker transfer source is invalid or cannot be staged.
pub(crate) use self::errors::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID;
/// Wallet import has no staged native-picker source.
pub(crate) use self::errors::ERROR_CODE_WALLET_IMPORT_SOURCE_MISSING;
/// Wallet inventory filesystem operation failed.
pub(crate) use self::errors::ERROR_CODE_WALLET_INVENTORY_FAILED;
/// No effective Config-owned Wallet password candidate is available.
@@ -146,6 +153,18 @@ pub(crate) use self::wallet_balance::format_lamports_as_sol;
pub(crate) use self::wallet_config::WalletConfigStartup;
/// Resolves the composite-selected Wallet Config and prepares its application-owned directory tree.
pub(crate) use self::wallet_config::initialize_wallet_config;
/// Rust-only staged import source containing bounded zeroizing transfer bytes.
pub(crate) use self::wallet_import::PendingWalletImport;
/// Import request whose new native Wallet credentials move frontend -> Rust only.
pub(crate) use self::wallet_import::WalletImportRequestDto;
/// Native picker request containing only the expected transfer format.
pub(crate) use self::wallet_import::WalletImportSourceRequestDto;
/// Safe transfer format DTO used by the import picker.
pub(crate) use self::wallet_import::WalletTransferFormatDto;
/// Safe inspection projection for one staged external transfer source.
pub(crate) use self::wallet_import::WalletTransferInspectionDto;
/// Loads and validates one native-picker import source into bounded zeroizing Rust memory.
pub(crate) use self::wallet_import::stage_wallet_import_source;
/// Safe locked Wallet projection returned after selection.
pub(crate) use self::wallet_inventory::LockedWalletDto;
/// Locked inspection outcome exposed by Wallet inventory rows in crate unit tests.

View File

@@ -1,8 +1,10 @@
// file: crates/ksp-app-wallet-desk/src/tauri.rs
// version: 5
// version: 6
//! Tauri runtime assembly for the KSP wallet desktop application.
use tauri_plugin_dialog::DialogExt; // rust-rules: trait-import
/// Runs the Wallet Desk application.
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
@@ -32,16 +34,19 @@ fn configure_state(builder: tauri::Builder<tauri::Wry>, app_state: crate::AppSta
fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<tauri::Wry>();
return builder.plugin(tracing_plugin);
return builder.plugin(tauri_plugin_dialog::init()).plugin(tracing_plugin);
}
#[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![
clear_import_source,
create_wallet,
deselect_wallet,
emit_frontend_log,
get_runtime_status,
import_wallet,
inspect_import_source,
list_wallets,
lock_wallet,
refresh_wallet_balance,
@@ -70,6 +75,15 @@ fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri:
});
}
#[tauri::command]
fn clear_import_source(state: tauri::State<'_, crate::AppState>) -> std::result::Result<(), crate::CommandErrorDto> {
let result = state.clear_import_source();
return match result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn create_wallet(
request: crate::WalletCreateRequestDto,
@@ -109,6 +123,59 @@ fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::
};
}
#[tauri::command]
async fn import_wallet(
request: crate::WalletImportRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.import_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]
async fn inspect_import_source(
app: tauri::AppHandle,
request: crate::WalletImportSourceRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::option::Option<crate::WalletTransferInspectionDto>, crate::CommandErrorDto> {
let cleared = state.clear_import_source();
if let std::result::Result::Err(error) = cleared {
return std::result::Result::Err(crate::CommandErrorDto::from_error(&error));
}
let format = request.format;
let extensions = [format.default_extension()];
let selected = app
.dialog()
.file()
.set_title("Select Solana keypair transfer source")
.add_filter("Solana keypair transfer", &extensions)
.blocking_pick_file();
let selected = match selected {
std::option::Option::Some(value) => value,
std::option::Option::None => {
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_IMPORT, transfer_format = format.code(), "Wallet import native picker cancelled");
return std::result::Result::Ok(std::option::Option::None);
},
};
let path = selected.into_path();
let path = match path {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
let error =
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Selected Wallet transfer source is not a local filesystem path");
return std::result::Result::Err(crate::CommandErrorDto::from_error(&error));
},
};
let result = state.stage_import_source(path, format).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn refresh_wallet_balance(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletBalanceDto, crate::CommandErrorDto> {
let result = state.refresh_wallet_balance().await;

View File

@@ -0,0 +1,190 @@
// file: crates/ksp-app-wallet-desk/src/wallet_import.rs
// version: 1
//! Native-picker transfer import staging with bounded secret material kept only in Rust.
use tokio::io::AsyncReadExt; // rust-rules: trait-import
use ts_rs::TS; // rust-rules: trait-import
/// Transfer format selected before opening the native import picker.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_import/WalletTransferFormatDto.ts")]
pub(crate) enum WalletTransferFormatDto {
/// Standard Solana CLI keypair JSON array containing 64 byte values.
SolanaCliJson,
/// Canonical Base58 encoding of the complete 64-byte Solana keypair.
SolanaKeypairBase58,
}
impl WalletTransferFormatDto {
/// Maps the desktop DTO to the stable Wallet transfer format.
#[must_use]
pub(crate) const fn wallet_format(self) -> ksp_wallet_lib::WalletTransferFormat {
return match self {
Self::SolanaCliJson => ksp_wallet_lib::WalletTransferFormat::SolanaCliJson,
Self::SolanaKeypairBase58 => ksp_wallet_lib::WalletTransferFormat::SolanaKeypairBase58,
};
}
/// Returns the native picker extension used as a convenience filter.
#[must_use]
pub(crate) const fn default_extension(self) -> &'static str {
return match self {
Self::SolanaCliJson => "json",
Self::SolanaKeypairBase58 => "txt",
};
}
/// Returns the stable transfer format code used by safe diagnostics.
#[must_use]
pub(crate) const fn code(self) -> &'static str {
return match self {
Self::SolanaCliJson => "solana_cli_json",
Self::SolanaKeypairBase58 => "solana_keypair_base58",
};
}
const fn maximum_source_bytes(self) -> usize {
return match self {
Self::SolanaCliJson => ksp_wallet_lib::KSPWALLET_TRANSFER_MAX_SOLANA_CLI_JSON_BYTES,
Self::SolanaKeypairBase58 => ksp_wallet_lib::KSPWALLET_TRANSFER_MAX_BASE58_BYTES,
};
}
}
/// Request that chooses only the transfer format; the source path comes from the native Rust picker.
#[derive(serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_import/WalletImportSourceRequestDto.ts")]
pub(crate) struct WalletImportSourceRequestDto {
/// Transfer encoding expected for the selected external file.
pub(crate) format: WalletTransferFormatDto,
}
/// Safe projection returned after validating one native-picker import source.
#[derive(Clone, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_import/WalletTransferInspectionDto.ts")]
pub(crate) struct WalletTransferInspectionDto {
/// Transfer format used to validate the secret source.
pub(crate) format: WalletTransferFormatDto,
/// Public identity derived from the imported keypair.
pub(crate) pubkey: String,
/// Basename only; no parent path is ever projected.
pub(crate) source_name: String,
}
/// Import request whose new native Wallet credentials move frontend -> Rust only.
#[derive(serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_import/WalletImportRequestDto.ts")]
pub(crate) struct WalletImportRequestDto {
/// Optional protected internal alias stored in the new native Wallet.
pub(crate) alias: std::option::Option<String>,
/// Root-scoped destination filename under the effective Wallet directory.
pub(crate) filename: String,
/// Optional first protected note stored in the imported Wallet.
pub(crate) initial_note: std::option::Option<String>,
/// New OWNER password transported only frontend -> Rust.
pub(crate) owner_password: String,
/// Optional new VIEW password; `None` keeps VIEW disabled.
pub(crate) view_password: std::option::Option<String>,
}
/// Rust-only staged transfer source. Secret bytes are bounded and zeroized on drop.
pub(crate) struct PendingWalletImport {
bytes: zeroize::Zeroizing<std::vec::Vec<u8>>,
format: WalletTransferFormatDto,
inspection: WalletTransferInspectionDto,
}
impl PendingWalletImport {
/// Returns the safe inspection projection without exposing source bytes or path.
#[must_use]
pub(crate) fn safe_projection(&self) -> WalletTransferInspectionDto {
return self.inspection.clone();
}
/// Returns the bounded secret transfer bytes for one immediate Wallet import operation.
#[must_use]
pub(crate) fn source_bytes(&self) -> &[u8] {
return self.bytes.as_slice();
}
/// Returns the stable Wallet transfer format corresponding to the staged source.
#[must_use]
pub(crate) const fn wallet_format(&self) -> ksp_wallet_lib::WalletTransferFormat {
return self.format.wallet_format();
}
}
/// Reads, bounds and validates one native-picker transfer source while retaining no external path.
pub(crate) async fn stage_wallet_import_source(source: std::path::PathBuf, format: WalletTransferFormatDto) -> ksp_core_lib::Result<PendingWalletImport> {
let metadata = tokio::fs::symlink_metadata(source.as_path()).await;
let metadata = match metadata {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Cannot inspect the selected Wallet transfer source")
.with_source(error),
);
},
};
if metadata.file_type().is_symlink() || !metadata.is_file() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID,
"Selected Wallet transfer source must be one regular non-symlink file",
));
}
let source_name = source.file_name().and_then(std::ffi::OsStr::to_str);
let source_name = match source_name {
std::option::Option::Some(value) if !value.is_empty() => value.to_owned(),
_ => "selected-transfer".to_owned(),
};
let file = tokio::fs::File::open(source.as_path()).await;
let file = match file {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Cannot open the selected Wallet transfer source").with_source(error),
);
},
};
let maximum_source_bytes = format.maximum_source_bytes();
let read_limit = maximum_source_bytes.saturating_add(1);
let read_limit = u64::try_from(read_limit);
let read_limit = match read_limit {
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, "Wallet transfer source bound exceeds platform range").with_source(error),
);
},
};
let mut reader = file.take(read_limit);
let mut bytes = zeroize::Zeroizing::new(std::vec::Vec::with_capacity(maximum_source_bytes.saturating_add(1)));
let read_result = reader.read_to_end(&mut *bytes).await;
if let std::result::Result::Err(error) = read_result {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID, "Cannot read the selected Wallet transfer source").with_source(error),
);
}
if bytes.len() > maximum_source_bytes {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_IMPORT_SOURCE_INVALID,
"Selected Wallet transfer source exceeds the supported format bound",
));
}
let inspection = ksp_wallet_lib::inspect_wallet_transfer(bytes.as_slice(), format.wallet_format());
let inspection = match inspection {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let dto = WalletTransferInspectionDto { format, pubkey: inspection.pubkey().to_string(), source_name };
return std::result::Result::Ok(PendingWalletImport { bytes, format, inspection: dto });
}
#[cfg(test)]
#[path = "../unit_tests/wallet_import.rs"]
mod tests;

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Wallet Desk",
"version": "0.2.6-pre.7.fix.2",
"version": "0.2.6-pre.8",
"identifier": "com.sasedev.ksp-app-wallet-desk",
"build": {
"beforeDevCommand": "npm run dev",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
// version: 7
// version: 8
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
@@ -198,3 +198,28 @@ fn pre_007_balance_uses_authorized_pubkey_and_composite_transport() {
assert!(main.contains("renderWalletBalance"));
assert!(main.contains("refresh_wallet_balance"));
}
#[test]
fn pre_008_import_uses_rust_native_picker_and_wallet_transfer_adapters() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
let state = read_text(root.join("src/app_state.rs").as_path());
let import = read_text(root.join("src/wallet_import.rs").as_path());
let tauri = read_text(root.join("src/tauri.rs").as_path());
let html = read_text(root.join("frontend/main.html").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(manifest.contains("tauri-plugin-dialog.workspace = true"));
assert!(manifest.contains("zeroize.workspace = true"));
assert!(tauri.contains("tauri_plugin_dialog::init()"));
assert!(tauri.contains("blocking_pick_file"));
assert!(tauri.contains("inspect_import_source"));
assert!(tauri.contains("import_wallet"));
assert!(import.contains("ksp_wallet_lib::inspect_wallet_transfer"));
assert!(import.contains("zeroize::Zeroizing"));
assert!(state.contains("ksp_wallet_lib::import_wallet_transfer_v1"));
assert!(html.contains("id=\"inspectImportSource\""));
assert!(html.contains("id=\"importWalletForm\""));
assert!(html.contains("id=\"importSourcePubkey\""));
assert!(main.contains("WalletTransferInspectionDto"));
assert!(main.contains("bindImportWalletForm"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
// version: 7
// version: 8
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
@@ -126,3 +126,39 @@ fn balance_refresh_never_accepts_pubkey_or_endpoint_url_from_frontend() {
assert!(!main.contains("endpointUrl"));
assert!(state.contains("wallet.pubkey()"));
}
#[test]
fn import_picker_path_and_transfer_secret_never_cross_into_frontend() {
let root = app_root();
let package = read_text(root.join("package.json").as_path());
let capability = read_text(root.join("capabilities/default.json").as_path());
let import = read_text(root.join("src/wallet_import.rs").as_path());
let tauri = read_text(root.join("src/tauri.rs").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(!package.contains("@tauri-apps/plugin-dialog"));
assert!(!capability.contains("dialog:"));
assert!(!capability.contains("fs:"));
assert!(!main.contains("@tauri-apps/plugin-dialog"));
assert!(!main.contains("sourcePath"));
assert!(!main.contains("source_path"));
assert!(import.contains("zeroize::Zeroizing"));
let inspection = import.split("pub(crate) struct WalletTransferInspectionDto").nth(1);
assert!(inspection.is_some());
let inspection = inspection.unwrap_or_default().split("}\n").next().unwrap_or_default();
let inspection_fields = inspection.lines().filter(|line| return line.trim_start().starts_with("pub(crate) ")).collect::<std::vec::Vec<_>>().join("\n");
assert!(!inspection_fields.contains("path"));
assert!(!inspection_fields.contains("password"));
let request = import.split("pub(crate) struct WalletImportRequestDto").nth(1);
assert!(request.is_some());
let request = request.unwrap_or_default().split("}\n").next().unwrap_or_default();
let request_fields = request.lines().filter(|line| return line.trim_start().starts_with("pub(crate) ")).collect::<std::vec::Vec<_>>().join("\n");
assert!(request_fields.contains("owner_password"));
assert!(request_fields.contains("view_password"));
assert!(!request_fields.contains("path"));
let command_start = tauri.find("async fn inspect_import_source");
assert!(command_start.is_some());
let command = &tauri[command_start.unwrap_or_default()..];
let command = command.split("#[tauri::command]").next().unwrap_or_default();
assert!(command.contains("WalletImportSourceRequestDto"));
assert!(!command.contains("source_path"));
}

View File

@@ -0,0 +1,49 @@
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_import.rs
// version: 1
#[test]
fn transfer_format_dto_maps_exact_supported_wallet_formats_and_bounds() {
assert_eq!(crate::WalletTransferFormatDto::SolanaCliJson.wallet_format(), ksp_wallet_lib::WalletTransferFormat::SolanaCliJson);
assert_eq!(crate::WalletTransferFormatDto::SolanaKeypairBase58.wallet_format(), ksp_wallet_lib::WalletTransferFormat::SolanaKeypairBase58);
assert_eq!(crate::WalletTransferFormatDto::SolanaCliJson.maximum_source_bytes(), ksp_wallet_lib::KSPWALLET_TRANSFER_MAX_SOLANA_CLI_JSON_BYTES);
assert_eq!(crate::WalletTransferFormatDto::SolanaKeypairBase58.maximum_source_bytes(), ksp_wallet_lib::KSPWALLET_TRANSFER_MAX_BASE58_BYTES);
}
#[test]
fn import_request_does_not_require_debug_or_clone_password_contracts() {
fn consume(_request: crate::WalletImportRequestDto) {}
consume(crate::WalletImportRequestDto {
alias: std::option::Option::None,
filename: "imported.kspwallet".to_owned(),
initial_note: std::option::Option::None,
owner_password: "owner-canary".to_owned(),
view_password: std::option::Option::Some("view-canary".to_owned()),
});
}
#[test]
fn staged_base58_source_keeps_only_safe_projection_and_bounded_zeroizing_bytes() {
let source = "99eUso3aSbE9tqGSTXzo3TLfKb9RkMTURrHKQ1K7Zh3StnzFNUx8FKCPPPPpR479qsw5zv2WNBKmgiz7WqgAJfM";
let path = std::env::temp_dir().join(format!("ksp-wallet-import-{}-seed7.txt", std::process::id()));
let written = std::fs::write(path.as_path(), source.as_bytes());
assert!(written.is_ok());
let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build();
assert!(runtime.is_ok());
let runtime = match runtime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let staged = runtime.block_on(crate::stage_wallet_import_source(path.clone(), crate::WalletTransferFormatDto::SolanaKeypairBase58));
let removed = std::fs::remove_file(path.as_path());
assert!(removed.is_ok());
assert!(staged.is_ok());
let staged = match staged {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let projection = staged.safe_projection();
assert_eq!(projection.pubkey, "GmaDrppBC7P5ARKV8g3djiwP89vz1jLK23V2GBjuAEGB");
assert_eq!(projection.format, crate::WalletTransferFormatDto::SolanaKeypairBase58);
assert!(projection.source_name.ends_with("seed7.txt"));
assert_eq!(staged.source_bytes(), source.as_bytes());
}

115
deltas/0.2.6/pre.008.md Normal file
View File

@@ -0,0 +1,115 @@
<!-- file: deltas/0.2.6/pre.008.md -->
<!-- version: 1 -->
# Delta `0.2.6-pre.008` — import natif Solana CLI JSON / Base58
## Base et version
- Base : `0.2.6-pre.007-fix.002`.
- Version Cargo : `0.2.6-pre.8`.
- Commit attendu : `v0.2.6-pre.008`.
- Aucun tag prerelease.
## Objet
Cette tranche matérialise l'import d'un keypair Solana externe vers un nouveau Wallet natif `.kspwallet` sans donner au frontend la propriété d'un chemin filesystem ni du contenu secret importé.
Les formats pris en charge sont les deux formats de transfert déjà possédés par `ksp-wallet-lib` :
- Solana CLI keypair JSON ;
- keypair Solana Base58 canonique.
## Picker natif backend-only
`tauri-plugin-dialog` est ajouté comme dépendance Rust workspace et initialisé par Wallet Desk. Le frontend ne dépend pas de `@tauri-apps/plugin-dialog` et aucune permission guest Dialog/FS n'est ajoutée.
Le frontend transmet uniquement le format attendu à `inspect_import_source`. Rust ouvre le picker natif, convertit la sélection en chemin local et ne renvoie jamais ce chemin par IPC.
Une annulation du picker est une sortie normale sans source staged.
## Staging borné et secret
`wallet_import.rs` lit uniquement un fichier régulier non-symlink et applique avant parsing les bornes publiques de `ksp-wallet-lib` :
- `KSPWALLET_TRANSFER_MAX_SOLANA_CLI_JSON_BYTES` ;
- `KSPWALLET_TRANSFER_MAX_BASE58_BYTES`.
Les octets sont conservés dans `Zeroizing<Vec<u8>>` côté Rust. La validation utilise `ksp-wallet-lib::inspect_wallet_transfer` et la projection frontend ne contient que :
- format ;
- Pubkey dérivée ;
- basename de la source.
Le parent du chemin, les octets du keypair et tout secret restent hors IPC/logs.
## Import vers `.kspwallet`
`import_wallet` consomme atomiquement la source staged et appelle `ksp-wallet-lib::import_wallet_transfer_v1` en mémoire. Le fichier destination :
- est dérivé uniquement de la racine Wallet effective possédée par Config et du filename root-scoped validé ;
- hérite des garanties no-clobber de Wallet ;
- reçoit un nouveau password OWNER et, optionnellement, un nouveau password VIEW ;
- peut recevoir alias et note initiale protégés ;
- conserve la même identité Solana que la source validée.
Après succès, le `WalletOwner` importé est conservé uniquement dans `AppState` et la session devient OWNER ouverte. La source staged est consommée et zéroïsée à sa destruction.
## Frontend
La vue `Create / Import` ajoute :
- sélection du format de transfert ;
- bouton d'ouverture du picker natif ;
- aperçu sûr basename / format / Pubkey ;
- filename `.kspwallet` destination ;
- alias optionnel ;
- password OWNER ;
- VIEW optionnel + password VIEW ;
- note initiale optionnelle ;
- action d'import.
Changer le format purge la projection et la source staged Rust. Après import ou erreur, les inputs sensibles et la projection de source sont purgés ; une nouvelle sélection native est nécessaire après un échec d'import.
## Sécurité / frontières
- aucun chemin source dans les DTO frontend ;
- aucun contenu keypair dans les DTO ou logs ;
- aucun accès filesystem guest ;
- aucun dialogue navigateur natif ;
- aucune dépendance npm Dialog ;
- le frontend ne choisit jamais la destination absolue ;
- `ksp-wallet-lib` reste propriétaire du décodage/import et de la publication no-clobber.
## Fichiers principaux
Ajouts :
- `crates/ksp-app-wallet-desk/src/wallet_import.rs` ;
- `crates/ksp-app-wallet-desk/unit_tests/wallet_import.rs` ;
- `deltas/0.2.6/pre.008.md`.
Modifications principales :
- dépendances/version workspace et Wallet Desk ;
- `AppState` pour staging/import ;
- commandes/plugins Tauri ;
- shell Create / Import et bindings frontend ;
- canaris desktop contract/security ;
- plan `0.2.6`, séquence fonctionnelle et roadmap.
## Validation opérateur attendue
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-app-wallet-desk
cargo test --workspace
KSP_WALLETS_DIRECTORY=var/wallet-desk-pre008 \
cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json
```
Ne pas exécuter `cargo tauri build` dans cette tranche.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long