v0.4.8-pre.012

This commit is contained in:
2026-08-07 09:54:53 +02:00
parent dcc1d382d9
commit f5efce576b
16 changed files with 1901 additions and 38 deletions

View File

@@ -1,8 +1,17 @@
<!-- file: kb-pipeline-demo-scenarios/CHANGELOG.md -->
<!-- version: 33 -->
<!-- version: 34 -->
# CHANGELOG — kb-pipeline-demo-scenarios
## `0.4.8-pre.012`
- ajoute une fixture native de mint Token-2022 avec `MetadataPointer` auto-référent initialisé avant `InitializeMint2` ;
- préfinance le mint pour les réallocations du TLV Token Metadata atteintes par la campagne ;
- ajoute une campagne Devnet ordonnée `Initialize → UpdateField → Emit → RemoveKey → UpdateAuthority` avec simulation, soumission autorisée, confirmation, hydratation, replay, matérialisation et postconditions stateful ;
- étend le préflight générique Token-2022 aux opérations Token Metadata et nexige aucune matérialisation instructionnelle pour `Emit` ;
- ajoute une matrice réseau conservative à cinq scénarios, initialement intégralement `not_run` ;
- ajoute un test Devnet opt-in avec sortie structurée des signatures, slots, postconditions et octets de `returnData`.
## `0.4.8-pre.011`
- adapte la fixture synthétique de simulation au nouveau champ optionnel `return_data` du contrat commun ;

View File

@@ -1,5 +1,5 @@
# file: kb-pipeline-demo-scenarios/Cargo.toml
# version: 7
# version: 9
[package]
name = "kb-pipeline-demo-scenarios"
@@ -19,6 +19,7 @@ path = "src/bin/kb_pipeline_demo_scenarios_cli.rs"
[dependencies]
base64.workspace = true
bs58.workspace = true
chrono.workspace = true
kb-core = { path = "../kb-core" }
kb-config = { path = "../kb-config" }
@@ -32,6 +33,7 @@ mpl-token-metadata.workspace = true
solana-instruction.workspace = true
solana-pubkey.workspace = true
spl-token-interface.workspace = true
spl-token-2022-interface.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: kb-pipeline-demo-scenarios/README.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# kb-pipeline-demo-scenarios
@@ -19,9 +19,9 @@ La crate contient :
- fournir des tests automatisés parallèles aux démonstrations UI, sans déplacer les scénarios UI hors du desktop ;
- conserver la séparation entre preuve synthétique, simulation RPC, soumission confirmée et postcondition stateful.
La crate couvre déjà les scénarios Solana Core et SPL historiques, le corpus synthétique Metaplex pour NFT, SFT, token fongible, collection et pNFT, ainsi que deux parcours Solana Program Metadata couvrant les neuf opérations stables. La fixture `ProgM6…` prépare des PDA non canoniques préfinancés et la campagne réutilisable conserve simulation, soumission, confirmation, lectures stateful et projections matérialisées.
La crate couvre les scénarios Solana Core et SPL historiques, le corpus synthétique et le runner Devnet Metaplex pour NFT, SFT, token fongible, collection et pNFT, ainsi que deux parcours Solana Program Metadata couvrant les neuf opérations stables. Les validations spécialisées restent qualifiées séparément au lieu dêtre déduites de la seule présence dun builder.
La crate couvre déjà les scénarios Solana Core et SPL historiques ainsi que le corpus synthétique Metaplex pour NFT, SFT, token fongible, collection et pNFT. Un exécuteur Devnet réel et réutilisable est disponible pour les opérations Metaplex courantes pilotées par un seul wallet de profil. Il réalise le contrôle du genesis hash, les lectures stateful bornées, le préflight, la simulation RPC exacte et, après confirmation explicite, la soumission, la confirmation et les lectures avant/après. Le runner Devnet sait construire les opérations courantes, tandis que les parcours réellement préparés fournissent leurs fixtures, preuves RPC et postconditions. Les validations spécialisées restent qualifiées séparément au lieu dêtre déduites de la seule présence dun builder.
Pour Token-2022 Token Metadata, `0.4.8-pre.012` ajoute une campagne Devnet dédiée aux cinq instructions de `spl-token-metadata-interface`. Elle prépare nativement un mint frais avec `MetadataPointer`, puis enchaîne `Initialize`, `UpdateField`, `Emit`, `RemoveKey` et `UpdateAuthority` avec simulation-first, soumission explicitement autorisée, confirmation, replay, matérialisation lorsque applicable et postcondition stateful. Sa matrice réseau reste `not_run` avant exécution réelle.
## Binaire CLI

View File

@@ -1,21 +1,20 @@
<!-- file: kb-pipeline-demo-scenarios/TODO.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# TODO — kb-pipeline-demo-scenarios
## Validation Devnet Solana Program Metadata
## Validation Devnet Token-2022 Token Metadata
- [ ] Exécuter la campagne confirmée des neuf opérations avec un wallet Devnet dédié.
- [ ] Conserver les signatures, simulations, lectures avant/après et postconditions dans la matrice de validation.
- [ ] Confirmer spécifiquement `Trim` et `Extend`, absentes de léchantillon mainnet `pre.008`.
- [ ] Vérifier le nettoyage ou la fermeture complète des deux PDA de fixture.
- [ ] Exécuter la campagne `0.4.8-pre.012` sur un mint frais avec confirmation opérateur explicite.
- [ ] Conserver les cinq signatures, slots, simulations, preuves de replay et postconditions dans la matrice et le rapport de validation.
- [ ] Conserver le `returnData` complet de `Emit` et vérifier son décodage contre le snapshot TLV autoritatif.
## Validation Devnet complémentaire Metaplex Token Metadata
## `0.4.8-pre.013` — réaudit Metaplex Token Metadata
- [ ] Préparer les fixtures dédition imprimée et de burn NFT.
- [ ] Préparer les fixtures collection parent/membre pour verify, unverify et set-and-verify.
- [ ] Préparer les token accounts, token records, delegates et rule sets des parcours pNFT.
- [ ] Qualifier chaque opération spécialisée comme simulée, soumise, non applicable ou indisponible.
- [ ] Réauditer la couverture courante/historique, les builders, les comptes, les matérialisations et les matrices Metaplex.
- [ ] Compléter uniquement les écarts réels constatés.
- [ ] Reprendre les campagnes Devnet et compléter les fixtures spécialisées encore nécessaires pour éditions/burn, collections et pNFT.
- [ ] Qualifier chaque opération réseau avec des preuves réellement observées ou une indisponibilité justifiée.
## Évolutions générales

View File

