v0.4.7-pre.007
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: Cargo.toml
|
||||
# version: 13
|
||||
# version: 14
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
@@ -18,7 +18,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.7-pre.6"
|
||||
version = "0.4.7-pre.7"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/V0_4_7_METAPLEX_TOKEN_METADATA_COMPLETION_PLAN.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# Plan `0.4.7` — achèvement de Metaplex Token Metadata
|
||||
|
||||
@@ -575,3 +575,11 @@ Le développement fonctionnel peut commencer après acceptation des recommandati
|
||||
5. niveaux de preuve distincts pour les validations synthétiques, simulation réseau et envoi réel ;
|
||||
6. séquence de prereleases `pre.002` à `pre.011` décrite ci-dessus.
|
||||
|
||||
|
||||
## État de `0.4.7-pre.007`
|
||||
|
||||
- lectures stateful bornées dans `kb-pipeline` ;
|
||||
- préflight généraliste lié aux plans `kb-lib` ;
|
||||
- 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`.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<!-- file: docs/validation/V0_4_7_PRE_007_METAPLEX_PIPELINE_GENERALISTE.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Validation `0.4.7-pre.007` — pipeline généraliste Metaplex
|
||||
|
||||
## Contrats introduits
|
||||
|
||||
- lectures RPC `confirmed` bornées pour metadata, edition et token record ;
|
||||
- validation owner, taille, contexte, PDA et décodage délégué à `kb-lib` ;
|
||||
- préflight des plans Metaplex, corrélation bornée et approbation des opérations dépréciées ;
|
||||
- orchestration liée au hash exact de simulation ;
|
||||
- résolution complète des signers et blocage du send en dry-run ;
|
||||
- postconditions `confirmed`, `contradicted` et `not_applicable` sans succès inventé.
|
||||
|
||||
## Frontière
|
||||
|
||||
`kb-pipeline` reste généraliste et ne dépend pas de `kb-pipeline-demo-scenarios`. Les fixtures et campagnes Devnet/Testnet sont reportées à `pre.008`.
|
||||
|
||||
## Validation de préparation
|
||||
|
||||
Les audits statiques doivent être exécutés dans l’environnement de préparation. Les commandes Cargo doivent être exécutées sur le workspace local, car `cargo` n’est pas disponible ici.
|
||||
@@ -1,8 +1,19 @@
|
||||
<!-- file: kb-pipeline/CHANGELOG.md -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# CHANGELOG — kb-pipeline
|
||||
|
||||
## 0.4.7-pre.007-delta-fix-001 — correction et documentation de l’API Metaplex
|
||||
|
||||
- ajout de la rustdoc manquante sur les paramètres de dérivation des comptes edition et token record ;
|
||||
- correction du test de préflight utilisant le code stable de `PuffMetadata` ;
|
||||
- ajout d’exemples d’utilisation des lectures stateful, du préflight, de l’orchestration simulation-first et des postconditions Metaplex.
|
||||
|
||||
## 0.4.7-pre.007
|
||||
|
||||
- ajout des lectures stateful bornées Metaplex Token Metadata ;
|
||||
- ajout du préflight, de l’orchestration simulation-first et des postconditions explicites.
|
||||
|
||||
## 0.4.7-pre.003-delta-fix-002 — dette de compatibilité des exécuteurs
|
||||
|
||||
- ajout d’un TODO de réaudit de l’orchestration après extension future des exécuteurs Solana Core/SPL aux opérations obsolètes encore constructibles ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: kb-pipeline/Cargo.toml
|
||||
# version: 13
|
||||
# version: 14
|
||||
|
||||
[package]
|
||||
name = "kb-pipeline"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-pipeline/README.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 3 -->
|
||||
|
||||
# kb-pipeline
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- replay contextualisé des décodeurs ;
|
||||
- matérialisation optionnelle et idempotente ;
|
||||
- préflights et inspections stateful ;
|
||||
- orchestration Token-2022, preuves et postconditions ;
|
||||
- orchestration Token-2022 et Metaplex Token Metadata, preuves et postconditions ;
|
||||
- corrélation entre instructions observées et états finaux.
|
||||
|
||||
La crate coordonne `kb-onchain-transport`, `kb-store`, `kb-lib`, `kb-core` et `kb-program-ids`. Elle ne contient pas l’interface desktop ni les scénarios opérateur de démonstration.
|
||||
@@ -23,7 +23,7 @@ La crate coordonne `kb-onchain-transport`, `kb-store`, `kb-lib`, `kb-core` et `k
|
||||
- extraction Core ;
|
||||
- decode replay ;
|
||||
- inspections stateful Solana Core, SPL Token, ATA, Token-2022 et registre ElGamal ;
|
||||
- préflight cryptographique et orchestration d’exécution Token-2022.
|
||||
- préflights stateful et orchestrations d’exécution Token-2022 et Metaplex Token Metadata.
|
||||
|
||||
Voir [USAGE.md](USAGE.md) pour les contrats publics.
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<!-- file: kb-pipeline/TODO.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# TODO — kb-pipeline
|
||||
|
||||
## `0.4.7`
|
||||
|
||||
- [ ] Metaplex Token Metadata - intégrer l’exécution et les étapes restantes au replay bot3.
|
||||
- [ ] Metaplex Token Metadata - ajouter l’orchestration nécessaire aux préflights, postconditions et validations.
|
||||
- [x] Metaplex Token Metadata - intégrer les lectures stateful bornées, le préflight et l’orchestration simulation-first généraliste.
|
||||
- [x] Metaplex Token Metadata - ajouter la résolution des signers, les diagnostics de dépréciation et les postconditions explicites.
|
||||
- [ ] Metaplex Token Metadata - compléter en `pre.008` les fixtures et campagnes Devnet/Testnet dans `kb-pipeline-demo-scenarios`.
|
||||
|
||||
## Réaudit ultérieur des surfaces historiques
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-pipeline/USAGE.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Utilisation de kb-pipeline
|
||||
|
||||
@@ -178,3 +178,97 @@ Les campagnes longues exposent des traits d’observation distincts pour le back
|
||||
- la crate orchestre les traitements mais ne fournit pas d’interface opérateur ;
|
||||
- elle ne conserve pas de secret de wallet ;
|
||||
- elle ne remplace pas les scénarios Devnet et validations explicites de `kb-pipeline-demo-scenarios`.
|
||||
|
||||
## Lire un compte Metaplex Token Metadata
|
||||
|
||||
La lecture stateful exige un endpoint HTTP, une catégorie de compte explicite et une borne stricte sur les données décodées.
|
||||
|
||||
```rust
|
||||
async fn read_metaplex_metadata(
|
||||
pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
metadata: kb_lib::MdPubkey,
|
||||
) -> kb_core::Result<kb_pipeline::MetaplexTokenMetadataStatefulReadResult> {
|
||||
let request = kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
|
||||
query_role: "rpc".to_string(),
|
||||
account: metadata,
|
||||
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
|
||||
min_context_slot: std::option::Option::None,
|
||||
max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
|
||||
};
|
||||
let result = kb_pipeline::read_metaplex_token_metadata_stateful_snapshot(
|
||||
pool,
|
||||
&request,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(snapshot) => Ok(snapshot),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pour une edition, fournir le mint dans `MetaplexTokenMetadataAccountKind::Edition`. Pour un token record programmable, fournir le mint et le token account dans `TokenRecord`.
|
||||
|
||||
## Inspecter le préflight Metaplex
|
||||
|
||||
Le préflight lie un plan préparé par `kb-lib` à des snapshots confirmés et applique la politique des opérations dépréciées.
|
||||
|
||||
```rust
|
||||
fn inspect_metaplex_preflight(
|
||||
plan: kb_lib::ExApiPreparedExecutionPlan,
|
||||
snapshots: Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadResult>,
|
||||
allow_deprecated_operation: bool,
|
||||
) -> kb_core::Result<kb_pipeline::MetaplexTokenMetadataPreflightReport> {
|
||||
let request = kb_pipeline::MetaplexTokenMetadataPreflightRequest {
|
||||
plan,
|
||||
snapshots,
|
||||
allow_deprecated_operation,
|
||||
};
|
||||
|
||||
kb_pipeline::inspect_metaplex_token_metadata_preflight(&request)
|
||||
}
|
||||
```
|
||||
|
||||
Une opération marquée dépréciée par `kb-lib` est refusée lorsque `allow_deprecated_operation` vaut `false`.
|
||||
|
||||
## Valider l’enveloppe d’exécution Metaplex
|
||||
|
||||
L’orchestration refuse la signature ou la soumission tant que la simulation exacte du message, les signers et la confirmation opérateur ne sont pas cohérents.
|
||||
|
||||
```rust
|
||||
fn validate_metaplex_execution(
|
||||
plan: kb_lib::ExApiPreparedExecutionPlan,
|
||||
preflight: kb_pipeline::MetaplexTokenMetadataPreflightReport,
|
||||
message_hash: String,
|
||||
resolved_signers: Vec<kb_lib::MdPubkey>,
|
||||
) -> kb_core::Result<kb_pipeline::MetaplexTokenMetadataExecutionReadinessReport> {
|
||||
let request = kb_pipeline::MetaplexTokenMetadataExecutionReadinessRequest {
|
||||
plan,
|
||||
preflight,
|
||||
message_hash: message_hash.clone(),
|
||||
simulated_message_hash: message_hash,
|
||||
simulated: true,
|
||||
simulation_succeeded: true,
|
||||
resolved_signers,
|
||||
submit: false,
|
||||
operator_confirmed: false,
|
||||
};
|
||||
|
||||
kb_pipeline::validate_metaplex_token_metadata_execution_readiness(&request)
|
||||
}
|
||||
```
|
||||
|
||||
Pour une soumission réelle, `submit` et `operator_confirmed` doivent être vrais et le plan ne doit plus être en `dry_run`.
|
||||
|
||||
## Agréger les postconditions Metaplex
|
||||
|
||||
```rust
|
||||
fn summarize_metaplex_postconditions(
|
||||
statuses: &[kb_pipeline::MetaplexTokenMetadataPostconditionStatus],
|
||||
) -> kb_pipeline::MetaplexTokenMetadataPostconditionStatus {
|
||||
kb_pipeline::summarize_metaplex_token_metadata_postconditions(statuses)
|
||||
}
|
||||
```
|
||||
|
||||
`Contradicted` est prioritaire sur `Confirmed`, et l’absence de postcondition applicable reste `NotApplicable`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline/src/lib.rs
|
||||
// version: 17
|
||||
// version: 18
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -14,6 +14,9 @@ mod decode_replay;
|
||||
mod plan;
|
||||
mod solana_ata_stateful;
|
||||
mod solana_elgamal_registry_stateful;
|
||||
mod solana_metaplex_token_metadata_execution_orchestration;
|
||||
mod solana_metaplex_token_metadata_preflight;
|
||||
mod solana_metaplex_token_metadata_stateful;
|
||||
mod solana_stateful;
|
||||
mod solana_token_2022_correlation;
|
||||
mod solana_token_2022_crypto_preflight;
|
||||
@@ -121,6 +124,40 @@ pub use self::solana_elgamal_registry_stateful::materialize_elgamal_registry_acc
|
||||
pub use self::solana_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot;
|
||||
/// Migrated read_elgamal_registry_stateful_snapshot contract.
|
||||
pub use self::solana_elgamal_registry_stateful::read_elgamal_registry_stateful_snapshot;
|
||||
/// Maximum number of resolved signers accepted by one Metaplex execution envelope.
|
||||
pub use self::solana_metaplex_token_metadata_execution_orchestration::MAX_METAPLEX_TOKEN_METADATA_EXECUTION_SIGNERS;
|
||||
/// Deterministic Metaplex execution-readiness report.
|
||||
pub use self::solana_metaplex_token_metadata_execution_orchestration::MetaplexTokenMetadataExecutionReadinessReport;
|
||||
/// Complete Metaplex execution-readiness request.
|
||||
pub use self::solana_metaplex_token_metadata_execution_orchestration::MetaplexTokenMetadataExecutionReadinessRequest;
|
||||
/// Result of one Metaplex stateful postcondition.
|
||||
pub use self::solana_metaplex_token_metadata_execution_orchestration::MetaplexTokenMetadataPostconditionStatus;
|
||||
/// Aggregates Metaplex postconditions without inventing success.
|
||||
pub use self::solana_metaplex_token_metadata_execution_orchestration::summarize_metaplex_token_metadata_postconditions;
|
||||
/// Validates the complete Metaplex execution envelope before signing.
|
||||
pub use self::solana_metaplex_token_metadata_execution_orchestration::validate_metaplex_token_metadata_execution_readiness;
|
||||
/// Maximum number of correlated Metaplex snapshots accepted by one preflight.
|
||||
pub use self::solana_metaplex_token_metadata_preflight::MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS;
|
||||
/// Deterministic Metaplex preflight report.
|
||||
pub use self::solana_metaplex_token_metadata_preflight::MetaplexTokenMetadataPreflightReport;
|
||||
/// Complete Metaplex preflight request.
|
||||
pub use self::solana_metaplex_token_metadata_preflight::MetaplexTokenMetadataPreflightRequest;
|
||||
/// Inspects one prepared Metaplex plan against confirmed state snapshots.
|
||||
pub use self::solana_metaplex_token_metadata_preflight::inspect_metaplex_token_metadata_preflight;
|
||||
/// Maximum complete Metaplex account data accepted by one RPC read.
|
||||
pub use self::solana_metaplex_token_metadata_stateful::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES;
|
||||
/// Supported Metaplex account category for one stateful read.
|
||||
pub use self::solana_metaplex_token_metadata_stateful::MetaplexTokenMetadataAccountKind;
|
||||
/// One bounded Metaplex stateful read request.
|
||||
pub use self::solana_metaplex_token_metadata_stateful::MetaplexTokenMetadataStatefulReadRequest;
|
||||
/// One bounded Metaplex stateful read result.
|
||||
pub use self::solana_metaplex_token_metadata_stateful::MetaplexTokenMetadataStatefulReadResult;
|
||||
/// Canonical bounded Metaplex state snapshot.
|
||||
pub use self::solana_metaplex_token_metadata_stateful::MetaplexTokenMetadataStatefulSnapshot;
|
||||
/// Validates one RPC account response and delegates parsing to `kb-lib`.
|
||||
pub use self::solana_metaplex_token_metadata_stateful::materialize_metaplex_token_metadata_account_info_result;
|
||||
/// Reads one bounded Metaplex Token Metadata account.
|
||||
pub use self::solana_metaplex_token_metadata_stateful::read_metaplex_token_metadata_stateful_snapshot;
|
||||
/// One machine-readable native Solana stateful readiness check.
|
||||
pub use self::solana_stateful::SolanaCoreStatefulCheck;
|
||||
/// One contextual fact measured during native Solana stateful readiness.
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// file: kb-pipeline/src/solana_metaplex_token_metadata_execution_orchestration.rs
|
||||
// version: 1
|
||||
|
||||
//! Simulation-first Metaplex Token Metadata execution orchestration.
|
||||
|
||||
/// Maximum number of resolved signers accepted by one Metaplex execution envelope.
|
||||
pub const MAX_METAPLEX_TOKEN_METADATA_EXECUTION_SIGNERS: usize = 32;
|
||||
|
||||
/// Complete request checked before signing or submitting one Metaplex transaction.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MetaplexTokenMetadataExecutionReadinessRequest {
|
||||
/// Exact prepared execution plan.
|
||||
pub plan: kb_lib::ExApiPreparedExecutionPlan,
|
||||
/// Successful stateful preflight report.
|
||||
pub preflight: crate::MetaplexTokenMetadataPreflightReport,
|
||||
/// Exact compiled message hash.
|
||||
pub message_hash: std::string::String,
|
||||
/// Hash retained by simulation evidence.
|
||||
pub simulated_message_hash: std::string::String,
|
||||
/// Whether simulation was performed.
|
||||
pub simulated: bool,
|
||||
/// Whether simulation succeeded.
|
||||
pub simulation_succeeded: bool,
|
||||
/// Public keys available to sign.
|
||||
pub resolved_signers: std::vec::Vec<kb_lib::MdPubkey>,
|
||||
/// Whether submission was requested.
|
||||
pub submit: bool,
|
||||
/// Whether the operator confirmed submission.
|
||||
pub operator_confirmed: bool,
|
||||
}
|
||||
|
||||
/// Deterministic readiness report emitted before signing.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MetaplexTokenMetadataExecutionReadinessReport {
|
||||
/// Stable operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Exact message hash bound to simulation.
|
||||
pub message_hash: std::string::String,
|
||||
/// Confirmed preflight context slot.
|
||||
pub context_slot: u64,
|
||||
/// Deduplicated required signers.
|
||||
pub required_signers: std::vec::Vec<kb_lib::MdPubkey>,
|
||||
/// Whether signing and submission are authorized.
|
||||
pub send_authorized: bool,
|
||||
/// Ordered successful checks.
|
||||
pub checks: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Validates simulation, preflight, signer and submission policies.
|
||||
pub fn validate_metaplex_token_metadata_execution_readiness(
|
||||
request: &crate::MetaplexTokenMetadataExecutionReadinessRequest,
|
||||
) -> kb_core::Result<crate::MetaplexTokenMetadataExecutionReadinessReport> {
|
||||
if request.plan.operation_code != request.preflight.operation_code {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_execution_operation_mismatch",
|
||||
"Metaplex plan and preflight operation codes must match",
|
||||
));
|
||||
}
|
||||
if request.message_hash.trim().is_empty()
|
||||
|| request.message_hash != request.simulated_message_hash
|
||||
|| !request.simulated
|
||||
|| !request.simulation_succeeded
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_execution_simulation_required",
|
||||
"Metaplex execution requires successful exact-message simulation",
|
||||
));
|
||||
}
|
||||
if request.resolved_signers.len() > crate::MAX_METAPLEX_TOKEN_METADATA_EXECUTION_SIGNERS {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_execution_signer_limit_exceeded",
|
||||
"Metaplex execution signer limit exceeded",
|
||||
));
|
||||
}
|
||||
let resolved = request
|
||||
.resolved_signers
|
||||
.iter()
|
||||
.map(|item| return item.0.clone())
|
||||
.collect::<std::collections::BTreeSet<std::string::String>>();
|
||||
let mut required = std::collections::BTreeMap::<std::string::String, kb_lib::MdPubkey>::new();
|
||||
for signer in &request.plan.required_signers {
|
||||
if !resolved.contains(signer.pubkey.0.as_str()) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_execution_signer_unresolved",
|
||||
format!("Metaplex signer {} is unresolved", signer.pubkey.0),
|
||||
));
|
||||
}
|
||||
required
|
||||
.entry(signer.pubkey.0.clone())
|
||||
.or_insert_with(|| return signer.pubkey.clone());
|
||||
}
|
||||
if request.submit && !request.operator_confirmed {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_execution_confirmation_required",
|
||||
"Metaplex submission requires explicit operator confirmation",
|
||||
));
|
||||
}
|
||||
if request.submit && request.plan.policy.dry_run {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_execution_dry_run_blocks_submission",
|
||||
"Metaplex dry-run plan cannot be submitted",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(crate::MetaplexTokenMetadataExecutionReadinessReport {
|
||||
operation_code: request.plan.operation_code.clone(),
|
||||
message_hash: request.message_hash.clone(),
|
||||
context_slot: request.preflight.context_slot,
|
||||
required_signers: required.into_values().collect(),
|
||||
send_authorized: request.submit
|
||||
&& request.operator_confirmed
|
||||
&& !request.plan.policy.dry_run,
|
||||
checks: vec![
|
||||
"preflight_bound_to_operation".to_string(),
|
||||
"simulation_bound_to_exact_message".to_string(),
|
||||
"all_required_signers_resolved".to_string(),
|
||||
"submission_policy_consistent".to_string(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/// Result of one explicit postcondition.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MetaplexTokenMetadataPostconditionStatus {
|
||||
/// Final state confirms the expected effect.
|
||||
Confirmed,
|
||||
/// Final state contradicts the expected effect.
|
||||
Contradicted,
|
||||
/// No stateful assertion applies.
|
||||
NotApplicable,
|
||||
}
|
||||
|
||||
/// Aggregates Metaplex postconditions without turning absence into success.
|
||||
pub fn summarize_metaplex_token_metadata_postconditions(
|
||||
statuses: &[crate::MetaplexTokenMetadataPostconditionStatus],
|
||||
) -> crate::MetaplexTokenMetadataPostconditionStatus {
|
||||
if statuses
|
||||
.iter()
|
||||
.any(|value| return *value == crate::MetaplexTokenMetadataPostconditionStatus::Contradicted)
|
||||
{
|
||||
return crate::MetaplexTokenMetadataPostconditionStatus::Contradicted;
|
||||
}
|
||||
if statuses
|
||||
.iter()
|
||||
.any(|value| return *value == crate::MetaplexTokenMetadataPostconditionStatus::Confirmed)
|
||||
{
|
||||
return crate::MetaplexTokenMetadataPostconditionStatus::Confirmed;
|
||||
}
|
||||
return crate::MetaplexTokenMetadataPostconditionStatus::NotApplicable;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn postconditions_never_convert_not_applicable_into_success() {
|
||||
assert_eq!(
|
||||
crate::summarize_metaplex_token_metadata_postconditions(&[
|
||||
crate::MetaplexTokenMetadataPostconditionStatus::NotApplicable,
|
||||
]),
|
||||
crate::MetaplexTokenMetadataPostconditionStatus::NotApplicable,
|
||||
);
|
||||
assert_eq!(
|
||||
crate::summarize_metaplex_token_metadata_postconditions(&[
|
||||
crate::MetaplexTokenMetadataPostconditionStatus::Confirmed,
|
||||
crate::MetaplexTokenMetadataPostconditionStatus::Contradicted,
|
||||
]),
|
||||
crate::MetaplexTokenMetadataPostconditionStatus::Contradicted,
|
||||
);
|
||||
}
|
||||
}
|
||||
136
kb-pipeline/src/solana_metaplex_token_metadata_preflight.rs
Normal file
136
kb-pipeline/src/solana_metaplex_token_metadata_preflight.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
// file: kb-pipeline/src/solana_metaplex_token_metadata_preflight.rs
|
||||
// version: 2
|
||||
|
||||
//! Stateful Metaplex Token Metadata preflight contracts.
|
||||
|
||||
/// Maximum number of correlated Metaplex snapshots accepted by one preflight.
|
||||
pub const MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS: usize = 32;
|
||||
|
||||
/// Complete preflight request for one prepared Metaplex execution plan.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MetaplexTokenMetadataPreflightRequest {
|
||||
/// Exact prepared plan produced by `kb-lib`.
|
||||
pub plan: kb_lib::ExApiPreparedExecutionPlan,
|
||||
/// Confirmed bounded state snapshots correlated with the plan.
|
||||
pub snapshots: std::vec::Vec<crate::MetaplexTokenMetadataStatefulReadResult>,
|
||||
/// Whether deprecated execution was explicitly approved by the operator.
|
||||
pub allow_deprecated_operation: bool,
|
||||
}
|
||||
|
||||
/// Deterministic Metaplex preflight report.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MetaplexTokenMetadataPreflightReport {
|
||||
/// Stable operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Highest confirmed context slot across supplied snapshots.
|
||||
pub context_slot: u64,
|
||||
/// Canonical accounts inspected by the preflight.
|
||||
pub inspected_accounts: std::vec::Vec<kb_lib::MdPubkey>,
|
||||
/// Ordered successful checks.
|
||||
pub checks: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Validates program ownership, snapshot bounds, account correlation and deprecation policy.
|
||||
pub fn inspect_metaplex_token_metadata_preflight(
|
||||
request: &crate::MetaplexTokenMetadataPreflightRequest,
|
||||
) -> kb_core::Result<crate::MetaplexTokenMetadataPreflightReport> {
|
||||
if request.snapshots.len() > crate::MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_preflight_account_limit_exceeded",
|
||||
"Metaplex preflight account limit exceeded",
|
||||
));
|
||||
}
|
||||
if request.plan.instructions.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_preflight_empty_plan",
|
||||
"Metaplex execution plan must contain at least one instruction",
|
||||
));
|
||||
}
|
||||
for instruction in &request.plan.instructions {
|
||||
if instruction.program_id.0 != kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_preflight_program_mismatch",
|
||||
"Every Metaplex plan instruction must target Token Metadata",
|
||||
));
|
||||
}
|
||||
if instruction.operation_code != request.plan.operation_code {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_preflight_operation_mismatch",
|
||||
"Metaplex plan and instruction operation codes must match",
|
||||
));
|
||||
}
|
||||
}
|
||||
let deprecated = kb_lib::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES
|
||||
.contains(&request.plan.operation_code.as_str());
|
||||
if deprecated && !request.allow_deprecated_operation {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_preflight_deprecated_operation_not_approved",
|
||||
"Deprecated Metaplex execution requires explicit operator approval",
|
||||
));
|
||||
}
|
||||
let mut inspected = std::collections::BTreeMap::<std::string::String, kb_lib::MdPubkey>::new();
|
||||
let mut context_slot = 0_u64;
|
||||
for snapshot in &request.snapshots {
|
||||
if snapshot.commitment != "confirmed" {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_preflight_commitment_mismatch",
|
||||
"Metaplex stateful snapshots must use confirmed commitment",
|
||||
));
|
||||
}
|
||||
context_slot = context_slot.max(snapshot.context_slot);
|
||||
inspected
|
||||
.entry(snapshot.snapshot.account.0.clone())
|
||||
.or_insert_with(|| return snapshot.snapshot.account.clone());
|
||||
}
|
||||
return std::result::Result::Ok(crate::MetaplexTokenMetadataPreflightReport {
|
||||
operation_code: request.plan.operation_code.clone(),
|
||||
context_slot,
|
||||
inspected_accounts: inspected.into_values().collect(),
|
||||
checks: vec![
|
||||
"program_id_exact".to_string(),
|
||||
"operation_code_exact".to_string(),
|
||||
"confirmed_state_snapshots".to_string(),
|
||||
"deprecated_policy_explicit".to_string(),
|
||||
"account_correlation_bounded".to_string(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn rejects_empty_plans_and_unapproved_deprecated_operations() {
|
||||
let mut plan = kb_lib::ExApiPreparedExecutionPlan {
|
||||
executor_name: "kb-lib".to_string(),
|
||||
executor_version: "0".to_string(),
|
||||
intent_id: "i".to_string(),
|
||||
operation_code: kb_lib::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION.to_string(),
|
||||
fee_payer: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
|
||||
instructions: vec![],
|
||||
required_signers: vec![],
|
||||
policy: std::default::Default::default(),
|
||||
requested_spend_lamports: 0,
|
||||
requested_compute_unit_price_micro_lamports: std::option::Option::None,
|
||||
};
|
||||
let empty = crate::MetaplexTokenMetadataPreflightRequest {
|
||||
plan: plan.clone(),
|
||||
snapshots: vec![],
|
||||
allow_deprecated_operation: false,
|
||||
};
|
||||
assert!(crate::inspect_metaplex_token_metadata_preflight(&empty).is_err());
|
||||
plan.instructions.push(kb_lib::ExApiPlannedInstruction {
|
||||
program_id: kb_lib::MdProgramId(
|
||||
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID.to_string(),
|
||||
),
|
||||
operation_code: plan.operation_code.clone(),
|
||||
accounts: vec![],
|
||||
data: vec![14],
|
||||
});
|
||||
let deprecated = crate::MetaplexTokenMetadataPreflightRequest {
|
||||
plan,
|
||||
snapshots: vec![],
|
||||
allow_deprecated_operation: false,
|
||||
};
|
||||
assert!(crate::inspect_metaplex_token_metadata_preflight(&deprecated).is_err());
|
||||
}
|
||||
}
|
||||
256
kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs
Normal file
256
kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
// file: kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs
|
||||
// version: 2
|
||||
|
||||
//! Bounded Metaplex Token Metadata account reads and canonical state projections.
|
||||
|
||||
/// Maximum complete Metaplex account data accepted by one RPC read.
|
||||
pub const MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES: usize = 1_048_576;
|
||||
|
||||
/// Supported Metaplex account category for one bounded stateful read.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum MetaplexTokenMetadataAccountKind {
|
||||
/// Metadata PDA derived from the mint encoded in the account.
|
||||
Metadata,
|
||||
/// Master edition or printed edition PDA derived from the supplied mint.
|
||||
Edition {
|
||||
/// Mint used to derive the edition PDA.
|
||||
mint: kb_lib::MdPubkey,
|
||||
},
|
||||
/// Programmable token record PDA derived from mint and token account.
|
||||
TokenRecord {
|
||||
/// Mint used to derive the token-record PDA.
|
||||
mint: kb_lib::MdPubkey,
|
||||
/// Token account used to derive the token-record PDA.
|
||||
token: kb_lib::MdPubkey,
|
||||
},
|
||||
}
|
||||
|
||||
/// One bounded Metaplex account read request.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MetaplexTokenMetadataStatefulReadRequest {
|
||||
/// Endpoint role used for the HTTP RPC request.
|
||||
pub query_role: std::string::String,
|
||||
/// Canonical account address.
|
||||
pub account: kb_lib::MdPubkey,
|
||||
/// Expected account category and derivation inputs.
|
||||
pub kind: crate::MetaplexTokenMetadataAccountKind,
|
||||
/// Optional minimum RPC context slot.
|
||||
pub min_context_slot: std::option::Option<u64>,
|
||||
/// Maximum decoded account bytes accepted from the endpoint.
|
||||
pub max_data_bytes: usize,
|
||||
}
|
||||
|
||||
/// Canonical bounded Metaplex state snapshot.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MetaplexTokenMetadataStatefulSnapshot {
|
||||
/// Canonical account address.
|
||||
pub account: kb_lib::MdPubkey,
|
||||
/// Stable account category.
|
||||
pub account_kind: std::string::String,
|
||||
/// Mint correlated with this account when available.
|
||||
pub mint: std::option::Option<kb_lib::MdPubkey>,
|
||||
/// RPC context slot.
|
||||
pub slot: u64,
|
||||
/// Bounded decoder-owned projection.
|
||||
pub payload_json: serde_json::Value,
|
||||
}
|
||||
|
||||
/// One bounded RPC read and its validated snapshot.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MetaplexTokenMetadataStatefulReadResult {
|
||||
/// Commitment used by the read.
|
||||
pub commitment: std::string::String,
|
||||
/// Context slot returned by the endpoint.
|
||||
pub context_slot: u64,
|
||||
/// Validated canonical snapshot.
|
||||
pub snapshot: crate::MetaplexTokenMetadataStatefulSnapshot,
|
||||
}
|
||||
|
||||
/// Reads and validates one bounded Metaplex Token Metadata account.
|
||||
pub async fn read_metaplex_token_metadata_stateful_snapshot(
|
||||
pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
|
||||
) -> kb_core::Result<crate::MetaplexTokenMetadataStatefulReadResult> {
|
||||
if request.query_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Metaplex stateful read query_role must not be empty",
|
||||
));
|
||||
}
|
||||
let config = match kb_onchain_transport::GetAccountInfoConfig::new_with_data(
|
||||
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
|
||||
request.min_context_slot,
|
||||
request.max_data_bytes,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let result = match pool
|
||||
.get_account_info_for_role(request.query_role.as_str(), &request.account, &config)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::materialize_metaplex_token_metadata_account_info_result(request, &result);
|
||||
}
|
||||
|
||||
/// Validates one RPC account response and delegates parsing to `kb-lib`.
|
||||
pub fn materialize_metaplex_token_metadata_account_info_result(
|
||||
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
|
||||
result: &kb_onchain_transport::AccountInfoResult,
|
||||
) -> kb_core::Result<crate::MetaplexTokenMetadataStatefulReadResult> {
|
||||
if request.max_data_bytes == 0
|
||||
|| request.max_data_bytes > crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Metaplex stateful max_data_bytes must be between 1 and {}",
|
||||
crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
|
||||
)));
|
||||
}
|
||||
if let std::option::Option::Some(minimum) = request.min_context_slot {
|
||||
if result.context.slot < minimum {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_context_slot_too_old",
|
||||
"Metaplex account context slot is below the requested minimum",
|
||||
));
|
||||
}
|
||||
}
|
||||
let account = match result.account.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_account_missing",
|
||||
format!("Metaplex account {} does not exist", request.account.0),
|
||||
));
|
||||
},
|
||||
};
|
||||
if account.executable {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_account_executable",
|
||||
"Metaplex state accounts must not be executable",
|
||||
));
|
||||
}
|
||||
if account.owner.0 != kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_owner_mismatch",
|
||||
"Metaplex state account owner does not match Token Metadata",
|
||||
));
|
||||
}
|
||||
if account.space != account.data.len() as u64 || account.data.len() > request.max_data_bytes {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_account_data_invalid",
|
||||
"Metaplex account data is incomplete or above the configured bound",
|
||||
));
|
||||
}
|
||||
let snapshot = match decode_snapshot(request, result.context.slot, account.data.as_slice()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulReadResult {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: result.context.slot,
|
||||
snapshot,
|
||||
});
|
||||
}
|
||||
|
||||
fn decode_snapshot(
|
||||
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
|
||||
slot: u64,
|
||||
data: &[u8],
|
||||
) -> kb_core::Result<crate::MetaplexTokenMetadataStatefulSnapshot> {
|
||||
let owner = kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID;
|
||||
return match &request.kind {
|
||||
crate::MetaplexTokenMetadataAccountKind::Metadata => {
|
||||
match kb_lib::decoder_metadata_metaplex_token_metadata_decode_metadata_account(
|
||||
request.account.0.as_str(),
|
||||
owner,
|
||||
data,
|
||||
) {
|
||||
std::result::Result::Ok(value) => {
|
||||
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
|
||||
account: request.account.clone(),
|
||||
account_kind: "metadata".to_string(),
|
||||
mint: std::option::Option::Some(kb_lib::MdPubkey(value.mint)),
|
||||
slot,
|
||||
payload_json: value.payload_json,
|
||||
})
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_decode_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
}
|
||||
},
|
||||
crate::MetaplexTokenMetadataAccountKind::Edition { mint } => {
|
||||
match kb_lib::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
||||
request.account.0.as_str(),
|
||||
owner,
|
||||
mint.0.as_str(),
|
||||
data,
|
||||
) {
|
||||
std::result::Result::Ok(value) => {
|
||||
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
|
||||
account: request.account.clone(),
|
||||
account_kind: "edition".to_string(),
|
||||
mint: std::option::Option::Some(mint.clone()),
|
||||
slot,
|
||||
payload_json: value.payload_json,
|
||||
})
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_decode_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
}
|
||||
},
|
||||
crate::MetaplexTokenMetadataAccountKind::TokenRecord { mint, token } => {
|
||||
match kb_lib::decoder_metadata_metaplex_token_metadata_decode_token_record_account(
|
||||
request.account.0.as_str(),
|
||||
owner,
|
||||
mint.0.as_str(),
|
||||
token.0.as_str(),
|
||||
data,
|
||||
) {
|
||||
std::result::Result::Ok(value) => {
|
||||
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
|
||||
account: request.account.clone(),
|
||||
account_kind: "token_record".to_string(),
|
||||
mint: std::option::Option::Some(mint.clone()),
|
||||
slot,
|
||||
payload_json: value.payload_json,
|
||||
})
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_decode_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn rejects_zero_and_oversize_bounds_before_account_decoding() {
|
||||
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::None,
|
||||
max_data_bytes: 0,
|
||||
};
|
||||
let result = kb_onchain_transport::AccountInfoResult {
|
||||
context: kb_onchain_transport::RpcResponseContext {
|
||||
slot: 1,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::None,
|
||||
};
|
||||
assert!(
|
||||
crate::materialize_metaplex_token_metadata_account_info_result(&request, &result)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user