v0.5.2-pre.006
This commit is contained in:
245
kb-app-demo-desktop/frontend/demo_wallet.html
Normal file
245
kb-app-demo-desktop/frontend/demo_wallet.html
Normal 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">© 2026 SASEDEV</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="ts/demo_wallet.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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>
|
||||
532
kb-app-demo-desktop/frontend/ts/demo_wallet.ts
Normal file
532
kb-app-demo-desktop/frontend/ts/demo_wallet.ts
Normal 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();
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user