v0.4.7-pre.007

This commit is contained in:
2026-08-02 11:46:25 +02:00
parent d34d70f619
commit d831623980
12 changed files with 747 additions and 13 deletions

View File

@@ -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 lAPI 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 dexemples dutilisation des lectures stateful, du préflight, de lorchestration 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 lorchestration simulation-first et des postconditions explicites.
## 0.4.7-pre.003-delta-fix-002 — dette de compatibilité des exécuteurs
- ajout dun TODO de réaudit de lorchestration après extension future des exécuteurs Solana Core/SPL aux opérations obsolètes encore constructibles ;

View File

@@ -1,5 +1,5 @@
# file: kb-pipeline/Cargo.toml
# version: 13
# version: 14
[package]
name = "kb-pipeline"

View File

@@ -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 linterface 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 dexécution Token-2022.
- préflights stateful et orchestrations dexécution Token-2022 et Metaplex Token Metadata.
Voir [USAGE.md](USAGE.md) pour les contrats publics.

View File

@@ -1,12 +1,13 @@
<!-- file: kb-pipeline/TODO.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# TODO — kb-pipeline
## `0.4.7`
- [ ] Metaplex Token Metadata - intégrer lexécution et les étapes restantes au replay bot3.
- [ ] Metaplex Token Metadata - ajouter lorchestration nécessaire aux préflights, postconditions et validations.
- [x] Metaplex Token Metadata - intégrer les lectures stateful bornées, le préflight et lorchestration 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

View File

@@ -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 dobservation distincts pour le back
- la crate orchestre les traitements mais ne fournit pas dinterface 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 lenveloppe dexécution Metaplex
Lorchestration 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 labsence de postcondition applicable reste `NotApplicable`.

View File

@@ -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.

View File

@@ -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,
);
}
}

View 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());
}
}

View 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()
);
}
}