v0.4.8-pre.013

This commit is contained in:
2026-08-08 22:28:17 +02:00
parent f5efce576b
commit 8b831f8692
77 changed files with 15590 additions and 1585 deletions

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml
# version: 35
# version: 36
[workspace]
resolver = "3"
@@ -18,7 +18,7 @@ members = [
]
[workspace.package]
version = "0.4.8-pre.12"
version = "0.4.8-pre.13"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"

View File

@@ -0,0 +1,333 @@
<!-- file: docs/audits/V0_4_8_PRE_013_METAPLEX_COMPLETENESS_AND_DEVNET_AUDIT.md -->
<!-- version: 30 -->
# Audit `0.4.8-pre.013` — Metaplex Token Metadata et campagnes Devnet
## Objet
Ce réaudit vérifie si la surface Metaplex Token Metadata doit encore être complétée avant de reprendre les campagnes Devnet. Il sépare strictement la complétude du code, la disponibilité dune fixture et la preuve réseau observée.
## Sources contrôlées
- IDL historique conservé `metadata.metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s.metaplex_token_metadata.V1_14_0.from_github_mpl-token-metadata.json` ;
- `METAPLEX_TOKEN_METADATA_MATRIX.json` ;
- intents, builders, exécuteur, préflight et matérialiseurs Metaplex de `kb-lib` et `kb-pipeline` ;
- inventaires synthétiques et Devnet de `kb-pipeline-demo-scenarios` ;
- surface Rust actuelle `mpl-token-metadata ^5.1`, résolue à `5.1.1` dans la base validée.
## Fermeture de la surface
LIDL contient exactement 58 instructions et la matrice dexécution contient exactement 58 lignes. Leur partition est :
- 20 `executable_current` ;
- 15 `executable_deprecated` ;
- 22 `decode_only_replaced` ;
- 1 `decode_only_bridge_boundary`, `BubblegumSetCollectionSize`, hors frontière du programme Metaplex Token Metadata.
Les 20 opérations courantes sont : `CreateEscrowAccount`, `CloseEscrowAccount`, `TransferOutOfEscrow`, `Burn`, `Create`, `Mint`, `Delegate`, `Revoke`, `Lock`, `Unlock`, `Migrate`, `Transfer`, `Update`, `Use`, `Verify`, `Unverify`, `Collect`, `Print`, `Resize` et `CloseAccounts`.
Le réaudit ne met en évidence aucune instruction courante du SDK actuel absente des intents ou de lexécuteur. Une seconde passe dexactitude des builders a toutefois révélé que `Print` et `Resize` utilisaient encore les account metas figées de lIDL historique : `Print` ne déclarait pas `edition_mint` ni `master_token_account_owner` signataires et `Resize` ne déclarait pas `payer` signataire. `pre.013-delta-fix-003` migre ces deux opérations vers les builders officiels `mpl-token-metadata 5.1.1` et verrouille leurs flags de comptes par tests directs. Les formes remplacées restent volontairement non exécutables et les formes obsolètes restent séparées, exécutables uniquement sous approbation explicite. Aucun élargissement de la frontière programme nest requis.
## Écart initial de validation réseau
Au début de `0.4.8-pre.013`, la matrice Devnet contenait les 20 opérations courantes mais aucune preuve réseau durable n'était conservée dans la base reprise : les 20 statuts ont donc été rouverts à `not_run`. Cette décision constitue le point de départ du réaudit, pas son état final ; la consolidation en fin de document ferme la matrice à 15 `confirmed`, 5 `unavailable` et 0 `not_run`.
À ce même point de départ, les cinq familles de parcours existantes — NFT, SFT, token fongible, collection et pNFT — décrivaient une cible cohérente, mais leur préparation Devnet réellement automatisée restait limitée à `Create` et `UpdateAsUpdateAuthorityV2`. Les opérations spécialisées exigeaient encore des graphes de comptes dédiés, notamment :
- édition maître, édition imprimée, `Print`, `Mint` et `Burn` ;
- collection parent/membre pour `Verify` et `Unverify` ;
- pNFT avec token records, delegates et rule set lorsque requis ;
- uses, escrow et opérations de maintenance selon leurs préconditions stateful.
Aucune disponibilité de builder ni preuve synthétique nest promue en preuve Devnet.
## Écart du runner corrigé
Le runner Metaplex construisait déjà une policy exigeant, après soumission, linsertion canonique, lextraction Core, le replay et éventuellement la matérialisation. Pourtant, son exécution sarrêtait après confirmation, lectures stateful et snapshots de comptes.
`pre.013` aligne le comportement sur le contrat déclaré :
1. confirmation de la signature ;
2. lectures stateful de postcondition ;
3. hydratation canonique bornée de la transaction ;
4. extraction Core de la signature ;
5. replay du décodeur Metaplex avec les matérialiseurs du domaine ;
6. lecture des matérialisations instructionnelles ;
7. seconde passe de replay avec état matérialisé, qui doit produire un skip idempotent propre ;
8. diagnostic post-exécution agrégé.
Le nombre de retries `getTransaction` est borné à 20. Le desktop est seulement adapté à ce contrat commun ; lajout des nouveaux parcours UI reste réservé à `pre.014`.
## État de la prerelease
Le réaudit de complétude est terminé et le défaut de post-validation du runner est corrigé dans le delta initial. La prerelease reste **en cours** tant que les fixtures spécialisées ne sont pas complétées et que les 20 opérations courantes ne sont pas qualifiées par des preuves Devnet réelles ou une indisponibilité explicitement démontrée.
## Correctif `pre.013-delta-fix-001`
La première validation locale du delta initial a mis en évidence deux erreurs de compilation, sans remettre en cause le réaudit ni la frontière fonctionnelle :
- le test Devnet opt-in appelait `PostgresStoreOptions::new` avec l'ancienne signature à trois paramètres et fournissait une `Duration` alors que le contrat courant attend `connect_timeout_ms: u64` puis `auto_initialize_schema: bool` ;
- le desktop injectait directement `BackfillSummary`, `CoreExtractionSummary` et `DecodeReplaySummary` dans `serde_json::json!`, alors que ces summaries internes ne dérivent volontairement pas `Serialize`.
`pre.013-delta-fix-001` corrige le premier point avec `5_000` ms et `false`, puis projette explicitement les trois summaries en `serde_json::Value` dans l'adaptateur desktop. Aucun derive public supplémentaire n'est ajouté à `kb-pipeline` et aucun contrat de production n'est élargi.
La validation locale de `pre.013-delta-fix-001` est ensuite réussie : `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets` et l'audit workspace sont propres ; `kb-pipeline-demo-scenarios` réussit 69 tests unitaires, 1 test CLI et 1 test d'API externe ; `kb-app-demo-desktop` réussit 135 tests unitaires.
## Correctif `pre.013-delta-fix-002` — cohérence des cinq fixtures Create
Le réaudit des cinq parcours Devnet a révélé un défaut latent indépendant du runner : le desktop adaptait le JSON `Create` à NFT, SFT, fungible, collection ou pNFT, tandis que `prepare_metaplex_create_fixture` préparait toujours un mint classique à zéro décimale. Cette séparation pouvait produire un parcours `Fungible` dont le `TokenStandard` annoncé ne correspondait pas aux décimales du mint réellement créé.
Le correctif déplace la spécialisation dans la fixture réutilisable :
- NFT, SFT, collection et pNFT préparent un mint classique à 0 décimale ;
- fungible prépare un mint classique à 9 décimales ;
- seules NFT, collection et pNFT demandent une master edition ;
- SFT et fungible conservent `master_edition = null` et aucun `print_supply` ;
- collection déclare `CollectionDetails::V1 { size: 0 }` ;
- pNFT déclare `ProgrammableNonFungible` sans rule set pour le parcours de base ;
- les cinq JSON produits sont vérifiés hors réseau par désérialisation vers `ExMetaplexTokenMetadataOperation::Create`.
Le desktop ne réécrit plus le contrat après préparation : le standalone Create reste NFT par défaut et chaque scénario transmet explicitement sa famille à la fixture commune.
La prerelease reste **en cours** : ce correctif ferme uniquement la cohérence des fixtures `Create`. Les token accounts/supply, éditions imprimées, collection parent-membre, token records pNFT, uses, escrow et la qualification réseau des 20 opérations restent à traiter.
## Correctif `pre.013-delta-fix-003` — exactitude `Print` et `Resize`
Le contrôle des graphes nécessaires aux éditions imprimées a mis en évidence un écart de builder avant même lexécution Devnet. Le SDK `mpl-token-metadata 5.1.1` déclare pour `Print` `edition_mint`, `edition_mint_authority`, `payer` et `master_token_account_owner` comme signataires ; lancien builder issu de lIDL ne déclarait que `edition_mint_authority` et `payer`. Pour `Resize`, le SDK courant déclare `payer` writable + signer, alors que lancien builder le rendait writable non signer.
Le correctif remplace uniquement ces deux constructions par `PrintBuilder` et `ResizeBuilder` du SDK officiel. Les types publics restent inchangés dans ce lot : les comptes actuellement obligatoires dans lintent sont transmis aux comptes optionnels du SDK sous forme `Some(...)`. Laudit des optionalités courantes et des autres opérations encore construites depuis lIDL brut reste explicitement ouvert avant leur qualification Devnet.
Deux tests de régression vérifient directement le discriminator, le nombre de comptes et les flags signer/writable critiques. Aucune ligne de la matrice Devnet nest promue par ce correctif.
## Correctif `pre.013-delta-fix-004` — optionalités positionnelles courantes
La validation locale de `pre.013-delta-fix-003` a dabord révélé deux violations `clippy::implicit-return` dans les deux closures ajoutées par ses tests. Lopérateur a ajouté les deux `return`, puis `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets` et laudit workspace ont réussi ; les deux régressions `Print`/`Resize` passent et la suite complète `kb-lib` atteint 697 tests unitaires réussis sans échec. `fix-004` incorpore ces deux corrections afin que larchive reproduise exactement cet état validé.
Le réaudit doptionalité montre ensuite que le décodeur et la matrice conservaient déjà le contrat Kinobi courant, mais que plusieurs intents/exécuteurs imposaient encore des comptes que le SDK accepte comme positions optionnelles. Le correctif aligne donc :
- `CreateEscrowAccount.authority` et `TransferOutOfEscrow.authority` sur lautorité optionnelle de fin de liste ;
- `Mint.token_owner`, `master_edition`, `token_record`, `delegate_record`, `authorization_rules_program` et `authorization_rules` sur les six positions optionnelles du wrapper courant ;
- `Migrate.authorization_rules_program` et `authorization_rules` sur les deux positions optionnelles finales ;
- `Use.delegate_record`, `token`, `edition`, `spl_token_program`, `authorization_rules_program` et `authorization_rules` sur les positions optionnelles courantes ;
- `Print.edition_token_record`, `Resize.authority` et `Resize.token` sur les comptes optionnels exposés par les builders officiels.
Les positions optionnelles absentes sont encodées avec le Program ID Metaplex readonly et non signer, ce qui préserve le nombre et lordre des comptes positionnels attendus par le programme et par le décodeur. Les comptes système, sysvar et programmes dont le builder propose seulement une valeur par défaut restent explicitement fournis lorsque la struct dinstruction courante les porte comme comptes requis : une commodité de builder nest pas reclassée comme optionalité positionnelle.
Deux régressions supplémentaires couvrent les placeholders de `CreateEscrowAccount`, `Mint`, `Migrate` et `Use`, puis labsence de `edition_token_record` pour `Print` et de `authority`/`token` pour `Resize`. Aucune ligne Devnet nest promue par ce correctif. La validation locale de `fix-004` réussit ensuite avec `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets`, laudit workspace, 699 tests unitaires `kb-lib`, 70 tests unitaires `kb-pipeline-demo-scenarios` et 135 tests desktop.
## Correctif `pre.013-delta-fix-005` — socle token accounts/supply
La préparation des campagnes `Mint` exigeait encore un compte token classique réellement utilisable. Le correctif étend donc la fixture `Create` sans préconsommer la preuve Metaplex :
- la transaction native de préparation crée le mint SPL classique, exécute `InitializeMint2`, puis crée lATA canonique de lopérateur ;
- la dépense déclarée du plan inclut les minima de rent du mint et du compte token ;
- après confirmation, le mint doit rester initialisé avec supply nulle et lATA doit être un compte classique de 165 octets, lié au mint et à lopérateur, avec amount nul et état initialisé ;
- le résumé expose lATA et produit un intent Metaplex `Mint` typé par famille ;
- NFT, collection et pNFT réservent un montant brut de 1, SFT un montant de 10 et le fungible à 9 décimales un montant de 1_000_000_000 ;
- pour pNFT, le token-record PDA canonique est dérivé et transmis à `Mint`, mais nest pas créé par la fixture native.
Cette séparation est intentionnelle : la fixture prépare les préconditions mais laisse `Mint` produire la supply et, pour pNFT, létat programmable que la campagne Devnet devra observer. Les 20 lignes réseau restent `not_run` jusquà réception de preuves réelles.
## Correctif `pre.013-delta-fix-006` — première campagne réseau `Create -> Mint`
La validation locale de `fix-005` est acquise : `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets` et laudit workspace sont propres ; `kb-pipeline-demo-scenarios` réussit 72 tests unitaires, 1 test CLI et 1 test dAPI externe, et `kb-app-demo-desktop` réussit 135 tests unitaires.
Le correctif ajoute une campagne Devnet volontairement bornée à une seule famille par invocation. Les valeurs acceptées sont NFT, SFT, fungible, collection et pNFT. La campagne :
1. prépare un mint SPL classique frais et lATA canonique opérateur avec supply/amount nuls ;
2. exécute `Create` avec matérialisation et snapshots metadata/master edition selon la famille ;
3. relit le mint et lATA au minimum au slot confirmé de `Create` et exige toujours supply=0/amount=0 ;
4. exécute `Mint` avec les snapshots `Create` comme préflight confirmé ;
5. relit les comptes Metaplex au slot confirmé de `Mint`, exige le token record pour pNFT et vérifie la transition SPL exacte vers `mint_amount_raw` ;
6. exige pour les deux transactions hydratation canonique, extraction Core, premier replay, matérialisation instructionnelle et second replay idempotent.
Le runner générique est durci en parallèle : les requêtes de postcondition sont clonées après confirmation et leur `min_context_slot` devient le maximum entre la borne demandée et le slot confirmé. Une lecture `after_state` ne peut donc plus être satisfaite par un contexte antérieur à la transaction.
Aucune ligne de `METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json` nest promue par ce correctif. `Create` et `Mint` restent `not_run` jusquà réception de signatures et slots Devnet réels.
## Correctif `pre.013-delta-fix-007` — structure de crate et diagnostic de la première campagne NFT
La première tentative réelle `Create -> Mint` sur la famille NFT a été lancée après validation locale de `fix-006`. La suite hors réseau est propre avec 77 tests unitaires de `kb-pipeline-demo-scenarios` et 135 tests desktop. La campagne a ensuite atteint la validation post-exécution de `Create`, mais s'est arrêtée sur `metaplex_create_mint_post_execution_incomplete`. Le message de `fix-006` ne permettait pas de distinguer hydratation, extraction Core, replay, matérialisation instructionnelle ou snapshots manquants, et aucune signature n'a été imprimée avant le `panic`. Cette tentative ne constitue donc pas une preuve réseau durable et ne modifie aucun statut de matrice.
Le réaudit du chemin de matérialisation identifie en parallèle une sélection de preuve trop indirecte : les lignes matérialisées étaient retenues à partir du `source_decoder_name`, alors que la preuve recherchée appartient explicitement au matérialiseur Metaplex. `fix-007` sélectionne désormais les lignes par `processor_name`, dérivé directement de l'identité publique de `MtMetadataMetaplexTokenMetadataMaterializer`. Si aucune ligne n'est trouvée, le diagnostic post-exécution conserve le nom exact du matérialiseur et la signature concernée. La validation de campagne expose en outre les quatre drapeaux post-exécution, le nombre de lignes matérialisées, le nombre de snapshots et les diagnostics accumulés. Une nouvelle tentative pourra donc confirmer la correction ou localiser exactement l'étape encore défaillante sans interprétation implicite.
La même correction réorganise `kb-pipeline-demo-scenarios` avant l'ajout de nouvelles familles de fixtures. Les fichiers sont désormais groupés par domaine : `metadata/solana_program`, `metadata/metaplex_token_metadata`, `spl/associated_token_account`, `spl/memo`, `spl/token`, `spl/token_2022` et `spl/token_2022/metadata`. `solana.rs` conserve les primitives d'orchestration communes. Les exports publics restent au crate-root ; le refactor ne change donc pas le contrat externe de la crate.
## Correctif `pre.013-delta-fix-008` — borne RPC stateful Metaplex
La validation locale de `fix-007` réussit avec 78 tests unitaires de `kb-pipeline-demo-scenarios`, 135 tests desktop, `cargo check --workspace`, Clippy et l'audit workspace propres. La seconde tentative NFT localise ensuite l'échec avant hydratation : une lecture de postcondition transmet `MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES = 1_048_576` à `GetAccountInfoConfig::new_with_data`, alors que le transport d'exécution borne une lecture complète à `MAX_COMPLETE_ACCOUNT_DATA_BYTES = 65536`.
`fix-008` aligne la constante stateful Metaplex sur cette borne de transport, valide la requête avant l'appel RPC et ajoute une régression analogue à celle déjà utilisée pour Solana Program Metadata. Les capacités propres aux décodeurs restent indépendantes pour des données déjà disponibles offline ou un futur lecteur par tranches. Le diagnostic de campagne inclut désormais aussi signature et slot lorsqu'une transaction confirmée échoue plus tard dans la post-validation.
Aucune ligne Devnet n'est promue par cette correction : la campagne doit être rejouée après validation locale.
## Correctif `pre.013-delta-fix-009` — compatibilité replay `Create` / `Mint`
La validation locale de `fix-008` est propre : `cargo check --workspace`, Clippy, 106 tests unitaires `kb-pipeline`, 78 tests unitaires `kb-pipeline-demo-scenarios` et 135 tests desktop réussissent. La troisième tentative NFT confirme ensuite réellement `Create` sur Devnet avec la signature `51MXdFFFNY7UTS23cmfCQBfrDKC99uk5Trtw2gkptvEug9aWJ2R1cE9Ta5QHbe4kS1NYgRUSSMmmdbqdhyxxnR4Y` au slot `481882374`. Lhydratation canonique et lextraction Core réussissent, et deux snapshots stateful sont matérialisés. Le blocage restant est exclusivement instructionnel : `decode_replayed=false`, aucune ligne du matérialiseur Metaplex nest produite et la seconde passe ne peut pas démontrer lidempotence.
Le réaudit local identifie une incompatibilité entre lexécuteur et son propre décodeur. `Create` permet officiellement de choisir si le mint et lupdate authority signent, mais le décodeur imposait encore les deux signatures. La fixture prépare le mint dans une transaction antérieure et utilise donc légitimement `mint_as_signer=false`. En outre, les flags signer/writable de `MdCoreInstructionReplayInput` décrivent les privilèges globaux du message : lorsque authority, payer et update authority désignent le même wallet, un rôle readonly peut apparaître writable sans rendre linstruction invalide. `Mint` présente la même propriété avec token owner, authority et payer.
`fix-009` valide donc pour `Create` et `Mint` uniquement les privilèges minimaux réellement requis, accepte les signataires dynamiques et les sur-ensembles transactionnels, tout en conservant writable obligatoire pour master edition/token record lorsquils ne sont pas des placeholders Metaplex. Deux régressions verrouillent ces formes. Le runner ajoute aussi un diagnostic des compteurs par processor lorsque le premier replay nest pas complet. Aucune ligne de matrice nest promue avant un replay NFT complet avec matérialisation et idempotence.
## Correctif `pre.013-delta-fix-010` — fixture `Create -> Mint` fresh-only
La tentative suivant `fix-009` expose un mismatch entre lautorité opérateur `J12WA6c42oqpWkLu1dFa4pCegkQJPpxabwc3RRCMSUuH` et des mint/freeze authorities observées à `3RJXuBF6xB3j47EeAHEBbcwvV66rJrkhmJvYNr36jz8x`. Linterprétation initiale attribuait ce mismatch à une fixture persistante. `fix-010` durcit donc légitimement la préparation : alias à granularité nanoseconde, `TemporaryWalletStore::create` sans fallback de chargement, absence on-chain obligatoire avant préparation, puis relecture du mint et de lATA avec `minContextSlot` égal au slot confirmé de la transaction native.
Le réaudit complet effectué après la tentative suivante corrige toutefois linterprétation causale : la validation qui produit ce message est exécutée après `validate_confirmed_execution("create", ...)`, donc après un `Create` confirmé et son pipeline post-exécution. Pour une famille NFT avec Master Edition, Token Metadata transfère normalement les mint/freeze authorities au PDA Master Edition. `fix-010` reste donc un durcissement fresh-only utile, mais il ne pouvait pas résoudre ce mismatch attendu.
## Correctif `pre.013-delta-fix-011` — postcondition dautorité après `Create`
La tentative suivant `fix-010` reproduit le mismatch avec une nouvelle valeur fraîche `KsjdBdmUUM3r8Mw6cQESkXJfxmnPXSmR482963gmsyo`. La préparation fresh-only ayant été validée immédiatement avant `Create`, cette variation confirme quil ne sagit pas dun vieux mint réutilisé. Surtout, le chemin de code natteint cette vérification quaprès simulation, confirmation, hydratation canonique, extraction Core, replay, matérialisation, snapshots et seconde passe idempotente réussis pour `Create`. Signature et slot ne sont cependant pas imprimés à ce stade, donc cette tentative ne peut pas encore promouvoir la matrice.
`fix-011` applique la postcondition officielle : NFT, collection et pNFT attendent après `Create` le PDA Master Edition comme mint authority et freeze authority ; SFT et fungible continuent dattendre lautorité opérateur. La même attente est conservée après `Mint`. Les erreurs SPL postérieures à une exécution confirmée incluent désormais signature + slot, et le helper `read_token_account` devenu inutilisé est supprimé afin de retrouver un build sans warning.
## Correctif `pre.013-delta-fix-012` — première qualification opérationnelle confirmée
Après `fix-011`, la campagne NFT `Create -> Mint` termine enfin sans erreur. Le mint frais `J6pDBZR4nWpM8Mj61fEdwQXH3KLZE8RuKSEsyNofC7bp` utilise la metadata PDA `7JLnovNwuatxAessRcWCEuKb5jg2H93KVpmyHNzx4euG`, la Master Edition `GxUFL8ZfosThA4hUVGPKDT8Zvcc2b2VmdjEiw6WQQYNx` et lATA opérateur `9LBJCDFkJghjVDDeccTe4vjZH3dDNpzsWVXqyTKs81To`. La préparation native est confirmée par `2u4sdcFhmoeeYiAe8X761AmCnWyPLHSxYwzwwocLYkfVCan9W5i13ipvK4v3dWRe9E4iJRqBCHHFRh8MwR2o8FLh`.
`Create` est confirmé avec la signature `okwkuQFp7yVguYW7GEY9Chv6ochM8TWBEeCGV1gu5Waku9wQvMfrKjkig6JxXBwpKoFTECignzJ8HFf8dLtpsvS` au slot `481904731` et produit une matérialisation instructionnelle. `Mint` est confirmé avec la signature `3hSizfdvezyFpfhqX4Za4wmhdMHhSPAisFvYjFAxpRzYcUXhryxCzc9qpwod3Yjm6GmijXTGjjDUuVksfpBzLwz1` au slot `481904744`, produit une matérialisation instructionnelle et fait passer la supply du mint ainsi que lamount de lATA de `0` à `1`. Le test retourne `ok` uniquement après les gardes de simulation, hydratation canonique, extraction Core, replay, matérialisation, snapshots stateful et seconde passe idempotente.
La matrice Devnet opérationnelle passe en version 2. Elle conserve désormais `observedFamilies` et une liste de preuves observées ; une ligne `confirmed` est refusée si elle ne couvre pas lunion de `requiredEvidence` et `submissionEvidence`. `Create` et `Mint` deviennent `confirmed` sur `nft`, tandis que les 18 autres opérations restent `not_run`. La largeur multi-famille nest pas confondue avec cette qualification opérationnelle : SFT, fungible, collection et pNFT restent à exécuter pour confirmer que les variantes de comptes et darguments empruntent le même pipeline complet.
Pour éviter une nouvelle perte de télémétrie, le résumé dexécution conserve aussi `simulation_context_slot` et le test opt-in imprime une ligne `METAPLEX_CREATE_MINT_EVIDENCE` machine-readable. Les futures campagnes peuvent ainsi fournir le genesis hash, le slot de simulation, le message hash, le fee, la confirmation et les compteurs de pipeline exacts sans nouvelle instrumentation.
## Correctif `pre.013-delta-fix-013` — qualification SFT et preuves par famille
La campagne SFT suivante termine également de bout en bout. `Create` est simulé au slot `481908023`, confirmé avec `2s81m6k9HYDLmCy7oXKaqBvtb62iyX4bJmij2f2VjHuoRQBBA6YqRuAsjyaM7JCaJXasbU3WCfiko91cZcDLLdxV` au slot `481908028`, puis matérialise une ligne. `Mint` est simulé au slot `481908034`, confirmé avec `4pAemhEJewuLAeqLkqwzPvVmsCRbkyLiN6uAUWBA5RMsofaEDt5Xr8YGiBEvLWQM7Qvxnd81mmCHpandmfmWuyAm` au slot `481908039`, matérialise une ligne et fait passer supply/amount de `0` à `10`. Les deux étapes conservent genesis hash, message hash, fee, hydratation canonique, extraction Core, replay et idempotence propres.
La seconde famille observée expose une faiblesse du schéma v2 : une seule liste `evidence` ne peut pas représenter proprement plusieurs signatures et slots tout en conservant des kinds uniques. La matrice passe donc en version 3 avec un bundle `familyEvidence` par famille observée. Le validateur exige une correspondance exacte entre `observedFamilies` et ces bundles, puis contrôle chaque bundle contre les six preuves de simulation et les dix preuves de soumission. `Create` et `Mint` restent les deux seules opérations `confirmed`, désormais sur NFT et SFT ; les 18 autres opérations restent `not_run`.
## Consolidation `pre.013-delta-fix-014` — troisième famille `Create -> Mint`
La campagne fungible confirme que le même pipeline fonctionne avec un mint SPL classique à 9 décimales et sans Master Edition. `Create` est confirmé avec `3J1Ebq9DVkupVFQmqQ84QaNeztwMuRwbtXj7CiyUWwZPj4KCAJpKLAbPvpYss5cdiY33foXXScWWZE98H7tvbZed` au slot `481910724`; `Mint` est confirmé avec `2GKiY9foLmioYtUHttA2D6nCSmsftRL1JDKxENHmCnb5w38FGSxwBiRNMfF7mH8htv3G45irmge6DVtr6VN359j2` au slot `481910734`. Les deux étapes terminent simulation exacte, confirmation, hydratation canonique, extraction Core, replay, matérialisation et idempotence. `Mint` fait passer supply et amount ATA de `0` à `1_000_000_000`.
La matrice v3 ajoute donc un troisième bundle `familyEvidence = fungible` à `Create` et `Mint`. Leur statut opérationnel reste `confirmed`; les 18 autres opérations courantes restent `not_run`. La couverture multi-famille de cette campagne nest plus ouverte que pour collection et pNFT.
### Consolidation `pre.013-delta-fix-015`
La campagne collection `Create -> Mint` est confirmée avec `Create` signature `3jpxyWuxQVyBtbvqDnV1oahLyxCXMw1zQQZBGCKgQSfn2TNcBaUdfvJ6kFa8zL3XJLMpM6tahQTnu8AUsTkYc3Dy` slot `481911942`, puis `Mint` signature `4WEciqZA5vmtrh1ZeSbqcgVnezUy7qT6B6utySfkg7dbAmhH6SMwwY862zGbzPW3nBVFzMjFVGPEhnTt6TcAiEe2` slot `481911955`. Les deux étapes terminent le pipeline de preuve complet ; `Create` et `Mint` possèdent désormais quatre bundles indépendants `nft`, `sft`, `fungible` et `collection`. Le test de matrice est corrigé pour exiger ces quatre familles déjà qualifiées tout en autorisant l'ajout ultérieur de `programmable_nft` lorsqu'un bundle complet existe. pNFT reste la dernière variante `Create -> Mint` à exécuter.
## Correctif `pre.013-delta-fix-016` — état SPL pNFT après `Mint`
La première campagne `programmable_nft` franchit `Create`, puis confirme `Mint` avec la signature `Gkg5A6ehuSzkU1N318kYoRHm5RdxV2yBRpaK9Gwyt6vN1ToYwAS6Vzpk2rQYWS9NJz1rnUpypLCn7DqWFzQzxFc` au slot `481913765`. La relecture de lATA confirme le mint attendu `Ecn3v1u21DKNhpFX1tqKvieVn5CkhMMukxWgoN59Hf2E`, lowner opérateur `J12WA6c42oqpWkLu1dFa4pCegkQJPpxabwc3RRCMSUuH` et `amount=1`, mais observe `state=2`. Le runner exigeait encore `state=1` pour toutes les familles et échoue donc après une transaction `Mint` déjà confirmée.
Le contrat SPL encode `Initialized=1` et `Frozen=2`. Pour un programmable NFT, Token Metadata conserve lATA SPL gelé tandis que le Token Record porte létat programmable et les délégations. `fix-016` rend cette distinction explicite : pNFT exige `Frozen` après `Mint`, les quatre autres familles exigent `Initialized`, et le Token Record reste une postcondition Metaplex séparée. Le résumé de campagne conserve aussi les états SPL bruts avant/après afin de produire la preuve `1 -> 2`.
Aucun bundle `programmable_nft` nest ajouté à la matrice sur la base de cette tentative interrompue : la campagne doit être rejouée et retourner `ok` avec sa ligne `METAPLEX_CREATE_MINT_EVIDENCE` complète avant promotion.
## `pre.013-delta-fix-017` — fermeture `Create -> Mint` et ouverture collection parent-membre
Le rerun pNFT après `fix-016` ferme la qualification des cinq familles `Create -> Mint`. `Create` est confirmé au slot `481921980` et `Mint` au slot `481921994`; le Token Record est créé, supply/amount passent `0 -> 1` et lATA passe `Initialized(1) -> Frozen(2)`. La matrice v3 peut donc conserver cinq bundles familiaux indépendants pour `Create` et `Mint`. Les 18 autres opérations restent `not_run`.
Le lot suivant est borné à la collection courante :
- parent Collection NFT avec `CollectionDetails::V1 { size: 0 }` ;
- membre NFT créé avec `collection.key=<parent mint>` et `verified=false` ;
- `Verify` courant avec `VerificationArgs::CollectionV1` et comptes parent mint/metadata/master-edition ;
- `Unverify` courant avec `VerificationArgs::CollectionV1` ;
- postconditions member `verified false -> true -> false` ;
- postconditions parent `size 0 -> 1 -> 0` ;
- aucune utilisation de `SetCollectionSize` pour fabriquer létat attendu.
Le runner réutilise le chemin confirmé `Create -> Mint`, puis exige pour `Verify` et `Unverify` le même contrat de preuve complet : simulation, confirmation, hydratation canonique, Core extraction, replay, matérialisation, snapshots et seconde passe idempotente. Aucun statut réseau de `Verify`/`Unverify` nest promu avant exécution réelle.
## `pre.013-delta-fix-018` — compatibilité replay `Verify` / `Unverify`
La première campagne collection spécialisée confirme `Verify(CollectionV1)` sur Devnet avec la signature `2kR1VeWdCW3FcKRZdNuKMMc3kGUmWRBXxXX8W1NNi2rhWfZYTg9zsDvJHx7WvebgUfc2K382J14i66E2qPEnjsqR` au slot `481931061`. Lhydratation canonique, lextraction Core et trois snapshots stateful réussissent, mais le décodeur retourne `failed=1`, `decoded=0`.
Le défaut est le même que pour `Create`/`Mint` : les flags Core sont les privilèges globaux du message. Lautorité est aussi fee payer et devient donc writable au niveau transactionnel, même si `Verify` ne requiert localement que sa signature. `fix-018` remplace les égalités négatives par des minima de privilèges pour `Verify` et `Unverify`, conserve lexigence writable de la metadata cible et de la collection metadata réellement fournie, et aligne `Unverify` sur ses 7 positions courantes. La matrice conserve `Verify` et `Unverify` à `not_run` jusquau rerun complet.
## `pre.013-delta-fix-019` — qualification collection et préparation `Print -> Burn`
Le rerun suivant `fix-018` ferme la campagne collection spécialisée. `Verify(CollectionV1)` est confirmé avec la signature `39JwUoxF2WWYWvEEWzFxSmKZVwoqXD1wH78sgGaoXv2vZKYUsXDNNnKikeuyQcfCqDftEMHSKFDHnTeSQ3bRpFcJ` au slot `481936730`, puis `Unverify(CollectionV1)` avec `4Kfv42BGZtuBEwqTie5RWwdQ772hyxKiU9d3F3gfRnwVNrxCs2iZMvwL17Rwmuyr3XCVoHdaJVgaqCwv8y7bKoZY` au slot `481936742`. Les deux étapes terminent simulation, confirmation, hydratation canonique, extraction Core, decode replay, matérialisation, trois snapshots stateful et seconde passe idempotente. Le membre suit `verified=false -> true -> false` et le parent `CollectionDetails::V1.size=0 -> 1 -> 0`. La matrice Devnet peut donc promouvoir `Verify` et `Unverify` sur `collection`; elle contient désormais 4 opérations `confirmed` et 16 `not_run`.
Le lot suivant prépare `Print -> Burn` sans réutiliser les anciens wrappers dédition. Le master est créé par le chemin courant `Create -> Mint` avec `PrintSupply::Limited(1)`. Le mint dédition est un compte SPL classique frais, decimals 0, supply 0, avec ATA opérateur vide ; le builder courant `Print` reste responsable de la création de lédition, du mint du token imprimé et du transfert dautorité du nouveau mint. Le runner dérive Metadata, Edition et Edition Marker pour lédition 1 et ajoute une lecture stateful `EditionMarker` dédiée.
`Print` exige un `edition_mint` signataire distinct du wallet opérateur. La surface Devnet ajoute donc une variante explicite multi-signataires : aucun signataire supplémentaire nest implicitement accepté, chacun doit être déclaré dans `additional_authorized_signers`, fourni au moment de la signature et rester dans la borne du runner. LAPI historique conserve son comportement profile-wallet-only. Pour la fixture fraîche limitée à une seule édition et possédée par le même opérateur que le master, `Burn` doit aussi ramener `MasterEdition.supply` de `1` à `0`, effacer lunique bit dEdition Marker puis fermer ce marker devenu vide, en plus de fermer le token account et l'Edition imprimée et de fermer sémantiquement Metadata selon le comportement du programme courant. La matrice garde `Print` et `Burn` à `not_run` tant que cette nouvelle campagne na pas terminé `ok` avec preuves réseau complètes.
## `pre.013-delta-fix-020` — correction de la précondition `Print` observée sur Devnet
La première tentative `Print -> Burn` de `fix-019` atteint le programme Metaplex pendant la simulation exacte mais nest pas soumise. `Print` retourne `MetadataError::NotEnoughTokens` (`0x20`). Lerreur est produite avant toute preuve réseau qualifiante ; `Print` et `Burn` restent donc `not_run`.
Le réaudit du processeur courant ferme le diagnostic. `fix-019` précréait un mint SPL dédition et son ATA opérateur avec `amount=0`. Pour `Print`, si le mint dédition est absent, le programme le crée ; si le token account dédition est absent, il crée lATA puis frappe exactement un token. En revanche, lorsquun token account est déjà présent, le chemin de validation exige `amount=1` et retourne `NotEnoughTokens` si cette quantité nest pas satisfaite. La fixture de `fix-019` sélectionnait donc mécaniquement le mauvais chemin avec un ATA existant vide.
`fix-020` corrige le contrat de fixture sans contourner le programme :
- création persistante du **keypair** `edition_mint` uniquement ;
- refus si le compte mint correspondant existe déjà on-chain ;
- dérivation de lATA canonique puis refus si cet ATA existe déjà ;
- validation séparée de lATA master, qui doit contenir exactement un token avant `Print` ;
- `edition_mint` reste un signataire supplémentaire explicitement déclaré et réellement fourni ;
- après confirmation, les mêmes postconditions fortes restent exigées sur mint/ATA, Metadata, Edition, Edition Marker et Master Edition.
Le `E0599` local signalé en parallèle ne correspond pas au contenu du delta `fix-019` archivé : ce delta contient bien `MetaplexTokenMetadataAccountKind::EditionMarker`. Il démontre néanmoins quun overlay partiel peut laisser `print_burn_campaign.rs` plus récent que `kb-pipeline`. `fix-020` réembarque donc le fichier stateful, valide ses pubkeys de dérivation et ajoute un test dintégration externe crate-root construisant réellement le variant `EditionMarker`.
## `pre.013-delta-fix-021` — compte `Edition` compact et futur Tauri `Send`
La tentative Devnet suivant `fix-020` franchit désormais `Print` : la transaction `2199PTvKyTAfnGRHVaaCceW4u8phdJSu5ULkceD9uJeQyjjenEupK9osRZu1Pzx2NuQaJnAwngSAXMvr5oNtf5y6` est confirmée au slot `482068400`. La campagne sarrête toutefois pendant les lectures stateful post-confirmation, avant hydratation canonique, extraction Core, replay et matérialisation. `Burn` nest donc pas atteint et aucune promotion de matrice nest effectuée.
Le compte `Edition` fraîchement créé expose la frontière de compatibilité liée à la réduction de taille Metaplex : la structure `Edition` du SDK Rust sérialise 41 octets, tandis que lallocation on-chain compacte courante utilise 42 octets et lallocation historique 241 octets. Le décodeur utilisait à tort `Edition::LEN` comme taille de compte stricte. `fix-021` accepte uniquement la longueur sérialisée exacte ou les allocations Metaplex connues avec padding nul, et refuse les longueurs ou paddings inconnus. Une régression `kb-pipeline` matérialise explicitement un compte `Edition` compact de 42 octets. Les erreurs stateful incluent désormais ladresse et le `kind` du compte visé.
La validation workspace de `fix-020` révèle en parallèle que `&dyn solana_signer::Signer` est conservé au travers du futur async multi-signataires. Comme le trait `Signer` ne garantit pas `Sync`, la commande Tauri ne satisfait plus son obligation `Future + Send`. Le keypair concret est `Send + Sync`; `fix-021` conserve donc `TemporaryWallet::as_signer()` et ajoute une vue `as_sync_signer()` dédiée, puis borne la nouvelle API multi-signataires à `Signer + Sync`. Les règles dautorisation des signataires supplémentaires restent inchangées. Le warning Clippy `cloned_ref_to_slice_refs` observé dans le test de garde des signataires est également corrigé.
## `pre.013-delta-fix-022` — fermeture Metadata, future `Send` et régression `Print`
Le rerun suivant `fix-021` atteint désormais `Burn` après validation confirmée de son exécution. La dernière garde rapporte `token=false, metadata=true, edition=false, edition_marker=false` : seuls les tests dexistence brute font encore apparaître Metadata. Le processeur courant ferme bien Metadata, mais son helper `close_program_account` réserve un traitement à `MetadataV1` : seul le rent minimum est retiré, car des fees peuvent rester. Lorsquun solde résiduel subsiste, le compte est réalloué à un octet `0x00` et devient `Uninitialized` au lieu de disparaître. `fix-022` accepte donc comme fermeture uniquement labsence du compte ou ce tombstone exact (`owner=Token Metadata`, non exécutable, lamports positifs, `space=1`, `data=[0]`). Token account, Edition et Edition Marker vide doivent toujours être réellement absents.
La validation workspace expose aussi leffacement incomplet du marqueur `Sync` : bien que lAPI de `fix-021` accepte `Signer + Sync`, elle recréait ensuite une `Vec<&dyn Signer>` qui survivait jusquaux `await` suivants et rendait le futur Tauri non-`Send`. `fix-022` confine cette conversion dans un helper synchrone qui signe immédiatement la transaction ; aucun trait object non-`Sync` nest désormais stocké dans létat du futur.
Enfin, le seul échec `kb-lib` de la validation locale (`703/704`) provient du test `modern_print_accepts_transaction_privilege_supersets` : il construisait un `edition_token_record` présent mais readonly, contrairement au builder courant qui exige ce compte writable lorsquil est fourni. Le test est corrigé ; le décodeur nest pas assoupli. La matrice reste à 4 opérations `confirmed` et 16 `not_run` jusquau rerun complet et à lémission des signatures/slots/evidences `Print -> Burn`.
## `pre.013-delta-fix-023` — qualification `Print/Burn` et préparation du cycle pNFT
La validation locale de `fix-022` est entièrement propre : `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets` et laudit workspace réussissent. `kb-lib` passe `704/704`, `kb-pipeline` `107/107` plus deux APIs externes, `kb-pipeline-demo-scenarios` `92/92` plus CLI/API externe et le desktop `135/135`.
La campagne Devnet `Print -> Burn` termine `ok`. `Print` est confirmé avec `REHAxUZrSdKDj8F8k27Amrg7hwuJFhVfstjc9xjn2XGfTX38fH4tFwPeNrF5zeMN5pc21PpUPSgaKXMMfTFd7TE` au slot `482087799`; `Burn` avec `4cNUxGGSWifuAtqdPJAtM3Cjpi4JATitRDqrvEAe85NLpMWcgWmC27bsfjNUmK6sEBDovNPtkPV3wDH1LpfGbhd1` au slot `482087818`. Les postconditions prouvent `MasterEdition.supply 0 -> 1 -> 0`, mint imprimé `0 -> 1 -> 0`, token `0 -> 1`, bit dEdition Marker consommé, puis fermeture du token account, de lEdition et du marker redevenu vide. Metadata subsiste uniquement comme tombstone fee exact et est donc sémantiquement fermée. Les preuves pipeline sont complètes et idempotentes. `Print` et `Burn` peuvent être promus : la matrice compte désormais 6 `confirmed` et 14 `not_run`.
Le sous-lot suivant prépare la campagne pNFT `Delegate(Staking) -> Lock -> Unlock -> Revoke(Staking) -> Delegate(Transfer) -> Transfer`. Cette séquence couvre cinq opérations courantes sans mélanger les rôles : le Staking delegate possède le droit de lock/unlock mais pas de transfer ; le Transfer delegate possède le droit de transfer mais pas le lock. Le runner vérifie à chaque étape le Token Record autoritatif et, après transfert, les deux ATA gelées ainsi que le Token Record destination sans delegate. Les cinq opérations restent `not_run` avant exécution réelle.
## `pre.013-delta-fix-029` — qualification du lifecycle pNFT et ouverture du probe `Use`
Le rerun `fix-028` ferme le cycle pNFT avec six transactions confirmées. `Delegate(StakingV1)` est confirmé au slot `482113803`, `Lock` au slot `482113812`, `Unlock` au slot `482113822`, `Revoke(StakingV1)` au slot `482113831`, `Delegate(TransferV1)` au slot `482113840` et `Transfer` au slot `482113850`. Les six étapes possèdent hydratation canonique, extraction Core, replay, matérialisation et idempotence propres. Létat suit `Unlocked -> Staking -> Locked -> Unlocked -> revoked -> Transfer`, puis le transfert laisse la source `amount=0/Initialized`, la destination `amount=1/Frozen` et le TokenRecord destination `Unlocked` sans delegate.
La matrice v3 promeut donc `Delegate`, `Revoke`, `Lock`, `Unlock` et `Transfer` sur `programmable_nft`. Le bilan devient **11 opérations `confirmed` et 9 `not_run`**.
Le réaudit de la source officielle courante impose une précaution avant `Use`. Le client Rust généré expose bien linstruction `Use` (discriminant 51), avec authority/payer signataires, Metadata writable et comptes token/edition optionnels writable. En revanche, le routeur du programme courant nexpose pas de branche nouvelle API `Use`; `Migrate` est même explicitement routé vers `MetadataError::Removed`. Le code legacy conserve `Utilize`, qui nest pas la même instruction. Cette divergence client/runtime ne doit être transformée ni en succès supposé ni en indisponibilité supposée.
`fix-029` prépare donc un probe Devnet borné : NFT classique frais, `Uses::Multiple { remaining: 2, total: 2 }`, `Create -> Mint`, puis simulation exacte de `Use`. Si la simulation est refusée, le probe retourne une classification `runtime_unavailable` avec erreur et logs sans soumettre. Si elle réussit, la transaction est soumise et doit prouver `remaining 2 -> 1`, token inchangé à `amount=1/Initialized`, pipeline complet et idempotence. `Use` reste `not_run` jusquà ce probe réel.
## `pre.013-delta-fix-030` — classification runtime de `Use`
Le probe Devnet réel de `Use` termine `Create -> Mint`, puis la simulation exacte de l'instruction courante de discriminant 51 est refusée par Token Metadata avec `InstructionError[0]=InvalidInstructionData`. Les logs contiennent `Program log: Error: InvalidInstructionData`; la simulation consomme `12085` compute units et estime les frais à `5000` lamports. Aucune transaction `Use` n'est soumise.
La classification canonique devient donc `unavailable`, et non `not_run` ni `confirmed`. La matrice contient désormais 11 opérations `confirmed`, 1 `unavailable` et 8 `not_run`. Le premier harness de probe remontait encore la simulation négative comme erreur de readiness ; `fix-030` corrige ce point en autorisant l'observation d'une simulation échouée uniquement pour `submit=false`, sans jamais autoriser l'envoi.
## `pre.013-delta-fix-033` — qualification escrow et bornage de la maintenance finale
Le rerun `fix-032` qualifie les trois opérations Token Owned Escrow. `CreateEscrowAccount` est confirmé au slot `482147206`, `TransferOutOfEscrow` au slot `482147252` et `CloseEscrowAccount` au slot `482147267`. Chacune termine hydratation canonique, extraction Core, replay, matérialisation et idempotence. Le dépôt contrôlé dune unité brute est intégralement restitué, lATA du PDA est fermée lorsquelle devient vide, le compte escrow est fermé et le NFT parent reste à `amount=1`. La matrice passe à **14 `confirmed`, 1 `unavailable`, 5 `not_run`**.
Le dernier lot distingue ensuite les opérations selon leurs autorités réelles. `Update` et `Resize` peuvent être exercés par un NFT frais détenu par lopérateur. `Migrate` est explicitement routé vers `Removed` dans le programme courant. `Collect` est réservé à `FEE_AUTHORITY=Levytx9LLPzAtDJJD7q813Zsm8zg9e1pb53mGxTKpD7`, et `CloseAccounts` à `OWNERLESS_CLOSE_AUTHORITY=C1oseLQExhuEzeBhsVbLtseSpVgvpHDbBj3PTevBCEBh`. La campagne de maintenance ne tente donc pas de contourner ces autorités : elle soumet seulement `Update`/`Resize` et simule les trois autres pour obtenir une classification runtime observable.
## `pre.013-delta-fix-034` — classification `Resize` sur comptes courants
Le premier run de maintenance après `fix-033` franchit la transaction `Update` et sa postcondition `primary_sale_happened false -> true`, puis la simulation de `Resize` atteint `IX: Resize` et s'arrête avec `Account has already been resized`, `Custom(201)`, après `14643` compute units. La politique simulation-first empêche toute soumission `Resize`. La sortie du test étant émise seulement après le résumé complet, la signature et le slot du `Update` déjà validé en interne ne sont pas disponibles et cette opération n'est pas encore promue.
Cette réponse est conforme à la fonction de `Resize` : les créations courantes sont déjà au layout cible et une exécution positive nécessite un compte legacy sous-dimensionné que le scénario ne peut pas fabriquer comme compte Token Metadata program-owned. `Resize` est donc classé `unavailable` pour la qualification contrôlée actuelle. La matrice passe à **14 `confirmed`, 2 `unavailable` et 4 `not_run`**. Le rerun final soumet uniquement `Update` et simule `Resize(201)`, `Migrate(75)`, `Collect(7)` et `CloseAccounts(188)`.
## Clôture fonctionnelle `pre.013-delta-fix-036`
Le dernier rerun maintenance ferme toutes les lignes encore ouvertes. `Update` est confirmé sur NFT avec simulation au slot `482162408`, confirmation au slot `482162413`, signature `4hxxBnsCA1JBJPNHgXRDeq1W7rXbFj2yySscWJKxU1VTMDcPzPfoWy7QzANHSDsbQfxxcwuAQNDLqEukgsug6qoZ`, replay, matérialisation, idempotence et transition `primary_sale_happened false -> true`.
Les quatre probes finaux sont tous négatifs avant soumission et correspondent au contrat courant : `Resize -> AccountAlreadyResized(201)` au slot `482162417`, `Migrate -> Removed(75)` au slot `482162422`, `Collect -> UpdateAuthorityIncorrect(7)` au slot `482162426`, `CloseAccounts -> InvalidCloseAuthority(188)` au slot `482162430`. `Resize` requiert un état legacy contrôlé absent des fixtures modernes ; `Migrate` est retiré ; `Collect` et `CloseAccounts` sont réservés à des autorités Metaplex fixes.
La matrice v3 est donc intégralement classée : **15 `confirmed`, 5 `unavailable`, 0 `not_run`**. Aucun statut `confirmed` n'est dérivé d'une preuve synthétique ou d'une simple simulation. Le travail fonctionnel de réaudit Metaplex de `pre.013` est terminé ; la suite est une consolidation documentaire et de conformité avant `pre.014`.
## `pre.013-delta-fix-037` — consolidation finale du réaudit Metaplex
La validation locale suivant `fix-036` ferme la prerelease sans nouvelle modification fonctionnelle : `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets` et `python3 scripts/audit_rust_workspace_rules.py` sont propres. `cargo test -p kb-pipeline-demo-scenarios` réussit avec 105 tests unitaires, 1 test CLI et 1 test API externe. Le warning `if_same_then_else` observé avant `fix-036` a disparu sans suppression de lint.
La matrice Devnet reste la source de vérité opérationnelle et contient exactement **15 opérations `confirmed`, 5 `unavailable` et 0 `not_run`**. Les cinq indisponibilités sont bornées et distinctes : `Use` est rejeté par le runtime courant avec `InvalidInstructionData`; `Resize` retourne `AlreadyResized(201)` sur les comptes créés au layout courant et nécessiterait un état legacy contrôlé pour un succès positif; `Migrate` retourne `Removed(75)`; `Collect` exige l'autorité Metaplex fixe de collecte; `CloseAccounts` exige l'autorité Metaplex ownerless-close fixe. Aucune de ces quatre dernières surfaces n'est soumise après une simulation négative.
Les formulations d'usage encore restées au statut préparatoire sont réconciliées : collection `Verify/Unverify`, lifecycle pNFT et Token Owned Escrow sont décrits comme campagnes qualifiées, le guide documente la campagne maintenance finale, et le TODO retire les tâches `pre.013` terminées. Les mentions `not_run` conservées dans les sections chronologiques de cet audit et des rapports intermédiaires restent intentionnelles : elles décrivent l'état observé au moment de chaque delta et ne constituent pas le statut courant.
`0.4.8-pre.013` est donc **terminée et validée**. Le prochain développement non-fix peut ouvrir `0.4.8-pre.014` pour la finalisation desktop metadata, sans rouvrir les campagnes Metaplex sauf régression ou changement de contrat explicitement observé.

File diff suppressed because one or more lines are too long

View File

@@ -1,83 +1,86 @@
<!-- file: docs/validation/V0_4_8_PRE_012_TOKEN_2022_METADATA_DEVNET_VALIDATION_REPORT.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# Validation Devnet `0.4.8-pre.012` — Token-2022 Token Metadata
## Statut
**Préparée, non exécutée.**
**Confirmée sur Devnet le 7 août 2026.**
Aucune signature, aucun slot et aucune postcondition réseau ne sont préremplis dans ce rapport. La preuve synthétique ou la seule réussite des tests locaux ne doit pas être présentée comme une validation Devnet confirmée.
La campagne complète a été exécutée sur un mint Token-2022 frais. Les cinq opérations prévues ont produit une transaction confirmée et une postcondition `Confirmed`.
## Périmètre réseau
Program ID :
## Program ID
```text
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
```
Campagne ordonnée :
## Fixture
1. `InitializeTokenMetadata` ;
2. `UpdateTokenMetadataField` ;
3. `EmitTokenMetadata` ;
4. `RemoveTokenMetadataKey` ;
5. `UpdateTokenMetadataAuthority`.
La préparation crée un mint Token-2022 frais avec `MetadataPointer` auto-référent, préfinancé pour les réallocations de metadata atteintes pendant la campagne.
## Matrice initiale
| Scénario | Statut initial | Preuve réseau |
|----------------------------------------|----------------|---------------|
| `token_2022_metadata_initialize` | `not_run` | aucune |
| `token_2022_metadata_update_field` | `not_run` | aucune |
| `token_2022_metadata_emit` | `not_run` | aucune |
| `token_2022_metadata_remove_key` | `not_run` | aucune |
| `token_2022_metadata_update_authority` | `not_run` | aucune |
Source machine-readable : `test-fixtures/contract-matrices/SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json`.
## Commande opérateur
Après les validations locales de compilation :
```bash
KB_DEVNET_TOKEN_2022_METADATA_CAMPAIGN_TEST=1 \
KB_DEVNET_TOKEN_2022_METADATA_OPERATOR_CONFIRMED=1 \
KB_POSTGRES_TEST_URL='postgresql://…' \
cargo test -p kb-pipeline-demo-scenarios optional_devnet_token_2022_metadata_campaign_from_env -- --nocapture
```text
mint=EHrx1evxgXLEgjb1sgc5dVTktXX1LiovXR9YFsZS3d4k
preparation_signature=2yYciRNoTTNSGuuaBFE6925FPjSj4fXp8usSkpLhgEPuhQMJYdsEzkAzxBdjDhpPHCT3db9bLz9Ud4BU5QSQZzQ5
initial_authority=J12WA6c42oqpWkLu1dFa4pCegkQJPpxabwc3RRCMSUuH
final_authority=3RVXUmVvY7quUUEVsDKh5EcenVygwv3mo1iagPa4Pmq7
```
`KB_DEVNET_PROFILE` permet de sélectionner un profil précis ; `KB_DEVNET_CONFIG_PATH` permet de remplacer le fichier de configuration par défaut.
La préparation crée `MetadataPointer` avant `InitializeMint2`, le fait pointer vers le mint lui-même et vérifie statefully l'absence initiale de `TokenMetadata`.
## Preuves à enregistrer
## Transactions confirmées
Pour les quatre mutations :
| Scénario | Opération canonique | Signature | Slot | Postcondition |
|----------------------------------------|--------------------------------------------------|--------------------------------------------------------------------------------------------|----------:|---------------|
| `token_2022_metadata_initialize` | `spl.token_2022.initialize_token_metadata` | `48kzJR5VW1ZYSRkVrg6qyNmuHdAUu7NXcnzaNsSmH7SSmRXK1X6RQdCeeSQi6LhrrkgRFRGVnxkwXPtkiqG6EgnY` | 481804441 | `Confirmed` |
| `token_2022_metadata_update_field` | `spl.token_2022.update_token_metadata_field` | `JrdB9EqePS1FVWLo55w1Vhv2eTCTfCpXZgMvPCocW3m3iu4nk3BFBBRHoe2Nbcpq1rmnZoFrhnag5fG2WwmJrYs` | 481804454 | `Confirmed` |
| `token_2022_metadata_emit` | `spl.token_2022.emit_token_metadata` | `3TKdBCJhM1VEZ9ti6EYR9gSLJnH5ni6FjW4kvoqwdYK1u14jdY6hS8hf2VGeLi21PbX5CWiyzjthJZERK5KHroWH` | 481804464 | `Confirmed` |
| `token_2022_metadata_remove_key` | `spl.token_2022.remove_token_metadata_key` | `3kDBV7pDFgoCQVJ9DYYuxvHFbCJzyA6J2rLg6ndGCETEhhFBZ1ipRQ52X2hZfwNPfdJwEFFBmbyMU6kFGnVXJNrm` | 481804473 | `Confirmed` |
| `token_2022_metadata_update_authority` | `spl.token_2022.update_token_metadata_authority` | `5aBpQsm6mAHA5xSN5tZ1ubALbfghGa7JScjXn9NGhg9agzEdjXydH6jgmn6UmxinGHmMvtqaGRLk9dgjmmz4HpU2` | 481804482 | `Confirmed` |
- simulation réussie ;
- signature ;
- confirmation et slot ;
- hydratation canonique ;
- extraction Core ;
- replay Token-2022 ;
- matérialisation instructionnelle Token Metadata ;
- postcondition stateful ;
- seconde passe d'idempotence.
## Preuves spécialisées
Pour `Emit` :
### `Initialize`
- simulation réussie ;
- `returnData` et longueur décodée ;
- signature ;
- confirmation et slot ;
- hydratation canonique ;
- extraction Core ;
- replay Token-2022 ;
- postcondition stateful comparée au contenu décodé ;
- seconde passe d'idempotence.
Le snapshot final confirme les champs initiaux du `TokenMetadata` et une matérialisation instructionnelle est observée.
## Résultat attendu
### `UpdateField`
La campagne est considérée validée uniquement si les cinq étapes sont confirmées et si leurs postconditions sont `Confirmed`. Après exécution, ce rapport et la matrice doivent être mis à jour avec les preuves réellement observées ; aucune valeur ne doit être reconstruite ou inventée.
La paire additionnelle `campaign=0.4.8-pre.012` est présente dans le snapshot autoritatif et une matérialisation instructionnelle est observée.
### `Emit`
La simulation retourne **202 octets** de `returnData`. Le runner décode la valeur complète et exige son égalité avec le `TokenMetadata` relu depuis le TLV du mint. La politique n'exige pas de matérialisation instructionnelle pour cette opération non mutante.
### `RemoveKey`
La paire `campaign` est absente du snapshot final et une matérialisation instructionnelle est observée.
### `UpdateAuthority`
L'autorité finale du snapshot TLV est :
```text
3RVXUmVvY7quUUEVsDKh5EcenVygwv3mo1iagPa4Pmq7
```
Elle correspond exactement à l'autorité fraîche préparée avant la campagne.
## Matrice machine-readable
`test-fixtures/contract-matrices/SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json` porte désormais les cinq scénarios au statut `confirmed` et conserve toutes les catégories de preuve requises.
## Validations locales
Avant et après la campagne :
```text
cargo fmt --all réussi
cargo check --workspace réussi
cargo clippy --all-targets réussi
python3 scripts/audit_rust_workspace_rules.py clean
cargo test -p kb-pipeline-demo-scenarios 69 + 1 + 1 tests réussis
cargo test --workspace réussi
```
## Conclusion
La campagne `0.4.8-pre.012` fournit une preuve Devnet complète pour les cinq instructions de `spl-token-metadata-interface` intégrées à Token-2022. Aucune des cinq lignes de la matrice ne reste `not_run`.

View File

@@ -0,0 +1,139 @@
<!-- file: docs/validation/V0_4_8_PRE_013_METAPLEX_COLLECTION_VERIFY_DEVNET_VALIDATION_REPORT.md -->
<!-- version: 3 -->
# Validation Devnet `0.4.8-pre.013` — collection Metaplex `Verify -> Unverify`
## Statut
**Confirmé sur Devnet — `Verify` et `Unverify` qualifiés avec transition collection complète.**
Le rerun après `pre.013-delta-fix-018` termine le parcours `Verify(CollectionV1) -> Unverify(CollectionV1)` de bout en bout. Les deux opérations passent simulation exacte, confirmation, hydratation canonique, extraction Core, decode replay, matérialisation instructionnelle, snapshots stateful et seconde passe idempotente. La matrice Devnet v3 les promeut donc à `confirmed` sur la famille `collection`.
## Première tentative réseau après `fix-017`
La transaction `Verify(CollectionV1)` est confirmée sur Devnet :
- signature : `2kR1VeWdCW3FcKRZdNuKMMc3kGUmWRBXxXX8W1NNi2rhWfZYTg9zsDvJHx7WvebgUfc2K382J14i66E2qPEnjsqR` ;
- slot confirmé : `481931061` ;
- hydratation canonique : réussie ;
- extraction Core : réussie ;
- snapshots stateful : `3` ;
- replay : `selected=1`, `started=1`, `completed=1`, `failed_inputs=1`, `decoded=0` ;
- matérialisation instructionnelle : absente à cause de léchec du décodeur ;
- `Unverify` : non atteint.
Le diagnostic est local au replay. Lautorité de collection est également le fee payer : le message Solana la projette donc `signer + writable`, alors que lAccountMeta `Verify` courant exige seulement `signer`. Le décodeur comparait encore les flags de façon exacte au lieu de traiter les privilèges transactionnels comme des sur-ensembles. Le même audit révèle que `Unverify` était artificiellement borné à 8 positions alors que le wrapper courant en construit 7. `fix-018` corrige ces deux défauts sans promouvoir la preuve partielle.
## Rerun réussi après `fix-018`
### `Verify(CollectionV1)`
- signature : `39JwUoxF2WWYWvEEWzFxSmKZVwoqXD1wH78sgGaoXv2vZKYUsXDNNnKikeuyQcfCqDftEMHSKFDHnTeSQ3bRpFcJ` ;
- simulation context slot : `481936725` ;
- confirmation slot : `481936730` ;
- simulation : succès, `5` logs ;
- message hash : `9o3WH8iWDcpoTuwx582MGPHAYiRmy36SRm1579Zrh9tw` ;
- fee : `5000` lamports ;
- snapshots avant/après : `3 / 3` ;
- hydratation canonique, extraction Core, decode replay, matérialisation et idempotence : réussis ;
- matérialisations instructionnelles : `1` ; snapshots matérialisés : `3`.
### `Unverify(CollectionV1)`
- signature : `4Kfv42BGZtuBEwqTie5RWwdQ772hyxKiU9d3F3gfRnwVNrxCs2iZMvwL17Rwmuyr3XCVoHdaJVgaqCwv8y7bKoZY` ;
- simulation context slot : `481936738` ;
- confirmation slot : `481936742` ;
- simulation : succès, `4` logs ;
- message hash : `HcrUFoW8i7oRVxbmyt8cpHpwgL2nSTaH9WuPsLEYfSKT` ;
- fee : `5000` lamports ;
- snapshots avant/après : `3 / 3` ;
- hydratation canonique, extraction Core, decode replay, matérialisation et idempotence : réussis ;
- matérialisations instructionnelles : `1` ; snapshots matérialisés : `3`.
### Transition autoritative observée
- membre : `collection.verified = false -> true -> false` ;
- parent : `CollectionDetails::V1.size = 0 -> 1 -> 0`.
Ces preuves remplissent les seize dimensions réseau exigées par opération dans la matrice v3.
## Contrat de fixture
Le parcours prépare deux actifs frais :
1. un Collection NFT parent avec `CollectionDetails::V1 { size: 0 }`, Master Edition et supply `1` ;
2. un NFT membre avec une relation `collection = { key: <parent mint>, verified: false }`, Master Edition et supply `1`.
Les deux actifs passent dabord par le runner qualifié `Create -> Mint`. Le parcours spécialisé refuse donc de commencer `Verify` si le membre nest pas explicitement non vérifié ou si la taille du parent nest pas exactement `0`.
## Opérations qualifiées par le runner
Le runner spécialisé exécute ensuite exactement :
1. `metadata.metaplex_token_metadata.verify` avec `VerificationArgs::CollectionV1` ;
2. `metadata.metaplex_token_metadata.unverify` avec `VerificationArgs::CollectionV1`.
`Verify` reçoit le mint, la metadata et la Master Edition du parent. `Unverify` reçoit le mint et la metadata du parent conformément au wrapper courant `mpl-token-metadata 5.1.1`.
## Postconditions obligatoires
Le runner lit après chaque confirmation :
- la Metadata PDA du membre ;
- la Metadata PDA du parent ;
- la Master Edition PDA du parent.
Les transitions attendues sont fermées :
| État | `collection.verified` membre | `CollectionDetails::V1.size` parent |
|------------------|-----------------------------:|------------------------------------:|
| avant `Verify` | `false` | `0` |
| après `Verify` | `true` | `1` |
| après `Unverify` | `false` | `0` |
La taille est validée comme état dérivé du programme ; le runner nutilise pas lopération obsolète `SetCollectionSize` pour fabriquer la postcondition.
Le snapshot stateful provient de `serde_json::to_value(mpl_token_metadata::accounts::Metadata)` ; le champ lu est donc exactement `collection_details.V1.size`, conformément au nom Rust sérialisé par le SDK 5.1.1.
Chaque opération doit également démontrer :
- simulation exacte réussie ;
- confirmation `Confirmed` ou `Finalized` avec slot ;
- hydratation canonique ;
- extraction Core ;
- decode replay Metaplex ;
- au moins une matérialisation instructionnelle ;
- snapshots stateful matérialisés ;
- seconde passe idempotente sans nouvel output ni refus.
## Commande opérateur
```bash
KB_DEVNET_METAPLEX_COLLECTION_VERIFY_CAMPAIGN_TEST=1 \
KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 \
KB_POSTGRES_TEST_URL='postgres://solana:solana@localhost:5432/solana_test' \
cargo test -p kb-pipeline-demo-scenarios \
optional_devnet_metaplex_collection_verify_campaign_from_env \
-- --nocapture
```
`KB_DEVNET_PROFILE` et `KB_DEVNET_CONFIG_PATH` restent optionnels comme pour les autres campagnes Devnet.
## Sortie à conserver
Une exécution réussie imprime :
```text
METAPLEX_COLLECTION_VERIFY_FIXTURE ...
METAPLEX_COLLECTION_VERIFY_STEP operation=metadata.metaplex_token_metadata.verify ...
METAPLEX_COLLECTION_VERIFY_STEP operation=metadata.metaplex_token_metadata.unverify ...
METAPLEX_COLLECTION_VERIFY_STATE verified_before=false verified_after_verify=true verified_after_unverify=false size_before=0 size_after_verify=1 size_after_unverify=0
METAPLEX_COLLECTION_VERIFY_EVIDENCE verify={...} unverify={...}
```
Les objets de la dernière ligne conservent les mêmes dimensions machine-readable que la campagne `Create -> Mint` : genesis hash, slot/logs de simulation, message hash, fee, signature, statut/slot de confirmation, snapshots, hydratation, Core, replay, matérialisation et idempotence.
## Promotion de matrice
`Verify` et `Unverify` sont désormais `confirmed` sur la famille `collection`, avec un bundle `familyEvidence` de seize preuves chacun. Avec `Create` et `Mint`, la matrice compte donc `4` opérations courantes confirmées et `16` opérations `not_run`.
La prochaine campagne spécialisée est `Print -> Burn`; aucune preuve de cette campagne nest promue avant une exécution Devnet complète terminée par `ok`.

View File

@@ -0,0 +1,268 @@
<!-- file: docs/validation/V0_4_8_PRE_013_METAPLEX_CREATE_MINT_DEVNET_VALIDATION_REPORT.md -->
<!-- version: 12 -->
# Validation Devnet `0.4.8-pre.013` — Metaplex `Create -> Mint`
## Statut
**Campagne multi-famille `Create -> Mint` fermée : NFT, SFT, fungible, collection et pNFT sont confirmés sur Devnet.**
Le 7 août 2026, une première campagne NFT a atteint la post-validation de `Create` puis sest arrêtée sur un diagnostic agrégé. Après `fix-007`, la seconde tentative localise une borne stateful incompatible avec le transport ; `fix-008` laligne sur `65536` octets. La troisième tentative franchit alors la lecture stateful, lhydratation canonique et lextraction Core : `Create` est réellement confirmée avec la signature `51MXdFFFNY7UTS23cmfCQBfrDKC99uk5Trtw2gkptvEug9aWJ2R1cE9Ta5QHbe4kS1NYgRUSSMmmdbqdhyxxnR4Y` au slot `481882374`, puis `fix-009` corrige le replay executor -> decoder. Les quatrième et cinquième tentatives atteignent ensuite une postcondition SPL qui exigeait à tort que lautorité opérateur survive à `Create` pour un NFT Master Edition. `fix-011` corrige cette attente vers le PDA Master Edition. La sixième tentative termine enfin la campagne : `Create` est confirmé au slot `481904731` avec une matérialisation, puis `Mint` au slot `481904744` avec une matérialisation et la transition SPL exacte supply/amount `0 -> 1`. `fix-012` promeut donc les deux opérations à `confirmed` sur la famille NFT dans la matrice opérationnelle. La campagne SFT suivante réussit également de bout en bout : `Create` est confirmé au slot `481908028`, `Mint` au slot `481908039`, chaque étape produit une matérialisation, et la supply ainsi que lamount passent de `0` à `10`. `fix-013` conserve donc `Create` et `Mint` comme seules opérations `confirmed`, désormais observées sur NFT et SFT ; les 18 autres opérations restent `not_run` et fungible/collection/pNFT restent à exécuter. La campagne fungible suivante réussit à son tour : `Create` est confirmé au slot `481910724`, puis `Mint` au slot `481910734`, avec une matérialisation par étape et la transition brute supply/amount `0 -> 1_000_000_000` sur un mint à 9 décimales. `fix-014` ajoute donc `fungible` aux bundles de preuves de `Create` et `Mint`. La campagne collection suivante réussit également de bout en bout : `Create` est confirmé au slot `481911942`, puis `Mint` au slot `481911955`, avec une matérialisation instructionnelle par étape, deux snapshots stateful et la transition supply/amount `0 -> 1`. `fix-015` ajoute `collection` aux bundles de preuves, corrige la régression du test de matrice resté figé sur NFT/SFT et laisse pNFT comme dernière variante `Create -> Mint` à exécuter.
## Première tentative NFT
Contrôles locaux avant réseau : `cargo fmt --all`, `cargo check --workspace`, audit workspace et tests de crate réussis ; `cargo clippy --all-targets` ne signalait qu'un warning `cloned_ref_to_slice_refs`, corrigé dans `fix-007`. La campagne réseau a ensuite échoué avec :
```text
metaplex_create_mint_post_execution_incomplete:
Metaplex create did not complete canonical replay and materialization
```
Ce message ne permettait pas d'identifier le sous-état fautif. Aucune promotion de matrice n'est autorisée à partir de cette tentative.
## Seconde tentative NFT
La validation locale de `fix-007` réussit avec `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets`, l'audit workspace, 78 tests unitaires `kb-pipeline-demo-scenarios` et 135 tests desktop. La nouvelle campagne NFT échoue ensuite avec le diagnostic exact :
```text
canonical_inserted=false, core_extracted=false, decode_replayed=false, materialized=false,
materialization_rows=0, materialized_snapshots=0,
diagnostics=["Metaplex stateful postcondition read failed: configuration error: getAccountInfo complete data limit must be between 1 and 65536 bytes"]
```
L'échec précède donc hydratation, extraction et replay : il provient du contrat de lecture stateful, pas d'une preuve que `Create` aurait échoué on-chain. La constante pipeline historique de `1 MiB` dépassait la borne du transport. `fix-008` remplace cette borne par `kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES`.
## Troisième tentative NFT
La validation locale de `fix-008` réussit avec `cargo check --workspace`, `cargo clippy --all-targets`, 106 tests unitaires `kb-pipeline`, 78 tests unitaires `kb-pipeline-demo-scenarios` et 135 tests desktop. La campagne produit ensuite :
```text
signature=51MXdFFFNY7UTS23cmfCQBfrDKC99uk5Trtw2gkptvEug9aWJ2R1cE9Ta5QHbe4kS1NYgRUSSMmmdbqdhyxxnR4Y
slot=481882374
canonical_inserted=true
core_extracted=true
decode_replayed=false
materialized=false
materialization_rows=0
materialized_snapshots=2
```
La transaction `Create` est donc confirmée et les deux snapshots stateful sont disponibles, mais le décodeur instructionnel refuse encore le replay. Le réaudit du contrat local montre que `validate_accounts("create")` imposait historiquement le mint signer et lupdate authority signer, alors que lexécuteur courant expose respectivement `mint_as_signer` et `update_authority_as_signer`. La fixture utilise volontairement `mint_as_signer=false` car le mint est créé dans la transaction de préparation. De plus, authority, payer et update authority partagent le même wallet : leurs flags visibles dans le replay sont des privilèges globaux de transaction et peuvent donc être des sur-ensembles des metas positionnelles. Le même problème aurait touché `Mint`, où token owner, authority et payer partagent également le wallet opérateur.
`fix-009` remplace pour `Create` et `Mint` légalité stricte des flags par des minima de privilèges requis, tout en conservant writable obligatoire sur les comptes optionnels réellement présents. Il ajoute aussi les compteurs détaillés du premier replay au diagnostic. Cette correction doit être validée localement puis la campagne NFT rejouée avant toute promotion de matrice.
## Frontière de la campagne
Une invocation couvre une seule famille afin de limiter le nombre de transactions et de rendre les diagnostics attribuables :
- `nft` ;
- `sft` ;
- `fungible` ;
- `collection` ;
- `programmable_nft` ou `pnft`.
Chaque invocation exécute trois transactions au maximum : préparation native du mint + ATA, `Create`, puis `Mint`.
## Preuves exigées
Pour `Create` et `Mint` :
- simulation réussie liée au message exact ;
- signature et confirmation Devnet ;
- slot de confirmation ;
- `after_state` Metaplex relu avec `minContextSlot >= confirmation_slot` ;
- hydratation canonique de la transaction ;
- extraction Core ;
- replay du décodeur Metaplex ;
- matérialisation instructionnelle ;
- seconde passe de replay sans nouvel output ;
- snapshot metadata et master edition lorsquelle est requise ;
- token record après `Mint` pour pNFT ;
- supply du mint et amount de lATA à zéro après `Create` ;
- supply du mint et amount de lATA exactement égaux à `mint_amount_raw` après `Mint`.
## Commande initiale — NFT
```bash
KB_DEVNET_METAPLEX_CREATE_MINT_CAMPAIGN_TEST=1 \
KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 \
KB_DEVNET_METAPLEX_CREATE_MINT_FAMILY=nft \
KB_POSTGRES_TEST_URL='postgresql://…' \
cargo test -p kb-pipeline-demo-scenarios \
optional_devnet_metaplex_create_mint_campaign_from_env \
-- --nocapture
```
`KB_DEVNET_PROFILE` et `KB_DEVNET_CONFIG_PATH` restent disponibles lorsque le profil par défaut nest pas celui à utiliser.
## Sortie attendue
La campagne imprime :
```text
METAPLEX_CREATE_MINT_FIXTURE ...
METAPLEX_CREATE_MINT_STEP operation=metadata.metaplex_token_metadata.create ...
METAPLEX_CREATE_MINT_STEP operation=metadata.metaplex_token_metadata.mint ...
```
Les cinq chemins NFT, SFT, fungible, collection et pNFT sont désormais qualifiés dans les bundles `familyEvidence` de `Create` et `Mint`. La campagne multi-famille `Create -> Mint` est donc fermée ; les opérations spécialisées suivantes restent qualifiées séparément.
## Quatrième tentative NFT — mismatch dautorité initialement mal interprété
La tentative suivant `fix-009` observe `3RJXuBF6xB3j47EeAHEBbcwvV66rJrkhmJvYNr36jz8x` comme mint authority et freeze authority au lieu de lopérateur `J12WA6c42oqpWkLu1dFa4pCegkQJPpxabwc3RRCMSUuH`. Cette sortie a dabord été interprétée comme une réutilisation de fixture. `pre.013-delta-fix-010` a donc rendu la préparation fresh-only avec `TemporaryWalletStore::create`, absence on-chain préalable et relectures liées au slot confirmé. Ce durcissement reste souhaitable, mais linterprétation « stale fixture » était incomplète.
Le réaudit du chemin exact montre que ce contrôle dautorité est exécuté seulement après `validate_confirmed_execution("create", ...)`. Le mismatch est donc postérieur à `Create`, et non antérieur à toute instruction Metaplex.
## Cinquième tentative NFT — pipeline `Create` complet, postcondition SPL incorrecte
Après `fix-010`, la validation locale confirme 701 tests unitaires `kb-lib`, 106 tests unitaires `kb-pipeline`, 78 tests unitaires de scénarios et 135 tests desktop ; un unique warning `dead_code` subsiste pour `read_token_account`. La campagne fresh-only observe ensuite `KsjdBdmUUM3r8Mw6cQESkXJfxmnPXSmR482963gmsyo` comme mint authority et freeze authority après `Create`, face à lattente opérateur `J12WA6c42oqpWkLu1dFa4pCegkQJPpxabwc3RRCMSUuH`.
Cette erreur intervient après `validate_confirmed_execution("create", ...)`. Par construction, `Create` a donc déjà franchi simulation, confirmation, hydratation canonique, extraction Core, replay, matérialisation instructionnelle, snapshots stateful et seconde passe idempotente. La campagne ne copiait cependant pas signature + slot dans cette erreur SPL, donc la preuve nest pas encore exploitable pour promouvoir `Create` dans la matrice.
Le contrat officiel Token Metadata explique la mutation : lors de la création dun NFT avec Master Edition, mint authority et freeze authority sont transférées au PDA Edition/Master Edition. `pre.013-delta-fix-011` attend donc ce PDA pour NFT, collection et pNFT, et conserve lopérateur pour SFT et fungible. Il maintient cette attente après `Mint`, ajoute signature + slot aux erreurs SPL post-confirmation et supprime le helper mort.
## Sixième tentative NFT — campagne complète confirmée
Après application de `pre.013-delta-fix-011`, la campagne NFT fresh-only termine les deux étapes et imprime les preuves suivantes :
```text
fixture family=Nft
mint=J6pDBZR4nWpM8Mj61fEdwQXH3KLZE8RuKSEsyNofC7bp
metadata=7JLnovNwuatxAessRcWCEuKb5jg2H93KVpmyHNzx4euG
master_edition=GxUFL8ZfosThA4hUVGPKDT8Zvcc2b2VmdjEiw6WQQYNx
token_account=9LBJCDFkJghjVDDeccTe4vjZH3dDNpzsWVXqyTKs81To
preparation_signature=2u4sdcFhmoeeYiAe8X761AmCnWyPLHSxYwzwwocLYkfVCan9W5i13ipvK4v3dWRe9E4iJRqBCHHFRh8MwR2o8FLh
Create:
signature=okwkuQFp7yVguYW7GEY9Chv6ochM8TWBEeCGV1gu5Waku9wQvMfrKjkig6JxXBwpKoFTECignzJ8HFf8dLtpsvS
slot=481904731
materializations=1
Mint:
signature=3hSizfdvezyFpfhqX4Za4wmhdMHhSPAisFvYjFAxpRzYcUXhryxCzc9qpwod3Yjm6GmijXTGjjDUuVksfpBzLwz1
slot=481904744
materializations=1
supply_before=0
supply_after=1
token_before=0
token_after=1
```
Le test ne peut retourner `ok` quaprès simulation réussie, confirmation, hydratation canonique, extraction Core, replay Metaplex, matérialisation instructionnelle, snapshots stateful et seconde passe idempotente pour chacune des deux transactions. La postcondition SPL vérifie en plus que la supply et lATA restent à zéro après `Create`, puis deviennent exactement `1` après `Mint`. Cette sortie constitue donc la première preuve complète de `pre.013`.
`METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json` passe en version 2 afin de conserver les familles observées et les preuves réellement qualifiantes. `Create` et `Mint` sont `confirmed` avec `observedFamilies=["nft"]`. Les 18 autres opérations restent `not_run`. Les valeurs de télémétrie qui nétaient pas imprimées par `fix-011` sont explicitement marquées comme telles au lieu dêtre inventées.
`fix-012` ajoute enfin une ligne `METAPLEX_CREATE_MINT_EVIDENCE` contenant, pour les prochaines familles, le genesis hash, le slot de simulation, le message hash, le fee, la signature, le statut/slot de confirmation et les états du pipeline. Les campagnes fungible, collection et pNFT pourront ainsi être reportées sans nouvelle perte de preuve ; la campagne SFT utilise déjà ce format machine-readable.
## Campagne SFT confirmée
Après application de `fix-012`, la campagne SFT termine sans erreur. La fixture fraîche utilise le mint `UhymxTPkMQJW3rzTa5Ff1FMhxgB4dPNRzqZM2ghgwZD`, la metadata PDA `47jQ8Smucco7QjSRa1gzR8pF7RXiugZW69eF2825svjt` et lATA opérateur `EF2epsY37WqXrGPuqB4VKCyMGMYcN1wVSkDZ2JNqQuJt`. La préparation native est confirmée par `53w14jG5KDEcDTfCAd2GDKmoJG1pRcVp9a9JZgzQ9DgFyPV8v5iYEGLdfAhS8jmhT8DaVodjDc4AqnrA6H7Fc5XM`.
### `Create` SFT
- genesis hash : `EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG` ;
- simulation context slot : `481908023` ;
- logs de simulation : `12` ;
- message hash : `6zv9nc8ARRfKx29LhiwAfzME8a5LSQ6AF4BUYdy1D3WE` ;
- fee : `5000` lamports ;
- signature : `2s81m6k9HYDLmCy7oXKaqBvtb62iyX4bJmij2f2VjHuoRQBBA6YqRuAsjyaM7JCaJXasbU3WCfiko91cZcDLLdxV` ;
- statut : `Confirmed` ;
- slot : `481908028` ;
- matérialisation instructionnelle : `1` ;
- snapshots : `0 -> 1` ;
- hydratation canonique, extraction Core, replay et seconde passe idempotente : réussis.
### `Mint` SFT
- simulation context slot : `481908034` ;
- logs de simulation : `7` ;
- message hash : `65VBkcnKtZdxLZgwTu669Voz57PqYJbQ8iAUe5jWUKJ6` ;
- fee : `5000` lamports ;
- signature : `4pAemhEJewuLAeqLkqwzPvVmsCRbkyLiN6uAUWBA5RMsofaEDt5Xr8YGiBEvLWQM7Qvxnd81mmCHpandmfmWuyAm` ;
- statut : `Confirmed` ;
- slot : `481908039` ;
- matérialisation instructionnelle : `1` ;
- snapshots : `1 -> 1` ;
- supply : `0 -> 10` ;
- amount ATA : `0 -> 10` ;
- hydratation canonique, extraction Core, replay et seconde passe idempotente : réussis.
La validation locale qui suit est propre : `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets`, audit workspace, 79 tests unitaires `kb-pipeline-demo-scenarios` et 135 tests desktop.
## Matrice v3 — preuves par famille
La campagne SFT montre quune simple liste `evidence` par opération nest plus suffisante : NFT et SFT ont leurs propres signatures, slots, message hashes et états. `fix-013` passe donc la matrice en version 3 et ajoute un bundle `familyEvidence` complet pour chaque entrée de `observedFamilies`. Une opération `confirmed` est refusée si une famille observée na pas exactement son bundle de preuves de simulation et de soumission. Lancien champ `evidence` reste réservé aux états non familiaux comme `unavailable`.
À ce stade, `Create` et `Mint` sont `confirmed` sur `nft` et `sft`. Les familles `fungible`, `collection` et `programmable_nft` restent à qualifier.
## Consolidation `pre.013-delta-fix-014` — qualification fungible
La campagne fungible `Create -> Mint` est confirmée de bout en bout sur Devnet. La fixture fraîche prépare le mint `8jBAXGfU95qVRSVeW67NxmuR6RaQxcRrqrDWF1kKn8u7` à 9 décimales et lATA opérateur `HHY3zXSjcmH4dkzNzE83NWrknGqWX4DFSPj8UeSNMLYT` avec supply et amount nuls ; la préparation native est confirmée par `39DyQvbnbgKaQxb36f4S1rjEuhH385jNAFe8CWH8j3dCgwSjgdkQMPJv2bzGLFLPet6GuC352QNGQLZ5vGhRT7wg`.
`Create` est simulé au slot `481910719` avec 12 logs, message hash `Ddpy6aTtWL4X1zvxmDxrU8W4uwBZMzLTs3AM5am5Wj9g` et fee `5000`, puis confirmé avec la signature `3J1Ebq9DVkupVFQmqQ84QaNeztwMuRwbtXj7CiyUWwZPj4KCAJpKLAbPvpYss5cdiY33foXXScWWZE98H7tvbZed` au slot `481910724`. La metadata PDA `J6W5PF4SESMvCoTw9H4uAoUvn6tZBWjiG9QWN3CZLyRE` est validée sans Master Edition, comme attendu pour cette famille. Hydratation canonique, extraction Core, replay, matérialisation et seconde passe idempotente sont propres.
`Mint` est simulé au slot `481910730` avec 7 logs, message hash `8SqHD12gMuaLXHcYLDx72eA2GVokTDDNnnxnuJPfaACh` et fee `5000`, puis confirmé avec `2GKiY9foLmioYtUHttA2D6nCSmsftRL1JDKxENHmCnb5w38FGSxwBiRNMfF7mH8htv3G45irmge6DVtr6VN359j2` au slot `481910734`. Une matérialisation instructionnelle et un snapshot stateful sont observés ; supply et amount ATA passent exactement de `0` à `1_000_000_000`, et le second replay est idempotent.
La matrice v3 conserve donc `Create` et `Mint` comme seules opérations `confirmed`, avec trois bundles familiaux indépendants `nft`, `sft` et `fungible`. Les 18 autres opérations restent `not_run`; les variantes `collection` et `programmable_nft` restent à exécuter avant les fixtures spécialisées.
## Consolidation `pre.013-delta-fix-015` — qualification collection
La campagne collection `Create -> Mint` est confirmée de bout en bout sur Devnet. La fixture fraîche prépare le mint `AhUDoidUMXy1aqMDQWBGTbdA8SFurXSfU665NbdyMoMZ`, la metadata PDA `D2BFXXDC3p9uy4YEaoRiPq65cu6XrSsVjZccJrxzqYFm`, la Master Edition PDA `HmqeFdeVPmetDRiscWnSfQT7QpJ7cQs4GbJJUfH6Jou8` et l'ATA opérateur `B12BbbZoeDjbJnTn8vods5ZSc9tPiUPuny3yjNV6Vj2b`. La préparation native est confirmée par `4MMxFf1c9crksnr1npyWp4u88TCWgABitS7HQcKbmejfhiBDL6Sacaum8UMUoj3iNdrp61abBoVWjhDRKw6iGK5J`.
`Create` est simulé au slot `481911937` avec 27 logs, message hash `GxWcLJaiA2zTMQBp6NuKCgHbnC1taCyAjuDH5n2375Ci` et fee `5000`, puis confirmé avec la signature `3jpxyWuxQVyBtbvqDnV1oahLyxCXMw1zQQZBGCKgQSfn2TNcBaUdfvJ6kFa8zL3XJLMpM6tahQTnu8AUsTkYc3Dy` au slot `481911942`. Une matérialisation instructionnelle et deux snapshots stateful sont observés ; metadata et Master Edition sont validées, la supply et l'amount ATA restent à zéro, et le second replay est idempotent.
`Mint` est simulé au slot `481911950` avec 7 logs, message hash `JCx6XUnQ7e7Veh4K8xtN8KrGFHafdQMRxAK9jej7nNZs` et fee `5000`, puis confirmé avec la signature `4WEciqZA5vmtrh1ZeSbqcgVnezUy7qT6B6utySfkg7dbAmhH6SMwwY862zGbzPW3nBVFzMjFVGPEhnTt6TcAiEe2` au slot `481911955`. Une matérialisation instructionnelle et deux snapshots stateful sont observés ; supply et amount ATA passent exactement de `0` à `1`, et le second replay est idempotent.
La matrice v3 conserve donc `Create` et `Mint` comme seules opérations `confirmed`, avec quatre bundles familiaux indépendants `nft`, `sft`, `fungible` et `collection`. Les 18 autres opérations restent `not_run`; seule la variante `programmable_nft` reste à exécuter avant les fixtures spécialisées.
## Première tentative pNFT — `Mint` confirmé, postcondition SPL trop stricte
La campagne `programmable_nft` confirme `Mint` avec la signature `Gkg5A6ehuSzkU1N318kYoRHm5RdxV2yBRpaK9Gwyt6vN1ToYwAS6Vzpk2rQYWS9NJz1rnUpypLCn7DqWFzQzxFc` au slot `481913765`. Après confirmation, le compte token classique relu contient exactement le mint `Ecn3v1u21DKNhpFX1tqKvieVn5CkhMMukxWgoN59Hf2E`, lowner `J12WA6c42oqpWkLu1dFa4pCegkQJPpxabwc3RRCMSUuH`, `amount=1` et `state=2`. Le test échoue uniquement parce que la postcondition commune attendait encore `state=1`.
`state=2` est `Frozen` dans le contrat SPL Token et correspond au comportement attendu dun pNFT : les opérations passent par Token Metadata et son Token Record au lieu de permettre les mutations SPL directes. `pre.013-delta-fix-016` exige donc `Initialized(1)` avant `Mint`, `Frozen(2)` après `Mint` uniquement pour pNFT, et conserve `Initialized(1)` pour NFT/SFT/fungible/collection.
La sortie machine-readable ajoute `tokenAccountStateBeforeMint` et `tokenAccountStateAfterMint`. Le rerun pNFT doit retourner `ok`, prouver la présence du Token Record et produire `1 -> 2` avant ajout du bundle `programmable_nft` à la matrice v3.
## Deuxième tentative pNFT — campagne complète confirmée
Après `pre.013-delta-fix-016`, le rerun `programmable_nft` termine sans erreur et ferme la qualification des cinq familles. La fixture fraîche utilise :
- mint : `4tu87iUVr8hyGGRTVkYMGww7HLMZRuLLFn7MLXxzkmXA` ;
- metadata : `Hvjpj78kPgWb14xoEuLPzqckYbjPRJaBxVRNDNX3qMBL` ;
- Master Edition : `cVJELihnvgWeRmwQ3HHJAoXyrTPtZjpMZjwWMsJPzAx` ;
- ATA opérateur : `BP8AAcqfAM4bKAZxnifKpTQM29QKXmkiDBoAYLT69ahS` ;
- Token Record : `Ejp6LTUMxXTFPDDM7iZGsutnCUriQBnArofikMsc97op` ;
- préparation native : `gHNtnEwTdw8Uyb32Fjkg9rdJ6ET9mg6Svnc1vXSZjBLYk7upcJUUBXu7VbtvXqYttGbvKQgnGEC2aLDkosvDjxw`.
### `Create` pNFT
- simulation context slot : `481921975` ;
- logs de simulation : `27` ;
- message hash : `4v1opnPoD8YRYr5ti5dFXjSBGbZ6gWCLfg4DwCErwtee` ;
- fee : `5000` lamports ;
- signature : `515t45UC5KPpGocEHtAuDf2qEqzeJt9TUrbUD1zhAFaXRHnefcu3MHAZTc1JqjSuscDgDSYFVTBX3AKJLk1G8fsN` ;
- statut : `Confirmed` ;
- slot : `481921980` ;
- matérialisation instructionnelle : `1` ;
- snapshots : `0 -> 2` ;
- hydratation canonique, extraction Core, replay et seconde passe idempotente : réussis.
### `Mint` pNFT
- simulation context slot : `481921989` ;
- logs de simulation : `19` ;
- message hash : `86raN9VfGwk1BymXf2wfkeQmMxnyPTyx49s3gxYoRH6b` ;
- fee : `5000` lamports ;
- signature : `2kYgd9zpY5xjWSfpoDzvLkjy6KhKFNPsXWfxK4jFrnD15z2BaVUgaxKNpJNiJXP275YgR8LAEzn32Lc7qPq9GdH8` ;
- statut : `Confirmed` ;
- slot : `481921994` ;
- matérialisation instructionnelle : `1` ;
- snapshots : `2 -> 3` ;
- supply : `0 -> 1` ;
- amount ATA : `0 -> 1` ;
- état ATA : `Initialized(1) -> Frozen(2)` ;
- Token Record absent avant `Mint`, présent et validé après `Mint` ;
- hydratation canonique, extraction Core, replay et seconde passe idempotente : réussis.
`pre.013-delta-fix-017` ajoute donc `programmable_nft` comme cinquième bundle `familyEvidence` de `Create` et `Mint`. Les deux opérations restent les seules entrées `confirmed`, mais leur qualification multi-famille est désormais complète `5/5`. Les 18 autres opérations restent `not_run`.

View File

@@ -0,0 +1,81 @@
<!-- file: docs/validation/V0_4_8_PRE_013_METAPLEX_ESCROW_DEVNET_VALIDATION_REPORT.md -->
<!-- version: 3 -->
# Validation Devnet `0.4.8-pre.013` — Token Owned Escrow
## Statut
**Qualifié sur Devnet — les trois opérations escrow sont `confirmed`.**
Le rerun du 8 août 2026 après `pre.013-delta-fix-032` termine entièrement la campagne et les checks/tests locaux annoncés restent sans régression. La correction du compte terminal `authority=None` permet aux builders escrow de produire les formes officielles sans faux signer.
La matrice passe de 11 `confirmed`, 1 `unavailable`, 8 `not_run` à **14 `confirmed`, 1 `unavailable`, 5 `not_run`**.
## Fixture qualifiée
```text
parent_mint=7Sib8Ugxi3urHBRAWCqdGGXD24em7QN3Vdp69f2RJNRM
parent_metadata=8rg9f1wt9wbqNsVBwv4PPiUSLFgsLrLPSzs6cddPhtyo
parent_edition=5j7kV9eFuFzXaAzqF6Gb6pG2sLG9iAsWq2YSMUn1kUGJ
parent_token=EEGchyQPrimCWs2DyQq7BDRYvrsVs5ctMa4sTjZu6erk
attribute_mint=C5e4ecUGoo1y94uXDNVTDfmiMGjGtir27YtLxnv8kfKU
attribute_token=2kPTg6P48zgDbefZKWMSpmVjXXv2aH61ah2DRZwpsne1
escrow=274ZJGXGVZ1ra2tkUvewtB21kNxSmALaHdHg8K35tVof
escrow_attribute_token=3bnvR635UFZm52DA38VXXjZzQ547f4kWzp47d9VBV6q6
```
Préparation SPL confirmée :
- ATA attribut escrow : `51K4hDTvyBCTZ1MNXhU7WZzgZiRbQyZbdbHAdVs1WfADB7PMhVszCeKqxGUvx3vEatXGJs2r15tHZ7T1ubxGoU1v`, slot `482147222` ;
- dépôt dune unité brute : `37B86chCnjWXPEbjExHry8XELn7UC8VbuTQnYe4KeKeYDikwXHZF4dPhNS27NSGXpzCJZjzUWyuJeRccz6DJWV2E`, slot `482147239`.
## Preuves Metaplex
### `CreateEscrowAccount`
- simulation : slot `482147200`, 13 logs, succès ;
- message hash : `E1V2rLgFqjxpyBVu1dkp32VNkZ3V3iTdBTPv4j8mmwSj` ;
- frais : `5000` lamports ;
- signature : `Yy7NEvme3obuiGRBbBjpuk3dv4aPMPBQ2ns7KmbY4K5EtyAimaaM8qN5SQzEqr9SPbRu1VDCFkNpAjnRGW4JYuj` ;
- confirmation : `Confirmed`, slot `482147206` ;
- hydratation canonique, extraction Core, decode replay, matérialisation et idempotence : réussies ;
- matérialisation : 1 ligne instructionnelle et 2 snapshots.
### `TransferOutOfEscrow`
- simulation : slot `482147246`, 10 logs, succès ;
- message hash : `FnDRr8JPNPzyECSYjzgN1ABuhyS88iQNxcrS7M4L21WD` ;
- frais : `5000` lamports ;
- signature : `4yJcSvnZJqihfZTE7nxN3o7H7zBCacpdZdwNQ1ueWQMmQZxR7KRb99C7rfrXX2AyezfSStMdqqTYL3oEWgwRhFi3` ;
- confirmation : `Confirmed`, slot `482147252` ;
- hydratation canonique, extraction Core, decode replay, matérialisation et idempotence : réussies ;
- matérialisation : 1 ligne instructionnelle et 2 snapshots.
### `CloseEscrowAccount`
- simulation : slot `482147262`, 4 logs, succès ;
- message hash : `8y6uvagE6Z1k2adnR7Vrk8MDQDPCPx1jY9fQ9TMyb95L` ;
- frais : `5000` lamports ;
- signature : `3GuwmGf9JEK7rHE3tudnxskvruyML9AGVXQimxii3uHdXqNEF4ScpmDXwhU3y2eaUnTbBPPhVkhRfLwJr9jbGJBo` ;
- confirmation : `Confirmed`, slot `482147267` ;
- hydratation canonique, extraction Core, decode replay, matérialisation et idempotence : réussies ;
- matérialisation : 1 ligne instructionnelle et 1 snapshot.
## Postconditions stateful
```text
deposit_amount=1
operator_before=1000000000
operator_after_deposit=999999999
escrow_after_deposit=1
operator_after_transfer_out=1000000000
escrow_attribute_closed=true
escrow_closed=true
parent_amount_after_close=1
```
Le Token Owned Escrow est donc créé avec son parent NFT, reçoit indirectement lunité SPL via son ATA, restitue exactement cette unité, ferme lATA devenue vide, puis se ferme lui-même sans modifier la possession du NFT parent.
## Décision
`CreateEscrowAccount`, `TransferOutOfEscrow` et `CloseEscrowAccount` sont promus à `confirmed` pour la famille `nft`. La campagne escrow nest plus un blocant de `pre.013`.

View File

@@ -0,0 +1,74 @@
<!-- file: docs/validation/V0_4_8_PRE_013_METAPLEX_FINAL_VALIDATION_REPORT.md -->
<!-- version: 1 -->
# Validation finale `0.4.8-pre.013` — Metaplex Token Metadata
## Statut
**Terminée et validée.**
La prerelease ferme le réaudit fonctionnel et réseau de Metaplex Token Metadata. La matrice canonique `test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json` contient les 20 opérations courantes et ne possède plus aucune ligne `not_run`.
| Statut | Nombre |
|---------------|-------:|
| `confirmed` | 15 |
| `unavailable` | 5 |
| `not_run` | 0 |
| total | 20 |
## Opérations `confirmed`
Les opérations suivantes disposent de preuves Devnet qualifiantes avec simulation, confirmation, hydratation canonique, extraction Core, replay, matérialisation et idempotence selon leur parcours :
- `Create` et `Mint` sur NFT, SFT, fungible, collection et pNFT ;
- `Verify` et `Unverify` sur collection parent-membre ;
- `Print` et `Burn` sur NFT imprimable ;
- `Delegate`, `Revoke`, `Lock`, `Unlock` et `Transfer` sur lifecycle pNFT ;
- `CreateEscrowAccount`, `TransferOutOfEscrow` et `CloseEscrowAccount` sur Token Owned Escrow ;
- `Update` sur NFT, avec `primary_sale_happened: false -> true`.
Les signatures, slots, message hashes et postconditions détaillés restent enregistrés dans la matrice et les rapports de campagne spécialisés de `docs/validation/`.
## Opérations `unavailable`
Les cinq statuts négatifs sont issus de preuves runtime réelles et ne sont pas des suppositions :
- `Use` : simulation Devnet rejetée par `InvalidInstructionData`; aucune soumission ;
- `Resize` : `Custom(201)` / compte déjà redimensionné sur le layout courant ; une réussite positive exige un état legacy sous-dimensionné contrôlé ;
- `Migrate` : `Custom(75)` / instruction retirée ;
- `Collect` : `Custom(7)` avec l'opérateur contrôlé ; la surface exige l'autorité Metaplex fixe de collecte ;
- `CloseAccounts` : `Custom(188)` avec l'opérateur contrôlé ; la surface exige l'autorité Metaplex ownerless-close fixe.
Aucune simulation négative n'est soumise. Aucun actif tiers ni aucune autorité réservée n'est imité pour fabriquer une validation positive.
## Validation locale finale
Après `pre.013-delta-fix-036`, l'état réel du workspace fourni par l'opérateur a produit :
```text
cargo fmt --all OK
cargo check --workspace OK
cargo clippy --all-targets OK, aucun warning
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit clean
Rust export completeness audit 0 candidate(s)
Khadhroony workspace rule audit clean
cargo test -p kb-pipeline-demo-scenarios
unitaires 105 passed
CLI 1 passed
API externe 1 passed
```
La campagne maintenance opt-in a également terminé `ok` avec `Update` confirmé et les quatre probes négatifs aux codes attendus.
## Frontières conservées
- Metaplex Token Metadata reste distinct de Token-2022 Token Metadata et de Solana Program Metadata ;
- les opérations historiques, remplacées ou dépréciées ne sont pas réinterprétées comme opérations courantes ;
- les statuts réseau ne sont jamais promus depuis une preuve synthétique ;
- le fetch off-chain reste hors périmètre de `0.4.8` ;
- les campagnes Devnet restent réexécutables pour détecter une régression ou un changement de runtime, mais ne constituent plus des tâches ouvertes de `pre.013`.
## Passage à `0.4.8-pre.014`
Le prochain développement non-fix peut ouvrir `0.4.8-pre.014`, consacré à la finalisation desktop metadata. Il doit réutiliser les contrats et statuts désormais fermés de `pre.013` sans rouvrir une campagne Metaplex fondamentale sauf régression observée.

View File

@@ -0,0 +1,91 @@
<!-- file: docs/validation/V0_4_8_PRE_013_METAPLEX_MAINTENANCE_DEVNET_VALIDATION_REPORT.md -->
<!-- version: 5 -->
# Validation Devnet `0.4.8-pre.013` — maintenance Metaplex finale
## Statut
**Qualifiée — aucune opération courante ne reste `not_run`.**
Le rerun `fix-035` ferme les cinq dernières lignes de maintenance. La matrice Devnet Metaplex Token Metadata contient désormais **15 opérations `confirmed`, 5 `unavailable` et 0 `not_run`**.
## Fixture qualifiante
- mint NFT : `66RQj9bF5MhzE7ZBwxvxpPTj9yaGMLmBAbTmysjrsDix` ;
- Metadata : `9e3wJgmRQnZGogM5c8yPSB5kNtg9EYYaCfqjaMB1vzXn` ;
- Master Edition : `3sCz28QcwqwrN5h5siJ617JbDCk7PwSDBUWgcpgXPTaM` ;
- token account opérateur : `12aMBDfAUvoPXA6qKY7p5nHb6Tck63TUSxZ4zKoCjnuw`.
## `Update` — `confirmed`
L'opération courante `metadata.metaplex_token_metadata.update` est exécutée sur le NFT frais et prouve la transition bornée `primary_sale_happened: false -> true`.
- simulation : succès au slot `482162408`, 5 logs ;
- message hash : `EakKCkQNhGyGivUcaeuSJx5F9PFVSpJY4JBsDFKNvRo8` ;
- fee : `5000` lamports ;
- signature : `4hxxBnsCA1JBJPNHgXRDeq1W7rXbFj2yySscWJKxU1VTMDcPzPfoWy7QzANHSDsbQfxxcwuAQNDLqEukgsug6qoZ` ;
- confirmation : `Confirmed`, slot `482162413` ;
- hydratation canonique, extraction Core, replay et matérialisation : réussis ;
- matérialisation : 1 instruction et 1 snapshot ;
- seconde passe idempotente : propre.
`Update` passe donc à `confirmed` sur la famille `nft`.
## `Resize` — `unavailable`
La simulation au slot `482162417` atteint `IX: Resize`, consomme `14643` CU et retourne :
```text
InstructionError[0] = Custom(201)
Program log: Account has already been resized
```
Les comptes créés par l'API actuelle sont déjà au layout cible. Une confirmation positive demanderait un compte legacy sous-dimensionné contrôlé ; la campagne ne fabrique pas artificiellement un compte program-owned et ne revendique pas un actif tiers. Aucune transaction `Resize` n'est soumise.
## `Migrate` — `unavailable`
La simulation au slot `482162422` consomme `7614` CU et retourne `Custom(75)` :
```text
This instruction was deprecated in a previous release and is now removed
```
L'opération est retirée du runtime courant. Aucune transaction n'est soumise.
## `Collect` — `unavailable`
La simulation au slot `482162426` consomme `2286` CU et retourne `Custom(7)` / `UpdateAuthorityIncorrect` :
```text
Update Authority given does not match
```
La surface courante est réservée à l'autorité de collecte Metaplex fixe. Le scénario contrôlé n'essaie pas de l'usurper et ne soumet aucune transaction.
## `CloseAccounts` — `unavailable`
La simulation au slot `482162430` consomme `4625` CU et retourne `Custom(188)` / `InvalidCloseAuthority` :
```text
IX: Close Accounts
The close authority needs to be revoked by the Utility Delegate
```
La surface courante exige l'autorité ownerless-close fixe. Le scénario ne l'usurpe pas et ne soumet aucune transaction.
## Validation locale observée
Le même état de travail a produit :
- `cargo fmt --all` : OK ;
- `cargo check --workspace` : OK ;
- `cargo clippy --all-targets` : OK, aucun warning ;
- `python3 scripts/audit_rust_workspace_rules.py` : `General Rust rule audit: clean`, `Rust export completeness audit: 0 candidate(s)`, `Khadhroony workspace rule audit: clean` ;
- `cargo test -p kb-pipeline-demo-scenarios` : 105 unitaires, 1 CLI et 1 test API externe, tous réussis ;
- campagne maintenance opt-in : réussie intégralement.
## Conclusion
La matrice des 20 opérations courantes est fermée : **15 `confirmed`, 5 `unavailable`, 0 `not_run`**. Les statuts `unavailable` restent explicitement qualifiés par leur cause : divergence runtime pour `Use`, besoin d'un état legacy contrôlé pour `Resize`, retrait runtime pour `Migrate`, et autorités Metaplex réservées pour `Collect` et `CloseAccounts`.
La consolidation documentaire et de conformité de `0.4.8-pre.013` est désormais effectuée. Le prochain développement non-fix peut ouvrir `0.4.8-pre.014` pour la finalisation desktop metadata ; aucune campagne Metaplex fondamentale supplémentaire n'est requise par l'état courant.

View File

@@ -0,0 +1,202 @@
<!-- file: docs/validation/V0_4_8_PRE_013_METAPLEX_PNFT_LIFECYCLE_DEVNET_VALIDATION_REPORT.md -->
<!-- version: 7 -->
# Validation Devnet `0.4.8-pre.013` — cycle pNFT delegates
## Statut
**Qualifié sur Devnet — le rerun `fix-028` termine les six exécutions, toutes les postconditions stateful et le bundle pipeline/idempotence. `Delegate`, `Revoke`, `Lock`, `Unlock` et `Transfer` sont promus par `pre.013-delta-fix-029`.**
La validation locale de `fix-023` n'avait exécuté aucune transaction pNFT : `cargo check --workspace` passait, mais `cargo clippy --all-targets` et `cargo test -p kb-pipeline-demo-scenarios` échouaient pendant la compilation du bloc `#[cfg(test)]`. `fix-024` a corrigé ce harness sans modifier le graphe.
Le premier rerun Devnet de `fix-024` atteint ensuite une vraie transaction `Create` pNFT :
- signature : `PNkQAnykFoHVsREkhDbTW9eccyJoQvQbRZ2MJ4FsFRXyXint9fj6FT9PW45W5R3yi9HJ2aiSZXssH6B6pmyCcEz` ;
- slot confirmé : `482096529` ;
- mint frais : `DtBji9AYCqLRB48LFEFir427Kwy4Ehie6Pc8S87aQ6Rb` ;
- Master Edition : `HmCuW752ukBChVTbXtKJphUwJTJDdSycrgs6SrU2nT4B`.
La transaction est confirmée mais le bundle de post-validation s'arrête avant hydratation/replay/matérialisation complets, sur `MetaplexTokenMetadataAccountKind::Edition`: le décodeur rejette la Master Edition compacte pNFT avec `metadata account layout is invalid`.
Le diagnostic officiel est exact : `MAX_MASTER_EDITION_LEN=20`, les 18 premiers octets portent la sérialisation Borsh maximale, l'avant-dernier octet est réservé au fee flag et le dernier au `TokenStandard`. `Create` écrit explicitement `ProgrammableNonFungible` dans ce dernier byte pour une pNFT. `fix-021` avait à tort soumis ces deux octets au contrôle générique de padding nul. `fix-025` sépare donc le padding réservé du trailer sémantique et valide ce dernier de manière bornée.
Le rerun `fix-025` valide ensuite localement lensemble du workspace (`cargo check`, Clippy, audit, `kb-lib` 705/705, `kb-pipeline` 107/107, scénarios 96/96 et desktop 135/135) et franchit complètement `Create -> Mint`. La première opération lifecycle est alors soumise et confirmée :
- opération : `Delegate(StakingV1)` ;
- signature : `4S2uDrVU6ci59eKeGtGeXoUCbsUr4WxzSa7uYhmBxLtjKfNwpBkZRqm59V9cfbD99oE9AugMYppiQqnMbV2Hy1p7` ;
- slot confirmé : `482106293` ;
- hydratation canonique : réussie ;
- extraction Core : réussie ;
- replay : `failed_inputs=1`, aucune observation décodée ;
- matérialisation : non atteinte.
Le défaut est la même frontière conceptuelle que celle déjà corrigée pour `Create`, `Mint`, `Print`, `Verify` et `Unverify` : les flags du Core replay sont les privilèges globaux du message, pas une copie stricte des `AccountMeta` de linstruction. Dans cette campagne, lautorité pNFT est aussi le fee payer ; la position `Delegate.authority`, officiellement signer readonly, apparaît donc signer+writable dans le replay. `fix-026` valide uniquement les minima obligatoires et conserve writable obligatoire pour les comptes optionnels réellement mutés.
Le même correctif est appliqué préventivement à `Lock/Unlock` : leur `token_owner` est le wallet opérateur et donc également le fee payer, ce qui lui donne légitimement des privilèges globaux signer+writable alors que lAccountMeta positionnel est readonly.
Le rerun `fix-026` valide ensuite entièrement la base locale :
```text
cargo fmt --all : ok
cargo check --workspace : ok
cargo clippy --all-targets : ok
audit_rust_workspace_rules.py : clean
kb-lib : 706/706
kb-pipeline : 107/107 + 2 API externes
kb-pipeline-demo-scenarios : 96/96 + CLI + API externe
kb-app-demo-desktop : 135/135
```
La campagne franchit alors `Delegate(StakingV1)` beaucoup plus loin que lors du run précédent. `validate_confirmed_execution("delegate-staking", ...)` réussit, ce qui prouve pour cette nouvelle transaction : simulation réussie, confirmation avec slot, hydratation canonique, extraction Core, replay décodé, matérialisation, snapshots stateful et seconde passe idempotente propre. La sortie échoue seulement ensuite sur la postcondition du TokenRecord :
```text
expected state=Unlocked
expected delegate=Some("CPZBygeMzo6WVXiHoc4xarkFP3xo92hD1tuDZU4SCG5t")
expected role=Some("Staking")
observed state=Some("Unlocked")
observed delegate=None
observed role=Some("Staking")
```
La nouvelle signature et son slot ne sont pas imprimés avant cette erreur terminale ; ils ne sont donc pas inventés ni utilisés pour une promotion. La signature `4S2uDr...` au slot `482106293` reste la preuve partielle du run `fix-025`, pas celle du rerun `fix-026`.
Le programme Metaplex courant écrit pourtant `token_record.delegate = Some(delegate)` et `token_record.delegate_role = Some(role)` ensemble pour les token delegates pNFT. Le défaut est local à notre projection : `DcMetadataMtmTokenRecordAccountSnapshot` calcule déjà `delegate` et `locked_transfer` comme chaînes base58, mais son `payload_json` était obtenu indépendamment par `serde_json::to_value(&mpl_token_metadata::accounts::TokenRecord)`. La couche stateful republie ce JSON brut ; la campagne cherche ensuite `payload_json["delegate"].as_str()`. Cette dépendance à la représentation Serde interne de `Pubkey` est incorrecte.
`fix-027` construit donc explicitement le JSON canonique du TokenRecord à partir des champs normalisés : `state`, `delegate`, `delegate_role`, `locked_transfer`, `rule_set_revision` et `bump`. Les adresses optionnelles deviennent des chaînes base58 ou `null`. La régression `token_record_decodes_programmable_state_delegate_and_exact_pda` vérifie désormais aussi les valeurs JSON `delegate`, `delegate_role` et `locked_transfer`. La postcondition lifecycle reste stricte : elle exige toujours ladresse exacte du delegate et ne transforme pas cet incident de projection en tolérance fonctionnelle.
À lissue du rerun `fix-026`, la campagne cible toujours cinq opérations courantes `not_run` : `Delegate`, `Revoke`, `Lock`, `Unlock` et `Transfer`. `Delegate(StakingV1)` possède alors une preuve de confirmation partielle mais ne peut pas être promu avant un bundle final imprimé ; les quatre autres opérations ne sont pas encore atteintes à ce stade historique.
Le rerun `fix-027` franchit ensuite la projection TokenRecord corrigée et poursuit toute la campagne jusquau `Transfer`. Lerreur terminale est désormais :
```text
source pNFT token account is invalid after Transfer:
expected amount=0 and state=Frozen;
observed amount=0 and state=1 (Initialized)
```
Cette erreur survient après `validate_confirmed_execution("transfer", ...)` et après validation du TokenRecord destination `Unlocked` sans delegate. Elle prouve donc que les étapes `Delegate(Staking)`, `Lock`, `Unlock`, `Revoke(Staking)`, `Delegate(Transfer)` et `Transfer` ont toutes franchi leur exécution et leur pipeline post-exécution interne lors de ce run. Les signatures et slots ne sont cependant imprimés quaprès construction complète du résumé ; léchec final empêche donc encore lémission du bundle `METAPLEX_PNFT_LIFECYCLE_STEP/EVIDENCE` et aucune promotion nest effectuée.
Le comportement observé est celui du programme Metaplex courant : `frozen_transfer` thaw le compte source, effectue le transfert SPL, puis freeze uniquement le compte destination. Il nexiste pas de second freeze du compte source. Après transfert intégral dune pNFT, létat attendu est donc :
```text
source ATA amount = 0
source ATA state = Initialized
destination ATA amount = 1
destination ATA state = Frozen
```
`fix-028` corrige uniquement cette postcondition et expose explicitement les deux états token dans `METAPLEX_PNFT_LIFECYCLE_STATE`.
## Graphe de campagne
```text
Create pNFT -> Mint pNFT
|
v
Delegate(StakingV1)
|
v
Lock -> Unlock
|
v
Revoke(StakingV1)
|
v
Delegate(TransferV1)
|
v
Transfer(delegate -> fresh destination)
```
Le Staking delegate et le Transfer delegate sont volontairement deux rôles successifs : le premier qualifie le cycle lock/unlock, le second le transfert délégué.
## Postconditions requises
Token Record source :
```text
after Mint : Unlocked, no delegate
after Delegate Staking : Unlocked, delegate=<fresh>, role=Staking
after Lock : Locked, delegate=<fresh>, role=Staking
after Unlock : Unlocked, delegate=<fresh>, role=Staking
after Revoke : Unlocked, no delegate
after Delegate Transfer : Unlocked, delegate=<fresh>, role=Transfer
```
Après `Transfer` :
```text
source ATA amount = 0
destination ATA amount = 1
source ATA state = Initialized
destination ATA state = Frozen
destination TokenRecord = Unlocked
destination delegate = none
```
Chaque étape doit aussi fournir simulation réussie, signature, confirmation, hydratation canonique, extraction Core, replay, au moins une matérialisation, snapshots stateful et seconde passe idempotente.
## Commande opérateur
```bash
KB_DEVNET_METAPLEX_PNFT_LIFECYCLE_CAMPAIGN_TEST=1 \
KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 \
KB_POSTGRES_TEST_URL='postgres://solana:solana@localhost:5432/solana_test' \
cargo test -p kb-pipeline-demo-scenarios \
optional_devnet_metaplex_pnft_lifecycle_campaign_from_env \
-- --nocapture
```
Une exécution complète doit produire :
```text
METAPLEX_PNFT_LIFECYCLE_FIXTURE ...
METAPLEX_PNFT_LIFECYCLE_STEP ... x6
METAPLEX_PNFT_LIFECYCLE_STATE ...
METAPLEX_PNFT_LIFECYCLE_EVIDENCE ...
```
## Qualification finale `fix-028`
Le rerun final retourne `ok` avec la fixture suivante :
```text
mint = 9oEszJo4wT4Qodyrq7D3wD723ZFckiqYL4yB3JcJXFYc
metadata = AWQwkdHkqjRiJ3Mz3CzmHgBwEUK7BSmpLTLr47S5799H
master edition = E2QV5v91hvHnYDgnwQ3dh6deWYt5CtEMEYYEUsGAqZoN
source token = 8ityw4e6phC1mdHYrvG2rrwTRmXa47W3e1tpXTfz2JcB
source token record = FkbrKCQMejNwXVfuAqkfcM6Cw2LDsGJ5nYPUs2GEUhgp
delegate = GcfXJhbS4fKaANtqt7pAuiFTR4S3jRAFGXv83bqbbWcQ
destination owner = ForoAqqvXENKD7GStMb9kEpe2dKGeHCZBxRkcmSaSiui
destination token = HbrpZbTu9MzQJrmnv3DYJQRXEkNMKNDFQCRxHxhzePEb
destination token record = 3FcpMMmLUAyHW7duEqPFQiYCuSiLtvWkbBcZtfYGyaLo
```
Les six exécutions réelles sont :
| Étape | Signature | Simulation | Confirmation |
|------------------------|--------------------------------------------------------------------------------------------|-----------:|-------------:|
| `Delegate(StakingV1)` | `mFpH2SUipx4E5fJod1kgypRBrD5HE7M6ZJen5T3KYvXPqNcU64KiAktSakZ7tB8oezrAiZTn6FdUkx48aheZN5N` | 482113797 | 482113803 |
| `Lock` | `676sMvUwBg3G2JZK9gWYFgoHfRJ3sSyVR5Luk3AqtqgQeLnFvQZptqdUSG27Np1qiVj8vB2XncjfLCJ3hzCQN2EZ` | 482113807 | 482113812 |
| `Unlock` | `4suhVVV6QwdSW1P1BX3NBMpRF4Yd7xVEUQ9KnvSLEUVygs4Yb4K5psKgxpod6m3Q4S28KAjPTp575ERRVbovYSTr` | 482113816 | 482113822 |
| `Revoke(StakingV1)` | `4Av8nq4QNTjqGGLAwGe9Syn6YVtpvNbjJ1JGfrK8CGyweG8mt5N9Ua5WFFwYGYcj3Nbu1pCu7bVCJ8hiopiuMKfk` | 482113826 | 482113831 |
| `Delegate(TransferV1)` | `KR44kRqhN8rS8bktvFDSjJnKDFGUwr6hmtTTocMBfhvuqXP8aeCQ78jTPXvianbi4wPRxQig2fFNt1ffqDTKhx1` | 482113836 | 482113840 |
| `Transfer` | `35ABd7yhmtL6YiA4UHoxM3TvGsJu1nZDvHXy2geLyNPj25ASaE7918SswSfEKRMMNLQ8TvagQehzR8F4Z4R8JHXa` | 482113845 | 482113850 |
Chaque étape possède `simulationSuccess=true`, `canonicalHydration=true`, `coreExtraction=true`, `decodeReplay=true`, `materialized=true`, exactement une matérialisation instructionnelle, un snapshot matérialisé et `idempotenceReplayClean=true`. Les fees observés sont 5000 lamports pour les deux `Delegate` et `Revoke`, puis 10000 lamports pour `Lock`, `Unlock` et `Transfer`.
La transition finale est :
```text
initial = Unlocked
staking role = Staking
after Lock = Locked / Staking
after Unlock = Unlocked / Staking
after Revoke = no delegate
transfer role = Transfer
source ATA after Transfer = amount 0 / Initialized
destination ATA after Transfer = amount 1 / Frozen
destination TokenRecord = Unlocked / no delegate
```
Cette preuve ferme le lot pNFT et autorise la promotion des cinq opérations canoniques `Delegate`, `Revoke`, `Lock`, `Unlock` et `Transfer` sur la famille `programmable_nft`. La matrice Devnet passe ainsi de 6/20 à 11/20 opérations confirmées.

View File

@@ -0,0 +1,117 @@
<!-- file: docs/validation/V0_4_8_PRE_013_METAPLEX_PRINT_BURN_DEVNET_VALIDATION_REPORT.md -->
<!-- version: 4 -->
# Validation Devnet `0.4.8-pre.013` — Metaplex `Print -> Burn`
## Statut
**Qualifié sur Devnet.**
Le rerun suivant `pre.013-delta-fix-022` termine `ok` et fournit les preuves complètes de `Print` et `Burn`. La matrice Devnet v3 peut promouvoir les deux opérations sur la famille `nft`.
## Fixture qualifiée
```text
master_mint = EjKZta4BpT1r9p8ECcANdxi7XQ6pjCBjuRpRp69EDCRc
master_metadata = GHiG8W9eBbBQpw7efLX59wLnKwyMTSLJ7xPZ8x1ZDWr5
master_edition = ARfpaZm4avbTRsFKFCHvr2xQQNepHV3QQ5uoR7hG1A86
master_token_account = 375W7k6uMhwQKJTywyi5tSdaGD1hJvzZWScVoEYqQ1qi
edition_mint = 5ZNDeUKSWRbWTppEk5v3Rk6pd5ETrgjCzKbM55JfM1nU
edition_metadata = BHPfP3RJqfw8jYXm8uoKz2QpHqs9tpj6apC6bZeVgoWt
edition = Cb7m21Kok8tWrzymtbY87T6WP6x2Eo4yLmkkthGL7mQG
edition_token = E94rbzWFgYRPXGEdmHXrLHnHXzHYyobzMKpgvchCf3vV
edition_marker = 2fnAxCkT76N7XDd7eB8z5zJAPGtVA9ToXssh65ZuTmdT
```
Le mint dédition et son ATA sont tous deux absents avant `Print`.
## `Print`
```text
cluster = devnet
genesis hash = EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG
simulation slot = 482087795
simulation success = true
simulation logs = 61
message hash = Hvue5NZTK2UauCHGcFmSLTBrLMioNwDWFMNhX8kCuPA9
fee = 10000 lamports
signature = REHAxUZrSdKDj8F8k27Amrg7hwuJFhVfstjc9xjn2XGfTX38fH4tFwPeNrF5zeMN5pc21PpUPSgaKXMMfTFd7TE
confirmation = Confirmed
confirmation slot = 482087799
before snapshots = 2
after snapshots = 5
instruction materialized = 1
materialized snapshots = 5
canonical hydration = true
core extraction = true
decode replay = true
idempotence replay clean = true
```
Postconditions :
```text
MasterEdition.supply : 0 -> 1
max_supply : 1
printed mint supply : 0 -> 1
printed ATA amount : 0 -> 1
Edition.edition : 1
Edition Marker bit : false -> true
```
## `Burn`
```text
cluster = devnet
genesis hash = EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG
simulation slot = 482087813
simulation success = true
simulation logs = 10
message hash = 8YpMC3PdLYZAssqAsyt6GzqjNnZFPVqAEErFo8gZ9dwF
fee = 5000 lamports
signature = 4cNUxGGSWifuAtqdPJAtM3Cjpi4JATitRDqrvEAe85NLpMWcgWmC27bsfjNUmK6sEBDovNPtkPV3wDH1LpfGbhd1
confirmation = Confirmed
confirmation slot = 482087818
before snapshots = 5
after snapshots = 2
instruction materialized = 1
materialized snapshots = 2
canonical hydration = true
core extraction = true
decode replay = true
idempotence replay clean = true
```
Postconditions finales :
```text
MasterEdition.supply : 1 -> 0
MasterEdition.max_supply : 1
printed mint supply : 1 -> 0
printed ATA : absent
printed Edition : absente
Edition Marker : absent
printed Metadata RPC : présente
printed Metadata fee tombstone : true
printed Metadata semantic closed : true
```
Le compte Metadata restant est exactement le tombstone officiel attendu : owner Token Metadata, non exécutable, lamports résiduels positifs, `space=1`, donnée `[0x00]`. Il ne sagit pas dune Metadata active.
## Qualification
Les deux opérations satisfont les 16 preuves réseau requises par famille : simulation, contexte réseau, message/fee, signature, confirmation, états avant/après, hydratation canonique, extraction Core, decode replay, matérialisation et idempotence.
La matrice après promotion est :
```text
confirmed : 6
not_run : 14
Create : confirmed
Mint : confirmed
Verify : confirmed
Unverify : confirmed
Print : confirmed
Burn : confirmed
```

View File

@@ -0,0 +1,86 @@
<!-- file: docs/validation/V0_4_8_PRE_013_METAPLEX_USE_DEVNET_VALIDATION_REPORT.md -->
<!-- version: 3 -->
# Validation Devnet `0.4.8-pre.013` — disponibilité `Use`
## Statut
**Indisponible au runtime Devnet avec la surface courante du programme.**
Le probe réel du 8 août 2026 a préparé un NFT classique frais avec `Uses::Multiple { remaining: 2, total: 2 }`, terminé le chemin qualifié `Create -> Mint`, puis simulé l'instruction courante `Use` de discriminant 51 sans soumettre de transaction.
La simulation a été exécutée et refusée par Token Metadata :
```text
simulated = true
success = false
cluster = devnet
blockhash_kind = latest
blockhash_age_slots = 1
units_consumed = 12085
estimated_fee_lamports = 5000
error = {"InstructionError":[0,"InvalidInstructionData"]}
```
Logs observés :
```text
Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [1]
Program log: Error: InvalidInstructionData
Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 12085 of 200000 compute units
Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s failed: invalid instruction data
```
Aucune signature `Use` n'a été produite et aucune soumission n'a été tentée.
## Interprétation
Le client Rust généré expose bien `Use`, mais le routeur du programme courant ne possède pas de branche nouvelle API correspondante. La surface legacy conserve `Utilize`, qui est une instruction distincte. La réponse Devnet confirme donc la divergence client/runtime : l'instruction actuelle est rejetée avant exécution avec `InvalidInstructionData`.
La matrice `METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json` classe désormais `Use` comme `unavailable`, avec la raison runtime et les logs observés. Cette classification ne doit pas être transformée en `confirmed` et ne doit pas être confondue avec `not_run`.
## Correction du probe
Le premier probe a terminé le processus de test en erreur parce que la readiness générale exigeait encore une simulation réussie avant même de retourner le résumé en mode `submit=false`.
`pre.013-delta-fix-030` distingue désormais les deux contrats :
- une simulation exacte échouée peut être retournée comme donnée lorsque `submit=false`, afin d'alimenter un probe de disponibilité ;
- une soumission reste strictement impossible tant que la simulation exacte n'a pas réussi ;
- `send_authorized` reste donc faux pour toute simulation négative.
Le probe `Use` peut ainsi imprimer proprement `status=runtime_unavailable` et terminer `ok` sans affaiblir la politique simulation-first de soumission.
## Commande de contrôle
```bash
KB_DEVNET_METAPLEX_USE_PROBE_TEST=1 \
KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 \
KB_POSTGRES_TEST_URL='postgres://solana:solana@localhost:5432/solana_test' \
cargo test -p kb-pipeline-demo-scenarios \
optional_devnet_metaplex_use_probe_from_env \
-- --nocapture
```
Après `fix-030`, la sortie attendue pour la surface runtime observée est :
```text
METAPLEX_USE_PROBE_FIXTURE ...
METAPLEX_USE_PROBE_SIMULATION success=false ...
METAPLEX_USE_PROBE_STATUS status=runtime_unavailable ...
METAPLEX_USE_PROBE_LOGS [...]
```
## Rerun qualifiant après correction du probe
Le rerun `fix-030` du 8 août 2026 confirme le contrat sans erreur dorchestration. La fixture observée est :
```text
mint = G9cXdhB1MjmbKk4Ari9Y9kposL7JJfDaQEWomz8xxfpM
metadata = D1snmEtU8BroQHV2fVBfw7CcWVqT4BhqBD8MQTyfSciY
master_edition = 8pV6JRefP5QQB4UYLSwaJCzEkHFfU8mLGjEb8i5TDR7z
token = BfEGL81HW9KMVodVf4oXeK4mX6KMNTAZaYqCsb4NF3BW
uses_before = 2
simulation_context_slot = 482133665
```
La simulation reste `success=false`, conserve exactement quatre logs et `InstructionError[0]=InvalidInstructionData`. La ligne `METAPLEX_USE_PROBE_STATUS` expose `status=runtime_unavailable`, `uses_after=null` et la raison complète ; aucune ligne de soumission nest produite. Le test opt-in termine `ok`. Les autres checks et suites de tests rejoués localement restent sans régression.

View File

@@ -1,8 +1,18 @@
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
<!-- version: 29 -->
<!-- version: 33 -->
# CHANGELOG — kb-app-demo-desktop
## 0.4.8-pre.013
- `pre.013-delta-fix-022` restaure la compatibilité `Future + Send` de la commande Tauri Metaplex en corrigeant le runner partagé : l'effacement temporaire `Signer + Sync -> &dyn Signer` est limité à un helper synchrone de signature et n'est plus conservé au travers des `await` ; aucun assouplissement de la commande desktop n'est nécessaire ;
- `pre.013-delta-fix-002` retire la réécriture locale du JSON `Create` par famille et consomme désormais directement la fixture Metaplex réutilisable, afin déviter quun parcours `Fungible` annonce un token standard incompatible avec le mint réellement préparé ;
- conserve laction Create autonome sur une fixture NFT par défaut, tandis que les parcours de scénario sélectionnent explicitement leur famille lors de la préparation.
- `pre.013-delta-fix-001` sérialise explicitement les summaries de backfill, extraction Core et replay dans l'evidence JSON au lieu d'exiger `serde::Serialize` sur les contrats internes de `kb-pipeline` ;
- adapte lappel Metaplex au runner Devnet renforcé sans ajouter encore de nouveau parcours UI ;
- initialise le store PostgreSQL avant une exécution Metaplex réelle et fournit le décodeur ainsi que les matérialiseurs runtime ;
- conserve dans levidence JSON le backfill canonique, lextraction Core, le replay, la seconde passe didempotence, les matérialisations instructionnelles et le diagnostic post-exécution.
## 0.4.8-pre.010
- scinde le panneau Metadata en un module commun, un adaptateur `metadata_metaplex_token_metadata` et un adaptateur `metadata_solana_program` ;

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/README.md -->
<!-- version: 9 -->
<!-- version: 10 -->
# kb-app-demo-desktop
@@ -23,7 +23,7 @@ Les scénarios UI Devnet/Testnet restent dans le desktop. Des tests automatisés
## Exécution Metaplex Token Metadata
Le panneau conserve les scénarios synthétiques et appelle directement le runner réutilisable de `kb-pipeline-demo-scenarios` pour les opérations Metaplex courantes. La simulation RPC est réelle ; la soumission exige deux autorisations distinctes dans la requête desktop.
Le panneau conserve les scénarios synthétiques et appelle directement le runner réutilisable de `kb-pipeline-demo-scenarios` pour les opérations Metaplex courantes. La simulation RPC est réelle ; la soumission exige deux autorisations distinctes dans la requête desktop. Après une confirmation, ladaptateur réutilise PostgreSQL pour conserver lhydratation canonique, lextraction Core, le replay, les matérialisations et la preuve didempotence produites par le runner.
## Exécution Solana Program Metadata

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/USAGE.md -->
<!-- version: 9 -->
<!-- version: 10 -->
# Utilisation de kb-app-demo-desktop
@@ -235,7 +235,7 @@ const summary = await invoke(
console.log(summary);
```
Les valeurs `<...>` dun modèle doivent être remplacées par des comptes réels avant exécution. `submit: true` exige simultanément `operatorConfirmed: true`. Le résultat distingue le plan, le préflight stateful, la simulation RPC, la signature éventuelle, la confirmation et les états avant/après.
Les valeurs `<...>` dun modèle doivent être remplacées par des comptes réels avant exécution. `submit: true` exige simultanément `operatorConfirmed: true`. Le résultat distingue le plan, le préflight stateful, la simulation RPC, la signature éventuelle, la confirmation et les états avant/après. Après soumission confirmée, `evidenceJson` contient également lhydratation canonique, lextraction Core, le replay, la seconde passe didempotence, les matérialisations instructionnelles et le diagnostic post-exécution.
### Diagnostics PostgreSQL

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs
// version: 1
// version: 4
//! Desktop adapters for Metaplex Token Metadata execution demo scenarios.
@@ -190,6 +190,13 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store = match crate::connect_postgres_store(&profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
return std::result::Result::Err(error.to_string());
}
let mut execution_request = match kb_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
request.intent_id,
request.operation_json.as_str(),
@@ -210,9 +217,20 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
execution_request.submit = request.submit;
execution_request.operator_confirmed = request.operator_confirmed;
execution_request.materialize_after_confirmation = request.materialize_after_confirmation;
execution_request.post_validation_max_retries = 20;
execution_request.force_post_validation_replay = false;
if let std::result::Result::Err(error) = execution_request.validate() {
return std::result::Result::Err(error.to_string());
}
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(kb_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> = std::vec![
std::sync::Arc::new(kb_lib::MtAdminMaterializer),
std::sync::Arc::new(kb_lib::MtComplianceAuditMaterializer),
std::sync::Arc::new(kb_lib::MtLifecycleMaterializer),
std::sync::Arc::new(kb_lib::MtMetadataMetaplexTokenMetadataMaterializer),
std::sync::Arc::new(kb_lib::MtRiskMaterializer),
];
let observer = crate::DemoExecutionMetadataObserver {
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
@@ -220,9 +238,12 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
let workspace_root = crate::workspace_root_dir();
let summary = match kb_pipeline_demo_scenarios::execute_devnet_metaplex_token_metadata(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
@@ -241,7 +262,13 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
"confirmation": &summary.confirmation,
"after": &summary.after,
"materializationRequested": summary.materialization_requested,
"materializedSnapshots": &summary.materialized_snapshots
"materializedSnapshots": &summary.materialized_snapshots,
"backfill": summary.backfill.as_ref().map(backfill_summary_json),
"coreExtraction": summary.core_extraction.as_ref().map(core_extraction_summary_json),
"decodeReplay": summary.decode_replay.as_ref().map(decode_replay_summary_json),
"idempotenceReplay": summary.idempotence_replay.as_ref().map(decode_replay_summary_json),
"instructionMaterializations": &summary.materializations,
"postExecution": &summary.post_execution
}));
return std::result::Result::Ok(
crate::DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload {
@@ -263,6 +290,88 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
);
}
fn backfill_summary_json(summary: &kb_pipeline::BackfillSummary) -> serde_json::Value {
return serde_json::json!({
"captureSessionId": summary.capture_session_id,
"filterCode": summary.filter_code,
"role": summary.role,
"provider": summary.provider,
"endpointCode": summary.endpoint_code,
"pagesFetched": summary.pages_fetched,
"candidatesSelected": summary.candidates_selected,
"candidatesStarted": summary.candidates_started,
"candidatesCompleted": summary.candidates_completed,
"candidatesCancelled": summary.candidates_cancelled,
"candidatesNotStarted": summary.candidates_not_started,
"transactionsReceived": summary.transactions_received,
"canonicalInserted": summary.canonical_inserted,
"canonicalSkipped": summary.canonical_skipped,
"existingSkipped": summary.existing_skipped,
"missing": summary.missing,
"failed": summary.failed,
"observationsInserted": summary.observations_inserted,
"attempts": summary.attempts,
"cancelled": summary.cancelled,
"resumeBeforeSignature": summary.resume_before_signature,
"startedAt": summary.started_at,
"finishedAt": summary.finished_at
});
}
fn core_extraction_summary_json(summary: &kb_pipeline::CoreExtractionSummary) -> serde_json::Value {
return serde_json::json!({
"processorVersion": summary.processor_version,
"selected": summary.selected,
"started": summary.started,
"completed": summary.completed,
"skipped": summary.skipped,
"extracted": summary.extracted,
"failed": summary.failed,
"cancelledCandidates": summary.cancelled_candidates,
"notStarted": summary.not_started,
"cancelled": summary.cancelled,
"startedAt": summary.started_at,
"finishedAt": summary.finished_at
});
}
fn decode_replay_summary_json(summary: &kb_pipeline::DecodeReplaySummary) -> serde_json::Value {
let processors = summary
.processors
.iter()
.map(|processor| {
return serde_json::json!({
"name": processor.processor_name,
"version": processor.processor_version,
"dispatched": processor.dispatched,
"skipped": processor.skipped,
"decoded": processor.decoded,
"ignored": processor.ignored,
"unsupported": processor.unsupported,
"failed": processor.failed,
"processingErrors": processor.processing_errors,
"materializedOutputs": processor.materialized_outputs,
"materializationRefused": processor.materialization_refused
});
})
.collect::<std::vec::Vec<serde_json::Value>>();
return serde_json::json!({
"campaignId": summary.campaign_id,
"pipelineVersion": summary.pipeline_version,
"selected": summary.selected,
"started": summary.started,
"completed": summary.completed,
"unmatched": summary.unmatched,
"notStarted": summary.not_started,
"failedInputs": summary.failed_inputs,
"processingErrorInputs": summary.processing_error_inputs,
"cancelled": summary.cancelled,
"processors": processors,
"startedAt": summary.started_at,
"finishedAt": summary.finished_at
});
}
fn parse_stateful_reads(
value: &str,
) -> std::result::Result<
@@ -280,13 +389,29 @@ fn parse_stateful_reads(
};
}
/// Creates or reuses the Devnet mint fixture used by the named Create scenario.
/// Creates or reuses the Devnet NFT mint fixture used by the standalone Create action.
pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_prepare_create_fixture(
state: tauri::State<'_, crate::AppState>,
profile_name: std::string::String,
) -> std::result::Result<
crate::DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload,
std::string::String,
> {
return prepare_create_fixture_for_family(
state,
profile_name,
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
)
.await;
}
async fn prepare_create_fixture_for_family(
state: tauri::State<'_, crate::AppState>,
profile_name: std::string::String,
asset_family: kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily,
) -> std::result::Result<
crate::DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload,
std::string::String,
> {
let profile = match crate::select_devnet_profile(state.app_config(), profile_name.as_str()) {
std::result::Result::Ok(value) => value,
@@ -303,11 +428,14 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_prepare_crea
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let workspace_root = crate::workspace_root_dir();
let options =
kb_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions::new(wallet_dir)
.with_asset_family(asset_family);
let summary = match kb_pipeline_demo_scenarios::prepare_metaplex_create_fixture(
&http_pool,
&profile,
workspace_root.as_path(),
&kb_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions::new(wallet_dir),
&options,
)
.await
{
@@ -326,84 +454,6 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_prepare_crea
);
}
fn adapt_create_operation_json(
operation_json: &str,
asset_family: kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily,
) -> std::result::Result<std::string::String, std::string::String> {
let mut value: serde_json::Value = match serde_json::from_str(operation_json) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => {
return std::result::Result::Err(format!(
"invalid generated Metaplex Create JSON: {error}"
));
},
};
if matches!(
asset_family,
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft
| kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible
) {
value["master_edition"] = serde_json::Value::Null;
}
let create = match value
.get_mut("create_args")
.and_then(|args| return args.get_mut("V1"))
.and_then(serde_json::Value::as_object_mut)
{
std::option::Option::Some(found) => found,
std::option::Option::None => {
return std::result::Result::Err(
"generated Metaplex Create JSON has no create_args.V1 object".to_string(),
);
},
};
match asset_family {
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft => {
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet NFT"));
create.insert("symbol".to_string(), serde_json::json!("KHNFT"));
create.insert("token_standard".to_string(), serde_json::json!("NonFungible"));
create.insert("collection_details".to_string(), serde_json::Value::Null);
create.insert("decimals".to_string(), serde_json::Value::Null);
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
},
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft => {
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet SFT"));
create.insert("symbol".to_string(), serde_json::json!("KHSFT"));
create.insert("token_standard".to_string(), serde_json::json!("FungibleAsset"));
create.insert("collection_details".to_string(), serde_json::Value::Null);
create.insert("decimals".to_string(), serde_json::json!(0));
create.insert("print_supply".to_string(), serde_json::Value::Null);
},
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible => {
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet Fungible"));
create.insert("symbol".to_string(), serde_json::json!("KHFUN"));
create.insert("token_standard".to_string(), serde_json::json!("Fungible"));
create.insert("collection_details".to_string(), serde_json::Value::Null);
create.insert("decimals".to_string(), serde_json::json!(0));
create.insert("print_supply".to_string(), serde_json::Value::Null);
},
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection => {
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet Collection"));
create.insert("symbol".to_string(), serde_json::json!("KHCOL"));
create.insert("token_standard".to_string(), serde_json::json!("NonFungible"));
create.insert("collection_details".to_string(), serde_json::json!({"V1": {"size": 0}}));
create.insert("decimals".to_string(), serde_json::Value::Null);
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
},
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft => {
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet pNFT"));
create.insert("symbol".to_string(), serde_json::json!("KHPNFT"));
create
.insert("token_standard".to_string(), serde_json::json!("ProgrammableNonFungible"));
create.insert("collection_details".to_string(), serde_json::Value::Null);
create.insert("rule_set".to_string(), serde_json::Value::Null);
create.insert("decimals".to_string(), serde_json::Value::Null);
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
},
}
return std::result::Result::Ok(crate::pretty_json(&value));
}
/// Prepares one coherent scenario step and all of its stateful reads.
pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_prepare_step(
state: tauri::State<'_, crate::AppState>,
@@ -452,27 +502,17 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_prepare_step
let (fixture_mint, fixture_metadata, fixture_master_edition, operation_json, fixture_state) =
if operation == "create" {
let fixture =
match crate::demo_execution_metadata_metaplex_token_metadata_prepare_create_fixture(
state,
profile_name,
)
.await
match prepare_create_fixture_for_family(state, profile_name, scenario.asset_family)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let operation_json = match adapt_create_operation_json(
fixture.operation_json.as_str(),
scenario.asset_family,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
(
fixture.mint,
fixture.metadata,
fixture.master_edition,
operation_json,
fixture.operation_json,
if fixture.mint_created {
"mint_prepared".to_string()
} else {

View File

@@ -1,8 +1,41 @@
<!-- file: kb-lib/CHANGELOG.md -->
<!-- version: 35 -->
<!-- version: 48 -->
# CHANGELOG — kb-lib
## `0.4.8-pre.013` — exactitude des builders Metaplex courants
- `pre.013-delta-fix-033` aligne la validation du décodeur `Update` sur les privilèges runtime globaux : lauthority doit signer et les comptes mutables doivent rester writable, mais un même wallet utilisé comme authority et fee payer peut apparaître writable à la position authority lors du replay Core ; le test `modern_update_variants_accept_transaction_privilege_supersets` couvre cette forme sans augmenter le nombre total de tests.
- `pre.013-delta-fix-032` corrige le contrat des comptes optionnels terminaux des instructions escrow historiques `CreateEscrowAccount` et `TransferOutOfEscrow` : lorsque `authority=None`, aucun placeholder Metaplex nest ajouté en fin de liste, conformément aux builders officiels et au handler qui déduit la présence de lautorité à partir du nombre de comptes ; les comptes optionnels positionnels des instructions courantes conservent leurs placeholders. Le test de builder existant couvre désormais les formes escrow sans authority et avec authority explicite.
- `pre.013-delta-fix-029` aligne le décodeur courant `Use` sur les privilèges minimaux du builder `mpl-token-metadata 5.1.1` : lautorité et le payer doivent signer, Metadata doit être writable, et les comptes optionnels réellement présents (`delegate_record`, `token`, `edition`) restent writable ; les privilèges globaux supplémentaires hérités du fee payer ne provoquent plus de faux rejet.
- `pre.013-delta-fix-027` normalise la projection JSON canonique des comptes `TokenRecord` : `delegate` et `locked_transfer` sont désormais exposés en chaînes base58, comme les champs typés du snapshot, au lieu de dépendre de la représentation Serde interne de `solana_pubkey::Pubkey` ; la régression existante verrouille explicitement ces deux projections et `delegate_role`.
- `pre.013-delta-fix-026` corrige le replay des opérations pNFT `Delegate`/`Revoke` et `Lock`/`Unlock` sur la frontière des privilèges globaux de transaction : le décodeur vérifie désormais uniquement les minima signer/writable réellement requis au lieu de rejeter les sur-ensembles hérités lorsquun même compte occupe aussi le rôle de fee payer ;
- conserve lexigence writable des comptes optionnels réellement mutés (`delegate_record`, `token_record`, `token`) lorsquils ne sont pas le placeholder positionnel du programme Token Metadata ;
- ajoute une régression reproduisant `Delegate/Revoke` avec lautorité également fee payer et `Lock/Unlock` avec le token owner également fee payer, sans relâcher les refus de signer/writable obligatoires manquants.
- `pre.013-delta-fix-025` corrige la lecture stateful des Master Edition pNFT compactes : les deux octets finaux de lallocation 20 octets sont des champs sémantiques Metaplex (`fee flag`, puis `TokenStandard`) et ne sont plus traités comme du padding nul ;
- conserve une validation fermée : les octets réservés intermédiaires doivent rester nuls, le fee flag est borné à `0/1`, et le trailer Master Edition naccepte que `NonFungible(0)` ou `ProgrammableNonFungible(4)` ;
- ajoute une régression reproduisant une Master Edition V2 compacte `supply=0,max_supply=0` avec trailer pNFT, ainsi que les refus dun fee flag, dun token standard ou dun octet réservé invalides.
- `pre.013-delta-fix-022` corrige la régression `modern_print_accepts_transaction_privilege_supersets` : un `edition_token_record` réellement fourni reste writable conformément au builder `Print` courant ; le test continue d'exercer les privilèges globaux supplémentaires sans retirer un minimum obligatoire ;
- aucun assouplissement du décodeur `Print` n'est introduit par ce correctif.
- `pre.013-delta-fix-021` corrige le décodage des comptes `Edition` réellement alloués par le programme courant : `Edition::LEN` décrit 41 octets sérialisés, tandis que les allocations Metaplex connues sont 42 octets en forme compacte et 241 octets en forme historique ;
- accepte uniquement la sérialisation exacte ou ces allocations connues avec padding nul, refuse les longueurs/paddings inconnus et verrouille aussi les allocations connues de Master Edition après lélargissement de la borne nécessaire aux anciennes Edition ;
- ajoute une régression couvrant Edition compacte/historique, Master Edition compacte/historique, longueur inconnue et padding non nul.
- `pre.013-delta-fix-018` corrige `Verify`/`Unverify` sur la même frontière de privilèges globaux déjà rencontrée pour `Create`/`Mint` : l'autorité peut être writable dans le message parce qu'elle est aussi fee payer, sans que l'AccountMeta Metaplex exige ce privilège ; le décodeur vérifie désormais uniquement les minima signer/writable réellement requis ;
- aligne `Unverify` sur les 7 positions du wrapper courant `mpl-token-metadata 5.1.1` au lieu d'exiger artificiellement 8 comptes, et ajoute une régression couvrant `Verify` et `Unverify` lorsque l'autorité fee payer hérite du privilège writable global ;
- `pre.013-delta-fix-009` corrige le décodeur des opérations courantes `Create` et `Mint` pour interpréter les flags signer/writable comme des privilèges globaux de transaction : seuls les privilèges requis constituent désormais des minima, sans rejeter les sur-ensembles légitimes dus au fee payer ou à un même wallet présent sous plusieurs rôles ;
- accepte explicitement `Create.mint_as_signer = false` pour un mint préexistant et ne force plus `update_authority_as_signer = true`, conformément aux booléens exposés par le SDK courant ;
- conserve lexigence writable pour un master edition/token record réellement fourni, tout en distinguant ces comptes des placeholders Metaplex positionnels ;
- ajoute deux régressions couvrant le `Create` à mint préexistant et le `Mint` où token owner, authority et payer héritent de privilèges transactionnels supérieurs.
- `pre.013-delta-fix-004` ferme laudit des comptes positionnels réellement optionnels du SDK courant pour `CreateEscrowAccount`, `TransferOutOfEscrow`, `Mint`, `Migrate`, `Use`, `Print` et `Resize` ;
- expose ces optionalités dans les intents publics et encode une absence avec le placeholder readonly du programme Metaplex, conformément au contrat Kinobi déjà attendu par le décodeur et la matrice ;
- conserve explicitement requis les comptes système/programmes qui ne sont que defaultables par le builder mais restent présents dans la struct dinstruction courante ;
- ajoute des régressions sur les positions optionnelles de `Mint`, `Migrate`, `Use`, `Print` et `Resize`, ainsi que sur lautorité optionnelle descrow ;
- intègre les deux `return` ajoutés lors de la validation locale de `fix-003` dans les closures de génération de pubkeys de test, afin que larchive reproduise létat Clippy réellement validé ;
- migre `Print` et `Resize` depuis les account metas figées de lIDL historique vers les builders officiels `mpl-token-metadata 5.1.1` ;
- rétablit le contrat signer courant de `Print` : `edition_mint`, `edition_mint_authority`, `payer` et `master_token_account_owner` sont désormais déclarés signataires conformément au SDK officiel ;
- rétablit le contrat signer courant de `Resize` : `payer` est writable + signer, tandis que lautorité fournie reste signer ;
- ajoute des régressions directes sur lordre, le discriminator et les flags signer/writable de ces deux opérations avant toute campagne Devnet associée.
## `0.4.8-pre.011` — complétude Token-2022 Token Metadata
- ajoute les intents et builders exacts `UpdateTokenMetadataAuthority` et `EmitTokenMetadata` ;

View File

@@ -1,5 +1,5 @@
<!-- file: kb-lib/USAGE.md -->
<!-- version: 15 -->
<!-- version: 16 -->
# Utilisation de kb-lib
@@ -307,6 +307,8 @@ La surface publique repose sur :
- les constantes `EX_METAPLEX_TOKEN_METADATA_*_OPERATION` ;
- `EX_METAPLEX_TOKEN_METADATA_SUPPORTED_OPERATION_CODES` et `EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES`.
Les comptes `Edition` Token Metadata doivent être décodés selon leur contenu Borsh **et** leur allocation on-chain. Le SDK `mpl-token-metadata 5.1.1` expose `Edition::LEN = 41`, tandis que le programme courant peut allouer 42 octets et les comptes historiques 241 octets. Le décodeur accepte seulement ces formes connues avec padding nul ; une longueur inconnue ou un padding non nul est refusé.
Le consommateur construit un intent typé, puis appelle `ExApiTypedInstructionExecutor::build_prepared_plan`. Le plan retourné contient les instructions, comptes et signers requis, mais neffectue ni simulation ni envoi.
Les opérations courantes utilisent leur wrapper canonique final. Les versions remplacées ne possèdent pas de builder public distinct. Les opérations obsolètes encore constructibles exigent que lintent autorise explicitement lexécution dépréciée ; leurs variantes Rust sont marquées `#[deprecated]`.

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/decoder/metadata/metaplex_token_metadata/account.rs
// version: 12
// version: 15
//! Bounded decoding and PDA validation for Metaplex Token Metadata accounts.
@@ -103,6 +103,10 @@ const MAX_NAME_BYTES: usize = 32;
const MAX_SYMBOL_BYTES: usize = 10;
const MAX_URI_BYTES: usize = 200;
const MAX_CREATORS: usize = 5;
const COMPACT_MASTER_EDITION_ACCOUNT_BYTES: usize = 20;
const LEGACY_MASTER_EDITION_ACCOUNT_BYTES: usize = 282;
const COMPACT_EDITION_ACCOUNT_BYTES: usize = 42;
const LEGACY_EDITION_ACCOUNT_BYTES: usize = 241;
const EDITION_MARKER_V1_BITS: u64 = 248;
const MAX_EDITION_MARKER_V2_LEDGER_BYTES: usize = 1_048_576;
@@ -325,6 +329,75 @@ pub fn decoder_metadata_metaplex_token_metadata_decode_metadata_account(
});
}
fn validate_known_account_allocation(
data: &[u8],
serialized_len: usize,
padded_account_lengths: &[usize],
) -> std::result::Result<(), DcMetadataMtmMetadataAccountDecodeError> {
if data.len() == serialized_len {
return std::result::Result::Ok(());
}
if data.len() < serialized_len || !padded_account_lengths.contains(&data.len()) {
return std::result::Result::Err(
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
);
}
if data[serialized_len..].iter().any(|byte| return *byte != 0) {
return std::result::Result::Err(
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
);
}
return std::result::Result::Ok(());
}
fn validate_master_edition_allocation(
data: &[u8],
serialized_len: usize,
padded_account_lengths: &[usize],
) -> std::result::Result<(), DcMetadataMtmMetadataAccountDecodeError> {
if data.len() == serialized_len {
return std::result::Result::Ok(());
}
if data.len() < serialized_len || !padded_account_lengths.contains(&data.len()) {
return std::result::Result::Err(
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
);
}
let trailer_start = match data.len().checked_sub(2) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
);
},
};
if trailer_start < serialized_len {
return std::result::Result::Err(
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
);
}
if data[serialized_len..trailer_start].iter().any(|byte| return *byte != 0) {
return std::result::Result::Err(
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
);
}
let fee_flag = data[trailer_start];
if fee_flag > 1 {
return std::result::Result::Err(
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
);
}
let token_standard = data[trailer_start + 1];
if token_standard != mpl_token_metadata::types::TokenStandard::NonFungible as u8
&& token_standard != mpl_token_metadata::types::TokenStandard::ProgrammableNonFungible as u8
{
return std::result::Result::Err(
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
);
}
return std::result::Result::Ok(());
}
/// Supported edition account layout.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DcMetadataMtmEditionAccountKind {
@@ -396,7 +469,7 @@ pub fn decoder_metadata_metaplex_token_metadata_decode_edition_account(
if owner_key != expected_owner {
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::ForeignOwner);
}
if data.is_empty() || data.len() > 128 {
if data.is_empty() || data.len() > LEGACY_MASTER_EDITION_ACCOUNT_BYTES {
return std::result::Result::Err(
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
);
@@ -427,6 +500,14 @@ pub fn decoder_metadata_metaplex_token_metadata_decode_edition_account(
);
},
};
let serialized_len = if value.max_supply.is_some() { 82 } else { 74 };
if let std::result::Result::Err(error) = validate_master_edition_allocation(
data,
serialized_len,
&[LEGACY_MASTER_EDITION_ACCOUNT_BYTES],
) {
return std::result::Result::Err(error);
}
let payload_json = match serde_json::to_value(&value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
@@ -460,6 +541,14 @@ pub fn decoder_metadata_metaplex_token_metadata_decode_edition_account(
);
},
};
let serialized_len = if value.max_supply.is_some() { 18 } else { 10 };
if let std::result::Result::Err(error) = validate_master_edition_allocation(
data,
serialized_len,
&[COMPACT_MASTER_EDITION_ACCOUNT_BYTES, LEGACY_MASTER_EDITION_ACCOUNT_BYTES],
) {
return std::result::Result::Err(error);
}
let payload_json = match serde_json::to_value(&value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
@@ -483,11 +572,6 @@ pub fn decoder_metadata_metaplex_token_metadata_decode_edition_account(
});
}
if key == mpl_token_metadata::types::Key::EditionV1 as u8 {
if data.len() != mpl_token_metadata::accounts::Edition::LEN {
return std::result::Result::Err(
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
);
}
let value = match mpl_token_metadata::accounts::Edition::from_bytes(data) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
@@ -496,6 +580,13 @@ pub fn decoder_metadata_metaplex_token_metadata_decode_edition_account(
);
},
};
if let std::result::Result::Err(error) = validate_known_account_allocation(
data,
mpl_token_metadata::accounts::Edition::LEN,
&[COMPACT_EDITION_ACCOUNT_BYTES, LEGACY_EDITION_ACCOUNT_BYTES],
) {
return std::result::Result::Err(error);
}
let payload_json = match serde_json::to_value(&value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
@@ -851,17 +942,21 @@ pub fn decoder_metadata_metaplex_token_metadata_decode_token_record_account(
DcMetadataMtmMetadataAccountDecodeError::InvalidTokenRecordPda,
);
}
let payload_json = match serde_json::to_value(&value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
);
},
};
let state = format!("{:?}", value.state);
let delegate = value.delegate.map(|delegate| return delegate.to_string());
let delegate_role = value.delegate_role.as_ref().map(|role| return format!("{role:?}"));
let locked_transfer =
value.locked_transfer.map(|locked_transfer| return locked_transfer.to_string());
let locked = value.state == mpl_token_metadata::types::TokenState::Locked;
let payload_json = serde_json::json!({
"key": "TokenRecord",
"bump": value.bump,
"state": &state,
"rule_set_revision": value.rule_set_revision,
"delegate": &delegate,
"delegate_role": &delegate_role,
"locked_transfer": &locked_transfer,
});
return std::result::Result::Ok(DcMetadataMtmTokenRecordAccountSnapshot {
account: account_key.to_string(),
bump,
@@ -870,11 +965,9 @@ pub fn decoder_metadata_metaplex_token_metadata_decode_token_record_account(
stored_bump: value.bump,
state,
rule_set_revision: value.rule_set_revision,
delegate: value.delegate.map(|delegate| return delegate.to_string()),
delegate,
delegate_role,
locked_transfer: value
.locked_transfer
.map(|locked_transfer| return locked_transfer.to_string()),
locked_transfer,
locked,
payload_json,
});
@@ -1975,6 +2068,179 @@ mod tests {
assert_eq!(edition_snapshot.edition, std::option::Option::Some(u64::MAX));
}
#[test]
fn edition_and_master_edition_accept_only_known_allocations() {
let mint = mpl_token_metadata::ID;
let (account, _) = mpl_token_metadata::accounts::MasterEdition::find_pda(&mint);
let master = mpl_token_metadata::accounts::MasterEdition {
key: mpl_token_metadata::types::Key::MasterEditionV2,
supply: 1,
max_supply: std::option::Option::Some(1),
};
let master_data = match borsh_0_10::to_vec(&master) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("master edition serialization failed: {error}")
},
};
for allocated_len in [
super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES,
super::LEGACY_MASTER_EDITION_ACCOUNT_BYTES,
] {
let mut padded = master_data.clone();
padded.resize(allocated_len, 0);
assert!(
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
&account.to_string(),
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
&mint.to_string(),
padded.as_slice(),
)
.is_ok()
);
}
let edition = mpl_token_metadata::accounts::Edition {
key: mpl_token_metadata::types::Key::EditionV1,
parent: account,
edition: 1,
};
let edition_data = match borsh_0_10::to_vec(&edition) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("edition serialization failed: {error}"),
};
for allocated_len in
[super::COMPACT_EDITION_ACCOUNT_BYTES, super::LEGACY_EDITION_ACCOUNT_BYTES]
{
let mut padded = edition_data.clone();
padded.resize(allocated_len, 0);
assert!(
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
&account.to_string(),
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
&mint.to_string(),
padded.as_slice(),
)
.is_ok()
);
}
let mut unexpected_len = edition_data.clone();
unexpected_len.resize(super::COMPACT_EDITION_ACCOUNT_BYTES + 1, 0);
assert_eq!(
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
&account.to_string(),
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
&mint.to_string(),
unexpected_len.as_slice(),
),
std::result::Result::Err(
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength
),
);
let mut non_zero_padding = edition_data;
non_zero_padding.resize(super::COMPACT_EDITION_ACCOUNT_BYTES, 1);
assert_eq!(
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
&account.to_string(),
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
&mint.to_string(),
non_zero_padding.as_slice(),
),
std::result::Result::Err(
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout
),
);
}
#[test]
fn compact_master_edition_trailer_accepts_pnft_and_rejects_invalid_flags() {
let mint = mpl_token_metadata::ID;
let (account, _) = mpl_token_metadata::accounts::MasterEdition::find_pda(&mint);
let master = mpl_token_metadata::accounts::MasterEdition {
key: mpl_token_metadata::types::Key::MasterEditionV2,
supply: 0,
max_supply: std::option::Option::Some(0),
};
let serialized = match borsh_0_10::to_vec(&master) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("master edition serialization failed: {error}")
},
};
let mut pnft = serialized.clone();
pnft.resize(super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES, 0);
pnft[super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES - 2] = 1;
pnft[super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES - 1] =
mpl_token_metadata::types::TokenStandard::ProgrammableNonFungible as u8;
let snapshot = match crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
&account.to_string(),
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
&mint.to_string(),
pnft.as_slice(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("compact pNFT master edition decode failed: {error}")
},
};
assert_eq!(snapshot.kind, crate::DcMetadataMtmEditionAccountKind::MasterEditionV2);
assert_eq!(snapshot.supply, std::option::Option::Some(0));
assert_eq!(snapshot.max_supply, std::option::Option::Some(0));
let mut invalid_fee_flag = pnft.clone();
invalid_fee_flag[super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES - 2] = 2;
assert_eq!(
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
&account.to_string(),
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
&mint.to_string(),
invalid_fee_flag.as_slice(),
),
std::result::Result::Err(
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout
),
);
let mut invalid_token_standard = pnft;
invalid_token_standard[super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES - 1] =
mpl_token_metadata::types::TokenStandard::Fungible as u8;
assert_eq!(
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
&account.to_string(),
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
&mint.to_string(),
invalid_token_standard.as_slice(),
),
std::result::Result::Err(
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout
),
);
let master_without_limit = mpl_token_metadata::accounts::MasterEdition {
key: mpl_token_metadata::types::Key::MasterEditionV2,
supply: 0,
max_supply: std::option::Option::None,
};
let mut invalid_reserved = match borsh_0_10::to_vec(&master_without_limit) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("master edition serialization failed: {error}")
},
};
let serialized_len = invalid_reserved.len();
invalid_reserved.resize(super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES, 0);
invalid_reserved[serialized_len] = 1;
invalid_reserved[super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES - 1] =
mpl_token_metadata::types::TokenStandard::ProgrammableNonFungible as u8;
assert_eq!(
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
&account.to_string(),
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
&mint.to_string(),
invalid_reserved.as_slice(),
),
std::result::Result::Err(
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout
),
);
}
#[test]
fn historical_master_edition_v1_and_invalid_boundaries_are_explicit() {
let mint = mpl_token_metadata::ID;
@@ -2218,6 +2484,15 @@ mod tests {
snapshot.locked_transfer,
std::option::Option::Some(mpl_token_metadata::ID.to_string())
);
assert_eq!(
snapshot.payload_json["delegate"],
serde_json::json!(mpl_token_metadata::ID.to_string())
);
assert_eq!(snapshot.payload_json["delegate_role"], serde_json::json!("Transfer"));
assert_eq!(
snapshot.payload_json["locked_transfer"],
serde_json::json!(mpl_token_metadata::ID.to_string())
);
}
#[test]

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/decoder/metadata/metaplex_token_metadata/decoder.rs
// version: 32
// version: 39
//! Exact contextual dispatch for Metaplex Token Metadata.
@@ -633,6 +633,69 @@ mod tests {
);
}
#[test]
fn modern_create_accepts_preexisting_mint_and_transaction_privilege_supersets() {
let decoder = crate::DcMetadataMetaplexTokenMetadataDecoder;
let result = crate::DcApiInstructionDecoder::decode(
&decoder,
&input_with_flags(
modern_create_wire().as_slice(),
&[
(false, true),
(false, true),
(false, true),
(true, true),
(true, true),
(true, true),
(false, false),
(false, false),
(false, false),
],
false,
"modern_create_preexisting_mint",
),
);
assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Decoded);
assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some("create"));
}
#[test]
fn modern_mint_accepts_authority_owner_payer_privilege_supersets() {
let decoder = crate::DcMetadataMetaplexTokenMetadataDecoder;
let args = mpl_token_metadata::types::MintArgs::V1 {
amount: 1,
authorization_data: std::option::Option::None,
};
let wire = programmable_wire(crate::DC_METADATA_MTM_MINT_DISCRIMINATOR, &args);
let result = crate::DcApiInstructionDecoder::decode(
&decoder,
&input_with_flags(
wire.as_slice(),
&[
(false, true),
(true, true),
(false, false),
(false, true),
(false, true),
(false, true),
(true, true),
(false, false),
(true, true),
(false, false),
(false, false),
(false, false),
(false, false),
(false, false),
(false, false),
],
false,
"modern_mint_privilege_superset",
),
);
assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Decoded);
assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some("mint"));
}
#[test]
fn modern_create_rejects_truncated_invalid_and_suffix_wires() {
let decoder = crate::DcMetadataMetaplexTokenMetadataDecoder;
@@ -661,12 +724,12 @@ mod tests {
&input_with_flags(
modern_create_wire().as_slice(),
&[
(false, true),
(false, false),
(false, true),
(false, true),
(true, false),
(true, true),
(true, false),
(false, false),
(false, false),
(false, false),
(false, false),
@@ -852,6 +915,53 @@ mod tests {
assert!(!v2.observations[0].observation_committed);
}
#[test]
fn modern_print_accepts_transaction_privilege_supersets() {
let decoder = crate::DcMetadataMetaplexTokenMetadataDecoder;
let flags = [
(false, true),
(false, true),
(true, true),
(true, true),
(false, true),
(true, true),
(false, true),
(false, true),
(false, true),
(true, true),
(true, true),
(false, false),
(false, false),
(true, true),
(false, false),
(false, false),
(false, false),
(false, false),
];
let decoded = crate::DcApiInstructionDecoder::decode(
&decoder,
&input_with_flags(
modern_print_wire_v1(1).as_slice(),
&flags,
false,
"modern_print_privilege_superset",
),
);
assert_eq!(decoded.status, crate::DcApiDecoderOutcomeStatus::Decoded);
let mut missing_mint_signature = flags;
missing_mint_signature[2] = (false, true);
let rejected = crate::DcApiInstructionDecoder::decode(
&decoder,
&input_with_flags(
modern_print_wire_v1(1).as_slice(),
&missing_mint_signature,
false,
"modern_print_missing_mint_signature",
),
);
assert_eq!(rejected.status, crate::DcApiDecoderOutcomeStatus::Failed);
}
#[test]
fn modern_print_rejects_truncated_invalid_and_suffix_wires() {
let decoder = crate::DcMetadataMetaplexTokenMetadataDecoder;
@@ -915,10 +1025,10 @@ mod tests {
}
#[test]
fn modern_update_variants_decode_exact_contract() {
fn modern_update_variants_accept_transaction_privilege_supersets() {
let decoder = crate::DcMetadataMetaplexTokenMetadataDecoder;
let flags = [
(true, false),
(true, true),
(false, false),
(false, false),
(false, false),
@@ -1012,13 +1122,13 @@ mod tests {
fn modern_use_verify_and_unverify_decode_exact_contracts() {
let decoder = crate::DcMetadataMetaplexTokenMetadataDecoder;
let use_flags = [
(true, false),
(true, true),
(false, true),
(false, true),
(false, false),
(false, true),
(false, true),
(true, false),
(true, true),
(false, false),
(false, false),
(false, false),
@@ -1081,6 +1191,74 @@ mod tests {
assert!(!unverify.observations[0].observation_committed);
}
#[test]
fn modern_verify_and_unverify_accept_fee_payer_privilege_supersets() {
let decoder = crate::DcMetadataMetaplexTokenMetadataDecoder;
let verify_flags = [
(true, true),
(false, false),
(false, true),
(false, false),
(false, true),
(false, false),
(false, false),
(false, false),
];
let verify = crate::DcApiInstructionDecoder::decode(
&decoder,
&input_with_flags(
modern_verification_wire(
crate::DC_METADATA_MTM_VERIFY_DISCRIMINATOR,
mpl_token_metadata::types::VerificationArgs::CollectionV1,
)
.as_slice(),
&verify_flags,
false,
"modern_verify_fee_payer",
),
);
assert_eq!(verify.status, crate::DcApiDecoderOutcomeStatus::Decoded);
let unverify_flags = [
(true, true),
(false, false),
(false, true),
(false, false),
(false, true),
(false, false),
(false, false),
];
let unverify = crate::DcApiInstructionDecoder::decode(
&decoder,
&input_with_flags(
modern_verification_wire(
crate::DC_METADATA_MTM_UNVERIFY_DISCRIMINATOR,
mpl_token_metadata::types::VerificationArgs::CollectionV1,
)
.as_slice(),
&unverify_flags,
false,
"modern_unverify_fee_payer",
),
);
assert_eq!(unverify.status, crate::DcApiDecoderOutcomeStatus::Decoded);
let mut missing_authority_signature = verify_flags;
missing_authority_signature[0] = (false, true);
let invalid = crate::DcApiInstructionDecoder::decode(
&decoder,
&input_with_flags(
modern_verification_wire(
crate::DC_METADATA_MTM_VERIFY_DISCRIMINATOR,
mpl_token_metadata::types::VerificationArgs::CollectionV1,
)
.as_slice(),
&missing_authority_signature,
false,
"modern_verify_missing_authority_signature",
),
);
assert_eq!(invalid.status, crate::DcApiDecoderOutcomeStatus::Failed);
}
#[test]
fn modern_use_verify_and_unverify_reject_invalid_wires() {
let decoder = crate::DcMetadataMetaplexTokenMetadataDecoder;
@@ -1120,6 +1298,18 @@ mod tests {
),
);
assert_eq!(invalid.status, crate::DcApiDecoderOutcomeStatus::Failed);
let mut readonly_token = use_flags;
readonly_token[2] = (false, false);
let invalid = crate::DcApiInstructionDecoder::decode(
&decoder,
&input_with_flags(
modern_use_wire().as_slice(),
&readonly_token,
false,
"modern_use_readonly_token",
),
);
assert_eq!(invalid.status, crate::DcApiDecoderOutcomeStatus::Failed);
let verify_flags = [
(true, false),
(false, false),
@@ -2261,6 +2451,93 @@ mod tests {
assert_eq!(revoke.observations[0].payload_json["parameters"], "StandardV1");
}
#[test]
fn programmable_delegate_revoke_lock_and_unlock_accept_transaction_privilege_supersets() {
let decoder = crate::DcMetadataMetaplexTokenMetadataDecoder;
let delegate_flags = &[
(false, true),
(false, false),
(false, true),
(false, false),
(false, true),
(false, false),
(false, true),
(true, true),
(true, true),
(false, false),
(false, false),
(false, false),
(false, false),
(false, false),
];
for (wire, path) in [
(
delegate_wire(&mpl_token_metadata::types::DelegateArgs::StakingV1 {
amount: 1,
authorization_data: std::option::Option::None,
}),
"delegate_privilege_superset",
),
(
revoke_wire(&mpl_token_metadata::types::RevokeArgs::StakingV1),
"revoke_privilege_superset",
),
] {
let result = crate::DcApiInstructionDecoder::decode(
&decoder,
&input_with_flags(wire.as_slice(), delegate_flags, false, path),
);
assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Decoded);
}
let lock_flags = &[
(true, false),
(true, true),
(false, true),
(false, false),
(false, true),
(false, false),
(false, true),
(true, true),
(false, false),
(false, false),
(false, false),
(false, false),
(false, false),
];
let lock = crate::DcApiInstructionDecoder::decode(
&decoder,
&input_with_flags(
programmable_wire(
crate::DC_METADATA_MTM_LOCK_DISCRIMINATOR,
&mpl_token_metadata::types::LockArgs::V1 {
authorization_data: std::option::Option::None,
},
)
.as_slice(),
lock_flags,
false,
"lock_privilege_superset",
),
);
assert_eq!(lock.status, crate::DcApiDecoderOutcomeStatus::Decoded);
let unlock = crate::DcApiInstructionDecoder::decode(
&decoder,
&input_with_flags(
programmable_wire(
crate::DC_METADATA_MTM_UNLOCK_DISCRIMINATOR,
&mpl_token_metadata::types::UnlockArgs::V1 {
authorization_data: std::option::Option::None,
},
)
.as_slice(),
lock_flags,
false,
"unlock_privilege_superset",
),
);
assert_eq!(unlock.status, crate::DcApiDecoderOutcomeStatus::Decoded);
}
#[test]
fn programmable_delegate_and_revoke_reject_truncated_invalid_and_suffix_wires() {
let decoder = crate::DcMetadataMetaplexTokenMetadataDecoder;

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/decoder/metadata/metaplex_token_metadata/instruction.rs
// version: 28
// version: 34
//! Bounded Metaplex Token Metadata instruction wire decoding.
@@ -1159,26 +1159,18 @@ fn validate_accounts(
accounts.len()
));
}
let mandatory: &[(usize, bool, bool)] = &[
(0, false, true),
(2, true, true),
(3, true, false),
(4, true, true),
(5, true, false),
(6, false, false),
(7, false, false),
(8, false, false),
];
for (index, signer, writable) in mandatory {
let required_privileges: &[(usize, bool, bool)] =
&[(0, false, true), (2, false, true), (3, true, false), (4, true, true)];
for (index, signer, writable) in required_privileges {
let account = match accounts.get(*index) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!("create is missing account {index}"));
},
};
if account.signer != *signer || account.writable != *writable {
if (*signer && !account.signer) || (*writable && !account.writable) {
return std::result::Result::Err(format!(
"create account {index} has invalid signer/writable flags"
"create account {index} lacks required signer/writable privileges"
));
}
}
@@ -1190,9 +1182,12 @@ fn validate_accounts(
);
},
};
if master_edition.signer {
if master_edition.account_key.as_str()
!= kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
&& !master_edition.writable
{
return std::result::Result::Err(
"create master edition account must not sign".to_string(),
"create master edition account lacks required writable privilege".to_string(),
);
}
return std::result::Result::Ok(());
@@ -1204,31 +1199,22 @@ fn validate_accounts(
accounts.len()
));
}
let mandatory: &[(usize, bool, bool)] = &[
(0, false, true),
(2, false, false),
(5, false, true),
(6, true, false),
(8, true, true),
(9, false, false),
(10, false, false),
(11, false, false),
(12, false, false),
];
for (index, signer, writable) in mandatory {
let required_privileges: &[(usize, bool, bool)] =
&[(0, false, true), (5, false, true), (6, true, false), (8, true, true)];
for (index, signer, writable) in required_privileges {
let account = match accounts.get(*index) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!("mint is missing account {index}"));
},
};
if account.signer != *signer || account.writable != *writable {
if (*signer && !account.signer) || (*writable && !account.writable) {
return std::result::Result::Err(format!(
"mint account {index} has invalid signer/writable flags"
"mint account {index} lacks required signer/writable privileges"
));
}
}
for index in [1_usize, 3_usize, 4_usize, 7_usize, 13_usize, 14_usize] {
for index in [3_usize, 4_usize] {
let account = match accounts.get(index) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
@@ -1237,9 +1223,12 @@ fn validate_accounts(
));
},
};
if account.signer {
if account.account_key.as_str()
!= kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
&& !account.writable
{
return std::result::Result::Err(format!(
"mint optional account {index} must not sign"
"mint optional account {index} lacks required writable privilege"
));
}
}
@@ -1252,35 +1241,27 @@ fn validate_accounts(
accounts.len()
));
}
let mandatory: &[(usize, bool, bool)] = &[
let required_privileges: &[(usize, bool, bool)] = &[
(0, false, true),
(1, false, true),
(2, true, true),
(3, false, false),
(4, false, true),
(5, true, false),
(7, false, true),
(8, false, true),
(9, true, true),
(10, true, false),
(11, false, false),
(12, false, false),
(13, false, false),
(14, false, false),
(15, false, false),
(16, false, false),
(17, false, false),
];
for (index, signer, writable) in mandatory {
for (index, signer, writable) in required_privileges {
let account = match accounts.get(*index) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!("print is missing account {index}"));
},
};
if account.signer != *signer || account.writable != *writable {
if (*signer && !account.signer) || (*writable && !account.writable) {
return std::result::Result::Err(format!(
"print account {index} has invalid signer/writable flags"
"print account {index} lacks required signer/writable privileges"
));
}
}
@@ -1297,6 +1278,14 @@ fn validate_accounts(
"print edition token record must not sign".to_string(),
);
}
if edition_token_record.account_key.as_str()
!= kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
&& !edition_token_record.writable
{
return std::result::Result::Err(
"print edition token record lacks required writable privilege".to_string(),
);
}
return std::result::Result::Ok(());
}
if code == "update" {
@@ -1321,9 +1310,9 @@ fn validate_accounts(
return std::result::Result::Err(format!("update is missing account {index}"));
},
};
if account.signer != *signer || account.writable != *writable {
if (*signer && !account.signer) || (*writable && !account.writable) {
return std::result::Result::Err(format!(
"update account {index} has invalid signer/writable flags"
"update account {index} lacks required signer/writable privileges"
));
}
}
@@ -1503,42 +1492,36 @@ fn validate_accounts(
accounts.len()
));
}
let mandatory: &[(usize, bool, bool)] = &[
(0, true, false),
(2, false, true),
(3, false, false),
(4, false, true),
(7, true, true),
(8, false, false),
(9, false, false),
];
for (index, signer, writable) in mandatory {
let required_privileges: &[(usize, bool, bool)] =
&[(0, true, false), (2, false, true), (4, false, true), (7, true, true)];
for (index, signer, writable) in required_privileges {
let account = match accounts.get(*index) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!("{code} is missing account {index}"));
},
};
if account.signer != *signer || account.writable != *writable {
if (*signer && !account.signer) || (*writable && !account.writable) {
return std::result::Result::Err(format!(
"{code} account {index} has invalid signer/writable flags"
"{code} account {index} lacks required signer/writable privileges"
));
}
}
for index in [1_usize, 5_usize, 6_usize, 10_usize, 11_usize, 12_usize] {
let account = match accounts.get(index) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!(
"{code} is missing optional placeholder account {index}"
));
},
};
if account.signer {
let token_record = match accounts.get(6) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!(
"{code} optional account {index} must not sign"
"{code} is missing token record placeholder"
));
}
},
};
if token_record.account_key.as_str()
!= kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
&& !token_record.writable
{
return std::result::Result::Err(format!(
"{code} token record lacks required writable privilege"
));
}
return std::result::Result::Ok(());
}
@@ -1549,27 +1532,18 @@ fn validate_accounts(
accounts.len()
));
}
let mandatory: &[(usize, bool, bool)] = &[
(0, true, false),
(3, false, false),
(4, false, true),
(6, true, false),
(7, false, false),
(8, false, false),
(9, false, false),
(10, false, false),
(11, false, false),
];
for (index, signer, writable) in mandatory {
let required_privileges: &[(usize, bool, bool)] =
&[(0, true, false), (4, false, true), (6, true, false)];
for (index, signer, writable) in required_privileges {
let account = match accounts.get(*index) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!("use is missing account {index}"));
},
};
if account.signer != *signer || account.writable != *writable {
if (*signer && !account.signer) || (*writable && !account.writable) {
return std::result::Result::Err(format!(
"use account {index} has invalid signer/writable flags"
"use account {index} lacks required signer/writable privileges"
));
}
}
@@ -1587,46 +1561,95 @@ fn validate_accounts(
"use optional account {index} must not sign"
));
}
if account.account_key.as_str()
!= kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
&& !account.writable
{
return std::result::Result::Err(format!(
"use optional account {index} lacks required writable privilege"
));
}
}
return std::result::Result::Ok(());
}
if code == "verify" || code == "unverify" {
if code == "verify" {
if accounts.len() < 8 {
return std::result::Result::Err(format!(
"{code} requires 8 positional accounts, received {}",
"verify requires 8 positional accounts, received {}",
accounts.len()
));
}
let mandatory: &[(usize, bool, bool)] =
&[(0, true, false), (2, false, true), (6, false, false), (7, false, false)];
for (index, signer, writable) in mandatory {
let required_privileges: &[(usize, bool, bool)] = &[(0, true, false), (2, false, true)];
for (index, signer, writable) in required_privileges {
let account = match accounts.get(*index) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!("{code} is missing account {index}"));
return std::result::Result::Err(format!("verify is missing account {index}"));
},
};
if account.signer != *signer || account.writable != *writable {
if (*signer && !account.signer) || (*writable && !account.writable) {
return std::result::Result::Err(format!(
"{code} account {index} has invalid signer/writable flags"
"verify account {index} lacks required signer/writable privileges"
));
}
}
for index in [1_usize, 3_usize, 4_usize, 5_usize] {
let account = match accounts.get(index) {
let collection_metadata = match accounts.get(4) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
"verify is missing collection metadata placeholder".to_string(),
);
},
};
if collection_metadata.account_key.as_str()
!= kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
&& !collection_metadata.writable
{
return std::result::Result::Err(
"verify collection metadata lacks required writable privilege".to_string(),
);
}
return std::result::Result::Ok(());
}
if code == "unverify" {
if accounts.len() < 7 {
return std::result::Result::Err(format!(
"unverify requires 7 positional accounts, received {}",
accounts.len()
));
}
let required_privileges: &[(usize, bool, bool)] = &[(0, true, false), (2, false, true)];
for (index, signer, writable) in required_privileges {
let account = match accounts.get(*index) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!(
"{code} is missing optional placeholder account {index}"
"unverify is missing account {index}"
));
},
};
if account.signer {
if (*signer && !account.signer) || (*writable && !account.writable) {
return std::result::Result::Err(format!(
"{code} optional account {index} must not sign"
"unverify account {index} lacks required signer/writable privileges"
));
}
}
let collection_metadata = match accounts.get(4) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
"unverify is missing collection metadata placeholder".to_string(),
);
},
};
if collection_metadata.account_key.as_str()
!= kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
&& !collection_metadata.writable
{
return std::result::Result::Err(
"unverify collection metadata lacks required writable privilege".to_string(),
);
}
return std::result::Result::Ok(());
}
if code == "burn" {
@@ -1682,29 +1705,18 @@ fn validate_accounts(
accounts.len()
));
}
let mandatory: &[(usize, bool, bool)] = &[
(1, false, false),
(2, false, true),
(3, false, false),
(5, false, false),
(7, true, false),
(8, true, true),
(9, false, false),
(10, false, false),
(11, false, false),
(12, false, false),
(13, false, false),
];
for (index, signer, writable) in mandatory {
let required_privileges: &[(usize, bool, bool)] =
&[(2, false, true), (7, true, false), (8, true, true)];
for (index, signer, writable) in required_privileges {
let account = match accounts.get(*index) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(format!("{code} is missing account {index}"));
},
};
if account.signer != *signer || account.writable != *writable {
if (*signer && !account.signer) || (*writable && !account.writable) {
return std::result::Result::Err(format!(
"{code} account {index} has invalid signer/writable flags"
"{code} account {index} lacks required signer/writable privileges"
));
}
}
@@ -1717,9 +1729,12 @@ fn validate_accounts(
));
},
};
if account.signer {
if account.account_key.as_str()
!= kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
&& !account.writable
{
return std::result::Result::Err(format!(
"{code} optional account {index} must not sign"
"{code} optional account {index} lacks required writable privilege"
));
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/executor/metadata/metaplex_token_metadata/builder.rs
// version: 8
// version: 11
//! Official Metaplex builders and conservative plan validation.
@@ -181,15 +181,15 @@ pub(crate) fn executor_metaplex_token_metadata_build_prepared_plan(
crate::ExMetaplexTokenMetadataOperation::Utilize { metadata, token_account, mint, use_authority, owner, token_program, ata_program, system_program, rent, use_authority_record, burner, args } => build_idl_instruction_with_args(19, &[(metadata, false, true), (token_account, false, true), (mint, false, true), (use_authority, true, true), (owner, false, false), (token_program, false, false), (ata_program, false, false), (system_program, false, false), (rent, false, false), (use_authority_record, false, true), (burner, false, false)], args),
crate::ExMetaplexTokenMetadataOperation::ApproveUseAuthority { use_authority_record, owner, payer, user, owner_token_account, metadata, mint, burner, token_program, system_program, rent, args } => build_idl_instruction_with_args(20, &[(use_authority_record, false, true), (owner, true, true), (payer, true, true), (user, false, false), (owner_token_account, false, true), (metadata, false, false), (mint, false, false), (burner, false, false), (token_program, false, false), (system_program, false, false), (rent, false, false)], args),
crate::ExMetaplexTokenMetadataOperation::RevokeUseAuthority { use_authority_record, owner, user, owner_token_account, mint, metadata, token_program, system_program, rent } => build_idl_instruction_no_args(21, &[(use_authority_record, false, true), (owner, true, true), (user, false, false), (owner_token_account, false, true), (mint, false, false), (metadata, false, false), (token_program, false, false), (system_program, false, false), (rent, false, false)]),
crate::ExMetaplexTokenMetadataOperation::CreateEscrowAccount { escrow, metadata, mint, token_account, edition, payer, system_program, sysvar_instructions, authority } => build_idl_instruction_no_args(38, &[(escrow, false, true), (metadata, false, true), (mint, false, false), (token_account, false, false), (edition, false, false), (payer, true, true), (system_program, false, false), (sysvar_instructions, false, false), (authority, true, false)]),
crate::ExMetaplexTokenMetadataOperation::CreateEscrowAccount { escrow, metadata, mint, token_account, edition, payer, system_program, sysvar_instructions, authority } => build_idl_instruction_trailing_optional_no_args(38, &[(escrow, false, true), (metadata, false, true), (mint, false, false), (token_account, false, false), (edition, false, false), (payer, true, true), (system_program, false, false), (sysvar_instructions, false, false)], authority.as_ref().map(|value| return (value, true, false))),
crate::ExMetaplexTokenMetadataOperation::CloseEscrowAccount { escrow, metadata, mint, token_account, edition, payer, system_program, sysvar_instructions } => build_idl_instruction_no_args(39, &[(escrow, false, true), (metadata, false, true), (mint, false, false), (token_account, false, false), (edition, false, false), (payer, true, true), (system_program, false, false), (sysvar_instructions, false, false)]),
crate::ExMetaplexTokenMetadataOperation::TransferOutOfEscrow { escrow, metadata, payer, attribute_mint, attribute_src, attribute_dst, escrow_mint, escrow_account, system_program, ata_program, token_program, sysvar_instructions, authority, args } => build_idl_instruction_with_args(40, &[(escrow, false, false), (metadata, false, true), (payer, true, true), (attribute_mint, false, false), (attribute_src, false, true), (attribute_dst, false, true), (escrow_mint, false, false), (escrow_account, false, false), (system_program, false, false), (ata_program, false, false), (token_program, false, false), (sysvar_instructions, false, false), (authority, true, false)], args),
crate::ExMetaplexTokenMetadataOperation::Mint { token, token_owner, metadata, master_edition, token_record, mint, authority, delegate_record, payer, system_program, sysvar_instructions, spl_token_program, spl_ata_program, authorization_rules_program, authorization_rules, args } => build_idl_instruction_with_args(43, &[(token, false, true), (token_owner, false, false), (metadata, false, false), (master_edition, false, true), (token_record, false, true), (mint, false, true), (authority, true, false), (delegate_record, false, false), (payer, true, true), (system_program, false, false), (sysvar_instructions, false, false), (spl_token_program, false, false), (spl_ata_program, false, false), (authorization_rules_program, false, false), (authorization_rules, false, false)], args),
crate::ExMetaplexTokenMetadataOperation::Migrate { metadata, edition, token, token_owner, mint, payer, authority, collection_metadata, delegate_record, token_record, system_program, sysvar_instructions, spl_token_program, authorization_rules_program, authorization_rules } => build_idl_instruction_no_args(48, &[(metadata, false, true), (edition, false, true), (token, false, true), (token_owner, false, false), (mint, false, false), (payer, true, true), (authority, true, false), (collection_metadata, false, false), (delegate_record, false, false), (token_record, false, true), (system_program, false, false), (sysvar_instructions, false, false), (spl_token_program, false, false), (authorization_rules_program, false, false), (authorization_rules, false, false)]),
crate::ExMetaplexTokenMetadataOperation::Use { authority, delegate_record, token, mint, metadata, edition, payer, system_program, sysvar_instructions, spl_token_program, authorization_rules_program, authorization_rules, args } => build_idl_instruction_with_args(51, &[(authority, true, false), (delegate_record, false, true), (token, false, true), (mint, false, false), (metadata, false, true), (edition, false, true), (payer, true, false), (system_program, false, false), (sysvar_instructions, false, false), (spl_token_program, false, false), (authorization_rules_program, false, false), (authorization_rules, false, false)], args),
crate::ExMetaplexTokenMetadataOperation::TransferOutOfEscrow { escrow, metadata, payer, attribute_mint, attribute_src, attribute_dst, escrow_mint, escrow_account, system_program, ata_program, token_program, sysvar_instructions, authority, args } => build_idl_instruction_trailing_optional_with_args(40, &[(escrow, false, false), (metadata, false, true), (payer, true, true), (attribute_mint, false, false), (attribute_src, false, true), (attribute_dst, false, true), (escrow_mint, false, false), (escrow_account, false, false), (system_program, false, false), (ata_program, false, false), (token_program, false, false), (sysvar_instructions, false, false)], authority.as_ref().map(|value| return (value, true, false)), args),
crate::ExMetaplexTokenMetadataOperation::Mint { token, token_owner, metadata, master_edition, token_record, mint, authority, delegate_record, payer, system_program, sysvar_instructions, spl_token_program, spl_ata_program, authorization_rules_program, authorization_rules, args } => build_idl_instruction_optional_with_args(43, &[(std::option::Option::Some(token), false, true), (token_owner.as_ref(), false, false), (std::option::Option::Some(metadata), false, false), (master_edition.as_ref(), false, true), (token_record.as_ref(), false, true), (std::option::Option::Some(mint), false, true), (std::option::Option::Some(authority), true, false), (delegate_record.as_ref(), false, false), (std::option::Option::Some(payer), true, true), (std::option::Option::Some(system_program), false, false), (std::option::Option::Some(sysvar_instructions), false, false), (std::option::Option::Some(spl_token_program), false, false), (std::option::Option::Some(spl_ata_program), false, false), (authorization_rules_program.as_ref(), false, false), (authorization_rules.as_ref(), false, false)], args),
crate::ExMetaplexTokenMetadataOperation::Migrate { metadata, edition, token, token_owner, mint, payer, authority, collection_metadata, delegate_record, token_record, system_program, sysvar_instructions, spl_token_program, authorization_rules_program, authorization_rules } => build_idl_instruction_optional_no_args(48, &[(std::option::Option::Some(metadata), false, true), (std::option::Option::Some(edition), false, true), (std::option::Option::Some(token), false, true), (std::option::Option::Some(token_owner), false, false), (std::option::Option::Some(mint), false, false), (std::option::Option::Some(payer), true, true), (std::option::Option::Some(authority), true, false), (std::option::Option::Some(collection_metadata), false, false), (std::option::Option::Some(delegate_record), false, false), (std::option::Option::Some(token_record), false, true), (std::option::Option::Some(system_program), false, false), (std::option::Option::Some(sysvar_instructions), false, false), (std::option::Option::Some(spl_token_program), false, false), (authorization_rules_program.as_ref(), false, false), (authorization_rules.as_ref(), false, false)]),
crate::ExMetaplexTokenMetadataOperation::Use { authority, delegate_record, token, mint, metadata, edition, payer, system_program, sysvar_instructions, spl_token_program, authorization_rules_program, authorization_rules, args } => build_idl_instruction_optional_with_args(51, &[(std::option::Option::Some(authority), true, false), (delegate_record.as_ref(), false, true), (token.as_ref(), false, true), (std::option::Option::Some(mint), false, false), (std::option::Option::Some(metadata), false, true), (edition.as_ref(), false, true), (std::option::Option::Some(payer), true, false), (std::option::Option::Some(system_program), false, false), (std::option::Option::Some(sysvar_instructions), false, false), (spl_token_program.as_ref(), false, false), (authorization_rules_program.as_ref(), false, false), (authorization_rules.as_ref(), false, false)], args),
crate::ExMetaplexTokenMetadataOperation::Collect { authority, recipient } => build_idl_instruction_no_args(54, &[(authority, true, false), (recipient, false, false)]),
crate::ExMetaplexTokenMetadataOperation::Print { edition_metadata, edition, edition_mint, edition_token_account_owner, edition_token_account, edition_mint_authority, edition_token_record, master_edition, edition_marker_pda, payer, master_token_account_owner, master_token_account, master_metadata, update_authority, spl_token_program, spl_ata_program, sysvar_instructions, system_program, args } => build_idl_instruction_with_args(55, &[(edition_metadata, false, true), (edition, false, true), (edition_mint, false, true), (edition_token_account_owner, false, false), (edition_token_account, false, true), (edition_mint_authority, true, false), (edition_token_record, false, true), (master_edition, false, true), (edition_marker_pda, false, true), (payer, true, true), (master_token_account_owner, false, false), (master_token_account, false, false), (master_metadata, false, false), (update_authority, false, false), (spl_token_program, false, false), (spl_ata_program, false, false), (sysvar_instructions, false, false), (system_program, false, false)], args),
crate::ExMetaplexTokenMetadataOperation::Resize { metadata, edition, mint, payer, authority, token, system_program } => build_idl_instruction_no_args(56, &[(metadata, false, true), (edition, false, true), (mint, false, false), (payer, false, true), (authority, true, false), (token, false, false), (system_program, false, false)]),
crate::ExMetaplexTokenMetadataOperation::Print { edition_metadata, edition, edition_mint, edition_token_account_owner, edition_token_account, edition_mint_authority, edition_token_record, master_edition, edition_marker_pda, payer, master_token_account_owner, master_token_account, master_metadata, update_authority, spl_token_program, spl_ata_program, sysvar_instructions, system_program, args } => build_print(edition_metadata, edition, edition_mint, edition_token_account_owner, edition_token_account, edition_mint_authority, edition_token_record.as_ref(), master_edition, edition_marker_pda, payer, master_token_account_owner, master_token_account, master_metadata, update_authority, spl_token_program, spl_ata_program, sysvar_instructions, system_program, args),
crate::ExMetaplexTokenMetadataOperation::Resize { metadata, edition, mint, payer, authority, token, system_program } => build_resize(metadata, edition, mint, payer, authority.as_ref(), token.as_ref(), system_program),
};
let instruction = match instruction_result {
std::result::Result::Ok(value) => value,
@@ -1207,6 +1207,192 @@ fn build_close_accounts(
return std::result::Result::Ok(convert_official_instruction!(official));
}
#[allow(clippy::too_many_arguments)]
fn build_print(
edition_metadata: &crate::MdPubkey,
edition: &crate::MdPubkey,
edition_mint: &crate::MdPubkey,
edition_token_account_owner: &crate::MdPubkey,
edition_token_account: &crate::MdPubkey,
edition_mint_authority: &crate::MdPubkey,
edition_token_record: std::option::Option<&crate::MdPubkey>,
master_edition: &crate::MdPubkey,
edition_marker_pda: &crate::MdPubkey,
payer: &crate::MdPubkey,
master_token_account_owner: &crate::MdPubkey,
master_token_account: &crate::MdPubkey,
master_metadata: &crate::MdPubkey,
update_authority: &crate::MdPubkey,
spl_token_program: &crate::MdPubkey,
spl_ata_program: &crate::MdPubkey,
sysvar_instructions: &crate::MdPubkey,
system_program: &crate::MdPubkey,
args: &mpl_token_metadata::types::PrintArgs,
) -> kb_core::Result<solana_instruction::Instruction> {
let mut builder = mpl_token_metadata::instructions::PrintBuilder::new();
builder
.edition_metadata(parse_official_pubkey!(&edition_metadata.0, "edition_metadata"))
.edition(parse_official_pubkey!(&edition.0, "edition"))
.edition_mint(parse_official_pubkey!(&edition_mint.0, "edition_mint"), true)
.edition_token_account_owner(parse_official_pubkey!(
&edition_token_account_owner.0,
"edition_token_account_owner"
))
.edition_token_account(parse_official_pubkey!(
&edition_token_account.0,
"edition_token_account"
))
.edition_mint_authority(parse_official_pubkey!(
&edition_mint_authority.0,
"edition_mint_authority"
))
.edition_token_record(parse_optional_official_pubkey!(
edition_token_record,
"edition_token_record"
))
.master_edition(parse_official_pubkey!(&master_edition.0, "master_edition"))
.edition_marker_pda(parse_official_pubkey!(&edition_marker_pda.0, "edition_marker_pda"))
.payer(parse_official_pubkey!(&payer.0, "payer"))
.master_token_account_owner(
parse_official_pubkey!(&master_token_account_owner.0, "master_token_account_owner"),
true,
)
.master_token_account(parse_official_pubkey!(
&master_token_account.0,
"master_token_account"
))
.master_metadata(parse_official_pubkey!(&master_metadata.0, "master_metadata"))
.update_authority(parse_official_pubkey!(&update_authority.0, "update_authority"))
.spl_token_program(parse_official_pubkey!(&spl_token_program.0, "spl_token_program"))
.spl_ata_program(parse_official_pubkey!(&spl_ata_program.0, "spl_ata_program"))
.sysvar_instructions(parse_official_pubkey!(&sysvar_instructions.0, "sysvar_instructions"))
.system_program(parse_official_pubkey!(&system_program.0, "system_program"))
.print_args(args.clone());
let official = builder.instruction();
return std::result::Result::Ok(convert_official_instruction!(official));
}
fn build_resize(
metadata: &crate::MdPubkey,
edition: &crate::MdPubkey,
mint: &crate::MdPubkey,
payer: &crate::MdPubkey,
authority: std::option::Option<&crate::MdPubkey>,
token: std::option::Option<&crate::MdPubkey>,
system_program: &crate::MdPubkey,
) -> kb_core::Result<solana_instruction::Instruction> {
let mut builder = mpl_token_metadata::instructions::ResizeBuilder::new();
builder
.metadata(parse_official_pubkey!(&metadata.0, "metadata"))
.edition(parse_official_pubkey!(&edition.0, "edition"))
.mint(parse_official_pubkey!(&mint.0, "mint"))
.payer(parse_official_pubkey!(&payer.0, "payer"), true)
.authority(parse_optional_official_pubkey!(authority, "authority"))
.token(parse_optional_official_pubkey!(token, "token"))
.system_program(parse_official_pubkey!(&system_program.0, "system_program"));
let official = builder.instruction();
return std::result::Result::Ok(convert_official_instruction!(official));
}
fn build_idl_instruction_trailing_optional_no_args(
discriminator: u8,
accounts: &[(&crate::MdPubkey, bool, bool)],
trailing_account: std::option::Option<(&crate::MdPubkey, bool, bool)>,
) -> kb_core::Result<solana_instruction::Instruction> {
let mut exact_accounts = accounts.to_vec();
if let std::option::Option::Some(value) = trailing_account {
exact_accounts.push(value);
}
return build_idl_instruction_no_args(discriminator, exact_accounts.as_slice());
}
fn build_idl_instruction_trailing_optional_with_args<T: borsh_0_10::BorshSerialize>(
discriminator: u8,
accounts: &[(&crate::MdPubkey, bool, bool)],
trailing_account: std::option::Option<(&crate::MdPubkey, bool, bool)>,
args: &T,
) -> kb_core::Result<solana_instruction::Instruction> {
let mut exact_accounts = accounts.to_vec();
if let std::option::Option::Some(value) = trailing_account {
exact_accounts.push(value);
}
return build_idl_instruction_with_args(discriminator, exact_accounts.as_slice(), args);
}
fn build_idl_instruction_optional_no_args(
discriminator: u8,
accounts: &[(std::option::Option<&crate::MdPubkey>, bool, bool)],
) -> kb_core::Result<solana_instruction::Instruction> {
return build_idl_instruction_optional(discriminator, accounts, std::option::Option::None);
}
fn build_idl_instruction_optional_with_args<T: borsh_0_10::BorshSerialize>(
discriminator: u8,
accounts: &[(std::option::Option<&crate::MdPubkey>, bool, bool)],
args: &T,
) -> kb_core::Result<solana_instruction::Instruction> {
let encoded = match borsh_0_10::BorshSerialize::try_to_vec(args) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_arguments_serialize_failed",
error.to_string(),
));
},
};
return build_idl_instruction_optional(
discriminator,
accounts,
std::option::Option::Some(encoded),
);
}
fn build_idl_instruction_optional(
discriminator: u8,
accounts: &[(std::option::Option<&crate::MdPubkey>, bool, bool)],
encoded_args: std::option::Option<std::vec::Vec<u8>>,
) -> kb_core::Result<solana_instruction::Instruction> {
let program_id = match parse_str_pubkey(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
"program_id",
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut metas = std::vec::Vec::with_capacity(accounts.len());
for (value, is_signer, is_writable) in accounts {
match value {
std::option::Option::Some(value) => {
let pubkey = match parse_pubkey(value, "instruction_account") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
metas.push(solana_instruction::AccountMeta {
pubkey,
is_signer: *is_signer,
is_writable: *is_writable,
});
},
std::option::Option::None => {
metas.push(solana_instruction::AccountMeta {
pubkey: program_id,
is_signer: false,
is_writable: false,
});
},
}
}
let mut data = vec![discriminator];
if let std::option::Option::Some(mut value) = encoded_args {
data.append(&mut value);
}
return std::result::Result::Ok(solana_instruction::Instruction {
program_id,
accounts: metas,
data,
});
}
fn build_idl_instruction_no_args(
discriminator: u8,
accounts: &[(&crate::MdPubkey, bool, bool)],
@@ -1447,6 +1633,316 @@ mod tests {
assert!(instruction.accounts[3].is_signer);
}
#[test]
fn print_uses_current_official_signer_contract() {
let accounts: std::vec::Vec<crate::MdPubkey> = (0..18)
.map(|_| return crate::MdPubkey(solana_pubkey::Pubkey::new_unique().to_string()))
.collect();
let result = super::build_print(
&accounts[0],
&accounts[1],
&accounts[2],
&accounts[3],
&accounts[4],
&accounts[5],
std::option::Option::Some(&accounts[6]),
&accounts[7],
&accounts[8],
&accounts[9],
&accounts[10],
&accounts[11],
&accounts[12],
&accounts[13],
&accounts[14],
&accounts[15],
&accounts[16],
&accounts[17],
&mpl_token_metadata::types::PrintArgs::V1 { edition: 1 },
);
assert!(result.is_ok());
let instruction = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(instruction.data.first().copied(), std::option::Option::Some(55));
assert_eq!(instruction.accounts.len(), 18);
assert!(instruction.accounts[2].is_signer);
assert!(instruction.accounts[2].is_writable);
assert!(instruction.accounts[5].is_signer);
assert!(!instruction.accounts[5].is_writable);
assert!(instruction.accounts[9].is_signer);
assert!(instruction.accounts[9].is_writable);
assert!(instruction.accounts[10].is_signer);
assert!(!instruction.accounts[10].is_writable);
}
#[test]
fn resize_uses_current_official_payer_signer_contract() {
let accounts: std::vec::Vec<crate::MdPubkey> = (0..7)
.map(|_| return crate::MdPubkey(solana_pubkey::Pubkey::new_unique().to_string()))
.collect();
let result = super::build_resize(
&accounts[0],
&accounts[1],
&accounts[2],
&accounts[3],
std::option::Option::Some(&accounts[4]),
std::option::Option::Some(&accounts[5]),
&accounts[6],
);
assert!(result.is_ok());
let instruction = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(instruction.data, vec![56]);
assert_eq!(instruction.accounts.len(), 7);
assert!(instruction.accounts[3].is_signer);
assert!(instruction.accounts[3].is_writable);
assert!(instruction.accounts[4].is_signer);
assert!(!instruction.accounts[4].is_writable);
}
#[test]
fn positional_optional_accounts_use_placeholders_but_escrow_tail_is_omitted() {
let accounts: std::vec::Vec<crate::MdPubkey> = (0..15)
.map(|_| return crate::MdPubkey(solana_pubkey::Pubkey::new_unique().to_string()))
.collect();
let program_id = match super::parse_str_pubkey(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
"program_id",
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let create = super::build_idl_instruction_trailing_optional_no_args(
38,
&[
(&accounts[0], false, true),
(&accounts[1], false, true),
(&accounts[2], false, false),
(&accounts[3], false, false),
(&accounts[4], false, false),
(&accounts[5], true, true),
(&accounts[6], false, false),
(&accounts[7], false, false),
],
std::option::Option::None,
);
assert!(create.is_ok());
let create = match create {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(create.accounts.len(), 8);
let create_with_authority = super::build_idl_instruction_trailing_optional_no_args(
38,
&[
(&accounts[0], false, true),
(&accounts[1], false, true),
(&accounts[2], false, false),
(&accounts[3], false, false),
(&accounts[4], false, false),
(&accounts[5], true, true),
(&accounts[6], false, false),
(&accounts[7], false, false),
],
std::option::Option::Some((&accounts[8], true, false)),
);
assert!(create_with_authority.is_ok());
let create_with_authority = match create_with_authority {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(create_with_authority.accounts.len(), 9);
assert_eq!(create_with_authority.accounts[8].pubkey.to_string(), accounts[8].0);
assert!(create_with_authority.accounts[8].is_signer);
assert!(!create_with_authority.accounts[8].is_writable);
let transfer_out = super::build_idl_instruction_trailing_optional_with_args(
40,
&[
(&accounts[0], false, false),
(&accounts[1], false, true),
(&accounts[2], true, true),
(&accounts[3], false, false),
(&accounts[4], false, true),
(&accounts[5], false, true),
(&accounts[6], false, false),
(&accounts[7], false, false),
(&accounts[8], false, false),
(&accounts[9], false, false),
(&accounts[10], false, false),
(&accounts[11], false, false),
],
std::option::Option::None,
&mpl_token_metadata::instructions::TransferOutOfEscrowInstructionArgs { amount: 1 },
);
assert!(transfer_out.is_ok());
let transfer_out = match transfer_out {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(transfer_out.accounts.len(), 12);
let mint = super::build_idl_instruction_optional_with_args(
43,
&[
(std::option::Option::Some(&accounts[0]), false, true),
(std::option::Option::None, false, false),
(std::option::Option::Some(&accounts[2]), false, false),
(std::option::Option::None, false, true),
(std::option::Option::None, false, true),
(std::option::Option::Some(&accounts[5]), false, true),
(std::option::Option::Some(&accounts[6]), true, false),
(std::option::Option::None, false, false),
(std::option::Option::Some(&accounts[8]), true, true),
(std::option::Option::Some(&accounts[9]), false, false),
(std::option::Option::Some(&accounts[10]), false, false),
(std::option::Option::Some(&accounts[11]), false, false),
(std::option::Option::Some(&accounts[12]), false, false),
(std::option::Option::None, false, false),
(std::option::Option::None, false, false),
],
&mpl_token_metadata::types::MintArgs::V1 {
amount: 1,
authorization_data: std::option::Option::None,
},
);
assert!(mint.is_ok());
let mint = match mint {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(mint.accounts.len(), 15);
for index in [1_usize, 3, 4, 7, 13, 14] {
assert_eq!(mint.accounts[index].pubkey, program_id);
assert!(!mint.accounts[index].is_signer);
assert!(!mint.accounts[index].is_writable);
}
let migrate = super::build_idl_instruction_optional_no_args(
48,
&[
(std::option::Option::Some(&accounts[0]), false, true),
(std::option::Option::Some(&accounts[1]), false, true),
(std::option::Option::Some(&accounts[2]), false, true),
(std::option::Option::Some(&accounts[3]), false, false),
(std::option::Option::Some(&accounts[4]), false, false),
(std::option::Option::Some(&accounts[5]), true, true),
(std::option::Option::Some(&accounts[6]), true, false),
(std::option::Option::Some(&accounts[7]), false, false),
(std::option::Option::Some(&accounts[8]), false, false),
(std::option::Option::Some(&accounts[9]), false, true),
(std::option::Option::Some(&accounts[10]), false, false),
(std::option::Option::Some(&accounts[11]), false, false),
(std::option::Option::Some(&accounts[12]), false, false),
(std::option::Option::None, false, false),
(std::option::Option::None, false, false),
],
);
assert!(migrate.is_ok());
let migrate = match migrate {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(migrate.accounts[13].pubkey, program_id);
assert_eq!(migrate.accounts[14].pubkey, program_id);
let use_instruction = super::build_idl_instruction_optional_with_args(
51,
&[
(std::option::Option::Some(&accounts[0]), true, false),
(std::option::Option::None, false, true),
(std::option::Option::None, false, true),
(std::option::Option::Some(&accounts[3]), false, false),
(std::option::Option::Some(&accounts[4]), false, true),
(std::option::Option::None, false, true),
(std::option::Option::Some(&accounts[6]), true, false),
(std::option::Option::Some(&accounts[7]), false, false),
(std::option::Option::Some(&accounts[8]), false, false),
(std::option::Option::None, false, false),
(std::option::Option::None, false, false),
(std::option::Option::None, false, false),
],
&mpl_token_metadata::types::UseArgs::V1 {
authorization_data: std::option::Option::None,
},
);
assert!(use_instruction.is_ok());
let use_instruction = match use_instruction {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(use_instruction.accounts.len(), 12);
for index in [1_usize, 2, 5, 9, 10, 11] {
assert_eq!(use_instruction.accounts[index].pubkey, program_id);
assert!(!use_instruction.accounts[index].is_signer);
assert!(!use_instruction.accounts[index].is_writable);
}
}
#[test]
fn print_and_resize_accept_absent_current_optional_accounts() {
let accounts: std::vec::Vec<crate::MdPubkey> = (0..18)
.map(|_| return crate::MdPubkey(solana_pubkey::Pubkey::new_unique().to_string()))
.collect();
let program_id = match super::parse_str_pubkey(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
"program_id",
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let print = super::build_print(
&accounts[0],
&accounts[1],
&accounts[2],
&accounts[3],
&accounts[4],
&accounts[5],
std::option::Option::None,
&accounts[7],
&accounts[8],
&accounts[9],
&accounts[10],
&accounts[11],
&accounts[12],
&accounts[13],
&accounts[14],
&accounts[15],
&accounts[16],
&accounts[17],
&mpl_token_metadata::types::PrintArgs::V1 { edition: 1 },
);
assert!(print.is_ok());
let print = match print {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(print.accounts.len(), 18);
assert_eq!(print.accounts[6].pubkey, program_id);
assert!(!print.accounts[6].is_signer);
assert!(!print.accounts[6].is_writable);
let resize = super::build_resize(
&accounts[0],
&accounts[1],
&accounts[2],
&accounts[3],
std::option::Option::None,
std::option::Option::None,
&accounts[6],
);
assert!(resize.is_ok());
let resize = match resize {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(resize.accounts.len(), 7);
assert_eq!(resize.accounts[4].pubkey, program_id);
assert_eq!(resize.accounts[5].pubkey, program_id);
assert!(!resize.accounts[4].is_signer);
assert!(!resize.accounts[4].is_writable);
assert!(!resize.accounts[5].is_signer);
assert!(!resize.accounts[5].is_writable);
}
#[test]
fn collect_builds_exact_discriminator_and_signer_contract() {
let authority = crate::MdPubkey("11111111111111111111111111111111".to_string());

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/executor/metadata/metaplex_token_metadata/intent.rs
// version: 8
// version: 9
//! Typed current and deprecated Metaplex Token Metadata execution intents.
@@ -729,8 +729,8 @@ pub enum ExMetaplexTokenMetadataOperation {
system_program: crate::MdPubkey,
/// Account `sysvarInstructions` from the official IDL contract.
sysvar_instructions: crate::MdPubkey,
/// Account `authority` from the official IDL contract.
authority: crate::MdPubkey,
/// Optional authority from the current SDK contract.
authority: std::option::Option<crate::MdPubkey>,
},
/// Build the current CloseEscrowAccount instruction.
CloseEscrowAccount {
@@ -777,30 +777,30 @@ pub enum ExMetaplexTokenMetadataOperation {
token_program: crate::MdPubkey,
/// Account `sysvarInstructions` from the official IDL contract.
sysvar_instructions: crate::MdPubkey,
/// Account `authority` from the official IDL contract.
authority: crate::MdPubkey,
/// Optional authority from the current SDK contract.
authority: std::option::Option<crate::MdPubkey>,
/// Official Metaplex instruction arguments.
#[ts(type = "unknown")]
args: mpl_token_metadata::instructions::TransferOutOfEscrowInstructionArgs,
},
/// Build the current Mint instruction.
Mint {
/// Account `token` from the official IDL contract.
/// Account `token` from the current SDK contract.
token: crate::MdPubkey,
/// Account `tokenOwner` from the official IDL contract.
token_owner: crate::MdPubkey,
/// Optional owner of the token account in the current SDK contract.
token_owner: std::option::Option<crate::MdPubkey>,
/// Account `metadata` from the official IDL contract.
metadata: crate::MdPubkey,
/// Account `masterEdition` from the official IDL contract.
master_edition: crate::MdPubkey,
/// Account `tokenRecord` from the official IDL contract.
token_record: crate::MdPubkey,
/// Optional Master Edition account in the current SDK contract.
master_edition: std::option::Option<crate::MdPubkey>,
/// Optional token record account in the current SDK contract.
token_record: std::option::Option<crate::MdPubkey>,
/// Account `mint` from the official IDL contract.
mint: crate::MdPubkey,
/// Account `authority` from the official IDL contract.
authority: crate::MdPubkey,
/// Account `delegateRecord` from the official IDL contract.
delegate_record: crate::MdPubkey,
/// Optional delegate record account in the current SDK contract.
delegate_record: std::option::Option<crate::MdPubkey>,
/// Account `payer` from the official IDL contract.
payer: crate::MdPubkey,
/// Account `systemProgram` from the official IDL contract.
@@ -811,10 +811,10 @@ pub enum ExMetaplexTokenMetadataOperation {
spl_token_program: crate::MdPubkey,
/// Account `splAtaProgram` from the official IDL contract.
spl_ata_program: crate::MdPubkey,
/// Account `authorizationRulesProgram` from the official IDL contract.
authorization_rules_program: crate::MdPubkey,
/// Account `authorizationRules` from the official IDL contract.
authorization_rules: crate::MdPubkey,
/// Optional Token Authorization Rules program in the current SDK contract.
authorization_rules_program: std::option::Option<crate::MdPubkey>,
/// Optional Token Authorization Rules account in the current SDK contract.
authorization_rules: std::option::Option<crate::MdPubkey>,
/// Official Metaplex instruction arguments.
#[ts(type = "unknown")]
args: mpl_token_metadata::types::MintArgs,
@@ -847,25 +847,25 @@ pub enum ExMetaplexTokenMetadataOperation {
sysvar_instructions: crate::MdPubkey,
/// Account `splTokenProgram` from the official IDL contract.
spl_token_program: crate::MdPubkey,
/// Account `authorizationRulesProgram` from the official IDL contract.
authorization_rules_program: crate::MdPubkey,
/// Account `authorizationRules` from the official IDL contract.
authorization_rules: crate::MdPubkey,
/// Optional Token Authorization Rules program in the current SDK contract.
authorization_rules_program: std::option::Option<crate::MdPubkey>,
/// Optional Token Authorization Rules account in the current SDK contract.
authorization_rules: std::option::Option<crate::MdPubkey>,
},
/// Build the current Use instruction.
Use {
/// Account `authority` from the official IDL contract.
authority: crate::MdPubkey,
/// Account `delegateRecord` from the official IDL contract.
delegate_record: crate::MdPubkey,
/// Optional delegate record account in the current SDK contract.
delegate_record: std::option::Option<crate::MdPubkey>,
/// Account `token` from the official IDL contract.
token: crate::MdPubkey,
token: std::option::Option<crate::MdPubkey>,
/// Account `mint` from the official IDL contract.
mint: crate::MdPubkey,
/// Account `metadata` from the official IDL contract.
metadata: crate::MdPubkey,
/// Account `edition` from the official IDL contract.
edition: crate::MdPubkey,
edition: std::option::Option<crate::MdPubkey>,
/// Account `payer` from the official IDL contract.
payer: crate::MdPubkey,
/// Account `systemProgram` from the official IDL contract.
@@ -873,11 +873,11 @@ pub enum ExMetaplexTokenMetadataOperation {
/// Account `sysvarInstructions` from the official IDL contract.
sysvar_instructions: crate::MdPubkey,
/// Account `splTokenProgram` from the official IDL contract.
spl_token_program: crate::MdPubkey,
/// Account `authorizationRulesProgram` from the official IDL contract.
authorization_rules_program: crate::MdPubkey,
/// Account `authorizationRules` from the official IDL contract.
authorization_rules: crate::MdPubkey,
spl_token_program: std::option::Option<crate::MdPubkey>,
/// Optional Token Authorization Rules program in the current SDK contract.
authorization_rules_program: std::option::Option<crate::MdPubkey>,
/// Optional Token Authorization Rules account in the current SDK contract.
authorization_rules: std::option::Option<crate::MdPubkey>,
/// Official Metaplex instruction arguments.
#[ts(type = "unknown")]
args: mpl_token_metadata::types::UseArgs,
@@ -903,8 +903,8 @@ pub enum ExMetaplexTokenMetadataOperation {
edition_token_account: crate::MdPubkey,
/// Account `editionMintAuthority` from the official IDL contract.
edition_mint_authority: crate::MdPubkey,
/// Account `editionTokenRecord` from the official IDL contract.
edition_token_record: crate::MdPubkey,
/// Optional edition token record account in the current SDK contract.
edition_token_record: std::option::Option<crate::MdPubkey>,
/// Account `masterEdition` from the official IDL contract.
master_edition: crate::MdPubkey,
/// Account `editionMarkerPda` from the official IDL contract.
@@ -941,10 +941,10 @@ pub enum ExMetaplexTokenMetadataOperation {
mint: crate::MdPubkey,
/// Account `payer` from the official IDL contract.
payer: crate::MdPubkey,
/// Account `authority` from the official IDL contract.
authority: crate::MdPubkey,
/// Account `token` from the official IDL contract.
token: crate::MdPubkey,
/// Optional authority from the current SDK contract.
authority: std::option::Option<crate::MdPubkey>,
/// Optional token account from the current SDK contract.
token: std::option::Option<crate::MdPubkey>,
/// Account `systemProgram` from the official IDL contract.
system_program: crate::MdPubkey,
},

View File

@@ -1,10 +1,119 @@
<!-- file: kb-pipeline-demo-scenarios/CHANGELOG.md -->
<!-- version: 34 -->
<!-- version: 71 -->
# CHANGELOG — kb-pipeline-demo-scenarios
- `pre.013-delta-fix-037` consolide la clôture Metaplex après validation locale complète de `fix-036` : `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets` sans warning, audit workspace clean et 105 tests unitaires + 1 CLI + 1 API externe réussissent. Les guides actifs cessent de présenter `Verify/Unverify`, le lifecycle pNFT ou Token Owned Escrow comme `not_run`, la campagne maintenance finale est documentée, et le TODO retire les tâches `pre.013` terminées. La matrice fonctionnelle reste inchangée à 15 `confirmed`, 5 `unavailable`, 0 `not_run`.
## `0.4.8-pre.013`
- `pre.013-delta-fix-036` ferme la matrice Devnet des 20 opérations courantes : `Update` est confirmé sur NFT par `4hxxBnsCA1JBJPNHgXRDeq1W7rXbFj2yySscWJKxU1VTMDcPzPfoWy7QzANHSDsbQfxxcwuAQNDLqEukgsug6qoZ` au slot `482162413`, avec simulation au slot `482162408`, replay, matérialisation, idempotence et transition `primary_sale_happened false -> true`; `Resize`, `Migrate`, `Collect` et `CloseAccounts` sont classés `unavailable` après probes Devnet exacts `Custom(201)`, `Custom(75)`, `Custom(7)` et `Custom(188)` sans soumission. La matrice finale compte 15 `confirmed`, 5 `unavailable`, 0 `not_run`; le warning Clippy `if_same_then_else` du test de matrice est supprimé en fusionnant les branches NFT identiques.
- `pre.013-delta-fix-035` corrige le parseur des refus runtime de la campagne maintenance : `simulation.error` contient déjà le JSON RPC sous forme de chaîne, et sa double sérialisation empêchait `fix-034` de reconnaître le `Custom(201)` pourtant observé. Le probe parse désormais structurellement `InstructionError[1].Custom`; le test de contrat existant couvre les codes `201`, `75`, `7` et `188`. La matrice reste à 14 `confirmed`, 2 `unavailable` et 4 `not_run`.
- `pre.013-delta-fix-034` enregistre le premier run de maintenance : `Update` franchit confirmation et postcondition `primary_sale_happened false -> true`, puis `Resize` atteint le handler courant et la simulation est refusée avec `AlreadyResized`, `Custom(201)`, `14643` CU consommées et aucune soumission. Comme les comptes créés aujourdhui sont déjà au layout cible et quun succès exigerait un compte legacy sous-dimensionné contrôlé, `Resize` passe à `unavailable`; la matrice devient 14 `confirmed`, 2 `unavailable` et 4 `not_run`. La campagne corrigée soumet uniquement `Update` et traite désormais `Resize`, `Migrate`, `Collect` et `CloseAccounts` comme probes simulation-only attendus respectivement en `201`, `75`, `7` et `188`.
- `pre.013-delta-fix-033` qualifie définitivement Token Owned Escrow : `CreateEscrowAccount` (`Yy7NEvme3obuiGRBbBjpuk3dv4aPMPBQ2ns7KmbY4K5EtyAimaaM8qN5SQzEqr9SPbRu1VDCFkNpAjnRGW4JYuj`, slot `482147206`), `TransferOutOfEscrow` (`4yJcSvnZJqihfZTE7nxN3o7H7zBCacpdZdwNQ1ueWQMmQZxR7KRb99C7rfrXX2AyezfSStMdqqTYL3oEWgwRhFi3`, slot `482147252`) et `CloseEscrowAccount` (`3GuwmGf9JEK7rHE3tudnxskvruyML9AGVXQimxii3uHdXqNEF4ScpmDXwhU3y2eaUnTbBPPhVkhRfLwJr9jbGJBo`, slot `482147267`) terminent simulation, confirmation, hydratation canonique, extraction Core, replay, matérialisation, postconditions stateful et idempotence ; la matrice passe à 14 `confirmed`, 1 `unavailable` et 5 `not_run`.
- prépare la campagne finale de maintenance sur un NFT frais : `Update` doit retourner `primary_sale_happened false -> true`, `Resize` doit confirmer sans altérer la possession du NFT, tandis que `Migrate`, `Collect` et `CloseAccounts` sont uniquement simulés et doivent respectivement exposer les refus runtime `Removed(75)`, `UpdateAuthorityIncorrect(7)` et `InvalidCloseAuthority(188)` sans aucune soumission.
- ajoute trois tests au scénario maintenance et conserve les probes réservés fail-closed : la campagne nessaie pas dusurper `FEE_AUTHORITY` ni `OWNERLESS_CLOSE_AUTHORITY`.
- `pre.013-delta-fix-032` enregistre le premier run Devnet escrow de `fix-031` : `CreateEscrowAccount` atteint bien le handler (`IX: Create Escrow Account`) mais la simulation est refusée avec `MissingRequiredSignature`, 17 268 CU consommées et aucune soumission. La cause est le placeholder `program_id` ajouté par le builder générique à l`authority=None` terminale ; les trois opérations escrow restent non promues pendant le correctif.
- `pre.013-delta-fix-031` enregistre le rerun `fix-030` du probe `Use` : le test termine `ok`, la simulation Devnet au slot `482133665` est refusée avec `InstructionError[0]=InvalidInstructionData`, quatre logs sont conservés et aucune soumission nest effectuée ; tous les autres checks et tests annoncés restent sans régression.
- prépare une campagne Devnet Token Owned Escrow strictement bornée : NFT parent frais, token attribut fungible frais, `CreateEscrowAccount`, création de lATA SPL classique détenu par le PDA escrow, dépôt contrôlé dune unité brute, `TransferOutOfEscrow` avec fermeture de lATA vidé, puis `CloseEscrowAccount` avec disparition du compte escrow.
- ajoute des postconditions stateful `TokenOwnedEscrow`, des preuves pipeline/idempotence pour les trois opérations Metaplex et maintient la matrice à 11 `confirmed`, 1 `unavailable` et 8 `not_run` tant que cette campagne na pas été exécutée sur Devnet.
- `pre.013-delta-fix-030` classe `Use` comme `unavailable` après le probe Devnet réel : l'instruction courante de discriminant 51 est simulée mais rejetée par Token Metadata avec `InstructionError[0]=InvalidInstructionData`, `Program log: Error: InvalidInstructionData`, `12085` compute units consommées et aucune soumission ; la matrice passe à 11 `confirmed`, 1 `unavailable` et 8 `not_run`.
- le probe `Use` peut désormais retourner cette simulation négative comme preuve et terminer `ok` en mode `submit=false`; le contrat de soumission reste simulation-first strict et ne signe jamais une simulation échouée.
- `pre.013-delta-fix-029` qualifie définitivement le cycle pNFT : `Delegate(StakingV1)`, `Lock`, `Unlock`, `Revoke(StakingV1)`, `Delegate(TransferV1)` et `Transfer` terminent tous simulation, confirmation, hydratation canonique, extraction Core, replay, matérialisation et idempotence ; les cinq opérations canoniques `Delegate`, `Revoke`, `Lock`, `Unlock`, `Transfer` passent à `confirmed` sur `programmable_nft`, portant la matrice à 11/20 opérations confirmées.
- prépare un probe Devnet borné pour `Use` sur un NFT classique frais avec `Uses::Multiple { remaining: 2, total: 2 }` : le probe simule dabord linstruction courante et distingue explicitement succès confirmé et indisponibilité runtime ; aucune promotion nest faite tant que la réponse Devnet nest pas observée.
- `pre.013-delta-fix-028` enregistre le rerun `fix-027` : la campagne pNFT franchit désormais `Delegate(Staking) -> Lock -> Unlock -> Revoke(Staking) -> Delegate(Transfer) -> Transfer` jusquà la postcondition finale du token account source ; le transfert est confirmé et son TokenRecord destination est `Unlocked` sans delegate, mais la campagne exigeait à tort que lATA source vide reste `Frozen` alors que le handler Metaplex courant thaw la source, transfère, puis freeze uniquement la destination ;
- corrige la postcondition finale en exigeant source `amount=0, state=Initialized` et destination `amount=1, state=Frozen`, et ajoute ces états à `METAPLEX_PNFT_LIFECYCLE_STATE` ; aucune opération lifecycle nest encore promue avant un rerun complet émettant les six signatures/slots et le bundle evidence final.
- `pre.013-delta-fix-027` enregistre le rerun `fix-026` : le workspace est entièrement propre (`kb-lib` 706/706, `kb-pipeline` 107/107, scénarios 96/96, desktop 135/135) et `Delegate(StakingV1)` franchit désormais confirmation, hydratation canonique, extraction Core, replay, matérialisation et idempotence avant quune postcondition de campagne ne lise à tort `delegate=None` dans la projection JSON brute du `TokenRecord` ; la campagne reste non qualifiée jusquau rerun avec projection canonique.
- `pre.013-delta-fix-026` enregistre le rerun `fix-025` : `Create -> Mint` franchit désormais entièrement sa préparation pNFT, puis `Delegate(StakingV1)` est réellement confirmé par `4S2uDrVU6ci59eKeGtGeXoUCbsUr4WxzSa7uYhmBxLtjKfNwpBkZRqm59V9cfbD99oE9AugMYppiQqnMbV2Hy1p7` au slot `482106293` avec hydratation canonique et extraction Core réussies ;
- le replay échoue ensuite dans `kb-lib` avant matérialisation parce que `Delegate.authority` et le fee payer sont le même wallet : Core replay expose donc le privilège writable global sur la position authority, alors que lAccountMeta officiel de cette position est seulement signer ;
- prépare le correctif de la même frontière pour `Delegate/Revoke` et pour `Lock/Unlock`, où `token_owner` est également le fee payer dans la campagne ; les cinq opérations lifecycle restent `not_run` jusquà un rerun complet.
- `pre.013-delta-fix-025` enregistre la première tentative réseau du cycle pNFT après correction du harness : `Create` est réellement confirmé par `PNkQAnykFoHVsREkhDbTW9eccyJoQvQbRZ2MJ4FsFRXyXint9fj6FT9PW45W5R3yi9HJ2aiSZXssH6B6pmyCcEz` au slot `482096529`, puis la campagne sarrête pendant sa post-validation stateful sur la Master Edition `HmCuW752ukBChVTbXtKJphUwJTJDdSycrgs6SrU2nT4B` du mint `DtBji9AYCqLRB48LFEFir427Kwy4Ehie6Pc8S87aQ6Rb` ;
- le défaut est dans `kb-lib` : le trailer officiel de 2 octets dune Master Edition compacte avait été introduit par `fix-021` comme padding nul, ce qui rejetait précisément le byte `ProgrammableNonFungible(4)` écrit par le programme courant ; aucune des cinq opérations lifecycle na encore été atteinte ni promue.
- `pre.013-delta-fix-024` corrige uniquement le harness `#[cfg(test)]` de la campagne pNFT préparée par `fix-023` : il réutilise le chargement de configuration, la surcharge PostgreSQL, la construction `PostgresStoreOptions`, le pool HTTP et le wallet directory déjà éprouvés par les autres campagnes Metaplex ;
- aligne l'émission `METAPLEX_PNFT_LIFECYCLE_EVIDENCE` sur les champs réels de `DevnetMetaplexTokenMetadataExecutionSummary` (`genesis_hash`, `simulation_context_slot`, `readiness.message_hash`, `fee.fee_lamports`) au lieu de champs inexistants du résultat brut de simulation ;
- enregistre que `cargo check --workspace` de `fix-023` était vert parce que le défaut restait sous `#[cfg(test)]`, tandis que `clippy --all-targets` et `cargo test -p kb-pipeline-demo-scenarios` compilaient ce harness et échouaient avant toute transaction pNFT ; la matrice reste à 6 opérations `confirmed` et 14 `not_run`.
- `pre.013-delta-fix-023` qualifie définitivement `Print` et `Burn` sur Devnet : `Print` est confirmé par `REHAxUZrSdKDj8F8k27Amrg7hwuJFhVfstjc9xjn2XGfTX38fH4tFwPeNrF5zeMN5pc21PpUPSgaKXMMfTFd7TE` au slot `482087799`, puis `Burn` par `4cNUxGGSWifuAtqdPJAtM3Cjpi4JATitRDqrvEAe85NLpMWcgWmC27bsfjNUmK6sEBDovNPtkPV3wDH1LpfGbhd1` au slot `482087818`; les deux étapes terminent hydratation canonique, extraction Core, replay, matérialisation et idempotence ;
- promeut `Print` et `Burn` à `confirmed` sur la famille NFT dans la matrice Devnet v3, qui passe à 6 opérations `confirmed` et 14 `not_run` ;
- prépare la campagne spécialisée pNFT `Delegate(Staking) -> Lock -> Unlock -> Revoke(Staking) -> Delegate(Transfer) -> Transfer` sur une fixture `programmable_nft` fraîche, avec signataire delegate supplémentaire, propriétaire destination frais, Token Records source/destination et ATA gelées validés à chaque transition ;
- conserve `Delegate`, `Revoke`, `Lock`, `Unlock` et `Transfer` à `not_run` jusquau rerun Devnet complet de cette nouvelle campagne.
- `pre.013-delta-fix-022` corrige la frontière `Future + Send` du runner Metaplex : les références `Signer + Sync` restent publiques, mais leur effacement vers `&dyn Signer` est désormais confiné à un helper synchrone de signature et ne traverse plus aucun `await` ;
- aligne la postcondition `Burn` sur le comportement courant de `MetadataV1` : l'absence reste valide, et un compte résiduel n'est accepté que comme tombstone fee exact `owner=Token Metadata`, non exécutable, lamports positifs, `space=1`, `data=[0x00]` ; les comptes token, Edition et Edition Marker doivent toujours être absents ;
- enregistre que le rerun `fix-021` atteint `Burn` et termine sa validation d'exécution avant cette seule garde sémantique, sans promouvoir `Print`/`Burn` tant que le bundle `METAPLEX_PRINT_BURN_*` complet n'est pas émis.
- `pre.013-delta-fix-021` enregistre la deuxième tentative `Print -> Burn` : `Print` est effectivement confirmé sur Devnet avec la signature `2199PTvKyTAfnGRHVaaCceW4u8phdJSu5ULkceD9uJeQyjjenEupK9osRZu1Pzx2NuQaJnAwngSAXMvr5oNtf5y6` au slot `482068400`, puis la campagne sarrête sur la lecture stateful du compte `Edition` avant hydratation/replay/matérialisation ; `Burn` nest pas atteint et la matrice conserve `Print`/`Burn` à `not_run` ;
- borne la voie multi-signataires publique à `Signer + Sync` tout en conservant la déclaration explicite, lunicité et la présence réelle de chaque signataire ; la validation suivante montrera que l'effacement local vers `Vec<&dyn Signer>` doit encore être confiné pour rendre effectivement le futur Tauri `Send` ;
- utilise `TemporaryWallet::as_sync_signer()` pour le keypair `edition_mint` et corrige le warning Clippy `cloned_ref_to_slice_refs` avec `std::slice::from_ref`.
- `pre.013-delta-fix-020` conserve la première tentative réseau `Print -> Burn` comme preuve de simulation négative : `Print` sarrête avant soumission avec `MetadataError::NotEnoughTokens` (`0x20`) parce que `fix-019` précréait lATA dédition vide alors que le chemin courant exige `amount=1` pour un compte token déjà existant ; aucune ligne `Print`/`Burn` nest promue ;
- corrige la fixture en conservant seulement le keypair `edition_mint` frais et en exigeant que le mint et son ATA soient tous deux absents avant `Print`, afin de laisser le programme courant créer le mint, créer lATA et frapper lunique token imprimé ;
- vérifie explicitement que lATA du master contient exactement un token avant `Print`, conserve les deux absences fraîches dans le résumé de campagne et supprime le re-export `NativeFixturePreparationEvidence` devenu inutilisé ;
- `pre.013-delta-fix-019` enregistre la campagne collection `Verify -> Unverify` entièrement confirmée : `Verify` signature `39JwUoxF2WWYWvEEWzFxSmKZVwoqXD1wH78sgGaoXv2vZKYUsXDNNnKikeuyQcfCqDftEMHSKFDHnTeSQ3bRpFcJ` slot `481936730`, puis `Unverify` signature `4Kfv42BGZtuBEwqTie5RWwdQ772hyxKiU9d3F3gfRnwVNrxCs2iZMvwL17Rwmuyr3XCVoHdaJVgaqCwv8y7bKoZY` slot `481936742`; les postconditions prouvent `verified false -> true -> false` et taille parent `0 -> 1 -> 0`, avec replay, matérialisation et idempotence complets ;
- promeut `Verify` et `Unverify` à `confirmed` sur la famille `collection` dans la matrice Devnet v3 ; létat réseau devient 4 opérations `confirmed` et 16 `not_run` ;
- ajoute un runner Devnet `Print -> Burn` sur un master NFT `PrintSupply::Limited(1)`, un mint dédition frais, lEdition PDA et lEdition Marker de lédition 1 ; `Print` doit prouver master supply `0 -> 1`, création Metadata/Edition/marker, mint imprimé supply `0 -> 1` et ATA amount `1`, puis `Burn` doit prouver supply imprimée `1 -> 0`, master supply `1 -> 0` et fermeture du token account, de lEdition et de lEdition Marker devenu vide, ainsi que fermeture sémantique de Metadata ;
- ajoute une voie explicite `execute_devnet_metaplex_token_metadata_with_signers` : les signataires supplémentaires doivent être déclarés dans `additional_authorized_signers`, effectivement fournis, uniques et bornés ; lAPI historique reste profil-wallet-only ;
- prépare les fixtures NFT imprimables avec `PrintSupply::Limited(1)` sans modifier les cinq profils `Create -> Mint` déjà qualifiés, et conserve `Print`/`Burn` à `not_run` jusquau test Devnet opt-in complet.
- `pre.013-delta-fix-018` conserve la première tentative collection comme preuve partielle : `Verify` est confirmé avec la signature `2kR1VeWdCW3FcKRZdNuKMMc3kGUmWRBXxXX8W1NNi2rhWfZYTg9zsDvJHx7WvebgUfc2K382J14i66E2qPEnjsqR` au slot `481931061`, mais le replay échoue avant matérialisation parce que le décodeur refusait le privilège writable global de l'autorité fee payer ; aucune promotion de matrice n'est effectuée avant rerun complet ;
- `pre.013-delta-fix-017` ferme la qualification multi-famille `Create -> Mint` : le rerun pNFT confirme `Create` au slot `481921980` puis `Mint` au slot `481921994`, avec Token Record présent, supply/amount `0 -> 1` et ATA `Initialized(1) -> Frozen(2)` ; la matrice v3 contient désormais cinq bundles `nft`, `sft`, `fungible`, `collection` et `programmable_nft` pour `Create` et `Mint` ;
- ajoute un runner Devnet réutilisable de collection parent-membre : Collection NFT parent `size=0`, membre NFT initialement `verified=false`, puis `Verify(CollectionV1)` et `Unverify(CollectionV1)` avec postconditions fermées `verified false -> true -> false` et taille `0 -> 1 -> 0` ;
- permet aux fixtures `Create` de déclarer un `collection_mint` initialement non vérifié sans modifier les cinq profils de base ;
- verrouille le format stateful du parent sur la sérialisation SDK réelle `collection_details.V1.size` et ajoute une régression empêchant un retour accidentel à une clé camelCase inexistante ;
- retire `SetCollectionSize` du parcours synthétique courant de collection : la taille est une postcondition stateful dérivée du programme, lopération obsolète nest pas utilisée pour fabriquer la preuve ;
- conserve `Verify` et `Unverify` à `not_run` tant que le nouveau test Devnet opt-in na pas produit signatures, slots et evidence machine-readable.
- `pre.013-delta-fix-016` corrige la postcondition SPL de la variante pNFT : après un `Mint` programmable confirmé, lATA classique est légitimement `Frozen` (`AccountState=2`) tandis que le Token Record Metaplex porte létat programmable ; NFT/SFT/fungible/collection continuent dexiger `Initialized` (`AccountState=1`) ;
- la première tentative pNFT confirme déjà `Mint` avec la signature `Gkg5A6ehuSzkU1N318kYoRHm5RdxV2yBRpaK9Gwyt6vN1ToYwAS6Vzpk2rQYWS9NJz1rnUpypLCn7DqWFzQzxFc` au slot `481913765`, amount `1`, owner et mint exacts, mais reste non promue car la campagne échoue ensuite sur lancienne attente `state=1` ;
- `DevnetMetaplexCreateMintTokenState` et `METAPLEX_CREATE_MINT_EVIDENCE` conservent désormais létat SPL brut avant/après `Mint`, afin que la preuve pNFT encode explicitement `Initialized(1) -> Frozen(2)` en plus de la transition supply/amount.
- `pre.013-delta-fix-015` enregistre la campagne collection `Create -> Mint` entièrement confirmée : `Create` signature `3jpxyWuxQVyBtbvqDnV1oahLyxCXMw1zQQZBGCKgQSfn2TNcBaUdfvJ6kFa8zL3XJLMpM6tahQTnu8AUsTkYc3Dy` slot `481911942`, puis `Mint` signature `4WEciqZA5vmtrh1ZeSbqcgVnezUy7qT6B6utySfkg7dbAmhH6SMwwY862zGbzPW3nBVFzMjFVGPEhnTt6TcAiEe2` slot `481911955`; les deux étapes terminent le pipeline de preuve complet avec deux snapshots stateful et supply/amount `0 -> 1`; la matrice v3 contient désormais les bundles `nft`, `sft`, `fungible` et `collection` pour `Create`/`Mint`, et le test de matrice nest plus figé sur les deux premières familles ;
- `pre.013-delta-fix-014` enregistre la campagne fungible `Create -> Mint` entièrement confirmée : `Create` signature `3J1Ebq9DVkupVFQmqQ84QaNeztwMuRwbtXj7CiyUWwZPj4KCAJpKLAbPvpYss5cdiY33foXXScWWZE98H7tvbZed` slot `481910724`, puis `Mint` signature `2GKiY9foLmioYtUHttA2D6nCSmsftRL1JDKxENHmCnb5w38FGSxwBiRNMfF7mH8htv3G45irmge6DVtr6VN359j2` slot `481910734`; chaque étape matérialise une ligne et `Mint` fait passer supply/amount de `0` à `1_000_000_000` sur un mint à 9 décimales ; la matrice v3 conserve désormais trois bundles `nft`, `sft` et `fungible` pour `Create` et `Mint`, tandis que collection et pNFT restent à exécuter ;
- `pre.013-delta-fix-013` enregistre la campagne SFT `Create -> Mint` entièrement confirmée : `Create` signature `2s81m6k9HYDLmCy7oXKaqBvtb62iyX4bJmij2f2VjHuoRQBBA6YqRuAsjyaM7JCaJXasbU3WCfiko91cZcDLLdxV` slot `481908028`, puis `Mint` signature `4pAemhEJewuLAeqLkqwzPvVmsCRbkyLiN6uAUWBA5RMsofaEDt5Xr8YGiBEvLWQM7Qvxnd81mmCHpandmfmWuyAm` slot `481908039` ; chaque étape matérialise une ligne et `Mint` fait passer supply/amount de `0` à `10` ;
- fait évoluer la matrice Devnet en version 3 afin de stocker une preuve complète distincte par famille au lieu dagréger plusieurs signatures, slots et message hashes dans une seule liste dévidence ; `Create` et `Mint` restent les deux seules opérations `confirmed`, désormais observées sur `nft` et `sft`, tandis que les 18 autres opérations restent `not_run` ;
- `pre.013-delta-fix-012` enregistre la première campagne NFT `Create -> Mint` entièrement confirmée : `Create` signature `okwkuQFp7yVguYW7GEY9Chv6ochM8TWBEeCGV1gu5Waku9wQvMfrKjkig6JxXBwpKoFTECignzJ8HFf8dLtpsvS` slot `481904731`, puis `Mint` signature `3hSizfdvezyFpfhqX4Za4wmhdMHhSPAisFvYjFAxpRzYcUXhryxCzc9qpwod3Yjm6GmijXTGjjDUuVksfpBzLwz1` slot `481904744` ; chaque étape produit une matérialisation et `Mint` fait passer supply/amount de `0` à `1` ;
- promeut `Create` et `Mint` à `confirmed` sur la famille NFT dans la matrice Devnet v2, conserve les 18 autres opérations à `not_run` et exige désormais des preuves observées complètes pour toute ligne `confirmed` ;
- conserve le slot RPC de simulation dans `DevnetMetaplexTokenMetadataExecutionSummary` et imprime une ligne `METAPLEX_CREATE_MINT_EVIDENCE` machine-readable avec genesis hash, slot de simulation, message hash, fee, confirmation et état du pipeline pour les futures campagnes SFT/fungible/collection/pNFT ;
- `pre.013-delta-fix-011` corrige la postcondition SPL de la campagne `Create -> Mint` : lorsquune famille demande une Master Edition, `Create` transfère légitimement les mint/freeze authorities vers le PDA Master Edition ; SFT et fungible conservent lautorité opérateur ;
- supprime le helper `read_token_account` devenu inutilisé et élimine le warning `dead_code` observé après `fix-010` ;
- conserve signature + slot dans toute erreur SPL postérieure à un `Create` ou `Mint` déjà confirmé afin quune preuve pipeline complète ne soit plus perdue par une postcondition ultérieure ;
- corrige linterprétation de la tentative précédente : le mismatch dautorité nétait pas une preuve de fixture stale. `fix-010` reste un durcissement fresh-only valide, mais le chemin de campagne montre que lerreur était atteinte après `Create`. La tentative suivant `fix-010` franchit même `validate_confirmed_execution("create", ...)`, donc replay, matérialisation et idempotence ont réussi avant la postcondition SPL ; signature et slot nayant pas été imprimés, la matrice reste néanmoins `not_run`.
- `pre.013-delta-fix-010` rend la fixture `Create -> Mint` strictement fraîche : le mint keypair est créé avec `TemporaryWalletStore::create` au lieu de `load_or_create`, son adresse doit être absente de Devnet avant préparation, puis le mint et lATA sont relus à `minContextSlot` égal au slot confirmé de la transaction native ;
- `fix-010` a initialement été motivé par un mismatch dautorité `3RJXuBF6xB3j47EeAHEBbcwvV66rJrkhmJvYNr36jz8x` contre lopérateur `J12WA6c42oqpWkLu1dFa4pCegkQJPpxabwc3RRCMSUuH` ; le réaudit de `fix-011` corrige cette interprétation : la postcondition était exécutée après `Create` et observait le transfert normal des autorités vers le PDA Master Edition ;
- `pre.013-delta-fix-009` enregistre la troisième tentative NFT : `Create` est confirmée sur Devnet avec la signature `51MXdFFFNY7UTS23cmfCQBfrDKC99uk5Trtw2gkptvEug9aWJ2R1cE9Ta5QHbe4kS1NYgRUSSMmmdbqdhyxxnR4Y` au slot `481882374`, puis la post-validation échoue uniquement au replay instructionnel ;
- relie cet échec au contrat trop strict du décodeur `Create`/`Mint`, qui confondait flags positionnels et privilèges globaux de transaction, et ajoute un diagnostic compact des compteurs `DecodeReplaySummary` lorsquun replay reste incomplet ;
- conserve `Create` et `Mint` à `not_run` tant que replay, matérialisation et idempotence ne sont pas tous démontrés.
- `pre.013-delta-fix-008` corrige la seconde tentative NFT `Create -> Mint` : les snapshots Metaplex utilisaient encore la borne pipeline historique de `1 MiB`, alors que le transport refuse une lecture complète `getAccountInfo` au-delà de `65536` octets ;
- utilise désormais la borne stateful commune du transport dans tous les snapshots `Create -> Mint` et verrouille cette valeur par test ;
- enrichit une éventuelle erreur post-exécution restante avec la signature et le slot déjà confirmés afin de ne plus perdre l'identité d'une transaction ayant atteint la post-validation ;
- conserve les 20 opérations à `not_run` jusqu'à une campagne terminée avec preuves exploitables.
- `pre.013-delta-fix-007` réorganise la crate par domaines stables (`metadata/solana_program`, `metadata/metaplex_token_metadata`, `spl/associated_token_account`, `spl/memo`, `spl/token`, `spl/token_2022` et `spl/token_2022/metadata`) sans modifier les exports publics de crate-root ;
- supprime les noms de modules racine historiques et incohérents `metadata_solana_program_*`, `solana_metaplex_token_metadata_*`, `solana_token_2022_*` et `token_2022_*` au profit d'une arborescence correspondant aux domaines du workspace ;
- corrige le warning Clippy `cloned_ref_to_slice_refs` du test de borne `minContextSlot` avec `std::slice::from_ref` ;
- durcit la sélection des preuves de matérialisation Metaplex : les lignes sont désormais retenues par l'identité du matérialiseur `MtMetadataMetaplexTokenMetadataMaterializer` et non par une chaîne de nom de décodeur ;
- enrichit l'erreur `Create -> Mint` avec les booléens post-exécution, les nombres de lignes/snapshots et les diagnostics accumulés afin qu'une nouvelle tentative Devnet distingue immédiatement hydratation, extraction, replay ou matérialisation manquante ;
- conserve `Create` et `Mint` à `not_run` : la première tentative NFT de `fix-006` a atteint la post-validation mais a été interrompue par `metaplex_create_mint_post_execution_incomplete` avant émission d'une preuve exploitable ;
- `pre.013-delta-fix-006` ajoute une campagne Devnet réutilisable et bornée `Create -> Mint` exécutée une famille à la fois (`nft`, `sft`, `fungible`, `collection` ou `programmable_nft`) ;
- lie désormais toutes les lectures Metaplex postcondition au slot de confirmation de la transaction via `minContextSlot`, afin que `after_state` ne puisse pas provenir dun contexte antérieur ;
- exige pour `Create` et `Mint` simulation réussie, confirmation, hydratation canonique, extraction Core, replay, matérialisation, seconde passe idempotente et snapshots Metaplex attendus ;
- relit en plus le mint SPL classique et lATA au slot confirmé : supply et amount doivent rester nuls après `Create`, puis devenir exactement `mint_amount_raw` après `Mint` ;
- exige la présence du master edition pour NFT/collection/pNFT et du token record après `Mint` pour pNFT, sans précréer ces preuves dans la fixture ;
- conserve les 20 opérations de la matrice réseau à `not_run` jusquà lexécution réelle de la nouvelle campagne ;
- `pre.013-delta-fix-005` prépare désormais, dans la même transaction native, le mint SPL classique et lATA canonique de lopérateur, tout en conservant une supply nulle avant lopération Metaplex `Mint` ;
- ajoute au résumé de fixture lATA, le token record pNFT éventuel, le montant brut par famille et un JSON `Mint` typé prêt à exécuter après `Create` ;
- vérifie après confirmation le layout exact du mint et de lATA classique, notamment mint, owner, amount=0, état initialisé et taille 165 octets ;
- dérive le token-record PDA uniquement pour pNFT et verrouille hors réseau les montants `Mint` : NFT/collection/pNFT=1, SFT=10 et fungible=1_000_000_000 à 9 décimales ;
- conserve les 20 opérations Metaplex à `not_run` : ce correctif prépare les préconditions token accounts/supply mais ne transforme aucune fixture en preuve Devnet ;
- `pre.013-delta-fix-002` rend les fixtures `Create` réellement cohérentes avec les cinq familles Devnet : NFT, SFT et collection restent à 0 décimale, tandis que le mint fongible est préparé à 9 décimales ;
- déplace la sélection du `TokenStandard`, de `collection_details`, de `master_edition` et de `print_supply` depuis ladaptateur desktop vers la fixture réutilisable afin que CLI, tests et futures campagnes consomment le même contrat ;
- vérifie hors réseau que les cinq JSON `Create` se désérialisent vers `ExMetaplexTokenMetadataOperation::Create` et que seules les familles NFT, collection et pNFT demandent une master edition ;
- `pre.013-delta-fix-001` corrige le test Devnet opt-in pour la signature actuelle de `PostgresStoreOptions::new(database_url, max_connections, connect_timeout_ms, auto_initialize_schema)` et conserve le timeout de cinq secondes sous forme `5_000` ms ;
- réaudite Metaplex Token Metadata contre lIDL historique de 58 instructions, la matrice dexécution et la surface SDK actuelle : 20 opérations courantes, 15 obsolètes exécutables sous approbation explicite, 22 remplacées en décodage uniquement et une frontière Bubblegum ;
- rouvre `METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json` sous `0.4.8-pre.013` sans inventer de preuve réseau : les 20 opérations courantes restent initialement `not_run` ;
- corrige le runner Devnet Metaplex afin dexécuter réellement la post-validation déjà déclarée par sa policy : hydratation canonique, extraction Core, replay, matérialisation et seconde passe didempotence ;
- expose dans le résumé les rapports de backfill, extraction, replay, idempotence, les matérialisations instructionnelles et le diagnostic agrégé ;
- borne les retries de post-validation à 20 et exige PostgreSQL dans le test Devnet opt-in dès quune exécution réelle est demandée ;
- conserve comme travail restant de la même prerelease les fixtures spécialisées et les preuves réseau des opérations autres que `Create` et `UpdateAsUpdateAuthorityV2`.
## `0.4.8-pre.012`
- corrige le delta initial en déclarant explicitement `bs58.workspace = true`, utilisé directement par les fixtures et gardes Token-2022 de la crate ;
- confirme sur Devnet les cinq opérations Token Metadata de la campagne avec cinq signatures, cinq slots et des postconditions `Confirmed` ;
- conserve pour `Emit` un `returnData` complet de 202 octets validé contre le snapshot TLV autoritatif ;
- promeut les cinq scénarios de `SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json` à `confirmed` avec leurs preuves réelles ;
- clôt les TODO opérateur de `pre.012` après `cargo fmt`, `cargo check --workspace`, `cargo clippy --all-targets`, audit workspace, tests de crate et `cargo test --workspace` réussis ;
- ajoute une fixture native de mint Token-2022 avec `MetadataPointer` auto-référent initialisé avant `InitializeMint2` ;
- préfinance le mint pour les réallocations du TLV Token Metadata atteintes par la campagne ;
- ajoute une campagne Devnet ordonnée `Initialize → UpdateField → Emit → RemoveKey → UpdateAuthority` avec simulation, soumission autorisée, confirmation, hydratation, replay, matérialisation et postconditions stateful ;

View File

@@ -1,5 +1,5 @@
# file: kb-pipeline-demo-scenarios/Cargo.toml
# version: 9
# version: 11
[package]
name = "kb-pipeline-demo-scenarios"
@@ -31,7 +31,10 @@ kb-store = { path = "../kb-store" }
kb-wallet = { path = "../kb-wallet" }
mpl-token-metadata.workspace = true
solana-instruction.workspace = true
solana-keypair.workspace = true
solana-pubkey.workspace = true
solana-signer.workspace = true
spl-associated-token-account-interface.workspace = true
spl-token-interface.workspace = true
spl-token-2022-interface.workspace = true
serde.workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: kb-pipeline-demo-scenarios/README.md -->
<!-- version: 9 -->
<!-- version: 28 -->
# kb-pipeline-demo-scenarios
@@ -19,9 +19,37 @@ La crate contient :
- fournir des tests automatisés parallèles aux démonstrations UI, sans déplacer les scénarios UI hors du desktop ;
- conserver la séparation entre preuve synthétique, simulation RPC, soumission confirmée et postcondition stateful.
La crate couvre les scénarios Solana Core et SPL historiques, le corpus synthétique et le runner Devnet Metaplex pour NFT, SFT, token fongible, collection et pNFT, ainsi que deux parcours Solana Program Metadata couvrant les neuf opérations stables. Les validations spécialisées restent qualifiées séparément au lieu dêtre déduites de la seule psence dun builder.
La crate couvre les scénarios Solana Core et SPL historiques, les inventaires synthétiques et Devnet-cibles Metaplex pour NFT, SFT, token fongible, collection et pNFT, ainsi que le runner générique des opérations Metaplex courantes et deux parcours Solana Program Metadata couvrant les neuf opérations stables. Une famille déclarée dans linventaire ne vaut pas fixture spécialisée ni preuve réseau ; les validations restent qualifiées sépament.
Pour Token-2022 Token Metadata, `0.4.8-pre.012` ajoute une campagne Devnet dédiée aux cinq instructions de `spl-token-metadata-interface`. Elle prépare nativement un mint frais avec `MetadataPointer`, puis enchaîne `Initialize`, `UpdateField`, `Emit`, `RemoveKey` et `UpdateAuthority` avec simulation-first, soumission explicitement autorisée, confirmation, replay, matérialisation lorsque applicable et postcondition stateful. Sa matrice réseau reste `not_run` avant exécution réelle.
Le réaudit Metaplex de `0.4.8-pre.013` ferme les 20 opérations courantes avec **15 `confirmed`, 5 `unavailable` et 0 `not_run`**. `Create` et `Mint` sont confirmés sur les cinq chemins NFT, SFT, fungible, collection et pNFT ; `Verify`/`Unverify`, `Print`/`Burn`, le lifecycle pNFT `Delegate/Revoke/Lock/Unlock/Transfer`, Token Owned Escrow `CreateEscrowAccount/TransferOutOfEscrow/CloseEscrowAccount` et `Update` disposent également de preuves Devnet complètes. `Use`, `Resize`, `Migrate`, `Collect` et `CloseAccounts` restent explicitement `unavailable` pour des causes distinctes documentées par leurs probes runtime. Toute soumission Metaplex confirmée passe par lhydratation canonique de sa signature, lextraction Core, le replay avec les matérialiseurs du domaine et une seconde passe didempotence. La fixture `Create` prépare en outre le mint SPL classique et lATA canonique de lopérateur dans une même transaction, vérifie les deux comptes après confirmation et laisse volontairement la supply à zéro. Elle fournit ensuite un intent `Mint` typé par famille ; pour pNFT, elle dérive le token-record PDA attendu sans le créer prématurément. La campagne collection parent-membre qualifiée réutilise ces fixtures et valide la relation `verified false -> true -> false` ainsi que la taille de collection `0 -> 1 -> 0` sans utiliser `SetCollectionSize`.
Depuis `pre.013-delta-fix-010`, cette fixture est **fresh-only** : elle crée un nouveau keypair persistant sans rechargement implicite, refuse toute adresse déjà présente on-chain, puis borne les relectures du mint et de son ATA au slot confirmé de la préparation native. Une campagne ne peut donc plus reprendre silencieusement un mint issu dune exécution antérieure.
Depuis `pre.013-delta-fix-011`, la campagne distingue aussi le contrat dautorité SPL avant et après `Create` : NFT, collection et pNFT attendent le PDA Master Edition comme mint/freeze authority après création, alors que SFT et fungible conservent lautorité opérateur.
Depuis `pre.013-delta-fix-016`, la postcondition du compte token distingue aussi pNFT : lATA est `Initialized` avant `Mint`, puis doit être `Frozen` après `Mint`; les quatre autres familles restent `Initialized`. Le Token Record pNFT est validé séparément comme état programmable Metaplex.
Depuis `pre.013-delta-fix-020`, la campagne `Print -> Burn` conserve uniquement un keypair frais pour `edition_mint` et exige que le mint **et** son ATA canonique soient absents avant `Print`. Le programme Metaplex courant crée alors le mint, crée lATA et frappe lunique token imprimé. Précréer un ATA vide est interdit : le chemin `Print` dun compte token déjà existant exige exactement un token. Le runner vérifie aussi que lATA du master contient exactement un token avant la simulation afin de distinguer une précondition master invalide dun défaut de lédition fraîche.
Depuis `pre.013-delta-fix-030`, le probe `Use` est classé : Devnet rejette linstruction courante de discriminant 51 avec `InvalidInstructionData`. Le scénario conserve ce refus comme preuve `unavailable`, ne soumet aucune transaction et peut retourner proprement une simulation négative en mode probe sans affaiblir la barrière de soumission.
Depuis `pre.013-delta-fix-021`, les références de signataires supplémentaires conservent explicitement `Sync` et la lecture stateful accepte l'`Edition` compacte réellement allouée. La validation suivante montre toutefois que la conversion locale en `Vec<&dyn Signer>` rend encore le futur desktop non-`Send`; `pre.013-delta-fix-022` confine donc cette conversion à la fonction synchrone qui signe immédiatement la transaction. Le rerun Devnet de `fix-022` termine désormais `Print -> Burn` avec les deux signatures, les slots, les postconditions stateful, lhydratation canonique, lextraction Core, le replay, les matérialisations et lidempotence. `Print` et `Burn` sont donc qualifiés sur la famille NFT. `fix-023` prépare ensuite une campagne pNFT spécialisée `Delegate(Staking) -> Lock -> Unlock -> Revoke(Staking) -> Delegate(Transfer) -> Transfer`, avec deux wallets temporaires frais pour le delegate et le propriétaire destination et des contrôles exacts du Token Record et des ATA gelées.
Pour Token-2022 Token Metadata, `0.4.8-pre.012` ajoute une campagne Devnet dédiée aux cinq instructions de `spl-token-metadata-interface`. Elle prépare nativement un mint frais avec `MetadataPointer`, puis enchaîne `Initialize`, `UpdateField`, `Emit`, `RemoveKey` et `UpdateAuthority` avec simulation-first, soumission explicitement autorisée, confirmation, replay, matérialisation lorsque applicable et postcondition stateful. La campagne de validation de `pre.012` a confirmé les cinq scénarios sur Devnet ; la matrice et le rapport conservent les signatures, slots et preuves observées.
## Organisation interne
Les modules internes suivent désormais les domaines fonctionnels plutôt que l'ordre historique des ajouts :
```text
src/
├── solana.rs
├── metadata/
│ ├── solana_program/
│ └── metaplex_token_metadata/
└── spl/
├── associated_token_account.rs
├── memo.rs
├── token/
└── token_2022/
└── metadata/
```
Les exports publics restent fournis depuis `lib.rs`. Le rangement interne ne doit donc pas imposer aux consommateurs des chemins de modules privés ni modifier les noms des contrats publics existants.
## Binaire CLI
@@ -37,3 +65,8 @@ La crate dépend du pipeline généraliste. `kb-pipeline` ne dépend jamais d
- [Travaux restant à réaliser](TODO.md)
- [Historique des changements](CHANGELOG.md)
- [Architecture du pipeline](../docs/architecture/PIPELINE_ARCHITECTURE.md)
## Qualification Metaplex `0.4.8-pre.013`
La matrice Devnet courante est désormais fermée avec **15 opérations `confirmed`**, **5 opérations `unavailable` (`Use`, `Resize`, `Migrate`, `Collect`, `CloseAccounts`)** et **0 opération `not_run`**. Les parcours qualifiés incluent `Create/Mint`, collection `Verify/Unverify`, `Print/Burn`, le lifecycle pNFT, les trois opérations Token Owned Escrow et `Update`.
La campagne finale `maintenance_campaign` confirme `Update` sur un NFT frais avec `primary_sale_happened false -> true`. Elle conserve `Resize`, `Migrate`, `Collect` et `CloseAccounts` comme probes simulation-only : `Resize` expose `AccountAlreadyResized(201)` sur les comptes au layout courant, `Migrate` est retiré avec `Removed(75)`, et `Collect`/`CloseAccounts` exigent des autorités Metaplex fixes que le scénario contrôlé nessaie jamais dimiter.

View File

@@ -1,21 +1,8 @@
<!-- file: kb-pipeline-demo-scenarios/TODO.md -->
<!-- version: 12 -->
<!-- version: 41 -->
# TODO — kb-pipeline-demo-scenarios
## Validation Devnet Token-2022 Token Metadata
- [ ] Exécuter la campagne `0.4.8-pre.012` sur un mint frais avec confirmation opérateur explicite.
- [ ] Conserver les cinq signatures, slots, simulations, preuves de replay et postconditions dans la matrice et le rapport de validation.
- [ ] Conserver le `returnData` complet de `Emit` et vérifier son décodage contre le snapshot TLV autoritatif.
## `0.4.8-pre.013` — réaudit Metaplex Token Metadata
- [ ] Réauditer la couverture courante/historique, les builders, les comptes, les matérialisations et les matrices Metaplex.
- [ ] Compléter uniquement les écarts réels constatés.
- [ ] Reprendre les campagnes Devnet et compléter les fixtures spécialisées encore nécessaires pour éditions/burn, collections et pNFT.
- [ ] Qualifier chaque opération réseau avec des preuves réellement observées ou une indisponibilité justifiée.
## Évolutions générales
- [ ] Ajouter au CLI uniquement les préparations de fixtures explicitement justifiées.

View File

@@ -1,5 +1,5 @@
<!-- file: kb-pipeline-demo-scenarios/USAGE.md -->
<!-- version: 12 -->
<!-- version: 24 -->
# Utilisation de kb-pipeline-demo-scenarios
@@ -115,7 +115,7 @@ cargo test -p kb-pipeline-demo-scenarios optional_devnet_token_2022_metadata_cam
`KB_DEVNET_PROFILE` peut sélectionner explicitement le profil Devnet et `KB_DEVNET_CONFIG_PATH` peut remplacer `config/example.config.json`. Le profil doit utiliser un wallet persistant, autoriser les soumissions Devnet et respecter les plafonds de dépense et de frais.
La sortie `TOKEN_2022_METADATA_FIXTURE` conserve le mint et la signature de préparation. Chaque ligne `TOKEN_2022_METADATA_STEP` conserve lopération, la signature, le slot, la postcondition, le nombre de matérialisations et, pour `Emit`, la taille du `returnData`. Ces preuves doivent être reportées dans `SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json` et dans le rapport `V0_4_8_PRE_012_TOKEN_2022_METADATA_DEVNET_VALIDATION_REPORT.md` ; elles ne sont jamais préremplies avant lexécution réelle.
La sortie `TOKEN_2022_METADATA_FIXTURE` conserve le mint et la signature de préparation. Chaque ligne `TOKEN_2022_METADATA_STEP` conserve lopération, la signature, le slot, la postcondition, le nombre de matérialisations et, pour `Emit`, la taille du `returnData`. La campagne de référence de `pre.012` a été exécutée avec succès et ses preuves sont conservées dans `SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json` et dans le rapport `V0_4_8_PRE_012_TOKEN_2022_METADATA_DEVNET_VALIDATION_REPORT.md`. Une nouvelle exécution produit de nouvelles transactions et ne remplace ces preuves quaprès validation explicite.
## Charger la matrice de validation Token-2022
@@ -308,12 +308,14 @@ cargo test -p kb-pipeline-demo-scenarios optional_devnet_current_operation_from_
Variables optionnelles :
- `KB_DEVNET_WALLET_DIR` pour sélectionner le répertoire du wallet persistant ;
- `KB_POSTGRES_TEST_URL` est obligatoire pour le test opt-in afin de permettre lhydratation canonique, lextraction Core, le replay et la vérification didempotence après une soumission ;
- `KB_DEVNET_METAPLEX_PREFLIGHT_READS_JSON` pour fournir les lectures stateful avant simulation ;
- `KB_DEVNET_METAPLEX_POSTCONDITION_READS_JSON` pour fournir les lectures après confirmation ;
- `KB_DEVNET_METAPLEX_MATERIALIZE_AFTER_CONFIRMATION=1` pour rendre obligatoire la preuve de matérialisation et produire les snapshots stateful demandés ;
- `KB_DEVNET_METAPLEX_SUBMIT=1` pour autoriser la soumission ;
- `KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1` pour confirmer explicitement la soumission.
La soumission reste impossible si les deux dernières variables ne sont pas activées. Les opérations dépréciées sont rejetées avant tout appel RPC.
La soumission reste impossible si `KB_DEVNET_METAPLEX_SUBMIT` et `KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED` ne sont pas activées ensemble. Après confirmation, le runner hydrate la transaction canonique, exécute lextraction Core, rejoue le décodeur Metaplex avec ses matérialiseurs puis effectue une seconde passe qui doit être idempotente. Les opérations dépréciées sont rejetées avant tout appel RPC.
## Préparer une campagne Metaplex courante
@@ -354,6 +356,124 @@ fn creator_verify_request(
`devnet_metaplex_creator_unverify_request` fournit le parcours inverse. Les deux requêtes restent en simulation-only par défaut.
Le test générique accepte toujours `KB_DEVNET_METAPLEX_OPERATION_JSON`, mais rejette explicitement le placeholder documentaire `...`. Les opérations plus complexes recevront des préparateurs de fixtures dédiés au lieu dexiger de longs JSON écrits à la main.
## Préparer `Create` puis `Mint` avec un ATA canonique
`prepare_metaplex_create_fixture` prépare maintenant une base réseau commune aux cinq familles : un mint SPL classique frais et lATA canonique de lopérateur sont créés dans une même transaction native, puis relus et validés. La supply du mint et le solde de lATA doivent rester à zéro à cette étape afin que lopération Metaplex `Mint` constitue la preuve de création de supply.
Le résumé expose notamment :
- `token_account`, lATA classique canonique ;
- `token_record`, renseigné uniquement pour le pNFT ;
- `mint_amount_raw`, égal à `1` pour NFT/collection/pNFT, `10` pour SFT et `1_000_000_000` pour le fungible à 9 décimales ;
- `operation_json`, lintent `Create` typé ;
- `mint_operation_json`, lintent `Mint` typé à exécuter seulement après confirmation de `Create`.
Le token record pNFT est uniquement dérivé pendant la préparation. Sa création reste la responsabilité de `Mint`, de sorte que la fixture ne fabrique pas à lavance létat que la campagne doit démontrer.
## Exécuter la campagne Devnet `Create -> Mint`
La campagne est volontairement exécutée **une famille à la fois**. Elle prépare une fixture fraîche, soumet `Create`, vérifie que la supply et lATA restent à zéro, puis soumet `Mint` et exige la transition exacte vers `mint_amount_raw`. Pour NFT, collection et pNFT, la validation SPL exige après `Create` que les mint/freeze authorities aient été transférées au PDA Master Edition ; pour SFT et fungible, elles restent sur lopérateur. Après `Mint`, NFT/SFT/fungible/collection exigent un ATA `Initialized` (`state=1`), alors que pNFT exige un ATA `Frozen` (`state=2`) et un Token Record Metaplex présent. Toutes les lectures Metaplex postcondition sont automatiquement bornées au slot de confirmation.
Le test opt-in utilise `nft` par défaut. Les valeurs acceptées par `KB_DEVNET_METAPLEX_CREATE_MINT_FAMILY` sont `nft`, `sft`, `fungible`, `collection`, `programmable_nft` et lalias `pnft`.
```bash
KB_DEVNET_METAPLEX_CREATE_MINT_CAMPAIGN_TEST=1 \
KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 \
KB_DEVNET_METAPLEX_CREATE_MINT_FAMILY=nft \
KB_POSTGRES_TEST_URL='postgresql://…' \
cargo test -p kb-pipeline-demo-scenarios \
optional_devnet_metaplex_create_mint_campaign_from_env \
-- --nocapture
```
La sortie `METAPLEX_CREATE_MINT_FIXTURE` conserve les comptes de la fixture. Deux lignes `METAPLEX_CREATE_MINT_STEP` exposent ensuite les signatures/slots de `Create` et `Mint`, les matérialisations, la transition supply/amount et les états bruts de lATA avant/après `Mint`. Ces valeurs doivent être conservées avant de promouvoir une ligne de la matrice réseau.
## Exécuter la campagne collection `Verify -> Unverify`
`execute_devnet_metaplex_collection_verify_campaign` réutilise les fixtures `Create -> Mint` qualifiées pour construire un Collection NFT parent et un NFT membre lié au parent avec `verified=false`. Le runner exécute ensuite les wrappers courants `Verify(CollectionV1)` puis `Unverify(CollectionV1)`.
La preuve stateful exige simultanément :
- `collection.verified` du membre : `false -> true -> false` ;
- `CollectionDetails::V1.size` du parent : `0 -> 1 -> 0` ;
- Metadata PDA membre et parent présentes ;
- Master Edition parent présente ;
- hydratation canonique, Core extraction, replay, matérialisation et idempotence propres pour `Verify` et `Unverify`.
Le parcours ne soumet pas `SetCollectionSize` : cette opération est obsolète et ne doit pas fabriquer artificiellement la postcondition de taille.
```bash
KB_DEVNET_METAPLEX_COLLECTION_VERIFY_CAMPAIGN_TEST=1 \
KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 \
KB_POSTGRES_TEST_URL='postgres://solana:solana@localhost:5432/solana_test' \
cargo test -p kb-pipeline-demo-scenarios \
optional_devnet_metaplex_collection_verify_campaign_from_env \
-- --nocapture
```
Une exécution réussie imprime `METAPLEX_COLLECTION_VERIFY_FIXTURE`, deux lignes `METAPLEX_COLLECTION_VERIFY_STEP`, létat fermé `METAPLEX_COLLECTION_VERIFY_STATE` et une ligne machine-readable `METAPLEX_COLLECTION_VERIFY_EVIDENCE`. La campagne qualifiante a été conservée sous `pre.013` ; `Verify` et `Unverify` sont désormais `confirmed` sur la famille `collection` dans la matrice Devnet.
## Exécuter la campagne pNFT `Delegate -> Lock -> Unlock -> Revoke -> Delegate -> Transfer`
`execute_devnet_metaplex_pnft_lifecycle_campaign` crée dabord une fixture `programmable_nft` fraîche via la campagne `Create -> Mint` qualifiée. Il crée ensuite un wallet temporaire de delegate et un propriétaire destination frais, puis enchaîne deux rôles de Token Delegate distincts :
1. `Delegate(StakingV1)` par le propriétaire ;
2. `Lock` puis `Unlock` par le staking delegate ;
3. `Revoke(StakingV1)` par le propriétaire ;
4. `Delegate(TransferV1)` par le propriétaire ;
5. `Transfer` par le transfer delegate vers une ATA destination fraîche.
Le runner exige que le Token Record source passe `Unlocked/Staking -> Locked/Staking -> Unlocked/Staking -> delegate cleared -> Unlocked/Transfer`. Après le transfert, lATA source doit contenir `0` et rester `Initialized`, tandis que lATA destination doit contenir `1` et rester `Frozen`; le Token Record destination doit être `Unlocked` sans delegate. Ce contrat suit le handler courant, qui thaw la source avant transfert puis freeze uniquement la destination. Les wallets temporaires ne remplacent jamais le wallet persistant du profil comme fee payer ; seul le delegate explicitement déclaré est ajouté aux signataires des étapes qui lexigent.
```bash
KB_DEVNET_METAPLEX_PNFT_LIFECYCLE_CAMPAIGN_TEST=1 \
KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 \
KB_POSTGRES_TEST_URL='postgres://solana:solana@localhost:5432/solana_test' \
cargo test -p kb-pipeline-demo-scenarios \
optional_devnet_metaplex_pnft_lifecycle_campaign_from_env \
-- --nocapture
```
Une exécution qualifiante doit terminer `ok` et produire `METAPLEX_PNFT_LIFECYCLE_FIXTURE`, six lignes `METAPLEX_PNFT_LIFECYCLE_STEP`, `METAPLEX_PNFT_LIFECYCLE_STATE` et `METAPLEX_PNFT_LIFECYCLE_EVIDENCE`. Le bundle qualifiant a été observé sous `pre.013` ; `Delegate`, `Revoke`, `Lock`, `Unlock` et `Transfer` sont désormais `confirmed` sur `programmable_nft`.
## Campagne Devnet Metaplex Token Owned Escrow
La campagne escrow prépare un NFT classique parent et un token fungible attribut, puis exécute un cycle `TokenOwner` strictement contrôlé. Elle crée le PDA escrow, crée son ATA SPL classique pour le mint attribut, y dépose exactement une unité brute, transfère cette unité vers lATA de lopérateur puis ferme le compte escrow.
```bash
KB_DEVNET_METAPLEX_ESCROW_CAMPAIGN_TEST=1 \
KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 \
KB_POSTGRES_TEST_URL='postgres://solana:solana@localhost:5432/solana_test' \
cargo test -p kb-pipeline-demo-scenarios \
optional_devnet_metaplex_escrow_campaign_from_env \
-- --nocapture
```
Une exécution qualifiante doit produire `METAPLEX_ESCROW_FIXTURE`, deux lignes `METAPLEX_ESCROW_SETUP`, trois lignes `METAPLEX_ESCROW_STEP`, `METAPLEX_ESCROW_STATE` et `METAPLEX_ESCROW_EVIDENCE`. Le bundle complet a été observé sous `pre.013` ; `CreateEscrowAccount`, `TransferOutOfEscrow` et `CloseEscrowAccount` sont désormais `confirmed` sur la famille `nft`.
## Campagne Devnet Metaplex maintenance
La campagne `maintenance_campaign` ferme les opérations courantes qui ne relèvent pas des parcours précédents. Elle crée un NFT classique frais, soumet réellement `Update` avec la transition bornée `primary_sale_happened: false -> true`, puis traite les surfaces non qualifiables positivement avec des probes **simulation-only**. Une simulation négative n'autorise jamais une soumission.
```bash
KB_DEVNET_METAPLEX_MAINTENANCE_CAMPAIGN_TEST=1 \
KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 \
KB_POSTGRES_TEST_URL='postgres://solana:solana@localhost:5432/solana_test' \
cargo test -p kb-pipeline-demo-scenarios \
optional_devnet_metaplex_maintenance_campaign_from_env \
-- --nocapture
```
Le bundle qualifiant de `pre.013` confirme `Update` et conserve les refus runtime exacts suivants :
- `Resize` : `Custom(201)`, compte déjà au layout cible ;
- `Migrate` : `Custom(75)`, instruction retirée ;
- `Collect` : `Custom(7)`, autorité de collecte Metaplex réservée ;
- `CloseAccounts` : `Custom(188)`, autorité ownerless-close Metaplex réservée.
Le bilan courant de la matrice Devnet Metaplex est **15 `confirmed`, 5 `unavailable`, 0 `not_run`**. Les campagnes restent réexécutables pour diagnostiquer une régression ou un changement de runtime, mais ne constituent plus des tâches ouvertes de `pre.013`.
## Solana Program Metadata
Linventaire fermé est exposé par :
@@ -403,4 +523,23 @@ where
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 nont pas été enregistrées. `Close` exige une preuve `account_absence`, car un compte absent ne produit pas de snapshot matérialisé.
### Preuve machine-readable `Create -> Mint`
À partir de `pre.013-delta-fix-012`, une campagne réussie imprime en plus une ligne unique `METAPLEX_CREATE_MINT_EVIDENCE`. Elle contient les deux objets `create` et `mint` avec le cluster, le genesis hash, le slot RPC de simulation, le nombre de logs, le message hash exact, le fee estimé, la signature, le statut/slot de confirmation, les nombres de snapshots et matérialisations ainsi que les drapeaux canonical/Core/replay/idempotence. Cette ligne est la forme à conserver pour les futures promotions de matrice.
Les cinq familles NFT, SFT, fungible, collection et pNFT sont confirmées. `Create` et `Mint` disposent chacune de cinq bundles `familyEvidence`; le pNFT prouve en plus la création du Token Record et létat ATA `Initialized(1) -> Frozen(2)`.
## Probe Devnet Metaplex `Use`
La surface courante `Use` est classée `unavailable` sur Devnet : la simulation exacte du discriminant 51 retourne `InvalidInstructionData` et aucune transaction n'est soumise. Le test opt-in reste utile comme contrôle de non-régression du runtime :
```bash
KB_DEVNET_METAPLEX_USE_PROBE_TEST=1 \
KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 \
KB_POSTGRES_TEST_URL='postgres://solana:solana@localhost:5432/solana_test' \
cargo test -p kb-pipeline-demo-scenarios \
optional_devnet_metaplex_use_probe_from_env \
-- --nocapture
```
Une simulation négative est retournée comme résultat uniquement lorsque `submit=false`. Elle ne peut jamais autoriser une signature ou une soumission.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/lib.rs
// version: 15
// version: 27
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
@@ -9,27 +9,9 @@
mod constants;
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 solana_ata_execution;
mod solana_execution;
mod solana_memo_execution;
mod solana_metaplex_token_metadata_devnet_execution;
mod solana_metaplex_token_metadata_scenarios;
mod solana_metaplex_token_metadata_validation;
mod solana_token_2022_devnet_execution;
mod solana_token_2022_devnet_scenarios;
mod solana_token_2022_metadata_campaign;
mod solana_token_2022_metadata_validation;
mod solana_token_2022_validation;
mod solana_token_execution;
mod solana_token_lifecycle;
mod token_2022_fixture;
mod token_2022_metadata_fixture;
mod metadata;
mod solana;
mod spl;
/// Readiness report for one Devnet profile PostgreSQL store.
pub use self::environment::DevnetProfileStoreReadiness;
@@ -39,284 +21,356 @@ pub use self::environment::initialize_demo_scenario_environment;
pub use self::environment::prepare_demo_devnet_profile_store;
/// Resolves one configured Devnet profile for UI, CLI and test scenarios.
pub use self::environment::resolve_demo_devnet_profile;
/// 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.
pub use self::metaplex_token_metadata_fixture::MetaplexCreateFixturePreparationOptions;
/// Public values generated for one Metaplex Create fixture.
pub use self::metaplex_token_metadata_fixture::MetaplexCreateFixturePreparationSummary;
/// Creates or reuses a classic SPL mint and derives its Metaplex PDAs.
pub use self::metaplex_token_metadata_fixture::prepare_metaplex_create_fixture;
/// Complete request for one Devnet Associated Token Account execution.
pub use self::solana_ata_execution::DevnetSplAssociatedTokenAccountExecutionRequest;
/// Complete result of one Devnet Associated Token Account execution.
pub use self::solana_ata_execution::DevnetSplAssociatedTokenAccountExecutionSummary;
/// Executes one Devnet Associated Token Account simulation or authorized submission.
pub use self::solana_ata_execution::execute_devnet_spl_associated_token_account;
/// Simulates one Devnet Associated Token Account operation after stateful preflight.
pub use self::solana_ata_execution::simulate_devnet_spl_associated_token_account;
/// Complete request for one bounded System Program transfer on Devnet.
pub use self::solana_execution::DevnetSystemTransferRequest;
/// Summary returned by one bounded System Program transfer execution.
pub use self::solana_execution::DevnetSystemTransferSummary;
/// No-op observer suitable for CLI tools and opt-in integration tests.
pub use self::solana_execution::NoopSolanaExecutionObserver;
/// Observer notified during Solana execution orchestration.
pub use self::solana_execution::SolanaExecutionObserver;
/// Progress event emitted by Solana execution orchestration.
pub use self::solana_execution::SolanaExecutionProgressEvent;
/// Severity of one Solana execution progress event.
pub use self::solana_execution::SolanaExecutionProgressLevel;
/// Emits one execution progress event for specialized pipeline orchestrators.
pub(crate) use self::solana_execution::emit;
/// Rejects one execution stage when the observer reports cancellation.
pub(crate) use self::solana_execution::ensure_not_cancelled;
/// Executes one bounded System Program transfer on Devnet.
pub use self::solana_execution::execute_devnet_system_transfer;
/// Loads the persistent wallet configured by one execution profile.
pub(crate) use self::solana_execution::load_profile_wallet;
/// Formats one failed simulation without discarding runtime diagnostics.
pub(crate) use self::solana_execution::simulation_failure_message;
/// Formats safety violations for one denied execution plan.
pub(crate) use self::solana_execution::violation_message;
/// Complete request for one SPL Memo v4 Devnet execution.
pub use self::solana_memo_execution::DevnetMemoExecutionRequest;
/// Complete result of one SPL Memo v4 Devnet execution and post-validation.
pub use self::solana_memo_execution::DevnetMemoExecutionSummary;
/// Executes one SPL Memo v4 Devnet simulation or explicitly authorized submission.
pub use self::solana_memo_execution::execute_devnet_memo;
/// Complete evidence produced by one collection parent/member verification campaign.
pub use self::metadata::metaplex_token_metadata::collection_verify_campaign::DevnetMetaplexCollectionVerifyCampaignSummary;
/// Collection relation and sized-collection state observed across verification.
pub use self::metadata::metaplex_token_metadata::collection_verify_campaign::DevnetMetaplexCollectionVerifyState;
/// Executes one fresh collection parent/member verification campaign on Devnet.
pub use self::metadata::metaplex_token_metadata::collection_verify_campaign::execute_devnet_metaplex_collection_verify_campaign;
/// Returns the exact current operations qualified by the collection verification campaign.
pub use self::metadata::metaplex_token_metadata::collection_verify_campaign::metaplex_collection_verify_campaign_operation_names;
/// Complete evidence produced by one family-specific Metaplex `Create -> Mint` campaign.
pub use self::metadata::metaplex_token_metadata::create_mint_campaign::DevnetMetaplexCreateMintCampaignSummary;
/// Confirmed SPL state observed around the Metaplex `Mint` operation.
pub use self::metadata::metaplex_token_metadata::create_mint_campaign::DevnetMetaplexCreateMintTokenState;
/// Executes one fresh family-specific Metaplex `Create -> Mint` campaign on Devnet.
pub use self::metadata::metaplex_token_metadata::create_mint_campaign::execute_devnet_metaplex_create_mint_campaign;
/// Returns the exact ordered operations covered by the `Create -> Mint` campaign.
pub use self::metadata::metaplex_token_metadata::create_mint_campaign::metaplex_create_mint_campaign_operation_names;
/// Complete request for one real Devnet Metaplex Token Metadata execution.
pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionRequest;
pub use self::metadata::metaplex_token_metadata::devnet_execution::DevnetMetaplexTokenMetadataExecutionRequest;
/// Complete evidence produced by one real Devnet Metaplex execution.
pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionSummary;
pub use self::metadata::metaplex_token_metadata::devnet_execution::DevnetMetaplexTokenMetadataExecutionSummary;
/// Creates one named creator unverification request without handwritten JSON.
pub use self::solana_metaplex_token_metadata_devnet_execution::devnet_metaplex_creator_unverify_request;
pub use self::metadata::metaplex_token_metadata::devnet_execution::devnet_metaplex_creator_unverify_request;
/// Creates one named creator verification request without handwritten JSON.
pub use self::solana_metaplex_token_metadata_devnet_execution::devnet_metaplex_creator_verify_request;
pub use self::metadata::metaplex_token_metadata::devnet_execution::devnet_metaplex_creator_verify_request;
/// Executes one real Devnet Metaplex simulation or explicitly authorized submission.
pub use self::solana_metaplex_token_metadata_devnet_execution::execute_devnet_metaplex_token_metadata;
pub use self::metadata::metaplex_token_metadata::devnet_execution::execute_devnet_metaplex_token_metadata;
/// Executes one real Devnet Metaplex submission with explicitly authorized extra signers.
pub use self::metadata::metaplex_token_metadata::devnet_execution::execute_devnet_metaplex_token_metadata_with_signers;
/// Returns a formatted JSON template for one named current operation.
pub use self::solana_metaplex_token_metadata_devnet_execution::metaplex_token_metadata_current_operation_json_template;
pub use self::metadata::metaplex_token_metadata::devnet_execution::metaplex_token_metadata_current_operation_json_template;
/// Returns the current operation names accepted by automatic Devnet campaigns.
pub use self::solana_metaplex_token_metadata_devnet_execution::metaplex_token_metadata_current_operation_names;
pub use self::metadata::metaplex_token_metadata::devnet_execution::metaplex_token_metadata_current_operation_names;
/// Simulates one current Metaplex operation against a real Devnet endpoint.
pub use self::solana_metaplex_token_metadata_devnet_execution::simulate_devnet_metaplex_token_metadata;
pub use self::metadata::metaplex_token_metadata::devnet_execution::simulate_devnet_metaplex_token_metadata;
/// Exact SPL state transitions exercised around the Token Owned Escrow operations.
pub use self::metadata::metaplex_token_metadata::escrow_campaign::DevnetMetaplexEscrowCampaignState;
/// Complete evidence retained by one Token Owned Escrow Devnet campaign.
pub use self::metadata::metaplex_token_metadata::escrow_campaign::DevnetMetaplexEscrowCampaignSummary;
/// Executes a fresh Token Owned Escrow lifecycle on Devnet.
pub use self::metadata::metaplex_token_metadata::escrow_campaign::execute_devnet_metaplex_escrow_campaign;
/// Returns the exact ordered Metaplex operations qualified by the escrow campaign.
pub use self::metadata::metaplex_token_metadata::escrow_campaign::metaplex_escrow_campaign_operation_names;
/// Options for one Metaplex Create fixture preparation.
pub use self::metadata::metaplex_token_metadata::fixture::MetaplexCreateFixturePreparationOptions;
/// Public values generated for one Metaplex Create fixture.
pub use self::metadata::metaplex_token_metadata::fixture::MetaplexCreateFixturePreparationSummary;
/// Reads the validated classic SPL mint supply for an internal Metaplex campaign.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::classic_fixture_mint_supply;
/// Reads the validated classic SPL token-account state for an internal Metaplex campaign.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::classic_fixture_token_account_state;
/// Reads the validated classic SPL token-account amount for an internal Metaplex campaign.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::classic_fixture_token_amount;
/// Creates or reuses a classic SPL mint and derives its Metaplex PDAs.
pub use self::metadata::metaplex_token_metadata::fixture::prepare_metaplex_create_fixture;
/// Reads one classic SPL mint account at or after a minimum context slot.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::read_mint_account_at_or_after;
/// Reads one classic SPL token account at or after a minimum context slot.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::read_token_account_at_or_after;
/// Validates one classic SPL mint fixture against an exact expected supply.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::validate_classic_fixture_mint_state;
/// Validates one classic SPL token-account fixture against an exact expected amount.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::validate_classic_fixture_token_account_state;
/// Complete evidence produced by the final Metaplex maintenance campaign.
pub use self::metadata::metaplex_token_metadata::maintenance_campaign::DevnetMetaplexMaintenanceCampaignSummary;
/// Executes the final bounded current maintenance campaign on Devnet.
pub use self::metadata::metaplex_token_metadata::maintenance_campaign::execute_devnet_metaplex_maintenance_campaign;
/// Returns the exact ordered operations exercised by the maintenance campaign.
pub use self::metadata::metaplex_token_metadata::maintenance_campaign::metaplex_maintenance_campaign_operation_names;
/// Complete evidence produced by one pNFT lifecycle campaign.
pub use self::metadata::metaplex_token_metadata::pnft_lifecycle_campaign::DevnetMetaplexPnftLifecycleCampaignSummary;
/// Exact programmable-token state observed across the pNFT lifecycle campaign.
pub use self::metadata::metaplex_token_metadata::pnft_lifecycle_campaign::DevnetMetaplexPnftLifecycleState;
/// Executes one fresh pNFT delegate/lock/unlock/revoke/transfer campaign on Devnet.
pub use self::metadata::metaplex_token_metadata::pnft_lifecycle_campaign::execute_devnet_metaplex_pnft_lifecycle_campaign;
/// Returns the exact ordered current operations exercised by the pNFT lifecycle campaign.
pub use self::metadata::metaplex_token_metadata::pnft_lifecycle_campaign::metaplex_pnft_lifecycle_campaign_operation_names;
/// Complete evidence produced by one printable master NFT `Print -> Burn` campaign.
pub use self::metadata::metaplex_token_metadata::print_burn_campaign::DevnetMetaplexPrintBurnCampaignSummary;
/// Exact printed-edition state transition observed around `Print` and `Burn`.
pub use self::metadata::metaplex_token_metadata::print_burn_campaign::DevnetMetaplexPrintBurnState;
/// Executes one printable master NFT `Print -> Burn` campaign on Devnet.
pub use self::metadata::metaplex_token_metadata::print_burn_campaign::execute_devnet_metaplex_print_burn_campaign;
/// Returns the exact ordered current operations qualified by the print/burn campaign.
pub use self::metadata::metaplex_token_metadata::print_burn_campaign::metaplex_print_burn_campaign_operation_names;
/// Stable asset family covered by one Metaplex scenario.
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataAssetFamily;
pub use self::metadata::metaplex_token_metadata::scenarios::MetaplexTokenMetadataAssetFamily;
/// Stable fixture graph required by one Metaplex scenario journey.
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataFixtureKind;
pub use self::metadata::metaplex_token_metadata::scenarios::MetaplexTokenMetadataFixtureKind;
/// Stable lifecycle state consumed or produced by one Metaplex scenario journey.
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataFixtureState;
pub use self::metadata::metaplex_token_metadata::scenarios::MetaplexTokenMetadataFixtureState;
/// One stable Metaplex scenario reusable by automated tests and demo adapters.
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataScenario;
pub use self::metadata::metaplex_token_metadata::scenarios::MetaplexTokenMetadataScenario;
/// Execution mode accepted by one Metaplex scenario.
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataScenarioMode;
pub use self::metadata::metaplex_token_metadata::scenarios::MetaplexTokenMetadataScenarioMode;
/// Returns the ordered Metaplex Devnet simulation scenario inventory.
pub use self::solana_metaplex_token_metadata_scenarios::metaplex_token_metadata_devnet_scenarios;
pub use self::metadata::metaplex_token_metadata::scenarios::metaplex_token_metadata_devnet_scenarios;
/// Returns the complete ordered synthetic Metaplex scenario inventory.
pub use self::solana_metaplex_token_metadata_scenarios::metaplex_token_metadata_synthetic_scenarios;
pub use self::metadata::metaplex_token_metadata::scenarios::metaplex_token_metadata_synthetic_scenarios;
/// Runtime availability classification produced by one bounded current `Use` probe.
pub use self::metadata::metaplex_token_metadata::use_campaign::DevnetMetaplexUseProbeStatus;
/// Complete evidence produced by one bounded current `Use` probe.
pub use self::metadata::metaplex_token_metadata::use_campaign::DevnetMetaplexUseProbeSummary;
/// Executes one bounded current `Use` availability probe on Devnet.
pub use self::metadata::metaplex_token_metadata::use_campaign::execute_devnet_metaplex_use_probe;
/// Returns the exact setup and target operations exercised by the `Use` probe.
pub use self::metadata::metaplex_token_metadata::use_campaign::metaplex_use_probe_operation_names;
/// Maximum evidence entries accepted by one Metaplex validation scenario.
pub use self::solana_metaplex_token_metadata_validation::MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE;
pub use self::metadata::metaplex_token_metadata::validation::MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE;
/// One case in the closed Metaplex cross-validation corpus.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataCrossValidationCase;
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataCrossValidationCase;
/// Closed Metaplex cross-validation corpus for `0.4.7-pre.010`.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataCrossValidationMatrix;
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataCrossValidationMatrix;
/// Family-specific evidence bundle for one current Metaplex Devnet operation.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataDevnetExecutionFamilyEvidence;
/// Closed inventory of every current Metaplex operation requiring Devnet coverage.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataDevnetExecutionMatrix;
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataDevnetExecutionMatrix;
/// One current Metaplex operation in the closed Devnet execution matrix.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataDevnetExecutionOperation;
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataDevnetExecutionOperation;
/// Stable failure category exercised by one negative case.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataFailureClass;
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataFailureClass;
/// Expected transaction outcome for one cross-validation case.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataTransactionOutcome;
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataTransactionOutcome;
/// Transaction location covered by one cross-validation case.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataTransactionPath;
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataTransactionPath;
/// One bounded Metaplex validation evidence entry.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataValidationEvidence;
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataValidationEvidence;
/// Canonical Metaplex validation matrix.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataValidationMatrix;
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataValidationMatrix;
/// One scenario declared by the canonical Metaplex validation matrix.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataValidationMatrixScenario;
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataValidationMatrixScenario;
/// Exact validation status for one Metaplex scenario.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataValidationStatus;
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataValidationStatus;
/// Loads and validates the closed Metaplex cross-validation corpus.
pub use self::solana_metaplex_token_metadata_validation::load_metaplex_token_metadata_cross_validation_matrix;
pub use self::metadata::metaplex_token_metadata::validation::load_metaplex_token_metadata_cross_validation_matrix;
/// Loads and validates the closed Devnet execution matrix.
pub use self::solana_metaplex_token_metadata_validation::load_metaplex_token_metadata_devnet_execution_matrix;
pub use self::metadata::metaplex_token_metadata::validation::load_metaplex_token_metadata_devnet_execution_matrix;
/// Loads and validates the canonical Metaplex validation matrix.
pub use self::solana_metaplex_token_metadata_validation::load_metaplex_token_metadata_validation_matrix;
pub use self::metadata::metaplex_token_metadata::validation::load_metaplex_token_metadata_validation_matrix;
/// Validates the closed Metaplex cross-validation corpus and evidence claims.
pub use self::solana_metaplex_token_metadata_validation::validate_metaplex_token_metadata_cross_validation_matrix;
pub use self::metadata::metaplex_token_metadata::validation::validate_metaplex_token_metadata_cross_validation_matrix;
/// Validates exact current-operation coverage and conservative network statuses.
pub use self::solana_metaplex_token_metadata_validation::validate_metaplex_token_metadata_devnet_execution_matrix;
pub use self::metadata::metaplex_token_metadata::validation::validate_metaplex_token_metadata_devnet_execution_matrix;
/// Validates the canonical Metaplex validation matrix.
pub use self::solana_metaplex_token_metadata_validation::validate_metaplex_token_metadata_validation_matrix;
/// Complete request for one Devnet Token-2022 execution.
pub use self::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionRequest;
/// Complete result of one Devnet Token-2022 execution.
pub use self::solana_token_2022_devnet_execution::DevnetSplToken2022ExecutionSummary;
/// Executes one Devnet Token-2022 simulation or authorized submission.
pub use self::solana_token_2022_devnet_execution::execute_devnet_spl_token_2022;
/// Simulates one Devnet Token-2022 operation after stateful preflight.
pub use self::solana_token_2022_devnet_execution::simulate_devnet_spl_token_2022;
/// Stable category of one independent Devnet validation scenario.
pub use self::solana_token_2022_devnet_scenarios::DevnetSplValidationFamily;
/// Current implementation status of one Devnet validation scenario.
pub use self::solana_token_2022_devnet_scenarios::DevnetSplValidationImplementationStatus;
/// One independent Devnet validation scenario exposed to applications.
pub use self::solana_token_2022_devnet_scenarios::DevnetSplValidationScenario;
/// Returns the complete ordered Devnet scenario inventory.
pub use self::solana_token_2022_devnet_scenarios::devnet_spl_validation_scenarios;
/// One confirmed step in the five-instruction Token Metadata Devnet campaign.
pub use self::solana_token_2022_metadata_campaign::DevnetToken2022MetadataCampaignStepSummary;
/// Complete evidence retained by the Token Metadata Devnet campaign.
pub use self::solana_token_2022_metadata_campaign::DevnetToken2022MetadataCampaignSummary;
/// Executes all five Token Metadata interface instructions on one fresh Devnet mint.
pub use self::solana_token_2022_metadata_campaign::execute_devnet_token_2022_metadata_campaign;
/// Returns the exact ordered operation names covered by the Token Metadata campaign.
pub use self::solana_token_2022_metadata_campaign::token_2022_metadata_campaign_operation_names;
/// Maximum evidence entries retained by one Token Metadata validation scenario.
pub use self::solana_token_2022_metadata_validation::MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE;
/// One bounded evidence item retained by the Token Metadata validation matrix.
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationEvidence;
/// Canonical Token Metadata Devnet validation matrix.
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationMatrix;
/// One scenario in the Token Metadata Devnet validation matrix.
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationScenario;
/// Exact network-validation status for one Token Metadata scenario.
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationStatus;
/// Loads and validates the Token Metadata Devnet validation matrix.
pub use self::solana_token_2022_metadata_validation::load_token_2022_metadata_validation_matrix;
/// Validates exact Token Metadata campaign coverage and evidence claims.
pub use self::solana_token_2022_metadata_validation::validate_token_2022_metadata_validation_matrix;
/// Maximum number of evidence records accepted in one validation report.
pub use self::solana_token_2022_validation::MAX_TOKEN_2022_VALIDATION_EVIDENCE;
/// Required validation environment for one Token-2022 scenario.
pub use self::solana_token_2022_validation::Token2022ValidationEnvironment;
/// One bounded proof attached to a Token-2022 validation scenario.
pub use self::solana_token_2022_validation::Token2022ValidationEvidence;
/// Machine-readable Token-2022 validation matrix.
pub use self::solana_token_2022_validation::Token2022ValidationMatrix;
/// One scenario declared by the canonical validation matrix.
pub use self::solana_token_2022_validation::Token2022ValidationMatrixScenario;
/// Complete bounded Token-2022 validation report.
pub use self::solana_token_2022_validation::Token2022ValidationReport;
/// One declared Token-2022 validation scenario and its observed evidence.
pub use self::solana_token_2022_validation::Token2022ValidationScenario;
/// Exact status of one Token-2022 validation scenario.
pub use self::solana_token_2022_validation::Token2022ValidationStatus;
/// Loads and validates the canonical Token-2022 validation matrix.
pub use self::solana_token_2022_validation::load_token_2022_validation_matrix;
/// Validates one Token-2022 validation matrix.
pub use self::solana_token_2022_validation::validate_token_2022_validation_matrix;
/// Validates one bounded Token-2022 milestone report.
pub use self::solana_token_2022_validation::validate_token_2022_validation_report;
pub use self::metadata::metaplex_token_metadata::validation::validate_metaplex_token_metadata_validation_matrix;
/// 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;
/// Complete request for one bounded System Program transfer on Devnet.
pub use self::solana::DevnetSystemTransferRequest;
/// Summary returned by one bounded System Program transfer execution.
pub use self::solana::DevnetSystemTransferSummary;
/// No-op observer suitable for CLI tools and opt-in integration tests.
pub use self::solana::NoopSolanaExecutionObserver;
/// Observer notified during Solana execution orchestration.
pub use self::solana::SolanaExecutionObserver;
/// Progress event emitted by Solana execution orchestration.
pub use self::solana::SolanaExecutionProgressEvent;
/// Severity of one Solana execution progress event.
pub use self::solana::SolanaExecutionProgressLevel;
/// Emits one execution progress event for specialized pipeline orchestrators.
pub(crate) use self::solana::emit;
/// Rejects one execution stage when the observer reports cancellation.
pub(crate) use self::solana::ensure_not_cancelled;
/// Executes one bounded System Program transfer on Devnet.
pub use self::solana::execute_devnet_system_transfer;
/// Loads the persistent wallet configured by one execution profile.
pub(crate) use self::solana::load_profile_wallet;
/// Formats one failed simulation without discarding runtime diagnostics.
pub(crate) use self::solana::simulation_failure_message;
/// Formats safety violations for one denied execution plan.
pub(crate) use self::solana::violation_message;
/// Complete request for one Devnet Associated Token Account execution.
pub use self::spl::associated_token_account::DevnetSplAssociatedTokenAccountExecutionRequest;
/// Complete result of one Devnet Associated Token Account execution.
pub use self::spl::associated_token_account::DevnetSplAssociatedTokenAccountExecutionSummary;
/// Executes one Devnet Associated Token Account simulation or authorized submission.
pub use self::spl::associated_token_account::execute_devnet_spl_associated_token_account;
/// Simulates one Devnet Associated Token Account operation after stateful preflight.
pub use self::spl::associated_token_account::simulate_devnet_spl_associated_token_account;
/// Complete request for one SPL Memo v4 Devnet execution.
pub use self::spl::memo::DevnetMemoExecutionRequest;
/// Complete result of one SPL Memo v4 Devnet execution and post-validation.
pub use self::spl::memo::DevnetMemoExecutionSummary;
/// Executes one SPL Memo v4 Devnet simulation or explicitly authorized submission.
pub use self::spl::memo::execute_devnet_memo;
/// Complete request for one Devnet classic SPL Token execution.
pub use self::solana_token_execution::DevnetSplTokenExecutionRequest;
pub use self::spl::token::execution::DevnetSplTokenExecutionRequest;
/// Complete result of one Devnet classic SPL Token execution.
pub use self::solana_token_execution::DevnetSplTokenExecutionSummary;
pub use self::spl::token::execution::DevnetSplTokenExecutionSummary;
/// Canonical hydration availability helper shared by Token lifecycle orchestration.
pub(crate) use self::solana_token_execution::canonical_available;
pub(crate) use self::spl::token::execution::canonical_available;
/// Decode completion helper shared by Token lifecycle orchestration.
pub(crate) use self::solana_token_execution::decode_completed;
pub(crate) use self::spl::token::execution::decode_completed;
/// Executes one Devnet classic SPL Token simulation or authorized submission.
pub use self::solana_token_execution::execute_devnet_spl_token;
pub use self::spl::token::execution::execute_devnet_spl_token;
/// Canonical signature hydration helper shared by Token lifecycle orchestration.
pub(crate) use self::solana_token_execution::hydrate_signature;
pub(crate) use self::spl::token::execution::hydrate_signature;
/// Targeted program replay helper shared by Token lifecycle orchestration.
pub(crate) use self::solana_token_execution::replay_program;
pub(crate) use self::spl::token::execution::replay_program;
/// Simulates one Devnet classic SPL Token operation after stateful preflight.
pub use self::solana_token_execution::simulate_devnet_spl_token;
pub use self::spl::token::execution::simulate_devnet_spl_token;
/// Complete request to prepare raw accounts for one Devnet SPL Token lifecycle.
pub use self::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationRequest;
pub use self::spl::token::lifecycle::DevnetSplTokenLifecyclePreparationRequest;
/// One prepared raw account step.
pub use self::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationStep;
pub use self::spl::token::lifecycle::DevnetSplTokenLifecyclePreparationStep;
/// Summary of raw-account preparation for one lifecycle.
pub use self::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationSummary;
pub use self::spl::token::lifecycle::DevnetSplTokenLifecyclePreparationSummary;
/// Complete request for one controlled Devnet SPL Token lifecycle.
pub use self::solana_token_lifecycle::DevnetSplTokenLifecycleRequest;
pub use self::spl::token::lifecycle::DevnetSplTokenLifecycleRequest;
/// Summary of one lifecycle operation step.
pub use self::solana_token_lifecycle::DevnetSplTokenLifecycleStepSummary;
pub use self::spl::token::lifecycle::DevnetSplTokenLifecycleStepSummary;
/// Complete controlled lifecycle summary.
pub use self::solana_token_lifecycle::DevnetSplTokenLifecycleSummary;
pub use self::spl::token::lifecycle::DevnetSplTokenLifecycleSummary;
/// Executes one controlled Devnet SPL Token lifecycle.
pub use self::solana_token_lifecycle::execute_devnet_spl_token_lifecycle;
pub use self::spl::token::lifecycle::execute_devnet_spl_token_lifecycle;
/// Prepares raw accounts for one controlled Devnet SPL Token lifecycle.
pub use self::solana_token_lifecycle::prepare_devnet_spl_token_lifecycle_accounts;
pub use self::spl::token::lifecycle::prepare_devnet_spl_token_lifecycle_accounts;
/// Complete request for one Devnet Token-2022 execution.
pub use self::spl::token_2022::devnet_execution::DevnetSplToken2022ExecutionRequest;
/// Complete result of one Devnet Token-2022 execution.
pub use self::spl::token_2022::devnet_execution::DevnetSplToken2022ExecutionSummary;
/// Executes one Devnet Token-2022 simulation or authorized submission.
pub use self::spl::token_2022::devnet_execution::execute_devnet_spl_token_2022;
/// Simulates one Devnet Token-2022 operation after stateful preflight.
pub use self::spl::token_2022::devnet_execution::simulate_devnet_spl_token_2022;
/// Stable category of one independent Devnet validation scenario.
pub use self::spl::token_2022::devnet_scenarios::DevnetSplValidationFamily;
/// Current implementation status of one Devnet validation scenario.
pub use self::spl::token_2022::devnet_scenarios::DevnetSplValidationImplementationStatus;
/// One independent Devnet validation scenario exposed to applications.
pub use self::spl::token_2022::devnet_scenarios::DevnetSplValidationScenario;
/// Returns the complete ordered Devnet scenario inventory.
pub use self::spl::token_2022::devnet_scenarios::devnet_spl_validation_scenarios;
/// Default raw amount delegated by the public ApproveChecked scenario.
pub use self::token_2022_fixture::DEFAULT_TOKEN_2022_APPROVE_AMOUNT_RAW;
pub use self::spl::token_2022::fixture::DEFAULT_TOKEN_2022_APPROVE_AMOUNT_RAW;
/// Default raw amount burned by the public BurnChecked scenario.
pub use self::token_2022_fixture::DEFAULT_TOKEN_2022_BURN_AMOUNT_RAW;
pub use self::spl::token_2022::fixture::DEFAULT_TOKEN_2022_BURN_AMOUNT_RAW;
/// Default raw amount minted by the public MintToChecked scenario.
pub use self::token_2022_fixture::DEFAULT_TOKEN_2022_MINT_AMOUNT_RAW;
pub use self::spl::token_2022::fixture::DEFAULT_TOKEN_2022_MINT_AMOUNT_RAW;
/// Default raw amount transferred by the public TransferChecked scenario.
pub use self::token_2022_fixture::DEFAULT_TOKEN_2022_TRANSFER_AMOUNT_RAW;
pub use self::spl::token_2022::fixture::DEFAULT_TOKEN_2022_TRANSFER_AMOUNT_RAW;
/// Command-line options for one Token-2022 fixture preparation.
pub use self::token_2022_fixture::Token2022FixturePreparationOptions;
pub use self::spl::token_2022::fixture::Token2022FixturePreparationOptions;
/// Public values written to one Token-2022 fixture file.
pub use self::token_2022_fixture::Token2022FixturePreparationSummary;
pub use self::spl::token_2022::fixture::Token2022FixturePreparationSummary;
/// Creates or reuses one complete Token-2022 public scenario fixture.
pub use self::token_2022_fixture::prepare_token_2022_fixture;
pub use self::spl::token_2022::fixture::prepare_token_2022_fixture;
/// One confirmed step in the five-instruction Token Metadata Devnet campaign.
pub use self::spl::token_2022::metadata::campaign::DevnetToken2022MetadataCampaignStepSummary;
/// Complete evidence retained by the Token Metadata Devnet campaign.
pub use self::spl::token_2022::metadata::campaign::DevnetToken2022MetadataCampaignSummary;
/// Executes all five Token Metadata interface instructions on one fresh Devnet mint.
pub use self::spl::token_2022::metadata::campaign::execute_devnet_token_2022_metadata_campaign;
/// Returns the exact ordered operation names covered by the Token Metadata campaign.
pub use self::spl::token_2022::metadata::campaign::token_2022_metadata_campaign_operation_names;
/// Additional metadata key created and removed by the campaign.
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_KEY;
pub use self::spl::token_2022::metadata::fixture::TOKEN_2022_METADATA_CAMPAIGN_KEY;
/// Stable metadata name used by the `0.4.8-pre.012` campaign.
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_NAME;
pub use self::spl::token_2022::metadata::fixture::TOKEN_2022_METADATA_CAMPAIGN_NAME;
/// Stable metadata symbol used by the `0.4.8-pre.012` campaign.
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_SYMBOL;
pub use self::spl::token_2022::metadata::fixture::TOKEN_2022_METADATA_CAMPAIGN_SYMBOL;
/// Stable opaque URI stored by the `0.4.8-pre.012` campaign.
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_URI;
pub use self::spl::token_2022::metadata::fixture::TOKEN_2022_METADATA_CAMPAIGN_URI;
/// Additional metadata value created and removed by the campaign.
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_VALUE;
pub use self::spl::token_2022::metadata::fixture::TOKEN_2022_METADATA_CAMPAIGN_VALUE;
/// Options used to prepare one fresh Token-2022 metadata fixture.
pub use self::token_2022_metadata_fixture::Token2022MetadataFixturePreparationOptions;
pub use self::spl::token_2022::metadata::fixture::Token2022MetadataFixturePreparationOptions;
/// Fresh mint and authorities prepared for one Token Metadata campaign.
pub use self::token_2022_metadata_fixture::Token2022MetadataFixturePreparationSummary;
pub use self::spl::token_2022::metadata::fixture::Token2022MetadataFixturePreparationSummary;
/// Creates one fresh Token-2022 mint with a self-referential Metadata Pointer.
pub use self::token_2022_metadata_fixture::prepare_token_2022_metadata_fixture;
pub use self::spl::token_2022::metadata::fixture::prepare_token_2022_metadata_fixture;
/// Maximum evidence entries retained by one Token Metadata validation scenario.
pub use self::spl::token_2022::metadata::validation::MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE;
/// One bounded evidence item retained by the Token Metadata validation matrix.
pub use self::spl::token_2022::metadata::validation::Token2022MetadataValidationEvidence;
/// Canonical Token Metadata Devnet validation matrix.
pub use self::spl::token_2022::metadata::validation::Token2022MetadataValidationMatrix;
/// One scenario in the Token Metadata Devnet validation matrix.
pub use self::spl::token_2022::metadata::validation::Token2022MetadataValidationScenario;
/// Exact network-validation status for one Token Metadata scenario.
pub use self::spl::token_2022::metadata::validation::Token2022MetadataValidationStatus;
/// Loads and validates the Token Metadata Devnet validation matrix.
pub use self::spl::token_2022::metadata::validation::load_token_2022_metadata_validation_matrix;
/// Validates exact Token Metadata campaign coverage and evidence claims.
pub use self::spl::token_2022::metadata::validation::validate_token_2022_metadata_validation_matrix;
/// Maximum number of evidence records accepted in one validation report.
pub use self::spl::token_2022::validation::MAX_TOKEN_2022_VALIDATION_EVIDENCE;
/// Required validation environment for one Token-2022 scenario.
pub use self::spl::token_2022::validation::Token2022ValidationEnvironment;
/// One bounded proof attached to a Token-2022 validation scenario.
pub use self::spl::token_2022::validation::Token2022ValidationEvidence;
/// Machine-readable Token-2022 validation matrix.
pub use self::spl::token_2022::validation::Token2022ValidationMatrix;
/// One scenario declared by the canonical validation matrix.
pub use self::spl::token_2022::validation::Token2022ValidationMatrixScenario;
/// Complete bounded Token-2022 validation report.
pub use self::spl::token_2022::validation::Token2022ValidationReport;
/// One declared Token-2022 validation scenario and its observed evidence.
pub use self::spl::token_2022::validation::Token2022ValidationScenario;
/// Exact status of one Token-2022 validation scenario.
pub use self::spl::token_2022::validation::Token2022ValidationStatus;
/// Loads and validates the canonical Token-2022 validation matrix.
pub use self::spl::token_2022::validation::load_token_2022_validation_matrix;
/// Validates one Token-2022 validation matrix.
pub use self::spl::token_2022::validation::validate_token_2022_validation_matrix;
/// Validates one bounded Token-2022 milestone report.
pub use self::spl::token_2022::validation::validate_token_2022_validation_report;
/// Total transactions reserved by one complete Solana Program Metadata campaign.
pub(crate) use self::constants::SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT;

View File

@@ -0,0 +1,7 @@
// file: kb-pipeline-demo-scenarios/src/metadata.rs
// version: 1
//! Metadata-oriented reusable demo and Devnet validation scenarios.
pub(crate) mod metaplex_token_metadata;
pub(crate) mod solana_program;

View File

@@ -0,0 +1,16 @@
// file: kb-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata.rs
// version: 7
//! Metaplex Token Metadata demo and Devnet validation scenarios.
pub(crate) mod collection_verify_campaign;
pub(crate) mod create_mint_campaign;
pub(crate) mod devnet_execution;
pub(crate) mod escrow_campaign;
pub(crate) mod fixture;
pub(crate) mod maintenance_campaign;
pub(crate) mod pnft_lifecycle_campaign;
pub(crate) mod print_burn_campaign;
pub(crate) mod scenarios;
pub(crate) mod use_campaign;
pub(crate) mod validation;

View File

@@ -0,0 +1,747 @@
// file: kb-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs
// version: 1
//! Confirmed Devnet collection parent/member `Verify -> Unverify` campaign.
/// Collection relation and sized-collection state observed across verification.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DevnetMetaplexCollectionVerifyState {
/// Member collection flag before `Verify`.
pub verified_before: bool,
/// Member collection flag after `Verify`.
pub verified_after_verify: bool,
/// Member collection flag after `Unverify`.
pub verified_after_unverify: bool,
/// Collection size before `Verify`.
pub collection_size_before: u64,
/// Collection size after `Verify`.
pub collection_size_after_verify: u64,
/// Collection size after `Unverify`.
pub collection_size_after_unverify: u64,
}
/// Complete evidence produced by one collection parent/member verification campaign.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexCollectionVerifyCampaignSummary {
/// Fully qualified collection parent fixture and `Create -> Mint` evidence.
pub parent: crate::DevnetMetaplexCreateMintCampaignSummary,
/// Fully qualified unverified member fixture and `Create -> Mint` evidence.
pub member: crate::DevnetMetaplexCreateMintCampaignSummary,
/// Confirmed collection verification evidence.
pub verify: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Confirmed collection unverification evidence.
pub unverify: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Exact collection relation and size transitions.
pub state: crate::DevnetMetaplexCollectionVerifyState,
}
/// Returns the exact current Metaplex operations qualified by this campaign after fixture setup.
pub fn metaplex_collection_verify_campaign_operation_names() -> &'static [&'static str; 2] {
return &["verify", "unverify"];
}
/// Executes one fresh collection parent/member `Verify -> Unverify` campaign on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_collection_verify_campaign<S, O>(
http_pool: &kb_onchain_transport::HttpEndpointPool,
store: &S,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> kb_core::Result<crate::DevnetMetaplexCollectionVerifyCampaignSummary>
where
S: kb_store::RawTransactionStore
+ kb_store::CoreExtractionStore
+ kb_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if options.collection_mint.is_some() {
return std::result::Result::Err(kb_core::Error::config(
"collection verification campaign base options must not preselect a collection mint",
));
}
let mut parent_options = options.clone();
parent_options.asset_family = crate::MetaplexTokenMetadataAssetFamily::Collection;
parent_options.collection_mint = std::option::Option::None;
let parent = match crate::execute_devnet_metaplex_create_mint_campaign(
http_pool,
store,
profile,
workspace_root,
&parent_options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut member_options = options.clone();
member_options.asset_family = crate::MetaplexTokenMetadataAssetFamily::Nft;
member_options.collection_mint = std::option::Option::Some(parent.fixture.mint.clone());
let member = match crate::execute_devnet_metaplex_create_mint_campaign(
http_pool,
store,
profile,
workspace_root,
&member_options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verified_before = match collection_member_verified(
member.mint.after.as_slice(),
member.fixture.metadata.as_str(),
parent.fixture.mint.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection_size_before =
match collection_size(parent.mint.after.as_slice(), parent.fixture.metadata.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if verified_before || collection_size_before != 0 {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_verify_initial_state_invalid",
format!(
"fresh collection member must start unverified with parent size 0; verified={verified_before}, size={collection_size_before}"
),
));
}
let parent_mint_slot = match confirmed_slot("parent mint", &parent.mint) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let member_mint_slot = match confirmed_slot("member mint", &member.mint) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture_slot = std::cmp::max(parent_mint_slot, member_mint_slot);
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(kb_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> = std::vec![
std::sync::Arc::new(kb_lib::MtAdminMaterializer),
std::sync::Arc::new(kb_lib::MtComplianceAuditMaterializer),
std::sync::Arc::new(kb_lib::MtLifecycleMaterializer),
std::sync::Arc::new(kb_lib::MtMetadataMetaplexTokenMetadataMaterializer),
std::sync::Arc::new(kb_lib::MtRiskMaterializer),
];
let verify_operation = kb_lib::ExMetaplexTokenMetadataOperation::Verify {
authority: kb_lib::MdPubkey(parent.fixture.authority.clone()),
delegate_record: std::option::Option::None,
metadata: kb_lib::MdPubkey(member.fixture.metadata.clone()),
collection_mint: std::option::Option::Some(kb_lib::MdPubkey(parent.fixture.mint.clone())),
collection_metadata: std::option::Option::Some(kb_lib::MdPubkey(
parent.fixture.metadata.clone(),
)),
collection_master_edition: std::option::Option::Some(kb_lib::MdPubkey(
parent.fixture.master_edition.clone(),
)),
verification_args: mpl_token_metadata::types::VerificationArgs::CollectionV1,
};
let mut verify_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-collection-verify-{}", uuid::Uuid::new_v4()),
verify_operation,
);
verify_request.query_role = options.query_role.clone();
verify_request.transaction_role = options.transaction_role.clone();
verify_request.submit = true;
verify_request.operator_confirmed = true;
verify_request.materialize_after_confirmation = true;
verify_request.post_validation_max_retries = 20;
verify_request.preflight_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(fixture_slot),
);
verify_request.postcondition_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(fixture_slot),
);
let verify = match crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&verify_request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_slot = match validate_confirmed_execution("verify", &verify) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verified_after_verify = match collection_member_verified(
verify.after.as_slice(),
member.fixture.metadata.as_str(),
parent.fixture.mint.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection_size_after_verify =
match collection_size(verify.after.as_slice(), parent.fixture.metadata.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !verified_after_verify || collection_size_after_verify != 1 {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_verify_postcondition_failed",
format!(
"confirmed collection Verify must set member verified=true and parent size=1; verified={verified_after_verify}, size={collection_size_after_verify}"
),
));
}
let unverify_operation = kb_lib::ExMetaplexTokenMetadataOperation::Unverify {
authority: kb_lib::MdPubkey(parent.fixture.authority.clone()),
delegate_record: std::option::Option::None,
metadata: kb_lib::MdPubkey(member.fixture.metadata.clone()),
collection_mint: std::option::Option::Some(kb_lib::MdPubkey(parent.fixture.mint.clone())),
collection_metadata: std::option::Option::Some(kb_lib::MdPubkey(
parent.fixture.metadata.clone(),
)),
verification_args: mpl_token_metadata::types::VerificationArgs::CollectionV1,
};
let mut unverify_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-collection-unverify-{}", uuid::Uuid::new_v4()),
unverify_operation,
);
unverify_request.query_role = options.query_role.clone();
unverify_request.transaction_role = options.transaction_role.clone();
unverify_request.submit = true;
unverify_request.operator_confirmed = true;
unverify_request.materialize_after_confirmation = true;
unverify_request.post_validation_max_retries = 20;
unverify_request.preflight_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(verify_slot),
);
unverify_request.postcondition_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(verify_slot),
);
let unverify = match crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&unverify_request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unverify_slot = match validate_confirmed_execution("unverify", &unverify) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verified_after_unverify = match collection_member_verified(
unverify.after.as_slice(),
member.fixture.metadata.as_str(),
parent.fixture.mint.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection_size_after_unverify =
match collection_size(unverify.after.as_slice(), parent.fixture.metadata.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if verified_after_unverify || collection_size_after_unverify != 0 {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_unverify_postcondition_failed",
format!(
"confirmed collection Unverify at slot {unverify_slot} must restore verified=false and parent size=0; verified={verified_after_unverify}, size={collection_size_after_unverify}"
),
));
}
return std::result::Result::Ok(crate::DevnetMetaplexCollectionVerifyCampaignSummary {
parent,
member,
verify,
unverify,
state: crate::DevnetMetaplexCollectionVerifyState {
verified_before,
verified_after_verify,
verified_after_unverify,
collection_size_before,
collection_size_after_verify,
collection_size_after_unverify,
},
});
}
fn collection_reads(
parent: &crate::MetaplexCreateFixturePreparationSummary,
member: &crate::MetaplexCreateFixturePreparationSummary,
query_role: &str,
min_context_slot: std::option::Option<u64>,
) -> std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
return vec![
kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: kb_lib::MdPubkey(member.metadata.clone()),
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot,
max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
},
kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: kb_lib::MdPubkey(parent.metadata.clone()),
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot,
max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
},
kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: kb_lib::MdPubkey(parent.master_edition.clone()),
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::Edition {
mint: kb_lib::MdPubkey(parent.mint.clone()),
},
min_context_slot,
max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
},
];
}
fn confirmed_slot(
step: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> kb_core::Result<u64> {
return match execution.confirmation.as_ref().and_then(|value| return value.slot) {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_fixture_confirmation_slot_missing",
format!("Metaplex {step} has no confirmed slot"),
)),
};
}
fn validate_confirmed_execution(
step: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> kb_core::Result<u64> {
if !execution.simulation.success {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_simulation_failed",
format!("Metaplex collection {step} simulation failed"),
));
}
let confirmation = match execution.confirmation.as_ref() {
std::option::Option::Some(value)
if matches!(
value.status,
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
) =>
{
value
},
std::option::Option::Some(value) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_confirmation_incomplete",
format!("Metaplex collection {step} stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_confirmation_missing",
format!("Metaplex collection {step} has no confirmation evidence"),
));
},
};
let diagnostic = match execution.post_execution.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_post_execution_missing",
format!("Metaplex collection {step} has no post-execution diagnostic"),
));
},
};
if !diagnostic.canonical_inserted
|| !diagnostic.core_extracted
|| !diagnostic.decode_replayed
|| !diagnostic.materialized
|| execution.materializations.is_empty()
|| execution.materialized_snapshots.is_empty()
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_post_execution_incomplete",
format!(
"Metaplex collection {step} post-execution incomplete: signature={}, slot={:?}, canonical_inserted={}, core_extracted={}, decode_replayed={}, materialized={}, materialization_rows={}, materialized_snapshots={}, diagnostics={:?}",
confirmation.signature.0,
confirmation.slot,
diagnostic.canonical_inserted,
diagnostic.core_extracted,
diagnostic.decode_replayed,
diagnostic.materialized,
execution.materializations.len(),
execution.materialized_snapshots.len(),
diagnostic.diagnostics
),
));
}
let second_replay = match execution.idempotence_replay.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_idempotence_missing",
format!("Metaplex collection {step} has no idempotence replay evidence"),
));
},
};
if second_replay.failed_inputs != 0
|| second_replay.processing_error_inputs != 0
|| second_replay.processors.iter().any(|processor| {
return processor.failed != 0
|| processor.processing_errors != 0
|| processor.materialized_outputs != 0
|| processor.materialization_refused != 0;
})
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_idempotence_failed",
format!("Metaplex collection {step} second replay is not idempotent"),
));
}
return match confirmation.slot {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_confirmation_slot_missing",
format!("Metaplex collection {step} confirmation has no slot"),
)),
};
}
fn metadata_payload<'a>(
snapshots: &'a [kb_pipeline::MetaplexTokenMetadataStatefulReadResult],
metadata: &str,
) -> kb_core::Result<&'a serde_json::Value> {
let snapshot = snapshots.iter().find(|value| {
return value.snapshot.account.0.as_str() == metadata
&& value.snapshot.account_kind == "metadata";
});
return match snapshot {
std::option::Option::Some(value) => std::result::Result::Ok(&value.snapshot.payload_json),
std::option::Option::None => std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_metadata_snapshot_missing",
format!("Metaplex metadata snapshot {metadata} is missing"),
)),
};
}
fn collection_member_verified(
snapshots: &[kb_pipeline::MetaplexTokenMetadataStatefulReadResult],
member_metadata: &str,
expected_collection_mint: &str,
) -> kb_core::Result<bool> {
let payload = match metadata_payload(snapshots, member_metadata) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection = match payload.get("collection") {
std::option::Option::Some(value) if value.is_object() => value,
_ => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_relation_missing",
"member metadata does not contain a collection relation",
));
},
};
let key = collection.get("key").and_then(serde_json::Value::as_str);
let verified = collection.get("verified").and_then(serde_json::Value::as_bool);
if key != std::option::Option::Some(expected_collection_mint) || verified.is_none() {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_relation_invalid",
format!(
"member collection relation mismatch: expected key={expected_collection_mint}, got key={key:?}, verified={verified:?}"
),
));
}
return match verified {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_verified_flag_missing",
"member collection relation does not expose a verified flag",
)),
};
}
fn collection_size(
snapshots: &[kb_pipeline::MetaplexTokenMetadataStatefulReadResult],
collection_metadata: &str,
) -> kb_core::Result<u64> {
let payload = match metadata_payload(snapshots, collection_metadata) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let size = payload
.get("collection_details")
.and_then(|value| return value.get("V1"))
.and_then(|value| return value.get("size"))
.and_then(serde_json::Value::as_u64);
return match size {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(kb_core::Error::new(
"metaplex_collection_size_missing",
"collection metadata does not expose CollectionDetails::V1 size",
)),
};
}
#[cfg(test)]
mod tests {
#[test]
fn collection_campaign_contract_is_exactly_verify_then_unverify() {
assert_eq!(
crate::metaplex_collection_verify_campaign_operation_names(),
&["verify", "unverify"]
);
}
#[test]
fn collection_payload_contract_tracks_relation_and_size() {
let collection = solana_pubkey::Pubkey::new_unique().to_string();
let relation = serde_json::json!({"collection": {"verified": false, "key": collection}});
let details = serde_json::json!({"collection_details": {"V1": {"size": 0}}});
assert_eq!(
relation.pointer("/collection/verified").and_then(serde_json::Value::as_bool),
std::option::Option::Some(false)
);
assert_eq!(
details
.pointer("/collection_details/V1/size")
.and_then(serde_json::Value::as_u64),
std::option::Option::Some(0)
);
}
#[test]
fn metadata_serde_contract_uses_stateful_collection_keys() {
let metadata = mpl_token_metadata::accounts::Metadata {
key: mpl_token_metadata::types::Key::MetadataV1,
update_authority: mpl_token_metadata::ID,
mint: mpl_token_metadata::ID,
name: "Collection".to_string(),
symbol: "COL".to_string(),
uri: "https://example.invalid/collection.json".to_string(),
seller_fee_basis_points: 0,
creators: std::option::Option::None,
primary_sale_happened: false,
is_mutable: true,
edition_nonce: std::option::Option::None,
token_standard: std::option::Option::Some(
mpl_token_metadata::types::TokenStandard::NonFungible,
),
collection: std::option::Option::None,
uses: std::option::Option::None,
collection_details: std::option::Option::Some(
mpl_token_metadata::types::CollectionDetails::V1 { size: 7 },
),
programmable_config: std::option::Option::None,
};
let payload = match serde_json::to_value(metadata) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("metadata serialization failed: {error}"),
};
assert_eq!(
payload
.pointer("/collection_details/V1/size")
.and_then(serde_json::Value::as_u64),
std::option::Option::Some(7)
);
assert!(payload.get("collectionDetails").is_none());
}
fn execution_evidence_json(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> serde_json::Value {
let confirmation = execution.confirmation.as_ref();
let confirmation_status = confirmation
.map(|value| return format!("{:?}", value.status))
.unwrap_or_else(|| return "missing".to_string());
let confirmation_slot = confirmation.and_then(|value| return value.slot);
let signature = confirmation
.map(|value| return value.signature.0.clone())
.unwrap_or_else(|| return "missing".to_string());
let diagnostic = execution.post_execution.as_ref();
let idempotence_clean = execution.idempotence_replay.as_ref().is_some_and(|replay| {
return replay.failed_inputs == 0
&& replay.processing_error_inputs == 0
&& replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
});
return serde_json::json!({
"operation": execution.plan.operation_code.as_str(),
"cluster": execution.cluster,
"genesisHash": execution.genesis_hash.as_str(),
"simulationContextSlot": execution.simulation_context_slot,
"simulationSuccess": execution.simulation.success,
"simulationLogCount": execution.simulation.logs.len(),
"messageHash": execution.readiness.message_hash.as_str(),
"feeLamports": execution.fee.fee_lamports,
"signature": signature,
"confirmationStatus": confirmation_status,
"confirmationSlot": confirmation_slot,
"beforeSnapshots": execution.before.len(),
"afterSnapshots": execution.after.len(),
"canonicalHydration": diagnostic.is_some_and(|value| return value.canonical_inserted),
"coreExtraction": diagnostic.is_some_and(|value| return value.core_extracted),
"decodeReplay": diagnostic.is_some_and(|value| return value.decode_replayed),
"materialized": diagnostic.is_some_and(|value| return value.materialized),
"instructionMaterializations": execution.materializations.len(),
"materializedSnapshots": execution.materialized_snapshots.len(),
"idempotenceReplayClean": idempotence_clean,
});
}
#[tokio::test]
async fn optional_devnet_metaplex_collection_verify_campaign_from_env() {
if std::env::var("KB_DEVNET_METAPLEX_COLLECTION_VERIFY_CAMPAIGN_TEST")
.ok()
.as_deref()
!= std::option::Option::Some("1")
{
return;
}
if std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref()
!= std::option::Option::Some("1")
{
panic!("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 is required for the spending campaign");
}
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
};
let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") {
std::result::Result::Ok(value) => {
let path = std::path::PathBuf::from(value);
if path.is_absolute() { path } else { workspace_root.join(path) }
},
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
};
let config = kb_config::read_config_json_file_with_environment(
config_path.as_path(),
workspace_root,
)
.unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}"));
let profile_name = std::env::var("KB_DEVNET_PROFILE").ok();
let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref())
.unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}"));
let database_url = std::env::var("KB_POSTGRES_TEST_URL")
.unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}"));
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
let pool = kb_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}"));
let store_options = kb_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
)
.unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}"));
let store = kb_store::PostgresStore::connect(store_options)
.await
.unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}"));
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("PostgreSQL schema initialization failed: {error}");
}
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
workspace_root.join(configured_wallet_dir)
};
let options = crate::MetaplexCreateFixturePreparationOptions::new(wallet_dir);
let summary = crate::execute_devnet_metaplex_collection_verify_campaign(
&pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| {
panic!("Metaplex collection Verify -> Unverify Devnet campaign failed: {error}")
});
let verify_confirmation = summary
.verify
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("Verify confirmation missing"));
let unverify_confirmation = summary
.unverify
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("Unverify confirmation missing"));
println!(
"METAPLEX_COLLECTION_VERIFY_FIXTURE parent_mint={} parent_metadata={} parent_master_edition={} member_mint={} member_metadata={} member_master_edition={}",
summary.parent.fixture.mint,
summary.parent.fixture.metadata,
summary.parent.fixture.master_edition,
summary.member.fixture.mint,
summary.member.fixture.metadata,
summary.member.fixture.master_edition,
);
println!(
"METAPLEX_COLLECTION_VERIFY_STEP operation={} signature={} slot={:?} materializations={}",
summary.verify.plan.operation_code,
verify_confirmation.signature.0,
verify_confirmation.slot,
summary.verify.materializations.len(),
);
println!(
"METAPLEX_COLLECTION_VERIFY_STEP operation={} signature={} slot={:?} materializations={}",
summary.unverify.plan.operation_code,
unverify_confirmation.signature.0,
unverify_confirmation.slot,
summary.unverify.materializations.len(),
);
println!(
"METAPLEX_COLLECTION_VERIFY_STATE verified_before={} verified_after_verify={} verified_after_unverify={} size_before={} size_after_verify={} size_after_unverify={}",
summary.state.verified_before,
summary.state.verified_after_verify,
summary.state.verified_after_unverify,
summary.state.collection_size_before,
summary.state.collection_size_after_verify,
summary.state.collection_size_after_unverify,
);
println!(
"METAPLEX_COLLECTION_VERIFY_EVIDENCE verify={} unverify={}",
execution_evidence_json(&summary.verify),
execution_evidence_json(&summary.unverify),
);
}
}

View File

@@ -0,0 +1,922 @@
// file: kb-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/create_mint_campaign.rs
// version: 9
//! Confirmed Devnet `Create -> Mint` campaign for one Metaplex asset family.
/// Confirmed SPL state observed before and after the Metaplex `Mint` operation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DevnetMetaplexCreateMintTokenState {
/// Raw amount minted by the campaign.
pub expected_amount_raw: u64,
/// SPL mint supply observed after `Create` and before `Mint`.
pub supply_before_mint: u64,
/// Operator token-account amount observed after `Create` and before `Mint`.
pub token_amount_before_mint: u64,
/// Raw classic SPL token-account state observed before `Mint`.
pub token_account_state_before_mint: u8,
/// SPL mint supply observed at or after the confirmed `Mint` slot.
pub supply_after_mint: u64,
/// Operator token-account amount observed at or after the confirmed `Mint` slot.
pub token_amount_after_mint: u64,
/// Raw classic SPL token-account state observed after `Mint`.
pub token_account_state_after_mint: u8,
}
/// Complete evidence produced by one family-specific Metaplex `Create -> Mint` campaign.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexCreateMintCampaignSummary {
/// Asset family exercised by this campaign.
pub asset_family: crate::MetaplexTokenMetadataAssetFamily,
/// Fresh classic SPL mint and ATA fixture.
pub fixture: crate::MetaplexCreateFixturePreparationSummary,
/// Confirmed `Create` execution evidence.
pub create: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Confirmed `Mint` execution evidence.
pub mint: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Exact classic SPL supply and token-account transition caused by `Mint`.
pub token_state: crate::DevnetMetaplexCreateMintTokenState,
}
/// Returns the exact ordered operation names exercised by this campaign.
pub fn metaplex_create_mint_campaign_operation_names() -> &'static [&'static str; 2] {
return &["create", "mint"];
}
/// Executes one fresh family-specific `Create -> Mint` campaign on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_create_mint_campaign<S, O>(
http_pool: &kb_onchain_transport::HttpEndpointPool,
store: &S,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> kb_core::Result<crate::DevnetMetaplexCreateMintCampaignSummary>
where
S: kb_store::RawTransactionStore
+ kb_store::CoreExtractionStore
+ kb_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if !profile.wallet.devnet_send_enabled {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex Create -> Mint Devnet campaign requires devnet_send_enabled=true",
));
}
let fixture =
match crate::prepare_metaplex_create_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 decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(kb_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> = std::vec![
std::sync::Arc::new(kb_lib::MtAdminMaterializer),
std::sync::Arc::new(kb_lib::MtComplianceAuditMaterializer),
std::sync::Arc::new(kb_lib::MtLifecycleMaterializer),
std::sync::Arc::new(kb_lib::MtMetadataMetaplexTokenMetadataMaterializer),
std::sync::Arc::new(kb_lib::MtRiskMaterializer),
];
let mut create_request =
match crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
format!("metaplex-create-mint-create-{}", uuid::Uuid::new_v4()),
fixture.operation_json.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
create_request.query_role = options.query_role.clone();
create_request.transaction_role = options.transaction_role.clone();
create_request.submit = true;
create_request.operator_confirmed = true;
create_request.materialize_after_confirmation = true;
create_request.post_validation_max_retries = 20;
create_request.postcondition_reads = postcondition_reads(
&fixture,
options.query_role.as_str(),
std::option::Option::None,
false,
);
let create = match crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&create_request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let create_slot = match validate_confirmed_execution("create", &create) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let create_signature = match create.confirmation.as_ref() {
std::option::Option::Some(value) => value.signature.0.as_str(),
std::option::Option::None => "<missing-signature>",
};
if let std::result::Result::Err(error) =
validate_expected_stateful_accounts(&fixture, &create.after, false)
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_post_create_stateful_invalid",
format!(
"confirmed Metaplex Create {create_signature} at slot {create_slot} left invalid expected stateful accounts: {error}"
),
));
}
let mint_before = match crate::read_mint_account_at_or_after(
http_pool,
options.query_role.as_str(),
fixture.mint.as_str(),
std::option::Option::Some(create_slot),
)
.await
{
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_mint_missing_before_mint",
format!(
"classic SPL mint is unavailable after confirmed Metaplex Create {create_signature} at slot {create_slot}"
),
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let expected_mint_authority = expected_mint_authority_after_create(&fixture);
if let std::result::Result::Err(error) = crate::validate_classic_fixture_mint_state(
&mint_before,
expected_mint_authority,
fixture.mint_decimals,
0,
) {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_post_create_spl_state_invalid",
format!(
"confirmed Metaplex Create {} at slot {} left an invalid classic mint state; expected mint/freeze authority {}; {error}",
create_signature, create_slot, expected_mint_authority
),
));
}
let token_before = match crate::read_token_account_at_or_after(
http_pool,
options.query_role.as_str(),
fixture.token_account.as_str(),
std::option::Option::Some(create_slot),
)
.await
{
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_token_missing_before_mint",
format!(
"classic SPL token account is unavailable after confirmed Metaplex Create {create_signature} at slot {create_slot}"
),
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = crate::validate_classic_fixture_token_account_state(
&token_before,
fixture.mint.as_str(),
fixture.authority.as_str(),
0,
spl_token_interface::state::AccountState::Initialized,
) {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_post_create_token_state_invalid",
format!(
"confirmed Metaplex Create {create_signature} at slot {create_slot} left an invalid classic token-account state: {error}"
),
));
}
let supply_before_mint = match crate::classic_fixture_mint_supply(&mint_before) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let token_amount_before_mint = match crate::classic_fixture_token_amount(&token_before) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let token_account_state_before_mint =
match crate::classic_fixture_token_account_state(&token_before) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut mint_request =
match crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
format!("metaplex-create-mint-mint-{}", uuid::Uuid::new_v4()),
fixture.mint_operation_json.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
mint_request.query_role = options.query_role.clone();
mint_request.transaction_role = options.transaction_role.clone();
mint_request.submit = true;
mint_request.operator_confirmed = true;
mint_request.materialize_after_confirmation = true;
mint_request.post_validation_max_retries = 20;
mint_request.preflight_reads = postcondition_reads(
&fixture,
options.query_role.as_str(),
std::option::Option::Some(create_slot),
false,
);
mint_request.postcondition_reads = postcondition_reads(
&fixture,
options.query_role.as_str(),
std::option::Option::Some(create_slot),
true,
);
let mint = match crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&mint_request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint_slot = match validate_confirmed_execution("mint", &mint) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint_signature = match mint.confirmation.as_ref() {
std::option::Option::Some(value) => value.signature.0.as_str(),
std::option::Option::None => "<missing-signature>",
};
if let std::result::Result::Err(error) =
validate_expected_stateful_accounts(&fixture, &mint.after, true)
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_post_mint_stateful_invalid",
format!(
"confirmed Metaplex Mint {mint_signature} at slot {mint_slot} left invalid expected stateful accounts: {error}"
),
));
}
let mint_after = match crate::read_mint_account_at_or_after(
http_pool,
options.query_role.as_str(),
fixture.mint.as_str(),
std::option::Option::Some(mint_slot),
)
.await
{
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_mint_missing_after_mint",
format!(
"classic SPL mint is unavailable after confirmed Metaplex Mint {mint_signature} at slot {mint_slot}"
),
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = crate::validate_classic_fixture_mint_state(
&mint_after,
expected_mint_authority,
fixture.mint_decimals,
fixture.mint_amount_raw,
) {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_post_mint_spl_state_invalid",
format!(
"confirmed Metaplex Mint {} at slot {} left an invalid classic mint state; expected mint/freeze authority {}; {error}",
mint_signature, mint_slot, expected_mint_authority
),
));
}
let token_after = match crate::read_token_account_at_or_after(
http_pool,
options.query_role.as_str(),
fixture.token_account.as_str(),
std::option::Option::Some(mint_slot),
)
.await
{
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_token_missing_after_mint",
format!(
"classic SPL token account is unavailable after confirmed Metaplex Mint {mint_signature} at slot {mint_slot}"
),
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let expected_token_account_state_after_mint =
expected_token_account_state_after_mint(options.asset_family);
if let std::result::Result::Err(error) = crate::validate_classic_fixture_token_account_state(
&token_after,
fixture.mint.as_str(),
fixture.authority.as_str(),
fixture.mint_amount_raw,
expected_token_account_state_after_mint,
) {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_post_mint_token_state_invalid",
format!(
"confirmed Metaplex Mint {mint_signature} at slot {mint_slot} left an invalid classic token-account state: {error}"
),
));
}
let supply_after_mint = match crate::classic_fixture_mint_supply(&mint_after) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let token_amount_after_mint = match crate::classic_fixture_token_amount(&token_after) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let token_account_state_after_mint =
match crate::classic_fixture_token_account_state(&token_after) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::DevnetMetaplexCreateMintCampaignSummary {
asset_family: options.asset_family,
token_state: crate::DevnetMetaplexCreateMintTokenState {
expected_amount_raw: fixture.mint_amount_raw,
supply_before_mint,
token_amount_before_mint,
token_account_state_before_mint,
supply_after_mint,
token_amount_after_mint,
token_account_state_after_mint,
},
fixture,
create,
mint,
});
}
fn expected_mint_authority_after_create(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
) -> &str {
if fixture.master_edition_requested {
return fixture.master_edition.as_str();
}
return fixture.authority.as_str();
}
fn expected_token_account_state_after_mint(
asset_family: crate::MetaplexTokenMetadataAssetFamily,
) -> spl_token_interface::state::AccountState {
if asset_family == crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft {
return spl_token_interface::state::AccountState::Frozen;
}
return spl_token_interface::state::AccountState::Initialized;
}
fn postcondition_reads(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
query_role: &str,
min_context_slot: std::option::Option<u64>,
include_token_record: bool,
) -> std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
let mint = kb_lib::MdPubkey(fixture.mint.clone());
let mut reads = vec![kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: kb_lib::MdPubkey(fixture.metadata.clone()),
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot,
max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
}];
if fixture.master_edition_requested {
reads.push(kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: kb_lib::MdPubkey(fixture.master_edition.clone()),
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::Edition { mint: mint.clone() },
min_context_slot,
max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
});
}
if include_token_record {
if let std::option::Option::Some(token_record) = fixture.token_record.as_ref() {
reads.push(kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: kb_lib::MdPubkey(token_record.clone()),
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::TokenRecord {
mint,
token: kb_lib::MdPubkey(fixture.token_account.clone()),
},
min_context_slot,
max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
});
}
}
return reads;
}
fn validate_confirmed_execution(
step: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> kb_core::Result<u64> {
if !execution.simulation.success {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_simulation_failed",
format!("Metaplex {step} simulation failed"),
));
}
let confirmation = match execution.confirmation.as_ref() {
std::option::Option::Some(value)
if matches!(
value.status,
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
) =>
{
value
},
std::option::Option::Some(value) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_confirmation_incomplete",
format!("Metaplex {step} stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_confirmation_missing",
format!("Metaplex {step} has no confirmation evidence"),
));
},
};
let diagnostic = match execution.post_execution.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_post_execution_missing",
format!("Metaplex {step} has no post-execution diagnostic"),
));
},
};
if !diagnostic.canonical_inserted
|| !diagnostic.core_extracted
|| !diagnostic.decode_replayed
|| !diagnostic.materialized
|| execution.materializations.is_empty()
|| execution.materialized_snapshots.is_empty()
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_post_execution_incomplete",
format!(
"Metaplex {step} post-execution incomplete: signature={}, slot={:?}, canonical_inserted={}, core_extracted={}, decode_replayed={}, materialized={}, materialization_rows={}, materialized_snapshots={}, diagnostics={:?}",
confirmation.signature.0,
confirmation.slot,
diagnostic.canonical_inserted,
diagnostic.core_extracted,
diagnostic.decode_replayed,
diagnostic.materialized,
execution.materializations.len(),
execution.materialized_snapshots.len(),
diagnostic.diagnostics
),
));
}
let second_replay = match execution.idempotence_replay.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_idempotence_missing",
format!("Metaplex {step} has no idempotence replay evidence"),
));
},
};
if second_replay.failed_inputs != 0
|| second_replay.processing_error_inputs != 0
|| second_replay.processors.iter().any(|processor| {
return processor.failed != 0
|| processor.processing_errors != 0
|| processor.materialized_outputs != 0
|| processor.materialization_refused != 0;
})
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_idempotence_failed",
format!("Metaplex {step} second replay is not idempotent"),
));
}
return match confirmation.slot {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_confirmation_slot_missing",
format!("Metaplex {step} confirmation has no slot"),
)),
};
}
fn validate_expected_stateful_accounts(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
snapshots: &[kb_pipeline::MetaplexTokenMetadataStatefulReadResult],
include_token_record: bool,
) -> kb_core::Result<()> {
let metadata = snapshots.iter().any(|value| {
return value.snapshot.account.0.as_str() == fixture.metadata.as_str()
&& value.snapshot.account_kind == "metadata"
&& value
.snapshot
.mint
.as_ref()
.is_some_and(|mint| return mint.0.as_str() == fixture.mint.as_str());
});
let edition = snapshots.iter().any(|value| {
return value.snapshot.account.0.as_str() == fixture.master_edition.as_str()
&& value.snapshot.account_kind == "edition"
&& value
.snapshot
.mint
.as_ref()
.is_some_and(|mint| return mint.0.as_str() == fixture.mint.as_str());
});
let token_record = fixture.token_record.as_ref().is_some_and(|expected| {
return snapshots.iter().any(|value| {
return value.snapshot.account.0.as_str() == expected.as_str()
&& value.snapshot.account_kind == "token_record"
&& value
.snapshot
.mint
.as_ref()
.is_some_and(|mint| return mint.0.as_str() == fixture.mint.as_str());
});
});
if !metadata
|| fixture.master_edition_requested != edition
|| (include_token_record && fixture.token_record.is_some() && !token_record)
|| (!include_token_record && token_record)
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_create_mint_stateful_postcondition_failed",
format!(
"Metaplex stateful accounts mismatch: metadata={metadata}, edition={edition}, token_record={token_record}, master_edition_requested={}, token_record_expected={}",
fixture.master_edition_requested,
include_token_record && fixture.token_record.is_some()
),
));
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
fn fixture(
family: crate::MetaplexTokenMetadataAssetFamily,
master_edition_requested: bool,
token_record: std::option::Option<&str>,
) -> crate::MetaplexCreateFixturePreparationSummary {
return crate::MetaplexCreateFixturePreparationSummary {
mint_keypair_path: std::path::PathBuf::from("mint.json"),
asset_family: family,
mint: "11111111111111111111111111111111".to_string(),
mint_decimals: if family == crate::MetaplexTokenMetadataAssetFamily::Fungible {
9
} else {
0
},
metadata: "SysvarC1ock11111111111111111111111111111111".to_string(),
master_edition: "SysvarRent111111111111111111111111111111111".to_string(),
master_edition_requested,
token_account: "SysvarS1otHashes111111111111111111111111111".to_string(),
token_record: token_record.map(|value| return value.to_string()),
collection_mint: std::option::Option::None,
authority: "11111111111111111111111111111111".to_string(),
mint_amount_raw: if family == crate::MetaplexTokenMetadataAssetFamily::Fungible {
1_000_000_000
} else {
1
},
operation_json: "{}".to_string(),
mint_operation_json: "{}".to_string(),
mint_created: true,
preparation_signature: std::option::Option::Some("signature".to_string()),
};
}
#[test]
fn campaign_contract_is_exactly_create_then_mint() {
assert_eq!(crate::metaplex_create_mint_campaign_operation_names(), &["create", "mint"],);
}
#[test]
fn stateful_read_inventory_tracks_master_edition_and_pnft_token_record() {
let nft =
fixture(crate::MetaplexTokenMetadataAssetFamily::Nft, true, std::option::Option::None);
let nft_create =
super::postcondition_reads(&nft, "rpc", std::option::Option::Some(7), false);
assert_eq!(nft_create.len(), 2);
assert!(
nft_create
.iter()
.all(|value| return value.min_context_slot == std::option::Option::Some(7))
);
assert!(nft_create.iter().all(|value| {
return value.max_data_bytes == kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES;
}));
let pnft = fixture(
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
true,
std::option::Option::Some("Stake11111111111111111111111111111111111111"),
);
let pnft_mint =
super::postcondition_reads(&pnft, "rpc", std::option::Option::Some(9), true);
assert_eq!(pnft_mint.len(), 3);
assert!(matches!(
&pnft_mint[2].kind,
kb_pipeline::MetaplexTokenMetadataAccountKind::TokenRecord { .. }
));
}
#[test]
fn campaign_supports_all_five_fixture_families() {
for family in [
crate::MetaplexTokenMetadataAssetFamily::Nft,
crate::MetaplexTokenMetadataAssetFamily::Sft,
crate::MetaplexTokenMetadataAssetFamily::Fungible,
crate::MetaplexTokenMetadataAssetFamily::Collection,
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
] {
let options = crate::MetaplexCreateFixturePreparationOptions::new(
std::path::PathBuf::from("wallets"),
)
.with_asset_family(family);
assert_eq!(options.asset_family, family);
}
}
fn family_from_env(value: &str) -> crate::MetaplexTokenMetadataAssetFamily {
return match value {
"nft" => crate::MetaplexTokenMetadataAssetFamily::Nft,
"sft" => crate::MetaplexTokenMetadataAssetFamily::Sft,
"fungible" => crate::MetaplexTokenMetadataAssetFamily::Fungible,
"collection" => crate::MetaplexTokenMetadataAssetFamily::Collection,
"programmable_nft" | "pnft" => crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
_ => panic!("unsupported KB_DEVNET_METAPLEX_CREATE_MINT_FAMILY `{value}`"),
};
}
fn execution_evidence_json(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> serde_json::Value {
let confirmation = execution.confirmation.as_ref();
let confirmation_status = confirmation
.map(|value| return format!("{:?}", value.status))
.unwrap_or_else(|| return "missing".to_string());
let confirmation_slot = confirmation.and_then(|value| return value.slot);
let signature = confirmation
.map(|value| return value.signature.0.clone())
.unwrap_or_else(|| return "missing".to_string());
let diagnostic = execution.post_execution.as_ref();
let idempotence_clean = execution.idempotence_replay.as_ref().is_some_and(|replay| {
return replay.failed_inputs == 0
&& replay.processing_error_inputs == 0
&& replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
});
return serde_json::json!({
"operation": execution.plan.operation_code.as_str(),
"cluster": execution.cluster,
"genesisHash": execution.genesis_hash.as_str(),
"simulationContextSlot": execution.simulation_context_slot,
"simulationSuccess": execution.simulation.success,
"simulationLogCount": execution.simulation.logs.len(),
"messageHash": execution.readiness.message_hash.as_str(),
"feeLamports": execution.fee.fee_lamports,
"signature": signature,
"confirmationStatus": confirmation_status,
"confirmationSlot": confirmation_slot,
"beforeSnapshots": execution.before.len(),
"afterSnapshots": execution.after.len(),
"canonicalHydration": diagnostic.is_some_and(|value| return value.canonical_inserted),
"coreExtraction": diagnostic.is_some_and(|value| return value.core_extracted),
"decodeReplay": diagnostic.is_some_and(|value| return value.decode_replayed),
"materialized": diagnostic.is_some_and(|value| return value.materialized),
"instructionMaterializations": execution.materializations.len(),
"materializedSnapshots": execution.materialized_snapshots.len(),
"idempotenceReplayClean": idempotence_clean,
});
}
#[test]
fn post_create_mint_authority_tracks_master_edition_only_for_nft_families() {
let authority = solana_pubkey::Pubkey::new_unique().to_string();
let master_edition = solana_pubkey::Pubkey::new_unique().to_string();
let families = [
(crate::MetaplexTokenMetadataAssetFamily::Nft, true),
(crate::MetaplexTokenMetadataAssetFamily::Sft, false),
(crate::MetaplexTokenMetadataAssetFamily::Fungible, false),
(crate::MetaplexTokenMetadataAssetFamily::Collection, true),
(crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft, true),
];
for (asset_family, master_edition_requested) in families {
let fixture = crate::MetaplexCreateFixturePreparationSummary {
mint_keypair_path: std::path::PathBuf::from("fixture.json"),
asset_family,
mint: solana_pubkey::Pubkey::new_unique().to_string(),
mint_decimals: 0,
metadata: solana_pubkey::Pubkey::new_unique().to_string(),
master_edition: master_edition.clone(),
master_edition_requested,
token_account: solana_pubkey::Pubkey::new_unique().to_string(),
token_record: std::option::Option::None,
collection_mint: std::option::Option::None,
authority: authority.clone(),
mint_amount_raw: 1,
operation_json: "{}".to_string(),
mint_operation_json: "{}".to_string(),
mint_created: true,
preparation_signature: std::option::Option::None,
};
assert_eq!(
super::expected_mint_authority_after_create(&fixture),
if master_edition_requested {
master_edition.as_str()
} else {
authority.as_str()
}
);
}
}
#[test]
fn post_mint_token_account_state_is_frozen_only_for_programmable_nft() {
let families = [
(
crate::MetaplexTokenMetadataAssetFamily::Nft,
spl_token_interface::state::AccountState::Initialized,
),
(
crate::MetaplexTokenMetadataAssetFamily::Sft,
spl_token_interface::state::AccountState::Initialized,
),
(
crate::MetaplexTokenMetadataAssetFamily::Fungible,
spl_token_interface::state::AccountState::Initialized,
),
(
crate::MetaplexTokenMetadataAssetFamily::Collection,
spl_token_interface::state::AccountState::Initialized,
),
(
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
spl_token_interface::state::AccountState::Frozen,
),
];
for (asset_family, expected_state) in families {
assert_eq!(
super::expected_token_account_state_after_mint(asset_family),
expected_state
);
}
}
#[tokio::test]
async fn optional_devnet_metaplex_create_mint_campaign_from_env() {
if std::env::var("KB_DEVNET_METAPLEX_CREATE_MINT_CAMPAIGN_TEST").ok().as_deref()
!= std::option::Option::Some("1")
{
return;
}
if std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref()
!= std::option::Option::Some("1")
{
panic!("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 is required for the spending campaign");
}
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
};
let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") {
std::result::Result::Ok(value) => {
let path = std::path::PathBuf::from(value);
if path.is_absolute() { path } else { workspace_root.join(path) }
},
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
};
let config = kb_config::read_config_json_file_with_environment(
config_path.as_path(),
workspace_root,
)
.unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}"));
let profile_name = std::env::var("KB_DEVNET_PROFILE").ok();
let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref())
.unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}"));
let database_url = std::env::var("KB_POSTGRES_TEST_URL")
.unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}"));
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
let pool = kb_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}"));
let store_options = kb_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
)
.unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}"));
let store = kb_store::PostgresStore::connect(store_options)
.await
.unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}"));
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("PostgreSQL schema initialization failed: {error}");
}
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
workspace_root.join(configured_wallet_dir)
};
let family = std::env::var("KB_DEVNET_METAPLEX_CREATE_MINT_FAMILY")
.map(|value| return family_from_env(value.as_str()))
.unwrap_or(crate::MetaplexTokenMetadataAssetFamily::Nft);
let options = crate::MetaplexCreateFixturePreparationOptions::new(wallet_dir)
.with_asset_family(family);
let summary = crate::execute_devnet_metaplex_create_mint_campaign(
&pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| panic!("Metaplex Create -> Mint Devnet campaign failed: {error}"));
let create_confirmation = summary
.create
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("Create confirmation missing"));
let mint_confirmation = summary
.mint
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("Mint confirmation missing"));
println!(
"METAPLEX_CREATE_MINT_FIXTURE family={:?} mint={} metadata={} master_edition={} token_account={} token_record={} preparation_signature={}",
summary.asset_family,
summary.fixture.mint,
summary.fixture.metadata,
summary.fixture.master_edition,
summary.fixture.token_account,
summary.fixture.token_record.as_deref().unwrap_or("none"),
summary.fixture.preparation_signature.as_deref().unwrap_or("missing"),
);
println!(
"METAPLEX_CREATE_MINT_STEP operation={} signature={} slot={:?} materializations={}",
summary.create.plan.operation_code,
create_confirmation.signature.0,
create_confirmation.slot,
summary.create.materializations.len(),
);
println!(
"METAPLEX_CREATE_MINT_STEP operation={} signature={} slot={:?} materializations={} supply_before={} supply_after={} token_before={} token_after={} token_state_before={} token_state_after={}",
summary.mint.plan.operation_code,
mint_confirmation.signature.0,
mint_confirmation.slot,
summary.mint.materializations.len(),
summary.token_state.supply_before_mint,
summary.token_state.supply_after_mint,
summary.token_state.token_amount_before_mint,
summary.token_state.token_amount_after_mint,
summary.token_state.token_account_state_before_mint,
summary.token_state.token_account_state_after_mint,
);
println!(
"METAPLEX_CREATE_MINT_EVIDENCE family={:?} create={} mint={} token_state={}",
summary.asset_family,
execution_evidence_json(&summary.create),
execution_evidence_json(&summary.mint),
serde_json::json!({
"expectedAmountRaw": summary.token_state.expected_amount_raw,
"supplyBeforeMint": summary.token_state.supply_before_mint,
"supplyAfterMint": summary.token_state.supply_after_mint,
"tokenAmountBeforeMint": summary.token_state.token_amount_before_mint,
"tokenAmountAfterMint": summary.token_state.token_amount_after_mint,
"tokenAccountStateBeforeMint": summary.token_state.token_account_state_before_mint,
"tokenAccountStateAfterMint": summary.token_state.token_account_state_after_mint,
}),
);
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_devnet_execution.rs
// version: 6
// file: kb-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs
// version: 17
//! Real Devnet simulation and submission for current Metaplex Token Metadata operations.
@@ -14,6 +14,8 @@ pub struct DevnetMetaplexTokenMetadataExecutionRequest {
pub transaction_role: std::string::String,
/// Exact current Metaplex operation.
pub operation: kb_lib::ExMetaplexTokenMetadataOperation,
/// Additional non-profile signers explicitly authorized for this exact operation.
pub additional_authorized_signers: std::vec::Vec<kb_lib::MdPubkey>,
/// Bounded state reads required before execution.
pub preflight_reads: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest>,
/// Bounded state reads repeated after confirmed submission.
@@ -24,6 +26,10 @@ pub struct DevnetMetaplexTokenMetadataExecutionRequest {
pub operator_confirmed: bool,
/// Materializes canonical account snapshots after confirmed submission.
pub materialize_after_confirmation: bool,
/// Number of `getTransaction` retries after the first hydration attempt.
pub post_validation_max_retries: u32,
/// Replaces existing core and decode outputs for the submitted signature.
pub force_post_validation_replay: bool,
}
impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
@@ -37,11 +43,14 @@ impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
query_role: "http_queries".to_string(),
transaction_role: "http_transactions".to_string(),
operation,
additional_authorized_signers: std::vec::Vec::new(),
preflight_reads: std::vec::Vec::new(),
postcondition_reads: std::vec::Vec::new(),
submit: false,
operator_confirmed: false,
materialize_after_confirmation: false,
post_validation_max_retries: 10,
force_post_validation_replay: false,
};
}
@@ -91,6 +100,27 @@ impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
"Devnet Metaplex execution endpoint roles must not be empty",
));
}
if self.additional_authorized_signers.len() > 4 {
return std::result::Result::Err(kb_core::Error::config(
"Devnet Metaplex execution supports at most four additional authorized signers",
));
}
let mut unique_authorized_signers = std::collections::BTreeSet::new();
for signer in &self.additional_authorized_signers {
if let std::result::Result::Err(error) =
kb_onchain_transport::validate_solana_pubkey_text(
signer.0.as_str(),
"Metaplex additional authorized signer",
)
{
return std::result::Result::Err(error);
}
if !unique_authorized_signers.insert(signer.0.as_str()) {
return std::result::Result::Err(kb_core::Error::config(
"Devnet Metaplex additional authorized signers must be unique",
));
}
}
if self.preflight_reads.len() > kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS
|| self.postcondition_reads.len()
> kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS
@@ -99,6 +129,11 @@ impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
"Devnet Metaplex execution stateful read limit exceeded",
));
}
if self.post_validation_max_retries > 20 {
return std::result::Result::Err(kb_core::Error::config(
"post-execution getTransaction retries must not exceed 20",
));
}
if self.submit && self.materialize_after_confirmation && self.postcondition_reads.is_empty()
{
return std::result::Result::Err(kb_core::Error::config(
@@ -299,6 +334,8 @@ pub struct DevnetMetaplexTokenMetadataExecutionSummary {
pub fee: kb_onchain_transport::FeeForMessageResult,
/// Exact real RPC simulation result.
pub simulation: kb_lib::ExApiExecutionSimulationResult,
/// RPC context slot returned by the exact simulation call.
pub simulation_context_slot: u64,
/// Readiness report proving exact-message simulation and signer resolution.
pub readiness: kb_pipeline::MetaplexTokenMetadataExecutionReadinessReport,
/// Submission result when explicitly authorized.
@@ -311,6 +348,18 @@ pub struct DevnetMetaplexTokenMetadataExecutionSummary {
pub materialization_requested: bool,
/// Canonical projections emitted from confirmed account snapshots.
pub materialized_snapshots: std::vec::Vec<serde_json::Value>,
/// Canonical hydration result for the exact submitted signature.
pub backfill: std::option::Option<kb_pipeline::BackfillSummary>,
/// Core extraction result for the exact submitted signature.
pub core_extraction: std::option::Option<kb_pipeline::CoreExtractionSummary>,
/// First Metaplex decode and materialization replay.
pub decode_replay: std::option::Option<kb_pipeline::DecodeReplaySummary>,
/// Second replay proving idempotence for the exact same input.
pub idempotence_replay: std::option::Option<kb_pipeline::DecodeReplaySummary>,
/// Exact materialized rows produced for the submitted Metaplex transaction.
pub materializations: std::vec::Vec<kb_store::MaterializedEventQueryRow>,
/// Aggregated canonical post-execution diagnostic.
pub post_execution: std::option::Option<kb_lib::ExApiPostExecutionDiagnostic>,
}
struct PreparedMetaplexExecution {
@@ -337,7 +386,7 @@ where
));
}
let prepared =
match prepare_execution(http_pool, profile, workspace_root, request, observer).await {
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),
};
@@ -345,21 +394,71 @@ where
}
/// Executes one real Devnet Metaplex simulation or explicitly authorized submission.
pub async fn execute_devnet_metaplex_token_metadata<O>(
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_token_metadata<S, O>(
http_pool: &kb_onchain_transport::HttpEndpointPool,
store: &S,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
decoders: &[std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
materializers: &[std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>],
observer: &O,
) -> kb_core::Result<crate::DevnetMetaplexTokenMetadataExecutionSummary>
where
S: kb_store::RawTransactionStore
+ kb_store::CoreExtractionStore
+ kb_store::DecodePipelineStore
+ Sync,
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),
};
return crate::execute_devnet_metaplex_token_metadata_with_signers(
http_pool,
store,
profile,
workspace_root,
request,
decoders,
materializers,
&[],
observer,
)
.await;
}
/// Executes one real Devnet Metaplex submission with explicitly authorized extra signers.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_token_metadata_with_signers<S, O>(
http_pool: &kb_onchain_transport::HttpEndpointPool,
store: &S,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
decoders: &[std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>],
materializers: &[std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>],
additional_signers: &[&(dyn solana_signer::Signer + std::marker::Sync)],
observer: &O,
) -> kb_core::Result<crate::DevnetMetaplexTokenMetadataExecutionSummary>
where
S: kb_store::RawTransactionStore
+ kb_store::CoreExtractionStore
+ kb_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
let prepared = match prepare_execution(
http_pool,
profile,
workspace_root,
request,
additional_signers,
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);
}
@@ -369,12 +468,6 @@ where
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)
{
@@ -387,10 +480,12 @@ where
crate::violation_message(send_evaluation.violations.as_slice()),
));
}
let signed = match prepared
.unsigned
.sign_after_simulation(&prepared.evidence, &[prepared.wallet.as_signer()])
{
let signed = match sign_prepared_transaction(
prepared.unsigned,
&prepared.evidence,
prepared.wallet.as_sync_signer(),
additional_signers,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -399,6 +494,14 @@ where
}
let signature = signed.primary_signature().clone();
let mut summary = prepared.summary;
let mut diagnostic = kb_lib::ExApiPostExecutionDiagnostic {
signature: signature.clone(),
canonical_inserted: false,
core_extracted: false,
decode_replayed: false,
materialized: false,
diagnostics: std::vec::Vec::new(),
};
let send_config = match kb_onchain_transport::SendTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(summary.latest_blockhash.context.slot),
@@ -416,7 +519,11 @@ where
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Metaplex submission failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
summary.send_result =
std::option::Option::Some(sent.to_execution_result(kb_lib::ExApiExecutionCluster::Devnet));
@@ -440,32 +547,267 @@ where
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Metaplex confirmation failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
let confirmed = matches!(
confirmation.status,
let confirmation_status = confirmation.status;
summary.confirmation = std::option::Option::Some(confirmation);
if !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),
};
if request.materialize_after_confirmation {
summary.materialized_snapshots =
materialize_confirmed_snapshots(summary.after.as_slice());
}
) {
diagnostic.diagnostics.push(format!(
"Metaplex post-validation stopped at confirmation status {confirmation_status:?}"
));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
let postcondition_reads = bind_postcondition_min_context_slot(
request.postcondition_reads.as_slice(),
summary.confirmation.as_ref().and_then(|value| return value.slot),
);
summary.after = match read_snapshots(http_pool, postcondition_reads.as_slice()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic
.diagnostics
.push(format!("Metaplex stateful postcondition read failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
if request.materialize_after_confirmation {
summary.materialized_snapshots = materialize_confirmed_snapshots(summary.after.as_slice());
}
let backfill = match crate::hydrate_signature(
http_pool,
store,
profile,
request.query_role.as_str(),
request.post_validation_max_retries,
observer,
&signature,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Metaplex hydration failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
diagnostic.canonical_inserted = crate::canonical_available(&backfill);
summary.backfill = std::option::Option::Some(backfill);
if !diagnostic.canonical_inserted {
diagnostic.diagnostics.push(
"confirmed Metaplex transaction was unavailable for canonical hydration".to_string(),
);
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
let extraction = match kb_pipeline::execute_core_extraction(
store,
&kb_pipeline::CoreExtractionRequest {
source: kb_pipeline::CoreExtractionSource::Signatures(std::vec![signature.0.clone()]),
limit: 1,
max_concurrent_extractions: 1,
force_replay: request.force_post_validation_replay,
},
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Metaplex core extraction failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
diagnostic.core_extracted = extraction.failed == 0
&& !extraction.cancelled
&& extraction.selected == 1
&& extraction.extracted.saturating_add(extraction.skipped) >= 1;
summary.core_extraction = std::option::Option::Some(extraction);
if !diagnostic.core_extracted {
diagnostic
.diagnostics
.push("Metaplex core extraction did not complete".to_string());
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
let first_replay = match crate::replay_program(
store,
&signature,
false,
request.force_post_validation_replay,
&[kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID],
decoders,
materializers,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Metaplex decode replay failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
diagnostic.decode_replayed = crate::decode_completed(&first_replay);
if !diagnostic.decode_replayed {
diagnostic
.diagnostics
.push(replay_summary_diagnostic("first Metaplex replay", &first_replay));
}
summary.decode_replay = std::option::Option::Some(first_replay);
let filter = match kb_store::MaterializedEventFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(signature.0.clone()),
64,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rows = match kb_store::DecodePipelineStore::list_materialized_events(store, &filter).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic
.diagnostics
.push(format!("Metaplex materialization query failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
summary.materializations = metaplex_instruction_materializations(rows);
let metaplex_materializer_name = kb_lib::MtApiEventMaterializer::identity(
&kb_lib::MtMetadataMetaplexTokenMetadataMaterializer,
)
.name;
diagnostic.materialized =
!summary.plan.policy.post_execution_validation.materialization_required
|| !summary.materializations.is_empty();
if summary.plan.policy.post_execution_validation.materialization_required
&& summary.materializations.is_empty()
{
diagnostic.diagnostics.push(format!(
"Metaplex instruction materializer `{metaplex_materializer_name}` produced no queryable row for signature {}",
signature.0
));
}
let second_replay = match crate::replay_program(
store,
&signature,
true,
request.force_post_validation_replay,
&[kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID],
decoders,
materializers,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic
.diagnostics
.push(format!("Metaplex idempotence replay failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
let idempotent = second_replay.failed_inputs == 0
&& second_replay.processing_error_inputs == 0
&& second_replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
summary.idempotence_replay = std::option::Option::Some(second_replay);
if !idempotent {
diagnostic
.diagnostics
.push("second Metaplex replay did not prove a clean idempotent skip".to_string());
} else if diagnostic.canonical_inserted
&& diagnostic.core_extracted
&& diagnostic.decode_replayed
&& diagnostic.materialized
{
diagnostic.diagnostics.push(
"Metaplex completed canonical hydration, core extraction, decode, materialization and idempotence validation"
.to_string(),
);
}
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
fn sign_prepared_transaction(
unsigned: kb_lib::ExSolanaUnsignedTransaction,
evidence: &kb_lib::ExSolanaSimulationEvidence,
wallet: &(dyn solana_signer::Signer + std::marker::Sync),
additional_signers: &[&(dyn solana_signer::Signer + std::marker::Sync)],
) -> kb_core::Result<kb_lib::ExSolanaSignedTransaction> {
let mut signers: std::vec::Vec<&dyn solana_signer::Signer> = std::vec![wallet];
for signer in additional_signers {
let signer: &dyn solana_signer::Signer = *signer;
signers.push(signer);
}
return unsigned.sign_after_simulation(evidence, signers.as_slice());
}
fn replay_summary_diagnostic(
label: &str,
summary: &kb_pipeline::DecodeReplaySummary,
) -> std::string::String {
let processors = summary
.processors
.iter()
.map(|processor| {
return format!(
"{}@{} dispatched={} skipped={} decoded={} ignored={} unsupported={} failed={} processing_errors={} materialized_outputs={} materialization_refused={}",
processor.processor_name,
processor.processor_version,
processor.dispatched,
processor.skipped,
processor.decoded,
processor.ignored,
processor.unsupported,
processor.failed,
processor.processing_errors,
processor.materialized_outputs,
processor.materialization_refused,
);
})
.collect::<std::vec::Vec<_>>()
.join("; ");
return format!(
"{label}: selected={} started={} completed={} unmatched={} failed_inputs={} processing_error_inputs={} cancelled={} processors=[{}]",
summary.selected,
summary.started,
summary.completed,
summary.unmatched,
summary.failed_inputs,
summary.processing_error_inputs,
summary.cancelled,
processors,
);
}
async fn prepare_execution<O>(
http_pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
additional_signers: &[&(dyn solana_signer::Signer + std::marker::Sync)],
observer: &O,
) -> kb_core::Result<PreparedMetaplexExecution>
where
@@ -565,12 +907,15 @@ where
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(
let resolved_signers = match validate_available_signers(
unsigned.required_signer_pubkeys(),
wallet_summary.public_key.as_str(),
request.additional_authorized_signers.as_slice(),
additional_signers,
) {
return std::result::Result::Err(error);
}
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(
request.query_role.as_str(),
@@ -636,7 +981,7 @@ where
simulated_message_hash: unsigned.message_hash().to_string(),
simulated: true,
simulation_succeeded: simulation.success,
resolved_signers: vec![fee_payer],
resolved_signers,
submit: request.submit,
operator_confirmed: request.operator_confirmed,
},
@@ -670,16 +1015,36 @@ where
latest_blockhash,
fee,
simulation,
simulation_context_slot: simulation_rpc.context.slot,
readiness,
send_result: std::option::Option::None,
confirmation: std::option::Option::None,
after: std::vec::Vec::new(),
materialization_requested: request.materialize_after_confirmation,
materialized_snapshots: std::vec::Vec::new(),
backfill: std::option::Option::None,
core_extraction: std::option::Option::None,
decode_replay: std::option::Option::None,
idempotence_replay: std::option::Option::None,
materializations: std::vec::Vec::new(),
post_execution: std::option::Option::None,
},
});
}
fn metaplex_instruction_materializations(
rows: std::vec::Vec<kb_store::MaterializedEventQueryRow>,
) -> std::vec::Vec<kb_store::MaterializedEventQueryRow> {
let materializer_name = kb_lib::MtApiEventMaterializer::identity(
&kb_lib::MtMetadataMetaplexTokenMetadataMaterializer,
)
.name;
return rows
.into_iter()
.filter(|row| return row.processor_name == materializer_name)
.collect();
}
fn materialize_confirmed_snapshots(
snapshots: &[kb_pipeline::MetaplexTokenMetadataStatefulReadResult],
) -> std::vec::Vec<serde_json::Value> {
@@ -702,6 +1067,28 @@ fn materialize_confirmed_snapshots(
.collect();
}
fn bind_postcondition_min_context_slot(
requests: &[kb_pipeline::MetaplexTokenMetadataStatefulReadRequest],
confirmation_slot: std::option::Option<u64>,
) -> std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
return requests
.iter()
.cloned()
.map(|mut request| {
request.min_context_slot = match (request.min_context_slot, confirmation_slot) {
(std::option::Option::Some(existing), std::option::Option::Some(confirmed)) => {
std::option::Option::Some(existing.max(confirmed))
},
(std::option::Option::None, std::option::Option::Some(confirmed)) => {
std::option::Option::Some(confirmed)
},
(existing, std::option::Option::None) => existing,
};
return request;
})
.collect();
}
async fn read_snapshots(
http_pool: &kb_onchain_transport::HttpEndpointPool,
requests: &[kb_pipeline::MetaplexTokenMetadataStatefulReadRequest],
@@ -753,7 +1140,15 @@ fn build_plan(
profile.execution.max_compute_unit_price_micro_lamports,
),
},
authorized_signers: vec![fee_payer],
authorized_signers: {
let mut authorized_signers = vec![fee_payer.clone()];
for signer in &request.additional_authorized_signers {
if !authorized_signers.contains(signer) {
authorized_signers.push(signer.clone());
}
}
authorized_signers
},
dry_run: !request.submit,
post_execution_validation: kb_lib::ExApiPostExecutionValidationPolicy {
canonical_insert_required: request.submit,
@@ -806,31 +1201,77 @@ fn validate_profile(
return std::result::Result::Ok(());
}
fn validate_profile_wallet_signers(
fn validate_available_signers(
required_signers: &[std::string::String],
wallet_pubkey: &str,
) -> kb_core::Result<()> {
if required_signers.iter().any(|value| return value.as_str() != wallet_pubkey) {
authorized_additional_signers: &[kb_lib::MdPubkey],
additional_signers: &[&(dyn solana_signer::Signer + std::marker::Sync)],
) -> kb_core::Result<std::vec::Vec<kb_lib::MdPubkey>> {
let authorized = authorized_additional_signers
.iter()
.map(|value| return value.0.as_str())
.collect::<std::collections::BTreeSet<_>>();
let mut available = std::collections::BTreeSet::from([wallet_pubkey.to_string()]);
let mut resolved = std::vec![kb_lib::MdPubkey(wallet_pubkey.to_string())];
for signer in additional_signers {
let pubkey = signer.pubkey().to_string();
if !authorized.contains(pubkey.as_str()) {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_undeclared_signer",
format!(
"Metaplex signer {pubkey} was supplied without explicit request authorization"
),
));
}
if !available.insert(pubkey.clone()) {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_duplicate_signer",
format!("Metaplex signer {pubkey} was supplied more than once"),
));
}
resolved.push(kb_lib::MdPubkey(pubkey));
}
let missing_authorized = authorized_additional_signers
.iter()
.filter(|value| return !available.contains(value.0.as_str()))
.map(|value| return value.0.clone())
.collect::<std::vec::Vec<_>>();
if !missing_authorized.is_empty() {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_external_signer_unavailable",
"execution_metaplex_authorized_signer_unavailable",
format!(
"the Devnet Metaplex orchestrator can sign only with profile wallet {}; required signers are {}",
wallet_pubkey,
required_signers.join(",")
"explicitly authorized Metaplex signers were not supplied: {}",
missing_authorized.join(",")
),
));
}
return std::result::Result::Ok(());
let missing = required_signers
.iter()
.filter(|value| return !available.contains(value.as_str()))
.cloned()
.collect::<std::vec::Vec<_>>();
if !missing.is_empty() {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_external_signer_unavailable",
format!(
"the Devnet Metaplex orchestrator is missing required signers {}; available signers are {}",
missing.join(","),
available.into_iter().collect::<std::vec::Vec<_>>().join(",")
),
));
}
return std::result::Result::Ok(resolved);
}
#[cfg(test)]
mod tests {
fn example_devnet_profile() -> kb_config::ProfileConfig {
let config =
match kb_config::parse_config_json(include_str!("../../config/example.config.json")) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
};
let config = match kb_config::parse_config_json(include_str!(
"../../../../config/example.config.json"
)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
};
return match crate::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(profile) => profile,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),
@@ -866,6 +1307,17 @@ mod tests {
let mut empty = deprecated;
empty.intent_id.clear();
assert!(empty.validate().is_err());
let mut excessive_retries = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
"excessive-retries",
kb_lib::ExMetaplexTokenMetadataOperation::Collect {
authority: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
recipient: kb_lib::MdPubkey(
"SysvarC1ock11111111111111111111111111111111".to_string(),
),
},
);
excessive_retries.post_validation_max_retries = 21;
assert!(excessive_retries.validate().is_err());
}
#[test]
@@ -970,6 +1422,24 @@ mod tests {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"),
};
let database_url = match std::env::var("KB_POSTGRES_TEST_URL") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("KB_POSTGRES_TEST_URL is required for Metaplex execution: {error}")
},
};
let store_options = match kb_store::PostgresStoreOptions::new(database_url, 5, 5_000, false)
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("Postgres options failed: {error}"),
};
let store = match kb_store::PostgresStore::connect(store_options).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("Postgres connection failed: {error}"),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("Postgres schema initialization failed: {error}");
}
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
@@ -993,11 +1463,29 @@ mod tests {
request.operator_confirmed = request.submit
&& std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref()
== std::option::Option::Some("1");
request.materialize_after_confirmation = request.submit
&& std::env::var("KB_DEVNET_METAPLEX_MATERIALIZE_AFTER_CONFIRMATION")
.ok()
.as_deref()
== std::option::Option::Some("1");
request.post_validation_max_retries = 20;
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(kb_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> = std::vec![
std::sync::Arc::new(kb_lib::MtAdminMaterializer),
std::sync::Arc::new(kb_lib::MtComplianceAuditMaterializer),
std::sync::Arc::new(kb_lib::MtLifecycleMaterializer),
std::sync::Arc::new(kb_lib::MtMetadataMetaplexTokenMetadataMaterializer),
std::sync::Arc::new(kb_lib::MtRiskMaterializer),
];
let summary = match crate::execute_devnet_metaplex_token_metadata(
&pool,
&store,
&profile,
workspace_root,
&request,
decoders.as_slice(),
materializers.as_slice(),
&crate::NoopSolanaExecutionObserver,
)
.await
@@ -1009,14 +1497,122 @@ mod tests {
if request.submit {
assert!(summary.send_result.is_some());
assert!(summary.confirmation.is_some());
let diagnostic = summary.post_execution.as_ref();
assert!(diagnostic.is_some());
assert!(diagnostic.is_some_and(|value| return value.canonical_inserted
&& value.core_extracted
&& value.decode_replayed
&& value.materialized));
if request.materialize_after_confirmation {
assert!(!summary.materializations.is_empty());
}
assert!(summary.idempotence_replay.is_some());
}
}
#[test]
fn signer_guard_rejects_external_authorities() {
assert!(super::validate_profile_wallet_signers(&["wallet".to_string()], "wallet").is_ok());
fn postcondition_reads_are_bound_to_the_confirmation_slot() {
let request = kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot: std::option::Option::Some(7),
max_data_bytes: 128,
};
let bound = super::bind_postcondition_min_context_slot(
std::slice::from_ref(&request),
std::option::Option::Some(11),
);
assert_eq!(bound[0].min_context_slot, std::option::Option::Some(11));
let preserved = super::bind_postcondition_min_context_slot(
std::slice::from_ref(&request),
std::option::Option::Some(5),
);
assert_eq!(preserved[0].min_context_slot, std::option::Option::Some(7));
}
#[test]
fn materialization_evidence_is_owned_by_the_metaplex_materializer() {
fn row(processor_name: &str) -> kb_store::MaterializedEventQueryRow {
return kb_store::MaterializedEventQueryRow {
processor_name: processor_name.to_string(),
processor_version: "0.4.8-pre.13".to_string(),
input_key: "input".to_string(),
output_key: "output".to_string(),
source_event_key: "event".to_string(),
source_decoder_name: "metadata.metaplex_token_metadata".to_string(),
source_decoder_version: "0.4.8-pre.13".to_string(),
signature: "signature".to_string(),
slot: 42,
materialized_family: "metadata".to_string(),
payload_json: serde_json::json!({}),
created_at: "created".to_string(),
updated_at: "updated".to_string(),
};
}
let metaplex_name = kb_lib::MtApiEventMaterializer::identity(
&kb_lib::MtMetadataMetaplexTokenMetadataMaterializer,
)
.name;
let rows = super::metaplex_instruction_materializations(vec![
row(metaplex_name.as_str()),
row("materializer.metadata.other"),
]);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].processor_name, metaplex_name);
}
#[test]
fn signer_guard_requires_every_external_signer_explicitly() {
let external = solana_keypair::Keypair::new();
let external_pubkey = solana_signer::Signer::pubkey(&external).to_string();
assert!(
super::validate_profile_wallet_signers(&["external".to_string()], "wallet").is_err()
super::validate_available_signers(&["wallet".to_string()], "wallet", &[], &[]).is_ok()
);
assert!(
super::validate_available_signers(
std::slice::from_ref(&external_pubkey),
"wallet",
&[],
&[],
)
.is_err()
);
let authorized = vec![kb_lib::MdPubkey(external_pubkey.clone())];
let resolved = match super::validate_available_signers(
&["wallet".to_string(), external_pubkey.clone()],
"wallet",
authorized.as_slice(),
&[&external],
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(resolved.len(), 2);
assert!(super::validate_available_signers(
&["wallet".to_string()],
"wallet",
&[],
&[&external],
)
.is_err());
assert!(
super::validate_available_signers(
&["wallet".to_string(), external_pubkey.clone()],
"wallet",
authorized.as_slice(),
&[],
)
.is_err()
);
assert!(
super::validate_available_signers(
&["wallet".to_string(), external_pubkey],
"wallet",
authorized.as_slice(),
&[&external, &external],
)
.is_err()
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,730 @@
// file: kb-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/maintenance_campaign.rs
// version: 3
//! Final bounded Devnet qualification campaign for current Metaplex maintenance operations.
const METAPLEX_FEE_DESTINATION: &str = "2fb1TjRrJQLy9BkYfBjcYgibV7LUsr9cf6QxvyRZyuXn";
const METAPLEX_OWNERLESS_CLOSE_DESTINATION: &str = "GxCXYtrnaU6JXeAza8Ugn4EE6QiFinpfn8t3Lo4UkBDX";
/// Complete evidence produced by the final bounded Metaplex maintenance campaign.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexMaintenanceCampaignSummary {
/// Fresh classic NFT used by the campaign.
pub fixture: crate::DevnetMetaplexCreateMintCampaignSummary,
/// Confirmed `Update` execution evidence.
pub update: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Simulation-only `Resize` evidence for a fresh already-resized NFT.
pub resize_probe: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Simulation-only `Migrate` evidence.
pub migrate_probe: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Simulation-only `Collect` evidence.
pub collect_probe: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Simulation-only `CloseAccounts` evidence.
pub close_accounts_probe: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Primary-sale flag before the current `Update` execution.
pub primary_sale_before_update: bool,
/// Primary-sale flag after the current `Update` execution.
pub primary_sale_after_update: bool,
}
/// Returns the exact ordered operations exercised by the maintenance campaign.
pub fn metaplex_maintenance_campaign_operation_names() -> &'static [&'static str; 7] {
return &["create", "mint", "update", "resize", "migrate", "collect", "close_accounts"];
}
/// Executes the final bounded Metaplex maintenance qualification campaign on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_maintenance_campaign<S, O>(
http_pool: &kb_onchain_transport::HttpEndpointPool,
store: &S,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> kb_core::Result<crate::DevnetMetaplexMaintenanceCampaignSummary>
where
S: kb_store::RawTransactionStore
+ kb_store::CoreExtractionStore
+ kb_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if options.asset_family != crate::MetaplexTokenMetadataAssetFamily::Nft {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex maintenance campaign requires the classic NFT fixture family",
));
}
let fixture = match crate::execute_devnet_metaplex_create_mint_campaign(
http_pool,
store,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let primary_sale_before_update = match metadata_primary_sale(
fixture.mint.after.as_slice(),
fixture.fixture.metadata.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if primary_sale_before_update {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_initial_primary_sale_invalid",
"fresh maintenance NFT already has primary_sale_happened=true",
));
}
let update_operation = match update_operation(&fixture.fixture) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let update = match execute_confirmed_operation(
"update",
update_operation,
metadata_reads(&fixture.fixture, options.query_role.as_str()),
http_pool,
store,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let _update_slot = match validate_confirmed_execution("update", &update) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let primary_sale_after_update =
match metadata_primary_sale(update.after.as_slice(), fixture.fixture.metadata.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !primary_sale_after_update {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_update_state_invalid",
"confirmed Update did not flip primary_sale_happened to true",
));
}
let resize_operation = kb_lib::ExMetaplexTokenMetadataOperation::Resize {
metadata: kb_lib::MdPubkey(fixture.fixture.metadata.clone()),
edition: kb_lib::MdPubkey(fixture.fixture.master_edition.clone()),
mint: kb_lib::MdPubkey(fixture.fixture.mint.clone()),
payer: kb_lib::MdPubkey(fixture.fixture.authority.clone()),
authority: std::option::Option::None,
token: std::option::Option::Some(kb_lib::MdPubkey(fixture.fixture.token_account.clone())),
system_program: kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()),
};
let resize_probe = match simulate_expected_unavailable(
"resize",
resize_operation,
201,
http_pool,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let migrate_probe = match simulate_expected_unavailable(
"migrate",
migrate_operation(&fixture.fixture),
75,
http_pool,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collect_probe = match simulate_expected_unavailable(
"collect",
kb_lib::ExMetaplexTokenMetadataOperation::Collect {
authority: kb_lib::MdPubkey(fixture.fixture.authority.clone()),
recipient: kb_lib::MdPubkey(METAPLEX_FEE_DESTINATION.to_string()),
},
7,
http_pool,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let close_accounts_probe = match simulate_expected_unavailable(
"close_accounts",
kb_lib::ExMetaplexTokenMetadataOperation::CloseAccounts {
metadata: kb_lib::MdPubkey(fixture.fixture.metadata.clone()),
edition: kb_lib::MdPubkey(fixture.fixture.master_edition.clone()),
mint: kb_lib::MdPubkey(fixture.fixture.mint.clone()),
authority: kb_lib::MdPubkey(fixture.fixture.authority.clone()),
destination: kb_lib::MdPubkey(METAPLEX_OWNERLESS_CLOSE_DESTINATION.to_string()),
},
188,
http_pool,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::DevnetMetaplexMaintenanceCampaignSummary {
fixture,
update,
resize_probe,
migrate_probe,
collect_probe,
close_accounts_probe,
primary_sale_before_update,
primary_sale_after_update,
});
}
fn update_operation(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
) -> kb_core::Result<kb_lib::ExMetaplexTokenMetadataOperation> {
let value = serde_json::json!({
"operation": "update_as_update_authority_v2",
"authority": fixture.authority.as_str(),
"mint": fixture.mint.as_str(),
"metadata": fixture.metadata.as_str(),
"edition": fixture.master_edition.as_str(),
"token": fixture.token_account.as_str(),
"authorization_rules_program": null,
"authorization_rules": null,
"update_args": {
"AsUpdateAuthorityV2": {
"new_update_authority": null,
"data": null,
"primary_sale_happened": true,
"is_mutable": null,
"collection": "None",
"collection_details": "None",
"uses": "None",
"rule_set": "None",
"token_standard": null,
"authorization_data": null
}
}
});
return match serde_json::from_value::<kb_lib::ExMetaplexTokenMetadataOperation>(value) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::config(
format!("Metaplex maintenance Update operation is invalid: {error}"),
)),
};
}
fn migrate_operation(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
) -> kb_lib::ExMetaplexTokenMetadataOperation {
return kb_lib::ExMetaplexTokenMetadataOperation::Migrate {
metadata: kb_lib::MdPubkey(fixture.metadata.clone()),
edition: kb_lib::MdPubkey(fixture.master_edition.clone()),
token: kb_lib::MdPubkey(fixture.token_account.clone()),
token_owner: kb_lib::MdPubkey(fixture.authority.clone()),
mint: kb_lib::MdPubkey(fixture.mint.clone()),
payer: kb_lib::MdPubkey(fixture.authority.clone()),
authority: kb_lib::MdPubkey(fixture.authority.clone()),
collection_metadata: kb_lib::MdPubkey(fixture.metadata.clone()),
delegate_record: kb_lib::MdPubkey(fixture.metadata.clone()),
token_record: kb_lib::MdPubkey(fixture.metadata.clone()),
system_program: kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()),
sysvar_instructions: kb_lib::MdPubkey(
kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID.to_string(),
),
spl_token_program: kb_lib::MdPubkey(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
authorization_rules_program: std::option::Option::None,
authorization_rules: std::option::Option::None,
};
}
#[allow(clippy::too_many_arguments)]
async fn execute_confirmed_operation<S, O>(
label: &str,
operation: kb_lib::ExMetaplexTokenMetadataOperation,
reads: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest>,
http_pool: &kb_onchain_transport::HttpEndpointPool,
store: &S,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> kb_core::Result<crate::DevnetMetaplexTokenMetadataExecutionSummary>
where
S: kb_store::RawTransactionStore
+ kb_store::CoreExtractionStore
+ kb_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(kb_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> = std::vec![
std::sync::Arc::new(kb_lib::MtAdminMaterializer),
std::sync::Arc::new(kb_lib::MtComplianceAuditMaterializer),
std::sync::Arc::new(kb_lib::MtLifecycleMaterializer),
std::sync::Arc::new(kb_lib::MtMetadataMetaplexTokenMetadataMaterializer),
std::sync::Arc::new(kb_lib::MtRiskMaterializer),
];
let mut request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-maintenance-{label}-{}", uuid::Uuid::new_v4()),
operation,
);
request.query_role = options.query_role.clone();
request.transaction_role = options.transaction_role.clone();
request.preflight_reads = reads.clone();
request.postcondition_reads = reads;
request.submit = true;
request.operator_confirmed = true;
request.materialize_after_confirmation = true;
request.post_validation_max_retries = 20;
return crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await;
}
#[allow(clippy::too_many_arguments)]
async fn simulate_expected_unavailable<O>(
label: &str,
operation: kb_lib::ExMetaplexTokenMetadataOperation,
expected_custom_error: u64,
http_pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> kb_core::Result<crate::DevnetMetaplexTokenMetadataExecutionSummary>
where
O: crate::SolanaExecutionObserver,
{
let mut request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-maintenance-{label}-probe-{}", uuid::Uuid::new_v4()),
operation,
);
request.query_role = options.query_role.clone();
request.transaction_role = options.transaction_role.clone();
let summary = match crate::simulate_devnet_metaplex_token_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),
};
if summary.simulation.success || summary.send_result.is_some() || summary.confirmation.is_some()
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_unavailable_probe_unexpected_success",
format!("Metaplex {label} probe unexpectedly became submit-capable or confirmed"),
));
}
let observed_custom_error = simulation_custom_error(summary.simulation.error.as_deref());
if observed_custom_error != std::option::Option::Some(expected_custom_error) {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_unavailable_probe_error_mismatch",
format!(
"Metaplex {label} probe expected custom error {expected_custom_error}; got custom_error={observed_custom_error:?}, error={:?}, logs={:?}",
summary.simulation.error, summary.simulation.logs
),
));
}
return std::result::Result::Ok(summary);
}
fn simulation_custom_error(error: std::option::Option<&str>) -> std::option::Option<u64> {
let error = match error {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let value = match serde_json::from_str::<serde_json::Value>(error) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let instruction_error =
match value.get("InstructionError").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) if value.len() == 2 => value,
_ => return std::option::Option::None,
};
let detail = match instruction_error.get(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
return detail.get("Custom").and_then(serde_json::Value::as_u64);
}
fn metadata_reads(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
query_role: &str,
) -> std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
return std::vec![kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: kb_lib::MdPubkey(fixture.metadata.clone()),
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot: std::option::Option::None,
max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
}];
}
fn metadata_primary_sale(
snapshots: &[kb_pipeline::MetaplexTokenMetadataStatefulReadResult],
metadata: &str,
) -> kb_core::Result<bool> {
let snapshot = match snapshots.iter().find(|value| {
return value.snapshot.account.0.as_str() == metadata
&& value.snapshot.account_kind == "metadata";
}) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_metadata_snapshot_missing",
format!("metadata snapshot {metadata} is missing"),
));
},
};
let value = snapshot
.snapshot
.payload_json
.get("primary_sale_happened")
.or_else(|| return snapshot.snapshot.payload_json.get("primarySaleHappened"));
return match value.and_then(serde_json::Value::as_bool) {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_primary_sale_missing",
"metadata snapshot has no boolean primary-sale field",
)),
};
}
fn validate_confirmed_execution(
label: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> kb_core::Result<u64> {
let confirmation = match execution.confirmation.as_ref() {
std::option::Option::Some(value)
if matches!(
value.status,
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
) =>
{
value
},
std::option::Option::Some(value) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_confirmation_incomplete",
format!("Metaplex {label} stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_confirmation_missing",
format!("Metaplex {label} has no confirmation evidence"),
));
},
};
let diagnostic = match execution.post_execution.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_post_execution_missing",
format!("Metaplex {label} has no post-execution diagnostic"),
));
},
};
if !diagnostic.canonical_inserted
|| !diagnostic.core_extracted
|| !diagnostic.decode_replayed
|| !diagnostic.materialized
|| execution.materializations.is_empty()
|| execution.materialized_snapshots.is_empty()
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_post_execution_incomplete",
format!(
"Metaplex {label} post-execution incomplete: signature={}, slot={:?}, canonical_inserted={}, core_extracted={}, decode_replayed={}, materialized={}, materialization_rows={}, materialized_snapshots={}, diagnostics={:?}",
confirmation.signature.0,
confirmation.slot,
diagnostic.canonical_inserted,
diagnostic.core_extracted,
diagnostic.decode_replayed,
diagnostic.materialized,
execution.materializations.len(),
execution.materialized_snapshots.len(),
diagnostic.diagnostics
),
));
}
let idempotence = match execution.idempotence_replay.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_idempotence_missing",
format!("Metaplex {label} has no idempotence replay evidence"),
));
},
};
if idempotence.failed_inputs != 0
|| idempotence.processing_error_inputs != 0
|| idempotence.processors.iter().any(|processor| {
return processor.failed != 0
|| processor.processing_errors != 0
|| processor.materialized_outputs != 0
|| processor.materialization_refused != 0;
})
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_idempotence_failed",
format!("Metaplex {label} second replay is not idempotent"),
));
}
return match confirmation.slot {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(kb_core::Error::new(
"metaplex_maintenance_confirmation_slot_missing",
format!("Metaplex {label} confirmation has no slot"),
)),
};
}
#[cfg(test)]
mod tests {
#[test]
fn maintenance_campaign_contract_is_exact() {
assert_eq!(
crate::metaplex_maintenance_campaign_operation_names(),
&["create", "mint", "update", "resize", "migrate", "collect", "close_accounts"],
);
for custom_error in [201_u64, 75_u64, 7_u64, 188_u64] {
let error = format!(r#"{{"InstructionError":[0,{{"Custom":{custom_error}}}]}}"#);
assert_eq!(
super::simulation_custom_error(std::option::Option::Some(error.as_str())),
std::option::Option::Some(custom_error),
);
}
assert_eq!(
super::simulation_custom_error(std::option::Option::Some(
r#"{"InstructionError":[0,"InvalidInstructionData"]}"#,
)),
std::option::Option::None,
);
}
#[test]
fn maintenance_reserved_destinations_are_exact() {
assert_eq!(super::METAPLEX_FEE_DESTINATION, "2fb1TjRrJQLy9BkYfBjcYgibV7LUsr9cf6QxvyRZyuXn");
assert_eq!(
super::METAPLEX_OWNERLESS_CLOSE_DESTINATION,
"GxCXYtrnaU6JXeAza8Ugn4EE6QiFinpfn8t3Lo4UkBDX",
);
}
#[tokio::test]
async fn optional_devnet_metaplex_maintenance_campaign_from_env() {
if std::env::var("KB_DEVNET_METAPLEX_MAINTENANCE_CAMPAIGN_TEST").ok().as_deref()
!= std::option::Option::Some("1")
{
return;
}
assert_eq!(
std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref(),
std::option::Option::Some("1"),
"set KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 before the maintenance campaign"
);
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
};
let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") {
std::result::Result::Ok(value) => {
let path = std::path::PathBuf::from(value);
if path.is_absolute() { path } else { workspace_root.join(path) }
},
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
};
let config = kb_config::read_config_json_file_with_environment(
config_path.as_path(),
workspace_root,
)
.unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}"));
let profile_name = std::env::var("KB_DEVNET_PROFILE").ok();
let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref())
.unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}"));
let database_url = std::env::var("KB_POSTGRES_TEST_URL")
.unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}"));
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
let http_pool = kb_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool initialization failed: {error}"));
let store_options = kb_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
)
.unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}"));
let store = kb_store::PostgresStore::connect(store_options)
.await
.unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}"));
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("PostgreSQL schema initialization failed: {error}");
}
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
workspace_root.join(configured_wallet_dir)
};
let options = crate::MetaplexCreateFixturePreparationOptions::new(wallet_dir)
.with_asset_family(crate::MetaplexTokenMetadataAssetFamily::Nft);
let summary = crate::execute_devnet_metaplex_maintenance_campaign(
&http_pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| panic!("Metaplex maintenance Devnet campaign failed: {error}"));
println!(
"METAPLEX_MAINTENANCE_FIXTURE mint={} metadata={} master_edition={} token={}",
summary.fixture.fixture.mint,
summary.fixture.fixture.metadata,
summary.fixture.fixture.master_edition,
summary.fixture.fixture.token_account,
);
print_confirmed_step("update", &summary.update);
println!(
"METAPLEX_MAINTENANCE_STATE primary_sale_before={} primary_sale_after={}",
summary.primary_sale_before_update, summary.primary_sale_after_update,
);
print_probe("resize", 201, &summary.resize_probe);
print_probe("migrate", 75, &summary.migrate_probe);
print_probe("collect", 7, &summary.collect_probe);
print_probe("close_accounts", 188, &summary.close_accounts_probe);
println!(
"METAPLEX_MAINTENANCE_EVIDENCE update={}",
execution_evidence_json(&summary.update),
);
}
fn print_confirmed_step(
label: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) {
let confirmation = execution
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("{label} confirmation is missing"));
println!(
"METAPLEX_MAINTENANCE_STEP label={} operation={} signature={} slot={:?} materializations={}",
label,
execution.plan.operation_code,
confirmation.signature.0,
confirmation.slot,
execution.materializations.len(),
);
}
fn print_probe(
label: &str,
expected_custom_error: u64,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) {
println!(
"METAPLEX_MAINTENANCE_PROBE label={} operation={} success={} context_slot={} expected_custom_error={} error={} logs={}",
label,
execution.plan.operation_code,
execution.simulation.success,
execution.simulation_context_slot,
expected_custom_error,
serde_json::to_string(&execution.simulation.error).unwrap_or_else(|error| panic!(
"{label} probe error serialization failed: {error}"
)),
execution.simulation.logs.len(),
);
println!(
"METAPLEX_MAINTENANCE_PROBE_LOGS label={} {}",
label,
serde_json::to_string(&execution.simulation.logs)
.unwrap_or_else(|error| panic!("{label} probe log serialization failed: {error}")),
);
}
fn execution_evidence_json(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> serde_json::Value {
let confirmation = execution.confirmation.as_ref();
let post_execution = execution.post_execution.as_ref();
let idempotence_clean = execution.idempotence_replay.as_ref().is_some_and(|replay| {
return replay.failed_inputs == 0
&& replay.processing_error_inputs == 0
&& replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
});
return serde_json::json!({
"operation": execution.plan.operation_code.as_str(),
"cluster": execution.cluster,
"genesisHash": execution.genesis_hash.as_str(),
"simulationContextSlot": execution.simulation_context_slot,
"simulationSuccess": execution.simulation.success,
"simulationLogCount": execution.simulation.logs.len(),
"messageHash": execution.readiness.message_hash.as_str(),
"feeLamports": execution.fee.fee_lamports,
"signature": confirmation.map(|value| return value.signature.0.clone()),
"confirmationStatus": confirmation.map(|value| return format!("{:?}", value.status)),
"confirmationSlot": confirmation.and_then(|value| return value.slot),
"beforeSnapshots": execution.before.len(),
"afterSnapshots": execution.after.len(),
"canonicalHydration": post_execution.is_some_and(|value| return value.canonical_inserted),
"coreExtraction": post_execution.is_some_and(|value| return value.core_extracted),
"decodeReplay": post_execution.is_some_and(|value| return value.decode_replayed),
"materialized": post_execution.is_some_and(|value| return value.materialized),
"instructionMaterializations": execution.materializations.len(),
"materializedSnapshots": execution.materialized_snapshots.len(),
"idempotenceReplayClean": idempotence_clean,
});
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_scenarios.rs
// version: 6
// file: kb-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/scenarios.rs
// version: 8
//! Coherent Metaplex Token Metadata scenario journeys for tests and Devnet campaigns.
@@ -192,11 +192,10 @@ pub fn metaplex_token_metadata_synthetic_scenarios()
crate::MetaplexTokenMetadataAssetFamily::Collection,
crate::MetaplexTokenMetadataFixtureKind::CollectionParentAndMember,
crate::MetaplexTokenMetadataFixtureState::CollectionMemberUnverified,
crate::MetaplexTokenMetadataFixtureState::CollectionMemberVerified,
crate::MetaplexTokenMetadataFixtureState::CollectionMemberUnverified,
vec![
kb_lib::EX_METAPLEX_TOKEN_METADATA_VERIFY_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_UNVERIFY_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_SET_COLLECTION_SIZE_OPERATION.to_string(),
],
true,
false,
@@ -338,6 +337,28 @@ mod tests {
}
}
#[test]
fn collection_journey_uses_only_current_verify_and_unverify_operations() {
let scenarios = crate::metaplex_token_metadata_synthetic_scenarios();
let collection = scenarios.iter().find(|scenario| {
return scenario.asset_family == crate::MetaplexTokenMetadataAssetFamily::Collection;
});
assert!(collection.is_some());
if let std::option::Option::Some(scenario) = collection {
assert_eq!(
scenario.operation_codes,
vec![
kb_lib::EX_METAPLEX_TOKEN_METADATA_VERIFY_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_UNVERIFY_OPERATION.to_string(),
]
);
assert_eq!(
scenario.resulting_state,
crate::MetaplexTokenMetadataFixtureState::CollectionMemberUnverified
);
}
}
#[test]
fn collection_and_programmable_journeys_declare_special_requirements() {
let scenarios = crate::metaplex_token_metadata_synthetic_scenarios();

View File

@@ -0,0 +1,574 @@
// file: kb-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/use_campaign.rs
// version: 1
//! Bounded Devnet availability probe for the current Metaplex `Use` surface.
/// Outcome of one bounded Devnet `Use` availability probe.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DevnetMetaplexUseProbeStatus {
/// The current `Use` instruction was confirmed with complete pipeline evidence.
Confirmed,
/// The exact current `Use` instruction was rejected by Devnet simulation.
RuntimeUnavailable,
}
/// Complete evidence produced by one current Metaplex `Use` availability probe.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexUseProbeSummary {
/// Fresh classic NFT prepared with two bounded `Multiple` uses.
pub fixture: crate::DevnetMetaplexCreateMintCampaignSummary,
/// Exact simulation evidence for the current `Use` instruction.
pub simulation: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Confirmed execution evidence when the runtime accepts the current instruction.
pub execution: std::option::Option<crate::DevnetMetaplexTokenMetadataExecutionSummary>,
/// Runtime availability classification produced by this probe.
pub status: crate::DevnetMetaplexUseProbeStatus,
/// Remaining uses observed immediately before the `Use` probe.
pub uses_before: u64,
/// Remaining uses observed after confirmed execution, when available.
pub uses_after: std::option::Option<u64>,
/// Exact bounded simulation failure diagnostic when runtime-unavailable.
pub unavailable_reason: std::option::Option<std::string::String>,
}
/// Returns the exact ordered operations exercised by the bounded `Use` probe.
pub fn metaplex_use_probe_operation_names() -> &'static [&'static str; 3] {
return &["create", "mint", "use"];
}
/// Executes one fresh classic NFT `Use` availability probe on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_use_probe<S, O>(
http_pool: &kb_onchain_transport::HttpEndpointPool,
store: &S,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> kb_core::Result<crate::DevnetMetaplexUseProbeSummary>
where
S: kb_store::RawTransactionStore
+ kb_store::CoreExtractionStore
+ kb_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if options.asset_family != crate::MetaplexTokenMetadataAssetFamily::Nft {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex Use probe requires the classic NFT fixture family",
));
}
let fixture_options = options.clone().with_multiple_uses(2);
let fixture = match crate::execute_devnet_metaplex_create_mint_campaign(
http_pool,
store,
profile,
workspace_root,
&fixture_options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let uses_before = match metadata_remaining_multiple_uses(
fixture.mint.after.as_slice(),
fixture.fixture.metadata.as_str(),
2,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if uses_before != 2 {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_initial_uses_invalid",
format!("Metaplex Use probe requires remaining=2 before execution; got {uses_before}"),
));
}
let operation = use_operation(&fixture.fixture);
let mut simulation_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-use-probe-simulation-{}", uuid::Uuid::new_v4()),
operation.clone(),
);
simulation_request.query_role = fixture_options.query_role.clone();
simulation_request.transaction_role = fixture_options.transaction_role.clone();
let simulation = match crate::simulate_devnet_metaplex_token_metadata(
http_pool,
profile,
workspace_root,
&simulation_request,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !simulation.simulation.success {
return std::result::Result::Ok(crate::DevnetMetaplexUseProbeSummary {
fixture,
unavailable_reason: std::option::Option::Some(crate::simulation_failure_message(
&simulation.simulation,
)),
simulation,
execution: std::option::Option::None,
status: crate::DevnetMetaplexUseProbeStatus::RuntimeUnavailable,
uses_before,
uses_after: std::option::Option::None,
});
}
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(kb_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> = std::vec![
std::sync::Arc::new(kb_lib::MtAdminMaterializer),
std::sync::Arc::new(kb_lib::MtComplianceAuditMaterializer),
std::sync::Arc::new(kb_lib::MtLifecycleMaterializer),
std::sync::Arc::new(kb_lib::MtMetadataMetaplexTokenMetadataMaterializer),
std::sync::Arc::new(kb_lib::MtRiskMaterializer),
];
let mut execution_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-use-probe-submit-{}", uuid::Uuid::new_v4()),
operation,
);
execution_request.query_role = fixture_options.query_role.clone();
execution_request.transaction_role = fixture_options.transaction_role.clone();
execution_request.submit = true;
execution_request.operator_confirmed = true;
execution_request.materialize_after_confirmation = true;
execution_request.post_validation_max_retries = 20;
execution_request.postcondition_reads =
metadata_read(fixture.fixture.metadata.as_str(), fixture_options.query_role.as_str());
let execution = match crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&execution_request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmation_slot = match validate_confirmed_use_execution(&execution) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let uses_after = match metadata_remaining_multiple_uses(
execution.after.as_slice(),
fixture.fixture.metadata.as_str(),
2,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if uses_after != 1 {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_remaining_uses_invalid",
format!(
"confirmed Metaplex Use must decrement remaining uses from 2 to 1; got {uses_after}"
),
));
}
let token_after = match crate::read_token_account_at_or_after(
http_pool,
fixture_options.query_role.as_str(),
fixture.fixture.token_account.as_str(),
std::option::Option::Some(confirmation_slot),
)
.await
{
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_token_missing",
"classic NFT token account disappeared after confirmed Use",
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = crate::validate_classic_fixture_token_account_state(
&token_after,
fixture.fixture.mint.as_str(),
fixture.fixture.authority.as_str(),
1,
spl_token_interface::state::AccountState::Initialized,
) {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_token_state_invalid",
format!("confirmed Metaplex Use left invalid NFT token state: {error}"),
));
}
return std::result::Result::Ok(crate::DevnetMetaplexUseProbeSummary {
fixture,
simulation,
execution: std::option::Option::Some(execution),
status: crate::DevnetMetaplexUseProbeStatus::Confirmed,
uses_before,
uses_after: std::option::Option::Some(uses_after),
unavailable_reason: std::option::Option::None,
});
}
fn use_operation(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
) -> kb_lib::ExMetaplexTokenMetadataOperation {
return kb_lib::ExMetaplexTokenMetadataOperation::Use {
authority: kb_lib::MdPubkey(fixture.authority.clone()),
delegate_record: std::option::Option::None,
token: std::option::Option::Some(kb_lib::MdPubkey(fixture.token_account.clone())),
mint: kb_lib::MdPubkey(fixture.mint.clone()),
metadata: kb_lib::MdPubkey(fixture.metadata.clone()),
edition: std::option::Option::Some(kb_lib::MdPubkey(fixture.master_edition.clone())),
payer: kb_lib::MdPubkey(fixture.authority.clone()),
system_program: kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()),
sysvar_instructions: kb_lib::MdPubkey(
kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID.to_string(),
),
spl_token_program: std::option::Option::Some(kb_lib::MdPubkey(
kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
)),
authorization_rules_program: std::option::Option::None,
authorization_rules: std::option::Option::None,
args: mpl_token_metadata::types::UseArgs::V1 {
authorization_data: std::option::Option::None,
},
};
}
fn metadata_read(
metadata: &str,
query_role: &str,
) -> std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
return std::vec![kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: kb_lib::MdPubkey(metadata.to_string()),
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot: std::option::Option::None,
max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
}];
}
fn metadata_remaining_multiple_uses(
snapshots: &[kb_pipeline::MetaplexTokenMetadataStatefulReadResult],
metadata: &str,
expected_total: u64,
) -> kb_core::Result<u64> {
let snapshot = match snapshots.iter().find(|value| {
return value.snapshot.account.0.as_str() == metadata
&& value.snapshot.account_kind == "metadata";
}) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_metadata_snapshot_missing",
format!("metadata snapshot {metadata} is missing from the Use probe"),
));
},
};
let uses_value = match snapshot.snapshot.payload_json.get("uses") {
std::option::Option::Some(value) if !value.is_null() => value.clone(),
_ => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_uses_missing",
"metadata snapshot has no Uses state",
));
},
};
let uses = match serde_json::from_value::<mpl_token_metadata::types::Uses>(uses_value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_uses_decode_failed",
format!("metadata Uses projection is invalid: {error}"),
));
},
};
if uses.use_method != mpl_token_metadata::types::UseMethod::Multiple
|| uses.total != expected_total
|| uses.remaining > uses.total
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_uses_state_invalid",
format!(
"metadata Uses state must remain Multiple with total={expected_total}; got method={:?}, remaining={}, total={}",
uses.use_method, uses.remaining, uses.total
),
));
}
return std::result::Result::Ok(uses.remaining);
}
fn validate_confirmed_use_execution(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> kb_core::Result<u64> {
let confirmation = match execution.confirmation.as_ref() {
std::option::Option::Some(value)
if matches!(
value.status,
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
) =>
{
value
},
std::option::Option::Some(value) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_confirmation_incomplete",
format!("Metaplex Use stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_confirmation_missing",
"Metaplex Use has no confirmation evidence",
));
},
};
let diagnostic = match execution.post_execution.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_post_execution_missing",
"Metaplex Use has no post-execution diagnostic",
));
},
};
if !diagnostic.canonical_inserted
|| !diagnostic.core_extracted
|| !diagnostic.decode_replayed
|| !diagnostic.materialized
|| execution.materializations.is_empty()
|| execution.materialized_snapshots.is_empty()
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_post_execution_incomplete",
format!(
"Metaplex Use post-execution incomplete: signature={}, slot={:?}, canonical_inserted={}, core_extracted={}, decode_replayed={}, materialized={}, materialization_rows={}, materialized_snapshots={}, diagnostics={:?}",
confirmation.signature.0,
confirmation.slot,
diagnostic.canonical_inserted,
diagnostic.core_extracted,
diagnostic.decode_replayed,
diagnostic.materialized,
execution.materializations.len(),
execution.materialized_snapshots.len(),
diagnostic.diagnostics
),
));
}
let idempotence = match execution.idempotence_replay.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_idempotence_missing",
"Metaplex Use has no idempotence replay evidence",
));
},
};
if idempotence.failed_inputs != 0
|| idempotence.processing_error_inputs != 0
|| idempotence.processors.iter().any(|processor| {
return processor.failed != 0
|| processor.processing_errors != 0
|| processor.materialized_outputs != 0
|| processor.materialization_refused != 0;
})
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_idempotence_failed",
"Metaplex Use second replay is not idempotent",
));
}
return match confirmation.slot {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(kb_core::Error::new(
"metaplex_use_probe_confirmation_slot_missing",
"Metaplex Use confirmation has no slot",
)),
};
}
#[cfg(test)]
mod tests {
#[test]
fn use_probe_contract_is_exactly_create_mint_then_use() {
assert_eq!(crate::metaplex_use_probe_operation_names(), &["create", "mint", "use"]);
assert_ne!(
crate::DevnetMetaplexUseProbeStatus::Confirmed,
crate::DevnetMetaplexUseProbeStatus::RuntimeUnavailable,
);
}
#[tokio::test]
async fn optional_devnet_metaplex_use_probe_from_env() {
if std::env::var("KB_DEVNET_METAPLEX_USE_PROBE_TEST").ok().as_deref()
!= std::option::Option::Some("1")
{
return;
}
assert_eq!(
std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref(),
std::option::Option::Some("1"),
"set KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 before submitting an accepted Use probe"
);
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
};
let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") {
std::result::Result::Ok(value) => {
let path = std::path::PathBuf::from(value);
if path.is_absolute() { path } else { workspace_root.join(path) }
},
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
};
let config = kb_config::read_config_json_file_with_environment(
config_path.as_path(),
workspace_root,
)
.unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}"));
let profile_name = std::env::var("KB_DEVNET_PROFILE").ok();
let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref())
.unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}"));
let database_url = std::env::var("KB_POSTGRES_TEST_URL")
.unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}"));
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
let http_pool = kb_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool initialization failed: {error}"));
let store_options = kb_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
)
.unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}"));
let store = kb_store::PostgresStore::connect(store_options)
.await
.unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}"));
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("PostgreSQL schema initialization failed: {error}");
}
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
workspace_root.join(configured_wallet_dir)
};
let options = crate::MetaplexCreateFixturePreparationOptions::new(wallet_dir)
.with_asset_family(crate::MetaplexTokenMetadataAssetFamily::Nft);
let summary = crate::execute_devnet_metaplex_use_probe(
&http_pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| panic!("Metaplex Use Devnet probe failed: {error}"));
println!(
"METAPLEX_USE_PROBE_FIXTURE mint={} metadata={} master_edition={} token={} uses_before={}",
summary.fixture.fixture.mint,
summary.fixture.fixture.metadata,
summary.fixture.fixture.master_edition,
summary.fixture.fixture.token_account,
summary.uses_before,
);
println!(
"METAPLEX_USE_PROBE_SIMULATION success={} context_slot={} logs={} error={}",
summary.simulation.simulation.success,
summary.simulation.simulation_context_slot,
summary.simulation.simulation.logs.len(),
serde_json::to_string(&summary.simulation.simulation.error).unwrap_or_else(
|error| panic!("Use simulation error serialization failed: {error}")
),
);
match summary.status {
crate::DevnetMetaplexUseProbeStatus::RuntimeUnavailable => {
println!(
"METAPLEX_USE_PROBE_STATUS status=runtime_unavailable uses_before={} uses_after=null reason={}",
summary.uses_before,
serde_json::to_string(&summary.unavailable_reason).unwrap_or_else(
|error| panic!("Use unavailable reason serialization failed: {error}")
),
);
println!(
"METAPLEX_USE_PROBE_LOGS {}",
serde_json::to_string(&summary.simulation.simulation.logs).unwrap_or_else(
|error| panic!("Use simulation log serialization failed: {error}")
),
);
},
crate::DevnetMetaplexUseProbeStatus::Confirmed => {
let execution = summary
.execution
.as_ref()
.unwrap_or_else(|| panic!("confirmed Use probe execution is missing"));
let confirmation = execution
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("confirmed Use probe confirmation is missing"));
println!(
"METAPLEX_USE_PROBE_STATUS status=confirmed uses_before={} uses_after={}",
summary.uses_before,
summary
.uses_after
.unwrap_or_else(|| panic!("confirmed Use uses_after is missing")),
);
println!(
"METAPLEX_USE_PROBE_STEP operation={} signature={} slot={:?} materializations={}",
execution.plan.operation_code,
confirmation.signature.0,
confirmation.slot,
execution.materializations.len(),
);
println!("METAPLEX_USE_PROBE_EVIDENCE {}", execution_evidence_json(execution),);
},
}
}
fn execution_evidence_json(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> serde_json::Value {
let confirmation = execution.confirmation.as_ref();
let post_execution = execution.post_execution.as_ref();
let idempotence_clean = execution.idempotence_replay.as_ref().is_some_and(|replay| {
return replay.failed_inputs == 0
&& replay.processing_error_inputs == 0
&& replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
});
return serde_json::json!({
"operation": execution.plan.operation_code.as_str(),
"cluster": execution.cluster,
"genesisHash": execution.genesis_hash.as_str(),
"simulationContextSlot": execution.simulation_context_slot,
"simulationSuccess": execution.simulation.success,
"simulationLogCount": execution.simulation.logs.len(),
"messageHash": execution.readiness.message_hash.as_str(),
"feeLamports": execution.fee.fee_lamports,
"signature": confirmation.map(|value| return value.signature.0.clone()),
"confirmationStatus": confirmation.map(|value| return format!("{:?}", value.status)),
"confirmationSlot": confirmation.and_then(|value| return value.slot),
"beforeSnapshots": execution.before.len(),
"afterSnapshots": execution.after.len(),
"canonicalHydration": post_execution.is_some_and(|value| return value.canonical_inserted),
"coreExtraction": post_execution.is_some_and(|value| return value.core_extracted),
"decodeReplay": post_execution.is_some_and(|value| return value.decode_replayed),
"materialized": post_execution.is_some_and(|value| return value.materialized),
"instructionMaterializations": execution.materializations.len(),
"materializedSnapshots": execution.materialized_snapshots.len(),
"idempotenceReplayClean": idempotence_clean,
});
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs
// version: 4
// file: kb-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/validation.rs
// version: 17
//! Machine-readable Metaplex Token Metadata validation contract.
@@ -154,7 +154,7 @@ pub fn load_metaplex_token_metadata_cross_validation_matrix()
-> kb_core::Result<crate::MetaplexTokenMetadataCrossValidationMatrix> {
let parsed = match serde_json::from_str::<crate::MetaplexTokenMetadataCrossValidationMatrix>(
include_str!(
"../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_CROSS_VALIDATION_MATRIX.json"
"../../../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_CROSS_VALIDATION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,
@@ -264,18 +264,19 @@ pub fn validate_metaplex_token_metadata_cross_validation_matrix(
/// Loads and validates the canonical Metaplex validation matrix.
pub fn load_metaplex_token_metadata_validation_matrix()
-> kb_core::Result<crate::MetaplexTokenMetadataValidationMatrix> {
let parsed =
match serde_json::from_str::<crate::MetaplexTokenMetadataValidationMatrix>(include_str!(
"../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_VALIDATION_MATRIX.json"
)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_token_metadata_validation_matrix_invalid_json",
format!("Metaplex validation matrix JSON is invalid: {error}"),
));
},
};
let parsed = match serde_json::from_str::<crate::MetaplexTokenMetadataValidationMatrix>(
include_str!(
"../../../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_VALIDATION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_token_metadata_validation_matrix_invalid_json",
format!("Metaplex validation matrix JSON is invalid: {error}"),
));
},
};
if let std::result::Result::Err(error) =
validate_metaplex_token_metadata_validation_matrix(&parsed)
{
@@ -439,6 +440,16 @@ mod tests {
}
}
/// One family-specific evidence bundle in the closed Devnet execution matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MetaplexTokenMetadataDevnetExecutionFamilyEvidence {
/// Asset family exercised by this evidence bundle.
pub family: crate::MetaplexTokenMetadataAssetFamily,
/// Complete evidence observed for this family.
pub evidence: std::vec::Vec<crate::MetaplexTokenMetadataValidationEvidence>,
}
/// One current Metaplex operation in the closed Devnet execution matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
@@ -457,10 +468,19 @@ pub struct MetaplexTokenMetadataDevnetExecutionOperation {
pub runner_status: std::string::String,
/// Exact observed network status.
pub network_status: std::string::String,
/// Asset families on which this operation has completed its declared network status.
#[serde(default)]
pub observed_families: std::vec::Vec<crate::MetaplexTokenMetadataAssetFamily>,
/// Evidence required after simulation.
pub required_evidence: std::vec::Vec<std::string::String>,
/// Additional evidence required after submission.
pub submission_evidence: std::vec::Vec<std::string::String>,
/// Operation-level evidence reserved for non-family states such as unavailability.
#[serde(default)]
pub evidence: std::vec::Vec<crate::MetaplexTokenMetadataValidationEvidence>,
/// Family-specific evidence bundles for simulated or confirmed operation paths.
#[serde(default)]
pub family_evidence: std::vec::Vec<crate::MetaplexTokenMetadataDevnetExecutionFamilyEvidence>,
}
/// Closed inventory of every current Metaplex operation requiring Devnet coverage.
@@ -482,7 +502,7 @@ pub fn load_metaplex_token_metadata_devnet_execution_matrix()
-> kb_core::Result<crate::MetaplexTokenMetadataDevnetExecutionMatrix> {
let matrix = match serde_json::from_str::<crate::MetaplexTokenMetadataDevnetExecutionMatrix>(
include_str!(
"../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json"
"../../../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,
@@ -505,14 +525,14 @@ pub fn load_metaplex_token_metadata_devnet_execution_matrix()
pub fn validate_metaplex_token_metadata_devnet_execution_matrix(
matrix: &crate::MetaplexTokenMetadataDevnetExecutionMatrix,
) -> kb_core::Result<()> {
if matrix.matrix_version != 1
|| matrix.milestone != "0.4.7-pre.012"
if matrix.matrix_version != 3
|| matrix.milestone != "0.4.8-pre.013"
|| matrix.program_id != kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
|| matrix.operations.len() != 20
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_contract_mismatch",
"Metaplex Devnet matrix must declare exactly 20 current operations for pre.012",
"Metaplex Devnet matrix must use version 3 and declare exactly 20 current operations for 0.4.8-pre.013",
));
}
let expected = [
@@ -550,22 +570,164 @@ pub fn validate_metaplex_token_metadata_devnet_execution_matrix(
"Metaplex Devnet matrix must cover the exact 20 current operation codes",
));
}
let required_evidence = [
"cluster",
"genesis_hash",
"simulation_slot",
"simulation_logs",
"message_hash",
"fee_lamports",
]
.into_iter()
.collect::<std::collections::BTreeSet<&str>>();
let submission_evidence = [
"signature",
"confirmation_status",
"confirmation_slot",
"before_state",
"after_state",
"canonical_hydration",
"core_extraction",
"decode_replay",
"materialization",
"idempotence_replay",
]
.into_iter()
.collect::<std::collections::BTreeSet<&str>>();
for operation in &matrix.operations {
let observed_required = operation
.required_evidence
.iter()
.map(std::string::String::as_str)
.collect::<std::collections::BTreeSet<&str>>();
let observed_submission = operation
.submission_evidence
.iter()
.map(std::string::String::as_str)
.collect::<std::collections::BTreeSet<&str>>();
if operation.deprecated
|| operation.runner_status != "implemented"
|| !matches!(operation.campaign_prerelease.as_str(), "0.4.7-pre.011" | "0.4.7-pre.012")
|| operation.required_evidence.is_empty()
|| operation.submission_evidence.is_empty()
|| operation.campaign_prerelease != "0.4.8-pre.013"
|| observed_required != required_evidence
|| observed_submission != submission_evidence
|| !matches!(
operation.network_status.as_str(),
"not_run" | "simulated" | "confirmed" | "unavailable"
)
|| operation.evidence.len() > crate::MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_entry_invalid",
"Metaplex Devnet entries must be current, implemented, evidenced and conservatively classified",
));
}
let observed_families = operation
.observed_families
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>();
if observed_families.len() != operation.observed_families.len() {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_duplicate_family",
"Metaplex Devnet observed families must be unique",
));
}
let mut observed_evidence = std::collections::BTreeSet::<&str>::new();
for evidence in &operation.evidence {
if evidence.kind.trim().is_empty()
|| evidence.value.trim().is_empty()
|| !observed_evidence.insert(evidence.kind.as_str())
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_evidence_invalid",
"Metaplex Devnet operation-level evidence kinds and values must be non-empty and unique",
));
}
}
let mut observed_family_evidence =
std::collections::BTreeSet::<crate::MetaplexTokenMetadataAssetFamily>::new();
for family_evidence in &operation.family_evidence {
if !observed_family_evidence.insert(family_evidence.family)
|| family_evidence.evidence.len()
> crate::MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_family_evidence_invalid",
"Metaplex Devnet family evidence bundles must be unique and bounded",
));
}
let mut family_kinds = std::collections::BTreeSet::<&str>::new();
for evidence in &family_evidence.evidence {
if evidence.kind.trim().is_empty()
|| evidence.value.trim().is_empty()
|| !family_kinds.insert(evidence.kind.as_str())
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_family_evidence_entry_invalid",
"Metaplex Devnet family evidence kinds and values must be non-empty and unique",
));
}
}
if operation.network_status == "simulated"
&& operation
.required_evidence
.iter()
.any(|kind| return !family_kinds.contains(kind.as_str()))
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_family_simulation_evidence_incomplete",
"Simulated Metaplex Devnet family evidence must contain every required simulation evidence kind",
));
}
if operation.network_status == "confirmed"
&& operation
.required_evidence
.iter()
.chain(operation.submission_evidence.iter())
.any(|kind| return !family_kinds.contains(kind.as_str()))
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_family_confirmation_evidence_incomplete",
"Confirmed Metaplex Devnet family evidence must contain every simulation and submission evidence kind",
));
}
}
match operation.network_status.as_str() {
"not_run" => {
if !operation.observed_families.is_empty()
|| !operation.evidence.is_empty()
|| !operation.family_evidence.is_empty()
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_not_run_with_evidence",
"Not-run Metaplex Devnet operations cannot claim families or evidence",
));
}
},
"simulated" | "confirmed" => {
if operation.observed_families.is_empty()
|| !operation.evidence.is_empty()
|| observed_family_evidence != observed_families
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_family_evidence_mismatch",
"Simulated or confirmed Metaplex Devnet operations require exactly one complete evidence bundle per observed family and no aggregate operation evidence",
));
}
},
"unavailable" => {
if !operation.observed_families.is_empty()
|| !operation.family_evidence.is_empty()
|| !observed_evidence.contains("unavailable_reason")
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_unavailable_without_reason",
"Unavailable Metaplex Devnet operations require an unavailable_reason and no observed family evidence",
));
}
},
_ => {},
}
}
return std::result::Result::Ok(());
}
@@ -573,7 +735,7 @@ pub fn validate_metaplex_token_metadata_devnet_execution_matrix(
#[cfg(test)]
mod devnet_execution_matrix_tests {
#[test]
fn devnet_execution_matrix_is_closed_current_and_conservative() {
fn devnet_execution_matrix_is_reopened_for_pre_013_and_conservative() {
let result = crate::load_metaplex_token_metadata_devnet_execution_matrix();
assert!(result.is_ok());
let matrix = if let std::result::Result::Ok(value) = result {
@@ -581,22 +743,13 @@ mod devnet_execution_matrix_tests {
} else {
return;
};
assert_eq!(matrix.matrix_version, 3);
assert_eq!(matrix.operations.len(), 20);
assert_eq!(
assert!(
matrix
.operations
.iter()
.filter(|operation| return operation.campaign_prerelease == "0.4.7-pre.011")
.count(),
4,
);
assert_eq!(
matrix
.operations
.iter()
.filter(|operation| return operation.campaign_prerelease == "0.4.7-pre.012")
.count(),
16,
.all(|operation| return operation.campaign_prerelease == "0.4.8-pre.013")
);
assert!(matrix.operations.iter().all(|operation| return !operation.deprecated));
assert!(
@@ -605,11 +758,161 @@ mod devnet_execution_matrix_tests {
.iter()
.all(|operation| return operation.runner_status == "implemented")
);
assert!(
let confirmed = matrix
.operations
.iter()
.filter(|operation| return operation.network_status == "confirmed")
.collect::<std::vec::Vec<_>>();
assert_eq!(confirmed.len(), 15);
assert_eq!(
confirmed
.iter()
.map(|operation| return operation.operation_code.as_str())
.collect::<std::collections::BTreeSet<_>>(),
[
"metadata.metaplex_token_metadata.burn",
"metadata.metaplex_token_metadata.close_escrow_account",
"metadata.metaplex_token_metadata.create",
"metadata.metaplex_token_metadata.create_escrow_account",
"metadata.metaplex_token_metadata.delegate",
"metadata.metaplex_token_metadata.lock",
"metadata.metaplex_token_metadata.mint",
"metadata.metaplex_token_metadata.print",
"metadata.metaplex_token_metadata.revoke",
"metadata.metaplex_token_metadata.transfer",
"metadata.metaplex_token_metadata.transfer_out_of_escrow",
"metadata.metaplex_token_metadata.unlock",
"metadata.metaplex_token_metadata.update",
"metadata.metaplex_token_metadata.verify",
"metadata.metaplex_token_metadata.unverify",
]
.into_iter()
.collect::<std::collections::BTreeSet<_>>()
);
let create_mint_families = vec![
crate::MetaplexTokenMetadataAssetFamily::Nft,
crate::MetaplexTokenMetadataAssetFamily::Sft,
crate::MetaplexTokenMetadataAssetFamily::Fungible,
crate::MetaplexTokenMetadataAssetFamily::Collection,
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
];
for operation in &confirmed {
let expected_families = if matches!(
operation.operation_code.as_str(),
"metadata.metaplex_token_metadata.create" | "metadata.metaplex_token_metadata.mint"
) {
create_mint_families.as_slice()
} else if matches!(
operation.operation_code.as_str(),
"metadata.metaplex_token_metadata.print"
| "metadata.metaplex_token_metadata.burn"
| "metadata.metaplex_token_metadata.create_escrow_account"
| "metadata.metaplex_token_metadata.close_escrow_account"
| "metadata.metaplex_token_metadata.transfer_out_of_escrow"
| "metadata.metaplex_token_metadata.update"
) {
&[crate::MetaplexTokenMetadataAssetFamily::Nft]
} else if matches!(
operation.operation_code.as_str(),
"metadata.metaplex_token_metadata.delegate"
| "metadata.metaplex_token_metadata.revoke"
| "metadata.metaplex_token_metadata.lock"
| "metadata.metaplex_token_metadata.unlock"
| "metadata.metaplex_token_metadata.transfer"
) {
&[crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft]
} else {
&[crate::MetaplexTokenMetadataAssetFamily::Collection]
};
assert_eq!(operation.observed_families.as_slice(), expected_families);
assert_eq!(
operation
.family_evidence
.iter()
.map(|family_evidence| return family_evidence.family)
.collect::<std::collections::BTreeSet<_>>(),
operation
.observed_families
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>()
);
}
assert_eq!(
matrix
.operations
.iter()
.all(|operation| return operation.network_status == "not_run")
.filter(|operation| return operation.network_status == "not_run")
.count(),
0
);
let unavailable = matrix
.operations
.iter()
.filter(|operation| return operation.network_status == "unavailable")
.collect::<std::vec::Vec<_>>();
assert_eq!(unavailable.len(), 5);
let use_operation = unavailable.iter().find(|operation| {
return operation.operation_code == "metadata.metaplex_token_metadata.use";
});
assert!(use_operation.is_some());
if let std::option::Option::Some(operation) = use_operation {
assert!(operation.evidence.iter().any(|evidence| {
return evidence.kind == "unavailable_reason"
&& evidence.value.contains("InvalidInstructionData");
}));
}
let resize_operation = unavailable.iter().find(|operation| {
return operation.operation_code == "metadata.metaplex_token_metadata.resize";
});
assert!(resize_operation.is_some());
if let std::option::Option::Some(operation) = resize_operation {
assert!(operation.evidence.iter().any(|evidence| {
return evidence.kind == "unavailable_reason"
&& evidence.value.contains("AlreadyResized")
&& evidence.value.contains("201");
}));
}
for (operation_code, expected_fragment) in [
("metadata.metaplex_token_metadata.migrate", "Removed"),
("metadata.metaplex_token_metadata.collect", "fee authority"),
("metadata.metaplex_token_metadata.close_accounts", "close authority"),
] {
let operation = unavailable
.iter()
.find(|operation| return operation.operation_code == operation_code);
assert!(operation.is_some());
if let std::option::Option::Some(operation) = operation {
assert!(operation.evidence.iter().any(|evidence| {
return evidence.kind == "unavailable_reason"
&& evidence.value.contains(expected_fragment);
}));
}
}
let update_operation = confirmed.iter().find(|operation| {
return operation.operation_code == "metadata.metaplex_token_metadata.update";
});
assert!(update_operation.is_some());
if let std::option::Option::Some(operation) = update_operation {
assert_eq!(
operation.observed_families.as_slice(),
&[crate::MetaplexTokenMetadataAssetFamily::Nft]
);
}
let mut incomplete = matrix.clone();
let create = incomplete.operations.iter_mut().find(|operation| {
return operation.operation_code == "metadata.metaplex_token_metadata.create";
});
assert!(create.is_some());
if let std::option::Option::Some(operation) = create {
if let std::option::Option::Some(family_evidence) =
operation.family_evidence.first_mut()
{
family_evidence.evidence.clear();
}
}
assert!(
crate::validate_metaplex_token_metadata_devnet_execution_matrix(&incomplete).is_err()
);
}
}

View File

@@ -0,0 +1,10 @@
// file: kb-pipeline-demo-scenarios/src/metadata/solana_program.rs
// version: 1
//! Solana Program Metadata demo and Devnet validation scenarios.
pub(crate) mod campaign;
pub(crate) mod devnet_execution;
pub(crate) mod fixture;
pub(crate) mod scenarios;
pub(crate) mod validation;

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_campaign.rs
// version: 2
// file: kb-pipeline-demo-scenarios/src/metadata/solana_program/campaign.rs
// version: 3
//! Ordered confirmed Devnet campaign for all Solana Program Metadata operations.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_devnet_execution.rs
// version: 1
// file: kb-pipeline-demo-scenarios/src/metadata/solana_program/devnet_execution.rs
// version: 2
//! Real Devnet simulation and controlled submission for Solana Program Metadata.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_fixture.rs
// version: 5
// file: kb-pipeline-demo-scenarios/src/metadata/solana_program/fixture.rs
// version: 6
//! Native Solana Program Metadata fixture preparation for Devnet journeys.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_scenarios.rs
// version: 3
// file: kb-pipeline-demo-scenarios/src/metadata/solana_program/scenarios.rs
// version: 4
//! Coherent Solana Program Metadata journeys for Devnet validation.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_validation.rs
// version: 3
// file: kb-pipeline-demo-scenarios/src/metadata/solana_program/validation.rs
// version: 4
//! Conservative Solana Program Metadata validation status and evidence contracts.
@@ -70,7 +70,7 @@ 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"
"../../../../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_DEVNET_VALIDATION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,

View File

@@ -1,703 +0,0 @@
// file: kb-pipeline-demo-scenarios/src/metaplex_token_metadata_fixture.rs
// version: 8
//! Native Metaplex Token Metadata fixture preparation for Devnet demos.
/// Options used to create or reuse one classic SPL mint for a Metaplex Create demo.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MetaplexCreateFixturePreparationOptions {
/// Endpoint role used for state, rent, balance, blockhash and fee calls.
pub query_role: std::string::String,
/// Endpoint role used for simulation, submission and confirmation polling.
pub transaction_role: std::string::String,
/// Directory receiving the persistent fixture mint keypair.
pub wallet_dir: std::path::PathBuf,
}
impl crate::MetaplexCreateFixturePreparationOptions {
/// Creates conservative defaults matching the Devnet execution profiles.
pub fn new(wallet_dir: std::path::PathBuf) -> Self {
return Self {
query_role: "http_queries".to_string(),
transaction_role: "http_transactions".to_string(),
wallet_dir,
};
}
}
/// Public fixture values and generated Create intent JSON.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct MetaplexCreateFixturePreparationSummary {
/// Fixture mint keypair path.
pub mint_keypair_path: std::path::PathBuf,
/// Classic SPL mint address.
pub mint: std::string::String,
/// Canonical Metaplex metadata PDA.
pub metadata: std::string::String,
/// Canonical Metaplex master-edition PDA.
pub master_edition: std::string::String,
/// Operator public key used as mint and update authority.
pub authority: std::string::String,
/// Ready-to-edit typed Create operation JSON.
pub operation_json: std::string::String,
/// Whether the mint account was created during this call.
pub mint_created: bool,
/// Confirmed signature of native mint preparation when newly created.
pub preparation_signature: std::option::Option<std::string::String>,
}
/// Creates or reuses a zero-decimal classic SPL mint and derives its Metaplex PDAs.
pub async fn prepare_metaplex_create_fixture(
http_pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
) -> kb_core::Result<crate::MetaplexCreateFixturePreparationSummary> {
if options.query_role.trim().is_empty() || options.transaction_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex fixture endpoint roles must not be empty",
));
}
if !profile.wallet.devnet_send_enabled {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex fixture preparation requires devnet_send_enabled=true",
));
}
let operator = match crate::load_profile_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture_dir = options.wallet_dir.join("metaplex_token_metadata_validation");
let fixture_store = match kb_wallet::TemporaryWalletStore::new(fixture_dir.clone()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture_epoch = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
std::result::Result::Ok(value) => value.as_millis(),
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"unable to create a unique Metaplex fixture identifier: {error}"
)));
},
};
let fixture_alias =
match kb_wallet::WalletAlias::parse(format!("create-mint-native-{fixture_epoch}").as_str())
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint_wallet = match fixture_store.load_or_create(fixture_alias.clone()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint_keypair_path = fixture_store.wallet_path(&fixture_alias);
let authority = operator.public_key();
let mint = mint_wallet.public_key();
let existing =
match read_mint_account(http_pool, options.query_role.as_str(), mint.as_str()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let preparation_signature = if let std::option::Option::Some(account) = existing {
if let std::result::Result::Err(error) =
validate_classic_nft_mint(&account, authority.as_str())
{
return std::result::Result::Err(error);
}
std::option::Option::None
} else {
let signature = match create_native_classic_nft_mint(
http_pool,
profile,
options,
&operator,
&mint_wallet,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let created =
match read_mint_account(http_pool, options.query_role.as_str(), mint.as_str()).await {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_fixture_mint_missing_after_confirmation",
"native Metaplex fixture mint is unavailable after confirmation",
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) =
validate_classic_nft_mint(&created, authority.as_str())
{
return std::result::Result::Err(error);
}
std::option::Option::Some(signature)
};
let mint_pubkey = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(mint.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid generated mint public key: {error}"
)));
},
};
let program_id = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid Metaplex Program ID: {error}"
)));
},
};
let (metadata, _) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program_id.as_ref(), mint_pubkey.as_ref()],
&program_id,
);
let (master_edition, _) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program_id.as_ref(), mint_pubkey.as_ref(), b"edition"],
&program_id,
);
let operation = create_operation_json(
mint_pubkey.to_string(),
metadata.to_string(),
master_edition.to_string(),
authority.clone(),
);
let operation_json = match serde_json::to_string_pretty(&operation) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_fixture_serialization_failed",
format!("unable to serialize Metaplex Create fixture: {error}"),
));
},
};
return std::result::Result::Ok(crate::MetaplexCreateFixturePreparationSummary {
mint_keypair_path,
mint: mint_pubkey.to_string(),
metadata: metadata.to_string(),
master_edition: master_edition.to_string(),
authority,
operation_json,
mint_created: preparation_signature.is_some(),
preparation_signature,
});
}
async fn create_native_classic_nft_mint(
http_pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
options: &crate::MetaplexCreateFixturePreparationOptions,
operator: &kb_wallet::TemporaryWallet,
mint_wallet: &kb_wallet::TemporaryWallet,
) -> kb_core::Result<std::string::String> {
const MINT_SPACE: u64 = 82;
let rent = match http_pool
.get_minimum_balance_for_rent_exemption_for_role(
options.query_role.as_str(),
MINT_SPACE,
&kb_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let payer = kb_lib::MdPubkey(operator.public_key());
let mint = kb_lib::MdPubkey(mint_wallet.public_key());
let policy =
fixture_policy(profile, payer.clone(), mint.clone(), rent.minimum_balance_lamports);
let system_intent = kb_lib::ExSolanaCoreExecutionIntent {
intent_id: format!("metaplex-fixture-create-account-{}", mint.0),
fee_payer: payer.clone(),
policy: policy.clone(),
operation: kb_lib::ExSolanaCoreOperation::SystemCreateAccount {
from: payer.clone(),
new_account: mint.clone(),
lamports: rent.minimum_balance_lamports,
space: MINT_SPACE,
owner: kb_lib::MdPubkey(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
},
};
let system_plan = match kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
&kb_lib::ExSolanaCoreExecutor,
&system_intent,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let token_program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid classic SPL Token Program ID: {error}"
)));
},
};
let mint_pubkey = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(mint.0.as_str())
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid native fixture mint public key: {error}"
)));
},
};
let authority_pubkey =
match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(payer.0.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid native fixture authority public key: {error}"
)));
},
};
let token_instruction = match spl_token_interface::instruction::initialize_mint2(
&token_program,
&mint_pubkey,
&authority_pubkey,
std::option::Option::Some(&authority_pubkey),
0,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_fixture_initialize_mint_build_failed",
error.to_string(),
));
},
};
let plan = merge_fixture_instruction(
system_plan,
planned_instruction("spl.token.initialize_mint2", &token_instruction),
policy,
rent.minimum_balance_lamports,
);
let plan_evaluation = match kb_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if plan_evaluation.decision == kb_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_fixture_plan_denied",
crate::violation_message(plan_evaluation.violations.as_slice()),
));
}
let latest_blockhash = match http_pool
.get_latest_blockhash_for_role(
options.query_role.as_str(),
&kb_onchain_transport::GetLatestBlockhashConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned = match kb_lib::executor_solana_build_legacy_transaction(
&plan,
latest_blockhash.blockhash.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fee = match http_pool
.get_fee_for_message_for_role(
options.query_role.as_str(),
unsigned.message_base64().as_str(),
&kb_onchain_transport::GetFeeForMessageConfig::new(
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
std::option::Option::Some(latest_blockhash.context.slot),
),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned_base64 = match unsigned.transaction_base64() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_config = match kb_onchain_transport::SimulateTransactionConfig::new(
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
false,
false,
std::option::Option::Some(latest_blockhash.context.slot),
true,
std::option::Option::None,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_rpc = match http_pool
.simulate_transaction_for_role(
options.transaction_role.as_str(),
unsigned_base64.as_str(),
&simulation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation = simulation_rpc.to_execution_result(
kb_lib::ExApiExecutionCluster::Devnet,
kb_lib::ExApiExecutionBlockhashKind::Latest,
std::option::Option::Some(
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(&fee),
);
if !simulation.success {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_fixture_simulation_failed",
crate::simulation_failure_message(&simulation),
));
}
let send_evaluation = match kb_lib::ExSafetyChecker.evaluate_send(&plan, &simulation) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if send_evaluation.decision == kb_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_fixture_send_denied",
crate::violation_message(send_evaluation.violations.as_slice()),
));
}
let evidence = unsigned.bind_simulation(simulation);
let signed = match unsigned
.sign_after_simulation(&evidence, &[operator.as_signer(), mint_wallet.as_signer()])
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = signed.verify_signatures() {
return std::result::Result::Err(error);
}
let signature = signed.primary_signature().clone();
let send_config = match kb_onchain_transport::SendTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = http_pool
.send_transaction_for_role(
options.transaction_role.as_str(),
signed.transaction_base64().as_str(),
&signature,
&send_config,
)
.await
{
return std::result::Result::Err(error);
}
let confirmation_config =
match kb_onchain_transport::ConfirmTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(latest_blockhash.last_valid_block_height),
std::option::Option::Some(latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmation = match http_pool
.confirm_transaction_for_roles(
options.transaction_role.as_str(),
options.query_role.as_str(),
kb_lib::ExApiExecutionCluster::Devnet,
&signature,
&confirmation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !matches!(
confirmation.status,
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
) {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_fixture_confirmation_failed",
format!("native mint preparation stopped at {:?}", confirmation.status),
));
}
tracing::info!(
target: "kb-pipeline-demo-scenarios.metaplex_fixture",
action = "prepare_native_metaplex_mint",
mint = mint_wallet.public_key(),
authority = operator.public_key(),
signature = signature.0.as_str(),
"created and initialized native classic SPL mint for Metaplex fixture"
);
return std::result::Result::Ok(signature.0);
}
fn planned_instruction(
operation_code: &str,
instruction: &solana_instruction::Instruction,
) -> kb_lib::ExApiPlannedInstruction {
return kb_lib::ExApiPlannedInstruction {
program_id: kb_lib::MdProgramId(instruction.program_id.to_string()),
operation_code: operation_code.to_string(),
accounts: instruction
.accounts
.iter()
.map(|account| {
return kb_lib::ExApiPlannedAccount {
pubkey: kb_lib::MdPubkey(account.pubkey.to_string()),
is_signer: account.is_signer,
is_writable: account.is_writable,
};
})
.collect(),
data: instruction.data.clone(),
};
}
fn fixture_policy(
profile: &kb_config::ProfileConfig,
payer: kb_lib::MdPubkey,
mint: kb_lib::MdPubkey,
_rent_lamports: u64,
) -> kb_lib::ExApiExecutionPolicy {
return kb_lib::ExApiExecutionPolicy {
cluster: kb_lib::ExApiExecutionClusterPolicy {
expected_cluster: kb_lib::ExApiExecutionCluster::Devnet,
allow_mainnet: false,
mainnet_confirmation: false,
},
simulation: kb_lib::ExApiExecutionSimulationPolicy::Required,
blockhash: kb_lib::ExApiExecutionBlockhashPolicy {
kind: kb_lib::ExApiExecutionBlockhashKind::Latest,
max_age_slots: std::option::Option::Some(
profile.execution.recent_blockhash_max_age_slots,
),
nonce_account: std::option::Option::None,
nonce_authority: std::option::Option::None,
},
cost_limit: kb_lib::ExApiExecutionCostLimit {
max_spend_lamports: std::option::Option::Some(
profile.execution.devnet_max_spend_lamports,
),
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
max_compute_unit_price_micro_lamports: std::option::Option::Some(
profile.execution.max_compute_unit_price_micro_lamports,
),
},
authorized_signers: std::vec![payer, mint],
dry_run: false,
post_execution_validation: kb_lib::ExApiPostExecutionValidationPolicy {
canonical_insert_required: false,
core_extraction_required: false,
decode_replay_required: false,
materialization_required: false,
},
};
}
fn merge_fixture_instruction(
mut system_plan: kb_lib::ExApiPreparedExecutionPlan,
token_instruction: kb_lib::ExApiPlannedInstruction,
policy: kb_lib::ExApiExecutionPolicy,
rent_lamports: u64,
) -> kb_lib::ExApiPreparedExecutionPlan {
system_plan.instructions.push(token_instruction);
system_plan.executor_name = "kb-pipeline-demo-scenarios.metaplex_fixture".to_string();
system_plan.operation_code = "metadata.metaplex_token_metadata.prepare_native_mint".to_string();
system_plan.policy = policy;
system_plan.requested_spend_lamports = rent_lamports;
return system_plan;
}
async fn read_mint_account(
http_pool: &kb_onchain_transport::HttpEndpointPool,
query_role: &str,
mint: &str,
) -> kb_core::Result<std::option::Option<kb_onchain_transport::AccountInfoValue>> {
let config = match kb_onchain_transport::GetAccountInfoConfig::confirmed_with_data(82) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint_pubkey = kb_lib::MdPubkey(mint.to_string());
let result = match http_pool.get_account_info_for_role(query_role, &mint_pubkey, &config).await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(result.account);
}
fn validate_classic_nft_mint(
account: &kb_onchain_transport::AccountInfoValue,
expected_authority: &str,
) -> kb_core::Result<()> {
if account.owner.0 != kb_program_ids::SPL_TOKEN_PROGRAM_ID || account.executable {
return std::result::Result::Err(kb_core::Error::config(format!(
"Metaplex Create fixture mint owner mismatch: expected classic SPL Token, got {}",
account.owner.0
)));
}
let bytes = account.data.as_slice();
if bytes.len() != 82 {
return std::result::Result::Err(kb_core::Error::config(format!(
"Metaplex Create fixture mint must use the classic 82-byte layout, got {} bytes",
bytes.len()
)));
}
let mint_authority = match decode_coption_pubkey(&bytes[0..36], "mint authority") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let supply_bytes = match <[u8; 8]>::try_from(&bytes[36..44]) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(kb_core::Error::config(
"invalid fixture mint supply bytes",
));
},
};
let supply = u64::from_le_bytes(supply_bytes);
let decimals = bytes[44];
let initialized = bytes[45] == 1;
let freeze_authority = match decode_coption_pubkey(&bytes[46..82], "freeze authority") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if decimals != 0 || supply != 0 || !initialized {
return std::result::Result::Err(kb_core::Error::config(format!(
"Metaplex NFT fixture mint requires initialized=true, decimals=0 and supply=0, got initialized={initialized}, decimals={decimals}, supply={supply}"
)));
}
if mint_authority.as_deref() != std::option::Option::Some(expected_authority)
|| freeze_authority.as_deref() != std::option::Option::Some(expected_authority)
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Metaplex NFT fixture mint requires mint and freeze authorities to equal {expected_authority}; mint_authority={mint_authority:?}, freeze_authority={freeze_authority:?}"
)));
}
return std::result::Result::Ok(());
}
fn decode_coption_pubkey(
bytes: &[u8],
label: &str,
) -> kb_core::Result<std::option::Option<std::string::String>> {
if bytes.len() != 36 {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid {label} field length: {}",
bytes.len()
)));
}
let tag = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
if tag == 0 {
return std::result::Result::Ok(std::option::Option::None);
}
if tag != 1 {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid {label} option tag: {tag}"
)));
}
let key = match <[u8; 32]>::try_from(&bytes[4..36]) {
std::result::Result::Ok(value) => solana_pubkey::Pubkey::new_from_array(value),
std::result::Result::Err(_) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid {label} public key bytes"
)));
},
};
return std::result::Result::Ok(std::option::Option::Some(key.to_string()));
}
fn create_operation_json(
mint: std::string::String,
metadata: std::string::String,
master_edition: std::string::String,
authority: std::string::String,
) -> serde_json::Value {
return serde_json::json!({
"operation": "create",
"metadata": metadata,
"master_edition": master_edition,
"mint": mint,
"mint_as_signer": false,
"authority": authority,
"update_authority": authority,
"update_authority_as_signer": true,
"spl_token_program": kb_program_ids::SPL_TOKEN_PROGRAM_ID,
"create_args": {"V1": {
"name": "Khadhroony Devnet Asset",
"symbol": "KHDEV",
"uri": "https://example.invalid/khadhroony-devnet.json",
"seller_fee_basis_points": 0,
"creators": null,
"primary_sale_happened": false,
"is_mutable": true,
"token_standard": "NonFungible",
"collection": null,
"uses": null,
"collection_details": null,
"rule_set": null,
"decimals": null,
"print_supply": "Zero"
}}
});
}
#[cfg(test)]
mod tests {
#[test]
fn native_fixture_contract_uses_profile_authorities() {
let authority = solana_pubkey::Pubkey::new_unique();
let mut mint = [0_u8; 82];
mint[0..4].copy_from_slice(&1_u32.to_le_bytes());
mint[4..36].copy_from_slice(authority.as_ref());
mint[44] = 0;
mint[45] = 1;
mint[46..50].copy_from_slice(&1_u32.to_le_bytes());
mint[50..82].copy_from_slice(authority.as_ref());
let decoded_mint = super::decode_coption_pubkey(&mint[0..36], "mint authority");
let decoded_freeze = super::decode_coption_pubkey(&mint[46..82], "freeze authority");
let authority_text = authority.to_string();
assert_eq!(
decoded_mint.ok().flatten().as_deref(),
std::option::Option::Some(authority_text.as_str())
);
assert_eq!(
decoded_freeze.ok().flatten().as_deref(),
std::option::Option::Some(authority_text.as_str())
);
}
#[test]
fn create_contract_uses_classic_token_program_and_zero_print_supply() {
let operation = super::create_operation_json(
solana_pubkey::Pubkey::new_unique().to_string(),
solana_pubkey::Pubkey::new_unique().to_string(),
solana_pubkey::Pubkey::new_unique().to_string(),
solana_pubkey::Pubkey::new_unique().to_string(),
);
assert_eq!(
operation.get("spl_token_program").and_then(serde_json::Value::as_str),
std::option::Option::Some(kb_program_ids::SPL_TOKEN_PROGRAM_ID),
);
assert_eq!(
operation
.pointer("/create_args/V1/print_supply")
.and_then(serde_json::Value::as_str),
std::option::Option::Some("Zero"),
);
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_execution.rs
// version: 10
// file: kb-pipeline-demo-scenarios/src/solana.rs
// version: 11
//! Devnet Solana execution orchestration with canonical post-validation.

View File

@@ -0,0 +1,9 @@
// file: kb-pipeline-demo-scenarios/src/spl.rs
// version: 1
//! SPL-oriented reusable demo and Devnet validation scenarios.
pub(crate) mod associated_token_account;
pub(crate) mod memo;
pub(crate) mod token;
pub(crate) mod token_2022;

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_ata_execution.rs
// version: 9
// file: kb-pipeline-demo-scenarios/src/spl/associated_token_account.rs
// version: 10
//! Devnet ATA execution with stateful and canonical post-validation.
@@ -994,7 +994,8 @@ mod tests {
fn example_devnet_profile() -> kb_config::ProfileConfig {
let config =
match kb_config::parse_config_json(include_str!("../../config/example.config.json")) {
match kb_config::parse_config_json(include_str!("../../../config/example.config.json"))
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
};

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_memo_execution.rs
// version: 5
// file: kb-pipeline-demo-scenarios/src/spl/memo.rs
// version: 6
//! Devnet SPL Memo v4 execution with canonical post-validation.
@@ -755,7 +755,8 @@ fn decode_completed(summary: &kb_pipeline::DecodeReplaySummary) -> bool {
mod tests {
fn example_devnet_profile() -> kb_config::ProfileConfig {
let config =
match kb_config::parse_config_json(include_str!("../../config/example.config.json")) {
match kb_config::parse_config_json(include_str!("../../../config/example.config.json"))
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
};

View File

@@ -0,0 +1,7 @@
// file: kb-pipeline-demo-scenarios/src/spl/token.rs
// version: 1
//! Classic SPL Token demo and Devnet validation scenarios.
pub(crate) mod execution;
pub(crate) mod lifecycle;

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_execution.rs
// version: 9
// file: kb-pipeline-demo-scenarios/src/spl/token/execution.rs
// version: 10
//! Devnet classic SPL Token execution with stateful and canonical post-validation.
@@ -974,11 +974,12 @@ pub(crate) fn decode_completed(summary: &kb_pipeline::DecodeReplaySummary) -> bo
#[cfg(test)]
mod tests {
fn example_devnet_profile() -> kb_config::ProfileConfig {
let config =
match kb_config::parse_config_json(include_str!("../../config/example.config.json")) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
};
let config = match kb_config::parse_config_json(include_str!(
"../../../../config/example.config.json"
)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
};
return match crate::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(profile) => profile,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_lifecycle.rs
// version: 8
// file: kb-pipeline-demo-scenarios/src/spl/token/lifecycle.rs
// version: 9
//! Controlled Devnet lifecycle for freshly prepared classic SPL Token accounts.
@@ -1687,11 +1687,12 @@ mod tests {
}
fn example_devnet_profile() -> kb_config::ProfileConfig {
let config =
match kb_config::parse_config_json(include_str!("../../config/example.config.json")) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
};
let config = match kb_config::parse_config_json(include_str!(
"../../../../config/example.config.json"
)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
};
return match crate::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(profile) => profile,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),

View File

@@ -0,0 +1,10 @@
// file: kb-pipeline-demo-scenarios/src/spl/token_2022.rs
// version: 1
//! SPL Token-2022 demo and Devnet validation scenarios.
pub(crate) mod devnet_execution;
pub(crate) mod devnet_scenarios;
pub(crate) mod fixture;
pub(crate) mod metadata;
pub(crate) mod validation;

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_devnet_execution.rs
// version: 6
// file: kb-pipeline-demo-scenarios/src/spl/token_2022/devnet_execution.rs
// version: 7
//! Devnet Token-2022 execution with stateful and canonical post-validation.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_devnet_scenarios.rs
// version: 7
// file: kb-pipeline-demo-scenarios/src/spl/token_2022/devnet_scenarios.rs
// version: 8
//! Stable Devnet validation scenarios required to close milestone 0.4.6.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/token_2022_fixture.rs
// version: 2
// file: kb-pipeline-demo-scenarios/src/spl/token_2022/fixture.rs
// version: 3
//! Idempotent Token-2022 public scenario fixture preparation.

View File

@@ -0,0 +1,8 @@
// file: kb-pipeline-demo-scenarios/src/spl/token_2022/metadata.rs
// version: 1
//! Token-2022 embedded Token Metadata fixtures, campaigns and validation.
pub(crate) mod campaign;
pub(crate) mod fixture;
pub(crate) mod validation;

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_metadata_campaign.rs
// version: 3
// file: kb-pipeline-demo-scenarios/src/spl/token_2022/metadata/campaign.rs
// version: 4
//! Ordered confirmed Devnet campaign for the five Token-2022 Token Metadata instructions.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/token_2022_metadata_fixture.rs
// version: 2
// file: kb-pipeline-demo-scenarios/src/spl/token_2022/metadata/fixture.rs
// version: 3
//! Native Token-2022 mint preparation for the embedded Token Metadata Devnet campaign.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_metadata_validation.rs
// version: 2
// file: kb-pipeline-demo-scenarios/src/spl/token_2022/metadata/validation.rs
// version: 3
//! Conservative validation matrix for the `0.4.8-pre.012` Token Metadata campaign.
@@ -59,11 +59,11 @@ pub struct Token2022MetadataValidationMatrix {
}
/// Loads and validates the canonical Token Metadata Devnet matrix.
pub fn load_token_2022_metadata_validation_matrix(
) -> kb_core::Result<crate::Token2022MetadataValidationMatrix> {
pub fn load_token_2022_metadata_validation_matrix()
-> kb_core::Result<crate::Token2022MetadataValidationMatrix> {
let matrix = match serde_json::from_str::<crate::Token2022MetadataValidationMatrix>(
include_str!(
"../../test-fixtures/contract-matrices/SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json"
"../../../../../test-fixtures/contract-matrices/SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,
@@ -74,10 +74,10 @@ pub fn load_token_2022_metadata_validation_matrix(
));
},
};
match crate::validate_token_2022_metadata_validation_matrix(&matrix) {
return match crate::validate_token_2022_metadata_validation_matrix(&matrix) {
std::result::Result::Ok(()) => std::result::Result::Ok(matrix),
std::result::Result::Err(error) => std::result::Result::Err(error),
}
};
}
/// Validates campaign coverage and prevents promotion without complete evidence.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_validation.rs
// version: 6
// file: kb-pipeline-demo-scenarios/src/spl/token_2022/validation.rs
// version: 7
//! Machine-readable validation evidence contract for the Token-2022 milestone.
@@ -112,7 +112,7 @@ pub struct Token2022ValidationMatrixScenario {
/// Loads and validates the canonical Token-2022 validation matrix.
pub fn load_token_2022_validation_matrix() -> kb_core::Result<crate::Token2022ValidationMatrix> {
let parsed = match serde_json::from_str::<crate::Token2022ValidationMatrix>(include_str!(
"../../test-fixtures/contract-matrices/SPL_TOKEN_2022_VALIDATION_MATRIX.json"
"../../../../test-fixtures/contract-matrices/SPL_TOKEN_2022_VALIDATION_MATRIX.json"
)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {

View File

@@ -1,8 +1,29 @@
<!-- file: kb-pipeline/CHANGELOG.md -->
<!-- version: 22 -->
<!-- version: 28 -->
# CHANGELOG — kb-pipeline
## `0.4.8-pre.013`
- `pre.013-delta-fix-031` expose `MetaplexTokenMetadataAccountKind::TokenOwnedEscrow` dans les lectures stateful et réutilise le décodeur strict déjà présent dans `kb-lib`, y compris validation du propriétaire, du discriminant, du PDA dautorité et du bump ;
- ajoute une régression de projection canonique pour un escrow `TokenOwner` et conserve `base_token`, `authority_kind`, `creator` et `stored_bump` comme postconditions queryables.
- `pre.013-delta-fix-030` conserve les simulations Metaplex exactes échouées comme résultats observables lorsque `submit=false`, afin que les probes de disponibilité puissent classifier un refus runtime sans le transformer en erreur d'orchestration ;
- maintient une barrière stricte pour toute soumission : `submit=true` exige toujours `simulation_succeeded=true`, et une simulation négative ne peut jamais produire `send_authorized=true` ;
- ajoute une régression dédiée au contrat « failed simulation reportable for probe only ».
- `pre.013-delta-fix-021` ajoute une régression stateful construisant un compte `Edition` compact de 42 octets et exige sa matérialisation autoritative, afin de couvrir directement lallocation produite par un `Print` courant ;
- enrichit `metaplex_stateful_decode_failed` avec ladresse et le `kind` de la requête, sans affaiblir le refus des comptes invalides.
- `pre.013-delta-fix-020` réembarque explicitement le variant public `MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition }` afin quun overlay incomplet de `fix-019` ne puisse plus laisser le scénario `Print -> Burn` compiler contre un contrat stateful ancien ;
- ajoute un test dAPI externe qui construit et sérialise une requête `EditionMarker` uniquement via les exports crate-root ;
- valide également les pubkeys de dérivation `mint` et `token` directement dans `MetaplexTokenMetadataStatefulReadRequest::validate`.
- `pre.013-delta-fix-019` ajoute `MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition }` aux lectures stateful afin de décoder et matérialiser explicitement `EditionMarker`/`EditionMarkerV2` lors des campagnes déditions imprimées ;
- la projection stateful conserve le groupe, lindex de bit, le masque, `edition_taken`, le ledger borné et le payload canonique sans inventer de sémantique de fermeture après burn.
## `0.4.8-pre.013` — borne stateful Metaplex
- aligne `MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES` sur `kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES`, soit `65536` octets pour une lecture complète `getAccountInfo` ;
- valide la borne dans `MetaplexTokenMetadataStatefulReadRequest` avant la construction de la requête RPC ;
- conserve séparées les bornes de décodage offline et la borne du transport d'exécution ;
- ajoute une régression empêchant de réintroduire une limite stateful supérieure au contrat du transport.
## `0.4.8-pre.011` — validation Token-2022 Token Metadata
- ajoute lextraction bornée du retour base64 produit par `Emit` ;

View File

@@ -1,5 +1,5 @@
<!-- file: kb-pipeline/README.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# kb-pipeline
@@ -18,7 +18,7 @@
- orchestration simulation-first, contrôles de readiness et postvalidation ;
- diagnostics stables exploitables par des applications ou scénarios.
Pour Metaplex Token Metadata, la crate expose les lectures stateful bornées, le préflight, la validation de readiness et lagrégation des postconditions.
Pour Metaplex Token Metadata, la crate expose les lectures stateful bornées, le préflight, la validation de readiness et lagrégation des postconditions. Une simulation exacte négative peut être conservée comme résultat observable uniquement lorsque `submit=false`; toute soumission continue dexiger une simulation réussie et ne peut jamais être autorisée à partir dun probe négatif.
Pour Solana Program Metadata, elle expose les mêmes frontières réutilisables pour les neuf opérations stables : lecture `Buffer`/`Metadata`, préflight stateful, gouvernance `ExSafetyChecker`, readiness simulation-first et postconditions. Elle ne contient ni fixture Devnet, ni wallet de démonstration, ni séquence métier spécifique à un test.

View File

@@ -1,8 +1,14 @@
<!-- file: kb-pipeline/USAGE.md -->
<!-- version: 10 -->
<!-- version: 16 -->
# Utilisation de kb-pipeline
## Lecture stateful des Edition Markers Metaplex
Les lectures stateful `Edition` acceptent les allocations Metaplex courantes après validation par `kb-lib`. Un `Print` récent produit notamment une allocation compacte de 42 octets alors que la structure Borsh `Edition` sérialise 41 octets ; le pipeline ne recalcule pas cette règle et consomme uniquement le snapshot déjà validé par le décodeur. Les erreurs de décodage stateful conservent ladresse et le `kind` demandés pour localiser la postcondition fautive.
Les campagnes déditions imprimées peuvent demander un snapshot borné `EditionMarker` en fournissant le mint maître et le numéro dédition. Le pipeline dérive et vérifie le contrat du compte via le décodeur Metaplex existant, puis expose notamment `marker_group`, `edition`, `byte_index`, `bit_mask`, `edition_taken` et le ledger. Cette lecture sert à prouver quun numéro dédition a été consommé ; elle ne suppose pas que le marker est fermé lors dun burn. La validation de requête contrôle aussi les pubkeys utilisées pour les dérivations `Edition`, `EditionMarker` et `TokenRecord` avant tout accès RPC.
## Objectif
La crate expose les campagnes bornées dacquisition, dextraction, de replay et les inspections stateful nécessaires aux applications et scénarios.
@@ -261,7 +267,9 @@ async fn read_metaplex_metadata(
}
```
Pour une edition, fournir le mint dans `MetaplexTokenMetadataAccountKind::Edition`. Pour un token record programmable, fournir le mint et le token account dans `TokenRecord`.
Pour une edition, fournir le mint dans `MetaplexTokenMetadataAccountKind::Edition`. Pour un token record programmable, fournir le mint et le token account dans `TokenRecord`. Pour un Token Owned Escrow, utiliser `TokenOwnedEscrow` : le décodeur récupère le mint parent et lautorité depuis le compte, puis valide lui-même le PDA et le bump avant projection stateful.
`MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES` représente la borne d'une lecture RPC complète et suit `kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES`, soit actuellement `65536` octets. Cette borne stateful ne doit pas être confondue avec les bornes propres aux décodeurs pour des octets déjà disponibles offline ou avec un futur lecteur par tranches.
## Inspecter le préflight Metaplex
@@ -326,6 +334,8 @@ fn validate_metaplex_execution(
Pour une soumission réelle, `submit` et `operator_confirmed` doivent être vrais et le plan ne doit plus être en `dry_run`.
Une requête `submit=false` peut conserver une simulation exacte ayant échoué afin quun appelant classe explicitement une indisponibilité runtime. Dans ce cas, le rapport reste non soumettable et contient le marqueur `failed_simulation_retained_for_probe_only`. Avec `submit=true`, une simulation échouée reste une erreur de readiness et `send_authorized` ne peut jamais devenir vrai.
## Agréger les postconditions Metaplex
```rust

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline/src/metadata_metaplex_token_metadata_execution_orchestration.rs
// version: 2
// version: 3
//! Simulation-first Metaplex Token Metadata execution orchestration.
@@ -42,11 +42,13 @@ pub struct MetaplexTokenMetadataExecutionReadinessReport {
pub required_signers: std::vec::Vec<kb_lib::MdPubkey>,
/// Whether signing and submission are authorized.
pub send_authorized: bool,
/// Ordered successful checks.
/// Ordered readiness checks, including a probe-only failed-simulation marker when applicable.
pub checks: std::vec::Vec<std::string::String>,
}
/// Validates simulation, preflight, signer and submission policies.
/// Validates preflight, exact simulation evidence, signers and submission policies.
///
/// Failed simulations remain observable only for `submit=false`; submission always requires success.
pub fn validate_metaplex_token_metadata_execution_readiness(
request: &crate::MetaplexTokenMetadataExecutionReadinessRequest,
) -> kb_core::Result<crate::MetaplexTokenMetadataExecutionReadinessReport> {
@@ -59,11 +61,16 @@ pub fn validate_metaplex_token_metadata_execution_readiness(
if request.message_hash.trim().is_empty()
|| request.message_hash != request.simulated_message_hash
|| !request.simulated
|| !request.simulation_succeeded
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_execution_simulation_required",
"Metaplex execution requires successful exact-message simulation",
"Metaplex execution requires exact-message simulation evidence",
));
}
if request.submit && !request.simulation_succeeded {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_execution_simulation_required",
"Metaplex submission requires successful exact-message simulation",
));
}
if request.resolved_signers.len() > crate::MAX_METAPLEX_TOKEN_METADATA_EXECUTION_SIGNERS {
@@ -108,10 +115,15 @@ pub fn validate_metaplex_token_metadata_execution_readiness(
required_signers: required.into_values().collect(),
send_authorized: request.submit
&& request.operator_confirmed
&& request.simulation_succeeded
&& !request.plan.policy.dry_run,
checks: vec![
"preflight_bound_to_operation".to_string(),
"simulation_bound_to_exact_message".to_string(),
if request.simulation_succeeded {
"simulation_bound_to_exact_message".to_string()
} else {
"failed_simulation_retained_for_probe_only".to_string()
},
"all_required_signers_resolved".to_string(),
"submission_policy_consistent".to_string(),
],
@@ -151,6 +163,64 @@ pub fn summarize_metaplex_token_metadata_postconditions(
#[cfg(test)]
mod tests {
#[test]
fn failed_simulation_is_reportable_only_without_submission() {
let fee_payer = kb_lib::MdPubkey("11111111111111111111111111111111".to_string());
let plan = kb_lib::ExApiPreparedExecutionPlan {
executor_name: "kb-lib.executor.metadata.metaplex_token_metadata".to_string(),
executor_version: "0.4.8".to_string(),
intent_id: "metaplex-failed-simulation-probe".to_string(),
operation_code: kb_lib::EX_METAPLEX_TOKEN_METADATA_USE_OPERATION.to_string(),
fee_payer: fee_payer.clone(),
instructions: vec![kb_lib::ExApiPlannedInstruction {
program_id: kb_lib::MdProgramId(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID.to_string(),
),
operation_code: kb_lib::EX_METAPLEX_TOKEN_METADATA_USE_OPERATION.to_string(),
accounts: vec![],
data: vec![51],
}],
required_signers: vec![],
policy: kb_lib::ExApiExecutionPolicy {
dry_run: false,
..kb_lib::ExApiExecutionPolicy::default()
},
requested_spend_lamports: 0,
requested_compute_unit_price_micro_lamports: std::option::Option::None,
};
let request = crate::MetaplexTokenMetadataExecutionReadinessRequest {
plan: plan.clone(),
preflight: crate::MetaplexTokenMetadataPreflightReport {
operation_code: plan.operation_code.clone(),
context_slot: 42,
inspected_accounts: vec![],
checks: vec![],
},
message_hash: "message-hash".to_string(),
simulated_message_hash: "message-hash".to_string(),
simulated: true,
simulation_succeeded: false,
resolved_signers: vec![],
submit: false,
operator_confirmed: false,
};
let report = crate::validate_metaplex_token_metadata_execution_readiness(&request);
assert!(report.is_ok());
if let std::result::Result::Ok(report) = report {
assert!(!report.send_authorized);
assert!(
report
.checks
.iter()
.any(|check| return check == "failed_simulation_retained_for_probe_only")
);
}
let mut submission = request;
submission.submit = true;
submission.operator_confirmed = true;
assert!(crate::validate_metaplex_token_metadata_execution_readiness(&submission).is_err());
}
#[test]
fn postconditions_never_convert_not_applicable_into_success() {
assert_eq!(

View File

@@ -1,10 +1,14 @@
// file: kb-pipeline/src/metadata_metaplex_token_metadata_stateful.rs
// version: 4
// version: 9
//! Bounded Metaplex Token Metadata account reads and canonical state projections.
/// Maximum complete Metaplex account data accepted by one RPC read.
pub const MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES: usize = 1_048_576;
///
/// This bound follows the complete `getAccountInfo` contract of the execution transport. Decoder
/// limits remain independent for already-available offline bytes or future chunked readers.
pub const MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES: usize =
kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES;
/// Supported Metaplex account category for one bounded stateful read.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@@ -17,6 +21,13 @@ pub enum MetaplexTokenMetadataAccountKind {
/// Mint used to derive the edition PDA.
mint: kb_lib::MdPubkey,
},
/// Edition marker PDA derived from the master mint and printed edition number.
EditionMarker {
/// Master mint used to derive the edition marker PDA.
mint: kb_lib::MdPubkey,
/// Printed edition number whose marker bit must be decoded.
edition: u64,
},
/// Programmable token record PDA derived from mint and token account.
TokenRecord {
/// Mint used to derive the token-record PDA.
@@ -24,6 +35,8 @@ pub enum MetaplexTokenMetadataAccountKind {
/// Token account used to derive the token-record PDA.
token: kb_lib::MdPubkey,
},
/// Token Owned Escrow PDA whose authority seeds and bump are self-validated by the decoder.
TokenOwnedEscrow,
}
/// One bounded Metaplex account read request.
@@ -41,6 +54,62 @@ pub struct MetaplexTokenMetadataStatefulReadRequest {
pub max_data_bytes: usize,
}
impl crate::MetaplexTokenMetadataStatefulReadRequest {
/// Validates the bounded complete-account RPC request.
pub fn validate(&self) -> kb_core::Result<()> {
if self.query_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex stateful read query_role must not be empty",
));
}
let account_result = kb_onchain_transport::validate_solana_pubkey_text(
self.account.0.as_str(),
"Metaplex stateful account",
);
if let std::result::Result::Err(error) = account_result {
return std::result::Result::Err(error);
}
let derivation_result = match &self.kind {
crate::MetaplexTokenMetadataAccountKind::Metadata => std::result::Result::Ok(()),
crate::MetaplexTokenMetadataAccountKind::Edition { mint }
| crate::MetaplexTokenMetadataAccountKind::EditionMarker { mint, .. } => {
kb_onchain_transport::validate_solana_pubkey_text(
mint.0.as_str(),
"Metaplex stateful derivation mint",
)
},
crate::MetaplexTokenMetadataAccountKind::TokenRecord { mint, token } => {
let mint_result = kb_onchain_transport::validate_solana_pubkey_text(
mint.0.as_str(),
"Metaplex stateful derivation mint",
);
if let std::result::Result::Err(error) = mint_result {
return std::result::Result::Err(error);
}
kb_onchain_transport::validate_solana_pubkey_text(
token.0.as_str(),
"Metaplex stateful derivation token",
)
},
crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow => {
std::result::Result::Ok(())
},
};
if let std::result::Result::Err(error) = derivation_result {
return std::result::Result::Err(error);
}
if self.max_data_bytes == 0
|| self.max_data_bytes > crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Metaplex stateful max_data_bytes must be between 1 and {}",
crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
)));
}
return std::result::Result::Ok(());
}
}
/// Canonical bounded Metaplex state snapshot.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MetaplexTokenMetadataStatefulSnapshot {
@@ -72,10 +141,8 @@ pub async fn read_metaplex_token_metadata_stateful_snapshot(
pool: &kb_onchain_transport::HttpEndpointPool,
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
) -> kb_core::Result<crate::MetaplexTokenMetadataStatefulReadResult> {
if request.query_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex stateful read query_role must not be empty",
));
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
let config = match kb_onchain_transport::GetAccountInfoConfig::new_with_data(
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
@@ -100,13 +167,8 @@ pub fn materialize_metaplex_token_metadata_account_info_result(
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
result: &kb_onchain_transport::AccountInfoResult,
) -> kb_core::Result<crate::MetaplexTokenMetadataStatefulReadResult> {
if request.max_data_bytes == 0
|| request.max_data_bytes > crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Metaplex stateful max_data_bytes must be between 1 and {}",
crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
)));
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(minimum) = request.min_context_slot {
if result.context.slot < minimum {
@@ -154,6 +216,19 @@ pub fn materialize_metaplex_token_metadata_account_info_result(
});
}
fn stateful_decode_error(
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
error: kb_lib::DcMetadataMtmMetadataAccountDecodeError,
) -> kb_core::Error {
return kb_core::Error::new(
"metaplex_stateful_decode_failed",
format!(
"Metaplex stateful decode failed for account {} kind {:?}: {error}",
request.account.0, request.kind
),
);
}
fn decode_snapshot(
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
slot: u64,
@@ -176,10 +251,9 @@ fn decode_snapshot(
payload_json: value.payload_json,
})
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
"metaplex_stateful_decode_failed",
error.to_string(),
)),
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::Edition { mint } => {
@@ -198,10 +272,46 @@ fn decode_snapshot(
payload_json: value.payload_json,
})
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
"metaplex_stateful_decode_failed",
error.to_string(),
)),
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition } => {
match kb_lib::decoder_metadata_metaplex_token_metadata_decode_edition_marker_account(
request.account.0.as_str(),
owner,
mint.0.as_str(),
*edition,
data,
) {
std::result::Result::Ok(value) => {
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
account: request.account.clone(),
account_kind: "edition_marker".to_string(),
mint: std::option::Option::Some(mint.clone()),
slot,
payload_json: serde_json::json!({
"account": value.account,
"bump": value.bump,
"mint": value.mint,
"kind": match value.kind {
kb_lib::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV1 => "edition_marker_v1",
kb_lib::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV2 => "edition_marker_v2",
},
"marker_group": value.marker_group,
"edition": value.edition,
"byte_index": value.byte_index,
"bit_mask": value.bit_mask,
"edition_taken": value.edition_taken,
"ledger": value.ledger,
"payload": value.payload_json,
}),
})
},
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::TokenRecord { mint, token } => {
@@ -221,10 +331,43 @@ fn decode_snapshot(
payload_json: value.payload_json,
})
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
"metaplex_stateful_decode_failed",
error.to_string(),
)),
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow => {
match kb_lib::decoder_metadata_metaplex_token_metadata_decode_token_owned_escrow_account(
request.account.0.as_str(),
owner,
data,
) {
std::result::Result::Ok(value) => {
let authority_kind = match value.authority_kind {
kb_lib::DcMetadataMtmTokenOwnedEscrowAuthorityKind::TokenOwner => {
"token_owner"
},
kb_lib::DcMetadataMtmTokenOwnedEscrowAuthorityKind::Creator => "creator",
};
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
account: request.account.clone(),
account_kind: "token_owned_escrow".to_string(),
mint: std::option::Option::Some(kb_lib::MdPubkey(value.base_token.clone())),
slot,
payload_json: serde_json::json!({
"account": value.account,
"bump": value.bump,
"base_token": value.base_token,
"authority_kind": authority_kind,
"creator": value.creator,
"stored_bump": value.stored_bump,
"payload": value.payload_json,
}),
})
},
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
};
@@ -252,6 +395,66 @@ mod tests {
std::result::Result::Err(error) => panic!("request deserialization failed: {error}"),
};
assert_eq!(decoded, request);
let marker = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::EditionMarker {
mint: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
edition: 1,
},
min_context_slot: std::option::Option::Some(43),
max_data_bytes: 1024,
};
let marker_json = match serde_json::to_string(&marker) {
std::result::Result::Ok(json) => json,
std::result::Result::Err(error) => {
panic!("marker request serialization failed: {error}")
},
};
let marker_decoded = match serde_json::from_str::<
crate::MetaplexTokenMetadataStatefulReadRequest,
>(marker_json.as_str())
{
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
panic!("marker request deserialization failed: {error}")
},
};
assert_eq!(marker_decoded, marker);
let escrow = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow,
min_context_slot: std::option::Option::Some(44),
max_data_bytes: 1024,
};
let escrow_json = match serde_json::to_string(&escrow) {
std::result::Result::Ok(json) => json,
std::result::Result::Err(error) => {
panic!("escrow request serialization failed: {error}")
},
};
let escrow_decoded = match serde_json::from_str::<
crate::MetaplexTokenMetadataStatefulReadRequest,
>(escrow_json.as_str())
{
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
panic!("escrow request deserialization failed: {error}")
},
};
assert_eq!(escrow_decoded, escrow);
let invalid_marker = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::EditionMarker {
mint: kb_lib::MdPubkey("invalid".to_string()),
edition: 1,
},
min_context_slot: std::option::Option::None,
max_data_bytes: 1024,
};
assert!(invalid_marker.validate().is_err());
}
#[test]
@@ -274,5 +477,124 @@ mod tests {
crate::materialize_metaplex_token_metadata_account_info_result(&request, &result)
.is_err()
);
let mut oversized = request;
oversized.max_data_bytes = kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES + 1;
assert!(oversized.validate().is_err());
}
#[test]
fn token_owned_escrow_materializes_with_canonical_authority_projection() {
let program = match kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
.parse::<solana_pubkey::Pubkey>()
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
};
let mint = solana_pubkey::Pubkey::new_from_array([6_u8; 32]);
let (account, bump) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program.as_ref(), mint.as_ref(), &[0], b"escrow"],
&program,
);
let mut data = std::vec![10_u8];
data.extend_from_slice(mint.as_ref());
data.push(0);
data.push(bump);
assert_eq!(data.len(), 35);
let request = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: kb_lib::MdPubkey(account.to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow,
min_context_slot: std::option::Option::Some(11),
max_data_bytes: 1024,
};
let result = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 11,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(kb_onchain_transport::AccountInfoValue {
lamports: 1_000,
owner: kb_lib::MdProgramId(program.to_string()),
executable: false,
rent_epoch: 0,
space: data.len() as u64,
data,
}),
};
let output =
crate::materialize_metaplex_token_metadata_account_info_result(&request, &result);
let snapshot = match output {
std::result::Result::Ok(value) => value.snapshot,
std::result::Result::Err(error) => panic!("escrow stateful decode failed: {error}"),
};
assert_eq!(snapshot.account_kind, "token_owned_escrow");
assert_eq!(snapshot.mint, std::option::Option::Some(kb_lib::MdPubkey(mint.to_string())));
assert_eq!(snapshot.payload_json["base_token"], serde_json::json!(mint.to_string()));
assert_eq!(snapshot.payload_json["authority_kind"], serde_json::json!("token_owner"));
assert_eq!(snapshot.payload_json["creator"], serde_json::Value::Null);
assert_eq!(snapshot.payload_json["stored_bump"], serde_json::json!(bump));
}
#[test]
fn compact_printed_edition_account_materializes_from_current_network_allocation() {
let program = match kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
.parse::<solana_pubkey::Pubkey>()
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
};
let mint = solana_pubkey::Pubkey::new_from_array([7_u8; 32]);
let parent = solana_pubkey::Pubkey::new_from_array([8_u8; 32]);
let (account, _) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program.as_ref(), mint.as_ref(), b"edition"],
&program,
);
let mut data = vec![1_u8];
data.extend_from_slice(parent.as_ref());
data.extend_from_slice(&1_u64.to_le_bytes());
data.push(0);
assert_eq!(data.len(), 42);
let request = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: kb_lib::MdPubkey(account.to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::Edition {
mint: kb_lib::MdPubkey(mint.to_string()),
},
min_context_slot: std::option::Option::Some(12),
max_data_bytes: 1024,
};
let result = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 12,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(kb_onchain_transport::AccountInfoValue {
lamports: 1_000,
owner: kb_lib::MdProgramId(program.to_string()),
executable: false,
rent_epoch: 0,
space: 42,
data,
}),
};
let output =
crate::materialize_metaplex_token_metadata_account_info_result(&request, &result);
let snapshot = match output {
std::result::Result::Ok(value) => value.snapshot,
std::result::Result::Err(error) => {
panic!("compact edition stateful decode failed: {error}")
},
};
assert_eq!(snapshot.account_kind, "edition");
assert_eq!(snapshot.payload_json["edition"], serde_json::json!(1));
assert_eq!(snapshot.payload_json["parent"], serde_json::json!(parent.to_string()));
}
#[test]
fn stateful_complete_read_bound_matches_the_transport_contract() {
assert_eq!(
crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES
);
}
}

View File

@@ -0,0 +1,24 @@
// file: kb-pipeline/tests/external_metadata_metaplex_token_metadata_pipeline_api.rs
// version: 1
//! External-only coverage for the public Metaplex Token Metadata pipeline contract.
#[test]
fn external_consumers_can_request_edition_marker_state_through_crate_root_exports() {
let account = kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([7_u8; 32]).to_string());
let mint = kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([8_u8; 32]).to_string());
let request = kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account,
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition: 1 },
min_context_slot: std::option::Option::Some(42),
max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
};
assert!(request.validate().is_ok());
let json = match serde_json::to_value(&request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("request serialization failed: {error}"),
};
assert_eq!(json["kind"]["kind"], "edition_marker");
assert_eq!(json["kind"]["edition"], 1);
}

View File

@@ -1,8 +1,13 @@
<!-- file: kb-wallet/CHANGELOG.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# CHANGELOG — kb-wallet
## `0.4.8-pre.013`
- `pre.013-delta-fix-021` ajoute `TemporaryWallet::as_sync_signer()` afin de préserver le marqueur `Sync` du `solana_keypair::Keypair` lorsque le signataire traverse un futur async qui doit être `Send` ;
- conserve `as_signer()` inchangé pour les consommateurs historiques et nexpose toujours aucun octet secret.
## 0.4.6
- alignement de la crate sur la version fonctionnelle bot3 `0.4.6` ;

View File

@@ -1,5 +1,5 @@
<!-- file: kb-wallet/USAGE.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# Utilisation de kb-wallet
@@ -61,6 +61,8 @@ println!("signature={signature}");
`as_signer()` retourne une référence au trait Solana `Signer` sans exposer les octets du keypair.
Pour une API async qui conserve la référence au travers dun `await` et impose un futur `Send`, `as_sync_signer()` préserve explicitement le marqueur `Sync` du keypair concret. Cette vue ne change ni la clé utilisée ni les règles dautorisation de la couche dexécution.
## Créer un stockage local
```rust

View File

@@ -1,5 +1,5 @@
// file: kb-wallet/src/wallet.rs
// version: 8
// version: 9
//! Local wallet storage and signing primitives.
@@ -104,6 +104,11 @@ impl crate::TemporaryWallet {
return &self.keypair;
}
/// Returns the signer interface with the `Sync` auto-trait preserved for `Send` async futures.
pub fn as_sync_signer(&self) -> &(dyn solana_signer::Signer + std::marker::Sync) {
return &self.keypair;
}
/// Signs arbitrary message bytes and returns the base58 signature.
pub fn sign_message(&self, message: &[u8]) -> kb_core::Result<std::string::String> {
let signature = match self.keypair.try_sign_message(message) {
@@ -437,6 +442,7 @@ mod tests {
.unwrap_or_else(|error| panic!("unexpected signing error: {error}"));
assert!(!signature.is_empty());
assert_eq!(wallet.public_key(), wallet.as_signer().pubkey().to_string());
assert_eq!(wallet.public_key(), wallet.as_sync_signer().pubkey().to_string());
assert!(wallet.summary().storage_path.is_none());
assert!(!format!("{wallet:?}").contains("secret"));
}

View File

@@ -1110,7 +1110,7 @@
}
],
"wire": "u8 discriminator + UseArgs or VerificationArgs enum",
"account_contract": "twelve positional accounts for Use; eight positional accounts for Verify/Unverify, with Metaplex placeholders for optional accounts",
"account_contract": "twelve positional accounts for Use; eight positional accounts for Verify and seven for Unverify, with Metaplex placeholders for optional accounts",
"runtime_registration": "enabled_in_kb_lib_pre_009"
},
{

View File

@@ -6,37 +6,272 @@
{
"id": "token_2022_metadata_initialize",
"operation": "initialize_token_metadata",
"status": "not_run",
"requiredEvidence": ["simulation_success", "signature", "confirmation", "canonical_hydration", "core_extraction", "decode_replay", "instruction_materialization", "stateful_postcondition", "idempotence_replay"],
"evidence": []
"status": "confirmed",
"requiredEvidence": [
"simulation_success",
"signature",
"confirmation",
"canonical_hydration",
"core_extraction",
"decode_replay",
"instruction_materialization",
"stateful_postcondition",
"idempotence_replay"
],
"evidence": [
{
"kind": "simulation_success",
"value": "true"
},
{
"kind": "signature",
"value": "48kzJR5VW1ZYSRkVrg6qyNmuHdAUu7NXcnzaNsSmH7SSmRXK1X6RQdCeeSQi6LhrrkgRFRGVnxkwXPtkiqG6EgnY"
},
{
"kind": "confirmation",
"value": "accepted confirmation at slot 481804441"
},
{
"kind": "canonical_hydration",
"value": "canonical transaction hydration completed"
},
{
"kind": "core_extraction",
"value": "core extraction completed"
},
{
"kind": "decode_replay",
"value": "Token-2022 decode replay completed"
},
{
"kind": "instruction_materialization",
"value": "1 Token Metadata materialization observed"
},
{
"kind": "stateful_postcondition",
"value": "Confirmed"
},
{
"kind": "idempotence_replay",
"value": "clean second replay"
}
]
},
{
"id": "token_2022_metadata_update_field",
"operation": "update_token_metadata_field",
"status": "not_run",
"requiredEvidence": ["simulation_success", "signature", "confirmation", "canonical_hydration", "core_extraction", "decode_replay", "instruction_materialization", "stateful_postcondition", "idempotence_replay"],
"evidence": []
"status": "confirmed",
"requiredEvidence": [
"simulation_success",
"signature",
"confirmation",
"canonical_hydration",
"core_extraction",
"decode_replay",
"instruction_materialization",
"stateful_postcondition",
"idempotence_replay"
],
"evidence": [
{
"kind": "simulation_success",
"value": "true"
},
{
"kind": "signature",
"value": "JrdB9EqePS1FVWLo55w1Vhv2eTCTfCpXZgMvPCocW3m3iu4nk3BFBBRHoe2Nbcpq1rmnZoFrhnag5fG2WwmJrYs"
},
{
"kind": "confirmation",
"value": "accepted confirmation at slot 481804454"
},
{
"kind": "canonical_hydration",
"value": "canonical transaction hydration completed"
},
{
"kind": "core_extraction",
"value": "core extraction completed"
},
{
"kind": "decode_replay",
"value": "Token-2022 decode replay completed"
},
{
"kind": "instruction_materialization",
"value": "1 Token Metadata materialization observed"
},
{
"kind": "stateful_postcondition",
"value": "Confirmed"
},
{
"kind": "idempotence_replay",
"value": "clean second replay"
}
]
},
{
"id": "token_2022_metadata_emit",
"operation": "emit_token_metadata",
"status": "not_run",
"requiredEvidence": ["simulation_success", "return_data", "signature", "confirmation", "canonical_hydration", "core_extraction", "decode_replay", "stateful_postcondition", "idempotence_replay"],
"evidence": []
"status": "confirmed",
"requiredEvidence": [
"simulation_success",
"return_data",
"signature",
"confirmation",
"canonical_hydration",
"core_extraction",
"decode_replay",
"stateful_postcondition",
"idempotence_replay"
],
"evidence": [
{
"kind": "simulation_success",
"value": "true"
},
{
"kind": "return_data",
"value": "202 decoded bytes"
},
{
"kind": "signature",
"value": "3TKdBCJhM1VEZ9ti6EYR9gSLJnH5ni6FjW4kvoqwdYK1u14jdY6hS8hf2VGeLi21PbX5CWiyzjthJZERK5KHroWH"
},
{
"kind": "confirmation",
"value": "accepted confirmation at slot 481804464"
},
{
"kind": "canonical_hydration",
"value": "canonical transaction hydration completed"
},
{
"kind": "core_extraction",
"value": "core extraction completed"
},
{
"kind": "decode_replay",
"value": "Token-2022 decode replay completed"
},
{
"kind": "stateful_postcondition",
"value": "Confirmed; returnData matches authoritative TLV snapshot"
},
{
"kind": "idempotence_replay",
"value": "clean second replay"
}
]
},
{
"id": "token_2022_metadata_remove_key",
"operation": "remove_token_metadata_key",
"status": "not_run",
"requiredEvidence": ["simulation_success", "signature", "confirmation", "canonical_hydration", "core_extraction", "decode_replay", "instruction_materialization", "stateful_postcondition", "idempotence_replay"],
"evidence": []
"status": "confirmed",
"requiredEvidence": [
"simulation_success",
"signature",
"confirmation",
"canonical_hydration",
"core_extraction",
"decode_replay",
"instruction_materialization",
"stateful_postcondition",
"idempotence_replay"
],
"evidence": [
{
"kind": "simulation_success",
"value": "true"
},
{
"kind": "signature",
"value": "3kDBV7pDFgoCQVJ9DYYuxvHFbCJzyA6J2rLg6ndGCETEhhFBZ1ipRQ52X2hZfwNPfdJwEFFBmbyMU6kFGnVXJNrm"
},
{
"kind": "confirmation",
"value": "accepted confirmation at slot 481804473"
},
{
"kind": "canonical_hydration",
"value": "canonical transaction hydration completed"
},
{
"kind": "core_extraction",
"value": "core extraction completed"
},
{
"kind": "decode_replay",
"value": "Token-2022 decode replay completed"
},
{
"kind": "instruction_materialization",
"value": "1 Token Metadata materialization observed"
},
{
"kind": "stateful_postcondition",
"value": "Confirmed"
},
{
"kind": "idempotence_replay",
"value": "clean second replay"
}
]
},
{
"id": "token_2022_metadata_update_authority",
"operation": "update_token_metadata_authority",
"status": "not_run",
"requiredEvidence": ["simulation_success", "signature", "confirmation", "canonical_hydration", "core_extraction", "decode_replay", "instruction_materialization", "stateful_postcondition", "idempotence_replay"],
"evidence": []
"status": "confirmed",
"requiredEvidence": [
"simulation_success",
"signature",
"confirmation",
"canonical_hydration",
"core_extraction",
"decode_replay",
"instruction_materialization",
"stateful_postcondition",
"idempotence_replay"
],
"evidence": [
{
"kind": "simulation_success",
"value": "true"
},
{
"kind": "signature",
"value": "5aBpQsm6mAHA5xSN5tZ1ubALbfghGa7JScjXn9NGhg9agzEdjXydH6jgmn6UmxinGHmMvtqaGRLk9dgjmmz4HpU2"
},
{
"kind": "confirmation",
"value": "accepted confirmation at slot 481804482"
},
{
"kind": "canonical_hydration",
"value": "canonical transaction hydration completed"
},
{
"kind": "core_extraction",
"value": "core extraction completed"
},
{
"kind": "decode_replay",
"value": "Token-2022 decode replay completed"
},
{
"kind": "instruction_materialization",
"value": "1 Token Metadata materialization observed"
},
{
"kind": "stateful_postcondition",
"value": "Confirmed"
},
{
"kind": "idempotence_replay",
"value": "clean second replay"
}
]
}
]
}