v0.4.7-pre.011

This commit is contained in:
2026-08-03 07:24:03 +02:00
parent e8a744828f
commit 4b727e2990
30 changed files with 2565 additions and 286 deletions

View File

@@ -1,23 +1,39 @@
<!-- file: kb-pipeline-demo-scenarios/CHANGELOG.md -->
<!-- version: 11 -->
<!-- version: 14 -->
# CHANGELOG — kb-pipeline-demo-scenarios
## 0.4.7-pre.010
## 0.4.7-pre.011 — réouverture des campagnes Devnet Metaplex
- ajout dun corpus fermé de validation croisée Metaplex couvrant outer/CPI, succès/échec, cinq familles dactifs et neuf classes derreur ;
- ajout de trois contrats de preuve réseau distincts pour simulation, soumission et postcondition ;
- refus machine-readable de toute promotion réseau fondée uniquement sur des preuves synthétiques.
### Exécution Devnet
## 0.4.7-pre.009-fix-001
- ajout dun exécuteur Metaplex Devnet réel, simulation-first et limité aux opérations non dépréciées ;
- contrôle du genesis hash Devnet, du wallet de profil, des signers et des plafonds de coût ;
- ajout des lectures stateful avant/après, du préflight, de la simulation RPC exacte, de la soumission contrôlée et de la confirmation ;
- ajout dune matrice fermée des 20 opérations courantes, avec `Create`, `Update`, `Verify` et `Unverify` attribuées au premier lot `pre.011` ;
- conservation de tous les statuts réseau à `not_run` tant quaucune preuve locale na été observée.
### Documentation
- réorganisation des quatre documents de crate ;
- ajout des lots `pre.011` et `pre.012` au TODO pour couvrir les 20 opérations courantes sur Devnet ;
- clarification du rôle ciblé du CLI et du maintien des scénarios UI dans le desktop.
## 0.4.7-pre.010 — corpus de validation croisée
- ajout dun corpus fermé couvrant outer/CPI, succès/échec, cinq familles dactifs et neuf classes derreur ;
- ajout de contrats distincts de preuve réseau pour simulation, soumission et postcondition ;
- refus de toute promotion réseau fondée uniquement sur des preuves synthétiques.
## 0.4.7-pre.009
- ajout de linventaire Metaplex Devnet simulation et de son test de couverture.
## 0.4.7-pre.008
- ajout de linventaire 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.
- ajout des scénarios synthétiques NFT, SFT, token fongible, collection et pNFT ;
- ajout de la matrice de validation Devnet/Testnet ;
- conservation du CLI sans commande Metaplex non justifiée.
## 0.4.6

View File

