v0.5.2-pre.006

This commit is contained in:
2026-08-10 23:14:17 +02:00
parent e3acb8ceb1
commit 066d969f5a
51 changed files with 1937 additions and 88 deletions

View File

@@ -0,0 +1,245 @@
<!-- file: kb-app-demo-desktop/frontend/demo_wallet.html -->
<!-- version: 3 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="icon" href="imgs/logo.png">
<link rel="stylesheet" href="sass/main.scss">
<title>Khadhroony Bot3 — Wallets</title>
</head>
<body>
<header class="app-header">
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
<div class="container-fluid px-4 my-0">
<div class="navbar-brand d-flex align-items-center">
<img alt="Logo" src="imgs/logo.png" class="app-logo">
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3 — <span class="badge text-bg-primary">Wallets</span></span>
</div>
</div>
</nav>
</header>
<main class="app-main">
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container-fluid px-4 py-4">
<div class="row g-4 justify-content-center">
<div class="col-12 col-xl-7">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
<div>
<h1 class="h3 card-title mb-1">Wallets natifs configurés</h1>
<p class="text-body-secondary mb-0">Inventaire des fichiers <code>.kswallet</code> du profil actif.</p>
</div>
<button id="refreshWalletInventoryButton" class="btn btn-primary" type="button">Rafraîchir</button>
</div>
<dl class="row mb-3">
<dt class="col-sm-3">Profil</dt>
<dd class="col-sm-9"><code id="walletProfile"></code></dd>
<dt class="col-sm-3">Cluster</dt>
<dd class="col-sm-9"><code id="walletCluster"></code></dd>
<dt class="col-sm-3">Statut</dt>
<dd class="col-sm-9"><code id="walletInventoryStatus">Chargement...</code></dd>
<dt class="col-sm-3">Alias sélectionné</dt>
<dd class="col-sm-9"><code id="walletSelectedAlias"></code></dd>
<dt class="col-sm-3">Wallets trouvés</dt>
<dd class="col-sm-9"><code id="walletInventoryCount">0</code></dd>
</dl>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0" id="walletInventoryTable">
<thead>
<tr>
<th>Alias</th>
<th>Pubkey déclarée</th>
<th>Format</th>
<th>Sélectionné</th>
<th>Action</th>
</tr>
</thead>
<tbody id="walletInventoryTableBody"></tbody>
</table>
</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-3">Créer un wallet natif</h2>
<p class="text-body-secondary mb-2">Le mot de passe ne traverse jamais Tauri IPC. Cette démo utilise le secret backend <code>KB_SECRET_DEMO_WALLET_PASSWORD</code>.</p>
<div class="mb-3">
<span class="fw-semibold">Secret backend :</span>
<code id="walletCreateSecretStatus" class="ms-1"></code>
</div>
<label for="createWalletAliasInput" class="form-label">Alias</label>
<input id="createWalletAliasInput" class="form-control font-monospace" type="text" autocomplete="off" maxlength="64" placeholder="operator-devnet">
<div class="d-flex flex-wrap gap-2 mt-3">
<button id="createWalletButton" class="btn btn-primary" type="button">Créer</button>
</div>
<div class="mt-3">
<span class="fw-semibold">Statut :</span>
<code id="walletCreateStatus" class="ms-1">En attente</code>
</div>
<p class="small text-body-secondary mb-0 mt-3">La création n'écrit pas automatiquement <code>wallet_alias</code> dans la configuration. Elle ajoute seulement le nouveau conteneur au store du profil actif.</p>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
<div>
<h2 class="h4 card-title mb-1">Explorateur on-chain</h2>
<p class="text-body-secondary mb-0">Lecture publique du solde SOL, des comptes SPL Token / Token-2022 et de l'historique récent impliquant l'adresse sur le profil RPC sélectionné.</p>
</div>
<button id="loadWalletOnchainButton" class="btn btn-primary" type="button">Charger l'état on-chain</button>
</div>
<div class="row g-3 align-items-end">
<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>
<input id="walletAddressInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="Pubkey Solana">
</div>
<div class="col-12 col-lg-2">
<label for="walletSignatureLimitInput" class="form-label">Transactions</label>
<input id="walletSignatureLimitInput" class="form-control" type="number" min="1" max="100" value="20">
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-12 col-md-4">
<div class="border rounded p-3 h-100">
<div class="small text-body-secondary">SOL</div>
<div class="fs-4 fw-semibold"><span id="walletSolBalance"></span> SOL</div>
<div class="small"><code id="walletLamportBalance"></code> lamports</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="border rounded p-3 h-100">
<div class="small text-body-secondary">Comptes token</div>
<div class="fs-4 fw-semibold" id="walletTokenCount"></div>
<div class="small" id="walletTokenStatus">En attente</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="border rounded p-3 h-100">
<div class="small text-body-secondary">Transactions récentes</div>
<div class="fs-4 fw-semibold" id="walletTransactionCount"></div>
<div class="small" id="walletTransactionStatus">En attente</div>
</div>
</div>
</div>
<div class="mt-3">
<span class="fw-semibold">Statut global :</span>
<code id="walletOnchainStatus" 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">
<h2 class="h5 card-title mb-3">Tokens détenus</h2>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0">
<thead>
<tr>
<th>Programme</th>
<th>Mint</th>
<th>Compte token</th>
<th>Montant</th>
<th>Raw</th>
</tr>
</thead>
<tbody id="walletTokensTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Transactions impliquant l'adresse</h2>
<div class="table-responsive app-table-full-width">
<table class="table table-sm align-middle mb-0">
<thead>
<tr>
<th>Signature</th>
<th>Slot</th>
<th>Date</th>
<th>Statut</th>
<th>Confirmation</th>
<th>Action</th>
</tr>
</thead>
<tbody id="walletTransactionsTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 card-title mb-3">Détail de transaction</h2>
<div id="walletTransactionJson" class="app-config-box app-json-viewer mb-0">{"status":"idle","message":"Sélectionner une transaction."}</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-3">Inspecter un fichier externe</h2>
<p class="text-body-secondary">Le chemin saisi est transmis au backend uniquement pour cette inspection et n'est pas retourné dans le résultat.</p>
<label for="walletExternalPathInput" class="form-label">Chemin du fichier <code>.kswallet</code></label>
<input id="walletExternalPathInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="/chemin/vers/operator.kswallet">
<div class="d-flex flex-wrap gap-2 mt-3">
<button id="inspectWalletFileButton" class="btn btn-primary" type="button">Inspecter</button>
<button id="clearWalletInspectionButton" class="btn btn-outline-secondary" type="button">Effacer</button>
</div>
<div class="mt-3">
<span class="fw-semibold">Statut :</span>
<code id="walletInspectionStatus" class="ms-1">En attente</code>
</div>
</div>
</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-3">Identité inspectée</h2>
<div id="walletInspectionJson" class="app-config-box app-json-viewer mb-0">{"status":"idle","message":"Aucun fichier inspecté."}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center">
<div class="row flex-grow-1 align-items-center">
<div class="col-12 text-center text-small my-1 my-md-0">&copy; 2026 SASEDEV</div>
</div>
</div>
</footer>
<script type="module" src="ts/demo_wallet.ts" defer></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/main.html -->
<!-- version: 11 -->
<!-- version: 12 -->
<!DOCTYPE html>
<html lang="fr">
@@ -26,6 +26,7 @@
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a id="openDemoConfigLink" class="dropdown-item" href="#">Configuration</a></li>
<li><a id="openDemoWalletLink" class="dropdown-item" href="#">Wallets</a></li>
<li>
<hr class="dropdown-divider">
</li>
@@ -99,4 +100,4 @@
<script type="module" src="ts/main.ts" defer></script>
</body>
</html>
</html>

