v0.5.2-pre.006-fix-004

This commit is contained in:
2026-08-11 00:30:03 +02:00
parent 066d969f5a
commit 1ee1d0297d
17 changed files with 698 additions and 41 deletions

View File

@@ -1,8 +1,17 @@
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
<!-- version: 56 -->
<!-- version: 57 -->
# CHANGELOG — kb-app-demo-desktop
## `0.5.2-pre.006-delta-fix-004`
- complète la fenêtre `Wallets` avec inspection publique, import et export des deux formats `ks-wallet` réellement supportés : Solana CLI JSON 64 octets et keypair Base58 64 octets ;
- garde le password hors Tauri IPC : création, import et export utilisent uniquement `KB_SECRET_DEMO_WALLET_PASSWORD` côté backend ;
- permet d'inspecter un ancien fichier keypair sans le convertir, d'en extraire uniquement la pubkey puis de l'envoyer directement vers l'explorateur Devnet/Mainnet ;
- peuple les sélecteurs de formats depuis l'inventaire exposé par `ks-wallet` afin d'éviter une liste frontend indépendante du backend ;
- aligne visuellement `Profil RPC`, `Adresse publique` et `Transactions`, puis affiche le profil/cluster sélectionné sous la ligne de contrôles ;
- prépare la validation suivante où les démos d'exécution Devnet devront consommer une capacité `.kswallet` explicite au lieu de recréer directement leur wallet JSON temporaire.
## `0.5.2-pre.006-delta-fix-003`
- ajoute dans `Wallets` un sélecteur explicite de profil RPC pour lexplorateur on-chain ; le profil actif reste la valeur initiale mais Devnet/Mainnet peuvent être interrogés sans redémarrer lapplication ni modifier `active_profile` ;

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/README.md -->
<!-- version: 19 -->
<!-- version: 20 -->
# kb-app-demo-desktop
@@ -8,7 +8,7 @@
## Fonctionnalités
- configuration et diagnostics de logging ;
- fenêtre Wallets dédiée : inventaire sûr des identités `.kswallet`, création dun conteneur persistant via un secret backend-only, inspection explicite dun fichier externe et exploration publique du solde SOL, des comptes SPL Token / Token-2022 et des transactions récentes sur un profil RPC explicitement sélectionnable ;
- fenêtre Wallets dédiée : inventaire sûr des identités `.kswallet`, création dun conteneur persistant via un secret backend-only, inspection/import/export des formats keypair supportés, extraction sûre de pubkey depuis un keypair legacy, inspection explicite dun `.kswallet` externe et exploration publique du solde SOL, des comptes SPL Token / Token-2022 et des transactions récentes sur un profil RPC explicitement sélectionnable ;
- transport HTTP JSON-RPC et WebSocket ;
- diagnostics PostgreSQL ;
- backfill, extraction Core et replay, avec enregistrement générique de Solana Program Metadata ;

View File

