v0.4.8-pre.006
This commit is contained in:
@@ -1,8 +1,21 @@
|
||||
<!-- file: kb-lib/CHANGELOG.md -->
|
||||
<!-- version: 21 -->
|
||||
<!-- version: 23 -->
|
||||
|
||||
# CHANGELOG — kb-lib
|
||||
|
||||
## 0.4.8-pre.006 — matérialisation Solana Program Metadata
|
||||
|
||||
- active `MtMetadataMaterializer` comme matérialiseur contextualisé de la surface exacte `metadata.solana_program_metadata` ;
|
||||
- matérialise les neuf instructions stables comme faits de mutation committés, sans leur attribuer l’autorité de l’état final ;
|
||||
- matérialise les snapshots `Buffer` et `Metadata` comme états autoritatifs bornés, avec conservation intégrale des données directes et Buffer en base64 et SHA-256 ;
|
||||
- conserve les URL et références `External` comme données on-chain sans fetch distant ni lecture implicite du compte référencé ;
|
||||
- refuse les instructions échouées ou non committées et ignore les programmes, familles ou entrées étrangers ;
|
||||
- ferme une matrice de onze projections : deux états de comptes et neuf faits d’instructions ;
|
||||
- confirme que les neuf instructions stables sont courantes, non dépréciées et non remplacées ; l’ancien nom pré-stable du tag `5` reste une provenance de `Trim` seulement ;
|
||||
- ajoute les helpers publics de matérialisation d’états et le test d’API externe correspondant.
|
||||
- corrige le helper de test de snapshot afin d’utiliser les variantes wire existantes `Encoding::Utf8` et `Format::Json`.
|
||||
- précise que les opérations destructrices, irréversibles ou écrasantes restent exécutables : leur progression vers simulation, signature et envoi doit être gouvernée par le cadre commun `ExSafetyChecker`, et non supprimée du périmètre de l’exécuteur.
|
||||
|
||||
## 0.4.8-pre.005 — instructions Solana Program Metadata
|
||||
|
||||
- active `DcMetadataSolanaProgramMetadataDecoder` pour les neuf discriminants stables `0..8` ;
|
||||
@@ -14,6 +27,7 @@
|
||||
- retire le décodeur Solana Program Metadata de l’inventaire des décodeurs réservés.
|
||||
- aligne la structure du composant sur les règles du workspace : façade limitée aux sous-modules et réexports, implémentation déplacée dans `decoder.rs`, constantes centralisées dans `constants.rs` et accès internes via `crate::`.
|
||||
- corrige les vecteurs négatifs `Initialize` afin de placer explicitement les valeurs invalides après les 16 octets du seed et de vérifier séparément `Encoding`, `Compression`, `Format` et `DataSource`.
|
||||
- validation reçue le 5 août 2026 : `cargo fmt`, `cargo check --workspace`, `cargo clippy --all-targets`, audit workspace et 660 tests unitaires `kb-lib` réussis, ainsi que les tests d’API externes et la matrice de nomenclature.
|
||||
|
||||
## 0.4.8-pre.004 — comptes Solana Program Metadata
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-lib/README.md -->
|
||||
<!-- version: 22 -->
|
||||
<!-- version: 23 -->
|
||||
|
||||
# kb-lib
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
## Périmètre
|
||||
|
||||
La crate couvre actuellement Solana Core, SPL Memo, SPL Token classique, SPL Associated Token Account, Token-2022, le registre SPL ElGamal et Metaplex Token Metadata. Elle expose également les modèles wire bornés, les décodeurs de comptes `Buffer` et `Metadata` et le décodeur contextualisé des neuf instructions stables de Solana Program Metadata.
|
||||
La crate couvre actuellement Solana Core, SPL Memo, SPL Token classique, SPL Associated Token Account, Token-2022, le registre SPL ElGamal et Metaplex Token Metadata. Elle expose également les modèles wire bornés, les décodeurs de comptes `Buffer` et `Metadata`, le décodeur contextualisé des neuf instructions stables et la matérialisation exhaustive de Solana Program Metadata.
|
||||
|
||||
Pour Solana Program Metadata, la surface publique fondatrice comprend :
|
||||
|
||||
@@ -21,6 +21,9 @@ Pour Solana Program Metadata, la surface publique fondatrice comprend :
|
||||
- les neuf instructions stables `Write`, `Initialize`, `SetAuthority`, `SetData`, `SetImmutable`, `Trim`, `Close`, `Allocate` et `Extend` ;
|
||||
- les formes runtime actuelles non produites par le SDK, notamment `SetData` sans remplacement de données et les suffixes ignorés de `SetImmutable`, `Trim` et `Close` ;
|
||||
- une frontière historique où l’ancien nom pré-stable `WithdrawExcessLamports` du tag `5` reste une provenance de `Trim`, sans dixième entrée de couverture.
|
||||
- deux projections d’état autoritatives pour `Buffer` et `Metadata`, avec conservation bornée du contenu on-chain ;
|
||||
- neuf faits de mutation d’instructions committées, non autoritatifs vis-à-vis de l’état final et réconciliables avec les snapshots post-exécution ;
|
||||
- une classification où les neuf instructions stables sont courantes, sans opération officiellement remplacée ou obsolète à annoter `Deprecated`.
|
||||
|
||||
Pour Metaplex Token Metadata, elle fournit :
|
||||
|
||||
@@ -53,7 +56,7 @@ Pour Metaplex Token Metadata, elle fournit :
|
||||
- contrats `MtApi*` et matérialisateurs concrets ;
|
||||
- contrats `ExApi*`, politiques de sécurité et exécuteurs `Ex*Executor` ;
|
||||
- `ExMetadataMetaplexTokenMetadataExecutor`, `ExMetaplexTokenMetadataExecutionIntent` et `ExMetaplexTokenMetadataOperation` ;
|
||||
- `DcMetadataSolanaProgramMetadataDecoder`, modèles `DcMetadataSpm*`, constantes `DC_METADATA_SPM_*` et fonctions `decoder_metadata_solana_program_metadata_decode_*_account` pour le contrat Solana Program Metadata ;
|
||||
- `DcMetadataSolanaProgramMetadataDecoder`, modèles `DcMetadataSpm*`, constantes `DC_METADATA_SPM_*`, fonctions `decoder_metadata_solana_program_metadata_decode_*_account` et helpers `materializer_metadata_materialize_solana_program_metadata_*_snapshot` ;
|
||||
- modèles canoniques `Md*` réexportés par la façade.
|
||||
|
||||
Les exemples d’appel et invariants sont documentés dans [USAGE.md](USAGE.md).
|
||||
@@ -68,7 +71,7 @@ Les exemples d’appel et invariants sont documentés dans [USAGE.md](USAGE.md).
|
||||
|
||||
La surface fonctionnelle Metaplex de la crate est achevée. Les campagnes Devnet restantes relèvent de `kb-pipeline-demo-scenarios` et de `kb-app-demo-desktop`, non d’un manque de builder dans `kb-lib`.
|
||||
|
||||
Pour Solana Program Metadata, les modèles wire, les décodeurs bornés de comptes et le décodeur contextualisé des neuf instructions stables sont actifs. `Buffer` accepte tout reliquat comme données allouées sans inventer de PDA stable après changement d’autorité ; `Metadata` valide la dérivation canonical ou non-canonical et distingue le contenu logique de la capacité ajoutée par `Extend`. Les variantes qui dépendent de l’état du compte restent des intentions wire jusqu’aux lectures stateful prévues, et aucune forme pré-stable n’est déclarée compatible réseau sans preuve de cluster.
|
||||
Pour Solana Program Metadata, les modèles wire, les décodeurs bornés de comptes, le décodeur contextualisé et la matérialisation des deux comptes et des neuf instructions stables sont actifs. `Buffer` accepte tout reliquat comme données allouées sans inventer de PDA stable après changement d’autorité ; `Metadata` valide la dérivation canonical ou non-canonical et distingue le contenu logique de la capacité ajoutée par `Extend`. Les instructions produisent des faits committés non autoritatifs ; les snapshots post-exécution restent la source de l’état final. Les variantes qui dépendent de l’état du compte restent des intentions wire jusqu’aux lectures stateful prévues, et aucune forme pré-stable n’est déclarée compatible réseau sans preuve de cluster.
|
||||
|
||||
Le registre ElGamal n’est pas déclaré validé réellement sur réseau.
|
||||
|
||||
@@ -80,4 +83,5 @@ Le registre ElGamal n’est pas déclaré validé réellement sur réseau.
|
||||
- [Matrice contractuelle Metaplex](../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_MATRIX.json)
|
||||
- [Matrice des comptes Solana Program Metadata](../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_ACCOUNT_MATRIX.json)
|
||||
- [Matrice des instructions Solana Program Metadata](../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_INSTRUCTION_MATRIX.json)
|
||||
- [Matrice de matérialisation Solana Program Metadata](../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_MATERIALIZATION_MATRIX.json)
|
||||
- [Audit de l’inventaire et de l’historique des instructions](../docs/audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-lib/USAGE.md -->
|
||||
<!-- version: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# Utilisation de kb-lib
|
||||
|
||||
@@ -153,6 +153,35 @@ println!("status={:?}", result.status);
|
||||
|
||||
Le tag `5` est toujours publié comme `trim`. Son ancien nom pré-stable `withdraw_excess_lamports` est conservé uniquement comme provenance, car aucune release stable ne l’expose comme instruction distincte. Les URL et références externes restent des données on-chain ; le décodeur ne réalise aucun fetch.
|
||||
|
||||
Le matérialiseur `MtMetadataMaterializer` accepte les neuf observations d’instructions de la surface exacte `metadata.solana_program_metadata`. Il refuse les transactions échouées et produit un fait de mutation par instruction. Ces faits ne remplacent pas un snapshot de compte : l’état final doit être matérialisé depuis la lecture post-exécution.
|
||||
|
||||
```rust
|
||||
let materializer = kb_lib::MtMetadataMaterializer;
|
||||
if kb_lib::MtApiEventMaterializer::accepts_observation(&materializer, &observation) {
|
||||
let result = kb_lib::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||||
println!("materialization status={:?}", result.status);
|
||||
}
|
||||
```
|
||||
|
||||
Les trois helpers d’état acceptent les snapshots déjà validés par le décodeur :
|
||||
|
||||
- `materializer_metadata_materialize_solana_program_metadata_account_snapshot` distribue `Buffer` ou `Metadata` ;
|
||||
- `materializer_metadata_materialize_solana_program_metadata_buffer_snapshot` conserve tous les octets du Buffer sous une forme bornée avec base64 et SHA-256 ;
|
||||
- `materializer_metadata_materialize_solana_program_metadata_metadata_snapshot` conserve le contenu direct, l’URL ou la référence externe, ainsi que l’autorité, la mutabilité, le format et la capacité allouée.
|
||||
|
||||
```rust
|
||||
let output = match kb_lib::materializer_metadata_materialize_solana_program_metadata_account_snapshot(
|
||||
slot,
|
||||
&snapshot,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
println!("projection={}", output.payload_json);
|
||||
```
|
||||
|
||||
Une URL reste une chaîne observée avec `fetched = false`. Une référence externe reste une adresse, un offset et une longueur avec `accountRead = false`. La matérialisation canonique ne réalise aucun enrichissement off-chain.
|
||||
|
||||
## Comptes Metaplex Token Metadata
|
||||
|
||||
Les fonctions `decoder_metadata_metaplex_token_metadata_decode_*_account` décodent les familles de comptes publiques prises en charge. Le consommateur doit fournir les données et le contexte attendus par la signature exacte de la fonction concernée. Les parseurs conservent les variantes historiques et refusent les longueurs, propriétaires ou suffixes incompatibles.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/lib.rs
|
||||
// version: 32
|
||||
// version: 33
|
||||
|
||||
//! Consolidated decoder, executor, materializer and shared model library.
|
||||
#![warn(missing_docs)]
|
||||
@@ -1391,7 +1391,7 @@ pub use self::materializer::MtLifecycleMaterializer;
|
||||
pub use self::materializer::MtLiquidityMaterializer;
|
||||
/// Exposes the legacy common business materializer trait.
|
||||
pub use self::materializer::MtMaterializer;
|
||||
/// Token-2022 and Metaplex metadata snapshot materializer.
|
||||
/// Token-2022, Metaplex and Solana Program Metadata materializer.
|
||||
pub use self::materializer::MtMetadataMaterializer;
|
||||
/// Reserved NFT materializer.
|
||||
pub use self::materializer::MtNftMaterializer;
|
||||
@@ -1439,6 +1439,12 @@ pub use self::materializer::materializer_fees_materialize_token_2022_state_snaps
|
||||
pub use self::materializer::materializer_metadata_materialize_metaplex_account_snapshot;
|
||||
/// Materializes one canonical Metaplex Metadata account snapshot.
|
||||
pub use self::materializer::materializer_metadata_materialize_metaplex_metadata_snapshot;
|
||||
/// Materializes one decoded Solana Program Metadata account snapshot.
|
||||
pub use self::materializer::materializer_metadata_materialize_solana_program_metadata_account_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Buffer account snapshot.
|
||||
pub use self::materializer::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Metadata account snapshot.
|
||||
pub use self::materializer::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot;
|
||||
/// Materializes Token-2022 embedded metadata and group snapshots.
|
||||
pub use self::materializer::materializer_metadata_materialize_token_2022_snapshot;
|
||||
/// Materializes one bounded Token-2022 account state snapshot.
|
||||
@@ -2048,6 +2054,12 @@ pub(crate) use self::executor::executor_spl_token_2022_build_prepared_plan;
|
||||
pub(crate) use self::executor::executor_spl_token_build_prepared_plan;
|
||||
/// Canonical processor name for the native and SPL administration materializer.
|
||||
pub(crate) use self::materializer::MT_ADMIN_PROCESSOR_NAME;
|
||||
/// Event families accepted by the Solana Program Metadata instruction materializer.
|
||||
pub(crate) use self::materializer::MT_METADATA_SPM_ACCEPTED_FAMILIES;
|
||||
/// Stable processor name for Solana Program Metadata projections.
|
||||
pub(crate) use self::materializer::MT_METADATA_SPM_PROCESSOR_NAME;
|
||||
/// Stable projection schema version for Solana Program Metadata outputs.
|
||||
pub(crate) use self::materializer::MT_METADATA_SPM_PROJECTION_VERSION;
|
||||
/// Stable processor name for SPL Token and ATA risk facts.
|
||||
pub(crate) use self::materializer::MT_RISK_PROCESSOR_NAME;
|
||||
/// Stable projection contract version for SPL Token and ATA risk facts.
|
||||
@@ -2068,6 +2080,8 @@ pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_ADMIN;
|
||||
pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT;
|
||||
/// Canonical tracing target for the native lifecycle materializer.
|
||||
pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_LIFECYCLE;
|
||||
/// Canonical tracing target for Solana Program Metadata materialization.
|
||||
pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA;
|
||||
/// Canonical tracing target for SPL Token and ATA risk facts.
|
||||
pub(crate) use self::materializer::TRACING_TARGET_MATERIALIZER_RISK;
|
||||
/// Canonical tracing target for the native stake and vote materializer.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/materializer.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
//! Consolidated materializer modules.
|
||||
|
||||
@@ -73,12 +73,18 @@ pub use self::lending::MtLendingMaterializer;
|
||||
pub use self::lifecycle::MtLifecycleMaterializer;
|
||||
/// Reserved liquidity materializer.
|
||||
pub use self::liquidity::MtLiquidityMaterializer;
|
||||
/// Token-2022 and Metaplex metadata snapshot materializer.
|
||||
/// Token-2022, Metaplex and Solana Program Metadata materializer.
|
||||
pub use self::metadata::MtMetadataMaterializer;
|
||||
/// Materializes one canonical non-Metadata Metaplex account snapshot.
|
||||
pub use self::metadata::materializer_metadata_materialize_metaplex_account_snapshot;
|
||||
/// Materializes one canonical Metaplex Metadata account snapshot.
|
||||
pub use self::metadata::materializer_metadata_materialize_metaplex_metadata_snapshot;
|
||||
/// Materializes one decoded Solana Program Metadata account snapshot.
|
||||
pub use self::metadata::materializer_metadata_materialize_solana_program_metadata_account_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Buffer account snapshot.
|
||||
pub use self::metadata::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Metadata account snapshot.
|
||||
pub use self::metadata::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot;
|
||||
/// Materializes Token-2022 embedded metadata and group snapshots.
|
||||
pub use self::metadata::materializer_metadata_materialize_token_2022_snapshot;
|
||||
/// Reserved NFT materializer.
|
||||
@@ -120,6 +126,14 @@ pub(crate) use self::admin::TRACING_TARGET_MATERIALIZER_ADMIN;
|
||||
pub(crate) use self::compliance::TRACING_TARGET_MATERIALIZER_COMPLIANCE_AUDIT;
|
||||
/// Canonical tracing target for the native lifecycle materializer.
|
||||
pub(crate) use self::lifecycle::TRACING_TARGET_MATERIALIZER_LIFECYCLE;
|
||||
/// Event families accepted by the Solana Program Metadata instruction materializer.
|
||||
pub(crate) use self::metadata::MT_METADATA_SPM_ACCEPTED_FAMILIES;
|
||||
/// Stable processor name for Solana Program Metadata projections.
|
||||
pub(crate) use self::metadata::MT_METADATA_SPM_PROCESSOR_NAME;
|
||||
/// Stable projection schema version for Solana Program Metadata outputs.
|
||||
pub(crate) use self::metadata::MT_METADATA_SPM_PROJECTION_VERSION;
|
||||
/// Canonical tracing target for Solana Program Metadata materialization.
|
||||
pub(crate) use self::metadata::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA;
|
||||
/// Stable processor name for SPL Token and ATA risk facts.
|
||||
pub(crate) use self::risk::MT_RISK_PROCESSOR_NAME;
|
||||
/// Stable projection contract version for SPL Token and ATA risk facts.
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// file: kb-lib/src/materializer/metadata.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! `metadata` materializer family.
|
||||
|
||||
mod core;
|
||||
mod solana_program_metadata;
|
||||
|
||||
/// Token-2022 and Metaplex metadata snapshot materializer.
|
||||
/// Token-2022, Metaplex and Solana Program Metadata materializer.
|
||||
pub use self::core::MtMetadataMaterializer;
|
||||
/// Materializes one canonical non-Metadata Metaplex account snapshot.
|
||||
pub use self::core::materializer_metadata_materialize_metaplex_account_snapshot;
|
||||
@@ -13,3 +14,18 @@ pub use self::core::materializer_metadata_materialize_metaplex_account_snapshot;
|
||||
pub use self::core::materializer_metadata_materialize_metaplex_metadata_snapshot;
|
||||
/// Materializes Token-2022 embedded metadata and group snapshots.
|
||||
pub use self::core::materializer_metadata_materialize_token_2022_snapshot;
|
||||
/// Materializes one decoded Solana Program Metadata account snapshot.
|
||||
pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_account_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Buffer account snapshot.
|
||||
pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Metadata account snapshot.
|
||||
pub use self::solana_program_metadata::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot;
|
||||
|
||||
/// Event families accepted by the Solana Program Metadata instruction materializer.
|
||||
pub(crate) use self::solana_program_metadata::MT_METADATA_SPM_ACCEPTED_FAMILIES;
|
||||
/// Stable processor name for Solana Program Metadata projections.
|
||||
pub(crate) use self::solana_program_metadata::MT_METADATA_SPM_PROCESSOR_NAME;
|
||||
/// Stable projection schema version for Solana Program Metadata outputs.
|
||||
pub(crate) use self::solana_program_metadata::MT_METADATA_SPM_PROJECTION_VERSION;
|
||||
/// Canonical tracing target for Solana Program Metadata materialization.
|
||||
pub(crate) use self::solana_program_metadata::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: kb-lib/src/materializer/metadata/core.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Stable Token-2022 and Metaplex metadata snapshot materializer.
|
||||
//! Stable Token-2022, Metaplex and Solana Program Metadata materializer.
|
||||
|
||||
/// Initial materializer implementation placeholder.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -16,8 +16,13 @@ impl crate::MtMaterializer for crate::MtMetadataMaterializer {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn accepts_event(&self, _event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return false;
|
||||
fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool {
|
||||
return event.event_family == crate::MdEventFamily::Metadata
|
||||
&& event.program_id.0 == kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID
|
||||
&& event.surface_code.0 == crate::DC_METADATA_SPM_SURFACE_CODE
|
||||
&& crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.any(|(_, code)| return *code == event.event_name.0.as_str());
|
||||
}
|
||||
|
||||
fn materialize_event(
|
||||
|
||||
24
kb-lib/src/materializer/metadata/solana_program_metadata.rs
Normal file
24
kb-lib/src/materializer/metadata/solana_program_metadata.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
// file: kb-lib/src/materializer/metadata/solana_program_metadata.rs
|
||||
// version: 1
|
||||
|
||||
//! Solana Program Metadata materialization surface.
|
||||
|
||||
mod constants;
|
||||
mod instruction;
|
||||
mod state;
|
||||
|
||||
/// Materializes one decoded Solana Program Metadata account snapshot.
|
||||
pub use self::state::materializer_metadata_materialize_solana_program_metadata_account_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Buffer account snapshot.
|
||||
pub use self::state::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot;
|
||||
/// Materializes one bounded Solana Program Metadata Metadata account snapshot.
|
||||
pub use self::state::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot;
|
||||
|
||||
/// Event families accepted by the Solana Program Metadata instruction materializer.
|
||||
pub(crate) use self::constants::MT_METADATA_SPM_ACCEPTED_FAMILIES;
|
||||
/// Stable processor name for Solana Program Metadata projections.
|
||||
pub(crate) use self::constants::MT_METADATA_SPM_PROCESSOR_NAME;
|
||||
/// Stable projection schema version for Solana Program Metadata outputs.
|
||||
pub(crate) use self::constants::MT_METADATA_SPM_PROJECTION_VERSION;
|
||||
/// Canonical tracing target for Solana Program Metadata materialization.
|
||||
pub(crate) use self::constants::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA;
|
||||
@@ -0,0 +1,16 @@
|
||||
// file: kb-lib/src/materializer/metadata/solana_program_metadata/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Solana Program Metadata materializer constants.
|
||||
|
||||
/// Event families accepted by the Solana Program Metadata instruction materializer.
|
||||
pub(crate) const MT_METADATA_SPM_ACCEPTED_FAMILIES: &[crate::MdEventFamily] =
|
||||
&[crate::MdEventFamily::Metadata];
|
||||
/// Stable processor name for Solana Program Metadata projections.
|
||||
pub(crate) const MT_METADATA_SPM_PROCESSOR_NAME: &str =
|
||||
"kb-lib.materializer.metadata.solana_program_metadata";
|
||||
/// Stable projection schema version for Solana Program Metadata outputs.
|
||||
pub(crate) const MT_METADATA_SPM_PROJECTION_VERSION: u16 = 1;
|
||||
/// Canonical tracing target for Solana Program Metadata materialization.
|
||||
pub(crate) const TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA: &str =
|
||||
"kb-lib.materializer.metadata.solana_program_metadata";
|
||||
@@ -0,0 +1,325 @@
|
||||
// file: kb-lib/src/materializer/metadata/solana_program_metadata/instruction.rs
|
||||
// version: 1
|
||||
|
||||
//! Committed Solana Program Metadata instruction projections.
|
||||
|
||||
impl crate::MtApiEventMaterializer for crate::MtMetadataMaterializer {
|
||||
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
||||
return crate::MtApiMaterializerIdentity {
|
||||
name: crate::MT_METADATA_SPM_PROCESSOR_NAME.to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
|
||||
return crate::MT_METADATA_SPM_ACCEPTED_FAMILIES;
|
||||
}
|
||||
|
||||
fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool {
|
||||
return observation.event.event_family == crate::MdEventFamily::Metadata
|
||||
&& observation.event.program_id.0
|
||||
== kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID
|
||||
&& observation.event.surface_code.0 == crate::DC_METADATA_SPM_SURFACE_CODE
|
||||
&& payload_contract_matches(observation)
|
||||
&& crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.any(|(_, code)| return *code == observation.event.event_name.0.as_str());
|
||||
}
|
||||
|
||||
fn transaction_policy(
|
||||
&self,
|
||||
_family: crate::MdEventFamily,
|
||||
) -> crate::MtApiMaterializationTransactionPolicy {
|
||||
return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
||||
}
|
||||
|
||||
fn materialize(
|
||||
&self,
|
||||
observation: &crate::DcApiDecodedObservation,
|
||||
) -> crate::MtApiMaterializerExecutionResult {
|
||||
if !crate::MtApiEventMaterializer::accepts_observation(self, observation) {
|
||||
return crate::MtApiMaterializerExecutionResult::ignored();
|
||||
}
|
||||
if observation.transaction_failed || !observation.observation_committed {
|
||||
return crate::MtApiMaterializerExecutionResult::refused(
|
||||
"failed_transaction_solana_program_metadata_refused",
|
||||
"failed or uncommitted Solana Program Metadata instructions cannot create mutation outputs",
|
||||
);
|
||||
}
|
||||
let operation = observation.event.event_name.0.as_str();
|
||||
let projection = instruction_projection(operation, &observation.payload_json);
|
||||
let output = crate::MtApiMaterializedOutput {
|
||||
output_key: format!(
|
||||
"solana_program_metadata_instruction:{}:{}:{}",
|
||||
observation.event.signature.0, observation.event.instruction_path.0, operation
|
||||
),
|
||||
family: crate::MdMaterializedEventFamily::Metadata,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": crate::MT_METADATA_SPM_PROJECTION_VERSION,
|
||||
"domain": "solana_program_metadata",
|
||||
"projectionKind": "committed_instruction_mutation",
|
||||
"projectionSemantics": "committed_instruction_fact_non_authoritative_state",
|
||||
"operation": operation,
|
||||
"programId": observation.event.program_id.0.clone(),
|
||||
"signature": observation.event.signature.0.clone(),
|
||||
"slot": observation.event.slot.0,
|
||||
"instructionPath": observation.event.instruction_path.0.clone(),
|
||||
"sourceKind": observation.event.source_kind,
|
||||
"transactionSucceeded": true,
|
||||
"stateProjectionAuthority": "post_execution_account_snapshot",
|
||||
"authoritativeFinalStateCaptured": false,
|
||||
"projection": projection,
|
||||
"decodedPayload": observation.payload_json.clone(),
|
||||
"externalContentFetched": false,
|
||||
"externalAccountRead": false,
|
||||
"provenance": {
|
||||
"processorName": crate::MT_METADATA_SPM_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"decoderProof": observation.proof.clone()
|
||||
}
|
||||
}),
|
||||
};
|
||||
tracing::debug!(
|
||||
target: crate::TRACING_TARGET_MATERIALIZER_METADATA_SOLANA_PROGRAM_METADATA,
|
||||
action = "materialize_solana_program_metadata_instruction",
|
||||
operation = operation,
|
||||
signature = %observation.event.signature.0,
|
||||
instruction_path = %observation.event.instruction_path.0,
|
||||
output_count = 1_usize,
|
||||
"materialize committed Solana Program Metadata instruction"
|
||||
);
|
||||
return crate::MtApiMaterializerExecutionResult {
|
||||
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
||||
outputs: std::vec![output],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn payload_contract_matches(observation: &crate::DcApiDecodedObservation) -> bool {
|
||||
let instruction =
|
||||
observation.payload_json.get("instruction").and_then(serde_json::Value::as_str);
|
||||
let program_id = observation.payload_json.get("programId").and_then(serde_json::Value::as_str);
|
||||
return instruction == std::option::Option::Some(observation.event.event_name.0.as_str())
|
||||
&& program_id
|
||||
== std::option::Option::Some(
|
||||
kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID,
|
||||
);
|
||||
}
|
||||
|
||||
fn instruction_projection(operation: &str, payload: &serde_json::Value) -> serde_json::Value {
|
||||
let parameters = match payload.get("parameters") {
|
||||
std::option::Option::Some(value) => value.clone(),
|
||||
std::option::Option::None => serde_json::Value::Null,
|
||||
};
|
||||
let accounts = match payload.get("accounts") {
|
||||
std::option::Option::Some(value) => value.clone(),
|
||||
std::option::Option::None => serde_json::Value::Null,
|
||||
};
|
||||
return serde_json::json!({
|
||||
"stateTransition": state_transition(operation),
|
||||
"targetAccount": target_account(operation, &accounts),
|
||||
"authority": account_for_role(&accounts, "authority"),
|
||||
"destination": account_for_role(&accounts, "destination"),
|
||||
"sourceBuffer": account_for_role(&accounts, "source_buffer"),
|
||||
"buffer": account_for_role(&accounts, "buffer"),
|
||||
"program": account_for_role(&accounts, "program"),
|
||||
"programData": account_for_role(&accounts, "program_data"),
|
||||
"parameters": parameters,
|
||||
"accounts": accounts,
|
||||
"currentStableInstruction": true,
|
||||
"deprecated": false,
|
||||
"replaced": false,
|
||||
"executionPolicy": "current_candidate",
|
||||
"preStableAlias": if operation == "trim" {
|
||||
serde_json::Value::String("withdraw_excess_lamports".to_string())
|
||||
} else {
|
||||
serde_json::Value::Null
|
||||
},
|
||||
"preStableAliasDecodedSeparately": false
|
||||
});
|
||||
}
|
||||
|
||||
fn state_transition(operation: &str) -> &'static str {
|
||||
match operation {
|
||||
"write" => return "buffer_content_written",
|
||||
"initialize" => return "metadata_account_initialized",
|
||||
"set_authority" => return "account_authority_updated",
|
||||
"set_data" => return "metadata_data_configuration_updated",
|
||||
"set_immutable" => return "metadata_account_marked_immutable",
|
||||
"trim" => return "account_capacity_trimmed",
|
||||
"close" => return "account_closed",
|
||||
"allocate" => return "buffer_account_allocated",
|
||||
"extend" => return "account_capacity_extended",
|
||||
_ => return "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
fn target_account(operation: &str, accounts: &serde_json::Value) -> serde_json::Value {
|
||||
let roles: &[&str] = match operation {
|
||||
"write" | "allocate" => &["buffer"],
|
||||
"initialize" | "set_data" | "set_immutable" => &["metadata"],
|
||||
"set_authority" | "trim" | "close" | "extend" => &["account"],
|
||||
_ => &[],
|
||||
};
|
||||
for role in roles {
|
||||
let value = account_for_role(accounts, role);
|
||||
if !value.is_null() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return serde_json::Value::Null;
|
||||
}
|
||||
|
||||
fn account_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Value {
|
||||
let values = match accounts.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return serde_json::Value::Null,
|
||||
};
|
||||
for value in values {
|
||||
let candidate = value.get("role").and_then(serde_json::Value::as_str);
|
||||
let present = match value.get("present").and_then(serde_json::Value::as_bool) {
|
||||
std::option::Option::Some(candidate) => candidate,
|
||||
std::option::Option::None => true,
|
||||
};
|
||||
if candidate == std::option::Option::Some(role) && present {
|
||||
return match value.get("accountKey") {
|
||||
std::option::Option::Some(candidate) => candidate.clone(),
|
||||
std::option::Option::None => serde_json::Value::Null,
|
||||
};
|
||||
}
|
||||
}
|
||||
return serde_json::Value::Null;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
const MATRIX_JSON: &str = include_str!(
|
||||
"../../../../../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_MATERIALIZATION_MATRIX.json"
|
||||
);
|
||||
|
||||
fn observation(operation: &str, committed: bool) -> crate::DcApiDecodedObservation {
|
||||
return crate::DcApiDecodedObservation {
|
||||
event_key: format!("solana_program_metadata:{operation}:0"),
|
||||
event: crate::MdDecodedProtocolEvent {
|
||||
signature: crate::MdSignature("signature111".to_string()),
|
||||
slot: crate::MdSlot(42),
|
||||
instruction_path: crate::MdInstructionPath("0".to_string()),
|
||||
program_id: crate::MdProgramId(
|
||||
kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID.to_string(),
|
||||
),
|
||||
protocol_code: crate::MdProtocolCode(
|
||||
crate::DC_METADATA_SPM_SURFACE_CODE.to_string(),
|
||||
),
|
||||
surface_code: crate::MdSurfaceCode(crate::DC_METADATA_SPM_SURFACE_CODE.to_string()),
|
||||
event_code: crate::MdEventCode(format!(
|
||||
"{}.{}",
|
||||
crate::DC_METADATA_SPM_SURFACE_CODE,
|
||||
operation
|
||||
)),
|
||||
event_name: crate::MdEventName(operation.to_string()),
|
||||
event_family: crate::MdEventFamily::Metadata,
|
||||
source_kind: crate::MdEventSourceKind::Instruction,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({
|
||||
"programId": kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID,
|
||||
"instruction": operation,
|
||||
"parameters": {"length": 8},
|
||||
"accounts": [
|
||||
{"role":"account","accountKey":"account111","present":true},
|
||||
{"role":"authority","accountKey":"authority111","present":true}
|
||||
],
|
||||
"wire": {"sha256":"00"}
|
||||
}),
|
||||
transaction_failed: !committed,
|
||||
transaction_error: if committed {
|
||||
std::option::Option::None
|
||||
} else {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]}))
|
||||
},
|
||||
observation_committed: committed,
|
||||
proof: crate::DcApiDecoderProof {
|
||||
kind: crate::DcApiDecoderProofKind::Manual,
|
||||
confidence: crate::MdDecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["wire".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_stable_instruction_has_one_materialized_mutation_fact() {
|
||||
let materializer = crate::MtMetadataMaterializer;
|
||||
for (_, operation) in crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES.iter().copied() {
|
||||
let value = observation(operation, true);
|
||||
assert!(crate::MtApiEventMaterializer::accepts_observation(&materializer, &value));
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &value);
|
||||
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted);
|
||||
assert_eq!(result.outputs.len(), 1);
|
||||
assert_eq!(result.outputs[0].payload_json["operation"], *operation);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["deprecated"], false);
|
||||
assert_eq!(result.outputs[0].payload_json["projection"]["replaced"], false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_instruction_is_refused_and_foreign_surface_is_ignored() {
|
||||
let materializer = crate::MtMetadataMaterializer;
|
||||
let failed = observation("set_data", false);
|
||||
let failure = crate::MtApiEventMaterializer::materialize(&materializer, &failed);
|
||||
assert_eq!(failure.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
||||
let mut foreign = observation("set_data", true);
|
||||
foreign.event.program_id =
|
||||
crate::MdProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string());
|
||||
let ignored = crate::MtApiEventMaterializer::materialize(&materializer, &foreign);
|
||||
assert_eq!(ignored.status, crate::MtApiMaterializerOutcomeStatus::Ignored);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialization_matrix_matches_two_account_states_and_nine_instructions() {
|
||||
let parsed = serde_json::from_str::<serde_json::Value>(MATRIX_JSON);
|
||||
assert!(parsed.is_ok());
|
||||
let matrix = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let accounts = match matrix.get("accountSnapshots").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let instructions =
|
||||
match matrix.get("instructionFacts").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(accounts.len(), 2);
|
||||
assert_eq!(instructions.len(), crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES.len());
|
||||
for (position, (tag, operation)) in
|
||||
crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES.iter().copied().enumerate()
|
||||
{
|
||||
assert_eq!(instructions[position]["tag"], tag);
|
||||
assert_eq!(instructions[position]["entryCode"], operation);
|
||||
assert_eq!(instructions[position]["deprecated"], false);
|
||||
assert_eq!(instructions[position]["replaced"], false);
|
||||
assert_eq!(
|
||||
instructions[position]["stateTransition"],
|
||||
super::state_transition(operation)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_keeps_prestable_alias_as_provenance_only() {
|
||||
let materializer = crate::MtMetadataMaterializer;
|
||||
let value = observation("trim", true);
|
||||
let result = crate::MtApiEventMaterializer::materialize(&materializer, &value);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["preStableAlias"],
|
||||
"withdraw_excess_lamports"
|
||||
);
|
||||
assert_eq!(
|
||||
result.outputs[0].payload_json["projection"]["preStableAliasDecodedSeparately"],
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// file: kb-lib/src/materializer/metadata/solana_program_metadata/state.rs
|
||||
// version: 2
|
||||
|
||||
//! Authoritative Solana Program Metadata account-state projections.
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
/// Materializes one decoded Solana Program Metadata account snapshot.
|
||||
pub fn materializer_metadata_materialize_solana_program_metadata_account_snapshot(
|
||||
slot: u64,
|
||||
snapshot: &crate::DcMetadataSpmAccountSnapshot,
|
||||
) -> std::result::Result<crate::MtApiMaterializedOutput, String> {
|
||||
match snapshot {
|
||||
crate::DcMetadataSpmAccountSnapshot::Buffer(value) => {
|
||||
return crate::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot(
|
||||
slot,
|
||||
value,
|
||||
);
|
||||
},
|
||||
crate::DcMetadataSpmAccountSnapshot::Metadata(value) => {
|
||||
return crate::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot(
|
||||
slot,
|
||||
value,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Materializes one bounded Solana Program Metadata Buffer account snapshot.
|
||||
pub fn materializer_metadata_materialize_solana_program_metadata_buffer_snapshot(
|
||||
slot: u64,
|
||||
snapshot: &crate::DcMetadataSpmBufferAccountSnapshot,
|
||||
) -> std::result::Result<crate::MtApiMaterializedOutput, String> {
|
||||
if snapshot.account.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata Buffer materialization requires a non-empty account"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.program.as_ref().is_some_and(|value| return value.trim().is_empty())
|
||||
|| snapshot.authority.as_ref().is_some_and(|value| return value.trim().is_empty())
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata Buffer optional identities must not be empty".to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.data.len() != snapshot.allocated_data_bytes {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata Buffer allocated byte count does not match decoded data"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.allocated_data_bytes
|
||||
> crate::DC_METADATA_SPM_MAX_ACCOUNT_BYTES
|
||||
.saturating_sub(crate::DC_METADATA_SPM_BUFFER_HEADER_BYTES)
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata Buffer exceeds the bounded account-data contract".to_string(),
|
||||
);
|
||||
}
|
||||
let seed_hex = hex(snapshot.seed.as_bytes());
|
||||
let data = bytes_projection(snapshot.data.as_slice());
|
||||
return std::result::Result::Ok(crate::MtApiMaterializedOutput {
|
||||
output_key: format!("solana_program_metadata_account_state:{}", snapshot.account),
|
||||
family: crate::MdMaterializedEventFamily::Metadata,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": crate::MT_METADATA_SPM_PROJECTION_VERSION,
|
||||
"domain": "solana_program_metadata",
|
||||
"projectionKind": "buffer_state",
|
||||
"projectionSemantics": "authoritative_bounded_owned_account_snapshot",
|
||||
"account": snapshot.account,
|
||||
"slot": slot,
|
||||
"programId": kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID,
|
||||
"accountDiscriminator": "buffer",
|
||||
"metadataProgram": snapshot.program,
|
||||
"authority": snapshot.authority,
|
||||
"mutable": snapshot.authority.is_some(),
|
||||
"canonical": snapshot.canonical,
|
||||
"seedHex": seed_hex,
|
||||
"allocatedDataBytes": snapshot.allocated_data_bytes,
|
||||
"data": data,
|
||||
"metadataSource": "solana_program_metadata_owned_buffer_account",
|
||||
"externalContentFetched": false,
|
||||
"externalAccountRead": false,
|
||||
"instructionProjectionRole": "mutation_observation_only_no_duplicate_state",
|
||||
"provenance": {
|
||||
"processorName": crate::MT_METADATA_SPM_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"source": "bounded_solana_program_metadata_account_decoder"
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/// Materializes one bounded Solana Program Metadata Metadata account snapshot.
|
||||
pub fn materializer_metadata_materialize_solana_program_metadata_metadata_snapshot(
|
||||
slot: u64,
|
||||
snapshot: &crate::DcMetadataSpmMetadataAccountSnapshot,
|
||||
) -> std::result::Result<crate::MtApiMaterializedOutput, String> {
|
||||
if snapshot.account.trim().is_empty() || snapshot.program.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata state materialization requires account and program identities"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.authority.as_ref().is_some_and(|value| return value.trim().is_empty()) {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata authority must not be empty when present".to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.data_source != snapshot.data.source() {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata data source does not match decoded content".to_string(),
|
||||
);
|
||||
}
|
||||
if let std::result::Result::Err(error) = snapshot.data.validate() {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let logical_data_bytes = match usize::try_from(snapshot.data_length) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata logical data length is not representable".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
if logical_data_bytes != snapshot.data.wire_byte_len() {
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata logical data length does not match decoded content"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.allocated_data_bytes
|
||||
> crate::DC_METADATA_SPM_MAX_ACCOUNT_BYTES
|
||||
.saturating_sub(crate::DC_METADATA_SPM_METADATA_HEADER_BYTES)
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata state exceeds the bounded account-data contract".to_string(),
|
||||
);
|
||||
}
|
||||
if snapshot.allocated_data_bytes
|
||||
!= logical_data_bytes.saturating_add(snapshot.trailing_capacity_bytes)
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
"Solana Program Metadata allocated capacity is inconsistent".to_string(),
|
||||
);
|
||||
}
|
||||
let seed_hex = hex(snapshot.seed.as_bytes());
|
||||
let data = data_projection(&snapshot.data);
|
||||
return std::result::Result::Ok(crate::MtApiMaterializedOutput {
|
||||
output_key: format!("solana_program_metadata_account_state:{}", snapshot.account),
|
||||
family: crate::MdMaterializedEventFamily::Metadata,
|
||||
payload_json: serde_json::json!({
|
||||
"projectionVersion": crate::MT_METADATA_SPM_PROJECTION_VERSION,
|
||||
"domain": "solana_program_metadata",
|
||||
"projectionKind": "metadata_state",
|
||||
"projectionSemantics": "authoritative_bounded_owned_account_snapshot",
|
||||
"account": snapshot.account,
|
||||
"slot": slot,
|
||||
"programId": kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID,
|
||||
"accountDiscriminator": "metadata",
|
||||
"bump": snapshot.bump,
|
||||
"metadataProgram": snapshot.program,
|
||||
"authority": snapshot.authority,
|
||||
"mutable": snapshot.mutable,
|
||||
"canonical": snapshot.canonical,
|
||||
"seedHex": seed_hex,
|
||||
"encoding": snapshot.encoding,
|
||||
"compression": snapshot.compression,
|
||||
"format": snapshot.format,
|
||||
"dataSource": snapshot.data_source,
|
||||
"dataLength": snapshot.data_length,
|
||||
"allocatedDataBytes": snapshot.allocated_data_bytes,
|
||||
"trailingCapacityBytes": snapshot.trailing_capacity_bytes,
|
||||
"data": data,
|
||||
"metadataSource": "solana_program_metadata_owned_metadata_account",
|
||||
"externalContentFetched": false,
|
||||
"externalAccountRead": false,
|
||||
"instructionProjectionRole": "mutation_observation_only_no_duplicate_state",
|
||||
"provenance": {
|
||||
"processorName": crate::MT_METADATA_SPM_PROCESSOR_NAME,
|
||||
"processorVersion": env!("CARGO_PKG_VERSION"),
|
||||
"source": "bounded_solana_program_metadata_account_decoder"
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
fn data_projection(data: &crate::DcMetadataSpmData) -> serde_json::Value {
|
||||
match data {
|
||||
crate::DcMetadataSpmData::Direct(value) => {
|
||||
let mut projection = bytes_projection(value.as_slice());
|
||||
projection["kind"] = serde_json::json!("direct");
|
||||
return projection;
|
||||
},
|
||||
crate::DcMetadataSpmData::Url(value) => {
|
||||
return serde_json::json!({
|
||||
"kind": "url",
|
||||
"value": value,
|
||||
"lengthBytes": value.len(),
|
||||
"sha256": hash(value.as_bytes()),
|
||||
"fetched": false
|
||||
});
|
||||
},
|
||||
crate::DcMetadataSpmData::External(value) => {
|
||||
return serde_json::json!({
|
||||
"kind": "external",
|
||||
"address": value.address.to_string(),
|
||||
"offset": value.offset,
|
||||
"length": value.length.map(std::num::NonZeroU32::get),
|
||||
"wireLengthBytes": crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES,
|
||||
"accountRead": false
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_projection(bytes: &[u8]) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"lengthBytes": bytes.len(),
|
||||
"sha256": hash(bytes),
|
||||
"base64": base64::engine::general_purpose::STANDARD.encode(bytes)
|
||||
});
|
||||
}
|
||||
|
||||
fn hash(bytes: &[u8]) -> std::string::String {
|
||||
let digest = sha2::Sha256::digest(bytes);
|
||||
let mut output = std::string::String::with_capacity(64);
|
||||
for byte in digest {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> std::string::String {
|
||||
let mut output = std::string::String::with_capacity(bytes.len().saturating_mul(2));
|
||||
for byte in bytes {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn buffer_snapshot_preserves_all_bounded_bytes_and_authority_state() {
|
||||
let snapshot = crate::DcMetadataSpmBufferAccountSnapshot {
|
||||
account: "buffer111".to_string(),
|
||||
program: std::option::Option::Some("program111".to_string()),
|
||||
authority: std::option::Option::Some("authority111".to_string()),
|
||||
canonical: true,
|
||||
seed: match crate::DcMetadataSpmSeed::from_wire_bytes(&[7_u8; 16]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => crate::DcMetadataSpmSeed::default(),
|
||||
},
|
||||
data: std::vec![1, 2, 3],
|
||||
allocated_data_bytes: 3,
|
||||
};
|
||||
let output =
|
||||
crate::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot(
|
||||
42, &snapshot,
|
||||
);
|
||||
assert!(output.is_ok());
|
||||
let payload = match output.as_ref() {
|
||||
std::result::Result::Ok(value) => value.payload_json.clone(),
|
||||
std::result::Result::Err(_) => serde_json::Value::Null,
|
||||
};
|
||||
assert_eq!(payload["projectionKind"], "buffer_state");
|
||||
assert_eq!(payload["data"]["base64"], "AQID");
|
||||
assert_eq!(payload["mutable"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_snapshots_materialize_every_onchain_source_without_resolution() {
|
||||
let direct = metadata_snapshot(crate::DcMetadataSpmData::Direct(std::vec![4, 5]));
|
||||
let direct_output =
|
||||
crate::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot(
|
||||
9, &direct,
|
||||
);
|
||||
assert_eq!(
|
||||
direct_output
|
||||
.as_ref()
|
||||
.map(|value| return value.payload_json["data"]["kind"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!("direct"))
|
||||
);
|
||||
let url_value = "https://example.invalid/idl.json".to_string();
|
||||
let mut url = metadata_snapshot(crate::DcMetadataSpmData::Url(url_value.clone()));
|
||||
url.data_source = crate::DcMetadataSpmDataSource::Url;
|
||||
url.data_length = match u32::try_from(url_value.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => 0,
|
||||
};
|
||||
url.allocated_data_bytes = url_value.len();
|
||||
let url_output =
|
||||
crate::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot(
|
||||
10, &url,
|
||||
);
|
||||
assert_eq!(
|
||||
url_output
|
||||
.as_ref()
|
||||
.map(|value| return value.payload_json["data"]["fetched"].clone()),
|
||||
std::result::Result::Ok(serde_json::json!(false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inconsistent_snapshot_lengths_fail_closed() {
|
||||
let mut snapshot = metadata_snapshot(crate::DcMetadataSpmData::Direct(std::vec![1, 2]));
|
||||
snapshot.data_length = 1;
|
||||
assert!(
|
||||
crate::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot(
|
||||
1, &snapshot
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
fn metadata_snapshot(
|
||||
data: crate::DcMetadataSpmData,
|
||||
) -> crate::DcMetadataSpmMetadataAccountSnapshot {
|
||||
return crate::DcMetadataSpmMetadataAccountSnapshot {
|
||||
account: "metadata111".to_string(),
|
||||
bump: 2,
|
||||
program: "program111".to_string(),
|
||||
authority: std::option::Option::Some("authority111".to_string()),
|
||||
mutable: true,
|
||||
canonical: true,
|
||||
seed: crate::DcMetadataSpmSeed::default(),
|
||||
encoding: crate::DcMetadataSpmEncoding::Utf8,
|
||||
compression: crate::DcMetadataSpmCompression::None,
|
||||
format: crate::DcMetadataSpmFormat::Json,
|
||||
data_source: crate::DcMetadataSpmDataSource::Direct,
|
||||
data_length: match u32::try_from(data.wire_byte_len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => 0,
|
||||
},
|
||||
allocated_data_bytes: data.wire_byte_len(),
|
||||
trailing_capacity_bytes: 0,
|
||||
data,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/tests/external_materializer_api.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Downstream-style compilation contract for externally implemented materializers.
|
||||
|
||||
@@ -50,6 +50,7 @@ fn external_materializer_uses_only_the_public_kb_lib_contract() {
|
||||
assert_event_materializer::<kb_lib::MtGovernanceMaterializer>();
|
||||
assert_event_materializer::<kb_lib::MtLendingMaterializer>();
|
||||
assert_event_materializer::<kb_lib::MtLifecycleMaterializer>();
|
||||
assert_event_materializer::<kb_lib::MtMetadataMaterializer>();
|
||||
assert_event_materializer::<kb_lib::MtLiquidityMaterializer>();
|
||||
assert_event_materializer::<kb_lib::MtNftMaterializer>();
|
||||
assert_event_materializer::<kb_lib::MtOracleMaterializer>();
|
||||
@@ -83,6 +84,9 @@ fn external_materializer_uses_only_the_public_kb_lib_contract() {
|
||||
let _ = kb_lib::materializer_admin_materialize_elgamal_registry_state_snapshot;
|
||||
let _ = kb_lib::materializer_metadata_materialize_metaplex_account_snapshot;
|
||||
let _ = kb_lib::materializer_metadata_materialize_metaplex_metadata_snapshot;
|
||||
let _ = kb_lib::materializer_metadata_materialize_solana_program_metadata_account_snapshot;
|
||||
let _ = kb_lib::materializer_metadata_materialize_solana_program_metadata_buffer_snapshot;
|
||||
let _ = kb_lib::materializer_metadata_materialize_solana_program_metadata_metadata_snapshot;
|
||||
let _ = kb_lib::materializer_admin_materialize_token_2022_state_snapshots;
|
||||
let _ = kb_lib::materializer_fees_materialize_token_2022_state_snapshots;
|
||||
let _ = kb_lib::materializer_metadata_materialize_token_2022_snapshot;
|
||||
|
||||
Reference in New Issue
Block a user