From d6cb4b0cce70356cd04198f3bc17337bb620b89c Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sun, 2 Aug 2026 11:58:34 +0200 Subject: [PATCH] 0.4.7-pre.008 --- Cargo.toml | 4 +- ...METAPLEX_TOKEN_METADATA_COMPLETION_PLAN.md | 11 +- ...TAPLEX_SYNTHETIC_AND_NETWORK_VALIDATION.md | 30 +++ kb-pipeline-demo-scenarios/CHANGELOG.md | 8 +- kb-pipeline-demo-scenarios/README.md | 3 +- kb-pipeline-demo-scenarios/TODO.md | 8 +- kb-pipeline-demo-scenarios/USAGE.md | 32 ++- kb-pipeline-demo-scenarios/src/lib.rs | 26 ++- ...olana_metaplex_token_metadata_scenarios.rs | 170 ++++++++++++++++ ...lana_metaplex_token_metadata_validation.rs | 192 ++++++++++++++++++ ...PLEX_TOKEN_METADATA_VALIDATION_MATRIX.json | 61 ++++++ 11 files changed, 534 insertions(+), 11 deletions(-) create mode 100644 docs/validation/V0_4_7_PRE_008_METAPLEX_SYNTHETIC_AND_NETWORK_VALIDATION.md create mode 100644 kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_scenarios.rs create mode 100644 kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs create mode 100644 test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_VALIDATION_MATRIX.json diff --git a/Cargo.toml b/Cargo.toml index acea3a1..208ac96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 14 +# version: 15 [workspace] resolver = "3" @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.4.7-pre.7" +version = "0.4.7-pre.8" 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 01ea827..452128f 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 @@ -583,3 +583,12 @@ Le développement fonctionnel peut commencer après acceptation des recommandati - orchestration simulation-first, résolution des signers et postconditions explicites ; - aucune dépendance vers `kb-pipeline-demo-scenarios` ; - les fixtures et campagnes réseau restent réservées à `pre.008`. + + +## État de `0.4.7-pre.008` + +- inventaire synthétique fermé pour NFT, SFT, fungible, collection et pNFT ; +- matrice automatisée de validation Metaplex avec preuves bornées ; +- aucune soumission automatique ; +- CLI inchangé faute de besoin démontré pour une fixture Metaplex dédiée ; +- scénarios UI du desktop conservés pour `pre.009`. diff --git a/docs/validation/V0_4_7_PRE_008_METAPLEX_SYNTHETIC_AND_NETWORK_VALIDATION.md b/docs/validation/V0_4_7_PRE_008_METAPLEX_SYNTHETIC_AND_NETWORK_VALIDATION.md new file mode 100644 index 0000000..e1008bc --- /dev/null +++ b/docs/validation/V0_4_7_PRE_008_METAPLEX_SYNTHETIC_AND_NETWORK_VALIDATION.md @@ -0,0 +1,30 @@ + + + +# Validation `0.4.7-pre.008` — scénarios Metaplex + +## Couverture + +- NFT ; +- SFT ; +- token fongible ; +- collection ; +- programmable NFT. + +## Contrats + +Les scénarios synthétiques sont déterministes et simulation-safe. La matrice réseau distingue `not_run`, `synthetic_validated`, `simulated`, `submitted`, `confirmed`, `unavailable` et `failed`. Aucun statut réseau n’est déclaré sans preuve observée. + +## CLI + +Aucune nouvelle commande n’est ajoutée : le CLI reste destiné à la préparation Token-2022 tant qu’une fixture Metaplex dédiée n’est pas nécessaire. + +## Validation locale à exécuter + +```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 +``` diff --git a/kb-pipeline-demo-scenarios/CHANGELOG.md b/kb-pipeline-demo-scenarios/CHANGELOG.md index 93755f3..c40b722 100644 --- a/kb-pipeline-demo-scenarios/CHANGELOG.md +++ b/kb-pipeline-demo-scenarios/CHANGELOG.md @@ -1,8 +1,14 @@ - + # CHANGELOG — kb-pipeline-demo-scenarios +## 0.4.7-pre.008 + +- ajout de l’inventaire synthétique Metaplex NFT, SFT, fungible, collection et pNFT ; +- ajout de la matrice de validation automatisée Devnet/Testnet et de ses preuves bornées ; +- conservation du CLI sans nouvelle commande, aucune fixture Metaplex réseau dédiée n’étant encore nécessaire. + ## 0.4.6 - alignement de la crate sur la version fonctionnelle bot3 `0.4.6` ; diff --git a/kb-pipeline-demo-scenarios/README.md b/kb-pipeline-demo-scenarios/README.md index 22d0a24..94c49e2 100644 --- a/kb-pipeline-demo-scenarios/README.md +++ b/kb-pipeline-demo-scenarios/README.md @@ -1,5 +1,5 @@ - + # kb-pipeline-demo-scenarios @@ -17,6 +17,7 @@ La crate est mixte : - simulation ou soumission explicitement autorisée ; - préparation de fixtures publiques ; - validation des matrices et rapports Token-2022 ; +- scénarios synthétiques et matrice de validation Metaplex Token Metadata ; - progression observable et annulation coopérative. Elle ne remplace ni le pipeline générique ni l’application desktop. diff --git a/kb-pipeline-demo-scenarios/TODO.md b/kb-pipeline-demo-scenarios/TODO.md index d435f5d..1ef618a 100644 --- a/kb-pipeline-demo-scenarios/TODO.md +++ b/kb-pipeline-demo-scenarios/TODO.md @@ -1,12 +1,12 @@ - + # TODO — kb-pipeline-demo-scenarios -## Entre `0.4.6` et `0.4.7` +## `0.4.7` -- [ ] Metaplex Token Metadata - ajouter les scénarios nécessaires à la clôture fonctionnelle de `0.4.7`. -- [ ] Tests Devnet - ajouter les validations de bout en bout Metaplex sans dépendre du desktop. +- [ ] Tests réseau Metaplex - exécuter les validations Devnet/Testnet lorsque les fixtures, comptes et fonds nécessaires sont disponibles. +- [ ] Preuves Metaplex - renseigner les signatures, simulations et postconditions observées sans déclarer de validation réseau non exécutée. ## Série `0.5.x` diff --git a/kb-pipeline-demo-scenarios/USAGE.md b/kb-pipeline-demo-scenarios/USAGE.md index 819d12e..99261aa 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 @@ -179,3 +179,33 @@ Les tests Devnet restent opt-in et peuvent produire des transactions réelles lo - cette crate est destinée aux démonstrations, validations et outils opérateur, pas au moteur de production autonome ; - les scénarios réseau exigent un endpoint, un wallet et des fonds compatibles ; - la bibliothèque ne fournit pas d’interface graphique. + + +## Charger les scénarios synthétiques Metaplex + +```rust +fn metaplex_scenario_ids() -> Vec { + kb_pipeline_demo_scenarios::metaplex_token_metadata_synthetic_scenarios() + .iter() + .map(|scenario| scenario.id.clone()) + .collect() +} +``` + +L’inventaire couvre NFT, SFT, token fongible, collection et pNFT. Ces scénarios sont déterministes et ne soumettent aucune transaction. + +## Charger la matrice de validation Metaplex + +```rust +fn load_metaplex_matrix( +) -> kb_core::Result { + let result = kb_pipeline_demo_scenarios::load_metaplex_token_metadata_validation_matrix(); + + match result { + Ok(matrix) => Ok(matrix), + Err(error) => Err(error), + } +} +``` + +Une validation réseau ne peut passer à `confirmed` que lorsque toutes les preuves déclarées sont présentes. Les statuts `not_run` et `unavailable` n’acceptent aucune preuve observée. diff --git a/kb-pipeline-demo-scenarios/src/lib.rs b/kb-pipeline-demo-scenarios/src/lib.rs index 6694e26..cd723a6 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: 5 +// version: 6 #![forbid(unsafe_code)] #![deny(unreachable_pub)] @@ -12,6 +12,8 @@ mod environment; mod solana_ata_execution; mod solana_execution; mod solana_memo_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_validation; @@ -65,6 +67,28 @@ pub use self::solana_memo_execution::DevnetMemoExecutionRequest; 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; +/// Stable asset family covered by one Metaplex scenario. +pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataAssetFamily; +/// One stable Metaplex scenario reusable by automated tests and demo adapters. +pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataScenario; +/// Execution mode accepted by one Metaplex scenario. +pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataScenarioMode; +/// Returns the complete ordered synthetic Metaplex scenario inventory. +pub use self::solana_metaplex_token_metadata_scenarios::metaplex_token_metadata_synthetic_scenarios; +/// Maximum evidence entries accepted by one Metaplex validation scenario. +pub use self::solana_metaplex_token_metadata_validation::MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE; +/// One bounded Metaplex validation evidence entry. +pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataValidationEvidence; +/// Canonical Metaplex validation matrix. +pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataValidationMatrix; +/// One scenario declared by the canonical Metaplex validation matrix. +pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataValidationMatrixScenario; +/// Exact validation status for one Metaplex scenario. +pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataValidationStatus; +/// Loads and validates the canonical Metaplex validation matrix. +pub use self::solana_metaplex_token_metadata_validation::load_metaplex_token_metadata_validation_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. diff --git a/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_scenarios.rs b/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_scenarios.rs new file mode 100644 index 0000000..26f8ac0 --- /dev/null +++ b/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_scenarios.rs @@ -0,0 +1,170 @@ +// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_scenarios.rs +// version: 1 + +//! Deterministic Metaplex Token Metadata scenario inventory for tests and Devnet campaigns. + +/// Stable asset family covered by one Metaplex scenario. +#[derive( + Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize, +)] +#[serde(rename_all = "snake_case")] +pub enum MetaplexTokenMetadataAssetFamily { + /// Non-fungible token. + Nft, + /// Semi-fungible token. + Sft, + /// Fungible token with Metaplex metadata. + Fungible, + /// Collection metadata and verification flow. + Collection, + /// Programmable non-fungible token. + ProgrammableNft, +} + +/// Execution mode accepted by one scenario. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum MetaplexTokenMetadataScenarioMode { + /// Purely deterministic scenario using synthetic accounts and plans. + Synthetic, + /// Devnet or Testnet simulation without submission. + NetworkSimulation, + /// Explicitly authorized Devnet or Testnet submission. + NetworkSubmission, +} + +/// One stable Metaplex scenario reusable by automated tests and demo adapters. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct MetaplexTokenMetadataScenario { + /// Stable scenario identifier. + pub id: std::string::String, + /// Asset family exercised by the scenario. + pub asset_family: crate::MetaplexTokenMetadataAssetFamily, + /// Execution mode. + pub mode: crate::MetaplexTokenMetadataScenarioMode, + /// Stable operation codes executed in order. + pub operation_codes: std::vec::Vec, + /// Whether the scenario requires a collection account. + pub requires_collection: bool, + /// Whether the scenario requires programmable authorization rules. + pub requires_programmable_rules: bool, + /// Whether a confirmed postcondition is mandatory. + pub requires_postcondition: bool, +} + +/// Returns the complete ordered synthetic Metaplex scenario inventory. +pub fn metaplex_token_metadata_synthetic_scenarios() +-> std::vec::Vec { + return vec![ + crate::MetaplexTokenMetadataScenario { + id: "metaplex_nft_create_update".to_string(), + asset_family: crate::MetaplexTokenMetadataAssetFamily::Nft, + mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic, + operation_codes: vec![ + kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(), + kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(), + ], + requires_collection: false, + requires_programmable_rules: false, + requires_postcondition: true, + }, + crate::MetaplexTokenMetadataScenario { + id: "metaplex_sft_create_mint".to_string(), + asset_family: crate::MetaplexTokenMetadataAssetFamily::Sft, + mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic, + operation_codes: vec![ + kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(), + kb_lib::EX_METAPLEX_TOKEN_METADATA_MINT_OPERATION.to_string(), + ], + requires_collection: false, + requires_programmable_rules: false, + requires_postcondition: true, + }, + crate::MetaplexTokenMetadataScenario { + id: "metaplex_fungible_create_update".to_string(), + asset_family: crate::MetaplexTokenMetadataAssetFamily::Fungible, + mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic, + operation_codes: vec![ + kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(), + kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(), + ], + requires_collection: false, + requires_programmable_rules: false, + requires_postcondition: true, + }, + crate::MetaplexTokenMetadataScenario { + id: "metaplex_collection_verify_unverify".to_string(), + asset_family: crate::MetaplexTokenMetadataAssetFamily::Collection, + mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic, + operation_codes: vec![ + kb_lib::EX_METAPLEX_TOKEN_METADATA_VERIFY_OPERATION.to_string(), + kb_lib::EX_METAPLEX_TOKEN_METADATA_UNVERIFY_OPERATION.to_string(), + ], + requires_collection: true, + requires_programmable_rules: false, + requires_postcondition: true, + }, + crate::MetaplexTokenMetadataScenario { + id: "metaplex_pnft_delegate_transfer_revoke".to_string(), + asset_family: crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft, + mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic, + operation_codes: vec![ + kb_lib::EX_METAPLEX_TOKEN_METADATA_DELEGATE_OPERATION.to_string(), + kb_lib::EX_METAPLEX_TOKEN_METADATA_TRANSFER_OPERATION.to_string(), + kb_lib::EX_METAPLEX_TOKEN_METADATA_REVOKE_OPERATION.to_string(), + ], + requires_collection: false, + requires_programmable_rules: true, + requires_postcondition: true, + }, + ]; +} + +#[cfg(test)] +mod tests { + #[test] + fn synthetic_inventory_is_exact_unique_and_covers_every_required_asset_family() { + let scenarios = crate::metaplex_token_metadata_synthetic_scenarios(); + assert_eq!(scenarios.len(), 5); + let ids = scenarios + .iter() + .map(|scenario| return scenario.id.as_str()) + .collect::>(); + assert_eq!(ids.len(), scenarios.len()); + let families = scenarios + .iter() + .map(|scenario| return scenario.asset_family) + .collect::>( + ); + assert_eq!(families.len(), 5); + } + + #[test] + fn synthetic_scenarios_are_simulation_safe_and_require_postconditions() { + for scenario in crate::metaplex_token_metadata_synthetic_scenarios() { + assert_eq!(scenario.mode, crate::MetaplexTokenMetadataScenarioMode::Synthetic); + assert!(!scenario.operation_codes.is_empty()); + assert!(scenario.requires_postcondition); + } + } + + #[test] + fn programmable_and_collection_requirements_are_declared_only_where_needed() { + let scenarios = crate::metaplex_token_metadata_synthetic_scenarios(); + let programmable = scenarios + .iter() + .filter(|scenario| return scenario.requires_programmable_rules) + .collect::>(); + assert_eq!(programmable.len(), 1); + assert_eq!( + programmable[0].asset_family, + crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft + ); + let collection = scenarios + .iter() + .filter(|scenario| return scenario.requires_collection) + .collect::>(); + assert_eq!(collection.len(), 1); + assert_eq!(collection[0].asset_family, crate::MetaplexTokenMetadataAssetFamily::Collection); + } +} 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 new file mode 100644 index 0000000..f3ac93c --- /dev/null +++ b/kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs @@ -0,0 +1,192 @@ +// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs +// version: 1 + +//! Machine-readable Metaplex Token Metadata validation contract. + +/// Maximum evidence entries accepted by one Metaplex validation scenario. +pub const MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE: usize = 64; + +/// Exact validation status for one Metaplex scenario. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum MetaplexTokenMetadataValidationStatus { + /// Scenario has not been executed. + NotRun, + /// Scenario passed deterministic synthetic validation. + SyntheticValidated, + /// Scenario was simulated on Devnet or Testnet. + Simulated, + /// Scenario was submitted but confirmation evidence is incomplete. + Submitted, + /// Scenario was confirmed and all postconditions passed. + Confirmed, + /// Scenario is unavailable in the selected environment. + Unavailable, + /// Scenario ran and failed. + Failed, +} + +/// One bounded validation evidence entry. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct MetaplexTokenMetadataValidationEvidence { + /// Stable evidence kind. + pub kind: std::string::String, + /// Bounded evidence value. + pub value: std::string::String, +} + +/// One scenario declared by the canonical Metaplex validation matrix. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MetaplexTokenMetadataValidationMatrixScenario { + /// Stable scenario identifier. + pub id: std::string::String, + /// Required scenario mode. + pub mode: crate::MetaplexTokenMetadataScenarioMode, + /// Exact validation status. + pub status: crate::MetaplexTokenMetadataValidationStatus, + /// Evidence kinds required before confirmation. + pub required_evidence: std::vec::Vec, + /// Observed evidence. + #[serde(default)] + pub evidence: std::vec::Vec, +} + +/// Canonical Metaplex validation matrix. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MetaplexTokenMetadataValidationMatrix { + /// Matrix schema version. + pub matrix_version: u32, + /// Owning milestone. + pub milestone: std::string::String, + /// Ordered validation scenarios. + pub scenarios: std::vec::Vec, +} + +/// Loads and validates the canonical Metaplex validation matrix. +pub fn load_metaplex_token_metadata_validation_matrix() +-> kb_core::Result { + let parsed = + match serde_json::from_str::(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) + { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(parsed); +} + +/// Validates the canonical Metaplex validation matrix. +pub fn validate_metaplex_token_metadata_validation_matrix( + matrix: &crate::MetaplexTokenMetadataValidationMatrix, +) -> kb_core::Result<()> { + if matrix.matrix_version != 1 || matrix.milestone != "0.4.7" { + return std::result::Result::Err(kb_core::Error::new( + "metaplex_token_metadata_validation_matrix_contract_mismatch", + "Metaplex validation matrix must use version 1 for milestone 0.4.7", + )); + } + let expected = crate::metaplex_token_metadata_synthetic_scenarios() + .iter() + .map(|scenario| return scenario.id.clone()) + .collect::>(); + let actual = matrix + .scenarios + .iter() + .map(|scenario| return scenario.id.clone()) + .collect::>(); + if actual != expected { + return std::result::Result::Err(kb_core::Error::new( + "metaplex_token_metadata_validation_matrix_inventory_mismatch", + "Metaplex validation matrix inventory differs from the compiled scenario inventory", + )); + } + let mut ids = std::collections::BTreeSet::<&str>::new(); + for scenario in &matrix.scenarios { + if !ids.insert(scenario.id.as_str()) || scenario.required_evidence.is_empty() { + return std::result::Result::Err(kb_core::Error::new( + "metaplex_token_metadata_validation_matrix_scenario_invalid", + "Metaplex validation scenarios must be unique and declare required evidence", + )); + } + if scenario.evidence.len() > crate::MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE { + return std::result::Result::Err(kb_core::Error::new( + "metaplex_token_metadata_validation_evidence_limit_exceeded", + "Metaplex validation evidence exceeds the compiled bound", + )); + } + if matches!( + scenario.status, + crate::MetaplexTokenMetadataValidationStatus::NotRun + | crate::MetaplexTokenMetadataValidationStatus::Unavailable + ) && !scenario.evidence.is_empty() + { + return std::result::Result::Err(kb_core::Error::new( + "metaplex_token_metadata_validation_unobserved_with_evidence", + "Not-run or unavailable Metaplex scenarios must not contain evidence", + )); + } + if scenario.status == crate::MetaplexTokenMetadataValidationStatus::Confirmed { + let observed = scenario + .evidence + .iter() + .map(|evidence| return evidence.kind.as_str()) + .collect::>(); + if !scenario + .required_evidence + .iter() + .all(|kind| return observed.contains(kind.as_str())) + { + return std::result::Result::Err(kb_core::Error::new( + "metaplex_token_metadata_validation_confirmed_without_required_evidence", + "Confirmed Metaplex scenario lacks required evidence", + )); + } + } + } + return std::result::Result::Ok(()); +} + +#[cfg(test)] +mod tests { + #[test] + fn canonical_matrix_is_machine_readable_and_matches_synthetic_inventory() { + let result = crate::load_metaplex_token_metadata_validation_matrix(); + assert!(result.is_ok()); + if let std::result::Result::Ok(matrix) = result { + assert_eq!(matrix.scenarios.len(), 5); + } + } + + #[test] + fn confirmed_scenario_requires_every_declared_evidence_kind() { + let result = crate::load_metaplex_token_metadata_validation_matrix(); + assert!(result.is_ok()); + if let std::result::Result::Ok(mut matrix) = result { + matrix.scenarios[0].status = crate::MetaplexTokenMetadataValidationStatus::Confirmed; + assert!(crate::validate_metaplex_token_metadata_validation_matrix(&matrix).is_err()); + matrix.scenarios[0].evidence = matrix.scenarios[0] + .required_evidence + .iter() + .map(|kind| { + return crate::MetaplexTokenMetadataValidationEvidence { + kind: kind.clone(), + value: "synthetic-proof".to_string(), + }; + }) + .collect(); + assert!(crate::validate_metaplex_token_metadata_validation_matrix(&matrix).is_ok()); + } + } +} diff --git a/test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_VALIDATION_MATRIX.json b/test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_VALIDATION_MATRIX.json new file mode 100644 index 0000000..e0f0d9d --- /dev/null +++ b/test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_VALIDATION_MATRIX.json @@ -0,0 +1,61 @@ +{ + "matrixVersion": 1, + "milestone": "0.4.7", + "scenarios": [ + { + "id": "metaplex_nft_create_update", + "mode": "synthetic", + "status": "not_run", + "requiredEvidence": [ + "test_suite", + "prepared_plan", + "postcondition" + ], + "evidence": [] + }, + { + "id": "metaplex_sft_create_mint", + "mode": "synthetic", + "status": "not_run", + "requiredEvidence": [ + "test_suite", + "prepared_plan", + "postcondition" + ], + "evidence": [] + }, + { + "id": "metaplex_fungible_create_update", + "mode": "synthetic", + "status": "not_run", + "requiredEvidence": [ + "test_suite", + "prepared_plan", + "postcondition" + ], + "evidence": [] + }, + { + "id": "metaplex_collection_verify_unverify", + "mode": "synthetic", + "status": "not_run", + "requiredEvidence": [ + "test_suite", + "prepared_plan", + "postcondition" + ], + "evidence": [] + }, + { + "id": "metaplex_pnft_delegate_transfer_revoke", + "mode": "synthetic", + "status": "not_run", + "requiredEvidence": [ + "test_suite", + "prepared_plan", + "postcondition" + ], + "evidence": [] + } + ] +}