v0.4.8-pre.012
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: Cargo.toml
|
||||
# version: 34
|
||||
# version: 35
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
@@ -18,7 +18,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.8-pre.11"
|
||||
version = "0.4.8-pre.12"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
|
||||
@@ -99,7 +99,7 @@ tracing = { version = "^0.1", features = [] }
|
||||
tracing-appender = { version = "^0.2", features = [] }
|
||||
tracing-subscriber = { version = "^0.3", features = ["ansi", "env-filter", "chrono", "serde", "json"] }
|
||||
wincode = { version = "^0.5", features = ["alloc", "bytes", "derive", "indexmap", "smallvec", "uuid", "uuid-serde-compat"] }
|
||||
ts-rs = { version = "^12.0", features = ["bigdecimal", "bson", "bytes", "chrono", "indexmap", "ordered-float", "serde_json", "serde-json-impl", "tokio", "url", "uuid"] }
|
||||
ts-rs = { version = "^12.0", features = ["bigdecimal", "bson", "bytes", "chrono", "indexmap", "ordered-float", "serde-json-impl", "tokio", "url", "uuid"] }
|
||||
yellowstone-grpc-client = { version = "^13.1", features = [] }
|
||||
yellowstone-grpc-proto = { version = "^12.5", features = [] }
|
||||
uuid = { version = "^1.23", features = ["v4", "serde"] }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/audits/V0_4_8_PRE_011_TOKEN_2022_METADATA_COMPLETENESS_AUDIT.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# Audit `0.4.8-pre.011` — complétude Token-2022 Token Metadata
|
||||
|
||||
@@ -55,8 +55,10 @@ cargo test --workspace 1 285 tests, 0 échec, doc-test
|
||||
|
||||
Le `cargo clean` préalable a supprimé les artefacts obsolètes qui empêchaient Cargo de reconstruire le fichier `executor.rs` extrait depuis l’archive. La recompilation complète a ensuite exécuté le test direct `token_metadata_operations_share_matrix_family` et le test général de parité de matrice avec succès.
|
||||
|
||||
`fix-005` ne modifie aucun code de production et ajoute deux tests unitaires. La première reprise locale a confirmé que tout le workspace compilait, passait Clippy et les audits, mais ces deux nouveaux tests échouaient uniquement parce qu’ils attendaient les noms courts `update_token_metadata_authority` et `emit_token_metadata` au lieu des codes canoniques `spl.token_2022.update_token_metadata_authority` et `spl.token_2022.emit_token_metadata`. `fix-006` corrige ces deux assertions sans modifier le comportement de production. Le nombre attendu de tests unitaires `kb-lib` reste 695.
|
||||
`fix-005` ne modifie aucun code de production et ajoute deux tests unitaires. La première reprise locale a confirmé que tout le workspace compilait, passait Clippy et les audits, mais ces deux nouveaux tests échouaient uniquement parce qu’ils attendaient les noms courts `update_token_metadata_authority` et `emit_token_metadata` au lieu des codes canoniques `spl.token_2022.update_token_metadata_authority` et `spl.token_2022.emit_token_metadata`. `fix-006` corrige ces deux assertions sans modifier le comportement de production.
|
||||
|
||||
La reprise finale de `fix-006` a ensuite réussi `cargo check --workspace`, `cargo clippy --all-targets`, `python3 scripts/audit_rust_workspace_rules.py` et `cargo test --workspace`. La suite unitaire `kb-lib` compte **695 tests réussis, 0 échec**. `pre.011` est donc clôturée sans réserve fonctionnelle.
|
||||
|
||||
## Frontières maintenues
|
||||
|
||||
Les fixtures, soumissions Devnet, confirmations et preuves réseau restent dans `pre.012`. Le sous-panneau desktop Token-2022 reste dans `pre.013`. Aucune résolution off-chain n’est ajoutée par cette phase.
|
||||
Les fixtures, soumissions Devnet, confirmations et preuves réseau Token-2022 restent dans `pre.012`. Le réaudit Metaplex et ses compléments Devnet sont insérés en `pre.013`, ce qui décale la finalisation desktop metadata en `pre.014`. Aucune résolution off-chain n’est ajoutée par cette phase.
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<!-- file: docs/audits/V0_4_8_PRE_012_TOKEN_2022_METADATA_DEVNET_CAMPAIGN_AUDIT.md -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# Audit `0.4.8-pre.012` — campagne Devnet Token-2022 Token Metadata
|
||||
|
||||
## Statut
|
||||
|
||||
**Terminée et validée sur Devnet.**
|
||||
|
||||
Le delta initial omettait la dépendance directe `bs58` dans `kb-pipeline-demo-scenarios/Cargo.toml`, alors que deux modules de la crate l'utilisent directement. `pre.012-delta-fix-001` déclare `bs58.workspace = true` et enregistre les preuves de la campagne réelle exécutée le 7 août 2026.
|
||||
|
||||
`pre.011` avait fermé la complétude fonctionnelle des cinq instructions de `spl-token-metadata-interface`. `pre.012` confirme désormais leur orchestration réseau de bout en bout sur Token-2022 Devnet.
|
||||
|
||||
## Contrat couvert
|
||||
|
||||
La campagne couvre exactement, dans cet ordre :
|
||||
|
||||
1. `Initialize` ;
|
||||
2. `UpdateField` ;
|
||||
3. `Emit` ;
|
||||
4. `RemoveKey` ;
|
||||
5. `UpdateAuthority`.
|
||||
|
||||
La fixture crée un mint Token-2022 frais, initialise `MetadataPointer` avant `InitializeMint2`, pointe le metadata pointer sur le mint lui-même et préfinance le compte pour les réallocations TLV atteintes par la campagne.
|
||||
|
||||
## Fixture Devnet observée
|
||||
|
||||
```text
|
||||
mint=EHrx1evxgXLEgjb1sgc5dVTktXX1LiovXR9YFsZS3d4k
|
||||
preparation_signature=2yYciRNoTTNSGuuaBFE6925FPjSj4fXp8usSkpLhgEPuhQMJYdsEzkAzxBdjDhpPHCT3db9bLz9Ud4BU5QSQZzQ5
|
||||
initial_authority=J12WA6c42oqpWkLu1dFa4pCegkQJPpxabwc3RRCMSUuH
|
||||
final_authority=3RVXUmVvY7quUUEVsDKh5EcenVygwv3mo1iagPa4Pmq7
|
||||
```
|
||||
|
||||
La préparation stateful impose que `metadata_pointer` soit présent, auto-référent et détenu par l'autorité attendue, tandis que `token_metadata` doit être absent avant `Initialize`.
|
||||
|
||||
## Résultats réseau
|
||||
|
||||
| Étape | Signature | Slot | Postcondition | Matérialisations | `returnData` |
|
||||
|--------------------------------|--------------------------------------------------------------------------------------------|----------:|---------------|-----------------------------------------:|-------------:|
|
||||
| `InitializeTokenMetadata` | `48kzJR5VW1ZYSRkVrg6qyNmuHdAUu7NXcnzaNsSmH7SSmRXK1X6RQdCeeSQi6LhrrkgRFRGVnxkwXPtkiqG6EgnY` | 481804441 | `Confirmed` | 1 | 0 |
|
||||
| `UpdateTokenMetadataField` | `JrdB9EqePS1FVWLo55w1Vhv2eTCTfCpXZgMvPCocW3m3iu4nk3BFBBRHoe2Nbcpq1rmnZoFrhnag5fG2WwmJrYs` | 481804454 | `Confirmed` | 1 | 0 |
|
||||
| `EmitTokenMetadata` | `3TKdBCJhM1VEZ9ti6EYR9gSLJnH5ni6FjW4kvoqwdYK1u14jdY6hS8hf2VGeLi21PbX5CWiyzjthJZERK5KHroWH` | 481804464 | `Confirmed` | 1 observée, non requise par la politique | 202 |
|
||||
| `RemoveTokenMetadataKey` | `3kDBV7pDFgoCQVJ9DYYuxvHFbCJzyA6J2rLg6ndGCETEhhFBZ1ipRQ52X2hZfwNPfdJwEFFBmbyMU6kFGnVXJNrm` | 481804473 | `Confirmed` | 1 | 0 |
|
||||
| `UpdateTokenMetadataAuthority` | `5aBpQsm6mAHA5xSN5tZ1ubALbfghGa7JScjXn9NGhg9agzEdjXydH6jgmn6UmxinGHmMvtqaGRLk9dgjmmz4HpU2` | 481804482 | `Confirmed` | 1 | 0 |
|
||||
|
||||
Le runner n'accepte une étape qu'après simulation réussie, confirmation, hydratation canonique, extraction Core, replay Token-2022, seconde passe d'idempotence propre et postcondition stateful confirmée. Les mutations exigent en plus une matérialisation instructionnelle. `Emit` n'en exige pas ; sa preuve spécialisée est le `returnData`, décodé ici sur 202 octets et comparé au snapshot TLV autoritatif.
|
||||
|
||||
## Matrice
|
||||
|
||||
`test-fixtures/contract-matrices/SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json` conserve exactement les cinq scénarios de la campagne. `pre.012-delta-fix-001` les promeut tous à `confirmed` et enregistre pour chacun toutes les catégories de preuve requises.
|
||||
|
||||
Le test de matrice est mis à jour pour exiger cet état confirmé et vérifier que chaque catégorie requise possède une preuve observée.
|
||||
|
||||
## Validation locale
|
||||
|
||||
Les contrôles opérateur réussissent :
|
||||
|
||||
```text
|
||||
cargo fmt --all réussi
|
||||
cargo check --workspace réussi
|
||||
cargo clippy --all-targets réussi
|
||||
python3 scripts/audit_rust_workspace_rules.py clean
|
||||
cargo test -p kb-pipeline-demo-scenarios 69 + 1 + 1 tests réussis
|
||||
cargo test --workspace réussi
|
||||
```
|
||||
|
||||
Le workspace complet de `pre.012` termine ses 34 suites de tests avec 1 294 tests réussis au total et aucun échec.
|
||||
|
||||
## Conclusion
|
||||
|
||||
`0.4.8-pre.012` est clôturée : les cinq instructions Token-2022 Token Metadata prévues sont confirmées sur Devnet avec preuves de simulation, transaction, replay et postcondition. La phase suivante est `0.4.8-pre.013`, consacrée au réaudit et à la complétude Metaplex Token Metadata et de ses campagnes Devnet.
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md -->
|
||||
<!-- version: 23 -->
|
||||
<!-- version: 25 -->
|
||||
|
||||
# Plan `0.4.8` — Solana Program Metadata et complétude Token-2022
|
||||
|
||||
## 1. Statut et rôle
|
||||
|
||||
Ce document est le livrable principal de `0.4.8-pre.001`, réconcilié après l’audit contractuel de `0.4.8-pre.002`, les modèles de `pre.003`, les décodeurs de comptes de `pre.004`, le décodeur d’instructions de `pre.005`, la matérialisation de `pre.006`, l’exécuteur de `pre.007`, l’orchestration généraliste de `pre.008`, les scénarios réutilisables de `pre.009`, leur intégration desktop et leur campagne Devnet de `pre.010`, puis la complétude Token-2022 Token Metadata de `pre.011`. Il constitue le plan vivant de la version et reste modifiable lorsque l’audit du code, des interfaces officielles, de l’IDL ou des validations Devnet impose un ajustement.
|
||||
Ce document est le livrable principal de `0.4.8-pre.001`, réconcilié après l’audit contractuel de `0.4.8-pre.002`, les modèles de `pre.003`, les décodeurs de comptes de `pre.004`, le décodeur d’instructions de `pre.005`, la matérialisation de `pre.006`, l’exécuteur de `pre.007`, l’orchestration généraliste de `pre.008`, les scénarios réutilisables de `pre.009`, leur intégration desktop et leur campagne Devnet de `pre.010`, puis la complétude Token-2022 Token Metadata de `pre.011` et la préparation de sa campagne Devnet dédiée en `pre.012`. Il constitue le plan vivant de la version et reste modifiable lorsque l’audit du code, des interfaces officielles, de l’IDL ou des validations Devnet impose un ajustement.
|
||||
|
||||
Il doit être maintenu pendant chaque prerelease, puis archivé pendant la dernière prerelease après transfert des décisions durables vers le ROADMAP, les matrices, les guides, les rapports, les TODO et les changelogs concernés.
|
||||
|
||||
@@ -324,7 +324,7 @@ Travaux parallèles intégrés : les fonctions de matérialisation Metaplex exis
|
||||
- `Extend` et `Trim` sont désormais observées et validées sur Devnet ;
|
||||
- preuve brute et rapport conservés sous `docs/validation/evidence/v0.4.8/pre.010/devnet/` et `docs/validation/V0_4_8_PRE_010_SOLANA_PROGRAM_METADATA_DEVNET_VALIDATION_REPORT.md`.
|
||||
|
||||
### `0.4.8-pre.011` — complétude Token-2022 Token Metadata — terminée, validation fonctionnelle acquise
|
||||
### `0.4.8-pre.011` — complétude Token-2022 Token Metadata — terminée et validée
|
||||
|
||||
- fermeture des écarts d’intents/builders/exécution confirmés pour `UpdateAuthority` et `Emit`, après réaudit des cinq instructions `Initialize`, `UpdateField`, `RemoveKey`, `UpdateAuthority` et `Emit` ;
|
||||
- conservation des snapshots dédiés `metadata/token_2022/` pour `TokenMetadata`, `TokenGroup` et `TokenGroupMember` ;
|
||||
@@ -332,28 +332,46 @@ Travaux parallèles intégrés : les fonctions de matérialisation Metaplex exis
|
||||
- maintien de `InitializeMetadataPointer` et `UpdateMetadataPointer` dans le matérialiseur d’administration qui en possède déjà les faits ;
|
||||
- tests synthétiques et stateful.
|
||||
|
||||
Résultat durable : les cinq instructions de `spl-token-metadata-interface` disposent désormais d’intents et de builders exacts. `UpdateAuthority` accepte une nouvelle autorité nullable ; `Emit` valide une plage maximale de 1 024 octets, refuse une plage ouverte avec `start` sans `end`, conserve le `returnData` de simulation et décode uniquement les réponses complètes. La postcondition stateful vérifie l’autorité finale depuis le snapshot TLV autoritatif. Les interfaces Token Group restent possédées par le même matérialiseur spécialisé, tandis que Metadata Pointer reste au matérialiseur d’administration. `pre.011-fix-001` active l’implémentation TS-RS de `serde_json::Value` via `serde-json-impl` et ferme la plage `Emit` ouverte ; `pre.011-fix-002` aligne la fixture de `kb-pipeline` sur `ExApiExecutionBlockhashKind::Latest` ; `pre.011-fix-004` remplace `fix-003`, rattache `UpdateAuthority` et `Emit` à l’entrée `token_metadata` et ajoute un test direct de cette projection. Après nettoyage intégral du cache Cargo, `cargo test -p kb-lib` réussit avec 693 tests unitaires et toutes les APIs externes, puis `cargo test --workspace` réussit avec 1 285 tests et tous les doc-tests. `pre.011-fix-005` clôt la documentation et ajoute les régressions directes pour l’autorité `None`, l’émission complète et la plage maximale de 1 024 octets. Leur première reprise a révélé uniquement deux attentes de nommage incorrectes dans les tests eux-mêmes : les plans exposent les codes canoniques préfixés `spl.token_2022.*`. `pre.011-fix-006` corrige ces deux assertions sans toucher au code de production ; sa validation ciblée constitue le dernier contrôle avant de commencer `pre.012`.
|
||||
Résultat durable : les cinq instructions de `spl-token-metadata-interface` disposent désormais d’intents et de builders exacts. `UpdateAuthority` accepte une nouvelle autorité nullable ; `Emit` valide une plage maximale de 1 024 octets, refuse une plage ouverte avec `start` sans `end`, conserve le `returnData` de simulation et décode uniquement les réponses complètes. La postcondition stateful vérifie l’autorité finale depuis le snapshot TLV autoritatif. Les interfaces Token Group restent possédées par le même matérialiseur spécialisé, tandis que Metadata Pointer reste au matérialiseur d’administration. `pre.011-fix-001` active l’implémentation TS-RS de `serde_json::Value` via `serde-json-impl` et ferme la plage `Emit` ouverte ; `pre.011-fix-002` aligne la fixture de `kb-pipeline` sur `ExApiExecutionBlockhashKind::Latest` ; `pre.011-fix-004` remplace `fix-003`, rattache `UpdateAuthority` et `Emit` à l’entrée `token_metadata` et ajoute un test direct de cette projection. `pre.011-fix-005` ajoute les régressions directes pour l’autorité `None`, l’émission complète et la plage maximale de 1 024 octets ; `pre.011-fix-006` corrige uniquement leurs attentes de nommage afin d’utiliser les codes canoniques préfixés `spl.token_2022.*`. La reprise finale réussit avec `cargo check --workspace`, `cargo clippy --all-targets`, l’audit workspace et `cargo test --workspace` ; `kb-lib` compte alors 695 tests unitaires réussis sans échec. `pre.011` est clôturée.
|
||||
|
||||
### `0.4.8-pre.012` — campagne Devnet Token-2022 Token Metadata
|
||||
### `0.4.8-pre.012` — campagne Devnet Token-2022 Token Metadata — terminée et validée
|
||||
|
||||
- fixture mint ;
|
||||
- simulations, soumissions sûres, confirmations, postconditions, replay et matérialisation.
|
||||
- préparer nativement un mint Token-2022 frais dont `MetadataPointer` est initialisé avant `InitializeMint2` et pointe vers le mint lui-même ;
|
||||
- préfinancer le mint pour la taille maximale atteinte par la campagne afin que les réallocations du TLV `TokenMetadata` restent rent-exempt ;
|
||||
- exécuter dans l’ordre `Initialize`, `UpdateField`, `Emit`, `RemoveKey`, puis `UpdateAuthority` ;
|
||||
- conserver simulation, signature, confirmation, hydratation canonique, extraction Core, replay, matérialisation instructionnelle et seconde passe d’idempotence ;
|
||||
- pour `Emit`, conserver et décoder le `returnData` sans exiger une matérialisation instructionnelle obligatoire ;
|
||||
- relire le mint après chaque transaction et vérifier la postcondition depuis le snapshot TLV autoritatif ;
|
||||
- promouvoir `SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json` uniquement après réception des preuves réseau réelles ;
|
||||
- conserver un test Devnet opt-in nécessitant une confirmation opérateur explicite avant toute dépense.
|
||||
|
||||
### `0.4.8-pre.013` — finalisation desktop metadata
|
||||
Résultat durable : la campagne réelle du 7 août 2026 confirme les cinq scénarios sur Devnet. Les cinq transactions possèdent une signature, un slot et une postcondition `Confirmed` ; `Emit` fournit 202 octets de `returnData` validés contre le snapshot TLV autoritatif. La matrice réseau est entièrement `confirmed`. `pre.012-delta-fix-001` corrige en outre l'omission de la dépendance directe `bs58` dans `kb-pipeline-demo-scenarios`, puis met à jour le test de matrice et les documents de preuve. Les contrôles `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets`, l'audit workspace, les tests de la crate et `cargo test --workspace` réussissent. `pre.012` est clôturée.
|
||||
|
||||
- compléter le sous-panneau Token-2022 après les travaux de `pre.011` et `pre.012` ;
|
||||
### `0.4.8-pre.013` — réaudit et complétude Metaplex Token Metadata et campagnes Devnet
|
||||
|
||||
- réauditer la couverture actuelle de Metaplex Token Metadata : opérations courantes, formes historiques/dépréciées, comptes, builders, exécuteur, préflight, postconditions, matérialisation et matrices ;
|
||||
- compléter uniquement les écarts réellement constatés, sans rouvrir artificiellement les surfaces déjà exactes ;
|
||||
- reprendre les campagnes Devnet existantes et vérifier leurs fixtures pour NFT, SFT, token fongible, collection et pNFT ;
|
||||
- compléter les fixtures spécialisées encore nécessaires, notamment éditions imprimées/burn, collection parent-membre et parcours pNFT avec token records, delegates ou rule sets lorsque le contrat l’exige ;
|
||||
- exécuter ou qualifier chaque opération réseau avec simulation, signature, confirmation, lectures stateful, replay, matérialisation et postcondition ;
|
||||
- ne jamais promouvoir une preuve synthétique ou une simple disponibilité de builder en preuve Devnet confirmée ;
|
||||
- produire ou mettre à jour les matrices et rapports Metaplex avant la finalisation desktop.
|
||||
|
||||
### `0.4.8-pre.014` — finalisation desktop metadata
|
||||
|
||||
- compléter le sous-panneau Token-2022 après les travaux de `pre.011` et `pre.012` et intégrer les éventuels compléments Metaplex de `pre.013` ;
|
||||
- conserver des accordéons ou sous-panneaux séparés pour `ProgM6…`, Token-2022 et Metaplex ;
|
||||
- réutiliser le JsonViewer commun pour les résultats intégralement JSON ;
|
||||
- réconcilier préparation, simulation, soumission, lectures et preuves des trois domaines ;
|
||||
- aucun bouton de fetch URI.
|
||||
|
||||
### `0.4.8-pre.014` — stockage, matrices et réconciliation transversale
|
||||
### `0.4.8-pre.015` — stockage, matrices et réconciliation transversale
|
||||
|
||||
- stockage si de nouveaux contrats persistants sont requis ;
|
||||
- exports, registres, matrices, nomenclature IDL et guides ;
|
||||
- retrait des statuts `reserved` uniquement lorsque l’implémentation correspondante est réelle.
|
||||
|
||||
### `0.4.8-pre.015` — clôture obligatoire
|
||||
### `0.4.8-pre.016` — clôture obligatoire
|
||||
|
||||
Réservée à :
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<!-- file: docs/validation/V0_4_8_PRE_012_TOKEN_2022_METADATA_DEVNET_VALIDATION_REPORT.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Validation Devnet `0.4.8-pre.012` — Token-2022 Token Metadata
|
||||
|
||||
## Statut
|
||||
|
||||
**Préparée, non exécutée.**
|
||||
|
||||
Aucune signature, aucun slot et aucune postcondition réseau ne sont préremplis dans ce rapport. La preuve synthétique ou la seule réussite des tests locaux ne doit pas être présentée comme une validation Devnet confirmée.
|
||||
|
||||
## Périmètre réseau
|
||||
|
||||
Program ID :
|
||||
|
||||
```text
|
||||
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
|
||||
```
|
||||
|
||||
Campagne ordonnée :
|
||||
|
||||
1. `InitializeTokenMetadata` ;
|
||||
2. `UpdateTokenMetadataField` ;
|
||||
3. `EmitTokenMetadata` ;
|
||||
4. `RemoveTokenMetadataKey` ;
|
||||
5. `UpdateTokenMetadataAuthority`.
|
||||
|
||||
La préparation crée un mint Token-2022 frais avec `MetadataPointer` auto-référent, préfinancé pour les réallocations de metadata atteintes pendant la campagne.
|
||||
|
||||
## Matrice initiale
|
||||
|
||||
| Scénario | Statut initial | Preuve réseau |
|
||||
|----------------------------------------|----------------|---------------|
|
||||
| `token_2022_metadata_initialize` | `not_run` | aucune |
|
||||
| `token_2022_metadata_update_field` | `not_run` | aucune |
|
||||
| `token_2022_metadata_emit` | `not_run` | aucune |
|
||||
| `token_2022_metadata_remove_key` | `not_run` | aucune |
|
||||
| `token_2022_metadata_update_authority` | `not_run` | aucune |
|
||||
|
||||
Source machine-readable : `test-fixtures/contract-matrices/SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json`.
|
||||
|
||||
## Commande opérateur
|
||||
|
||||
Après les validations locales de compilation :
|
||||
|
||||
```bash
|
||||
KB_DEVNET_TOKEN_2022_METADATA_CAMPAIGN_TEST=1 \
|
||||
KB_DEVNET_TOKEN_2022_METADATA_OPERATOR_CONFIRMED=1 \
|
||||
KB_POSTGRES_TEST_URL='postgresql://…' \
|
||||
cargo test -p kb-pipeline-demo-scenarios optional_devnet_token_2022_metadata_campaign_from_env -- --nocapture
|
||||
```
|
||||
|
||||
`KB_DEVNET_PROFILE` permet de sélectionner un profil précis ; `KB_DEVNET_CONFIG_PATH` permet de remplacer le fichier de configuration par défaut.
|
||||
|
||||
## Preuves à enregistrer
|
||||
|
||||
Pour les quatre mutations :
|
||||
|
||||
- simulation réussie ;
|
||||
- signature ;
|
||||
- confirmation et slot ;
|
||||
- hydratation canonique ;
|
||||
- extraction Core ;
|
||||
- replay Token-2022 ;
|
||||
- matérialisation instructionnelle Token Metadata ;
|
||||
- postcondition stateful ;
|
||||
- seconde passe d'idempotence.
|
||||
|
||||
Pour `Emit` :
|
||||
|
||||
- simulation réussie ;
|
||||
- `returnData` et longueur décodée ;
|
||||
- signature ;
|
||||
- confirmation et slot ;
|
||||
- hydratation canonique ;
|
||||
- extraction Core ;
|
||||
- replay Token-2022 ;
|
||||
- postcondition stateful comparée au contenu décodé ;
|
||||
- seconde passe d'idempotence.
|
||||
|
||||
## Résultat attendu
|
||||
|
||||
La campagne est considérée validée uniquement si les cinq étapes sont confirmées et si leurs postconditions sont `Confirmed`. Après exécution, ce rapport et la matrice doivent être mis à jour avec les preuves réellement observées ; aucune valeur ne doit être reconstruite ou inventée.
|
||||
@@ -1,8 +1,17 @@
|
||||
<!-- file: kb-pipeline-demo-scenarios/CHANGELOG.md -->
|
||||
<!-- version: 33 -->
|
||||
<!-- version: 34 -->
|
||||
|
||||
# CHANGELOG — kb-pipeline-demo-scenarios
|
||||
|
||||
## `0.4.8-pre.012`
|
||||
|
||||
- ajoute une fixture native de mint Token-2022 avec `MetadataPointer` auto-référent initialisé avant `InitializeMint2` ;
|
||||
- préfinance le mint pour les réallocations du TLV Token Metadata atteintes par la campagne ;
|
||||
- ajoute une campagne Devnet ordonnée `Initialize → UpdateField → Emit → RemoveKey → UpdateAuthority` avec simulation, soumission autorisée, confirmation, hydratation, replay, matérialisation et postconditions stateful ;
|
||||
- étend le préflight générique Token-2022 aux opérations Token Metadata et n’exige aucune matérialisation instructionnelle pour `Emit` ;
|
||||
- ajoute une matrice réseau conservative à cinq scénarios, initialement intégralement `not_run` ;
|
||||
- ajoute un test Devnet opt-in avec sortie structurée des signatures, slots, postconditions et octets de `returnData`.
|
||||
|
||||
## `0.4.8-pre.011`
|
||||
|
||||
- adapte la fixture synthétique de simulation au nouveau champ optionnel `return_data` du contrat commun ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: kb-pipeline-demo-scenarios/Cargo.toml
|
||||
# version: 7
|
||||
# version: 9
|
||||
|
||||
[package]
|
||||
name = "kb-pipeline-demo-scenarios"
|
||||
@@ -19,6 +19,7 @@ path = "src/bin/kb_pipeline_demo_scenarios_cli.rs"
|
||||
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
bs58.workspace = true
|
||||
chrono.workspace = true
|
||||
kb-core = { path = "../kb-core" }
|
||||
kb-config = { path = "../kb-config" }
|
||||
@@ -32,6 +33,7 @@ mpl-token-metadata.workspace = true
|
||||
solana-instruction.workspace = true
|
||||
solana-pubkey.workspace = true
|
||||
spl-token-interface.workspace = true
|
||||
spl-token-2022-interface.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-pipeline-demo-scenarios/README.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# kb-pipeline-demo-scenarios
|
||||
|
||||
@@ -19,9 +19,9 @@ La crate contient :
|
||||
- fournir des tests automatisés parallèles aux démonstrations UI, sans déplacer les scénarios UI hors du desktop ;
|
||||
- conserver la séparation entre preuve synthétique, simulation RPC, soumission confirmée et postcondition stateful.
|
||||
|
||||
La crate couvre déjà les scénarios Solana Core et SPL historiques, le corpus synthétique Metaplex pour NFT, SFT, token fongible, collection et pNFT, ainsi que deux parcours Solana Program Metadata couvrant les neuf opérations stables. La fixture `ProgM6…` prépare des PDA non canoniques préfinancés et la campagne réutilisable conserve simulation, soumission, confirmation, lectures stateful et projections matérialisées.
|
||||
La crate couvre les scénarios Solana Core et SPL historiques, le corpus synthétique et le runner Devnet Metaplex pour NFT, SFT, token fongible, collection et pNFT, ainsi que deux parcours Solana Program Metadata couvrant les neuf opérations stables. Les validations spécialisées restent qualifiées séparément au lieu d’être déduites de la seule présence d’un builder.
|
||||
|
||||
La crate couvre déjà les scénarios Solana Core et SPL historiques ainsi que le corpus synthétique Metaplex pour NFT, SFT, token fongible, collection et pNFT. Un exécuteur Devnet réel et réutilisable est disponible pour les opérations Metaplex courantes pilotées par un seul wallet de profil. Il réalise le contrôle du genesis hash, les lectures stateful bornées, le préflight, la simulation RPC exacte et, après confirmation explicite, la soumission, la confirmation et les lectures avant/après. Le runner Devnet sait construire les opérations courantes, tandis que les parcours réellement préparés fournissent leurs fixtures, preuves RPC et postconditions. Les validations spécialisées restent qualifiées séparément au lieu d’être déduites de la seule présence d’un builder.
|
||||
Pour Token-2022 Token Metadata, `0.4.8-pre.012` ajoute une campagne Devnet dédiée aux cinq instructions de `spl-token-metadata-interface`. Elle prépare nativement un mint frais avec `MetadataPointer`, puis enchaîne `Initialize`, `UpdateField`, `Emit`, `RemoveKey` et `UpdateAuthority` avec simulation-first, soumission explicitement autorisée, confirmation, replay, matérialisation lorsque applicable et postcondition stateful. Sa matrice réseau reste `not_run` avant exécution réelle.
|
||||
|
||||
## Binaire CLI
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
<!-- file: kb-pipeline-demo-scenarios/TODO.md -->
|
||||
<!-- version: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# TODO — kb-pipeline-demo-scenarios
|
||||
|
||||
## Validation Devnet Solana Program Metadata
|
||||
## Validation Devnet Token-2022 Token Metadata
|
||||
|
||||
- [ ] Exécuter la campagne confirmée des neuf opérations avec un wallet Devnet dédié.
|
||||
- [ ] Conserver les signatures, simulations, lectures avant/après et postconditions dans la matrice de validation.
|
||||
- [ ] Confirmer spécifiquement `Trim` et `Extend`, absentes de l’échantillon mainnet `pre.008`.
|
||||
- [ ] Vérifier le nettoyage ou la fermeture complète des deux PDA de fixture.
|
||||
- [ ] Exécuter la campagne `0.4.8-pre.012` sur un mint frais avec confirmation opérateur explicite.
|
||||
- [ ] Conserver les cinq signatures, slots, simulations, preuves de replay et postconditions dans la matrice et le rapport de validation.
|
||||
- [ ] Conserver le `returnData` complet de `Emit` et vérifier son décodage contre le snapshot TLV autoritatif.
|
||||
|
||||
## Validation Devnet complémentaire Metaplex Token Metadata
|
||||
## `0.4.8-pre.013` — réaudit Metaplex Token Metadata
|
||||
|
||||
- [ ] Préparer les fixtures d’édition imprimée et de burn NFT.
|
||||
- [ ] Préparer les fixtures collection parent/membre pour verify, unverify et set-and-verify.
|
||||
- [ ] Préparer les token accounts, token records, delegates et rule sets des parcours pNFT.
|
||||
- [ ] Qualifier chaque opération spécialisée comme simulée, soumise, non applicable ou indisponible.
|
||||
- [ ] Réauditer la couverture courante/historique, les builders, les comptes, les matérialisations et les matrices Metaplex.
|
||||
- [ ] Compléter uniquement les écarts réels constatés.
|
||||
- [ ] Reprendre les campagnes Devnet et compléter les fixtures spécialisées encore nécessaires pour éditions/burn, collections et pNFT.
|
||||
- [ ] Qualifier chaque opération réseau avec des preuves réellement observées ou une indisponibilité justifiée.
|
||||
|
||||
## Évolutions générales
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-pipeline-demo-scenarios/USAGE.md -->
|
||||
<!-- version: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# Utilisation de kb-pipeline-demo-scenarios
|
||||
|
||||
@@ -100,6 +100,23 @@ where
|
||||
}
|
||||
```
|
||||
|
||||
## Exécuter la campagne Devnet Token-2022 Token Metadata
|
||||
|
||||
La campagne `0.4.8-pre.012` prépare un mint Token-2022 frais, initialise son `MetadataPointer` vers le mint lui-même, puis exécute les cinq opérations Token Metadata dans l’ordre. Elle soumet des transactions réelles et exige donc une confirmation opérateur explicite.
|
||||
|
||||
Le test opt-in est le chemin opérateur recommandé :
|
||||
|
||||
```bash
|
||||
KB_DEVNET_TOKEN_2022_METADATA_CAMPAIGN_TEST=1 \
|
||||
KB_DEVNET_TOKEN_2022_METADATA_OPERATOR_CONFIRMED=1 \
|
||||
KB_POSTGRES_TEST_URL='postgresql://…' \
|
||||
cargo test -p kb-pipeline-demo-scenarios optional_devnet_token_2022_metadata_campaign_from_env -- --nocapture
|
||||
```
|
||||
|
||||
`KB_DEVNET_PROFILE` peut sélectionner explicitement le profil Devnet et `KB_DEVNET_CONFIG_PATH` peut remplacer `config/example.config.json`. Le profil doit utiliser un wallet persistant, autoriser les soumissions Devnet et respecter les plafonds de dépense et de frais.
|
||||
|
||||
La sortie `TOKEN_2022_METADATA_FIXTURE` conserve le mint et la signature de préparation. Chaque ligne `TOKEN_2022_METADATA_STEP` conserve l’opération, la signature, le slot, la postcondition, le nombre de matérialisations et, pour `Emit`, la taille du `returnData`. Ces preuves doivent être reportées dans `SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json` et dans le rapport `V0_4_8_PRE_012_TOKEN_2022_METADATA_DEVNET_VALIDATION_REPORT.md` ; elles ne sont jamais préremplies avant l’exécution réelle.
|
||||
|
||||
## Charger la matrice de validation Token-2022
|
||||
|
||||
```rust
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/lib.rs
|
||||
// version: 14
|
||||
// version: 15
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -23,10 +23,13 @@ mod solana_metaplex_token_metadata_scenarios;
|
||||
mod solana_metaplex_token_metadata_validation;
|
||||
mod solana_token_2022_devnet_execution;
|
||||
mod solana_token_2022_devnet_scenarios;
|
||||
mod solana_token_2022_metadata_campaign;
|
||||
mod solana_token_2022_metadata_validation;
|
||||
mod solana_token_2022_validation;
|
||||
mod solana_token_execution;
|
||||
mod solana_token_lifecycle;
|
||||
mod token_2022_fixture;
|
||||
mod token_2022_metadata_fixture;
|
||||
|
||||
/// Readiness report for one Devnet profile PostgreSQL store.
|
||||
pub use self::environment::DevnetProfileStoreReadiness;
|
||||
@@ -208,6 +211,28 @@ pub use self::solana_token_2022_devnet_scenarios::DevnetSplValidationImplementat
|
||||
pub use self::solana_token_2022_devnet_scenarios::DevnetSplValidationScenario;
|
||||
/// Returns the complete ordered Devnet scenario inventory.
|
||||
pub use self::solana_token_2022_devnet_scenarios::devnet_spl_validation_scenarios;
|
||||
/// One confirmed step in the five-instruction Token Metadata Devnet campaign.
|
||||
pub use self::solana_token_2022_metadata_campaign::DevnetToken2022MetadataCampaignStepSummary;
|
||||
/// Complete evidence retained by the Token Metadata Devnet campaign.
|
||||
pub use self::solana_token_2022_metadata_campaign::DevnetToken2022MetadataCampaignSummary;
|
||||
/// Executes all five Token Metadata interface instructions on one fresh Devnet mint.
|
||||
pub use self::solana_token_2022_metadata_campaign::execute_devnet_token_2022_metadata_campaign;
|
||||
/// Returns the exact ordered operation names covered by the Token Metadata campaign.
|
||||
pub use self::solana_token_2022_metadata_campaign::token_2022_metadata_campaign_operation_names;
|
||||
/// Maximum evidence entries retained by one Token Metadata validation scenario.
|
||||
pub use self::solana_token_2022_metadata_validation::MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE;
|
||||
/// One bounded evidence item retained by the Token Metadata validation matrix.
|
||||
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationEvidence;
|
||||
/// Canonical Token Metadata Devnet validation matrix.
|
||||
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationMatrix;
|
||||
/// One scenario in the Token Metadata Devnet validation matrix.
|
||||
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationScenario;
|
||||
/// Exact network-validation status for one Token Metadata scenario.
|
||||
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationStatus;
|
||||
/// Loads and validates the Token Metadata Devnet validation matrix.
|
||||
pub use self::solana_token_2022_metadata_validation::load_token_2022_metadata_validation_matrix;
|
||||
/// Validates exact Token Metadata campaign coverage and evidence claims.
|
||||
pub use self::solana_token_2022_metadata_validation::validate_token_2022_metadata_validation_matrix;
|
||||
/// Maximum number of evidence records accepted in one validation report.
|
||||
pub use self::solana_token_2022_validation::MAX_TOKEN_2022_VALIDATION_EVIDENCE;
|
||||
/// Required validation environment for one Token-2022 scenario.
|
||||
@@ -276,6 +301,22 @@ pub use self::token_2022_fixture::Token2022FixturePreparationOptions;
|
||||
pub use self::token_2022_fixture::Token2022FixturePreparationSummary;
|
||||
/// Creates or reuses one complete Token-2022 public scenario fixture.
|
||||
pub use self::token_2022_fixture::prepare_token_2022_fixture;
|
||||
/// Additional metadata key created and removed by the campaign.
|
||||
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_KEY;
|
||||
/// Stable metadata name used by the `0.4.8-pre.012` campaign.
|
||||
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_NAME;
|
||||
/// Stable metadata symbol used by the `0.4.8-pre.012` campaign.
|
||||
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_SYMBOL;
|
||||
/// Stable opaque URI stored by the `0.4.8-pre.012` campaign.
|
||||
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_URI;
|
||||
/// Additional metadata value created and removed by the campaign.
|
||||
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_VALUE;
|
||||
/// Options used to prepare one fresh Token-2022 metadata fixture.
|
||||
pub use self::token_2022_metadata_fixture::Token2022MetadataFixturePreparationOptions;
|
||||
/// Fresh mint and authorities prepared for one Token Metadata campaign.
|
||||
pub use self::token_2022_metadata_fixture::Token2022MetadataFixturePreparationSummary;
|
||||
/// Creates one fresh Token-2022 mint with a self-referential Metadata Pointer.
|
||||
pub use self::token_2022_metadata_fixture::prepare_token_2022_metadata_fixture;
|
||||
|
||||
/// Total transactions reserved by one complete Solana Program Metadata campaign.
|
||||
pub(crate) use self::constants::SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_devnet_execution.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Devnet Token-2022 execution with stateful and canonical post-validation.
|
||||
|
||||
@@ -749,6 +749,43 @@ fn preflight_request(
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
},
|
||||
kb_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
|
||||
metadata,
|
||||
update_authority: _,
|
||||
mint,
|
||||
mint_authority: _,
|
||||
name: _,
|
||||
symbol: _,
|
||||
uri: _,
|
||||
} => {
|
||||
if metadata != mint {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_token_2022_metadata_mint_mismatch",
|
||||
"Token Metadata Initialize on Token-2022 requires metadata and mint to be the same account",
|
||||
));
|
||||
}
|
||||
requirements.push(metadata_mint_requirement("metadata", metadata, "metadata_pointer"));
|
||||
},
|
||||
kb_lib::ExSplTokenSingleOperation::UpdateTokenMetadataField {
|
||||
metadata,
|
||||
update_authority: _,
|
||||
field: _,
|
||||
value: _,
|
||||
}
|
||||
| kb_lib::ExSplTokenSingleOperation::RemoveTokenMetadataKey {
|
||||
metadata,
|
||||
update_authority: _,
|
||||
key: _,
|
||||
idempotent: _,
|
||||
}
|
||||
| kb_lib::ExSplTokenSingleOperation::UpdateTokenMetadataAuthority {
|
||||
metadata,
|
||||
current_authority: _,
|
||||
new_authority: _,
|
||||
}
|
||||
| kb_lib::ExSplTokenSingleOperation::EmitTokenMetadata { metadata, start: _, end: _ } => {
|
||||
requirements.push(metadata_mint_requirement("metadata", metadata, "token_metadata"));
|
||||
},
|
||||
_ => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_token_2022_devnet_operation_unsupported",
|
||||
@@ -797,6 +834,16 @@ fn mint_requirement(
|
||||
};
|
||||
}
|
||||
|
||||
fn metadata_mint_requirement(
|
||||
role: &str,
|
||||
mint: &kb_lib::MdPubkey,
|
||||
required_extension: &str,
|
||||
) -> kb_pipeline::Token2022PreflightRequirement {
|
||||
let mut requirement = mint_requirement(role, mint, std::option::Option::None);
|
||||
requirement.required_extensions = std::vec![required_extension.to_string()];
|
||||
return requirement;
|
||||
}
|
||||
|
||||
fn account_requirement(
|
||||
role: &str,
|
||||
account: &kb_lib::MdPubkey,
|
||||
@@ -902,8 +949,12 @@ fn build_plan(
|
||||
);
|
||||
}
|
||||
|
||||
fn operation_requires_materialization(_operation: &kb_lib::ExSplToken2022Operation) -> bool {
|
||||
return true;
|
||||
fn operation_requires_materialization(operation: &kb_lib::ExSplToken2022Operation) -> bool {
|
||||
return !matches!(
|
||||
operation,
|
||||
kb_lib::ExSplToken2022Operation::Instruction { value }
|
||||
if matches!(value.as_ref(), kb_lib::ExSplTokenSingleOperation::EmitTokenMetadata { .. })
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_profile_wallet_signers(
|
||||
@@ -922,3 +973,73 @@ fn validate_profile_wallet_signers(
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn pubkey(byte: u8) -> kb_lib::MdPubkey {
|
||||
return kb_lib::MdPubkey(bs58::encode([byte; 32]).into_string());
|
||||
}
|
||||
|
||||
fn request(
|
||||
operation: kb_lib::ExSplTokenSingleOperation,
|
||||
) -> crate::DevnetSplToken2022ExecutionRequest {
|
||||
return crate::DevnetSplToken2022ExecutionRequest::new(
|
||||
"metadata-preflight-test",
|
||||
kb_lib::ExSplToken2022Operation::Instruction { value: std::boxed::Box::new(operation) },
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_preflight_requires_pointer_before_initialize_and_metadata_afterward() {
|
||||
let mint = pubkey(1);
|
||||
let initialize = request(kb_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
|
||||
metadata: mint.clone(),
|
||||
update_authority: pubkey(2),
|
||||
mint: mint.clone(),
|
||||
mint_authority: pubkey(2),
|
||||
name: "Token".to_string(),
|
||||
symbol: "TOK".to_string(),
|
||||
uri: "https://example.invalid/token.json".to_string(),
|
||||
});
|
||||
let initialize_preflight =
|
||||
super::preflight_request(&initialize).unwrap_or_else(|error| panic!("{error}"));
|
||||
assert_eq!(
|
||||
initialize_preflight.requirements[0].required_extensions,
|
||||
vec!["metadata_pointer".to_string()]
|
||||
);
|
||||
let update = request(kb_lib::ExSplTokenSingleOperation::UpdateTokenMetadataField {
|
||||
metadata: mint,
|
||||
update_authority: pubkey(2),
|
||||
field: kb_lib::ExSplTokenMetadataField::Key("campaign".to_string()),
|
||||
value: "0.4.8-pre.012".to_string(),
|
||||
});
|
||||
let update_preflight =
|
||||
super::preflight_request(&update).unwrap_or_else(|error| panic!("{error}"));
|
||||
assert_eq!(
|
||||
update_preflight.requirements[0].required_extensions,
|
||||
vec!["token_metadata".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_initialize_rejects_external_metadata_account_and_emit_skips_materialization() {
|
||||
let invalid = request(kb_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
|
||||
metadata: pubkey(1),
|
||||
update_authority: pubkey(2),
|
||||
mint: pubkey(3),
|
||||
mint_authority: pubkey(2),
|
||||
name: "Token".to_string(),
|
||||
symbol: "TOK".to_string(),
|
||||
uri: "https://example.invalid/token.json".to_string(),
|
||||
});
|
||||
assert!(super::preflight_request(&invalid).is_err());
|
||||
let emit = kb_lib::ExSplToken2022Operation::Instruction {
|
||||
value: std::boxed::Box::new(kb_lib::ExSplTokenSingleOperation::EmitTokenMetadata {
|
||||
metadata: pubkey(1),
|
||||
start: std::option::Option::None,
|
||||
end: std::option::Option::None,
|
||||
}),
|
||||
};
|
||||
assert!(!super::operation_requires_materialization(&emit));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,586 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_metadata_campaign.rs
|
||||
// version: 3
|
||||
|
||||
//! Ordered confirmed Devnet campaign for the five Token-2022 Token Metadata instructions.
|
||||
|
||||
/// One confirmed Token Metadata campaign step and its authoritative postcondition.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetToken2022MetadataCampaignStepSummary {
|
||||
/// Stable step identifier.
|
||||
pub step_id: std::string::String,
|
||||
/// Canonical operation code built by `kb-lib`.
|
||||
pub operation_code: std::string::String,
|
||||
/// Complete simulation, submission, replay and instruction-materialization evidence.
|
||||
pub execution: crate::DevnetSplToken2022ExecutionSummary,
|
||||
/// Authoritative mint snapshot read at or after the confirmed transaction slot.
|
||||
pub stateful_snapshot: kb_pipeline::Token2022StatefulReadResult,
|
||||
/// Validated return-data evidence for the `Emit` step only.
|
||||
pub emit_evidence: std::option::Option<kb_pipeline::Token2022MetadataEmitEvidence>,
|
||||
/// Explicit stateful or return-data postcondition.
|
||||
pub postcondition: kb_pipeline::Token2022ExecutionPostcondition,
|
||||
}
|
||||
|
||||
/// Complete evidence retained by the five-step Token Metadata campaign.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetToken2022MetadataCampaignSummary {
|
||||
/// Fresh Token-2022 mint prepared for this campaign.
|
||||
pub fixture: crate::Token2022MetadataFixturePreparationSummary,
|
||||
/// Five confirmed steps in interface order.
|
||||
pub steps: std::vec::Vec<crate::DevnetToken2022MetadataCampaignStepSummary>,
|
||||
}
|
||||
|
||||
/// Returns the exact ordered operation names exercised by the Devnet campaign.
|
||||
pub fn token_2022_metadata_campaign_operation_names() -> &'static [&'static str; 5] {
|
||||
return &[
|
||||
"initialize_token_metadata",
|
||||
"update_token_metadata_field",
|
||||
"emit_token_metadata",
|
||||
"remove_token_metadata_key",
|
||||
"update_token_metadata_authority",
|
||||
];
|
||||
}
|
||||
|
||||
/// Prepares a fresh mint and executes all five Token Metadata interface instructions on Devnet.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute_devnet_token_2022_metadata_campaign<S, O>(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
options: &crate::Token2022MetadataFixturePreparationOptions,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DevnetToken2022MetadataCampaignSummary>
|
||||
where
|
||||
S: kb_store::RawTransactionStore
|
||||
+ kb_store::CoreExtractionStore
|
||||
+ kb_store::DecodePipelineStore
|
||||
+ Sync,
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
if !options.operator_confirmed {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 Token Metadata Devnet campaign requires explicit operator confirmation",
|
||||
));
|
||||
}
|
||||
let fixture = match crate::prepare_token_2022_metadata_fixture(
|
||||
http_pool,
|
||||
profile,
|
||||
workspace_root,
|
||||
options,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let operations = campaign_operations(&fixture);
|
||||
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
|
||||
std::vec![std::sync::Arc::new(kb_lib::DcSplToken2022Decoder)];
|
||||
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> =
|
||||
std::vec![std::sync::Arc::new(kb_lib::MtMetadataToken2022Materializer)];
|
||||
let mut steps = std::vec::Vec::with_capacity(operations.len());
|
||||
for (index, (step_id, operation)) in operations.into_iter().enumerate() {
|
||||
let operation_code = operation.operation_code().to_string();
|
||||
let mut request = crate::DevnetSplToken2022ExecutionRequest::new(
|
||||
format!("token-2022-metadata-devnet-{step_id}-{}", uuid::Uuid::new_v4()),
|
||||
operation.clone(),
|
||||
);
|
||||
request.query_role = options.query_role.clone();
|
||||
request.transaction_role = options.transaction_role.clone();
|
||||
request.submit = true;
|
||||
request.operator_confirmed = true;
|
||||
request.post_validation_max_retries = 20;
|
||||
let execution = match crate::execute_devnet_spl_token_2022(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&request,
|
||||
decoders.as_slice(),
|
||||
materializers.as_slice(),
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let confirmation = match execution.confirmation.as_ref() {
|
||||
std::option::Option::Some(value)
|
||||
if matches!(
|
||||
value.status,
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||
) =>
|
||||
{
|
||||
value
|
||||
},
|
||||
std::option::Option::Some(value) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_campaign_step_not_confirmed",
|
||||
format!("campaign step {step_id} stopped at {:?}", value.status),
|
||||
));
|
||||
},
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_campaign_confirmation_missing",
|
||||
format!("campaign step {step_id} has no confirmation evidence"),
|
||||
));
|
||||
},
|
||||
};
|
||||
let diagnostic = match execution.post_execution.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_campaign_post_execution_missing",
|
||||
format!("campaign step {step_id} has no post-execution diagnostic"),
|
||||
));
|
||||
},
|
||||
};
|
||||
if !execution.simulation.success
|
||||
|| !diagnostic.canonical_inserted
|
||||
|| !diagnostic.core_extracted
|
||||
|| !diagnostic.decode_replayed
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_campaign_replay_incomplete",
|
||||
format!(
|
||||
"campaign step {step_id} did not complete simulation and canonical decode replay"
|
||||
),
|
||||
));
|
||||
}
|
||||
let idempotence_replay = match execution.idempotence_replay.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_campaign_idempotence_missing",
|
||||
format!("campaign step {step_id} has no idempotence replay evidence"),
|
||||
));
|
||||
},
|
||||
};
|
||||
if idempotence_replay.failed_inputs != 0
|
||||
|| idempotence_replay.processing_error_inputs != 0
|
||||
|| idempotence_replay.processors.iter().any(|processor| {
|
||||
return processor.failed != 0
|
||||
|| processor.processing_errors != 0
|
||||
|| processor.materialized_outputs != 0
|
||||
|| processor.materialization_refused != 0;
|
||||
})
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_campaign_idempotence_failed",
|
||||
format!("campaign step {step_id} did not produce a clean second replay"),
|
||||
));
|
||||
}
|
||||
if !execution.plan.policy.post_execution_validation.materialization_required
|
||||
&& operation_code != kb_lib::EX_SPL_TOKEN_2022_EMIT_TOKEN_METADATA_OPERATION
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_campaign_materialization_policy_invalid",
|
||||
format!("campaign step {step_id} unexpectedly disabled materialization"),
|
||||
));
|
||||
}
|
||||
if execution.plan.policy.post_execution_validation.materialization_required
|
||||
&& execution.materializations.is_empty()
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_campaign_materialization_missing",
|
||||
format!("campaign step {step_id} produced no metadata materialization"),
|
||||
));
|
||||
}
|
||||
let stateful_snapshot = match kb_pipeline::read_token_2022_stateful_snapshot(
|
||||
http_pool,
|
||||
&kb_pipeline::Token2022StatefulReadRequest {
|
||||
query_role: options.query_role.clone(),
|
||||
account: fixture.mint.clone(),
|
||||
kind: kb_lib::DcToken2022StateKind::Mint,
|
||||
min_context_slot: confirmation.slot,
|
||||
max_data_bytes: kb_pipeline::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES,
|
||||
context: kb_pipeline::Token2022StatefulContext::default(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let emit_evidence = match &operation {
|
||||
kb_lib::ExSplToken2022Operation::Instruction { value } => match value.as_ref() {
|
||||
kb_lib::ExSplTokenSingleOperation::EmitTokenMetadata { start, end, .. } => {
|
||||
match kb_pipeline::inspect_token_2022_metadata_emit_simulation(
|
||||
&execution.simulation,
|
||||
*start,
|
||||
*end,
|
||||
) {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
_ => std::option::Option::None,
|
||||
},
|
||||
kb_lib::ExSplToken2022Operation::Batch { instructions: _ } => std::option::Option::None,
|
||||
};
|
||||
let postcondition = campaign_postcondition(
|
||||
index,
|
||||
&fixture,
|
||||
&stateful_snapshot.snapshot,
|
||||
emit_evidence.as_ref(),
|
||||
);
|
||||
if postcondition.status != kb_pipeline::Token2022ExecutionPostconditionStatus::Confirmed {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_campaign_postcondition_failed",
|
||||
format!("campaign step {step_id}: {}", postcondition.diagnostic),
|
||||
));
|
||||
}
|
||||
steps.push(crate::DevnetToken2022MetadataCampaignStepSummary {
|
||||
step_id: step_id.to_string(),
|
||||
operation_code,
|
||||
execution,
|
||||
stateful_snapshot,
|
||||
emit_evidence,
|
||||
postcondition,
|
||||
});
|
||||
}
|
||||
return std::result::Result::Ok(crate::DevnetToken2022MetadataCampaignSummary {
|
||||
fixture,
|
||||
steps,
|
||||
});
|
||||
}
|
||||
|
||||
fn campaign_operations(
|
||||
fixture: &crate::Token2022MetadataFixturePreparationSummary,
|
||||
) -> std::vec::Vec<(&'static str, kb_lib::ExSplToken2022Operation)> {
|
||||
let metadata = fixture.mint.clone();
|
||||
let authority = fixture.initial_authority.clone();
|
||||
return std::vec![
|
||||
(
|
||||
"token_2022_metadata_initialize",
|
||||
instruction(kb_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
|
||||
metadata: metadata.clone(),
|
||||
update_authority: authority.clone(),
|
||||
mint: metadata.clone(),
|
||||
mint_authority: authority.clone(),
|
||||
name: crate::TOKEN_2022_METADATA_CAMPAIGN_NAME.to_string(),
|
||||
symbol: crate::TOKEN_2022_METADATA_CAMPAIGN_SYMBOL.to_string(),
|
||||
uri: crate::TOKEN_2022_METADATA_CAMPAIGN_URI.to_string(),
|
||||
}),
|
||||
),
|
||||
(
|
||||
"token_2022_metadata_update_field",
|
||||
instruction(kb_lib::ExSplTokenSingleOperation::UpdateTokenMetadataField {
|
||||
metadata: metadata.clone(),
|
||||
update_authority: authority.clone(),
|
||||
field: kb_lib::ExSplTokenMetadataField::Key(
|
||||
crate::TOKEN_2022_METADATA_CAMPAIGN_KEY.to_string(),
|
||||
),
|
||||
value: crate::TOKEN_2022_METADATA_CAMPAIGN_VALUE.to_string(),
|
||||
}),
|
||||
),
|
||||
(
|
||||
"token_2022_metadata_emit",
|
||||
instruction(kb_lib::ExSplTokenSingleOperation::EmitTokenMetadata {
|
||||
metadata: metadata.clone(),
|
||||
start: std::option::Option::None,
|
||||
end: std::option::Option::None,
|
||||
}),
|
||||
),
|
||||
(
|
||||
"token_2022_metadata_remove_key",
|
||||
instruction(kb_lib::ExSplTokenSingleOperation::RemoveTokenMetadataKey {
|
||||
metadata: metadata.clone(),
|
||||
update_authority: authority.clone(),
|
||||
key: crate::TOKEN_2022_METADATA_CAMPAIGN_KEY.to_string(),
|
||||
idempotent: false,
|
||||
}),
|
||||
),
|
||||
(
|
||||
"token_2022_metadata_update_authority",
|
||||
instruction(kb_lib::ExSplTokenSingleOperation::UpdateTokenMetadataAuthority {
|
||||
metadata,
|
||||
current_authority: authority,
|
||||
new_authority: std::option::Option::Some(fixture.final_authority.clone()),
|
||||
}),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
fn instruction(value: kb_lib::ExSplTokenSingleOperation) -> kb_lib::ExSplToken2022Operation {
|
||||
return kb_lib::ExSplToken2022Operation::Instruction { value: std::boxed::Box::new(value) };
|
||||
}
|
||||
|
||||
fn campaign_postcondition(
|
||||
step_index: usize,
|
||||
fixture: &crate::Token2022MetadataFixturePreparationSummary,
|
||||
snapshot: &kb_pipeline::Token2022StatefulSnapshotBundle,
|
||||
emit_evidence: std::option::Option<&kb_pipeline::Token2022MetadataEmitEvidence>,
|
||||
) -> kb_pipeline::Token2022ExecutionPostcondition {
|
||||
if step_index == 4 {
|
||||
return kb_pipeline::inspect_token_2022_metadata_authority_postcondition(
|
||||
&fixture.mint,
|
||||
std::option::Option::Some(&fixture.final_authority),
|
||||
snapshot,
|
||||
);
|
||||
}
|
||||
let value_fields = metadata_value_fields(snapshot);
|
||||
let confirmed = match step_index {
|
||||
0 => value_fields.is_some_and(|value| {
|
||||
return required_fields_match(value, fixture.initial_authority.0.as_str())
|
||||
&& !contains_campaign_pair(value);
|
||||
}),
|
||||
1 => value_fields.is_some_and(|value| {
|
||||
return required_fields_match(value, fixture.initial_authority.0.as_str())
|
||||
&& contains_campaign_pair(value);
|
||||
}),
|
||||
2 => {
|
||||
let snapshot_matches = value_fields.is_some_and(|value| {
|
||||
return required_fields_match(value, fixture.initial_authority.0.as_str())
|
||||
&& contains_campaign_pair(value);
|
||||
});
|
||||
let emit_matches = emit_evidence
|
||||
.and_then(|evidence| return evidence.decoded_metadata.as_ref())
|
||||
.is_some_and(|value| {
|
||||
return required_fields_match(value, fixture.initial_authority.0.as_str())
|
||||
&& contains_campaign_pair(value);
|
||||
});
|
||||
snapshot_matches && emit_matches
|
||||
},
|
||||
3 => value_fields.is_some_and(|value| {
|
||||
return required_fields_match(value, fixture.initial_authority.0.as_str())
|
||||
&& !contains_campaign_pair(value);
|
||||
}),
|
||||
_ => false,
|
||||
};
|
||||
return kb_pipeline::Token2022ExecutionPostcondition {
|
||||
role: "metadata".to_string(),
|
||||
account: fixture.mint.clone(),
|
||||
status: if confirmed {
|
||||
kb_pipeline::Token2022ExecutionPostconditionStatus::Confirmed
|
||||
} else {
|
||||
kb_pipeline::Token2022ExecutionPostconditionStatus::Contradicted
|
||||
},
|
||||
diagnostic: if confirmed {
|
||||
format!("Token-2022 metadata campaign postcondition {step_index} is confirmed")
|
||||
} else {
|
||||
format!("Token-2022 metadata campaign postcondition {step_index} is contradicted")
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn metadata_value_fields(
|
||||
snapshot: &kb_pipeline::Token2022StatefulSnapshotBundle,
|
||||
) -> std::option::Option<&serde_json::Value> {
|
||||
return snapshot.outputs.iter().find_map(|output| {
|
||||
if output.family != kb_lib::MdMaterializedEventFamily::Metadata
|
||||
|| output.payload_json.get("projectionKind").and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some("token_metadata")
|
||||
{
|
||||
return std::option::Option::None;
|
||||
}
|
||||
return output.payload_json.get("valueFields");
|
||||
});
|
||||
}
|
||||
|
||||
fn required_fields_match(value: &serde_json::Value, authority: &str) -> bool {
|
||||
return value.get("updateAuthority").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(authority)
|
||||
&& value.get("name").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_NAME)
|
||||
&& value.get("symbol").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_SYMBOL)
|
||||
&& value.get("uri").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_URI);
|
||||
}
|
||||
|
||||
fn contains_campaign_pair(value: &serde_json::Value) -> bool {
|
||||
return value
|
||||
.get("additionalMetadata")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.is_some_and(|pairs| {
|
||||
return pairs.iter().any(|pair| {
|
||||
return pair.get("key").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_KEY)
|
||||
&& pair.get("value").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_VALUE);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn pubkey(byte: u8) -> kb_lib::MdPubkey {
|
||||
return kb_lib::MdPubkey(bs58::encode([byte; 32]).into_string());
|
||||
}
|
||||
|
||||
fn fixture() -> crate::Token2022MetadataFixturePreparationSummary {
|
||||
return crate::Token2022MetadataFixturePreparationSummary {
|
||||
mint_keypair_path: std::path::PathBuf::from("mint.json"),
|
||||
final_authority_keypair_path: std::path::PathBuf::from("authority.json"),
|
||||
mint: pubkey(1),
|
||||
initial_authority: pubkey(2),
|
||||
final_authority: pubkey(3),
|
||||
mint_space: 234,
|
||||
rent_budget_space: 512,
|
||||
rent_reserve_lamports: 1,
|
||||
preparation_signature: "signature".to_string(),
|
||||
initial_snapshot: kb_pipeline::Token2022StatefulReadResult {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: 1,
|
||||
snapshot: kb_pipeline::Token2022StatefulSnapshotBundle {
|
||||
account_key: pubkey(1).0,
|
||||
slot: 1,
|
||||
state_kind: "mint".to_string(),
|
||||
extension_names: vec!["metadata_pointer".to_string()],
|
||||
outputs: std::vec::Vec::new(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn campaign_contract_covers_five_interface_operations_once_in_order() {
|
||||
let operations = super::campaign_operations(&fixture());
|
||||
assert_eq!(
|
||||
operations
|
||||
.iter()
|
||||
.map(|(_, operation)| return operation.operation_code())
|
||||
.collect::<std::vec::Vec<_>>(),
|
||||
vec![
|
||||
kb_lib::EX_SPL_TOKEN_2022_INITIALIZE_TOKEN_METADATA_OPERATION,
|
||||
kb_lib::EX_SPL_TOKEN_2022_UPDATE_TOKEN_METADATA_FIELD_OPERATION,
|
||||
kb_lib::EX_SPL_TOKEN_2022_EMIT_TOKEN_METADATA_OPERATION,
|
||||
kb_lib::EX_SPL_TOKEN_2022_REMOVE_TOKEN_METADATA_KEY_OPERATION,
|
||||
kb_lib::EX_SPL_TOKEN_2022_UPDATE_TOKEN_METADATA_AUTHORITY_OPERATION,
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
crate::token_2022_metadata_campaign_operation_names(),
|
||||
&[
|
||||
"initialize_token_metadata",
|
||||
"update_token_metadata_field",
|
||||
"emit_token_metadata",
|
||||
"remove_token_metadata_key",
|
||||
"update_token_metadata_authority",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn campaign_pair_detection_is_exact_and_removal_is_observable() {
|
||||
let with_pair = serde_json::json!({
|
||||
"additionalMetadata": [{
|
||||
"key": crate::TOKEN_2022_METADATA_CAMPAIGN_KEY,
|
||||
"value": crate::TOKEN_2022_METADATA_CAMPAIGN_VALUE
|
||||
}]
|
||||
});
|
||||
let without_pair = serde_json::json!({"additionalMetadata": []});
|
||||
assert!(super::contains_campaign_pair(&with_pair));
|
||||
assert!(!super::contains_campaign_pair(&without_pair));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn optional_devnet_token_2022_metadata_campaign_from_env() {
|
||||
if std::env::var("KB_DEVNET_TOKEN_2022_METADATA_CAMPAIGN_TEST").ok().as_deref()
|
||||
!= std::option::Option::Some("1")
|
||||
{
|
||||
return;
|
||||
}
|
||||
if std::env::var("KB_DEVNET_TOKEN_2022_METADATA_OPERATOR_CONFIRMED")
|
||||
.ok()
|
||||
.as_deref()
|
||||
!= std::option::Option::Some("1")
|
||||
{
|
||||
panic!(
|
||||
"KB_DEVNET_TOKEN_2022_METADATA_OPERATOR_CONFIRMED=1 is required for the spending campaign"
|
||||
);
|
||||
}
|
||||
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("workspace root cannot be resolved"),
|
||||
};
|
||||
let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") {
|
||||
std::result::Result::Ok(value) => {
|
||||
let path = std::path::PathBuf::from(value);
|
||||
if path.is_absolute() { path } else { workspace_root.join(path) }
|
||||
},
|
||||
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
|
||||
};
|
||||
let config = kb_config::read_config_json_file_with_environment(
|
||||
config_path.as_path(),
|
||||
workspace_root,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}"));
|
||||
let profile_name = std::env::var("KB_DEVNET_PROFILE").ok();
|
||||
let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref())
|
||||
.unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}"));
|
||||
let database_url = std::env::var("KB_POSTGRES_TEST_URL")
|
||||
.unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}"));
|
||||
profile.database.backend = "postgres".to_string();
|
||||
profile.database.postgres.url = database_url;
|
||||
let pool = kb_onchain_transport::HttpEndpointPool::from_profile(&profile)
|
||||
.unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}"));
|
||||
let store_options = kb_store::PostgresStoreOptions::new(
|
||||
profile.database.postgres.url.clone(),
|
||||
profile.database.postgres.max_connections,
|
||||
profile.database.postgres.connect_timeout_ms,
|
||||
false,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}"));
|
||||
let store = kb_store::PostgresStore::connect(store_options)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}"));
|
||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
||||
panic!("PostgreSQL schema initialization failed: {error}");
|
||||
}
|
||||
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
|
||||
let wallet_dir = if configured_wallet_dir.is_absolute() {
|
||||
configured_wallet_dir
|
||||
} else {
|
||||
workspace_root.join(configured_wallet_dir)
|
||||
};
|
||||
let mut options = crate::Token2022MetadataFixturePreparationOptions::new(wallet_dir);
|
||||
options.operator_confirmed = true;
|
||||
let summary = crate::execute_devnet_token_2022_metadata_campaign(
|
||||
&pool,
|
||||
&store,
|
||||
&profile,
|
||||
workspace_root,
|
||||
&options,
|
||||
&crate::NoopSolanaExecutionObserver,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|error| {
|
||||
panic!("Token-2022 Token Metadata Devnet campaign failed: {error}")
|
||||
});
|
||||
assert_eq!(summary.steps.len(), 5);
|
||||
assert!(summary.steps.iter().all(|step| {
|
||||
return step.postcondition.status
|
||||
== kb_pipeline::Token2022ExecutionPostconditionStatus::Confirmed;
|
||||
}));
|
||||
println!(
|
||||
"TOKEN_2022_METADATA_FIXTURE mint={} preparation_signature={} initial_authority={} final_authority={}",
|
||||
summary.fixture.mint.0,
|
||||
summary.fixture.preparation_signature,
|
||||
summary.fixture.initial_authority.0,
|
||||
summary.fixture.final_authority.0,
|
||||
);
|
||||
for step in &summary.steps {
|
||||
let signature = step
|
||||
.execution
|
||||
.confirmation
|
||||
.as_ref()
|
||||
.map(|value| return value.signature.0.as_str())
|
||||
.unwrap_or("missing");
|
||||
let slot = step.execution.confirmation.as_ref().and_then(|value| return value.slot);
|
||||
println!(
|
||||
"TOKEN_2022_METADATA_STEP id={} operation={} signature={} slot={:?} postcondition={:?} materializations={} emit_bytes={}",
|
||||
step.step_id,
|
||||
step.operation_code,
|
||||
signature,
|
||||
slot,
|
||||
step.postcondition.status,
|
||||
step.execution.materializations.len(),
|
||||
step.emit_evidence.as_ref().map(|value| return value.data.len()).unwrap_or(0),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_metadata_validation.rs
|
||||
// version: 2
|
||||
|
||||
//! Conservative validation matrix for the `0.4.8-pre.012` Token Metadata campaign.
|
||||
|
||||
/// Maximum evidence items retained by one campaign scenario.
|
||||
pub const MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE: usize = 16;
|
||||
|
||||
/// Exact network-validation status for one Token Metadata scenario.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Token2022MetadataValidationStatus {
|
||||
/// No real Devnet execution has been recorded.
|
||||
NotRun,
|
||||
/// The scenario completed with all declared evidence.
|
||||
Confirmed,
|
||||
/// The scenario is unavailable for an explicit recorded reason.
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// One bounded evidence item retained by the validation matrix.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Token2022MetadataValidationEvidence {
|
||||
/// Stable evidence kind.
|
||||
pub kind: std::string::String,
|
||||
/// Bounded evidence value such as a signature, slot or diagnostic.
|
||||
pub value: std::string::String,
|
||||
}
|
||||
|
||||
/// One ordered Token Metadata scenario in the canonical Devnet matrix.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Token2022MetadataValidationScenario {
|
||||
/// Stable campaign step identifier.
|
||||
pub id: std::string::String,
|
||||
/// Short Token Metadata interface operation name.
|
||||
pub operation: std::string::String,
|
||||
/// Current evidence-qualified network status.
|
||||
pub status: crate::Token2022MetadataValidationStatus,
|
||||
/// Evidence kinds required before promotion to `confirmed`.
|
||||
pub required_evidence: std::vec::Vec<std::string::String>,
|
||||
/// Bounded observed evidence.
|
||||
pub evidence: std::vec::Vec<crate::Token2022MetadataValidationEvidence>,
|
||||
}
|
||||
|
||||
/// Canonical `0.4.8-pre.012` Token Metadata Devnet validation matrix.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Token2022MetadataValidationMatrix {
|
||||
/// Matrix schema version.
|
||||
pub matrix_version: u32,
|
||||
/// Milestone owning the campaign.
|
||||
pub milestone: std::string::String,
|
||||
/// Token-2022 program ID.
|
||||
pub program_id: std::string::String,
|
||||
/// Five scenarios in campaign order.
|
||||
pub scenarios: std::vec::Vec<crate::Token2022MetadataValidationScenario>,
|
||||
}
|
||||
|
||||
/// Loads and validates the canonical Token Metadata Devnet matrix.
|
||||
pub fn load_token_2022_metadata_validation_matrix(
|
||||
) -> kb_core::Result<crate::Token2022MetadataValidationMatrix> {
|
||||
let matrix = match serde_json::from_str::<crate::Token2022MetadataValidationMatrix>(
|
||||
include_str!(
|
||||
"../../test-fixtures/contract-matrices/SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json"
|
||||
),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_validation_matrix_invalid_json",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
match crate::validate_token_2022_metadata_validation_matrix(&matrix) {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(matrix),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates campaign coverage and prevents promotion without complete evidence.
|
||||
pub fn validate_token_2022_metadata_validation_matrix(
|
||||
matrix: &crate::Token2022MetadataValidationMatrix,
|
||||
) -> kb_core::Result<()> {
|
||||
if matrix.matrix_version != 1
|
||||
|| matrix.milestone != "0.4.8-pre.012"
|
||||
|| matrix.program_id != kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 metadata validation matrix identity is invalid",
|
||||
));
|
||||
}
|
||||
let expected = crate::token_2022_metadata_campaign_operation_names();
|
||||
if matrix.scenarios.len() != expected.len() {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 metadata validation matrix requires exactly {} scenarios",
|
||||
expected.len()
|
||||
)));
|
||||
}
|
||||
let mut ids = std::collections::BTreeSet::new();
|
||||
for (index, scenario) in matrix.scenarios.iter().enumerate() {
|
||||
if !ids.insert(scenario.id.as_str()) {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"duplicate Token-2022 metadata validation scenario {}",
|
||||
scenario.id
|
||||
)));
|
||||
}
|
||||
if scenario.operation != expected[index] {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 metadata scenario {} expected operation {}, got {}",
|
||||
scenario.id, expected[index], scenario.operation
|
||||
)));
|
||||
}
|
||||
if scenario.required_evidence.is_empty()
|
||||
|| scenario.required_evidence.len() > MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE
|
||||
|| scenario.evidence.len() > MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 metadata scenario {} has invalid evidence bounds",
|
||||
scenario.id
|
||||
)));
|
||||
}
|
||||
let mut required = std::collections::BTreeSet::new();
|
||||
if scenario
|
||||
.required_evidence
|
||||
.iter()
|
||||
.any(|kind| return kind.trim().is_empty() || !required.insert(kind.as_str()))
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 metadata scenario {} has empty or duplicate required evidence",
|
||||
scenario.id
|
||||
)));
|
||||
}
|
||||
let mut observed = std::collections::BTreeSet::new();
|
||||
for evidence in &scenario.evidence {
|
||||
if evidence.kind.trim().is_empty()
|
||||
|| evidence.value.trim().is_empty()
|
||||
|| !observed.insert(evidence.kind.as_str())
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 metadata scenario {} has invalid observed evidence",
|
||||
scenario.id
|
||||
)));
|
||||
}
|
||||
}
|
||||
match scenario.status {
|
||||
crate::Token2022MetadataValidationStatus::NotRun => {
|
||||
if !scenario.evidence.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"not-run Token-2022 metadata scenario {} cannot claim evidence",
|
||||
scenario.id
|
||||
)));
|
||||
}
|
||||
},
|
||||
crate::Token2022MetadataValidationStatus::Confirmed => {
|
||||
if scenario
|
||||
.required_evidence
|
||||
.iter()
|
||||
.any(|kind| return !observed.contains(kind.as_str()))
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"confirmed Token-2022 metadata scenario {} is missing required evidence",
|
||||
scenario.id
|
||||
)));
|
||||
}
|
||||
},
|
||||
crate::Token2022MetadataValidationStatus::Unavailable => {
|
||||
if !observed.contains("unavailable_reason") {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"unavailable Token-2022 metadata scenario {} requires unavailable_reason",
|
||||
scenario.id
|
||||
)));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn canonical_matrix_is_closed_ordered_and_confirmed_after_network_execution() {
|
||||
let matrix = crate::load_token_2022_metadata_validation_matrix()
|
||||
.unwrap_or_else(|error| panic!("Token-2022 metadata matrix failed: {error}"));
|
||||
assert_eq!(matrix.scenarios.len(), 5);
|
||||
assert!(matrix.scenarios.iter().all(|scenario| {
|
||||
let observed = scenario
|
||||
.evidence
|
||||
.iter()
|
||||
.map(|evidence| return evidence.kind.as_str())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
return scenario.status == crate::Token2022MetadataValidationStatus::Confirmed
|
||||
&& scenario
|
||||
.required_evidence
|
||||
.iter()
|
||||
.all(|kind| return observed.contains(kind.as_str()));
|
||||
}));
|
||||
assert_eq!(
|
||||
matrix
|
||||
.scenarios
|
||||
.iter()
|
||||
.map(|scenario| return scenario.operation.as_str())
|
||||
.collect::<std::vec::Vec<_>>(),
|
||||
crate::token_2022_metadata_campaign_operation_names().as_slice()
|
||||
);
|
||||
}
|
||||
}
|
||||
661
kb-pipeline-demo-scenarios/src/token_2022_metadata_fixture.rs
Normal file
661
kb-pipeline-demo-scenarios/src/token_2022_metadata_fixture.rs
Normal file
@@ -0,0 +1,661 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/token_2022_metadata_fixture.rs
|
||||
// version: 2
|
||||
|
||||
//! Native Token-2022 mint preparation for the embedded Token Metadata Devnet campaign.
|
||||
|
||||
/// Stable metadata name used by the `0.4.8-pre.012` campaign.
|
||||
pub const TOKEN_2022_METADATA_CAMPAIGN_NAME: &str = "Khadhroony Token-2022 Metadata";
|
||||
/// Stable metadata symbol used by the `0.4.8-pre.012` campaign.
|
||||
pub const TOKEN_2022_METADATA_CAMPAIGN_SYMBOL: &str = "KBTM";
|
||||
/// Opaque URI stored on-chain by the `0.4.8-pre.012` campaign.
|
||||
pub const TOKEN_2022_METADATA_CAMPAIGN_URI: &str =
|
||||
"https://example.invalid/khadhroony/token-2022-metadata.json";
|
||||
/// Additional metadata key created and removed by the campaign.
|
||||
pub const TOKEN_2022_METADATA_CAMPAIGN_KEY: &str = "campaign";
|
||||
/// Additional metadata value created and removed by the campaign.
|
||||
pub const TOKEN_2022_METADATA_CAMPAIGN_VALUE: &str = "0.4.8-pre.012";
|
||||
|
||||
const TOKEN_2022_TLV_HEADER_BYTES: usize = 4;
|
||||
|
||||
/// Options used to prepare one fresh Token-2022 mint for the metadata campaign.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Token2022MetadataFixturePreparationOptions {
|
||||
/// 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 persistent fixture keypairs.
|
||||
pub wallet_dir: std::path::PathBuf,
|
||||
/// Explicit operator authorization for the lamport-spending fixture transaction.
|
||||
pub operator_confirmed: bool,
|
||||
}
|
||||
|
||||
impl crate::Token2022MetadataFixturePreparationOptions {
|
||||
/// Creates conservative defaults matching 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,
|
||||
operator_confirmed: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Fresh Token-2022 mint and authority identities used by the metadata campaign.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
|
||||
pub struct Token2022MetadataFixturePreparationSummary {
|
||||
/// Persistent fixture mint keypair path.
|
||||
pub mint_keypair_path: std::path::PathBuf,
|
||||
/// Persistent final metadata-authority keypair path.
|
||||
pub final_authority_keypair_path: std::path::PathBuf,
|
||||
/// Fresh Token-2022 mint address. Embedded metadata uses this same account.
|
||||
pub mint: kb_lib::MdPubkey,
|
||||
/// Profile wallet used as mint and initial metadata update authority.
|
||||
pub initial_authority: kb_lib::MdPubkey,
|
||||
/// Fresh public key installed by the final `UpdateAuthority` campaign step.
|
||||
pub final_authority: kb_lib::MdPubkey,
|
||||
/// Initial mint account bytes containing only the fixed Metadata Pointer extension.
|
||||
pub mint_space: usize,
|
||||
/// Account-size budget used to pre-fund rent for the largest campaign metadata state.
|
||||
pub rent_budget_space: usize,
|
||||
/// Rent reserve deposited when the mint is created.
|
||||
pub rent_reserve_lamports: u64,
|
||||
/// Confirmed signature that created the Token-2022 mint fixture.
|
||||
pub preparation_signature: std::string::String,
|
||||
/// Authoritative snapshot after mint creation and before Token Metadata initialization.
|
||||
pub initial_snapshot: kb_pipeline::Token2022StatefulReadResult,
|
||||
}
|
||||
|
||||
/// Creates a fresh Token-2022 mint with Metadata Pointer initialized to the mint itself.
|
||||
///
|
||||
/// The mint account is created with the fixed Metadata Pointer size but is pre-funded
|
||||
/// for the largest state reached by this campaign so Token Metadata reallocations
|
||||
/// remain rent-exempt without introducing an additional payer instruction.
|
||||
pub async fn prepare_token_2022_metadata_fixture(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
options: &crate::Token2022MetadataFixturePreparationOptions,
|
||||
) -> kb_core::Result<crate::Token2022MetadataFixturePreparationSummary> {
|
||||
if options.query_role.trim().is_empty() || options.transaction_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 metadata fixture endpoint roles must not be empty",
|
||||
));
|
||||
}
|
||||
if profile.wallet.cluster != "devnet"
|
||||
|| !profile.wallet.temporary_wallet_enabled
|
||||
|| !profile.wallet.temporary_wallet_persist
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 metadata fixture requires a persistent Devnet wallet profile",
|
||||
));
|
||||
}
|
||||
if !profile.wallet.devnet_send_enabled || !options.operator_confirmed {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 metadata fixture requires enabled Devnet submission and explicit operator confirmation",
|
||||
));
|
||||
}
|
||||
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_store = match kb_wallet::TemporaryWalletStore::new(
|
||||
options.wallet_dir.join("token_2022_metadata_validation"),
|
||||
) {
|
||||
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 Token-2022 metadata fixture identifier: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let mint_alias = match kb_wallet::WalletAlias::parse(
|
||||
format!("token-2022-metadata-mint-{fixture_epoch}").as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let final_authority_alias = match kb_wallet::WalletAlias::parse(
|
||||
format!("token-2022-metadata-authority-{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(mint_alias.clone()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let final_authority_wallet =
|
||||
match fixture_store.load_or_create(final_authority_alias.clone()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mint = kb_lib::MdPubkey(mint_wallet.public_key());
|
||||
let initial_authority = kb_lib::MdPubkey(operator.public_key());
|
||||
let final_authority = kb_lib::MdPubkey(final_authority_wallet.public_key());
|
||||
let mint_space = match token_2022_metadata_pointer_mint_space() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rent_budget_space = metadata_campaign_rent_budget_space(mint_space);
|
||||
let preparation_signature = match create_native_token_2022_metadata_mint(
|
||||
http_pool,
|
||||
profile,
|
||||
options,
|
||||
&operator,
|
||||
&mint_wallet,
|
||||
mint_space,
|
||||
rent_budget_space,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let initial_snapshot = match kb_pipeline::read_token_2022_stateful_snapshot(
|
||||
http_pool,
|
||||
&kb_pipeline::Token2022StatefulReadRequest {
|
||||
query_role: options.query_role.clone(),
|
||||
account: mint.clone(),
|
||||
kind: kb_lib::DcToken2022StateKind::Mint,
|
||||
min_context_slot: std::option::Option::None,
|
||||
max_data_bytes: kb_pipeline::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES,
|
||||
context: kb_pipeline::Token2022StatefulContext::default(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !initial_snapshot
|
||||
.snapshot
|
||||
.extension_names
|
||||
.iter()
|
||||
.any(|value| return value == "metadata_pointer")
|
||||
|| initial_snapshot
|
||||
.snapshot
|
||||
.extension_names
|
||||
.iter()
|
||||
.any(|value| return value == "token_metadata")
|
||||
|| !metadata_pointer_snapshot_matches(&initial_snapshot.snapshot, &mint, &initial_authority)
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_fixture_state_invalid",
|
||||
"fresh metadata fixture must contain the expected self-referential Metadata Pointer and no Token Metadata extension",
|
||||
));
|
||||
}
|
||||
let rent = match http_pool
|
||||
.get_minimum_balance_for_rent_exemption_for_role(
|
||||
options.query_role.as_str(),
|
||||
match u64::try_from(rent_budget_space) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 metadata rent budget does not fit u64: {error}"
|
||||
)));
|
||||
},
|
||||
},
|
||||
&kb_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::Token2022MetadataFixturePreparationSummary {
|
||||
mint_keypair_path: fixture_store.wallet_path(&mint_alias),
|
||||
final_authority_keypair_path: fixture_store.wallet_path(&final_authority_alias),
|
||||
mint,
|
||||
initial_authority,
|
||||
final_authority,
|
||||
mint_space,
|
||||
rent_budget_space,
|
||||
rent_reserve_lamports: rent.minimum_balance_lamports,
|
||||
preparation_signature,
|
||||
initial_snapshot,
|
||||
});
|
||||
}
|
||||
|
||||
fn metadata_pointer_snapshot_matches(
|
||||
snapshot: &kb_pipeline::Token2022StatefulSnapshotBundle,
|
||||
mint: &kb_lib::MdPubkey,
|
||||
authority: &kb_lib::MdPubkey,
|
||||
) -> bool {
|
||||
return snapshot.outputs.iter().any(|output| {
|
||||
return output.family == kb_lib::MdMaterializedEventFamily::Admin
|
||||
&& output.payload_json.get("extensionName").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some("metadata_pointer")
|
||||
&& output
|
||||
.payload_json
|
||||
.get("valueFields")
|
||||
.and_then(|value| return value.get("address"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(mint.0.as_str())
|
||||
&& output
|
||||
.payload_json
|
||||
.get("valueFields")
|
||||
.and_then(|value| return value.get("authority"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(authority.0.as_str());
|
||||
});
|
||||
}
|
||||
|
||||
fn token_2022_metadata_pointer_mint_space() -> kb_core::Result<usize> {
|
||||
return match spl_token_2022_interface::extension::ExtensionType::try_calculate_account_len::<
|
||||
spl_token_2022_interface::pod::PodMint,
|
||||
>(&[spl_token_2022_interface::extension::ExtensionType::MetadataPointer])
|
||||
{
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_fixture_space_failed",
|
||||
format!("unable to calculate Metadata Pointer mint space: {error}"),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
fn metadata_campaign_rent_budget_space(mint_space: usize) -> usize {
|
||||
return mint_space
|
||||
.saturating_add(TOKEN_2022_TLV_HEADER_BYTES)
|
||||
.saturating_add(metadata_campaign_max_value_bytes());
|
||||
}
|
||||
|
||||
fn metadata_campaign_max_value_bytes() -> usize {
|
||||
return 64_usize
|
||||
.saturating_add(4)
|
||||
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_NAME.len())
|
||||
.saturating_add(4)
|
||||
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_SYMBOL.len())
|
||||
.saturating_add(4)
|
||||
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_URI.len())
|
||||
.saturating_add(4)
|
||||
.saturating_add(4)
|
||||
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_KEY.len())
|
||||
.saturating_add(4)
|
||||
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_VALUE.len());
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_native_token_2022_metadata_mint(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
options: &crate::Token2022MetadataFixturePreparationOptions,
|
||||
operator: &kb_wallet::TemporaryWallet,
|
||||
mint_wallet: &kb_wallet::TemporaryWallet,
|
||||
mint_space: usize,
|
||||
rent_budget_space: usize,
|
||||
) -> kb_core::Result<std::string::String> {
|
||||
let rent_space = match u64::try_from(rent_budget_space) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 metadata rent budget does not fit u64: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let account_space = match u64::try_from(mint_space) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 Metadata Pointer mint space does not fit u64: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let rent = match http_pool
|
||||
.get_minimum_balance_for_rent_exemption_for_role(
|
||||
options.query_role.as_str(),
|
||||
rent_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!("token-2022-metadata-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: account_space,
|
||||
owner: kb_lib::MdPubkey(kb_program_ids::SPL_TOKEN_2022_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_2022_PROGRAM_ID,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"invalid Token-2022 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 Token-2022 metadata fixture mint: {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 Token-2022 metadata fixture authority: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let pointer_instruction =
|
||||
match spl_token_2022_interface::extension::metadata_pointer::instruction::initialize(
|
||||
&token_program,
|
||||
&mint_pubkey,
|
||||
std::option::Option::Some(authority_pubkey),
|
||||
std::option::Option::Some(mint_pubkey),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_metadata_fixture_pointer_build_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let initialize_mint = match spl_token_2022_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(
|
||||
"token_2022_metadata_fixture_initialize_mint_build_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let plan = merge_fixture_instructions(
|
||||
system_plan,
|
||||
std::vec![
|
||||
planned_instruction("spl.token_2022.initialize_metadata_pointer", &pointer_instruction,),
|
||||
planned_instruction("spl.token_2022.initialize_mint2", &initialize_mint),
|
||||
],
|
||||
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(
|
||||
"token_2022_metadata_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(
|
||||
"token_2022_metadata_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(
|
||||
"token_2022_metadata_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(
|
||||
"token_2022_metadata_fixture_confirmation_failed",
|
||||
format!("Token-2022 metadata mint preparation stopped at {:?}", confirmation.status),
|
||||
));
|
||||
}
|
||||
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_instructions(
|
||||
mut system_plan: kb_lib::ExApiPreparedExecutionPlan,
|
||||
token_instructions: std::vec::Vec<kb_lib::ExApiPlannedInstruction>,
|
||||
policy: kb_lib::ExApiExecutionPolicy,
|
||||
rent_lamports: u64,
|
||||
) -> kb_lib::ExApiPreparedExecutionPlan {
|
||||
system_plan.instructions.extend(token_instructions);
|
||||
system_plan.executor_name =
|
||||
"kb-pipeline-demo-scenarios.token_2022_metadata_fixture".to_string();
|
||||
system_plan.operation_code = "spl.token_2022.metadata.prepare_mint".to_string();
|
||||
system_plan.policy = policy;
|
||||
system_plan.requested_spend_lamports = rent_lamports;
|
||||
return system_plan;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn metadata_rent_budget_covers_pointer_and_largest_campaign_value() {
|
||||
let mint_space = super::token_2022_metadata_pointer_mint_space()
|
||||
.unwrap_or_else(|error| panic!("metadata pointer mint size failed: {error}"));
|
||||
assert!(mint_space > 82);
|
||||
let budget = super::metadata_campaign_rent_budget_space(mint_space);
|
||||
assert_eq!(
|
||||
budget,
|
||||
mint_space
|
||||
+ super::TOKEN_2022_TLV_HEADER_BYTES
|
||||
+ super::metadata_campaign_max_value_bytes()
|
||||
);
|
||||
assert!(budget > mint_space);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"matrixVersion": 1,
|
||||
"milestone": "0.4.8-pre.012",
|
||||
"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
|
||||
"scenarios": [
|
||||
{
|
||||
"id": "token_2022_metadata_initialize",
|
||||
"operation": "initialize_token_metadata",
|
||||
"status": "not_run",
|
||||
"requiredEvidence": ["simulation_success", "signature", "confirmation", "canonical_hydration", "core_extraction", "decode_replay", "instruction_materialization", "stateful_postcondition", "idempotence_replay"],
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "token_2022_metadata_update_field",
|
||||
"operation": "update_token_metadata_field",
|
||||
"status": "not_run",
|
||||
"requiredEvidence": ["simulation_success", "signature", "confirmation", "canonical_hydration", "core_extraction", "decode_replay", "instruction_materialization", "stateful_postcondition", "idempotence_replay"],
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "token_2022_metadata_emit",
|
||||
"operation": "emit_token_metadata",
|
||||
"status": "not_run",
|
||||
"requiredEvidence": ["simulation_success", "return_data", "signature", "confirmation", "canonical_hydration", "core_extraction", "decode_replay", "stateful_postcondition", "idempotence_replay"],
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "token_2022_metadata_remove_key",
|
||||
"operation": "remove_token_metadata_key",
|
||||
"status": "not_run",
|
||||
"requiredEvidence": ["simulation_success", "signature", "confirmation", "canonical_hydration", "core_extraction", "decode_replay", "instruction_materialization", "stateful_postcondition", "idempotence_replay"],
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "token_2022_metadata_update_authority",
|
||||
"operation": "update_token_metadata_authority",
|
||||
"status": "not_run",
|
||||
"requiredEvidence": ["simulation_success", "signature", "confirmation", "canonical_hydration", "core_extraction", "decode_replay", "instruction_materialization", "stateful_postcondition", "idempotence_replay"],
|
||||
"evidence": []
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user