Files
khadhroony-bot3/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/use_campaign.rs
2026-08-09 22:52:37 +02:00

575 lines
25 KiB
Rust

// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/use_campaign.rs
// version: 3
//! Bounded Devnet availability probe for the current Metaplex `Use` surface.
/// Outcome of one bounded Devnet `Use` availability probe.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DevnetMetaplexUseProbeStatus {
/// The current `Use` instruction was confirmed with complete pipeline evidence.
Confirmed,
/// The exact current `Use` instruction was rejected by Devnet simulation.
RuntimeUnavailable,
}
/// Complete evidence produced by one current Metaplex `Use` availability probe.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexUseProbeSummary {
/// Fresh classic NFT prepared with two bounded `Multiple` uses.
pub fixture: crate::DevnetMetaplexCreateMintCampaignSummary,
/// Exact simulation evidence for the current `Use` instruction.
pub simulation: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Confirmed execution evidence when the runtime accepts the current instruction.
pub execution: std::option::Option<crate::DevnetMetaplexTokenMetadataExecutionSummary>,
/// Runtime availability classification produced by this probe.
pub status: crate::DevnetMetaplexUseProbeStatus,
/// Remaining uses observed immediately before the `Use` probe.
pub uses_before: u64,
/// Remaining uses observed after confirmed execution, when available.
pub uses_after: std::option::Option<u64>,
/// Exact bounded simulation failure diagnostic when runtime-unavailable.
pub unavailable_reason: std::option::Option<std::string::String>,
}
/// Returns the exact ordered operations exercised by the bounded `Use` probe.
pub fn metaplex_use_probe_operation_names() -> &'static [&'static str; 3] {
return &["create", "mint", "use"];
}
/// Executes one fresh classic NFT `Use` availability probe on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_use_probe<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> ks_core::Result<crate::DevnetMetaplexUseProbeSummary>
where
S: ks_store::RawTransactionStore
+ ks_store::CoreExtractionStore
+ ks_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if options.asset_family != crate::MetaplexTokenMetadataAssetFamily::Nft {
return std::result::Result::Err(ks_core::Error::config(
"Metaplex Use probe requires the classic NFT fixture family",
));
}
let fixture_options = options.clone().with_multiple_uses(2);
let fixture = match crate::execute_devnet_metaplex_create_mint_campaign(
http_pool,
store,
profile,
workspace_root,
&fixture_options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let uses_before = match metadata_remaining_multiple_uses(
fixture.mint.after.as_slice(),
fixture.fixture.metadata.as_str(),
2,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if uses_before != 2 {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_initial_uses_invalid",
format!("Metaplex Use probe requires remaining=2 before execution; got {uses_before}"),
));
}
let operation = use_operation(&fixture.fixture);
let mut simulation_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-use-probe-simulation-{}", uuid::Uuid::new_v4()),
operation.clone(),
);
simulation_request.query_role = fixture_options.query_role.clone();
simulation_request.transaction_role = fixture_options.transaction_role.clone();
let simulation = match crate::simulate_devnet_metaplex_token_metadata(
http_pool,
profile,
workspace_root,
&simulation_request,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !simulation.simulation.success {
return std::result::Result::Ok(crate::DevnetMetaplexUseProbeSummary {
fixture,
unavailable_reason: std::option::Option::Some(crate::simulation_failure_message(
&simulation.simulation,
)),
simulation,
execution: std::option::Option::None,
status: crate::DevnetMetaplexUseProbeStatus::RuntimeUnavailable,
uses_before,
uses_after: std::option::Option::None,
});
}
let decoders: std::vec::Vec<std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(ks_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>> = std::vec![
std::sync::Arc::new(ks_lib::MtAdminMaterializer),
std::sync::Arc::new(ks_lib::MtComplianceAuditMaterializer),
std::sync::Arc::new(ks_lib::MtLifecycleMaterializer),
std::sync::Arc::new(ks_lib::MtMetadataMetaplexTokenMetadataMaterializer),
std::sync::Arc::new(ks_lib::MtRiskMaterializer),
];
let mut execution_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-use-probe-submit-{}", uuid::Uuid::new_v4()),
operation,
);
execution_request.query_role = fixture_options.query_role.clone();
execution_request.transaction_role = fixture_options.transaction_role.clone();
execution_request.submit = true;
execution_request.operator_confirmed = true;
execution_request.materialize_after_confirmation = true;
execution_request.post_validation_max_retries = 20;
execution_request.postcondition_reads =
metadata_read(fixture.fixture.metadata.as_str(), fixture_options.query_role.as_str());
let execution = match crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&execution_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_slot = match validate_confirmed_use_execution(&execution) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let uses_after = match metadata_remaining_multiple_uses(
execution.after.as_slice(),
fixture.fixture.metadata.as_str(),
2,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if uses_after != 1 {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_remaining_uses_invalid",
format!(
"confirmed Metaplex Use must decrement remaining uses from 2 to 1; got {uses_after}"
),
));
}
let token_after = match crate::read_token_account_at_or_after(
http_pool,
fixture_options.query_role.as_str(),
fixture.fixture.token_account.as_str(),
std::option::Option::Some(confirmation_slot),
)
.await
{
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_token_missing",
"classic NFT token account disappeared after confirmed Use",
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = crate::validate_classic_fixture_token_account_state(
&token_after,
fixture.fixture.mint.as_str(),
fixture.fixture.authority.as_str(),
1,
spl_token_interface::state::AccountState::Initialized,
) {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_token_state_invalid",
format!("confirmed Metaplex Use left invalid NFT token state: {error}"),
));
}
return std::result::Result::Ok(crate::DevnetMetaplexUseProbeSummary {
fixture,
simulation,
execution: std::option::Option::Some(execution),
status: crate::DevnetMetaplexUseProbeStatus::Confirmed,
uses_before,
uses_after: std::option::Option::Some(uses_after),
unavailable_reason: std::option::Option::None,
});
}
fn use_operation(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
) -> ks_lib::ExMetaplexTokenMetadataOperation {
return ks_lib::ExMetaplexTokenMetadataOperation::Use {
authority: ks_lib::MdPubkey(fixture.authority.clone()),
delegate_record: std::option::Option::None,
token: std::option::Option::Some(ks_lib::MdPubkey(fixture.token_account.clone())),
mint: ks_lib::MdPubkey(fixture.mint.clone()),
metadata: ks_lib::MdPubkey(fixture.metadata.clone()),
edition: std::option::Option::Some(ks_lib::MdPubkey(fixture.master_edition.clone())),
payer: ks_lib::MdPubkey(fixture.authority.clone()),
system_program: ks_lib::MdPubkey(ks_program_ids::SYSTEM_PROGRAM_ID.to_string()),
sysvar_instructions: ks_lib::MdPubkey(
ks_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID.to_string(),
),
spl_token_program: std::option::Option::Some(ks_lib::MdPubkey(
ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
)),
authorization_rules_program: std::option::Option::None,
authorization_rules: std::option::Option::None,
args: mpl_token_metadata::types::UseArgs::V1 {
authorization_data: std::option::Option::None,
},
};
}
fn metadata_read(
metadata: &str,
query_role: &str,
) -> std::vec::Vec<ks_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
return std::vec![ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(metadata.to_string()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot: std::option::Option::None,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
}];
}
fn metadata_remaining_multiple_uses(
snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
metadata: &str,
expected_total: u64,
) -> ks_core::Result<u64> {
let snapshot = match snapshots.iter().find(|value| {
return value.snapshot.account.0.as_str() == metadata
&& value.snapshot.account_kind == "metadata";
}) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_metadata_snapshot_missing",
format!("metadata snapshot {metadata} is missing from the Use probe"),
));
},
};
let uses_value = match snapshot.snapshot.payload_json.get("uses") {
std::option::Option::Some(value) if !value.is_null() => value.clone(),
_ => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_uses_missing",
"metadata snapshot has no Uses state",
));
},
};
let uses = match serde_json::from_value::<mpl_token_metadata::types::Uses>(uses_value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_uses_decode_failed",
format!("metadata Uses projection is invalid: {error}"),
));
},
};
if uses.use_method != mpl_token_metadata::types::UseMethod::Multiple
|| uses.total != expected_total
|| uses.remaining > uses.total
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_uses_state_invalid",
format!(
"metadata Uses state must remain Multiple with total={expected_total}; got method={:?}, remaining={}, total={}",
uses.use_method, uses.remaining, uses.total
),
));
}
return std::result::Result::Ok(uses.remaining);
}
fn validate_confirmed_use_execution(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> ks_core::Result<u64> {
let confirmation = match execution.confirmation.as_ref() {
std::option::Option::Some(value)
if matches!(
value.status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
) =>
{
value
},
std::option::Option::Some(value) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_confirmation_incomplete",
format!("Metaplex Use stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_confirmation_missing",
"Metaplex Use 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(ks_core::Error::new(
"metaplex_use_probe_post_execution_missing",
"Metaplex Use has no post-execution diagnostic",
));
},
};
if !diagnostic.canonical_inserted
|| !diagnostic.core_extracted
|| !diagnostic.decode_replayed
|| !diagnostic.materialized
|| execution.materializations.is_empty()
|| execution.materialized_snapshots.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_post_execution_incomplete",
format!(
"Metaplex Use post-execution incomplete: signature={}, slot={:?}, canonical_inserted={}, core_extracted={}, decode_replayed={}, materialized={}, materialization_rows={}, materialized_snapshots={}, diagnostics={:?}",
confirmation.signature.0,
confirmation.slot,
diagnostic.canonical_inserted,
diagnostic.core_extracted,
diagnostic.decode_replayed,
diagnostic.materialized,
execution.materializations.len(),
execution.materialized_snapshots.len(),
diagnostic.diagnostics
),
));
}
let idempotence = match execution.idempotence_replay.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_idempotence_missing",
"Metaplex Use has no idempotence replay evidence",
));
},
};
if idempotence.failed_inputs != 0
|| idempotence.processing_error_inputs != 0
|| idempotence.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(ks_core::Error::new(
"metaplex_use_probe_idempotence_failed",
"Metaplex Use second replay is not idempotent",
));
}
return match confirmation.slot {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_confirmation_slot_missing",
"Metaplex Use confirmation has no slot",
)),
};
}
#[cfg(test)]
mod tests {
#[test]
fn use_probe_contract_is_exactly_create_mint_then_use() {
assert_eq!(crate::metaplex_use_probe_operation_names(), &["create", "mint", "use"]);
assert_ne!(
crate::DevnetMetaplexUseProbeStatus::Confirmed,
crate::DevnetMetaplexUseProbeStatus::RuntimeUnavailable,
);
}
#[tokio::test]
async fn optional_devnet_metaplex_use_probe_from_env() {
if std::env::var("KS_DEVNET_METAPLEX_USE_PROBE_TEST").ok().as_deref()
!= std::option::Option::Some("1")
{
return;
}
assert_eq!(
std::env::var("KS_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref(),
std::option::Option::Some("1"),
"set KS_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 before submitting an accepted Use probe"
);
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("KS_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 = ks_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("KS_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("KS_SECRET_POSTGRES_TEST_URL")
.unwrap_or_else(|error| panic!("KS_SECRET_POSTGRES_TEST_URL is required: {error}"));
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
let http_pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool initialization failed: {error}"));
let store_options = ks_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 = ks_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 options = crate::MetaplexCreateFixturePreparationOptions::new(wallet_dir)
.with_asset_family(crate::MetaplexTokenMetadataAssetFamily::Nft);
let summary = crate::execute_devnet_metaplex_use_probe(
&http_pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| panic!("Metaplex Use Devnet probe failed: {error}"));
println!(
"METAPLEX_USE_PROBE_FIXTURE mint={} metadata={} master_edition={} token={} uses_before={}",
summary.fixture.fixture.mint,
summary.fixture.fixture.metadata,
summary.fixture.fixture.master_edition,
summary.fixture.fixture.token_account,
summary.uses_before,
);
println!(
"METAPLEX_USE_PROBE_SIMULATION success={} context_slot={} logs={} error={}",
summary.simulation.simulation.success,
summary.simulation.simulation_context_slot,
summary.simulation.simulation.logs.len(),
serde_json::to_string(&summary.simulation.simulation.error).unwrap_or_else(
|error| panic!("Use simulation error serialization failed: {error}")
),
);
match summary.status {
crate::DevnetMetaplexUseProbeStatus::RuntimeUnavailable => {
println!(
"METAPLEX_USE_PROBE_STATUS status=runtime_unavailable uses_before={} uses_after=null reason={}",
summary.uses_before,
serde_json::to_string(&summary.unavailable_reason).unwrap_or_else(
|error| panic!("Use unavailable reason serialization failed: {error}")
),
);
println!(
"METAPLEX_USE_PROBE_LOGS {}",
serde_json::to_string(&summary.simulation.simulation.logs).unwrap_or_else(
|error| panic!("Use simulation log serialization failed: {error}")
),
);
},
crate::DevnetMetaplexUseProbeStatus::Confirmed => {
let execution = summary
.execution
.as_ref()
.unwrap_or_else(|| panic!("confirmed Use probe execution is missing"));
let confirmation = execution
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("confirmed Use probe confirmation is missing"));
println!(
"METAPLEX_USE_PROBE_STATUS status=confirmed uses_before={} uses_after={}",
summary.uses_before,
summary
.uses_after
.unwrap_or_else(|| panic!("confirmed Use uses_after is missing")),
);
println!(
"METAPLEX_USE_PROBE_STEP operation={} signature={} slot={:?} materializations={}",
execution.plan.operation_code,
confirmation.signature.0,
confirmation.slot,
execution.materializations.len(),
);
println!("METAPLEX_USE_PROBE_EVIDENCE {}", execution_evidence_json(execution),);
},
}
}
fn execution_evidence_json(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> serde_json::Value {
let confirmation = execution.confirmation.as_ref();
let post_execution = execution.post_execution.as_ref();
let idempotence_clean = execution.idempotence_replay.as_ref().is_some_and(|replay| {
return replay.failed_inputs == 0
&& replay.processing_error_inputs == 0
&& replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
});
return serde_json::json!({
"operation": execution.plan.operation_code.as_str(),
"cluster": execution.cluster,
"genesisHash": execution.genesis_hash.as_str(),
"simulationContextSlot": execution.simulation_context_slot,
"simulationSuccess": execution.simulation.success,
"simulationLogCount": execution.simulation.logs.len(),
"messageHash": execution.readiness.message_hash.as_str(),
"feeLamports": execution.fee.fee_lamports,
"signature": confirmation.map(|value| return value.signature.0.clone()),
"confirmationStatus": confirmation.map(|value| return format!("{:?}", value.status)),
"confirmationSlot": confirmation.and_then(|value| return value.slot),
"beforeSnapshots": execution.before.len(),
"afterSnapshots": execution.after.len(),
"canonicalHydration": post_execution.is_some_and(|value| return value.canonical_inserted),
"coreExtraction": post_execution.is_some_and(|value| return value.core_extracted),
"decodeReplay": post_execution.is_some_and(|value| return value.decode_replayed),
"materialized": post_execution.is_some_and(|value| return value.materialized),
"instructionMaterializations": execution.materializations.len(),
"materializedSnapshots": execution.materialized_snapshots.len(),
"idempotenceReplayClean": idempotence_clean,
});
}
}