Files
khadhroony-bot3/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/print_burn_campaign.rs
2026-08-10 09:28:57 +02:00

1283 lines
56 KiB
Rust

// 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<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::DevnetMetaplexPrintBurnCampaignSummary>
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::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 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<solana_pubkey::Pubkey> {
return match <solana_pubkey::Pubkey as std::str::FromStr>::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<u64>,
) -> std::vec::Vec<ks_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
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<u64>,
) -> std::vec::Vec<ks_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
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<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_print_burn_confirmation_slot_missing",
format!("{step} has no confirmation 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_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<u64>)> {
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<bool> {
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<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 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),
);
}
}