v0.4.7-pre.013

This commit is contained in:
2026-08-04 17:14:01 +02:00
parent 4d08c75b2b
commit 22228149a3
26 changed files with 2367 additions and 223 deletions

View File

@@ -1,8 +1,57 @@
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
<!-- version: 12 -->
<!-- version: 21 -->
# CHANGELOG — kb-app-demo-desktop
## 0.4.7-pre.013-fix-015
- aligne les tests desktop sur le nombre réel de scénarios synthétiques et Devnet exposés par les registres réutilisables ;
- conserve explicitement au moins un scénario collection et un scénario programmable dans linventaire ;
- borne les lectures Metadata et Edition à `65536` octets conformément au contrat `getAccountInfo` ;
- sérialise les toggles Metaplex `Collection`, `CollectionDetails`, `Uses` et `RuleSet` avec la variante `None` au lieu de `null` dans létape Update.
## 0.4.7-pre.013-fix-014
- remplace le flux global opération/modèle/fixture Create par une préparation contextuelle de létape du scénario ;
- remplit automatiquement lintent et les lectures stateful avant/après ;
- ajoute la navigation étape précédente/suivante et refuse lexécution dun intent périmé ;
- rend la matérialisation utilisable sans saisie manuelle des comptes de postcondition ;
- expose uniquement le parcours Devnet NFT actuellement entièrement préparé (`create` puis `update`).
### 0.4.7-pre.13 fix-011
- remplace la préparation Metaplex fondée sur `solana-keygen`, `solana` et `spl-token` par une transaction Rust native ;
- crée le mint par `SystemCreateAccount` puis `SPL Token InitializeMint2` dans un même message simulé, signé et confirmé ;
- utilise explicitement le wallet du profil comme mint authority et freeze authority ;
- conserve le keypair du mint dans `kb-wallet` sans exposer ni déléguer ses secrets à une CLI externe.
### 0.4.7-pre.013-fix.009
- affiche les parcours, fixtures et états Metaplex dans le panneau Devnet ;
- ajoute loption de matérialisation des snapshots confirmés ;
- expose le nombre de projections produites dans les résultats.
## 0.4.7-pre.13-fix.008 — les fixtures `Create` utilisent un nouveau mint à freeze authority active, valident owner/layout/décimales/supply/autorités avant simulation et empêchent la réutilisation dun intent précédent lorsquaucun modèle nommé nexiste.
## 0.4.7-pre.13-fix.005 — alignement du panneau Metadata sur les paramètres communs Devnet, boutons simulation/soumission séparés et correction du contrat dry-run après simulation.-fix.4
- correction de linitialisation du panneau Metadata : ajout des contrôles HTML du journal attendus par le TypeScript ;
- ajout dun test de contrat empêchant la disparition des sélecteurs de profil, scénario, opération et journal.
## 0.4.7-pre.13-fix.002
- Ajout du guide intégré, du journal dexécution et de titres explicites pour les résultats JSON Metaplex.
- Ajout dun préparateur idempotent de fixture `Create` : mint SPL classique Devnet, PDA metadata et master edition dérivés, intent JSON prêt à simuler.
## 0.4.7-pre.013 — exécution Metadata Devnet réelle
- ajout des commandes Tauri minces pour inventorier les opérations courantes, générer les modèles typés et appeler le runner Devnet réutilisable ;
- ajout de la simulation RPC réelle et de la soumission doublement autorisée depuis le panneau Metadata ;
- affichage séparé du plan, du préflight, de la simulation, de la signature, de la confirmation et des états avant/après ;
- conservation parallèle des scénarios synthétiques ;
- ajout des bindings TS-RS de requête et de résultat.
- correction de lextraction de la signature réseau depuis le newtype `MdSignature` sans dépendre de `Display`.
## 0.4.7-pre.011 — réouverture des démonstrations Devnet
### Documentation

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/README.md -->
<!-- version: 4 -->
<!-- version: 6 -->
# kb-app-demo-desktop
@@ -12,7 +12,7 @@
- diagnostics PostgreSQL ;
- backfill, extraction Core et replay ;
- démonstrations Solana Core, SPL Memo, SPL Token, ATA et Token-2022 ;
- panneau Metadata avec scénarios synthétiques, sélection du profil Devnet, préparation de la base et affichage structuré par le JSON viewer commun ;
- panneau Metadata avec scénarios synthétiques, préparation du profil et de la base, intents typés, simulation RPC Devnet réelle et soumission explicitement confirmée ;
- progression, annulation et restauration de létat des fenêtres.
## Frontière architecturale
@@ -23,10 +23,12 @@ Les scénarios UI Devnet/Testnet restent dans le desktop. Des tests automatisés
## Statut Metaplex `0.4.7`
Le panneau distingue correctement les contrats synthétiques et le contexte Devnet. Les exécutions RPC réelles des 20 opérations courantes seront raccordées pendant `pre.013`, après achèvement des campagnes réutilisables de `pre.011` et `pre.012`.
Le panneau conserve les scénarios synthétiques et appelle directement le runner réutilisable de `kb-pipeline-demo-scenarios` pour les opérations Metaplex courantes. La simulation RPC est réelle ; la soumission exige deux autorisations distinctes dans la requête desktop.
## Documentation
- [Guide dutilisation](USAGE.md)
- [Travaux restant à réaliser](TODO.md)
- [Historique des changements](CHANGELOG.md)
- Le parcours Metaplex desktop fournit un guide intégré, un journal, des résultats JSON titrés et un préparateur de fixture `Create`.

View File