@@ -1,5 +1,5 @@
<!-- file: kb-pipeline-demo-scenarios/USAGE.md -->
<!-- version: 11 -->
<!-- version: 12 -->
# Utilisation de kb-pipeline-demo-scenarios
@@ -100,6 +100,23 @@ where
}
```
## Exécuter la campagne Devnet Token-2022 Token Metadata
La campagne `0.4.8-pre.012` prépare un mint Token-2022 frais, initialise son `MetadataPointer` vers le mint lui-même, puis exécute les cinq opérations Token Metadata dans lordre. Elle soumet des transactions réelles et exige donc une confirmation opérateur explicite.
Le test opt-in est le chemin opérateur recommandé :
```bash
KB_DEVNET_TOKEN_2022_METADATA_CAMPAIGN_TEST=1 \
KB_DEVNET_TOKEN_2022_METADATA_OPERATOR_CONFIRMED=1 \
KB_POSTGRES_TEST_URL='postgresql://…' \
cargo test -p kb-pipeline-demo-scenarios optional_devnet_token_2022_metadata_campaign_from_env -- --nocapture
```
`KB_DEVNET_PROFILE` peut sélectionner explicitement le profil Devnet et `KB_DEVNET_CONFIG_PATH` peut remplacer `config/example.config.json`. Le profil doit utiliser un wallet persistant, autoriser les soumissions Devnet et respecter les plafonds de dépense et de frais.
La sortie `TOKEN_2022_METADATA_FIXTURE` conserve le mint et la signature de préparation. Chaque ligne `TOKEN_2022_METADATA_STEP` conserve lopération, la signature, le slot, la postcondition, le nombre de matérialisations et, pour `Emit`, la taille du `returnData`. Ces preuves doivent être reportées dans `SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json` et dans le rapport `V0_4_8_PRE_012_TOKEN_2022_METADATA_DEVNET_VALIDATION_REPORT.md` ; elles ne sont jamais préremplies avant lexécution réelle.
## Charger la matrice de validation Token-2022
```rust

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/lib.rs
// version: 14
// version: 15
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
@@ -23,10 +23,13 @@ mod solana_metaplex_token_metadata_scenarios;
mod solana_metaplex_token_metadata_validation;
mod solana_token_2022_devnet_execution;
mod solana_token_2022_devnet_scenarios;
mod solana_token_2022_metadata_campaign;
mod solana_token_2022_metadata_validation;
mod solana_token_2022_validation;
mod solana_token_execution;
mod solana_token_lifecycle;
mod token_2022_fixture;
mod token_2022_metadata_fixture;
/// Readiness report for one Devnet profile PostgreSQL store.
pub use self::environment::DevnetProfileStoreReadiness;
@@ -208,6 +211,28 @@ pub use self::solana_token_2022_devnet_scenarios::DevnetSplValidationImplementat
pub use self::solana_token_2022_devnet_scenarios::DevnetSplValidationScenario;
/// Returns the complete ordered Devnet scenario inventory.
pub use self::solana_token_2022_devnet_scenarios::devnet_spl_validation_scenarios;
/// One confirmed step in the five-instruction Token Metadata Devnet campaign.
pub use self::solana_token_2022_metadata_campaign::DevnetToken2022MetadataCampaignStepSummary;
/// Complete evidence retained by the Token Metadata Devnet campaign.
pub use self::solana_token_2022_metadata_campaign::DevnetToken2022MetadataCampaignSummary;
/// Executes all five Token Metadata interface instructions on one fresh Devnet mint.
pub use self::solana_token_2022_metadata_campaign::execute_devnet_token_2022_metadata_campaign;
/// Returns the exact ordered operation names covered by the Token Metadata campaign.
pub use self::solana_token_2022_metadata_campaign::token_2022_metadata_campaign_operation_names;
/// Maximum evidence entries retained by one Token Metadata validation scenario.
pub use self::solana_token_2022_metadata_validation::MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE;
/// One bounded evidence item retained by the Token Metadata validation matrix.
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationEvidence;
/// Canonical Token Metadata Devnet validation matrix.
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationMatrix;
/// One scenario in the Token Metadata Devnet validation matrix.
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationScenario;
/// Exact network-validation status for one Token Metadata scenario.
pub use self::solana_token_2022_metadata_validation::Token2022MetadataValidationStatus;
/// Loads and validates the Token Metadata Devnet validation matrix.
pub use self::solana_token_2022_metadata_validation::load_token_2022_metadata_validation_matrix;
/// Validates exact Token Metadata campaign coverage and evidence claims.
pub use self::solana_token_2022_metadata_validation::validate_token_2022_metadata_validation_matrix;
/// Maximum number of evidence records accepted in one validation report.
pub use self::solana_token_2022_validation::MAX_TOKEN_2022_VALIDATION_EVIDENCE;
/// Required validation environment for one Token-2022 scenario.
@@ -276,6 +301,22 @@ pub use self::token_2022_fixture::Token2022FixturePreparationOptions;
pub use self::token_2022_fixture::Token2022FixturePreparationSummary;
/// Creates or reuses one complete Token-2022 public scenario fixture.
pub use self::token_2022_fixture::prepare_token_2022_fixture;
/// Additional metadata key created and removed by the campaign.
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_KEY;
/// Stable metadata name used by the `0.4.8-pre.012` campaign.
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_NAME;
/// Stable metadata symbol used by the `0.4.8-pre.012` campaign.
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_SYMBOL;
/// Stable opaque URI stored by the `0.4.8-pre.012` campaign.
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_URI;
/// Additional metadata value created and removed by the campaign.
pub use self::token_2022_metadata_fixture::TOKEN_2022_METADATA_CAMPAIGN_VALUE;
/// Options used to prepare one fresh Token-2022 metadata fixture.
pub use self::token_2022_metadata_fixture::Token2022MetadataFixturePreparationOptions;
/// Fresh mint and authorities prepared for one Token Metadata campaign.
pub use self::token_2022_metadata_fixture::Token2022MetadataFixturePreparationSummary;
/// Creates one fresh Token-2022 mint with a self-referential Metadata Pointer.
pub use self::token_2022_metadata_fixture::prepare_token_2022_metadata_fixture;
/// Total transactions reserved by one complete Solana Program Metadata campaign.
pub(crate) use self::constants::SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT;

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_devnet_execution.rs
// version: 5
// version: 6
//! Devnet Token-2022 execution with stateful and canonical post-validation.
@@ -749,6 +749,43 @@ fn preflight_request(
return std::result::Result::Err(error);
}
},
kb_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
metadata,
update_authority: _,
mint,
mint_authority: _,
name: _,
symbol: _,
uri: _,
} => {
if metadata != mint {
return std::result::Result::Err(kb_core::Error::new(
"execution_spl_token_2022_metadata_mint_mismatch",
"Token Metadata Initialize on Token-2022 requires metadata and mint to be the same account",
));
}
requirements.push(metadata_mint_requirement("metadata", metadata, "metadata_pointer"));
},
kb_lib::ExSplTokenSingleOperation::UpdateTokenMetadataField {
metadata,
update_authority: _,
field: _,
value: _,
}
| kb_lib::ExSplTokenSingleOperation::RemoveTokenMetadataKey {
metadata,
update_authority: _,
key: _,
idempotent: _,
}
| kb_lib::ExSplTokenSingleOperation::UpdateTokenMetadataAuthority {
metadata,
current_authority: _,
new_authority: _,
}
| kb_lib::ExSplTokenSingleOperation::EmitTokenMetadata { metadata, start: _, end: _ } => {
requirements.push(metadata_mint_requirement("metadata", metadata, "token_metadata"));
},
_ => {
return std::result::Result::Err(kb_core::Error::new(
"execution_spl_token_2022_devnet_operation_unsupported",
@@ -797,6 +834,16 @@ fn mint_requirement(
};
}
fn metadata_mint_requirement(
role: &str,
mint: &kb_lib::MdPubkey,
required_extension: &str,
) -> kb_pipeline::Token2022PreflightRequirement {
let mut requirement = mint_requirement(role, mint, std::option::Option::None);
requirement.required_extensions = std::vec![required_extension.to_string()];
return requirement;
}
fn account_requirement(
role: &str,
account: &kb_lib::MdPubkey,
@@ -902,8 +949,12 @@ fn build_plan(
);
}
fn operation_requires_materialization(_operation: &kb_lib::ExSplToken2022Operation) -> bool {
return true;
fn operation_requires_materialization(operation: &kb_lib::ExSplToken2022Operation) -> bool {
return !matches!(
operation,
kb_lib::ExSplToken2022Operation::Instruction { value }
if matches!(value.as_ref(), kb_lib::ExSplTokenSingleOperation::EmitTokenMetadata { .. })
);
}
fn validate_profile_wallet_signers(
@@ -922,3 +973,73 @@ fn validate_profile_wallet_signers(
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
fn pubkey(byte: u8) -> kb_lib::MdPubkey {
return kb_lib::MdPubkey(bs58::encode([byte; 32]).into_string());
}
fn request(
operation: kb_lib::ExSplTokenSingleOperation,
) -> crate::DevnetSplToken2022ExecutionRequest {
return crate::DevnetSplToken2022ExecutionRequest::new(
"metadata-preflight-test",
kb_lib::ExSplToken2022Operation::Instruction { value: std::boxed::Box::new(operation) },
);
}
#[test]
fn metadata_preflight_requires_pointer_before_initialize_and_metadata_afterward() {
let mint = pubkey(1);
let initialize = request(kb_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
metadata: mint.clone(),
update_authority: pubkey(2),
mint: mint.clone(),
mint_authority: pubkey(2),
name: "Token".to_string(),
symbol: "TOK".to_string(),
uri: "https://example.invalid/token.json".to_string(),
});
let initialize_preflight =
super::preflight_request(&initialize).unwrap_or_else(|error| panic!("{error}"));
assert_eq!(
initialize_preflight.requirements[0].required_extensions,
vec!["metadata_pointer".to_string()]
);
let update = request(kb_lib::ExSplTokenSingleOperation::UpdateTokenMetadataField {
metadata: mint,
update_authority: pubkey(2),
field: kb_lib::ExSplTokenMetadataField::Key("campaign".to_string()),
value: "0.4.8-pre.012".to_string(),
});
let update_preflight =
super::preflight_request(&update).unwrap_or_else(|error| panic!("{error}"));
assert_eq!(
update_preflight.requirements[0].required_extensions,
vec!["token_metadata".to_string()]
);
}
#[test]
fn metadata_initialize_rejects_external_metadata_account_and_emit_skips_materialization() {
let invalid = request(kb_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
metadata: pubkey(1),
update_authority: pubkey(2),
mint: pubkey(3),
mint_authority: pubkey(2),
name: "Token".to_string(),
symbol: "TOK".to_string(),
uri: "https://example.invalid/token.json".to_string(),
});
assert!(super::preflight_request(&invalid).is_err());
let emit = kb_lib::ExSplToken2022Operation::Instruction {
value: std::boxed::Box::new(kb_lib::ExSplTokenSingleOperation::EmitTokenMetadata {
metadata: pubkey(1),
start: std::option::Option::None,
end: std::option::Option::None,
}),
};
assert!(!super::operation_requires_materialization(&emit));
}
}

View File

@@ -0,0 +1,586 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_metadata_campaign.rs
// version: 3
//! Ordered confirmed Devnet campaign for the five Token-2022 Token Metadata instructions.
/// One confirmed Token Metadata campaign step and its authoritative postcondition.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetToken2022MetadataCampaignStepSummary {
/// Stable step identifier.
pub step_id: std::string::String,
/// Canonical operation code built by `kb-lib`.
pub operation_code: std::string::String,
/// Complete simulation, submission, replay and instruction-materialization evidence.
pub execution: crate::DevnetSplToken2022ExecutionSummary,
/// Authoritative mint snapshot read at or after the confirmed transaction slot.
pub stateful_snapshot: kb_pipeline::Token2022StatefulReadResult,
/// Validated return-data evidence for the `Emit` step only.
pub emit_evidence: std::option::Option<kb_pipeline::Token2022MetadataEmitEvidence>,
/// Explicit stateful or return-data postcondition.
pub postcondition: kb_pipeline::Token2022ExecutionPostcondition,
}
/// Complete evidence retained by the five-step Token Metadata campaign.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetToken2022MetadataCampaignSummary {
/// Fresh Token-2022 mint prepared for this campaign.
pub fixture: crate::Token2022MetadataFixturePreparationSummary,
/// Five confirmed steps in interface order.
pub steps: std::vec::Vec<crate::DevnetToken2022MetadataCampaignStepSummary>,
}
/// Returns the exact ordered operation names exercised by the Devnet campaign.
pub fn token_2022_metadata_campaign_operation_names() -> &'static [&'static str; 5] {
return &[
"initialize_token_metadata",
"update_token_metadata_field",
"emit_token_metadata",
"remove_token_metadata_key",
"update_token_metadata_authority",
];
}
/// Prepares a fresh mint and executes all five Token Metadata interface instructions on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_token_2022_metadata_campaign<S, O>(
http_pool: &kb_onchain_transport::HttpEndpointPool,
store: &S,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::Token2022MetadataFixturePreparationOptions,
observer: &O,
) -> kb_core::Result<crate::DevnetToken2022MetadataCampaignSummary>
where
S: kb_store::RawTransactionStore
+ kb_store::CoreExtractionStore
+ kb_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if !options.operator_confirmed {
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 Token Metadata Devnet campaign requires explicit operator confirmation",
));
}
let fixture = match crate::prepare_token_2022_metadata_fixture(
http_pool,
profile,
workspace_root,
options,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let operations = campaign_operations(&fixture);
let decoders: std::vec::Vec<std::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(kb_lib::DcSplToken2022Decoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> =
std::vec![std::sync::Arc::new(kb_lib::MtMetadataToken2022Materializer)];
let mut steps = std::vec::Vec::with_capacity(operations.len());
for (index, (step_id, operation)) in operations.into_iter().enumerate() {
let operation_code = operation.operation_code().to_string();
let mut request = crate::DevnetSplToken2022ExecutionRequest::new(
format!("token-2022-metadata-devnet-{step_id}-{}", uuid::Uuid::new_v4()),
operation.clone(),
);
request.query_role = options.query_role.clone();
request.transaction_role = options.transaction_role.clone();
request.submit = true;
request.operator_confirmed = true;
request.post_validation_max_retries = 20;
let execution = match crate::execute_devnet_spl_token_2022(
http_pool,
store,
profile,
workspace_root,
&request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmation = match execution.confirmation.as_ref() {
std::option::Option::Some(value)
if matches!(
value.status,
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
) =>
{
value
},
std::option::Option::Some(value) => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_campaign_step_not_confirmed",
format!("campaign step {step_id} stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_campaign_confirmation_missing",
format!("campaign step {step_id} has no confirmation evidence"),
));
},
};
let diagnostic = match execution.post_execution.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_campaign_post_execution_missing",
format!("campaign step {step_id} has no post-execution diagnostic"),
));
},
};
if !execution.simulation.success
|| !diagnostic.canonical_inserted
|| !diagnostic.core_extracted
|| !diagnostic.decode_replayed
{
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_campaign_replay_incomplete",
format!(
"campaign step {step_id} did not complete simulation and canonical decode replay"
),
));
}
let idempotence_replay = match execution.idempotence_replay.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_campaign_idempotence_missing",
format!("campaign step {step_id} has no idempotence replay evidence"),
));
},
};
if idempotence_replay.failed_inputs != 0
|| idempotence_replay.processing_error_inputs != 0
|| idempotence_replay.processors.iter().any(|processor| {
return processor.failed != 0
|| processor.processing_errors != 0
|| processor.materialized_outputs != 0
|| processor.materialization_refused != 0;
})
{
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_campaign_idempotence_failed",
format!("campaign step {step_id} did not produce a clean second replay"),
));
}
if !execution.plan.policy.post_execution_validation.materialization_required
&& operation_code != kb_lib::EX_SPL_TOKEN_2022_EMIT_TOKEN_METADATA_OPERATION
{
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_campaign_materialization_policy_invalid",
format!("campaign step {step_id} unexpectedly disabled materialization"),
));
}
if execution.plan.policy.post_execution_validation.materialization_required
&& execution.materializations.is_empty()
{
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_campaign_materialization_missing",
format!("campaign step {step_id} produced no metadata materialization"),
));
}
let stateful_snapshot = match kb_pipeline::read_token_2022_stateful_snapshot(
http_pool,
&kb_pipeline::Token2022StatefulReadRequest {
query_role: options.query_role.clone(),
account: fixture.mint.clone(),
kind: kb_lib::DcToken2022StateKind::Mint,
min_context_slot: confirmation.slot,
max_data_bytes: kb_pipeline::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES,
context: kb_pipeline::Token2022StatefulContext::default(),
},
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let emit_evidence = match &operation {
kb_lib::ExSplToken2022Operation::Instruction { value } => match value.as_ref() {
kb_lib::ExSplTokenSingleOperation::EmitTokenMetadata { start, end, .. } => {
match kb_pipeline::inspect_token_2022_metadata_emit_simulation(
&execution.simulation,
*start,
*end,
) {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
},
_ => std::option::Option::None,
},
kb_lib::ExSplToken2022Operation::Batch { instructions: _ } => std::option::Option::None,
};
let postcondition = campaign_postcondition(
index,
&fixture,
&stateful_snapshot.snapshot,
emit_evidence.as_ref(),
);
if postcondition.status != kb_pipeline::Token2022ExecutionPostconditionStatus::Confirmed {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_campaign_postcondition_failed",
format!("campaign step {step_id}: {}", postcondition.diagnostic),
));
}
steps.push(crate::DevnetToken2022MetadataCampaignStepSummary {
step_id: step_id.to_string(),
operation_code,
execution,
stateful_snapshot,
emit_evidence,
postcondition,
});
}
return std::result::Result::Ok(crate::DevnetToken2022MetadataCampaignSummary {
fixture,
steps,
});
}
fn campaign_operations(
fixture: &crate::Token2022MetadataFixturePreparationSummary,
) -> std::vec::Vec<(&'static str, kb_lib::ExSplToken2022Operation)> {
let metadata = fixture.mint.clone();
let authority = fixture.initial_authority.clone();
return std::vec![
(
"token_2022_metadata_initialize",
instruction(kb_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
metadata: metadata.clone(),
update_authority: authority.clone(),
mint: metadata.clone(),
mint_authority: authority.clone(),
name: crate::TOKEN_2022_METADATA_CAMPAIGN_NAME.to_string(),
symbol: crate::TOKEN_2022_METADATA_CAMPAIGN_SYMBOL.to_string(),
uri: crate::TOKEN_2022_METADATA_CAMPAIGN_URI.to_string(),
}),
),
(
"token_2022_metadata_update_field",
instruction(kb_lib::ExSplTokenSingleOperation::UpdateTokenMetadataField {
metadata: metadata.clone(),
update_authority: authority.clone(),
field: kb_lib::ExSplTokenMetadataField::Key(
crate::TOKEN_2022_METADATA_CAMPAIGN_KEY.to_string(),
),
value: crate::TOKEN_2022_METADATA_CAMPAIGN_VALUE.to_string(),
}),
),
(
"token_2022_metadata_emit",
instruction(kb_lib::ExSplTokenSingleOperation::EmitTokenMetadata {
metadata: metadata.clone(),
start: std::option::Option::None,
end: std::option::Option::None,
}),
),
(
"token_2022_metadata_remove_key",
instruction(kb_lib::ExSplTokenSingleOperation::RemoveTokenMetadataKey {
metadata: metadata.clone(),
update_authority: authority.clone(),
key: crate::TOKEN_2022_METADATA_CAMPAIGN_KEY.to_string(),
idempotent: false,
}),
),
(
"token_2022_metadata_update_authority",
instruction(kb_lib::ExSplTokenSingleOperation::UpdateTokenMetadataAuthority {
metadata,
current_authority: authority,
new_authority: std::option::Option::Some(fixture.final_authority.clone()),
}),
),
];
}
fn instruction(value: kb_lib::ExSplTokenSingleOperation) -> kb_lib::ExSplToken2022Operation {
return kb_lib::ExSplToken2022Operation::Instruction { value: std::boxed::Box::new(value) };
}
fn campaign_postcondition(
step_index: usize,
fixture: &crate::Token2022MetadataFixturePreparationSummary,
snapshot: &kb_pipeline::Token2022StatefulSnapshotBundle,
emit_evidence: std::option::Option<&kb_pipeline::Token2022MetadataEmitEvidence>,
) -> kb_pipeline::Token2022ExecutionPostcondition {
if step_index == 4 {
return kb_pipeline::inspect_token_2022_metadata_authority_postcondition(
&fixture.mint,
std::option::Option::Some(&fixture.final_authority),
snapshot,
);
}
let value_fields = metadata_value_fields(snapshot);
let confirmed = match step_index {
0 => value_fields.is_some_and(|value| {
return required_fields_match(value, fixture.initial_authority.0.as_str())
&& !contains_campaign_pair(value);
}),
1 => value_fields.is_some_and(|value| {
return required_fields_match(value, fixture.initial_authority.0.as_str())
&& contains_campaign_pair(value);
}),
2 => {
let snapshot_matches = value_fields.is_some_and(|value| {
return required_fields_match(value, fixture.initial_authority.0.as_str())
&& contains_campaign_pair(value);
});
let emit_matches = emit_evidence
.and_then(|evidence| return evidence.decoded_metadata.as_ref())
.is_some_and(|value| {
return required_fields_match(value, fixture.initial_authority.0.as_str())
&& contains_campaign_pair(value);
});
snapshot_matches && emit_matches
},
3 => value_fields.is_some_and(|value| {
return required_fields_match(value, fixture.initial_authority.0.as_str())
&& !contains_campaign_pair(value);
}),
_ => false,
};
return kb_pipeline::Token2022ExecutionPostcondition {
role: "metadata".to_string(),
account: fixture.mint.clone(),
status: if confirmed {
kb_pipeline::Token2022ExecutionPostconditionStatus::Confirmed
} else {
kb_pipeline::Token2022ExecutionPostconditionStatus::Contradicted
},
diagnostic: if confirmed {
format!("Token-2022 metadata campaign postcondition {step_index} is confirmed")
} else {
format!("Token-2022 metadata campaign postcondition {step_index} is contradicted")
},
};
}
fn metadata_value_fields(
snapshot: &kb_pipeline::Token2022StatefulSnapshotBundle,
) -> std::option::Option<&serde_json::Value> {
return snapshot.outputs.iter().find_map(|output| {
if output.family != kb_lib::MdMaterializedEventFamily::Metadata
|| output.payload_json.get("projectionKind").and_then(serde_json::Value::as_str)
!= std::option::Option::Some("token_metadata")
{
return std::option::Option::None;
}
return output.payload_json.get("valueFields");
});
}
fn required_fields_match(value: &serde_json::Value, authority: &str) -> bool {
return value.get("updateAuthority").and_then(serde_json::Value::as_str)
== std::option::Option::Some(authority)
&& value.get("name").and_then(serde_json::Value::as_str)
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_NAME)
&& value.get("symbol").and_then(serde_json::Value::as_str)
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_SYMBOL)
&& value.get("uri").and_then(serde_json::Value::as_str)
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_URI);
}
fn contains_campaign_pair(value: &serde_json::Value) -> bool {
return value
.get("additionalMetadata")
.and_then(serde_json::Value::as_array)
.is_some_and(|pairs| {
return pairs.iter().any(|pair| {
return pair.get("key").and_then(serde_json::Value::as_str)
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_KEY)
&& pair.get("value").and_then(serde_json::Value::as_str)
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_VALUE);
});
});
}
#[cfg(test)]
mod tests {
fn pubkey(byte: u8) -> kb_lib::MdPubkey {
return kb_lib::MdPubkey(bs58::encode([byte; 32]).into_string());
}
fn fixture() -> crate::Token2022MetadataFixturePreparationSummary {
return crate::Token2022MetadataFixturePreparationSummary {
mint_keypair_path: std::path::PathBuf::from("mint.json"),
final_authority_keypair_path: std::path::PathBuf::from("authority.json"),
mint: pubkey(1),
initial_authority: pubkey(2),
final_authority: pubkey(3),
mint_space: 234,
rent_budget_space: 512,
rent_reserve_lamports: 1,
preparation_signature: "signature".to_string(),
initial_snapshot: kb_pipeline::Token2022StatefulReadResult {
commitment: "confirmed".to_string(),
context_slot: 1,
snapshot: kb_pipeline::Token2022StatefulSnapshotBundle {
account_key: pubkey(1).0,
slot: 1,
state_kind: "mint".to_string(),
extension_names: vec!["metadata_pointer".to_string()],
outputs: std::vec::Vec::new(),
},
},
};
}
#[test]
fn campaign_contract_covers_five_interface_operations_once_in_order() {
let operations = super::campaign_operations(&fixture());
assert_eq!(
operations
.iter()
.map(|(_, operation)| return operation.operation_code())
.collect::<std::vec::Vec<_>>(),
vec![
kb_lib::EX_SPL_TOKEN_2022_INITIALIZE_TOKEN_METADATA_OPERATION,
kb_lib::EX_SPL_TOKEN_2022_UPDATE_TOKEN_METADATA_FIELD_OPERATION,
kb_lib::EX_SPL_TOKEN_2022_EMIT_TOKEN_METADATA_OPERATION,
kb_lib::EX_SPL_TOKEN_2022_REMOVE_TOKEN_METADATA_KEY_OPERATION,
kb_lib::EX_SPL_TOKEN_2022_UPDATE_TOKEN_METADATA_AUTHORITY_OPERATION,
]
);
assert_eq!(
crate::token_2022_metadata_campaign_operation_names(),
&[
"initialize_token_metadata",
"update_token_metadata_field",
"emit_token_metadata",
"remove_token_metadata_key",
"update_token_metadata_authority",
]
);
}
#[test]
fn campaign_pair_detection_is_exact_and_removal_is_observable() {
let with_pair = serde_json::json!({
"additionalMetadata": [{
"key": crate::TOKEN_2022_METADATA_CAMPAIGN_KEY,
"value": crate::TOKEN_2022_METADATA_CAMPAIGN_VALUE
}]
});
let without_pair = serde_json::json!({"additionalMetadata": []});
assert!(super::contains_campaign_pair(&with_pair));
assert!(!super::contains_campaign_pair(&without_pair));
}
#[tokio::test]
async fn optional_devnet_token_2022_metadata_campaign_from_env() {
if std::env::var("KB_DEVNET_TOKEN_2022_METADATA_CAMPAIGN_TEST").ok().as_deref()
!= std::option::Option::Some("1")
{
return;
}
if std::env::var("KB_DEVNET_TOKEN_2022_METADATA_OPERATOR_CONFIRMED")
.ok()
.as_deref()
!= std::option::Option::Some("1")
{
panic!(
"KB_DEVNET_TOKEN_2022_METADATA_OPERATOR_CONFIRMED=1 is required for the spending campaign"
);
}
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
};
let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") {
std::result::Result::Ok(value) => {
let path = std::path::PathBuf::from(value);
if path.is_absolute() { path } else { workspace_root.join(path) }
},
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
};
let config = kb_config::read_config_json_file_with_environment(
config_path.as_path(),
workspace_root,
)
.unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}"));
let profile_name = std::env::var("KB_DEVNET_PROFILE").ok();
let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref())
.unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}"));
let database_url = std::env::var("KB_POSTGRES_TEST_URL")
.unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}"));
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
let pool = kb_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}"));
let store_options = kb_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
)
.unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}"));
let store = kb_store::PostgresStore::connect(store_options)
.await
.unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}"));
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("PostgreSQL schema initialization failed: {error}");
}
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
workspace_root.join(configured_wallet_dir)
};
let mut options = crate::Token2022MetadataFixturePreparationOptions::new(wallet_dir);
options.operator_confirmed = true;
let summary = crate::execute_devnet_token_2022_metadata_campaign(
&pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| {
panic!("Token-2022 Token Metadata Devnet campaign failed: {error}")
});
assert_eq!(summary.steps.len(), 5);
assert!(summary.steps.iter().all(|step| {
return step.postcondition.status
== kb_pipeline::Token2022ExecutionPostconditionStatus::Confirmed;
}));
println!(
"TOKEN_2022_METADATA_FIXTURE mint={} preparation_signature={} initial_authority={} final_authority={}",
summary.fixture.mint.0,
summary.fixture.preparation_signature,
summary.fixture.initial_authority.0,
summary.fixture.final_authority.0,
);
for step in &summary.steps {
let signature = step
.execution
.confirmation
.as_ref()
.map(|value| return value.signature.0.as_str())
.unwrap_or("missing");
let slot = step.execution.confirmation.as_ref().and_then(|value| return value.slot);
println!(
"TOKEN_2022_METADATA_STEP id={} operation={} signature={} slot={:?} postcondition={:?} materializations={} emit_bytes={}",
step.step_id,
step.operation_code,
signature,
slot,
step.postcondition.status,
step.execution.materializations.len(),
step.emit_evidence.as_ref().map(|value| return value.data.len()).unwrap_or(0),
);
}
}
}

