v0.4.7-pre.013
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: Cargo.toml
|
||||
# version: 19
|
||||
# version: 20
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
@@ -18,7 +18,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.7-pre.12"
|
||||
version = "0.4.7-pre.13"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/DEVNET_EXECUTION_GUIDE.md -->
|
||||
<!-- version: 20 -->
|
||||
<!-- version: 22 -->
|
||||
|
||||
# Guide d’exécution Devnet
|
||||
|
||||
@@ -14,7 +14,8 @@ Ce guide décrit la campagne Devnet de `kb-app-demo-desktop` et `kb-pipeline-dem
|
||||
5. SPL Associated Token Account — mint Token-2022 ;
|
||||
6. SPL Token classique ;
|
||||
7. Token-2022 ;
|
||||
8. registre ElGamal.
|
||||
8. registre ElGamal ;
|
||||
9. Metaplex Token Metadata — préparation de fixture, simulation puis soumission explicite.
|
||||
|
||||
Les commandes réutilisables sont centralisées en section 4. Chaque scénario référence leur identifiant `Cxx` au lieu de les recopier.
|
||||
|
||||
@@ -791,3 +792,21 @@ La campagne est clôturable uniquement lorsque :
|
||||
- le replay ciblé est sans échec ni erreur de traitement ;
|
||||
- les projections sont présentes et idempotentes ;
|
||||
- le présent guide reflète les commandes réellement exécutées.
|
||||
|
||||
## 10. Metaplex Token Metadata
|
||||
|
||||
Dans `demo_execution_metadata`, sélectionner un profil Devnet puis préparer sa base. Le panneau charge le même inventaire de profils que les démonstrations Solana Core et SPL.
|
||||
|
||||
Pour `Create`, utiliser **Préparer la fixture Create**. Cette action :
|
||||
|
||||
1. charge le wallet persistant du profil ;
|
||||
2. crée ou réutilise un mint SPL classique à zéro décimale ;
|
||||
3. dérive automatiquement les PDA `metadata` et `master edition` ;
|
||||
4. génère l’intent typé prêt à simuler.
|
||||
|
||||
Les placeholders entre chevrons ne sont jamais des valeurs exécutables. Toujours lancer une simulation RPC avant d’activer la soumission et la confirmation opérateur. Le journal du panneau et les blocs JSON titrés constituent les preuves applicatives immédiates ; la signature, le slot et les postconditions sont requis pour déclarer une validation réseau.
|
||||
|
||||
|
||||
### Paramètres communs du panneau Metadata
|
||||
|
||||
Le panneau Metaplex utilise le même modèle opérateur que Solana Core et SPL : profil Devnet préparé, limites de dépense visibles, journal commun avec une seule barre `Copier` / `Effacer`, bouton de simulation distinct du bouton de soumission et confirmation opérateur séparée. Une soumission construit un plan avec `dry_run = false`, mais la transaction ne peut être signée ou envoyée qu'après réussite de la simulation exacte du même message.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/guides/DEVNET_VALIDATION.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 3 -->
|
||||
|
||||
# Guide de validation Devnet
|
||||
|
||||
@@ -63,3 +63,14 @@ Le registre ElGamal ne doit pas être déclaré validé sur Devnet ou Mainnet sa
|
||||
- `docs/DEVNET_EXECUTION_GUIDE.md` ;
|
||||
- `kb-pipeline-demo-scenarios/USAGE.md` ;
|
||||
- `kb-app-demo-desktop/USAGE.md`.
|
||||
|
||||
## Metaplex Token Metadata
|
||||
|
||||
Les scénarios Metadata utilisent un profil Devnet existant, le runner de `kb-pipeline-demo-scenarios` et le panneau `demo_execution_metadata`. Les PDA dérivables ne doivent pas être saisis manuellement. Une fixture `Create` prépare le mint et dérive les PDA canoniques.
|
||||
|
||||
Une liste de profils vide est une erreur de configuration ou de raccordement et doit être signalée explicitement. Une validation réseau exige une simulation RPC réelle ; une soumission exige en plus confirmation opérateur, signature, confirmation et postconditions observées.
|
||||
|
||||
|
||||
### Paramètres communs du panneau Metadata
|
||||
|
||||
Le panneau Metaplex utilise le même modèle opérateur que Solana Core et SPL : profil Devnet préparé, limites de dépense visibles, journal commun avec une seule barre `Copier` / `Effacer`, bouton de simulation distinct du bouton de soumission et confirmation opérateur séparée. Une soumission construit un plan avec `dry_run = false`, mais la transaction ne peut être signée ou envoyée qu'après réussite de la simulation exacte du même message.
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<!-- file: docs/validation/V0_4_7_PRE_013_METAPLEX_DESKTOP_DEVNET_EXECUTION.md -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# Validation `0.4.7-pre.013` — exécution Metadata Devnet desktop
|
||||
|
||||
## Périmètre
|
||||
|
||||
- adaptateurs Tauri minces vers `kb-pipeline-demo-scenarios` ;
|
||||
- inventaire des 20 opérations courantes ;
|
||||
- modèles JSON typés disponibles pour les parcours nommés ;
|
||||
- simulation RPC Devnet réelle par défaut ;
|
||||
- soumission uniquement avec `submit` et `operatorConfirmed` ;
|
||||
- affichage distinct du plan, du préflight, de la simulation et des preuves stateful ;
|
||||
- maintien des scénarios synthétiques.
|
||||
|
||||
## Contrats de sécurité
|
||||
|
||||
- profil obligatoirement classé Devnet ;
|
||||
- opérations dépréciées rejetées par le runner réutilisable ;
|
||||
- intent et lectures stateful désérialisés côté Rust ;
|
||||
- une campagne Devnet déjà active bloque une seconde exécution ;
|
||||
- aucune preuve réseau n’est produite par les scénarios synthétiques.
|
||||
|
||||
## Correctif `pre.013-fix-001`
|
||||
|
||||
- correction de la conversion de `MdSignature` vers la chaîne UI en lisant explicitement la valeur du newtype ;
|
||||
- aucun changement du contrat réseau, du runner ou des DTO ;
|
||||
- compilation et validations locales à rejouer après application.
|
||||
|
||||
## Validation locale attendue
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --all-targets
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
cargo test -p kb-app-demo-desktop
|
||||
cargo test -p kb-pipeline-demo-scenarios
|
||||
cargo tauri dev -c kb-app-demo-desktop/tauri.conf.json
|
||||
```
|
||||
|
||||
La validation manuelle doit remplacer les placeholders par des comptes Devnet réels, lancer une simulation puis, pour un parcours maîtrisé, une soumission explicitement confirmée. Les signatures, slots et postconditions doivent être conservés comme preuves avant la clôture.
|
||||
|
||||
## Correctif guide, fixture Create et profils Devnet
|
||||
|
||||
- ajout du guide intégré et du journal d’exécution dans le panneau Metadata ;
|
||||
- ajout des titres des blocs JSON ;
|
||||
- ajout du préparateur de fixture `Create` avec mint SPL classique et PDA dérivés ;
|
||||
- ajout des dépendances directes `mpl-token-metadata` et `solana-pubkey` dans `kb-pipeline-demo-scenarios` ;
|
||||
- chargement des profils via une commande Metadata dédiée ;
|
||||
- préparation automatique du profil sélectionné, comme dans les panneaux Solana Core et SPL ;
|
||||
- diagnostic explicite lorsqu’aucun profil Devnet compatible n’est retourné.
|
||||
|
||||
## Correctif d’initialisation frontend
|
||||
|
||||
- les listes de profils, contrats et opérations restaient vides parce que l’initialisation `DOMContentLoaded` échouait avant les appels Tauri ;
|
||||
- le TypeScript attendait `clearMetadataLogButton` et `metadataExecutionLogOutput`, absents du HTML ;
|
||||
- les deux contrôles sont ajoutés au panneau ;
|
||||
- un test de contrat vérifie désormais la présence de tous les identifiants nécessaires au chargement initial.
|
||||
|
||||
|
||||
## Correctif fix-005
|
||||
|
||||
- suppression de la duplication des contrôles Copier/Effacer du journal ;
|
||||
- ajout d’un panneau de paramètres communs Devnet avec limites du profil ;
|
||||
- séparation des boutons Simuler et Soumettre ;
|
||||
- autorisation de construire un plan de soumission avec `dry_run = false`, la simulation exacte restant obligatoire avant signature et envoi ;
|
||||
- maintien du refus de soumission sans confirmation opérateur.
|
||||
|
||||
## Correctif `pre.013-fix-006` — print supply NFT
|
||||
|
||||
La campagne Devnet a atteint le programme Metaplex et exécuté l’instruction `Create` en simulation réelle. Le programme a refusé le modèle avec l’erreur `Print supply is required for non-fungibles` (`Custom(166)`) parce que `print_supply` était `null`.
|
||||
|
||||
Le modèle nommé et le préparateur de fixture utilisent désormais `PrintSupply::Zero`, sérialisé sous la forme JSON `"Zero"`, pour le scénario `NonFungible`. Un test empêche la régression. La simulation et la soumission doivent être rejouées après application ; aucune soumission réussie n’est déclarée dans ce rapport.
|
||||
## Correctif `pre.013-fix-007` — programme SPL Token requis
|
||||
|
||||
La simulation réelle suivante a dépassé la validation de `PrintSupply::Zero`, puis le programme Metaplex a refusé l’instruction avec `Missing SPL token program` (`Custom(149)`). Le transport RPC, la compilation du message, l’estimation des frais et l’appel `simulateTransaction` ont donc été confirmés ; aucune soumission n’a eu lieu.
|
||||
|
||||
Le modèle nommé et le préparateur de fixture `Create` renseignent désormais explicitement le programme SPL Token classique canonique :
|
||||
|
||||
```text
|
||||
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
|
||||
```
|
||||
|
||||
Les tests vérifient conjointement `print_supply = "Zero"` et ce Program ID. La campagne doit être rejouée en simulation avant toute soumission.
|
||||
|
||||
|
||||
## Correctif `fix-008` — préconditions réseau par opération
|
||||
|
||||
- la fixture `Create` utilise un nouveau mint versionné créé avec freeze authority active ;
|
||||
- owner, layout classique, décimales, supply, mint authority et freeze authority sont vérifiés avant génération de l’intent ;
|
||||
- une opération sans modèle nommé efface l’intent courant et bloque simulation/soumission, au lieu de réutiliser le JSON précédent ;
|
||||
- les requirements Metaplex sont documentés dans `kb-lib/USAGE.md` et `kb-pipeline/USAGE.md` ;
|
||||
- les simulations observées avant ce correctif ont toutes échoué et ne sont pas déclarées validées.
|
||||
|
||||
## Correctif 009 — parcours cohérents et matérialisation optionnelle
|
||||
|
||||
La validation desktop n’utilise plus implicitement une opération isolée sans contexte de fixture. L’inventaire expose cinq parcours : NFT classique, SFT, fungible, collection parent/membre et pNFT. Chaque parcours précise la fixture, l’état initial, l’état terminal, les opérations applicables et les projections attendues.
|
||||
|
||||
La matérialisation est désormais une option opérateur indépendante. Après confirmation, elle exige des lectures de postcondition, décode les comptes Metaplex bornés et produit les projections canoniques correspondantes. Elle ne télécharge pas les URI off-chain.
|
||||
|
||||
La fixture NFT a été versionnée en `create-mint-profile-authority-v3.json` afin de ne pas réutiliser le mint déjà soumis. La mint authority et la freeze authority doivent être le wallet du profil, tandis que le keypair du mint ne sert qu’à signer la création du compte mint.
|
||||
|
||||
## Correctif `0.4.7-pre.013-fix-010` — compatibilité de la CLI `spl-token`
|
||||
|
||||
La CLI locale utilisée pour les validations ne prend pas en charge l’option `create-token --freeze-authority`. Le préparateur utilise donc le parcours compatible suivant :
|
||||
|
||||
1. création du mint classique à `0` décimale avec `--enable-freeze` ;
|
||||
2. transfert explicite de l’autorité `freeze` depuis le keypair du mint vers le wallet du profil ;
|
||||
3. transfert explicite de l’autorité `mint` vers le même wallet ;
|
||||
4. relecture du compte et validation stricte des deux autorités avant génération de l’intent Metaplex.
|
||||
|
||||
La fixture est versionnée en `create-mint-profile-authority-v4.json` afin de ne pas réutiliser un mint absent ou partiellement préparé par le correctif précédent.
|
||||
|
||||
### 0.4.7-pre.13 fix-011
|
||||
|
||||
- remplace la préparation Metaplex fondée sur `solana-keygen`, `solana` et `spl-token` par une transaction Rust native ;
|
||||
- crée le mint par `SystemCreateAccount` puis `SPL Token InitializeMint2` dans un même message simulé, signé et confirmé ;
|
||||
- utilise explicitement le wallet du profil comme mint authority et freeze authority ;
|
||||
- conserve le keypair du mint dans `kb-wallet` sans exposer ni déléguer ses secrets à une CLI externe.
|
||||
|
||||
|
||||
## Correctif fonctionnel fix-014
|
||||
|
||||
Le panneau n’expose plus les cinq parcours Devnet comme s’ils étaient tous préparables. Le premier parcours réellement exécutable est le NFT classique `create -> update`. La préparation d’étape génère conjointement l’intent, les lectures avant confirmation et les lectures après confirmation utilisées par la matérialisation. Les parcours collection, SFT, fungible et pNFT restent présents dans la matrice synthétique cible et devront être activés seulement après ajout de leurs fixtures complètes.
|
||||
@@ -1,8 +1,57 @@
|
||||
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
|
||||
<!-- version: 12 -->
|
||||
<!-- version: 21 -->
|
||||
|
||||
# CHANGELOG — kb-app-demo-desktop
|
||||
|
||||
## 0.4.7-pre.013-fix-015
|
||||
|
||||
- aligne les tests desktop sur le nombre réel de scénarios synthétiques et Devnet exposés par les registres réutilisables ;
|
||||
- conserve explicitement au moins un scénario collection et un scénario programmable dans l’inventaire ;
|
||||
- borne les lectures Metadata et Edition à `65536` octets conformément au contrat `getAccountInfo` ;
|
||||
- sérialise les toggles Metaplex `Collection`, `CollectionDetails`, `Uses` et `RuleSet` avec la variante `None` au lieu de `null` dans l’étape Update.
|
||||
|
||||
## 0.4.7-pre.013-fix-014
|
||||
|
||||
- remplace le flux global opération/modèle/fixture Create par une préparation contextuelle de l’étape du scénario ;
|
||||
- remplit automatiquement l’intent et les lectures stateful avant/après ;
|
||||
- ajoute la navigation étape précédente/suivante et refuse l’exécution d’un intent périmé ;
|
||||
- rend la matérialisation utilisable sans saisie manuelle des comptes de postcondition ;
|
||||
- expose uniquement le parcours Devnet NFT actuellement entièrement préparé (`create` puis `update`).
|
||||
|
||||
### 0.4.7-pre.13 fix-011
|
||||
|
||||
- remplace la préparation Metaplex fondée sur `solana-keygen`, `solana` et `spl-token` par une transaction Rust native ;
|
||||
- crée le mint par `SystemCreateAccount` puis `SPL Token InitializeMint2` dans un même message simulé, signé et confirmé ;
|
||||
- utilise explicitement le wallet du profil comme mint authority et freeze authority ;
|
||||
- conserve le keypair du mint dans `kb-wallet` sans exposer ni déléguer ses secrets à une CLI externe.
|
||||
|
||||
### 0.4.7-pre.013-fix.009
|
||||
|
||||
- affiche les parcours, fixtures et états Metaplex dans le panneau Devnet ;
|
||||
- ajoute l’option de matérialisation des snapshots confirmés ;
|
||||
- expose le nombre de projections produites dans les résultats.
|
||||
|
||||
## 0.4.7-pre.13-fix.008 — les fixtures `Create` utilisent un nouveau mint à freeze authority active, valident owner/layout/décimales/supply/autorités avant simulation et empêchent la réutilisation d’un intent précédent lorsqu’aucun modèle nommé n’existe.
|
||||
|
||||
## 0.4.7-pre.13-fix.005 — alignement du panneau Metadata sur les paramètres communs Devnet, boutons simulation/soumission séparés et correction du contrat dry-run après simulation.-fix.4
|
||||
|
||||
- correction de l’initialisation du panneau Metadata : ajout des contrôles HTML du journal attendus par le TypeScript ;
|
||||
- ajout d’un test de contrat empêchant la disparition des sélecteurs de profil, scénario, opération et journal.
|
||||
|
||||
## 0.4.7-pre.13-fix.002
|
||||
|
||||
- Ajout du guide intégré, du journal d’exécution et de titres explicites pour les résultats JSON Metaplex.
|
||||
- Ajout d’un préparateur idempotent de fixture `Create` : mint SPL classique Devnet, PDA metadata et master edition dérivés, intent JSON prêt à simuler.
|
||||
|
||||
## 0.4.7-pre.013 — exécution Metadata Devnet réelle
|
||||
|
||||
- ajout des commandes Tauri minces pour inventorier les opérations courantes, générer les modèles typés et appeler le runner Devnet réutilisable ;
|
||||
- ajout de la simulation RPC réelle et de la soumission doublement autorisée depuis le panneau Metadata ;
|
||||
- affichage séparé du plan, du préflight, de la simulation, de la signature, de la confirmation et des états avant/après ;
|
||||
- conservation parallèle des scénarios synthétiques ;
|
||||
- ajout des bindings TS-RS de requête et de résultat.
|
||||
- correction de l’extraction de la signature réseau depuis le newtype `MdSignature` sans dépendre de `Display`.
|
||||
|
||||
## 0.4.7-pre.011 — réouverture des démonstrations Devnet
|
||||
|
||||
### Documentation
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/README.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# kb-app-demo-desktop
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- diagnostics PostgreSQL ;
|
||||
- backfill, extraction Core et replay ;
|
||||
- démonstrations Solana Core, SPL Memo, SPL Token, ATA et Token-2022 ;
|
||||
- panneau Metadata avec scénarios synthétiques, sélection du profil Devnet, préparation de la base et affichage structuré par le JSON viewer commun ;
|
||||
- panneau Metadata avec scénarios synthétiques, préparation du profil et de la base, intents typés, simulation RPC Devnet réelle et soumission explicitement confirmée ;
|
||||
- progression, annulation et restauration de l’état des fenêtres.
|
||||
|
||||
## Frontière architecturale
|
||||
@@ -23,10 +23,12 @@ Les scénarios UI Devnet/Testnet restent dans le desktop. Des tests automatisés
|
||||
|
||||
## Statut Metaplex `0.4.7`
|
||||
|
||||
Le panneau distingue correctement les contrats synthétiques et le contexte Devnet. Les exécutions RPC réelles des 20 opérations courantes seront raccordées pendant `pre.013`, après achèvement des campagnes réutilisables de `pre.011` et `pre.012`.
|
||||
Le panneau conserve les scénarios synthétiques et appelle directement le runner réutilisable de `kb-pipeline-demo-scenarios` pour les opérations Metaplex courantes. La simulation RPC est réelle ; la soumission exige deux autorisations distinctes dans la requête desktop.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Guide d’utilisation](USAGE.md)
|
||||
- [Travaux restant à réaliser](TODO.md)
|
||||
- [Historique des changements](CHANGELOG.md)
|
||||
|
||||
- Le parcours Metaplex desktop fournit un guide intégré, un journal, des résultats JSON titrés et un préparateur de fixture `Create`.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<!-- file: kb-app-demo-desktop/TODO.md -->
|
||||
<!-- version: 6 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# TODO — kb-app-demo-desktop
|
||||
|
||||
## `0.4.7-pre.013` — démonstrations Metadata Devnet
|
||||
|
||||
- [ ] Commandes Tauri - raccorder les campagnes Metaplex Devnet réutilisables sans dupliquer leur logique.
|
||||
- [ ] Interface - permettre simulation et soumission explicitement confirmée pour les opérations courantes disponibles.
|
||||
- [ ] Contexte - afficher profil, cluster, base, wallet, mode, signature, slot et preuves de postcondition.
|
||||
- [ ] Cohérence - conserver en parallèle les scénarios synthétiques et empêcher qu’ils soient présentés comme preuves réseau.
|
||||
- [x] Commandes Tauri - raccorder les campagnes Metaplex Devnet réutilisables sans dupliquer leur logique.
|
||||
- [x] Interface - permettre simulation et soumission explicitement confirmée pour les opérations courantes disponibles.
|
||||
- [x] Contexte - afficher profil, cluster, base, wallet, mode, signature, slot et preuves de postcondition.
|
||||
- [x] Cohérence - conserver en parallèle les scénarios synthétiques et empêcher qu’ils soient présentés comme preuves réseau.
|
||||
- [ ] Validation manuelle - exécuter les parcours disponibles et confirmer la cohérence préflight, simulation, confirmation, soumission et postconditions.
|
||||
|
||||
## Versions ultérieures
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/USAGE.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Utilisation de kb-app-demo-desktop
|
||||
|
||||
@@ -194,6 +194,36 @@ console.log(scenarios);
|
||||
|
||||
La simulation et l’envoi doivent rester explicitement distingués dans les requêtes.
|
||||
|
||||
### Exécution Metadata Devnet
|
||||
|
||||
Le panneau Metadata charge les opérations courantes depuis `demo_execution_metadata_current_operations` et peut demander un modèle avec `demo_execution_metadata_operation_template`.
|
||||
|
||||
```typescript
|
||||
const operationJson = await invoke(
|
||||
"demo_execution_metadata_operation_template",
|
||||
{ operation: "verify" }
|
||||
);
|
||||
|
||||
const summary = await invoke(
|
||||
"demo_execution_metadata_execute",
|
||||
{
|
||||
request: {
|
||||
profileName: "devnet",
|
||||
intentId: `desktop-metaplex-${Date.now()}`,
|
||||
operationJson,
|
||||
preflightReadsJson: "[]",
|
||||
postconditionReadsJson: "[]",
|
||||
submit: false,
|
||||
operatorConfirmed: false
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(summary);
|
||||
```
|
||||
|
||||
Les valeurs `<...>` d’un modèle doivent être remplacées par des comptes réels avant exécution. `submit: true` exige simultanément `operatorConfirmed: true`. Le résultat distingue le plan, le préflight stateful, la simulation RPC, la signature éventuelle, la confirmation et les états avant/après.
|
||||
|
||||
### Diagnostics PostgreSQL
|
||||
|
||||
```typescript
|
||||
@@ -258,3 +288,7 @@ Depuis la fenêtre principale, choisir **Exécution Devnet → Exécution Metada
|
||||
Un contrat marqué Devnet n’est pas, à lui seul, une exécution réseau. Les champs `networkExecutionPerformed` et `networkEvidenceCollected` doivent rester faux tant qu’aucun appel RPC réel et aucune preuve correspondante n’ont été produits.
|
||||
|
||||
Les commandes de simulation et de soumission Metaplex seront documentées ici après leur raccordement effectif pendant `0.4.7-pre.013`.
|
||||
|
||||
### Préparation de la fixture Metaplex `Create`
|
||||
|
||||
Le panneau desktop peut créer ou réutiliser un mint SPL classique Devnet à zéro décimale. Le préparateur dérive ensuite automatiquement les PDA `metadata` et `master edition` et génère un intent `Create` typé. Les valeurs entre chevrons des modèles génériques restent des placeholders non exécutables.
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
"windows": [
|
||||
"splash",
|
||||
"main",
|
||||
"demo_backfill",
|
||||
"demo_config",
|
||||
"demo_http",
|
||||
"demo_ws",
|
||||
"demo_backfill",
|
||||
"demo_core_extraction",
|
||||
"demo_decode_replay",
|
||||
"demo_sql_diag",
|
||||
"demo_sql_pg_raw",
|
||||
"demo_sql_pg_core",
|
||||
"demo_sql_replay_candidates",
|
||||
"demo_config",
|
||||
"demo_core_extraction",
|
||||
"demo_decode_replay",
|
||||
"demo_execution_solana_core",
|
||||
"demo_execution_spl",
|
||||
"demo_execution_metadata"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/frontend/demo_execution_metadata.html -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 9 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -24,6 +24,23 @@
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container-fluid px-4 py-4">
|
||||
<div class="alert alert-info">Cette fenêtre accueille les familles d’exécution metadata. Metaplex Token Metadata est la première famille ; SPL Token Metadata sera ajouté ultérieurement sans changer la frontière de la fenêtre.</div>
|
||||
<div class="accordion shadow-sm mb-4" id="metadataGuideAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header"><button class="accordion-button collapsed" data-bs-target="#metadataGuideCollapse" data-bs-toggle="collapse" type="button">Guide d’exécution Metaplex Devnet</button></h2>
|
||||
<div class="accordion-collapse collapse" id="metadataGuideCollapse" data-bs-parent="#metadataGuideAccordion">
|
||||
<div class="accordion-body">
|
||||
<ol class="mb-2">
|
||||
<li>Choisir un profil Devnet puis préparer sa base PostgreSQL.</li>
|
||||
<li>Choisir le parcours Devnet puis son étape. Le parcours actuellement exécutable couvre <code>create</code> puis <code>update</code> sur un NFT classique.</li>
|
||||
<li>Cliquer sur <strong>Préparer l’étape courante</strong> : l’intent, les lectures stateful et les postconditions sont générés ensemble.</li>
|
||||
<li>Lancer d’abord une simulation. La soumission exige les deux confirmations explicites.</li>
|
||||
<li>Contrôler le journal, le plan exact, la simulation RPC, la signature éventuelle et les états avant/après.</li>
|
||||
</ol>
|
||||
<div class="small text-muted">Les chaînes entre chevrons sont des placeholders non exécutables. Ne jamais les soumettre telles quelles.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="accordion shadow-sm" id="metadataExecutionAccordion">
|
||||
@@ -49,13 +66,41 @@
|
||||
<div class="accordion-collapse collapse" id="metaplexDevnetCollapse" data-bs-parent="#metaplexScenarioModeAccordion">
|
||||
<div class="accordion-body">
|
||||
<label class="form-label" for="metadataProfileSelect">Profil Devnet</label>
|
||||
<select class="form-select mb-3" id="metadataProfileSelect"></select>
|
||||
<select class="form-select" id="metadataProfileSelect"></select>
|
||||
<div class="form-text mb-3" id="metadataProfileHelp">Chargement du profil Devnet.</div>
|
||||
<label class="form-label" for="metadataDevnetScenarioSelect">Contrat de scénario</label>
|
||||
<select class="form-select" id="metadataDevnetScenarioSelect"></select>
|
||||
<div class="d-grid mt-3"><button class="btn btn-outline-primary" id="prepareMetadataProfileButton" type="button">Charger le profil et préparer la base</button></div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-3"><span class="small text-muted">État du profil</span><span class="badge text-bg-secondary" id="metadataProfileStatusBadge">Non chargé</span></div>
|
||||
<div class="mt-3" id="metadataProfileOutput"></div>
|
||||
<div class="alert alert-warning mt-3 mb-0">Cette version prépare réellement le profil et sa base Devnet, mais n’exécute pas encore de transaction Metaplex. Les contrats affichés ne constituent pas une preuve de simulation réseau.</div>
|
||||
<label class="form-label mt-3" for="metadataStepSelect">Étape du parcours</label>
|
||||
<select class="form-select" id="metadataStepSelect"></select>
|
||||
<div class="d-flex gap-2 mt-2">
|
||||
<button class="btn btn-outline-secondary flex-fill" id="previousMetadataStepButton" type="button">Étape précédente</button>
|
||||
<button class="btn btn-outline-secondary flex-fill" id="nextMetadataStepButton" type="button">Étape suivante</button>
|
||||
</div>
|
||||
<div class="d-grid mt-2"><button class="btn btn-outline-success" id="prepareMetadataScenarioStepButton" type="button">Préparer l’étape courante</button></div>
|
||||
<div class="form-text">Le scénario remplit ensemble l’intent, les lectures avant/après et les cibles de matérialisation. Les étapes non encore préparables sont refusées explicitement.</div>
|
||||
<label class="form-label mt-3" for="metadataOperationJson">Intent Metaplex typé</label>
|
||||
<textarea class="form-control font-monospace" id="metadataOperationJson" rows="12" spellcheck="false"></textarea>
|
||||
<label class="form-label mt-3" for="metadataPreflightReadsJson">Lectures stateful avant simulation</label>
|
||||
<textarea class="form-control font-monospace" id="metadataPreflightReadsJson" rows="4" spellcheck="false">[]</textarea>
|
||||
<label class="form-label mt-3" for="metadataPostconditionReadsJson">Lectures stateful après confirmation</label>
|
||||
<textarea class="form-control font-monospace" id="metadataPostconditionReadsJson" rows="4" spellcheck="false">[]</textarea>
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" id="metadataMaterializeInput" type="checkbox" checked>
|
||||
<label class="form-check-label" for="metadataMaterializeInput">Matérialiser les snapshots Metaplex confirmés</label>
|
||||
<div class="form-text">Après confirmation, relit les comptes déclarés, les décode canoniquement et produit les projections d’état. Aucun fetch URI off-chain.</div>
|
||||
</div>
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" id="metadataOperatorConfirmed" type="checkbox">
|
||||
<label class="form-check-label" for="metadataOperatorConfirmed">Je confirme explicitement la soumission</label>
|
||||
</div>
|
||||
<div class="d-grid gap-2 mt-3">
|
||||
<button class="btn btn-outline-primary" id="simulateMetadataButton" type="button">Simuler sur Devnet</button>
|
||||
<button class="btn btn-primary" id="submitMetadataButton" type="button">Soumettre sur Devnet</button>
|
||||
</div>
|
||||
<div class="alert alert-warning mt-3 mb-0">La simulation appelle réellement le RPC Devnet. La soumission reste désactivée tant que l’opérateur ne l’autorise et ne la confirme pas explicitement.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,6 +112,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-2">Journal d’exécution</h2>
|
||||
<textarea class="form-control font-monospace" id="metadataExecutionLogOutput" readonly rows="12" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Exigences</h2>
|
||||
@@ -78,19 +129,19 @@
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Préflight et opérations</h2>
|
||||
<div id="metadataPreflightOutput"></div>
|
||||
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataPreflightOutput" readonly rows="14">{"title":"Préflight et opérations","status":"idle","message":"Aucun résultat."}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Simulation et confirmation</h2>
|
||||
<div id="metadataSimulationOutput"></div>
|
||||
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataSimulationOutput" readonly rows="16">{"title":"Simulation et confirmation","status":"idle","message":"Aucune simulation."}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Postconditions et preuves</h2>
|
||||
<div id="metadataPostconditionsOutput"></div>
|
||||
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataPostconditionsOutput" readonly rows="16">{"title":"Postconditions et preuves","status":"idle","message":"Aucune preuve."}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,47 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
|
||||
// version: 3
|
||||
import "bootstrap";
|
||||
// version: 10
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { frontendError } from "./frontend_log.ts";
|
||||
import { installFrontendConsoleBridge } from "./frontend_log.ts";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts";
|
||||
import { renderJsonViewer } from "./json_viewer.ts";
|
||||
import type { DemoExecutionMetadataScenarioPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataScenarioPayload.ts";
|
||||
import type { DemoExecutionMetadataPreparedStepPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataPreparedStepPayload.ts";
|
||||
import type { DemoExecutionMetadataRequest } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataRequest.ts";
|
||||
import type { DemoExecutionMetadataSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSummaryPayload.ts";
|
||||
import type { DemoExecutionDevnetStoreReadinessPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionDevnetStoreReadinessPayload.ts";
|
||||
import type { DemoExecutionSolanaCoreOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreOptionsPayload.ts";
|
||||
import type { DemoExecutionSolanaCoreProfileOption } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreProfileOption.ts";
|
||||
|
||||
const storageKey = "kb-app-demo-desktop.metadata.selected-scenario";
|
||||
const profileStorageKey = "kb-app-demo-desktop.metadata.selected-profile";
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
let scenarios: DemoExecutionMetadataScenarioPayload[] = [];
|
||||
let selectedProfileName: string | null = null;
|
||||
let executionRunning = false;
|
||||
let profileStoreReady = false;
|
||||
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
|
||||
let preparedScenarioId: string | null = null;
|
||||
let preparedStepIndex: number | null = null;
|
||||
const metadataLogLines: string[] = [];
|
||||
const maximumMetadataLogLines = 1200;
|
||||
|
||||
type MetadataProgressPayload = { timestamp: string; level: string; stage: string; message: string; signature?: string | null };
|
||||
|
||||
function appendMetadataLog(payload: MetadataProgressPayload): void {
|
||||
const signature = payload.signature ? ` signature=${payload.signature}` : "";
|
||||
metadataLogLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`);
|
||||
if (metadataLogLines.length > maximumMetadataLogLines) {
|
||||
metadataLogLines.splice(0, metadataLogLines.length - maximumMetadataLogLines);
|
||||
}
|
||||
const output = element<HTMLTextAreaElement>("#metadataExecutionLogOutput");
|
||||
output.value = metadataLogLines.join("\n");
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
function titled(title: string, value: unknown): unknown { return { title, value }; }
|
||||
|
||||
function element<T extends HTMLElement>(selector: string): T {
|
||||
const value = document.querySelector<T>(selector);
|
||||
@@ -38,11 +66,37 @@ function fillSelect(selector: string, mode: string): void {
|
||||
for (const scenario of scenarios.filter(candidate => candidate.mode === mode)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = scenario.id;
|
||||
option.textContent = `${scenario.assetFamily} — ${scenario.id}`;
|
||||
option.textContent = `${scenario.assetFamily} — ${scenario.label}`;
|
||||
select.append(option);
|
||||
}
|
||||
}
|
||||
|
||||
function operationName(operationCode: string): string {
|
||||
const parts = operationCode.split(".");
|
||||
return parts.length === 0 ? operationCode : parts[parts.length - 1] ?? operationCode;
|
||||
}
|
||||
|
||||
function renderScenarioSteps(scenario: DemoExecutionMetadataScenarioPayload): void {
|
||||
const select = element<HTMLSelectElement>("#metadataStepSelect");
|
||||
const previousValue = Number.parseInt(select.value, 10);
|
||||
select.replaceChildren();
|
||||
scenario.operationCodes.forEach((operationCode, index) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = String(index);
|
||||
option.textContent = `${index + 1}. ${operationName(operationCode)}`;
|
||||
select.append(option);
|
||||
});
|
||||
if (Number.isInteger(previousValue) && previousValue >= 0 && previousValue < scenario.operationCodes.length) {
|
||||
select.value = String(previousValue);
|
||||
}
|
||||
const index = Number.parseInt(select.value, 10);
|
||||
element<HTMLButtonElement>("#previousMetadataStepButton").disabled = !Number.isInteger(index) || index <= 0;
|
||||
element<HTMLButtonElement>("#nextMetadataStepButton").disabled = !Number.isInteger(index) || index >= scenario.operationCodes.length - 1;
|
||||
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").textContent = Number.isInteger(index) && scenario.operationCodes[index] !== undefined
|
||||
? `Préparer l’étape ${index + 1} — ${operationName(scenario.operationCodes[index])}`
|
||||
: "Préparer l’étape courante";
|
||||
}
|
||||
|
||||
function renderSelectedScenario(): void {
|
||||
const scenario = selectedScenario();
|
||||
if (scenario === null) {
|
||||
@@ -50,32 +104,188 @@ function renderSelectedScenario(): void {
|
||||
}
|
||||
window.localStorage.setItem(storageKey, scenario.id);
|
||||
const networkMode = scenario.mode !== "synthetic";
|
||||
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Famille</dt><dd class="col-6"><code>${scenario.assetFamily}</code></dd><dt class="col-6">Mode</dt><dd class="col-6"><code>${scenario.mode}</code></dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${networkMode ? selectedProfileName ?? "non chargé" : "non applicable"}</code></dd><dt class="col-6">Collection</dt><dd class="col-6">${scenario.requiresCollection ? "requise" : "non requise"}</dd><dt class="col-6">Rule set</dt><dd class="col-6">${scenario.requiresProgrammableRules ? "requis" : "non requis"}</dd><dt class="col-6">Validation</dt><dd class="col-6"><code>${scenario.validationStatus}</code></dd>`;
|
||||
renderJsonViewer("#metadataPreflightOutput", {
|
||||
renderScenarioSteps(scenario);
|
||||
preparedScenarioId = null;
|
||||
preparedStepIndex = null;
|
||||
element<HTMLTextAreaElement>("#metadataOperationJson").value = "";
|
||||
element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value = "[]";
|
||||
element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value = "[]";
|
||||
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Parcours</dt><dd class="col-6">${scenario.label}</dd><dt class="col-6">Famille</dt><dd class="col-6"><code>${scenario.assetFamily}</code></dd><dt class="col-6">Fixture</dt><dd class="col-6"><code>${scenario.fixtureKind}</code></dd><dt class="col-6">État initial</dt><dd class="col-6"><code>${scenario.initialState}</code></dd><dt class="col-6">État attendu</dt><dd class="col-6"><code>${scenario.resultingState}</code></dd><dt class="col-6">Mode</dt><dd class="col-6"><code>${scenario.mode}</code></dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${networkMode ? selectedProfileName ?? "non chargé" : "non applicable"}</code></dd><dt class="col-6">Collection</dt><dd class="col-6">${scenario.requiresCollection ? "requise" : "non requise"}</dd><dt class="col-6">Rule set</dt><dd class="col-6">${scenario.requiresProgrammableRules ? "requis" : "non requis"}</dd><dt class="col-6">Validation</dt><dd class="col-6"><code>${scenario.validationStatus}</code></dd>`;
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", {
|
||||
scenarioId: scenario.id,
|
||||
operationCodes: scenario.operationCodes,
|
||||
checks: ["program_id", "owner", "pda_and_seeds", "required_accounts", "authorities", "stateful_correlations"],
|
||||
networkExecutionPerformed: false,
|
||||
});
|
||||
renderJsonViewer("#metadataSimulationOutput", {
|
||||
}));
|
||||
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
|
||||
mode: scenario.mode,
|
||||
policy: "simulation_first",
|
||||
networkExecutionPerformed: false,
|
||||
automaticSubmission: false,
|
||||
operatorConfirmationRequired: true,
|
||||
expectedState: scenario.validationStatus,
|
||||
note: networkMode ? "Le contrat Devnet est sélectionné, mais aucune transaction Metaplex n'est simulée par cette commande de chargement." : "Scénario synthétique déterministe.",
|
||||
});
|
||||
renderJsonViewer("#metadataPostconditionsOutput", {
|
||||
note: networkMode ? "Renseignez un intent typé puis lancez une simulation RPC réelle." : "Scénario synthétique déterministe.",
|
||||
}));
|
||||
renderJsonViewer("#metadataPostconditionsOutput", titled("Postconditions et preuves", {
|
||||
required: scenario.requiresPostcondition,
|
||||
evidenceKinds: scenario.requiredEvidence,
|
||||
networkEvidenceCollected: false,
|
||||
materializationTargets: scenario.materializationTargets,
|
||||
rule: "Aucune validation réseau n'est déclarée sans simulation RPC réelle, signature éventuelle, slot et postconditions.",
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function prepareScenarioStep(): Promise<void> {
|
||||
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
|
||||
const scenario = selectedScenario();
|
||||
const stepIndex = Number.parseInt(element<HTMLSelectElement>("#metadataStepSelect").value, 10);
|
||||
if (profileName.length === 0) { throw new Error("Sélectionnez un profil Devnet."); }
|
||||
if (scenario === null || scenario.mode === "synthetic") { throw new Error("Sélectionnez un scénario Devnet."); }
|
||||
if (!Number.isInteger(stepIndex) || stepIndex < 0) { throw new Error("Sélectionnez une étape valide."); }
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "fixture", message: `Préparation de ${scenario.id} étape ${stepIndex + 1}` });
|
||||
const prepared = await invoke<DemoExecutionMetadataPreparedStepPayload>("demo_execution_metadata_prepare_step", {
|
||||
profileName,
|
||||
scenarioId: scenario.id,
|
||||
stepIndex,
|
||||
previousOperationJson: element<HTMLTextAreaElement>("#metadataOperationJson").value.trim().length === 0
|
||||
? null
|
||||
: element<HTMLTextAreaElement>("#metadataOperationJson").value,
|
||||
});
|
||||
element<HTMLTextAreaElement>("#metadataOperationJson").value = prepared.operationJson;
|
||||
element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value = prepared.preflightReadsJson;
|
||||
element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value = prepared.postconditionReadsJson;
|
||||
preparedScenarioId = prepared.scenarioId;
|
||||
preparedStepIndex = prepared.stepIndex;
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Étape de scénario préparée", {
|
||||
scenarioId: prepared.scenarioId,
|
||||
stepIndex: prepared.stepIndex,
|
||||
operation: prepared.operation,
|
||||
fixtureState: prepared.fixtureState,
|
||||
preflightReads: JSON.parse(prepared.preflightReadsJson),
|
||||
postconditionReads: JSON.parse(prepared.postconditionReadsJson),
|
||||
simulationReady: prepared.simulationReady,
|
||||
submissionReady: prepared.submissionReady,
|
||||
}));
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "fixture", message: `${prepared.label} prête (${prepared.fixtureState})` });
|
||||
setExecutionRunning(executionRunning);
|
||||
}
|
||||
|
||||
function moveScenarioStep(offset: number): void {
|
||||
const select = element<HTMLSelectElement>("#metadataStepSelect");
|
||||
const current = Number.parseInt(select.value, 10);
|
||||
const next = current + offset;
|
||||
if (Number.isInteger(next) && next >= 0 && next < select.options.length) {
|
||||
select.value = String(next);
|
||||
const scenario = selectedScenario();
|
||||
if (scenario !== null) { renderScenarioSteps(scenario); }
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonText(value: string, label: string): unknown {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (caught) {
|
||||
const message = caught instanceof Error ? caught.message : String(caught);
|
||||
throw new Error(`${label} invalide : ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function selectedProfile(): DemoExecutionSolanaCoreProfileOption | null {
|
||||
const name = element<HTMLSelectElement>("#metadataProfileSelect").value;
|
||||
return profileOptions.find(profile => profile.name === name) ?? null;
|
||||
}
|
||||
|
||||
function updateProfileHelp(): void {
|
||||
const profile = selectedProfile();
|
||||
const help = element<HTMLElement>("#metadataProfileHelp");
|
||||
if (profile === null) {
|
||||
help.textContent = "Aucun profil Devnet compatible.";
|
||||
return;
|
||||
}
|
||||
help.textContent = `wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled ? "enabled" : "disabled"}`;
|
||||
}
|
||||
|
||||
function setExecutionRunning(value: boolean): void {
|
||||
executionRunning = value;
|
||||
const profile = selectedProfile();
|
||||
const missingOperation = element<HTMLTextAreaElement>("#metadataOperationJson").value.trim().length === 0;
|
||||
const unavailable = value || profile === null || !profileStoreReady || missingOperation;
|
||||
element<HTMLButtonElement>("#simulateMetadataButton").disabled = unavailable;
|
||||
element<HTMLButtonElement>("#submitMetadataButton").disabled = unavailable || !profile?.sendEnabled;
|
||||
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").disabled = value || profile === null || !profileStoreReady;
|
||||
}
|
||||
|
||||
async function executeMetadata(submit: boolean): Promise<void> {
|
||||
if (executionRunning) {
|
||||
return;
|
||||
}
|
||||
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
|
||||
if (profileName.length === 0 || !profileStoreReady) {
|
||||
throw new Error("Sélectionnez et préparez un profil Devnet.");
|
||||
}
|
||||
const operationJson = element<HTMLTextAreaElement>("#metadataOperationJson").value;
|
||||
const scenario = selectedScenario();
|
||||
const stepIndex = Number.parseInt(element<HTMLSelectElement>("#metadataStepSelect").value, 10);
|
||||
if (operationJson.trim().length === 0) {
|
||||
throw new Error("Préparez l’étape courante avant l’exécution.");
|
||||
}
|
||||
if (scenario === null || preparedScenarioId !== scenario.id || preparedStepIndex !== stepIndex) {
|
||||
throw new Error("Préparez de nouveau l’étape après toute modification du scénario ou de l’étape.");
|
||||
}
|
||||
const preflightReadsJson = element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value;
|
||||
const postconditionReadsJson = element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value;
|
||||
parseJsonText(operationJson, "Intent Metaplex");
|
||||
parseJsonText(preflightReadsJson, "Lectures de préflight");
|
||||
parseJsonText(postconditionReadsJson, "Lectures de postcondition");
|
||||
const operatorConfirmed = element<HTMLInputElement>("#metadataOperatorConfirmed").checked;
|
||||
if (submit && !operatorConfirmed) {
|
||||
throw new Error("La soumission exige la confirmation explicite de l’opérateur.");
|
||||
}
|
||||
const request: DemoExecutionMetadataRequest = {
|
||||
profileName,
|
||||
intentId: `desktop-metaplex-${Date.now()}`,
|
||||
operationJson,
|
||||
preflightReadsJson,
|
||||
postconditionReadsJson,
|
||||
submit,
|
||||
operatorConfirmed,
|
||||
materializeAfterConfirmation: element<HTMLInputElement>("#metadataMaterializeInput").checked,
|
||||
};
|
||||
setExecutionRunning(true);
|
||||
const badge = element<HTMLElement>("#metadataStatusBadge");
|
||||
badge.textContent = submit ? "Soumission" : "Simulation";
|
||||
badge.className = "badge text-bg-warning";
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: submit ? "submit" : "simulation", message: submit ? "Démarrage du parcours Devnet complet." : "Démarrage de la simulation RPC exacte." });
|
||||
try {
|
||||
const summary = await invoke<DemoExecutionMetadataSummaryPayload>("demo_execution_metadata_execute", { request });
|
||||
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", JSON.parse(summary.preflightJson)));
|
||||
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
|
||||
profileName: summary.profileName,
|
||||
cluster: summary.cluster,
|
||||
genesisHash: summary.genesisHash,
|
||||
walletPublicKey: summary.walletPublicKey,
|
||||
balanceLamports: summary.balanceLamports,
|
||||
simulationSuccess: summary.simulationSuccess,
|
||||
transactionSignature: summary.transactionSignature,
|
||||
confirmationStatus: summary.confirmationStatus,
|
||||
materializationRequested: summary.materializationRequested,
|
||||
materializedSnapshotCount: summary.materializedSnapshotCount,
|
||||
plan: JSON.parse(summary.planJson),
|
||||
simulation: JSON.parse(summary.simulationJson),
|
||||
}));
|
||||
renderJsonViewer("#metadataPostconditionsOutput", titled("Postconditions et preuves", JSON.parse(summary.evidenceJson)));
|
||||
badge.textContent = summary.transactionSignature === null ? "Simulé" : "Confirmé";
|
||||
badge.className = summary.simulationSuccess ? "badge text-bg-success" : "badge text-bg-danger";
|
||||
} finally {
|
||||
setExecutionRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProfiles(): Promise<void> {
|
||||
const options = await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_solana_core_options");
|
||||
const options = await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_metadata_options");
|
||||
profileOptions = options.profiles;
|
||||
const select = element<HTMLSelectElement>("#metadataProfileSelect");
|
||||
select.replaceChildren();
|
||||
for (const profile of options.profiles) {
|
||||
@@ -91,7 +301,18 @@ async function loadProfiles(): Promise<void> {
|
||||
select.value = options.defaultProfileName;
|
||||
}
|
||||
selectedProfileName = select.value.length === 0 ? null : select.value;
|
||||
updateProfileHelp();
|
||||
if (options.profiles.length === 0) {
|
||||
const message = "Aucun profil Devnet compatible n’a été retourné par la configuration active.";
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: "error", stage: "profile_options", message });
|
||||
element<HTMLElement>("#metadataProfileStatusBadge").textContent = "Aucun profil";
|
||||
element<HTMLElement>("#metadataProfileStatusBadge").className = "badge text-bg-danger";
|
||||
renderSelectedScenario();
|
||||
return;
|
||||
}
|
||||
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "profile_options", message: `${options.profiles.length} profil(s) Devnet chargé(s).` });
|
||||
renderSelectedScenario();
|
||||
await prepareProfile();
|
||||
}
|
||||
|
||||
async function prepareProfile(): Promise<void> {
|
||||
@@ -102,6 +323,8 @@ async function prepareProfile(): Promise<void> {
|
||||
badge.className = "badge text-bg-danger";
|
||||
return;
|
||||
}
|
||||
profileStoreReady = false;
|
||||
setExecutionRunning(executionRunning);
|
||||
badge.textContent = "Préparation";
|
||||
badge.className = "badge text-bg-warning";
|
||||
try {
|
||||
@@ -109,13 +332,17 @@ async function prepareProfile(): Promise<void> {
|
||||
selectedProfileName = profileName;
|
||||
window.localStorage.setItem(profileStorageKey, profileName);
|
||||
renderJsonViewer("#metadataProfileOutput", readiness);
|
||||
profileStoreReady = true;
|
||||
badge.textContent = "Base prête";
|
||||
badge.className = "badge text-bg-success";
|
||||
setExecutionRunning(executionRunning);
|
||||
renderSelectedScenario();
|
||||
} catch (caught) {
|
||||
const message = caught instanceof Error ? caught.message : String(caught);
|
||||
profileStoreReady = false;
|
||||
badge.textContent = "Erreur";
|
||||
badge.className = "badge text-bg-danger";
|
||||
setExecutionRunning(executionRunning);
|
||||
frontendError("kb-app-demo-desktop.frontend.execution_metadata", message);
|
||||
}
|
||||
}
|
||||
@@ -140,8 +367,10 @@ async function loadScenarios(): Promise<void> {
|
||||
badge.className = "badge text-bg-success";
|
||||
} catch (caught) {
|
||||
const message = caught instanceof Error ? caught.message : String(caught);
|
||||
profileStoreReady = false;
|
||||
badge.textContent = "Erreur";
|
||||
badge.className = "badge text-bg-danger";
|
||||
setExecutionRunning(executionRunning);
|
||||
frontendError("kb-app-demo-desktop.frontend.execution_metadata", message);
|
||||
}
|
||||
}
|
||||
@@ -157,10 +386,36 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
element<HTMLSelectElement>("#metadataProfileSelect").addEventListener("change", event => {
|
||||
const target = event.currentTarget as HTMLSelectElement;
|
||||
selectedProfileName = target.value.length === 0 ? null : target.value;
|
||||
profileStoreReady = false;
|
||||
updateProfileHelp();
|
||||
setExecutionRunning(executionRunning);
|
||||
renderSelectedScenario();
|
||||
void prepareProfile();
|
||||
});
|
||||
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => void prepareProfile());
|
||||
element<HTMLButtonElement>("#refreshMetadataScenariosButton").addEventListener("click", () => void loadScenarios());
|
||||
element<HTMLSelectElement>("#metadataStepSelect").addEventListener("change", () => {
|
||||
const scenario = selectedScenario();
|
||||
if (scenario !== null) { renderScenarioSteps(scenario); }
|
||||
});
|
||||
element<HTMLButtonElement>("#previousMetadataStepButton").addEventListener("click", () => moveScenarioStep(-1));
|
||||
element<HTMLButtonElement>("#nextMetadataStepButton").addEventListener("click", () => moveScenarioStep(1));
|
||||
element<HTMLTextAreaElement>("#metadataOperationJson").addEventListener("input", () => {
|
||||
preparedScenarioId = null;
|
||||
preparedStepIndex = null;
|
||||
setExecutionRunning(executionRunning);
|
||||
});
|
||||
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").addEventListener("click", () => { void prepareScenarioStep().catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught))); });
|
||||
element<HTMLTextAreaElement>("#metadataExecutionLogOutput").addEventListener("app-log-clear", () => { metadataLogLines.length = 0; });
|
||||
void listen<MetadataProgressPayload>("demo-execution-solana-core-progress", event => appendMetadataLog(event.payload));
|
||||
element<HTMLButtonElement>("#simulateMetadataButton").addEventListener("click", () => {
|
||||
void executeMetadata(false).catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught)));
|
||||
});
|
||||
element<HTMLButtonElement>("#submitMetadataButton").addEventListener("click", () => {
|
||||
void executeMetadata(true).catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught)));
|
||||
});
|
||||
setExecutionRunning(false);
|
||||
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Metadata execution window loaded");
|
||||
void loadProfiles().catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught)));
|
||||
void loadScenarios();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/demo_execution_metadata.rs
|
||||
// version: 2
|
||||
// version: 12
|
||||
|
||||
//! Thin desktop adapters for metadata execution demo scenarios.
|
||||
|
||||
@@ -13,8 +13,16 @@
|
||||
pub(crate) struct DemoExecutionMetadataScenarioPayload {
|
||||
/// Stable scenario identifier.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible scenario label.
|
||||
pub label: std::string::String,
|
||||
/// Stable asset-family code.
|
||||
pub asset_family: std::string::String,
|
||||
/// Stable fixture graph code.
|
||||
pub fixture_kind: std::string::String,
|
||||
/// Required initial lifecycle state.
|
||||
pub initial_state: std::string::String,
|
||||
/// Expected terminal lifecycle state.
|
||||
pub resulting_state: std::string::String,
|
||||
/// Stable execution-mode code.
|
||||
pub mode: std::string::String,
|
||||
/// Ordered operation codes.
|
||||
@@ -29,6 +37,486 @@ pub(crate) struct DemoExecutionMetadataScenarioPayload {
|
||||
pub validation_status: std::string::String,
|
||||
/// Required evidence kinds.
|
||||
pub required_evidence: std::vec::Vec<std::string::String>,
|
||||
/// Canonical account projections expected when materialization is enabled.
|
||||
pub materialization_targets: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// UI-safe result of one prepared Metaplex Create fixture.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataCreateFixturePayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataCreateFixturePayload {
|
||||
/// Fixture mint address.
|
||||
pub mint: std::string::String,
|
||||
/// Canonical metadata PDA.
|
||||
pub metadata: std::string::String,
|
||||
/// Canonical master-edition PDA.
|
||||
pub master_edition: std::string::String,
|
||||
/// Wallet authority.
|
||||
pub authority: std::string::String,
|
||||
/// Generated typed Create operation JSON.
|
||||
pub operation_json: std::string::String,
|
||||
/// Whether the mint account was newly created.
|
||||
pub mint_created: bool,
|
||||
}
|
||||
|
||||
/// UI-safe prepared step for one coherent Metaplex Devnet scenario.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataPreparedStepPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataPreparedStepPayload {
|
||||
/// Stable scenario identifier.
|
||||
pub scenario_id: std::string::String,
|
||||
/// Zero-based step index.
|
||||
pub step_index: usize,
|
||||
/// Stable operation name accepted by the executor JSON contract.
|
||||
pub operation: std::string::String,
|
||||
/// Operator-visible step label.
|
||||
pub label: std::string::String,
|
||||
/// Prepared typed operation JSON.
|
||||
pub operation_json: std::string::String,
|
||||
/// Automatically generated bounded preflight reads.
|
||||
pub preflight_reads_json: std::string::String,
|
||||
/// Automatically generated bounded postcondition reads.
|
||||
pub postcondition_reads_json: std::string::String,
|
||||
/// Whether this step can be simulated immediately.
|
||||
pub simulation_ready: bool,
|
||||
/// Whether this step can be submitted immediately.
|
||||
pub submission_ready: bool,
|
||||
/// Fixture state used to prepare the step.
|
||||
pub fixture_state: std::string::String,
|
||||
}
|
||||
|
||||
/// Request for one real Devnet Metaplex execution from the desktop panel.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataRequest {
|
||||
/// Devnet profile selected by the operator.
|
||||
pub profile_name: std::string::String,
|
||||
/// Stable execution identifier shown in diagnostics.
|
||||
pub intent_id: std::string::String,
|
||||
/// Serialized typed current Metaplex operation.
|
||||
pub operation_json: std::string::String,
|
||||
/// Serialized bounded preflight read requests.
|
||||
pub preflight_reads_json: std::string::String,
|
||||
/// Serialized bounded postcondition read requests.
|
||||
pub postcondition_reads_json: std::string::String,
|
||||
/// Whether signing and submission are explicitly requested.
|
||||
pub submit: bool,
|
||||
/// Explicit operator confirmation for submission.
|
||||
pub operator_confirmed: bool,
|
||||
/// Materializes confirmed Metaplex account snapshots after submission.
|
||||
pub materialize_after_confirmation: bool,
|
||||
}
|
||||
|
||||
/// UI-safe result of one real Devnet Metaplex execution.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataSummaryPayload {
|
||||
/// Selected profile name.
|
||||
pub profile_name: std::string::String,
|
||||
/// Exact cluster code.
|
||||
pub cluster: std::string::String,
|
||||
/// Genesis hash observed from RPC.
|
||||
pub genesis_hash: std::string::String,
|
||||
/// Wallet public key used by the runner.
|
||||
pub wallet_public_key: std::string::String,
|
||||
/// Wallet balance before execution.
|
||||
pub balance_lamports: std::string::String,
|
||||
/// Whether the real RPC simulation succeeded.
|
||||
pub simulation_success: bool,
|
||||
/// Optional submitted signature.
|
||||
pub transaction_signature: std::option::Option<std::string::String>,
|
||||
/// Optional terminal confirmation status.
|
||||
pub confirmation_status: std::option::Option<std::string::String>,
|
||||
/// Exact prepared plan as formatted JSON.
|
||||
pub plan_json: std::string::String,
|
||||
/// Stateful preflight as formatted JSON.
|
||||
pub preflight_json: std::string::String,
|
||||
/// Real RPC simulation as formatted JSON.
|
||||
pub simulation_json: std::string::String,
|
||||
/// Before/after state and readiness diagnostics as formatted JSON.
|
||||
pub evidence_json: std::string::String,
|
||||
/// Whether post-confirmation account materialization was requested.
|
||||
pub materialization_requested: bool,
|
||||
/// Number of canonical post-confirmation snapshots materialized.
|
||||
pub materialized_snapshot_count: usize,
|
||||
}
|
||||
|
||||
/// Returns Devnet profile options dedicated to the Metadata execution panel.
|
||||
pub(crate) fn demo_execution_metadata_options(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> crate::DemoExecutionSolanaCoreOptionsPayload {
|
||||
return crate::demo_execution_solana_core_options(state);
|
||||
}
|
||||
|
||||
/// Executes one real Devnet Metaplex simulation or explicitly confirmed submission.
|
||||
pub(crate) async fn demo_execution_metadata_execute(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataRequest,
|
||||
) -> std::result::Result<crate::DemoExecutionMetadataSummaryPayload, std::string::String> {
|
||||
let acquire_result = state.demo_execution_solana_core_running().compare_exchange(
|
||||
false,
|
||||
true,
|
||||
std::sync::atomic::Ordering::AcqRel,
|
||||
std::sync::atomic::Ordering::Acquire,
|
||||
);
|
||||
if acquire_result.is_err() {
|
||||
return std::result::Result::Err("a Devnet execution is already running".to_string());
|
||||
}
|
||||
let _run_guard = crate::DemoExecutionSolanaCoreRunGuard {
|
||||
running: state.demo_execution_solana_core_running(),
|
||||
};
|
||||
state
|
||||
.demo_execution_solana_core_cancel_requested()
|
||||
.store(false, std::sync::atomic::Ordering::Release);
|
||||
let profile =
|
||||
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let http_pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let mut execution_request = match kb_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
|
||||
request.intent_id,
|
||||
request.operation_json.as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
execution_request.preflight_reads =
|
||||
match parse_stateful_reads(request.preflight_reads_json.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
execution_request.postcondition_reads =
|
||||
match parse_stateful_reads(request.postcondition_reads_json.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
execution_request.submit = request.submit;
|
||||
execution_request.operator_confirmed = request.operator_confirmed;
|
||||
execution_request.materialize_after_confirmation = request.materialize_after_confirmation;
|
||||
if let std::result::Result::Err(error) = execution_request.validate() {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let observer = crate::DemoExecutionSolanaCoreObserver {
|
||||
app_handle,
|
||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||
};
|
||||
let workspace_root = crate::workspace_root_dir();
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_metaplex_token_metadata(
|
||||
&http_pool,
|
||||
&profile,
|
||||
workspace_root.as_path(),
|
||||
&execution_request,
|
||||
&observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let transaction_signature =
|
||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||
let confirmation_status =
|
||||
summary.confirmation.as_ref().map(|value| return format!("{:?}", value.status));
|
||||
let evidence_json = crate::pretty_json(&serde_json::json!({
|
||||
"before": &summary.before,
|
||||
"readiness": &summary.readiness,
|
||||
"sendResult": &summary.send_result,
|
||||
"confirmation": &summary.confirmation,
|
||||
"after": &summary.after,
|
||||
"materializationRequested": summary.materialization_requested,
|
||||
"materializedSnapshots": &summary.materialized_snapshots
|
||||
}));
|
||||
return std::result::Result::Ok(crate::DemoExecutionMetadataSummaryPayload {
|
||||
profile_name: summary.profile_name,
|
||||
cluster: format!("{:?}", summary.cluster).to_lowercase(),
|
||||
genesis_hash: summary.genesis_hash,
|
||||
wallet_public_key: summary.wallet.public_key,
|
||||
balance_lamports: summary.balance_lamports.to_string(),
|
||||
simulation_success: summary.simulation.success,
|
||||
transaction_signature,
|
||||
confirmation_status,
|
||||
plan_json: crate::pretty_json(&summary.plan),
|
||||
preflight_json: crate::pretty_json(&summary.stateful_preflight),
|
||||
simulation_json: crate::pretty_json(&summary.simulation),
|
||||
evidence_json,
|
||||
materialization_requested: summary.materialization_requested,
|
||||
materialized_snapshot_count: summary.materialized_snapshots.len(),
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_stateful_reads(
|
||||
value: &str,
|
||||
) -> std::result::Result<
|
||||
std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest>,
|
||||
std::string::String,
|
||||
> {
|
||||
if value.trim().is_empty() {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
return match serde_json::from_str(value) {
|
||||
std::result::Result::Ok(parsed) => std::result::Result::Ok(parsed),
|
||||
std::result::Result::Err(error) => {
|
||||
std::result::Result::Err(format!("invalid Metaplex stateful reads JSON: {error}"))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates or reuses the Devnet mint fixture used by the named Create scenario.
|
||||
pub(crate) async fn demo_execution_metadata_prepare_create_fixture(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
profile_name: std::string::String,
|
||||
) -> std::result::Result<crate::DemoExecutionMetadataCreateFixturePayload, std::string::String> {
|
||||
let profile = match crate::select_devnet_profile(state.app_config(), profile_name.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
|
||||
let wallet_dir = if configured.is_absolute() {
|
||||
configured
|
||||
} else {
|
||||
crate::workspace_root_dir().join(configured)
|
||||
};
|
||||
let http_pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let workspace_root = crate::workspace_root_dir();
|
||||
let summary = match kb_pipeline_demo_scenarios::prepare_metaplex_create_fixture(
|
||||
&http_pool,
|
||||
&profile,
|
||||
workspace_root.as_path(),
|
||||
&kb_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions::new(wallet_dir),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(crate::DemoExecutionMetadataCreateFixturePayload {
|
||||
mint: summary.mint,
|
||||
metadata: summary.metadata,
|
||||
master_edition: summary.master_edition,
|
||||
authority: summary.authority,
|
||||
operation_json: summary.operation_json,
|
||||
mint_created: summary.mint_created,
|
||||
});
|
||||
}
|
||||
|
||||
/// Prepares one coherent scenario step and all of its stateful reads.
|
||||
pub(crate) async fn demo_execution_metadata_prepare_step(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
profile_name: std::string::String,
|
||||
scenario_id: std::string::String,
|
||||
step_index: usize,
|
||||
previous_operation_json: std::option::Option<std::string::String>,
|
||||
) -> std::result::Result<crate::DemoExecutionMetadataPreparedStepPayload, std::string::String> {
|
||||
let scenario = kb_pipeline_demo_scenarios::metaplex_token_metadata_devnet_scenarios()
|
||||
.into_iter()
|
||||
.find(|candidate| return candidate.id == scenario_id);
|
||||
let scenario = match scenario {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(format!(
|
||||
"unknown Metaplex Devnet scenario `{scenario_id}`"
|
||||
));
|
||||
},
|
||||
};
|
||||
let operation_code = match scenario.operation_codes.get(step_index) {
|
||||
std::option::Option::Some(value) => value.clone(),
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(format!(
|
||||
"scenario `{}` has no step at index {step_index}",
|
||||
scenario.id
|
||||
));
|
||||
},
|
||||
};
|
||||
let operation_suffix = match operation_code.rsplit('.').next() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => operation_code.as_str(),
|
||||
};
|
||||
let operation = if operation_suffix == "update" {
|
||||
"update_as_update_authority_v2".to_string()
|
||||
} else {
|
||||
operation_suffix.to_string()
|
||||
};
|
||||
if operation != "create" && operation != "update_as_update_authority_v2" {
|
||||
return std::result::Result::Err(format!(
|
||||
"scenario step `{operation}` is declared but its Devnet fixture builder is not implemented yet"
|
||||
));
|
||||
}
|
||||
let (fixture_mint, fixture_metadata, fixture_master_edition, operation_json, fixture_state) =
|
||||
if operation == "create" {
|
||||
let fixture =
|
||||
match crate::demo_execution_metadata_prepare_create_fixture(state, profile_name)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
(
|
||||
fixture.mint,
|
||||
fixture.metadata,
|
||||
fixture.master_edition,
|
||||
fixture.operation_json,
|
||||
if fixture.mint_created {
|
||||
"mint_prepared".to_string()
|
||||
} else {
|
||||
"mint_reused".to_string()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let previous = match previous_operation_json {
|
||||
std::option::Option::Some(value) if !value.trim().is_empty() => value,
|
||||
_ => {
|
||||
return std::result::Result::Err(
|
||||
"prepare and submit the Create step before preparing Update".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let previous_value: serde_json::Value = match serde_json::from_str(previous.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"invalid previous scenario operation JSON: {error}"
|
||||
));
|
||||
},
|
||||
};
|
||||
let string_field = |name: &str| -> std::result::Result<
|
||||
std::string::String,
|
||||
std::string::String,
|
||||
> {
|
||||
return match previous_value.get(name).and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value.to_string()),
|
||||
std::option::Option::None => std::result::Result::Err(format!(
|
||||
"previous Create step does not contain `{name}`"
|
||||
)),
|
||||
};
|
||||
};
|
||||
let mint = match string_field("mint") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let metadata = match string_field("metadata") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let master_edition = match string_field("master_edition") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let authority = match string_field("authority") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let value = serde_json::json!({
|
||||
"operation": "update_as_update_authority_v2",
|
||||
"authority": authority.clone(),
|
||||
"mint": mint.clone(),
|
||||
"metadata": metadata.clone(),
|
||||
"edition": null,
|
||||
"token": null,
|
||||
"authorization_rules_program": null,
|
||||
"authorization_rules": null,
|
||||
"update_args": {
|
||||
"AsUpdateAuthorityV2": {
|
||||
"new_update_authority": null,
|
||||
"data": null,
|
||||
"primary_sale_happened": true,
|
||||
"is_mutable": null,
|
||||
"collection": "None",
|
||||
"collection_details": "None",
|
||||
"uses": "None",
|
||||
"rule_set": "None",
|
||||
"token_standard": null,
|
||||
"authorization_data": null
|
||||
}
|
||||
}
|
||||
});
|
||||
(
|
||||
mint,
|
||||
metadata,
|
||||
master_edition,
|
||||
crate::pretty_json(&value),
|
||||
"metadata_created".to_string(),
|
||||
)
|
||||
};
|
||||
let metadata_read = serde_json::json!({
|
||||
"query_role": "http_queries",
|
||||
"account": fixture_metadata.clone(),
|
||||
"kind": { "kind": "metadata" },
|
||||
"min_context_slot": null,
|
||||
"max_data_bytes": 65536
|
||||
});
|
||||
let edition_read = serde_json::json!({
|
||||
"query_role": "http_queries",
|
||||
"account": fixture_master_edition.clone(),
|
||||
"kind": { "kind": "edition", "mint": fixture_mint.clone() },
|
||||
"min_context_slot": null,
|
||||
"max_data_bytes": 65536
|
||||
});
|
||||
let preflight_reads = if operation == "create" {
|
||||
serde_json::json!([])
|
||||
} else {
|
||||
serde_json::json!([metadata_read.clone()])
|
||||
};
|
||||
let postcondition_reads = if operation == "create" {
|
||||
serde_json::json!([metadata_read, edition_read])
|
||||
} else {
|
||||
serde_json::json!([metadata_read])
|
||||
};
|
||||
return std::result::Result::Ok(crate::DemoExecutionMetadataPreparedStepPayload {
|
||||
scenario_id: scenario.id,
|
||||
step_index,
|
||||
operation: operation.clone(),
|
||||
label: format!("Étape {} — {}", step_index + 1, operation),
|
||||
operation_json,
|
||||
preflight_reads_json: crate::pretty_json(&preflight_reads),
|
||||
postcondition_reads_json: crate::pretty_json(&postcondition_reads),
|
||||
simulation_ready: true,
|
||||
submission_ready: true,
|
||||
fixture_state,
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns current operation names accepted by automatic Devnet campaigns.
|
||||
pub(crate) fn demo_execution_metadata_current_operations() -> std::vec::Vec<std::string::String> {
|
||||
return kb_pipeline_demo_scenarios::metaplex_token_metadata_current_operation_names()
|
||||
.iter()
|
||||
.map(|value| return (*value).to_string())
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Returns one generated typed JSON template for a named current operation.
|
||||
pub(crate) fn demo_execution_metadata_operation_template(
|
||||
operation: std::string::String,
|
||||
) -> std::result::Result<std::string::String, std::string::String> {
|
||||
return match kb_pipeline_demo_scenarios::metaplex_token_metadata_current_operation_json_template(
|
||||
operation.as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the canonical Metaplex scenario inventory for the desktop panel.
|
||||
@@ -68,7 +556,11 @@ pub(crate) fn demo_execution_metadata_scenarios() -> std::result::Result<
|
||||
};
|
||||
payloads.push(crate::DemoExecutionMetadataScenarioPayload {
|
||||
id: scenario.id,
|
||||
label: scenario.label,
|
||||
asset_family: asset_family_code(scenario.asset_family),
|
||||
fixture_kind: fixture_kind_code(scenario.fixture_kind),
|
||||
initial_state: fixture_state_code(scenario.initial_state),
|
||||
resulting_state: fixture_state_code(scenario.resulting_state),
|
||||
mode: scenario_mode_code(scenario.mode),
|
||||
operation_codes: scenario.operation_codes,
|
||||
requires_collection: scenario.requires_collection,
|
||||
@@ -76,6 +568,7 @@ pub(crate) fn demo_execution_metadata_scenarios() -> std::result::Result<
|
||||
requires_postcondition: scenario.requires_postcondition,
|
||||
validation_status,
|
||||
required_evidence,
|
||||
materialization_targets: scenario.materialization_targets,
|
||||
});
|
||||
}
|
||||
return std::result::Result::Ok(payloads);
|
||||
@@ -99,6 +592,18 @@ fn asset_family_code(
|
||||
};
|
||||
}
|
||||
|
||||
fn fixture_kind_code(
|
||||
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataFixtureKind,
|
||||
) -> std::string::String {
|
||||
return format!("{value:?}").to_lowercase();
|
||||
}
|
||||
|
||||
fn fixture_state_code(
|
||||
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataFixtureState,
|
||||
) -> std::string::String {
|
||||
return format!("{value:?}").to_lowercase();
|
||||
}
|
||||
|
||||
fn scenario_mode_code(
|
||||
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode,
|
||||
) -> std::string::String {
|
||||
@@ -154,7 +659,10 @@ mod tests {
|
||||
return;
|
||||
},
|
||||
};
|
||||
assert_eq!(payloads.len(), 10);
|
||||
let expected_count =
|
||||
kb_pipeline_demo_scenarios::metaplex_token_metadata_synthetic_scenarios().len()
|
||||
+ kb_pipeline_demo_scenarios::metaplex_token_metadata_devnet_scenarios().len();
|
||||
assert_eq!(payloads.len(), expected_count);
|
||||
assert!(payloads.iter().all(|payload| return !payload.operation_codes.is_empty()));
|
||||
assert!(payloads.iter().all(|payload| return payload.requires_postcondition));
|
||||
assert!(
|
||||
@@ -167,10 +675,41 @@ mod tests {
|
||||
.iter()
|
||||
.filter(|payload| return payload.mode == "network_simulation")
|
||||
.count(),
|
||||
5
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_stateful_reads_are_accepted() {
|
||||
let parsed = super::parse_stateful_reads(" ");
|
||||
assert!(parsed.is_ok());
|
||||
let values = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
assert_eq!(std::option::Option::Some(error), std::option::Option::None);
|
||||
return;
|
||||
},
|
||||
};
|
||||
assert_eq!(values.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_frontend_contains_every_required_runtime_control() {
|
||||
let source = std::include_str!("../frontend/demo_execution_metadata.html");
|
||||
for id in [
|
||||
"metadataProfileSelect",
|
||||
"metadataDevnetScenarioSelect",
|
||||
"metadataStepSelect",
|
||||
"prepareMetadataScenarioStepButton",
|
||||
"metadataExecutionLogOutput",
|
||||
"metadataProfileHelp",
|
||||
"simulateMetadataButton",
|
||||
"submitMetadataButton",
|
||||
] {
|
||||
assert!(source.contains(format!("id=\"{id}\"").as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn programmable_and_collection_requirements_remain_explicit() {
|
||||
let payloads = match crate::demo_execution_metadata_scenarios() {
|
||||
@@ -180,13 +719,7 @@ mod tests {
|
||||
return;
|
||||
},
|
||||
};
|
||||
assert_eq!(payloads.iter().filter(|payload| return payload.requires_collection).count(), 2);
|
||||
assert_eq!(
|
||||
payloads
|
||||
.iter()
|
||||
.filter(|payload| return payload.requires_programmable_rules)
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(payloads.iter().any(|payload| return payload.requires_collection));
|
||||
assert!(payloads.iter().any(|payload| return payload.requires_programmable_rules));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/lib.rs
|
||||
// version: 26
|
||||
// version: 29
|
||||
|
||||
//! Tauri desktop demo application for `khadhroony-bot3`.
|
||||
|
||||
@@ -127,8 +127,28 @@ pub(crate) use self::demo_devnet_common::DemoExecutionDevnetStoreReadinessPayloa
|
||||
pub(crate) use self::demo_devnet_common::devnet_store_readiness_payload;
|
||||
/// Serializes a diagnostic value to indented JSON without panicking.
|
||||
pub(crate) use self::demo_devnet_common::pretty_json;
|
||||
/// UI-safe result of one prepared Metaplex Create fixture.
|
||||
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataCreateFixturePayload;
|
||||
/// Prepared coherent Metaplex scenario step.
|
||||
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataPreparedStepPayload;
|
||||
/// Request for one real Devnet Metaplex execution from the desktop panel.
|
||||
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataRequest;
|
||||
/// UI-safe Metaplex scenario row.
|
||||
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataScenarioPayload;
|
||||
/// UI-safe result of one real Devnet Metaplex execution.
|
||||
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataSummaryPayload;
|
||||
/// Returns current Metaplex operation names accepted by Devnet campaigns.
|
||||
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_current_operations;
|
||||
/// Executes one real Devnet Metaplex simulation or submission.
|
||||
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_execute;
|
||||
/// Returns one generated operation JSON template.
|
||||
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_operation_template;
|
||||
/// Returns Devnet profile options dedicated to the Metadata execution panel.
|
||||
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_options;
|
||||
/// Creates or reuses the Devnet mint fixture used by the Create scenario.
|
||||
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_prepare_create_fixture;
|
||||
/// Prepares one coherent scenario step and its automatic stateful reads.
|
||||
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_prepare_step;
|
||||
/// Returns the canonical Metaplex scenario inventory for the desktop panel.
|
||||
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_scenarios;
|
||||
/// Request sent by the Memo v4 Devnet execution panel.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/tauri.rs
|
||||
// version: 26
|
||||
// version: 30
|
||||
|
||||
//! Tauri runtime assembly and private command wrappers.
|
||||
|
||||
@@ -30,8 +30,24 @@ pub fn run() -> kb_core::Result<()> {
|
||||
let mut builder = tauri::Builder::default();
|
||||
builder = builder.manage(app_state);
|
||||
builder = builder.invoke_handler(tauri::generate_handler![
|
||||
demo_backfill_cancel,
|
||||
splash_frontend_ready,
|
||||
emit_frontend_log,
|
||||
load_project_readme,
|
||||
open_demo_config_window,
|
||||
load_demo_config,
|
||||
open_demo_http_window,
|
||||
demo_http_list_pool_clients,
|
||||
demo_http_options,
|
||||
demo_http_execute_request,
|
||||
open_demo_ws_window,
|
||||
demo_ws_list_pool_clients,
|
||||
demo_ws_options,
|
||||
demo_ws_status,
|
||||
demo_ws_connect,
|
||||
demo_ws_unsubscribe,
|
||||
demo_ws_disconnect,
|
||||
open_demo_backfill_window,
|
||||
demo_backfill_cancel,
|
||||
demo_backfill_options,
|
||||
demo_backfill_execute,
|
||||
open_demo_core_extraction_window,
|
||||
@@ -44,32 +60,6 @@ pub fn run() -> kb_core::Result<()> {
|
||||
demo_decode_replay_cancel,
|
||||
demo_decode_replay_diagnostics,
|
||||
demo_decode_replay_annotations,
|
||||
open_demo_execution_solana_core_window,
|
||||
demo_execution_devnet_prepare_profile,
|
||||
demo_execution_solana_core_options,
|
||||
demo_execution_solana_core_generate_recipient,
|
||||
demo_execution_solana_core_execute,
|
||||
demo_execution_spl_memo_execute,
|
||||
demo_execution_solana_core_cancel,
|
||||
demo_execution_spl_validation_scenarios,
|
||||
open_demo_execution_spl_window,
|
||||
open_demo_execution_metadata_window,
|
||||
demo_execution_metadata_scenarios,
|
||||
demo_execution_spl_token_execute,
|
||||
demo_spl_token_journal,
|
||||
demo_spl_ata_derive,
|
||||
demo_execution_spl_ata_execute,
|
||||
demo_spl_ata_journal,
|
||||
demo_execution_spl_token_2022_execute,
|
||||
demo_spl_token_2022_fixture,
|
||||
open_demo_config_window,
|
||||
load_demo_config,
|
||||
emit_frontend_log,
|
||||
load_project_readme,
|
||||
open_demo_http_window,
|
||||
demo_http_list_pool_clients,
|
||||
demo_http_options,
|
||||
demo_http_execute_request,
|
||||
open_demo_sql_diag_window,
|
||||
load_demo_sql_diag,
|
||||
open_demo_sql_pg_raw_window,
|
||||
@@ -82,14 +72,30 @@ pub fn run() -> kb_core::Result<()> {
|
||||
load_demo_sql_replay_transactions,
|
||||
load_demo_sql_replay_programs,
|
||||
load_demo_sql_replay_entities,
|
||||
open_demo_ws_window,
|
||||
demo_ws_list_pool_clients,
|
||||
demo_ws_options,
|
||||
demo_ws_status,
|
||||
demo_ws_connect,
|
||||
demo_ws_unsubscribe,
|
||||
demo_ws_disconnect,
|
||||
splash_frontend_ready,
|
||||
open_demo_execution_solana_core_window,
|
||||
demo_execution_devnet_prepare_profile,
|
||||
demo_execution_solana_core_options,
|
||||
demo_execution_solana_core_generate_recipient,
|
||||
demo_execution_solana_core_execute,
|
||||
demo_execution_spl_memo_execute,
|
||||
demo_execution_solana_core_cancel,
|
||||
demo_execution_spl_validation_scenarios,
|
||||
open_demo_execution_spl_window,
|
||||
demo_execution_spl_token_execute,
|
||||
demo_spl_token_journal,
|
||||
demo_spl_ata_derive,
|
||||
demo_execution_spl_ata_execute,
|
||||
demo_spl_ata_journal,
|
||||
demo_execution_spl_token_2022_execute,
|
||||
demo_spl_token_2022_fixture,
|
||||
open_demo_execution_metadata_window,
|
||||
demo_execution_metadata_scenarios,
|
||||
demo_execution_metadata_options,
|
||||
demo_execution_metadata_current_operations,
|
||||
demo_execution_metadata_operation_template,
|
||||
demo_execution_metadata_prepare_create_fixture,
|
||||
demo_execution_metadata_prepare_step,
|
||||
demo_execution_metadata_execute,
|
||||
]);
|
||||
builder = builder.on_window_event(|window, event| {
|
||||
if window.label() != "main" || !matches!(event, tauri::WindowEvent::Destroyed) {
|
||||
@@ -615,6 +621,13 @@ fn open_demo_execution_metadata_window(
|
||||
);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_options(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> crate::DemoExecutionSolanaCoreOptionsPayload {
|
||||
return crate::demo_execution_metadata_options(state);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_scenarios() -> std::result::Result<
|
||||
std::vec::Vec<crate::DemoExecutionMetadataScenarioPayload>,
|
||||
@@ -623,6 +636,53 @@ fn demo_execution_metadata_scenarios() -> std::result::Result<
|
||||
return crate::demo_execution_metadata_scenarios();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_current_operations() -> std::vec::Vec<std::string::String> {
|
||||
return crate::demo_execution_metadata_current_operations();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_operation_template(
|
||||
operation: std::string::String,
|
||||
) -> std::result::Result<std::string::String, std::string::String> {
|
||||
return crate::demo_execution_metadata_operation_template(operation);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_metadata_prepare_create_fixture(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
profile_name: std::string::String,
|
||||
) -> std::result::Result<crate::DemoExecutionMetadataCreateFixturePayload, std::string::String> {
|
||||
return crate::demo_execution_metadata_prepare_create_fixture(state, profile_name).await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_metadata_prepare_step(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
profile_name: std::string::String,
|
||||
scenario_id: std::string::String,
|
||||
step_index: usize,
|
||||
previous_operation_json: std::option::Option<std::string::String>,
|
||||
) -> std::result::Result<crate::DemoExecutionMetadataPreparedStepPayload, std::string::String> {
|
||||
return crate::demo_execution_metadata_prepare_step(
|
||||
state,
|
||||
profile_name,
|
||||
scenario_id,
|
||||
step_index,
|
||||
previous_operation_json,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_metadata_execute(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataRequest,
|
||||
) -> std::result::Result<crate::DemoExecutionMetadataSummaryPayload, std::string::String> {
|
||||
return crate::demo_execution_metadata_execute(app_handle, state, request).await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_spl_token_execute(
|
||||
app_handle: tauri::AppHandle,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/vite.config.ts
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
import { defineConfig, normalizePath } from "vite";
|
||||
import { NodePackageImporter } from "sass-embedded";
|
||||
@@ -23,18 +23,19 @@ export default defineConfig(() => ({
|
||||
input: {
|
||||
"splash": normalizePath(resolve(__dirname, 'frontend/splash.html')),
|
||||
"main": normalizePath(resolve(__dirname, 'frontend/main.html')),
|
||||
"demo_backfill": normalizePath(resolve(__dirname, 'frontend/demo_backfill.html')),
|
||||
"demo_config": normalizePath(resolve(__dirname, 'frontend/demo_config.html')),
|
||||
"demo_http": normalizePath(resolve(__dirname, 'frontend/demo_http.html')),
|
||||
"demo_ws": normalizePath(resolve(__dirname, 'frontend/demo_ws.html')),
|
||||
"demo_backfill": normalizePath(resolve(__dirname, 'frontend/demo_backfill.html')),
|
||||
"demo_core_extraction": normalizePath(resolve(__dirname, 'frontend/demo_core_extraction.html')),
|
||||
"demo_decode_replay": normalizePath(resolve(__dirname, 'frontend/demo_decode_replay.html')),
|
||||
"demo_sql_diag": normalizePath(resolve(__dirname, 'frontend/demo_sql_diag.html')),
|
||||
"demo_sql_pg_raw": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_raw.html')),
|
||||
"demo_sql_pg_core": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_core.html')),
|
||||
"demo_sql_replay_candidates": normalizePath(resolve(__dirname, 'frontend/demo_sql_replay_candidates.html')),
|
||||
"demo_config": normalizePath(resolve(__dirname, 'frontend/demo_config.html')),
|
||||
"demo_core_extraction": normalizePath(resolve(__dirname, 'frontend/demo_core_extraction.html')),
|
||||
"demo_decode_replay": normalizePath(resolve(__dirname, 'frontend/demo_decode_replay.html')),
|
||||
"demo_execution_solana_core": normalizePath(resolve(__dirname, 'frontend/demo_execution_solana_core.html')),
|
||||
"demo_execution_spl": normalizePath(resolve(__dirname, 'frontend/demo_execution_spl.html')),
|
||||
"demo_execution_metatdata": normalizePath(resolve(__dirname, 'frontend/demo_execution_metatdata.html')),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: 'js/[name]-[hash].js',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-lib/USAGE.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# Utilisation de kb-lib
|
||||
|
||||
@@ -172,6 +172,19 @@ Les opérations courantes utilisent leur wrapper canonique final. Les versions r
|
||||
|
||||
Avant utilisation du plan, le consommateur doit transmettre celui-ci au préflight et à l’orchestration de `kb-pipeline`.
|
||||
|
||||
### Prérequis des opérations Metaplex
|
||||
|
||||
Les builders n’inventent aucun compte ni état préalable. L’appelant doit fournir des comptes cohérents avec l’opération :
|
||||
|
||||
- `Create` NFT classique : mint SPL classique existant, owner `Tokenkeg...`, `decimals = 0`, supply initiale nulle, mint authority et freeze authority correspondant à `authority`, PDA metadata et master edition dérivés du mint, `spl_token_program` explicite et `print_supply` renseigné ;
|
||||
- `Update` : metadata existante, update authority courante et comptes programmables éventuels cohérents avec le token standard ;
|
||||
- `Verify` / `Unverify` : metadata existante et autorité correspondant exactement au créateur, à la collection ou au delegate visé par `VerificationArgs` ;
|
||||
- `Delegate` / `Revoke`, `Lock` / `Unlock`, `Transfer` : token account, token record, delegate record, edition et rule set requis par la variante exacte ;
|
||||
- `Burn`, `CloseAccounts` et opérations d’escrow : comptes de destination, token records et autorités de fermeture explicitement fournis ;
|
||||
- `Mint`, `Print`, `Migrate`, `Use`, `Collect` et `Resize` : état préalable spécifique déjà créé et compatible avec la variante demandée.
|
||||
|
||||
Une simulation réseau échouée ne constitue pas une validation de l’opération. Les logs du programme doivent être conservés et la précondition manquante doit être corrigée avant toute soumission.
|
||||
|
||||
## Sécurité
|
||||
|
||||
Les types `ExSafetyChecker`, `ExSafetyDecision`, `ExSafetyEvaluation` et `ExSafetyViolation` permettent d’évaluer un plan avant son utilisation. Une décision conservatrice doit être respectée par le consommateur ; elle ne doit pas être contournée par l’application.
|
||||
@@ -244,3 +257,19 @@ Les tests des exécuteurs comparent les instructions produites aux builders offi
|
||||
- aucune persistance ni acquisition réseau ;
|
||||
- aucun chargement dynamique des IDL en production ;
|
||||
- les squelettes réservés ne sont pas des implémentations ;
|
||||
|
||||
## Parcours de validation Metaplex Token Metadata sur Devnet
|
||||
|
||||
La validation de l’exécuteur ne doit pas utiliser un produit cartésien artificiel entre tous les types d’assets et toutes les opérations. Les campagnes doivent sélectionner un parcours cohérent composé d’une fixture, d’un état initial, d’opérations applicables et d’un état terminal attendu.
|
||||
|
||||
Les familles actuelles sont le NFT classique, le pNFT, le SFT, le fungible et le parcours collection parent/membre. Une collection est une relation et un état supplémentaire, pas un token standard indépendant. Les opérations incompatibles avec une famille doivent être classées `not_applicable`; les opérations valides mais sans fixture doivent être classées `unsupported`.
|
||||
|
||||
Pour un NFT classique créé par le runner :
|
||||
|
||||
- le mint SPL classique possède `decimals = 0` et `supply = 0` avant `Create` ;
|
||||
- la mint authority et la freeze authority sont toutes deux le wallet du profil ;
|
||||
- `spl_token_program` est fourni explicitement ;
|
||||
- `print_supply` vaut `Zero` ;
|
||||
- un mint dont le PDA metadata est déjà créé ne peut pas être réutilisé pour un second `Create`.
|
||||
|
||||
La matérialisation Metaplex est optionnelle. Lorsqu’elle est activée après une soumission confirmée, chaque compte déclaré dans les lectures de postcondition est relu, validé, décodé canoniquement puis projeté comme snapshot d’état. Aucun contenu URI off-chain n’est téléchargé par ce parcours.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: kb-lib/src/decoder/spl/token_2022/state.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Strict Token-2022 base-state and TLV parsing.
|
||||
|
||||
use base64::Engine as _; // rust-rules: trait-import
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
const MINT_BASE_LEN: usize = 82;
|
||||
const ACCOUNT_BASE_LEN: usize = 165;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/executor/metadata/metaplex_token_metadata/builder.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Official Metaplex builders and conservative plan validation.
|
||||
|
||||
@@ -233,12 +233,6 @@ fn validate_intent(intent: &crate::ExMetaplexTokenMetadataExecutionIntent) -> kb
|
||||
"Metaplex Token Metadata execution requires simulation before signing or sending",
|
||||
));
|
||||
}
|
||||
if !intent.policy.dry_run {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_metaplex_dry_run_required",
|
||||
"Metaplex Token Metadata pre.006 intents are simulation-only",
|
||||
));
|
||||
}
|
||||
match intent.policy.cost_limit.max_fee_lamports {
|
||||
std::option::Option::Some(value) if value > 0 => {},
|
||||
std::option::Option::Some(_) | std::option::Option::None => {
|
||||
@@ -1309,21 +1303,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn policy_requires_dry_run_and_sufficient_limits() {
|
||||
fn policy_requires_simulation_and_sufficient_limits() {
|
||||
let mut intent = approved_puff_intent();
|
||||
intent.policy.dry_run = false;
|
||||
let dry_run_result = super::validate_intent(&intent);
|
||||
assert_eq!(
|
||||
dry_run_result.as_ref().err().map(kb_core::Error::code),
|
||||
std::option::Option::Some("execution_metaplex_dry_run_required"),
|
||||
);
|
||||
intent.policy.dry_run = true;
|
||||
intent.policy.cost_limit.max_spend_lamports = std::option::Option::Some(9_999);
|
||||
let spend_result = super::validate_intent(&intent);
|
||||
assert_eq!(
|
||||
spend_result.as_ref().err().map(kb_core::Error::code),
|
||||
std::option::Option::Some("execution_metaplex_spend_limit_insufficient"),
|
||||
);
|
||||
intent.policy.cost_limit.max_spend_lamports = std::option::Option::Some(10_000);
|
||||
intent.policy.dry_run = false;
|
||||
assert!(super::validate_intent(&intent).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,15 +1,74 @@
|
||||
<!-- file: kb-pipeline-demo-scenarios/CHANGELOG.md -->
|
||||
<!-- version: 17 -->
|
||||
<!-- version: 24 -->
|
||||
|
||||
# CHANGELOG — kb-pipeline-demo-scenarios
|
||||
|
||||
### Correctif `0.4.7-pre.012-fix-003`
|
||||
## 0.4.7-pre.013-fix-014
|
||||
|
||||
- crée un nouveau mint natif pour chaque nouvelle préparation `Create`, afin de ne jamais réutiliser un PDA metadata déjà créé ;
|
||||
- limite l’inventaire Devnet visible au parcours NFT `create -> update` réellement préparé de bout en bout ;
|
||||
- conserve les cinq parcours synthétiques comme matrice cible sans les présenter comme exécutables sur Devnet.
|
||||
|
||||
## 0.4.7-pre.013-fix-013
|
||||
|
||||
- retire l’appel résiduel à `ExSplTokenExecutor` dans la préparation native du mint Metaplex ;
|
||||
- construit directement l’instruction officielle `spl_token_interface::instruction::initialize_mint2` ;
|
||||
- ajoute cette instruction au plan composite `SystemCreateAccount + InitializeMint2` sans falsifier les exigences de post-validation de l’exécuteur SPL ;
|
||||
- ajoute les dépendances directes `solana-instruction` et `spl-token-interface` à la crate de scénarios ;
|
||||
- conserve la simulation exacte, les deux signatures, la confirmation et la relecture on-chain du mint.
|
||||
|
||||
## 0.4.7-pre.013-fix-012
|
||||
|
||||
- aligne la fixture Rust native sur les APIs publiques réelles du workspace ;
|
||||
- évalue le `ExApiPreparedExecutionPlan` avec `evaluate_prepared_plan` ;
|
||||
- conserve `MdSignature` pour l’envoi et la confirmation, puis retourne sa valeur chaîne ;
|
||||
- convertit la clé du mint en `MdPubkey` pour `getAccountInfo` ;
|
||||
- supprime l’avertissement du paramètre de rent conservé dans le contrat de politique.
|
||||
|
||||
## 0.4.7-pre.13 fix-011
|
||||
|
||||
- remplace la préparation Metaplex fondée sur `solana-keygen`, `solana` et `spl-token` par une transaction Rust native ;
|
||||
- crée le mint par `SystemCreateAccount` puis `SPL Token InitializeMint2` dans un même message simulé, signé et confirmé ;
|
||||
- utilise explicitement le wallet du profil comme mint authority et freeze authority ;
|
||||
- conserve le keypair du mint dans `kb-wallet` sans exposer ni déléguer ses secrets à une CLI externe.
|
||||
|
||||
|
||||
## 0.4.7-pre.013-fix-010
|
||||
|
||||
- adapte la création de la fixture NFT à la CLI `spl-token` installée, qui ne prend pas en charge `create-token --freeze-authority` ;
|
||||
- crée le mint avec `--enable-freeze`, puis transfère explicitement les autorités `freeze` et `mint` au wallet du profil ;
|
||||
- versionne la fixture en `create-mint-profile-authority-v4.json` afin de ne pas réutiliser un mint incomplet issu du fix précédent.
|
||||
|
||||
## 0.4.7-pre.013-fix-009
|
||||
|
||||
- remplace l’inventaire Metaplex plat par cinq parcours cohérents avec fixture, état initial, état attendu et cibles de matérialisation ;
|
||||
- ajoute la matérialisation optionnelle des snapshots confirmés ;
|
||||
- crée la fixture NFT `v3` avec mint authority et freeze authority détenues par le wallet du profil ;
|
||||
- interdit une matérialisation demandée sans lecture de postcondition.
|
||||
|
||||
## 0.4.7-pre.013-fix-007
|
||||
|
||||
- fournit explicitement le Program ID SPL Token classique dans le modèle et la fixture `Create` NFT ;
|
||||
- ajoute des assertions empêchant le retour de `spl_token_program: null` pour ce parcours ;
|
||||
- conserve `PrintSupply::Zero` et les PDA dérivés depuis le mint préparé.
|
||||
|
||||
## 0.4.7-pre.013-fix-006
|
||||
|
||||
- corrige le modèle et la fixture `Create` NFT avec `PrintSupply::Zero` ;
|
||||
- ajoute un test garantissant que le modèle JSON nommé ne réintroduit pas un `print_supply` absent.
|
||||
|
||||
## 0.4.7-pre.013-fix-003
|
||||
|
||||
- Ajout du guide intégré, du journal d’exécution et de titres explicites pour les résultats JSON Metaplex.
|
||||
- Ajout d’un préparateur idempotent de fixture `Create` : mint SPL classique Devnet, PDA metadata et master edition dérivés, intent JSON prêt à simuler.
|
||||
|
||||
## 0.4.7-pre.012-fix-003
|
||||
|
||||
- ajout explicite de la dépendance workspace `mpl-token-metadata` requise par les modèles et constructeurs de scénarios ;
|
||||
- alignement du test de placeholder sur le code stable `config` réellement exposé par `kb_core::Error::config` ;
|
||||
- validation locale : formatage, workspace check, clippy et audits propres ; 53 tests de bibliothèque réussis avant correction de l’assertion, un seul test échouant sur le code attendu.
|
||||
|
||||
### Correctif `0.4.7-pre.012-fix-002`
|
||||
## 0.4.7-pre.012-fix-002
|
||||
|
||||
- rejet explicite des placeholders `...` dans le JSON d’opération Metaplex ;
|
||||
- inventaire public des vingt opérations courantes acceptées par les campagnes automatiques ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: kb-pipeline-demo-scenarios/Cargo.toml
|
||||
# version: 4
|
||||
# version: 7
|
||||
|
||||
[package]
|
||||
name = "kb-pipeline-demo-scenarios"
|
||||
@@ -18,6 +18,7 @@ name = "kb-pipeline-demo-scenarios-cli"
|
||||
path = "src/bin/kb_pipeline_demo_scenarios_cli.rs"
|
||||
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
chrono.workspace = true
|
||||
kb-core = { path = "../kb-core" }
|
||||
kb-config = { path = "../kb-config" }
|
||||
@@ -28,6 +29,9 @@ kb-program-ids = { path = "../kb-program-ids" }
|
||||
kb-store = { path = "../kb-store" }
|
||||
kb-wallet = { path = "../kb-wallet" }
|
||||
mpl-token-metadata.workspace = true
|
||||
solana-instruction.workspace = true
|
||||
solana-pubkey.workspace = true
|
||||
spl-token-interface.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -337,4 +337,3 @@ fn creator_verify_request(
|
||||
`devnet_metaplex_creator_unverify_request` fournit le parcours inverse. Les deux requêtes restent en simulation-only par défaut.
|
||||
|
||||
Le test générique accepte toujours `KB_DEVNET_METAPLEX_OPERATION_JSON`, mais rejette explicitement le placeholder documentaire `...`. Les opérations plus complexes recevront des préparateurs de fixtures dédiés au lieu d’exiger de longs JSON écrits à la main.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/lib.rs
|
||||
// version: 10
|
||||
// version: 12
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
mod constants;
|
||||
mod environment;
|
||||
mod metaplex_token_metadata_fixture;
|
||||
mod solana_ata_execution;
|
||||
mod solana_execution;
|
||||
mod solana_memo_execution;
|
||||
@@ -30,6 +31,12 @@ pub use self::environment::initialize_demo_scenario_environment;
|
||||
pub use self::environment::prepare_demo_devnet_profile_store;
|
||||
/// Resolves one configured Devnet profile for UI, CLI and test scenarios.
|
||||
pub use self::environment::resolve_demo_devnet_profile;
|
||||
/// Options for one Metaplex Create fixture preparation.
|
||||
pub use self::metaplex_token_metadata_fixture::MetaplexCreateFixturePreparationOptions;
|
||||
/// Public values generated for one Metaplex Create fixture.
|
||||
pub use self::metaplex_token_metadata_fixture::MetaplexCreateFixturePreparationSummary;
|
||||
/// Creates or reuses a classic SPL mint and derives its Metaplex PDAs.
|
||||
pub use self::metaplex_token_metadata_fixture::prepare_metaplex_create_fixture;
|
||||
/// Complete request for one Devnet Associated Token Account execution.
|
||||
pub use self::solana_ata_execution::DevnetSplAssociatedTokenAccountExecutionRequest;
|
||||
/// Complete result of one Devnet Associated Token Account execution.
|
||||
@@ -70,7 +77,7 @@ pub use self::solana_memo_execution::DevnetMemoExecutionSummary;
|
||||
pub use self::solana_memo_execution::execute_devnet_memo;
|
||||
/// Complete request for one real Devnet Metaplex Token Metadata execution.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionRequest;
|
||||
/// Complete evidence produced by one real Devnet Metaplex Token Metadata execution.
|
||||
/// Complete evidence produced by one real Devnet Metaplex execution.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionSummary;
|
||||
/// Creates one named creator unverification request without handwritten JSON.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::devnet_metaplex_creator_unverify_request;
|
||||
@@ -86,6 +93,10 @@ pub use self::solana_metaplex_token_metadata_devnet_execution::metaplex_token_me
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::simulate_devnet_metaplex_token_metadata;
|
||||
/// Stable asset family covered by one Metaplex scenario.
|
||||
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataAssetFamily;
|
||||
/// Stable fixture graph required by one Metaplex scenario journey.
|
||||
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataFixtureKind;
|
||||
/// Stable lifecycle state consumed or produced by one Metaplex scenario journey.
|
||||
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataFixtureState;
|
||||
/// One stable Metaplex scenario reusable by automated tests and demo adapters.
|
||||
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataScenario;
|
||||
/// Execution mode accepted by one Metaplex scenario.
|
||||
|
||||
@@ -0,0 +1,703 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/metaplex_token_metadata_fixture.rs
|
||||
// version: 8
|
||||
|
||||
//! Native Metaplex Token Metadata fixture preparation for Devnet demos.
|
||||
|
||||
/// Options used to create or reuse one classic SPL mint for a Metaplex Create demo.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MetaplexCreateFixturePreparationOptions {
|
||||
/// Endpoint role used for state, rent, balance, blockhash and fee calls.
|
||||
pub query_role: std::string::String,
|
||||
/// Endpoint role used for simulation, submission and confirmation polling.
|
||||
pub transaction_role: std::string::String,
|
||||
/// Directory receiving the persistent fixture mint keypair.
|
||||
pub wallet_dir: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl crate::MetaplexCreateFixturePreparationOptions {
|
||||
/// Creates conservative defaults matching the Devnet execution profiles.
|
||||
pub fn new(wallet_dir: std::path::PathBuf) -> Self {
|
||||
return Self {
|
||||
query_role: "http_queries".to_string(),
|
||||
transaction_role: "http_transactions".to_string(),
|
||||
wallet_dir,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Public fixture values and generated Create intent JSON.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct MetaplexCreateFixturePreparationSummary {
|
||||
/// Fixture mint keypair path.
|
||||
pub mint_keypair_path: std::path::PathBuf,
|
||||
/// Classic SPL mint address.
|
||||
pub mint: std::string::String,
|
||||
/// Canonical Metaplex metadata PDA.
|
||||
pub metadata: std::string::String,
|
||||
/// Canonical Metaplex master-edition PDA.
|
||||
pub master_edition: std::string::String,
|
||||
/// Operator public key used as mint and update authority.
|
||||
pub authority: std::string::String,
|
||||
/// Ready-to-edit typed Create operation JSON.
|
||||
pub operation_json: std::string::String,
|
||||
/// Whether the mint account was created during this call.
|
||||
pub mint_created: bool,
|
||||
/// Confirmed signature of native mint preparation when newly created.
|
||||
pub preparation_signature: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// Creates or reuses a zero-decimal classic SPL mint and derives its Metaplex PDAs.
|
||||
pub async fn prepare_metaplex_create_fixture(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
options: &crate::MetaplexCreateFixturePreparationOptions,
|
||||
) -> kb_core::Result<crate::MetaplexCreateFixturePreparationSummary> {
|
||||
if options.query_role.trim().is_empty() || options.transaction_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Metaplex fixture endpoint roles must not be empty",
|
||||
));
|
||||
}
|
||||
if !profile.wallet.devnet_send_enabled {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Metaplex fixture preparation requires devnet_send_enabled=true",
|
||||
));
|
||||
}
|
||||
let operator = match crate::load_profile_wallet(profile, workspace_root).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fixture_dir = options.wallet_dir.join("metaplex_token_metadata_validation");
|
||||
let fixture_store = match kb_wallet::TemporaryWalletStore::new(fixture_dir.clone()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fixture_epoch = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
||||
std::result::Result::Ok(value) => value.as_millis(),
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"unable to create a unique Metaplex fixture identifier: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let fixture_alias =
|
||||
match kb_wallet::WalletAlias::parse(format!("create-mint-native-{fixture_epoch}").as_str())
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mint_wallet = match fixture_store.load_or_create(fixture_alias.clone()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mint_keypair_path = fixture_store.wallet_path(&fixture_alias);
|
||||
let authority = operator.public_key();
|
||||
let mint = mint_wallet.public_key();
|
||||
let existing =
|
||||
match read_mint_account(http_pool, options.query_role.as_str(), mint.as_str()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let preparation_signature = if let std::option::Option::Some(account) = existing {
|
||||
if let std::result::Result::Err(error) =
|
||||
validate_classic_nft_mint(&account, authority.as_str())
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
std::option::Option::None
|
||||
} else {
|
||||
let signature = match create_native_classic_nft_mint(
|
||||
http_pool,
|
||||
profile,
|
||||
options,
|
||||
&operator,
|
||||
&mint_wallet,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let created =
|
||||
match read_mint_account(http_pool, options.query_role.as_str(), mint.as_str()).await {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_fixture_mint_missing_after_confirmation",
|
||||
"native Metaplex fixture mint is unavailable after confirmation",
|
||||
));
|
||||
},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) =
|
||||
validate_classic_nft_mint(&created, authority.as_str())
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
std::option::Option::Some(signature)
|
||||
};
|
||||
let mint_pubkey = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(mint.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"invalid generated mint public key: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let program_id = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
||||
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"invalid Metaplex Program ID: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let (metadata, _) = solana_pubkey::Pubkey::find_program_address(
|
||||
&[b"metadata", program_id.as_ref(), mint_pubkey.as_ref()],
|
||||
&program_id,
|
||||
);
|
||||
let (master_edition, _) = solana_pubkey::Pubkey::find_program_address(
|
||||
&[b"metadata", program_id.as_ref(), mint_pubkey.as_ref(), b"edition"],
|
||||
&program_id,
|
||||
);
|
||||
let operation = create_operation_json(
|
||||
mint_pubkey.to_string(),
|
||||
metadata.to_string(),
|
||||
master_edition.to_string(),
|
||||
authority.clone(),
|
||||
);
|
||||
let operation_json = match serde_json::to_string_pretty(&operation) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_fixture_serialization_failed",
|
||||
format!("unable to serialize Metaplex Create fixture: {error}"),
|
||||
));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(crate::MetaplexCreateFixturePreparationSummary {
|
||||
mint_keypair_path,
|
||||
mint: mint_pubkey.to_string(),
|
||||
metadata: metadata.to_string(),
|
||||
master_edition: master_edition.to_string(),
|
||||
authority,
|
||||
operation_json,
|
||||
mint_created: preparation_signature.is_some(),
|
||||
preparation_signature,
|
||||
});
|
||||
}
|
||||
|
||||
async fn create_native_classic_nft_mint(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
options: &crate::MetaplexCreateFixturePreparationOptions,
|
||||
operator: &kb_wallet::TemporaryWallet,
|
||||
mint_wallet: &kb_wallet::TemporaryWallet,
|
||||
) -> kb_core::Result<std::string::String> {
|
||||
const MINT_SPACE: u64 = 82;
|
||||
let rent = match http_pool
|
||||
.get_minimum_balance_for_rent_exemption_for_role(
|
||||
options.query_role.as_str(),
|
||||
MINT_SPACE,
|
||||
&kb_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let payer = kb_lib::MdPubkey(operator.public_key());
|
||||
let mint = kb_lib::MdPubkey(mint_wallet.public_key());
|
||||
let policy =
|
||||
fixture_policy(profile, payer.clone(), mint.clone(), rent.minimum_balance_lamports);
|
||||
let system_intent = kb_lib::ExSolanaCoreExecutionIntent {
|
||||
intent_id: format!("metaplex-fixture-create-account-{}", mint.0),
|
||||
fee_payer: payer.clone(),
|
||||
policy: policy.clone(),
|
||||
operation: kb_lib::ExSolanaCoreOperation::SystemCreateAccount {
|
||||
from: payer.clone(),
|
||||
new_account: mint.clone(),
|
||||
lamports: rent.minimum_balance_lamports,
|
||||
space: MINT_SPACE,
|
||||
owner: kb_lib::MdPubkey(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
|
||||
},
|
||||
};
|
||||
let system_plan = match kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&kb_lib::ExSolanaCoreExecutor,
|
||||
&system_intent,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let token_program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"invalid classic SPL Token Program ID: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let mint_pubkey = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(mint.0.as_str())
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"invalid native fixture mint public key: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let authority_pubkey =
|
||||
match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(payer.0.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"invalid native fixture authority public key: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let token_instruction = match spl_token_interface::instruction::initialize_mint2(
|
||||
&token_program,
|
||||
&mint_pubkey,
|
||||
&authority_pubkey,
|
||||
std::option::Option::Some(&authority_pubkey),
|
||||
0,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_fixture_initialize_mint_build_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let plan = merge_fixture_instruction(
|
||||
system_plan,
|
||||
planned_instruction("spl.token.initialize_mint2", &token_instruction),
|
||||
policy,
|
||||
rent.minimum_balance_lamports,
|
||||
);
|
||||
let plan_evaluation = match kb_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if plan_evaluation.decision == kb_lib::ExSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_fixture_plan_denied",
|
||||
crate::violation_message(plan_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let latest_blockhash = match http_pool
|
||||
.get_latest_blockhash_for_role(
|
||||
options.query_role.as_str(),
|
||||
&kb_onchain_transport::GetLatestBlockhashConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let unsigned = match kb_lib::executor_solana_build_legacy_transaction(
|
||||
&plan,
|
||||
latest_blockhash.blockhash.as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fee = match http_pool
|
||||
.get_fee_for_message_for_role(
|
||||
options.query_role.as_str(),
|
||||
unsigned.message_base64().as_str(),
|
||||
&kb_onchain_transport::GetFeeForMessageConfig::new(
|
||||
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
|
||||
std::option::Option::Some(latest_blockhash.context.slot),
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let unsigned_base64 = match unsigned.transaction_base64() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let simulation_config = match kb_onchain_transport::SimulateTransactionConfig::new(
|
||||
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
|
||||
false,
|
||||
false,
|
||||
std::option::Option::Some(latest_blockhash.context.slot),
|
||||
true,
|
||||
std::option::Option::None,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let simulation_rpc = match http_pool
|
||||
.simulate_transaction_for_role(
|
||||
options.transaction_role.as_str(),
|
||||
unsigned_base64.as_str(),
|
||||
&simulation_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let simulation = simulation_rpc.to_execution_result(
|
||||
kb_lib::ExApiExecutionCluster::Devnet,
|
||||
kb_lib::ExApiExecutionBlockhashKind::Latest,
|
||||
std::option::Option::Some(
|
||||
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
|
||||
),
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(&fee),
|
||||
);
|
||||
if !simulation.success {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_fixture_simulation_failed",
|
||||
crate::simulation_failure_message(&simulation),
|
||||
));
|
||||
}
|
||||
let send_evaluation = match kb_lib::ExSafetyChecker.evaluate_send(&plan, &simulation) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if send_evaluation.decision == kb_lib::ExSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_fixture_send_denied",
|
||||
crate::violation_message(send_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let evidence = unsigned.bind_simulation(simulation);
|
||||
let signed = match unsigned
|
||||
.sign_after_simulation(&evidence, &[operator.as_signer(), mint_wallet.as_signer()])
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = signed.verify_signatures() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let signature = signed.primary_signature().clone();
|
||||
let send_config = match kb_onchain_transport::SendTransactionConfig::from_execution_config(
|
||||
&profile.execution,
|
||||
std::option::Option::Some(latest_blockhash.context.slot),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = http_pool
|
||||
.send_transaction_for_role(
|
||||
options.transaction_role.as_str(),
|
||||
signed.transaction_base64().as_str(),
|
||||
&signature,
|
||||
&send_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let confirmation_config =
|
||||
match kb_onchain_transport::ConfirmTransactionConfig::from_execution_config(
|
||||
&profile.execution,
|
||||
std::option::Option::Some(latest_blockhash.last_valid_block_height),
|
||||
std::option::Option::Some(latest_blockhash.context.slot),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let confirmation = match http_pool
|
||||
.confirm_transaction_for_roles(
|
||||
options.transaction_role.as_str(),
|
||||
options.query_role.as_str(),
|
||||
kb_lib::ExApiExecutionCluster::Devnet,
|
||||
&signature,
|
||||
&confirmation_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !matches!(
|
||||
confirmation.status,
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||
) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_fixture_confirmation_failed",
|
||||
format!("native mint preparation stopped at {:?}", confirmation.status),
|
||||
));
|
||||
}
|
||||
tracing::info!(
|
||||
target: "kb-pipeline-demo-scenarios.metaplex_fixture",
|
||||
action = "prepare_native_metaplex_mint",
|
||||
mint = mint_wallet.public_key(),
|
||||
authority = operator.public_key(),
|
||||
signature = signature.0.as_str(),
|
||||
"created and initialized native classic SPL mint for Metaplex fixture"
|
||||
);
|
||||
return std::result::Result::Ok(signature.0);
|
||||
}
|
||||
|
||||
fn planned_instruction(
|
||||
operation_code: &str,
|
||||
instruction: &solana_instruction::Instruction,
|
||||
) -> kb_lib::ExApiPlannedInstruction {
|
||||
return kb_lib::ExApiPlannedInstruction {
|
||||
program_id: kb_lib::MdProgramId(instruction.program_id.to_string()),
|
||||
operation_code: operation_code.to_string(),
|
||||
accounts: instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|account| {
|
||||
return kb_lib::ExApiPlannedAccount {
|
||||
pubkey: kb_lib::MdPubkey(account.pubkey.to_string()),
|
||||
is_signer: account.is_signer,
|
||||
is_writable: account.is_writable,
|
||||
};
|
||||
})
|
||||
.collect(),
|
||||
data: instruction.data.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
fn fixture_policy(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
payer: kb_lib::MdPubkey,
|
||||
mint: kb_lib::MdPubkey,
|
||||
_rent_lamports: u64,
|
||||
) -> kb_lib::ExApiExecutionPolicy {
|
||||
return kb_lib::ExApiExecutionPolicy {
|
||||
cluster: kb_lib::ExApiExecutionClusterPolicy {
|
||||
expected_cluster: kb_lib::ExApiExecutionCluster::Devnet,
|
||||
allow_mainnet: false,
|
||||
mainnet_confirmation: false,
|
||||
},
|
||||
simulation: kb_lib::ExApiExecutionSimulationPolicy::Required,
|
||||
blockhash: kb_lib::ExApiExecutionBlockhashPolicy {
|
||||
kind: kb_lib::ExApiExecutionBlockhashKind::Latest,
|
||||
max_age_slots: std::option::Option::Some(
|
||||
profile.execution.recent_blockhash_max_age_slots,
|
||||
),
|
||||
nonce_account: std::option::Option::None,
|
||||
nonce_authority: std::option::Option::None,
|
||||
},
|
||||
cost_limit: kb_lib::ExApiExecutionCostLimit {
|
||||
max_spend_lamports: std::option::Option::Some(
|
||||
profile.execution.devnet_max_spend_lamports,
|
||||
),
|
||||
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
|
||||
max_compute_unit_price_micro_lamports: std::option::Option::Some(
|
||||
profile.execution.max_compute_unit_price_micro_lamports,
|
||||
),
|
||||
},
|
||||
authorized_signers: std::vec![payer, mint],
|
||||
dry_run: false,
|
||||
post_execution_validation: kb_lib::ExApiPostExecutionValidationPolicy {
|
||||
canonical_insert_required: false,
|
||||
core_extraction_required: false,
|
||||
decode_replay_required: false,
|
||||
materialization_required: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn merge_fixture_instruction(
|
||||
mut system_plan: kb_lib::ExApiPreparedExecutionPlan,
|
||||
token_instruction: kb_lib::ExApiPlannedInstruction,
|
||||
policy: kb_lib::ExApiExecutionPolicy,
|
||||
rent_lamports: u64,
|
||||
) -> kb_lib::ExApiPreparedExecutionPlan {
|
||||
system_plan.instructions.push(token_instruction);
|
||||
system_plan.executor_name = "kb-pipeline-demo-scenarios.metaplex_fixture".to_string();
|
||||
system_plan.operation_code = "metadata.metaplex_token_metadata.prepare_native_mint".to_string();
|
||||
system_plan.policy = policy;
|
||||
system_plan.requested_spend_lamports = rent_lamports;
|
||||
return system_plan;
|
||||
}
|
||||
|
||||
async fn read_mint_account(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
query_role: &str,
|
||||
mint: &str,
|
||||
) -> kb_core::Result<std::option::Option<kb_onchain_transport::AccountInfoValue>> {
|
||||
let config = match kb_onchain_transport::GetAccountInfoConfig::confirmed_with_data(82) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mint_pubkey = kb_lib::MdPubkey(mint.to_string());
|
||||
let result = match http_pool.get_account_info_for_role(query_role, &mint_pubkey, &config).await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(result.account);
|
||||
}
|
||||
|
||||
fn validate_classic_nft_mint(
|
||||
account: &kb_onchain_transport::AccountInfoValue,
|
||||
expected_authority: &str,
|
||||
) -> kb_core::Result<()> {
|
||||
if account.owner.0 != kb_program_ids::SPL_TOKEN_PROGRAM_ID || account.executable {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Metaplex Create fixture mint owner mismatch: expected classic SPL Token, got {}",
|
||||
account.owner.0
|
||||
)));
|
||||
}
|
||||
let bytes = account.data.as_slice();
|
||||
if bytes.len() != 82 {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Metaplex Create fixture mint must use the classic 82-byte layout, got {} bytes",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
let mint_authority = match decode_coption_pubkey(&bytes[0..36], "mint authority") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let supply_bytes = match <[u8; 8]>::try_from(&bytes[36..44]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"invalid fixture mint supply bytes",
|
||||
));
|
||||
},
|
||||
};
|
||||
let supply = u64::from_le_bytes(supply_bytes);
|
||||
let decimals = bytes[44];
|
||||
let initialized = bytes[45] == 1;
|
||||
let freeze_authority = match decode_coption_pubkey(&bytes[46..82], "freeze authority") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if decimals != 0 || supply != 0 || !initialized {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Metaplex NFT fixture mint requires initialized=true, decimals=0 and supply=0, got initialized={initialized}, decimals={decimals}, supply={supply}"
|
||||
)));
|
||||
}
|
||||
if mint_authority.as_deref() != std::option::Option::Some(expected_authority)
|
||||
|| freeze_authority.as_deref() != std::option::Option::Some(expected_authority)
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Metaplex NFT fixture mint requires mint and freeze authorities to equal {expected_authority}; mint_authority={mint_authority:?}, freeze_authority={freeze_authority:?}"
|
||||
)));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn decode_coption_pubkey(
|
||||
bytes: &[u8],
|
||||
label: &str,
|
||||
) -> kb_core::Result<std::option::Option<std::string::String>> {
|
||||
if bytes.len() != 36 {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"invalid {label} field length: {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
let tag = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
|
||||
if tag == 0 {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
if tag != 1 {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"invalid {label} option tag: {tag}"
|
||||
)));
|
||||
}
|
||||
let key = match <[u8; 32]>::try_from(&bytes[4..36]) {
|
||||
std::result::Result::Ok(value) => solana_pubkey::Pubkey::new_from_array(value),
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"invalid {label} public key bytes"
|
||||
)));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(std::option::Option::Some(key.to_string()));
|
||||
}
|
||||
|
||||
fn create_operation_json(
|
||||
mint: std::string::String,
|
||||
metadata: std::string::String,
|
||||
master_edition: std::string::String,
|
||||
authority: std::string::String,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"operation": "create",
|
||||
"metadata": metadata,
|
||||
"master_edition": master_edition,
|
||||
"mint": mint,
|
||||
"mint_as_signer": false,
|
||||
"authority": authority,
|
||||
"update_authority": authority,
|
||||
"update_authority_as_signer": true,
|
||||
"spl_token_program": kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
"create_args": {"V1": {
|
||||
"name": "Khadhroony Devnet Asset",
|
||||
"symbol": "KHDEV",
|
||||
"uri": "https://example.invalid/khadhroony-devnet.json",
|
||||
"seller_fee_basis_points": 0,
|
||||
"creators": null,
|
||||
"primary_sale_happened": false,
|
||||
"is_mutable": true,
|
||||
"token_standard": "NonFungible",
|
||||
"collection": null,
|
||||
"uses": null,
|
||||
"collection_details": null,
|
||||
"rule_set": null,
|
||||
"decimals": null,
|
||||
"print_supply": "Zero"
|
||||
}}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn native_fixture_contract_uses_profile_authorities() {
|
||||
let authority = solana_pubkey::Pubkey::new_unique();
|
||||
let mut mint = [0_u8; 82];
|
||||
mint[0..4].copy_from_slice(&1_u32.to_le_bytes());
|
||||
mint[4..36].copy_from_slice(authority.as_ref());
|
||||
mint[44] = 0;
|
||||
mint[45] = 1;
|
||||
mint[46..50].copy_from_slice(&1_u32.to_le_bytes());
|
||||
mint[50..82].copy_from_slice(authority.as_ref());
|
||||
let decoded_mint = super::decode_coption_pubkey(&mint[0..36], "mint authority");
|
||||
let decoded_freeze = super::decode_coption_pubkey(&mint[46..82], "freeze authority");
|
||||
let authority_text = authority.to_string();
|
||||
assert_eq!(
|
||||
decoded_mint.ok().flatten().as_deref(),
|
||||
std::option::Option::Some(authority_text.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
decoded_freeze.ok().flatten().as_deref(),
|
||||
std::option::Option::Some(authority_text.as_str())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_contract_uses_classic_token_program_and_zero_print_supply() {
|
||||
let operation = super::create_operation_json(
|
||||
solana_pubkey::Pubkey::new_unique().to_string(),
|
||||
solana_pubkey::Pubkey::new_unique().to_string(),
|
||||
solana_pubkey::Pubkey::new_unique().to_string(),
|
||||
solana_pubkey::Pubkey::new_unique().to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
operation.get("spl_token_program").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some(kb_program_ids::SPL_TOKEN_PROGRAM_ID),
|
||||
);
|
||||
assert_eq!(
|
||||
operation
|
||||
.pointer("/create_args/V1/print_supply")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("Zero"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_devnet_execution.rs
|
||||
// version: 4
|
||||
// version: 6
|
||||
|
||||
//! Real Devnet simulation and submission for current Metaplex Token Metadata operations.
|
||||
|
||||
@@ -22,6 +22,8 @@ pub struct DevnetMetaplexTokenMetadataExecutionRequest {
|
||||
pub submit: bool,
|
||||
/// Explicit operator confirmation required for submission.
|
||||
pub operator_confirmed: bool,
|
||||
/// Materializes canonical account snapshots after confirmed submission.
|
||||
pub materialize_after_confirmation: bool,
|
||||
}
|
||||
|
||||
impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
|
||||
@@ -39,6 +41,7 @@ impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
|
||||
postcondition_reads: std::vec::Vec::new(),
|
||||
submit: false,
|
||||
operator_confirmed: false,
|
||||
materialize_after_confirmation: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,6 +99,12 @@ impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
|
||||
"Devnet Metaplex execution stateful read limit exceeded",
|
||||
));
|
||||
}
|
||||
if self.submit && self.materialize_after_confirmation && self.postcondition_reads.is_empty()
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Metaplex materialization requires at least one postcondition account read",
|
||||
));
|
||||
}
|
||||
let operation_code = self.operation.operation_code();
|
||||
if kb_lib::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES.contains(&operation_code) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
@@ -149,7 +158,7 @@ pub fn metaplex_token_metadata_current_operation_json_template(
|
||||
"authority": "<MINT_AUTHORITY>",
|
||||
"update_authority": "<UPDATE_AUTHORITY>",
|
||||
"update_authority_as_signer": true,
|
||||
"spl_token_program": null,
|
||||
"spl_token_program": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
||||
"create_args": {
|
||||
"V1": {
|
||||
"name": "Khadhroony Devnet Asset",
|
||||
@@ -165,7 +174,7 @@ pub fn metaplex_token_metadata_current_operation_json_template(
|
||||
"collection_details": null,
|
||||
"rule_set": null,
|
||||
"decimals": null,
|
||||
"print_supply": null
|
||||
"print_supply": "Zero"
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -298,6 +307,10 @@ pub struct DevnetMetaplexTokenMetadataExecutionSummary {
|
||||
pub confirmation: std::option::Option<kb_lib::ExApiExecutionConfirmationResult>,
|
||||
/// Stateful snapshots observed after confirmed submission.
|
||||
pub after: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadResult>,
|
||||
/// Whether canonical account materialization was requested.
|
||||
pub materialization_requested: bool,
|
||||
/// Canonical projections emitted from confirmed account snapshots.
|
||||
pub materialized_snapshots: std::vec::Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
struct PreparedMetaplexExecution {
|
||||
@@ -440,6 +453,10 @@ where
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if request.materialize_after_confirmation {
|
||||
summary.materialized_snapshots =
|
||||
materialize_confirmed_snapshots(summary.after.as_slice());
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(summary);
|
||||
}
|
||||
@@ -625,7 +642,16 @@ where
|
||||
},
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
std::result::Result::Err(error) => {
|
||||
let simulation_json = match serde_json::to_string_pretty(&simulation) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => "<simulation serialization unavailable>".to_string(),
|
||||
};
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
error.code(),
|
||||
format!("{}; simulation={simulation_json}", error.message()),
|
||||
));
|
||||
},
|
||||
};
|
||||
let evidence = unsigned.bind_simulation(simulation.clone());
|
||||
return std::result::Result::Ok(PreparedMetaplexExecution {
|
||||
@@ -648,10 +674,34 @@ where
|
||||
send_result: std::option::Option::None,
|
||||
confirmation: std::option::Option::None,
|
||||
after: std::vec::Vec::new(),
|
||||
materialization_requested: request.materialize_after_confirmation,
|
||||
materialized_snapshots: std::vec::Vec::new(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn materialize_confirmed_snapshots(
|
||||
snapshots: &[kb_pipeline::MetaplexTokenMetadataStatefulReadResult],
|
||||
) -> std::vec::Vec<serde_json::Value> {
|
||||
return snapshots
|
||||
.iter()
|
||||
.map(|result| {
|
||||
return serde_json::json!({
|
||||
"projectionVersion": 1,
|
||||
"domain": "metaplex_token_metadata",
|
||||
"projectionSemantics": "confirmed_bounded_account_snapshot",
|
||||
"account": &result.snapshot.account,
|
||||
"accountKind": &result.snapshot.account_kind,
|
||||
"mint": &result.snapshot.mint,
|
||||
"slot": result.snapshot.slot.to_string(),
|
||||
"commitment": &result.commitment,
|
||||
"state": &result.snapshot.payload_json,
|
||||
"externalUriFetched": false
|
||||
});
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
async fn read_snapshots(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
requests: &[kb_pipeline::MetaplexTokenMetadataStatefulReadRequest],
|
||||
@@ -709,7 +759,7 @@ fn build_plan(
|
||||
canonical_insert_required: request.submit,
|
||||
core_extraction_required: request.submit,
|
||||
decode_replay_required: request.submit,
|
||||
materialization_required: request.submit,
|
||||
materialization_required: request.submit && request.materialize_after_confirmation,
|
||||
},
|
||||
},
|
||||
allow_deprecated_operation: false,
|
||||
@@ -850,7 +900,24 @@ mod tests {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(serde_json::from_str::<serde_json::Value>(template.as_str()).is_ok());
|
||||
let parsed = serde_json::from_str::<serde_json::Value>(template.as_str());
|
||||
assert!(parsed.is_ok());
|
||||
if operation == "create" {
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(
|
||||
parsed
|
||||
.pointer("/create_args/V1/print_supply")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("Zero"),
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.get("spl_token_program").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",),
|
||||
);
|
||||
}
|
||||
}
|
||||
let placeholder = crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
|
||||
"placeholder",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_scenarios.rs
|
||||
// version: 2
|
||||
// version: 4
|
||||
|
||||
//! Deterministic Metaplex Token Metadata scenario inventory for tests and Devnet campaigns.
|
||||
//! Coherent Metaplex Token Metadata scenario journeys for tests and Devnet campaigns.
|
||||
|
||||
/// Stable asset family covered by one Metaplex scenario.
|
||||
#[derive(
|
||||
@@ -15,12 +15,52 @@ pub enum MetaplexTokenMetadataAssetFamily {
|
||||
Sft,
|
||||
/// Fungible token with Metaplex metadata.
|
||||
Fungible,
|
||||
/// Collection metadata and verification flow.
|
||||
/// Collection parent and member relationship.
|
||||
Collection,
|
||||
/// Programmable non-fungible token.
|
||||
ProgrammableNft,
|
||||
}
|
||||
|
||||
/// Stable fixture graph required by one scenario journey.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MetaplexTokenMetadataFixtureKind {
|
||||
/// Fresh zero-decimal classic SPL mint with profile-owned authorities.
|
||||
ClassicNftMint,
|
||||
/// Fresh SFT mint and token account.
|
||||
SemiFungibleMint,
|
||||
/// Fresh fungible mint and token account.
|
||||
FungibleMint,
|
||||
/// Collection parent plus one unverified member.
|
||||
CollectionParentAndMember,
|
||||
/// Programmable NFT with token record and optional rule set.
|
||||
ProgrammableNftTokenRecord,
|
||||
}
|
||||
|
||||
/// Stable lifecycle state consumed or produced by one scenario journey.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MetaplexTokenMetadataFixtureState {
|
||||
/// Mint exists but metadata does not.
|
||||
MintPrepared,
|
||||
/// Metadata exists and remains mutable.
|
||||
MetadataCreated,
|
||||
/// Master edition exists.
|
||||
MasterEditionCreated,
|
||||
/// Collection member exists but is not verified.
|
||||
CollectionMemberUnverified,
|
||||
/// Collection member is verified.
|
||||
CollectionMemberVerified,
|
||||
/// Programmable token record exists.
|
||||
ProgrammableTokenRecordCreated,
|
||||
/// Programmable asset is delegated.
|
||||
Delegated,
|
||||
/// Programmable asset is locked.
|
||||
Locked,
|
||||
/// Scenario has reached its terminal validated state.
|
||||
Validated,
|
||||
}
|
||||
|
||||
/// Execution mode accepted by one scenario.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -33,13 +73,21 @@ pub enum MetaplexTokenMetadataScenarioMode {
|
||||
NetworkSubmission,
|
||||
}
|
||||
|
||||
/// One stable Metaplex scenario reusable by automated tests and demo adapters.
|
||||
/// One stable Metaplex journey reusable by automated tests and demo adapters.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MetaplexTokenMetadataScenario {
|
||||
/// Stable scenario identifier.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible scenario label.
|
||||
pub label: std::string::String,
|
||||
/// Asset family exercised by the scenario.
|
||||
pub asset_family: crate::MetaplexTokenMetadataAssetFamily,
|
||||
/// Fixture graph required by the scenario.
|
||||
pub fixture_kind: crate::MetaplexTokenMetadataFixtureKind,
|
||||
/// Required initial lifecycle state.
|
||||
pub initial_state: crate::MetaplexTokenMetadataFixtureState,
|
||||
/// Expected terminal lifecycle state.
|
||||
pub resulting_state: crate::MetaplexTokenMetadataFixtureState,
|
||||
/// Execution mode.
|
||||
pub mode: crate::MetaplexTokenMetadataScenarioMode,
|
||||
/// Stable operation codes executed in order.
|
||||
@@ -50,93 +98,155 @@ pub struct MetaplexTokenMetadataScenario {
|
||||
pub requires_programmable_rules: bool,
|
||||
/// Whether a confirmed postcondition is mandatory.
|
||||
pub requires_postcondition: bool,
|
||||
/// Canonical account projections expected when materialization is enabled.
|
||||
pub materialization_targets: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Returns the complete ordered synthetic Metaplex scenario inventory.
|
||||
fn scenario(
|
||||
id: &str,
|
||||
label: &str,
|
||||
asset_family: crate::MetaplexTokenMetadataAssetFamily,
|
||||
fixture_kind: crate::MetaplexTokenMetadataFixtureKind,
|
||||
initial_state: crate::MetaplexTokenMetadataFixtureState,
|
||||
resulting_state: crate::MetaplexTokenMetadataFixtureState,
|
||||
operation_codes: std::vec::Vec<std::string::String>,
|
||||
requires_collection: bool,
|
||||
requires_programmable_rules: bool,
|
||||
materialization_targets: &[&str],
|
||||
) -> crate::MetaplexTokenMetadataScenario {
|
||||
return crate::MetaplexTokenMetadataScenario {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
asset_family,
|
||||
fixture_kind,
|
||||
initial_state,
|
||||
resulting_state,
|
||||
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
|
||||
operation_codes,
|
||||
requires_collection,
|
||||
requires_programmable_rules,
|
||||
requires_postcondition: true,
|
||||
materialization_targets: materialization_targets
|
||||
.iter()
|
||||
.map(|value| return (*value).to_string())
|
||||
.collect(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the complete ordered synthetic Metaplex journey inventory.
|
||||
pub fn metaplex_token_metadata_synthetic_scenarios()
|
||||
-> std::vec::Vec<crate::MetaplexTokenMetadataScenario> {
|
||||
return vec![
|
||||
crate::MetaplexTokenMetadataScenario {
|
||||
id: "metaplex_nft_create_update".to_string(),
|
||||
asset_family: crate::MetaplexTokenMetadataAssetFamily::Nft,
|
||||
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
|
||||
operation_codes: vec![
|
||||
scenario(
|
||||
"metaplex_nft_create_update",
|
||||
"NFT classique : création, mise à jour et édition maître",
|
||||
crate::MetaplexTokenMetadataAssetFamily::Nft,
|
||||
crate::MetaplexTokenMetadataFixtureKind::ClassicNftMint,
|
||||
crate::MetaplexTokenMetadataFixtureState::MintPrepared,
|
||||
crate::MetaplexTokenMetadataFixtureState::MasterEditionCreated,
|
||||
vec![
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION.to_string(),
|
||||
],
|
||||
requires_collection: false,
|
||||
requires_programmable_rules: false,
|
||||
requires_postcondition: true,
|
||||
},
|
||||
crate::MetaplexTokenMetadataScenario {
|
||||
id: "metaplex_sft_create_mint".to_string(),
|
||||
asset_family: crate::MetaplexTokenMetadataAssetFamily::Sft,
|
||||
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
|
||||
operation_codes: vec![
|
||||
false,
|
||||
false,
|
||||
&["metadata_state", "edition_state"],
|
||||
),
|
||||
scenario(
|
||||
"metaplex_sft_create_mint",
|
||||
"SFT : création des metadata puis émission de supply",
|
||||
crate::MetaplexTokenMetadataAssetFamily::Sft,
|
||||
crate::MetaplexTokenMetadataFixtureKind::SemiFungibleMint,
|
||||
crate::MetaplexTokenMetadataFixtureState::MintPrepared,
|
||||
crate::MetaplexTokenMetadataFixtureState::Validated,
|
||||
vec![
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_MINT_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
|
||||
],
|
||||
requires_collection: false,
|
||||
requires_programmable_rules: false,
|
||||
requires_postcondition: true,
|
||||
},
|
||||
crate::MetaplexTokenMetadataScenario {
|
||||
id: "metaplex_fungible_create_update".to_string(),
|
||||
asset_family: crate::MetaplexTokenMetadataAssetFamily::Fungible,
|
||||
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
|
||||
operation_codes: vec![
|
||||
false,
|
||||
false,
|
||||
&["metadata_state"],
|
||||
),
|
||||
scenario(
|
||||
"metaplex_fungible_create_update",
|
||||
"Fungible : création et transitions d’autorité",
|
||||
crate::MetaplexTokenMetadataAssetFamily::Fungible,
|
||||
crate::MetaplexTokenMetadataFixtureKind::FungibleMint,
|
||||
crate::MetaplexTokenMetadataFixtureState::MintPrepared,
|
||||
crate::MetaplexTokenMetadataFixtureState::Validated,
|
||||
vec![
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
|
||||
],
|
||||
requires_collection: false,
|
||||
requires_programmable_rules: false,
|
||||
requires_postcondition: true,
|
||||
},
|
||||
crate::MetaplexTokenMetadataScenario {
|
||||
id: "metaplex_collection_verify_unverify".to_string(),
|
||||
asset_family: crate::MetaplexTokenMetadataAssetFamily::Collection,
|
||||
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
|
||||
operation_codes: vec![
|
||||
false,
|
||||
false,
|
||||
&["metadata_state"],
|
||||
),
|
||||
scenario(
|
||||
"metaplex_collection_verify_unverify",
|
||||
"Collection : membre non vérifié, vérification puis révocation",
|
||||
crate::MetaplexTokenMetadataAssetFamily::Collection,
|
||||
crate::MetaplexTokenMetadataFixtureKind::CollectionParentAndMember,
|
||||
crate::MetaplexTokenMetadataFixtureState::CollectionMemberUnverified,
|
||||
crate::MetaplexTokenMetadataFixtureState::CollectionMemberVerified,
|
||||
vec![
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_VERIFY_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_UNVERIFY_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_SET_COLLECTION_SIZE_OPERATION.to_string(),
|
||||
],
|
||||
requires_collection: true,
|
||||
requires_programmable_rules: false,
|
||||
requires_postcondition: true,
|
||||
},
|
||||
crate::MetaplexTokenMetadataScenario {
|
||||
id: "metaplex_pnft_delegate_transfer_revoke".to_string(),
|
||||
asset_family: crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
|
||||
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
|
||||
operation_codes: vec![
|
||||
true,
|
||||
false,
|
||||
&["metadata_state", "edition_state"],
|
||||
),
|
||||
scenario(
|
||||
"metaplex_pnft_delegate_transfer_revoke",
|
||||
"pNFT : token record, délégation, verrouillage, transfert et révocation",
|
||||
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
|
||||
crate::MetaplexTokenMetadataFixtureKind::ProgrammableNftTokenRecord,
|
||||
crate::MetaplexTokenMetadataFixtureState::ProgrammableTokenRecordCreated,
|
||||
crate::MetaplexTokenMetadataFixtureState::Validated,
|
||||
vec![
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_DELEGATE_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_LOCK_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_UNLOCK_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_TRANSFER_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_REVOKE_OPERATION.to_string(),
|
||||
],
|
||||
requires_collection: false,
|
||||
requires_programmable_rules: true,
|
||||
requires_postcondition: true,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
&["metadata_state", "programmable_token_state", "delegate_admin_state"],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns the ordered Metaplex scenarios intended for opt-in Devnet simulation.
|
||||
/// Returns the ordered Metaplex journeys intended for opt-in Devnet simulation.
|
||||
pub fn metaplex_token_metadata_devnet_scenarios()
|
||||
-> std::vec::Vec<crate::MetaplexTokenMetadataScenario> {
|
||||
return crate::metaplex_token_metadata_synthetic_scenarios()
|
||||
.into_iter()
|
||||
.map(|mut scenario| {
|
||||
scenario.id = format!("{}_devnet_simulation", scenario.id);
|
||||
let mut scenario = scenario(
|
||||
"metaplex_nft_create_update_devnet",
|
||||
"NFT classique : création confirmée puis mise à jour",
|
||||
crate::MetaplexTokenMetadataAssetFamily::Nft,
|
||||
crate::MetaplexTokenMetadataFixtureKind::ClassicNftMint,
|
||||
crate::MetaplexTokenMetadataFixtureState::MintPrepared,
|
||||
crate::MetaplexTokenMetadataFixtureState::MetadataCreated,
|
||||
vec![
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
|
||||
],
|
||||
false,
|
||||
false,
|
||||
&["metadata_state", "edition_state"],
|
||||
);
|
||||
scenario.mode = crate::MetaplexTokenMetadataScenarioMode::NetworkSimulation;
|
||||
return scenario;
|
||||
})
|
||||
.collect::<std::vec::Vec<crate::MetaplexTokenMetadataScenario>>();
|
||||
return vec![scenario];
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn synthetic_inventory_is_exact_unique_and_covers_every_required_asset_family() {
|
||||
fn inventory_is_exact_unique_and_uses_coherent_fixture_graphs() {
|
||||
let scenarios = crate::metaplex_token_metadata_synthetic_scenarios();
|
||||
assert_eq!(scenarios.len(), 5);
|
||||
let ids = scenarios
|
||||
@@ -144,56 +254,43 @@ mod tests {
|
||||
.map(|scenario| return scenario.id.as_str())
|
||||
.collect::<std::collections::BTreeSet<&str>>();
|
||||
assert_eq!(ids.len(), scenarios.len());
|
||||
let families = scenarios
|
||||
.iter()
|
||||
.map(|scenario| return scenario.asset_family)
|
||||
.collect::<std::collections::BTreeSet<crate::MetaplexTokenMetadataAssetFamily>>(
|
||||
);
|
||||
assert_eq!(families.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthetic_scenarios_are_simulation_safe_and_require_postconditions() {
|
||||
for scenario in crate::metaplex_token_metadata_synthetic_scenarios() {
|
||||
assert_eq!(scenario.mode, crate::MetaplexTokenMetadataScenarioMode::Synthetic);
|
||||
for scenario in scenarios {
|
||||
assert!(!scenario.label.trim().is_empty());
|
||||
assert!(!scenario.operation_codes.is_empty());
|
||||
assert!(!scenario.materialization_targets.is_empty());
|
||||
assert!(scenario.requires_postcondition);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn devnet_inventory_mirrors_every_asset_family_as_network_simulation() {
|
||||
let synthetic = crate::metaplex_token_metadata_synthetic_scenarios();
|
||||
let devnet = crate::metaplex_token_metadata_devnet_scenarios();
|
||||
assert_eq!(devnet.len(), synthetic.len());
|
||||
assert!(devnet.iter().all(|scenario| return scenario.mode
|
||||
== crate::MetaplexTokenMetadataScenarioMode::NetworkSimulation));
|
||||
assert!(devnet.iter().all(|scenario| return scenario.id.ends_with("_devnet_simulation")));
|
||||
let families = devnet
|
||||
.iter()
|
||||
.map(|scenario| return scenario.asset_family)
|
||||
.collect::<std::collections::BTreeSet<crate::MetaplexTokenMetadataAssetFamily>>(
|
||||
);
|
||||
assert_eq!(families.len(), 5);
|
||||
fn collection_and_programmable_journeys_declare_special_requirements() {
|
||||
let scenarios = crate::metaplex_token_metadata_synthetic_scenarios();
|
||||
let collection = scenarios.iter().find(|scenario| {
|
||||
return scenario.asset_family == crate::MetaplexTokenMetadataAssetFamily::Collection;
|
||||
});
|
||||
assert!(collection.is_some_and(|scenario| return scenario.requires_collection));
|
||||
let programmable = scenarios.iter().find(|scenario| {
|
||||
return scenario.asset_family
|
||||
== crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft;
|
||||
});
|
||||
assert!(programmable.is_some_and(|scenario| return scenario.requires_programmable_rules));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn programmable_and_collection_requirements_are_declared_only_where_needed() {
|
||||
let scenarios = crate::metaplex_token_metadata_synthetic_scenarios();
|
||||
let programmable = scenarios
|
||||
.iter()
|
||||
.filter(|scenario| return scenario.requires_programmable_rules)
|
||||
.collect::<std::vec::Vec<&crate::MetaplexTokenMetadataScenario>>();
|
||||
assert_eq!(programmable.len(), 1);
|
||||
fn devnet_inventory_exposes_only_fully_preparable_journeys() {
|
||||
let devnet = crate::metaplex_token_metadata_devnet_scenarios();
|
||||
assert_eq!(devnet.len(), 1);
|
||||
let scenario = &devnet[0];
|
||||
assert_eq!(scenario.asset_family, crate::MetaplexTokenMetadataAssetFamily::Nft);
|
||||
assert_eq!(scenario.operation_codes.len(), 2);
|
||||
assert_eq!(
|
||||
programmable[0].asset_family,
|
||||
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft
|
||||
scenario.operation_codes[0],
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION
|
||||
);
|
||||
let collection = scenarios
|
||||
.iter()
|
||||
.filter(|scenario| return scenario.requires_collection)
|
||||
.collect::<std::vec::Vec<&crate::MetaplexTokenMetadataScenario>>();
|
||||
assert_eq!(collection.len(), 1);
|
||||
assert_eq!(collection[0].asset_family, crate::MetaplexTokenMetadataAssetFamily::Collection);
|
||||
assert_eq!(
|
||||
scenario.operation_codes[1],
|
||||
kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION
|
||||
);
|
||||
assert_eq!(scenario.mode, crate::MetaplexTokenMetadataScenarioMode::NetworkSimulation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-pipeline/USAGE.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# Utilisation de kb-pipeline
|
||||
|
||||
@@ -232,6 +232,18 @@ fn inspect_metaplex_preflight(
|
||||
|
||||
Une opération marquée dépréciée par `kb-lib` est refusée lorsque `allow_deprecated_operation` vaut `false`.
|
||||
|
||||
### Exigences stateful par famille
|
||||
|
||||
Le pipeline ne doit pas considérer un intent sérialisé comme prêt à exécuter sans snapshots adaptés :
|
||||
|
||||
- création : lire le mint et vérifier owner, décimales, supply, mint authority et freeze authority avant la construction finale ; les PDA metadata et edition doivent être absents ou compatibles selon la variante ;
|
||||
- mutation : lire metadata et, lorsque requis, edition, token account, token record, delegate record, collection metadata et rule set ;
|
||||
- vérification : prouver la relation d’autorité visée avant l’instruction et relire le bit ou l’enregistrement modifié après confirmation ;
|
||||
- délégation, verrouillage, transfert et burn : corréler le mint, le token account, le token record programmable et les autorités exactes ;
|
||||
- escrow, print, use, collect, migrate et resize : fournir les comptes spécialisés de la variante et définir une postcondition observable.
|
||||
|
||||
Une campagne sans modèle préparé doit rester bloquée. Elle ne doit jamais réutiliser l’intent de l’opération précédemment sélectionnée.
|
||||
|
||||
## Valider l’enveloppe d’exécution Metaplex
|
||||
|
||||
L’orchestration refuse la signature ou la soumission tant que la simulation exacte du message, les signers et la confirmation opérateur ne sont pas cohérents.
|
||||
@@ -272,3 +284,16 @@ fn summarize_metaplex_postconditions(
|
||||
```
|
||||
|
||||
`Contradicted` est prioritaire sur `Confirmed`, et l’absence de postcondition applicable reste `NotApplicable`.
|
||||
|
||||
## Orchestration des scénarios Metaplex
|
||||
|
||||
`kb-pipeline-demo-scenarios` expose des parcours déclaratifs plutôt que cent combinaisons forcées. Chaque parcours déclare :
|
||||
|
||||
- la famille d’asset ;
|
||||
- la fixture requise ;
|
||||
- l’état initial ;
|
||||
- les opérations ordonnées ;
|
||||
- l’état terminal attendu ;
|
||||
- les comptes et projections nécessaires aux postconditions.
|
||||
|
||||
Lorsque `materialize_after_confirmation` est activé, une soumission exige au moins une lecture de postcondition. Après confirmation, les snapshots bornés sont décodés et projetés. Lorsque l’option est désactivée, le runner conserve la simulation, la soumission, la confirmation et les postconditions demandées, mais n’émet pas les projections de matérialisation.
|
||||
|
||||
Reference in New Issue
Block a user