v0.4.8-pre.010
This commit is contained in:
@@ -1,8 +1,23 @@
|
||||
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
|
||||
<!-- version: 27 -->
|
||||
<!-- version: 29 -->
|
||||
|
||||
# CHANGELOG — kb-app-demo-desktop
|
||||
|
||||
## 0.4.8-pre.010
|
||||
|
||||
- scinde le panneau Metadata en un module commun, un adaptateur `metadata_metaplex_token_metadata` et un adaptateur `metadata_solana_program` ;
|
||||
- renomme les contrats et commandes Tauri Metaplex afin de rendre leur domaine explicite ;
|
||||
- remplace la réutilisation erronée de l’observer Solana Core par un observer Metadata ciblant exclusivement `demo_execution_metadata` ;
|
||||
- déplace le garde de l’exécution Devnet partagée dans `demo_devnet_common` sous le nom générique `DemoExecutionRunGuard` ;
|
||||
- consomme la borne RPC complète de `65536` octets exposée par `kb-onchain-transport` pour les lectures Solana Program Metadata ;
|
||||
- ajoute un adaptateur desktop séparé pour les deux parcours Solana Program Metadata ;
|
||||
- expose l’inventaire exact des neuf opérations et leurs états attendus ;
|
||||
- synchronise le profil Devnet entre les sections Metaplex et Solana Program Metadata ;
|
||||
- appelle directement la campagne réutilisable de `kb-pipeline-demo-scenarios` sans dupliquer la logique réseau ;
|
||||
- affiche la fixture, les deux signatures de préfinancement, les neuf simulations, confirmations, postconditions et snapshots matérialisés ;
|
||||
- exige une confirmation explicite unique couvrant les deux préfinancements et les neuf mutations ;
|
||||
- conserve les parcours Metaplex existants inchangés.
|
||||
|
||||
## 0.4.8-pre.008
|
||||
|
||||
- enregistre `metadata.solana_program_metadata` dans le replay générique afin que les signatures hydratées par `demo_backfill` puissent être décodées ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/README.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# kb-app-demo-desktop
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- diagnostics PostgreSQL ;
|
||||
- backfill, extraction Core et replay, avec enregistrement générique de Solana Program Metadata ;
|
||||
- démonstrations Solana Core, SPL Memo, SPL Token, ATA et Token-2022 ;
|
||||
- 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 ;
|
||||
- panneau Metadata avec parcours séparés Metaplex Token Metadata et Solana Program Metadata, préparation du profil et de la base, simulation RPC réelle, soumission explicitement confirmée et affichage des postconditions ;
|
||||
- progression, annulation et restauration de l’état des fenêtres.
|
||||
|
||||
## Frontière architecturale
|
||||
@@ -25,6 +25,10 @@ Les scénarios UI Devnet/Testnet restent dans le desktop. Des tests automatisés
|
||||
|
||||
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.
|
||||
|
||||
## Exécution Solana Program Metadata
|
||||
|
||||
Le même panneau expose séparément les deux parcours `Buffer` et `Metadata` de `ProgM6…`. La campagne complète crée deux PDA frais, confirme leurs préfinancements, exécute les neuf opérations stables, vérifie les lectures avant/après et affiche les snapshots matérialisés. La logique réseau reste entièrement dans `kb-pipeline-demo-scenarios` ; le desktop ne fait que sélectionner le profil, demander la confirmation et présenter les preuves.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Guide d’utilisation](USAGE.md)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/TODO.md -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 10 -->
|
||||
|
||||
# TODO — kb-app-demo-desktop
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
- [ ] UX — empêcher les doubles déclenchements pendant une préparation, simulation ou soumission active.
|
||||
- [ ] Observabilité — afficher explicitement les comptes relus et les projections matérialisées.
|
||||
|
||||
## Validation Devnet Solana Program Metadata
|
||||
|
||||
- [ ] Exécuter la campagne desktop complète sur Devnet et archiver les signatures, confirmations, postconditions et snapshots dans la preuve `0.4.8`.
|
||||
- [ ] Renseigner la matrice réseau uniquement après validation des neuf étapes.
|
||||
|
||||
## Validation Devnet complémentaire Metaplex Token Metadata
|
||||
|
||||
- [ ] Ajouter les parcours spécialisés Print/Burn, collection avancée et pNFT uniquement lorsque leurs fixtures réutilisables sont disponibles.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/USAGE.md -->
|
||||
<!-- version: 6 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Utilisation de kb-app-demo-desktop
|
||||
|
||||
@@ -207,16 +207,18 @@ La simulation et l’envoi doivent rester explicitement distingués dans les req
|
||||
|
||||
### 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`.
|
||||
Le backend distingue le module commun Metadata, Metaplex Token Metadata et Solana Program Metadata.
|
||||
|
||||
Le panneau Metadata charge les opérations courantes depuis `demo_execution_metadata_metaplex_token_metadata_current_operations` et peut demander un modèle avec `demo_execution_metadata_metaplex_token_metadata_operation_template`.
|
||||
|
||||
```typescript
|
||||
const operationJson = await invoke(
|
||||
"demo_execution_metadata_operation_template",
|
||||
"demo_execution_metadata_metaplex_token_metadata_operation_template",
|
||||
{ operation: "verify" }
|
||||
);
|
||||
|
||||
const summary = await invoke(
|
||||
"demo_execution_metadata_execute",
|
||||
"demo_execution_metadata_metaplex_token_metadata_execute",
|
||||
{
|
||||
request: {
|
||||
profileName: "devnet",
|
||||
@@ -303,3 +305,23 @@ Le panneau distingue préparation de l’étape, simulation exacte et soumission
|
||||
### 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.
|
||||
|
||||
### Campagne Solana Program Metadata
|
||||
|
||||
Ouvrir l’accordéon **Solana Program Metadata**, sélectionner un profil Devnet persistant et préparer sa base. L’inventaire affiche deux parcours totalisant neuf opérations :
|
||||
|
||||
```text
|
||||
Buffer : Allocate → Extend → Write → SetAuthority → Trim → Close
|
||||
Metadata : Initialize → SetData → SetImmutable
|
||||
```
|
||||
|
||||
Après confirmation explicite, le bouton **Exécuter la campagne complète** appelle le runner réutilisable. Celui-ci réalise deux transferts de préfinancement, puis les neuf simulations et soumissions dans l’ordre. Le panneau affiche :
|
||||
|
||||
- la fixture et les opérations préparées ;
|
||||
- les signatures de préfinancement et les PDA frais ;
|
||||
- les neuf simulations et confirmations ;
|
||||
- les lectures stateful avant/après ;
|
||||
- les rapports de postcondition ;
|
||||
- les snapshots matérialisés, sauf pour `Close` qui exige l’absence du compte.
|
||||
|
||||
La campagne s’arrête au premier échec. Elle ne reprend pas un parcours partiellement exécuté et ne réutilise pas les PDA d’une campagne précédente.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/frontend/demo_execution_metadata.html -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 10 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -23,20 +23,27 @@
|
||||
<main class="app-main">
|
||||
<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="alert alert-info">Cette fenêtre sépare les familles metadata. Metaplex Token Metadata conserve ses scénarios propres ; Solana Program Metadata exécute les neuf opérations stables de <code>ProgM6…</code> via deux parcours Devnet. Token-2022 Token Metadata sera ajouté dans une étape ultérieure.</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>
|
||||
<h2 class="accordion-header"><button class="accordion-button collapsed" data-bs-target="#metadataGuideCollapse" data-bs-toggle="collapse" type="button">Guide d’exécution Metadata Devnet</button></h2>
|
||||
<div class="accordion-collapse collapse" id="metadataGuideCollapse" data-bs-parent="#metadataGuideAccordion">
|
||||
<div class="accordion-body">
|
||||
<ol class="mb-2">
|
||||
<h3 class="h6">Metaplex Token Metadata</h3>
|
||||
<ol class="mb-3">
|
||||
<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>
|
||||
<li>Choisir le parcours Devnet puis son étape.</li>
|
||||
<li>Préparer l’étape afin de générer ensemble l’intent, les lectures stateful et les postconditions.</li>
|
||||
<li>Lancer d’abord une simulation. La soumission exige une confirmation explicite.</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>
|
||||
<h3 class="h6">Solana Program Metadata</h3>
|
||||
<ol class="mb-2">
|
||||
<li>Choisir et préparer un profil Devnet persistant, autorisé à soumettre.</li>
|
||||
<li>Contrôler les deux parcours et leurs neuf étapes dans l’inventaire.</li>
|
||||
<li>Confirmer la campagne complète : deux transferts de préfinancement puis neuf transactions <code>ProgM6…</code>.</li>
|
||||
<li>Contrôler les simulations, signatures, confirmations, lectures avant/après, postconditions et snapshots matérialisés.</li>
|
||||
</ol>
|
||||
<div class="small text-muted">La campagne Solana Program Metadata est indivisible : elle crée des PDA frais et arrête immédiatement le parcours au premier échec ou à la première preuve manquante.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,7 +87,7 @@
|
||||
<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>
|
||||
<div class="form-text">Le scénario remplit ensemble l’intent, les lectures avant/après et les cibles de matérialisation.</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>
|
||||
@@ -106,11 +113,36 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid mt-3">
|
||||
<button class="btn btn-primary" id="refreshMetadataScenariosButton" type="button">Recharger les contrats</button>
|
||||
<button class="btn btn-primary" id="refreshMetadataScenariosButton" type="button">Recharger les contrats Metaplex</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" data-bs-target="#solanaProgramMetadataCollapse" data-bs-toggle="collapse" type="button">Solana Program Metadata</button>
|
||||
</h2>
|
||||
<div class="accordion-collapse collapse" id="solanaProgramMetadataCollapse" data-bs-parent="#metadataExecutionAccordion">
|
||||
<div class="accordion-body">
|
||||
<label class="form-label" for="solanaProgramMetadataProfileSelect">Profil Devnet</label>
|
||||
<select class="form-select" id="solanaProgramMetadataProfileSelect"></select>
|
||||
<div class="form-text mb-3" id="solanaProgramMetadataProfileHelp">Chargement du profil Devnet.</div>
|
||||
<div class="d-grid"><button class="btn btn-outline-primary" id="prepareSolanaProgramMetadataProfileButton" 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="solanaProgramMetadataProfileStatusBadge">Non chargé</span></div>
|
||||
<div class="mt-3" id="solanaProgramMetadataProfileOutput"></div>
|
||||
<label class="form-label mt-3" for="solanaProgramMetadataScenarioSelect">Parcours</label>
|
||||
<select class="form-select" id="solanaProgramMetadataScenarioSelect"></select>
|
||||
<div class="mt-3" id="solanaProgramMetadataScenarioOutput"></div>
|
||||
<div class="alert alert-warning mt-3">Cette campagne soumet onze transactions : deux transferts de préfinancement et les neuf opérations stables. Elle utilise des PDA frais et ne peut pas reprendre au milieu d’un parcours.</div>
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" id="solanaProgramMetadataOperatorConfirmed" type="checkbox">
|
||||
<label class="form-check-label" for="solanaProgramMetadataOperatorConfirmed">Je confirme le préfinancement et les neuf mutations Devnet</label>
|
||||
</div>
|
||||
<div class="d-grid mt-3"><button class="btn btn-danger" id="executeSolanaProgramMetadataCampaignButton" type="button">Exécuter la campagne complète</button></div>
|
||||
<div class="form-text">Le runner arrête la campagne dès qu’une simulation, une confirmation, une postcondition ou une matérialisation requise échoue.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-body">
|
||||
@@ -152,4 +184,4 @@
|
||||
<script type="module" src="ts/demo_execution_metadata.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
|
||||
// version: 10
|
||||
// version: 12
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
@@ -7,18 +7,24 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts";
|
||||
import { renderJsonViewer } from "./json_viewer.ts";
|
||||
import type { DemoExecutionMetadataScenarioPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataScenarioPayload.ts";
|
||||
import type { DemoExecutionMetadataPreparedStepPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataPreparedStepPayload.ts";
|
||||
import type { DemoExecutionMetadataRequest } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataRequest.ts";
|
||||
import type { DemoExecutionMetadataSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSummaryPayload.ts";
|
||||
import type { DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload.ts";
|
||||
import type { DemoExecutionMetadataProgressPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataProgressPayload.ts";
|
||||
import type { DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload.ts";
|
||||
import type { DemoExecutionMetadataMetaplexTokenMetadataRequest } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataRequest.ts";
|
||||
import type { DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload.ts";
|
||||
import type { DemoExecutionMetadataSolanaProgramCampaignRequest } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSolanaProgramCampaignRequest.ts";
|
||||
import type { DemoExecutionMetadataSolanaProgramCampaignSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSolanaProgramCampaignSummaryPayload.ts";
|
||||
import type { DemoExecutionMetadataSolanaProgramScenarioPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSolanaProgramScenarioPayload.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 solanaProgramMetadataStorageKey = "kb-app-demo-desktop.metadata.solana-program.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 scenarios: DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload[] = [];
|
||||
let solanaProgramMetadataScenarios: DemoExecutionMetadataSolanaProgramScenarioPayload[] = [];
|
||||
let selectedProfileName: string | null = null;
|
||||
let executionRunning = false;
|
||||
let profileStoreReady = false;
|
||||
@@ -28,9 +34,9 @@ let preparedStepIndex: number | null = null;
|
||||
const metadataLogLines: string[] = [];
|
||||
const maximumMetadataLogLines = 1200;
|
||||
|
||||
type MetadataProgressPayload = { timestamp: string; level: string; stage: string; message: string; signature?: string | null };
|
||||
type MetadataProgressInput = Omit<DemoExecutionMetadataProgressPayload, "signature"> & { signature?: string | null };
|
||||
|
||||
function appendMetadataLog(payload: MetadataProgressPayload): void {
|
||||
function appendMetadataLog(payload: MetadataProgressInput): void {
|
||||
const signature = payload.signature ? ` signature=${payload.signature}` : "";
|
||||
metadataLogLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`);
|
||||
if (metadataLogLines.length > maximumMetadataLogLines) {
|
||||
@@ -51,11 +57,34 @@ function element<T extends HTMLElement>(selector: string): T {
|
||||
return value;
|
||||
}
|
||||
|
||||
const profileSelectSelectors = ["#metadataProfileSelect", "#solanaProgramMetadataProfileSelect"] as const;
|
||||
const profileHelpSelectors = ["#metadataProfileHelp", "#solanaProgramMetadataProfileHelp"] as const;
|
||||
const profileStatusSelectors = ["#metadataProfileStatusBadge", "#solanaProgramMetadataProfileStatusBadge"] as const;
|
||||
const profileOutputSelectors = ["#metadataProfileOutput", "#solanaProgramMetadataProfileOutput"] as const;
|
||||
|
||||
function synchronizeProfileSelections(profileName: string): void {
|
||||
for (const selector of profileSelectSelectors) {
|
||||
const select = element<HTMLSelectElement>(selector);
|
||||
if (Array.from(select.options).some(option => option.value === profileName)) {
|
||||
select.value = profileName;
|
||||
}
|
||||
}
|
||||
selectedProfileName = profileName.length === 0 ? null : profileName;
|
||||
}
|
||||
|
||||
function updateProfileStatus(text: string, className: string): void {
|
||||
for (const selector of profileStatusSelectors) {
|
||||
const badge = element<HTMLElement>(selector);
|
||||
badge.textContent = text;
|
||||
badge.className = className;
|
||||
}
|
||||
}
|
||||
|
||||
function activeSelect(): HTMLSelectElement {
|
||||
return document.querySelector("#metaplexDevnetCollapse.show") !== null ? element<HTMLSelectElement>("#metadataDevnetScenarioSelect") : element<HTMLSelectElement>("#metadataSyntheticScenarioSelect");
|
||||
}
|
||||
|
||||
function selectedScenario(): DemoExecutionMetadataScenarioPayload | null {
|
||||
function selectedScenario(): DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload | null {
|
||||
const id = activeSelect().value;
|
||||
return scenarios.find(scenario => scenario.id === id) ?? null;
|
||||
}
|
||||
@@ -76,7 +105,7 @@ function operationName(operationCode: string): string {
|
||||
return parts.length === 0 ? operationCode : parts[parts.length - 1] ?? operationCode;
|
||||
}
|
||||
|
||||
function renderScenarioSteps(scenario: DemoExecutionMetadataScenarioPayload): void {
|
||||
function renderScenarioSteps(scenario: DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload): void {
|
||||
const select = element<HTMLSelectElement>("#metadataStepSelect");
|
||||
const previousValue = Number.parseInt(select.value, 10);
|
||||
select.replaceChildren();
|
||||
@@ -145,7 +174,7 @@ async function prepareScenarioStep(): Promise<void> {
|
||||
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", {
|
||||
const prepared = await invoke<DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload>("demo_execution_metadata_metaplex_token_metadata_prepare_step", {
|
||||
profileName,
|
||||
scenarioId: scenario.id,
|
||||
stepIndex,
|
||||
@@ -193,18 +222,20 @@ function parseJsonText(value: string, label: string): unknown {
|
||||
}
|
||||
|
||||
function selectedProfile(): DemoExecutionSolanaCoreProfileOption | null {
|
||||
const name = element<HTMLSelectElement>("#metadataProfileSelect").value;
|
||||
return profileOptions.find(profile => profile.name === name) ?? null;
|
||||
if (selectedProfileName === null) {
|
||||
return null;
|
||||
}
|
||||
return profileOptions.find(profile => profile.name === selectedProfileName) ?? null;
|
||||
}
|
||||
|
||||
function updateProfileHelp(): void {
|
||||
const profile = selectedProfile();
|
||||
const help = element<HTMLElement>("#metadataProfileHelp");
|
||||
if (profile === null) {
|
||||
help.textContent = "Aucun profil Devnet compatible.";
|
||||
return;
|
||||
const text = profile === null
|
||||
? "Aucun profil Devnet compatible."
|
||||
: `wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled ? "enabled" : "disabled"}`;
|
||||
for (const selector of profileHelpSelectors) {
|
||||
element<HTMLElement>(selector).textContent = text;
|
||||
}
|
||||
help.textContent = `wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled ? "enabled" : "disabled"}`;
|
||||
}
|
||||
|
||||
function setExecutionRunning(value: boolean): void {
|
||||
@@ -215,6 +246,8 @@ function setExecutionRunning(value: boolean): void {
|
||||
element<HTMLButtonElement>("#simulateMetadataButton").disabled = unavailable;
|
||||
element<HTMLButtonElement>("#submitMetadataButton").disabled = unavailable || !profile?.sendEnabled;
|
||||
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").disabled = value || profile === null || !profileStoreReady;
|
||||
const solanaProgramConfirmed = element<HTMLInputElement>("#solanaProgramMetadataOperatorConfirmed").checked;
|
||||
element<HTMLButtonElement>("#executeSolanaProgramMetadataCampaignButton").disabled = value || profile === null || !profileStoreReady || !profile.sendEnabled || !solanaProgramConfirmed;
|
||||
}
|
||||
|
||||
async function executeMetadata(submit: boolean): Promise<void> {
|
||||
@@ -243,7 +276,7 @@ async function executeMetadata(submit: boolean): Promise<void> {
|
||||
if (submit && !operatorConfirmed) {
|
||||
throw new Error("La soumission exige la confirmation explicite de l’opérateur.");
|
||||
}
|
||||
const request: DemoExecutionMetadataRequest = {
|
||||
const request: DemoExecutionMetadataMetaplexTokenMetadataRequest = {
|
||||
profileName,
|
||||
intentId: `desktop-metaplex-${Date.now()}`,
|
||||
operationJson,
|
||||
@@ -259,7 +292,7 @@ async function executeMetadata(submit: boolean): Promise<void> {
|
||||
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 });
|
||||
const summary = await invoke<DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload>("demo_execution_metadata_metaplex_token_metadata_execute", { request });
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", JSON.parse(summary.preflightJson)));
|
||||
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
|
||||
profileName: summary.profileName,
|
||||
@@ -283,65 +316,160 @@ async function executeMetadata(submit: boolean): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function selectedSolanaProgramMetadataScenario(): DemoExecutionMetadataSolanaProgramScenarioPayload | null {
|
||||
const id = element<HTMLSelectElement>("#solanaProgramMetadataScenarioSelect").value;
|
||||
return solanaProgramMetadataScenarios.find(scenario => scenario.id === id) ?? null;
|
||||
}
|
||||
|
||||
function renderActiveMetadataSelection(): void {
|
||||
if (document.querySelector("#solanaProgramMetadataCollapse.show") !== null) {
|
||||
renderSolanaProgramMetadataScenario();
|
||||
return;
|
||||
}
|
||||
renderSelectedScenario();
|
||||
}
|
||||
|
||||
function renderSolanaProgramMetadataScenario(): void {
|
||||
const scenario = selectedSolanaProgramMetadataScenario();
|
||||
if (scenario === null) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(solanaProgramMetadataStorageKey, scenario.id);
|
||||
renderJsonViewer("#solanaProgramMetadataScenarioOutput", titled("Parcours Solana Program Metadata", {
|
||||
scenarioId: scenario.id,
|
||||
label: scenario.label,
|
||||
fixtureAccount: scenario.fixtureAccount,
|
||||
steps: scenario.steps,
|
||||
}));
|
||||
const allSteps = solanaProgramMetadataScenarios.flatMap(candidate => candidate.steps);
|
||||
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Famille</dt><dd class="col-6"><code>metadata.solana_program_metadata</code></dd><dt class="col-6">Program ID</dt><dd class="col-6"><code>ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S</code></dd><dt class="col-6">Parcours affiché</dt><dd class="col-6">${scenario.label}</dd><dt class="col-6">Étapes totales</dt><dd class="col-6">${allSteps.length}</dd><dt class="col-6">Transactions</dt><dd class="col-6">11 minimum</dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${selectedProfileName ?? "non chargé"}</code></dd>`;
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", {
|
||||
family: "metadata.solana_program_metadata",
|
||||
journeys: solanaProgramMetadataScenarios,
|
||||
operationCount: allSteps.length,
|
||||
fixtureAccounts: ["buffer", "metadata"],
|
||||
prefundingTransactionCount: 2,
|
||||
}));
|
||||
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
|
||||
policy: "simulation_first",
|
||||
submission: "required",
|
||||
operatorConfirmationRequired: true,
|
||||
stopOnFirstFailure: true,
|
||||
networkExecutionPerformed: false,
|
||||
}));
|
||||
renderJsonViewer("#metadataPostconditionsOutput", titled("Postconditions et preuves", {
|
||||
requiredEvidence: ["stateful_preflight", "rpc_simulation", "confirmed_signature", "stateful_postcondition", "materialized_snapshot_or_account_absence"],
|
||||
currentStatus: "not_run",
|
||||
networkEvidenceCollected: false,
|
||||
}));
|
||||
}
|
||||
|
||||
async function executeSolanaProgramMetadataCampaign(): Promise<void> {
|
||||
if (executionRunning) {
|
||||
return;
|
||||
}
|
||||
const profile = selectedProfile();
|
||||
if (profile === null || selectedProfileName === null || !profileStoreReady) {
|
||||
throw new Error("Sélectionnez et préparez un profil Devnet.");
|
||||
}
|
||||
if (!profile.sendEnabled) {
|
||||
throw new Error("Le profil sélectionné n’autorise pas les soumissions Devnet.");
|
||||
}
|
||||
const operatorConfirmed = element<HTMLInputElement>("#solanaProgramMetadataOperatorConfirmed").checked;
|
||||
if (!operatorConfirmed) {
|
||||
throw new Error("La campagne exige la confirmation explicite du préfinancement et des neuf mutations.");
|
||||
}
|
||||
const request: DemoExecutionMetadataSolanaProgramCampaignRequest = {
|
||||
profileName: selectedProfileName,
|
||||
operatorConfirmed,
|
||||
};
|
||||
setExecutionRunning(true);
|
||||
const badge = element<HTMLElement>("#metadataStatusBadge");
|
||||
badge.textContent = "Campagne ProgM6";
|
||||
badge.className = "badge text-bg-warning";
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "spm_campaign", message: "Démarrage de la campagne complète : 2 préfinancements et 9 opérations." });
|
||||
try {
|
||||
const summary = await invoke<DemoExecutionMetadataSolanaProgramCampaignSummaryPayload>("demo_execution_metadata_solana_program_execute_campaign", { request });
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Fixture et opérations préparées", JSON.parse(summary.fixtureJson)));
|
||||
renderJsonViewer("#metadataSimulationOutput", titled("Synthèse de campagne", {
|
||||
profileName: summary.profileName,
|
||||
authority: summary.authority,
|
||||
describedProgram: summary.describedProgram,
|
||||
buffer: summary.buffer,
|
||||
bufferPrefundSignature: summary.bufferPrefundSignature,
|
||||
metadata: summary.metadata,
|
||||
metadataPrefundSignature: summary.metadataPrefundSignature,
|
||||
stepCount: summary.stepCount,
|
||||
confirmedStepCount: summary.confirmedStepCount,
|
||||
materializedSnapshotCount: summary.materializedSnapshotCount,
|
||||
completed: summary.completed,
|
||||
}));
|
||||
renderJsonViewer("#metadataPostconditionsOutput", titled("Preuves ordonnées des neuf opérations", JSON.parse(summary.stepsJson)));
|
||||
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Famille</dt><dd class="col-6"><code>metadata.solana_program_metadata</code></dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${summary.profileName}</code></dd><dt class="col-6">Buffer</dt><dd class="col-6"><code>${summary.buffer}</code></dd><dt class="col-6">Metadata</dt><dd class="col-6"><code>${summary.metadata}</code></dd><dt class="col-6">Confirmées</dt><dd class="col-6">${summary.confirmedStepCount}/${summary.stepCount}</dd><dt class="col-6">Snapshots</dt><dd class="col-6">${summary.materializedSnapshotCount}</dd>`;
|
||||
badge.textContent = summary.completed ? "Campagne confirmée" : "Campagne incomplète";
|
||||
badge.className = summary.completed ? "badge text-bg-success" : "badge text-bg-danger";
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: summary.completed ? "info" : "error", stage: "spm_campaign", message: `Campagne terminée : confirmed=${summary.confirmedStepCount}/${summary.stepCount}, snapshots=${summary.materializedSnapshotCount}` });
|
||||
} finally {
|
||||
setExecutionRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProfiles(): Promise<void> {
|
||||
const options = await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_metadata_options");
|
||||
profileOptions = options.profiles;
|
||||
const select = element<HTMLSelectElement>("#metadataProfileSelect");
|
||||
select.replaceChildren();
|
||||
for (const profile of options.profiles) {
|
||||
const option = document.createElement("option");
|
||||
option.value = profile.name;
|
||||
option.textContent = `${profile.name} — ${profile.walletAlias}`;
|
||||
select.append(option);
|
||||
for (const selector of profileSelectSelectors) {
|
||||
const select = element<HTMLSelectElement>(selector);
|
||||
select.replaceChildren();
|
||||
for (const profile of options.profiles) {
|
||||
const option = document.createElement("option");
|
||||
option.value = profile.name;
|
||||
option.textContent = `${profile.name} — ${profile.walletAlias}`;
|
||||
select.append(option);
|
||||
}
|
||||
}
|
||||
const restored = window.localStorage.getItem(profileStorageKey);
|
||||
if (restored !== null && Array.from(select.options).some(option => option.value === restored)) {
|
||||
select.value = restored;
|
||||
} else if (options.defaultProfileName !== null) {
|
||||
select.value = options.defaultProfileName;
|
||||
}
|
||||
selectedProfileName = select.value.length === 0 ? null : select.value;
|
||||
const availableNames = options.profiles.map(profile => profile.name);
|
||||
const selected = restored !== null && availableNames.includes(restored)
|
||||
? restored
|
||||
: options.defaultProfileName ?? availableNames[0] ?? "";
|
||||
synchronizeProfileSelections(selected);
|
||||
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();
|
||||
updateProfileStatus("Aucun profil", "badge text-bg-danger");
|
||||
renderActiveMetadataSelection();
|
||||
return;
|
||||
}
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "profile_options", message: `${options.profiles.length} profil(s) Devnet chargé(s).` });
|
||||
renderSelectedScenario();
|
||||
renderActiveMetadataSelection();
|
||||
await prepareProfile();
|
||||
}
|
||||
|
||||
async function prepareProfile(): Promise<void> {
|
||||
const badge = element<HTMLElement>("#metadataProfileStatusBadge");
|
||||
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
|
||||
const profileName = selectedProfileName ?? "";
|
||||
if (profileName.length === 0) {
|
||||
badge.textContent = "Aucun profil";
|
||||
badge.className = "badge text-bg-danger";
|
||||
updateProfileStatus("Aucun profil", "badge text-bg-danger");
|
||||
return;
|
||||
}
|
||||
profileStoreReady = false;
|
||||
setExecutionRunning(executionRunning);
|
||||
badge.textContent = "Préparation";
|
||||
badge.className = "badge text-bg-warning";
|
||||
updateProfileStatus("Préparation", "badge text-bg-warning");
|
||||
try {
|
||||
const readiness = await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile", { profileName });
|
||||
selectedProfileName = profileName;
|
||||
synchronizeProfileSelections(profileName);
|
||||
window.localStorage.setItem(profileStorageKey, profileName);
|
||||
renderJsonViewer("#metadataProfileOutput", readiness);
|
||||
for (const selector of profileOutputSelectors) {
|
||||
renderJsonViewer(selector, readiness);
|
||||
}
|
||||
profileStoreReady = true;
|
||||
badge.textContent = "Base prête";
|
||||
badge.className = "badge text-bg-success";
|
||||
updateProfileStatus("Base prête", "badge text-bg-success");
|
||||
setExecutionRunning(executionRunning);
|
||||
renderSelectedScenario();
|
||||
renderActiveMetadataSelection();
|
||||
} catch (caught) {
|
||||
const message = caught instanceof Error ? caught.message : String(caught);
|
||||
profileStoreReady = false;
|
||||
badge.textContent = "Erreur";
|
||||
badge.className = "badge text-bg-danger";
|
||||
updateProfileStatus("Erreur", "badge text-bg-danger");
|
||||
setExecutionRunning(executionRunning);
|
||||
frontendError("kb-app-demo-desktop.frontend.execution_metadata", message);
|
||||
}
|
||||
@@ -352,7 +480,8 @@ async function loadScenarios(): Promise<void> {
|
||||
badge.textContent = "Chargement";
|
||||
badge.className = "badge text-bg-warning";
|
||||
try {
|
||||
scenarios = await invoke<DemoExecutionMetadataScenarioPayload[]>("demo_execution_metadata_scenarios");
|
||||
scenarios = await invoke<DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload[]>("demo_execution_metadata_metaplex_token_metadata_scenarios");
|
||||
solanaProgramMetadataScenarios = await invoke<DemoExecutionMetadataSolanaProgramScenarioPayload[]>("demo_execution_metadata_solana_program_scenarios");
|
||||
fillSelect("#metadataSyntheticScenarioSelect", "synthetic");
|
||||
fillSelect("#metadataDevnetScenarioSelect", "network_simulation");
|
||||
const restored = window.localStorage.getItem(storageKey);
|
||||
@@ -362,8 +491,20 @@ async function loadScenarios(): Promise<void> {
|
||||
select.value = restored;
|
||||
}
|
||||
}
|
||||
renderSelectedScenario();
|
||||
badge.textContent = `${scenarios.length} contrats`;
|
||||
const solanaSelect = element<HTMLSelectElement>("#solanaProgramMetadataScenarioSelect");
|
||||
solanaSelect.replaceChildren();
|
||||
for (const scenario of solanaProgramMetadataScenarios) {
|
||||
const option = document.createElement("option");
|
||||
option.value = scenario.id;
|
||||
option.textContent = `${scenario.fixtureAccount} — ${scenario.label}`;
|
||||
solanaSelect.append(option);
|
||||
}
|
||||
const restoredSolana = window.localStorage.getItem(solanaProgramMetadataStorageKey);
|
||||
if (restoredSolana !== null && Array.from(solanaSelect.options).some(option => option.value === restoredSolana)) {
|
||||
solanaSelect.value = restoredSolana;
|
||||
}
|
||||
renderActiveMetadataSelection();
|
||||
badge.textContent = `${scenarios.length + solanaProgramMetadataScenarios.length} contrats`;
|
||||
badge.className = "badge text-bg-success";
|
||||
} catch (caught) {
|
||||
const message = caught instanceof Error ? caught.message : String(caught);
|
||||
@@ -383,16 +524,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
for (const id of ["metaplexSyntheticCollapse", "metaplexDevnetCollapse"]) {
|
||||
element<HTMLElement>(`#${id}`).addEventListener("shown.bs.collapse", renderSelectedScenario);
|
||||
}
|
||||
element<HTMLSelectElement>("#metadataProfileSelect").addEventListener("change", event => {
|
||||
const target = event.currentTarget as HTMLSelectElement;
|
||||
selectedProfileName = target.value.length === 0 ? null : target.value;
|
||||
profileStoreReady = false;
|
||||
updateProfileHelp();
|
||||
setExecutionRunning(executionRunning);
|
||||
renderSelectedScenario();
|
||||
void prepareProfile();
|
||||
});
|
||||
element<HTMLElement>("#solanaProgramMetadataCollapse").addEventListener("shown.bs.collapse", renderSolanaProgramMetadataScenario);
|
||||
element<HTMLSelectElement>("#solanaProgramMetadataScenarioSelect").addEventListener("change", renderSolanaProgramMetadataScenario);
|
||||
for (const selector of profileSelectSelectors) {
|
||||
element<HTMLSelectElement>(selector).addEventListener("change", event => {
|
||||
const target = event.currentTarget as HTMLSelectElement;
|
||||
synchronizeProfileSelections(target.value);
|
||||
profileStoreReady = false;
|
||||
updateProfileHelp();
|
||||
setExecutionRunning(executionRunning);
|
||||
renderActiveMetadataSelection();
|
||||
void prepareProfile();
|
||||
});
|
||||
}
|
||||
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => void prepareProfile());
|
||||
element<HTMLButtonElement>("#prepareSolanaProgramMetadataProfileButton").addEventListener("click", () => void prepareProfile());
|
||||
element<HTMLButtonElement>("#refreshMetadataScenariosButton").addEventListener("click", () => void loadScenarios());
|
||||
element<HTMLSelectElement>("#metadataStepSelect").addEventListener("change", () => {
|
||||
const scenario = selectedScenario();
|
||||
@@ -407,13 +553,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
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));
|
||||
void listen<DemoExecutionMetadataProgressPayload>("demo-execution-metadata-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)));
|
||||
});
|
||||
element<HTMLInputElement>("#solanaProgramMetadataOperatorConfirmed").addEventListener("change", () => setExecutionRunning(executionRunning));
|
||||
element<HTMLButtonElement>("#executeSolanaProgramMetadataCampaignButton").addEventListener("click", () => {
|
||||
void executeSolanaProgramMetadataCampaign().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)));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/demo_devnet_common.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Shared Devnet demo UI contracts.
|
||||
|
||||
@@ -41,6 +41,18 @@ pub(crate) fn devnet_store_readiness_payload(
|
||||
};
|
||||
}
|
||||
|
||||
/// Releases the shared Devnet execution slot when one workflow terminates.
|
||||
pub(crate) struct DemoExecutionRunGuard<'a> {
|
||||
/// Shared running flag reset on drop.
|
||||
pub(crate) running: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl std::ops::Drop for crate::DemoExecutionRunGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes a diagnostic value to indented JSON without panicking.
|
||||
pub(crate) fn pretty_json<T>(value: &T) -> std::string::String
|
||||
where
|
||||
|
||||
@@ -1,840 +1,158 @@
|
||||
// file: kb-app-demo-desktop/src/demo_execution_metadata.rs
|
||||
// version: 14
|
||||
// version: 15
|
||||
|
||||
//! Thin desktop adapters for metadata execution demo scenarios.
|
||||
//! Shared desktop contracts for metadata execution demos.
|
||||
|
||||
/// UI-safe Metaplex scenario row.
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
|
||||
/// UI-safe progress event shared by Metadata execution workflows.
|
||||
#[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/DemoExecutionMetadataScenarioPayload.ts"
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataProgressPayload.ts"
|
||||
)]
|
||||
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.
|
||||
pub operation_codes: std::vec::Vec<std::string::String>,
|
||||
/// Whether a collection account is required.
|
||||
pub requires_collection: bool,
|
||||
/// Whether programmable authorization rules are required.
|
||||
pub requires_programmable_rules: bool,
|
||||
/// Whether postconditions are mandatory.
|
||||
pub requires_postcondition: bool,
|
||||
/// Current validation status from the canonical matrix.
|
||||
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>,
|
||||
pub(crate) struct DemoExecutionMetadataProgressPayload {
|
||||
/// RFC3339 event timestamp.
|
||||
pub timestamp: std::string::String,
|
||||
/// Stable lowercase log level.
|
||||
pub level: std::string::String,
|
||||
/// Stable execution stage.
|
||||
pub stage: std::string::String,
|
||||
/// Operator-visible message.
|
||||
pub message: std::string::String,
|
||||
/// Optional transaction signature.
|
||||
pub signature: std::option::Option<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,
|
||||
/// Metadata-window observer shared by Metaplex and Solana Program Metadata workflows.
|
||||
pub(crate) struct DemoExecutionMetadataObserver<'a> {
|
||||
/// Tauri application handle used for targeted progress events.
|
||||
pub(crate) app_handle: tauri::AppHandle,
|
||||
/// Shared cooperative cancellation flag.
|
||||
pub(crate) cancel_requested: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
impl crate::DemoExecutionMetadataObserver<'_> {
|
||||
fn emit(
|
||||
&self,
|
||||
timestamp: std::string::String,
|
||||
level: &str,
|
||||
stage: &str,
|
||||
message: std::string::String,
|
||||
signature: std::option::Option<std::string::String>,
|
||||
) {
|
||||
let payload = crate::DemoExecutionMetadataProgressPayload {
|
||||
timestamp,
|
||||
level: level.to_string(),
|
||||
stage: stage.to_string(),
|
||||
message,
|
||||
signature,
|
||||
};
|
||||
let emit_result = self.app_handle.emit_to(
|
||||
"demo_execution_metadata",
|
||||
"demo-execution-metadata-progress",
|
||||
payload,
|
||||
);
|
||||
if let std::result::Result::Err(error) = emit_result {
|
||||
tracing::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "emit_metadata_execution_progress",
|
||||
error = %error,
|
||||
"cannot emit Metadata execution progress"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancelled(&self) -> bool {
|
||||
return self.cancel_requested.load(std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
impl kb_pipeline::BackfillObserver for crate::DemoExecutionMetadataObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::BackfillProgressEvent) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
"canonical_insert",
|
||||
event.message.clone(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
impl kb_pipeline::CoreExtractionObserver for crate::DemoExecutionMetadataObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::CoreExtractionProgressEvent) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
"core_extraction",
|
||||
event.message.clone(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Devnet profile options dedicated to the Metadata execution panel.
|
||||
impl kb_pipeline::DecodeReplayObserver for crate::DemoExecutionMetadataObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::DecodeReplayProgressEvent) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
"decode_replay",
|
||||
event.message.clone(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_pipeline_demo_scenarios::SolanaExecutionObserver
|
||||
for crate::DemoExecutionMetadataObserver<'_>
|
||||
{
|
||||
fn on_execution_progress(
|
||||
&self,
|
||||
event: &kb_pipeline_demo_scenarios::SolanaExecutionProgressEvent,
|
||||
) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
event.stage.as_str(),
|
||||
event.message.clone(),
|
||||
event.signature.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
fn is_execution_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Devnet profile options shared by 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,
|
||||
});
|
||||
}
|
||||
|
||||
fn adapt_create_operation_json(
|
||||
operation_json: &str,
|
||||
asset_family: kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily,
|
||||
) -> std::result::Result<std::string::String, std::string::String> {
|
||||
let mut value: serde_json::Value = match serde_json::from_str(operation_json) {
|
||||
std::result::Result::Ok(parsed) => parsed,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"invalid generated Metaplex Create JSON: {error}"
|
||||
));
|
||||
},
|
||||
};
|
||||
if matches!(
|
||||
asset_family,
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft
|
||||
| kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible
|
||||
) {
|
||||
value["master_edition"] = serde_json::Value::Null;
|
||||
}
|
||||
let create = match value
|
||||
.get_mut("create_args")
|
||||
.and_then(|args| return args.get_mut("V1"))
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
{
|
||||
std::option::Option::Some(found) => found,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"generated Metaplex Create JSON has no create_args.V1 object".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
match asset_family {
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft => {
|
||||
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet NFT"));
|
||||
create.insert("symbol".to_string(), serde_json::json!("KHNFT"));
|
||||
create.insert("token_standard".to_string(), serde_json::json!("NonFungible"));
|
||||
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
||||
create.insert("decimals".to_string(), serde_json::Value::Null);
|
||||
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft => {
|
||||
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet SFT"));
|
||||
create.insert("symbol".to_string(), serde_json::json!("KHSFT"));
|
||||
create.insert("token_standard".to_string(), serde_json::json!("FungibleAsset"));
|
||||
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
||||
create.insert("decimals".to_string(), serde_json::json!(0));
|
||||
create.insert("print_supply".to_string(), serde_json::Value::Null);
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible => {
|
||||
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet Fungible"));
|
||||
create.insert("symbol".to_string(), serde_json::json!("KHFUN"));
|
||||
create.insert("token_standard".to_string(), serde_json::json!("Fungible"));
|
||||
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
||||
create.insert("decimals".to_string(), serde_json::json!(0));
|
||||
create.insert("print_supply".to_string(), serde_json::Value::Null);
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection => {
|
||||
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet Collection"));
|
||||
create.insert("symbol".to_string(), serde_json::json!("KHCOL"));
|
||||
create.insert("token_standard".to_string(), serde_json::json!("NonFungible"));
|
||||
create.insert("collection_details".to_string(), serde_json::json!({"V1": {"size": 0}}));
|
||||
create.insert("decimals".to_string(), serde_json::Value::Null);
|
||||
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft => {
|
||||
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet pNFT"));
|
||||
create.insert("symbol".to_string(), serde_json::json!("KHPNFT"));
|
||||
create
|
||||
.insert("token_standard".to_string(), serde_json::json!("ProgrammableNonFungible"));
|
||||
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
||||
create.insert("rule_set".to_string(), serde_json::Value::Null);
|
||||
create.insert("decimals".to_string(), serde_json::Value::Null);
|
||||
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
|
||||
},
|
||||
}
|
||||
return std::result::Result::Ok(crate::pretty_json(&value));
|
||||
}
|
||||
|
||||
/// 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),
|
||||
};
|
||||
let operation_json = match adapt_create_operation_json(
|
||||
fixture.operation_json.as_str(),
|
||||
scenario.asset_family,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
(
|
||||
fixture.mint,
|
||||
fixture.metadata,
|
||||
fixture.master_edition,
|
||||
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 previous NFT lifecycle step before continuing"
|
||||
.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 scenario 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 authority = match string_field("authority") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let master_edition =
|
||||
match previous_value.get("master_edition").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) => value.to_string(),
|
||||
std::option::Option::None => std::string::String::new(),
|
||||
};
|
||||
let immutable_transition = scenario.asset_family
|
||||
== kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft
|
||||
&& step_index == 2;
|
||||
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": if immutable_transition {
|
||||
serde_json::Value::Null
|
||||
} else {
|
||||
serde_json::json!(true)
|
||||
},
|
||||
"is_mutable": if immutable_transition {
|
||||
serde_json::json!(false)
|
||||
} else {
|
||||
serde_json::Value::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),
|
||||
if immutable_transition {
|
||||
"metadata_immutable".to_string()
|
||||
} else {
|
||||
"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 create_reads_edition = matches!(
|
||||
scenario.asset_family,
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft
|
||||
| kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection
|
||||
| kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft
|
||||
);
|
||||
let postcondition_reads = if operation == "create" && create_reads_edition {
|
||||
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: if scenario.asset_family
|
||||
== kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft
|
||||
&& step_index == 2
|
||||
{
|
||||
"Étape 3 — rendre les metadata immutables".to_string()
|
||||
} else {
|
||||
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.
|
||||
pub(crate) fn demo_execution_metadata_scenarios() -> std::result::Result<
|
||||
std::vec::Vec<crate::DemoExecutionMetadataScenarioPayload>,
|
||||
std::string::String,
|
||||
> {
|
||||
let matrix = match kb_pipeline_demo_scenarios::load_metaplex_token_metadata_validation_matrix()
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let statuses = matrix
|
||||
.scenarios
|
||||
.into_iter()
|
||||
.map(|scenario| return (scenario.id, (scenario.status, scenario.required_evidence)))
|
||||
.collect::<std::collections::BTreeMap<
|
||||
std::string::String,
|
||||
(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus,
|
||||
std::vec::Vec<std::string::String>,
|
||||
),
|
||||
>>();
|
||||
let mut payloads = std::vec::Vec::new();
|
||||
for scenario in kb_pipeline_demo_scenarios::metaplex_token_metadata_synthetic_scenarios()
|
||||
.into_iter()
|
||||
.chain(kb_pipeline_demo_scenarios::metaplex_token_metadata_devnet_scenarios())
|
||||
{
|
||||
let status = statuses.get(scenario.id.as_str());
|
||||
let validation_status = match status {
|
||||
std::option::Option::Some((value, _)) => validation_status_code(*value),
|
||||
std::option::Option::None => "not_run".to_string(),
|
||||
};
|
||||
let required_evidence = match status {
|
||||
std::option::Option::Some((_, evidence)) => evidence.clone(),
|
||||
std::option::Option::None => std::vec::Vec::new(),
|
||||
};
|
||||
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,
|
||||
requires_programmable_rules: scenario.requires_programmable_rules,
|
||||
requires_postcondition: scenario.requires_postcondition,
|
||||
validation_status,
|
||||
required_evidence,
|
||||
materialization_targets: scenario.materialization_targets,
|
||||
});
|
||||
}
|
||||
return std::result::Result::Ok(payloads);
|
||||
}
|
||||
|
||||
fn asset_family_code(
|
||||
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily,
|
||||
) -> std::string::String {
|
||||
return match value {
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft => "nft".to_string(),
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft => "sft".to_string(),
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible => {
|
||||
"fungible".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection => {
|
||||
"collection".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft => {
|
||||
"programmable_nft".to_string()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
return match value {
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode::Synthetic => {
|
||||
"synthetic".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode::NetworkSimulation => {
|
||||
"network_simulation".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode::NetworkSubmission => {
|
||||
"network_submission".to_string()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn validation_status_code(
|
||||
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus,
|
||||
) -> std::string::String {
|
||||
return match value {
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::NotRun => {
|
||||
"not_run".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::SyntheticValidated => {
|
||||
"synthetic_validated".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Simulated => {
|
||||
"simulated".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Submitted => {
|
||||
"submitted".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Confirmed => {
|
||||
"confirmed".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Unavailable => {
|
||||
"unavailable".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Failed => {
|
||||
"failed".to_string()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn desktop_inventory_matches_reusable_scenarios_and_matrix() {
|
||||
let payloads = match crate::demo_execution_metadata_scenarios() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
assert_eq!(std::option::Option::Some(error), std::option::Option::None);
|
||||
return;
|
||||
},
|
||||
};
|
||||
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!(
|
||||
payloads
|
||||
.iter()
|
||||
.all(|payload| return payload.validation_status != "missing_from_matrix")
|
||||
);
|
||||
assert_eq!(
|
||||
payloads
|
||||
.iter()
|
||||
.filter(|payload| return payload.mode == "network_simulation")
|
||||
.count(),
|
||||
5
|
||||
);
|
||||
}
|
||||
|
||||
#[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() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
assert_eq!(std::option::Option::Some(error), std::option::Option::None);
|
||||
return;
|
||||
},
|
||||
};
|
||||
assert!(payloads.iter().any(|payload| return payload.requires_collection));
|
||||
assert!(payloads.iter().any(|payload| return payload.requires_programmable_rules));
|
||||
fn metadata_frontend_listens_to_the_metadata_specific_progress_event() {
|
||||
let source = std::include_str!("../frontend/ts/demo_execution_metadata.ts");
|
||||
assert!(source.contains("demo-execution-metadata-progress"));
|
||||
assert!(!source.contains(
|
||||
"listen<DemoExecutionMetadataProgressPayload>(\"demo-execution-solana-core-progress\""
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,852 @@
|
||||
// file: kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs
|
||||
// version: 1
|
||||
|
||||
//! Desktop adapters for Metaplex Token Metadata execution demo scenarios.
|
||||
|
||||
/// UI-safe Metaplex scenario row.
|
||||
#[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/DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload {
|
||||
/// 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.
|
||||
pub operation_codes: std::vec::Vec<std::string::String>,
|
||||
/// Whether a collection account is required.
|
||||
pub requires_collection: bool,
|
||||
/// Whether programmable authorization rules are required.
|
||||
pub requires_programmable_rules: bool,
|
||||
/// Whether postconditions are mandatory.
|
||||
pub requires_postcondition: bool,
|
||||
/// Current validation status from the canonical matrix.
|
||||
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/DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload {
|
||||
/// 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/DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload {
|
||||
/// 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/DemoExecutionMetadataMetaplexTokenMetadataRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataMetaplexTokenMetadataRequest {
|
||||
/// 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/DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Executes one real Devnet Metaplex simulation or explicitly confirmed submission.
|
||||
pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataMetaplexTokenMetadataRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload,
|
||||
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::DemoExecutionRunGuard {
|
||||
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::DemoExecutionMetadataObserver {
|
||||
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::DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload {
|
||||
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_metaplex_token_metadata_prepare_create_fixture(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
profile_name: std::string::String,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload,
|
||||
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::DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload {
|
||||
mint: summary.mint,
|
||||
metadata: summary.metadata,
|
||||
master_edition: summary.master_edition,
|
||||
authority: summary.authority,
|
||||
operation_json: summary.operation_json,
|
||||
mint_created: summary.mint_created,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn adapt_create_operation_json(
|
||||
operation_json: &str,
|
||||
asset_family: kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily,
|
||||
) -> std::result::Result<std::string::String, std::string::String> {
|
||||
let mut value: serde_json::Value = match serde_json::from_str(operation_json) {
|
||||
std::result::Result::Ok(parsed) => parsed,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"invalid generated Metaplex Create JSON: {error}"
|
||||
));
|
||||
},
|
||||
};
|
||||
if matches!(
|
||||
asset_family,
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft
|
||||
| kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible
|
||||
) {
|
||||
value["master_edition"] = serde_json::Value::Null;
|
||||
}
|
||||
let create = match value
|
||||
.get_mut("create_args")
|
||||
.and_then(|args| return args.get_mut("V1"))
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
{
|
||||
std::option::Option::Some(found) => found,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"generated Metaplex Create JSON has no create_args.V1 object".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
match asset_family {
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft => {
|
||||
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet NFT"));
|
||||
create.insert("symbol".to_string(), serde_json::json!("KHNFT"));
|
||||
create.insert("token_standard".to_string(), serde_json::json!("NonFungible"));
|
||||
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
||||
create.insert("decimals".to_string(), serde_json::Value::Null);
|
||||
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft => {
|
||||
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet SFT"));
|
||||
create.insert("symbol".to_string(), serde_json::json!("KHSFT"));
|
||||
create.insert("token_standard".to_string(), serde_json::json!("FungibleAsset"));
|
||||
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
||||
create.insert("decimals".to_string(), serde_json::json!(0));
|
||||
create.insert("print_supply".to_string(), serde_json::Value::Null);
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible => {
|
||||
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet Fungible"));
|
||||
create.insert("symbol".to_string(), serde_json::json!("KHFUN"));
|
||||
create.insert("token_standard".to_string(), serde_json::json!("Fungible"));
|
||||
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
||||
create.insert("decimals".to_string(), serde_json::json!(0));
|
||||
create.insert("print_supply".to_string(), serde_json::Value::Null);
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection => {
|
||||
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet Collection"));
|
||||
create.insert("symbol".to_string(), serde_json::json!("KHCOL"));
|
||||
create.insert("token_standard".to_string(), serde_json::json!("NonFungible"));
|
||||
create.insert("collection_details".to_string(), serde_json::json!({"V1": {"size": 0}}));
|
||||
create.insert("decimals".to_string(), serde_json::Value::Null);
|
||||
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft => {
|
||||
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet pNFT"));
|
||||
create.insert("symbol".to_string(), serde_json::json!("KHPNFT"));
|
||||
create
|
||||
.insert("token_standard".to_string(), serde_json::json!("ProgrammableNonFungible"));
|
||||
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
||||
create.insert("rule_set".to_string(), serde_json::Value::Null);
|
||||
create.insert("decimals".to_string(), serde_json::Value::Null);
|
||||
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
|
||||
},
|
||||
}
|
||||
return std::result::Result::Ok(crate::pretty_json(&value));
|
||||
}
|
||||
|
||||
/// Prepares one coherent scenario step and all of its stateful reads.
|
||||
pub(crate) async fn demo_execution_metadata_metaplex_token_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::DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload,
|
||||
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_metaplex_token_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),
|
||||
};
|
||||
let operation_json = match adapt_create_operation_json(
|
||||
fixture.operation_json.as_str(),
|
||||
scenario.asset_family,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
(
|
||||
fixture.mint,
|
||||
fixture.metadata,
|
||||
fixture.master_edition,
|
||||
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 previous NFT lifecycle step before continuing"
|
||||
.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 scenario 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 authority = match string_field("authority") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let master_edition =
|
||||
match previous_value.get("master_edition").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) => value.to_string(),
|
||||
std::option::Option::None => std::string::String::new(),
|
||||
};
|
||||
let immutable_transition = scenario.asset_family
|
||||
== kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft
|
||||
&& step_index == 2;
|
||||
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": if immutable_transition {
|
||||
serde_json::Value::Null
|
||||
} else {
|
||||
serde_json::json!(true)
|
||||
},
|
||||
"is_mutable": if immutable_transition {
|
||||
serde_json::json!(false)
|
||||
} else {
|
||||
serde_json::Value::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),
|
||||
if immutable_transition {
|
||||
"metadata_immutable".to_string()
|
||||
} else {
|
||||
"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 create_reads_edition = matches!(
|
||||
scenario.asset_family,
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft
|
||||
| kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection
|
||||
| kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft
|
||||
);
|
||||
let postcondition_reads = if operation == "create" && create_reads_edition {
|
||||
serde_json::json!([metadata_read, edition_read])
|
||||
} else {
|
||||
serde_json::json!([metadata_read])
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
crate::DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload {
|
||||
scenario_id: scenario.id,
|
||||
step_index,
|
||||
operation: operation.clone(),
|
||||
label: if scenario.asset_family
|
||||
== kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft
|
||||
&& step_index == 2
|
||||
{
|
||||
"Étape 3 — rendre les metadata immutables".to_string()
|
||||
} else {
|
||||
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_metaplex_token_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_metaplex_token_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.
|
||||
pub(crate) fn demo_execution_metadata_metaplex_token_metadata_scenarios() -> std::result::Result<
|
||||
std::vec::Vec<crate::DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload>,
|
||||
std::string::String,
|
||||
> {
|
||||
let matrix = match kb_pipeline_demo_scenarios::load_metaplex_token_metadata_validation_matrix()
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let statuses = matrix
|
||||
.scenarios
|
||||
.into_iter()
|
||||
.map(|scenario| return (scenario.id, (scenario.status, scenario.required_evidence)))
|
||||
.collect::<std::collections::BTreeMap<
|
||||
std::string::String,
|
||||
(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus,
|
||||
std::vec::Vec<std::string::String>,
|
||||
),
|
||||
>>();
|
||||
let mut payloads = std::vec::Vec::new();
|
||||
for scenario in kb_pipeline_demo_scenarios::metaplex_token_metadata_synthetic_scenarios()
|
||||
.into_iter()
|
||||
.chain(kb_pipeline_demo_scenarios::metaplex_token_metadata_devnet_scenarios())
|
||||
{
|
||||
let status = statuses.get(scenario.id.as_str());
|
||||
let validation_status = match status {
|
||||
std::option::Option::Some((value, _)) => validation_status_code(*value),
|
||||
std::option::Option::None => "not_run".to_string(),
|
||||
};
|
||||
let required_evidence = match status {
|
||||
std::option::Option::Some((_, evidence)) => evidence.clone(),
|
||||
std::option::Option::None => std::vec::Vec::new(),
|
||||
};
|
||||
payloads.push(crate::DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload {
|
||||
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,
|
||||
requires_programmable_rules: scenario.requires_programmable_rules,
|
||||
requires_postcondition: scenario.requires_postcondition,
|
||||
validation_status,
|
||||
required_evidence,
|
||||
materialization_targets: scenario.materialization_targets,
|
||||
});
|
||||
}
|
||||
return std::result::Result::Ok(payloads);
|
||||
}
|
||||
|
||||
fn asset_family_code(
|
||||
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily,
|
||||
) -> std::string::String {
|
||||
return match value {
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft => "nft".to_string(),
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft => "sft".to_string(),
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible => {
|
||||
"fungible".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection => {
|
||||
"collection".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft => {
|
||||
"programmable_nft".to_string()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
return match value {
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode::Synthetic => {
|
||||
"synthetic".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode::NetworkSimulation => {
|
||||
"network_simulation".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode::NetworkSubmission => {
|
||||
"network_submission".to_string()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn validation_status_code(
|
||||
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus,
|
||||
) -> std::string::String {
|
||||
return match value {
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::NotRun => {
|
||||
"not_run".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::SyntheticValidated => {
|
||||
"synthetic_validated".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Simulated => {
|
||||
"simulated".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Submitted => {
|
||||
"submitted".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Confirmed => {
|
||||
"confirmed".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Unavailable => {
|
||||
"unavailable".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Failed => {
|
||||
"failed".to_string()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn desktop_inventory_matches_reusable_scenarios_and_matrix() {
|
||||
let payloads = match crate::demo_execution_metadata_metaplex_token_metadata_scenarios() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
assert_eq!(std::option::Option::Some(error), std::option::Option::None);
|
||||
return;
|
||||
},
|
||||
};
|
||||
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!(
|
||||
payloads
|
||||
.iter()
|
||||
.all(|payload| return payload.validation_status != "missing_from_matrix")
|
||||
);
|
||||
assert_eq!(
|
||||
payloads
|
||||
.iter()
|
||||
.filter(|payload| return payload.mode == "network_simulation")
|
||||
.count(),
|
||||
5
|
||||
);
|
||||
}
|
||||
|
||||
#[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_metaplex_token_metadata_scenarios() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
assert_eq!(std::option::Option::Some(error), std::option::Option::None);
|
||||
return;
|
||||
},
|
||||
};
|
||||
assert!(payloads.iter().any(|payload| return payload.requires_collection));
|
||||
assert!(payloads.iter().any(|payload| return payload.requires_programmable_rules));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
// file: kb-app-demo-desktop/src/demo_execution_metadata_solana_program.rs
|
||||
// version: 2
|
||||
|
||||
//! Thin desktop adapter for complete Solana Program Metadata Devnet campaigns.
|
||||
|
||||
/// UI-safe step in one Solana Program Metadata Devnet journey.
|
||||
#[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/DemoExecutionMetadataSolanaProgramScenarioStepPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataSolanaProgramScenarioStepPayload {
|
||||
/// Stable scenario-step identifier.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible label.
|
||||
pub label: std::string::String,
|
||||
/// Stable executor operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Expected state before execution.
|
||||
pub initial_state: std::string::String,
|
||||
/// Expected state after confirmed execution.
|
||||
pub resulting_state: std::string::String,
|
||||
/// Whether explicit approval is required.
|
||||
pub requires_explicit_approval: bool,
|
||||
/// Evidence kinds required by the validation contract.
|
||||
pub required_evidence: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// UI-safe Solana Program Metadata Devnet journey.
|
||||
#[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/DemoExecutionMetadataSolanaProgramScenarioPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataSolanaProgramScenarioPayload {
|
||||
/// Stable scenario identifier.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible label.
|
||||
pub label: std::string::String,
|
||||
/// Fixture account selected by the journey.
|
||||
pub fixture_account: std::string::String,
|
||||
/// Ordered steps.
|
||||
pub steps: std::vec::Vec<crate::DemoExecutionMetadataSolanaProgramScenarioStepPayload>,
|
||||
}
|
||||
|
||||
/// Request for one complete confirmed Solana Program Metadata Devnet campaign.
|
||||
#[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/DemoExecutionMetadataSolanaProgramCampaignRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataSolanaProgramCampaignRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub profile_name: std::string::String,
|
||||
/// Explicit operator confirmation for prefunding and all nine mutations.
|
||||
pub operator_confirmed: bool,
|
||||
}
|
||||
|
||||
/// UI-safe result of one complete Solana Program Metadata Devnet campaign.
|
||||
#[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/DemoExecutionMetadataSolanaProgramCampaignSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataSolanaProgramCampaignSummaryPayload {
|
||||
/// Profile used by the campaign.
|
||||
pub profile_name: std::string::String,
|
||||
/// Profile-wallet authority.
|
||||
pub authority: std::string::String,
|
||||
/// Executable program described by both non-canonical PDAs.
|
||||
pub described_program: std::string::String,
|
||||
/// Fresh Buffer PDA.
|
||||
pub buffer: std::string::String,
|
||||
/// Confirmed Buffer prefunding signature.
|
||||
pub buffer_prefund_signature: std::string::String,
|
||||
/// Fresh Metadata PDA.
|
||||
pub metadata: std::string::String,
|
||||
/// Confirmed Metadata prefunding signature.
|
||||
pub metadata_prefund_signature: std::string::String,
|
||||
/// Total number of stable operations executed.
|
||||
pub step_count: usize,
|
||||
/// Number of operations reaching confirmed or finalized state.
|
||||
pub confirmed_step_count: usize,
|
||||
/// Total number of canonical account snapshots materialized.
|
||||
pub materialized_snapshot_count: usize,
|
||||
/// Whether all nine steps completed with required evidence.
|
||||
pub completed: bool,
|
||||
/// Complete fixture as formatted JSON.
|
||||
pub fixture_json: std::string::String,
|
||||
/// Ordered execution evidence as formatted JSON.
|
||||
pub steps_json: std::string::String,
|
||||
}
|
||||
|
||||
/// Returns the two canonical Solana Program Metadata Devnet journeys.
|
||||
pub(crate) fn demo_execution_metadata_solana_program_scenarios()
|
||||
-> std::vec::Vec<crate::DemoExecutionMetadataSolanaProgramScenarioPayload> {
|
||||
return kb_pipeline_demo_scenarios::solana_program_metadata_devnet_scenarios()
|
||||
.into_iter()
|
||||
.map(|scenario| {
|
||||
return crate::DemoExecutionMetadataSolanaProgramScenarioPayload {
|
||||
id: scenario.id,
|
||||
label: scenario.label,
|
||||
fixture_account: scenario.fixture_account,
|
||||
steps: scenario
|
||||
.steps
|
||||
.into_iter()
|
||||
.map(|step| {
|
||||
return crate::DemoExecutionMetadataSolanaProgramScenarioStepPayload {
|
||||
id: step.id,
|
||||
label: step.label,
|
||||
operation_code: step.operation_code,
|
||||
initial_state: fixture_state_code(step.initial_state),
|
||||
resulting_state: fixture_state_code(step.resulting_state),
|
||||
requires_explicit_approval: step.requires_explicit_approval,
|
||||
required_evidence: step.required_evidence,
|
||||
};
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Executes the complete eleven-transaction Solana Program Metadata Devnet campaign.
|
||||
pub(crate) async fn demo_execution_metadata_solana_program_execute_campaign(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataSolanaProgramCampaignRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataSolanaProgramCampaignSummaryPayload,
|
||||
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::DemoExecutionRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
.demo_execution_solana_core_cancel_requested()
|
||||
.store(false, std::sync::atomic::Ordering::Release);
|
||||
if !request.operator_confirmed {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata campaign requires explicit operator confirmation".to_string(),
|
||||
);
|
||||
}
|
||||
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 options =
|
||||
kb_pipeline_demo_scenarios::SolanaProgramMetadataFixturePreparationOptions::new();
|
||||
options.operator_confirmed = true;
|
||||
let observer = crate::DemoExecutionMetadataObserver {
|
||||
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_solana_program_metadata_campaign(
|
||||
&http_pool,
|
||||
&profile,
|
||||
workspace_root.as_path(),
|
||||
&options,
|
||||
&observer,
|
||||
)
|
||||
.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(campaign_summary_payload(summary));
|
||||
}
|
||||
|
||||
fn campaign_summary_payload(
|
||||
summary: kb_pipeline_demo_scenarios::DevnetSolanaProgramMetadataCampaignSummary,
|
||||
) -> crate::DemoExecutionMetadataSolanaProgramCampaignSummaryPayload {
|
||||
let mut confirmed_step_count = 0_usize;
|
||||
let mut materialized_snapshot_count = 0_usize;
|
||||
let mut step_values = std::vec::Vec::with_capacity(summary.steps.len());
|
||||
for step in &summary.steps {
|
||||
let confirmation_status = step
|
||||
.execution
|
||||
.confirmation
|
||||
.as_ref()
|
||||
.map(|value| return format!("{:?}", value.status).to_lowercase());
|
||||
let confirmed = match step.execution.confirmation.as_ref() {
|
||||
std::option::Option::Some(value) => matches!(
|
||||
value.status,
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||
),
|
||||
std::option::Option::None => false,
|
||||
};
|
||||
if confirmed {
|
||||
confirmed_step_count = confirmed_step_count.saturating_add(1);
|
||||
}
|
||||
materialized_snapshot_count =
|
||||
materialized_snapshot_count.saturating_add(step.execution.materialized_snapshots.len());
|
||||
let operation_code = summary
|
||||
.fixture
|
||||
.prepared_steps
|
||||
.iter()
|
||||
.find(|prepared| return prepared.step_id == step.step_id)
|
||||
.map(|prepared| return prepared.operation.operation_code().to_string());
|
||||
step_values.push(serde_json::json!({
|
||||
"scenarioId": &step.scenario_id,
|
||||
"stepId": &step.step_id,
|
||||
"stepIndex": step.step_index,
|
||||
"operationCode": operation_code,
|
||||
"simulationSuccess": step.execution.simulation.success,
|
||||
"transactionSignature": step.execution.send_result.as_ref().map(|value| return value.signature.0.clone()),
|
||||
"confirmationStatus": confirmation_status,
|
||||
"before": &step.execution.before,
|
||||
"statefulPreflight": &step.execution.stateful_preflight,
|
||||
"plan": &step.execution.plan,
|
||||
"simulation": &step.execution.simulation,
|
||||
"readiness": &step.execution.readiness,
|
||||
"after": &step.execution.after,
|
||||
"postExecution": &step.execution.post_execution,
|
||||
"materializedSnapshots": &step.execution.materialized_snapshots
|
||||
}));
|
||||
}
|
||||
let expected_step_count = kb_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES.len();
|
||||
let completed = summary.steps.len() == expected_step_count
|
||||
&& confirmed_step_count == expected_step_count
|
||||
&& summary.steps.iter().all(|step| {
|
||||
let close_step = match summary
|
||||
.fixture
|
||||
.prepared_steps
|
||||
.iter()
|
||||
.find(|prepared| return prepared.step_id == step.step_id)
|
||||
{
|
||||
std::option::Option::Some(prepared) => {
|
||||
prepared.operation.operation_code() == kb_lib::EX_METADATA_SPM_CLOSE_OPERATION
|
||||
},
|
||||
std::option::Option::None => false,
|
||||
};
|
||||
return step.execution.post_execution.is_some()
|
||||
&& (!step.execution.materialized_snapshots.is_empty() || close_step);
|
||||
});
|
||||
let fixture_json = crate::pretty_json(&serde_json::json!({
|
||||
"profileName": &summary.fixture.profile_name,
|
||||
"authority": &summary.fixture.authority,
|
||||
"describedProgram": &summary.fixture.described_program,
|
||||
"bufferSeed": &summary.fixture.buffer_seed,
|
||||
"buffer": &summary.fixture.buffer,
|
||||
"bufferPrefundLamports": summary.fixture.buffer_prefund_lamports.to_string(),
|
||||
"bufferPrefundSignature": &summary.fixture.buffer_prefund_signature,
|
||||
"metadataSeed": &summary.fixture.metadata_seed,
|
||||
"metadata": &summary.fixture.metadata,
|
||||
"metadataPrefundLamports": summary.fixture.metadata_prefund_lamports.to_string(),
|
||||
"metadataPrefundSignature": &summary.fixture.metadata_prefund_signature,
|
||||
"preparedSteps": &summary.fixture.prepared_steps
|
||||
}));
|
||||
return crate::DemoExecutionMetadataSolanaProgramCampaignSummaryPayload {
|
||||
profile_name: summary.fixture.profile_name.clone(),
|
||||
authority: summary.fixture.authority.clone(),
|
||||
described_program: summary.fixture.described_program.clone(),
|
||||
buffer: summary.fixture.buffer.clone(),
|
||||
buffer_prefund_signature: summary.fixture.buffer_prefund_signature.clone(),
|
||||
metadata: summary.fixture.metadata.clone(),
|
||||
metadata_prefund_signature: summary.fixture.metadata_prefund_signature.clone(),
|
||||
step_count: summary.steps.len(),
|
||||
confirmed_step_count,
|
||||
materialized_snapshot_count,
|
||||
completed,
|
||||
fixture_json,
|
||||
steps_json: crate::pretty_json(&step_values),
|
||||
};
|
||||
}
|
||||
|
||||
fn fixture_state_code(
|
||||
value: kb_pipeline_demo_scenarios::SolanaProgramMetadataFixtureState,
|
||||
) -> std::string::String {
|
||||
return match value {
|
||||
kb_pipeline_demo_scenarios::SolanaProgramMetadataFixtureState::Prefunded => {
|
||||
"prefunded".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::SolanaProgramMetadataFixtureState::Buffer => {
|
||||
"buffer".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::SolanaProgramMetadataFixtureState::MutableMetadata => {
|
||||
"mutable_metadata".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::SolanaProgramMetadataFixtureState::ImmutableMetadata => {
|
||||
"immutable_metadata".to_string()
|
||||
},
|
||||
kb_pipeline_demo_scenarios::SolanaProgramMetadataFixtureState::Closed => {
|
||||
"closed".to_string()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn desktop_inventory_matches_the_two_reusable_journeys() {
|
||||
let payloads = crate::demo_execution_metadata_solana_program_scenarios();
|
||||
assert_eq!(payloads.len(), 2);
|
||||
assert_eq!(
|
||||
payloads.iter().map(|scenario| return scenario.steps.len()).sum::<usize>(),
|
||||
kb_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES.len()
|
||||
);
|
||||
assert!(payloads.iter().flat_map(|scenario| return scenario.steps.iter()).all(|step| {
|
||||
return !step.required_evidence.is_empty()
|
||||
&& kb_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES
|
||||
.contains(&step.operation_code.as_str());
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_frontend_contains_solana_program_campaign_controls() {
|
||||
let source = std::include_str!("../frontend/demo_execution_metadata.html");
|
||||
for id in [
|
||||
"solanaProgramMetadataScenarioSelect",
|
||||
"solanaProgramMetadataScenarioOutput",
|
||||
"solanaProgramMetadataOperatorConfirmed",
|
||||
"executeSolanaProgramMetadataCampaignButton",
|
||||
] {
|
||||
assert!(source.contains(format!("id=\"{id}\"").as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/demo_execution_solana_core.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
//! Tauri adapter for bounded Solana Core execution on Devnet.
|
||||
|
||||
@@ -359,15 +359,6 @@ impl kb_pipeline_demo_scenarios::SolanaExecutionObserver
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DemoExecutionSolanaCoreRunGuard<'a> {
|
||||
pub(crate) running: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
impl std::ops::Drop for crate::DemoExecutionSolanaCoreRunGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn select_devnet_profile(
|
||||
config: &kb_config::AppConfig,
|
||||
profile_name: &str,
|
||||
@@ -467,7 +458,7 @@ pub(crate) async fn demo_execution_solana_core_execute(
|
||||
if acquire_result.is_err() {
|
||||
return std::result::Result::Err("a Solana Core execution is already running".to_string());
|
||||
}
|
||||
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
|
||||
let _run_guard = crate::DemoExecutionRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
@@ -552,7 +543,7 @@ pub(crate) async fn demo_execution_spl_memo_execute(
|
||||
if acquire_result.is_err() {
|
||||
return std::result::Result::Err("a Devnet execution is already running".to_string());
|
||||
}
|
||||
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
|
||||
let _run_guard = crate::DemoExecutionRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/demo_spl_ata.rs
|
||||
// version: 11
|
||||
// version: 12
|
||||
|
||||
//! Thin Tauri adapter for ATA execution, derivation and lifecycle journal reads.
|
||||
|
||||
@@ -204,7 +204,7 @@ pub(crate) async fn demo_execution_spl_ata_execute(
|
||||
if acquired.is_err() {
|
||||
return std::result::Result::Err("a Devnet execution is already running".to_string());
|
||||
}
|
||||
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
|
||||
let _run_guard = crate::DemoExecutionRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/demo_spl_token.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
//! Thin Tauri adapter for classic SPL Token execution and materialized journals.
|
||||
|
||||
@@ -168,7 +168,7 @@ pub(crate) async fn demo_execution_spl_token_execute(
|
||||
if acquire_result.is_err() {
|
||||
return std::result::Result::Err("a Devnet execution is already running".to_string());
|
||||
}
|
||||
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
|
||||
let _run_guard = crate::DemoExecutionRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/demo_spl_token_2022.rs
|
||||
// version: 11
|
||||
// version: 12
|
||||
|
||||
//! Thin Tauri adapter for independent public Token-2022 Devnet validation scenarios.
|
||||
|
||||
@@ -146,7 +146,7 @@ pub(crate) async fn demo_execution_spl_token_2022_execute(
|
||||
if acquire_result.is_err() {
|
||||
return std::result::Result::Err("a Devnet execution is already running".to_string());
|
||||
}
|
||||
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
|
||||
let _run_guard = crate::DemoExecutionRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/lib.rs
|
||||
// version: 29
|
||||
// version: 31
|
||||
|
||||
//! Tauri desktop demo application for `khadhroony-bot3`.
|
||||
|
||||
@@ -15,6 +15,8 @@ mod demo_core_extraction;
|
||||
mod demo_decode_replay;
|
||||
mod demo_devnet_common;
|
||||
mod demo_execution_metadata;
|
||||
mod demo_execution_metadata_metaplex_token_metadata;
|
||||
mod demo_execution_metadata_solana_program;
|
||||
mod demo_execution_solana_core;
|
||||
mod demo_execution_spl;
|
||||
mod demo_http;
|
||||
@@ -123,34 +125,52 @@ pub(crate) use self::demo_decode_replay::demo_decode_replay_execute;
|
||||
pub(crate) use self::demo_decode_replay::demo_decode_replay_options;
|
||||
/// Readiness report for one Devnet profile PostgreSQL store.
|
||||
pub(crate) use self::demo_devnet_common::DemoExecutionDevnetStoreReadinessPayload;
|
||||
/// Restores the shared single-execution state flag when a Devnet run finishes.
|
||||
pub(crate) use self::demo_devnet_common::DemoExecutionRunGuard;
|
||||
/// Converts the reusable scenario readiness report to the desktop TS-RS payload.
|
||||
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.
|
||||
/// Bridges Metadata execution progress to the Metadata desktop window.
|
||||
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataObserver;
|
||||
/// Progress event emitted by Metadata execution workflows.
|
||||
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataProgressPayload;
|
||||
/// Returns Devnet profile options shared by 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;
|
||||
/// UI-safe result of one prepared Metaplex Token Metadata Create fixture.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload;
|
||||
/// Prepared coherent Metaplex Token Metadata scenario step.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload;
|
||||
/// Request for one real Devnet Metaplex Token Metadata execution.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexTokenMetadataRequest;
|
||||
/// UI-safe Metaplex Token Metadata scenario row.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload;
|
||||
/// UI-safe result of one real Devnet Metaplex Token Metadata execution.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload;
|
||||
/// Returns current Metaplex Token Metadata operation names accepted by Devnet campaigns.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::demo_execution_metadata_metaplex_token_metadata_current_operations;
|
||||
/// Executes one real Devnet Metaplex Token Metadata simulation or submission.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::demo_execution_metadata_metaplex_token_metadata_execute;
|
||||
/// Returns one generated Metaplex Token Metadata operation JSON template.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::demo_execution_metadata_metaplex_token_metadata_operation_template;
|
||||
/// Creates or reuses the Devnet mint fixture used by a Metaplex Token Metadata Create scenario.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::demo_execution_metadata_metaplex_token_metadata_prepare_create_fixture;
|
||||
/// Prepares one coherent Metaplex Token Metadata scenario step and its stateful reads.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::demo_execution_metadata_metaplex_token_metadata_prepare_step;
|
||||
/// Returns the canonical Metaplex Token Metadata scenario inventory.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::demo_execution_metadata_metaplex_token_metadata_scenarios;
|
||||
/// Request for one complete Solana Program Metadata Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_solana_program::DemoExecutionMetadataSolanaProgramCampaignRequest;
|
||||
/// UI-safe result of one complete Solana Program Metadata Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_solana_program::DemoExecutionMetadataSolanaProgramCampaignSummaryPayload;
|
||||
/// UI-safe Solana Program Metadata Devnet journey.
|
||||
pub(crate) use self::demo_execution_metadata_solana_program::DemoExecutionMetadataSolanaProgramScenarioPayload;
|
||||
/// UI-safe step in one Solana Program Metadata Devnet journey.
|
||||
pub(crate) use self::demo_execution_metadata_solana_program::DemoExecutionMetadataSolanaProgramScenarioStepPayload;
|
||||
/// Executes the complete Solana Program Metadata Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_solana_program::demo_execution_metadata_solana_program_execute_campaign;
|
||||
/// Returns the two canonical Solana Program Metadata Devnet journeys.
|
||||
pub(crate) use self::demo_execution_metadata_solana_program::demo_execution_metadata_solana_program_scenarios;
|
||||
/// Request sent by the Memo v4 Devnet execution panel.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionMemoRequest;
|
||||
/// UI-safe result of one Memo v4 Devnet execution orchestration.
|
||||
@@ -167,8 +187,6 @@ pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreProfileO
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreProgressPayload;
|
||||
/// Request sent by the execution demo.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreRequest;
|
||||
/// Restores the single-execution state flag when a run finishes.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreRunGuard;
|
||||
/// UI-safe result of one execution orchestration.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreSummaryPayload;
|
||||
/// Maps a confirmation status to its stable UI code.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/tauri.rs
|
||||
// version: 30
|
||||
// version: 32
|
||||
|
||||
//! Tauri runtime assembly and private command wrappers.
|
||||
|
||||
@@ -89,13 +89,15 @@ pub fn run() -> kb_core::Result<()> {
|
||||
demo_execution_spl_token_2022_execute,
|
||||
demo_spl_token_2022_fixture,
|
||||
open_demo_execution_metadata_window,
|
||||
demo_execution_metadata_scenarios,
|
||||
demo_execution_metadata_metaplex_token_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,
|
||||
demo_execution_metadata_metaplex_token_metadata_current_operations,
|
||||
demo_execution_metadata_metaplex_token_metadata_operation_template,
|
||||
demo_execution_metadata_metaplex_token_metadata_prepare_create_fixture,
|
||||
demo_execution_metadata_metaplex_token_metadata_prepare_step,
|
||||
demo_execution_metadata_metaplex_token_metadata_execute,
|
||||
demo_execution_metadata_solana_program_scenarios,
|
||||
demo_execution_metadata_solana_program_execute_campaign,
|
||||
]);
|
||||
builder = builder.on_window_event(|window, event| {
|
||||
if window.label() != "main" || !matches!(event, tauri::WindowEvent::Destroyed) {
|
||||
@@ -629,42 +631,53 @@ fn demo_execution_metadata_options(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_scenarios() -> std::result::Result<
|
||||
std::vec::Vec<crate::DemoExecutionMetadataScenarioPayload>,
|
||||
fn demo_execution_metadata_metaplex_token_metadata_scenarios() -> std::result::Result<
|
||||
std::vec::Vec<crate::DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload>,
|
||||
std::string::String,
|
||||
> {
|
||||
return crate::demo_execution_metadata_scenarios();
|
||||
return crate::demo_execution_metadata_metaplex_token_metadata_scenarios();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_current_operations() -> std::vec::Vec<std::string::String> {
|
||||
return crate::demo_execution_metadata_current_operations();
|
||||
fn demo_execution_metadata_metaplex_token_metadata_current_operations()
|
||||
-> std::vec::Vec<std::string::String> {
|
||||
return crate::demo_execution_metadata_metaplex_token_metadata_current_operations();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_operation_template(
|
||||
fn demo_execution_metadata_metaplex_token_metadata_operation_template(
|
||||
operation: std::string::String,
|
||||
) -> std::result::Result<std::string::String, std::string::String> {
|
||||
return crate::demo_execution_metadata_operation_template(operation);
|
||||
return crate::demo_execution_metadata_metaplex_token_metadata_operation_template(operation);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_metadata_prepare_create_fixture(
|
||||
async fn demo_execution_metadata_metaplex_token_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;
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload,
|
||||
std::string::String,
|
||||
> {
|
||||
return crate::demo_execution_metadata_metaplex_token_metadata_prepare_create_fixture(
|
||||
state,
|
||||
profile_name,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_metadata_prepare_step(
|
||||
async fn demo_execution_metadata_metaplex_token_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(
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload,
|
||||
std::string::String,
|
||||
> {
|
||||
return crate::demo_execution_metadata_metaplex_token_metadata_prepare_step(
|
||||
state,
|
||||
profile_name,
|
||||
scenario_id,
|
||||
@@ -675,12 +688,39 @@ async fn demo_execution_metadata_prepare_step(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_metadata_execute(
|
||||
async fn demo_execution_metadata_metaplex_token_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;
|
||||
request: crate::DemoExecutionMetadataMetaplexTokenMetadataRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload,
|
||||
std::string::String,
|
||||
> {
|
||||
return crate::demo_execution_metadata_metaplex_token_metadata_execute(
|
||||
app_handle, state, request,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_solana_program_scenarios()
|
||||
-> std::vec::Vec<crate::DemoExecutionMetadataSolanaProgramScenarioPayload> {
|
||||
return crate::demo_execution_metadata_solana_program_scenarios();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_metadata_solana_program_execute_campaign(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataSolanaProgramCampaignRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataSolanaProgramCampaignSummaryPayload,
|
||||
std::string::String,
|
||||
> {
|
||||
return crate::demo_execution_metadata_solana_program_execute_campaign(
|
||||
app_handle, state, request,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
Reference in New Issue
Block a user