View File

@@ -0,0 +1,210 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_metadata_validation.rs
// version: 2
//! Conservative validation matrix for the `0.4.8-pre.012` Token Metadata campaign.
/// Maximum evidence items retained by one campaign scenario.
pub const MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE: usize = 16;
/// Exact network-validation status for one Token Metadata scenario.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Token2022MetadataValidationStatus {
/// No real Devnet execution has been recorded.
NotRun,
/// The scenario completed with all declared evidence.
Confirmed,
/// The scenario is unavailable for an explicit recorded reason.
Unavailable,
}
/// One bounded evidence item retained by the validation matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Token2022MetadataValidationEvidence {
/// Stable evidence kind.
pub kind: std::string::String,
/// Bounded evidence value such as a signature, slot or diagnostic.
pub value: std::string::String,
}
/// One ordered Token Metadata scenario in the canonical Devnet matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Token2022MetadataValidationScenario {
/// Stable campaign step identifier.
pub id: std::string::String,
/// Short Token Metadata interface operation name.
pub operation: std::string::String,
/// Current evidence-qualified network status.
pub status: crate::Token2022MetadataValidationStatus,
/// Evidence kinds required before promotion to `confirmed`.
pub required_evidence: std::vec::Vec<std::string::String>,
/// Bounded observed evidence.
pub evidence: std::vec::Vec<crate::Token2022MetadataValidationEvidence>,
}
/// Canonical `0.4.8-pre.012` Token Metadata Devnet validation matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Token2022MetadataValidationMatrix {
/// Matrix schema version.
pub matrix_version: u32,
/// Milestone owning the campaign.
pub milestone: std::string::String,
/// Token-2022 program ID.
pub program_id: std::string::String,
/// Five scenarios in campaign order.
pub scenarios: std::vec::Vec<crate::Token2022MetadataValidationScenario>,
}
/// Loads and validates the canonical Token Metadata Devnet matrix.
pub fn load_token_2022_metadata_validation_matrix(
) -> kb_core::Result<crate::Token2022MetadataValidationMatrix> {
let matrix = match serde_json::from_str::<crate::Token2022MetadataValidationMatrix>(
include_str!(
"../../test-fixtures/contract-matrices/SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_validation_matrix_invalid_json",
error.to_string(),
));
},
};
match crate::validate_token_2022_metadata_validation_matrix(&matrix) {
std::result::Result::Ok(()) => std::result::Result::Ok(matrix),
std::result::Result::Err(error) => std::result::Result::Err(error),
}
}
/// Validates campaign coverage and prevents promotion without complete evidence.
pub fn validate_token_2022_metadata_validation_matrix(
matrix: &crate::Token2022MetadataValidationMatrix,
) -> kb_core::Result<()> {
if matrix.matrix_version != 1
|| matrix.milestone != "0.4.8-pre.012"
|| matrix.program_id != kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID
{
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 metadata validation matrix identity is invalid",
));
}
let expected = crate::token_2022_metadata_campaign_operation_names();
if matrix.scenarios.len() != expected.len() {
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata validation matrix requires exactly {} scenarios",
expected.len()
)));
}
let mut ids = std::collections::BTreeSet::new();
for (index, scenario) in matrix.scenarios.iter().enumerate() {
if !ids.insert(scenario.id.as_str()) {
return std::result::Result::Err(kb_core::Error::config(format!(
"duplicate Token-2022 metadata validation scenario {}",
scenario.id
)));
}
if scenario.operation != expected[index] {
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata scenario {} expected operation {}, got {}",
scenario.id, expected[index], scenario.operation
)));
}
if scenario.required_evidence.is_empty()
|| scenario.required_evidence.len() > MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE
|| scenario.evidence.len() > MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata scenario {} has invalid evidence bounds",
scenario.id
)));
}
let mut required = std::collections::BTreeSet::new();
if scenario
.required_evidence
.iter()
.any(|kind| return kind.trim().is_empty() || !required.insert(kind.as_str()))
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata scenario {} has empty or duplicate required evidence",
scenario.id
)));
}
let mut observed = std::collections::BTreeSet::new();
for evidence in &scenario.evidence {
if evidence.kind.trim().is_empty()
|| evidence.value.trim().is_empty()
|| !observed.insert(evidence.kind.as_str())
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata scenario {} has invalid observed evidence",
scenario.id
)));
}
}
match scenario.status {
crate::Token2022MetadataValidationStatus::NotRun => {
if !scenario.evidence.is_empty() {
return std::result::Result::Err(kb_core::Error::config(format!(
"not-run Token-2022 metadata scenario {} cannot claim evidence",
scenario.id
)));
}
},
crate::Token2022MetadataValidationStatus::Confirmed => {
if scenario
.required_evidence
.iter()
.any(|kind| return !observed.contains(kind.as_str()))
{
return std::result::Result::Err(kb_core::Error::config(format!(
"confirmed Token-2022 metadata scenario {} is missing required evidence",
scenario.id
)));
}
},
crate::Token2022MetadataValidationStatus::Unavailable => {
if !observed.contains("unavailable_reason") {
return std::result::Result::Err(kb_core::Error::config(format!(
"unavailable Token-2022 metadata scenario {} requires unavailable_reason",
scenario.id
)));
}
},
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
fn canonical_matrix_is_closed_ordered_and_confirmed_after_network_execution() {
let matrix = crate::load_token_2022_metadata_validation_matrix()
.unwrap_or_else(|error| panic!("Token-2022 metadata matrix failed: {error}"));
assert_eq!(matrix.scenarios.len(), 5);
assert!(matrix.scenarios.iter().all(|scenario| {
let observed = scenario
.evidence
.iter()
.map(|evidence| return evidence.kind.as_str())
.collect::<std::collections::BTreeSet<_>>();
return scenario.status == crate::Token2022MetadataValidationStatus::Confirmed
&& scenario
.required_evidence
.iter()
.all(|kind| return observed.contains(kind.as_str()));
}));
assert_eq!(
matrix
.scenarios
.iter()
.map(|scenario| return scenario.operation.as_str())
.collect::<std::vec::Vec<_>>(),
crate::token_2022_metadata_campaign_operation_names().as_slice()
);
}
}

