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 # file: Cargo.toml
# version: 174 # version: 175
[workspace] [workspace]
resolver = "3" 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"] 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] [workspace.package]
version = "0.2.6-pre.7.fix.2" version = "0.2.6-pre.8"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
@@ -34,6 +34,7 @@ tempfile = { version = "^3.27" }
chrono = { version = "^0.4", default-features = false } chrono = { version = "^0.4", default-features = false }
tauri = { version = "^2.11" } tauri = { version = "^2.11" }
tauri-build = { version = "^2.6" } tauri-build = { version = "^2.6" }
tauri-plugin-dialog = { version = "^2.7" }
tauri-plugin-tracing = { version = "^0.3" } tauri-plugin-tracing = { version = "^0.3" }
ts-rs = { version = "^12.0" } ts-rs = { version = "^12.0" }
zeroize = { version = "^1.9" } 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 # file: crates/ksp-app-wallet-desk/Cargo.toml
# version: 3 # version: 4
[package] [package]
name = "ksp-app-wallet-desk" 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" } ksp-wallet-lib = { path = "../ksp-wallet-lib" }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
tauri.workspace = true tauri.workspace = true
tauri-plugin-dialog.workspace = true
tauri-plugin-tracing.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 ts-rs.workspace = true
zeroize.workspace = true
[dev-dependencies] [dev-dependencies]
serde_json.workspace = true serde_json.workspace = true

View File

