From 4d08c75b2bcc15adda214781c01786d646c99ac1 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Mon, 3 Aug 2026 09:34:16 +0200 Subject: [PATCH] v0.4.7-pre.012 --- Cargo.toml | 4 +- ...METAPLEX_TOKEN_METADATA_COMPLETION_PLAN.md | 10 +- ...PRE_012_METAPLEX_DEVNET_RUNNER_COVERAGE.md | 54 +++ kb-pipeline-demo-scenarios/CHANGELOG.md | 25 +- kb-pipeline-demo-scenarios/Cargo.toml | 3 +- kb-pipeline-demo-scenarios/README.md | 2 +- kb-pipeline-demo-scenarios/TODO.md | 8 +- kb-pipeline-demo-scenarios/USAGE.md | 62 +++- kb-pipeline-demo-scenarios/src/lib.rs | 10 +- ...etaplex_token_metadata_devnet_execution.rs | 349 +++++++++++++++++- ...lana_metaplex_token_metadata_validation.rs | 24 +- ...solana_metaplex_token_metadata_stateful.rs | 26 +- ...OKEN_METADATA_DEVNET_EXECUTION_MATRIX.json | 34 +- 13 files changed, 575 insertions(+), 36 deletions(-) create mode 100644 docs/validation/V0_4_7_PRE_012_METAPLEX_DEVNET_RUNNER_COVERAGE.md diff --git a/Cargo.toml b/Cargo.toml index 7db00e1..6f8f0e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 18 +# version: 19 [workspace] resolver = "3" @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.4.7-pre.11" +version = "0.4.7-pre.12" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3" diff --git a/docs/plans/V0_4_7_METAPLEX_TOKEN_METADATA_COMPLETION_PLAN.md b/docs/plans/V0_4_7_METAPLEX_TOKEN_METADATA_COMPLETION_PLAN.md index f4224c1..1c7eb54 100644 --- a/docs/plans/V0_4_7_METAPLEX_TOKEN_METADATA_COMPLETION_PLAN.md +++ b/docs/plans/V0_4_7_METAPLEX_TOKEN_METADATA_COMPLETION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.4.7` — achèvement de Metaplex Token Metadata @@ -670,3 +670,11 @@ Le développement fonctionnel peut commencer après acceptation des recommandati - premier lot attribué à `Create`, `Update`, `Verify` et `Unverify` ; - preuves réseau encore `not_run` jusqu’aux exécutions locales et à la validation des postconditions métier ; - opérations dépréciées refusées dans les campagnes automatiques. + +## État de `0.4.7-pre.012` + +- le runner RPC réutilisable accepte les 20 opérations `executable_current` ; +- la matrice ne contient plus aucune opération au statut `planned` ; +- un test opt-in permet de fournir l’intent typé et les lectures stateful par JSON ; +- les statuts réseau restent `not_run` jusqu’aux campagnes locales avec fixtures ; +- la préparation des fixtures et la collecte des preuves observées restent obligatoires avant `pre.013`. diff --git a/docs/validation/V0_4_7_PRE_012_METAPLEX_DEVNET_RUNNER_COVERAGE.md b/docs/validation/V0_4_7_PRE_012_METAPLEX_DEVNET_RUNNER_COVERAGE.md new file mode 100644 index 0000000..e4a0612 --- /dev/null +++ b/docs/validation/V0_4_7_PRE_012_METAPLEX_DEVNET_RUNNER_COVERAGE.md @@ -0,0 +1,54 @@ + + + +# Validation `0.4.7-pre.012` — couverture du runner Devnet Metaplex + +## Résultat + +La matrice Devnet couvre exactement les 20 opérations `executable_current`. Les quatre opérations du premier lot restent attribuées à `pre.011` et les seize restantes à `pre.012`. Toutes portent désormais `runnerStatus: implemented`. + +## Accès aux campagnes + +`DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json` désérialise une opération typée, rejette les opérations dépréciées et produit une requête simulation-only. Le test opt-in `optional_devnet_current_operation_from_env` permet une simulation RPC réelle ou une soumission explicitement confirmée. + +## Correctif de compilation + +Le contrat `MetaplexTokenMetadataStatefulReadRequest` dérive désormais `Deserialize` et `Serialize`. Les listes JSON fournies par `KB_DEVNET_METAPLEX_PREFLIGHT_READS_JSON` et `KB_DEVNET_METAPLEX_POSTCONDITION_READS_JSON` peuvent donc être chargées par le test opt-in. Un test de round-trip JSON protège ce contrat. + +## Statut réseau + +Aucune preuve réseau n’est inventée dans ce livrable. Les 20 opérations restent `not_run` jusqu’à exécution locale avec fixtures, endpoint Devnet, wallet et préconditions compatibles. + +## Validations à exécuter localement + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --all-targets +python3 scripts/audit_rust_workspace_rules.py +cargo test -p kb-pipeline-demo-scenarios +``` + +## Correctif ergonomique `fix-002` + +Le placeholder `{"operation":"..."}` n'est plus présenté comme exécutable et est rejeté avec une erreur dédiée. La crate expose désormais : + +- l'inventaire des vingt opérations courantes ; +- des modèles JSON pour `Create`, `Update`, `Verify` et `Unverify` ; +- des constructeurs nommés pour `Verify` et `Unverify` créateur ; +- des tests vérifiant la validité JSON des modèles et le caractère conservateur des requêtes nommées. + +Les modèles utilisent des valeurs entre chevrons qui doivent être remplacées par des comptes Devnet réels. Ils ne constituent pas une preuve réseau. + + +## Correctif de dépendance et d’assertion `fix-003` + +La crate déclare désormais explicitement `mpl-token-metadata.workspace = true`, nécessaire aux types utilisés directement par les modèles de campagnes. Le test de rejet du placeholder attend le code stable `config`, conformément au contrat de `kb_core::Error::config`. + +Validations opérateur observées avant ce correctif : + +- `cargo fmt --all` : réussi ; +- `cargo check --workspace` : réussi ; +- `cargo clippy --all-targets` : réussi ; +- audit général, exports et règles Khadhroony : propres ; +- `cargo test -p kb-pipeline-demo-scenarios` : 53 tests réussis et un seul échec limité à l’assertion `configuration_error` au lieu de `config`. diff --git a/kb-pipeline-demo-scenarios/CHANGELOG.md b/kb-pipeline-demo-scenarios/CHANGELOG.md index 812d0e9..8fd6c66 100644 --- a/kb-pipeline-demo-scenarios/CHANGELOG.md +++ b/kb-pipeline-demo-scenarios/CHANGELOG.md @@ -1,8 +1,31 @@ - + # CHANGELOG — kb-pipeline-demo-scenarios +### Correctif `0.4.7-pre.012-fix-003` + +- ajout explicite de la dépendance workspace `mpl-token-metadata` requise par les modèles et constructeurs de scénarios ; +- alignement du test de placeholder sur le code stable `config` réellement exposé par `kb_core::Error::config` ; +- validation locale : formatage, workspace check, clippy et audits propres ; 53 tests de bibliothèque réussis avant correction de l’assertion, un seul test échouant sur le code attendu. + +### Correctif `0.4.7-pre.012-fix-002` + +- rejet explicite des placeholders `...` dans le JSON d’opération Metaplex ; +- inventaire public des vingt opérations courantes acceptées par les campagnes automatiques ; +- modèles JSON structurés pour `Create`, `Update`, `Verify` et `Unverify` ; +- constructeurs nommés pour les parcours créateur `Verify` et `Unverify` ; +- documentation corrigée afin de ne plus présenter un placeholder comme une commande exécutable. + +## 0.4.7-pre.012 — couverture du runner Devnet Metaplex + +- passage de la matrice Devnet à `0.4.7-pre.012` ; +- fermeture du statut `implemented` pour les 20 opérations courantes ; +- ajout d’un constructeur de requête depuis le JSON typé `ExMetaplexTokenMetadataOperation` ; +- ajout d’un test Devnet opt-in piloté par variables d’environnement pour simuler ou soumettre n’importe quelle opération courante ; +- maintien des statuts réseau à `not_run` tant qu’aucune exécution locale n’a fourni les preuves attendues. +- correction du contrat stateful partagé afin que les lectures préflight et postcondition soient désérialisables depuis les variables JSON du test Devnet opt-in. + ## 0.4.7-pre.011 — réouverture des campagnes Devnet Metaplex ### Exécution Devnet diff --git a/kb-pipeline-demo-scenarios/Cargo.toml b/kb-pipeline-demo-scenarios/Cargo.toml index 978d0cc..d9f7999 100644 --- a/kb-pipeline-demo-scenarios/Cargo.toml +++ b/kb-pipeline-demo-scenarios/Cargo.toml @@ -1,5 +1,5 @@ # file: kb-pipeline-demo-scenarios/Cargo.toml -# version: 3 +# version: 4 [package] name = "kb-pipeline-demo-scenarios" @@ -27,6 +27,7 @@ kb-pipeline = { path = "../kb-pipeline" } kb-program-ids = { path = "../kb-program-ids" } kb-store = { path = "../kb-store" } kb-wallet = { path = "../kb-wallet" } +mpl-token-metadata.workspace = true serde.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/kb-pipeline-demo-scenarios/README.md b/kb-pipeline-demo-scenarios/README.md index e659e0c..19a8842 100644 --- a/kb-pipeline-demo-scenarios/README.md +++ b/kb-pipeline-demo-scenarios/README.md @@ -19,7 +19,7 @@ La crate contient : - fournir des tests automatisés parallèles aux démonstrations UI, sans déplacer les scénarios UI hors du desktop ; - conserver la séparation entre preuve synthétique, simulation RPC, soumission confirmée et postcondition stateful. -La crate couvre déjà les scénarios Solana Core et SPL historiques ainsi que le corpus synthétique Metaplex pour NFT, SFT, token fongible, collection et pNFT. Un exécuteur Devnet réel et réutilisable est disponible pour les opérations Metaplex courantes pilotées par un seul wallet de profil. Il réalise le contrôle du genesis hash, les lectures stateful bornées, le préflight, la simulation RPC exacte et, après confirmation explicite, la soumission, la confirmation et les lectures avant/après. La préparation complète des fixtures et la couverture des 20 opérations restent réparties entre `0.4.7-pre.011` et `pre.012`. +La crate couvre déjà les scénarios Solana Core et SPL historiques ainsi que le corpus synthétique Metaplex pour NFT, SFT, token fongible, collection et pNFT. Un exécuteur Devnet réel et réutilisable est disponible pour les opérations Metaplex courantes pilotées par un seul wallet de profil. Il réalise le contrôle du genesis hash, les lectures stateful bornées, le préflight, la simulation RPC exacte et, après confirmation explicite, la soumission, la confirmation et les lectures avant/après. Le runner Devnet couvre désormais les 20 opérations courantes. Les fixtures concrètes, les exécutions observées et les preuves réseau restent à fournir opération par opération avant la clôture. ## Binaire CLI diff --git a/kb-pipeline-demo-scenarios/TODO.md b/kb-pipeline-demo-scenarios/TODO.md index fd90b20..8621528 100644 --- a/kb-pipeline-demo-scenarios/TODO.md +++ b/kb-pipeline-demo-scenarios/TODO.md @@ -1,11 +1,11 @@ - + # TODO — kb-pipeline-demo-scenarios ## `0.4.7-pre.011` — infrastructure et premier lot Devnet Metaplex -- [ ] Infrastructure - préparer des fixtures Devnet réutilisables pour les familles d’actifs nécessaires. +- [ ] Fixtures - préparer des fixtures Devnet réutilisables pour les familles d’actifs nécessaires. - [x] Exécution réseau - raccorder les intents Metaplex courants au transport RPC réel et au pipeline généraliste. - [x] Contrat de preuves - exposer cluster, genesis hash, simulation exacte, signature, confirmation et états stateful avant/après dans le résultat structuré. - [ ] Preuves observées - exécuter les campagnes locales et reporter les valeurs RPC effectivement observées dans la matrice. @@ -13,10 +13,10 @@ ## `0.4.7-pre.012` — couverture Devnet restante -- [ ] Couverture - fournir un scénario réel pour chacune des 20 opérations `executable_current`. +- [x] Couverture - fournir un runner réel commun et un contrat de campagne pour chacune des 20 opérations `executable_current`. - [ ] Soumission - soumettre et confirmer les opérations réalisables avec les fixtures disponibles. - [ ] Non-exécutabilité - documenter par opération toute impossibilité Devnet avec une cause technique et des préconditions vérifiées. -- [ ] Tests - empêcher toute promotion d’un résultat synthétique en preuve réseau. +- [x] Tests - empêcher toute promotion d’un résultat synthétique en preuve réseau. ## Versions ultérieures diff --git a/kb-pipeline-demo-scenarios/USAGE.md b/kb-pipeline-demo-scenarios/USAGE.md index 97bb236..f3ef34b 100644 --- a/kb-pipeline-demo-scenarios/USAGE.md +++ b/kb-pipeline-demo-scenarios/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de kb-pipeline-demo-scenarios @@ -278,3 +278,63 @@ fn load_metaplex_devnet_contract( ``` La matrice ne transforme jamais une implémentation disponible en validation réseau. Les valeurs restent `not_run` jusqu’à l’observation d’une simulation ou d’une confirmation réelle. +## Exécuter une opération Metaplex courante depuis un JSON typé + +Le runner Devnet commun accepte toute variante non dépréciée de `ExMetaplexTokenMetadataOperation`. Le test opt-in peut être lancé avec : + +```bash +KB_DEVNET_METAPLEX_EXECUTION_TEST=1 \ +KB_DEVNET_METAPLEX_OPERATION_JSON='{"operation":"..."}' \ +cargo test -p kb-pipeline-demo-scenarios optional_devnet_current_operation_from_env -- --nocapture +``` + +Variables optionnelles : + +- `KB_DEVNET_WALLET_DIR` pour sélectionner le répertoire du wallet persistant ; +- `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_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. + +## Préparer une campagne Metaplex courante + +Les exemples contenant `...` ne sont jamais exécutables. Pour connaître les opérations acceptées : + +```rust +let operations = + kb_pipeline_demo_scenarios::metaplex_token_metadata_current_operation_names(); +``` + +Les quatre premières campagnes disposent de modèles JSON structurés : + +```rust +fn verify_template() -> kb_core::Result { + kb_pipeline_demo_scenarios::metaplex_token_metadata_current_operation_json_template( + "verify", + ) +} +``` + +Les valeurs entre chevrons doivent être remplacées par les comptes Devnet réels. Le modèle peut ensuite être passé à `DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json`. + +Pour les parcours créateur, il est préférable de ne pas écrire de JSON manuellement : + +```rust +fn creator_verify_request( + authority: kb_lib::MdPubkey, + metadata: kb_lib::MdPubkey, +) -> kb_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionRequest { + kb_pipeline_demo_scenarios::devnet_metaplex_creator_verify_request( + "metaplex-verify-creator", + authority, + metadata, + ) +} +``` + +`devnet_metaplex_creator_unverify_request` fournit le parcours inverse. Les deux requêtes restent en simulation-only par défaut. + +Le test générique accepte toujours `KB_DEVNET_METAPLEX_OPERATION_JSON`, mais rejette explicitement le placeholder documentaire `...`. Les opérations plus complexes recevront des préparateurs de fixtures dédiés au lieu d’exiger de longs JSON écrits à la main. + diff --git a/kb-pipeline-demo-scenarios/src/lib.rs b/kb-pipeline-demo-scenarios/src/lib.rs index 401806b..f8f5957 100644 --- a/kb-pipeline-demo-scenarios/src/lib.rs +++ b/kb-pipeline-demo-scenarios/src/lib.rs @@ -1,5 +1,5 @@ // file: kb-pipeline-demo-scenarios/src/lib.rs -// version: 9 +// version: 10 #![forbid(unsafe_code)] #![deny(unreachable_pub)] @@ -72,8 +72,16 @@ pub use self::solana_memo_execution::execute_devnet_memo; pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionRequest; /// Complete evidence produced by one real Devnet Metaplex Token Metadata execution. pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionSummary; +/// Creates one named creator unverification request without handwritten JSON. +pub use self::solana_metaplex_token_metadata_devnet_execution::devnet_metaplex_creator_unverify_request; +/// Creates one named creator verification request without handwritten JSON. +pub use self::solana_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; +/// 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; +/// 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; /// Simulates one current Metaplex operation against a real Devnet endpoint. pub use self::solana_metaplex_token_metadata_devnet_execution::simulate_devnet_metaplex_token_metadata; /// Stable asset family covered by one Metaplex scenario. diff --git a/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_devnet_execution.rs b/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_devnet_execution.rs index 13997a1..c61d6df 100644 --- a/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_devnet_execution.rs +++ b/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_devnet_execution.rs @@ -1,5 +1,5 @@ // file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_devnet_execution.rs -// version: 1 +// version: 4 //! Real Devnet simulation and submission for current Metaplex Token Metadata operations. @@ -42,6 +42,40 @@ impl crate::DevnetMetaplexTokenMetadataExecutionRequest { }; } + /// Creates a conservative request from one serialized typed Metaplex operation. + /// + /// This entry point lets automated Devnet campaigns and the desktop reuse the + /// complete executor surface without duplicating one Rust constructor per + /// operation. The JSON must deserialize to `ExMetaplexTokenMetadataOperation`. + pub fn from_operation_json( + intent_id: impl std::convert::Into, + operation_json: &str, + ) -> kb_core::Result { + if operation_json.contains("\"operation\":\"...\"") + || operation_json.contains("\"operation\": \"...\"") + || operation_json.trim() == "..." + { + return std::result::Result::Err(kb_core::Error::config( + "Metaplex operation JSON still contains the documentation placeholder `...`; use metaplex_token_metadata_current_operation_json_template or one of the named request constructors", + )); + } + let operation = match serde_json::from_str::( + operation_json, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(kb_core::Error::config(format!( + "invalid typed Metaplex operation JSON: {error}" + ))); + }, + }; + let request = Self::new(intent_id, operation); + if let std::result::Result::Err(error) = request.validate() { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(request); + } + /// Validates request-local bounds and rejects deprecated operations. pub fn validate(&self) -> kb_core::Result<()> { if self.intent_id.trim().is_empty() { @@ -73,6 +107,164 @@ impl crate::DevnetMetaplexTokenMetadataExecutionRequest { } } +/// Returns the current operation names accepted by automatic Devnet campaigns. +pub fn metaplex_token_metadata_current_operation_names() -> &'static [&'static str] { + return &[ + "create", + "update_as_update_authority_v2", + "verify", + "unverify", + "burn", + "delegate", + "revoke", + "lock", + "unlock", + "transfer", + "close_accounts", + "create_escrow_account", + "close_escrow_account", + "transfer_out_of_escrow", + "mint", + "migrate", + "use", + "collect", + "print", + "resize", + ]; +} + +/// Returns a formatted JSON template for the first named Devnet campaign operations. +/// +/// Placeholders beginning with `<` must be replaced before parsing or execution. +pub fn metaplex_token_metadata_current_operation_json_template( + operation: &str, +) -> kb_core::Result { + let value = match operation { + "create" => serde_json::json!({ + "operation": "create", + "metadata": "", + "master_edition": "", + "mint": "", + "mint_as_signer": false, + "authority": "", + "update_authority": "", + "update_authority_as_signer": true, + "spl_token_program": null, + "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": null + } + } + }), + "update_as_update_authority_v2" => serde_json::json!({ + "operation": "update_as_update_authority_v2", + "authority": "", + "mint": "", + "metadata": "", + "edition": null, + "token": null, + "authorization_rules_program": null, + "authorization_rules": null, + "update_args": { + "AsUpdateAuthorityV2": { + "new_update_authority": null, + "data": null, + "primary_sale_happened": null, + "is_mutable": null, + "collection": null, + "collection_details": null, + "uses": null, + "rule_set": null, + "authorization_data": null + } + } + }), + "verify_creator" | "verify" => serde_json::json!({ + "operation": "verify", + "authority": "", + "delegate_record": null, + "metadata": "", + "collection_mint": null, + "collection_metadata": null, + "collection_master_edition": null, + "verification_args": "CreatorV1" + }), + "unverify_creator" | "unverify" => serde_json::json!({ + "operation": "unverify", + "authority": "", + "delegate_record": null, + "metadata": "", + "collection_mint": null, + "collection_metadata": null, + "verification_args": "CreatorV1" + }), + _ => { + return std::result::Result::Err(kb_core::Error::config(format!( + "no named JSON template for `{operation}`; current operations: {}", + metaplex_token_metadata_current_operation_names().join(", ") + ))); + }, + }; + return match serde_json::to_string_pretty(&value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(format!( + "unable to serialize Metaplex operation template: {error}" + ))), + }; +} + +/// Creates one named creator verification request without handwritten operation JSON. +pub fn devnet_metaplex_creator_verify_request( + intent_id: impl std::convert::Into, + authority: kb_lib::MdPubkey, + metadata: kb_lib::MdPubkey, +) -> crate::DevnetMetaplexTokenMetadataExecutionRequest { + return crate::DevnetMetaplexTokenMetadataExecutionRequest::new( + intent_id, + kb_lib::ExMetaplexTokenMetadataOperation::Verify { + authority, + delegate_record: std::option::Option::None, + metadata, + collection_mint: std::option::Option::None, + collection_metadata: std::option::Option::None, + collection_master_edition: std::option::Option::None, + verification_args: mpl_token_metadata::types::VerificationArgs::CreatorV1, + }, + ); +} + +/// Creates one named creator unverification request without handwritten operation JSON. +pub fn devnet_metaplex_creator_unverify_request( + intent_id: impl std::convert::Into, + authority: kb_lib::MdPubkey, + metadata: kb_lib::MdPubkey, +) -> crate::DevnetMetaplexTokenMetadataExecutionRequest { + return crate::DevnetMetaplexTokenMetadataExecutionRequest::new( + intent_id, + kb_lib::ExMetaplexTokenMetadataOperation::Unverify { + authority, + delegate_record: std::option::Option::None, + metadata, + collection_mint: std::option::Option::None, + collection_metadata: std::option::Option::None, + verification_args: mpl_token_metadata::types::VerificationArgs::CreatorV1, + }, + ); +} + /// Complete evidence produced by one real Devnet Metaplex execution. #[derive(Clone, Debug, PartialEq)] pub struct DevnetMetaplexTokenMetadataExecutionSummary { @@ -583,6 +775,34 @@ fn validate_profile_wallet_signers( #[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}"), + }; + 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}"), + }; + } + + fn optional_stateful_reads_from_env( + variable: &str, + ) -> std::vec::Vec { + let value = match std::env::var(variable) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::vec::Vec::new(), + }; + return match serde_json::from_str::< + std::vec::Vec, + >(value.as_str()) + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("invalid {variable}: {error}"), + }; + } + #[test] #[allow(deprecated)] fn request_rejects_deprecated_operations_and_empty_ids() { @@ -598,6 +818,133 @@ mod tests { assert!(empty.validate().is_err()); } + #[test] + fn typed_operation_json_rejects_invalid_and_deprecated_payloads() { + assert!( + crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json( + "invalid-json", + "{}", + ) + .is_err() + ); + let deprecated = serde_json::json!({ + "operation": "puff_metadata", + "metadata": "11111111111111111111111111111111" + }); + assert!( + crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json( + "deprecated-operation", + deprecated.to_string().as_str(), + ) + .is_err() + ); + } + + #[test] + fn named_templates_are_json_and_placeholder_is_rejected_explicitly() { + for operation in ["create", "update_as_update_authority_v2", "verify", "unverify"] { + let template = + super::metaplex_token_metadata_current_operation_json_template(operation); + assert!(template.is_ok()); + let template = match template { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert!(serde_json::from_str::(template.as_str()).is_ok()); + } + let placeholder = crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json( + "placeholder", + r#"{"operation":"..."}"#, + ); + assert_eq!( + placeholder.as_ref().err().map(kb_core::Error::code), + std::option::Option::Some("config"), + ); + } + + #[test] + fn named_creator_requests_are_conservative_and_current() { + let authority = kb_lib::MdPubkey("11111111111111111111111111111111".to_string()); + let metadata = kb_lib::MdPubkey("SysvarC1ock11111111111111111111111111111111".to_string()); + let verify = super::devnet_metaplex_creator_verify_request( + "verify-creator", + authority.clone(), + metadata.clone(), + ); + let unverify = super::devnet_metaplex_creator_unverify_request( + "unverify-creator", + authority, + metadata, + ); + assert!(!verify.submit); + assert!(!verify.operator_confirmed); + assert!(verify.validate().is_ok()); + assert!(unverify.validate().is_ok()); + } + + #[tokio::test] + async fn optional_devnet_current_operation_from_env() { + if std::env::var("KB_DEVNET_METAPLEX_EXECUTION_TEST").ok().as_deref() + != std::option::Option::Some("1") + { + return; + } + let operation_json = match std::env::var("KB_DEVNET_METAPLEX_OPERATION_JSON") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("KB_DEVNET_METAPLEX_OPERATION_JSON is required: {error}"); + }, + }; + let mut profile = example_devnet_profile(); + if let std::result::Result::Ok(directory) = std::env::var("KB_DEVNET_WALLET_DIR") { + profile.wallet.wallet_dir = directory; + } + let pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("HTTP pool creation 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"), + }; + let mut request = + match crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json( + format!("devnet-metaplex-test-{}", uuid::Uuid::new_v4()), + operation_json.as_str(), + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("Metaplex request parsing failed: {error}") + }, + }; + request.preflight_reads = + optional_stateful_reads_from_env("KB_DEVNET_METAPLEX_PREFLIGHT_READS_JSON"); + request.postcondition_reads = + optional_stateful_reads_from_env("KB_DEVNET_METAPLEX_POSTCONDITION_READS_JSON"); + request.submit = std::env::var("KB_DEVNET_METAPLEX_SUBMIT").ok().as_deref() + == std::option::Option::Some("1"); + request.operator_confirmed = request.submit + && std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref() + == std::option::Option::Some("1"); + let summary = match crate::execute_devnet_metaplex_token_metadata( + &pool, + &profile, + workspace_root, + &request, + &crate::NoopSolanaExecutionObserver, + ) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("Devnet Metaplex execution failed: {error}"), + }; + assert!(summary.simulation.success, "{:#?}", summary.simulation); + if request.submit { + assert!(summary.send_result.is_some()); + assert!(summary.confirmation.is_some()); + } + } + #[test] fn signer_guard_rejects_external_authorities() { assert!(super::validate_profile_wallet_signers(&["wallet".to_string()], "wallet").is_ok()); diff --git a/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs b/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs index dcfc208..9e201ad 100644 --- a/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs +++ b/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs @@ -1,5 +1,5 @@ // file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs -// version: 3 +// version: 4 //! Machine-readable Metaplex Token Metadata validation contract. @@ -506,13 +506,13 @@ 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.011" + || matrix.milestone != "0.4.7-pre.012" || 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.011", + "Metaplex Devnet matrix must declare exactly 20 current operations for pre.012", )); } let expected = [ @@ -552,6 +552,8 @@ pub fn validate_metaplex_token_metadata_devnet_execution_matrix( } for operation in &matrix.operations { 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() || !matches!( @@ -561,7 +563,7 @@ pub fn validate_metaplex_token_metadata_devnet_execution_matrix( { return std::result::Result::Err(kb_core::Error::new( "metaplex_devnet_execution_matrix_entry_invalid", - "Metaplex Devnet entries must be current, evidenced and conservatively classified", + "Metaplex Devnet entries must be current, implemented, evidenced and conservatively classified", )); } } @@ -588,7 +590,21 @@ mod devnet_execution_matrix_tests { .count(), 4, ); + assert_eq!( + matrix + .operations + .iter() + .filter(|operation| return operation.campaign_prerelease == "0.4.7-pre.012") + .count(), + 16, + ); assert!(matrix.operations.iter().all(|operation| return !operation.deprecated)); + assert!( + matrix + .operations + .iter() + .all(|operation| return operation.runner_status == "implemented") + ); assert!( matrix .operations diff --git a/kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs b/kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs index bfaa427..9ff2702 100644 --- a/kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs +++ b/kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs @@ -1,5 +1,5 @@ // file: kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs -// version: 2 +// version: 3 //! Bounded Metaplex Token Metadata account reads and canonical state projections. @@ -27,7 +27,7 @@ pub enum MetaplexTokenMetadataAccountKind { } /// One bounded Metaplex account read request. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct MetaplexTokenMetadataStatefulReadRequest { /// Endpoint role used for the HTTP RPC request. pub query_role: std::string::String, @@ -232,6 +232,28 @@ fn decode_snapshot( #[cfg(test)] mod tests { + #[test] + fn stateful_read_request_round_trips_through_json() { + let request = crate::MetaplexTokenMetadataStatefulReadRequest { + query_role: "rpc".to_string(), + account: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()), + kind: crate::MetaplexTokenMetadataAccountKind::Metadata, + min_context_slot: std::option::Option::Some(42), + max_data_bytes: 1024, + }; + let json = match serde_json::to_string(&request) { + std::result::Result::Ok(json) => json, + std::result::Result::Err(error) => panic!("request serialization failed: {error}"), + }; + let decoded = match serde_json::from_str::( + json.as_str(), + ) { + std::result::Result::Ok(decoded) => decoded, + std::result::Result::Err(error) => panic!("request deserialization failed: {error}"), + }; + assert_eq!(decoded, request); + } + #[test] fn rejects_zero_and_oversize_bounds_before_account_decoding() { let request = crate::MetaplexTokenMetadataStatefulReadRequest { diff --git a/test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json b/test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json index f93a865..12c03e8 100644 --- a/test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json +++ b/test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json @@ -1,6 +1,6 @@ { "matrixVersion": 1, - "milestone": "0.4.7-pre.011", + "milestone": "0.4.7-pre.012", "programId": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", "operations": [ { @@ -9,7 +9,7 @@ "operationCode": "metadata.metaplex_token_metadata.create_escrow_account", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -33,7 +33,7 @@ "operationCode": "metadata.metaplex_token_metadata.close_escrow_account", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -57,7 +57,7 @@ "operationCode": "metadata.metaplex_token_metadata.transfer_out_of_escrow", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -81,7 +81,7 @@ "operationCode": "metadata.metaplex_token_metadata.burn", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -129,7 +129,7 @@ "operationCode": "metadata.metaplex_token_metadata.mint", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -153,7 +153,7 @@ "operationCode": "metadata.metaplex_token_metadata.delegate", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -177,7 +177,7 @@ "operationCode": "metadata.metaplex_token_metadata.revoke", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -201,7 +201,7 @@ "operationCode": "metadata.metaplex_token_metadata.lock", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -225,7 +225,7 @@ "operationCode": "metadata.metaplex_token_metadata.unlock", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -249,7 +249,7 @@ "operationCode": "metadata.metaplex_token_metadata.migrate", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -273,7 +273,7 @@ "operationCode": "metadata.metaplex_token_metadata.transfer", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -321,7 +321,7 @@ "operationCode": "metadata.metaplex_token_metadata.use", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -393,7 +393,7 @@ "operationCode": "metadata.metaplex_token_metadata.collect", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -417,7 +417,7 @@ "operationCode": "metadata.metaplex_token_metadata.print", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -441,7 +441,7 @@ "operationCode": "metadata.metaplex_token_metadata.resize", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster", @@ -465,7 +465,7 @@ "operationCode": "metadata.metaplex_token_metadata.close_accounts", "deprecated": false, "campaignPrerelease": "0.4.7-pre.012", - "runnerStatus": "planned", + "runnerStatus": "implemented", "networkStatus": "not_run", "requiredEvidence": [ "cluster",