v0.2.6-pre.008
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
190
crates/ksp-app-wallet-desk/src/wallet_import.rs
Normal file
190
crates/ksp-app-wallet-desk/src/wallet_import.rs
Normal 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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
49
crates/ksp-app-wallet-desk/unit_tests/wallet_import.rs
Normal file
49
crates/ksp-app-wallet-desk/unit_tests/wallet_import.rs
Normal 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
115
deltas/0.2.6/pre.008.md
Normal 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
Reference in New Issue
Block a user