@@ -1,10 +1,11 @@
<!-- file: kb-app-demo-desktop/TODO.md -->
<!-- version: 16 -->
<!-- version: 17 -->
# TODO — kb-app-demo-desktop
## Évolutions générales
- [ ] `0.5.2` — valider les démos Devnet Solana Core/SPL/Metadata avec un `.kswallet` sélectionné et déverrouillé côté backend, sans transmettre le password au frontend.
- [ ] `0.5.4` — réduire les panneaux d'exécution à l'adaptation UI/Tauri en déplaçant toute orchestration de campagne encore réutilisable vers `ks-pipeline-demo-scenarios`.
- [ ] UX — empêcher les doubles déclenchements pendant une préparation, simulation ou soumission active.
- [ ] Observabilité — afficher explicitement les comptes relus et les projections matérialisées.

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/USAGE.md -->
<!-- version: 19 -->
<!-- version: 20 -->
# Utilisation de kb-app-demo-desktop
@@ -11,10 +11,12 @@ Lapplication expose une interface opérateur pour diagnostiquer, simuler et v
Le backend desktop peut lister les `.kswallet` du répertoire configuré, créer un wallet natif protégé, inspecter un fichier sélectionné explicitement ailleurs et explorer létat on-chain dune adresse publique. Les DTO frontend ne contiennent jamais de matériau secret. Aucun password, chemin interne du store, sel, nonce, ciphertext ou byte privé nest renvoyé par Tauri.
La fenêtre `Wallets`, accessible depuis le menu `Démos` de `main`, fournit quatre surfaces :
La fenêtre `Wallets`, accessible depuis le menu `Démos` de `main`, fournit maintenant six surfaces :
- inventaire des wallets natifs du profil actif ;
- création dun nouveau `.kswallet` dans le store du profil actif ;
- inspection et import d'un keypair externe dans un format supporté ;
- export authentifié d'un `.kswallet` vers un format supporté ;
- explorateur public on-chain ;
- inspection structurale dun `.kswallet` explicite situé hors du store.
@@ -31,6 +33,19 @@ Le secret peut aussi être placé dans le fichier `.env` local chargé par `ks-c
La création demande uniquement lalias. Elle crée `<alias>.kswallet` dans le répertoire wallet résolu du profil actif et ne modifie pas automatiquement `wallet_alias` dans la configuration. Un changement de valeur de `KB_SECRET_DEMO_WALLET_PASSWORD` avant une autre session permet dutiliser un autre mot de passe pour de nouveaux wallets ; ce secret de démonstration nest pas un mot de passe global du format `.kswallet`.
### Inspecter, importer et exporter les formats keypair
Les sélecteurs de format sont alimentés par `ks-wallet` et exposent actuellement :
- `Solana CLI keypair JSON (64 octets)` ;
- `Solana private key Base58 (64 octets)`.
L'action **Inspecter la keypair** ne crée aucun `.kswallet` : le backend valide le fichier avec le codec choisi et ne renvoie que la pubkey. Cette pubkey peut être envoyée directement à l'explorateur on-chain, ce qui permet par exemple de contrôler un ancien `wallets/temporary/local_devnet/*.json` avant de décider de le migrer.
L'action **Importer en .kswallet** demande en plus un alias destination et utilise `KB_SECRET_DEMO_WALLET_PASSWORD` uniquement côté backend. La source reste inchangée. L'export fonctionne dans le sens inverse : choix d'un alias natif, du format et d'un chemin destination explicite ; le backend authentifie le `.kswallet` avec le même secret de démonstration et refuse d'écraser un fichier existant.
Un keypair JSON de 64 octets ne contient pas son rôle métier. L'inspection peut dire « keypair Solana valide » et donner sa pubkey, mais elle ne peut pas décider localement si cette clé a servi de wallet opérateur, mint, authority ou autre fixture. Cette classification nécessite le contexte du scénario ou une lecture on-chain séparée.
### Explorer une adresse sur un profil RPC sélectionné
Lexplorateur on-chain ne déverrouille aucun wallet : seule la pubkey publique est nécessaire. Le bouton `Explorer` dun wallet inventorié remplit ladresse automatiquement ; toute autre pubkey Solana peut également être saisie.

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_wallet.html -->
<!-- version: 3 -->
<!-- version: 4 -->
<!DOCTYPE html>
<html lang="fr">
@@ -90,6 +90,62 @@
</div>
</div>
<div class="col-12 col-xl-7">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h2 class="h5 card-title mb-2">Importer ou inspecter une keypair externe</h2>
<p class="text-body-secondary">Le fichier source reste inchangé. L'inspection valide la keypair et ne retourne que sa pubkey ; l'import crée ensuite un nouveau <code>.kswallet</code> dans le store du profil actif.</p>
<label for="walletImportSourcePathInput" class="form-label">Chemin source</label>
<input id="walletImportSourcePathInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="/chemin/vers/local-devnet-operator.json">
<div class="row g-3 mt-1">
<div class="col-12 col-md-6">
<label for="walletImportFormatSelect" class="form-label">Format source</label>
<select id="walletImportFormatSelect" class="form-select"></select>
</div>
<div class="col-12 col-md-6">
<label for="walletImportAliasInput" class="form-label">Alias destination</label>
<input id="walletImportAliasInput" class="form-control font-monospace" type="text" autocomplete="off" maxlength="64" placeholder="local-devnet-operator">
</div>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<button id="inspectWalletTransferButton" class="btn btn-outline-primary" type="button">Inspecter la keypair</button>
<button id="importWalletTransferButton" class="btn btn-primary" type="button">Importer en .kswallet</button>
<button id="exploreWalletTransferButton" class="btn btn-outline-secondary" type="button" disabled>Explorer la pubkey</button>
</div>
<div class="mt-3">
<span class="fw-semibold">Pubkey détectée :</span>
<code id="walletTransferPublicKey" class="ms-1 text-break"></code>
</div>
<div class="mt-2">
<span class="fw-semibold">Statut :</span>
<code id="walletTransferStatus" class="ms-1">En attente</code>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h2 class="h5 card-title mb-2">Exporter un wallet natif</h2>
<p class="text-body-secondary">L'export exige le même secret backend que la création et refuse d'écraser un fichier existant.</p>
<label for="walletExportAliasSelect" class="form-label">Wallet</label>
<select id="walletExportAliasSelect" class="form-select"></select>
<label for="walletExportFormatSelect" class="form-label mt-3">Format d'export</label>
<select id="walletExportFormatSelect" class="form-select"></select>
<label for="walletExportDestinationPathInput" class="form-label mt-3">Chemin destination</label>
<input id="walletExportDestinationPathInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="/chemin/vers/export.json">
<div class="d-flex flex-wrap gap-2 mt-3">
<button id="exportWalletTransferButton" class="btn btn-primary" type="button">Exporter</button>
</div>
<div class="mt-3">
<span class="fw-semibold">Statut :</span>
<code id="walletExportStatus" class="ms-1">En attente</code>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
@@ -104,7 +160,6 @@
<div class="col-12 col-lg-3">
<label for="walletOnchainProfileSelect" class="form-label">Profil RPC</label>
<select id="walletOnchainProfileSelect" class="form-select"></select>
<div class="small text-body-secondary mt-1">Cluster : <code id="walletOnchainCluster"></code></div>
</div>
<div class="col-12 col-lg-7">
<label for="walletAddressInput" class="form-label">Adresse publique</label>
@@ -115,6 +170,7 @@
<input id="walletSignatureLimitInput" class="form-control" type="number" min="1" max="100" value="20">
</div>
</div>
<div class="small text-body-secondary mt-2">Profil sélectionné : <code id="walletOnchainSelectedProfile"></code> · Cluster : <code id="walletOnchainCluster"></code></div>
<div class="row g-3 mt-2">
<div class="col-12 col-md-4">
<div class="border rounded p-3 h-100">
@@ -242,4 +298,4 @@
<script type="module" src="ts/demo_wallet.ts" defer></script>
</body>
</html>
</html>

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_wallet.ts
// version: 3
// version: 4
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -9,11 +9,16 @@ import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./fr
import { renderJsonViewer } from "./json_viewer.ts";
import type { DemoWalletBalancePayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletBalancePayload.ts";
import type { DemoWalletCreateRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletCreateRequest.ts";
import type { DemoWalletExportRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExportRequest.ts";
import type { DemoWalletIdentityPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletIdentityPayload.ts";
import type { DemoWalletImportRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletImportRequest.ts";
import type { DemoWalletInventoryPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletInventoryPayload.ts";
import type { DemoWalletOnchainProfilePayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletOnchainProfilePayload.ts";
import type { DemoWalletRpcExecutionPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcExecutionPayload.ts";
import type { DemoWalletRpcRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcRequest.ts";
import type { DemoWalletTransferFormatPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferFormatPayload.ts";
import type { DemoWalletTransferInspectionPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionPayload.ts";
import type { DemoWalletTransferInspectionRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionRequest.ts";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
@@ -37,6 +42,7 @@ interface ParsedSignatureInfo {
}
let walletInventory: DemoWalletInventoryPayload | null = null;
let inspectedTransferPublicKey = "";
function setText(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
@@ -63,6 +69,7 @@ function selectedOnchainProfilePayload(): DemoWalletOnchainProfilePayload | null
function updateOnchainClusterLabel(): void {
const selected = selectedOnchainProfilePayload();
setText("#walletOnchainSelectedProfile", selected?.name ?? "—");
setText("#walletOnchainCluster", selected?.cluster ?? "—");
}
@@ -102,6 +109,65 @@ function parseJsonResponse(response: DemoWalletRpcExecutionPayload): unknown {
return JSON.parse(response.response_json) as unknown;
}
function renderTransferFormats(formats: ReadonlyArray<DemoWalletTransferFormatPayload>): void {
for (const selector of ["#walletImportFormatSelect", "#walletExportFormatSelect"]) {
const select = document.querySelector<HTMLSelectElement>(selector);
if (!select) {
continue;
}
const previous = select.value;
select.replaceChildren();
for (const format of formats) {
const option = document.createElement("option");
option.value = format.code;
option.textContent = `${format.label} (.${format.default_extension})`;
option.dataset.defaultExtension = format.default_extension;
select.appendChild(option);
}
if (formats.some(format => format.code === previous)) {
select.value = previous;
}
}
}
function renderExportWalletAliases(wallets: ReadonlyArray<DemoWalletIdentityPayload>): void {
const select = document.querySelector<HTMLSelectElement>("#walletExportAliasSelect");
if (!select) {
return;
}
const previous = select.value;
select.replaceChildren();
if (wallets.length === 0) {
const option = document.createElement("option");
option.value = "";
option.textContent = "Aucun wallet natif";
select.appendChild(option);
return;
}
for (const wallet of wallets) {
const option = document.createElement("option");
option.value = wallet.alias;
option.textContent = `${wallet.alias}${wallet.public_key}`;
select.appendChild(option);
}
if (wallets.some(wallet => wallet.alias === previous)) {
select.value = previous;
}
}
function selectedTransferFormat(selector: string): string {
return document.querySelector<HTMLSelectElement>(selector)?.value.trim() ?? "";
}
function setTransferPublicKey(publicKey: string): void {
inspectedTransferPublicKey = publicKey;
setText("#walletTransferPublicKey", publicKey.length > 0 ? publicKey : "—");
const exploreButton = document.querySelector<HTMLButtonElement>("#exploreWalletTransferButton");
if (exploreButton) {
exploreButton.disabled = publicKey.length === 0;
}
}
function renderWalletRows(wallets: ReadonlyArray<DemoWalletIdentityPayload>): void {
const body = document.querySelector<HTMLTableSectionElement>("#walletInventoryTableBody");
if (!body) {
@@ -158,6 +224,8 @@ async function refreshWalletInventory(): Promise<void> {
setText("#walletProfile", payload.profile);
setText("#walletCluster", payload.cluster);
renderOnchainProfiles(payload.onchain_profiles, payload.profile);
renderTransferFormats(payload.transfer_formats);
renderExportWalletAliases(payload.wallets);
setText("#walletCreateSecretStatus", payload.create_password_configured ? "configuré" : "absent");
setText("#walletSelectedAlias", payload.selected_alias ?? "—");
setText("#walletInventoryCount", String(payload.wallets.length));
@@ -169,6 +237,7 @@ async function refreshWalletInventory(): Promise<void> {
setText("#walletInventoryStatus", `Erreur : ${message}`);
setText("#walletInventoryCount", "0");
renderWalletRows([]);
renderExportWalletAliases([]);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Wallet inventory loading failed: ${message}`);
}
}
@@ -201,6 +270,89 @@ async function createWallet(): Promise<void> {
}
}
async function inspectTransferFile(): Promise<void> {
const sourcePath = inputValue("#walletImportSourcePathInput");
const format = selectedTransferFormat("#walletImportFormatSelect");
if (sourcePath.length === 0 || format.length === 0) {
setText("#walletTransferStatus", "Chemin source et format requis");
setTransferPublicKey("");
return;
}
const request: DemoWalletTransferInspectionRequest = { source_path: sourcePath, format };
setText("#walletTransferStatus", "Inspection...");
setTransferPublicKey("");
try {
const payload = await invoke<DemoWalletTransferInspectionPayload>("demo_wallet_inspect_transfer_file", { request });
setTransferPublicKey(payload.public_key);
setText("#walletTransferStatus", `Keypair valide — ${payload.format}`);
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", "external transfer keypair inspected");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletTransferStatus", `Erreur : ${message}`);
setTransferPublicKey("");
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `External transfer inspection failed: ${message}`);
}
}
async function importTransferFile(): Promise<void> {
const sourcePath = inputValue("#walletImportSourcePathInput");
const format = selectedTransferFormat("#walletImportFormatSelect");
const alias = inputValue("#walletImportAliasInput");
if (sourcePath.length === 0 || format.length === 0 || alias.length === 0) {
setText("#walletTransferStatus", "Chemin source, format et alias requis");
return;
}
const request: DemoWalletImportRequest = { alias, source_path: sourcePath, format };
setText("#walletTransferStatus", "Import...");
try {
const payload = await invoke<DemoWalletIdentityPayload>("demo_wallet_import_file", { request });
setTransferPublicKey(payload.public_key);
setText("#walletTransferStatus", `Importé : ${payload.alias}.kswallet`);
const addressInput = document.querySelector<HTMLInputElement>("#walletAddressInput");
if (addressInput) {
addressInput.value = payload.public_key;
}
await refreshWalletInventory();
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `external keypair imported: ${payload.alias}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletTransferStatus", `Erreur : ${message}`);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `External keypair import failed: ${message}`);
}
}
async function exportWallet(): Promise<void> {
const alias = document.querySelector<HTMLSelectElement>("#walletExportAliasSelect")?.value.trim() ?? "";
const format = selectedTransferFormat("#walletExportFormatSelect");
const destinationPath = inputValue("#walletExportDestinationPathInput");
if (alias.length === 0 || format.length === 0 || destinationPath.length === 0) {
setText("#walletExportStatus", "Wallet, format et destination requis");
return;
}
const request: DemoWalletExportRequest = { alias, destination_path: destinationPath, format };
setText("#walletExportStatus", "Export...");
try {
await invoke<void>("demo_wallet_export_file", { request });
setText("#walletExportStatus", `Exporté : ${alias}`);
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `native wallet exported: ${alias}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletExportStatus", `Erreur : ${message}`);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Native wallet export failed: ${message}`);
}
}
function exploreInspectedTransfer(): void {
if (inspectedTransferPublicKey.length === 0) {
return;
}
const addressInput = document.querySelector<HTMLInputElement>("#walletAddressInput");
if (addressInput) {
addressInput.value = inspectedTransferPublicKey;
}
document.querySelector<HTMLElement>("#walletAddressInput")?.scrollIntoView({ behavior: "smooth", block: "center" });
}
function parseTokenAccounts(value: unknown, program: string): ParsedTokenAccount[] {
if (typeof value !== "object" || value === null || !("value" in value)) {
return [];
@@ -504,6 +656,26 @@ document.addEventListener("DOMContentLoaded", () => {
void createWallet();
}
});
document.querySelector<HTMLButtonElement>("#inspectWalletTransferButton")?.addEventListener("click", () => {
void inspectTransferFile();
});
document.querySelector<HTMLButtonElement>("#importWalletTransferButton")?.addEventListener("click", () => {
void importTransferFile();
});
document.querySelector<HTMLButtonElement>("#exploreWalletTransferButton")?.addEventListener("click", () => {
exploreInspectedTransfer();
});
document.querySelector<HTMLButtonElement>("#exportWalletTransferButton")?.addEventListener("click", () => {
void exportWallet();
});
document.querySelector<HTMLInputElement>("#walletImportSourcePathInput")?.addEventListener("input", () => {
setTransferPublicKey("");
setText("#walletTransferStatus", "En attente");
});
document.querySelector<HTMLSelectElement>("#walletImportFormatSelect")?.addEventListener("change", () => {
setTransferPublicKey("");
setText("#walletTransferStatus", "En attente");
});
document.querySelector<HTMLSelectElement>("#walletOnchainProfileSelect")?.addEventListener("change", () => {
updateOnchainClusterLabel();
});

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_wallet.rs
// version: 4
// version: 5
//! UI-safe wallet management and public on-chain exploration.
@@ -21,6 +21,8 @@ pub(crate) struct DemoWalletInventoryPayload {
pub(crate) cluster: std::string::String,
/// Profiles available for explicit read-only on-chain exploration.
pub(crate) onchain_profiles: std::vec::Vec<DemoWalletOnchainProfilePayload>,
/// Secret transfer formats supported by the reusable wallet boundary.
pub(crate) transfer_formats: std::vec::Vec<DemoWalletTransferFormatPayload>,
/// Canonical classic SPL Token program identifier used by the explorer.
pub(crate) token_program_id: std::string::String,
/// Canonical Token-2022 program identifier used by the explorer.
@@ -61,6 +63,77 @@ pub(crate) struct DemoWalletCreateRequest {
pub(crate) alias: std::string::String,
}
/// One transfer format selectable by the wallet demo.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferFormatPayload.ts"
)]
pub(crate) struct DemoWalletTransferFormatPayload {
/// Stable machine-readable transfer format code.
pub(crate) code: std::string::String,
/// Human-readable transfer format label.
pub(crate) label: std::string::String,
/// Conventional extension suggested for explicit export paths.
pub(crate) default_extension: std::string::String,
}
/// Request to inspect one external keypair transfer file without importing it.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionRequest.ts"
)]
pub(crate) struct DemoWalletTransferInspectionRequest {
/// Explicit path of the external transfer file.
pub(crate) source_path: std::string::String,
/// Stable transfer format code selected by the operator.
pub(crate) format: std::string::String,
}
/// Safe public identity extracted from one external keypair transfer file.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionPayload.ts"
)]
pub(crate) struct DemoWalletTransferInspectionPayload {
/// Public key derived from the validated secret keypair.
pub(crate) public_key: std::string::String,
/// Stable transfer format code used for validation.
pub(crate) format: std::string::String,
}
/// Request to import one external keypair into the active native wallet store.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletImportRequest.ts"
)]
pub(crate) struct DemoWalletImportRequest {
/// Alias assigned to the newly created native wallet.
pub(crate) alias: std::string::String,
/// Explicit path of the external source file.
pub(crate) source_path: std::string::String,
/// Stable transfer format code selected by the operator.
pub(crate) format: std::string::String,
}
/// Request to export one native wallet to an explicit external file.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExportRequest.ts"
)]
pub(crate) struct DemoWalletExportRequest {
/// Alias of the native wallet to export.
pub(crate) alias: std::string::String,
/// Explicit destination path created without overwrite.
pub(crate) destination_path: std::string::String,
/// Stable transfer format code selected by the operator.
pub(crate) format: std::string::String,
}
/// Exact SOL balance returned for one public wallet address.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
@@ -157,6 +230,7 @@ pub(crate) async fn demo_wallet_inventory(
profile: profile.name.clone(),
cluster: profile.wallet.cluster.clone(),
onchain_profiles: onchain_profile_payloads(state),
transfer_formats: wallet_transfer_format_payloads(),
token_program_id: ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
token_2022_program_id: ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(),
create_password_configured: std::env::var_os(DEMO_WALLET_PASSWORD_ENV).is_some(),
@@ -174,17 +248,9 @@ pub(crate) async fn demo_wallet_create_native(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let password_text = match std::env::var(DEMO_WALLET_PASSWORD_ENV) {
let password = match demo_wallet_password() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(format!(
"backend secret {DEMO_WALLET_PASSWORD_ENV} is not configured as UTF-8"
));
},
};
let password = match ks_wallet::WalletPassword::new(password_text) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile = state.active_profile();
let manager = match wallet_manager_for_profile(profile) {
@@ -207,6 +273,100 @@ pub(crate) async fn demo_wallet_create_native(
return std::result::Result::Ok(payload);
}
/// Inspects one external transfer file and returns only the derived public identity.
pub(crate) async fn demo_wallet_inspect_transfer_file(
request: crate::DemoWalletTransferInspectionRequest,
) -> std::result::Result<DemoWalletTransferInspectionPayload, std::string::String> {
let format = match wallet_transfer_format_from_code(request.format.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let inspection = match ks_wallet::inspect_transfer_file(
std::path::PathBuf::from(request.source_path),
format,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
return std::result::Result::Ok(DemoWalletTransferInspectionPayload {
public_key: inspection.public_key().to_string(),
format: inspection.format().code().to_string(),
});
}
/// Imports one external keypair into a new password-protected native wallet.
pub(crate) async fn demo_wallet_import_file(
state: &crate::AppState,
request: crate::DemoWalletImportRequest,
) -> std::result::Result<DemoWalletIdentityPayload, std::string::String> {
let alias = match ks_wallet::WalletAlias::parse(request.alias.trim().to_string()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let format = match wallet_transfer_format_from_code(request.format.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = match demo_wallet_password() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile = state.active_profile();
let manager = match wallet_manager_for_profile(profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallet = match manager
.import_file(alias, password, std::path::PathBuf::from(request.source_path), format)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let identity = wallet.identity();
let payload = DemoWalletIdentityPayload {
selected: profile.wallet.wallet_alias.as_deref()
== std::option::Option::Some(identity.alias.as_str()),
alias: identity.alias.as_str().to_string(),
public_key: identity.public_key,
format_version: 1,
};
wallet.lock();
return std::result::Result::Ok(payload);
}
/// Exports one native wallet after backend-only password authentication.
pub(crate) async fn demo_wallet_export_file(
state: &crate::AppState,
request: crate::DemoWalletExportRequest,
) -> std::result::Result<(), std::string::String> {
let alias = match ks_wallet::WalletAlias::parse(request.alias.trim().to_string()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let format = match wallet_transfer_format_from_code(request.format.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = match demo_wallet_password() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let manager = match wallet_manager_for_profile(state.active_profile()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match manager
.export_file(&alias, password, std::path::PathBuf::from(request.destination_path), format)
.await
{
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
/// Inspects one explicitly selected native wallet file and returns only its safe identity.
pub(crate) async fn demo_wallet_inspect_file(
state: &crate::AppState,
@@ -329,6 +489,45 @@ pub(crate) async fn demo_wallet_rpc_execute(
});
}
fn demo_wallet_password() -> std::result::Result<ks_wallet::WalletPassword, std::string::String> {
let password_text = match std::env::var(DEMO_WALLET_PASSWORD_ENV) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(format!(
"backend secret {DEMO_WALLET_PASSWORD_ENV} is not configured as UTF-8"
));
},
};
return match ks_wallet::WalletPassword::new(password_text) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
fn wallet_transfer_format_payloads() -> std::vec::Vec<crate::DemoWalletTransferFormatPayload> {
let mut formats = std::vec::Vec::new();
for format in ks_wallet::WalletTransferFormat::supported() {
formats.push(crate::DemoWalletTransferFormatPayload {
code: format.code().to_string(),
label: format.label().to_string(),
default_extension: format.default_extension().to_string(),
});
}
return formats;
}
fn wallet_transfer_format_from_code(
code: &str,
) -> std::result::Result<ks_wallet::WalletTransferFormat, std::string::String> {
let code = code.trim();
for format in ks_wallet::WalletTransferFormat::supported() {
if format.code() == code {
return std::result::Result::Ok(format);
}
}
return std::result::Result::Err(format!("wallet transfer format '{code}' is not supported"));
}
fn onchain_profile<'a>(
state: &'a crate::AppState,
profile_name: &str,
@@ -470,6 +669,15 @@ mod tests {
"walletTokensTableBody",
"walletTransactionsTableBody",
"walletTransactionJson",
"walletImportSourcePathInput",
"walletImportFormatSelect",
"walletImportAliasInput",
"inspectWalletTransferButton",
"importWalletTransferButton",
"walletExportAliasSelect",
"walletExportFormatSelect",
"walletExportDestinationPathInput",
"exportWalletTransferButton",
"walletExternalPathInput",
"inspectWalletFileButton",
"walletInspectionJson",
@@ -477,6 +685,9 @@ mod tests {
assert!(html.contains(required));
}
assert!(script.contains("demo_wallet_create_native"));
assert!(script.contains("demo_wallet_inspect_transfer_file"));
assert!(script.contains("demo_wallet_import_file"));
assert!(script.contains("demo_wallet_export_file"));
assert!(script.contains("demo_wallet_balance"));
assert!(script.contains("demo_wallet_inventory"));
assert!(script.contains("demo_wallet_rpc_execute"));
@@ -490,6 +701,9 @@ mod tests {
assert!(main_script.contains("open_demo_wallet_window"));
assert!(tauri_runtime.contains("open_demo_wallet_window"));
assert!(tauri_runtime.contains("demo_wallet_create_native"));
assert!(tauri_runtime.contains("demo_wallet_inspect_transfer_file"));
assert!(tauri_runtime.contains("demo_wallet_import_file"));
assert!(tauri_runtime.contains("demo_wallet_export_file"));
assert!(tauri_runtime.contains("demo_wallet_balance"));
assert!(tauri_runtime.contains("demo_wallet_rpc_execute"));
assert!(tauri_runtime.contains("demo_wallet.html"));
@@ -507,6 +721,7 @@ mod tests {
cluster: "devnet".to_string(),
active: false,
}],
transfer_formats: super::wallet_transfer_format_payloads(),
token_program_id: ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
token_2022_program_id: ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(),
create_password_configured: true,
@@ -549,6 +764,17 @@ mod tests {
}
}
#[test]
fn wallet_transfer_format_inventory_matches_reusable_wallet_boundary() {
let formats = super::wallet_transfer_format_payloads();
assert_eq!(formats.len(), 2);
assert_eq!(formats[0].code, "solana_cli_json");
assert_eq!(formats[1].code, "solana_private_key_base58");
assert!(super::wallet_transfer_format_from_code("solana_cli_json").is_ok());
assert!(super::wallet_transfer_format_from_code("solana_private_key_base58").is_ok());
assert!(super::wallet_transfer_format_from_code("unknown").is_err());
}
#[test]
fn lamport_formatting_is_exact_without_float_conversion() {
assert_eq!(super::format_lamports_as_sol(0), "0");

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/lib.rs
// version: 40
// version: 41
//! Tauri desktop demo application for `khadhroony-bot3`.
@@ -367,8 +367,12 @@ pub(crate) use self::demo_transport::DemoEndpointRolePayload;
pub(crate) use self::demo_wallet::DemoWalletBalancePayload;
/// Request to create one native wallet from the backend-only demo secret.
pub(crate) use self::demo_wallet::DemoWalletCreateRequest;
/// Request to export one native wallet secret through the backend-only demo secret.
pub(crate) use self::demo_wallet::DemoWalletExportRequest;
/// UI-safe native wallet identity exposed by the desktop adapter.
pub(crate) use self::demo_wallet::DemoWalletIdentityPayload;
/// Request to import one external keypair into the native wallet store.
pub(crate) use self::demo_wallet::DemoWalletImportRequest;
/// UI-safe native wallet inventory exposed by the desktop adapter.
pub(crate) use self::demo_wallet::DemoWalletInventoryPayload;
/// One selectable profile for read-only wallet on-chain exploration.
@@ -377,12 +381,24 @@ pub(crate) use self::demo_wallet::DemoWalletOnchainProfilePayload;
pub(crate) use self::demo_wallet::DemoWalletRpcExecutionPayload;
/// Request for one wallet-scoped public JSON-RPC read.
pub(crate) use self::demo_wallet::DemoWalletRpcRequest;
/// One secret transfer format selectable by the wallet demo.
pub(crate) use self::demo_wallet::DemoWalletTransferFormatPayload;
/// Safe public identity extracted from one external keypair transfer file.
pub(crate) use self::demo_wallet::DemoWalletTransferInspectionPayload;
/// Request to inspect one external keypair transfer file without importing it.
pub(crate) use self::demo_wallet::DemoWalletTransferInspectionRequest;
/// Loads the exact SOL balance of one public wallet address.
pub(crate) use self::demo_wallet::demo_wallet_balance;
/// Creates one password-protected native wallet without exposing the password to Tauri IPC.
pub(crate) use self::demo_wallet::demo_wallet_create_native;
/// Exports one native wallet through an explicitly selected transfer format.
pub(crate) use self::demo_wallet::demo_wallet_export_file;
/// Imports one external keypair into a new native wallet.
pub(crate) use self::demo_wallet::demo_wallet_import_file;
/// Inspects an explicitly selected native wallet file without exposing its path in the result.
pub(crate) use self::demo_wallet::demo_wallet_inspect_file;
/// Inspects one external transfer file and returns only its derived public identity.
pub(crate) use self::demo_wallet::demo_wallet_inspect_transfer_file;
/// Returns native wallet identities without paths, passwords or protected payloads.
pub(crate) use self::demo_wallet::demo_wallet_inventory;
/// Executes one bounded read-only wallet explorer RPC through an explicitly selected profile.

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/tauri.rs
// version: 42
// version: 43
//! Tauri runtime assembly and private command wrappers.
@@ -39,6 +39,9 @@ pub fn run() -> ks_core::Result<()> {
open_demo_wallet_window,
demo_wallet_inventory,
demo_wallet_create_native,
demo_wallet_inspect_transfer_file,
demo_wallet_import_file,
demo_wallet_export_file,
demo_wallet_balance,
demo_wallet_inspect_file,
demo_wallet_rpc_execute,
@@ -263,6 +266,29 @@ async fn demo_wallet_create_native(
return crate::demo_wallet_create_native(state.inner(), request).await;
}
#[tauri::command]
async fn demo_wallet_inspect_transfer_file(
request: crate::DemoWalletTransferInspectionRequest,
) -> std::result::Result<crate::DemoWalletTransferInspectionPayload, std::string::String> {
return crate::demo_wallet_inspect_transfer_file(request).await;
}
#[tauri::command]
async fn demo_wallet_import_file(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoWalletImportRequest,
) -> std::result::Result<crate::DemoWalletIdentityPayload, std::string::String> {
return crate::demo_wallet_import_file(state.inner(), request).await;
}
#[tauri::command]
async fn demo_wallet_export_file(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoWalletExportRequest,
) -> std::result::Result<(), std::string::String> {
return crate::demo_wallet_export_file(state.inner(), request).await;
}
#[tauri::command]
async fn demo_wallet_balance(
state: tauri::State<'_, crate::AppState>,