Files
khadhroony-bot3/kb-pipeline-demo-scenarios/src/solana_token_2022_metadata_campaign.rs
2026-08-07 09:54:53 +02:00

587 lines
25 KiB
Rust

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