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,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) => {