v0.2.6-pre.008
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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",
|
"$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",
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
|||||||
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