View File

@@ -0,0 +1,661 @@
// file: kb-pipeline-demo-scenarios/src/token_2022_metadata_fixture.rs
// version: 2
//! Native Token-2022 mint preparation for the embedded Token Metadata Devnet campaign.
/// Stable metadata name used by the `0.4.8-pre.012` campaign.
pub const TOKEN_2022_METADATA_CAMPAIGN_NAME: &str = "Khadhroony Token-2022 Metadata";
/// Stable metadata symbol used by the `0.4.8-pre.012` campaign.
pub const TOKEN_2022_METADATA_CAMPAIGN_SYMBOL: &str = "KBTM";
/// Opaque URI stored on-chain by the `0.4.8-pre.012` campaign.
pub const TOKEN_2022_METADATA_CAMPAIGN_URI: &str =
"https://example.invalid/khadhroony/token-2022-metadata.json";
/// Additional metadata key created and removed by the campaign.
pub const TOKEN_2022_METADATA_CAMPAIGN_KEY: &str = "campaign";
/// Additional metadata value created and removed by the campaign.
pub const TOKEN_2022_METADATA_CAMPAIGN_VALUE: &str = "0.4.8-pre.012";
const TOKEN_2022_TLV_HEADER_BYTES: usize = 4;
/// Options used to prepare one fresh Token-2022 mint for the metadata campaign.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Token2022MetadataFixturePreparationOptions {
/// Endpoint role used for state, rent, balance, blockhash and fee calls.
pub query_role: std::string::String,
/// Endpoint role used for simulation, submission and confirmation polling.
pub transaction_role: std::string::String,
/// Directory receiving persistent fixture keypairs.
pub wallet_dir: std::path::PathBuf,
/// Explicit operator authorization for the lamport-spending fixture transaction.
pub operator_confirmed: bool,
}
impl crate::Token2022MetadataFixturePreparationOptions {
/// Creates conservative defaults matching Devnet execution profiles.
pub fn new(wallet_dir: std::path::PathBuf) -> Self {
return Self {
query_role: "http_queries".to_string(),
transaction_role: "http_transactions".to_string(),
wallet_dir,
operator_confirmed: false,
};
}
}
/// Fresh Token-2022 mint and authority identities used by the metadata campaign.
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
pub struct Token2022MetadataFixturePreparationSummary {
/// Persistent fixture mint keypair path.
pub mint_keypair_path: std::path::PathBuf,
/// Persistent final metadata-authority keypair path.
pub final_authority_keypair_path: std::path::PathBuf,
/// Fresh Token-2022 mint address. Embedded metadata uses this same account.
pub mint: kb_lib::MdPubkey,
/// Profile wallet used as mint and initial metadata update authority.
pub initial_authority: kb_lib::MdPubkey,
/// Fresh public key installed by the final `UpdateAuthority` campaign step.
pub final_authority: kb_lib::MdPubkey,
/// Initial mint account bytes containing only the fixed Metadata Pointer extension.
pub mint_space: usize,
/// Account-size budget used to pre-fund rent for the largest campaign metadata state.
pub rent_budget_space: usize,
/// Rent reserve deposited when the mint is created.
pub rent_reserve_lamports: u64,
/// Confirmed signature that created the Token-2022 mint fixture.
pub preparation_signature: std::string::String,
/// Authoritative snapshot after mint creation and before Token Metadata initialization.
pub initial_snapshot: kb_pipeline::Token2022StatefulReadResult,
}
/// Creates a fresh Token-2022 mint with Metadata Pointer initialized to the mint itself.
///
/// The mint account is created with the fixed Metadata Pointer size but is pre-funded
/// for the largest state reached by this campaign so Token Metadata reallocations
/// remain rent-exempt without introducing an additional payer instruction.
pub async fn prepare_token_2022_metadata_fixture(
http_pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::Token2022MetadataFixturePreparationOptions,
) -> kb_core::Result<crate::Token2022MetadataFixturePreparationSummary> {
if options.query_role.trim().is_empty() || options.transaction_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 metadata fixture endpoint roles must not be empty",
));
}
if profile.wallet.cluster != "devnet"
|| !profile.wallet.temporary_wallet_enabled
|| !profile.wallet.temporary_wallet_persist
{
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 metadata fixture requires a persistent Devnet wallet profile",
));
}
if !profile.wallet.devnet_send_enabled || !options.operator_confirmed {
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 metadata fixture requires enabled Devnet submission and explicit operator confirmation",
));
}
let operator = match crate::load_profile_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture_store = match kb_wallet::TemporaryWalletStore::new(
options.wallet_dir.join("token_2022_metadata_validation"),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture_epoch = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
std::result::Result::Ok(value) => value.as_millis(),
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"unable to create a unique Token-2022 metadata fixture identifier: {error}"
)));
},
};
let mint_alias = match kb_wallet::WalletAlias::parse(
format!("token-2022-metadata-mint-{fixture_epoch}").as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let final_authority_alias = match kb_wallet::WalletAlias::parse(
format!("token-2022-metadata-authority-{fixture_epoch}").as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint_wallet = match fixture_store.load_or_create(mint_alias.clone()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let final_authority_wallet =
match fixture_store.load_or_create(final_authority_alias.clone()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint = kb_lib::MdPubkey(mint_wallet.public_key());
let initial_authority = kb_lib::MdPubkey(operator.public_key());
let final_authority = kb_lib::MdPubkey(final_authority_wallet.public_key());
let mint_space = match token_2022_metadata_pointer_mint_space() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rent_budget_space = metadata_campaign_rent_budget_space(mint_space);
let preparation_signature = match create_native_token_2022_metadata_mint(
http_pool,
profile,
options,
&operator,
&mint_wallet,
mint_space,
rent_budget_space,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let initial_snapshot = match kb_pipeline::read_token_2022_stateful_snapshot(
http_pool,
&kb_pipeline::Token2022StatefulReadRequest {
query_role: options.query_role.clone(),
account: mint.clone(),
kind: kb_lib::DcToken2022StateKind::Mint,
min_context_slot: std::option::Option::None,
max_data_bytes: kb_pipeline::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES,
context: kb_pipeline::Token2022StatefulContext::default(),
},
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !initial_snapshot
.snapshot
.extension_names
.iter()
.any(|value| return value == "metadata_pointer")
|| initial_snapshot
.snapshot
.extension_names
.iter()
.any(|value| return value == "token_metadata")
|| !metadata_pointer_snapshot_matches(&initial_snapshot.snapshot, &mint, &initial_authority)
{
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_fixture_state_invalid",
"fresh metadata fixture must contain the expected self-referential Metadata Pointer and no Token Metadata extension",
));
}
let rent = match http_pool
.get_minimum_balance_for_rent_exemption_for_role(
options.query_role.as_str(),
match u64::try_from(rent_budget_space) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata rent budget does not fit u64: {error}"
)));
},
},
&kb_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::Token2022MetadataFixturePreparationSummary {
mint_keypair_path: fixture_store.wallet_path(&mint_alias),
final_authority_keypair_path: fixture_store.wallet_path(&final_authority_alias),
mint,
initial_authority,
final_authority,
mint_space,
rent_budget_space,
rent_reserve_lamports: rent.minimum_balance_lamports,
preparation_signature,
initial_snapshot,
});
}
fn metadata_pointer_snapshot_matches(
snapshot: &kb_pipeline::Token2022StatefulSnapshotBundle,
mint: &kb_lib::MdPubkey,
authority: &kb_lib::MdPubkey,
) -> bool {
return snapshot.outputs.iter().any(|output| {
return output.family == kb_lib::MdMaterializedEventFamily::Admin
&& output.payload_json.get("extensionName").and_then(serde_json::Value::as_str)
== std::option::Option::Some("metadata_pointer")
&& output
.payload_json
.get("valueFields")
.and_then(|value| return value.get("address"))
.and_then(serde_json::Value::as_str)
== std::option::Option::Some(mint.0.as_str())
&& output
.payload_json
.get("valueFields")
.and_then(|value| return value.get("authority"))
.and_then(serde_json::Value::as_str)
== std::option::Option::Some(authority.0.as_str());
});
}
fn token_2022_metadata_pointer_mint_space() -> kb_core::Result<usize> {
return match spl_token_2022_interface::extension::ExtensionType::try_calculate_account_len::<
spl_token_2022_interface::pod::PodMint,
>(&[spl_token_2022_interface::extension::ExtensionType::MetadataPointer])
{
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_fixture_space_failed",
format!("unable to calculate Metadata Pointer mint space: {error}"),
)),
};
}
fn metadata_campaign_rent_budget_space(mint_space: usize) -> usize {
return mint_space
.saturating_add(TOKEN_2022_TLV_HEADER_BYTES)
.saturating_add(metadata_campaign_max_value_bytes());
}
fn metadata_campaign_max_value_bytes() -> usize {
return 64_usize
.saturating_add(4)
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_NAME.len())
.saturating_add(4)
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_SYMBOL.len())
.saturating_add(4)
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_URI.len())
.saturating_add(4)
.saturating_add(4)
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_KEY.len())
.saturating_add(4)
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_VALUE.len());
}
#[allow(clippy::too_many_arguments)]
async fn create_native_token_2022_metadata_mint(
http_pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
options: &crate::Token2022MetadataFixturePreparationOptions,
operator: &kb_wallet::TemporaryWallet,
mint_wallet: &kb_wallet::TemporaryWallet,
mint_space: usize,
rent_budget_space: usize,
) -> kb_core::Result<std::string::String> {
let rent_space = match u64::try_from(rent_budget_space) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 metadata rent budget does not fit u64: {error}"
)));
},
};
let account_space = match u64::try_from(mint_space) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 Metadata Pointer mint space does not fit u64: {error}"
)));
},
};
let rent = match http_pool
.get_minimum_balance_for_rent_exemption_for_role(
options.query_role.as_str(),
rent_space,
&kb_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let payer = kb_lib::MdPubkey(operator.public_key());
let mint = kb_lib::MdPubkey(mint_wallet.public_key());
let policy =
fixture_policy(profile, payer.clone(), mint.clone(), rent.minimum_balance_lamports);
let system_intent = kb_lib::ExSolanaCoreExecutionIntent {
intent_id: format!("token-2022-metadata-fixture-create-account-{}", mint.0),
fee_payer: payer.clone(),
policy: policy.clone(),
operation: kb_lib::ExSolanaCoreOperation::SystemCreateAccount {
from: payer.clone(),
new_account: mint.clone(),
lamports: rent.minimum_balance_lamports,
space: account_space,
owner: kb_lib::MdPubkey(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()),
},
};
let system_plan = match kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
&kb_lib::ExSolanaCoreExecutor,
&system_intent,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let token_program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid Token-2022 Program ID: {error}"
)));
},
};
let mint_pubkey = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(mint.0.as_str())
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid Token-2022 metadata fixture mint: {error}"
)));
},
};
let authority_pubkey =
match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(payer.0.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid Token-2022 metadata fixture authority: {error}"
)));
},
};
let pointer_instruction =
match spl_token_2022_interface::extension::metadata_pointer::instruction::initialize(
&token_program,
&mint_pubkey,
std::option::Option::Some(authority_pubkey),
std::option::Option::Some(mint_pubkey),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_fixture_pointer_build_failed",
error.to_string(),
));
},
};
let initialize_mint = match spl_token_2022_interface::instruction::initialize_mint2(
&token_program,
&mint_pubkey,
&authority_pubkey,
std::option::Option::Some(&authority_pubkey),
0,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_fixture_initialize_mint_build_failed",
error.to_string(),
));
},
};
let plan = merge_fixture_instructions(
system_plan,
std::vec![
planned_instruction("spl.token_2022.initialize_metadata_pointer", &pointer_instruction,),
planned_instruction("spl.token_2022.initialize_mint2", &initialize_mint),
],
policy,
rent.minimum_balance_lamports,
);
let plan_evaluation = match kb_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if plan_evaluation.decision == kb_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_fixture_plan_denied",
crate::violation_message(plan_evaluation.violations.as_slice()),
));
}
let latest_blockhash = match http_pool
.get_latest_blockhash_for_role(
options.query_role.as_str(),
&kb_onchain_transport::GetLatestBlockhashConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned = match kb_lib::executor_solana_build_legacy_transaction(
&plan,
latest_blockhash.blockhash.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fee = match http_pool
.get_fee_for_message_for_role(
options.query_role.as_str(),
unsigned.message_base64().as_str(),
&kb_onchain_transport::GetFeeForMessageConfig::new(
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
std::option::Option::Some(latest_blockhash.context.slot),
),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned_base64 = match unsigned.transaction_base64() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_config = match kb_onchain_transport::SimulateTransactionConfig::new(
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
false,
false,
std::option::Option::Some(latest_blockhash.context.slot),
true,
std::option::Option::None,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_rpc = match http_pool
.simulate_transaction_for_role(
options.transaction_role.as_str(),
unsigned_base64.as_str(),
&simulation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation = simulation_rpc.to_execution_result(
kb_lib::ExApiExecutionCluster::Devnet,
kb_lib::ExApiExecutionBlockhashKind::Latest,
std::option::Option::Some(
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(&fee),
);
if !simulation.success {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_fixture_simulation_failed",
crate::simulation_failure_message(&simulation),
));
}
let send_evaluation = match kb_lib::ExSafetyChecker.evaluate_send(&plan, &simulation) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if send_evaluation.decision == kb_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_fixture_send_denied",
crate::violation_message(send_evaluation.violations.as_slice()),
));
}
let evidence = unsigned.bind_simulation(simulation);
let signed = match unsigned
.sign_after_simulation(&evidence, &[operator.as_signer(), mint_wallet.as_signer()])
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = signed.verify_signatures() {
return std::result::Result::Err(error);
}
let signature = signed.primary_signature().clone();
let send_config = match kb_onchain_transport::SendTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = http_pool
.send_transaction_for_role(
options.transaction_role.as_str(),
signed.transaction_base64().as_str(),
&signature,
&send_config,
)
.await
{
return std::result::Result::Err(error);
}
let confirmation_config =
match kb_onchain_transport::ConfirmTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(latest_blockhash.last_valid_block_height),
std::option::Option::Some(latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmation = match http_pool
.confirm_transaction_for_roles(
options.transaction_role.as_str(),
options.query_role.as_str(),
kb_lib::ExApiExecutionCluster::Devnet,
&signature,
&confirmation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !matches!(
confirmation.status,
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
) {
return std::result::Result::Err(kb_core::Error::new(
"token_2022_metadata_fixture_confirmation_failed",
format!("Token-2022 metadata mint preparation stopped at {:?}", confirmation.status),
));
}
return std::result::Result::Ok(signature.0);
}
fn planned_instruction(
operation_code: &str,
instruction: &solana_instruction::Instruction,
) -> kb_lib::ExApiPlannedInstruction {
return kb_lib::ExApiPlannedInstruction {
program_id: kb_lib::MdProgramId(instruction.program_id.to_string()),
operation_code: operation_code.to_string(),
accounts: instruction
.accounts
.iter()
.map(|account| {
return kb_lib::ExApiPlannedAccount {
pubkey: kb_lib::MdPubkey(account.pubkey.to_string()),
is_signer: account.is_signer,
is_writable: account.is_writable,
};
})
.collect(),
data: instruction.data.clone(),
};
}
fn fixture_policy(
profile: &kb_config::ProfileConfig,
payer: kb_lib::MdPubkey,
mint: kb_lib::MdPubkey,
_rent_lamports: u64,
) -> kb_lib::ExApiExecutionPolicy {
return kb_lib::ExApiExecutionPolicy {
cluster: kb_lib::ExApiExecutionClusterPolicy {
expected_cluster: kb_lib::ExApiExecutionCluster::Devnet,
allow_mainnet: false,
mainnet_confirmation: false,
},
simulation: kb_lib::ExApiExecutionSimulationPolicy::Required,
blockhash: kb_lib::ExApiExecutionBlockhashPolicy {
kind: kb_lib::ExApiExecutionBlockhashKind::Latest,
max_age_slots: std::option::Option::Some(
profile.execution.recent_blockhash_max_age_slots,
),
nonce_account: std::option::Option::None,
nonce_authority: std::option::Option::None,
},
cost_limit: kb_lib::ExApiExecutionCostLimit {
max_spend_lamports: std::option::Option::Some(
profile.execution.devnet_max_spend_lamports,
),
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
max_compute_unit_price_micro_lamports: std::option::Option::Some(
profile.execution.max_compute_unit_price_micro_lamports,
),
},
authorized_signers: std::vec![payer, mint],
dry_run: false,
post_execution_validation: kb_lib::ExApiPostExecutionValidationPolicy {
canonical_insert_required: false,
core_extraction_required: false,
decode_replay_required: false,
materialization_required: false,
},
};
}
fn merge_fixture_instructions(
mut system_plan: kb_lib::ExApiPreparedExecutionPlan,
token_instructions: std::vec::Vec<kb_lib::ExApiPlannedInstruction>,
policy: kb_lib::ExApiExecutionPolicy,
rent_lamports: u64,
) -> kb_lib::ExApiPreparedExecutionPlan {
system_plan.instructions.extend(token_instructions);
system_plan.executor_name =
"kb-pipeline-demo-scenarios.token_2022_metadata_fixture".to_string();
system_plan.operation_code = "spl.token_2022.metadata.prepare_mint".to_string();
system_plan.policy = policy;
system_plan.requested_spend_lamports = rent_lamports;
return system_plan;
}
#[cfg(test)]
mod tests {
#[test]
fn metadata_rent_budget_covers_pointer_and_largest_campaign_value() {
let mint_space = super::token_2022_metadata_pointer_mint_space()
.unwrap_or_else(|error| panic!("metadata pointer mint size failed: {error}"));
assert!(mint_space > 82);
let budget = super::metadata_campaign_rent_budget_space(mint_space);
assert_eq!(
budget,
mint_space
+ super::TOKEN_2022_TLV_HEADER_BYTES
+ super::metadata_campaign_max_value_bytes()
);
assert!(budget > mint_space);
}
}