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,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();
});