1283 lines
56 KiB
Rust
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),
|
|
);
|
|
}
|
|
}
|