@@ -42,7 +42,7 @@
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1> <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> <p class="text-body-secondary mb-0">Identité autorisée et balance SOL via le Transport HTTP composite sont actives.</p>
</div> </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> </div>
<section data-view-panel="dashboard"> <section data-view-panel="dashboard">
<div class="row g-3"> <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. 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> </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> </div>
</section> </section>
<section data-view-panel="details" hidden> <section data-view-panel="details" hidden>
@@ -246,7 +311,7 @@
</div> </div>
</main> </main>
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4"> <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> </footer>
<script type="module" src="./ts/main.ts"></script> <script type="module" src="./ts/main.ts"></script>
</body> </body>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts // file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 7 // version: 8
import "bootstrap"; import "bootstrap";
import DataTable from "datatables.net-bs5"; import DataTable from "datatables.net-bs5";
@@ -9,6 +9,10 @@ import "simplebar";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import type { WalletBalanceDto } from "./bindings/ksp_app_wallet_desk/wallet_balance/WalletBalanceDto.ts"; 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 { 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 { 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 { WalletInventoryEntryDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletInventoryEntryDto.ts";
import type { WalletSelectionRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletSelectionRequestDto.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 activeSessionState: ActiveSessionState = "no_selection";
let activeViewEnabled = false; let activeViewEnabled = false;
let configuredSecretCandidateCount = 0; let configuredSecretCandidateCount = 0;
let stagedImportSource: WalletTransferInspectionDto | null = null;
function isViewId(value: string): value is ViewId { function isViewId(value: string): value is ViewId {
return value in viewTitles; return value in viewTitles;
@@ -170,7 +175,7 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.", 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 { function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
@@ -230,6 +235,44 @@ function updateUnlockActions(): void {
setText("#unlockSecretCandidateCount", configuredSecretCandidateCount.toString()); 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 { function clearCreateFormSensitiveInputs(): void {
const ownerPassword = document.querySelector<HTMLInputElement>("#createWalletOwnerPassword"); const ownerPassword = document.querySelector<HTMLInputElement>("#createWalletOwnerPassword");
const viewPassword = document.querySelector<HTMLInputElement>("#createWalletViewPassword"); const viewPassword = document.querySelector<HTMLInputElement>("#createWalletViewPassword");
@@ -483,6 +526,129 @@ function bindCreateWalletForm(): void {
frontendTrace("main", "Create Wallet form handlers installed"); 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 { function beginFrontendUnlock(capability: UnlockCapability, provider: "manual" | "configured"): string | null {
if (activeSessionState !== "locked" || !activeWalletId) { if (activeSessionState !== "locked" || !activeWalletId) {
return null; return null;
@@ -658,7 +824,7 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory); setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory);
setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent"); setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent");
setText("#runtimeShellPhase", status.shellPhase); 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", { frontendTrace("main", "Wallet Desk runtime status rendered", {
compositeProfile: status.activeCompositeProfile, compositeProfile: status.activeCompositeProfile,
fallbackLoggingActive: status.fallbackLoggingActive, fallbackLoggingActive: status.fallbackLoggingActive,
@@ -706,6 +872,7 @@ async function initializeMain(): Promise<void> {
bindNavigation(); bindNavigation();
bindWalletTableSelection(); bindWalletTableSelection();
bindCreateWalletForm(); bindCreateWalletForm();
bindImportWalletForm();
bindUnlockActions(); bindUnlockActions();
bindBalanceActions(); bindBalanceActions();
bindShellActions(); bindShellActions();

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/app_state.rs // file: crates/ksp-app-wallet-desk/src/app_state.rs
// version: 8 // version: 9
//! Shared backend state owned by the Wallet Desk Tauri application. //! 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, splash_sequence_started: std::sync::atomic::AtomicBool,
transport_runtime: crate::TransportRuntime, transport_runtime: crate::TransportRuntime,
wallet_config_startup: crate::WalletConfigStartup, wallet_config_startup: crate::WalletConfigStartup,
wallet_import_source: std::sync::Mutex<std::option::Option<crate::PendingWalletImport>>,
wallet_session: std::sync::Mutex<crate::WalletSession>, wallet_session: std::sync::Mutex<crate::WalletSession>,
} }
@@ -69,6 +70,7 @@ impl AppState {
splash_sequence_started: std::sync::atomic::AtomicBool::new(false), splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
transport_runtime, transport_runtime,
wallet_config_startup, wallet_config_startup,
wallet_import_source: std::sync::Mutex::new(std::option::Option::None),
wallet_session: std::sync::Mutex::new(crate::WalletSession::no_selection()), 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(), effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(),
fallback_logging_active: runtime.fallback_active, fallback_logging_active: runtime.fallback_active,
root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(), 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(), startup_diagnostic: runtime.startup_diagnostic.clone(),
transport_available_endpoint_count, transport_available_endpoint_count,
transport_clusters: self.transport_runtime.clusters(), transport_clusters: self.transport_runtime.clusters(),
@@ -220,6 +222,102 @@ impl AppState {
return std::result::Result::Ok(dto); 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. /// 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> { 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 = 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 { 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"); 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 // file: crates/ksp-app-wallet-desk/src/constants.rs
// version: 6 // version: 7
//! Logging targets, domains and composite component identifiers owned by Wallet Desk. //! 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"; pub(crate) const TRACING_DOMAIN_TRANSPORT: &str = "wallet.transport";
/// Structured domain used while preparing Wallet filesystem roots. /// Structured domain used while preparing Wallet filesystem roots.
pub(crate) const TRACING_DOMAIN_WALLET_CONFIG: &str = "wallet.config"; 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. /// Structured domain used while enumerating and inspecting locked Wallet files.
pub(crate) const TRACING_DOMAIN_WALLET_INVENTORY: &str = "wallet.inventory"; pub(crate) const TRACING_DOMAIN_WALLET_INVENTORY: &str = "wallet.inventory";
/// Structured domain used by Config-owned Wallet password candidate operations. /// 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 // 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. //! 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. /// 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 = pub(crate) const ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_directory_prepare_failed"); 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. /// 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"); 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. /// 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 // file: crates/ksp-app-wallet-desk/src/lib.rs
// version: 7 // version: 8
//! Tauri desktop application shell for KSP Wallet management and inspection. //! Tauri desktop application shell for KSP Wallet management and inspection.
@@ -21,6 +21,7 @@ mod tw_main;
mod tw_splash; mod tw_splash;
mod wallet_balance; mod wallet_balance;
mod wallet_config; mod wallet_config;
mod wallet_import;
mod wallet_inventory; mod wallet_inventory;
mod wallet_secrets; mod wallet_secrets;
mod wallet_session; mod wallet_session;
@@ -56,6 +57,8 @@ pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
pub(crate) use self::constants::TRACING_DOMAIN_TRANSPORT; pub(crate) use self::constants::TRACING_DOMAIN_TRANSPORT;
/// Structured domain used while preparing Wallet filesystem roots. /// Structured domain used while preparing Wallet filesystem roots.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_CONFIG; 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. /// Structured domain used while enumerating and inspecting locked Wallet files.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_INVENTORY; pub(crate) use self::constants::TRACING_DOMAIN_WALLET_INVENTORY;
/// Structured domain used by Config-owned Wallet password candidate operations. /// 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; pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_INVALID;
/// Wallet Desk could not inspect or create the configured Wallet directory tree. /// Wallet Desk could not inspect or create the configured Wallet directory tree.
pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED; 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. /// Wallet inventory filesystem operation failed.
pub(crate) use self::errors::ERROR_CODE_WALLET_INVENTORY_FAILED; pub(crate) use self::errors::ERROR_CODE_WALLET_INVENTORY_FAILED;
/// No effective Config-owned Wallet password candidate is available. /// 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; pub(crate) use self::wallet_config::WalletConfigStartup;
/// Resolves the composite-selected Wallet Config and prepares its application-owned directory tree. /// Resolves the composite-selected Wallet Config and prepares its application-owned directory tree.
pub(crate) use self::wallet_config::initialize_wallet_config; 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. /// Safe locked Wallet projection returned after selection.
pub(crate) use self::wallet_inventory::LockedWalletDto; pub(crate) use self::wallet_inventory::LockedWalletDto;
/// Locked inspection outcome exposed by Wallet inventory rows in crate unit tests. /// 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 // file: crates/ksp-app-wallet-desk/src/tauri.rs
// version: 5 // version: 6
//! Tauri runtime assembly for the KSP wallet desktop application. //! Tauri runtime assembly for the KSP wallet desktop application.
use tauri_plugin_dialog::DialogExt; // rust-rules: trait-import
/// Runs the Wallet Desk application. /// Runs the Wallet Desk application.
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> { 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> { fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<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. #[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> { fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![ return builder.invoke_handler(tauri::generate_handler![
clear_import_source,
create_wallet, create_wallet,
deselect_wallet, deselect_wallet,
emit_frontend_log, emit_frontend_log,
get_runtime_status, get_runtime_status,
import_wallet,
inspect_import_source,
list_wallets, list_wallets,
lock_wallet, lock_wallet,
refresh_wallet_balance, 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] #[tauri::command]
async fn create_wallet( async fn create_wallet(
request: crate::WalletCreateRequestDto, 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] #[tauri::command]
async fn refresh_wallet_balance(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletBalanceDto, crate::CommandErrorDto> { async fn refresh_wallet_balance(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletBalanceDto, crate::CommandErrorDto> {
let result = state.refresh_wallet_balance().await; 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", "$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Wallet Desk", "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", "identifier": "com.sasedev.ksp-app-wallet-desk",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs // 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. //! 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("renderWalletBalance"));
assert!(main.contains("refresh_wallet_balance")); 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 // 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. //! 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!(!main.contains("endpointUrl"));
assert!(state.contains("wallet.pubkey()")); 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