View File

@@ -0,0 +1,532 @@
// file: kb-app-demo-desktop/frontend/ts/demo_wallet.ts
// version: 3
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts";
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 { DemoWalletIdentityPayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletIdentityPayload.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";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
interface ParsedTokenAccount {
program: string;
mint: string;
tokenAccount: string;
uiAmount: string;
rawAmount: string;
decimals: number;
}
interface ParsedSignatureInfo {
signature: string;
slot: number;
err: unknown | null;
memo: string | null;
blockTime: number | null;
confirmationStatus: string | null;
}
let walletInventory: DemoWalletInventoryPayload | null = null;
function setText(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
function inputValue(selector: string): string {
return document.querySelector<HTMLInputElement>(selector)?.value.trim() ?? "";
}
function selectedOnchainProfile(): string {
return document.querySelector<HTMLSelectElement>("#walletOnchainProfileSelect")?.value.trim() ?? "";
}
function selectedOnchainProfilePayload(): DemoWalletOnchainProfilePayload | null {
const profile = selectedOnchainProfile();
if (!walletInventory || profile.length === 0) {
return null;
}
return walletInventory.onchain_profiles.find(candidate => candidate.name === profile) ?? null;
}
function updateOnchainClusterLabel(): void {
const selected = selectedOnchainProfilePayload();
setText("#walletOnchainCluster", selected?.cluster ?? "—");
}
function renderOnchainProfiles(profiles: ReadonlyArray<DemoWalletOnchainProfilePayload>, activeProfile: string): void {
const select = document.querySelector<HTMLSelectElement>("#walletOnchainProfileSelect");
if (!select) {
return;
}
const previous = select.value;
select.replaceChildren();
for (const profile of profiles) {
const option = document.createElement("option");
option.value = profile.name;
option.textContent = `${profile.name}${profile.cluster}${profile.active ? " (actif)" : ""}`;
select.appendChild(option);
}
if (profiles.some(profile => profile.name === previous)) {
select.value = previous;
} else if (profiles.some(profile => profile.name === activeProfile)) {
select.value = activeProfile;
} else if (profiles.length > 0) {
select.value = profiles[0].name;
}
updateOnchainClusterLabel();
}
async function executeWalletRpc(profile: string, method: string, params: unknown[]): Promise<DemoWalletRpcExecutionPayload> {
const request: DemoWalletRpcRequest = {
profile,
method,
params_json: JSON.stringify(params),
};
return await invoke<DemoWalletRpcExecutionPayload>("demo_wallet_rpc_execute", { request });
}
function parseJsonResponse(response: DemoWalletRpcExecutionPayload): unknown {
return JSON.parse(response.response_json) as unknown;
}
function renderWalletRows(wallets: ReadonlyArray<DemoWalletIdentityPayload>): void {
const body = document.querySelector<HTMLTableSectionElement>("#walletInventoryTableBody");
if (!body) {
return;
}
body.replaceChildren();
if (wallets.length === 0) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 5;
cell.className = "text-body-secondary text-center py-3";
cell.textContent = "Aucun wallet natif trouvé.";
row.appendChild(cell);
body.appendChild(row);
return;
}
for (const wallet of wallets) {
const row = document.createElement("tr");
const aliasCell = document.createElement("td");
const publicKeyCell = document.createElement("td");
const formatCell = document.createElement("td");
const selectedCell = document.createElement("td");
const actionCell = document.createElement("td");
const exploreButton = document.createElement("button");
aliasCell.textContent = wallet.alias;
publicKeyCell.textContent = wallet.public_key;
publicKeyCell.className = "font-monospace text-break";
formatCell.textContent = `v${wallet.format_version}`;
selectedCell.textContent = wallet.selected ? "Oui" : "Non";
exploreButton.type = "button";
exploreButton.className = "btn btn-sm btn-outline-primary";
exploreButton.textContent = "Explorer";
exploreButton.addEventListener("click", () => {
const input = document.querySelector<HTMLInputElement>("#walletAddressInput");
if (input) {
input.value = wallet.public_key;
}
document.querySelector<HTMLElement>("#walletAddressInput")?.scrollIntoView({ behavior: "smooth", block: "center" });
});
actionCell.appendChild(exploreButton);
if (wallet.selected) {
row.classList.add("table-primary");
}
row.append(aliasCell, publicKeyCell, formatCell, selectedCell, actionCell);
body.appendChild(row);
}
}
async function refreshWalletInventory(): Promise<void> {
setText("#walletInventoryStatus", "Chargement...");
try {
const payload = await invoke<DemoWalletInventoryPayload>("demo_wallet_inventory");
walletInventory = payload;
setText("#walletProfile", payload.profile);
setText("#walletCluster", payload.cluster);
renderOnchainProfiles(payload.onchain_profiles, payload.profile);
setText("#walletCreateSecretStatus", payload.create_password_configured ? "configuré" : "absent");
setText("#walletSelectedAlias", payload.selected_alias ?? "—");
setText("#walletInventoryCount", String(payload.wallets.length));
setText("#walletInventoryStatus", "OK");
renderWalletRows(payload.wallets);
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", "wallet inventory refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletInventoryStatus", `Erreur : ${message}`);
setText("#walletInventoryCount", "0");
renderWalletRows([]);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Wallet inventory loading failed: ${message}`);
}
}
async function createWallet(): Promise<void> {
const alias = inputValue("#createWalletAliasInput");
if (alias.length === 0) {
setText("#walletCreateStatus", "Alias requis");
return;
}
const request: DemoWalletCreateRequest = { alias };
setText("#walletCreateStatus", "Création...");
try {
const created = await invoke<DemoWalletIdentityPayload>("demo_wallet_create_native", { request });
setText("#walletCreateStatus", `Créé : ${created.alias}`);
const addressInput = document.querySelector<HTMLInputElement>("#walletAddressInput");
if (addressInput) {
addressInput.value = created.public_key;
}
const aliasInput = document.querySelector<HTMLInputElement>("#createWalletAliasInput");
if (aliasInput) {
aliasInput.value = "";
}
await refreshWalletInventory();
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `native wallet created: ${created.alias}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletCreateStatus", `Erreur : ${message}`);
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Native wallet creation failed: ${message}`);
}
}
function parseTokenAccounts(value: unknown, program: string): ParsedTokenAccount[] {
if (typeof value !== "object" || value === null || !("value" in value)) {
return [];
}
const rows = (value as { value?: unknown }).value;
if (!Array.isArray(rows)) {
return [];
}
const parsed: ParsedTokenAccount[] = [];
for (const row of rows) {
if (typeof row !== "object" || row === null) {
continue;
}
const keyed = row as { pubkey?: unknown; account?: { data?: unknown } };
if (typeof keyed.pubkey !== "string" || typeof keyed.account?.data !== "object" || keyed.account.data === null) {
continue;
}
const data = keyed.account.data as { parsed?: { info?: unknown } };
const info = data.parsed?.info;
if (typeof info !== "object" || info === null) {
continue;
}
const typedInfo = info as { mint?: unknown; tokenAmount?: unknown };
const amount = typedInfo.tokenAmount;
if (typeof typedInfo.mint !== "string" || typeof amount !== "object" || amount === null) {
continue;
}
const typedAmount = amount as { amount?: unknown; decimals?: unknown; uiAmountString?: unknown };
if (typeof typedAmount.amount !== "string" || typeof typedAmount.decimals !== "number" || typeof typedAmount.uiAmountString !== "string") {
continue;
}
parsed.push({
program,
mint: typedInfo.mint,
tokenAccount: keyed.pubkey,
uiAmount: typedAmount.uiAmountString,
rawAmount: typedAmount.amount,
decimals: typedAmount.decimals,
});
}
return parsed;
}
function renderTokenRows(tokens: ReadonlyArray<ParsedTokenAccount>): void {
const body = document.querySelector<HTMLTableSectionElement>("#walletTokensTableBody");
if (!body) {
return;
}
body.replaceChildren();
if (tokens.length === 0) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 5;
cell.className = "text-body-secondary text-center py-3";
cell.textContent = "Aucun compte token trouvé.";
row.appendChild(cell);
body.appendChild(row);
return;
}
for (const token of tokens) {
const row = document.createElement("tr");
for (const value of [token.program, token.mint, token.tokenAccount, token.uiAmount, token.rawAmount]) {
const cell = document.createElement("td");
cell.textContent = value;
if (value === token.mint || value === token.tokenAccount) {
cell.className = "font-monospace text-break";
}
row.appendChild(cell);
}
body.appendChild(row);
}
}
function parseSignatureRows(value: unknown): ParsedSignatureInfo[] {
if (!Array.isArray(value)) {
return [];
}
const rows: ParsedSignatureInfo[] = [];
for (const item of value) {
if (typeof item !== "object" || item === null) {
continue;
}
const typed = item as Record<string, unknown>;
if (typeof typed.signature !== "string" || typeof typed.slot !== "number") {
continue;
}
rows.push({
signature: typed.signature,
slot: typed.slot,
err: typed.err ?? null,
memo: typeof typed.memo === "string" ? typed.memo : null,
blockTime: typeof typed.blockTime === "number" ? typed.blockTime : null,
confirmationStatus: typeof typed.confirmationStatus === "string" ? typed.confirmationStatus : null,
});
}
return rows;
}
function formatBlockTime(blockTime: number | null): string {
if (blockTime === null) {
return "—";
}
return new Date(blockTime * 1000).toLocaleString();
}
async function loadTransactionDetails(profile: string, signature: string): Promise<void> {
renderJsonViewer("#walletTransactionJson", { status: "loading", signature });
try {
const response = await executeWalletRpc(profile, "getTransaction", [
signature,
{ commitment: "confirmed", encoding: "jsonParsed", maxSupportedTransactionVersion: 0 },
]);
renderJsonViewer("#walletTransactionJson", parseJsonResponse(response));
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
renderJsonViewer("#walletTransactionJson", { status: "error", message, signature });
}
}
function renderSignatureRows(rows: ReadonlyArray<ParsedSignatureInfo>, profile: string): void {
const body = document.querySelector<HTMLTableSectionElement>("#walletTransactionsTableBody");
if (!body) {
return;
}
body.replaceChildren();
if (rows.length === 0) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 6;
cell.className = "text-body-secondary text-center py-3";
cell.textContent = "Aucune transaction trouvée.";
row.appendChild(cell);
body.appendChild(row);
return;
}
for (const item of rows) {
const row = document.createElement("tr");
const signatureCell = document.createElement("td");
const slotCell = document.createElement("td");
const timeCell = document.createElement("td");
const statusCell = document.createElement("td");
const confirmationCell = document.createElement("td");
const actionCell = document.createElement("td");
const detailsButton = document.createElement("button");
signatureCell.textContent = item.signature;
signatureCell.className = "font-monospace text-break";
slotCell.textContent = String(item.slot);
timeCell.textContent = formatBlockTime(item.blockTime);
statusCell.textContent = item.err === null ? "OK" : "Erreur";
confirmationCell.textContent = item.confirmationStatus ?? "—";
detailsButton.type = "button";
detailsButton.className = "btn btn-sm btn-outline-primary";
detailsButton.textContent = "Détails";
detailsButton.addEventListener("click", () => {
void loadTransactionDetails(profile, item.signature);
});
actionCell.appendChild(detailsButton);
row.append(signatureCell, slotCell, timeCell, statusCell, confirmationCell, actionCell);
body.appendChild(row);
}
}
async function loadWalletOnchain(): Promise<void> {
const profile = selectedOnchainProfile();
const address = inputValue("#walletAddressInput");
if (profile.length === 0) {
setText("#walletOnchainStatus", "Profil RPC requis");
return;
}
if (address.length === 0) {
setText("#walletOnchainStatus", "Adresse requise");
return;
}
const requestedLimit = Number.parseInt(inputValue("#walletSignatureLimitInput"), 10);
const signatureLimit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(100, requestedLimit)) : 20;
setText("#walletOnchainStatus", "Chargement...");
setText("#walletTokenStatus", "Chargement...");
setText("#walletTransactionStatus", "Chargement...");
setText("#walletSolBalance", "—");
setText("#walletLamportBalance", "—");
setText("#walletTokenCount", "—");
setText("#walletTransactionCount", "—");
renderTokenRows([]);
renderSignatureRows([], profile);
renderJsonViewer("#walletTransactionJson", { status: "idle", message: "Sélectionner une transaction." });
const operations: Promise<void>[] = [];
operations.push((async () => {
try {
const balance = await invoke<DemoWalletBalancePayload>("demo_wallet_balance", { profile, address });
setText("#walletSolBalance", balance.sol);
setText("#walletLamportBalance", balance.lamports);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletSolBalance", "Erreur");
setText("#walletLamportBalance", message);
}
})());
operations.push((async () => {
try {
if (!walletInventory) {
throw new Error("Inventaire wallet non chargé.");
}
const [classic, token2022] = await Promise.all([
executeWalletRpc(profile, "getTokenAccountsByOwner", [
address,
{ programId: walletInventory.token_program_id },
{ encoding: "jsonParsed", commitment: "confirmed" },
]),
executeWalletRpc(profile, "getTokenAccountsByOwner", [
address,
{ programId: walletInventory.token_2022_program_id },
{ encoding: "jsonParsed", commitment: "confirmed" },
]),
]);
const tokens = [
...parseTokenAccounts(parseJsonResponse(classic), "SPL Token"),
...parseTokenAccounts(parseJsonResponse(token2022), "Token-2022"),
];
tokens.sort((left, right) => left.mint.localeCompare(right.mint) || left.tokenAccount.localeCompare(right.tokenAccount));
setText("#walletTokenCount", String(tokens.length));
setText("#walletTokenStatus", "OK");
renderTokenRows(tokens);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletTokenCount", "0");
setText("#walletTokenStatus", `Erreur : ${message}`);
renderTokenRows([]);
}
})());
operations.push((async () => {
try {
const history = await executeWalletRpc(profile, "getSignaturesForAddress", [
address,
{ commitment: "confirmed", limit: signatureLimit },
]);
const rows = parseSignatureRows(parseJsonResponse(history));
setText("#walletTransactionCount", String(rows.length));
setText("#walletTransactionStatus", "OK");
renderSignatureRows(rows, profile);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletTransactionCount", "0");
setText("#walletTransactionStatus", `Erreur : ${message}`);
renderSignatureRows([], profile);
}
})());
await Promise.all(operations);
const selected = selectedOnchainProfilePayload();
setText("#walletOnchainStatus", `Terminé — ${selected?.name ?? profile} / ${selected?.cluster ?? "cluster inconnu"}`);
}
async function inspectWalletFile(): Promise<void> {
const path = inputValue("#walletExternalPathInput");
if (path.length === 0) {
setText("#walletInspectionStatus", "Chemin requis");
renderJsonViewer("#walletInspectionJson", { status: "error", message: "Saisir un chemin vers un fichier .kswallet." });
return;
}
setText("#walletInspectionStatus", "Inspection...");
renderJsonViewer("#walletInspectionJson", { status: "loading", message: "Inspection du wallet natif." });
try {
const payload = await invoke<DemoWalletIdentityPayload>("demo_wallet_inspect_file", { path });
setText("#walletInspectionStatus", "OK");
renderJsonViewer("#walletInspectionJson", payload);
const addressInput = document.querySelector<HTMLInputElement>("#walletAddressInput");
if (addressInput) {
addressInput.value = payload.public_key;
}
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", "external wallet inspected");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#walletInspectionStatus", `Erreur : ${message}`);
renderJsonViewer("#walletInspectionJson", { status: "error", message });
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `External wallet inspection failed: ${message}`);
}
}
function clearWalletInspection(): void {
const pathInput = document.querySelector<HTMLInputElement>("#walletExternalPathInput");
if (pathInput) {
pathInput.value = "";
}
setText("#walletInspectionStatus", "En attente");
renderJsonViewer("#walletInspectionJson", { status: "idle", message: "Aucun fichier inspecté." });
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb-app-demo-desktop.frontend.demo_wallet");
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", "wallet demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLButtonElement>("#refreshWalletInventoryButton")?.addEventListener("click", () => {
void refreshWalletInventory();
});
document.querySelector<HTMLButtonElement>("#createWalletButton")?.addEventListener("click", () => {
void createWallet();
});
document.querySelector<HTMLInputElement>("#createWalletAliasInput")?.addEventListener("keydown", event => {
if (event.key === "Enter") {
event.preventDefault();
void createWallet();
}
});
document.querySelector<HTMLSelectElement>("#walletOnchainProfileSelect")?.addEventListener("change", () => {
updateOnchainClusterLabel();
});
document.querySelector<HTMLButtonElement>("#loadWalletOnchainButton")?.addEventListener("click", () => {
void loadWalletOnchain();
});
document.querySelector<HTMLInputElement>("#walletAddressInput")?.addEventListener("keydown", event => {
if (event.key === "Enter") {
event.preventDefault();
void loadWalletOnchain();
}
});
document.querySelector<HTMLButtonElement>("#inspectWalletFileButton")?.addEventListener("click", () => {
void inspectWalletFile();
});
document.querySelector<HTMLButtonElement>("#clearWalletInspectionButton")?.addEventListener("click", () => {
clearWalletInspection();
});
document.querySelector<HTMLInputElement>("#walletExternalPathInput")?.addEventListener("keydown", event => {
if (event.key === "Enter") {
event.preventDefault();
void inspectWalletFile();
}
});
void refreshWalletInventory();
});

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/main.ts
// version: 6
// version: 7
import * as bootstrap from "bootstrap";
import MarkdownIt from "markdown-it";
@@ -44,6 +44,10 @@ document.addEventListener("DOMContentLoaded", async () => {
event.preventDefault();
void openDemoConfigWindow();
});
document.querySelector<HTMLAnchorElement>("#openDemoWalletLink")?.addEventListener("click", event => {
event.preventDefault();
void invoke("open_demo_wallet_window");
});
frontendDebug("kb-app-demo-desktop.frontend.main", "main window loaded");
const readmeContent = document.querySelector<HTMLElement>("#readmeContent");
if (readmeContent) {