// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/print_burn_campaign.rs // version: 9 //! Devnet printable master NFT `Print -> Burn` campaign. /// Exact printed-edition state transition observed around `Print` and `Burn`. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DevnetMetaplexPrintBurnState { /// Printed edition number exercised by the campaign. pub edition_number: u64, /// Master Edition supply before `Print`. pub master_supply_before_print: u64, /// Master Edition supply after `Print`. pub master_supply_after_print: u64, /// Master Edition supply after `Burn`; this fresh single-print fixture must return to zero. pub master_supply_after_burn: u64, /// Master Edition maximum supply. pub master_max_supply: u64, /// Printed mint supply after `Print`. pub edition_supply_after_print: u64, /// Printed mint supply after `Burn`. pub edition_supply_after_burn: u64, /// Printed token-account amount after `Print`. pub edition_token_amount_after_print: u64, /// Raw classic SPL token-account state after `Print`. pub edition_token_state_after_print: u8, /// Whether the Edition Marker bit is set after `Print`. pub edition_taken_after_print: bool, /// Whether the printed Metadata address still returns an account after `Burn`. pub edition_metadata_exists_after_burn: bool, /// Whether the retained Metadata account is the exact one-byte uninitialized fee tombstone. pub edition_metadata_fee_tombstone_after_burn: bool, /// Whether Metadata is semantically closed after `Burn`, either absent or an exact fee tombstone. pub edition_metadata_closed_after_burn: bool, /// Whether the printed Edition account still exists after `Burn`. pub edition_exists_after_burn: bool, /// Whether the printed token account still exists after `Burn`. pub edition_token_exists_after_burn: bool, /// Whether the Edition Marker account still exists after `Burn`; this fixture requires `false`. pub edition_marker_exists_after_burn: bool, } /// Complete evidence produced by one printable master NFT `Print -> Burn` campaign. #[derive(Clone, Debug, PartialEq)] pub struct DevnetMetaplexPrintBurnCampaignSummary { /// Printable master NFT prepared through the already-qualified `Create -> Mint` campaign. pub master: crate::DevnetMetaplexCreateMintCampaignSummary, /// Persistent fresh printed-edition mint keypair path. pub edition_mint_keypair_path: std::path::PathBuf, /// Fresh printed-edition mint. pub edition_mint: std::string::String, /// Printed Metadata PDA. pub edition_metadata: std::string::String, /// Printed Edition PDA. pub edition: std::string::String, /// Printed classic SPL associated token account. pub edition_token_account: std::string::String, /// Master-mint Edition Marker PDA containing edition 1. pub edition_marker: std::string::String, /// Whether the fresh printed-edition mint was absent immediately before `Print`. pub edition_mint_absent_before_print: bool, /// Whether the fresh printed-edition ATA was absent immediately before `Print`. pub edition_token_absent_before_print: bool, /// Confirmed `Print` execution evidence. pub print: crate::DevnetMetaplexTokenMetadataExecutionSummary, /// Confirmed `Burn` execution evidence. pub burn: crate::DevnetMetaplexTokenMetadataExecutionSummary, /// Exact state transition around the two current operations. pub state: crate::DevnetMetaplexPrintBurnState, } /// Returns the exact ordered current operations qualified by this campaign. pub fn metaplex_print_burn_campaign_operation_names() -> &'static [&'static str; 2] { return &["print", "burn"]; } /// Executes one printable master NFT `Print -> Burn` campaign on Devnet. #[allow(clippy::too_many_arguments)] pub async fn execute_devnet_metaplex_print_burn_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, { if options.collection_mint.is_some() || options.print_supply_limit.is_some() { return std::result::Result::Err(ks_core::Error::config( "Print -> Burn campaign base options must not preselect collection or print-supply state", )); } let mut master_options = options.clone(); master_options.asset_family = crate::MetaplexTokenMetadataAssetFamily::Nft; master_options.collection_mint = std::option::Option::None; master_options.print_supply_limit = std::option::Option::Some(1); let master = match crate::execute_devnet_metaplex_create_mint_campaign( http_pool, store, profile, workspace_root, &master_options, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let master_mint_slot = match confirmed_slot("master mint", &master.mint) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let (master_supply_before_print, master_max_supply) = match master_edition_state( master.mint.after.as_slice(), master.fixture.master_edition.as_str(), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if master_supply_before_print != 0 || master_max_supply != std::option::Option::Some(1) { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_master_supply_invalid", format!( "printable master must start supply=0,max_supply=1; supply={master_supply_before_print}, max_supply={master_max_supply:?}" ), )); } let master_token_before_print = match crate::read_token_account_at_or_after( http_pool, options.query_role.as_str(), master.fixture.token_account.as_str(), std::option::Option::Some(master_mint_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_print_burn_master_token_missing", "printable master token account is unavailable before Print", )); }, 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( &master_token_before_print, master.fixture.mint.as_str(), master.fixture.authority.as_str(), 1, spl_token_interface::state::AccountState::Initialized, ) { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_master_token_invalid", format!( "printable master token account must contain exactly one token before Print: {error}" ), )); } let fixture_dir = options.wallet_dir.join("metaplex_token_metadata_validation"); let fixture_store = match ks_wallet::TemporaryWalletStore::new(fixture_dir) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let fixture_epoch = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { std::result::Result::Ok(value) => value.as_nanos(), std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "unable to create a unique printed-edition fixture identifier: {error}" ))); }, }; let edition_alias = match ks_wallet::WalletAlias::parse(format!("print-edition-{fixture_epoch}").as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let edition_wallet = match fixture_store.create(edition_alias.clone()).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let edition_mint_keypair_path = fixture_store.wallet_path(&edition_alias); let edition_mint = edition_wallet.public_key(); let existing_edition_mint = match crate::read_mint_account_at_or_after( http_pool, options.query_role.as_str(), edition_mint.as_str(), std::option::Option::None, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if existing_edition_mint.is_some() { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_fresh_edition_mint_exists", "fresh printed-edition mint unexpectedly already exists", )); } let edition_number = 1_u64; let program_id = match parse_pubkey( ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, "Metaplex program", ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let edition_mint_pubkey = match parse_pubkey(edition_mint.as_str(), "printed edition mint") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let master_mint_pubkey = match parse_pubkey(master.fixture.mint.as_str(), "master mint") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let operator_pubkey = match parse_pubkey(master.fixture.authority.as_str(), "operator") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let (edition_metadata, _) = solana_pubkey::Pubkey::find_program_address( &[b"metadata", program_id.as_ref(), edition_mint_pubkey.as_ref()], &program_id, ); let (edition, _) = solana_pubkey::Pubkey::find_program_address( &[b"metadata", program_id.as_ref(), edition_mint_pubkey.as_ref(), b"edition"], &program_id, ); let marker_group = edition_number / 248; let marker_group_seed = marker_group.to_string(); let (edition_marker, _) = solana_pubkey::Pubkey::find_program_address( &[ b"metadata", program_id.as_ref(), master_mint_pubkey.as_ref(), b"edition", marker_group_seed.as_bytes(), ], &program_id, ); let token_program = match parse_pubkey(ks_program_ids::SPL_TOKEN_PROGRAM_ID, "classic SPL Token program") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let edition_token_account = spl_associated_token_account_interface::address::get_associated_token_address_with_program_id( &operator_pubkey, &edition_mint_pubkey, &token_program, ); let existing_edition_token = match crate::read_token_account_at_or_after( http_pool, options.query_role.as_str(), edition_token_account.to_string().as_str(), std::option::Option::Some(master_mint_slot), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if existing_edition_token.is_some() { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_fresh_edition_token_exists", "fresh printed-edition ATA unexpectedly already exists", )); } 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 print_operation = ks_lib::ExMetaplexTokenMetadataOperation::Print { edition_metadata: ks_lib::MdPubkey(edition_metadata.to_string()), edition: ks_lib::MdPubkey(edition.to_string()), edition_mint: ks_lib::MdPubkey(edition_mint.clone()), edition_token_account_owner: ks_lib::MdPubkey(master.fixture.authority.clone()), edition_token_account: ks_lib::MdPubkey(edition_token_account.to_string()), edition_mint_authority: ks_lib::MdPubkey(master.fixture.authority.clone()), edition_token_record: std::option::Option::None, master_edition: ks_lib::MdPubkey(master.fixture.master_edition.clone()), edition_marker_pda: ks_lib::MdPubkey(edition_marker.to_string()), payer: ks_lib::MdPubkey(master.fixture.authority.clone()), master_token_account_owner: ks_lib::MdPubkey(master.fixture.authority.clone()), master_token_account: ks_lib::MdPubkey(master.fixture.token_account.clone()), master_metadata: ks_lib::MdPubkey(master.fixture.metadata.clone()), update_authority: ks_lib::MdPubkey(master.fixture.authority.clone()), spl_token_program: ks_lib::MdPubkey(ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()), spl_ata_program: ks_lib::MdPubkey(ks_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string()), sysvar_instructions: ks_lib::MdPubkey( ks_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID.to_string(), ), system_program: ks_lib::MdPubkey(ks_program_ids::SYSTEM_PROGRAM_ID.to_string()), args: mpl_token_metadata::types::PrintArgs::V1 { edition: edition_number }, }; let mut print_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new( format!("metaplex-print-burn-print-{}", uuid::Uuid::new_v4()), print_operation, ); print_request.query_role = options.query_role.clone(); print_request.transaction_role = options.transaction_role.clone(); print_request.additional_authorized_signers = std::vec![ks_lib::MdPubkey(edition_mint.clone())]; print_request.submit = true; print_request.operator_confirmed = true; print_request.materialize_after_confirmation = true; print_request.post_validation_max_retries = 20; print_request.preflight_reads = master_reads( &master.fixture, options.query_role.as_str(), std::option::Option::Some(master_mint_slot), ); print_request.postcondition_reads = print_reads( &master.fixture, edition_mint.as_str(), edition_metadata.to_string().as_str(), edition.to_string().as_str(), edition_marker.to_string().as_str(), edition_number, options.query_role.as_str(), std::option::Option::Some(master_mint_slot), ); let print = match crate::execute_devnet_metaplex_token_metadata_with_signers( http_pool, store, profile, workspace_root, &print_request, decoders.as_slice(), materializers.as_slice(), &[edition_wallet.as_sync_signer()], observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let print_slot = match validate_confirmed_execution("print", &print) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let (master_supply_after_print, master_max_supply_after_print) = match master_edition_state( print.after.as_slice(), master.fixture.master_edition.as_str(), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if master_supply_after_print != 1 || master_max_supply_after_print != std::option::Option::Some(1) { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_master_post_print_invalid", format!( "confirmed Print must move master supply 0->1 while keeping max_supply=1; supply={master_supply_after_print}, max_supply={master_max_supply_after_print:?}" ), )); } if let std::result::Result::Err(error) = validate_printed_stateful_accounts( print.after.as_slice(), edition_metadata.to_string().as_str(), edition.to_string().as_str(), edition_marker.to_string().as_str(), master.fixture.master_edition.as_str(), edition_number, ) { return std::result::Result::Err(error); } let edition_mint_after_print = match crate::read_mint_account_at_or_after( http_pool, options.query_role.as_str(), edition_mint.as_str(), std::option::Option::Some(print_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_print_burn_edition_mint_missing_after_print", "printed mint is unavailable after confirmed Print", )); }, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if let std::result::Result::Err(error) = crate::validate_classic_fixture_mint_state( &edition_mint_after_print, edition.to_string().as_str(), 0, 1, ) { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_edition_mint_invalid_after_print", error.to_string(), )); } let edition_token_after_print = match crate::read_token_account_at_or_after( http_pool, options.query_role.as_str(), edition_token_account.to_string().as_str(), std::option::Option::Some(print_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_print_burn_edition_token_missing_after_print", "printed token account is unavailable after confirmed Print", )); }, 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( &edition_token_after_print, edition_mint.as_str(), master.fixture.authority.as_str(), 1, spl_token_interface::state::AccountState::Initialized, ) { return std::result::Result::Err(error); } let edition_supply_after_print = match crate::classic_fixture_mint_supply(&edition_mint_after_print) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let edition_token_amount_after_print = match crate::classic_fixture_token_amount(&edition_token_after_print) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let edition_token_state_after_print = match crate::classic_fixture_token_account_state(&edition_token_after_print) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let edition_taken_after_print = match edition_taken( print.after.as_slice(), edition_marker.to_string().as_str(), edition_number, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let burn_operation = ks_lib::ExMetaplexTokenMetadataOperation::Burn { authority: ks_lib::MdPubkey(master.fixture.authority.clone()), collection_metadata: std::option::Option::None, metadata: ks_lib::MdPubkey(edition_metadata.to_string()), edition: std::option::Option::Some(ks_lib::MdPubkey(edition.to_string())), mint: ks_lib::MdPubkey(edition_mint.clone()), token: ks_lib::MdPubkey(edition_token_account.to_string()), master_edition: std::option::Option::Some(ks_lib::MdPubkey( master.fixture.master_edition.clone(), )), master_edition_mint: std::option::Option::Some(ks_lib::MdPubkey( master.fixture.mint.clone(), )), master_edition_token: std::option::Option::Some(ks_lib::MdPubkey( master.fixture.token_account.clone(), )), edition_marker: std::option::Option::Some(ks_lib::MdPubkey(edition_marker.to_string())), token_record: std::option::Option::None, burn_args: mpl_token_metadata::types::BurnArgs::V1 { amount: 1 }, }; let mut burn_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new( format!("metaplex-print-burn-burn-{}", uuid::Uuid::new_v4()), burn_operation, ); burn_request.query_role = options.query_role.clone(); burn_request.transaction_role = options.transaction_role.clone(); burn_request.submit = true; burn_request.operator_confirmed = true; burn_request.materialize_after_confirmation = true; burn_request.post_validation_max_retries = 20; burn_request.preflight_reads = print_reads( &master.fixture, edition_mint.as_str(), edition_metadata.to_string().as_str(), edition.to_string().as_str(), edition_marker.to_string().as_str(), edition_number, options.query_role.as_str(), std::option::Option::Some(print_slot), ); burn_request.postcondition_reads = master_reads( &master.fixture, options.query_role.as_str(), std::option::Option::Some(print_slot), ); let burn = match crate::execute_devnet_metaplex_token_metadata( http_pool, store, profile, workspace_root, &burn_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 burn_slot = match validate_confirmed_execution("burn", &burn) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let (master_supply_after_burn, master_max_supply_after_burn) = match master_edition_state(burn.after.as_slice(), master.fixture.master_edition.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if master_max_supply_after_burn != std::option::Option::Some(1) { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_master_max_supply_changed", format!( "Burn changed master max_supply unexpectedly to {master_max_supply_after_burn:?}" ), )); } if master_supply_after_burn != 0 { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_master_supply_not_decremented", format!( "confirmed Burn left master edition supply {master_supply_after_burn}, expected 0 after burning the only printed edition" ), )); } let edition_mint_after_burn = match crate::read_mint_account_at_or_after( http_pool, options.query_role.as_str(), edition_mint.as_str(), std::option::Option::Some(burn_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_print_burn_edition_mint_missing_after_burn", "classic printed-edition mint unexpectedly disappeared after Burn", )); }, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let edition_supply_after_burn = match crate::classic_fixture_mint_supply(&edition_mint_after_burn) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if edition_supply_after_burn != 0 { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_edition_supply_not_burned", format!( "confirmed Burn left printed mint supply {edition_supply_after_burn}, expected 0" ), )); } let edition_token_exists_after_burn = match crate::read_token_account_at_or_after( http_pool, options.query_role.as_str(), edition_token_account.to_string().as_str(), std::option::Option::Some(burn_slot), ) .await { std::result::Result::Ok(value) => value.is_some(), std::result::Result::Err(error) => return std::result::Result::Err(error), }; let (edition_metadata_exists_after_burn, edition_metadata_fee_tombstone_after_burn) = match metadata_closure_state_at_or_after( http_pool, options.query_role.as_str(), edition_metadata.to_string().as_str(), burn_slot, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let edition_metadata_closed_after_burn = !edition_metadata_exists_after_burn || edition_metadata_fee_tombstone_after_burn; let edition_exists_after_burn = match account_exists_at_or_after( http_pool, options.query_role.as_str(), edition.to_string().as_str(), burn_slot, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let edition_marker_exists_after_burn = match account_exists_at_or_after( http_pool, options.query_role.as_str(), edition_marker.to_string().as_str(), burn_slot, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if edition_token_exists_after_burn || !edition_metadata_closed_after_burn || edition_exists_after_burn || edition_marker_exists_after_burn { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_accounts_not_closed", format!( "confirmed Burn must close the printed token account, Edition and now-empty Edition Marker and must semantically close Metadata as absent or an exact one-byte uninitialized fee tombstone; token_exists={edition_token_exists_after_burn}, metadata_exists={edition_metadata_exists_after_burn}, metadata_fee_tombstone={edition_metadata_fee_tombstone_after_burn}, edition_exists={edition_exists_after_burn}, edition_marker_exists={edition_marker_exists_after_burn}" ), )); } return std::result::Result::Ok(crate::DevnetMetaplexPrintBurnCampaignSummary { master, edition_mint_keypair_path, edition_mint, edition_metadata: edition_metadata.to_string(), edition: edition.to_string(), edition_token_account: edition_token_account.to_string(), edition_marker: edition_marker.to_string(), edition_mint_absent_before_print: true, edition_token_absent_before_print: true, print, burn, state: crate::DevnetMetaplexPrintBurnState { edition_number, master_supply_before_print, master_supply_after_print, master_supply_after_burn, master_max_supply: 1, edition_supply_after_print, edition_supply_after_burn, edition_token_amount_after_print, edition_token_state_after_print, edition_taken_after_print, edition_metadata_exists_after_burn, edition_metadata_fee_tombstone_after_burn, edition_metadata_closed_after_burn, edition_exists_after_burn, edition_token_exists_after_burn, edition_marker_exists_after_burn, }, }); } fn parse_pubkey(value: &str, label: &str) -> ks_core::Result { return match ::from_str(value) { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::config( format!("invalid {label} public key: {error}"), )), }; } fn master_reads( fixture: &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(fixture.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(fixture.master_edition.clone()), kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Edition { mint: ks_lib::MdPubkey(fixture.mint.clone()), }, min_context_slot, max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES, }, ]; } #[allow(clippy::too_many_arguments)] fn print_reads( master: &crate::MetaplexCreateFixturePreparationSummary, edition_mint: &str, edition_metadata: &str, edition: &str, edition_marker: &str, edition_number: u64, query_role: &str, min_context_slot: std::option::Option, ) -> std::vec::Vec { let mut reads = master_reads(master, query_role, min_context_slot); reads.push(ks_pipeline::MetaplexTokenMetadataStatefulReadRequest { query_role: query_role.to_string(), account: ks_lib::MdPubkey(edition_metadata.to_string()), kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata, min_context_slot, max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES, }); reads.push(ks_pipeline::MetaplexTokenMetadataStatefulReadRequest { query_role: query_role.to_string(), account: ks_lib::MdPubkey(edition.to_string()), kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Edition { mint: ks_lib::MdPubkey(edition_mint.to_string()), }, min_context_slot, max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES, }); reads.push(ks_pipeline::MetaplexTokenMetadataStatefulReadRequest { query_role: query_role.to_string(), account: ks_lib::MdPubkey(edition_marker.to_string()), kind: ks_pipeline::MetaplexTokenMetadataAccountKind::EditionMarker { mint: ks_lib::MdPubkey(master.mint.clone()), edition: edition_number, }, min_context_slot, max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES, }); return reads; } 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_print_burn_confirmation_slot_missing", format!("{step} has no confirmation 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_print_burn_simulation_failed", format!("Metaplex {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_print_burn_confirmation_incomplete", format!("Metaplex {step} stopped at {:?}", value.status), )); }, std::option::Option::None => { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_confirmation_missing", format!("Metaplex {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_print_burn_post_execution_missing", format!("Metaplex {step} has no post-execution diagnostic"), )); }, }; 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; }); }); if !diagnostic.canonical_inserted || !diagnostic.core_extracted || !diagnostic.decode_replayed || !diagnostic.materialized || execution.materializations.is_empty() || execution.materialized_snapshots.is_empty() || !idempotence_clean { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_post_execution_incomplete", format!( "Metaplex {step} post-execution incomplete: signature={}, slot={:?}, canonical_inserted={}, core_extracted={}, decode_replayed={}, materialized={}, materialization_rows={}, materialized_snapshots={}, idempotence_clean={}, diagnostics={:?}", confirmation.signature.0, confirmation.slot, diagnostic.canonical_inserted, diagnostic.core_extracted, diagnostic.decode_replayed, diagnostic.materialized, execution.materializations.len(), execution.materialized_snapshots.len(), idempotence_clean, diagnostic.diagnostics, ), )); } 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_print_burn_confirmation_slot_missing", format!("Metaplex {step} confirmation has no slot"), )), }; } fn master_edition_state( snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult], master_edition: &str, ) -> ks_core::Result<(u64, std::option::Option)> { let snapshot = match snapshots.iter().find(|value| { return value.snapshot.account.0.as_str() == master_edition && value.snapshot.account_kind == "edition"; }) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_master_edition_snapshot_missing", "master edition snapshot is missing", )); }, }; let supply = match snapshot.snapshot.payload_json.get("supply").and_then(serde_json::Value::as_u64) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_master_supply_missing", "master edition payload has no numeric supply", )); }, }; let max_supply = match snapshot.snapshot.payload_json.get("max_supply") { std::option::Option::Some(value) if value.is_null() => std::option::Option::None, std::option::Option::Some(value) => value.as_u64(), std::option::Option::None => std::option::Option::None, }; return std::result::Result::Ok((supply, max_supply)); } fn validate_printed_stateful_accounts( snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult], edition_metadata: &str, edition: &str, edition_marker: &str, master_edition: &str, edition_number: u64, ) -> ks_core::Result<()> { let metadata = snapshots.iter().find(|value| { return value.snapshot.account.0.as_str() == edition_metadata && value.snapshot.account_kind == "metadata"; }); let edition_snapshot = snapshots.iter().find(|value| { return value.snapshot.account.0.as_str() == edition && value.snapshot.account_kind == "edition"; }); let marker = snapshots.iter().find(|value| { return value.snapshot.account.0.as_str() == edition_marker && value.snapshot.account_kind == "edition_marker"; }); let token_standard = metadata.and_then(|value| { return value .snapshot .payload_json .get("token_standard") .and_then(serde_json::Value::as_str); }); let parent = edition_snapshot.and_then(|value| { return value.snapshot.payload_json.get("parent").and_then(serde_json::Value::as_str); }); let observed_edition = edition_snapshot.and_then(|value| { return value.snapshot.payload_json.get("edition").and_then(serde_json::Value::as_u64); }); let edition_taken = marker.and_then(|value| { return value .snapshot .payload_json .get("edition_taken") .and_then(serde_json::Value::as_bool); }); if token_standard != std::option::Option::Some("NonFungibleEdition") || parent != std::option::Option::Some(master_edition) || observed_edition != std::option::Option::Some(edition_number) || edition_taken != std::option::Option::Some(true) { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_printed_state_invalid", format!( "printed state mismatch: token_standard={token_standard:?}, parent={parent:?}, edition={observed_edition:?}, edition_taken={edition_taken:?}" ), )); } return std::result::Result::Ok(()); } fn edition_taken( snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult], edition_marker: &str, edition_number: u64, ) -> ks_core::Result { let marker = match snapshots.iter().find(|value| { return value.snapshot.account.0.as_str() == edition_marker && value.snapshot.account_kind == "edition_marker"; }) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_edition_marker_missing", "edition marker snapshot is missing", )); }, }; if marker.snapshot.payload_json.get("edition").and_then(serde_json::Value::as_u64) != std::option::Option::Some(edition_number) { return std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_edition_marker_number_mismatch", "edition marker snapshot targets another edition number", )); } return match marker .snapshot .payload_json .get("edition_taken") .and_then(serde_json::Value::as_bool) { std::option::Option::Some(value) => std::result::Result::Ok(value), std::option::Option::None => std::result::Result::Err(ks_core::Error::new( "metaplex_print_burn_edition_marker_state_missing", "edition marker snapshot has no edition_taken flag", )), }; } fn metadata_is_exact_fee_tombstone(account: &ks_onchain_transport::AccountInfoValue) -> bool { return account.owner.0 == ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID && !account.executable && account.lamports > 0 && account.space == 1 && account.data.as_slice() == std::slice::from_ref(&0_u8); } async fn metadata_closure_state_at_or_after( http_pool: &ks_onchain_transport::HttpEndpointPool, query_role: &str, account: &str, min_context_slot: u64, ) -> ks_core::Result<(bool, bool)> { let config = match ks_onchain_transport::GetAccountInfoConfig::new_with_data( ks_onchain_transport::RpcCommitmentLevel::Confirmed, std::option::Option::Some(min_context_slot), ks_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let result = match http_pool .get_account_info_for_role(query_role, &ks_lib::MdPubkey(account.to_string()), &config) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return match result.account.as_ref() { std::option::Option::Some(value) => { std::result::Result::Ok((true, metadata_is_exact_fee_tombstone(value))) }, std::option::Option::None => std::result::Result::Ok((false, false)), }; } async fn account_exists_at_or_after( http_pool: &ks_onchain_transport::HttpEndpointPool, query_role: &str, account: &str, min_context_slot: u64, ) -> ks_core::Result { let config = match ks_onchain_transport::GetAccountInfoConfig::new_with_data( ks_onchain_transport::RpcCommitmentLevel::Confirmed, std::option::Option::Some(min_context_slot), ks_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let result = match http_pool .get_account_info_for_role(query_role, &ks_lib::MdPubkey(account.to_string()), &config) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(result.account.is_some()); } #[cfg(test)] mod tests { #[test] fn campaign_contract_is_exactly_print_then_burn() { assert_eq!(crate::metaplex_print_burn_campaign_operation_names(), &["print", "burn"]); } #[test] fn marker_group_contract_uses_248_editions() { assert_eq!(1_u64 / 248, 0); assert_eq!(247_u64 / 248, 0); assert_eq!(248_u64 / 248, 1); } #[test] fn printed_stateful_contract_tracks_master_parent_and_marker_bit() { let master = solana_pubkey::Pubkey::new_unique().to_string(); let metadata = solana_pubkey::Pubkey::new_unique().to_string(); let edition = solana_pubkey::Pubkey::new_unique().to_string(); let marker = solana_pubkey::Pubkey::new_unique().to_string(); let snapshots = vec![ result( metadata.clone(), "metadata", serde_json::json!({"token_standard":"NonFungibleEdition"}), ), result(edition.clone(), "edition", serde_json::json!({"parent":master,"edition":1})), result( marker.clone(), "edition_marker", serde_json::json!({"edition":1,"edition_taken":true}), ), ]; assert!( super::validate_printed_stateful_accounts( snapshots.as_slice(), metadata.as_str(), edition.as_str(), marker.as_str(), master.as_str(), 1, ) .is_ok() ); } #[test] fn metadata_fee_tombstone_contract_is_exact() { let mut account = ks_onchain_transport::AccountInfoValue { lamports: 1, owner: ks_lib::MdProgramId( ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID.to_string(), ), executable: false, rent_epoch: 0, space: 1, data: vec![0], }; assert!(super::metadata_is_exact_fee_tombstone(&account)); account.data[0] = 4; assert!(!super::metadata_is_exact_fee_tombstone(&account)); account.data[0] = 0; account.space = 2; assert!(!super::metadata_is_exact_fee_tombstone(&account)); account.space = 1; account.lamports = 0; assert!(!super::metadata_is_exact_fee_tombstone(&account)); account.lamports = 1; account.owner = ks_lib::MdProgramId(ks_program_ids::SYSTEM_PROGRAM_ID.to_string()); assert!(!super::metadata_is_exact_fee_tombstone(&account)); } fn result( account: std::string::String, kind: &str, payload_json: serde_json::Value, ) -> ks_pipeline::MetaplexTokenMetadataStatefulReadResult { return ks_pipeline::MetaplexTokenMetadataStatefulReadResult { commitment: "confirmed".to_string(), context_slot: 1, snapshot: ks_pipeline::MetaplexTokenMetadataStatefulSnapshot { account: ks_lib::MdPubkey(account), account_kind: kind.to_string(), mint: std::option::Option::None, slot: 1, payload_json, }, }; } 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_print_burn_campaign_from_env() { if std::env::var("KS_DEVNET_METAPLEX_PRINT_BURN_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.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_print_burn_campaign( &pool, &store, &profile, workspace_root, &options, &crate::NoopSolanaExecutionObserver, ) .await .unwrap_or_else(|error| panic!("Metaplex Print -> Burn Devnet campaign failed: {error}")); let print_confirmation = summary .print .confirmation .as_ref() .unwrap_or_else(|| panic!("Print confirmation missing")); let burn_confirmation = summary .burn .confirmation .as_ref() .unwrap_or_else(|| panic!("Burn confirmation missing")); println!( "METAPLEX_PRINT_BURN_FIXTURE master_mint={} master_metadata={} master_edition={} master_token_account={} edition_mint={} edition_metadata={} edition={} edition_token_account={} edition_marker={} edition_mint_absent_before_print={} edition_token_absent_before_print={}", summary.master.fixture.mint, summary.master.fixture.metadata, summary.master.fixture.master_edition, summary.master.fixture.token_account, summary.edition_mint, summary.edition_metadata, summary.edition, summary.edition_token_account, summary.edition_marker, summary.edition_mint_absent_before_print, summary.edition_token_absent_before_print, ); println!( "METAPLEX_PRINT_BURN_STEP operation={} signature={} slot={:?} materializations={}", summary.print.plan.operation_code, print_confirmation.signature.0, print_confirmation.slot, summary.print.materializations.len(), ); println!( "METAPLEX_PRINT_BURN_STEP operation={} signature={} slot={:?} materializations={}", summary.burn.plan.operation_code, burn_confirmation.signature.0, burn_confirmation.slot, summary.burn.materializations.len(), ); println!( "METAPLEX_PRINT_BURN_STATE edition_number={} master_supply_before_print={} master_supply_after_print={} master_supply_after_burn={} master_max_supply={} edition_supply_after_print={} edition_supply_after_burn={} edition_token_amount_after_print={} edition_token_state_after_print={} edition_taken_after_print={} edition_metadata_exists_after_burn={} edition_metadata_fee_tombstone_after_burn={} edition_metadata_closed_after_burn={} edition_exists_after_burn={} edition_token_exists_after_burn={} edition_marker_exists_after_burn={}", summary.state.edition_number, summary.state.master_supply_before_print, summary.state.master_supply_after_print, summary.state.master_supply_after_burn, summary.state.master_max_supply, summary.state.edition_supply_after_print, summary.state.edition_supply_after_burn, summary.state.edition_token_amount_after_print, summary.state.edition_token_state_after_print, summary.state.edition_taken_after_print, summary.state.edition_metadata_exists_after_burn, summary.state.edition_metadata_fee_tombstone_after_burn, summary.state.edition_metadata_closed_after_burn, summary.state.edition_exists_after_burn, summary.state.edition_token_exists_after_burn, summary.state.edition_marker_exists_after_burn, ); println!( "METAPLEX_PRINT_BURN_EVIDENCE print={} burn={}", execution_evidence_json(&summary.print), execution_evidence_json(&summary.burn), ); } }