575 lines
25 KiB
Rust
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,
|
|
});
|
|
}
|
|
}
|