// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/fixture.rs // version: 22 //! Native Metaplex Token Metadata fixture preparation for Devnet demos. /// Options used to create one fresh classic SPL mint for a Metaplex Create demo. #[derive(Clone, Debug, Eq, PartialEq)] pub struct MetaplexCreateFixturePreparationOptions { /// Endpoint role used for state, rent, balance, blockhash and fee calls. pub query_role: std::string::String, /// Endpoint role used for simulation, submission and confirmation polling. pub transaction_role: std::string::String, /// Directory receiving the persistent fixture mint keypair. pub wallet_dir: std::path::PathBuf, /// Asset family whose mint and Create arguments must remain coherent. pub asset_family: crate::MetaplexTokenMetadataAssetFamily, /// Optional collection parent mint linked to this asset as an initially unverified member. pub collection_mint: std::option::Option, /// Optional limited print supply reserved for one printable NFT master edition. pub print_supply_limit: std::option::Option, /// Optional bounded `Multiple` uses metadata reserved for the current Use probe. pub uses: std::option::Option, } impl crate::MetaplexCreateFixturePreparationOptions { /// Creates conservative defaults matching the Devnet execution profiles. pub fn new(wallet_dir: std::path::PathBuf) -> Self { return Self { query_role: "http_queries".to_string(), transaction_role: "http_transactions".to_string(), wallet_dir, asset_family: crate::MetaplexTokenMetadataAssetFamily::Nft, collection_mint: std::option::Option::None, print_supply_limit: std::option::Option::None, uses: std::option::Option::None, }; } /// Selects the asset family prepared by this fixture. pub fn with_asset_family( mut self, asset_family: crate::MetaplexTokenMetadataAssetFamily, ) -> Self { self.asset_family = asset_family; return self; } /// Links the prepared asset to one existing collection mint as an unverified member. pub fn with_unverified_collection( mut self, collection_mint: impl std::convert::Into, ) -> Self { self.collection_mint = std::option::Option::Some(collection_mint.into()); return self; } /// Configures a bounded limited print supply for one printable NFT master edition. pub fn with_limited_print_supply(mut self, limit: u64) -> Self { self.print_supply_limit = std::option::Option::Some(limit); return self; } /// Configures a bounded `Multiple` uses budget with `remaining == total`. pub fn with_multiple_uses(mut self, total: u64) -> Self { self.uses = std::option::Option::Some(mpl_token_metadata::types::Uses { use_method: mpl_token_metadata::types::UseMethod::Multiple, remaining: total, total, }); return self; } } /// Public fixture values and generated Create intent JSON. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct MetaplexCreateFixturePreparationSummary { /// Fixture mint keypair path. pub mint_keypair_path: std::path::PathBuf, /// Asset family prepared by this fixture. pub asset_family: crate::MetaplexTokenMetadataAssetFamily, /// Classic SPL mint address. pub mint: std::string::String, /// Exact decimals encoded in the classic SPL mint. pub mint_decimals: u8, /// Canonical Metaplex metadata PDA. pub metadata: std::string::String, /// Canonical Metaplex master-edition PDA. pub master_edition: std::string::String, /// Whether Create must allocate the canonical master-edition PDA. pub master_edition_requested: bool, /// Canonical classic SPL associated token account owned by the operator. pub token_account: std::string::String, /// Canonical programmable token-record PDA when the fixture is a pNFT. pub token_record: std::option::Option, /// Optional collection parent mint linked to this asset as initially unverified. pub collection_mint: std::option::Option, /// Operator public key used as mint, token owner and update authority. pub authority: std::string::String, /// Family-specific raw amount reserved for the Metaplex `Mint` operation. pub mint_amount_raw: u64, /// Family-specific typed Create operation JSON. pub operation_json: std::string::String, /// Family-specific typed Mint operation JSON executed only after Create. pub mint_operation_json: std::string::String, /// Whether the mint account was created during this call. pub mint_created: bool, /// Confirmed signature of native mint plus operator-ATA preparation when newly created. pub preparation_signature: std::option::Option, } /// Creates one family-consistent classic SPL mint and derives its Metaplex PDAs. pub async fn prepare_metaplex_create_fixture( http_pool: &ks_onchain_transport::HttpEndpointPool, profile: &ks_config::ProfileConfig, workspace_root: &std::path::Path, options: &crate::MetaplexCreateFixturePreparationOptions, ) -> ks_core::Result { if options.query_role.trim().is_empty() || options.transaction_role.trim().is_empty() { return std::result::Result::Err(ks_core::Error::config( "Metaplex fixture endpoint roles must not be empty", )); } if !profile.execution.devnet_send_enabled { return std::result::Result::Err(ks_core::Error::config( "Metaplex fixture preparation requires devnet_send_enabled=true", )); } if let std::option::Option::Some(limit) = options.print_supply_limit { if options.asset_family != crate::MetaplexTokenMetadataAssetFamily::Nft { return std::result::Result::Err(ks_core::Error::config( "Metaplex limited print supply is supported only for the NFT fixture family", )); } if limit == 0 || limit > 100 { return std::result::Result::Err(ks_core::Error::config( "Metaplex limited print supply must be between 1 and 100", )); } } if let std::option::Option::Some(uses) = options.uses.as_ref() { if options.asset_family != crate::MetaplexTokenMetadataAssetFamily::Nft { return std::result::Result::Err(ks_core::Error::config( "Metaplex Multiple-use fixture is supported only for the NFT family", )); } if !matches!(&uses.use_method, mpl_token_metadata::types::UseMethod::Multiple) || uses.total == 0 || uses.total > 100 || uses.remaining != uses.total { return std::result::Result::Err(ks_core::Error::config( "Metaplex Multiple-use fixture requires 1..=100 total uses and remaining == total", )); } } if let std::option::Option::Some(collection_mint) = options.collection_mint.as_ref() { if options.asset_family == crate::MetaplexTokenMetadataAssetFamily::Collection || options.asset_family == crate::MetaplexTokenMetadataAssetFamily::Fungible { return std::result::Result::Err(ks_core::Error::config( "Metaplex collection members must not use the Collection or Fungible asset family", )); } if let std::result::Result::Err(error) = ks_onchain_transport::validate_solana_pubkey_text( collection_mint.as_str(), "Metaplex collection parent mint", ) { return std::result::Result::Err(error); } } let operator = match crate::load_profile_temporary_wallet(profile, workspace_root).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let fixture_dir = options.wallet_dir.join("metaplex_token_metadata_validation"); let fixture_store = match ks_wallet::TemporaryWalletStore::new(fixture_dir.clone()) { 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 Metaplex fixture identifier: {error}" ))); }, }; let fixture_alias = match ks_wallet::WalletAlias::parse(format!("create-mint-native-{fixture_epoch}").as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mint_wallet = match fixture_store.create(fixture_alias.clone()).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mint_keypair_path = fixture_store.wallet_path(&fixture_alias); let authority = operator.public_key(); let mint = mint_wallet.public_key(); let mint_decimals = fixture_mint_decimals(options.asset_family); let existing = match read_mint_account(http_pool, options.query_role.as_str(), mint.as_str()).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if existing.is_some() { return std::result::Result::Err(ks_core::Error::new( "metaplex_fixture_fresh_mint_already_exists", format!( "fresh Metaplex fixture mint {mint} unexpectedly already exists on Devnet; refusing to reuse persisted fixture state" ), )); } let preparation = match create_native_classic_mint( http_pool, profile, options, &operator, &mint_wallet, mint_decimals, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let created = match read_mint_account_at_or_after( http_pool, options.query_role.as_str(), mint.as_str(), std::option::Option::Some(preparation.confirmation_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_fixture_mint_missing_after_confirmation", format!( "native Metaplex fixture mint {mint} is unavailable at or after preparation slot {}", preparation.confirmation_slot ), )); }, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if let std::result::Result::Err(error) = validate_classic_fixture_mint(&created, authority.as_str(), mint_decimals) { return std::result::Result::Err(ks_core::Error::new( "metaplex_fixture_created_mint_state_mismatch", format!( "fresh fixture mint {mint} prepared by authority {authority} at slot {} failed immediate state validation: {error}", preparation.confirmation_slot ), )); } let mint_pubkey = match ::from_str(mint.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "invalid generated mint public key: {error}" ))); }, }; let program_id = match ::from_str( ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "invalid Metaplex Program ID: {error}" ))); }, }; let (metadata, _) = solana_pubkey::Pubkey::find_program_address( &[b"metadata", program_id.as_ref(), mint_pubkey.as_ref()], &program_id, ); let (master_edition, _) = solana_pubkey::Pubkey::find_program_address( &[b"metadata", program_id.as_ref(), mint_pubkey.as_ref(), b"edition"], &program_id, ); let master_edition_requested = fixture_requires_master_edition(options.asset_family); let token_program = match ::from_str( ks_program_ids::SPL_TOKEN_PROGRAM_ID, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "invalid classic SPL Token Program ID: {error}" ))); }, }; let authority_pubkey = match ::from_str(authority.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "invalid Metaplex fixture authority public key: {error}" ))); }, }; let token_account = spl_associated_token_account_interface::address::get_associated_token_address_with_program_id( &authority_pubkey, &mint_pubkey, &token_program, ); let mint_text = mint_pubkey.to_string(); let token_account_text = token_account.to_string(); let token_account_state = match read_token_account_at_or_after( http_pool, options.query_role.as_str(), token_account_text.as_str(), std::option::Option::Some(preparation.confirmation_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_fixture_token_account_missing_after_confirmation", "native Metaplex fixture ATA is unavailable after confirmation", )); }, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if let std::result::Result::Err(error) = validate_classic_fixture_token_account( &token_account_state, mint_text.as_str(), authority.as_str(), ) { return std::result::Result::Err(error); } let token_record = fixture_token_record(options.asset_family, &program_id, &mint_pubkey, &token_account); let mint_amount_raw = fixture_mint_amount_raw(options.asset_family); let operation = create_operation_json( mint_text.clone(), metadata.to_string(), master_edition.to_string(), authority.clone(), options.asset_family, options.collection_mint.as_deref(), options.print_supply_limit, options.uses.as_ref(), ); let mint_operation = mint_operation_json( mint_text.clone(), metadata.to_string(), master_edition.to_string(), token_account_text.clone(), token_record.as_ref().map(|value| return value.to_string()), authority.clone(), mint_amount_raw, options.asset_family, ); let operation_json = match serde_json::to_string_pretty(&operation) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::new( "metaplex_fixture_serialization_failed", format!("unable to serialize Metaplex Create fixture: {error}"), )); }, }; let mint_operation_json = match serde_json::to_string_pretty(&mint_operation) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::new( "metaplex_fixture_serialization_failed", format!("unable to serialize Metaplex Mint fixture: {error}"), )); }, }; return std::result::Result::Ok(crate::MetaplexCreateFixturePreparationSummary { mint_keypair_path, asset_family: options.asset_family, mint: mint_text, mint_decimals, metadata: metadata.to_string(), master_edition: master_edition.to_string(), master_edition_requested, token_account: token_account_text, token_record: token_record.map(|value| return value.to_string()), collection_mint: options.collection_mint.clone(), authority, mint_amount_raw, operation_json, mint_operation_json, mint_created: true, preparation_signature: std::option::Option::Some(preparation.signature), }); } #[derive(Clone, Debug, Eq, PartialEq)] struct NativeFixturePreparationEvidence { signature: std::string::String, confirmation_slot: u64, } async fn create_native_classic_mint( http_pool: &ks_onchain_transport::HttpEndpointPool, profile: &ks_config::ProfileConfig, options: &crate::MetaplexCreateFixturePreparationOptions, operator: &ks_wallet::TemporaryWallet, mint_wallet: &ks_wallet::TemporaryWallet, mint_decimals: u8, ) -> ks_core::Result { const MINT_SPACE: u64 = 82; let rent = match http_pool .get_minimum_balance_for_rent_exemption_for_role( options.query_role.as_str(), MINT_SPACE, &ks_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let token_account_rent = match http_pool .get_minimum_balance_for_rent_exemption_for_role( options.query_role.as_str(), 165, &ks_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let payer = ks_lib::MdPubkey(operator.public_key()); let mint = ks_lib::MdPubkey(mint_wallet.public_key()); let policy = fixture_policy(profile, payer.clone(), mint.clone(), rent.minimum_balance_lamports); let system_intent = ks_lib::ExSolanaCoreExecutionIntent { intent_id: format!("metaplex-fixture-create-account-{}", mint.0), fee_payer: payer.clone(), policy: policy.clone(), operation: ks_lib::ExSolanaCoreOperation::SystemCreateAccount { from: payer.clone(), new_account: mint.clone(), lamports: rent.minimum_balance_lamports, space: MINT_SPACE, owner: ks_lib::MdPubkey(ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()), }, }; let system_plan = match ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan( &ks_lib::ExSolanaCoreExecutor, &system_intent, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let token_program = match ::from_str( ks_program_ids::SPL_TOKEN_PROGRAM_ID, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "invalid classic SPL Token Program ID: {error}" ))); }, }; let mint_pubkey = match ::from_str(mint.0.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "invalid native fixture mint public key: {error}" ))); }, }; let authority_pubkey = match ::from_str(payer.0.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "invalid native fixture authority public key: {error}" ))); }, }; let token_instruction = match spl_token_interface::instruction::initialize_mint2( &token_program, &mint_pubkey, &authority_pubkey, std::option::Option::Some(&authority_pubkey), mint_decimals, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::new( "metaplex_fixture_initialize_mint_build_failed", error.to_string(), )); }, }; let ata_instruction = spl_associated_token_account_interface::instruction::create_associated_token_account( &authority_pubkey, &authority_pubkey, &mint_pubkey, &token_program, ); let requested_spend_lamports = rent .minimum_balance_lamports .saturating_add(token_account_rent.minimum_balance_lamports); let plan = merge_fixture_instructions( system_plan, &[ planned_instruction("spl.token.initialize_mint2", &token_instruction), planned_instruction("spl.associated_token_account.create", &ata_instruction), ], policy, requested_spend_lamports, ); let plan_evaluation = match ks_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if plan_evaluation.decision == ks_lib::ExSafetyDecision::Deny { return std::result::Result::Err(ks_core::Error::new( "metaplex_fixture_plan_denied", crate::violation_message(plan_evaluation.violations.as_slice()), )); } let latest_blockhash = match http_pool .get_latest_blockhash_for_role( options.query_role.as_str(), &ks_onchain_transport::GetLatestBlockhashConfig::confirmed(), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let unsigned = match ks_lib::executor_solana_build_legacy_transaction( &plan, latest_blockhash.blockhash.as_str(), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let fee = match http_pool .get_fee_for_message_for_role( options.query_role.as_str(), unsigned.message_base64().as_str(), &ks_onchain_transport::GetFeeForMessageConfig::new( ks_onchain_transport::RpcCommitmentLevel::Confirmed, std::option::Option::Some(latest_blockhash.context.slot), ), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let unsigned_base64 = match unsigned.transaction_base64() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let simulation_config = match ks_onchain_transport::SimulateTransactionConfig::new( ks_onchain_transport::RpcCommitmentLevel::Confirmed, false, false, std::option::Option::Some(latest_blockhash.context.slot), true, std::option::Option::None, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let simulation_rpc = match http_pool .simulate_transaction_for_role( options.transaction_role.as_str(), unsigned_base64.as_str(), &simulation_config, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let simulation = simulation_rpc.to_execution_result( ks_lib::ExApiExecutionCluster::Devnet, ks_lib::ExApiExecutionBlockhashKind::Latest, std::option::Option::Some( simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot), ), std::option::Option::None, std::option::Option::None, std::option::Option::Some(&fee), ); if !simulation.success { return std::result::Result::Err(ks_core::Error::new( "metaplex_fixture_simulation_failed", crate::simulation_failure_message(&simulation), )); } let send_evaluation = match ks_lib::ExSafetyChecker.evaluate_send(&plan, &simulation) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if send_evaluation.decision == ks_lib::ExSafetyDecision::Deny { return std::result::Result::Err(ks_core::Error::new( "metaplex_fixture_send_denied", crate::violation_message(send_evaluation.violations.as_slice()), )); } let evidence = unsigned.bind_simulation(simulation); let signed = match unsigned .sign_after_simulation(&evidence, &[operator.as_signer(), mint_wallet.as_signer()]) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if let std::result::Result::Err(error) = signed.verify_signatures() { return std::result::Result::Err(error); } let signature = signed.primary_signature().clone(); let send_config = match ks_onchain_transport::SendTransactionConfig::from_execution_config( &profile.execution, std::option::Option::Some(latest_blockhash.context.slot), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if let std::result::Result::Err(error) = http_pool .send_transaction_for_role( options.transaction_role.as_str(), signed.transaction_base64().as_str(), &signature, &send_config, ) .await { return std::result::Result::Err(error); } let confirmation_config = match ks_onchain_transport::ConfirmTransactionConfig::from_execution_config( &profile.execution, std::option::Option::Some(latest_blockhash.last_valid_block_height), std::option::Option::Some(latest_blockhash.context.slot), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let confirmation = match http_pool .confirm_transaction_for_roles( options.transaction_role.as_str(), options.query_role.as_str(), ks_lib::ExApiExecutionCluster::Devnet, &signature, &confirmation_config, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if !matches!( confirmation.status, ks_lib::ExApiExecutionConfirmationStatus::Confirmed | ks_lib::ExApiExecutionConfirmationStatus::Finalized ) { return std::result::Result::Err(ks_core::Error::new( "metaplex_fixture_confirmation_failed", format!("native mint preparation stopped at {:?}", confirmation.status), )); } let confirmation_slot = match confirmation.slot { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(ks_core::Error::new( "metaplex_fixture_confirmation_slot_missing", format!( "native mint preparation {} was confirmed without a confirmation slot", signature.0 ), )); }, }; tracing::info!( target: "ks-pipeline-demo-scenarios.metaplex_fixture", action = "prepare_native_metaplex_mint", mint = mint_wallet.public_key(), authority = operator.public_key(), signature = signature.0.as_str(), confirmation_slot, "created native classic SPL mint and operator ATA for Metaplex fixture" ); return std::result::Result::Ok(NativeFixturePreparationEvidence { signature: signature.0, confirmation_slot, }); } fn planned_instruction( operation_code: &str, instruction: &solana_instruction::Instruction, ) -> ks_lib::ExApiPlannedInstruction { return ks_lib::ExApiPlannedInstruction { program_id: ks_lib::MdProgramId(instruction.program_id.to_string()), operation_code: operation_code.to_string(), accounts: instruction .accounts .iter() .map(|account| { return ks_lib::ExApiPlannedAccount { pubkey: ks_lib::MdPubkey(account.pubkey.to_string()), is_signer: account.is_signer, is_writable: account.is_writable, }; }) .collect(), data: instruction.data.clone(), }; } fn fixture_policy( profile: &ks_config::ProfileConfig, payer: ks_lib::MdPubkey, mint: ks_lib::MdPubkey, _rent_lamports: u64, ) -> ks_lib::ExApiExecutionPolicy { return ks_lib::ExApiExecutionPolicy { cluster: ks_lib::ExApiExecutionClusterPolicy { expected_cluster: ks_lib::ExApiExecutionCluster::Devnet, allow_mainnet: false, mainnet_confirmation: false, }, simulation: ks_lib::ExApiExecutionSimulationPolicy::Required, blockhash: ks_lib::ExApiExecutionBlockhashPolicy { kind: ks_lib::ExApiExecutionBlockhashKind::Latest, max_age_slots: std::option::Option::Some( profile.execution.recent_blockhash_max_age_slots, ), nonce_account: std::option::Option::None, nonce_authority: std::option::Option::None, }, cost_limit: ks_lib::ExApiExecutionCostLimit { max_spend_lamports: std::option::Option::Some( profile.execution.devnet_max_spend_lamports, ), max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports), max_compute_unit_price_micro_lamports: std::option::Option::Some( profile.execution.max_compute_unit_price_micro_lamports, ), }, authorized_signers: std::vec![payer, mint], dry_run: false, post_execution_validation: ks_lib::ExApiPostExecutionValidationPolicy { canonical_insert_required: false, core_extraction_required: false, decode_replay_required: false, materialization_required: false, }, }; } fn merge_fixture_instructions( mut system_plan: ks_lib::ExApiPreparedExecutionPlan, additional_instructions: &[ks_lib::ExApiPlannedInstruction], policy: ks_lib::ExApiExecutionPolicy, requested_spend_lamports: u64, ) -> ks_lib::ExApiPreparedExecutionPlan { system_plan.instructions.extend_from_slice(additional_instructions); system_plan.executor_name = "ks-pipeline-demo-scenarios.metaplex_fixture".to_string(); system_plan.operation_code = "metadata.metaplex_token_metadata.prepare_native_mint_and_ata".to_string(); system_plan.policy = policy; system_plan.requested_spend_lamports = requested_spend_lamports; return system_plan; } async fn read_mint_account( http_pool: &ks_onchain_transport::HttpEndpointPool, query_role: &str, mint: &str, ) -> ks_core::Result> { return read_mint_account_at_or_after(http_pool, query_role, mint, std::option::Option::None) .await; } pub(crate) async fn read_mint_account_at_or_after( http_pool: &ks_onchain_transport::HttpEndpointPool, query_role: &str, mint: &str, min_context_slot: std::option::Option, ) -> ks_core::Result> { let config = match ks_onchain_transport::GetAccountInfoConfig::new_with_data( ks_onchain_transport::RpcCommitmentLevel::Confirmed, min_context_slot, 82, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mint_pubkey = ks_lib::MdPubkey(mint.to_string()); let result = match http_pool.get_account_info_for_role(query_role, &mint_pubkey, &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); } fn validate_classic_fixture_mint( account: &ks_onchain_transport::AccountInfoValue, expected_authority: &str, expected_decimals: u8, ) -> ks_core::Result<()> { return validate_classic_fixture_mint_state(account, expected_authority, expected_decimals, 0); } pub(crate) fn classic_fixture_mint_supply( account: &ks_onchain_transport::AccountInfoValue, ) -> ks_core::Result { let bytes = account.data.as_slice(); if bytes.len() != 82 { return std::result::Result::Err(ks_core::Error::config(format!( "Metaplex Create fixture mint must use the classic 82-byte layout, got {} bytes", bytes.len() ))); } let supply_bytes = match <[u8; 8]>::try_from(&bytes[36..44]) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::config( "invalid fixture mint supply bytes", )); }, }; return std::result::Result::Ok(u64::from_le_bytes(supply_bytes)); } pub(crate) fn validate_classic_fixture_mint_state( account: &ks_onchain_transport::AccountInfoValue, expected_authority: &str, expected_decimals: u8, expected_supply: u64, ) -> ks_core::Result<()> { if account.owner.0 != ks_program_ids::SPL_TOKEN_PROGRAM_ID || account.executable { return std::result::Result::Err(ks_core::Error::config(format!( "Metaplex Create fixture mint owner mismatch: expected classic SPL Token, got {}", account.owner.0 ))); } let bytes = account.data.as_slice(); if bytes.len() != 82 { return std::result::Result::Err(ks_core::Error::config(format!( "Metaplex Create fixture mint must use the classic 82-byte layout, got {} bytes", bytes.len() ))); } let mint_authority = match decode_coption_pubkey(&bytes[0..36], "mint authority") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let supply = match classic_fixture_mint_supply(account) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let decimals = bytes[44]; let initialized = bytes[45] == 1; let freeze_authority = match decode_coption_pubkey(&bytes[46..82], "freeze authority") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if decimals != expected_decimals || supply != expected_supply || !initialized { return std::result::Result::Err(ks_core::Error::config(format!( "Metaplex fixture mint requires initialized=true, decimals={expected_decimals} and supply={expected_supply}, got initialized={initialized}, decimals={decimals}, supply={supply}" ))); } if mint_authority.as_deref() != std::option::Option::Some(expected_authority) || freeze_authority.as_deref() != std::option::Option::Some(expected_authority) { return std::result::Result::Err(ks_core::Error::config(format!( "Metaplex fixture mint requires mint and freeze authorities to equal {expected_authority}; mint_authority={mint_authority:?}, freeze_authority={freeze_authority:?}" ))); } return std::result::Result::Ok(()); } fn decode_coption_pubkey( bytes: &[u8], label: &str, ) -> ks_core::Result> { if bytes.len() != 36 { return std::result::Result::Err(ks_core::Error::config(format!( "invalid {label} field length: {}", bytes.len() ))); } let tag = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); if tag == 0 { return std::result::Result::Ok(std::option::Option::None); } if tag != 1 { return std::result::Result::Err(ks_core::Error::config(format!( "invalid {label} option tag: {tag}" ))); } let key = match <[u8; 32]>::try_from(&bytes[4..36]) { std::result::Result::Ok(value) => solana_pubkey::Pubkey::new_from_array(value), std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::config(format!( "invalid {label} public key bytes" ))); }, }; return std::result::Result::Ok(std::option::Option::Some(key.to_string())); } pub(crate) async fn read_token_account_at_or_after( http_pool: &ks_onchain_transport::HttpEndpointPool, query_role: &str, token_account: &str, min_context_slot: std::option::Option, ) -> ks_core::Result> { let config = match ks_onchain_transport::GetAccountInfoConfig::new_with_data( ks_onchain_transport::RpcCommitmentLevel::Confirmed, min_context_slot, 165, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let address = ks_lib::MdPubkey(token_account.to_string()); let result = match http_pool.get_account_info_for_role(query_role, &address, &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); } fn validate_classic_fixture_token_account( account: &ks_onchain_transport::AccountInfoValue, expected_mint: &str, expected_owner: &str, ) -> ks_core::Result<()> { return validate_classic_fixture_token_account_state( account, expected_mint, expected_owner, 0, spl_token_interface::state::AccountState::Initialized, ); } pub(crate) fn classic_fixture_token_account_state( account: &ks_onchain_transport::AccountInfoValue, ) -> ks_core::Result { let bytes = account.data.as_slice(); if account.space != 165 || bytes.len() != 165 { return std::result::Result::Err(ks_core::Error::config(format!( "Metaplex fixture token account must use the classic 165-byte layout, got space={} and {} decoded bytes", account.space, bytes.len() ))); } let state = bytes[108]; if state != spl_token_interface::state::AccountState::Initialized as u8 && state != spl_token_interface::state::AccountState::Frozen as u8 { return std::result::Result::Err(ks_core::Error::config(format!( "Metaplex fixture token account must be initialized or frozen, got state={state}" ))); } return std::result::Result::Ok(state); } pub(crate) fn classic_fixture_token_amount( account: &ks_onchain_transport::AccountInfoValue, ) -> ks_core::Result { let bytes = account.data.as_slice(); if account.space != 165 || bytes.len() != 165 { return std::result::Result::Err(ks_core::Error::config(format!( "Metaplex fixture token account must use the classic 165-byte layout, got space={} and {} decoded bytes", account.space, bytes.len() ))); } let amount_bytes = match <[u8; 8]>::try_from(&bytes[64..72]) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::config( "invalid fixture token account amount bytes", )); }, }; return std::result::Result::Ok(u64::from_le_bytes(amount_bytes)); } pub(crate) fn validate_classic_fixture_token_account_state( account: &ks_onchain_transport::AccountInfoValue, expected_mint: &str, expected_owner: &str, expected_amount: u64, expected_state: spl_token_interface::state::AccountState, ) -> ks_core::Result<()> { if account.owner.0 != ks_program_ids::SPL_TOKEN_PROGRAM_ID || account.executable { return std::result::Result::Err(ks_core::Error::config(format!( "Metaplex fixture token account owner mismatch: expected classic SPL Token, got {}", account.owner.0 ))); } let bytes = account.data.as_slice(); if account.space != 165 || bytes.len() != 165 { return std::result::Result::Err(ks_core::Error::config(format!( "Metaplex fixture token account must use the classic 165-byte layout, got space={} and {} decoded bytes", account.space, bytes.len() ))); } let mint = solana_pubkey::Pubkey::new_from_array(match <[u8; 32]>::try_from(&bytes[0..32]) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::config( "invalid fixture token account mint bytes", )); }, }); let owner = solana_pubkey::Pubkey::new_from_array(match <[u8; 32]>::try_from(&bytes[32..64]) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::config( "invalid fixture token account owner bytes", )); }, }); let amount = match classic_fixture_token_amount(account) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if mint.to_string() != expected_mint || owner.to_string() != expected_owner || amount != expected_amount || bytes[108] != expected_state as u8 { return std::result::Result::Err(ks_core::Error::config(format!( "Metaplex fixture ATA requires mint={expected_mint}, owner={expected_owner}, amount={expected_amount} and state={expected_state:?}; got mint={mint}, owner={owner}, amount={amount}, state={}", bytes[108] ))); } return std::result::Result::Ok(()); } fn fixture_mint_amount_raw(asset_family: crate::MetaplexTokenMetadataAssetFamily) -> u64 { return match asset_family { crate::MetaplexTokenMetadataAssetFamily::Nft | crate::MetaplexTokenMetadataAssetFamily::Collection | crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft => 1, crate::MetaplexTokenMetadataAssetFamily::Sft => 10, crate::MetaplexTokenMetadataAssetFamily::Fungible => 1_000_000_000, }; } fn fixture_token_record( asset_family: crate::MetaplexTokenMetadataAssetFamily, program_id: &solana_pubkey::Pubkey, mint: &solana_pubkey::Pubkey, token_account: &solana_pubkey::Pubkey, ) -> std::option::Option { if asset_family != crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft { return std::option::Option::None; } let (token_record, _) = solana_pubkey::Pubkey::find_program_address( &[ b"metadata", program_id.as_ref(), mint.as_ref(), b"token_record", token_account.as_ref(), ], program_id, ); return std::option::Option::Some(token_record); } fn fixture_mint_decimals(asset_family: crate::MetaplexTokenMetadataAssetFamily) -> u8 { return match asset_family { crate::MetaplexTokenMetadataAssetFamily::Fungible => 9, crate::MetaplexTokenMetadataAssetFamily::Nft | crate::MetaplexTokenMetadataAssetFamily::Sft | crate::MetaplexTokenMetadataAssetFamily::Collection | crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft => 0, }; } fn fixture_requires_master_edition(asset_family: crate::MetaplexTokenMetadataAssetFamily) -> bool { return matches!( asset_family, crate::MetaplexTokenMetadataAssetFamily::Nft | crate::MetaplexTokenMetadataAssetFamily::Collection | crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft ); } fn create_operation_json( mint: std::string::String, metadata: std::string::String, master_edition: std::string::String, authority: std::string::String, asset_family: crate::MetaplexTokenMetadataAssetFamily, collection_mint: std::option::Option<&str>, print_supply_limit: std::option::Option, uses: std::option::Option<&mpl_token_metadata::types::Uses>, ) -> serde_json::Value { let (name, symbol, token_standard, collection_details) = match asset_family { crate::MetaplexTokenMetadataAssetFamily::Nft => { ("Khadhroony Devnet NFT", "KHNFT", "NonFungible", serde_json::Value::Null) }, crate::MetaplexTokenMetadataAssetFamily::Sft => { ("Khadhroony Devnet SFT", "KHSFT", "FungibleAsset", serde_json::Value::Null) }, crate::MetaplexTokenMetadataAssetFamily::Fungible => { ("Khadhroony Devnet Fungible", "KHFUN", "Fungible", serde_json::Value::Null) }, crate::MetaplexTokenMetadataAssetFamily::Collection => ( "Khadhroony Devnet Collection", "KHCOL", "NonFungible", serde_json::json!({"V1": {"size": 0}}), ), crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft => ( "Khadhroony Devnet pNFT", "KHPNFT", "ProgrammableNonFungible", serde_json::Value::Null, ), }; let requested_master_edition = if fixture_requires_master_edition(asset_family) { serde_json::Value::String(master_edition) } else { serde_json::Value::Null }; let collection = match collection_mint { std::option::Option::Some(value) => serde_json::json!({"verified": false, "key": value}), std::option::Option::None => serde_json::Value::Null, }; let uses = match uses { std::option::Option::Some(value) => { let use_method = match &value.use_method { mpl_token_metadata::types::UseMethod::Burn => "Burn", mpl_token_metadata::types::UseMethod::Multiple => "Multiple", mpl_token_metadata::types::UseMethod::Single => "Single", }; serde_json::json!({ "use_method": use_method, "remaining": value.remaining, "total": value.total, }) }, std::option::Option::None => serde_json::Value::Null, }; return serde_json::json!({ "operation": "create", "metadata": metadata, "master_edition": requested_master_edition, "mint": mint, "mint_as_signer": false, "authority": authority, "update_authority": authority, "update_authority_as_signer": true, "spl_token_program": ks_program_ids::SPL_TOKEN_PROGRAM_ID, "create_args": {"V1": { "name": name, "symbol": symbol, "uri": "https://example.invalid/khadhroony-devnet.json", "seller_fee_basis_points": 0, "creators": null, "primary_sale_happened": false, "is_mutable": true, "token_standard": token_standard, "collection": collection, "uses": uses, "collection_details": collection_details, "rule_set": null, "decimals": null, "print_supply": if let std::option::Option::Some(limit) = print_supply_limit { serde_json::json!({"Limited": limit}) } else if fixture_requires_master_edition(asset_family) { serde_json::json!("Zero") } else { serde_json::Value::Null } }} }); } fn mint_operation_json( mint: std::string::String, metadata: std::string::String, master_edition: std::string::String, token_account: std::string::String, token_record: std::option::Option, authority: std::string::String, amount: u64, asset_family: crate::MetaplexTokenMetadataAssetFamily, ) -> serde_json::Value { let master_edition = if fixture_requires_master_edition(asset_family) { serde_json::Value::String(master_edition) } else { serde_json::Value::Null }; return serde_json::json!({ "operation": "mint", "token": token_account, "token_owner": authority, "metadata": metadata, "master_edition": master_edition, "token_record": token_record, "mint": mint, "authority": authority, "delegate_record": null, "payer": authority, "system_program": ks_program_ids::SYSTEM_PROGRAM_ID, "sysvar_instructions": ks_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID, "spl_token_program": ks_program_ids::SPL_TOKEN_PROGRAM_ID, "spl_ata_program": ks_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID, "authorization_rules_program": null, "authorization_rules": null, "args": {"V1": { "amount": amount, "authorization_data": null }} }); } #[cfg(test)] mod tests { #[test] fn native_fixture_contract_uses_profile_authorities() { let authority = solana_pubkey::Pubkey::new_unique(); let mut mint = [0_u8; 82]; mint[0..4].copy_from_slice(&1_u32.to_le_bytes()); mint[4..36].copy_from_slice(authority.as_ref()); mint[44] = 0; mint[45] = 1; mint[46..50].copy_from_slice(&1_u32.to_le_bytes()); mint[50..82].copy_from_slice(authority.as_ref()); let decoded_mint = super::decode_coption_pubkey(&mint[0..36], "mint authority"); let decoded_freeze = super::decode_coption_pubkey(&mint[46..82], "freeze authority"); let authority_text = authority.to_string(); assert_eq!( decoded_mint.ok().flatten().as_deref(), std::option::Option::Some(authority_text.as_str()) ); assert_eq!( decoded_freeze.ok().flatten().as_deref(), std::option::Option::Some(authority_text.as_str()) ); } #[test] fn family_profiles_keep_mint_decimals_and_master_edition_contracts_exact() { let profiles = [ (crate::MetaplexTokenMetadataAssetFamily::Nft, 0, true), (crate::MetaplexTokenMetadataAssetFamily::Sft, 0, false), (crate::MetaplexTokenMetadataAssetFamily::Fungible, 9, false), (crate::MetaplexTokenMetadataAssetFamily::Collection, 0, true), (crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft, 0, true), ]; for (family, decimals, master_edition) in profiles { assert_eq!(super::fixture_mint_decimals(family), decimals); assert_eq!(super::fixture_requires_master_edition(family), master_edition); } } #[test] fn family_create_contracts_deserialize_to_the_typed_executor_surface() { let mint = solana_pubkey::Pubkey::new_unique().to_string(); let metadata = solana_pubkey::Pubkey::new_unique().to_string(); let master_edition = solana_pubkey::Pubkey::new_unique().to_string(); let authority = solana_pubkey::Pubkey::new_unique().to_string(); let profiles = [ (crate::MetaplexTokenMetadataAssetFamily::Nft, "NonFungible", true), (crate::MetaplexTokenMetadataAssetFamily::Sft, "FungibleAsset", false), (crate::MetaplexTokenMetadataAssetFamily::Fungible, "Fungible", false), (crate::MetaplexTokenMetadataAssetFamily::Collection, "NonFungible", true), ( crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft, "ProgrammableNonFungible", true, ), ]; for (family, token_standard, expects_master_edition) in profiles { let operation = super::create_operation_json( mint.clone(), metadata.clone(), master_edition.clone(), authority.clone(), family, std::option::Option::None, std::option::Option::None, std::option::Option::None, ); assert_eq!( operation .pointer("/create_args/V1/token_standard") .and_then(serde_json::Value::as_str), std::option::Option::Some(token_standard), ); assert_eq!( operation.get("master_edition").is_some_and(|value| return !value.is_null()), expects_master_edition ); let parsed = serde_json::from_value::(operation); assert!(parsed.is_ok()); assert_eq!( parsed.ok().map(|value| return value.operation_code()), std::option::Option::Some(ks_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION), ); } } #[test] fn collection_member_create_contract_keeps_relation_unverified() { let mint = solana_pubkey::Pubkey::new_unique().to_string(); let metadata = solana_pubkey::Pubkey::new_unique().to_string(); let master_edition = solana_pubkey::Pubkey::new_unique().to_string(); let authority = solana_pubkey::Pubkey::new_unique().to_string(); let collection = solana_pubkey::Pubkey::new_unique().to_string(); let operation = super::create_operation_json( mint, metadata, master_edition, authority, crate::MetaplexTokenMetadataAssetFamily::Nft, std::option::Option::Some(collection.as_str()), std::option::Option::None, std::option::Option::None, ); assert_eq!( operation .pointer("/create_args/V1/collection/key") .and_then(serde_json::Value::as_str), std::option::Option::Some(collection.as_str()), ); assert_eq!( operation .pointer("/create_args/V1/collection/verified") .and_then(serde_json::Value::as_bool), std::option::Option::Some(false), ); assert!( serde_json::from_value::(operation).is_ok() ); } #[test] fn printable_nft_create_contract_uses_bounded_limited_supply() { let operation = super::create_operation_json( solana_pubkey::Pubkey::new_unique().to_string(), solana_pubkey::Pubkey::new_unique().to_string(), solana_pubkey::Pubkey::new_unique().to_string(), solana_pubkey::Pubkey::new_unique().to_string(), crate::MetaplexTokenMetadataAssetFamily::Nft, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None, ); assert_eq!( operation .pointer("/create_args/V1/print_supply/Limited") .and_then(serde_json::Value::as_u64), std::option::Option::Some(1), ); assert!( serde_json::from_value::(operation).is_ok() ); let invalid = crate::MetaplexCreateFixturePreparationOptions::new(std::path::PathBuf::new()) .with_asset_family(crate::MetaplexTokenMetadataAssetFamily::Fungible) .with_limited_print_supply(1); assert_eq!(invalid.print_supply_limit, std::option::Option::Some(1)); } #[test] fn multiple_use_fixture_contract_is_bounded_and_typed() { let uses = mpl_token_metadata::types::Uses { use_method: mpl_token_metadata::types::UseMethod::Multiple, remaining: 2, total: 2, }; let operation = super::create_operation_json( solana_pubkey::Pubkey::new_unique().to_string(), solana_pubkey::Pubkey::new_unique().to_string(), solana_pubkey::Pubkey::new_unique().to_string(), solana_pubkey::Pubkey::new_unique().to_string(), crate::MetaplexTokenMetadataAssetFamily::Nft, std::option::Option::None, std::option::Option::None, std::option::Option::Some(&uses), ); assert_eq!( operation .pointer("/create_args/V1/uses/use_method") .and_then(serde_json::Value::as_str), std::option::Option::Some("Multiple"), ); assert_eq!( operation .pointer("/create_args/V1/uses/remaining") .and_then(serde_json::Value::as_u64), std::option::Option::Some(2), ); assert_eq!( operation .pointer("/create_args/V1/uses/total") .and_then(serde_json::Value::as_u64), std::option::Option::Some(2), ); assert!( serde_json::from_value::(operation).is_ok() ); let options = crate::MetaplexCreateFixturePreparationOptions::new( std::path::PathBuf::from("wallets"), ) .with_multiple_uses(2); assert_eq!(options.uses, std::option::Option::Some(uses)); } #[test] fn classic_operator_ata_contract_is_exact_before_metaplex_mint() { let mint = solana_pubkey::Pubkey::new_unique(); let owner = solana_pubkey::Pubkey::new_unique(); let mut data = std::vec![0_u8; 165]; data[0..32].copy_from_slice(mint.as_ref()); data[32..64].copy_from_slice(owner.as_ref()); data[64..72].copy_from_slice(&0_u64.to_le_bytes()); data[108] = 1; let account = ks_onchain_transport::AccountInfoValue { lamports: 0, owner: ks_lib::MdProgramId(ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()), executable: false, rent_epoch: 0, space: 165, data, }; assert!( super::validate_classic_fixture_token_account( &account, mint.to_string().as_str(), owner.to_string().as_str(), ) .is_ok() ); let mut funded = account.clone(); funded.data[64..72].copy_from_slice(&1_u64.to_le_bytes()); assert!( super::validate_classic_fixture_token_account( &funded, mint.to_string().as_str(), owner.to_string().as_str(), ) .is_err() ); let mut frozen = account.clone(); frozen.data[108] = spl_token_interface::state::AccountState::Frozen as u8; assert!( super::validate_classic_fixture_token_account_state( &frozen, mint.to_string().as_str(), owner.to_string().as_str(), 0, spl_token_interface::state::AccountState::Frozen, ) .is_ok() ); assert!( super::validate_classic_fixture_token_account_state( &frozen, mint.to_string().as_str(), owner.to_string().as_str(), 0, spl_token_interface::state::AccountState::Initialized, ) .is_err() ); } #[test] fn family_mint_contracts_keep_amount_master_edition_and_token_record_exact() { let program_id = match ::from_str( ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let mint = solana_pubkey::Pubkey::new_unique(); let metadata = solana_pubkey::Pubkey::new_unique(); let master_edition = solana_pubkey::Pubkey::new_unique(); let token_account = solana_pubkey::Pubkey::new_unique(); let authority = solana_pubkey::Pubkey::new_unique().to_string(); let profiles = [ (crate::MetaplexTokenMetadataAssetFamily::Nft, 1_u64, true, false), (crate::MetaplexTokenMetadataAssetFamily::Sft, 10_u64, false, false), ( crate::MetaplexTokenMetadataAssetFamily::Fungible, 1_000_000_000_u64, false, false, ), (crate::MetaplexTokenMetadataAssetFamily::Collection, 1_u64, true, false), (crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft, 1_u64, true, true), ]; for (family, expected_amount, expects_master_edition, expects_token_record) in profiles { let token_record = super::fixture_token_record(family, &program_id, &mint, &token_account); assert_eq!(token_record.is_some(), expects_token_record); assert_eq!(super::fixture_mint_amount_raw(family), expected_amount); let operation = super::mint_operation_json( mint.to_string(), metadata.to_string(), master_edition.to_string(), token_account.to_string(), token_record.map(|value| return value.to_string()), authority.clone(), expected_amount, family, ); let parsed = serde_json::from_value::(operation); assert!(matches!( parsed, std::result::Result::Ok(ks_lib::ExMetaplexTokenMetadataOperation::Mint { token_owner: std::option::Option::Some(_), master_edition, token_record, args: mpl_token_metadata::types::MintArgs::V1 { amount, authorization_data: std::option::Option::None, }, .. }) if amount == expected_amount && master_edition.is_some() == expects_master_edition && token_record.is_some() == expects_token_record )); } } }