v0.4.8-pre.014
This commit is contained in:
@@ -1,8 +1,31 @@
|
||||
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
|
||||
<!-- version: 33 -->
|
||||
<!-- version: 38 -->
|
||||
|
||||
# CHANGELOG — kb-app-demo-desktop
|
||||
|
||||
## 0.4.8-pre.014
|
||||
|
||||
- `pre.014-delta-fix-004` clôture la finalisation desktop Metadata après validation de `fix-003` : formatage, `cargo check --workspace`, Clippy sans warning, audit workspace et 144 tests desktop sont propres ; la disposition finale des trois accordéons de résultats, de la carte **Exigences** et du journal pleine largeur est validée visuellement ;
|
||||
- réconcilie le rapport et le TODO avec cet état final, sans nouvelle campagne réseau ni changement fonctionnel ;
|
||||
- `pre.014-delta-fix-003` consolide l’intégration Metadata après validation réelle de `fix-002` : les 11 campagnes Metaplex qualifiées atteignent toutes `campaign_completed`, les campagnes Maintenance et Use conservent leurs probes indisponibles sans soumission et aucun fichier `error*.jsonl` fourni n’est non vide ;
|
||||
- fusionne `demo_execution_metadata_metaplex_campaign.rs` dans `demo_execution_metadata_metaplex_token_metadata.rs` afin que Metaplex suive le même modèle de module unique que Token-2022 Token Metadata et Solana Program Metadata, sans modifier les commandes Tauri ni les bindings TS-RS ;
|
||||
- transforme les blocs `Préflight et opérations`, `Simulation et confirmation` et `Postconditions et preuves` en accordéons Bootstrap, sur le modèle de `demo_execution_spl`, tout en conservant `Exigences` sous les détails et le journal d’exécution en pleine largeur ;
|
||||
- `pre.014-delta-fix-002` réaligne le test de contrôles Metaplex sur la nouvelle UI qualifiée (`metadataMetaplexCampaignSelect` + confirmation + exécution) et retire les assertions sur les anciens contrôles Devnet supprimés par `fix-001` ;
|
||||
- après deux crashs reproductibles du desktop pendant `Create -> Mint — NFT`, les logs montrent `Create` simulé, soumis et confirmé, puis un arrêt brutal pendant le `getTransaction` de l'hydratation canonique, sans panic Rust ni erreur JSONL ;
|
||||
- boxe explicitement le futur volumineux du dispatch des 11 campagnes Metaplex avant son exécution dans le runtime Tauri afin de réduire l'état async conservé par la commande desktop et ajoute des marqueurs backend `campaign_start`, `campaign_completed` et `campaign_failed` ; cette correction reste volontairement limitée à l'adaptateur desktop et ne modifie aucune campagne Devnet déjà qualifiée ;
|
||||
- `pre.014-delta-fix-001` remplace le chemin Devnet Metaplex générique du panneau par 11 campagnes qualifiées réutilisant directement les runners spécialisés de `kb-pipeline-demo-scenarios` : cinq `Create -> Mint`, collection, Print/Burn, lifecycle pNFT, escrow, maintenance et probe Use ;
|
||||
- conserve les scénarios Metaplex synthétiques pour l’inspection de contrats, mais ne présente plus `scenario + step + intent JSON` comme parcours Devnet principal ;
|
||||
- confine `Use`, `Resize`, `Migrate`, `Collect` et `CloseAccounts` aux probes simulation-only de leurs campagnes qualifiées et ne les expose jamais comme soumissions isolées ;
|
||||
- déplace la carte **Exigences** en bas de la colonne droite, après les postconditions, tout en conservant le journal d’exécution en pleine largeur sous la grille ;
|
||||
- enregistre la première validation Tauri de `pre.014` : `cargo check --workspace`, Clippy et l’audit workspace sont propres, `kb-pipeline-demo-scenarios` réussit 105 tests unitaires + 1 CLI + 1 API externe, et `kb-app-demo-desktop` réussit 139 tests unitaires ;
|
||||
- les logs fournis confirment l’exécution desktop complète de Token-2022 Token Metadata et Solana Program Metadata sur Devnet, tandis que l’ancien parcours Metaplex s’arrêtait après la seule préparation de fixture, défaut de câblage corrigé par ce delta ;
|
||||
- ajoute le troisième sous-panneau **Token-2022 Token Metadata** dans `demo_execution_metadata` ;
|
||||
- appelle directement la campagne réutilisable de cinq opérations `Initialize -> UpdateField -> Emit -> RemoveKey -> UpdateAuthority` sur un mint Token-2022 frais ;
|
||||
- exige la préparation PostgreSQL, un profil Devnet autorisé et une confirmation explicite avant la fixture et les cinq mutations ;
|
||||
- affiche la fixture, les simulations, confirmations, snapshots stateful, return data `Emit`, matérialisations et postconditions avec le JsonViewer commun ;
|
||||
- synchronise le profil entre Metaplex, Token-2022 et Solana Program Metadata ;
|
||||
- déplace le journal d’exécution hors de la colonne gauche afin qu’il occupe toute la largeur du panneau.
|
||||
|
||||
## 0.4.8-pre.013
|
||||
|
||||
- `pre.013-delta-fix-022` restaure la compatibilité `Future + Send` de la commande Tauri Metaplex en corrigeant le runner partagé : l'effacement temporaire `Signer + Sync -> &dyn Signer` est limité à un helper synchrone de signature et n'est plus conservé au travers des `await` ; aucun assouplissement de la commande desktop n'est nécessaire ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/README.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# 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 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 ;
|
||||
- panneau Metadata avec parcours séparés Metaplex Token Metadata, Token-2022 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
|
||||
@@ -23,11 +23,15 @@ Les scénarios UI Devnet/Testnet restent dans le desktop. Des tests automatisés
|
||||
|
||||
## Exécution Metaplex Token Metadata
|
||||
|
||||
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. Après une confirmation, l’adaptateur réutilise PostgreSQL pour conserver l’hydratation canonique, l’extraction Core, le replay, les matérialisations et la preuve d’idempotence produites par le runner.
|
||||
Le panneau conserve les scénarios synthétiques pour inspecter les contrats, mais son parcours Devnet principal expose désormais 11 campagnes qualifiées de `pre.013` et appelle directement leurs runners spécialisés de `kb-pipeline-demo-scenarios`. Les cinq variantes `Create -> Mint`, collection, Print/Burn, lifecycle pNFT et escrow soumettent uniquement les opérations autorisées ; Maintenance et Use conservent les opérations `unavailable` dans des probes simulation-only. Le desktop ne reconstruit ni les builders ni les postconditions et présente la fixture, les exécutions/probes et les preuves produites par les runners.
|
||||
|
||||
## Exécution Token-2022 Token Metadata
|
||||
|
||||
Le sous-panneau dédié appelle directement la campagne réutilisable de `kb-pipeline-demo-scenarios`; son parcours desktop complet a été confirmé sur Devnet pendant `pre.014`. Il crée un mint Token-2022 frais avec Metadata Pointer auto-référencé puis exécute, dans l’ordre, `Initialize`, `UpdateField`, `Emit`, `RemoveKey` et `UpdateAuthority`. Chaque étape passe par simulation, confirmation, replay canonique, matérialisation lorsque requise, lecture stateful et postcondition ; `Emit` conserve en plus sa return data validée.
|
||||
|
||||
## 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.
|
||||
Le même panneau expose séparément les deux parcours `Buffer` et `Metadata` de `ProgM6…`; sa campagne desktop complète a été confirmée sur Devnet pendant `pre.014`. 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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/TODO.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# TODO — kb-app-demo-desktop
|
||||
|
||||
@@ -8,16 +8,6 @@
|
||||
- [ ] 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.
|
||||
- [ ] Conserver simulation et soumission comme actions distinctes avec confirmation opérateur.
|
||||
|
||||
## Report conditionnel — registre ElGamal
|
||||
|
||||
- [ ] Confirmer le déploiement et les preuves réseau avant de rendre le panneau exécutable.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/USAGE.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 13 -->
|
||||
|
||||
# Utilisation de kb-app-demo-desktop
|
||||
|
||||
@@ -207,35 +207,32 @@ La simulation et l’envoi doivent rester explicitement distingués dans les req
|
||||
|
||||
### Exécution Metadata Devnet
|
||||
|
||||
Le backend distingue le module commun Metadata, Metaplex Token Metadata et Solana Program Metadata.
|
||||
Le backend distingue le module commun Metadata, Metaplex Token Metadata, Token-2022 Token Metadata et Solana Program Metadata. Metaplex est porté par un seul module Rust desktop, comme les deux autres domaines : ce module contient à la fois les contrats génériques conservés pour les tests/outils avancés et les campagnes Devnet qualifiées. Les commandes génériques Metaplex (`current_operations`, `operation_template`, `prepare_step`, `execute`) restent disponibles, mais le panneau Devnet n’utilise plus ce chemin comme workflow opérateur principal.
|
||||
|
||||
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`.
|
||||
Le panneau charge désormais l’inventaire borné avec `demo_execution_metadata_metaplex_campaigns`, puis exécute une campagne complète avec `demo_execution_metadata_metaplex_execute_campaign`.
|
||||
|
||||
```typescript
|
||||
const operationJson = await invoke(
|
||||
"demo_execution_metadata_metaplex_token_metadata_operation_template",
|
||||
{ operation: "verify" }
|
||||
const campaigns = await invoke(
|
||||
"demo_execution_metadata_metaplex_campaigns"
|
||||
);
|
||||
|
||||
const summary = await invoke(
|
||||
"demo_execution_metadata_metaplex_token_metadata_execute",
|
||||
"demo_execution_metadata_metaplex_execute_campaign",
|
||||
{
|
||||
request: {
|
||||
profileName: "devnet",
|
||||
intentId: `desktop-metaplex-${Date.now()}`,
|
||||
operationJson,
|
||||
preflightReadsJson: "[]",
|
||||
postconditionReadsJson: "[]",
|
||||
submit: false,
|
||||
operatorConfirmed: false
|
||||
campaignId: "create_mint_nft",
|
||||
operatorConfirmed: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(summary);
|
||||
console.log(campaigns, summary);
|
||||
```
|
||||
|
||||
Les valeurs `<...>` d’un modèle doivent être remplacées par des comptes réels avant exécution. `submit: true` exige simultanément `operatorConfirmed: true`. Le résultat distingue le plan, le préflight stateful, la simulation RPC, la signature éventuelle, la confirmation et les états avant/après. Après soumission confirmée, `evidenceJson` contient également l’hydratation canonique, l’extraction Core, le replay, la seconde passe d’idempotence, les matérialisations instructionnelles et le diagnostic post-exécution.
|
||||
L’inventaire contient cinq variantes `Create -> Mint`, puis collection `Verify -> Unverify`, `Print -> Burn`, lifecycle pNFT, Token Owned Escrow, Maintenance et le probe Use. Maintenance et Use sont les seules campagnes mixtes/probe : `Use`, `Resize`, `Migrate`, `Collect` et `CloseAccounts` y restent simulation-only lorsqu’elles rencontrent leur indisponibilité qualifiée et ne sont jamais exposées comme boutons de soumission isolés.
|
||||
|
||||
Dans la colonne de résultats, `Préflight et opérations`, `Simulation et confirmation` et `Postconditions et preuves` sont présentés dans un accordéon commun. La carte `Exigences` reste sous cet accordéon et le journal d’exécution reste sous la grille, sur toute la largeur.
|
||||
|
||||
### Diagnostics PostgreSQL
|
||||
|
||||
@@ -290,7 +287,7 @@ Le nombre de tests évolue avec les surfaces raccordées ; la référence de val
|
||||
|
||||
## Panneau Exécution Metadata
|
||||
|
||||
Depuis la fenêtre principale, choisir **Exécution Devnet → Exécution Metadata**. Le panneau permet actuellement :
|
||||
Depuis la fenêtre principale, choisir **Exécution Devnet → Exécution Metadata**. Le panneau sépare Metaplex Token Metadata, Token-2022 Token Metadata et Solana Program Metadata. Il permet actuellement :
|
||||
|
||||
- de choisir un scénario synthétique ou un contrat de campagne Devnet ;
|
||||
- de sélectionner le profil Devnet ;
|
||||
@@ -300,11 +297,30 @@ Depuis la fenêtre principale, choisir **Exécution Devnet → Exécution Metada
|
||||
|
||||
Un contrat marqué Devnet n’est pas, à lui seul, une exécution réseau. Les champs `networkExecutionPerformed` et `networkEvidenceCollected` doivent rester faux tant qu’aucun appel RPC réel et aucune preuve correspondante n’ont été produits.
|
||||
|
||||
Le panneau distingue préparation de l’étape, simulation exacte et soumission explicitement confirmée. Les lectures de postcondition et la matérialisation sont dérivées du scénario préparé.
|
||||
Les scénarios synthétiques restent disponibles pour inspecter les contrats sans réseau. Le parcours Devnet Metaplex sélectionne au contraire une campagne qualifiée complète ; fixture, simulation, soumission autorisée, replay, matérialisation, idempotence et postconditions restent la responsabilité du runner spécialisé.
|
||||
|
||||
### Préparation de la fixture Metaplex `Create`
|
||||
### Campagnes Metaplex qualifiées
|
||||
|
||||
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.
|
||||
Ouvrir **Metaplex Token Metadata -> Campagnes Devnet qualifiées**, sélectionner un profil persistant et préparer PostgreSQL. Le sélecteur présente 11 campagnes : cinq familles `Create -> Mint`, collection, Print/Burn, lifecycle pNFT, escrow, maintenance et Use. Après confirmation opérateur, le desktop appelle le runner correspondant et affiche :
|
||||
|
||||
- la fixture et son setup ;
|
||||
- les exécutions confirmées et les probes indisponibles dans leur ordre réel ;
|
||||
- les postconditions de campagne ;
|
||||
- les compteurs `confirmed` et `unavailable`.
|
||||
|
||||
La surface générique par opération reste une API avancée et ne doit pas être utilisée pour reconstruire manuellement ces campagnes dans l’UI.
|
||||
|
||||
### Campagne Token-2022 Token Metadata
|
||||
|
||||
Ouvrir l’accordéon **Token-2022 Token Metadata**, sélectionner un profil Devnet persistant et préparer sa base PostgreSQL. Le panneau affiche le contrat fixe de cinq opérations :
|
||||
|
||||
```text
|
||||
Initialize -> UpdateField -> Emit -> RemoveKey -> UpdateAuthority
|
||||
```
|
||||
|
||||
Après confirmation explicite, **Exécuter la campagne Token-2022 Metadata** crée un mint frais avec Metadata Pointer vers lui-même, puis appelle le runner réutilisable pour les cinq opérations. La campagne conserve les simulations, signatures, confirmations, snapshots stateful, postconditions et matérialisations ; `Emit` expose également la return data validée. La fixture et les preuves sont affichées avec le JsonViewer commun.
|
||||
|
||||
La carte **Exigences** est placée en bas de la colonne droite, après les postconditions. Le journal d’exécution est placé sous les deux colonnes principales et occupe toute la largeur disponible de la page.
|
||||
|
||||
### Campagne Solana Program Metadata
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/frontend/demo_execution_metadata.html -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 13 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<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 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="alert alert-info">Cette fenêtre sépare les trois familles metadata. Metaplex Token Metadata conserve ses scénarios synthétiques mais exécute sur Devnet les campagnes qualifiées de <code>pre.013</code> ; Token-2022 Token Metadata exécute sa campagne native de cinq opérations ; Solana Program Metadata exécute les neuf opérations stables de <code>ProgM6…</code> via deux parcours Devnet.</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 Metadata Devnet</button></h2>
|
||||
@@ -31,10 +31,17 @@
|
||||
<div class="accordion-body">
|
||||
<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.</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>
|
||||
<li>Utiliser les scénarios synthétiques uniquement pour inspecter les contrats et exigences.</li>
|
||||
<li>Pour Devnet, choisir et préparer un profil persistant puis sélectionner une campagne qualifiée de <code>pre.013</code>.</li>
|
||||
<li>Confirmer explicitement la création des fixtures et les soumissions autorisées du runner spécialisé.</li>
|
||||
<li>Contrôler la fixture, les exécutions/probes et les postconditions dans les trois JsonViewer communs.</li>
|
||||
</ol>
|
||||
<h3 class="h6">Token-2022 Token Metadata</h3>
|
||||
<ol class="mb-3">
|
||||
<li>Choisir et préparer un profil Devnet persistant, autorisé à soumettre.</li>
|
||||
<li>Contrôler les cinq opérations natives : Initialize, UpdateField, Emit, RemoveKey et UpdateAuthority.</li>
|
||||
<li>Confirmer la création d’un mint frais et l’exécution complète de la campagne.</li>
|
||||
<li>Contrôler les simulations, signatures, confirmations, snapshots stateful, return data Emit et matérialisations.</li>
|
||||
</ol>
|
||||
<h3 class="h6">Solana Program Metadata</h3>
|
||||
<ol class="mb-2">
|
||||
@@ -68,46 +75,26 @@
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h3 class="accordion-header">
|
||||
<button class="accordion-button collapsed" data-bs-target="#metaplexDevnetCollapse" data-bs-toggle="collapse" type="button">Scénarios Devnet</button>
|
||||
<button class="accordion-button collapsed" data-bs-target="#metaplexDevnetCollapse" data-bs-toggle="collapse" type="button">Campagnes Devnet qualifiées</button>
|
||||
</h3>
|
||||
<div class="accordion-collapse collapse" id="metaplexDevnetCollapse" data-bs-parent="#metaplexScenarioModeAccordion">
|
||||
<div class="accordion-body">
|
||||
<label class="form-label" for="metadataProfileSelect">Profil Devnet</label>
|
||||
<select class="form-select" id="metadataProfileSelect"></select>
|
||||
<div class="form-text mb-3" id="metadataProfileHelp">Chargement du profil Devnet.</div>
|
||||
<label class="form-label" for="metadataDevnetScenarioSelect">Contrat de scénario</label>
|
||||
<select class="form-select" id="metadataDevnetScenarioSelect"></select>
|
||||
<div class="d-grid mt-3"><button class="btn btn-outline-primary" id="prepareMetadataProfileButton" type="button">Charger le profil et préparer la base</button></div>
|
||||
<div class="d-grid"><button class="btn btn-outline-primary" id="prepareMetadataProfileButton" type="button">Charger le profil et préparer la base</button></div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-3"><span class="small text-muted">État du profil</span><span class="badge text-bg-secondary" id="metadataProfileStatusBadge">Non chargé</span></div>
|
||||
<div class="mt-3" id="metadataProfileOutput"></div>
|
||||
<label class="form-label mt-3" for="metadataStepSelect">Étape du parcours</label>
|
||||
<select class="form-select" id="metadataStepSelect"></select>
|
||||
<div class="d-flex gap-2 mt-2">
|
||||
<button class="btn btn-outline-secondary flex-fill" id="previousMetadataStepButton" type="button">Étape précédente</button>
|
||||
<button class="btn btn-outline-secondary flex-fill" id="nextMetadataStepButton" type="button">Étape suivante</button>
|
||||
</div>
|
||||
<div class="d-grid mt-2"><button class="btn btn-outline-success" id="prepareMetadataScenarioStepButton" type="button">Préparer l’étape courante</button></div>
|
||||
<div class="form-text">Le scénario remplit ensemble l’intent, les lectures avant/après et les cibles de matérialisation.</div>
|
||||
<label class="form-label mt-3" for="metadataOperationJson">Intent Metaplex typé</label>
|
||||
<textarea class="form-control font-monospace" id="metadataOperationJson" rows="12" spellcheck="false"></textarea>
|
||||
<label class="form-label mt-3" for="metadataPreflightReadsJson">Lectures stateful avant simulation</label>
|
||||
<textarea class="form-control font-monospace" id="metadataPreflightReadsJson" rows="4" spellcheck="false">[]</textarea>
|
||||
<label class="form-label mt-3" for="metadataPostconditionReadsJson">Lectures stateful après confirmation</label>
|
||||
<textarea class="form-control font-monospace" id="metadataPostconditionReadsJson" rows="4" spellcheck="false">[]</textarea>
|
||||
<label class="form-label mt-3" for="metadataMetaplexCampaignSelect">Campagne qualifiée</label>
|
||||
<select class="form-select" id="metadataMetaplexCampaignSelect"></select>
|
||||
<div class="mt-3" id="metadataMetaplexCampaignOutput"></div>
|
||||
<div class="alert alert-warning mt-3">Les campagnes Devnet réutilisent directement les runners qualifiés de <code>pre.013</code>. Les opérations classées <code>unavailable</code> restent confinées aux probes simulation-only de Maintenance ou Use ; elles ne sont jamais exposées comme soumissions isolées.</div>
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" id="metadataMaterializeInput" type="checkbox" checked>
|
||||
<label class="form-check-label" for="metadataMaterializeInput">Matérialiser les snapshots Metaplex confirmés</label>
|
||||
<div class="form-text">Après confirmation, relit les comptes déclarés, les décode canoniquement et produit les projections d’état. Aucun fetch URI off-chain.</div>
|
||||
<input class="form-check-input" id="metadataMetaplexCampaignOperatorConfirmed" type="checkbox">
|
||||
<label class="form-check-label" for="metadataMetaplexCampaignOperatorConfirmed">Je confirme la création des fixtures et toutes les soumissions autorisées de la campagne</label>
|
||||
</div>
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" id="metadataOperatorConfirmed" type="checkbox">
|
||||
<label class="form-check-label" for="metadataOperatorConfirmed">Je confirme explicitement la soumission</label>
|
||||
</div>
|
||||
<div class="d-grid gap-2 mt-3">
|
||||
<button class="btn btn-outline-primary" id="simulateMetadataButton" type="button">Simuler sur Devnet</button>
|
||||
<button class="btn btn-primary" id="submitMetadataButton" type="button">Soumettre sur Devnet</button>
|
||||
</div>
|
||||
<div class="alert alert-warning mt-3 mb-0">La simulation appelle réellement le RPC Devnet. La soumission reste désactivée tant que l’opérateur ne l’autorise et ne la confirme pas explicitement.</div>
|
||||
<div class="d-grid mt-3"><button class="btn btn-danger" id="executeMetadataMetaplexCampaignButton" type="button">Exécuter la campagne Metaplex</button></div>
|
||||
<div class="form-text">Le runner arrête la campagne au premier échec et conserve simulation, signature, confirmation, postconditions, matérialisation et idempotence selon son contrat.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,6 +105,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" data-bs-target="#token2022MetadataCollapse" data-bs-toggle="collapse" type="button">Token-2022 Token Metadata</button>
|
||||
</h2>
|
||||
<div class="accordion-collapse collapse" id="token2022MetadataCollapse" data-bs-parent="#metadataExecutionAccordion">
|
||||
<div class="accordion-body">
|
||||
<label class="form-label" for="token2022MetadataProfileSelect">Profil Devnet</label>
|
||||
<select class="form-select" id="token2022MetadataProfileSelect"></select>
|
||||
<div class="form-text mb-3" id="token2022MetadataProfileHelp">Chargement du profil Devnet.</div>
|
||||
<div class="d-grid"><button class="btn btn-outline-primary" id="prepareToken2022MetadataProfileButton" 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="token2022MetadataProfileStatusBadge">Non chargé</span></div>
|
||||
<div class="mt-3" id="token2022MetadataProfileOutput"></div>
|
||||
<div class="mt-3" id="token2022MetadataCampaignOutput"></div>
|
||||
<div class="alert alert-warning mt-3">Cette campagne crée un mint Token-2022 frais avec Metadata Pointer, puis soumet exactement cinq opérations Token Metadata. Chaque mutation est simulée avant soumission et la campagne s’arrête à la première preuve manquante.</div>
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" id="token2022MetadataOperatorConfirmed" type="checkbox">
|
||||
<label class="form-check-label" for="token2022MetadataOperatorConfirmed">Je confirme la création de la fixture et les cinq mutations Devnet</label>
|
||||
</div>
|
||||
<div class="d-grid mt-3"><button class="btn btn-danger" id="executeToken2022MetadataCampaignButton" type="button">Exécuter la campagne Token-2022 Metadata</button></div>
|
||||
<div class="form-text">Le résultat réutilise les trois JsonViewer communs pour la fixture, les confirmations et les preuves ordonnées.</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>
|
||||
@@ -144,10 +154,42 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-2">Journal d’exécution</h2>
|
||||
<textarea class="form-control font-monospace" id="metadataExecutionLogOutput" readonly rows="12" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<h2 class="h5 mb-0">Résultats détaillés</h2>
|
||||
<small class="text-body-secondary">Préflight, simulation, confirmation et preuves</small>
|
||||
</div>
|
||||
<div class="accordion shadow-sm mt-4" id="metadataExecutionDetailsAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="metadataPreflightHeading">
|
||||
<button aria-controls="metadataPreflightCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#metadataPreflightCollapse" data-bs-toggle="collapse" type="button">Préflight et opérations</button>
|
||||
</h2>
|
||||
<div aria-labelledby="metadataPreflightHeading" class="accordion-collapse collapse" data-bs-parent="#metadataExecutionDetailsAccordion" id="metadataPreflightCollapse">
|
||||
<div class="accordion-body">
|
||||
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataPreflightOutput" readonly rows="14">{"title":"Préflight et opérations","status":"idle","message":"Aucun résultat."}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="metadataSimulationHeading">
|
||||
<button aria-controls="metadataSimulationCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#metadataSimulationCollapse" data-bs-toggle="collapse" type="button">Simulation et confirmation</button>
|
||||
</h2>
|
||||
<div aria-labelledby="metadataSimulationHeading" class="accordion-collapse collapse" data-bs-parent="#metadataExecutionDetailsAccordion" id="metadataSimulationCollapse">
|
||||
<div class="accordion-body">
|
||||
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataSimulationOutput" readonly rows="16">{"title":"Simulation et confirmation","status":"idle","message":"Aucune simulation."}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="metadataPostconditionsHeading">
|
||||
<button aria-controls="metadataPostconditionsCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#metadataPostconditionsCollapse" data-bs-toggle="collapse" type="button">Postconditions et preuves</button>
|
||||
</h2>
|
||||
<div aria-labelledby="metadataPostconditionsHeading" class="accordion-collapse collapse" data-bs-parent="#metadataExecutionDetailsAccordion" id="metadataPostconditionsCollapse">
|
||||
<div class="accordion-body">
|
||||
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataPostconditionsOutput" readonly rows="16">{"title":"Postconditions et preuves","status":"idle","message":"Aucune preuve."}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
@@ -157,25 +199,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Préflight et opérations</h2>
|
||||
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataPreflightOutput" readonly rows="14">{"title":"Préflight et opérations","status":"idle","message":"Aucun résultat."}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Simulation et confirmation</h2>
|
||||
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataSimulationOutput" readonly rows="16">{"title":"Simulation et confirmation","status":"idle","message":"Aucune simulation."}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Postconditions et preuves</h2>
|
||||
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataPostconditionsOutput" readonly rows="16">{"title":"Postconditions et preuves","status":"idle","message":"Aucune preuve."}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-2">Journal d’exécution</h2>
|
||||
<textarea class="form-control font-monospace" id="metadataExecutionLogOutput" readonly rows="12" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,4 +212,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: 12
|
||||
// version: 14
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
@@ -7,30 +7,32 @@ 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 { DemoExecutionMetadataMetaplexCampaignOptionPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexCampaignOptionPayload.ts";
|
||||
import type { DemoExecutionMetadataMetaplexCampaignRequest } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexCampaignRequest.ts";
|
||||
import type { DemoExecutionMetadataMetaplexCampaignSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexCampaignSummaryPayload.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 { DemoExecutionMetadataToken2022CampaignRequest } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataToken2022CampaignRequest.ts";
|
||||
import type { DemoExecutionMetadataToken2022CampaignSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataToken2022CampaignSummaryPayload.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 metaplexCampaignStorageKey = "kb-app-demo-desktop.metadata.metaplex.selected-campaign";
|
||||
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: DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload[] = [];
|
||||
let metaplexCampaigns: DemoExecutionMetadataMetaplexCampaignOptionPayload[] = [];
|
||||
let solanaProgramMetadataScenarios: DemoExecutionMetadataSolanaProgramScenarioPayload[] = [];
|
||||
let selectedProfileName: string | null = null;
|
||||
let executionRunning = false;
|
||||
let profileStoreReady = false;
|
||||
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
|
||||
let preparedScenarioId: string | null = null;
|
||||
let preparedStepIndex: number | null = null;
|
||||
const metadataLogLines: string[] = [];
|
||||
const maximumMetadataLogLines = 1200;
|
||||
|
||||
@@ -57,10 +59,10 @@ 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;
|
||||
const profileSelectSelectors = ["#metadataProfileSelect", "#token2022MetadataProfileSelect", "#solanaProgramMetadataProfileSelect"] as const;
|
||||
const profileHelpSelectors = ["#metadataProfileHelp", "#token2022MetadataProfileHelp", "#solanaProgramMetadataProfileHelp"] as const;
|
||||
const profileStatusSelectors = ["#metadataProfileStatusBadge", "#token2022MetadataProfileStatusBadge", "#solanaProgramMetadataProfileStatusBadge"] as const;
|
||||
const profileOutputSelectors = ["#metadataProfileOutput", "#token2022MetadataProfileOutput", "#solanaProgramMetadataProfileOutput"] as const;
|
||||
|
||||
function synchronizeProfileSelections(profileName: string): void {
|
||||
for (const selector of profileSelectSelectors) {
|
||||
@@ -80,50 +82,19 @@ function updateProfileStatus(text: string, className: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function activeSelect(): HTMLSelectElement {
|
||||
return document.querySelector("#metaplexDevnetCollapse.show") !== null ? element<HTMLSelectElement>("#metadataDevnetScenarioSelect") : element<HTMLSelectElement>("#metadataSyntheticScenarioSelect");
|
||||
}
|
||||
|
||||
function selectedScenario(): DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload | null {
|
||||
const id = activeSelect().value;
|
||||
const id = element<HTMLSelectElement>("#metadataSyntheticScenarioSelect").value;
|
||||
return scenarios.find(scenario => scenario.id === id) ?? null;
|
||||
}
|
||||
|
||||
function fillSelect(selector: string, mode: string): void {
|
||||
const select = element<HTMLSelectElement>(selector);
|
||||
select.replaceChildren();
|
||||
for (const scenario of scenarios.filter(candidate => candidate.mode === mode)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = scenario.id;
|
||||
option.textContent = `${scenario.assetFamily} — ${scenario.label}`;
|
||||
select.append(option);
|
||||
}
|
||||
function selectedMetaplexCampaign(): DemoExecutionMetadataMetaplexCampaignOptionPayload | null {
|
||||
const id = element<HTMLSelectElement>("#metadataMetaplexCampaignSelect").value;
|
||||
return metaplexCampaigns.find(campaign => campaign.id === id) ?? null;
|
||||
}
|
||||
|
||||
function operationName(operationCode: string): string {
|
||||
const parts = operationCode.split(".");
|
||||
return parts.length === 0 ? operationCode : parts[parts.length - 1] ?? operationCode;
|
||||
}
|
||||
|
||||
function renderScenarioSteps(scenario: DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload): void {
|
||||
const select = element<HTMLSelectElement>("#metadataStepSelect");
|
||||
const previousValue = Number.parseInt(select.value, 10);
|
||||
select.replaceChildren();
|
||||
scenario.operationCodes.forEach((operationCode, index) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = String(index);
|
||||
option.textContent = `${index + 1}. ${operationName(operationCode)}`;
|
||||
select.append(option);
|
||||
});
|
||||
if (Number.isInteger(previousValue) && previousValue >= 0 && previousValue < scenario.operationCodes.length) {
|
||||
select.value = String(previousValue);
|
||||
}
|
||||
const index = Number.parseInt(select.value, 10);
|
||||
element<HTMLButtonElement>("#previousMetadataStepButton").disabled = !Number.isInteger(index) || index <= 0;
|
||||
element<HTMLButtonElement>("#nextMetadataStepButton").disabled = !Number.isInteger(index) || index >= scenario.operationCodes.length - 1;
|
||||
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").textContent = Number.isInteger(index) && scenario.operationCodes[index] !== undefined
|
||||
? `Préparer l’étape ${index + 1} — ${operationName(scenario.operationCodes[index])}`
|
||||
: "Préparer l’étape courante";
|
||||
function selectedSolanaProgramMetadataScenario(): DemoExecutionMetadataSolanaProgramScenarioPayload | null {
|
||||
const id = element<HTMLSelectElement>("#solanaProgramMetadataScenarioSelect").value;
|
||||
return solanaProgramMetadataScenarios.find(scenario => scenario.id === id) ?? null;
|
||||
}
|
||||
|
||||
function renderSelectedScenario(): void {
|
||||
@@ -132,95 +103,62 @@ function renderSelectedScenario(): void {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(storageKey, scenario.id);
|
||||
const networkMode = scenario.mode !== "synthetic";
|
||||
renderScenarioSteps(scenario);
|
||||
preparedScenarioId = null;
|
||||
preparedStepIndex = null;
|
||||
element<HTMLTextAreaElement>("#metadataOperationJson").value = "";
|
||||
element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value = "[]";
|
||||
element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value = "[]";
|
||||
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Parcours</dt><dd class="col-6">${scenario.label}</dd><dt class="col-6">Famille</dt><dd class="col-6"><code>${scenario.assetFamily}</code></dd><dt class="col-6">Fixture</dt><dd class="col-6"><code>${scenario.fixtureKind}</code></dd><dt class="col-6">État initial</dt><dd class="col-6"><code>${scenario.initialState}</code></dd><dt class="col-6">État attendu</dt><dd class="col-6"><code>${scenario.resultingState}</code></dd><dt class="col-6">Mode</dt><dd class="col-6"><code>${scenario.mode}</code></dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${networkMode ? selectedProfileName ?? "non chargé" : "non applicable"}</code></dd><dt class="col-6">Collection</dt><dd class="col-6">${scenario.requiresCollection ? "requise" : "non requise"}</dd><dt class="col-6">Rule set</dt><dd class="col-6">${scenario.requiresProgrammableRules ? "requis" : "non requis"}</dd><dt class="col-6">Validation</dt><dd class="col-6"><code>${scenario.validationStatus}</code></dd>`;
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", {
|
||||
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Parcours</dt><dd class="col-6">${scenario.label}</dd><dt class="col-6">Famille</dt><dd class="col-6"><code>${scenario.assetFamily}</code></dd><dt class="col-6">Fixture</dt><dd class="col-6"><code>${scenario.fixtureKind}</code></dd><dt class="col-6">État initial</dt><dd class="col-6"><code>${scenario.initialState}</code></dd><dt class="col-6">État attendu</dt><dd class="col-6"><code>${scenario.resultingState}</code></dd><dt class="col-6">Mode</dt><dd class="col-6"><code>${scenario.mode}</code></dd><dt class="col-6">Collection</dt><dd class="col-6">${scenario.requiresCollection ? "requise" : "non requise"}</dd><dt class="col-6">Rule set</dt><dd class="col-6">${scenario.requiresProgrammableRules ? "requis" : "non requis"}</dd><dt class="col-6">Validation</dt><dd class="col-6"><code>${scenario.validationStatus}</code></dd>`;
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Contrat synthétique — opérations", {
|
||||
scenarioId: scenario.id,
|
||||
operationCodes: scenario.operationCodes,
|
||||
fixtureKind: scenario.fixtureKind,
|
||||
checks: ["program_id", "owner", "pda_and_seeds", "required_accounts", "authorities", "stateful_correlations"],
|
||||
}));
|
||||
renderJsonViewer("#metadataSimulationOutput", titled("Contrat synthétique — politique", {
|
||||
mode: scenario.mode,
|
||||
networkExecutionPerformed: false,
|
||||
automaticSubmission: false,
|
||||
validationStatus: scenario.validationStatus,
|
||||
note: "Ce mode inspecte le contrat. Les exécutions Devnet utilisent exclusivement les campagnes qualifiées de pre.013.",
|
||||
}));
|
||||
renderJsonViewer("#metadataPostconditionsOutput", titled("Contrat synthétique — preuves", {
|
||||
required: scenario.requiresPostcondition,
|
||||
evidenceKinds: scenario.requiredEvidence,
|
||||
materializationTargets: scenario.materializationTargets,
|
||||
networkEvidenceCollected: false,
|
||||
}));
|
||||
}
|
||||
|
||||
function renderMetaplexCampaign(): void {
|
||||
const campaign = selectedMetaplexCampaign();
|
||||
if (campaign === null) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(metaplexCampaignStorageKey, campaign.id);
|
||||
renderJsonViewer("#metadataMetaplexCampaignOutput", titled("Campagne Metaplex qualifiée", campaign));
|
||||
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Famille</dt><dd class="col-6"><code>metadata.metaplex_token_metadata</code></dd><dt class="col-6">Campagne</dt><dd class="col-6">${campaign.label}</dd><dt class="col-6">Asset</dt><dd class="col-6"><code>${campaign.assetFamily}</code></dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${selectedProfileName ?? "non chargé"}</code></dd><dt class="col-6">Qualification</dt><dd class="col-6"><code>${campaign.qualification}</code></dd><dt class="col-6">Opérations cibles</dt><dd class="col-6">${campaign.operationNames.length}</dd>`;
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", {
|
||||
campaignId: campaign.id,
|
||||
label: campaign.label,
|
||||
assetFamily: campaign.assetFamily,
|
||||
setup: campaign.setup,
|
||||
targetOperations: campaign.operationNames,
|
||||
runner: "qualified_pre_013_specialized_campaign",
|
||||
networkExecutionPerformed: false,
|
||||
}));
|
||||
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
|
||||
mode: scenario.mode,
|
||||
policy: "simulation_first",
|
||||
networkExecutionPerformed: false,
|
||||
automaticSubmission: false,
|
||||
policy: "specialized_campaign_simulation_first",
|
||||
operatorConfirmationRequired: true,
|
||||
expectedState: scenario.validationStatus,
|
||||
note: networkMode ? "Renseignez un intent typé puis lancez une simulation RPC réelle." : "Scénario synthétique déterministe.",
|
||||
stopOnFirstFailure: true,
|
||||
isolatedUnavailableSubmission: false,
|
||||
qualification: campaign.qualification,
|
||||
networkExecutionPerformed: false,
|
||||
}));
|
||||
renderJsonViewer("#metadataPostconditionsOutput", titled("Postconditions et preuves", {
|
||||
required: scenario.requiresPostcondition,
|
||||
evidenceKinds: scenario.requiredEvidence,
|
||||
campaignId: campaign.id,
|
||||
qualification: campaign.qualification,
|
||||
requiredEvidence: ["rpc_simulation", "confirmed_signature_when_submitted", "canonical_replay", "stateful_postconditions", "materialization", "idempotence"],
|
||||
unavailablePolicy: "simulation_only_probe_without_submission",
|
||||
networkEvidenceCollected: false,
|
||||
materializationTargets: scenario.materializationTargets,
|
||||
rule: "Aucune validation réseau n'est déclarée sans simulation RPC réelle, signature éventuelle, slot et postconditions.",
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function prepareScenarioStep(): Promise<void> {
|
||||
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
|
||||
const scenario = selectedScenario();
|
||||
const stepIndex = Number.parseInt(element<HTMLSelectElement>("#metadataStepSelect").value, 10);
|
||||
if (profileName.length === 0) { throw new Error("Sélectionnez un profil Devnet."); }
|
||||
if (scenario === null || scenario.mode === "synthetic") { throw new Error("Sélectionnez un scénario Devnet."); }
|
||||
if (!Number.isInteger(stepIndex) || stepIndex < 0) { throw new Error("Sélectionnez une étape valide."); }
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "fixture", message: `Préparation de ${scenario.id} étape ${stepIndex + 1}` });
|
||||
const prepared = await invoke<DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload>("demo_execution_metadata_metaplex_token_metadata_prepare_step", {
|
||||
profileName,
|
||||
scenarioId: scenario.id,
|
||||
stepIndex,
|
||||
previousOperationJson: element<HTMLTextAreaElement>("#metadataOperationJson").value.trim().length === 0
|
||||
? null
|
||||
: element<HTMLTextAreaElement>("#metadataOperationJson").value,
|
||||
});
|
||||
element<HTMLTextAreaElement>("#metadataOperationJson").value = prepared.operationJson;
|
||||
element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value = prepared.preflightReadsJson;
|
||||
element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value = prepared.postconditionReadsJson;
|
||||
preparedScenarioId = prepared.scenarioId;
|
||||
preparedStepIndex = prepared.stepIndex;
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Étape de scénario préparée", {
|
||||
scenarioId: prepared.scenarioId,
|
||||
stepIndex: prepared.stepIndex,
|
||||
operation: prepared.operation,
|
||||
fixtureState: prepared.fixtureState,
|
||||
preflightReads: JSON.parse(prepared.preflightReadsJson),
|
||||
postconditionReads: JSON.parse(prepared.postconditionReadsJson),
|
||||
simulationReady: prepared.simulationReady,
|
||||
submissionReady: prepared.submissionReady,
|
||||
}));
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "fixture", message: `${prepared.label} prête (${prepared.fixtureState})` });
|
||||
setExecutionRunning(executionRunning);
|
||||
}
|
||||
|
||||
function moveScenarioStep(offset: number): void {
|
||||
const select = element<HTMLSelectElement>("#metadataStepSelect");
|
||||
const current = Number.parseInt(select.value, 10);
|
||||
const next = current + offset;
|
||||
if (Number.isInteger(next) && next >= 0 && next < select.options.length) {
|
||||
select.value = String(next);
|
||||
const scenario = selectedScenario();
|
||||
if (scenario !== null) { renderScenarioSteps(scenario); }
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonText(value: string, label: string): unknown {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (caught) {
|
||||
const message = caught instanceof Error ? caught.message : String(caught);
|
||||
throw new Error(`${label} invalide : ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function selectedProfile(): DemoExecutionSolanaCoreProfileOption | null {
|
||||
if (selectedProfileName === null) {
|
||||
return null;
|
||||
@@ -241,92 +179,158 @@ function updateProfileHelp(): void {
|
||||
function setExecutionRunning(value: boolean): void {
|
||||
executionRunning = value;
|
||||
const profile = selectedProfile();
|
||||
const missingOperation = element<HTMLTextAreaElement>("#metadataOperationJson").value.trim().length === 0;
|
||||
const unavailable = value || profile === null || !profileStoreReady || missingOperation;
|
||||
element<HTMLButtonElement>("#simulateMetadataButton").disabled = unavailable;
|
||||
element<HTMLButtonElement>("#submitMetadataButton").disabled = unavailable || !profile?.sendEnabled;
|
||||
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").disabled = value || profile === null || !profileStoreReady;
|
||||
const metaplexConfirmed = element<HTMLInputElement>("#metadataMetaplexCampaignOperatorConfirmed").checked;
|
||||
element<HTMLButtonElement>("#executeMetadataMetaplexCampaignButton").disabled = value || profile === null || !profileStoreReady || !profile.sendEnabled || !metaplexConfirmed || selectedMetaplexCampaign() === null;
|
||||
const token2022Confirmed = element<HTMLInputElement>("#token2022MetadataOperatorConfirmed").checked;
|
||||
element<HTMLButtonElement>("#executeToken2022MetadataCampaignButton").disabled = value || profile === null || !profileStoreReady || !profile.sendEnabled || !token2022Confirmed;
|
||||
const solanaProgramConfirmed = element<HTMLInputElement>("#solanaProgramMetadataOperatorConfirmed").checked;
|
||||
element<HTMLButtonElement>("#executeSolanaProgramMetadataCampaignButton").disabled = value || profile === null || !profileStoreReady || !profile.sendEnabled || !solanaProgramConfirmed;
|
||||
for (const selector of ["#prepareMetadataProfileButton", "#prepareToken2022MetadataProfileButton", "#prepareSolanaProgramMetadataProfileButton", "#refreshMetadataScenariosButton"]) {
|
||||
element<HTMLButtonElement>(selector).disabled = value;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeMetadata(submit: boolean): Promise<void> {
|
||||
async function executeMetaplexCampaign(): Promise<void> {
|
||||
if (executionRunning) {
|
||||
return;
|
||||
}
|
||||
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
|
||||
if (profileName.length === 0 || !profileStoreReady) {
|
||||
const profile = selectedProfile();
|
||||
const campaign = selectedMetaplexCampaign();
|
||||
if (profile === null || selectedProfileName === null || !profileStoreReady) {
|
||||
throw new Error("Sélectionnez et préparez un profil Devnet.");
|
||||
}
|
||||
const operationJson = element<HTMLTextAreaElement>("#metadataOperationJson").value;
|
||||
const scenario = selectedScenario();
|
||||
const stepIndex = Number.parseInt(element<HTMLSelectElement>("#metadataStepSelect").value, 10);
|
||||
if (operationJson.trim().length === 0) {
|
||||
throw new Error("Préparez l’étape courante avant l’exécution.");
|
||||
if (campaign === null) {
|
||||
throw new Error("Sélectionnez une campagne Metaplex qualifiée.");
|
||||
}
|
||||
if (scenario === null || preparedScenarioId !== scenario.id || preparedStepIndex !== stepIndex) {
|
||||
throw new Error("Préparez de nouveau l’étape après toute modification du scénario ou de l’étape.");
|
||||
if (!profile.sendEnabled) {
|
||||
throw new Error("Le profil sélectionné n’autorise pas les soumissions Devnet requises par les fixtures Metaplex.");
|
||||
}
|
||||
const preflightReadsJson = element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value;
|
||||
const postconditionReadsJson = element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value;
|
||||
parseJsonText(operationJson, "Intent Metaplex");
|
||||
parseJsonText(preflightReadsJson, "Lectures de préflight");
|
||||
parseJsonText(postconditionReadsJson, "Lectures de postcondition");
|
||||
const operatorConfirmed = element<HTMLInputElement>("#metadataOperatorConfirmed").checked;
|
||||
if (submit && !operatorConfirmed) {
|
||||
throw new Error("La soumission exige la confirmation explicite de l’opérateur.");
|
||||
const operatorConfirmed = element<HTMLInputElement>("#metadataMetaplexCampaignOperatorConfirmed").checked;
|
||||
if (!operatorConfirmed) {
|
||||
throw new Error("La campagne Metaplex exige la confirmation explicite de l’opérateur.");
|
||||
}
|
||||
const request: DemoExecutionMetadataMetaplexTokenMetadataRequest = {
|
||||
profileName,
|
||||
intentId: `desktop-metaplex-${Date.now()}`,
|
||||
operationJson,
|
||||
preflightReadsJson,
|
||||
postconditionReadsJson,
|
||||
submit,
|
||||
const request: DemoExecutionMetadataMetaplexCampaignRequest = {
|
||||
profileName: selectedProfileName,
|
||||
campaignId: campaign.id,
|
||||
operatorConfirmed,
|
||||
materializeAfterConfirmation: element<HTMLInputElement>("#metadataMaterializeInput").checked,
|
||||
};
|
||||
setExecutionRunning(true);
|
||||
const badge = element<HTMLElement>("#metadataStatusBadge");
|
||||
badge.textContent = submit ? "Soumission" : "Simulation";
|
||||
badge.textContent = "Campagne Metaplex";
|
||||
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." });
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "metaplex_campaign", message: `Démarrage de la campagne qualifiée ${campaign.id} — ${campaign.label}.` });
|
||||
try {
|
||||
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", {
|
||||
const summary = await invoke<DemoExecutionMetadataMetaplexCampaignSummaryPayload>("demo_execution_metadata_metaplex_execute_campaign", { request });
|
||||
const executions = JSON.parse(summary.executionsJson) as unknown;
|
||||
const postconditions = JSON.parse(summary.postconditionsJson) as unknown;
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Fixture et setup Metaplex", JSON.parse(summary.fixtureJson)));
|
||||
renderJsonViewer("#metadataSimulationOutput", titled("Synthèse de campagne Metaplex", {
|
||||
profileName: summary.profileName,
|
||||
cluster: summary.cluster,
|
||||
genesisHash: summary.genesisHash,
|
||||
walletPublicKey: summary.walletPublicKey,
|
||||
balanceLamports: summary.balanceLamports,
|
||||
simulationSuccess: summary.simulationSuccess,
|
||||
transactionSignature: summary.transactionSignature,
|
||||
confirmationStatus: summary.confirmationStatus,
|
||||
materializationRequested: summary.materializationRequested,
|
||||
materializedSnapshotCount: summary.materializedSnapshotCount,
|
||||
plan: JSON.parse(summary.planJson),
|
||||
simulation: JSON.parse(summary.simulationJson),
|
||||
campaignId: summary.campaignId,
|
||||
label: summary.label,
|
||||
operationCount: summary.operationCount,
|
||||
confirmedOperationCount: summary.confirmedOperationCount,
|
||||
unavailableOperationCount: summary.unavailableOperationCount,
|
||||
completed: summary.completed,
|
||||
}));
|
||||
renderJsonViewer("#metadataPostconditionsOutput", titled("Postconditions et preuves", JSON.parse(summary.evidenceJson)));
|
||||
badge.textContent = summary.transactionSignature === null ? "Simulé" : "Confirmé";
|
||||
badge.className = summary.simulationSuccess ? "badge text-bg-success" : "badge text-bg-danger";
|
||||
renderJsonViewer("#metadataPostconditionsOutput", titled("Exécutions et postconditions Metaplex", { executions, postconditions }));
|
||||
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Famille</dt><dd class="col-6"><code>metadata.metaplex_token_metadata</code></dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${summary.profileName}</code></dd><dt class="col-6">Campagne</dt><dd class="col-6">${summary.label}</dd><dt class="col-6">Qualification</dt><dd class="col-6"><code>${campaign.qualification}</code></dd><dt class="col-6">Confirmées</dt><dd class="col-6">${summary.confirmedOperationCount}</dd><dt class="col-6">Indisponibles</dt><dd class="col-6">${summary.unavailableOperationCount}</dd><dt class="col-6">Éléments de preuve</dt><dd class="col-6">${summary.operationCount}</dd>`;
|
||||
badge.textContent = summary.completed ? "Campagne qualifié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: "metaplex_campaign", message: `Campagne terminée : confirmed=${summary.confirmedOperationCount}, unavailable=${summary.unavailableOperationCount}, evidence=${summary.operationCount}.` });
|
||||
} finally {
|
||||
setExecutionRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
function selectedSolanaProgramMetadataScenario(): DemoExecutionMetadataSolanaProgramScenarioPayload | null {
|
||||
const id = element<HTMLSelectElement>("#solanaProgramMetadataScenarioSelect").value;
|
||||
return solanaProgramMetadataScenarios.find(scenario => scenario.id === id) ?? null;
|
||||
function renderToken2022MetadataCampaign(): void {
|
||||
const operations = [
|
||||
"initialize_token_metadata",
|
||||
"update_token_metadata_field",
|
||||
"emit_token_metadata",
|
||||
"remove_token_metadata_key",
|
||||
"update_token_metadata_authority",
|
||||
];
|
||||
renderJsonViewer("#token2022MetadataCampaignOutput", titled("Campagne Token-2022 Token Metadata", {
|
||||
family: "metadata.token_2022",
|
||||
operations,
|
||||
fixture: "fresh_token_2022_mint_with_self_metadata_pointer",
|
||||
transactionCount: 6,
|
||||
policy: "simulation_first_then_confirmed_submission",
|
||||
desktopValidation: "confirmed_pre_014",
|
||||
}));
|
||||
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Famille</dt><dd class="col-6"><code>metadata.token_2022</code></dd><dt class="col-6">Programme</dt><dd class="col-6"><code>TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb</code></dd><dt class="col-6">Opérations</dt><dd class="col-6">${operations.length}</dd><dt class="col-6">Transactions</dt><dd class="col-6">6</dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${selectedProfileName ?? "non chargé"}</code></dd><dt class="col-6">Validation desktop</dt><dd class="col-6"><code>confirmed_pre_014</code></dd>`;
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", {
|
||||
family: "metadata.token_2022",
|
||||
operations,
|
||||
fixturePreparation: ["fresh_mint", "metadata_pointer_to_self", "rent_budget_for_metadata_growth"],
|
||||
desktopValidation: "confirmed_pre_014",
|
||||
networkExecutionPerformed: false,
|
||||
}));
|
||||
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
|
||||
policy: "simulation_first",
|
||||
submission: "required",
|
||||
operatorConfirmationRequired: true,
|
||||
stopOnFirstFailure: true,
|
||||
fixtureTransactionCount: 1,
|
||||
metadataTransactionCount: operations.length,
|
||||
desktopValidation: "confirmed_pre_014",
|
||||
networkExecutionPerformed: false,
|
||||
}));
|
||||
renderJsonViewer("#metadataPostconditionsOutput", titled("Postconditions et preuves", {
|
||||
requiredEvidence: ["metadata_pointer", "stateful_snapshot", "rpc_simulation", "confirmed_signature", "canonical_replay", "materialization", "idempotence", "emit_return_data"],
|
||||
operationCount: operations.length,
|
||||
desktopValidation: "confirmed_pre_014",
|
||||
networkEvidenceCollected: false,
|
||||
}));
|
||||
}
|
||||
|
||||
function renderActiveMetadataSelection(): void {
|
||||
if (document.querySelector("#solanaProgramMetadataCollapse.show") !== null) {
|
||||
renderSolanaProgramMetadataScenario();
|
||||
async function executeToken2022MetadataCampaign(): Promise<void> {
|
||||
if (executionRunning) {
|
||||
return;
|
||||
}
|
||||
renderSelectedScenario();
|
||||
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>("#token2022MetadataOperatorConfirmed").checked;
|
||||
if (!operatorConfirmed) {
|
||||
throw new Error("La campagne exige la confirmation explicite de la fixture et des cinq mutations.");
|
||||
}
|
||||
const request: DemoExecutionMetadataToken2022CampaignRequest = {
|
||||
profileName: selectedProfileName,
|
||||
operatorConfirmed,
|
||||
};
|
||||
setExecutionRunning(true);
|
||||
const badge = element<HTMLElement>("#metadataStatusBadge");
|
||||
badge.textContent = "Campagne Token-2022";
|
||||
badge.className = "badge text-bg-warning";
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "token_2022_metadata_campaign", message: "Démarrage : création de la fixture puis cinq opérations Token Metadata." });
|
||||
try {
|
||||
const summary = await invoke<DemoExecutionMetadataToken2022CampaignSummaryPayload>("demo_execution_metadata_token_2022_execute_campaign", { request });
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Fixture Token-2022 Metadata", JSON.parse(summary.fixtureJson)));
|
||||
renderJsonViewer("#metadataSimulationOutput", titled("Synthèse de campagne", {
|
||||
profileName: summary.profileName,
|
||||
mint: summary.mint,
|
||||
initialAuthority: summary.initialAuthority,
|
||||
finalAuthority: summary.finalAuthority,
|
||||
preparationSignature: summary.preparationSignature,
|
||||
stepCount: summary.stepCount,
|
||||
confirmedStepCount: summary.confirmedStepCount,
|
||||
materializationCount: summary.materializationCount,
|
||||
completed: summary.completed,
|
||||
}));
|
||||
renderJsonViewer("#metadataPostconditionsOutput", titled("Preuves ordonnées des cinq opérations", JSON.parse(summary.stepsJson)));
|
||||
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Famille</dt><dd class="col-6"><code>metadata.token_2022</code></dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${summary.profileName}</code></dd><dt class="col-6">Mint</dt><dd class="col-6"><code>${summary.mint}</code></dd><dt class="col-6">Confirmées</dt><dd class="col-6">${summary.confirmedStepCount}/${summary.stepCount}</dd><dt class="col-6">Matérialisations</dt><dd class="col-6">${summary.materializationCount}</dd><dt class="col-6">Autorité finale</dt><dd class="col-6"><code>${summary.finalAuthority}</code></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: "token_2022_metadata_campaign", message: `Campagne terminée : confirmed=${summary.confirmedStepCount}/${summary.stepCount}, materializations=${summary.materializationCount}` });
|
||||
} finally {
|
||||
setExecutionRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSolanaProgramMetadataScenario(): void {
|
||||
@@ -340,26 +344,29 @@ function renderSolanaProgramMetadataScenario(): void {
|
||||
label: scenario.label,
|
||||
fixtureAccount: scenario.fixtureAccount,
|
||||
steps: scenario.steps,
|
||||
desktopValidation: "confirmed_pre_014",
|
||||
}));
|
||||
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>`;
|
||||
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><dt class="col-6">Validation desktop</dt><dd class="col-6"><code>confirmed_pre_014</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,
|
||||
desktopValidation: "confirmed_pre_014",
|
||||
}));
|
||||
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
|
||||
policy: "simulation_first",
|
||||
submission: "required",
|
||||
operatorConfirmationRequired: true,
|
||||
stopOnFirstFailure: true,
|
||||
desktopValidation: "confirmed_pre_014",
|
||||
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",
|
||||
desktopValidation: "confirmed_pre_014",
|
||||
networkEvidenceCollected: false,
|
||||
}));
|
||||
}
|
||||
@@ -414,6 +421,22 @@ async function executeSolanaProgramMetadataCampaign(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function renderActiveMetadataSelection(): void {
|
||||
if (document.querySelector("#token2022MetadataCollapse.show") !== null) {
|
||||
renderToken2022MetadataCampaign();
|
||||
return;
|
||||
}
|
||||
if (document.querySelector("#solanaProgramMetadataCollapse.show") !== null) {
|
||||
renderSolanaProgramMetadataScenario();
|
||||
return;
|
||||
}
|
||||
if (document.querySelector("#metaplexDevnetCollapse.show") !== null) {
|
||||
renderMetaplexCampaign();
|
||||
return;
|
||||
}
|
||||
renderSelectedScenario();
|
||||
}
|
||||
|
||||
async function loadProfiles(): Promise<void> {
|
||||
const options = await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_metadata_options");
|
||||
profileOptions = options.profiles;
|
||||
@@ -481,15 +504,31 @@ async function loadScenarios(): Promise<void> {
|
||||
badge.className = "badge text-bg-warning";
|
||||
try {
|
||||
scenarios = await invoke<DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload[]>("demo_execution_metadata_metaplex_token_metadata_scenarios");
|
||||
metaplexCampaigns = await invoke<DemoExecutionMetadataMetaplexCampaignOptionPayload[]>("demo_execution_metadata_metaplex_campaigns");
|
||||
solanaProgramMetadataScenarios = await invoke<DemoExecutionMetadataSolanaProgramScenarioPayload[]>("demo_execution_metadata_solana_program_scenarios");
|
||||
fillSelect("#metadataSyntheticScenarioSelect", "synthetic");
|
||||
fillSelect("#metadataDevnetScenarioSelect", "network_simulation");
|
||||
const syntheticSelect = element<HTMLSelectElement>("#metadataSyntheticScenarioSelect");
|
||||
syntheticSelect.replaceChildren();
|
||||
for (const scenario of scenarios.filter(candidate => candidate.mode === "synthetic")) {
|
||||
const option = document.createElement("option");
|
||||
option.value = scenario.id;
|
||||
option.textContent = `${scenario.assetFamily} — ${scenario.label}`;
|
||||
syntheticSelect.append(option);
|
||||
}
|
||||
const restored = window.localStorage.getItem(storageKey);
|
||||
for (const selector of ["#metadataSyntheticScenarioSelect", "#metadataDevnetScenarioSelect"]) {
|
||||
const select = element<HTMLSelectElement>(selector);
|
||||
if (restored !== null && Array.from(select.options).some(option => option.value === restored)) {
|
||||
select.value = restored;
|
||||
}
|
||||
if (restored !== null && Array.from(syntheticSelect.options).some(option => option.value === restored)) {
|
||||
syntheticSelect.value = restored;
|
||||
}
|
||||
const metaplexSelect = element<HTMLSelectElement>("#metadataMetaplexCampaignSelect");
|
||||
metaplexSelect.replaceChildren();
|
||||
for (const campaign of metaplexCampaigns) {
|
||||
const option = document.createElement("option");
|
||||
option.value = campaign.id;
|
||||
option.textContent = `${campaign.assetFamily} — ${campaign.label}`;
|
||||
metaplexSelect.append(option);
|
||||
}
|
||||
const restoredMetaplex = window.localStorage.getItem(metaplexCampaignStorageKey);
|
||||
if (restoredMetaplex !== null && Array.from(metaplexSelect.options).some(option => option.value === restoredMetaplex)) {
|
||||
metaplexSelect.value = restoredMetaplex;
|
||||
}
|
||||
const solanaSelect = element<HTMLSelectElement>("#solanaProgramMetadataScenarioSelect");
|
||||
solanaSelect.replaceChildren();
|
||||
@@ -504,8 +543,10 @@ async function loadScenarios(): Promise<void> {
|
||||
solanaSelect.value = restoredSolana;
|
||||
}
|
||||
renderActiveMetadataSelection();
|
||||
badge.textContent = `${scenarios.length + solanaProgramMetadataScenarios.length} contrats`;
|
||||
const syntheticCount = scenarios.filter(candidate => candidate.mode === "synthetic").length;
|
||||
badge.textContent = `${syntheticCount + metaplexCampaigns.length + solanaProgramMetadataScenarios.length + 1} contrats`;
|
||||
badge.className = "badge text-bg-success";
|
||||
setExecutionRunning(executionRunning);
|
||||
} catch (caught) {
|
||||
const message = caught instanceof Error ? caught.message : String(caught);
|
||||
profileStoreReady = false;
|
||||
@@ -518,12 +559,14 @@ async function loadScenarios(): Promise<void> {
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
installFrontendConsoleBridge("kb-app-demo-desktop.frontend.execution_metadata");
|
||||
for (const selector of ["#metadataSyntheticScenarioSelect", "#metadataDevnetScenarioSelect"]) {
|
||||
element<HTMLSelectElement>(selector).addEventListener("change", renderSelectedScenario);
|
||||
}
|
||||
for (const id of ["metaplexSyntheticCollapse", "metaplexDevnetCollapse"]) {
|
||||
element<HTMLElement>(`#${id}`).addEventListener("shown.bs.collapse", renderSelectedScenario);
|
||||
}
|
||||
element<HTMLSelectElement>("#metadataSyntheticScenarioSelect").addEventListener("change", renderSelectedScenario);
|
||||
element<HTMLElement>("#metaplexSyntheticCollapse").addEventListener("shown.bs.collapse", renderSelectedScenario);
|
||||
element<HTMLElement>("#metaplexDevnetCollapse").addEventListener("shown.bs.collapse", renderMetaplexCampaign);
|
||||
element<HTMLSelectElement>("#metadataMetaplexCampaignSelect").addEventListener("change", () => {
|
||||
renderMetaplexCampaign();
|
||||
setExecutionRunning(executionRunning);
|
||||
});
|
||||
element<HTMLElement>("#token2022MetadataCollapse").addEventListener("shown.bs.collapse", renderToken2022MetadataCampaign);
|
||||
element<HTMLElement>("#solanaProgramMetadataCollapse").addEventListener("shown.bs.collapse", renderSolanaProgramMetadataScenario);
|
||||
element<HTMLSelectElement>("#solanaProgramMetadataScenarioSelect").addEventListener("change", renderSolanaProgramMetadataScenario);
|
||||
for (const selector of profileSelectSelectors) {
|
||||
@@ -538,27 +581,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
}
|
||||
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => void prepareProfile());
|
||||
element<HTMLButtonElement>("#prepareToken2022MetadataProfileButton").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();
|
||||
if (scenario !== null) { renderScenarioSteps(scenario); }
|
||||
});
|
||||
element<HTMLButtonElement>("#previousMetadataStepButton").addEventListener("click", () => moveScenarioStep(-1));
|
||||
element<HTMLButtonElement>("#nextMetadataStepButton").addEventListener("click", () => moveScenarioStep(1));
|
||||
element<HTMLTextAreaElement>("#metadataOperationJson").addEventListener("input", () => {
|
||||
preparedScenarioId = null;
|
||||
preparedStepIndex = null;
|
||||
setExecutionRunning(executionRunning);
|
||||
});
|
||||
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").addEventListener("click", () => { void prepareScenarioStep().catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught))); });
|
||||
element<HTMLTextAreaElement>("#metadataExecutionLogOutput").addEventListener("app-log-clear", () => { metadataLogLines.length = 0; });
|
||||
void listen<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<HTMLInputElement>("#metadataMetaplexCampaignOperatorConfirmed").addEventListener("change", () => setExecutionRunning(executionRunning));
|
||||
element<HTMLButtonElement>("#executeMetadataMetaplexCampaignButton").addEventListener("click", () => {
|
||||
void executeMetaplexCampaign().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>("#token2022MetadataOperatorConfirmed").addEventListener("change", () => setExecutionRunning(executionRunning));
|
||||
element<HTMLButtonElement>("#executeToken2022MetadataCampaignButton").addEventListener("click", () => {
|
||||
void executeToken2022MetadataCampaign().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", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs
|
||||
// version: 4
|
||||
// version: 6
|
||||
|
||||
//! Desktop adapters for Metaplex Token Metadata execution demo scenarios.
|
||||
//! Desktop adapters for generic and qualified Metaplex Token Metadata execution workflows.
|
||||
|
||||
/// UI-safe Metaplex scenario row.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
||||
@@ -815,6 +815,876 @@ fn validation_status_code(
|
||||
};
|
||||
}
|
||||
|
||||
// Qualified Metaplex Devnet campaigns.
|
||||
|
||||
/// UI-safe descriptor for one qualified Metaplex 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/DemoExecutionMetadataMetaplexCampaignOptionPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataMetaplexCampaignOptionPayload {
|
||||
/// Stable campaign identifier accepted by the desktop executor.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible campaign label.
|
||||
pub label: std::string::String,
|
||||
/// Primary asset family exercised by the campaign.
|
||||
pub asset_family: std::string::String,
|
||||
/// Target operations qualified by the campaign after any fixture setup.
|
||||
pub operation_names: std::vec::Vec<std::string::String>,
|
||||
/// Stable qualification code shown by the desktop.
|
||||
pub qualification: std::string::String,
|
||||
/// Human-readable fixture/setup contract.
|
||||
pub setup: std::string::String,
|
||||
}
|
||||
|
||||
/// Request for one complete qualified Metaplex 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/DemoExecutionMetadataMetaplexCampaignRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataMetaplexCampaignRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub profile_name: std::string::String,
|
||||
/// Stable campaign identifier returned by the campaign inventory.
|
||||
pub campaign_id: std::string::String,
|
||||
/// Explicit authorization for fixture creation and every campaign submission.
|
||||
pub operator_confirmed: bool,
|
||||
}
|
||||
|
||||
/// UI-safe result of one qualified Metaplex 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/DemoExecutionMetadataMetaplexCampaignSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataMetaplexCampaignSummaryPayload {
|
||||
/// Profile used by the campaign.
|
||||
pub profile_name: std::string::String,
|
||||
/// Stable campaign identifier.
|
||||
pub campaign_id: std::string::String,
|
||||
/// Operator-visible campaign label.
|
||||
pub label: std::string::String,
|
||||
/// Number of Metaplex operation executions or probes retained in this run.
|
||||
pub operation_count: usize,
|
||||
/// Number of Metaplex operations reaching confirmed or finalized state.
|
||||
pub confirmed_operation_count: usize,
|
||||
/// Number of simulation-only runtime-unavailable probes retained in this run.
|
||||
pub unavailable_operation_count: usize,
|
||||
/// Whether the complete campaign reached its expected terminal contract.
|
||||
pub completed: bool,
|
||||
/// Fixture/setup description as formatted JSON.
|
||||
pub fixture_json: std::string::String,
|
||||
/// Ordered Metaplex execution evidence as formatted JSON.
|
||||
pub executions_json: std::string::String,
|
||||
/// Exact campaign postconditions as formatted JSON.
|
||||
pub postconditions_json: std::string::String,
|
||||
}
|
||||
|
||||
struct CampaignProjection {
|
||||
label: std::string::String,
|
||||
fixture: serde_json::Value,
|
||||
executions: std::vec::Vec<serde_json::Value>,
|
||||
postconditions: serde_json::Value,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
type CampaignProjectionFuture<'a> = std::pin::Pin<
|
||||
std::boxed::Box<
|
||||
dyn std::future::Future<Output = kb_core::Result<CampaignProjection>>
|
||||
+ std::marker::Send
|
||||
+ 'a,
|
||||
>,
|
||||
>;
|
||||
|
||||
/// Returns the qualified Metaplex Devnet campaign inventory exposed by the desktop.
|
||||
pub(crate) fn demo_execution_metadata_metaplex_campaigns()
|
||||
-> std::vec::Vec<crate::DemoExecutionMetadataMetaplexCampaignOptionPayload> {
|
||||
let mut values = std::vec::Vec::new();
|
||||
for (id, label, family) in [
|
||||
("create_mint_nft", "Create → Mint — NFT", "nft"),
|
||||
("create_mint_sft", "Create → Mint — SFT", "sft"),
|
||||
("create_mint_fungible", "Create → Mint — Fungible", "fungible"),
|
||||
("create_mint_collection", "Create → Mint — Collection", "collection"),
|
||||
("create_mint_programmable_nft", "Create → Mint — pNFT", "programmable_nft"),
|
||||
] {
|
||||
values.push(crate::DemoExecutionMetadataMetaplexCampaignOptionPayload {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
asset_family: family.to_string(),
|
||||
operation_names:
|
||||
kb_pipeline_demo_scenarios::metaplex_create_mint_campaign_operation_names()
|
||||
.iter()
|
||||
.map(|value| return (*value).to_string())
|
||||
.collect(),
|
||||
qualification: "confirmed".to_string(),
|
||||
setup: "fresh classic SPL mint + operator ATA".to_string(),
|
||||
});
|
||||
}
|
||||
values.extend([
|
||||
campaign_option(
|
||||
"collection_verify",
|
||||
"Collection — Verify → Unverify",
|
||||
"collection",
|
||||
kb_pipeline_demo_scenarios::metaplex_collection_verify_campaign_operation_names(),
|
||||
"confirmed",
|
||||
"fresh collection parent + fresh unverified member",
|
||||
),
|
||||
campaign_option(
|
||||
"print_burn",
|
||||
"NFT imprimable — Print → Burn",
|
||||
"nft",
|
||||
kb_pipeline_demo_scenarios::metaplex_print_burn_campaign_operation_names(),
|
||||
"confirmed",
|
||||
"fresh limited-supply master NFT + fresh printed edition",
|
||||
),
|
||||
campaign_option(
|
||||
"pnft_lifecycle",
|
||||
"pNFT — Delegate → Lock → Unlock → Revoke → Delegate → Transfer",
|
||||
"programmable_nft",
|
||||
kb_pipeline_demo_scenarios::metaplex_pnft_lifecycle_campaign_operation_names(),
|
||||
"confirmed",
|
||||
"fresh pNFT + delegate wallet + destination owner",
|
||||
),
|
||||
campaign_option(
|
||||
"escrow",
|
||||
"Token Owned Escrow — Create → TransferOut → Close",
|
||||
"nft",
|
||||
kb_pipeline_demo_scenarios::metaplex_escrow_campaign_operation_names(),
|
||||
"confirmed",
|
||||
"fresh NFT parent + fresh fungible attribute + escrow ATA deposit",
|
||||
),
|
||||
campaign_option(
|
||||
"maintenance",
|
||||
"Maintenance — Update + probes réservés/legacy",
|
||||
"nft",
|
||||
kb_pipeline_demo_scenarios::metaplex_maintenance_campaign_operation_names(),
|
||||
"mixed_confirmed_unavailable",
|
||||
"fresh NFT; Update submitted; Resize/Migrate/Collect/CloseAccounts simulation-only probes",
|
||||
),
|
||||
campaign_option(
|
||||
"use_probe",
|
||||
"Use — probe runtime",
|
||||
"nft",
|
||||
kb_pipeline_demo_scenarios::metaplex_use_probe_operation_names(),
|
||||
"runtime_unavailable_probe",
|
||||
"fresh NFT with two bounded Multiple uses; Use is simulation-only when rejected",
|
||||
),
|
||||
]);
|
||||
return values;
|
||||
}
|
||||
|
||||
fn campaign_option<const N: usize>(
|
||||
id: &str,
|
||||
label: &str,
|
||||
asset_family: &str,
|
||||
operation_names: &'static [&'static str; N],
|
||||
qualification: &str,
|
||||
setup: &str,
|
||||
) -> crate::DemoExecutionMetadataMetaplexCampaignOptionPayload {
|
||||
return crate::DemoExecutionMetadataMetaplexCampaignOptionPayload {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
asset_family: asset_family.to_string(),
|
||||
operation_names: operation_names.iter().map(|value| return (*value).to_string()).collect(),
|
||||
qualification: qualification.to_string(),
|
||||
setup: setup.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one complete qualified Metaplex Devnet campaign.
|
||||
pub(crate) async fn demo_execution_metadata_metaplex_execute_campaign(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataMetaplexCampaignRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataMetaplexCampaignSummaryPayload,
|
||||
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(
|
||||
"Metaplex qualified 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),
|
||||
};
|
||||
if !profile.wallet.devnet_send_enabled {
|
||||
return std::result::Result::Err(
|
||||
"selected profile does not authorize Devnet submissions required by Metaplex fixtures"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
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 store = match crate::connect_postgres_store(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let workspace_root = crate::workspace_root_dir();
|
||||
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
|
||||
let wallet_dir = if configured_wallet_dir.is_absolute() {
|
||||
configured_wallet_dir
|
||||
} else {
|
||||
workspace_root.join(configured_wallet_dir)
|
||||
};
|
||||
let observer = crate::DemoExecutionMetadataObserver {
|
||||
app_handle,
|
||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||
};
|
||||
tracing::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "execute_metadata_metaplex_campaign",
|
||||
campaign_id = %request.campaign_id,
|
||||
phase = "campaign_start",
|
||||
"start qualified Metaplex desktop campaign"
|
||||
);
|
||||
let projection = match execute_campaign_projection(
|
||||
request.campaign_id.as_str(),
|
||||
wallet_dir,
|
||||
&http_pool,
|
||||
&store,
|
||||
&profile,
|
||||
workspace_root.as_path(),
|
||||
&observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
tracing::error!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "execute_metadata_metaplex_campaign",
|
||||
campaign_id = %request.campaign_id,
|
||||
phase = "campaign_failed",
|
||||
error = %error,
|
||||
"qualified Metaplex desktop campaign failed"
|
||||
);
|
||||
return std::result::Result::Err(error.to_string());
|
||||
},
|
||||
};
|
||||
tracing::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "execute_metadata_metaplex_campaign",
|
||||
campaign_id = %request.campaign_id,
|
||||
phase = "campaign_completed",
|
||||
completed = projection.completed,
|
||||
execution_count = projection.executions.len(),
|
||||
"qualified Metaplex desktop campaign completed"
|
||||
);
|
||||
let confirmed_operation_count = projection
|
||||
.executions
|
||||
.iter()
|
||||
.filter(|value| {
|
||||
return value.get("classification").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some("confirmed");
|
||||
})
|
||||
.count();
|
||||
let unavailable_operation_count = projection
|
||||
.executions
|
||||
.iter()
|
||||
.filter(|value| {
|
||||
return value.get("classification").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some("unavailable");
|
||||
})
|
||||
.count();
|
||||
return std::result::Result::Ok(crate::DemoExecutionMetadataMetaplexCampaignSummaryPayload {
|
||||
profile_name: request.profile_name,
|
||||
campaign_id: request.campaign_id,
|
||||
label: projection.label,
|
||||
operation_count: projection.executions.len(),
|
||||
confirmed_operation_count,
|
||||
unavailable_operation_count,
|
||||
completed: projection.completed,
|
||||
fixture_json: crate::pretty_json(&projection.fixture),
|
||||
executions_json: crate::pretty_json(&projection.executions),
|
||||
postconditions_json: crate::pretty_json(&projection.postconditions),
|
||||
});
|
||||
}
|
||||
|
||||
fn execute_campaign_projection<'a, S, O>(
|
||||
campaign_id: &'a str,
|
||||
wallet_dir: std::path::PathBuf,
|
||||
http_pool: &'a kb_onchain_transport::HttpEndpointPool,
|
||||
store: &'a S,
|
||||
profile: &'a kb_config::ProfileConfig,
|
||||
workspace_root: &'a std::path::Path,
|
||||
observer: &'a O,
|
||||
) -> CampaignProjectionFuture<'a>
|
||||
where
|
||||
S: kb_store::RawTransactionStore
|
||||
+ kb_store::CoreExtractionStore
|
||||
+ kb_store::DecodePipelineStore
|
||||
+ Sync
|
||||
+ 'a,
|
||||
O: kb_pipeline_demo_scenarios::SolanaExecutionObserver + 'a,
|
||||
{
|
||||
return std::boxed::Box::pin(execute_campaign_projection_unboxed(
|
||||
campaign_id,
|
||||
wallet_dir,
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
));
|
||||
}
|
||||
|
||||
async fn execute_campaign_projection_unboxed<S, O>(
|
||||
campaign_id: &str,
|
||||
wallet_dir: std::path::PathBuf,
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<CampaignProjection>
|
||||
where
|
||||
S: kb_store::RawTransactionStore
|
||||
+ kb_store::CoreExtractionStore
|
||||
+ kb_store::DecodePipelineStore
|
||||
+ Sync,
|
||||
O: kb_pipeline_demo_scenarios::SolanaExecutionObserver,
|
||||
{
|
||||
let base_options =
|
||||
kb_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions::new(wallet_dir);
|
||||
return match campaign_id {
|
||||
"create_mint_nft" => {
|
||||
execute_create_mint_projection(
|
||||
"Create → Mint — NFT",
|
||||
base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
|
||||
),
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"create_mint_sft" => {
|
||||
execute_create_mint_projection(
|
||||
"Create → Mint — SFT",
|
||||
base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft,
|
||||
),
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"create_mint_fungible" => {
|
||||
execute_create_mint_projection(
|
||||
"Create → Mint — Fungible",
|
||||
base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible,
|
||||
),
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"create_mint_collection" => {
|
||||
execute_create_mint_projection(
|
||||
"Create → Mint — Collection",
|
||||
base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection,
|
||||
),
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"create_mint_programmable_nft" => {
|
||||
execute_create_mint_projection(
|
||||
"Create → Mint — pNFT",
|
||||
base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
|
||||
),
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"collection_verify" => {
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_metaplex_collection_verify_campaign(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&base_options,
|
||||
observer,
|
||||
).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let executions = vec![
|
||||
metaplex_execution_json("parent.create", &summary.parent.create),
|
||||
metaplex_execution_json("parent.mint", &summary.parent.mint),
|
||||
metaplex_execution_json("member.create", &summary.member.create),
|
||||
metaplex_execution_json("member.mint", &summary.member.mint),
|
||||
metaplex_execution_json("verify", &summary.verify),
|
||||
metaplex_execution_json("unverify", &summary.unverify),
|
||||
];
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "Collection — Verify → Unverify".to_string(),
|
||||
fixture: serde_json::json!({
|
||||
"parent": &summary.parent.fixture,
|
||||
"member": &summary.member.fixture
|
||||
}),
|
||||
completed: executions.iter().all(execution_is_confirmed_json),
|
||||
executions,
|
||||
postconditions: serde_json::json!({
|
||||
"verifiedBefore": summary.state.verified_before,
|
||||
"verifiedAfterVerify": summary.state.verified_after_verify,
|
||||
"verifiedAfterUnverify": summary.state.verified_after_unverify,
|
||||
"collectionSizeBefore": summary.state.collection_size_before,
|
||||
"collectionSizeAfterVerify": summary.state.collection_size_after_verify,
|
||||
"collectionSizeAfterUnverify": summary.state.collection_size_after_unverify
|
||||
}),
|
||||
})
|
||||
},
|
||||
"print_burn" => {
|
||||
let summary =
|
||||
match kb_pipeline_demo_scenarios::execute_devnet_metaplex_print_burn_campaign(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&base_options,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let executions = vec![
|
||||
metaplex_execution_json("master.create", &summary.master.create),
|
||||
metaplex_execution_json("master.mint", &summary.master.mint),
|
||||
metaplex_execution_json("print", &summary.print),
|
||||
metaplex_execution_json("burn", &summary.burn),
|
||||
];
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "NFT imprimable — Print → Burn".to_string(),
|
||||
fixture: serde_json::json!({
|
||||
"master": &summary.master.fixture,
|
||||
"editionMint": &summary.edition_mint,
|
||||
"editionMetadata": &summary.edition_metadata,
|
||||
"edition": &summary.edition,
|
||||
"editionTokenAccount": &summary.edition_token_account,
|
||||
"editionMarker": &summary.edition_marker
|
||||
}),
|
||||
completed: executions.iter().all(execution_is_confirmed_json)
|
||||
&& summary.state.edition_metadata_closed_after_burn
|
||||
&& !summary.state.edition_exists_after_burn
|
||||
&& !summary.state.edition_token_exists_after_burn,
|
||||
executions,
|
||||
postconditions: print_burn_state_json(&summary.state),
|
||||
})
|
||||
},
|
||||
"pnft_lifecycle" => {
|
||||
let options = base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
|
||||
);
|
||||
let summary =
|
||||
match kb_pipeline_demo_scenarios::execute_devnet_metaplex_pnft_lifecycle_campaign(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&options,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let executions = vec![
|
||||
metaplex_execution_json("pnft.create", &summary.pnft.create),
|
||||
metaplex_execution_json("pnft.mint", &summary.pnft.mint),
|
||||
metaplex_execution_json("delegate_staking", &summary.delegate_staking),
|
||||
metaplex_execution_json("lock", &summary.lock),
|
||||
metaplex_execution_json("unlock", &summary.unlock),
|
||||
metaplex_execution_json("revoke", &summary.revoke),
|
||||
metaplex_execution_json("delegate_transfer", &summary.delegate_transfer),
|
||||
metaplex_execution_json("transfer", &summary.transfer),
|
||||
];
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "pNFT lifecycle".to_string(),
|
||||
fixture: serde_json::json!({"pnft": &summary.pnft.fixture}),
|
||||
completed: executions.iter().all(execution_is_confirmed_json)
|
||||
&& summary.state.staking_delegate_revoked
|
||||
&& summary.state.destination_delegate_cleared,
|
||||
executions,
|
||||
postconditions: pnft_state_json(&summary.state),
|
||||
})
|
||||
},
|
||||
"escrow" => {
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_metaplex_escrow_campaign(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&base_options,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let executions = vec![
|
||||
metaplex_execution_json("parent.create", &summary.parent_fixture.create),
|
||||
metaplex_execution_json("parent.mint", &summary.parent_fixture.mint),
|
||||
metaplex_execution_json("attribute.create", &summary.attribute_fixture.create),
|
||||
metaplex_execution_json("attribute.mint", &summary.attribute_fixture.mint),
|
||||
metaplex_execution_json("create_escrow", &summary.create_escrow),
|
||||
metaplex_execution_json("transfer_out", &summary.transfer_out),
|
||||
metaplex_execution_json("close_escrow", &summary.close_escrow),
|
||||
];
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "Token Owned Escrow".to_string(),
|
||||
fixture: serde_json::json!({
|
||||
"parent": &summary.parent_fixture.fixture,
|
||||
"attribute": &summary.attribute_fixture.fixture,
|
||||
"escrow": &summary.escrow,
|
||||
"escrowAttributeTokenAccount": &summary.escrow_attribute_token_account,
|
||||
"setup": {
|
||||
"ataCreationConfirmed": execution_confirmation_is_confirmed(&summary.escrow_attribute_ata_creation.confirmation),
|
||||
"attributeDepositConfirmed": execution_confirmation_is_confirmed(&summary.attribute_deposit.confirmation),
|
||||
"ataMaterializations": summary.escrow_attribute_ata_creation.materializations.len(),
|
||||
"depositMaterializations": summary.attribute_deposit.materializations.len()
|
||||
}
|
||||
}),
|
||||
completed: executions.iter().all(execution_is_confirmed_json)
|
||||
&& summary.state.escrow_attribute_account_closed
|
||||
&& summary.state.escrow_account_closed
|
||||
&& summary.state.parent_token_amount_after_close == 1,
|
||||
executions,
|
||||
postconditions: escrow_state_json(&summary.state),
|
||||
})
|
||||
},
|
||||
"maintenance" => {
|
||||
let options = base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
|
||||
);
|
||||
let summary =
|
||||
match kb_pipeline_demo_scenarios::execute_devnet_metaplex_maintenance_campaign(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&options,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let executions = vec![
|
||||
metaplex_execution_json("fixture.create", &summary.fixture.create),
|
||||
metaplex_execution_json("fixture.mint", &summary.fixture.mint),
|
||||
metaplex_execution_json("update", &summary.update),
|
||||
metaplex_execution_json("resize_probe", &summary.resize_probe),
|
||||
metaplex_execution_json("migrate_probe", &summary.migrate_probe),
|
||||
metaplex_execution_json("collect_probe", &summary.collect_probe),
|
||||
metaplex_execution_json("close_accounts_probe", &summary.close_accounts_probe),
|
||||
];
|
||||
let classifications = executions
|
||||
.iter()
|
||||
.filter_map(|value| {
|
||||
return value.get("classification").and_then(serde_json::Value::as_str);
|
||||
})
|
||||
.collect::<std::vec::Vec<&str>>();
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "Maintenance — Update + probes".to_string(),
|
||||
fixture: serde_json::json!({"fixture": &summary.fixture.fixture}),
|
||||
completed: classifications
|
||||
== vec![
|
||||
"confirmed",
|
||||
"confirmed",
|
||||
"confirmed",
|
||||
"unavailable",
|
||||
"unavailable",
|
||||
"unavailable",
|
||||
"unavailable",
|
||||
]
|
||||
&& !summary.primary_sale_before_update
|
||||
&& summary.primary_sale_after_update,
|
||||
executions,
|
||||
postconditions: serde_json::json!({
|
||||
"primarySaleBeforeUpdate": summary.primary_sale_before_update,
|
||||
"primarySaleAfterUpdate": summary.primary_sale_after_update,
|
||||
"probePolicy": "simulation_only_no_submission_on_failure"
|
||||
}),
|
||||
})
|
||||
},
|
||||
"use_probe" => {
|
||||
let options = base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
|
||||
);
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_metaplex_use_probe(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&options,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut executions = vec![
|
||||
metaplex_execution_json("fixture.create", &summary.fixture.create),
|
||||
metaplex_execution_json("fixture.mint", &summary.fixture.mint),
|
||||
metaplex_execution_json("use_probe", &summary.simulation),
|
||||
];
|
||||
if let std::option::Option::Some(execution) = summary.execution.as_ref() {
|
||||
executions.push(metaplex_execution_json("use_execution", execution));
|
||||
}
|
||||
let status = format!("{:?}", summary.status).to_lowercase();
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "Use — probe runtime".to_string(),
|
||||
fixture: serde_json::json!({"fixture": &summary.fixture.fixture}),
|
||||
completed: executions[0..2].iter().all(execution_is_confirmed_json)
|
||||
&& (status == "runtimeunavailable" || status == "confirmed"),
|
||||
executions,
|
||||
postconditions: serde_json::json!({
|
||||
"status": status,
|
||||
"usesBefore": summary.uses_before,
|
||||
"usesAfter": summary.uses_after,
|
||||
"unavailableReason": summary.unavailable_reason
|
||||
}),
|
||||
})
|
||||
},
|
||||
_ => std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"unknown qualified Metaplex desktop campaign `{campaign_id}`"
|
||||
))),
|
||||
};
|
||||
}
|
||||
|
||||
async fn execute_create_mint_projection<S, O>(
|
||||
label: &str,
|
||||
options: kb_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions,
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<CampaignProjection>
|
||||
where
|
||||
S: kb_store::RawTransactionStore
|
||||
+ kb_store::CoreExtractionStore
|
||||
+ kb_store::DecodePipelineStore
|
||||
+ Sync,
|
||||
O: kb_pipeline_demo_scenarios::SolanaExecutionObserver,
|
||||
{
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_metaplex_create_mint_campaign(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&options,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let executions = vec![
|
||||
metaplex_execution_json("create", &summary.create),
|
||||
metaplex_execution_json("mint", &summary.mint),
|
||||
];
|
||||
return std::result::Result::Ok(CampaignProjection {
|
||||
label: label.to_string(),
|
||||
fixture: serde_json::json!({"fixture": &summary.fixture}),
|
||||
completed: executions.iter().all(execution_is_confirmed_json)
|
||||
&& summary.token_state.supply_after_mint == summary.token_state.expected_amount_raw
|
||||
&& summary.token_state.token_amount_after_mint
|
||||
== summary.token_state.expected_amount_raw,
|
||||
executions,
|
||||
postconditions: create_mint_state_json(&summary.token_state),
|
||||
});
|
||||
}
|
||||
|
||||
fn metaplex_execution_json(
|
||||
label: &str,
|
||||
summary: &kb_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionSummary,
|
||||
) -> serde_json::Value {
|
||||
let classification = if execution_confirmation_is_confirmed(&summary.confirmation) {
|
||||
"confirmed"
|
||||
} else if !summary.simulation.success && summary.send_result.is_none() {
|
||||
"unavailable"
|
||||
} else {
|
||||
"incomplete"
|
||||
};
|
||||
return serde_json::json!({
|
||||
"label": label,
|
||||
"classification": classification,
|
||||
"profileName": &summary.profile_name,
|
||||
"cluster": format!("{:?}", summary.cluster).to_lowercase(),
|
||||
"genesisHash": &summary.genesis_hash,
|
||||
"walletPublicKey": &summary.wallet.public_key,
|
||||
"balanceLamports": summary.balance_lamports.to_string(),
|
||||
"plan": &summary.plan,
|
||||
"statefulPreflight": &summary.stateful_preflight,
|
||||
"simulation": &summary.simulation,
|
||||
"simulationContextSlot": summary.simulation_context_slot,
|
||||
"readiness": &summary.readiness,
|
||||
"sendResult": &summary.send_result,
|
||||
"confirmation": &summary.confirmation,
|
||||
"before": &summary.before,
|
||||
"after": &summary.after,
|
||||
"materializationRequested": summary.materialization_requested,
|
||||
"materializedSnapshots": &summary.materialized_snapshots,
|
||||
"instructionMaterializations": &summary.materializations,
|
||||
"postExecution": &summary.post_execution,
|
||||
"pipelineEvidence": {
|
||||
"canonicalHydration": summary.backfill.is_some(),
|
||||
"coreExtraction": summary.core_extraction.is_some(),
|
||||
"decodeReplay": summary.decode_replay.is_some(),
|
||||
"idempotenceReplay": summary.idempotence_replay.is_some()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn execution_confirmation_is_confirmed(
|
||||
confirmation: &std::option::Option<kb_lib::ExApiExecutionConfirmationResult>,
|
||||
) -> bool {
|
||||
return confirmation.as_ref().is_some_and(|value| {
|
||||
return matches!(
|
||||
value.status,
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn execution_is_confirmed_json(value: &serde_json::Value) -> bool {
|
||||
return value.get("classification").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some("confirmed");
|
||||
}
|
||||
|
||||
fn create_mint_state_json(
|
||||
state: &kb_pipeline_demo_scenarios::DevnetMetaplexCreateMintTokenState,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"expectedAmountRaw": state.expected_amount_raw,
|
||||
"supplyBeforeMint": state.supply_before_mint,
|
||||
"tokenAmountBeforeMint": state.token_amount_before_mint,
|
||||
"tokenAccountStateBeforeMint": state.token_account_state_before_mint,
|
||||
"supplyAfterMint": state.supply_after_mint,
|
||||
"tokenAmountAfterMint": state.token_amount_after_mint,
|
||||
"tokenAccountStateAfterMint": state.token_account_state_after_mint
|
||||
});
|
||||
}
|
||||
|
||||
fn print_burn_state_json(
|
||||
state: &kb_pipeline_demo_scenarios::DevnetMetaplexPrintBurnState,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"editionNumber": state.edition_number,
|
||||
"masterSupplyBeforePrint": state.master_supply_before_print,
|
||||
"masterSupplyAfterPrint": state.master_supply_after_print,
|
||||
"masterSupplyAfterBurn": state.master_supply_after_burn,
|
||||
"masterMaxSupply": state.master_max_supply,
|
||||
"editionSupplyAfterPrint": state.edition_supply_after_print,
|
||||
"editionSupplyAfterBurn": state.edition_supply_after_burn,
|
||||
"editionTokenAmountAfterPrint": state.edition_token_amount_after_print,
|
||||
"editionTokenStateAfterPrint": state.edition_token_state_after_print,
|
||||
"editionTakenAfterPrint": state.edition_taken_after_print,
|
||||
"editionMetadataExistsAfterBurn": state.edition_metadata_exists_after_burn,
|
||||
"editionMetadataFeeTombstoneAfterBurn": state.edition_metadata_fee_tombstone_after_burn,
|
||||
"editionMetadataClosedAfterBurn": state.edition_metadata_closed_after_burn,
|
||||
"editionExistsAfterBurn": state.edition_exists_after_burn,
|
||||
"editionTokenExistsAfterBurn": state.edition_token_exists_after_burn,
|
||||
"editionMarkerExistsAfterBurn": state.edition_marker_exists_after_burn
|
||||
});
|
||||
}
|
||||
|
||||
fn pnft_state_json(
|
||||
state: &kb_pipeline_demo_scenarios::DevnetMetaplexPnftLifecycleState,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"delegate": &state.delegate,
|
||||
"destinationOwner": &state.destination_owner,
|
||||
"destinationTokenAccount": &state.destination_token_account,
|
||||
"destinationTokenRecord": &state.destination_token_record,
|
||||
"initialState": &state.initial_state,
|
||||
"stakingDelegateRole": &state.staking_delegate_role,
|
||||
"lockedState": &state.locked_state,
|
||||
"unlockedState": &state.unlocked_state,
|
||||
"stakingDelegateRevoked": state.staking_delegate_revoked,
|
||||
"transferDelegateRole": &state.transfer_delegate_role,
|
||||
"sourceTokenAmountAfterTransfer": state.source_token_amount_after_transfer,
|
||||
"sourceTokenStateAfterTransfer": &state.source_token_state_after_transfer,
|
||||
"destinationTokenAmountAfterTransfer": state.destination_token_amount_after_transfer,
|
||||
"destinationTokenStateAfterTransfer": &state.destination_token_state_after_transfer,
|
||||
"destinationTokenRecordState": &state.destination_token_record_state,
|
||||
"destinationDelegateCleared": state.destination_delegate_cleared
|
||||
});
|
||||
}
|
||||
|
||||
fn escrow_state_json(
|
||||
state: &kb_pipeline_demo_scenarios::DevnetMetaplexEscrowCampaignState,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"depositAmountRaw": state.deposit_amount_raw,
|
||||
"operatorAttributeAmountBeforeDeposit": state.operator_attribute_amount_before_deposit,
|
||||
"operatorAttributeAmountAfterDeposit": state.operator_attribute_amount_after_deposit,
|
||||
"escrowAttributeAmountAfterDeposit": state.escrow_attribute_amount_after_deposit,
|
||||
"operatorAttributeAmountAfterTransferOut": state.operator_attribute_amount_after_transfer_out,
|
||||
"escrowAttributeAccountClosed": state.escrow_attribute_account_closed,
|
||||
"escrowAccountClosed": state.escrow_account_closed,
|
||||
"parentTokenAmountAfterClose": state.parent_token_amount_after_close
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
@@ -865,13 +1735,16 @@ mod tests {
|
||||
let source = std::include_str!("../frontend/demo_execution_metadata.html");
|
||||
for id in [
|
||||
"metadataProfileSelect",
|
||||
"metadataDevnetScenarioSelect",
|
||||
"metadataStepSelect",
|
||||
"prepareMetadataScenarioStepButton",
|
||||
"metadataSyntheticScenarioSelect",
|
||||
"metadataMetaplexCampaignSelect",
|
||||
"metadataMetaplexCampaignOperatorConfirmed",
|
||||
"executeMetadataMetaplexCampaignButton",
|
||||
"metadataExecutionLogOutput",
|
||||
"metadataProfileHelp",
|
||||
"simulateMetadataButton",
|
||||
"submitMetadataButton",
|
||||
"metadataExecutionDetailsAccordion",
|
||||
"metadataPreflightCollapse",
|
||||
"metadataSimulationCollapse",
|
||||
"metadataPostconditionsCollapse",
|
||||
] {
|
||||
assert!(source.contains(format!("id=\"{id}\"").as_str()));
|
||||
}
|
||||
@@ -890,3 +1763,42 @@ mod tests {
|
||||
assert!(payloads.iter().any(|payload| return payload.requires_programmable_rules));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod campaign_tests {
|
||||
#[test]
|
||||
fn qualified_campaign_inventory_is_exact_and_keeps_unavailable_operations_inside_probes() {
|
||||
let campaigns = crate::demo_execution_metadata_metaplex_campaigns();
|
||||
assert_eq!(campaigns.len(), 11);
|
||||
let ids = campaigns
|
||||
.iter()
|
||||
.map(|value| return value.id.as_str())
|
||||
.collect::<std::collections::BTreeSet<&str>>();
|
||||
assert_eq!(ids.len(), campaigns.len());
|
||||
let maintenance = campaigns.iter().find(|value| return value.id == "maintenance");
|
||||
assert!(
|
||||
maintenance
|
||||
.is_some_and(|value| return value.qualification == "mixed_confirmed_unavailable")
|
||||
);
|
||||
let use_probe = campaigns.iter().find(|value| return value.id == "use_probe");
|
||||
assert!(
|
||||
use_probe
|
||||
.is_some_and(|value| return value.qualification == "runtime_unavailable_probe")
|
||||
);
|
||||
let source = std::include_str!("demo_execution_metadata_metaplex_token_metadata.rs");
|
||||
assert!(source.contains("Box::pin(execute_campaign_projection_unboxed("));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_frontend_uses_qualified_metaplex_campaign_controls() {
|
||||
let source = std::include_str!("../frontend/demo_execution_metadata.html");
|
||||
for id in [
|
||||
"metadataMetaplexCampaignSelect",
|
||||
"metadataMetaplexCampaignOutput",
|
||||
"metadataMetaplexCampaignOperatorConfirmed",
|
||||
"executeMetadataMetaplexCampaignButton",
|
||||
] {
|
||||
assert!(source.contains(format!("id=\"{id}\"").as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
224
kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs
Normal file
224
kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
// file: kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs
|
||||
// version: 1
|
||||
|
||||
//! Thin desktop adapter for the complete Token-2022 Token Metadata Devnet campaign.
|
||||
|
||||
/// Request for one complete Token-2022 Token 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/DemoExecutionMetadataToken2022CampaignRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataToken2022CampaignRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub profile_name: std::string::String,
|
||||
/// Explicit authorization for fixture creation and all five metadata mutations.
|
||||
pub operator_confirmed: bool,
|
||||
}
|
||||
|
||||
/// UI-safe result of one complete Token-2022 Token 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/DemoExecutionMetadataToken2022CampaignSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataToken2022CampaignSummaryPayload {
|
||||
/// Profile used by the campaign.
|
||||
pub profile_name: std::string::String,
|
||||
/// Fresh Token-2022 mint containing the embedded metadata extension.
|
||||
pub mint: std::string::String,
|
||||
/// Initial update authority, equal to the selected profile wallet.
|
||||
pub initial_authority: std::string::String,
|
||||
/// Fresh authority installed by the final campaign step.
|
||||
pub final_authority: std::string::String,
|
||||
/// Confirmed signature that created the Token-2022 fixture mint.
|
||||
pub preparation_signature: std::string::String,
|
||||
/// Number of Token Metadata interface operations executed.
|
||||
pub step_count: usize,
|
||||
/// Number of operations reaching confirmed or finalized state.
|
||||
pub confirmed_step_count: usize,
|
||||
/// Total number of instruction materializations produced by the five operations.
|
||||
pub materialization_count: usize,
|
||||
/// Whether the complete five-step campaign and every postcondition succeeded.
|
||||
pub completed: bool,
|
||||
/// Complete fixture description as formatted JSON.
|
||||
pub fixture_json: std::string::String,
|
||||
/// Ordered execution and postcondition evidence as formatted JSON.
|
||||
pub steps_json: std::string::String,
|
||||
}
|
||||
|
||||
/// Executes the complete Token-2022 Token Metadata Devnet campaign.
|
||||
pub(crate) async fn demo_execution_metadata_token_2022_execute_campaign(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataToken2022CampaignRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataToken2022CampaignSummaryPayload,
|
||||
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(
|
||||
"Token-2022 Token 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 store = match crate::connect_postgres_store(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let workspace_root = crate::workspace_root_dir();
|
||||
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
|
||||
let wallet_dir = if configured_wallet_dir.is_absolute() {
|
||||
configured_wallet_dir
|
||||
} else {
|
||||
workspace_root.join(configured_wallet_dir)
|
||||
};
|
||||
let mut options =
|
||||
kb_pipeline_demo_scenarios::Token2022MetadataFixturePreparationOptions::new(wallet_dir);
|
||||
options.operator_confirmed = true;
|
||||
let observer = crate::DemoExecutionMetadataObserver {
|
||||
app_handle,
|
||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||
};
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_token_2022_metadata_campaign(
|
||||
&http_pool,
|
||||
&store,
|
||||
&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(request.profile_name, summary));
|
||||
}
|
||||
|
||||
fn campaign_summary_payload(
|
||||
profile_name: std::string::String,
|
||||
summary: kb_pipeline_demo_scenarios::DevnetToken2022MetadataCampaignSummary,
|
||||
) -> crate::DemoExecutionMetadataToken2022CampaignSummaryPayload {
|
||||
let mut confirmed_step_count = 0_usize;
|
||||
let mut materialization_count = 0_usize;
|
||||
let mut step_values = std::vec::Vec::with_capacity(summary.steps.len());
|
||||
for step in &summary.steps {
|
||||
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);
|
||||
}
|
||||
materialization_count =
|
||||
materialization_count.saturating_add(step.execution.materializations.len());
|
||||
step_values.push(serde_json::json!({
|
||||
"stepId": &step.step_id,
|
||||
"operationCode": &step.operation_code,
|
||||
"simulation": &step.execution.simulation,
|
||||
"sendResult": &step.execution.send_result,
|
||||
"confirmation": &step.execution.confirmation,
|
||||
"statefulPreflight": &step.execution.stateful_preflight,
|
||||
"statefulSnapshot": &step.stateful_snapshot,
|
||||
"emitEvidence": &step.emit_evidence,
|
||||
"postcondition": &step.postcondition,
|
||||
"postExecution": &step.execution.post_execution,
|
||||
"materializations": &step.execution.materializations
|
||||
}));
|
||||
}
|
||||
let expected_step_count =
|
||||
kb_pipeline_demo_scenarios::token_2022_metadata_campaign_operation_names().len();
|
||||
let completed = summary.steps.len() == expected_step_count
|
||||
&& confirmed_step_count == expected_step_count
|
||||
&& summary.steps.iter().all(|step| {
|
||||
return step.postcondition.status
|
||||
== kb_pipeline::Token2022ExecutionPostconditionStatus::Confirmed;
|
||||
});
|
||||
let fixture_json = crate::pretty_json(&serde_json::json!({
|
||||
"mint": &summary.fixture.mint,
|
||||
"initialAuthority": &summary.fixture.initial_authority,
|
||||
"finalAuthority": &summary.fixture.final_authority,
|
||||
"mintSpace": summary.fixture.mint_space,
|
||||
"rentBudgetSpace": summary.fixture.rent_budget_space,
|
||||
"rentReserveLamports": summary.fixture.rent_reserve_lamports.to_string(),
|
||||
"preparationSignature": &summary.fixture.preparation_signature,
|
||||
"initialSnapshot": &summary.fixture.initial_snapshot
|
||||
}));
|
||||
return crate::DemoExecutionMetadataToken2022CampaignSummaryPayload {
|
||||
profile_name,
|
||||
mint: summary.fixture.mint.0.clone(),
|
||||
initial_authority: summary.fixture.initial_authority.0.clone(),
|
||||
final_authority: summary.fixture.final_authority.0.clone(),
|
||||
preparation_signature: summary.fixture.preparation_signature.clone(),
|
||||
step_count: summary.steps.len(),
|
||||
confirmed_step_count,
|
||||
materialization_count,
|
||||
completed,
|
||||
fixture_json,
|
||||
steps_json: crate::pretty_json(&step_values),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn desktop_campaign_exposes_the_exact_five_token_metadata_operations() {
|
||||
assert_eq!(
|
||||
kb_pipeline_demo_scenarios::token_2022_metadata_campaign_operation_names(),
|
||||
&[
|
||||
"initialize_token_metadata",
|
||||
"update_token_metadata_field",
|
||||
"emit_token_metadata",
|
||||
"remove_token_metadata_key",
|
||||
"update_token_metadata_authority",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_frontend_contains_token_2022_campaign_controls() {
|
||||
let source = std::include_str!("../frontend/demo_execution_metadata.html");
|
||||
for id in [
|
||||
"token2022MetadataProfileSelect",
|
||||
"token2022MetadataCampaignOutput",
|
||||
"token2022MetadataOperatorConfirmed",
|
||||
"executeToken2022MetadataCampaignButton",
|
||||
] {
|
||||
assert!(source.contains(format!("id=\"{id}\"").as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/lib.rs
|
||||
// version: 31
|
||||
// version: 34
|
||||
|
||||
//! Tauri desktop demo application for `khadhroony-bot3`.
|
||||
|
||||
@@ -17,6 +17,7 @@ mod demo_devnet_common;
|
||||
mod demo_execution_metadata;
|
||||
mod demo_execution_metadata_metaplex_token_metadata;
|
||||
mod demo_execution_metadata_solana_program;
|
||||
mod demo_execution_metadata_token_2022;
|
||||
mod demo_execution_solana_core;
|
||||
mod demo_execution_spl;
|
||||
mod demo_http;
|
||||
@@ -137,6 +138,12 @@ pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataObserver;
|
||||
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;
|
||||
/// UI-safe descriptor for one qualified Metaplex Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexCampaignOptionPayload;
|
||||
/// Request for one complete qualified Metaplex Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexCampaignRequest;
|
||||
/// UI-safe result of one complete qualified Metaplex Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexCampaignSummaryPayload;
|
||||
/// 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.
|
||||
@@ -147,6 +154,10 @@ pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecut
|
||||
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 the qualified Metaplex Devnet campaign inventory.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::demo_execution_metadata_metaplex_campaigns;
|
||||
/// Executes one complete qualified Metaplex Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::demo_execution_metadata_metaplex_execute_campaign;
|
||||
/// 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.
|
||||
@@ -171,6 +182,12 @@ pub(crate) use self::demo_execution_metadata_solana_program::DemoExecutionMetada
|
||||
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 for the complete Token-2022 Token Metadata Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_token_2022::DemoExecutionMetadataToken2022CampaignRequest;
|
||||
/// UI-safe result of the complete Token-2022 Token Metadata Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_token_2022::DemoExecutionMetadataToken2022CampaignSummaryPayload;
|
||||
/// Executes the complete Token-2022 Token Metadata Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_token_2022::demo_execution_metadata_token_2022_execute_campaign;
|
||||
/// 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/tauri.rs
|
||||
// version: 32
|
||||
// version: 34
|
||||
|
||||
//! Tauri runtime assembly and private command wrappers.
|
||||
|
||||
@@ -90,6 +90,8 @@ pub fn run() -> kb_core::Result<()> {
|
||||
demo_spl_token_2022_fixture,
|
||||
open_demo_execution_metadata_window,
|
||||
demo_execution_metadata_metaplex_token_metadata_scenarios,
|
||||
demo_execution_metadata_metaplex_campaigns,
|
||||
demo_execution_metadata_metaplex_execute_campaign,
|
||||
demo_execution_metadata_options,
|
||||
demo_execution_metadata_metaplex_token_metadata_current_operations,
|
||||
demo_execution_metadata_metaplex_token_metadata_operation_template,
|
||||
@@ -98,6 +100,7 @@ pub fn run() -> kb_core::Result<()> {
|
||||
demo_execution_metadata_metaplex_token_metadata_execute,
|
||||
demo_execution_metadata_solana_program_scenarios,
|
||||
demo_execution_metadata_solana_program_execute_campaign,
|
||||
demo_execution_metadata_token_2022_execute_campaign,
|
||||
]);
|
||||
builder = builder.on_window_event(|window, event| {
|
||||
if window.label() != "main" || !matches!(event, tauri::WindowEvent::Destroyed) {
|
||||
@@ -638,6 +641,25 @@ fn demo_execution_metadata_metaplex_token_metadata_scenarios() -> std::result::R
|
||||
return crate::demo_execution_metadata_metaplex_token_metadata_scenarios();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_metaplex_campaigns()
|
||||
-> std::vec::Vec<crate::DemoExecutionMetadataMetaplexCampaignOptionPayload> {
|
||||
return crate::demo_execution_metadata_metaplex_campaigns();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_metadata_metaplex_execute_campaign(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataMetaplexCampaignRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataMetaplexCampaignSummaryPayload,
|
||||
std::string::String,
|
||||
> {
|
||||
return crate::demo_execution_metadata_metaplex_execute_campaign(app_handle, state, request)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_metaplex_token_metadata_current_operations()
|
||||
-> std::vec::Vec<std::string::String> {
|
||||
@@ -723,6 +745,19 @@ async fn demo_execution_metadata_solana_program_execute_campaign(
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_metadata_token_2022_execute_campaign(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataToken2022CampaignRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataToken2022CampaignSummaryPayload,
|
||||
std::string::String,
|
||||
> {
|
||||
return crate::demo_execution_metadata_token_2022_execute_campaign(app_handle, state, request)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_spl_token_execute(
|
||||
app_handle: tauri::AppHandle,
|
||||
|
||||
Reference in New Issue
Block a user