// 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( 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 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( http_pool: &ks_onchain_transport::HttpEndpointPool, store: &S, profile: &ks_config::ProfileConfig, wallet: &crate::DevnetExecutionWallet, options: &crate::MetaplexCreateFixturePreparationOptions, observer: &O, ) -> ks_core::Result 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::vec![std::sync::Arc::new(ks_lib::DcMetadataMetaplexTokenMetadataDecoder)]; let materializers: std::vec::Vec> = 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, ) -> std::vec::Vec { 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 { 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 { 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 { 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 { 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), ); } }