Files
khadhroony-bot3/kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
2026-08-04 17:14:01 +02:00

422 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
// version: 10
import * as bootstrap from "bootstrap";
import "simplebar";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts";
import { renderJsonViewer } from "./json_viewer.ts";
import type { DemoExecutionMetadataScenarioPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataScenarioPayload.ts";
import type { DemoExecutionMetadataPreparedStepPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataPreparedStepPayload.ts";
import type { DemoExecutionMetadataRequest } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataRequest.ts";
import type { DemoExecutionMetadataSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSummaryPayload.ts";
import type { DemoExecutionDevnetStoreReadinessPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionDevnetStoreReadinessPayload.ts";
import type { DemoExecutionSolanaCoreOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreOptionsPayload.ts";
import type { DemoExecutionSolanaCoreProfileOption } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreProfileOption.ts";
const storageKey = "kb-app-demo-desktop.metadata.selected-scenario";
const profileStorageKey = "kb-app-demo-desktop.metadata.selected-profile";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
let scenarios: DemoExecutionMetadataScenarioPayload[] = [];
let selectedProfileName: string | null = null;
let executionRunning = false;
let profileStoreReady = false;
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
let preparedScenarioId: string | null = null;
let preparedStepIndex: number | null = null;
const metadataLogLines: string[] = [];
const maximumMetadataLogLines = 1200;
type MetadataProgressPayload = { timestamp: string; level: string; stage: string; message: string; signature?: string | null };
function appendMetadataLog(payload: MetadataProgressPayload): void {
const signature = payload.signature ? ` signature=${payload.signature}` : "";
metadataLogLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`);
if (metadataLogLines.length > maximumMetadataLogLines) {
metadataLogLines.splice(0, metadataLogLines.length - maximumMetadataLogLines);
}
const output = element<HTMLTextAreaElement>("#metadataExecutionLogOutput");
output.value = metadataLogLines.join("\n");
output.scrollTop = output.scrollHeight;
}
function titled(title: string, value: unknown): unknown { return { title, value }; }
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
if (value === null) {
throw new Error(`Élément introuvable : ${selector}`);
}
return value;
}
function activeSelect(): HTMLSelectElement {
return document.querySelector("#metaplexDevnetCollapse.show") !== null ? element<HTMLSelectElement>("#metadataDevnetScenarioSelect") : element<HTMLSelectElement>("#metadataSyntheticScenarioSelect");
}
function selectedScenario(): DemoExecutionMetadataScenarioPayload | null {
const id = activeSelect().value;
return scenarios.find(scenario => scenario.id === id) ?? null;
}
function fillSelect(selector: string, mode: string): void {
const select = element<HTMLSelectElement>(selector);
select.replaceChildren();
for (const scenario of scenarios.filter(candidate => candidate.mode === mode)) {
const option = document.createElement("option");
option.value = scenario.id;
option.textContent = `${scenario.assetFamily}${scenario.label}`;
select.append(option);
}
}
function operationName(operationCode: string): string {
const parts = operationCode.split(".");
return parts.length === 0 ? operationCode : parts[parts.length - 1] ?? operationCode;
}
function renderScenarioSteps(scenario: DemoExecutionMetadataScenarioPayload): void {
const select = element<HTMLSelectElement>("#metadataStepSelect");
const previousValue = Number.parseInt(select.value, 10);
select.replaceChildren();
scenario.operationCodes.forEach((operationCode, index) => {
const option = document.createElement("option");
option.value = String(index);
option.textContent = `${index + 1}. ${operationName(operationCode)}`;
select.append(option);
});
if (Number.isInteger(previousValue) && previousValue >= 0 && previousValue < scenario.operationCodes.length) {
select.value = String(previousValue);
}
const index = Number.parseInt(select.value, 10);
element<HTMLButtonElement>("#previousMetadataStepButton").disabled = !Number.isInteger(index) || index <= 0;
element<HTMLButtonElement>("#nextMetadataStepButton").disabled = !Number.isInteger(index) || index >= scenario.operationCodes.length - 1;
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").textContent = Number.isInteger(index) && scenario.operationCodes[index] !== undefined
? `Préparer létape ${index + 1}${operationName(scenario.operationCodes[index])}`
: "Préparer létape courante";
}
function renderSelectedScenario(): void {
const scenario = selectedScenario();
if (scenario === null) {
return;
}
window.localStorage.setItem(storageKey, scenario.id);
const networkMode = scenario.mode !== "synthetic";
renderScenarioSteps(scenario);
preparedScenarioId = null;
preparedStepIndex = null;
element<HTMLTextAreaElement>("#metadataOperationJson").value = "";
element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value = "[]";
element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value = "[]";
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Parcours</dt><dd class="col-6">${scenario.label}</dd><dt class="col-6">Famille</dt><dd class="col-6"><code>${scenario.assetFamily}</code></dd><dt class="col-6">Fixture</dt><dd class="col-6"><code>${scenario.fixtureKind}</code></dd><dt class="col-6">État initial</dt><dd class="col-6"><code>${scenario.initialState}</code></dd><dt class="col-6">État attendu</dt><dd class="col-6"><code>${scenario.resultingState}</code></dd><dt class="col-6">Mode</dt><dd class="col-6"><code>${scenario.mode}</code></dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${networkMode ? selectedProfileName ?? "non chargé" : "non applicable"}</code></dd><dt class="col-6">Collection</dt><dd class="col-6">${scenario.requiresCollection ? "requise" : "non requise"}</dd><dt class="col-6">Rule set</dt><dd class="col-6">${scenario.requiresProgrammableRules ? "requis" : "non requis"}</dd><dt class="col-6">Validation</dt><dd class="col-6"><code>${scenario.validationStatus}</code></dd>`;
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", {
scenarioId: scenario.id,
operationCodes: scenario.operationCodes,
checks: ["program_id", "owner", "pda_and_seeds", "required_accounts", "authorities", "stateful_correlations"],
networkExecutionPerformed: false,
}));
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
mode: scenario.mode,
policy: "simulation_first",
networkExecutionPerformed: false,
automaticSubmission: false,
operatorConfirmationRequired: true,
expectedState: scenario.validationStatus,
note: networkMode ? "Renseignez un intent typé puis lancez une simulation RPC réelle." : "Scénario synthétique déterministe.",
}));
renderJsonViewer("#metadataPostconditionsOutput", titled("Postconditions et preuves", {
required: scenario.requiresPostcondition,
evidenceKinds: scenario.requiredEvidence,
networkEvidenceCollected: false,
materializationTargets: scenario.materializationTargets,
rule: "Aucune validation réseau n'est déclarée sans simulation RPC réelle, signature éventuelle, slot et postconditions.",
}));
}
async function prepareScenarioStep(): Promise<void> {
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
const scenario = selectedScenario();
const stepIndex = Number.parseInt(element<HTMLSelectElement>("#metadataStepSelect").value, 10);
if (profileName.length === 0) { throw new Error("Sélectionnez un profil Devnet."); }
if (scenario === null || scenario.mode === "synthetic") { throw new Error("Sélectionnez un scénario Devnet."); }
if (!Number.isInteger(stepIndex) || stepIndex < 0) { throw new Error("Sélectionnez une étape valide."); }
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "fixture", message: `Préparation de ${scenario.id} étape ${stepIndex + 1}` });
const prepared = await invoke<DemoExecutionMetadataPreparedStepPayload>("demo_execution_metadata_prepare_step", {
profileName,
scenarioId: scenario.id,
stepIndex,
previousOperationJson: element<HTMLTextAreaElement>("#metadataOperationJson").value.trim().length === 0
? null
: element<HTMLTextAreaElement>("#metadataOperationJson").value,
});
element<HTMLTextAreaElement>("#metadataOperationJson").value = prepared.operationJson;
element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value = prepared.preflightReadsJson;
element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value = prepared.postconditionReadsJson;
preparedScenarioId = prepared.scenarioId;
preparedStepIndex = prepared.stepIndex;
renderJsonViewer("#metadataPreflightOutput", titled("Étape de scénario préparée", {
scenarioId: prepared.scenarioId,
stepIndex: prepared.stepIndex,
operation: prepared.operation,
fixtureState: prepared.fixtureState,
preflightReads: JSON.parse(prepared.preflightReadsJson),
postconditionReads: JSON.parse(prepared.postconditionReadsJson),
simulationReady: prepared.simulationReady,
submissionReady: prepared.submissionReady,
}));
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "fixture", message: `${prepared.label} prête (${prepared.fixtureState})` });
setExecutionRunning(executionRunning);
}
function moveScenarioStep(offset: number): void {
const select = element<HTMLSelectElement>("#metadataStepSelect");
const current = Number.parseInt(select.value, 10);
const next = current + offset;
if (Number.isInteger(next) && next >= 0 && next < select.options.length) {
select.value = String(next);
const scenario = selectedScenario();
if (scenario !== null) { renderScenarioSteps(scenario); }
}
}
function parseJsonText(value: string, label: string): unknown {
try {
return JSON.parse(value);
} catch (caught) {
const message = caught instanceof Error ? caught.message : String(caught);
throw new Error(`${label} invalide : ${message}`);
}
}
function selectedProfile(): DemoExecutionSolanaCoreProfileOption | null {
const name = element<HTMLSelectElement>("#metadataProfileSelect").value;
return profileOptions.find(profile => profile.name === name) ?? null;
}
function updateProfileHelp(): void {
const profile = selectedProfile();
const help = element<HTMLElement>("#metadataProfileHelp");
if (profile === null) {
help.textContent = "Aucun profil Devnet compatible.";
return;
}
help.textContent = `wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled ? "enabled" : "disabled"}`;
}
function setExecutionRunning(value: boolean): void {
executionRunning = value;
const profile = selectedProfile();
const missingOperation = element<HTMLTextAreaElement>("#metadataOperationJson").value.trim().length === 0;
const unavailable = value || profile === null || !profileStoreReady || missingOperation;
element<HTMLButtonElement>("#simulateMetadataButton").disabled = unavailable;
element<HTMLButtonElement>("#submitMetadataButton").disabled = unavailable || !profile?.sendEnabled;
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").disabled = value || profile === null || !profileStoreReady;
}
async function executeMetadata(submit: boolean): Promise<void> {
if (executionRunning) {
return;
}
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
if (profileName.length === 0 || !profileStoreReady) {
throw new Error("Sélectionnez et préparez un profil Devnet.");
}
const operationJson = element<HTMLTextAreaElement>("#metadataOperationJson").value;
const scenario = selectedScenario();
const stepIndex = Number.parseInt(element<HTMLSelectElement>("#metadataStepSelect").value, 10);
if (operationJson.trim().length === 0) {
throw new Error("Préparez létape courante avant lexécution.");
}
if (scenario === null || preparedScenarioId !== scenario.id || preparedStepIndex !== stepIndex) {
throw new Error("Préparez de nouveau létape après toute modification du scénario ou de létape.");
}
const preflightReadsJson = element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value;
const postconditionReadsJson = element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value;
parseJsonText(operationJson, "Intent Metaplex");
parseJsonText(preflightReadsJson, "Lectures de préflight");
parseJsonText(postconditionReadsJson, "Lectures de postcondition");
const operatorConfirmed = element<HTMLInputElement>("#metadataOperatorConfirmed").checked;
if (submit && !operatorConfirmed) {
throw new Error("La soumission exige la confirmation explicite de lopérateur.");
}
const request: DemoExecutionMetadataRequest = {
profileName,
intentId: `desktop-metaplex-${Date.now()}`,
operationJson,
preflightReadsJson,
postconditionReadsJson,
submit,
operatorConfirmed,
materializeAfterConfirmation: element<HTMLInputElement>("#metadataMaterializeInput").checked,
};
setExecutionRunning(true);
const badge = element<HTMLElement>("#metadataStatusBadge");
badge.textContent = submit ? "Soumission" : "Simulation";
badge.className = "badge text-bg-warning";
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: submit ? "submit" : "simulation", message: submit ? "Démarrage du parcours Devnet complet." : "Démarrage de la simulation RPC exacte." });
try {
const summary = await invoke<DemoExecutionMetadataSummaryPayload>("demo_execution_metadata_execute", { request });
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", JSON.parse(summary.preflightJson)));
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
profileName: summary.profileName,
cluster: summary.cluster,
genesisHash: summary.genesisHash,
walletPublicKey: summary.walletPublicKey,
balanceLamports: summary.balanceLamports,
simulationSuccess: summary.simulationSuccess,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
materializationRequested: summary.materializationRequested,
materializedSnapshotCount: summary.materializedSnapshotCount,
plan: JSON.parse(summary.planJson),
simulation: JSON.parse(summary.simulationJson),
}));
renderJsonViewer("#metadataPostconditionsOutput", titled("Postconditions et preuves", JSON.parse(summary.evidenceJson)));
badge.textContent = summary.transactionSignature === null ? "Simulé" : "Confirmé";
badge.className = summary.simulationSuccess ? "badge text-bg-success" : "badge text-bg-danger";
} finally {
setExecutionRunning(false);
}
}
async function loadProfiles(): Promise<void> {
const options = await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_metadata_options");
profileOptions = options.profiles;
const select = element<HTMLSelectElement>("#metadataProfileSelect");
select.replaceChildren();
for (const profile of options.profiles) {
const option = document.createElement("option");
option.value = profile.name;
option.textContent = `${profile.name}${profile.walletAlias}`;
select.append(option);
}
const restored = window.localStorage.getItem(profileStorageKey);
if (restored !== null && Array.from(select.options).some(option => option.value === restored)) {
select.value = restored;
} else if (options.defaultProfileName !== null) {
select.value = options.defaultProfileName;
}
selectedProfileName = select.value.length === 0 ? null : select.value;
updateProfileHelp();
if (options.profiles.length === 0) {
const message = "Aucun profil Devnet compatible na été retourné par la configuration active.";
appendMetadataLog({ timestamp: new Date().toISOString(), level: "error", stage: "profile_options", message });
element<HTMLElement>("#metadataProfileStatusBadge").textContent = "Aucun profil";
element<HTMLElement>("#metadataProfileStatusBadge").className = "badge text-bg-danger";
renderSelectedScenario();
return;
}
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "profile_options", message: `${options.profiles.length} profil(s) Devnet chargé(s).` });
renderSelectedScenario();
await prepareProfile();
}
async function prepareProfile(): Promise<void> {
const badge = element<HTMLElement>("#metadataProfileStatusBadge");
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
if (profileName.length === 0) {
badge.textContent = "Aucun profil";
badge.className = "badge text-bg-danger";
return;
}
profileStoreReady = false;
setExecutionRunning(executionRunning);
badge.textContent = "Préparation";
badge.className = "badge text-bg-warning";
try {
const readiness = await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile", { profileName });
selectedProfileName = profileName;
window.localStorage.setItem(profileStorageKey, profileName);
renderJsonViewer("#metadataProfileOutput", readiness);
profileStoreReady = true;
badge.textContent = "Base prête";
badge.className = "badge text-bg-success";
setExecutionRunning(executionRunning);
renderSelectedScenario();
} catch (caught) {
const message = caught instanceof Error ? caught.message : String(caught);
profileStoreReady = false;
badge.textContent = "Erreur";
badge.className = "badge text-bg-danger";
setExecutionRunning(executionRunning);
frontendError("kb-app-demo-desktop.frontend.execution_metadata", message);
}
}
async function loadScenarios(): Promise<void> {
const badge = element<HTMLElement>("#metadataStatusBadge");
badge.textContent = "Chargement";
badge.className = "badge text-bg-warning";
try {
scenarios = await invoke<DemoExecutionMetadataScenarioPayload[]>("demo_execution_metadata_scenarios");
fillSelect("#metadataSyntheticScenarioSelect", "synthetic");
fillSelect("#metadataDevnetScenarioSelect", "network_simulation");
const restored = window.localStorage.getItem(storageKey);
for (const selector of ["#metadataSyntheticScenarioSelect", "#metadataDevnetScenarioSelect"]) {
const select = element<HTMLSelectElement>(selector);
if (restored !== null && Array.from(select.options).some(option => option.value === restored)) {
select.value = restored;
}
}
renderSelectedScenario();
badge.textContent = `${scenarios.length} contrats`;
badge.className = "badge text-bg-success";
} catch (caught) {
const message = caught instanceof Error ? caught.message : String(caught);
profileStoreReady = false;
badge.textContent = "Erreur";
badge.className = "badge text-bg-danger";
setExecutionRunning(executionRunning);
frontendError("kb-app-demo-desktop.frontend.execution_metadata", message);
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb-app-demo-desktop.frontend.execution_metadata");
for (const selector of ["#metadataSyntheticScenarioSelect", "#metadataDevnetScenarioSelect"]) {
element<HTMLSelectElement>(selector).addEventListener("change", renderSelectedScenario);
}
for (const id of ["metaplexSyntheticCollapse", "metaplexDevnetCollapse"]) {
element<HTMLElement>(`#${id}`).addEventListener("shown.bs.collapse", renderSelectedScenario);
}
element<HTMLSelectElement>("#metadataProfileSelect").addEventListener("change", event => {
const target = event.currentTarget as HTMLSelectElement;
selectedProfileName = target.value.length === 0 ? null : target.value;
profileStoreReady = false;
updateProfileHelp();
setExecutionRunning(executionRunning);
renderSelectedScenario();
void prepareProfile();
});
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => void prepareProfile());
element<HTMLButtonElement>("#refreshMetadataScenariosButton").addEventListener("click", () => void loadScenarios());
element<HTMLSelectElement>("#metadataStepSelect").addEventListener("change", () => {
const scenario = selectedScenario();
if (scenario !== null) { renderScenarioSteps(scenario); }
});
element<HTMLButtonElement>("#previousMetadataStepButton").addEventListener("click", () => moveScenarioStep(-1));
element<HTMLButtonElement>("#nextMetadataStepButton").addEventListener("click", () => moveScenarioStep(1));
element<HTMLTextAreaElement>("#metadataOperationJson").addEventListener("input", () => {
preparedScenarioId = null;
preparedStepIndex = null;
setExecutionRunning(executionRunning);
});
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").addEventListener("click", () => { void prepareScenarioStep().catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught))); });
element<HTMLTextAreaElement>("#metadataExecutionLogOutput").addEventListener("app-log-clear", () => { metadataLogLines.length = 0; });
void listen<MetadataProgressPayload>("demo-execution-solana-core-progress", event => appendMetadataLog(event.payload));
element<HTMLButtonElement>("#simulateMetadataButton").addEventListener("click", () => {
void executeMetadata(false).catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught)));
});
element<HTMLButtonElement>("#submitMetadataButton").addEventListener("click", () => {
void executeMetadata(true).catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught)));
});
setExecutionRunning(false);
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Metadata execution window loaded");
void loadProfiles().catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught)));
void loadScenarios();
});