@@ -1,31 +1,37 @@
<!-- file: kb-pipeline-demo-scenarios/README.md -->
<!-- version: 3 -->
<!-- version: 6 -->
# kb-pipeline-demo-scenarios
`kb-pipeline-demo-scenarios` fournit des scénarios opérateur et Devnet réutilisables au-dessus de `kb-pipeline`.
`kb-pipeline-demo-scenarios` fournit des scénarios synthétiques et des campagnes Devnet/Testnet automatisables au-dessus de `kb-pipeline`.
La crate est mixte :
La crate contient :
- bibliothèque Rust `kb_pipeline_demo_scenarios` ;
- binaire `kb-pipeline-demo-scenarios-cli`.
- la bibliothèque Rust `kb_pipeline_demo_scenarios` ;
- le binaire ciblé `kb-pipeline-demo-scenarios-cli`.
## Responsabilités
- préparation de lenvironnement et du profil Devnet ;
- scénarios System Program, Memo v4, ATA, SPL Token et Token-2022 ;
- 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 ;
- corpus fermé de validation croisée outer/CPI, cas positifs/négatifs et preuves réseau exactes ;
- progression observable et annulation coopérative.
- préparer les profils, comptes et fixtures nécessaires aux campagnes de test ;
- exécuter des scénarios synthétiques déterministes ;
- exécuter des campagnes réseau explicites avec simulation ou soumission contrôlée ;
- produire des résultats structurés et des preuves vérifiables ;
- 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.
Elle ne remplace ni le pipeline générique ni lapplication desktop.
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`.
## Binaire CLI
Le CLI a été introduit pour les préparations de fixtures qui nécessitent une commande autonome, notamment Token-2022. Il nest pas linterface générale obligatoire de tous les scénarios. Une commande Metaplex ne doit être ajoutée que si une préparation de fixture ne peut pas être réalisée proprement par les tests ou APIs existantes.
## Relations avec le workspace
La crate dépend du pipeline généraliste. `kb-pipeline` ne dépend jamais delle. Le desktop peut réutiliser ses contrats, mais conserve ses propres scénarios UI Devnet/Testnet.
## Documentation
- [USAGE.md](USAGE.md)
- [TODO.md](TODO.md)
- [CHANGELOG.md](CHANGELOG.md)
- [Guide dutilisation](USAGE.md)
- [Travaux restant à réaliser](TODO.md)
- [Historique des changements](CHANGELOG.md)
- [Architecture du pipeline](../docs/architecture/PIPELINE_ARCHITECTURE.md)

View File

@@ -1,20 +1,28 @@
<!-- file: kb-pipeline-demo-scenarios/TODO.md -->
<!-- version: 4 -->
<!-- version: 7 -->
# TODO — kb-pipeline-demo-scenarios
## `0.4.7`
## `0.4.7-pre.011` — infrastructure et premier lot Devnet Metaplex
- [x] Corpus croisé Metaplex - fermer les chemins outer/CPI, cinq familles dactifs, échecs transactionnels et entrées invalides.
- [ ] 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.
- [ ] Infrastructure - préparer des fixtures Devnet réutilisables pour les familles dactifs 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.
- [ ] Premier lot - préparer puis valider sur Devnet `Create`, `Update`, `Verify` et `Unverify` avec le wallet de profil et les fixtures requises.
## Série `0.5.x`
## `0.4.7-pre.012` — couverture Devnet restante
- [ ] CLI - rendre chaque scénario public exécutable hors `kb-app-demo-desktop`.
- [ ] CLI - compléter les arguments, sorties structurées et diagnostics.
- [ ] Fixtures - améliorer la création, la réutilisation et la validation des fixtures publiques.
- [ ] Documentation - ajouter un guide opérateur des scénarios et de leurs effets réseau.
- [ ] Couverture - fournir un scénario réel 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 dun résultat synthétique en preuve réseau.
## Versions ultérieures
- [ ] CLI - ajouter uniquement les commandes de préparation de fixtures explicitement justifiées.
- [ ] Fixtures - améliorer la réutilisation, le nettoyage et la validation des fixtures publiques.
- [ ] Documentation - maintenir un guide opérateur des campagnes et de leurs effets réseau.
## Report conditionnel — registre ElGamal

View File

@@ -1,5 +1,5 @@
<!-- file: kb-pipeline-demo-scenarios/USAGE.md -->
<!-- version: 4 -->
<!-- version: 6 -->
# Utilisation de kb-pipeline-demo-scenarios
@@ -234,3 +234,47 @@ fn load_metaplex_cross_validation() -> kb_core::Result<usize> {
```
Les cas marqués `requiresNetworkEvidence` ne peuvent pas être déclarés validés à partir de preuves synthétiques. Une simulation, une soumission ou une confirmation exige les preuves RPC déclarées par le cas.
## Simuler une opération Metaplex réelle sur Devnet
LAPI réseau prend un intent Metaplex typé déjà lié aux comptes et autorités de la fixture. Le wallet persistant du profil doit être lunique signer requis.
```rust
async fn simulate_metaplex_operation<O>(
pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
operation: kb_lib::ExMetaplexTokenMetadataOperation,
observer: &O,
) -> kb_core::Result<kb_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionSummary>
where
O: kb_pipeline_demo_scenarios::SolanaExecutionObserver,
{
let request = kb_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionRequest::new(
"metaplex-devnet-simulation",
operation,
);
return kb_pipeline_demo_scenarios::simulate_devnet_metaplex_token_metadata(
pool,
profile,
workspace_root,
&request,
observer,
)
.await;
}
```
Pour une soumission, lappelant doit définir `submit = true` et `operator_confirmed = true`. Les opérations dépréciées sont refusées par cette API automatique. Les lectures `preflight_reads` et `postcondition_reads` doivent décrire les comptes Metadata, Edition, Token Record ou Collection nécessaires au scénario.
## Charger la matrice des 20 opérations courantes
```rust
fn load_metaplex_devnet_contract(
) -> kb_core::Result<kb_pipeline_demo_scenarios::MetaplexTokenMetadataDevnetExecutionMatrix> {
return kb_pipeline_demo_scenarios::load_metaplex_token_metadata_devnet_execution_matrix();
}
```
La matrice ne transforme jamais une implémentation disponible en validation réseau. Les valeurs restent `not_run` jusquà lobservation dune simulation ou dune confirmation réelle.

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/lib.rs
// version: 8
// version: 9
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
@@ -12,6 +12,7 @@ mod environment;
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;
@@ -67,6 +68,14 @@ 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;
/// Complete request for one real Devnet Metaplex Token Metadata execution.
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;
/// Executes one real Devnet Metaplex simulation or explicitly authorized submission.
pub use self::solana_metaplex_token_metadata_devnet_execution::execute_devnet_metaplex_token_metadata;
/// 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.
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataAssetFamily;
/// One stable Metaplex scenario reusable by automated tests and demo adapters.
@@ -83,6 +92,10 @@ pub use self::solana_metaplex_token_metadata_validation::MAX_METAPLEX_TOKEN_META
pub use self::solana_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;
/// Closed inventory of every current Metaplex operation requiring Devnet coverage.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataDevnetExecutionMatrix;
/// One current Metaplex operation in the closed Devnet execution matrix.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataDevnetExecutionOperation;
/// Stable failure category exercised by one negative case.
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataFailureClass;
/// Expected transaction outcome for one cross-validation case.
@@ -99,10 +112,14 @@ pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataVa
pub use self::solana_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;
/// Loads and validates the closed Devnet execution matrix.
pub use self::solana_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;
/// 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;
/// Validates exact current-operation coverage and conservative network statuses.
pub use self::solana_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.