@@ -1,14 +1,14 @@
<!-- file: kb-app-demo-desktop/TODO.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# TODO — kb-app-demo-desktop
## `0.4.7-pre.013` — démonstrations Metadata Devnet
- [ ] Commandes Tauri - raccorder les campagnes Metaplex Devnet réutilisables sans dupliquer leur logique.
- [ ] Interface - permettre simulation et soumission explicitement confirmée pour les opérations courantes disponibles.
- [ ] Contexte - afficher profil, cluster, base, wallet, mode, signature, slot et preuves de postcondition.
- [ ] Cohérence - conserver en parallèle les scénarios synthétiques et empêcher quils soient présentés comme preuves réseau.
- [x] Commandes Tauri - raccorder les campagnes Metaplex Devnet réutilisables sans dupliquer leur logique.
- [x] Interface - permettre simulation et soumission explicitement confirmée pour les opérations courantes disponibles.
- [x] Contexte - afficher profil, cluster, base, wallet, mode, signature, slot et preuves de postcondition.
- [x] Cohérence - conserver en parallèle les scénarios synthétiques et empêcher quils soient présentés comme preuves réseau.
- [ ] Validation manuelle - exécuter les parcours disponibles et confirmer la cohérence préflight, simulation, confirmation, soumission et postconditions.
## Versions ultérieures

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/USAGE.md -->
<!-- version: 2 -->
<!-- version: 4 -->
# Utilisation de kb-app-demo-desktop
@@ -194,6 +194,36 @@ console.log(scenarios);
La simulation et lenvoi doivent rester explicitement distingués dans les requêtes.
### Exécution Metadata Devnet
Le panneau Metadata charge les opérations courantes depuis `demo_execution_metadata_current_operations` et peut demander un modèle avec `demo_execution_metadata_operation_template`.
```typescript
const operationJson = await invoke(
"demo_execution_metadata_operation_template",
{ operation: "verify" }
);
const summary = await invoke(
"demo_execution_metadata_execute",
{
request: {
profileName: "devnet",
intentId: `desktop-metaplex-${Date.now()}`,
operationJson,
preflightReadsJson: "[]",
postconditionReadsJson: "[]",
submit: false,
operatorConfirmed: false
}
}
);
console.log(summary);
```
Les valeurs `<...>` dun modèle doivent être remplacées par des comptes réels avant exécution. `submit: true` exige simultanément `operatorConfirmed: true`. Le résultat distingue le plan, le préflight stateful, la simulation RPC, la signature éventuelle, la confirmation et les états avant/après.
### Diagnostics PostgreSQL
```typescript
@@ -258,3 +288,7 @@ Depuis la fenêtre principale, choisir **Exécution Devnet → Exécution Metada
Un contrat marqué Devnet nest pas, à lui seul, une exécution réseau. Les champs `networkExecutionPerformed` et `networkEvidenceCollected` doivent rester faux tant quaucun appel RPC réel et aucune preuve correspondante nont été produits.
Les commandes de simulation et de soumission Metaplex seront documentées ici après leur raccordement effectif pendant `0.4.7-pre.013`.
### Préparation de la fixture Metaplex `Create`
Le panneau desktop peut créer ou réutiliser un mint SPL classique Devnet à zéro décimale. Le préparateur dérive ensuite automatiquement les PDA `metadata` et `master edition` et génère un intent `Create` typé. Les valeurs entre chevrons des modèles génériques restent des placeholders non exécutables.

View File

@@ -5,16 +5,16 @@
"windows": [
"splash",
"main",
"demo_backfill",
"demo_config",
"demo_http",
"demo_ws",
"demo_backfill",
"demo_core_extraction",
"demo_decode_replay",
"demo_sql_diag",
"demo_sql_pg_raw",
"demo_sql_pg_core",
"demo_sql_replay_candidates",
"demo_config",
"demo_core_extraction",
"demo_decode_replay",
"demo_execution_solana_core",
"demo_execution_spl",
"demo_execution_metadata"

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_execution_metadata.html -->
<!-- version: 3 -->
<!-- version: 9 -->
<!DOCTYPE html>
<html lang="fr">
@@ -24,6 +24,23 @@
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container-fluid px-4 py-4">
<div class="alert alert-info">Cette fenêtre accueille les familles dexécution metadata. Metaplex Token Metadata est la première famille ; SPL Token Metadata sera ajouté ultérieurement sans changer la frontière de la fenêtre.</div>
<div class="accordion shadow-sm mb-4" id="metadataGuideAccordion">
<div class="accordion-item">
<h2 class="accordion-header"><button class="accordion-button collapsed" data-bs-target="#metadataGuideCollapse" data-bs-toggle="collapse" type="button">Guide dexécution Metaplex Devnet</button></h2>
<div class="accordion-collapse collapse" id="metadataGuideCollapse" data-bs-parent="#metadataGuideAccordion">
<div class="accordion-body">
<ol class="mb-2">
<li>Choisir un profil Devnet puis préparer sa base PostgreSQL.</li>
<li>Choisir le parcours Devnet puis son étape. Le parcours actuellement exécutable couvre <code>create</code> puis <code>update</code> sur un NFT classique.</li>
<li>Cliquer sur <strong>Préparer létape courante</strong> : lintent, les lectures stateful et les postconditions sont générés ensemble.</li>
<li>Lancer dabord une simulation. La soumission exige les deux confirmations explicites.</li>
<li>Contrôler le journal, le plan exact, la simulation RPC, la signature éventuelle et les états avant/après.</li>
</ol>
<div class="small text-muted">Les chaînes entre chevrons sont des placeholders non exécutables. Ne jamais les soumettre telles quelles.</div>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-12 col-xl-5">
<div class="accordion shadow-sm" id="metadataExecutionAccordion">
@@ -49,13 +66,41 @@
<div class="accordion-collapse collapse" id="metaplexDevnetCollapse" data-bs-parent="#metaplexScenarioModeAccordion">
<div class="accordion-body">
<label class="form-label" for="metadataProfileSelect">Profil Devnet</label>
<select class="form-select mb-3" id="metadataProfileSelect"></select>
<select class="form-select" id="metadataProfileSelect"></select>
<div class="form-text mb-3" id="metadataProfileHelp">Chargement du profil Devnet.</div>
<label class="form-label" for="metadataDevnetScenarioSelect">Contrat de scénario</label>
<select class="form-select" id="metadataDevnetScenarioSelect"></select>
<div class="d-grid mt-3"><button class="btn btn-outline-primary" id="prepareMetadataProfileButton" type="button">Charger le profil et préparer la base</button></div>
<div class="d-flex justify-content-between align-items-center mt-3"><span class="small text-muted">État du profil</span><span class="badge text-bg-secondary" id="metadataProfileStatusBadge">Non chargé</span></div>
<div class="mt-3" id="metadataProfileOutput"></div>
<div class="alert alert-warning mt-3 mb-0">Cette version prépare réellement le profil et sa base Devnet, mais nexécute pas encore de transaction Metaplex. Les contrats affichés ne constituent pas une preuve de simulation réseau.</div>
<label class="form-label mt-3" for="metadataStepSelect">Étape du parcours</label>
<select class="form-select" id="metadataStepSelect"></select>
<div class="d-flex gap-2 mt-2">
<button class="btn btn-outline-secondary flex-fill" id="previousMetadataStepButton" type="button">Étape précédente</button>
<button class="btn btn-outline-secondary flex-fill" id="nextMetadataStepButton" type="button">Étape suivante</button>
</div>
<div class="d-grid mt-2"><button class="btn btn-outline-success" id="prepareMetadataScenarioStepButton" type="button">Préparer létape courante</button></div>
<div class="form-text">Le scénario remplit ensemble lintent, les lectures avant/après et les cibles de matérialisation. Les étapes non encore préparables sont refusées explicitement.</div>
<label class="form-label mt-3" for="metadataOperationJson">Intent Metaplex typé</label>
<textarea class="form-control font-monospace" id="metadataOperationJson" rows="12" spellcheck="false"></textarea>
<label class="form-label mt-3" for="metadataPreflightReadsJson">Lectures stateful avant simulation</label>
<textarea class="form-control font-monospace" id="metadataPreflightReadsJson" rows="4" spellcheck="false">[]</textarea>
<label class="form-label mt-3" for="metadataPostconditionReadsJson">Lectures stateful après confirmation</label>
<textarea class="form-control font-monospace" id="metadataPostconditionReadsJson" rows="4" spellcheck="false">[]</textarea>
<div class="form-check mt-3">
<input class="form-check-input" id="metadataMaterializeInput" type="checkbox" checked>
<label class="form-check-label" for="metadataMaterializeInput">Matérialiser les snapshots Metaplex confirmés</label>
<div class="form-text">Après confirmation, relit les comptes déclarés, les décode canoniquement et produit les projections détat. Aucun fetch URI off-chain.</div>
</div>
<div class="form-check mt-3">
<input class="form-check-input" id="metadataOperatorConfirmed" type="checkbox">
<label class="form-check-label" for="metadataOperatorConfirmed">Je confirme explicitement la soumission</label>
</div>
<div class="d-grid gap-2 mt-3">
<button class="btn btn-outline-primary" id="simulateMetadataButton" type="button">Simuler sur Devnet</button>
<button class="btn btn-primary" id="submitMetadataButton" type="button">Soumettre sur Devnet</button>
</div>
<div class="alert alert-warning mt-3 mb-0">La simulation appelle réellement le RPC Devnet. La soumission reste désactivée tant que lopérateur ne lautorise et ne la confirme pas explicitement.</div>
</div>
</div>
</div>
@@ -67,6 +112,12 @@
</div>
</div>
</div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-body">
<h2 class="h5 mb-2">Journal dexécution</h2>
<textarea class="form-control font-monospace" id="metadataExecutionLogOutput" readonly rows="12" spellcheck="false"></textarea>
</div>
</div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-body">
<h2 class="h5">Exigences</h2>
@@ -78,19 +129,19 @@
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h2 class="h5">Préflight et opérations</h2>
<div id="metadataPreflightOutput"></div>
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataPreflightOutput" readonly rows="14">{"title":"Préflight et opérations","status":"idle","message":"Aucun résultat."}</textarea>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h2 class="h5">Simulation et confirmation</h2>
<div id="metadataSimulationOutput"></div>
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataSimulationOutput" readonly rows="16">{"title":"Simulation et confirmation","status":"idle","message":"Aucune simulation."}</textarea>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5">Postconditions et preuves</h2>
<div id="metadataPostconditionsOutput"></div>
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataPostconditionsOutput" readonly rows="16">{"title":"Postconditions et preuves","status":"idle","message":"Aucune preuve."}</textarea>
</div>
</div>
</div>

View File

@@ -1,19 +1,47 @@
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
// version: 3
import "bootstrap";
// version: 10
import * as bootstrap from "bootstrap";
import "simplebar";
import { invoke } from "@tauri-apps/api/core";
import { frontendError } from "./frontend_log.ts";
import { installFrontendConsoleBridge } from "./frontend_log.ts";
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);
@@ -38,11 +66,37 @@ function fillSelect(selector: string, mode: string): void {
for (const scenario of scenarios.filter(candidate => candidate.mode === mode)) {
const option = document.createElement("option");
option.value = scenario.id;
option.textContent = `${scenario.assetFamily}${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) {
@@ -50,32 +104,188 @@ function renderSelectedScenario(): void {
}
window.localStorage.setItem(storageKey, scenario.id);
const networkMode = scenario.mode !== "synthetic";
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Famille</dt><dd class="col-6"><code>${scenario.assetFamily}</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", {
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", {
}));
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
mode: scenario.mode,
policy: "simulation_first",
networkExecutionPerformed: false,
automaticSubmission: false,
operatorConfirmationRequired: true,
expectedState: scenario.validationStatus,
note: networkMode ? "Le contrat Devnet est sélectionné, mais aucune transaction Metaplex n'est simulée par cette commande de chargement." : "Scénario synthétique déterministe.",
});
renderJsonViewer("#metadataPostconditionsOutput", {
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_solana_core_options");
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) {
@@ -91,7 +301,18 @@ async function loadProfiles(): Promise<void> {
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> {
@@ -102,6 +323,8 @@ async function prepareProfile(): Promise<void> {
badge.className = "badge text-bg-danger";
return;
}
profileStoreReady = false;
setExecutionRunning(executionRunning);
badge.textContent = "Préparation";
badge.className = "badge text-bg-warning";
try {
@@ -109,13 +332,17 @@ async function prepareProfile(): Promise<void> {
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);
}
}
@@ -140,8 +367,10 @@ async function loadScenarios(): Promise<void> {
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);
}
}
@@ -157,10 +386,36 @@ document.addEventListener("DOMContentLoaded", () => {
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();
});

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_metadata.rs
// version: 2
// version: 12
//! Thin desktop adapters for metadata execution demo scenarios.
@@ -13,8 +13,16 @@
pub(crate) struct DemoExecutionMetadataScenarioPayload {
/// Stable scenario identifier.
pub id: std::string::String,
/// Operator-visible scenario label.
pub label: std::string::String,
/// Stable asset-family code.
pub asset_family: std::string::String,
/// Stable fixture graph code.
pub fixture_kind: std::string::String,
/// Required initial lifecycle state.
pub initial_state: std::string::String,
/// Expected terminal lifecycle state.
pub resulting_state: std::string::String,
/// Stable execution-mode code.
pub mode: std::string::String,
/// Ordered operation codes.
@@ -29,6 +37,486 @@ pub(crate) struct DemoExecutionMetadataScenarioPayload {
pub validation_status: std::string::String,
/// Required evidence kinds.
pub required_evidence: std::vec::Vec<std::string::String>,
/// Canonical account projections expected when materialization is enabled.
pub materialization_targets: std::vec::Vec<std::string::String>,
}
/// UI-safe result of one prepared Metaplex Create fixture.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataCreateFixturePayload.ts"
)]
pub(crate) struct DemoExecutionMetadataCreateFixturePayload {
/// Fixture mint address.
pub mint: std::string::String,
/// Canonical metadata PDA.
pub metadata: std::string::String,
/// Canonical master-edition PDA.
pub master_edition: std::string::String,
/// Wallet authority.
pub authority: std::string::String,
/// Generated typed Create operation JSON.
pub operation_json: std::string::String,
/// Whether the mint account was newly created.
pub mint_created: bool,
}
/// UI-safe prepared step for one coherent Metaplex Devnet scenario.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataPreparedStepPayload.ts"
)]
pub(crate) struct DemoExecutionMetadataPreparedStepPayload {
/// Stable scenario identifier.
pub scenario_id: std::string::String,
/// Zero-based step index.
pub step_index: usize,
/// Stable operation name accepted by the executor JSON contract.
pub operation: std::string::String,
/// Operator-visible step label.
pub label: std::string::String,
/// Prepared typed operation JSON.
pub operation_json: std::string::String,
/// Automatically generated bounded preflight reads.
pub preflight_reads_json: std::string::String,
/// Automatically generated bounded postcondition reads.
pub postcondition_reads_json: std::string::String,
/// Whether this step can be simulated immediately.
pub simulation_ready: bool,
/// Whether this step can be submitted immediately.
pub submission_ready: bool,
/// Fixture state used to prepare the step.
pub fixture_state: std::string::String,
}
/// Request for one real Devnet Metaplex execution from the desktop panel.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataRequest.ts"
)]
pub(crate) struct DemoExecutionMetadataRequest {
/// Devnet profile selected by the operator.
pub profile_name: std::string::String,
/// Stable execution identifier shown in diagnostics.
pub intent_id: std::string::String,
/// Serialized typed current Metaplex operation.
pub operation_json: std::string::String,
/// Serialized bounded preflight read requests.
pub preflight_reads_json: std::string::String,
/// Serialized bounded postcondition read requests.
pub postcondition_reads_json: std::string::String,
/// Whether signing and submission are explicitly requested.
pub submit: bool,
/// Explicit operator confirmation for submission.
pub operator_confirmed: bool,
/// Materializes confirmed Metaplex account snapshots after submission.
pub materialize_after_confirmation: bool,
}
/// UI-safe result of one real Devnet Metaplex execution.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSummaryPayload.ts"
)]
pub(crate) struct DemoExecutionMetadataSummaryPayload {
/// Selected profile name.
pub profile_name: std::string::String,
/// Exact cluster code.
pub cluster: std::string::String,
/// Genesis hash observed from RPC.
pub genesis_hash: std::string::String,
/// Wallet public key used by the runner.
pub wallet_public_key: std::string::String,
/// Wallet balance before execution.
pub balance_lamports: std::string::String,
/// Whether the real RPC simulation succeeded.
pub simulation_success: bool,
/// Optional submitted signature.
pub transaction_signature: std::option::Option<std::string::String>,
/// Optional terminal confirmation status.
pub confirmation_status: std::option::Option<std::string::String>,
/// Exact prepared plan as formatted JSON.
pub plan_json: std::string::String,
/// Stateful preflight as formatted JSON.
pub preflight_json: std::string::String,
/// Real RPC simulation as formatted JSON.
pub simulation_json: std::string::String,
/// Before/after state and readiness diagnostics as formatted JSON.
pub evidence_json: std::string::String,
/// Whether post-confirmation account materialization was requested.
pub materialization_requested: bool,
/// Number of canonical post-confirmation snapshots materialized.
pub materialized_snapshot_count: usize,
}
/// Returns Devnet profile options dedicated to the Metadata execution panel.
pub(crate) fn demo_execution_metadata_options(
state: tauri::State<'_, crate::AppState>,
) -> crate::DemoExecutionSolanaCoreOptionsPayload {
return crate::demo_execution_solana_core_options(state);
}
/// Executes one real Devnet Metaplex simulation or explicitly confirmed submission.
pub(crate) async fn demo_execution_metadata_execute(
app_handle: tauri::AppHandle,
state: tauri::State<'_, crate::AppState>,
request: crate::DemoExecutionMetadataRequest,
) -> std::result::Result<crate::DemoExecutionMetadataSummaryPayload, std::string::String> {
let acquire_result = state.demo_execution_solana_core_running().compare_exchange(
false,
true,
std::sync::atomic::Ordering::AcqRel,
std::sync::atomic::Ordering::Acquire,
);
if acquire_result.is_err() {
return std::result::Result::Err("a Devnet execution is already running".to_string());
}
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
running: state.demo_execution_solana_core_running(),
};
state
.demo_execution_solana_core_cancel_requested()
.store(false, std::sync::atomic::Ordering::Release);
let profile =
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let http_pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let mut execution_request = match kb_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
request.intent_id,
request.operation_json.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
execution_request.preflight_reads =
match parse_stateful_reads(request.preflight_reads_json.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
execution_request.postcondition_reads =
match parse_stateful_reads(request.postcondition_reads_json.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
execution_request.submit = request.submit;
execution_request.operator_confirmed = request.operator_confirmed;
execution_request.materialize_after_confirmation = request.materialize_after_confirmation;
if let std::result::Result::Err(error) = execution_request.validate() {
return std::result::Result::Err(error.to_string());
}
let observer = crate::DemoExecutionSolanaCoreObserver {
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match kb_pipeline_demo_scenarios::execute_devnet_metaplex_token_metadata(
&http_pool,
&profile,
workspace_root.as_path(),
&execution_request,
&observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let transaction_signature =
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
let confirmation_status =
summary.confirmation.as_ref().map(|value| return format!("{:?}", value.status));
let evidence_json = crate::pretty_json(&serde_json::json!({
"before": &summary.before,
"readiness": &summary.readiness,
"sendResult": &summary.send_result,
"confirmation": &summary.confirmation,
"after": &summary.after,
"materializationRequested": summary.materialization_requested,
"materializedSnapshots": &summary.materialized_snapshots
}));
return std::result::Result::Ok(crate::DemoExecutionMetadataSummaryPayload {
profile_name: summary.profile_name,
cluster: format!("{:?}", summary.cluster).to_lowercase(),
genesis_hash: summary.genesis_hash,
wallet_public_key: summary.wallet.public_key,
balance_lamports: summary.balance_lamports.to_string(),
simulation_success: summary.simulation.success,
transaction_signature,
confirmation_status,
plan_json: crate::pretty_json(&summary.plan),
preflight_json: crate::pretty_json(&summary.stateful_preflight),
simulation_json: crate::pretty_json(&summary.simulation),
evidence_json,
materialization_requested: summary.materialization_requested,
materialized_snapshot_count: summary.materialized_snapshots.len(),
});
}
fn parse_stateful_reads(
value: &str,
) -> std::result::Result<
std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest>,
std::string::String,
> {
if value.trim().is_empty() {
return std::result::Result::Ok(std::vec::Vec::new());
}
return match serde_json::from_str(value) {
std::result::Result::Ok(parsed) => std::result::Result::Ok(parsed),
std::result::Result::Err(error) => {
std::result::Result::Err(format!("invalid Metaplex stateful reads JSON: {error}"))
},
};
}
/// Creates or reuses the Devnet mint fixture used by the named Create scenario.
pub(crate) async fn demo_execution_metadata_prepare_create_fixture(
state: tauri::State<'_, crate::AppState>,
profile_name: std::string::String,
) -> std::result::Result<crate::DemoExecutionMetadataCreateFixturePayload, std::string::String> {
let profile = match crate::select_devnet_profile(state.app_config(), profile_name.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let wallet_dir = if configured.is_absolute() {
configured
} else {
crate::workspace_root_dir().join(configured)
};
let http_pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let workspace_root = crate::workspace_root_dir();
let summary = match kb_pipeline_demo_scenarios::prepare_metaplex_create_fixture(
&http_pool,
&profile,
workspace_root.as_path(),
&kb_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions::new(wallet_dir),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
return std::result::Result::Ok(crate::DemoExecutionMetadataCreateFixturePayload {
mint: summary.mint,
metadata: summary.metadata,
master_edition: summary.master_edition,
authority: summary.authority,
operation_json: summary.operation_json,
mint_created: summary.mint_created,
});
}
/// Prepares one coherent scenario step and all of its stateful reads.
pub(crate) async fn demo_execution_metadata_prepare_step(
state: tauri::State<'_, crate::AppState>,
profile_name: std::string::String,
scenario_id: std::string::String,
step_index: usize,
previous_operation_json: std::option::Option<std::string::String>,
) -> std::result::Result<crate::DemoExecutionMetadataPreparedStepPayload, std::string::String> {
let scenario = kb_pipeline_demo_scenarios::metaplex_token_metadata_devnet_scenarios()
.into_iter()
.find(|candidate| return candidate.id == scenario_id);
let scenario = match scenario {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!(
"unknown Metaplex Devnet scenario `{scenario_id}`"
));
},
};
let operation_code = match scenario.operation_codes.get(step_index) {
std::option::Option::Some(value) => value.clone(),
std::option::Option::None => {
return std::result::Result::Err(format!(
"scenario `{}` has no step at index {step_index}",
scenario.id
));
},
};
let operation_suffix = match operation_code.rsplit('.').next() {
std::option::Option::Some(value) => value,
std::option::Option::None => operation_code.as_str(),
};
let operation = if operation_suffix == "update" {
"update_as_update_authority_v2".to_string()
} else {
operation_suffix.to_string()
};
if operation != "create" && operation != "update_as_update_authority_v2" {
return std::result::Result::Err(format!(
"scenario step `{operation}` is declared but its Devnet fixture builder is not implemented yet"
));
}
let (fixture_mint, fixture_metadata, fixture_master_edition, operation_json, fixture_state) =
if operation == "create" {
let fixture =
match crate::demo_execution_metadata_prepare_create_fixture(state, profile_name)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
(
fixture.mint,
fixture.metadata,
fixture.master_edition,
fixture.operation_json,
if fixture.mint_created {
"mint_prepared".to_string()
} else {
"mint_reused".to_string()
},
)
} else {
let previous = match previous_operation_json {
std::option::Option::Some(value) if !value.trim().is_empty() => value,
_ => {
return std::result::Result::Err(
"prepare and submit the Create step before preparing Update".to_string(),
);
},
};
let previous_value: serde_json::Value = match serde_json::from_str(previous.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(format!(
"invalid previous scenario operation JSON: {error}"
));
},
};
let string_field = |name: &str| -> std::result::Result<
std::string::String,
std::string::String,
> {
return match previous_value.get(name).and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) => std::result::Result::Ok(value.to_string()),
std::option::Option::None => std::result::Result::Err(format!(
"previous Create step does not contain `{name}`"
)),
};
};
let mint = match string_field("mint") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let metadata = match string_field("metadata") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let master_edition = match string_field("master_edition") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let authority = match string_field("authority") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let value = serde_json::json!({
"operation": "update_as_update_authority_v2",
"authority": authority.clone(),
"mint": mint.clone(),
"metadata": metadata.clone(),
"edition": null,
"token": null,
"authorization_rules_program": null,
"authorization_rules": null,
"update_args": {
"AsUpdateAuthorityV2": {
"new_update_authority": null,
"data": null,
"primary_sale_happened": true,
"is_mutable": null,
"collection": "None",
"collection_details": "None",
"uses": "None",
"rule_set": "None",
"token_standard": null,
"authorization_data": null
}
}
});
(
mint,
metadata,
master_edition,
crate::pretty_json(&value),
"metadata_created".to_string(),
)
};
let metadata_read = serde_json::json!({
"query_role": "http_queries",
"account": fixture_metadata.clone(),
"kind": { "kind": "metadata" },
"min_context_slot": null,
"max_data_bytes": 65536
});
let edition_read = serde_json::json!({
"query_role": "http_queries",
"account": fixture_master_edition.clone(),
"kind": { "kind": "edition", "mint": fixture_mint.clone() },
"min_context_slot": null,
"max_data_bytes": 65536
});
let preflight_reads = if operation == "create" {
serde_json::json!([])
} else {
serde_json::json!([metadata_read.clone()])
};
let postcondition_reads = if operation == "create" {
serde_json::json!([metadata_read, edition_read])
} else {
serde_json::json!([metadata_read])
};
return std::result::Result::Ok(crate::DemoExecutionMetadataPreparedStepPayload {
scenario_id: scenario.id,
step_index,
operation: operation.clone(),
label: format!("Étape {} — {}", step_index + 1, operation),
operation_json,
preflight_reads_json: crate::pretty_json(&preflight_reads),
postcondition_reads_json: crate::pretty_json(&postcondition_reads),
simulation_ready: true,
submission_ready: true,
fixture_state,
});
}
/// Returns current operation names accepted by automatic Devnet campaigns.
pub(crate) fn demo_execution_metadata_current_operations() -> std::vec::Vec<std::string::String> {
return kb_pipeline_demo_scenarios::metaplex_token_metadata_current_operation_names()
.iter()
.map(|value| return (*value).to_string())
.collect();
}
/// Returns one generated typed JSON template for a named current operation.
pub(crate) fn demo_execution_metadata_operation_template(
operation: std::string::String,
) -> std::result::Result<std::string::String, std::string::String> {
return match kb_pipeline_demo_scenarios::metaplex_token_metadata_current_operation_json_template(
operation.as_str(),
) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
/// Returns the canonical Metaplex scenario inventory for the desktop panel.
@@ -68,7 +556,11 @@ pub(crate) fn demo_execution_metadata_scenarios() -> std::result::Result<
};
payloads.push(crate::DemoExecutionMetadataScenarioPayload {
id: scenario.id,
label: scenario.label,
asset_family: asset_family_code(scenario.asset_family),
fixture_kind: fixture_kind_code(scenario.fixture_kind),
initial_state: fixture_state_code(scenario.initial_state),
resulting_state: fixture_state_code(scenario.resulting_state),
mode: scenario_mode_code(scenario.mode),
operation_codes: scenario.operation_codes,
requires_collection: scenario.requires_collection,
@@ -76,6 +568,7 @@ pub(crate) fn demo_execution_metadata_scenarios() -> std::result::Result<
requires_postcondition: scenario.requires_postcondition,
validation_status,
required_evidence,
materialization_targets: scenario.materialization_targets,
});
}
return std::result::Result::Ok(payloads);
@@ -99,6 +592,18 @@ fn asset_family_code(
};
}
fn fixture_kind_code(
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataFixtureKind,
) -> std::string::String {
return format!("{value:?}").to_lowercase();
}
fn fixture_state_code(
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataFixtureState,
) -> std::string::String {
return format!("{value:?}").to_lowercase();
}
fn scenario_mode_code(
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode,
) -> std::string::String {
@@ -154,7 +659,10 @@ mod tests {
return;
},
};
assert_eq!(payloads.len(), 10);
let expected_count =
kb_pipeline_demo_scenarios::metaplex_token_metadata_synthetic_scenarios().len()
+ kb_pipeline_demo_scenarios::metaplex_token_metadata_devnet_scenarios().len();
assert_eq!(payloads.len(), expected_count);
assert!(payloads.iter().all(|payload| return !payload.operation_codes.is_empty()));
assert!(payloads.iter().all(|payload| return payload.requires_postcondition));
assert!(
@@ -167,10 +675,41 @@ mod tests {
.iter()
.filter(|payload| return payload.mode == "network_simulation")
.count(),
5
1
);
}
#[test]
fn empty_stateful_reads_are_accepted() {
let parsed = super::parse_stateful_reads(" ");
assert!(parsed.is_ok());
let values = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
assert_eq!(std::option::Option::Some(error), std::option::Option::None);
return;
},
};
assert_eq!(values.len(), 0);
}
#[test]
fn metadata_frontend_contains_every_required_runtime_control() {
let source = std::include_str!("../frontend/demo_execution_metadata.html");
for id in [
"metadataProfileSelect",
"metadataDevnetScenarioSelect",
"metadataStepSelect",
"prepareMetadataScenarioStepButton",
"metadataExecutionLogOutput",
"metadataProfileHelp",
"simulateMetadataButton",
"submitMetadataButton",
] {
assert!(source.contains(format!("id=\"{id}\"").as_str()));
}
}
#[test]
fn programmable_and_collection_requirements_remain_explicit() {
let payloads = match crate::demo_execution_metadata_scenarios() {
@@ -180,13 +719,7 @@ mod tests {
return;
},
};
assert_eq!(payloads.iter().filter(|payload| return payload.requires_collection).count(), 2);
assert_eq!(
payloads
.iter()
.filter(|payload| return payload.requires_programmable_rules)
.count(),
2
);
assert!(payloads.iter().any(|payload| return payload.requires_collection));
assert!(payloads.iter().any(|payload| return payload.requires_programmable_rules));
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/lib.rs
// version: 26
// version: 29
//! Tauri desktop demo application for `khadhroony-bot3`.
@@ -127,8 +127,28 @@ pub(crate) use self::demo_devnet_common::DemoExecutionDevnetStoreReadinessPayloa
pub(crate) use self::demo_devnet_common::devnet_store_readiness_payload;
/// Serializes a diagnostic value to indented JSON without panicking.
pub(crate) use self::demo_devnet_common::pretty_json;
/// UI-safe result of one prepared Metaplex Create fixture.
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataCreateFixturePayload;
/// Prepared coherent Metaplex scenario step.
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataPreparedStepPayload;
/// Request for one real Devnet Metaplex execution from the desktop panel.
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataRequest;
/// UI-safe Metaplex scenario row.
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataScenarioPayload;
/// UI-safe result of one real Devnet Metaplex execution.
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataSummaryPayload;
/// Returns current Metaplex operation names accepted by Devnet campaigns.
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_current_operations;
/// Executes one real Devnet Metaplex simulation or submission.
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_execute;
/// Returns one generated operation JSON template.
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_operation_template;
/// Returns Devnet profile options dedicated to the Metadata execution panel.
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_options;
/// Creates or reuses the Devnet mint fixture used by the Create scenario.
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_prepare_create_fixture;
/// Prepares one coherent scenario step and its automatic stateful reads.
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_prepare_step;
/// Returns the canonical Metaplex scenario inventory for the desktop panel.
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_scenarios;
/// Request sent by the Memo v4 Devnet execution panel.

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/tauri.rs
// version: 26
// version: 30
//! Tauri runtime assembly and private command wrappers.
@@ -30,8 +30,24 @@ pub fn run() -> kb_core::Result<()> {
let mut builder = tauri::Builder::default();
builder = builder.manage(app_state);
builder = builder.invoke_handler(tauri::generate_handler![
demo_backfill_cancel,
splash_frontend_ready,
emit_frontend_log,
load_project_readme,
open_demo_config_window,
load_demo_config,
open_demo_http_window,
demo_http_list_pool_clients,
demo_http_options,
demo_http_execute_request,
open_demo_ws_window,
demo_ws_list_pool_clients,
demo_ws_options,
demo_ws_status,
demo_ws_connect,
demo_ws_unsubscribe,
demo_ws_disconnect,
open_demo_backfill_window,
demo_backfill_cancel,
demo_backfill_options,
demo_backfill_execute,
open_demo_core_extraction_window,
@@ -44,32 +60,6 @@ pub fn run() -> kb_core::Result<()> {
demo_decode_replay_cancel,
demo_decode_replay_diagnostics,
demo_decode_replay_annotations,
open_demo_execution_solana_core_window,
demo_execution_devnet_prepare_profile,
demo_execution_solana_core_options,
demo_execution_solana_core_generate_recipient,
demo_execution_solana_core_execute,
demo_execution_spl_memo_execute,
demo_execution_solana_core_cancel,
demo_execution_spl_validation_scenarios,
open_demo_execution_spl_window,
open_demo_execution_metadata_window,
demo_execution_metadata_scenarios,
demo_execution_spl_token_execute,
demo_spl_token_journal,
demo_spl_ata_derive,
demo_execution_spl_ata_execute,
demo_spl_ata_journal,
demo_execution_spl_token_2022_execute,
demo_spl_token_2022_fixture,
open_demo_config_window,
load_demo_config,
emit_frontend_log,
load_project_readme,
open_demo_http_window,
demo_http_list_pool_clients,
demo_http_options,
demo_http_execute_request,
open_demo_sql_diag_window,
load_demo_sql_diag,
open_demo_sql_pg_raw_window,
@@ -82,14 +72,30 @@ pub fn run() -> kb_core::Result<()> {
load_demo_sql_replay_transactions,
load_demo_sql_replay_programs,
load_demo_sql_replay_entities,
open_demo_ws_window,
demo_ws_list_pool_clients,
demo_ws_options,
demo_ws_status,
demo_ws_connect,
demo_ws_unsubscribe,
demo_ws_disconnect,
splash_frontend_ready,
open_demo_execution_solana_core_window,
demo_execution_devnet_prepare_profile,
demo_execution_solana_core_options,
demo_execution_solana_core_generate_recipient,
demo_execution_solana_core_execute,
demo_execution_spl_memo_execute,
demo_execution_solana_core_cancel,
demo_execution_spl_validation_scenarios,
open_demo_execution_spl_window,
demo_execution_spl_token_execute,
demo_spl_token_journal,
demo_spl_ata_derive,
demo_execution_spl_ata_execute,
demo_spl_ata_journal,
demo_execution_spl_token_2022_execute,
demo_spl_token_2022_fixture,
open_demo_execution_metadata_window,
demo_execution_metadata_scenarios,
demo_execution_metadata_options,
demo_execution_metadata_current_operations,
demo_execution_metadata_operation_template,
demo_execution_metadata_prepare_create_fixture,
demo_execution_metadata_prepare_step,
demo_execution_metadata_execute,
]);
builder = builder.on_window_event(|window, event| {
if window.label() != "main" || !matches!(event, tauri::WindowEvent::Destroyed) {
@@ -615,6 +621,13 @@ fn open_demo_execution_metadata_window(
);
}
#[tauri::command]
fn demo_execution_metadata_options(
state: tauri::State<'_, crate::AppState>,
) -> crate::DemoExecutionSolanaCoreOptionsPayload {
return crate::demo_execution_metadata_options(state);
}
#[tauri::command]
fn demo_execution_metadata_scenarios() -> std::result::Result<
std::vec::Vec<crate::DemoExecutionMetadataScenarioPayload>,
@@ -623,6 +636,53 @@ fn demo_execution_metadata_scenarios() -> std::result::Result<
return crate::demo_execution_metadata_scenarios();
}
#[tauri::command]
fn demo_execution_metadata_current_operations() -> std::vec::Vec<std::string::String> {
return crate::demo_execution_metadata_current_operations();
}
#[tauri::command]
fn demo_execution_metadata_operation_template(
operation: std::string::String,
) -> std::result::Result<std::string::String, std::string::String> {
return crate::demo_execution_metadata_operation_template(operation);
}
#[tauri::command]
async fn demo_execution_metadata_prepare_create_fixture(
state: tauri::State<'_, crate::AppState>,
profile_name: std::string::String,
) -> std::result::Result<crate::DemoExecutionMetadataCreateFixturePayload, std::string::String> {
return crate::demo_execution_metadata_prepare_create_fixture(state, profile_name).await;
}
#[tauri::command]
async fn demo_execution_metadata_prepare_step(
state: tauri::State<'_, crate::AppState>,
profile_name: std::string::String,
scenario_id: std::string::String,
step_index: usize,
previous_operation_json: std::option::Option<std::string::String>,
) -> std::result::Result<crate::DemoExecutionMetadataPreparedStepPayload, std::string::String> {
return crate::demo_execution_metadata_prepare_step(
state,
profile_name,
scenario_id,
step_index,
previous_operation_json,
)
.await;
}
#[tauri::command]
async fn demo_execution_metadata_execute(
app_handle: tauri::AppHandle,
state: tauri::State<'_, crate::AppState>,
request: crate::DemoExecutionMetadataRequest,
) -> std::result::Result<crate::DemoExecutionMetadataSummaryPayload, std::string::String> {
return crate::demo_execution_metadata_execute(app_handle, state, request).await;
}
#[tauri::command]
async fn demo_execution_spl_token_execute(
app_handle: tauri::AppHandle,

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/vite.config.ts
// version: 7
// version: 8
import { defineConfig, normalizePath } from "vite";
import { NodePackageImporter } from "sass-embedded";
@@ -23,18 +23,19 @@ export default defineConfig(() => ({
input: {
"splash": normalizePath(resolve(__dirname, 'frontend/splash.html')),
"main": normalizePath(resolve(__dirname, 'frontend/main.html')),
"demo_backfill": normalizePath(resolve(__dirname, 'frontend/demo_backfill.html')),
"demo_config": normalizePath(resolve(__dirname, 'frontend/demo_config.html')),
"demo_http": normalizePath(resolve(__dirname, 'frontend/demo_http.html')),
"demo_ws": normalizePath(resolve(__dirname, 'frontend/demo_ws.html')),
"demo_backfill": normalizePath(resolve(__dirname, 'frontend/demo_backfill.html')),
"demo_core_extraction": normalizePath(resolve(__dirname, 'frontend/demo_core_extraction.html')),
"demo_decode_replay": normalizePath(resolve(__dirname, 'frontend/demo_decode_replay.html')),
"demo_sql_diag": normalizePath(resolve(__dirname, 'frontend/demo_sql_diag.html')),
"demo_sql_pg_raw": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_raw.html')),
"demo_sql_pg_core": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_core.html')),
"demo_sql_replay_candidates": normalizePath(resolve(__dirname, 'frontend/demo_sql_replay_candidates.html')),
"demo_config": normalizePath(resolve(__dirname, 'frontend/demo_config.html')),
"demo_core_extraction": normalizePath(resolve(__dirname, 'frontend/demo_core_extraction.html')),
"demo_decode_replay": normalizePath(resolve(__dirname, 'frontend/demo_decode_replay.html')),
"demo_execution_solana_core": normalizePath(resolve(__dirname, 'frontend/demo_execution_solana_core.html')),
"demo_execution_spl": normalizePath(resolve(__dirname, 'frontend/demo_execution_spl.html')),
"demo_execution_metatdata": normalizePath(resolve(__dirname, 'frontend/demo_execution_metatdata.html')),
},
output: {
entryFileNames: 'js/[name]-[hash].js',