856 lines
38 KiB
TypeScript
856 lines
38 KiB
TypeScript
// file: kb-app-demo-desktop/frontend/ts/demo_wallet.ts
|
|
// version: 5
|
|
|
|
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 { DemoWalletExportRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExportRequest.ts";
|
|
import type { DemoWalletExecutionProfilePayload } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExecutionProfilePayload.ts";
|
|
import type { DemoWalletExecutionSelectionRequest } from "./bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExecutionSelectionRequest.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;
|
|
|
|
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;
|
|
let inspectedTransferPublicKey = "";
|
|
|
|
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("#walletOnchainSelectedProfile", selected?.name ?? "—");
|
|
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();
|
|
}
|
|
|
|
function selectedExecutionProfilePayload(): DemoWalletExecutionProfilePayload | null {
|
|
const profile = document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect")?.value.trim() ?? "";
|
|
if (!walletInventory || profile.length === 0) {
|
|
return null;
|
|
}
|
|
return walletInventory.execution_profiles.find(candidate => candidate.name === profile) ?? null;
|
|
}
|
|
|
|
function renderExecutionProfiles(profiles: ReadonlyArray<DemoWalletExecutionProfilePayload>): void {
|
|
const select = document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect");
|
|
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;
|
|
select.appendChild(option);
|
|
}
|
|
if (profiles.some(profile => profile.name === previous)) {
|
|
select.value = previous;
|
|
} else if (profiles.length > 0) {
|
|
select.value = profiles[0].name;
|
|
}
|
|
}
|
|
|
|
function renderExecutionWalletAliases(wallets: ReadonlyArray<DemoWalletIdentityPayload>): void {
|
|
const select = document.querySelector<HTMLSelectElement>("#walletExecutionAliasSelect");
|
|
if (!select) {
|
|
return;
|
|
}
|
|
select.replaceChildren();
|
|
const configuredOption = document.createElement("option");
|
|
configuredOption.value = "";
|
|
configuredOption.textContent = "Utiliser la configuration du profil";
|
|
select.appendChild(configuredOption);
|
|
for (const wallet of wallets) {
|
|
const option = document.createElement("option");
|
|
option.value = wallet.alias;
|
|
option.textContent = `${wallet.alias} — ${wallet.public_key}`;
|
|
select.appendChild(option);
|
|
}
|
|
}
|
|
|
|
function updateExecutionSelectionDisplay(): void {
|
|
const profile = selectedExecutionProfilePayload();
|
|
const aliasSelect = document.querySelector<HTMLSelectElement>("#walletExecutionAliasSelect");
|
|
if (!profile) {
|
|
setText("#walletExecutionConfiguredAlias", "—");
|
|
setText("#walletExecutionRuntimeAlias", "—");
|
|
setText("#walletExecutionEffectiveAlias", "temporaire");
|
|
if (aliasSelect) {
|
|
aliasSelect.value = "";
|
|
}
|
|
return;
|
|
}
|
|
setText("#walletExecutionConfiguredAlias", profile.configured_alias ?? "—");
|
|
setText("#walletExecutionRuntimeAlias", profile.runtime_alias ?? "—");
|
|
setText("#walletExecutionEffectiveAlias", profile.effective_alias ?? "temporaire");
|
|
if (aliasSelect) {
|
|
const requested = profile.runtime_alias ?? "";
|
|
if (Array.from(aliasSelect.options).some(option => option.value === requested)) {
|
|
aliasSelect.value = requested;
|
|
} else {
|
|
aliasSelect.value = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
async function applyExecutionWalletSelection(): Promise<void> {
|
|
const profile = document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect")?.value.trim() ?? "";
|
|
const aliasText = document.querySelector<HTMLSelectElement>("#walletExecutionAliasSelect")?.value.trim() ?? "";
|
|
if (profile.length === 0) {
|
|
setText("#walletExecutionSelectionStatus", "Profil Devnet requis");
|
|
return;
|
|
}
|
|
const request: DemoWalletExecutionSelectionRequest = {
|
|
profile,
|
|
alias: aliasText.length === 0 ? null : aliasText,
|
|
};
|
|
setText("#walletExecutionSelectionStatus", "Validation...");
|
|
try {
|
|
const payload = await invoke<DemoWalletInventoryPayload>("demo_wallet_select_execution_wallet", { request });
|
|
applyWalletInventoryPayload(payload);
|
|
const effective = payload.execution_profiles.find(candidate => candidate.name === profile)?.effective_alias ?? null;
|
|
setText("#walletExecutionSelectionStatus", effective ? `Persistant : ${effective}` : "Configuration du profil");
|
|
frontendDebug("kb-app-demo-desktop.frontend.demo_wallet", `execution wallet selection updated for ${profile}`);
|
|
} catch (caughtError) {
|
|
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
|
setText("#walletExecutionSelectionStatus", `Erreur : ${message}`);
|
|
frontendError("kb-app-demo-desktop.frontend.demo_wallet", `Execution wallet selection failed: ${message}`);
|
|
}
|
|
}
|
|
|
|
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 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 selectedExportFormatPayload(): DemoWalletTransferFormatPayload | null {
|
|
const code = selectedTransferFormat("#walletExportFormatSelect");
|
|
if (!walletInventory || code.length === 0) {
|
|
return null;
|
|
}
|
|
return walletInventory.transfer_formats.find(format => format.code === code) ?? null;
|
|
}
|
|
|
|
function updateExportFileName(force: boolean): void {
|
|
const input = document.querySelector<HTMLInputElement>("#walletExportFileNameInput");
|
|
const alias = document.querySelector<HTMLSelectElement>("#walletExportAliasSelect")?.value.trim() ?? "";
|
|
const format = selectedExportFormatPayload();
|
|
if (!input || alias.length === 0 || !format) {
|
|
return;
|
|
}
|
|
const generated = `${alias}.${format.default_extension}`;
|
|
if (force || input.value.trim().length === 0 || input.dataset.autoGenerated === "true") {
|
|
input.value = generated;
|
|
input.dataset.autoGenerated = "true";
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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}`;
|
|
const executionSelected = selectedExecutionProfilePayload()?.effective_alias === wallet.alias;
|
|
selectedCell.textContent = executionSelected ? "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 (executionSelected) {
|
|
row.classList.add("table-primary");
|
|
}
|
|
row.append(aliasCell, publicKeyCell, formatCell, selectedCell, actionCell);
|
|
body.appendChild(row);
|
|
}
|
|
}
|
|
|
|
function applyWalletInventoryPayload(payload: DemoWalletInventoryPayload): void {
|
|
walletInventory = payload;
|
|
setText("#walletProfile", payload.profile);
|
|
setText("#walletCluster", payload.cluster);
|
|
renderOnchainProfiles(payload.onchain_profiles, payload.profile);
|
|
renderExecutionProfiles(payload.execution_profiles);
|
|
renderTransferFormats(payload.transfer_formats);
|
|
renderExportWalletAliases(payload.wallets);
|
|
renderExecutionWalletAliases(payload.wallets);
|
|
setText("#walletCreateSecretStatus", payload.create_password_configured ? "configuré" : "absent");
|
|
setText("#walletSelectedAlias", payload.selected_alias ?? "—");
|
|
setText("#walletInventoryCount", String(payload.wallets.length));
|
|
setText("#walletExportDirectory", `${payload.export_directory}/`);
|
|
setText("#walletInventoryStatus", "OK");
|
|
renderWalletRows(payload.wallets);
|
|
updateExecutionSelectionDisplay();
|
|
updateExportFileName(false);
|
|
}
|
|
|
|
async function refreshWalletInventory(): Promise<void> {
|
|
setText("#walletInventoryStatus", "Chargement...");
|
|
try {
|
|
const payload = await invoke<DemoWalletInventoryPayload>("demo_wallet_inventory");
|
|
applyWalletInventoryPayload(payload);
|
|
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([]);
|
|
renderExportWalletAliases([]);
|
|
renderExecutionWalletAliases([]);
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
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 fileName = inputValue("#walletExportFileNameInput");
|
|
if (alias.length === 0 || format.length === 0 || fileName.length === 0) {
|
|
setText("#walletExportStatus", "Wallet, format et nom de fichier requis");
|
|
return;
|
|
}
|
|
const request: DemoWalletExportRequest = { alias, file_name: fileName, format };
|
|
setText("#walletExportStatus", "Export...");
|
|
try {
|
|
const exportedPath = await invoke<string>("demo_wallet_export_file", { request });
|
|
setText("#walletExportStatus", `Exporté : ${exportedPath}`);
|
|
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 [];
|
|
}
|
|
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>("#applyWalletExecutionSelectionButton")?.addEventListener("click", () => {
|
|
void applyExecutionWalletSelection();
|
|
});
|
|
document.querySelector<HTMLSelectElement>("#walletExecutionProfileSelect")?.addEventListener("change", () => {
|
|
updateExecutionSelectionDisplay();
|
|
if (walletInventory) {
|
|
renderWalletRows(walletInventory.wallets);
|
|
}
|
|
setText("#walletExecutionSelectionStatus", "En attente");
|
|
});
|
|
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<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<HTMLSelectElement>("#walletExportAliasSelect")?.addEventListener("change", () => {
|
|
updateExportFileName(false);
|
|
});
|
|
document.querySelector<HTMLSelectElement>("#walletExportFormatSelect")?.addEventListener("change", () => {
|
|
updateExportFileName(false);
|
|
});
|
|
document.querySelector<HTMLInputElement>("#walletExportFileNameInput")?.addEventListener("input", event => {
|
|
const input = event.currentTarget as HTMLInputElement;
|
|
input.dataset.autoGenerated = "false";
|
|
});
|
|
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();
|
|
});
|
|
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();
|
|
});
|