Files
khadhroony-bot3/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs

767 lines
33 KiB
Rust

// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs
// version: 7
//! Confirmed Devnet collection parent/member `Verify -> Unverify` campaign.
/// Collection relation and sized-collection state observed across verification.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DevnetMetaplexCollectionVerifyState {
/// Member collection flag before `Verify`.
pub verified_before: bool,
/// Member collection flag after `Verify`.
pub verified_after_verify: bool,
/// Member collection flag after `Unverify`.
pub verified_after_unverify: bool,
/// Collection size before `Verify`.
pub collection_size_before: u64,
/// Collection size after `Verify`.
pub collection_size_after_verify: u64,
/// Collection size after `Unverify`.
pub collection_size_after_unverify: u64,
}
/// Complete evidence produced by one collection parent/member verification campaign.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexCollectionVerifyCampaignSummary {
/// Fully qualified collection parent fixture and `Create -> Mint` evidence.
pub parent: crate::DevnetMetaplexCreateMintCampaignSummary,
/// Fully qualified unverified member fixture and `Create -> Mint` evidence.
pub member: crate::DevnetMetaplexCreateMintCampaignSummary,
/// Confirmed collection verification evidence.
pub verify: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Confirmed collection unverification evidence.
pub unverify: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Exact collection relation and size transitions.
pub state: crate::DevnetMetaplexCollectionVerifyState,
}
/// Returns the exact current Metaplex operations qualified by this campaign after fixture setup.
pub fn metaplex_collection_verify_campaign_operation_names() -> &'static [&'static str; 2] {
return &["verify", "unverify"];
}
/// Executes one fresh collection parent/member `Verify -> Unverify` campaign on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_collection_verify_campaign<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::DevnetMetaplexCollectionVerifyCampaignSummary>
where
S: ks_store::RawTransactionStore
+ ks_store::CoreExtractionStore
+ ks_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
let temporary = match crate::load_profile_temporary_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallet = crate::DevnetExecutionWallet::Temporary(temporary);
return execute_devnet_metaplex_collection_verify_campaign_with_wallet(
http_pool, store, profile, &wallet, options, observer,
)
.await;
}
/// Executes one collection verification campaign with an explicit operator wallet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_collection_verify_campaign_with_wallet<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
wallet: &crate::DevnetExecutionWallet,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> ks_core::Result<crate::DevnetMetaplexCollectionVerifyCampaignSummary>
where
S: ks_store::RawTransactionStore
+ ks_store::CoreExtractionStore
+ ks_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if options.collection_mint.is_some() {
return std::result::Result::Err(ks_core::Error::config(
"collection verification campaign base options must not preselect a collection mint",
));
}
let mut parent_options = options.clone();
parent_options.asset_family = crate::MetaplexTokenMetadataAssetFamily::Collection;
parent_options.collection_mint = std::option::Option::None;
let parent = match crate::execute_devnet_metaplex_create_mint_campaign_with_wallet(
http_pool,
store,
profile,
wallet,
&parent_options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut member_options = options.clone();
member_options.asset_family = crate::MetaplexTokenMetadataAssetFamily::Nft;
member_options.collection_mint = std::option::Option::Some(parent.fixture.mint.clone());
let member = match crate::execute_devnet_metaplex_create_mint_campaign_with_wallet(
http_pool,
store,
profile,
wallet,
&member_options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verified_before = match collection_member_verified(
member.mint.after.as_slice(),
member.fixture.metadata.as_str(),
parent.fixture.mint.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection_size_before =
match collection_size(parent.mint.after.as_slice(), parent.fixture.metadata.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if verified_before || collection_size_before != 0 {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_verify_initial_state_invalid",
format!(
"fresh collection member must start unverified with parent size 0; verified={verified_before}, size={collection_size_before}"
),
));
}
let parent_mint_slot = match confirmed_slot("parent mint", &parent.mint) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let member_mint_slot = match confirmed_slot("member mint", &member.mint) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture_slot = std::cmp::max(parent_mint_slot, member_mint_slot);
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 verify_operation = ks_lib::ExMetaplexTokenMetadataOperation::Verify {
authority: ks_lib::MdPubkey(parent.fixture.authority.clone()),
delegate_record: std::option::Option::None,
metadata: ks_lib::MdPubkey(member.fixture.metadata.clone()),
collection_mint: std::option::Option::Some(ks_lib::MdPubkey(parent.fixture.mint.clone())),
collection_metadata: std::option::Option::Some(ks_lib::MdPubkey(
parent.fixture.metadata.clone(),
)),
collection_master_edition: std::option::Option::Some(ks_lib::MdPubkey(
parent.fixture.master_edition.clone(),
)),
verification_args: mpl_token_metadata::types::VerificationArgs::CollectionV1,
};
let mut verify_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-collection-verify-{}", uuid::Uuid::new_v4()),
verify_operation,
);
verify_request.query_role = options.query_role.clone();
verify_request.transaction_role = options.transaction_role.clone();
verify_request.submit = true;
verify_request.operator_confirmed = true;
verify_request.materialize_after_confirmation = true;
verify_request.post_validation_max_retries = 20;
verify_request.preflight_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(fixture_slot),
);
verify_request.postcondition_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(fixture_slot),
);
let verify = match crate::execute_devnet_metaplex_token_metadata_with_wallet(
http_pool,
store,
profile,
wallet,
&verify_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 verify_slot = match validate_confirmed_execution("verify", &verify) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verified_after_verify = match collection_member_verified(
verify.after.as_slice(),
member.fixture.metadata.as_str(),
parent.fixture.mint.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection_size_after_verify =
match collection_size(verify.after.as_slice(), parent.fixture.metadata.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !verified_after_verify || collection_size_after_verify != 1 {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_verify_postcondition_failed",
format!(
"confirmed collection Verify must set member verified=true and parent size=1; verified={verified_after_verify}, size={collection_size_after_verify}"
),
));
}
let unverify_operation = ks_lib::ExMetaplexTokenMetadataOperation::Unverify {
authority: ks_lib::MdPubkey(parent.fixture.authority.clone()),
delegate_record: std::option::Option::None,
metadata: ks_lib::MdPubkey(member.fixture.metadata.clone()),
collection_mint: std::option::Option::Some(ks_lib::MdPubkey(parent.fixture.mint.clone())),
collection_metadata: std::option::Option::Some(ks_lib::MdPubkey(
parent.fixture.metadata.clone(),
)),
verification_args: mpl_token_metadata::types::VerificationArgs::CollectionV1,
};
let mut unverify_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-collection-unverify-{}", uuid::Uuid::new_v4()),
unverify_operation,
);
unverify_request.query_role = options.query_role.clone();
unverify_request.transaction_role = options.transaction_role.clone();
unverify_request.submit = true;
unverify_request.operator_confirmed = true;
unverify_request.materialize_after_confirmation = true;
unverify_request.post_validation_max_retries = 20;
unverify_request.preflight_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(verify_slot),
);
unverify_request.postcondition_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(verify_slot),
);
let unverify = match crate::execute_devnet_metaplex_token_metadata_with_wallet(
http_pool,
store,
profile,
wallet,
&unverify_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 unverify_slot = match validate_confirmed_execution("unverify", &unverify) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verified_after_unverify = match collection_member_verified(
unverify.after.as_slice(),
member.fixture.metadata.as_str(),
parent.fixture.mint.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection_size_after_unverify =
match collection_size(unverify.after.as_slice(), parent.fixture.metadata.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if verified_after_unverify || collection_size_after_unverify != 0 {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_unverify_postcondition_failed",
format!(
"confirmed collection Unverify at slot {unverify_slot} must restore verified=false and parent size=0; verified={verified_after_unverify}, size={collection_size_after_unverify}"
),
));
}
return std::result::Result::Ok(crate::DevnetMetaplexCollectionVerifyCampaignSummary {
parent,
member,
verify,
unverify,
state: crate::DevnetMetaplexCollectionVerifyState {
verified_before,
verified_after_verify,
verified_after_unverify,
collection_size_before,
collection_size_after_verify,
collection_size_after_unverify,
},
});
}
fn collection_reads(
parent: &crate::MetaplexCreateFixturePreparationSummary,
member: &crate::MetaplexCreateFixturePreparationSummary,
query_role: &str,
min_context_slot: std::option::Option<u64>,
) -> std::vec::Vec<ks_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
return vec![
ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(member.metadata.clone()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
},
ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(parent.metadata.clone()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
},
ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(parent.master_edition.clone()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Edition {
mint: ks_lib::MdPubkey(parent.mint.clone()),
},
min_context_slot,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
},
];
}
fn confirmed_slot(
step: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> ks_core::Result<u64> {
return match execution.confirmation.as_ref().and_then(|value| return value.slot) {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_fixture_confirmation_slot_missing",
format!("Metaplex {step} has no confirmed slot"),
)),
};
}
fn validate_confirmed_execution(
step: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> ks_core::Result<u64> {
if !execution.simulation.success {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_simulation_failed",
format!("Metaplex collection {step} simulation failed"),
));
}
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_collection_confirmation_incomplete",
format!("Metaplex collection {step} stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_confirmation_missing",
format!("Metaplex collection {step} 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_collection_post_execution_missing",
format!("Metaplex collection {step} 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_collection_post_execution_incomplete",
format!(
"Metaplex collection {step} 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 second_replay = 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_collection_idempotence_missing",
format!("Metaplex collection {step} has no idempotence replay evidence"),
));
},
};
if second_replay.failed_inputs != 0
|| second_replay.processing_error_inputs != 0
|| second_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(ks_core::Error::new(
"metaplex_collection_idempotence_failed",
format!("Metaplex collection {step} 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_collection_confirmation_slot_missing",
format!("Metaplex collection {step} confirmation has no slot"),
)),
};
}
fn metadata_payload<'a>(
snapshots: &'a [ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
metadata: &str,
) -> ks_core::Result<&'a serde_json::Value> {
let snapshot = snapshots.iter().find(|value| {
return value.snapshot.account.0.as_str() == metadata
&& value.snapshot.account_kind == "metadata";
});
return match snapshot {
std::option::Option::Some(value) => std::result::Result::Ok(&value.snapshot.payload_json),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_metadata_snapshot_missing",
format!("Metaplex metadata snapshot {metadata} is missing"),
)),
};
}
fn collection_member_verified(
snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
member_metadata: &str,
expected_collection_mint: &str,
) -> ks_core::Result<bool> {
let payload = match metadata_payload(snapshots, member_metadata) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection = match payload.get("collection") {
std::option::Option::Some(value) if value.is_object() => value,
_ => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_relation_missing",
"member metadata does not contain a collection relation",
));
},
};
let key = collection.get("key").and_then(serde_json::Value::as_str);
let verified = collection.get("verified").and_then(serde_json::Value::as_bool);
if key != std::option::Option::Some(expected_collection_mint) || verified.is_none() {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_relation_invalid",
format!(
"member collection relation mismatch: expected key={expected_collection_mint}, got key={key:?}, verified={verified:?}"
),
));
}
return match verified {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_verified_flag_missing",
"member collection relation does not expose a verified flag",
)),
};
}
fn collection_size(
snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
collection_metadata: &str,
) -> ks_core::Result<u64> {
let payload = match metadata_payload(snapshots, collection_metadata) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let size = payload
.get("collection_details")
.and_then(|value| return value.get("V1"))
.and_then(|value| return value.get("size"))
.and_then(serde_json::Value::as_u64);
return match size {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_size_missing",
"collection metadata does not expose CollectionDetails::V1 size",
)),
};
}
#[cfg(test)]
mod tests {
#[test]
fn collection_campaign_contract_is_exactly_verify_then_unverify() {
assert_eq!(
crate::metaplex_collection_verify_campaign_operation_names(),
&["verify", "unverify"]
);
}
#[test]
fn collection_payload_contract_tracks_relation_and_size() {
let collection = solana_pubkey::Pubkey::new_unique().to_string();
let relation = serde_json::json!({"collection": {"verified": false, "key": collection}});
let details = serde_json::json!({"collection_details": {"V1": {"size": 0}}});
assert_eq!(
relation.pointer("/collection/verified").and_then(serde_json::Value::as_bool),
std::option::Option::Some(false)
);
assert_eq!(
details
.pointer("/collection_details/V1/size")
.and_then(serde_json::Value::as_u64),
std::option::Option::Some(0)
);
}
#[test]
fn metadata_serde_contract_uses_stateful_collection_keys() {
let metadata = mpl_token_metadata::accounts::Metadata {
key: mpl_token_metadata::types::Key::MetadataV1,
update_authority: mpl_token_metadata::ID,
mint: mpl_token_metadata::ID,
name: "Collection".to_string(),
symbol: "COL".to_string(),
uri: "https://example.invalid/collection.json".to_string(),
seller_fee_basis_points: 0,
creators: std::option::Option::None,
primary_sale_happened: false,
is_mutable: true,
edition_nonce: std::option::Option::None,
token_standard: std::option::Option::Some(
mpl_token_metadata::types::TokenStandard::NonFungible,
),
collection: std::option::Option::None,
uses: std::option::Option::None,
collection_details: std::option::Option::Some(
mpl_token_metadata::types::CollectionDetails::V1 { size: 7 },
),
programmable_config: std::option::Option::None,
};
let payload = match serde_json::to_value(metadata) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("metadata serialization failed: {error}"),
};
assert_eq!(
payload
.pointer("/collection_details/V1/size")
.and_then(serde_json::Value::as_u64),
std::option::Option::Some(7)
);
assert!(payload.get("collectionDetails").is_none());
}
fn execution_evidence_json(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> serde_json::Value {
let confirmation = execution.confirmation.as_ref();
let confirmation_status = confirmation
.map(|value| return format!("{:?}", value.status))
.unwrap_or_else(|| return "missing".to_string());
let confirmation_slot = confirmation.and_then(|value| return value.slot);
let signature = confirmation
.map(|value| return value.signature.0.clone())
.unwrap_or_else(|| return "missing".to_string());
let diagnostic = 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": signature,
"confirmationStatus": confirmation_status,
"confirmationSlot": confirmation_slot,
"beforeSnapshots": execution.before.len(),
"afterSnapshots": execution.after.len(),
"canonicalHydration": diagnostic.is_some_and(|value| return value.canonical_inserted),
"coreExtraction": diagnostic.is_some_and(|value| return value.core_extracted),
"decodeReplay": diagnostic.is_some_and(|value| return value.decode_replayed),
"materialized": diagnostic.is_some_and(|value| return value.materialized),
"instructionMaterializations": execution.materializations.len(),
"materializedSnapshots": execution.materialized_snapshots.len(),
"idempotenceReplayClean": idempotence_clean,
});
}
#[tokio::test]
async fn optional_devnet_metaplex_collection_verify_campaign_from_env() {
if std::env::var("KS_DEVNET_METAPLEX_COLLECTION_VERIFY_CAMPAIGN_TEST")
.ok()
.as_deref()
!= std::option::Option::Some("1")
{
return;
}
if std::env::var("KS_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref()
!= std::option::Option::Some("1")
{
panic!("KS_DEVNET_METAPLEX_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 = crate::load_demo_scenario_config(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 pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool creation 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.temporary_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);
let summary = crate::execute_devnet_metaplex_collection_verify_campaign(
&pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| {
panic!("Metaplex collection Verify -> Unverify Devnet campaign failed: {error}")
});
let verify_confirmation = summary
.verify
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("Verify confirmation missing"));
let unverify_confirmation = summary
.unverify
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("Unverify confirmation missing"));
println!(
"METAPLEX_COLLECTION_VERIFY_FIXTURE parent_mint={} parent_metadata={} parent_master_edition={} member_mint={} member_metadata={} member_master_edition={}",
summary.parent.fixture.mint,
summary.parent.fixture.metadata,
summary.parent.fixture.master_edition,
summary.member.fixture.mint,
summary.member.fixture.metadata,
summary.member.fixture.master_edition,
);
println!(
"METAPLEX_COLLECTION_VERIFY_STEP operation={} signature={} slot={:?} materializations={}",
summary.verify.plan.operation_code,
verify_confirmation.signature.0,
verify_confirmation.slot,
summary.verify.materializations.len(),
);
println!(
"METAPLEX_COLLECTION_VERIFY_STEP operation={} signature={} slot={:?} materializations={}",
summary.unverify.plan.operation_code,
unverify_confirmation.signature.0,
unverify_confirmation.slot,
summary.unverify.materializations.len(),
);
println!(
"METAPLEX_COLLECTION_VERIFY_STATE verified_before={} verified_after_verify={} verified_after_unverify={} size_before={} size_after_verify={} size_after_unverify={}",
summary.state.verified_before,
summary.state.verified_after_verify,
summary.state.verified_after_unverify,
summary.state.collection_size_before,
summary.state.collection_size_after_verify,
summary.state.collection_size_after_unverify,
);
println!(
"METAPLEX_COLLECTION_VERIFY_EVIDENCE verify={} unverify={}",
execution_evidence_json(&summary.verify),
execution_evidence_json(&summary.unverify),
);
}
}