// 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(selector); if (element) { element.textContent = value; } } function inputValue(selector: string): string { return document.querySelector(selector)?.value.trim() ?? ""; } function selectedOnchainProfile(): string { return document.querySelector("#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, activeProfile: string): void { const select = document.querySelector("#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 { const request: DemoWalletRpcRequest = { profile, method, params_json: JSON.stringify(params), }; return await invoke("demo_wallet_rpc_execute", { request }); } function parseJsonResponse(response: DemoWalletRpcExecutionPayload): unknown { return JSON.parse(response.response_json) as unknown; } function renderWalletRows(wallets: ReadonlyArray): void { const body = document.querySelector("#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("#walletAddressInput"); if (input) { input.value = wallet.public_key; } document.querySelector("#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 { setText("#walletInventoryStatus", "Chargement..."); try { const payload = await invoke("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 { 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("demo_wallet_create_native", { request }); setText("#walletCreateStatus", `Créé : ${created.alias}`); const addressInput = document.querySelector("#walletAddressInput"); if (addressInput) { addressInput.value = created.public_key; } const aliasInput = document.querySelector("#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): void { const body = document.querySelector("#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; 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 { 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, profile: string): void { const body = document.querySelector("#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 { 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[] = []; operations.push((async () => { try { const balance = await invoke("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 { 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("demo_wallet_inspect_file", { path }); setText("#walletInspectionStatus", "OK"); renderJsonViewer("#walletInspectionJson", payload); const addressInput = document.querySelector("#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("#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("#refreshWalletInventoryButton")?.addEventListener("click", () => { void refreshWalletInventory(); }); document.querySelector("#createWalletButton")?.addEventListener("click", () => { void createWallet(); }); document.querySelector("#createWalletAliasInput")?.addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); void createWallet(); } }); document.querySelector("#walletOnchainProfileSelect")?.addEventListener("change", () => { updateOnchainClusterLabel(); }); document.querySelector("#loadWalletOnchainButton")?.addEventListener("click", () => { void loadWalletOnchain(); }); document.querySelector("#walletAddressInput")?.addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); void loadWalletOnchain(); } }); document.querySelector("#inspectWalletFileButton")?.addEventListener("click", () => { void inspectWalletFile(); }); document.querySelector("#clearWalletInspectionButton")?.addEventListener("click", () => { clearWalletInspection(); }); document.querySelector("#walletExternalPathInput")?.addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); void inspectWalletFile(); } }); void refreshWalletInventory(); });