View File

@@ -0,0 +1,608 @@
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_devnet_execution.rs
// version: 1
//! Real Devnet simulation and submission for current Metaplex Token Metadata operations.
/// Complete request for one real Devnet Metaplex Token Metadata execution.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexTokenMetadataExecutionRequest {
/// Stable caller-provided execution identifier.
pub intent_id: std::string::String,
/// Endpoint role used for reads, balance, blockhash and fee queries.
pub query_role: std::string::String,
/// Endpoint role used for simulation, submission and confirmation.
pub transaction_role: std::string::String,
/// Exact current Metaplex operation.
pub operation: kb_lib::ExMetaplexTokenMetadataOperation,
/// Bounded state reads required before execution.
pub preflight_reads: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest>,
/// Bounded state reads repeated after confirmed submission.
pub postcondition_reads: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest>,
/// Explicitly authorizes signing and submission after successful simulation.
pub submit: bool,
/// Explicit operator confirmation required for submission.
pub operator_confirmed: bool,
}
impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
/// Creates a conservative simulation-only request.
pub fn new(
intent_id: impl std::convert::Into<std::string::String>,
operation: kb_lib::ExMetaplexTokenMetadataOperation,
) -> Self {
return Self {
intent_id: intent_id.into(),
query_role: "http_queries".to_string(),
transaction_role: "http_transactions".to_string(),
operation,
preflight_reads: std::vec::Vec::new(),
postcondition_reads: std::vec::Vec::new(),
submit: false,
operator_confirmed: false,
};
}
/// Validates request-local bounds and rejects deprecated operations.
pub fn validate(&self) -> kb_core::Result<()> {
if self.intent_id.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Devnet Metaplex execution intent id must not be empty",
));
}
if self.query_role.trim().is_empty() || self.transaction_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Devnet Metaplex execution endpoint roles must not be empty",
));
}
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
{
return std::result::Result::Err(kb_core::Error::config(
"Devnet Metaplex execution stateful read limit exceeded",
));
}
let operation_code = self.operation.operation_code();
if kb_lib::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES.contains(&operation_code) {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_devnet_deprecated_operation_forbidden",
"automatic Devnet campaigns do not execute deprecated Metaplex operations",
));
}
return std::result::Result::Ok(());
}
}
/// Complete evidence produced by one real Devnet Metaplex execution.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexTokenMetadataExecutionSummary {
/// Profile used by the orchestration.
pub profile_name: std::string::String,
/// Exact classified cluster.
pub cluster: kb_lib::ExApiExecutionCluster,
/// Genesis hash returned by the selected endpoint.
pub genesis_hash: std::string::String,
/// Non-secret persistent wallet description.
pub wallet: kb_wallet::WalletSummary,
/// Wallet balance observed before planning.
pub balance_lamports: u64,
/// Stateful snapshots observed before simulation.
pub before: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadResult>,
/// Stateful preflight report bound to the exact plan.
pub stateful_preflight: kb_pipeline::MetaplexTokenMetadataPreflightReport,
/// Exact prepared Metaplex plan.
pub plan: kb_lib::ExApiPreparedExecutionPlan,
/// Recent blockhash used by the exact transaction.
pub latest_blockhash: kb_onchain_transport::LatestBlockhashResult,
/// Fee estimate for the exact compiled message.
pub fee: kb_onchain_transport::FeeForMessageResult,
/// Exact real RPC simulation result.
pub simulation: kb_lib::ExApiExecutionSimulationResult,
/// Readiness report proving exact-message simulation and signer resolution.
pub readiness: kb_pipeline::MetaplexTokenMetadataExecutionReadinessReport,
/// Submission result when explicitly authorized.
pub send_result: std::option::Option<kb_lib::ExApiExecutionSendResult>,
/// Confirmation result when submitted.
pub confirmation: std::option::Option<kb_lib::ExApiExecutionConfirmationResult>,
/// Stateful snapshots observed after confirmed submission.
pub after: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadResult>,
}
struct PreparedMetaplexExecution {
wallet: kb_wallet::TemporaryWallet,
unsigned: kb_lib::ExSolanaUnsignedTransaction,
evidence: kb_lib::ExSolanaSimulationEvidence,
summary: crate::DevnetMetaplexTokenMetadataExecutionSummary,
}
/// Simulates one current Metaplex operation against a real Devnet endpoint.
pub async fn simulate_devnet_metaplex_token_metadata<O>(
http_pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
observer: &O,
) -> kb_core::Result<crate::DevnetMetaplexTokenMetadataExecutionSummary>
where
O: crate::SolanaExecutionObserver,
{
if request.submit {
return std::result::Result::Err(kb_core::Error::config(
"simulate_devnet_metaplex_token_metadata requires submit=false",
));
}
let prepared =
match prepare_execution(http_pool, profile, workspace_root, request, observer).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(prepared.summary);
}
/// Executes one real Devnet Metaplex simulation or explicitly authorized submission.
pub async fn execute_devnet_metaplex_token_metadata<O>(
http_pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
observer: &O,
) -> kb_core::Result<crate::DevnetMetaplexTokenMetadataExecutionSummary>
where
O: crate::SolanaExecutionObserver,
{
let prepared =
match prepare_execution(http_pool, profile, workspace_root, request, observer).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !request.submit {
return std::result::Result::Ok(prepared.summary);
}
if !prepared.summary.simulation.success {
return std::result::Result::Err(kb_core::Error::new(
"execution_simulation_failed",
crate::simulation_failure_message(&prepared.summary.simulation),
));
}
if let std::result::Result::Err(error) = validate_profile_wallet_signers(
prepared.unsigned.required_signer_pubkeys(),
prepared.summary.wallet.public_key.as_str(),
) {
return std::result::Result::Err(error);
}
let send_evaluation = match kb_lib::ExSafetyChecker
.evaluate_send(&prepared.summary.plan, &prepared.summary.simulation)
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if send_evaluation.decision == kb_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(kb_core::Error::new(
"execution_send_denied",
crate::violation_message(send_evaluation.violations.as_slice()),
));
}
let signed = match prepared
.unsigned
.sign_after_simulation(&prepared.evidence, &[prepared.wallet.as_signer()])
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = signed.verify_signatures() {
return std::result::Result::Err(error);
}
let signature = signed.primary_signature().clone();
let mut summary = prepared.summary;
let send_config = match kb_onchain_transport::SendTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(summary.latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let sent = match http_pool
.send_transaction_for_role(
request.transaction_role.as_str(),
signed.transaction_base64().as_str(),
&signature,
&send_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
summary.send_result =
std::option::Option::Some(sent.to_execution_result(kb_lib::ExApiExecutionCluster::Devnet));
let confirmation_config =
match kb_onchain_transport::ConfirmTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(summary.latest_blockhash.last_valid_block_height),
std::option::Option::Some(summary.latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmation = match http_pool
.confirm_transaction_for_roles(
request.transaction_role.as_str(),
request.query_role.as_str(),
kb_lib::ExApiExecutionCluster::Devnet,
&signature,
&confirmation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmed = matches!(
confirmation.status,
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
);
summary.confirmation = std::option::Option::Some(confirmation);
if confirmed {
summary.after = match read_snapshots(http_pool, &request.postcondition_reads).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
}
return std::result::Result::Ok(summary);
}
async fn prepare_execution<O>(
http_pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
observer: &O,
) -> kb_core::Result<PreparedMetaplexExecution>
where
O: crate::SolanaExecutionObserver,
{
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = validate_profile(profile, request) {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) =
crate::ensure_not_cancelled(observer, "metaplex_validate")
{
return std::result::Result::Err(error);
}
let genesis = match http_pool.get_genesis_hash_for_role(request.query_role.as_str()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if genesis.classified_cluster
!= std::option::Option::Some(kb_lib::ExApiExecutionCluster::Devnet)
{
return std::result::Result::Err(kb_core::Error::new(
"execution_cluster_mismatch",
format!(
"expected Devnet genesis hash but endpoint returned {} classified as {:?}",
genesis.genesis_hash, genesis.classified_cluster
),
));
}
let wallet = match crate::load_profile_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallet_summary = wallet.summary();
let fee_payer = kb_lib::MdPubkey(wallet_summary.public_key.clone());
let balance = match http_pool
.get_balance_for_role(
request.query_role.as_str(),
&fee_payer,
&kb_onchain_transport::GetBalanceConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if balance.lamports < profile.execution.max_fee_lamports {
return std::result::Result::Err(kb_core::Error::new(
"execution_balance_insufficient",
"Devnet wallet balance is below the configured fee ceiling",
));
}
let plan = match build_plan(profile, request, fee_payer.clone()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let before = match read_snapshots(http_pool, &request.preflight_reads).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let stateful_preflight = match kb_pipeline::inspect_metaplex_token_metadata_preflight(
&kb_pipeline::MetaplexTokenMetadataPreflightRequest {
plan: plan.clone(),
snapshots: before.clone(),
allow_deprecated_operation: false,
},
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let plan_evaluation = match kb_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if plan_evaluation.decision == kb_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(kb_core::Error::new(
"execution_plan_denied",
crate::violation_message(plan_evaluation.violations.as_slice()),
));
}
let latest_blockhash = match http_pool
.get_latest_blockhash_for_role(
request.query_role.as_str(),
&kb_onchain_transport::GetLatestBlockhashConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned = match kb_lib::executor_solana_build_legacy_transaction(
&plan,
latest_blockhash.blockhash.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = validate_profile_wallet_signers(
unsigned.required_signer_pubkeys(),
wallet_summary.public_key.as_str(),
) {
return std::result::Result::Err(error);
}
let fee = match http_pool
.get_fee_for_message_for_role(
request.query_role.as_str(),
unsigned.message_base64().as_str(),
&kb_onchain_transport::GetFeeForMessageConfig::new(
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
std::option::Option::Some(latest_blockhash.context.slot),
),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned_base64 = match unsigned.transaction_base64() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_config = match kb_onchain_transport::SimulateTransactionConfig::new(
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
false,
false,
std::option::Option::Some(latest_blockhash.context.slot),
true,
std::option::Option::None,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::emit(
observer,
crate::SolanaExecutionProgressLevel::Info,
"metaplex_token_metadata_simulation",
format!("simulating exact Metaplex message {}", unsigned.message_hash()),
std::option::Option::None,
);
let simulation_rpc = match http_pool
.simulate_transaction_for_role(
request.transaction_role.as_str(),
unsigned_base64.as_str(),
&simulation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation = simulation_rpc.to_execution_result(
kb_lib::ExApiExecutionCluster::Devnet,
kb_lib::ExApiExecutionBlockhashKind::Latest,
std::option::Option::Some(
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(&fee),
);
let readiness = match kb_pipeline::validate_metaplex_token_metadata_execution_readiness(
&kb_pipeline::MetaplexTokenMetadataExecutionReadinessRequest {
plan: plan.clone(),
preflight: stateful_preflight.clone(),
message_hash: unsigned.message_hash().to_string(),
simulated_message_hash: unsigned.message_hash().to_string(),
simulated: true,
simulation_succeeded: simulation.success,
resolved_signers: vec![fee_payer],
submit: request.submit,
operator_confirmed: request.operator_confirmed,
},
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let evidence = unsigned.bind_simulation(simulation.clone());
return std::result::Result::Ok(PreparedMetaplexExecution {
wallet,
unsigned,
evidence,
summary: crate::DevnetMetaplexTokenMetadataExecutionSummary {
profile_name: profile.name.clone(),
cluster: kb_lib::ExApiExecutionCluster::Devnet,
genesis_hash: genesis.genesis_hash,
wallet: wallet_summary,
balance_lamports: balance.lamports,
before,
stateful_preflight,
plan,
latest_blockhash,
fee,
simulation,
readiness,
send_result: std::option::Option::None,
confirmation: std::option::Option::None,
after: std::vec::Vec::new(),
},
});
}
async fn read_snapshots(
http_pool: &kb_onchain_transport::HttpEndpointPool,
requests: &[kb_pipeline::MetaplexTokenMetadataStatefulReadRequest],
) -> kb_core::Result<std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadResult>> {
let mut results = std::vec::Vec::with_capacity(requests.len());
for request in requests {
let value =
match kb_pipeline::read_metaplex_token_metadata_stateful_snapshot(http_pool, request)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
results.push(value);
}
return std::result::Result::Ok(results);
}
fn build_plan(
profile: &kb_config::ProfileConfig,
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
fee_payer: kb_lib::MdPubkey,
) -> kb_core::Result<kb_lib::ExApiPreparedExecutionPlan> {
let intent = kb_lib::ExMetaplexTokenMetadataExecutionIntent {
intent_id: request.intent_id.clone(),
fee_payer: fee_payer.clone(),
max_rent_lamports: profile.execution.devnet_max_spend_lamports,
policy: kb_lib::ExApiExecutionPolicy {
cluster: kb_lib::ExApiExecutionClusterPolicy {
expected_cluster: kb_lib::ExApiExecutionCluster::Devnet,
allow_mainnet: false,
mainnet_confirmation: false,
},
simulation: kb_lib::ExApiExecutionSimulationPolicy::Required,
blockhash: kb_lib::ExApiExecutionBlockhashPolicy {
kind: kb_lib::ExApiExecutionBlockhashKind::Latest,
max_age_slots: std::option::Option::Some(
profile.execution.recent_blockhash_max_age_slots,
),
nonce_account: std::option::Option::None,
nonce_authority: std::option::Option::None,
},
cost_limit: kb_lib::ExApiExecutionCostLimit {
max_spend_lamports: std::option::Option::Some(
profile.execution.devnet_max_spend_lamports,
),
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
max_compute_unit_price_micro_lamports: std::option::Option::Some(
profile.execution.max_compute_unit_price_micro_lamports,
),
},
authorized_signers: vec![fee_payer],
dry_run: !request.submit,
post_execution_validation: kb_lib::ExApiPostExecutionValidationPolicy {
canonical_insert_required: request.submit,
core_extraction_required: request.submit,
decode_replay_required: request.submit,
materialization_required: request.submit,
},
},
allow_deprecated_operation: false,
operation: request.operation.clone(),
};
return kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
&kb_lib::ExMetadataMetaplexTokenMetadataExecutor,
&intent,
);
}
fn validate_profile(
profile: &kb_config::ProfileConfig,
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
) -> kb_core::Result<()> {
if profile.wallet.cluster != "devnet" {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex Devnet orchestration requires a Devnet wallet profile",
));
}
if !profile.wallet.temporary_wallet_enabled || !profile.wallet.temporary_wallet_persist {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex Devnet orchestration requires an enabled persistent temporary wallet",
));
}
if !profile.execution.require_simulation {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex Devnet orchestration requires simulation",
));
}
if request.submit && !profile.wallet.devnet_send_enabled {
return std::result::Result::Err(kb_core::Error::config(
"Devnet transaction submission is disabled by the wallet profile",
));
}
if request.submit
&& profile.execution.require_operator_confirmation
&& !request.operator_confirmed
{
return std::result::Result::Err(kb_core::Error::config(
"Metaplex Devnet submission requires explicit operator confirmation",
));
}
return std::result::Result::Ok(());
}
fn validate_profile_wallet_signers(
required_signers: &[std::string::String],
wallet_pubkey: &str,
) -> kb_core::Result<()> {
if required_signers.iter().any(|value| return value.as_str() != wallet_pubkey) {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_external_signer_unavailable",
format!(
"the Devnet Metaplex orchestrator can sign only with profile wallet {}; required signers are {}",
wallet_pubkey,
required_signers.join(",")
),
));
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
#[allow(deprecated)]
fn request_rejects_deprecated_operations_and_empty_ids() {
let deprecated = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
"deprecated",
kb_lib::ExMetaplexTokenMetadataOperation::PuffMetadata {
metadata: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
},
);
assert!(deprecated.validate().is_err());
let mut empty = deprecated;
empty.intent_id.clear();
assert!(empty.validate().is_err());
}
#[test]
fn signer_guard_rejects_external_authorities() {
assert!(super::validate_profile_wallet_signers(&["wallet".to_string()], "wallet").is_ok());
assert!(
super::validate_profile_wallet_signers(&["external".to_string()], "wallet").is_err()
);
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs
// version: 2
// version: 3
//! Machine-readable Metaplex Token Metadata validation contract.
@@ -438,3 +438,162 @@ mod tests {
}
}
}
/// One current Metaplex operation in the closed Devnet execution matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MetaplexTokenMetadataDevnetExecutionOperation {
/// Stable instruction discriminator.
pub discriminator: u8,
/// Official instruction name.
pub name: std::string::String,
/// Stable executor operation code.
pub operation_code: std::string::String,
/// Whether the operation is deprecated.
pub deprecated: bool,
/// Prerelease responsible for the campaign.
pub campaign_prerelease: std::string::String,
/// Implementation status of the reusable runner.
pub runner_status: std::string::String,
/// Exact observed network status.
pub network_status: std::string::String,
/// 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>,
}
/// Closed inventory of every current Metaplex operation requiring Devnet coverage.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MetaplexTokenMetadataDevnetExecutionMatrix {
/// Matrix schema version.
pub matrix_version: u32,
/// Owning prerelease.
pub milestone: std::string::String,
/// Canonical Token Metadata program ID.
pub program_id: std::string::String,
/// Ordered current operations.
pub operations: std::vec::Vec<crate::MetaplexTokenMetadataDevnetExecutionOperation>,
}
/// Loads and validates the closed Devnet execution matrix.
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"
),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_invalid_json",
format!("Metaplex Devnet execution matrix JSON is invalid: {error}"),
));
},
};
if let std::result::Result::Err(error) =
validate_metaplex_token_metadata_devnet_execution_matrix(&matrix)
{
return std::result::Result::Err(error);
}
return std::result::Result::Ok(matrix);
}
/// Validates exact current-operation coverage and conservative network statuses.
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.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",
));
}
let expected = [
"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.burn",
"metadata.metaplex_token_metadata.create",
"metadata.metaplex_token_metadata.mint",
"metadata.metaplex_token_metadata.delegate",
"metadata.metaplex_token_metadata.revoke",
"metadata.metaplex_token_metadata.lock",
"metadata.metaplex_token_metadata.unlock",
"metadata.metaplex_token_metadata.migrate",
"metadata.metaplex_token_metadata.transfer",
"metadata.metaplex_token_metadata.update",
"metadata.metaplex_token_metadata.use",
"metadata.metaplex_token_metadata.verify",
"metadata.metaplex_token_metadata.unverify",
"metadata.metaplex_token_metadata.collect",
"metadata.metaplex_token_metadata.print",
"metadata.metaplex_token_metadata.resize",
"metadata.metaplex_token_metadata.close_accounts",
]
.into_iter()
.collect::<std::collections::BTreeSet<&str>>();
let observed = matrix
.operations
.iter()
.map(|operation| return operation.operation_code.as_str())
.collect::<std::collections::BTreeSet<&str>>();
if observed != expected {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_devnet_execution_matrix_operation_mismatch",
"Metaplex Devnet matrix must cover the exact 20 current operation codes",
));
}
for operation in &matrix.operations {
if operation.deprecated
|| operation.required_evidence.is_empty()
|| operation.submission_evidence.is_empty()
|| !matches!(
operation.network_status.as_str(),
"not_run" | "simulated" | "confirmed" | "unavailable"
)
{
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",
));
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod devnet_execution_matrix_tests {
#[test]
fn devnet_execution_matrix_is_closed_current_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 {
value
} else {
return;
};
assert_eq!(matrix.operations.len(), 20);
assert_eq!(
matrix
.operations
.iter()
.filter(|operation| return operation.campaign_prerelease == "0.4.7-pre.011")
.count(),
4,
);
assert!(matrix.operations.iter().all(|operation| return !operation.deprecated));
assert!(
matrix
.operations
.iter()
.all(|operation| return operation.network_status == "not_run")
);
}
}