v0.4.7-pre.013
This commit is contained in:
@@ -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 l’inventaire ;
|
||||
- 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 l’intent et les lectures stateful avant/après ;
|
||||
- ajoute la navigation étape précédente/suivante et refuse l’exécution d’un 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 l’option 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 d’un intent précédent lorsqu’aucun modèle nommé n’existe.
|
||||
|
||||
## 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 l’initialisation du panneau Metadata : ajout des contrôles HTML du journal attendus par le TypeScript ;
|
||||
- ajout d’un 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 d’exécution et de titres explicites pour les résultats JSON Metaplex.
|
||||
- Ajout d’un 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 l’extraction 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
|
||||
|
||||
@@ -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 d’utilisation](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`.
|
||||
|
||||
@@ -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 qu’ils 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 qu’ils 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
|
||||
|
||||
@@ -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 l’envoi 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 `<...>` d’un 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 n’est pas, à lui seul, une exécution réseau. Les champs `networkExecutionPerformed` et `networkEvidenceCollected` doivent rester faux tant qu’aucun appel RPC réel et aucune preuve correspondante n’ont é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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 d’exé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 d’exé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> : l’intent, les lectures stateful et les postconditions sont générés ensemble.</li>
|
||||
<li>Lancer d’abord 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 n’exé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 l’intent, 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 l’opérateur ne l’autorise 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 d’exé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>
|
||||
|
||||
@@ -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 l’exé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 l’opé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 n’a é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();
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user