v0.4.7-pre.007
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 13
|
# version: 14
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
@@ -18,7 +18,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.4.7-pre.6"
|
version = "0.4.7-pre.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/V0_4_7_METAPLEX_TOKEN_METADATA_COMPLETION_PLAN.md -->
|
<!-- 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
|
# 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 ;
|
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.
|
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 -->
|
<!-- file: kb-pipeline/CHANGELOG.md -->
|
||||||
<!-- version: 9 -->
|
<!-- version: 12 -->
|
||||||
|
|
||||||
# CHANGELOG — kb-pipeline
|
# 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
|
## 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 ;
|
- 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
|
# file: kb-pipeline/Cargo.toml
|
||||||
# version: 13
|
# version: 14
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "kb-pipeline"
|
name = "kb-pipeline"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-pipeline/README.md -->
|
<!-- file: kb-pipeline/README.md -->
|
||||||
<!-- version: 1 -->
|
<!-- version: 3 -->
|
||||||
|
|
||||||
# kb-pipeline
|
# kb-pipeline
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
- replay contextualisé des décodeurs ;
|
- replay contextualisé des décodeurs ;
|
||||||
- matérialisation optionnelle et idempotente ;
|
- matérialisation optionnelle et idempotente ;
|
||||||
- préflights et inspections stateful ;
|
- 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.
|
- 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.
|
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 ;
|
- extraction Core ;
|
||||||
- decode replay ;
|
- decode replay ;
|
||||||
- inspections stateful Solana Core, SPL Token, ATA, Token-2022 et registre ElGamal ;
|
- 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.
|
Voir [USAGE.md](USAGE.md) pour les contrats publics.
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<!-- file: kb-pipeline/TODO.md -->
|
<!-- file: kb-pipeline/TODO.md -->
|
||||||
<!-- version: 7 -->
|
<!-- version: 8 -->
|
||||||
|
|
||||||
# TODO — kb-pipeline
|
# TODO — kb-pipeline
|
||||||
|
|
||||||
## `0.4.7`
|
## `0.4.7`
|
||||||
|
|
||||||
- [ ] Metaplex Token Metadata - intégrer l’exécution et les étapes restantes au replay bot3.
|
- [x] Metaplex Token Metadata - intégrer les lectures stateful bornées, le préflight et l’orchestration simulation-first généraliste.
|
||||||
- [ ] Metaplex Token Metadata - ajouter l’orchestration nécessaire aux préflights, postconditions et validations.
|
- [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
|
## Réaudit ultérieur des surfaces historiques
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-pipeline/USAGE.md -->
|
<!-- file: kb-pipeline/USAGE.md -->
|
||||||
<!-- version: 2 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Utilisation de kb-pipeline
|
# 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 ;
|
- la crate orchestre les traitements mais ne fournit pas d’interface opérateur ;
|
||||||
- elle ne conserve pas de secret de wallet ;
|
- elle ne conserve pas de secret de wallet ;
|
||||||
- elle ne remplace pas les scénarios Devnet et validations explicites de `kb-pipeline-demo-scenarios`.
|
- 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
|
// file: kb-pipeline/src/lib.rs
|
||||||
// version: 17
|
// version: 18
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -14,6 +14,9 @@ mod decode_replay;
|
|||||||
mod plan;
|
mod plan;
|
||||||
mod solana_ata_stateful;
|
mod solana_ata_stateful;
|
||||||
mod solana_elgamal_registry_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_stateful;
|
||||||
mod solana_token_2022_correlation;
|
mod solana_token_2022_correlation;
|
||||||
mod solana_token_2022_crypto_preflight;
|
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;
|
pub use self::solana_elgamal_registry_stateful::materialize_elgamal_registry_stateful_snapshot;
|
||||||
/// Migrated read_elgamal_registry_stateful_snapshot contract.
|
/// Migrated read_elgamal_registry_stateful_snapshot contract.
|
||||||
pub use self::solana_elgamal_registry_stateful::read_elgamal_registry_stateful_snapshot;
|
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.
|
/// One machine-readable native Solana stateful readiness check.
|
||||||
pub use self::solana_stateful::SolanaCoreStatefulCheck;
|
pub use self::solana_stateful::SolanaCoreStatefulCheck;
|
||||||
/// One contextual fact measured during native Solana stateful readiness.
|
/// 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