v0.4.8-pre.009
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 30
|
# version: 31
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
@@ -18,7 +18,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.4.8-pre.8"
|
version = "0.4.8-pre.9"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/README.md -->
|
<!-- file: docs/README.md -->
|
||||||
<!-- version: 21 -->
|
<!-- version: 22 -->
|
||||||
|
|
||||||
# Documentation active de Khadhroony Bot3
|
# Documentation active de Khadhroony Bot3
|
||||||
|
|
||||||
@@ -45,6 +45,7 @@ Modèles documentaires non génératifs :
|
|||||||
- [`audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md`](audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md) ;
|
- [`audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md`](audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md) ;
|
||||||
- [`audits/V0_4_8_PRE_008_METADATA_AND_PIPELINE_NOMENCLATURE_AUDIT.md`](audits/V0_4_8_PRE_008_METADATA_AND_PIPELINE_NOMENCLATURE_AUDIT.md) ;
|
- [`audits/V0_4_8_PRE_008_METADATA_AND_PIPELINE_NOMENCLATURE_AUDIT.md`](audits/V0_4_8_PRE_008_METADATA_AND_PIPELINE_NOMENCLATURE_AUDIT.md) ;
|
||||||
- [`audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md`](audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md) ;
|
- [`audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md`](audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md) ;
|
||||||
|
- [`audits/V0_4_8_PRE_009_METADATA_SOLANA_PROGRAM_DEVNET_SCENARIOS_AUDIT.md`](audits/V0_4_8_PRE_009_METADATA_SOLANA_PROGRAM_DEVNET_SCENARIOS_AUDIT.md) ;
|
||||||
- [`decisions/DOCUMENT_ARCHIVE_SELECTION_POLICY.md`](decisions/DOCUMENT_ARCHIVE_SELECTION_POLICY.md) ;
|
- [`decisions/DOCUMENT_ARCHIVE_SELECTION_POLICY.md`](decisions/DOCUMENT_ARCHIVE_SELECTION_POLICY.md) ;
|
||||||
- [`decisions/WINCODE_COMPATIBILITY_POLICY.md`](decisions/WINCODE_COMPATIBILITY_POLICY.md).
|
- [`decisions/WINCODE_COMPATIBILITY_POLICY.md`](decisions/WINCODE_COMPATIBILITY_POLICY.md).
|
||||||
|
|
||||||
@@ -98,6 +99,7 @@ Les onze crates possèdent désormais `README.md`, `TODO.md`, `USAGE.md` et `CHA
|
|||||||
- [Validation Metaplex Token Metadata 0.4.7](validation/V0_4_7_METAPLEX_TOKEN_METADATA_VALIDATION_REPORT.md)
|
- [Validation Metaplex Token Metadata 0.4.7](validation/V0_4_7_METAPLEX_TOKEN_METADATA_VALIDATION_REPORT.md)
|
||||||
- [`validation/WEBSOCKET_MAINNET_RESEARCH_VALIDATION_REPORT.md`](validation/WEBSOCKET_MAINNET_RESEARCH_VALIDATION_REPORT.md) ;
|
- [`validation/WEBSOCKET_MAINNET_RESEARCH_VALIDATION_REPORT.md`](validation/WEBSOCKET_MAINNET_RESEARCH_VALIDATION_REPORT.md) ;
|
||||||
- [`validation/MAINNET_RESEARCH_BACKFILL_VALIDATION_SCENARIO.md`](validation/MAINNET_RESEARCH_BACKFILL_VALIDATION_SCENARIO.md) ;
|
- [`validation/MAINNET_RESEARCH_BACKFILL_VALIDATION_SCENARIO.md`](validation/MAINNET_RESEARCH_BACKFILL_VALIDATION_SCENARIO.md) ;
|
||||||
|
- [`validation/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_MAINNET_REPLAY_REPORT.md`](validation/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_MAINNET_REPLAY_REPORT.md) ;
|
||||||
|
|
||||||
## Audits actifs
|
## Audits actifs
|
||||||
|
|
||||||
@@ -106,7 +108,8 @@ Les onze crates possèdent désormais `README.md`, `TODO.md`, `USAGE.md` et `CHA
|
|||||||
- [`Audit 0.4.8-pre.007 — structure du matérialiseur Metaplex Token Metadata`](audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md) ;
|
- [`Audit 0.4.8-pre.007 — structure du matérialiseur Metaplex Token Metadata`](audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md) ;
|
||||||
- [`Audit 0.4.8-pre.007 — conventions et structure de tous les matérialisateurs`](audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md) ;
|
- [`Audit 0.4.8-pre.007 — conventions et structure de tous les matérialisateurs`](audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md) ;
|
||||||
- [`Audit 0.4.8-pre.008 — pipeline Solana Program Metadata`](audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md).
|
- [`Audit 0.4.8-pre.008 — pipeline Solana Program Metadata`](audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md).
|
||||||
- [`Correctif 0.4.8-pre.008 — replay historique Solana Program Metadata`](audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md).
|
- [`Correctif 0.4.8-pre.008 — replay historique Solana Program Metadata`](audits/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_HISTORICAL_REPLAY_FIX.md) ;
|
||||||
|
- [`Audit 0.4.8-pre.009 — scénarios Devnet Solana Program Metadata`](audits/V0_4_8_PRE_009_METADATA_SOLANA_PROGRAM_DEVNET_SCENARIOS_AUDIT.md).
|
||||||
|
|
||||||
## Plans de version actifs
|
## Plans de version actifs
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<!-- file: docs/audits/V0_4_8_PRE_009_METADATA_SOLANA_PROGRAM_DEVNET_SCENARIOS_AUDIT.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Audit `0.4.8-pre.009` — scénarios Devnet Solana Program Metadata
|
||||||
|
|
||||||
|
## Frontière de crate
|
||||||
|
|
||||||
|
`kb-pipeline` conserve les contrats réutilisables indépendants du cluster. `kb-pipeline-demo-scenarios` possède les wallets de démonstration, fixtures, appels RPC Devnet et campagnes destructrices. Aucun lien inverse n’est ajouté.
|
||||||
|
|
||||||
|
`kb-app-demo-desktop` n’est pas modifié dans cette prerelease. Son intégration est réservée à `pre.010`.
|
||||||
|
|
||||||
|
## Fixture
|
||||||
|
|
||||||
|
La fixture utilise le wallet persistant du profil Devnet comme autorité et le System Program comme programme exécutable décrit. Deux PDA non canoniques uniques sont dérivés avec des seeds UTF-8 de seize octets :
|
||||||
|
|
||||||
|
- un PDA `Buffer` ;
|
||||||
|
- un PDA `Metadata`.
|
||||||
|
|
||||||
|
Les deux adresses doivent être absentes avant préparation. Leur rent est calculé par RPC et les comptes sont préfinancés par des transferts System Program simulation-first, évalués par `ExSafetyChecker`, signés, soumis et confirmés.
|
||||||
|
|
||||||
|
## Parcours fermés
|
||||||
|
|
||||||
|
### Buffer
|
||||||
|
|
||||||
|
1. `Allocate` ;
|
||||||
|
2. `Extend` ;
|
||||||
|
3. `Write` ;
|
||||||
|
4. `Trim` ;
|
||||||
|
5. `Close`.
|
||||||
|
|
||||||
|
`Extend` précède `Write`, car l’allocation initiale ne réserve que l’en-tête et l’écriture exige la capacité du payload.
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
|
||||||
|
1. `Initialize` ;
|
||||||
|
2. `SetData` ;
|
||||||
|
3. `SetAuthority` ;
|
||||||
|
4. `SetImmutable`.
|
||||||
|
|
||||||
|
Les neuf opérations stables sont couvertes exactement une fois. Les six opérations classées sensibles par l’exécuteur reçoivent une approbation explicite.
|
||||||
|
|
||||||
|
## Orchestration
|
||||||
|
|
||||||
|
Chaque étape passe par :
|
||||||
|
|
||||||
|
- construction d’un intent typé ;
|
||||||
|
- construction et évaluation du plan ;
|
||||||
|
- lectures stateful avant exécution ;
|
||||||
|
- préflight et preuves de rent ;
|
||||||
|
- transaction exacte et simulation RPC ;
|
||||||
|
- readiness du pipeline ;
|
||||||
|
- évaluation `ExSafetyChecker` avant envoi ;
|
||||||
|
- signature, soumission et confirmation ;
|
||||||
|
- lectures stateful après confirmation ;
|
||||||
|
- postcondition ;
|
||||||
|
- projection matérialisée pour les comptes existants.
|
||||||
|
|
||||||
|
`Close` exige une preuve d’absence et non un snapshot matérialisé.
|
||||||
|
|
||||||
|
## Statut réseau
|
||||||
|
|
||||||
|
Le code et la matrice sont livrés avec des statuts `not_run`. Aucune validation Devnet n’est déclarée avant l’exécution opérateur de `pre.010`.
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<!-- file: docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md -->
|
<!-- file: docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md -->
|
||||||
<!-- version: 13 -->
|
<!-- version: 14 -->
|
||||||
|
|
||||||
# Plan `0.4.8` — Solana Program Metadata et complétude Token-2022
|
# Plan `0.4.8` — Solana Program Metadata et complétude Token-2022
|
||||||
|
|
||||||
## 1. Statut et rôle
|
## 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` et l’orchestration généraliste de `pre.008`. 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` et les scénarios réutilisables de `pre.009`. 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.
|
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.
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ Résultat durable : les neuf instructions stables possèdent une opération typ
|
|||||||
|
|
||||||
Travaux parallèles intégrés : les fonctions de matérialisation Metaplex existaient déjà dans `materializer/metadata/core.rs`; il ne s’agissait donc pas d’un oubli fonctionnel de `0.4.6/0.4.7`. Elles sont déplacées dans un sous-module dédié sans changement de clé métier. Token-2022 reçoit également son composant dédié de snapshots, puis `pre.008` ajoute son matérialiseur instructionnel spécialisé ainsi que celui de Metaplex. L’audit est ensuite étendu à l’ensemble de `src/materializer` : 25 identités sont harmonisées, les façades deviennent pures, les implémentations utilisent `materializer.rs`, les constantes sont centralisées et les coquilles réservées ne produisent plus de faux événements legacy. Les constats sont consignés dans `docs/audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md` et `docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md`. Un audit transversal de toutes les surfaces déjà livrées est planifié en `0.5.x` pour vérifier les règles de décodage exhaustif, propriété de matérialisation, exécution des opérations dangereuses via `ExSafetyChecker`, dépréciation officielle et versions remplacées decode-only.
|
Travaux parallèles intégrés : les fonctions de matérialisation Metaplex existaient déjà dans `materializer/metadata/core.rs`; il ne s’agissait donc pas d’un oubli fonctionnel de `0.4.6/0.4.7`. Elles sont déplacées dans un sous-module dédié sans changement de clé métier. Token-2022 reçoit également son composant dédié de snapshots, puis `pre.008` ajoute son matérialiseur instructionnel spécialisé ainsi que celui de Metaplex. L’audit est ensuite étendu à l’ensemble de `src/materializer` : 25 identités sont harmonisées, les façades deviennent pures, les implémentations utilisent `materializer.rs`, les constantes sont centralisées et les coquilles réservées ne produisent plus de faux événements legacy. Les constats sont consignés dans `docs/audits/V0_4_8_PRE_007_METAPLEX_MATERIALIZER_STRUCTURE_AUDIT.md` et `docs/audits/V0_4_8_PRE_007_MATERIALIZER_CONVENTION_AUDIT.md`. Un audit transversal de toutes les surfaces déjà livrées est planifié en `0.5.x` pour vérifier les règles de décodage exhaustif, propriété de matérialisation, exécution des opérations dangereuses via `ExSafetyChecker`, dépréciation officielle et versions remplacées decode-only.
|
||||||
|
|
||||||
### `0.4.8-pre.008` — pipeline stateful Solana Program Metadata — code terminé, replay historique à revalider
|
### `0.4.8-pre.008` — pipeline stateful et replay mainnet Solana Program Metadata — terminé
|
||||||
|
|
||||||
- lectures RPC bornées des comptes absents, non initialisés, `Buffer` et `Metadata` ;
|
- lectures RPC bornées des comptes absents, non initialisés, `Buffer` et `Metadata` ;
|
||||||
- décodage et matérialisation autoritative des états via `kb-lib` ;
|
- décodage et matérialisation autoritative des états via `kb-lib` ;
|
||||||
@@ -284,50 +284,66 @@ Travaux parallèles intégrés : les fonctions de matérialisation Metaplex exis
|
|||||||
- passage par `ExSafetyChecker`, readiness simulation-first, résolution des signers et autorisation de soumission ;
|
- passage par `ExSafetyChecker`, readiness simulation-first, résolution des signers et autorisation de soumission ;
|
||||||
- postconditions avant/après des neuf opérations, sans succès inventé lorsqu’une lecture manque ;
|
- postconditions avant/après des neuf opérations, sans succès inventé lorsqu’une lecture manque ;
|
||||||
- matrice pipeline machine-readable ;
|
- matrice pipeline machine-readable ;
|
||||||
- enregistrement du décodeur et des trois matérialiseurs metadata spécialisés dans le replay générique desktop pour le futur test `demo_backfill` ;
|
- enregistrement du décodeur et des trois matérialiseurs metadata spécialisés dans le replay générique desktop ;
|
||||||
- nomenclature interne `metadata_*`, `spl_*` et `solana_*` alignée sur les domaines de `kb-lib` ;
|
- nomenclature interne `metadata_*`, `spl_*` et `solana_*` alignée sur les domaines de `kb-lib` ;
|
||||||
- aucune modification de `kb-pipeline-demo-scenarios` ni de `demo_execution_metadata`.
|
- correction du décodeur pour valider les privilèges requis comme minima au lieu d’exiger l’égalité avec les privilèges globaux du message ;
|
||||||
- audit consigné dans `docs/audits/V0_4_8_PRE_008_METADATA_SOLANA_PROGRAM_PIPELINE_AUDIT.md`.
|
- replay mainnet final : 132 instructions `ProgM6…` dispatchées, 132 décodées, 132 matérialisées, zéro échec et zéro refus ;
|
||||||
- première campagne historique : 200 transactions importées, 582 instructions rejouées et 132 instructions `ProgM6…` reconnues ; toutes ont été rejetées à tort parce que le décodeur comparait les privilèges globaux du message par égalité avec les minima de l’instruction ;
|
- opérations observées : `Write` 88, `Allocate` 15, `Initialize` 11, `SetAuthority` 7, `Close` 5, `SetData` 5 et `SetImmutable` 1 ;
|
||||||
- `pre.008-fix-002` remplace cette égalité par une validation de minima et ajoute une régression sur les neuf instructions ; la campagne des 132 entrées doit être rejouée avant d’ouvrir `pre.009`.
|
- `Trim` et `Extend` restent non observées dans l’échantillon mainnet et doivent être prouvées sur Devnet ;
|
||||||
|
- preuve conservée sous `docs/validation/evidence/v0.4.8/pre.008/mainnet/`.
|
||||||
|
|
||||||
### `0.4.8-pre.009` — campagne Devnet et desktop Solana Program Metadata
|
### `0.4.8-pre.009` — scénarios réutilisables Solana Program Metadata — terminé côté delta
|
||||||
|
|
||||||
- fixture Rust native et comptes préfinancés ;
|
- séparation stricte : `kb-pipeline` reste généraliste et inchangé, tandis que les fixtures et campagnes réseau appartiennent à `kb-pipeline-demo-scenarios` ;
|
||||||
- scénarios réutilisables dans `kb-pipeline-demo-scenarios` ;
|
- deux parcours ordonnés couvrent exactement les neuf opérations stables ;
|
||||||
- appels RPC de simulation, soumission, confirmation, lectures avant/après et replay ;
|
- parcours Buffer : `Allocate → Extend → Write → Trim → Close` ;
|
||||||
- adaptation du sous-panneau `ProgM6…` de `demo_execution_metadata` pour consommer les runners réutilisables ;
|
- parcours Metadata : `Initialize → SetData → SetAuthority → SetImmutable` ;
|
||||||
- validation historique préalable via `demo_backfill`, Core extraction et decode replay sur un échantillon de signatures ;
|
- création de deux PDA non canoniques uniques et préfinancés avec le wallet persistant du profil Devnet ;
|
||||||
- preuves RPC et rapport intermédiaire.
|
- calcul RPC du rent pour les tailles initiales et finales ;
|
||||||
|
- runner unitaire simulation-first avec `ExSafetyChecker`, soumission contrôlée, confirmation, lectures avant/après et postconditions ;
|
||||||
|
- campagne confirmée réutilisable exécutant séquentiellement les neuf étapes ;
|
||||||
|
- projections stateful matérialisées après confirmation, ou preuve d’absence après `Close` ;
|
||||||
|
- matrice Devnet conservative à `not_run` avant exécution réelle ;
|
||||||
|
- aucune modification de `demo_execution_metadata` dans cette prerelease.
|
||||||
|
|
||||||
### `0.4.8-pre.010` — complétude Token-2022 Token Metadata
|
### `0.4.8-pre.010` — intégration desktop et campagne Devnet Solana Program Metadata
|
||||||
|
|
||||||
|
- adapter le sous-panneau `ProgM6…` de `demo_execution_metadata` pour consommer exclusivement les runners de `kb-pipeline-demo-scenarios` ;
|
||||||
|
- préparer la fixture, présenter les deux parcours et exiger la confirmation explicite avant les transferts ou mutations ;
|
||||||
|
- exécuter les neuf opérations sur Devnet, avec signatures, simulations, confirmations et postconditions ;
|
||||||
|
- hydrater les signatures confirmées, lancer Core extraction, decode replay et matérialisation canonique ;
|
||||||
|
- mettre à jour la matrice de validation sans convertir une simulation ou une construction synthétique en preuve confirmée ;
|
||||||
|
- confirmer particulièrement `Trim` et `Extend`, absentes de l’échantillon mainnet ;
|
||||||
|
- conserver des résultats JSON dans le JsonViewer commun.
|
||||||
|
|
||||||
|
### `0.4.8-pre.011` — complétude Token-2022 Token Metadata
|
||||||
|
|
||||||
- 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` ;
|
- 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` ;
|
- conservation des snapshots dédiés `metadata/token_2022/` pour `TokenMetadata`, `TokenGroup` et `TokenGroupMember` ;
|
||||||
- validation de la matérialisation instructionnelle spécialisée déjà ajoutée en `pre.008` pour les interfaces Token Metadata et Token Group ;
|
- validation de la matérialisation instructionnelle spécialisée des interfaces Token Metadata et Token Group ;
|
||||||
- maintien de `InitializeMetadataPointer` et `UpdateMetadataPointer` dans le matérialiseur d’administration qui en possède déjà les faits ;
|
- maintien de `InitializeMetadataPointer` et `UpdateMetadataPointer` dans le matérialiseur d’administration qui en possède déjà les faits ;
|
||||||
- tests synthétiques et stateful.
|
- tests synthétiques et stateful.
|
||||||
|
|
||||||
### `0.4.8-pre.011` — campagne Devnet Token-2022 Token Metadata
|
### `0.4.8-pre.012` — campagne Devnet Token-2022 Token Metadata
|
||||||
|
|
||||||
- fixture mint ;
|
- fixture mint ;
|
||||||
- simulations, soumissions sûres, confirmations, postconditions, replay et matérialisation.
|
- simulations, soumissions sûres, confirmations, postconditions, replay et matérialisation.
|
||||||
|
|
||||||
### `0.4.8-pre.012` — finalisation desktop metadata
|
### `0.4.8-pre.013` — finalisation desktop metadata
|
||||||
|
|
||||||
- compléter le sous-panneau Token-2022 après les travaux de `pre.010` et `pre.011` ;
|
- compléter le sous-panneau Token-2022 après les travaux de `pre.011` et `pre.012` ;
|
||||||
- conserver des accordéons ou sous-panneaux séparés pour `ProgM6…`, Token-2022 et Metaplex ;
|
- 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éutiliser le JsonViewer commun pour les résultats intégralement JSON ;
|
||||||
- réconcilier préparation, simulation, soumission, lectures et preuves des trois domaines ;
|
- réconcilier préparation, simulation, soumission, lectures et preuves des trois domaines ;
|
||||||
- aucun bouton de fetch URI.
|
- aucun bouton de fetch URI.
|
||||||
|
|
||||||
### `0.4.8-pre.013` — stockage, matrices et réconciliation transversale
|
### `0.4.8-pre.014` — stockage, matrices et réconciliation transversale
|
||||||
|
|
||||||
- stockage si de nouveaux contrats persistants sont requis ;
|
- stockage si de nouveaux contrats persistants sont requis ;
|
||||||
- exports, registres, matrices, nomenclature IDL et guides ;
|
- exports, registres, matrices, nomenclature IDL et guides ;
|
||||||
- retrait des statuts `reserved` uniquement lorsque l’implémentation correspondante est réelle.
|
- retrait des statuts `reserved` uniquement lorsque l’implémentation correspondante est réelle.
|
||||||
|
|
||||||
### `0.4.8-pre.014` — clôture obligatoire
|
### `0.4.8-pre.015` — clôture obligatoire
|
||||||
|
|
||||||
Réservée à :
|
Réservée à :
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<!-- file: docs/validation/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_MAINNET_REPLAY_REPORT.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Validation mainnet du replay Solana Program Metadata — `0.4.8-pre.008`
|
||||||
|
|
||||||
|
## Résultat
|
||||||
|
|
||||||
|
La campagne historique exécutée après `pre.008-fix-002` valide le parcours réel `backfill → Core extraction → decode replay → matérialisation` pour les instructions Solana Program Metadata observées.
|
||||||
|
|
||||||
|
```text
|
||||||
|
dispatched = 132
|
||||||
|
decoded = 132
|
||||||
|
failed = 0
|
||||||
|
processingErrors = 0
|
||||||
|
materializedOutputs = 132
|
||||||
|
materializationRefused = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Répartition observée
|
||||||
|
|
||||||
|
| Opération | Observée | Décodée | Matérialisée |
|
||||||
|
|----------------|---------:|--------:|-------------:|
|
||||||
|
| `Write` | 88 | 88 | 88 |
|
||||||
|
| `Allocate` | 15 | 15 | 15 |
|
||||||
|
| `Initialize` | 11 | 11 | 11 |
|
||||||
|
| `SetAuthority` | 7 | 7 | 7 |
|
||||||
|
| `Close` | 5 | 5 | 5 |
|
||||||
|
| `SetData` | 5 | 5 | 5 |
|
||||||
|
| `SetImmutable` | 1 | 1 | 1 |
|
||||||
|
| `Trim` | 0 | 0 | 0 |
|
||||||
|
| `Extend` | 0 | 0 | 0 |
|
||||||
|
|
||||||
|
Les sept opérations rencontrées sont validées sur données mainnet réelles. `Trim` et `Extend` restent validées synthétiquement mais non observées dans cet échantillon. Elles doivent être exercées par les scénarios Devnet de `pre.009` et `pre.010`.
|
||||||
|
|
||||||
|
## Anomalies non liées
|
||||||
|
|
||||||
|
Quatre entrées Loader v4 ont été classées `Unsupported` sans échec de traitement. Elles proviennent de transactions déjà échouées avec `ProgramAccountNotFound` et ne concernent pas `ProgM6…`.
|
||||||
|
|
||||||
|
## Preuve conservée
|
||||||
|
|
||||||
|
- archive : `docs/validation/evidence/v0.4.8/pre.008/mainnet/mainnet_research_v0.4.8-pre.008-01.zip` ;
|
||||||
|
- SHA-256 : `16b3722d4b75afdef304f7b16af4baa3b51713d87a74e9e4a7aac0c0f0411335` ;
|
||||||
|
- manifeste : `docs/validation/evidence/v0.4.8/pre.008/mainnet/SHA256SUMS`.
|
||||||
|
|
||||||
|
Cette archive est une preuve documentaire immuable. Le code, les tests et les runners ne doivent jamais en dépendre.
|
||||||
18
docs/validation/evidence/v0.4.8/pre.008/mainnet/README.md
Normal file
18
docs/validation/evidence/v0.4.8/pre.008/mainnet/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- file: docs/validation/evidence/v0.4.8/pre.008/mainnet/README.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Preuve mainnet `0.4.8-pre.008`
|
||||||
|
|
||||||
|
Ce répertoire conserve la preuve binaire de la campagne historique ayant validé le décodeur et le matérialiseur Solana Program Metadata après `pre.008-fix-002`.
|
||||||
|
|
||||||
|
## Artefact
|
||||||
|
|
||||||
|
- fichier : `mainnet_research_v0.4.8-pre.008-01.zip` ;
|
||||||
|
- SHA-256 : `16b3722d4b75afdef304f7b16af4baa3b51713d87a74e9e4a7aac0c0f0411335` ;
|
||||||
|
- origine : export de logs produit le 6 août 2026 pendant le replay mainnet ;
|
||||||
|
- usage : preuve documentaire et diagnostic reproductible ;
|
||||||
|
- chargement applicatif : aucun.
|
||||||
|
|
||||||
|
L’archive ne doit pas être extraite dans l’arbre source. Elle est référencée par le rapport `docs/validation/V0_4_8_PRE_008_SOLANA_PROGRAM_METADATA_MAINNET_REPLAY_REPORT.md`.
|
||||||
|
|
||||||
|
Un contrôle avant archivage n’a trouvé ni fichier de wallet, ni keypair, ni `.env`, ni marqueur d’URL ou d’autorisation dans les contenus textuels. Cette vérification ne remplace pas une revue humaine avant publication hors du dépôt privé.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
16b3722d4b75afdef304f7b16af4baa3b51713d87a74e9e4a7aac0c0f0411335 mainnet_research_v0.4.8-pre.008-01.zip
|
||||||
Binary file not shown.
@@ -1,8 +1,21 @@
|
|||||||
<!-- file: kb-pipeline-demo-scenarios/CHANGELOG.md -->
|
<!-- file: kb-pipeline-demo-scenarios/CHANGELOG.md -->
|
||||||
<!-- version: 27 -->
|
<!-- version: 30 -->
|
||||||
|
|
||||||
# CHANGELOG — kb-pipeline-demo-scenarios
|
# CHANGELOG — kb-pipeline-demo-scenarios
|
||||||
|
|
||||||
|
## 0.4.8-pre.009 — scénarios Devnet Solana Program Metadata
|
||||||
|
|
||||||
|
- corrige les derniers diagnostics Clippy des tests : `str::len()` direct pour les seeds UTF-8 et `return` explicite dans la closure de statut de validation ;
|
||||||
|
- corrige les tests de réconciliation des opérations : comparaison de références `&str` et durée de vie de l’inventaire de scénarios ;
|
||||||
|
- documente la crate du test d’API publique externe afin de conserver `missing_docs` propre ;
|
||||||
|
- ajoute deux parcours ordonnés couvrant exactement les neuf opérations stables de `ProgM6…` ;
|
||||||
|
- prépare deux PDA non canoniques préfinancés, un `Buffer` et un compte `Metadata`, à partir du wallet persistant du profil Devnet ;
|
||||||
|
- ajoute un runner simulation-first avec préflight stateful, `ExSafetyChecker`, signature, soumission, confirmation, lectures avant/après et matérialisation des snapshots ;
|
||||||
|
- ajoute une campagne confirmée réutilisable qui exécute les neuf étapes sans déplacer l’orchestration dans le desktop ;
|
||||||
|
- distingue la preuve `materialized_snapshot` de la preuve terminale `account_absence` après `Close` ;
|
||||||
|
- ajoute une matrice conservative dont tous les statuts réseau restent `not_run` avant exécution réelle ;
|
||||||
|
- conserve `kb-pipeline` indépendant de cette crate et ne modifie pas encore `demo_execution_metadata`.
|
||||||
|
|
||||||
## 0.4.7-pre.016
|
## 0.4.7-pre.016
|
||||||
|
|
||||||
- généralise README et USAGE autour des groupes fonctionnels ;
|
- généralise README et USAGE autour des groupes fonctionnels ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-pipeline-demo-scenarios/README.md -->
|
<!-- file: kb-pipeline-demo-scenarios/README.md -->
|
||||||
<!-- version: 7 -->
|
<!-- version: 8 -->
|
||||||
|
|
||||||
# kb-pipeline-demo-scenarios
|
# kb-pipeline-demo-scenarios
|
||||||
|
|
||||||
@@ -19,6 +19,8 @@ La crate contient :
|
|||||||
- fournir des tests automatisés parallèles aux démonstrations UI, sans déplacer les scénarios UI hors du desktop ;
|
- 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.
|
- 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 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.
|
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.
|
||||||
|
|
||||||
## Binaire CLI
|
## Binaire CLI
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
<!-- file: kb-pipeline-demo-scenarios/TODO.md -->
|
<!-- file: kb-pipeline-demo-scenarios/TODO.md -->
|
||||||
<!-- version: 10 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# TODO — kb-pipeline-demo-scenarios
|
# TODO — kb-pipeline-demo-scenarios
|
||||||
|
|
||||||
|
## Validation Devnet Solana Program 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.
|
||||||
|
|
||||||
## Validation Devnet complémentaire Metaplex Token Metadata
|
## Validation Devnet complémentaire Metaplex Token Metadata
|
||||||
|
|
||||||
- [ ] Préparer les fixtures d’édition imprimée et de burn NFT.
|
- [ ] Préparer les fixtures d’édition imprimée et de burn NFT.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-pipeline-demo-scenarios/USAGE.md -->
|
<!-- file: kb-pipeline-demo-scenarios/USAGE.md -->
|
||||||
<!-- version: 9 -->
|
<!-- version: 10 -->
|
||||||
|
|
||||||
# Utilisation de kb-pipeline-demo-scenarios
|
# Utilisation de kb-pipeline-demo-scenarios
|
||||||
|
|
||||||
@@ -337,3 +337,53 @@ fn creator_verify_request(
|
|||||||
`devnet_metaplex_creator_unverify_request` fournit le parcours inverse. Les deux requêtes restent en simulation-only par défaut.
|
`devnet_metaplex_creator_unverify_request` fournit le parcours inverse. Les deux requêtes restent en simulation-only par défaut.
|
||||||
|
|
||||||
Le test générique accepte toujours `KB_DEVNET_METAPLEX_OPERATION_JSON`, mais rejette explicitement le placeholder documentaire `...`. Les opérations plus complexes recevront des préparateurs de fixtures dédiés au lieu d’exiger de longs JSON écrits à la main.
|
Le test générique accepte toujours `KB_DEVNET_METAPLEX_OPERATION_JSON`, mais rejette explicitement le placeholder documentaire `...`. Les opérations plus complexes recevront des préparateurs de fixtures dédiés au lieu d’exiger de longs JSON écrits à la main.
|
||||||
|
## Solana Program Metadata
|
||||||
|
|
||||||
|
L’inventaire fermé est exposé par :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn program_metadata_scenarios(
|
||||||
|
) -> Vec<kb_pipeline_demo_scenarios::SolanaProgramMetadataScenario> {
|
||||||
|
kb_pipeline_demo_scenarios::solana_program_metadata_devnet_scenarios()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux parcours sont ordonnés ainsi :
|
||||||
|
|
||||||
|
1. `Allocate → Extend → Write → Trim → Close` pour le `Buffer` ;
|
||||||
|
2. `Initialize → SetData → SetAuthority → SetImmutable` pour le compte `Metadata`.
|
||||||
|
|
||||||
|
`Extend` précède volontairement `Write` : `Allocate` ne réserve que l’en-tête du Buffer, alors que l’écriture exige une capacité suffisante.
|
||||||
|
|
||||||
|
La préparation crée deux PDA non canoniques, les préfinance avec deux transferts System Program confirmés, puis produit neuf opérations typées avec leurs lectures, preuves de rent et postconditions. Elle exige un profil Devnet persistant, `devnet_send_enabled` et une confirmation opérateur explicite.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn run_program_metadata_campaign<O>(
|
||||||
|
pool: &kb_onchain_transport::HttpEndpointPool,
|
||||||
|
profile: &kb_config::ProfileConfig,
|
||||||
|
workspace_root: &std::path::Path,
|
||||||
|
observer: &O,
|
||||||
|
) -> kb_core::Result<kb_pipeline_demo_scenarios::DevnetSolanaProgramMetadataCampaignSummary>
|
||||||
|
where
|
||||||
|
O: kb_pipeline_demo_scenarios::SolanaExecutionObserver,
|
||||||
|
{
|
||||||
|
let options = kb_pipeline_demo_scenarios::SolanaProgramMetadataFixturePreparationOptions {
|
||||||
|
query_role: "http_queries".to_string(),
|
||||||
|
transaction_role: "http_transactions".to_string(),
|
||||||
|
operator_confirmed: true,
|
||||||
|
};
|
||||||
|
kb_pipeline_demo_scenarios::execute_devnet_solana_program_metadata_campaign(
|
||||||
|
pool,
|
||||||
|
profile,
|
||||||
|
workspace_root,
|
||||||
|
&options,
|
||||||
|
observer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Cet appel crée des comptes et soumet onze transactions réelles au minimum : deux transferts de préfinancement et neuf opérations `ProgM6…`. Il ne doit jamais être déclenché implicitement par une ouverture de fenêtre ou une simple demande de simulation.
|
||||||
|
|
||||||
|
La matrice `SOLANA_PROGRAM_METADATA_DEVNET_VALIDATION_MATRIX.json` reste à `not_run` tant que les signatures, simulations, postconditions et preuves matérialisées n’ont pas été enregistrées. `Close` exige une preuve `account_absence`, car un compte absent ne produit pas de snapshot matérialisé.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
// file: kb-pipeline-demo-scenarios/src/constants.rs
|
// file: kb-pipeline-demo-scenarios/src/constants.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
//! Demo scenario constants.
|
//! Demo scenario constants.
|
||||||
|
|
||||||
/// Canonical tracing target for demo pipeline scenarios.
|
/// Canonical tracing target for demo pipeline scenarios.
|
||||||
pub(crate) const TRACING_TARGET: &str = "kb-pipeline-demo-scenarios";
|
pub(crate) const TRACING_TARGET: &str = "kb-pipeline-demo-scenarios";
|
||||||
|
|
||||||
|
/// Total transactions reserved by one complete Solana Program Metadata campaign.
|
||||||
|
pub(crate) const SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT: u64 = 11;
|
||||||
|
/// Canonical tracing target for Solana Program Metadata Devnet fixtures.
|
||||||
|
pub(crate) const TRACING_TARGET_METADATA_SOLANA_PROGRAM_FIXTURE: &str =
|
||||||
|
"kb-pipeline-demo-scenarios.metadata.solana_program.fixture";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-pipeline-demo-scenarios/src/lib.rs
|
// file: kb-pipeline-demo-scenarios/src/lib.rs
|
||||||
// version: 12
|
// version: 13
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -9,6 +9,11 @@
|
|||||||
|
|
||||||
mod constants;
|
mod constants;
|
||||||
mod environment;
|
mod environment;
|
||||||
|
mod metadata_solana_program_campaign;
|
||||||
|
mod metadata_solana_program_devnet_execution;
|
||||||
|
mod metadata_solana_program_fixture;
|
||||||
|
mod metadata_solana_program_scenarios;
|
||||||
|
mod metadata_solana_program_validation;
|
||||||
mod metaplex_token_metadata_fixture;
|
mod metaplex_token_metadata_fixture;
|
||||||
mod solana_ata_execution;
|
mod solana_ata_execution;
|
||||||
mod solana_execution;
|
mod solana_execution;
|
||||||
@@ -31,6 +36,52 @@ pub use self::environment::initialize_demo_scenario_environment;
|
|||||||
pub use self::environment::prepare_demo_devnet_profile_store;
|
pub use self::environment::prepare_demo_devnet_profile_store;
|
||||||
/// Resolves one configured Devnet profile for UI, CLI and test scenarios.
|
/// Resolves one configured Devnet profile for UI, CLI and test scenarios.
|
||||||
pub use self::environment::resolve_demo_devnet_profile;
|
pub use self::environment::resolve_demo_devnet_profile;
|
||||||
|
/// One confirmed step retained by the complete Devnet campaign.
|
||||||
|
pub use self::metadata_solana_program_campaign::DevnetSolanaProgramMetadataCampaignStepSummary;
|
||||||
|
/// Complete evidence retained by the two-journey Devnet campaign.
|
||||||
|
pub use self::metadata_solana_program_campaign::DevnetSolanaProgramMetadataCampaignSummary;
|
||||||
|
/// Prepares fresh accounts and executes the nine stable operations on Devnet.
|
||||||
|
pub use self::metadata_solana_program_campaign::execute_devnet_solana_program_metadata_campaign;
|
||||||
|
/// Complete request for one Solana Program Metadata Devnet execution.
|
||||||
|
pub use self::metadata_solana_program_devnet_execution::DevnetSolanaProgramMetadataExecutionRequest;
|
||||||
|
/// Complete evidence produced by one Solana Program Metadata Devnet execution.
|
||||||
|
pub use self::metadata_solana_program_devnet_execution::DevnetSolanaProgramMetadataExecutionSummary;
|
||||||
|
/// Executes one Solana Program Metadata simulation or authorized submission.
|
||||||
|
pub use self::metadata_solana_program_devnet_execution::execute_devnet_solana_program_metadata;
|
||||||
|
/// Simulates one stable Solana Program Metadata operation against Devnet.
|
||||||
|
pub use self::metadata_solana_program_devnet_execution::simulate_devnet_solana_program_metadata;
|
||||||
|
/// Bytes reserved for the public Buffer journey before `Trim`.
|
||||||
|
pub use self::metadata_solana_program_fixture::SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES;
|
||||||
|
/// Options used to prepare one complete Solana Program Metadata fixture.
|
||||||
|
pub use self::metadata_solana_program_fixture::SolanaProgramMetadataFixturePreparationOptions;
|
||||||
|
/// Public fixture values for the two Solana Program Metadata journeys.
|
||||||
|
pub use self::metadata_solana_program_fixture::SolanaProgramMetadataFixturePreparationSummary;
|
||||||
|
/// One prepared executable step in a Solana Program Metadata journey.
|
||||||
|
pub use self::metadata_solana_program_fixture::SolanaProgramMetadataPreparedStep;
|
||||||
|
/// Creates two unique pre-funded PDAs and the nine typed journey steps.
|
||||||
|
pub use self::metadata_solana_program_fixture::prepare_solana_program_metadata_fixture;
|
||||||
|
/// Stable lifecycle state consumed or produced by one scenario step.
|
||||||
|
pub use self::metadata_solana_program_scenarios::SolanaProgramMetadataFixtureState;
|
||||||
|
/// One ordered multi-step Solana Program Metadata journey.
|
||||||
|
pub use self::metadata_solana_program_scenarios::SolanaProgramMetadataScenario;
|
||||||
|
/// One stable step in a Solana Program Metadata Devnet journey.
|
||||||
|
pub use self::metadata_solana_program_scenarios::SolanaProgramMetadataScenarioStep;
|
||||||
|
/// Returns the two ordered Devnet journeys covering all nine stable operations.
|
||||||
|
pub use self::metadata_solana_program_scenarios::solana_program_metadata_devnet_scenarios;
|
||||||
|
/// Maximum evidence entries accepted by one operation validation row.
|
||||||
|
pub use self::metadata_solana_program_validation::MAX_SOLANA_PROGRAM_METADATA_VALIDATION_EVIDENCE;
|
||||||
|
/// One bounded evidence record.
|
||||||
|
pub use self::metadata_solana_program_validation::SolanaProgramMetadataValidationEvidence;
|
||||||
|
/// Closed validation matrix for all nine stable operations.
|
||||||
|
pub use self::metadata_solana_program_validation::SolanaProgramMetadataValidationMatrix;
|
||||||
|
/// One operation row in the closed Devnet validation matrix.
|
||||||
|
pub use self::metadata_solana_program_validation::SolanaProgramMetadataValidationOperation;
|
||||||
|
/// Exact network validation status.
|
||||||
|
pub use self::metadata_solana_program_validation::SolanaProgramMetadataValidationStatus;
|
||||||
|
/// Loads and validates the canonical Solana Program Metadata Devnet matrix.
|
||||||
|
pub use self::metadata_solana_program_validation::load_solana_program_metadata_validation_matrix;
|
||||||
|
/// Validates exact inventory, order and conservative evidence claims.
|
||||||
|
pub use self::metadata_solana_program_validation::validate_solana_program_metadata_validation_matrix;
|
||||||
/// Options for one Metaplex Create fixture preparation.
|
/// Options for one Metaplex Create fixture preparation.
|
||||||
pub use self::metaplex_token_metadata_fixture::MetaplexCreateFixturePreparationOptions;
|
pub use self::metaplex_token_metadata_fixture::MetaplexCreateFixturePreparationOptions;
|
||||||
/// Public values generated for one Metaplex Create fixture.
|
/// Public values generated for one Metaplex Create fixture.
|
||||||
@@ -226,5 +277,9 @@ pub use self::token_2022_fixture::Token2022FixturePreparationSummary;
|
|||||||
/// Creates or reuses one complete Token-2022 public scenario fixture.
|
/// Creates or reuses one complete Token-2022 public scenario fixture.
|
||||||
pub use self::token_2022_fixture::prepare_token_2022_fixture;
|
pub use self::token_2022_fixture::prepare_token_2022_fixture;
|
||||||
|
|
||||||
|
/// Total transactions reserved by one complete Solana Program Metadata campaign.
|
||||||
|
pub(crate) use self::constants::SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT;
|
||||||
/// Canonical tracing target for demo pipeline scenarios.
|
/// Canonical tracing target for demo pipeline scenarios.
|
||||||
pub(crate) use self::constants::TRACING_TARGET;
|
pub(crate) use self::constants::TRACING_TARGET;
|
||||||
|
/// Canonical tracing target for Solana Program Metadata Devnet fixtures.
|
||||||
|
pub(crate) use self::constants::TRACING_TARGET_METADATA_SOLANA_PROGRAM_FIXTURE;
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_campaign.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Ordered confirmed Devnet campaign for all Solana Program Metadata operations.
|
||||||
|
|
||||||
|
/// One confirmed step retained by the complete Devnet campaign.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct DevnetSolanaProgramMetadataCampaignStepSummary {
|
||||||
|
/// Stable scenario identifier.
|
||||||
|
pub scenario_id: std::string::String,
|
||||||
|
/// Stable scenario-step identifier.
|
||||||
|
pub step_id: std::string::String,
|
||||||
|
/// Zero-based position inside the scenario.
|
||||||
|
pub step_index: usize,
|
||||||
|
/// Complete simulation, submission and stateful evidence.
|
||||||
|
pub execution: crate::DevnetSolanaProgramMetadataExecutionSummary,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete evidence retained by the two-journey Devnet campaign.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct DevnetSolanaProgramMetadataCampaignSummary {
|
||||||
|
/// Fresh pre-funded fixture used by the campaign.
|
||||||
|
pub fixture: crate::SolanaProgramMetadataFixturePreparationSummary,
|
||||||
|
/// Nine confirmed steps in deterministic journey order.
|
||||||
|
pub steps: std::vec::Vec<crate::DevnetSolanaProgramMetadataCampaignStepSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepares fresh accounts and executes the nine stable operations on Devnet.
|
||||||
|
///
|
||||||
|
/// The campaign always submits transactions. The caller must explicitly confirm
|
||||||
|
/// both fixture prefunding and all subsequent state mutations.
|
||||||
|
pub async fn execute_devnet_solana_program_metadata_campaign<O>(
|
||||||
|
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||||
|
profile: &kb_config::ProfileConfig,
|
||||||
|
workspace_root: &std::path::Path,
|
||||||
|
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
|
||||||
|
observer: &O,
|
||||||
|
) -> kb_core::Result<crate::DevnetSolanaProgramMetadataCampaignSummary>
|
||||||
|
where
|
||||||
|
O: crate::SolanaExecutionObserver,
|
||||||
|
{
|
||||||
|
if !options.operator_confirmed {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata Devnet campaign requires explicit operator confirmation",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let fixture = match crate::prepare_solana_program_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 mut steps = std::vec::Vec::with_capacity(fixture.prepared_steps.len());
|
||||||
|
for prepared in &fixture.prepared_steps {
|
||||||
|
let mut request = prepared
|
||||||
|
.execution_request(format!("spm-devnet-{}-{}", prepared.scenario_id, prepared.step_id));
|
||||||
|
request.query_role = options.query_role.clone();
|
||||||
|
request.transaction_role = options.transaction_role.clone();
|
||||||
|
request.submit = true;
|
||||||
|
request.operator_confirmed = true;
|
||||||
|
let execution = match crate::execute_devnet_solana_program_metadata(
|
||||||
|
http_pool,
|
||||||
|
profile,
|
||||||
|
workspace_root,
|
||||||
|
&request,
|
||||||
|
observer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let confirmed = match execution.confirmation.as_ref() {
|
||||||
|
std::option::Option::Some(value) => matches!(
|
||||||
|
value.status,
|
||||||
|
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||||
|
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||||
|
),
|
||||||
|
std::option::Option::None => false,
|
||||||
|
};
|
||||||
|
if !confirmed || execution.post_execution.is_none() {
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"solana_program_metadata_campaign_step_not_confirmed",
|
||||||
|
format!(
|
||||||
|
"campaign step {} did not reach confirmed stateful completion",
|
||||||
|
prepared.step_id
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if prepared.operation.operation_code() != kb_lib::EX_METADATA_SPM_CLOSE_OPERATION
|
||||||
|
&& execution.materialized_snapshots.is_empty()
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"solana_program_metadata_campaign_snapshot_missing",
|
||||||
|
format!(
|
||||||
|
"campaign step {} did not produce its required materialized snapshot",
|
||||||
|
prepared.step_id
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
steps.push(crate::DevnetSolanaProgramMetadataCampaignStepSummary {
|
||||||
|
scenario_id: prepared.scenario_id.clone(),
|
||||||
|
step_id: prepared.step_id.clone(),
|
||||||
|
step_index: prepared.step_index,
|
||||||
|
execution,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(crate::DevnetSolanaProgramMetadataCampaignSummary {
|
||||||
|
fixture,
|
||||||
|
steps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn campaign_contract_requires_exactly_nine_ordered_steps() {
|
||||||
|
let scenarios = crate::solana_program_metadata_devnet_scenarios();
|
||||||
|
let step_ids = scenarios
|
||||||
|
.iter()
|
||||||
|
.flat_map(|scenario| return scenario.steps.iter())
|
||||||
|
.map(|step| return step.id.as_str())
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
assert_eq!(step_ids.len(), 9);
|
||||||
|
assert_eq!(step_ids.first(), std::option::Option::Some(&"spm_buffer_allocate"));
|
||||||
|
assert_eq!(step_ids.last(), std::option::Option::Some(&"spm_metadata_set_immutable"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,715 @@
|
|||||||
|
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_devnet_execution.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Real Devnet simulation and controlled submission for Solana Program Metadata.
|
||||||
|
|
||||||
|
/// Complete request for one Solana Program Metadata Devnet execution.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct DevnetSolanaProgramMetadataExecutionRequest {
|
||||||
|
/// Stable caller-provided execution identifier.
|
||||||
|
pub intent_id: std::string::String,
|
||||||
|
/// Endpoint role used for reads, balance, blockhash and fee queries.
|
||||||
|
pub query_role: std::string::String,
|
||||||
|
/// Endpoint role used for simulation, submission and confirmation.
|
||||||
|
pub transaction_role: std::string::String,
|
||||||
|
/// Exact stable Solana Program Metadata operation.
|
||||||
|
pub operation: kb_lib::ExMetadataSpmOperation,
|
||||||
|
/// Bounded state reads required before execution.
|
||||||
|
pub preflight_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||||
|
/// Explicit rent observations required by allocation and growth operations.
|
||||||
|
pub rent_observations: std::vec::Vec<kb_pipeline::SolanaProgramMetadataRentObservation>,
|
||||||
|
/// Bounded state reads repeated after confirmed submission.
|
||||||
|
pub postcondition_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||||
|
/// Explicit approval for overwrite, destructive or irreversible operations.
|
||||||
|
pub allow_destructive_operation: bool,
|
||||||
|
/// Explicitly authorizes signing and submission after successful simulation.
|
||||||
|
pub submit: bool,
|
||||||
|
/// Explicit operator confirmation required for submission.
|
||||||
|
pub operator_confirmed: bool,
|
||||||
|
/// Materializes confirmed account snapshots after submission.
|
||||||
|
pub materialize_after_confirmation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::DevnetSolanaProgramMetadataExecutionRequest {
|
||||||
|
/// Creates a conservative simulation-only request.
|
||||||
|
pub fn new(
|
||||||
|
intent_id: impl std::convert::Into<std::string::String>,
|
||||||
|
operation: kb_lib::ExMetadataSpmOperation,
|
||||||
|
) -> Self {
|
||||||
|
return Self {
|
||||||
|
intent_id: intent_id.into(),
|
||||||
|
query_role: "http_queries".to_string(),
|
||||||
|
transaction_role: "http_transactions".to_string(),
|
||||||
|
operation,
|
||||||
|
preflight_reads: std::vec::Vec::new(),
|
||||||
|
rent_observations: std::vec::Vec::new(),
|
||||||
|
postcondition_reads: std::vec::Vec::new(),
|
||||||
|
allow_destructive_operation: false,
|
||||||
|
submit: false,
|
||||||
|
operator_confirmed: false,
|
||||||
|
materialize_after_confirmation: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates one conservative request from serialized typed operation JSON.
|
||||||
|
pub fn from_operation_json(
|
||||||
|
intent_id: impl std::convert::Into<std::string::String>,
|
||||||
|
operation_json: &str,
|
||||||
|
) -> kb_core::Result<Self> {
|
||||||
|
if operation_json.contains('<') || operation_json.trim() == "..." {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata operation JSON still contains a placeholder",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let operation = match serde_json::from_str::<kb_lib::ExMetadataSpmOperation>(operation_json)
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||||
|
"invalid typed Solana Program Metadata operation JSON: {error}"
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(Self::new(intent_id, operation));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates request-local bounds independently from one profile.
|
||||||
|
pub fn validate(&self) -> kb_core::Result<()> {
|
||||||
|
if self.intent_id.trim().is_empty() {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata Devnet intent id must not be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.query_role.trim().is_empty() || self.transaction_role.trim().is_empty() {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata endpoint roles must not be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.preflight_reads.is_empty()
|
||||||
|
|| self.preflight_reads.len()
|
||||||
|
> kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS
|
||||||
|
|| self.postcondition_reads.len()
|
||||||
|
> kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata stateful read inventory is empty or above the compiled bound",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.operation.requires_explicit_approval() && !self.allow_destructive_operation {
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"execution_metadata_spm_destructive_approval_required",
|
||||||
|
"destructive Solana Program Metadata execution requires explicit approval",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.submit && self.postcondition_reads.is_empty() {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"submitted Solana Program Metadata execution requires postcondition reads",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.materialize_after_confirmation && self.postcondition_reads.is_empty() {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata materialization requires postcondition reads",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete evidence produced by one Solana Program Metadata Devnet execution.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct DevnetSolanaProgramMetadataExecutionSummary {
|
||||||
|
/// Profile used by the orchestration.
|
||||||
|
pub profile_name: std::string::String,
|
||||||
|
/// Exact classified cluster.
|
||||||
|
pub cluster: kb_lib::ExApiExecutionCluster,
|
||||||
|
/// Genesis hash returned by the selected endpoint.
|
||||||
|
pub genesis_hash: std::string::String,
|
||||||
|
/// Non-secret persistent wallet description.
|
||||||
|
pub wallet: kb_wallet::WalletSummary,
|
||||||
|
/// Wallet balance observed before planning.
|
||||||
|
pub balance_lamports: u64,
|
||||||
|
/// Stateful snapshots observed before simulation.
|
||||||
|
pub before: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadResult>,
|
||||||
|
/// Stateful preflight report bound to the exact plan.
|
||||||
|
pub stateful_preflight: kb_pipeline::SolanaProgramMetadataPreflightReport,
|
||||||
|
/// Exact prepared plan.
|
||||||
|
pub plan: kb_lib::ExApiPreparedExecutionPlan,
|
||||||
|
/// Recent blockhash used by the exact transaction.
|
||||||
|
pub latest_blockhash: kb_onchain_transport::LatestBlockhashResult,
|
||||||
|
/// Fee estimate for the exact compiled message.
|
||||||
|
pub fee: kb_onchain_transport::FeeForMessageResult,
|
||||||
|
/// Exact real RPC simulation result.
|
||||||
|
pub simulation: kb_lib::ExApiExecutionSimulationResult,
|
||||||
|
/// Readiness report proving exact-message simulation and signer resolution.
|
||||||
|
pub readiness: kb_pipeline::SolanaProgramMetadataExecutionReadinessReport,
|
||||||
|
/// Submission result when explicitly authorized.
|
||||||
|
pub send_result: std::option::Option<kb_lib::ExApiExecutionSendResult>,
|
||||||
|
/// Confirmation result when submitted.
|
||||||
|
pub confirmation: std::option::Option<kb_lib::ExApiExecutionConfirmationResult>,
|
||||||
|
/// Stateful snapshots observed after confirmed submission.
|
||||||
|
pub after: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadResult>,
|
||||||
|
/// Stateful postcondition report after confirmed submission.
|
||||||
|
pub post_execution: std::option::Option<kb_pipeline::SolanaProgramMetadataPostExecutionReport>,
|
||||||
|
/// Canonical projections emitted from confirmed account snapshots.
|
||||||
|
pub materialized_snapshots: std::vec::Vec<kb_lib::MtApiMaterializedOutput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreparedSolanaProgramMetadataExecution {
|
||||||
|
wallet: kb_wallet::TemporaryWallet,
|
||||||
|
unsigned: kb_lib::ExSolanaUnsignedTransaction,
|
||||||
|
evidence: kb_lib::ExSolanaSimulationEvidence,
|
||||||
|
summary: crate::DevnetSolanaProgramMetadataExecutionSummary,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulates one stable Solana Program Metadata operation against Devnet.
|
||||||
|
pub async fn simulate_devnet_solana_program_metadata<O>(
|
||||||
|
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||||
|
profile: &kb_config::ProfileConfig,
|
||||||
|
workspace_root: &std::path::Path,
|
||||||
|
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
|
||||||
|
observer: &O,
|
||||||
|
) -> kb_core::Result<crate::DevnetSolanaProgramMetadataExecutionSummary>
|
||||||
|
where
|
||||||
|
O: crate::SolanaExecutionObserver,
|
||||||
|
{
|
||||||
|
if request.submit {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"simulate_devnet_solana_program_metadata requires submit=false",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let prepared =
|
||||||
|
match prepare_execution(http_pool, profile, workspace_root, request, observer).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(prepared.summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes one Solana Program Metadata simulation or authorized submission.
|
||||||
|
pub async fn execute_devnet_solana_program_metadata<O>(
|
||||||
|
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||||
|
profile: &kb_config::ProfileConfig,
|
||||||
|
workspace_root: &std::path::Path,
|
||||||
|
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
|
||||||
|
observer: &O,
|
||||||
|
) -> kb_core::Result<crate::DevnetSolanaProgramMetadataExecutionSummary>
|
||||||
|
where
|
||||||
|
O: crate::SolanaExecutionObserver,
|
||||||
|
{
|
||||||
|
let prepared =
|
||||||
|
match prepare_execution(http_pool, profile, workspace_root, request, observer).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if !request.submit {
|
||||||
|
return std::result::Result::Ok(prepared.summary);
|
||||||
|
}
|
||||||
|
if !prepared.summary.simulation.success {
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"execution_simulation_failed",
|
||||||
|
crate::simulation_failure_message(&prepared.summary.simulation),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = validate_profile_wallet_signers(
|
||||||
|
prepared.unsigned.required_signer_pubkeys(),
|
||||||
|
prepared.summary.wallet.public_key.as_str(),
|
||||||
|
) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let send_evaluation = match kb_lib::ExSafetyChecker
|
||||||
|
.evaluate_send(&prepared.summary.plan, &prepared.summary.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(
|
||||||
|
"execution_send_denied",
|
||||||
|
crate::violation_message(send_evaluation.violations.as_slice()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let signed = match prepared
|
||||||
|
.unsigned
|
||||||
|
.sign_after_simulation(&prepared.evidence, &[prepared.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 mut summary = prepared.summary;
|
||||||
|
let send_config = match kb_onchain_transport::SendTransactionConfig::from_execution_config(
|
||||||
|
&profile.execution,
|
||||||
|
std::option::Option::Some(summary.latest_blockhash.context.slot),
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let sent = match http_pool
|
||||||
|
.send_transaction_for_role(
|
||||||
|
request.transaction_role.as_str(),
|
||||||
|
signed.transaction_base64().as_str(),
|
||||||
|
&signature,
|
||||||
|
&send_config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
summary.send_result =
|
||||||
|
std::option::Option::Some(sent.to_execution_result(kb_lib::ExApiExecutionCluster::Devnet));
|
||||||
|
let confirmation_config =
|
||||||
|
match kb_onchain_transport::ConfirmTransactionConfig::from_execution_config(
|
||||||
|
&profile.execution,
|
||||||
|
std::option::Option::Some(summary.latest_blockhash.last_valid_block_height),
|
||||||
|
std::option::Option::Some(summary.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(
|
||||||
|
request.transaction_role.as_str(),
|
||||||
|
request.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),
|
||||||
|
};
|
||||||
|
let confirmed = matches!(
|
||||||
|
confirmation.status,
|
||||||
|
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||||
|
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||||
|
);
|
||||||
|
summary.confirmation = std::option::Option::Some(confirmation);
|
||||||
|
if confirmed {
|
||||||
|
summary.after = match read_snapshots(http_pool, &request.postcondition_reads).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let report = match kb_pipeline::inspect_solana_program_metadata_post_execution(
|
||||||
|
&kb_pipeline::SolanaProgramMetadataPostExecutionRequest {
|
||||||
|
operation: request.operation.clone(),
|
||||||
|
before: summary.before.clone(),
|
||||||
|
after: summary.after.clone(),
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if report.status == kb_pipeline::SolanaProgramMetadataPostconditionStatus::Contradicted {
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"execution_metadata_spm_postcondition_contradicted",
|
||||||
|
"confirmed Solana Program Metadata transaction contradicted its stateful postcondition",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
summary.post_execution = std::option::Option::Some(report);
|
||||||
|
if request.materialize_after_confirmation {
|
||||||
|
summary.materialized_snapshots =
|
||||||
|
materialize_confirmed_snapshots(summary.after.as_slice());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare_execution<O>(
|
||||||
|
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||||
|
profile: &kb_config::ProfileConfig,
|
||||||
|
workspace_root: &std::path::Path,
|
||||||
|
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
|
||||||
|
observer: &O,
|
||||||
|
) -> kb_core::Result<PreparedSolanaProgramMetadataExecution>
|
||||||
|
where
|
||||||
|
O: crate::SolanaExecutionObserver,
|
||||||
|
{
|
||||||
|
if let std::result::Result::Err(error) = request.validate() {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = validate_profile(profile, request) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) =
|
||||||
|
crate::ensure_not_cancelled(observer, "solana_program_metadata_validate")
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let genesis = match http_pool.get_genesis_hash_for_role(request.query_role.as_str()).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if genesis.classified_cluster
|
||||||
|
!= std::option::Option::Some(kb_lib::ExApiExecutionCluster::Devnet)
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"execution_cluster_mismatch",
|
||||||
|
format!(
|
||||||
|
"expected Devnet genesis hash but endpoint returned {} classified as {:?}",
|
||||||
|
genesis.genesis_hash, genesis.classified_cluster
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let wallet = 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 wallet_summary = wallet.summary();
|
||||||
|
let fee_payer = kb_lib::MdPubkey(wallet_summary.public_key.clone());
|
||||||
|
let balance = match http_pool
|
||||||
|
.get_balance_for_role(
|
||||||
|
request.query_role.as_str(),
|
||||||
|
&fee_payer,
|
||||||
|
&kb_onchain_transport::GetBalanceConfig::confirmed(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if balance.lamports < profile.execution.max_fee_lamports {
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"execution_balance_insufficient",
|
||||||
|
"Devnet wallet balance is below the configured fee ceiling",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let intent = build_intent(profile, request, fee_payer.clone());
|
||||||
|
let plan = match kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||||
|
&kb_lib::ExMetadataSolanaProgramMetadataExecutor,
|
||||||
|
&intent,
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
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(
|
||||||
|
"execution_metadata_spm_plan_denied",
|
||||||
|
crate::violation_message(plan_evaluation.violations.as_slice()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let before = match read_snapshots(http_pool, &request.preflight_reads).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let stateful_preflight = match kb_pipeline::inspect_solana_program_metadata_preflight(
|
||||||
|
&kb_pipeline::SolanaProgramMetadataPreflightRequest {
|
||||||
|
intent: intent.clone(),
|
||||||
|
plan: plan.clone(),
|
||||||
|
before: before.clone(),
|
||||||
|
rent_observations: request.rent_observations.clone(),
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let latest_blockhash = match http_pool
|
||||||
|
.get_latest_blockhash_for_role(
|
||||||
|
request.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),
|
||||||
|
};
|
||||||
|
if let std::result::Result::Err(error) = validate_profile_wallet_signers(
|
||||||
|
unsigned.required_signer_pubkeys(),
|
||||||
|
wallet_summary.public_key.as_str(),
|
||||||
|
) {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let fee = match http_pool
|
||||||
|
.get_fee_for_message_for_role(
|
||||||
|
request.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),
|
||||||
|
};
|
||||||
|
crate::emit(
|
||||||
|
observer,
|
||||||
|
crate::SolanaExecutionProgressLevel::Info,
|
||||||
|
"solana_program_metadata_simulation",
|
||||||
|
format!("simulating exact Solana Program Metadata message {}", unsigned.message_hash()),
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let simulation_rpc = match http_pool
|
||||||
|
.simulate_transaction_for_role(
|
||||||
|
request.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),
|
||||||
|
);
|
||||||
|
let readiness = match kb_pipeline::validate_solana_program_metadata_execution_readiness(
|
||||||
|
&kb_pipeline::SolanaProgramMetadataExecutionReadinessRequest {
|
||||||
|
plan: plan.clone(),
|
||||||
|
preflight: stateful_preflight.clone(),
|
||||||
|
message_hash: unsigned.message_hash().to_string(),
|
||||||
|
simulated_message_hash: unsigned.message_hash().to_string(),
|
||||||
|
simulated: true,
|
||||||
|
simulation_succeeded: simulation.success,
|
||||||
|
resolved_signers: vec![fee_payer],
|
||||||
|
submit: request.submit,
|
||||||
|
operator_confirmed: request.operator_confirmed,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
let simulation_json = match serde_json::to_string_pretty(&simulation) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => "<simulation serialization unavailable>".to_string(),
|
||||||
|
};
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
error.code(),
|
||||||
|
format!("{}; simulation={simulation_json}", error.message()),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let evidence = unsigned.bind_simulation(simulation.clone());
|
||||||
|
return std::result::Result::Ok(PreparedSolanaProgramMetadataExecution {
|
||||||
|
wallet,
|
||||||
|
unsigned,
|
||||||
|
evidence,
|
||||||
|
summary: crate::DevnetSolanaProgramMetadataExecutionSummary {
|
||||||
|
profile_name: profile.name.clone(),
|
||||||
|
cluster: kb_lib::ExApiExecutionCluster::Devnet,
|
||||||
|
genesis_hash: genesis.genesis_hash,
|
||||||
|
wallet: wallet_summary,
|
||||||
|
balance_lamports: balance.lamports,
|
||||||
|
before,
|
||||||
|
stateful_preflight,
|
||||||
|
plan,
|
||||||
|
latest_blockhash,
|
||||||
|
fee,
|
||||||
|
simulation,
|
||||||
|
readiness,
|
||||||
|
send_result: std::option::Option::None,
|
||||||
|
confirmation: std::option::Option::None,
|
||||||
|
after: std::vec::Vec::new(),
|
||||||
|
post_execution: std::option::Option::None,
|
||||||
|
materialized_snapshots: std::vec::Vec::new(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_intent(
|
||||||
|
profile: &kb_config::ProfileConfig,
|
||||||
|
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
|
||||||
|
fee_payer: kb_lib::MdPubkey,
|
||||||
|
) -> kb_lib::ExMetadataSpmExecutionIntent {
|
||||||
|
return kb_lib::ExMetadataSpmExecutionIntent {
|
||||||
|
intent_id: request.intent_id.clone(),
|
||||||
|
fee_payer: fee_payer.clone(),
|
||||||
|
policy: 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: vec![fee_payer],
|
||||||
|
dry_run: !request.submit,
|
||||||
|
post_execution_validation: kb_lib::ExApiPostExecutionValidationPolicy {
|
||||||
|
canonical_insert_required: true,
|
||||||
|
core_extraction_required: true,
|
||||||
|
decode_replay_required: true,
|
||||||
|
materialization_required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allow_destructive_operation: request.allow_destructive_operation,
|
||||||
|
operation: request.operation.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_snapshots(
|
||||||
|
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||||
|
requests: &[kb_pipeline::SolanaProgramMetadataStatefulReadRequest],
|
||||||
|
) -> kb_core::Result<std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadResult>> {
|
||||||
|
let mut results = std::vec::Vec::with_capacity(requests.len());
|
||||||
|
for request in requests {
|
||||||
|
let value =
|
||||||
|
match kb_pipeline::read_solana_program_metadata_stateful_snapshot(http_pool, request)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
results.push(value);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn materialize_confirmed_snapshots(
|
||||||
|
snapshots: &[kb_pipeline::SolanaProgramMetadataStatefulReadResult],
|
||||||
|
) -> std::vec::Vec<kb_lib::MtApiMaterializedOutput> {
|
||||||
|
return snapshots
|
||||||
|
.iter()
|
||||||
|
.filter_map(|result| return result.materialized_output.clone())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_profile(
|
||||||
|
profile: &kb_config::ProfileConfig,
|
||||||
|
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
|
||||||
|
) -> kb_core::Result<()> {
|
||||||
|
if profile.wallet.cluster != "devnet" {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata orchestration requires a Devnet wallet profile",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !profile.wallet.temporary_wallet_enabled || !profile.wallet.temporary_wallet_persist {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata orchestration requires an enabled persistent temporary wallet",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !profile.execution.require_simulation {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata orchestration requires simulation",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if request.submit && !profile.wallet.devnet_send_enabled {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Devnet transaction submission is disabled by the wallet profile",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if request.submit
|
||||||
|
&& profile.execution.require_operator_confirmation
|
||||||
|
&& !request.operator_confirmed
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata submission requires explicit operator confirmation",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_profile_wallet_signers(
|
||||||
|
required_signers: &[std::string::String],
|
||||||
|
wallet_pubkey: &str,
|
||||||
|
) -> kb_core::Result<()> {
|
||||||
|
if required_signers.iter().any(|value| return value.as_str() != wallet_pubkey) {
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"execution_metadata_spm_external_signer_unavailable",
|
||||||
|
format!(
|
||||||
|
"the Devnet Solana Program Metadata orchestrator can sign only with profile wallet {}; required signers are {}",
|
||||||
|
wallet_pubkey,
|
||||||
|
required_signers.join(",")
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
fn wallet() -> kb_lib::MdPubkey {
|
||||||
|
return kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([7_u8; 32]).to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_rejects_destructive_operations_without_approval() {
|
||||||
|
let wallet = wallet();
|
||||||
|
let mut request = crate::DevnetSolanaProgramMetadataExecutionRequest::new(
|
||||||
|
"write",
|
||||||
|
kb_lib::ExMetadataSpmOperation::Write {
|
||||||
|
buffer: wallet.clone(),
|
||||||
|
authority: wallet,
|
||||||
|
offset: 0,
|
||||||
|
source: kb_lib::ExMetadataSpmWriteSource::Inline { data: vec![1] },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
request
|
||||||
|
.preflight_reads
|
||||||
|
.push(kb_pipeline::SolanaProgramMetadataStatefulReadRequest {
|
||||||
|
query_role: "http_queries".to_string(),
|
||||||
|
account: request.operation_target_for_test(),
|
||||||
|
expected_state: kb_pipeline::SolanaProgramMetadataExpectedAccountState::Buffer,
|
||||||
|
min_context_slot: std::option::Option::None,
|
||||||
|
max_data_bytes: kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES,
|
||||||
|
});
|
||||||
|
assert!(request.validate().is_err());
|
||||||
|
request.allow_destructive_operation = true;
|
||||||
|
assert!(request.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
trait OperationTargetForTest {
|
||||||
|
fn operation_target_for_test(&self) -> kb_lib::MdPubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OperationTargetForTest for crate::DevnetSolanaProgramMetadataExecutionRequest {
|
||||||
|
fn operation_target_for_test(&self) -> kb_lib::MdPubkey {
|
||||||
|
return match &self.operation {
|
||||||
|
kb_lib::ExMetadataSpmOperation::Write { buffer, .. } => buffer.clone(),
|
||||||
|
_ => kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,949 @@
|
|||||||
|
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_fixture.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
//! Native Solana Program Metadata fixture preparation for Devnet journeys.
|
||||||
|
|
||||||
|
/// Bytes reserved for the public Buffer journey before `Trim`.
|
||||||
|
pub const SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES: u16 = 128;
|
||||||
|
|
||||||
|
/// Options used to prepare one complete Solana Program Metadata fixture.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct SolanaProgramMetadataFixturePreparationOptions {
|
||||||
|
/// Endpoint role used for account, rent, balance, blockhash and fee calls.
|
||||||
|
pub query_role: std::string::String,
|
||||||
|
/// Endpoint role used for simulation, submission and confirmation.
|
||||||
|
pub transaction_role: std::string::String,
|
||||||
|
/// Explicit operator confirmation for the two prefunding transfers.
|
||||||
|
pub operator_confirmed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::SolanaProgramMetadataFixturePreparationOptions {
|
||||||
|
/// Creates conservative defaults matching the Devnet execution profiles.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
return Self {
|
||||||
|
query_role: "http_queries".to_string(),
|
||||||
|
transaction_role: "http_transactions".to_string(),
|
||||||
|
operator_confirmed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::default::Default for crate::SolanaProgramMetadataFixturePreparationOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
return Self::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One prepared executable step in a Solana Program Metadata journey.
|
||||||
|
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SolanaProgramMetadataPreparedStep {
|
||||||
|
/// Stable scenario identifier.
|
||||||
|
pub scenario_id: std::string::String,
|
||||||
|
/// Stable scenario-step identifier.
|
||||||
|
pub step_id: std::string::String,
|
||||||
|
/// Zero-based step position inside the scenario.
|
||||||
|
pub step_index: usize,
|
||||||
|
/// Exact typed operation.
|
||||||
|
pub operation: kb_lib::ExMetadataSpmOperation,
|
||||||
|
/// Bounded state reads required before execution.
|
||||||
|
pub preflight_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||||
|
/// Explicit rent observations required by the operation.
|
||||||
|
pub rent_observations: std::vec::Vec<kb_pipeline::SolanaProgramMetadataRentObservation>,
|
||||||
|
/// Bounded state reads required after confirmation.
|
||||||
|
pub postcondition_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||||
|
/// Whether explicit destructive approval is required.
|
||||||
|
pub allow_destructive_operation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::SolanaProgramMetadataPreparedStep {
|
||||||
|
/// Builds one simulation-only execution request from the prepared fixture step.
|
||||||
|
pub fn execution_request(
|
||||||
|
&self,
|
||||||
|
intent_id: impl std::convert::Into<std::string::String>,
|
||||||
|
) -> crate::DevnetSolanaProgramMetadataExecutionRequest {
|
||||||
|
let mut request = crate::DevnetSolanaProgramMetadataExecutionRequest::new(
|
||||||
|
intent_id,
|
||||||
|
self.operation.clone(),
|
||||||
|
);
|
||||||
|
request.preflight_reads = self.preflight_reads.clone();
|
||||||
|
request.rent_observations = self.rent_observations.clone();
|
||||||
|
request.postcondition_reads = self.postcondition_reads.clone();
|
||||||
|
request.allow_destructive_operation = self.allow_destructive_operation;
|
||||||
|
request.materialize_after_confirmation = true;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Public fixture values for the two Solana Program Metadata journeys.
|
||||||
|
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SolanaProgramMetadataFixturePreparationSummary {
|
||||||
|
/// Profile used by the fixture preparation.
|
||||||
|
pub profile_name: std::string::String,
|
||||||
|
/// Profile-wallet authority.
|
||||||
|
pub authority: std::string::String,
|
||||||
|
/// Executable program described by the non-canonical metadata PDA.
|
||||||
|
pub described_program: std::string::String,
|
||||||
|
/// Exact 16-byte Buffer seed as UTF-8 text.
|
||||||
|
pub buffer_seed: std::string::String,
|
||||||
|
/// Non-canonical Buffer PDA.
|
||||||
|
pub buffer: std::string::String,
|
||||||
|
/// Lamports transferred to pre-fund the Buffer PDA.
|
||||||
|
pub buffer_prefund_lamports: u64,
|
||||||
|
/// Confirmed Buffer prefunding signature.
|
||||||
|
pub buffer_prefund_signature: std::string::String,
|
||||||
|
/// Exact 16-byte Metadata seed as UTF-8 text.
|
||||||
|
pub metadata_seed: std::string::String,
|
||||||
|
/// Non-canonical Metadata PDA.
|
||||||
|
pub metadata: std::string::String,
|
||||||
|
/// Lamports transferred to pre-fund the Metadata PDA.
|
||||||
|
pub metadata_prefund_lamports: u64,
|
||||||
|
/// Confirmed Metadata prefunding signature.
|
||||||
|
pub metadata_prefund_signature: std::string::String,
|
||||||
|
/// Ordered executable steps for both journeys.
|
||||||
|
pub prepared_steps: std::vec::Vec<crate::SolanaProgramMetadataPreparedStep>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates two unique pre-funded PDAs and the nine typed journey steps.
|
||||||
|
pub async fn prepare_solana_program_metadata_fixture(
|
||||||
|
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||||
|
profile: &kb_config::ProfileConfig,
|
||||||
|
workspace_root: &std::path::Path,
|
||||||
|
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
|
||||||
|
) -> kb_core::Result<crate::SolanaProgramMetadataFixturePreparationSummary> {
|
||||||
|
if options.query_role.trim().is_empty() || options.transaction_role.trim().is_empty() {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata fixture endpoint roles must not be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if profile.wallet.cluster != "devnet"
|
||||||
|
|| !profile.wallet.devnet_send_enabled
|
||||||
|
|| !profile.wallet.temporary_wallet_enabled
|
||||||
|
|| !profile.wallet.temporary_wallet_persist
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata fixture requires one persistent send-enabled Devnet profile",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !options.operator_confirmed {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata fixture prefunding requires explicit operator confirmation",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let genesis = match http_pool.get_genesis_hash_for_role(options.query_role.as_str()).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if genesis.classified_cluster
|
||||||
|
!= std::option::Option::Some(kb_lib::ExApiExecutionCluster::Devnet)
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"execution_cluster_mismatch",
|
||||||
|
"Solana Program Metadata fixture preparation requires a Devnet endpoint",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
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 authority = kb_lib::MdPubkey(operator.public_key());
|
||||||
|
let described_program = kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string());
|
||||||
|
let unique = uuid::Uuid::new_v4().simple().to_string();
|
||||||
|
let buffer_seed = format!("buf{}", &unique[0..13]);
|
||||||
|
let metadata_seed = format!("met{}", &unique[13..26]);
|
||||||
|
let buffer = match kb_lib::executor_metadata_solana_program_metadata_derive_non_canonical_pda(
|
||||||
|
&described_program,
|
||||||
|
&authority,
|
||||||
|
buffer_seed.as_bytes(),
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let metadata = match kb_lib::executor_metadata_solana_program_metadata_derive_non_canonical_pda(
|
||||||
|
&described_program,
|
||||||
|
&authority,
|
||||||
|
metadata_seed.as_bytes(),
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if let std::result::Result::Err(error) = ensure_fixture_addresses_are_missing(
|
||||||
|
http_pool,
|
||||||
|
options,
|
||||||
|
&[buffer.clone(), metadata.clone()],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let buffer_header = match u64::try_from(kb_lib::DC_METADATA_SPM_BUFFER_HEADER_BYTES) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(error.to_string()));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let metadata_header = match u64::try_from(kb_lib::DC_METADATA_SPM_METADATA_HEADER_BYTES) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(error.to_string()));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let buffer_target_space = buffer_header
|
||||||
|
.saturating_add(u64::from(crate::SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES));
|
||||||
|
let initial_metadata_bytes = br#"{"name":"Khadhroony Program Metadata","version":1}"#.to_vec();
|
||||||
|
let updated_metadata_bytes =
|
||||||
|
br#"{"name":"Khadhroony Program Metadata","version":2,"validated":true}"#.to_vec();
|
||||||
|
let initial_metadata_data_len = match u64::try_from(initial_metadata_bytes.len()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(error.to_string()));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let updated_metadata_data_len = match u64::try_from(updated_metadata_bytes.len()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(error.to_string()));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let initial_metadata_space = metadata_header.saturating_add(initial_metadata_data_len);
|
||||||
|
let updated_metadata_space = metadata_header.saturating_add(updated_metadata_data_len);
|
||||||
|
let buffer_header_rent =
|
||||||
|
match rent_for_space(http_pool, options.query_role.as_str(), buffer_header).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let buffer_target_rent =
|
||||||
|
match rent_for_space(http_pool, options.query_role.as_str(), buffer_target_space).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let initial_metadata_rent = match rent_for_space(
|
||||||
|
http_pool,
|
||||||
|
options.query_role.as_str(),
|
||||||
|
initial_metadata_space,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let updated_metadata_rent = match rent_for_space(
|
||||||
|
http_pool,
|
||||||
|
options.query_role.as_str(),
|
||||||
|
updated_metadata_space,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let buffer_prefund_lamports = buffer_target_rent.max(buffer_header_rent);
|
||||||
|
let metadata_prefund_lamports = updated_metadata_rent.max(initial_metadata_rent);
|
||||||
|
let total =
|
||||||
|
match buffer_prefund_lamports
|
||||||
|
.checked_add(metadata_prefund_lamports)
|
||||||
|
.and_then(|value| {
|
||||||
|
return value.checked_add(
|
||||||
|
profile
|
||||||
|
.execution
|
||||||
|
.max_fee_lamports
|
||||||
|
.saturating_mul(crate::SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT),
|
||||||
|
);
|
||||||
|
}) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => {
|
||||||
|
return std::result::Result::Err(kb_core::Error::config(
|
||||||
|
"Solana Program Metadata fixture funding requirement overflowed u64",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let balance = match http_pool
|
||||||
|
.get_balance_for_role(
|
||||||
|
options.query_role.as_str(),
|
||||||
|
&authority,
|
||||||
|
&kb_onchain_transport::GetBalanceConfig::confirmed(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if balance.lamports < total {
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"solana_program_metadata_fixture_balance_insufficient",
|
||||||
|
format!(
|
||||||
|
"Devnet fixture wallet has {} lamports but requires at least {}; fund or airdrop the profile wallet first",
|
||||||
|
balance.lamports, total
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let buffer_prefund_signature = match submit_prefund_transfer(
|
||||||
|
http_pool,
|
||||||
|
profile,
|
||||||
|
options,
|
||||||
|
&operator,
|
||||||
|
buffer.clone(),
|
||||||
|
buffer_prefund_lamports,
|
||||||
|
"buffer",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let metadata_prefund_signature = match submit_prefund_transfer(
|
||||||
|
http_pool,
|
||||||
|
profile,
|
||||||
|
options,
|
||||||
|
&operator,
|
||||||
|
metadata.clone(),
|
||||||
|
metadata_prefund_lamports,
|
||||||
|
"metadata",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let prepared_steps = prepared_steps(
|
||||||
|
options.query_role.as_str(),
|
||||||
|
authority.clone(),
|
||||||
|
described_program.clone(),
|
||||||
|
buffer.clone(),
|
||||||
|
buffer_seed.as_bytes(),
|
||||||
|
metadata.clone(),
|
||||||
|
metadata_seed.as_bytes(),
|
||||||
|
initial_metadata_bytes,
|
||||||
|
updated_metadata_bytes,
|
||||||
|
buffer_prefund_lamports,
|
||||||
|
buffer_header,
|
||||||
|
buffer_header_rent,
|
||||||
|
buffer_target_space,
|
||||||
|
buffer_target_rent,
|
||||||
|
metadata_prefund_lamports,
|
||||||
|
initial_metadata_space,
|
||||||
|
initial_metadata_rent,
|
||||||
|
updated_metadata_space,
|
||||||
|
updated_metadata_rent,
|
||||||
|
);
|
||||||
|
tracing::info!(
|
||||||
|
target: crate::TRACING_TARGET_METADATA_SOLANA_PROGRAM_FIXTURE,
|
||||||
|
action = "prepare_solana_program_metadata_fixture",
|
||||||
|
profile = profile.name.as_str(),
|
||||||
|
authority = authority.0.as_str(),
|
||||||
|
buffer = buffer.0.as_str(),
|
||||||
|
metadata = metadata.0.as_str(),
|
||||||
|
"prepared pre-funded Solana Program Metadata Devnet fixture"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(crate::SolanaProgramMetadataFixturePreparationSummary {
|
||||||
|
profile_name: profile.name.clone(),
|
||||||
|
authority: authority.0,
|
||||||
|
described_program: described_program.0,
|
||||||
|
buffer_seed,
|
||||||
|
buffer: buffer.0,
|
||||||
|
buffer_prefund_lamports,
|
||||||
|
buffer_prefund_signature,
|
||||||
|
metadata_seed,
|
||||||
|
metadata: metadata.0,
|
||||||
|
metadata_prefund_lamports,
|
||||||
|
metadata_prefund_signature,
|
||||||
|
prepared_steps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn prepared_steps(
|
||||||
|
query_role: &str,
|
||||||
|
authority: kb_lib::MdPubkey,
|
||||||
|
described_program: kb_lib::MdPubkey,
|
||||||
|
buffer: kb_lib::MdPubkey,
|
||||||
|
buffer_seed: &[u8],
|
||||||
|
metadata: kb_lib::MdPubkey,
|
||||||
|
metadata_seed: &[u8],
|
||||||
|
initial_metadata_bytes: std::vec::Vec<u8>,
|
||||||
|
updated_metadata_bytes: std::vec::Vec<u8>,
|
||||||
|
buffer_observed_lamports: u64,
|
||||||
|
buffer_header_space: u64,
|
||||||
|
buffer_header_rent: u64,
|
||||||
|
buffer_target_space: u64,
|
||||||
|
buffer_target_rent: u64,
|
||||||
|
metadata_observed_lamports: u64,
|
||||||
|
initial_metadata_space: u64,
|
||||||
|
initial_metadata_rent: u64,
|
||||||
|
updated_metadata_space: u64,
|
||||||
|
updated_metadata_rent: u64,
|
||||||
|
) -> std::vec::Vec<crate::SolanaProgramMetadataPreparedStep> {
|
||||||
|
let context = kb_lib::ExMetadataSpmProgramContext {
|
||||||
|
program: described_program.clone(),
|
||||||
|
program_data: std::option::Option::None,
|
||||||
|
};
|
||||||
|
let buffer_vacant = read(
|
||||||
|
query_role,
|
||||||
|
buffer.clone(),
|
||||||
|
kb_pipeline::SolanaProgramMetadataExpectedAccountState::Vacant,
|
||||||
|
);
|
||||||
|
let buffer_state = read(
|
||||||
|
query_role,
|
||||||
|
buffer.clone(),
|
||||||
|
kb_pipeline::SolanaProgramMetadataExpectedAccountState::Buffer,
|
||||||
|
);
|
||||||
|
let buffer_missing = read(
|
||||||
|
query_role,
|
||||||
|
buffer.clone(),
|
||||||
|
kb_pipeline::SolanaProgramMetadataExpectedAccountState::Missing,
|
||||||
|
);
|
||||||
|
let metadata_vacant = read(
|
||||||
|
query_role,
|
||||||
|
metadata.clone(),
|
||||||
|
kb_pipeline::SolanaProgramMetadataExpectedAccountState::Vacant,
|
||||||
|
);
|
||||||
|
let metadata_state = read(
|
||||||
|
query_role,
|
||||||
|
metadata.clone(),
|
||||||
|
kb_pipeline::SolanaProgramMetadataExpectedAccountState::Metadata,
|
||||||
|
);
|
||||||
|
return vec![
|
||||||
|
prepared_step(
|
||||||
|
"spm_buffer_lifecycle",
|
||||||
|
"spm_buffer_allocate",
|
||||||
|
0,
|
||||||
|
kb_lib::ExMetadataSpmOperation::Allocate {
|
||||||
|
buffer: buffer.clone(),
|
||||||
|
authority: authority.clone(),
|
||||||
|
seed: std::option::Option::Some(buffer_seed.to_vec()),
|
||||||
|
program_context: std::option::Option::Some(context.clone()),
|
||||||
|
canonical: false,
|
||||||
|
allocate_account: true,
|
||||||
|
},
|
||||||
|
vec![buffer_vacant],
|
||||||
|
vec![rent(
|
||||||
|
buffer.clone(),
|
||||||
|
buffer_header_space,
|
||||||
|
buffer_observed_lamports,
|
||||||
|
buffer_header_rent,
|
||||||
|
)],
|
||||||
|
vec![buffer_state.clone()],
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
prepared_step(
|
||||||
|
"spm_buffer_lifecycle",
|
||||||
|
"spm_buffer_extend",
|
||||||
|
1,
|
||||||
|
kb_lib::ExMetadataSpmOperation::Extend {
|
||||||
|
account: buffer.clone(),
|
||||||
|
authority: authority.clone(),
|
||||||
|
length: crate::SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES,
|
||||||
|
program_context: std::option::Option::None,
|
||||||
|
},
|
||||||
|
vec![buffer_state.clone()],
|
||||||
|
vec![rent(
|
||||||
|
buffer.clone(),
|
||||||
|
buffer_target_space,
|
||||||
|
buffer_observed_lamports,
|
||||||
|
buffer_target_rent,
|
||||||
|
)],
|
||||||
|
vec![buffer_state.clone()],
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
prepared_step(
|
||||||
|
"spm_buffer_lifecycle",
|
||||||
|
"spm_buffer_write",
|
||||||
|
2,
|
||||||
|
kb_lib::ExMetadataSpmOperation::Write {
|
||||||
|
buffer: buffer.clone(),
|
||||||
|
authority: authority.clone(),
|
||||||
|
offset: 0,
|
||||||
|
source: kb_lib::ExMetadataSpmWriteSource::Inline {
|
||||||
|
data: b"Khadhroony Solana Program Metadata Buffer fixture".to_vec(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vec![buffer_state.clone()],
|
||||||
|
std::vec::Vec::new(),
|
||||||
|
vec![buffer_state.clone()],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
prepared_step(
|
||||||
|
"spm_buffer_lifecycle",
|
||||||
|
"spm_buffer_trim",
|
||||||
|
3,
|
||||||
|
kb_lib::ExMetadataSpmOperation::Trim {
|
||||||
|
account: buffer.clone(),
|
||||||
|
authority: authority.clone(),
|
||||||
|
destination: authority.clone(),
|
||||||
|
program_context: std::option::Option::None,
|
||||||
|
},
|
||||||
|
vec![buffer_state.clone()],
|
||||||
|
std::vec::Vec::new(),
|
||||||
|
vec![buffer_state.clone()],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
prepared_step(
|
||||||
|
"spm_buffer_lifecycle",
|
||||||
|
"spm_buffer_close",
|
||||||
|
4,
|
||||||
|
kb_lib::ExMetadataSpmOperation::Close {
|
||||||
|
account: buffer.clone(),
|
||||||
|
authority: authority.clone(),
|
||||||
|
destination: authority.clone(),
|
||||||
|
program_context: std::option::Option::None,
|
||||||
|
},
|
||||||
|
vec![buffer_state.clone()],
|
||||||
|
std::vec::Vec::new(),
|
||||||
|
vec![buffer_missing],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
prepared_step(
|
||||||
|
"spm_metadata_lifecycle",
|
||||||
|
"spm_metadata_initialize",
|
||||||
|
0,
|
||||||
|
kb_lib::ExMetadataSpmOperation::Initialize {
|
||||||
|
metadata: metadata.clone(),
|
||||||
|
authority: authority.clone(),
|
||||||
|
program: described_program,
|
||||||
|
program_data: std::option::Option::None,
|
||||||
|
canonical: false,
|
||||||
|
seed: metadata_seed.to_vec(),
|
||||||
|
encoding: kb_lib::ExMetadataSpmEncoding::Utf8,
|
||||||
|
compression: kb_lib::ExMetadataSpmCompression::None,
|
||||||
|
format: kb_lib::ExMetadataSpmFormat::Json,
|
||||||
|
data_source: kb_lib::ExMetadataSpmDataSource::Direct,
|
||||||
|
data: std::option::Option::Some(kb_lib::ExMetadataSpmDataInput::Direct {
|
||||||
|
bytes: initial_metadata_bytes,
|
||||||
|
}),
|
||||||
|
allocate_account: true,
|
||||||
|
},
|
||||||
|
vec![metadata_vacant],
|
||||||
|
vec![rent(
|
||||||
|
metadata.clone(),
|
||||||
|
initial_metadata_space,
|
||||||
|
metadata_observed_lamports,
|
||||||
|
initial_metadata_rent,
|
||||||
|
)],
|
||||||
|
vec![metadata_state.clone()],
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
prepared_step(
|
||||||
|
"spm_metadata_lifecycle",
|
||||||
|
"spm_metadata_set_data",
|
||||||
|
1,
|
||||||
|
kb_lib::ExMetadataSpmOperation::SetData {
|
||||||
|
metadata: metadata.clone(),
|
||||||
|
authority: authority.clone(),
|
||||||
|
encoding: kb_lib::ExMetadataSpmEncoding::Utf8,
|
||||||
|
compression: kb_lib::ExMetadataSpmCompression::None,
|
||||||
|
format: kb_lib::ExMetadataSpmFormat::Json,
|
||||||
|
source: kb_lib::ExMetadataSpmSetDataSource::Inline {
|
||||||
|
data: kb_lib::ExMetadataSpmDataInput::Direct { bytes: updated_metadata_bytes },
|
||||||
|
},
|
||||||
|
program_context: std::option::Option::None,
|
||||||
|
},
|
||||||
|
vec![metadata_state.clone()],
|
||||||
|
vec![rent(
|
||||||
|
metadata.clone(),
|
||||||
|
updated_metadata_space,
|
||||||
|
metadata_observed_lamports,
|
||||||
|
updated_metadata_rent,
|
||||||
|
)],
|
||||||
|
vec![metadata_state.clone()],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
prepared_step(
|
||||||
|
"spm_metadata_lifecycle",
|
||||||
|
"spm_metadata_set_authority",
|
||||||
|
2,
|
||||||
|
kb_lib::ExMetadataSpmOperation::SetAuthority {
|
||||||
|
account: metadata.clone(),
|
||||||
|
authority: authority.clone(),
|
||||||
|
new_authority: std::option::Option::Some(authority.clone()),
|
||||||
|
program_context: std::option::Option::None,
|
||||||
|
},
|
||||||
|
vec![metadata_state.clone()],
|
||||||
|
std::vec::Vec::new(),
|
||||||
|
vec![metadata_state.clone()],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
prepared_step(
|
||||||
|
"spm_metadata_lifecycle",
|
||||||
|
"spm_metadata_set_immutable",
|
||||||
|
3,
|
||||||
|
kb_lib::ExMetadataSpmOperation::SetImmutable {
|
||||||
|
metadata: metadata.clone(),
|
||||||
|
authority,
|
||||||
|
program_context: std::option::Option::None,
|
||||||
|
},
|
||||||
|
vec![metadata_state.clone()],
|
||||||
|
std::vec::Vec::new(),
|
||||||
|
vec![metadata_state],
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepared_step(
|
||||||
|
scenario_id: &str,
|
||||||
|
step_id: &str,
|
||||||
|
step_index: usize,
|
||||||
|
operation: kb_lib::ExMetadataSpmOperation,
|
||||||
|
preflight_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||||
|
rent_observations: std::vec::Vec<kb_pipeline::SolanaProgramMetadataRentObservation>,
|
||||||
|
postcondition_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||||
|
allow_destructive_operation: bool,
|
||||||
|
) -> crate::SolanaProgramMetadataPreparedStep {
|
||||||
|
return crate::SolanaProgramMetadataPreparedStep {
|
||||||
|
scenario_id: scenario_id.to_string(),
|
||||||
|
step_id: step_id.to_string(),
|
||||||
|
step_index,
|
||||||
|
operation,
|
||||||
|
preflight_reads,
|
||||||
|
rent_observations,
|
||||||
|
postcondition_reads,
|
||||||
|
allow_destructive_operation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(
|
||||||
|
query_role: &str,
|
||||||
|
account: kb_lib::MdPubkey,
|
||||||
|
expected_state: kb_pipeline::SolanaProgramMetadataExpectedAccountState,
|
||||||
|
) -> kb_pipeline::SolanaProgramMetadataStatefulReadRequest {
|
||||||
|
return kb_pipeline::SolanaProgramMetadataStatefulReadRequest {
|
||||||
|
query_role: query_role.to_string(),
|
||||||
|
account,
|
||||||
|
expected_state,
|
||||||
|
min_context_slot: std::option::Option::None,
|
||||||
|
max_data_bytes: kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rent(
|
||||||
|
account: kb_lib::MdPubkey,
|
||||||
|
target_space: u64,
|
||||||
|
observed_lamports: u64,
|
||||||
|
required_lamports: u64,
|
||||||
|
) -> kb_pipeline::SolanaProgramMetadataRentObservation {
|
||||||
|
return kb_pipeline::SolanaProgramMetadataRentObservation {
|
||||||
|
account,
|
||||||
|
target_space,
|
||||||
|
observed_lamports,
|
||||||
|
required_lamports,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_fixture_addresses_are_missing(
|
||||||
|
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||||
|
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
|
||||||
|
accounts: &[kb_lib::MdPubkey],
|
||||||
|
) -> kb_core::Result<()> {
|
||||||
|
for account in accounts {
|
||||||
|
let value = match http_pool
|
||||||
|
.get_account_info_for_role(
|
||||||
|
options.query_role.as_str(),
|
||||||
|
account,
|
||||||
|
&kb_onchain_transport::GetAccountInfoConfig::confirmed(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if value.account.is_some() {
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"solana_program_metadata_fixture_address_collision",
|
||||||
|
format!("generated fixture address {} already exists", account.0),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rent_for_space(
|
||||||
|
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||||
|
query_role: &str,
|
||||||
|
space: u64,
|
||||||
|
) -> kb_core::Result<u64> {
|
||||||
|
let result = match http_pool
|
||||||
|
.get_minimum_balance_for_rent_exemption_for_role(
|
||||||
|
query_role,
|
||||||
|
space,
|
||||||
|
&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(result.minimum_balance_lamports);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn submit_prefund_transfer(
|
||||||
|
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||||
|
profile: &kb_config::ProfileConfig,
|
||||||
|
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
|
||||||
|
operator: &kb_wallet::TemporaryWallet,
|
||||||
|
recipient: kb_lib::MdPubkey,
|
||||||
|
lamports: u64,
|
||||||
|
label: &str,
|
||||||
|
) -> kb_core::Result<std::string::String> {
|
||||||
|
let payer = kb_lib::MdPubkey(operator.public_key());
|
||||||
|
let policy = 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: vec![payer.clone()],
|
||||||
|
dry_run: false,
|
||||||
|
post_execution_validation: kb_lib::ExApiPostExecutionValidationPolicy {
|
||||||
|
canonical_insert_required: false,
|
||||||
|
core_extraction_required: false,
|
||||||
|
decode_replay_required: false,
|
||||||
|
materialization_required: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let intent = kb_lib::ExSolanaCoreExecutionIntent {
|
||||||
|
intent_id: format!("spm-fixture-prefund-{label}-{}", recipient.0),
|
||||||
|
fee_payer: payer.clone(),
|
||||||
|
policy,
|
||||||
|
operation: kb_lib::ExSolanaCoreOperation::SystemTransfer {
|
||||||
|
from: payer,
|
||||||
|
to: recipient,
|
||||||
|
lamports,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let plan = match kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||||
|
&kb_lib::ExSolanaCoreExecutor,
|
||||||
|
&intent,
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
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(
|
||||||
|
"solana_program_metadata_fixture_prefund_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(
|
||||||
|
"solana_program_metadata_fixture_prefund_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(
|
||||||
|
"solana_program_metadata_fixture_prefund_send_denied",
|
||||||
|
crate::violation_message(send_evaluation.violations.as_slice()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let evidence = unsigned.bind_simulation(simulation.clone());
|
||||||
|
let signed = match unsigned.sign_after_simulation(&evidence, &[operator.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(
|
||||||
|
"solana_program_metadata_fixture_prefund_confirmation_failed",
|
||||||
|
format!("prefunding transaction stopped at {:?}", confirmation.status),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(signature.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn fixture_seed_shapes_and_journey_inventory_are_exact() {
|
||||||
|
let unique = uuid::Uuid::nil().simple().to_string();
|
||||||
|
let buffer_seed = format!("buf{}", &unique[0..13]);
|
||||||
|
let metadata_seed = format!("met{}", &unique[13..26]);
|
||||||
|
assert_eq!(buffer_seed.len(), kb_lib::DC_METADATA_SPM_SEED_BYTES);
|
||||||
|
assert_eq!(metadata_seed.len(), kb_lib::DC_METADATA_SPM_SEED_BYTES);
|
||||||
|
let authority =
|
||||||
|
kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([1_u8; 32]).to_string());
|
||||||
|
let buffer =
|
||||||
|
kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([2_u8; 32]).to_string());
|
||||||
|
let metadata =
|
||||||
|
kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([3_u8; 32]).to_string());
|
||||||
|
let steps = super::prepared_steps(
|
||||||
|
"custom_queries",
|
||||||
|
authority,
|
||||||
|
kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()),
|
||||||
|
buffer,
|
||||||
|
buffer_seed.as_bytes(),
|
||||||
|
metadata,
|
||||||
|
metadata_seed.as_bytes(),
|
||||||
|
br#"{"version":1}"#.to_vec(),
|
||||||
|
br#"{"version":2}"#.to_vec(),
|
||||||
|
1_000_000,
|
||||||
|
96,
|
||||||
|
1_000_000,
|
||||||
|
224,
|
||||||
|
1_000_000,
|
||||||
|
1_000_000,
|
||||||
|
109,
|
||||||
|
1_000_000,
|
||||||
|
109,
|
||||||
|
1_000_000,
|
||||||
|
);
|
||||||
|
let actual = steps
|
||||||
|
.iter()
|
||||||
|
.map(|step| return step.operation.operation_code())
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
let scenarios = crate::solana_program_metadata_devnet_scenarios();
|
||||||
|
let expected = scenarios
|
||||||
|
.iter()
|
||||||
|
.flat_map(|scenario| return scenario.steps.iter())
|
||||||
|
.map(|step| return step.operation_code.as_str())
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
assert!(steps.iter().all(|step| {
|
||||||
|
return step
|
||||||
|
.preflight_reads
|
||||||
|
.iter()
|
||||||
|
.chain(step.postcondition_reads.iter())
|
||||||
|
.all(|read| return read.query_role == "custom_queries");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_scenarios.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Coherent Solana Program Metadata journeys for Devnet validation.
|
||||||
|
|
||||||
|
/// Stable lifecycle state consumed or produced by one scenario step.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SolanaProgramMetadataFixtureState {
|
||||||
|
/// Pre-funded but uninitialized PDA.
|
||||||
|
Prefunded,
|
||||||
|
/// Initialized Buffer account.
|
||||||
|
Buffer,
|
||||||
|
/// Initialized mutable Metadata account.
|
||||||
|
MutableMetadata,
|
||||||
|
/// Initialized immutable Metadata account.
|
||||||
|
ImmutableMetadata,
|
||||||
|
/// Closed account.
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One stable step in a Solana Program Metadata Devnet journey.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SolanaProgramMetadataScenarioStep {
|
||||||
|
/// Stable scenario-step identifier.
|
||||||
|
pub id: std::string::String,
|
||||||
|
/// Operator-visible label.
|
||||||
|
pub label: std::string::String,
|
||||||
|
/// Stable executor operation code.
|
||||||
|
pub operation_code: std::string::String,
|
||||||
|
/// Expected state before the operation.
|
||||||
|
pub initial_state: crate::SolanaProgramMetadataFixtureState,
|
||||||
|
/// Expected state after confirmed execution.
|
||||||
|
pub resulting_state: crate::SolanaProgramMetadataFixtureState,
|
||||||
|
/// Whether the operation requires explicit destructive approval.
|
||||||
|
pub requires_explicit_approval: bool,
|
||||||
|
/// Required evidence kinds.
|
||||||
|
pub required_evidence: std::vec::Vec<std::string::String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One ordered multi-step Solana Program Metadata journey.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SolanaProgramMetadataScenario {
|
||||||
|
/// Stable scenario identifier.
|
||||||
|
pub id: std::string::String,
|
||||||
|
/// Operator-visible label.
|
||||||
|
pub label: std::string::String,
|
||||||
|
/// Fixture account selected by the journey.
|
||||||
|
pub fixture_account: std::string::String,
|
||||||
|
/// Ordered scenario steps.
|
||||||
|
pub steps: std::vec::Vec<crate::SolanaProgramMetadataScenarioStep>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the two ordered Devnet journeys covering all nine stable operations.
|
||||||
|
pub fn solana_program_metadata_devnet_scenarios()
|
||||||
|
-> std::vec::Vec<crate::SolanaProgramMetadataScenario> {
|
||||||
|
return vec![
|
||||||
|
crate::SolanaProgramMetadataScenario {
|
||||||
|
id: "spm_buffer_lifecycle".to_string(),
|
||||||
|
label: "Buffer : allocation, écriture, extension, trim et fermeture".to_string(),
|
||||||
|
fixture_account: "buffer".to_string(),
|
||||||
|
steps: vec![
|
||||||
|
step(
|
||||||
|
"spm_buffer_allocate",
|
||||||
|
"Allouer le Buffer préfinancé",
|
||||||
|
kb_lib::EX_METADATA_SPM_ALLOCATE_OPERATION,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Prefunded,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
step(
|
||||||
|
"spm_buffer_extend",
|
||||||
|
"Étendre la capacité du Buffer",
|
||||||
|
kb_lib::EX_METADATA_SPM_EXTEND_OPERATION,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
step(
|
||||||
|
"spm_buffer_write",
|
||||||
|
"Écrire les données inline",
|
||||||
|
kb_lib::EX_METADATA_SPM_WRITE_OPERATION,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
step(
|
||||||
|
"spm_buffer_trim",
|
||||||
|
"Réduire le Buffer et restituer l’excédent",
|
||||||
|
kb_lib::EX_METADATA_SPM_TRIM_OPERATION,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
step(
|
||||||
|
"spm_buffer_close",
|
||||||
|
"Fermer le Buffer",
|
||||||
|
kb_lib::EX_METADATA_SPM_CLOSE_OPERATION,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Closed,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
crate::SolanaProgramMetadataScenario {
|
||||||
|
id: "spm_metadata_lifecycle".to_string(),
|
||||||
|
label: "Metadata : initialisation, données, autorité et immutabilité".to_string(),
|
||||||
|
fixture_account: "metadata".to_string(),
|
||||||
|
steps: vec![
|
||||||
|
step(
|
||||||
|
"spm_metadata_initialize",
|
||||||
|
"Initialiser les metadata non-canoniques",
|
||||||
|
kb_lib::EX_METADATA_SPM_INITIALIZE_OPERATION,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Prefunded,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
step(
|
||||||
|
"spm_metadata_set_data",
|
||||||
|
"Mettre à jour les données JSON",
|
||||||
|
kb_lib::EX_METADATA_SPM_SET_DATA_OPERATION,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
step(
|
||||||
|
"spm_metadata_set_authority",
|
||||||
|
"Réaffirmer l’autorité du wallet de profil",
|
||||||
|
kb_lib::EX_METADATA_SPM_SET_AUTHORITY_OPERATION,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
step(
|
||||||
|
"spm_metadata_set_immutable",
|
||||||
|
"Rendre les metadata immuables",
|
||||||
|
kb_lib::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||||
|
crate::SolanaProgramMetadataFixtureState::ImmutableMetadata,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step(
|
||||||
|
id: &str,
|
||||||
|
label: &str,
|
||||||
|
operation_code: &str,
|
||||||
|
initial_state: crate::SolanaProgramMetadataFixtureState,
|
||||||
|
resulting_state: crate::SolanaProgramMetadataFixtureState,
|
||||||
|
requires_explicit_approval: bool,
|
||||||
|
) -> crate::SolanaProgramMetadataScenarioStep {
|
||||||
|
return crate::SolanaProgramMetadataScenarioStep {
|
||||||
|
id: id.to_string(),
|
||||||
|
label: label.to_string(),
|
||||||
|
operation_code: operation_code.to_string(),
|
||||||
|
initial_state,
|
||||||
|
resulting_state,
|
||||||
|
requires_explicit_approval,
|
||||||
|
required_evidence: required_evidence(resulting_state),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_evidence(
|
||||||
|
resulting_state: crate::SolanaProgramMetadataFixtureState,
|
||||||
|
) -> std::vec::Vec<std::string::String> {
|
||||||
|
let terminal_evidence = match resulting_state {
|
||||||
|
crate::SolanaProgramMetadataFixtureState::Closed => "account_absence",
|
||||||
|
_ => "materialized_snapshot",
|
||||||
|
};
|
||||||
|
return vec![
|
||||||
|
"stateful_preflight".to_string(),
|
||||||
|
"rpc_simulation".to_string(),
|
||||||
|
"confirmed_signature".to_string(),
|
||||||
|
"stateful_postcondition".to_string(),
|
||||||
|
terminal_evidence.to_string(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn journeys_cover_each_stable_operation_exactly_once() {
|
||||||
|
let scenarios = crate::solana_program_metadata_devnet_scenarios();
|
||||||
|
assert_eq!(scenarios.len(), 2);
|
||||||
|
let operations = scenarios
|
||||||
|
.iter()
|
||||||
|
.flat_map(|scenario| return scenario.steps.iter())
|
||||||
|
.map(|step| return step.operation_code.as_str())
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
assert_eq!(operations.len(), kb_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES.len());
|
||||||
|
for operation in kb_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES {
|
||||||
|
assert_eq!(
|
||||||
|
operations.iter().filter(|candidate| return **candidate == *operation).count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn destructive_flags_match_the_executor_contract() {
|
||||||
|
for scenario in crate::solana_program_metadata_devnet_scenarios() {
|
||||||
|
for step in scenario.steps {
|
||||||
|
assert_eq!(
|
||||||
|
step.requires_explicit_approval,
|
||||||
|
kb_lib::EX_METADATA_SPM_DESTRUCTIVE_OPERATION_CODES
|
||||||
|
.contains(&step.operation_code.as_str())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_validation.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Conservative Solana Program Metadata validation status and evidence contracts.
|
||||||
|
|
||||||
|
/// Maximum evidence entries accepted by one operation validation row.
|
||||||
|
pub const MAX_SOLANA_PROGRAM_METADATA_VALIDATION_EVIDENCE: usize = 16;
|
||||||
|
|
||||||
|
/// Exact network validation status.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SolanaProgramMetadataValidationStatus {
|
||||||
|
/// No network execution was attempted.
|
||||||
|
NotRun,
|
||||||
|
/// Exact transaction simulated successfully.
|
||||||
|
Simulated,
|
||||||
|
/// Transaction submitted but not yet confirmed.
|
||||||
|
Submitted,
|
||||||
|
/// Transaction confirmed and postconditions validated.
|
||||||
|
Confirmed,
|
||||||
|
/// Required network capability was unavailable.
|
||||||
|
Unavailable,
|
||||||
|
/// Validation failed with retained diagnostics.
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One bounded evidence record.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SolanaProgramMetadataValidationEvidence {
|
||||||
|
/// Stable evidence kind.
|
||||||
|
pub kind: std::string::String,
|
||||||
|
/// Non-secret evidence value.
|
||||||
|
pub value: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One operation row in the closed Devnet validation matrix.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SolanaProgramMetadataValidationOperation {
|
||||||
|
/// Stable operation code.
|
||||||
|
pub operation_code: std::string::String,
|
||||||
|
/// Scenario step that exercises the operation.
|
||||||
|
pub scenario_step_id: std::string::String,
|
||||||
|
/// Exact current validation status.
|
||||||
|
pub status: crate::SolanaProgramMetadataValidationStatus,
|
||||||
|
/// Whether a destructive approval is required.
|
||||||
|
pub requires_explicit_approval: bool,
|
||||||
|
/// Evidence required for confirmed status.
|
||||||
|
pub required_evidence: std::vec::Vec<std::string::String>,
|
||||||
|
/// Bounded observed evidence.
|
||||||
|
#[serde(default)]
|
||||||
|
pub evidence: std::vec::Vec<crate::SolanaProgramMetadataValidationEvidence>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closed validation matrix for all nine stable operations.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SolanaProgramMetadataValidationMatrix {
|
||||||
|
/// Matrix schema version.
|
||||||
|
pub matrix_version: u32,
|
||||||
|
/// Exact owning prerelease.
|
||||||
|
pub milestone: std::string::String,
|
||||||
|
/// Ordered stable operations.
|
||||||
|
pub operations: std::vec::Vec<crate::SolanaProgramMetadataValidationOperation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads and validates the canonical Solana Program Metadata Devnet matrix.
|
||||||
|
pub fn load_solana_program_metadata_validation_matrix()
|
||||||
|
-> kb_core::Result<crate::SolanaProgramMetadataValidationMatrix> {
|
||||||
|
let parsed = match serde_json::from_str::<crate::SolanaProgramMetadataValidationMatrix>(
|
||||||
|
include_str!(
|
||||||
|
"../../test-fixtures/contract-matrices/SOLANA_PROGRAM_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(
|
||||||
|
"solana_program_metadata_validation_matrix_invalid_json",
|
||||||
|
format!("Solana Program Metadata validation matrix is invalid: {error}"),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let std::result::Result::Err(error) =
|
||||||
|
validate_solana_program_metadata_validation_matrix(&parsed)
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates exact inventory, order and conservative evidence claims.
|
||||||
|
pub fn validate_solana_program_metadata_validation_matrix(
|
||||||
|
matrix: &crate::SolanaProgramMetadataValidationMatrix,
|
||||||
|
) -> kb_core::Result<()> {
|
||||||
|
if matrix.matrix_version != 1 || matrix.milestone != "0.4.8-pre.009" {
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"solana_program_metadata_validation_matrix_contract_mismatch",
|
||||||
|
"Solana Program Metadata validation matrix must use version 1 for 0.4.8-pre.009",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let compiled_steps = crate::solana_program_metadata_devnet_scenarios()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|scenario| return scenario.steps)
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
let expected = compiled_steps
|
||||||
|
.iter()
|
||||||
|
.map(|step| return step.operation_code.as_str())
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
let actual = matrix
|
||||||
|
.operations
|
||||||
|
.iter()
|
||||||
|
.map(|operation| return operation.operation_code.as_str())
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
if actual != expected {
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"solana_program_metadata_validation_matrix_inventory_mismatch",
|
||||||
|
"Solana Program Metadata validation matrix differs from the compiled journey order",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for (operation, step) in matrix.operations.iter().zip(compiled_steps.iter()) {
|
||||||
|
if operation.scenario_step_id != step.id
|
||||||
|
|| operation.requires_explicit_approval != step.requires_explicit_approval
|
||||||
|
|| operation.required_evidence != step.required_evidence
|
||||||
|
|| operation.evidence.len() > crate::MAX_SOLANA_PROGRAM_METADATA_VALIDATION_EVIDENCE
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"solana_program_metadata_validation_matrix_row_invalid",
|
||||||
|
"Solana Program Metadata validation row differs from the compiled scenario contract",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if matches!(
|
||||||
|
operation.status,
|
||||||
|
crate::SolanaProgramMetadataValidationStatus::NotRun
|
||||||
|
| crate::SolanaProgramMetadataValidationStatus::Unavailable
|
||||||
|
) && !operation.evidence.is_empty()
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"solana_program_metadata_validation_unobserved_with_evidence",
|
||||||
|
"not-run or unavailable validation rows must not contain observed evidence",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if operation.status == crate::SolanaProgramMetadataValidationStatus::Confirmed {
|
||||||
|
let observed = operation
|
||||||
|
.evidence
|
||||||
|
.iter()
|
||||||
|
.map(|evidence| return evidence.kind.as_str())
|
||||||
|
.collect::<std::collections::BTreeSet<_>>();
|
||||||
|
if !operation
|
||||||
|
.required_evidence
|
||||||
|
.iter()
|
||||||
|
.all(|kind| return observed.contains(kind.as_str()))
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(kb_core::Error::new(
|
||||||
|
"solana_program_metadata_validation_confirmed_without_evidence",
|
||||||
|
"confirmed validation row lacks required evidence",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn canonical_matrix_matches_the_compiled_two_journey_inventory() {
|
||||||
|
let matrix = match crate::load_solana_program_metadata_validation_matrix() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("matrix load failed: {error}"),
|
||||||
|
};
|
||||||
|
assert_eq!(matrix.operations.len(), 9);
|
||||||
|
assert!(matrix.operations.iter().all(|operation| return operation.status
|
||||||
|
== crate::SolanaProgramMetadataValidationStatus::NotRun));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// file: kb-pipeline-demo-scenarios/tests/external_metadata_solana_program_scenarios_api.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Public API regression test for Solana Program Metadata Devnet scenarios.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_consumers_use_only_public_solana_program_metadata_scenario_exports() {
|
||||||
|
let scenarios = kb_pipeline_demo_scenarios::solana_program_metadata_devnet_scenarios();
|
||||||
|
assert_eq!(scenarios.len(), 2);
|
||||||
|
let matrix = match kb_pipeline_demo_scenarios::load_solana_program_metadata_validation_matrix()
|
||||||
|
{
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
panic!("unable to load Solana Program Metadata validation matrix: {error}");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(matrix.operations.len(), 9);
|
||||||
|
let wallet = kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([3_u8; 32]).to_string());
|
||||||
|
let operation = kb_lib::ExMetadataSpmOperation::Extend {
|
||||||
|
account: wallet.clone(),
|
||||||
|
authority: wallet.clone(),
|
||||||
|
length: 1,
|
||||||
|
program_context: std::option::Option::None,
|
||||||
|
};
|
||||||
|
let mut request = kb_pipeline_demo_scenarios::DevnetSolanaProgramMetadataExecutionRequest::new(
|
||||||
|
"external-spm",
|
||||||
|
operation.clone(),
|
||||||
|
);
|
||||||
|
request
|
||||||
|
.preflight_reads
|
||||||
|
.push(kb_pipeline::SolanaProgramMetadataStatefulReadRequest {
|
||||||
|
query_role: "http_queries".to_string(),
|
||||||
|
account: wallet.clone(),
|
||||||
|
expected_state: kb_pipeline::SolanaProgramMetadataExpectedAccountState::Buffer,
|
||||||
|
min_context_slot: std::option::Option::None,
|
||||||
|
max_data_bytes: kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES,
|
||||||
|
});
|
||||||
|
assert!(request.validate().is_ok());
|
||||||
|
let step = kb_pipeline_demo_scenarios::SolanaProgramMetadataPreparedStep {
|
||||||
|
scenario_id: "external".to_string(),
|
||||||
|
step_id: "extend".to_string(),
|
||||||
|
step_index: 0,
|
||||||
|
operation,
|
||||||
|
preflight_reads: request.preflight_reads.clone(),
|
||||||
|
rent_observations: std::vec::Vec::new(),
|
||||||
|
postcondition_reads: std::vec::Vec::new(),
|
||||||
|
allow_destructive_operation: false,
|
||||||
|
};
|
||||||
|
assert_eq!(step.execution_request("prepared").intent_id, "prepared");
|
||||||
|
let fixture_options =
|
||||||
|
kb_pipeline_demo_scenarios::SolanaProgramMetadataFixturePreparationOptions::new();
|
||||||
|
assert_eq!(fixture_options.query_role, "http_queries");
|
||||||
|
assert_eq!(
|
||||||
|
kb_pipeline_demo_scenarios::SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES,
|
||||||
|
128
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"matrixVersion": 1,
|
||||||
|
"milestone": "0.4.8-pre.009",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"operationCode": "metadata.solana_program_metadata.allocate",
|
||||||
|
"scenarioStepId": "spm_buffer_allocate",
|
||||||
|
"status": "not_run",
|
||||||
|
"requiresExplicitApproval": false,
|
||||||
|
"requiredEvidence": [
|
||||||
|
"stateful_preflight",
|
||||||
|
"rpc_simulation",
|
||||||
|
"confirmed_signature",
|
||||||
|
"stateful_postcondition",
|
||||||
|
"materialized_snapshot"
|
||||||
|
],
|
||||||
|
"evidence": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationCode": "metadata.solana_program_metadata.extend",
|
||||||
|
"scenarioStepId": "spm_buffer_extend",
|
||||||
|
"status": "not_run",
|
||||||
|
"requiresExplicitApproval": false,
|
||||||
|
"requiredEvidence": [
|
||||||
|
"stateful_preflight",
|
||||||
|
"rpc_simulation",
|
||||||
|
"confirmed_signature",
|
||||||
|
"stateful_postcondition",
|
||||||
|
"materialized_snapshot"
|
||||||
|
],
|
||||||
|
"evidence": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationCode": "metadata.solana_program_metadata.write",
|
||||||
|
"scenarioStepId": "spm_buffer_write",
|
||||||
|
"status": "not_run",
|
||||||
|
"requiresExplicitApproval": true,
|
||||||
|
"requiredEvidence": [
|
||||||
|
"stateful_preflight",
|
||||||
|
"rpc_simulation",
|
||||||
|
"confirmed_signature",
|
||||||
|
"stateful_postcondition",
|
||||||
|
"materialized_snapshot"
|
||||||
|
],
|
||||||
|
"evidence": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationCode": "metadata.solana_program_metadata.trim",
|
||||||
|
"scenarioStepId": "spm_buffer_trim",
|
||||||
|
"status": "not_run",
|
||||||
|
"requiresExplicitApproval": true,
|
||||||
|
"requiredEvidence": [
|
||||||
|
"stateful_preflight",
|
||||||
|
"rpc_simulation",
|
||||||
|
"confirmed_signature",
|
||||||
|
"stateful_postcondition",
|
||||||
|
"materialized_snapshot"
|
||||||
|
],
|
||||||
|
"evidence": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationCode": "metadata.solana_program_metadata.close",
|
||||||
|
"scenarioStepId": "spm_buffer_close",
|
||||||
|
"status": "not_run",
|
||||||
|
"requiresExplicitApproval": true,
|
||||||
|
"requiredEvidence": [
|
||||||
|
"stateful_preflight",
|
||||||
|
"rpc_simulation",
|
||||||
|
"confirmed_signature",
|
||||||
|
"stateful_postcondition",
|
||||||
|
"account_absence"
|
||||||
|
],
|
||||||
|
"evidence": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationCode": "metadata.solana_program_metadata.initialize",
|
||||||
|
"scenarioStepId": "spm_metadata_initialize",
|
||||||
|
"status": "not_run",
|
||||||
|
"requiresExplicitApproval": false,
|
||||||
|
"requiredEvidence": [
|
||||||
|
"stateful_preflight",
|
||||||
|
"rpc_simulation",
|
||||||
|
"confirmed_signature",
|
||||||
|
"stateful_postcondition",
|
||||||
|
"materialized_snapshot"
|
||||||
|
],
|
||||||
|
"evidence": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationCode": "metadata.solana_program_metadata.set_data",
|
||||||
|
"scenarioStepId": "spm_metadata_set_data",
|
||||||
|
"status": "not_run",
|
||||||
|
"requiresExplicitApproval": true,
|
||||||
|
"requiredEvidence": [
|
||||||
|
"stateful_preflight",
|
||||||
|
"rpc_simulation",
|
||||||
|
"confirmed_signature",
|
||||||
|
"stateful_postcondition",
|
||||||
|
"materialized_snapshot"
|
||||||
|
],
|
||||||
|
"evidence": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationCode": "metadata.solana_program_metadata.set_authority",
|
||||||
|
"scenarioStepId": "spm_metadata_set_authority",
|
||||||
|
"status": "not_run",
|
||||||
|
"requiresExplicitApproval": true,
|
||||||
|
"requiredEvidence": [
|
||||||
|
"stateful_preflight",
|
||||||
|
"rpc_simulation",
|
||||||
|
"confirmed_signature",
|
||||||
|
"stateful_postcondition",
|
||||||
|
"materialized_snapshot"
|
||||||
|
],
|
||||||
|
"evidence": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operationCode": "metadata.solana_program_metadata.set_immutable",
|
||||||
|
"scenarioStepId": "spm_metadata_set_immutable",
|
||||||
|
"status": "not_run",
|
||||||
|
"requiresExplicitApproval": true,
|
||||||
|
"requiredEvidence": [
|
||||||
|
"stateful_preflight",
|
||||||
|
"rpc_simulation",
|
||||||
|
"confirmed_signature",
|
||||||
|
"stateful_postcondition",
|
||||||
|
"materialized_snapshot"
|
||||||
|
],
|
||||||
|
"evidence": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user