v0.2.6-pre.012
This commit is contained in:
@@ -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.008</span>
|
||||
<span class="badge text-bg-secondary">0.2.6-pre.012</span>
|
||||
</div>
|
||||
<section data-view-panel="dashboard">
|
||||
<div class="row g-3">
|
||||
@@ -380,6 +380,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-danger-subtle">
|
||||
<div class="card-header fw-semibold">Export keypair Solana — OWNER uniquement</div>
|
||||
<div class="card-body">
|
||||
<p class="small text-body-secondary">L’export produit un fichier secret non chiffré contenant la keypair Solana complète. Le contenu et le chemin complet restent exclusivement côté Rust ; le frontend choisit seulement le format puis confirme l’opération.</p>
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label" for="walletExportFormat">Format d’export</label>
|
||||
<select id="walletExportFormat" class="form-select" data-owner-export-control disabled>
|
||||
<option value="solana_cli_json">Solana CLI JSON</option>
|
||||
<option value="solana_keypair_base58">Keypair Base58 complet</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-6 d-grid">
|
||||
<button id="exportWalletKeypair" class="btn btn-outline-danger" type="button" data-owner-export-control disabled>Export secret keypair file</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="walletExportStatus" class="alert alert-secondary mt-3 mb-0" role="status">Session OWNER requise. Aucun export texte ou clipboard n’est disponible.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div id="unlockStatus" class="alert alert-secondary mb-0" role="status">Sélectionner un wallet verrouillé.</div>
|
||||
</div>
|
||||
@@ -443,8 +464,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="exportWalletKeypairModal" tabindex="-1" aria-labelledby="exportWalletKeypairModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="exportWalletKeypairModalLabel">Exporter la keypair Solana ?</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">Cette opération OWNER crée un fichier secret non chiffré contenant les 64 octets de la keypair Solana. Le fichier doit être protégé comme une clé privée. Un fichier existant ne sera jamais écrasé par Wallet.</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button id="confirmExportWalletKeypair" type="button" class="btn btn-danger">Choose destination and export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
|
||||
<span>KSP Wallet Desk · strong VIEW administration pre.011</span>
|
||||
<span>KSP Wallet Desk · OWNER keypair export pre.012</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: 12
|
||||
// version: 13
|
||||
|
||||
import { Modal } from "bootstrap";
|
||||
import DataTable from "datatables.net-bs5";
|
||||
@@ -10,9 +10,11 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { WalletBalanceDto } from "./bindings/ksp_app_wallet_desk/wallet_balance/WalletBalanceDto.ts";
|
||||
import type { CommandErrorDto } from "./bindings/ksp_app_wallet_desk/dto_common/CommandErrorDto.ts";
|
||||
import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts";
|
||||
import type { WalletExportRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_export/WalletExportRequestDto.ts";
|
||||
import type { WalletExportResultDto } from "./bindings/ksp_app_wallet_desk/wallet_export/WalletExportResultDto.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 { WalletTransferFormatDto } from "./bindings/ksp_app_wallet_desk/wallet_transfer/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";
|
||||
@@ -54,6 +56,7 @@ let authorizedWalletProjection: WalletAuthorizedDto | null = null;
|
||||
let configuredSecretCandidateCount = 0;
|
||||
let deleteNoteModal: Modal | null = null;
|
||||
let disableViewModal: Modal | null = null;
|
||||
let exportWalletModal: Modal | null = null;
|
||||
let recreateViewModal: Modal | null = null;
|
||||
let pendingDeleteNoteId: string | null = null;
|
||||
let stagedImportSource: WalletTransferInspectionDto | null = null;
|
||||
@@ -187,7 +190,7 @@ function initializeWalletTable(): void {
|
||||
zeroRecords: "Aucun wallet correspondant.",
|
||||
},
|
||||
});
|
||||
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.011-strong-view-administration" });
|
||||
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.012-owner-transfer-export" });
|
||||
}
|
||||
|
||||
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
|
||||
@@ -227,6 +230,7 @@ function updateSessionActions(): void {
|
||||
updateOwnerMetadataActions();
|
||||
updateSecurityRotationActions();
|
||||
updateStrongViewSecurityActions();
|
||||
updateWalletExportActions();
|
||||
}
|
||||
|
||||
function updateOwnerMetadataActions(): void {
|
||||
@@ -259,6 +263,13 @@ function updateStrongViewSecurityActions(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function updateWalletExportActions(): void {
|
||||
const ownerOpen = activeSessionState === "owner_open" && authorizedWalletProjection?.capability === "owner";
|
||||
document.querySelectorAll<HTMLSelectElement | HTMLButtonElement>("[data-owner-export-control]").forEach(control => {
|
||||
control.disabled = !ownerOpen;
|
||||
});
|
||||
}
|
||||
|
||||
function updateUnlockActions(): void {
|
||||
const locked = activeSessionState === "locked";
|
||||
const viewManual = document.querySelector<HTMLButtonElement>("#unlockViewManual");
|
||||
@@ -392,6 +403,7 @@ function clearAuthorizedProjection(): void {
|
||||
clearUnlockSensitiveInputs();
|
||||
setText("#rotationStatus", "Session OWNER requise.");
|
||||
setText("#viewSecurityStatus", "Session OWNER requise pour administrer fortement VIEW.");
|
||||
setText("#walletExportStatus", "Session OWNER requise. Aucun export texte ou clipboard n’est disponible.");
|
||||
}
|
||||
|
||||
function clearSelectedWallet(): void {
|
||||
@@ -430,6 +442,7 @@ function renderLockedWallet(wallet: LockedWalletDto): void {
|
||||
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
|
||||
setText("#unlockStatus", "Prêt pour une tentative explicite. Argon2 peut prendre plusieurs secondes.");
|
||||
setText("#viewSecurityStatus", "Unlock OWNER requis pour administrer fortement VIEW.");
|
||||
setText("#walletExportStatus", "Unlock OWNER requis pour exporter la keypair Solana.");
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "Locked Wallet selection rendered", {
|
||||
configuredSecretCandidateCount,
|
||||
@@ -481,6 +494,12 @@ function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
|
||||
: "OWNER ouvert : VIEW disabled. Strong recreate disponible."
|
||||
: "VIEW ouvert : l’administration forte VIEW est réservée à OWNER.",
|
||||
);
|
||||
setText(
|
||||
"#walletExportStatus",
|
||||
wallet.capability === "owner"
|
||||
? "OWNER ouvert : export fichier Solana CLI JSON ou Base58 disponible via save picker natif."
|
||||
: "VIEW ouvert : export de keypair interdit ; OWNER requis.",
|
||||
);
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "Authorized Wallet session rendered", {
|
||||
capability: wallet.capability,
|
||||
@@ -1244,6 +1263,67 @@ function bindStrongViewSecurityActions(): void {
|
||||
frontendTrace("main", "Strong VIEW security action handlers installed");
|
||||
}
|
||||
|
||||
async function runWalletExport(walletId: string, format: WalletTransferFormatDto): Promise<void> {
|
||||
activeSessionState = "privileged_operation";
|
||||
setText("#currentWalletState", "OWNER export…");
|
||||
setText("#walletExportStatus", "Save picker natif / export secret en cours…");
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "Wallet OWNER transfer export requested", { format, walletId });
|
||||
const request: WalletExportRequestDto = { format };
|
||||
try {
|
||||
const result = await invokeKsp<WalletExportResultDto | null>("main", "export_wallet_transfer", { request });
|
||||
if (activeWalletId !== walletId || authorizedWalletProjection?.capability !== "owner") {
|
||||
frontendWarn("main", "Stale Wallet export response ignored", { format, walletId });
|
||||
return;
|
||||
}
|
||||
activeSessionState = "owner_open";
|
||||
setText("#currentWalletState", "OWNER open");
|
||||
if (result === null) {
|
||||
setText("#walletExportStatus", "Export annulé dans le save picker natif ; aucun fichier créé.");
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "Wallet OWNER transfer export cancelled", { format, walletId });
|
||||
return;
|
||||
}
|
||||
setText("#walletExportStatus", `Export ${importFormatLabel(result.format)} créé : ${result.destinationName}. Le chemin complet et le contenu secret n’ont pas traversé IPC.`);
|
||||
updateSessionActions();
|
||||
frontendInfo("main", "Wallet OWNER transfer export completed", { format: result.format, walletId });
|
||||
} catch {
|
||||
if (activeWalletId === walletId && authorizedWalletProjection?.capability === "owner") {
|
||||
activeSessionState = "owner_open";
|
||||
setText("#currentWalletState", "OWNER open");
|
||||
setText("#walletExportStatus", "Export refusé ou impossible ; aucun écrasement n’est autorisé et la session OWNER reste ouverte.");
|
||||
updateSessionActions();
|
||||
}
|
||||
frontendWarn("main", "Wallet OWNER transfer export failed", { format, walletId });
|
||||
}
|
||||
}
|
||||
|
||||
function bindWalletExportActions(): void {
|
||||
const modalElement = document.querySelector<HTMLElement>("#exportWalletKeypairModal");
|
||||
if (modalElement) {
|
||||
exportWalletModal = new Modal(modalElement);
|
||||
}
|
||||
document.querySelector<HTMLButtonElement>("#exportWalletKeypair")?.addEventListener("click", () => {
|
||||
if (!activeWalletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner") {
|
||||
setText("#walletExportStatus", "Session OWNER requise.");
|
||||
return;
|
||||
}
|
||||
frontendDebug("main", "Wallet OWNER transfer export confirmation opened", { walletId: activeWalletId });
|
||||
exportWalletModal?.show();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#confirmExportWalletKeypair")?.addEventListener("click", () => {
|
||||
const walletId = activeWalletId;
|
||||
const formatControl = document.querySelector<HTMLSelectElement>("#walletExportFormat");
|
||||
exportWalletModal?.hide();
|
||||
if (!walletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner" || !formatControl) {
|
||||
return;
|
||||
}
|
||||
const format = formatControl.value as WalletTransferFormatDto;
|
||||
void runWalletExport(walletId, format);
|
||||
});
|
||||
frontendTrace("main", "Wallet OWNER transfer export handlers installed");
|
||||
}
|
||||
|
||||
function bindRotationActions(): void {
|
||||
document.querySelector<HTMLButtonElement>("#rotateOwnerPasswordSubmit")?.addEventListener("click", () => {
|
||||
void rotateWalletPassword("owner");
|
||||
@@ -1357,7 +1437,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 ; import, VIEW/OWNER, metadata, rotations, strong VIEW et getBalance prêts.");
|
||||
setText("#shellStatus", "Config/Transport résolus ; import/export, VIEW/OWNER, metadata, rotations, strong VIEW et getBalance prêts.");
|
||||
frontendTrace("main", "Wallet Desk runtime status rendered", {
|
||||
compositeProfile: status.activeCompositeProfile,
|
||||
fallbackLoggingActive: status.fallbackLoggingActive,
|
||||
@@ -1411,6 +1491,7 @@ async function initializeMain(): Promise<void> {
|
||||
bindOwnerMetadataActions();
|
||||
bindRotationActions();
|
||||
bindStrongViewSecurityActions();
|
||||
bindWalletExportActions();
|
||||
bindShellActions();
|
||||
clearSelectedWallet();
|
||||
activateView("dashboard", "startup");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ksp-app-wallet-desk",
|
||||
"private": true,
|
||||
"version": "0.2.6-pre.11",
|
||||
"version": "0.2.6-pre.12",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/app_state.rs
|
||||
// version: 15
|
||||
// version: 16
|
||||
|
||||
//! Shared backend state owned by the Wallet Desk Tauri application.
|
||||
|
||||
@@ -126,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.010-credential-rotation".to_owned(),
|
||||
shell_phase: "pre.012-owner-transfer-export".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
transport_available_endpoint_count,
|
||||
transport_clusters: self.transport_runtime.clusters(),
|
||||
@@ -722,6 +722,71 @@ impl AppState {
|
||||
return self.finish_owner_operation(context, result).await;
|
||||
}
|
||||
|
||||
/// Returns the native save-picker default filename when an OWNER session is currently authorized.
|
||||
pub(crate) fn wallet_export_picker_filename(&self, format: crate::WalletTransferFormatDto) -> ksp_core_lib::Result<String> {
|
||||
let session = self.wallet_session.lock();
|
||||
let session = match session {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
|
||||
};
|
||||
return match &*session {
|
||||
crate::WalletSession::Owner { wallet_id, .. } => std::result::Result::Ok(crate::wallet_export_default_filename(wallet_id.as_str(), format)),
|
||||
_ => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED,
|
||||
"Wallet transfer export requires an authorized OWNER session",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Exports the immutable Solana keypair to one Rust-owned native save-picker destination.
|
||||
pub(crate) async fn export_wallet_transfer(
|
||||
&self,
|
||||
destination: std::path::PathBuf,
|
||||
request: crate::WalletExportRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::WalletExportResultDto> {
|
||||
let destination_name = crate::wallet_export_destination_name(destination.as_path());
|
||||
let destination_name = match destination_name {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let format = request.format;
|
||||
let context = self.begin_owner_operation("keypair_export");
|
||||
let context = match context {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet_id = context.wallet_id.clone();
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WALLET_EXPORT,
|
||||
wallet_id = wallet_id.as_str(),
|
||||
transfer_format = format.code(),
|
||||
"Wallet OWNER transfer export started"
|
||||
);
|
||||
let result = context.owner.export_transfer_file(destination.as_path(), format.wallet_format()).await;
|
||||
let restored = self.finish_owner_operation(context, result).await;
|
||||
if let std::result::Result::Err(error) = restored {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WALLET_EXPORT,
|
||||
wallet_id = wallet_id.as_str(),
|
||||
transfer_format = format.code(),
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"Wallet OWNER transfer export failed"
|
||||
);
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
ksp_logging_lib::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_WALLET_EXPORT,
|
||||
wallet_id = wallet_id.as_str(),
|
||||
transfer_format = format.code(),
|
||||
"Wallet OWNER transfer export completed"
|
||||
);
|
||||
return std::result::Result::Ok(crate::WalletExportResultDto { destination_name, format, wallet_id });
|
||||
}
|
||||
|
||||
/// Strongly disables VIEW from the current OWNER session while preserving OWNER authorization.
|
||||
pub(crate) async fn disable_wallet_view(&self) -> ksp_core_lib::Result<crate::WalletViewSecurityStatusDto> {
|
||||
let context = self.begin_owner_operation("view_disable");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/constants.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! 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-save-picker OWNER transfer export operations.
|
||||
pub(crate) const TRACING_DOMAIN_WALLET_EXPORT: &str = "wallet.export";
|
||||
/// 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/errors.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Application-local error codes for Wallet Desk composition and desktop runtime surfaces.
|
||||
|
||||
@@ -34,6 +34,9 @@ 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 save-picker Wallet export destination is not a usable local filesystem path.
|
||||
pub(crate) const ERROR_CODE_WALLET_EXPORT_DESTINATION_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_export_destination_invalid");
|
||||
/// 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
//! Tauri desktop application shell for KSP Wallet management and inspection.
|
||||
|
||||
@@ -21,12 +21,14 @@ mod tw_main;
|
||||
mod tw_splash;
|
||||
mod wallet_balance;
|
||||
mod wallet_config;
|
||||
mod wallet_export;
|
||||
mod wallet_import;
|
||||
mod wallet_inventory;
|
||||
mod wallet_metadata;
|
||||
mod wallet_secrets;
|
||||
mod wallet_security;
|
||||
mod wallet_session;
|
||||
mod wallet_transfer;
|
||||
|
||||
/// Runs the KSP wallet desktop application.
|
||||
pub use self::tauri::run;
|
||||
@@ -59,6 +61,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-save-picker OWNER transfer export operations.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_EXPORT;
|
||||
/// 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.
|
||||
@@ -111,6 +115,8 @@ 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 save-picker Wallet export destination is invalid.
|
||||
pub(crate) use self::errors::ERROR_CODE_WALLET_EXPORT_DESTINATION_INVALID;
|
||||
/// 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.
|
||||
@@ -155,14 +161,20 @@ 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;
|
||||
/// OWNER transfer export request carrying only the format selected by the frontend.
|
||||
pub(crate) use self::wallet_export::WalletExportRequestDto;
|
||||
/// Safe OWNER transfer export result without path or key material.
|
||||
pub(crate) use self::wallet_export::WalletExportResultDto;
|
||||
/// Builds the deterministic native save-picker filename for the current OWNER Wallet.
|
||||
pub(crate) use self::wallet_export::wallet_export_default_filename;
|
||||
/// Extracts a safe basename from the Rust-only native save-picker destination.
|
||||
pub(crate) use self::wallet_export::wallet_export_destination_name;
|
||||
/// 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.
|
||||
@@ -225,3 +237,5 @@ pub(crate) use self::wallet_session::WalletUnlockRequestDto;
|
||||
pub(crate) use self::wallet_session::owner_projection;
|
||||
/// Builds an authorized VIEW DTO from a Rust-only Wallet handle.
|
||||
pub(crate) use self::wallet_session::view_projection;
|
||||
/// Shared import/export transfer format selected by Wallet Desk.
|
||||
pub(crate) use self::wallet_transfer::WalletTransferFormatDto;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/tauri.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Tauri runtime assembly for the KSP wallet desktop application.
|
||||
|
||||
@@ -47,6 +47,7 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
|
||||
deselect_wallet,
|
||||
disable_wallet_view,
|
||||
emit_frontend_log,
|
||||
export_wallet_transfer,
|
||||
get_runtime_status,
|
||||
import_wallet,
|
||||
inspect_import_source,
|
||||
@@ -155,6 +156,51 @@ fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Resu
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn export_wallet_transfer(
|
||||
app: tauri::AppHandle,
|
||||
request: crate::WalletExportRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<std::option::Option<crate::WalletExportResultDto>, crate::CommandErrorDto> {
|
||||
let format = request.format;
|
||||
let default_filename = state.wallet_export_picker_filename(format);
|
||||
let default_filename = match default_filename {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
let extensions = [format.default_extension()];
|
||||
let selected = app
|
||||
.dialog()
|
||||
.file()
|
||||
.set_title("Export Solana keypair — secret material")
|
||||
.set_file_name(default_filename)
|
||||
.add_filter("Solana keypair transfer", &extensions)
|
||||
.blocking_save_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_EXPORT, transfer_format = format.code(), "Wallet OWNER transfer export native save 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_EXPORT_DESTINATION_INVALID,
|
||||
"Selected Wallet transfer export destination is not a local filesystem path",
|
||||
);
|
||||
return std::result::Result::Err(crate::CommandErrorDto::from_error(&error));
|
||||
},
|
||||
};
|
||||
let result = state.export_wallet_transfer(path, request).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]
|
||||
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::RuntimeStatusDto, crate::CommandErrorDto> {
|
||||
let result = state.runtime_status();
|
||||
|
||||
52
crates/ksp-app-wallet-desk/src/wallet_export.rs
Normal file
52
crates/ksp-app-wallet-desk/src/wallet_export.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/wallet_export.rs
|
||||
// version: 1
|
||||
|
||||
//! OWNER-only native save-picker Wallet transfer export contracts.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// OWNER export request containing only the explicit transfer format.
|
||||
#[derive(serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_export/WalletExportRequestDto.ts")]
|
||||
pub(crate) struct WalletExportRequestDto {
|
||||
/// Secret-transfer encoding selected before the Rust-owned save picker opens.
|
||||
pub(crate) format: crate::WalletTransferFormatDto,
|
||||
}
|
||||
|
||||
/// Safe result returned after an OWNER keypair export completes.
|
||||
#[derive(serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_export/WalletExportResultDto.ts")]
|
||||
pub(crate) struct WalletExportResultDto {
|
||||
/// Basename only for the newly created export file; the parent path remains Rust-only.
|
||||
pub(crate) destination_name: String,
|
||||
/// Explicit transfer format used by the export.
|
||||
pub(crate) format: crate::WalletTransferFormatDto,
|
||||
/// Root-scoped native Wallet identifier that authorized the export.
|
||||
pub(crate) wallet_id: String,
|
||||
}
|
||||
|
||||
/// Builds a deterministic native-save default filename without exposing Wallet metadata or Pubkey.
|
||||
#[must_use]
|
||||
pub(crate) fn wallet_export_default_filename(wallet_id: &str, format: crate::WalletTransferFormatDto) -> String {
|
||||
let stem = wallet_id.strip_suffix(crate::WALLET_FILE_SUFFIX).unwrap_or(wallet_id);
|
||||
let stem = if stem.is_empty() { "wallet" } else { stem };
|
||||
return format!("{stem}-solana-keypair.{}", format.default_extension());
|
||||
}
|
||||
|
||||
/// Extracts one safe basename from a native save-picker destination.
|
||||
pub(crate) fn wallet_export_destination_name(destination: &std::path::Path) -> ksp_core_lib::Result<String> {
|
||||
let name = destination.file_name().and_then(std::ffi::OsStr::to_str);
|
||||
return match name {
|
||||
std::option::Option::Some(value) if !value.is_empty() => std::result::Result::Ok(value.to_owned()),
|
||||
_ => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_WALLET_EXPORT_DESTINATION_INVALID,
|
||||
"Selected Wallet export destination has no usable local filename",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/wallet_export.rs"]
|
||||
mod tests;
|
||||
@@ -1,65 +1,18 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/wallet_import.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! 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,
|
||||
pub(crate) format: crate::WalletTransferFormatDto,
|
||||
}
|
||||
|
||||
/// Safe projection returned after validating one native-picker import source.
|
||||
@@ -68,7 +21,7 @@ pub(crate) struct WalletImportSourceRequestDto {
|
||||
#[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,
|
||||
pub(crate) format: crate::WalletTransferFormatDto,
|
||||
/// Public identity derived from the imported keypair.
|
||||
pub(crate) pubkey: String,
|
||||
/// Basename only; no parent path is ever projected.
|
||||
@@ -95,7 +48,7 @@ pub(crate) struct WalletImportRequestDto {
|
||||
/// 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,
|
||||
format: crate::WalletTransferFormatDto,
|
||||
inspection: WalletTransferInspectionDto,
|
||||
}
|
||||
|
||||
@@ -120,7 +73,10 @@ impl PendingWalletImport {
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
pub(crate) async fn stage_wallet_import_source(
|
||||
source: std::path::PathBuf,
|
||||
format: crate::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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/wallet_session.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Durable root-scoped Wallet session lifecycle for Wallet Desk.
|
||||
|
||||
@@ -170,7 +170,7 @@ pub(crate) enum WalletSession {
|
||||
/// Whether the authenticated Wallet currently exposes a VIEW slot.
|
||||
view_enabled: bool,
|
||||
},
|
||||
/// One OWNER-authorized privileged mutation temporarily owns the handle outside the session mutex.
|
||||
/// One OWNER-authorized privileged operation temporarily owns the handle outside the session mutex.
|
||||
OwnerOperation {
|
||||
/// Native Wallet filename / root-scoped identifier.
|
||||
wallet_id: String,
|
||||
|
||||
59
crates/ksp-app-wallet-desk/src/wallet_transfer.rs
Normal file
59
crates/ksp-app-wallet-desk/src/wallet_transfer.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/wallet_transfer.rs
|
||||
// version: 1
|
||||
|
||||
//! Shared Wallet Desk transfer format contract used by import and OWNER export workflows.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Explicit Solana keypair transfer format selected by Wallet Desk.
|
||||
#[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_transfer/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/save 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",
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the maximum accepted import source size for this transfer encoding.
|
||||
#[must_use]
|
||||
pub(crate) 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/wallet_transfer.rs"]
|
||||
mod tests;
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "KSP Wallet Desk",
|
||||
"version": "0.2.6-pre.11",
|
||||
"version": "0.2.6-pre.12",
|
||||
"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: 12
|
||||
// version: 13
|
||||
|
||||
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
|
||||
|
||||
@@ -302,5 +302,32 @@ fn pre_011_strong_view_administration_is_owner_only_and_refreshes_safe_status()
|
||||
assert!(main.contains("runStrongViewDisable"));
|
||||
assert!(main.contains("runStrongViewRecreate"));
|
||||
assert!(main.contains("applyViewSecurityStatus"));
|
||||
assert!(main.contains("pre.011-strong-view-administration"));
|
||||
assert!(main.contains("bindStrongViewSecurityActions"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_012_owner_export_uses_rust_native_save_picker_and_wallet_file_adapter() {
|
||||
let root = app_root();
|
||||
let state = read_text(root.join("src/app_state.rs").as_path());
|
||||
let export = read_text(root.join("src/wallet_export.rs").as_path());
|
||||
let transfer = read_text(root.join("src/wallet_transfer.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!(export.contains("WalletExportRequestDto"));
|
||||
assert!(export.contains("WalletExportResultDto"));
|
||||
assert!(transfer.contains("WalletTransferFormatDto"));
|
||||
assert!(transfer.contains("solana_cli_json"));
|
||||
assert!(transfer.contains("solana_keypair_base58"));
|
||||
assert!(state.contains("wallet_export_picker_filename"));
|
||||
assert!(state.contains("begin_owner_operation(\"keypair_export\")"));
|
||||
assert!(state.contains("owner.export_transfer_file"));
|
||||
assert!(tauri.contains("blocking_save_file"));
|
||||
assert!(tauri.contains("export_wallet_transfer"));
|
||||
assert!(html.contains("id=\"walletExportFormat\""));
|
||||
assert!(html.contains("id=\"exportWalletKeypair\""));
|
||||
assert!(html.contains("id=\"exportWalletKeypairModal\""));
|
||||
assert!(main.contains("bindWalletExportActions"));
|
||||
assert!(main.contains("export_wallet_transfer"));
|
||||
assert!(main.contains("pre.012-owner-transfer-export"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
|
||||
|
||||
@@ -181,7 +181,7 @@ fn owner_metadata_values_are_request_only_and_state_conflict_forces_reauthorizat
|
||||
assert!(!main.contains("frontendTrace(\"main\", request.text"));
|
||||
assert!(!main.contains("frontendDebug(\"main\", request.alias"));
|
||||
assert!(!main.contains("window.confirm"));
|
||||
assert!(main.contains("new Modal(modalElement)"));
|
||||
assert!(main.contains("deleteNoteModal = new Modal(modalElement)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -262,3 +262,63 @@ fn strong_view_administration_is_owner_only_uses_bootstrap_modals_and_returns_no
|
||||
assert!(!main.contains("frontendDebug(\"main\", request.password"));
|
||||
assert!(!main.contains("frontendTrace(\"main\", request.password"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owner_export_never_projects_key_material_or_full_destination_path_to_frontend() {
|
||||
let root = app_root();
|
||||
let export = read_text(root.join("src/wallet_export.rs").as_path());
|
||||
let state = read_text(root.join("src/app_state.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());
|
||||
let request = export.split("pub(crate) struct WalletExportRequestDto").nth(1);
|
||||
assert!(request.is_some());
|
||||
let request_fields = request
|
||||
.unwrap_or_default()
|
||||
.split("}\n")
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.lines()
|
||||
.filter(|line| return line.trim_start().starts_with("pub(crate) "))
|
||||
.collect::<std::vec::Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(request_fields.contains("format"));
|
||||
assert!(!export.contains("#[derive(Clone, serde::Deserialize"));
|
||||
assert!(!export.contains("#[derive(Debug, serde::Deserialize"));
|
||||
assert!(!request_fields.contains("path"));
|
||||
assert!(!request_fields.contains("password"));
|
||||
assert!(!request_fields.contains("secret"));
|
||||
let result = export.split("pub(crate) struct WalletExportResultDto").nth(1);
|
||||
assert!(result.is_some());
|
||||
let result_fields = result
|
||||
.unwrap_or_default()
|
||||
.split("}\n")
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.lines()
|
||||
.filter(|line| return line.trim_start().starts_with("pub(crate) "))
|
||||
.collect::<std::vec::Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(result_fields.contains("destination_name"));
|
||||
assert!(result_fields.contains("format"));
|
||||
assert!(result_fields.contains("wallet_id"));
|
||||
assert!(!result_fields.contains("path"));
|
||||
assert!(!result_fields.contains("pubkey"));
|
||||
assert!(!result_fields.contains("password"));
|
||||
assert!(!result_fields.contains("secret"));
|
||||
assert!(!result_fields.contains("content"));
|
||||
assert!(!result_fields.contains("bytes"));
|
||||
assert!(state.contains("owner.export_transfer_file"));
|
||||
assert!(!state.contains("owner.export_transfer(format"));
|
||||
let command_start = tauri.find("async fn export_wallet_transfer");
|
||||
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("WalletExportRequestDto"));
|
||||
assert!(command.contains("WalletExportResultDto"));
|
||||
assert!(!main.contains("navigator.clipboard"));
|
||||
assert!(!main.contains("destinationPath"));
|
||||
assert!(!main.contains("destination_path"));
|
||||
assert!(!main.contains("keypairBytes"));
|
||||
assert!(!main.contains("secretKey"));
|
||||
assert!(main.contains("exportWalletModal = new Modal(modalElement)"));
|
||||
}
|
||||
|
||||
41
crates/ksp-app-wallet-desk/unit_tests/wallet_export.rs
Normal file
41
crates/ksp-app-wallet-desk/unit_tests/wallet_export.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_export.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn export_request_is_deserializable_without_secret_or_path_fields() {
|
||||
fn assert_deserializable<T>(_marker: std::marker::PhantomData<T>)
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_deserializable(std::marker::PhantomData::<crate::WalletExportRequestDto>);
|
||||
let source = include_str!("../src/wallet_export.rs");
|
||||
let request = source.split("pub(crate) struct WalletExportRequestDto").nth(1);
|
||||
assert!(request.is_some());
|
||||
let fields = request
|
||||
.unwrap_or_default()
|
||||
.split("}\n")
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.lines()
|
||||
.filter(|line| return line.trim_start().starts_with("pub(crate) "))
|
||||
.collect::<std::vec::Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(fields.contains("format"));
|
||||
assert!(!fields.contains("path"));
|
||||
assert!(!fields.contains("password"));
|
||||
assert!(!fields.contains("secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_result_keeps_only_safe_basename_format_and_wallet_id() {
|
||||
let filename = crate::wallet_export_default_filename("alpha.kspwallet", crate::WalletTransferFormatDto::SolanaCliJson);
|
||||
assert_eq!(filename, "alpha-solana-keypair.json");
|
||||
let base58_filename = crate::wallet_export_default_filename("alpha.kspwallet", crate::WalletTransferFormatDto::SolanaKeypairBase58);
|
||||
assert_eq!(base58_filename, "alpha-solana-keypair.txt");
|
||||
let path = std::path::Path::new("/tmp/private/alpha-solana-keypair.json");
|
||||
let name = crate::wallet_export_destination_name(path);
|
||||
assert!(name.is_ok());
|
||||
assert_eq!(name.unwrap_or_default(), "alpha-solana-keypair.json");
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
// 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);
|
||||
}
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn import_request_does_not_require_debug_or_clone_password_contracts() {
|
||||
|
||||
10
crates/ksp-app-wallet-desk/unit_tests/wallet_transfer.rs
Normal file
10
crates/ksp-app-wallet-desk/unit_tests/wallet_transfer.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_transfer.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);
|
||||
}
|
||||
Reference in New Issue
Block a user