// file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/fixture.rs // version: 5 //! Native Token-2022 mint preparation for the embedded Token Metadata Devnet campaign. /// Stable metadata name used by the `0.4.8-pre.012` campaign. pub const TOKEN_2022_METADATA_CAMPAIGN_NAME: &str = "Khadhroony Token-2022 Metadata"; /// Stable metadata symbol used by the `0.4.8-pre.012` campaign. pub const TOKEN_2022_METADATA_CAMPAIGN_SYMBOL: &str = "KBTM"; /// Opaque URI stored on-chain by the `0.4.8-pre.012` campaign. pub const TOKEN_2022_METADATA_CAMPAIGN_URI: &str = "https://example.invalid/khadhroony/token-2022-metadata.json"; /// Additional metadata key created and removed by the campaign. pub const TOKEN_2022_METADATA_CAMPAIGN_KEY: &str = "campaign"; /// Additional metadata value created and removed by the campaign. pub const TOKEN_2022_METADATA_CAMPAIGN_VALUE: &str = "0.4.8-pre.012"; const TOKEN_2022_TLV_HEADER_BYTES: usize = 4; /// Options used to prepare one fresh Token-2022 mint for the metadata campaign. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Token2022MetadataFixturePreparationOptions { /// 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 persistent fixture keypairs. pub wallet_dir: std::path::PathBuf, /// Explicit operator authorization for the lamport-spending fixture transaction. pub operator_confirmed: bool, } impl crate::Token2022MetadataFixturePreparationOptions { /// Creates conservative defaults matching 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, operator_confirmed: false, }; } } /// Fresh Token-2022 mint and authority identities used by the metadata campaign. #[derive(Clone, Debug, PartialEq, serde::Serialize)] pub struct Token2022MetadataFixturePreparationSummary { /// Persistent fixture mint keypair path. pub mint_keypair_path: std::path::PathBuf, /// Persistent final metadata-authority keypair path. pub final_authority_keypair_path: std::path::PathBuf, /// Fresh Token-2022 mint address. Embedded metadata uses this same account. pub mint: ks_lib::MdPubkey, /// Profile wallet used as mint and initial metadata update authority. pub initial_authority: ks_lib::MdPubkey, /// Fresh public key installed by the final `UpdateAuthority` campaign step. pub final_authority: ks_lib::MdPubkey, /// Initial mint account bytes containing only the fixed Metadata Pointer extension. pub mint_space: usize, /// Account-size budget used to pre-fund rent for the largest campaign metadata state. pub rent_budget_space: usize, /// Rent reserve deposited when the mint is created. pub rent_reserve_lamports: u64, /// Confirmed signature that created the Token-2022 mint fixture. pub preparation_signature: std::string::String, /// Authoritative snapshot after mint creation and before Token Metadata initialization. pub initial_snapshot: ks_pipeline::Token2022StatefulReadResult, } /// Creates a fresh Token-2022 mint with Metadata Pointer initialized to the mint itself. /// /// The mint account is created with the fixed Metadata Pointer size but is pre-funded /// for the largest state reached by this campaign so Token Metadata reallocations /// remain rent-exempt without introducing an additional payer instruction. pub async fn prepare_token_2022_metadata_fixture( http_pool: &ks_onchain_transport::HttpEndpointPool, profile: &ks_config::ProfileConfig, workspace_root: &std::path::Path, options: &crate::Token2022MetadataFixturePreparationOptions, ) -> 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( "Token-2022 metadata fixture endpoint roles must not be empty", )); } if profile.wallet.cluster != "devnet" || !profile.wallet.temporary_wallet_enabled || !profile.wallet.temporary_wallet_persist { return std::result::Result::Err(ks_core::Error::config( "Token-2022 metadata fixture requires a persistent Devnet wallet profile", )); } if !profile.execution.devnet_send_enabled || !options.operator_confirmed { return std::result::Result::Err(ks_core::Error::config( "Token-2022 metadata fixture requires enabled Devnet submission and explicit operator confirmation", )); } let operator = match crate::load_profile_wallet(profile, workspace_root).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let fixture_store = match ks_wallet::TemporaryWalletStore::new( options.wallet_dir.join("token_2022_metadata_validation"), ) { 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_millis(), std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "unable to create a unique Token-2022 metadata fixture identifier: {error}" ))); }, }; let mint_alias = match ks_wallet::WalletAlias::parse( format!("token-2022-metadata-mint-{fixture_epoch}").as_str(), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let final_authority_alias = match ks_wallet::WalletAlias::parse( format!("token-2022-metadata-authority-{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.load_or_create(mint_alias.clone()).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let final_authority_wallet = match fixture_store.load_or_create(final_authority_alias.clone()).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mint = ks_lib::MdPubkey(mint_wallet.public_key()); let initial_authority = ks_lib::MdPubkey(operator.public_key()); let final_authority = ks_lib::MdPubkey(final_authority_wallet.public_key()); let mint_space = match token_2022_metadata_pointer_mint_space() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let rent_budget_space = metadata_campaign_rent_budget_space(mint_space); let preparation_signature = match create_native_token_2022_metadata_mint( http_pool, profile, options, &operator, &mint_wallet, mint_space, rent_budget_space, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let initial_snapshot = match ks_pipeline::read_token_2022_stateful_snapshot( http_pool, &ks_pipeline::Token2022StatefulReadRequest { query_role: options.query_role.clone(), account: mint.clone(), kind: ks_lib::DcToken2022StateKind::Mint, min_context_slot: std::option::Option::None, max_data_bytes: ks_pipeline::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES, context: ks_pipeline::Token2022StatefulContext::default(), }, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if !initial_snapshot .snapshot .extension_names .iter() .any(|value| return value == "metadata_pointer") || initial_snapshot .snapshot .extension_names .iter() .any(|value| return value == "token_metadata") || !metadata_pointer_snapshot_matches(&initial_snapshot.snapshot, &mint, &initial_authority) { return std::result::Result::Err(ks_core::Error::new( "token_2022_metadata_fixture_state_invalid", "fresh metadata fixture must contain the expected self-referential Metadata Pointer and no Token Metadata extension", )); } let rent = match http_pool .get_minimum_balance_for_rent_exemption_for_role( options.query_role.as_str(), match u64::try_from(rent_budget_space) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "Token-2022 metadata rent budget does not fit u64: {error}" ))); }, }, &ks_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(crate::Token2022MetadataFixturePreparationSummary { mint_keypair_path: fixture_store.wallet_path(&mint_alias), final_authority_keypair_path: fixture_store.wallet_path(&final_authority_alias), mint, initial_authority, final_authority, mint_space, rent_budget_space, rent_reserve_lamports: rent.minimum_balance_lamports, preparation_signature, initial_snapshot, }); } fn metadata_pointer_snapshot_matches( snapshot: &ks_pipeline::Token2022StatefulSnapshotBundle, mint: &ks_lib::MdPubkey, authority: &ks_lib::MdPubkey, ) -> bool { return snapshot.outputs.iter().any(|output| { return output.family == ks_lib::MdMaterializedEventFamily::Admin && output.payload_json.get("extensionName").and_then(serde_json::Value::as_str) == std::option::Option::Some("metadata_pointer") && output .payload_json .get("valueFields") .and_then(|value| return value.get("address")) .and_then(serde_json::Value::as_str) == std::option::Option::Some(mint.0.as_str()) && output .payload_json .get("valueFields") .and_then(|value| return value.get("authority")) .and_then(serde_json::Value::as_str) == std::option::Option::Some(authority.0.as_str()); }); } fn token_2022_metadata_pointer_mint_space() -> ks_core::Result { return match spl_token_2022_interface::extension::ExtensionType::try_calculate_account_len::< spl_token_2022_interface::pod::PodMint, >(&[spl_token_2022_interface::extension::ExtensionType::MetadataPointer]) { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new( "token_2022_metadata_fixture_space_failed", format!("unable to calculate Metadata Pointer mint space: {error}"), )), }; } fn metadata_campaign_rent_budget_space(mint_space: usize) -> usize { return mint_space .saturating_add(TOKEN_2022_TLV_HEADER_BYTES) .saturating_add(metadata_campaign_max_value_bytes()); } fn metadata_campaign_max_value_bytes() -> usize { return 64_usize .saturating_add(4) .saturating_add(TOKEN_2022_METADATA_CAMPAIGN_NAME.len()) .saturating_add(4) .saturating_add(TOKEN_2022_METADATA_CAMPAIGN_SYMBOL.len()) .saturating_add(4) .saturating_add(TOKEN_2022_METADATA_CAMPAIGN_URI.len()) .saturating_add(4) .saturating_add(4) .saturating_add(TOKEN_2022_METADATA_CAMPAIGN_KEY.len()) .saturating_add(4) .saturating_add(TOKEN_2022_METADATA_CAMPAIGN_VALUE.len()); } #[allow(clippy::too_many_arguments)] async fn create_native_token_2022_metadata_mint( http_pool: &ks_onchain_transport::HttpEndpointPool, profile: &ks_config::ProfileConfig, options: &crate::Token2022MetadataFixturePreparationOptions, operator: &ks_wallet::TemporaryWallet, mint_wallet: &ks_wallet::TemporaryWallet, mint_space: usize, rent_budget_space: usize, ) -> ks_core::Result { let rent_space = match u64::try_from(rent_budget_space) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "Token-2022 metadata rent budget does not fit u64: {error}" ))); }, }; let account_space = match u64::try_from(mint_space) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "Token-2022 Metadata Pointer mint space does not fit u64: {error}" ))); }, }; let rent = match http_pool .get_minimum_balance_for_rent_exemption_for_role( options.query_role.as_str(), rent_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 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!("token-2022-metadata-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: account_space, owner: ks_lib::MdPubkey(ks_program_ids::SPL_TOKEN_2022_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_2022_PROGRAM_ID, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "invalid Token-2022 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 Token-2022 metadata fixture mint: {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 Token-2022 metadata fixture authority: {error}" ))); }, }; let pointer_instruction = match spl_token_2022_interface::extension::metadata_pointer::instruction::initialize( &token_program, &mint_pubkey, std::option::Option::Some(authority_pubkey), std::option::Option::Some(mint_pubkey), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::new( "token_2022_metadata_fixture_pointer_build_failed", error.to_string(), )); }, }; let initialize_mint = match spl_token_2022_interface::instruction::initialize_mint2( &token_program, &mint_pubkey, &authority_pubkey, std::option::Option::Some(&authority_pubkey), 0, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::new( "token_2022_metadata_fixture_initialize_mint_build_failed", error.to_string(), )); }, }; let plan = merge_fixture_instructions( system_plan, std::vec![ planned_instruction("spl.token_2022.initialize_metadata_pointer", &pointer_instruction,), planned_instruction("spl.token_2022.initialize_mint2", &initialize_mint), ], policy, rent.minimum_balance_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( "token_2022_metadata_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( "token_2022_metadata_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( "token_2022_metadata_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( "token_2022_metadata_fixture_confirmation_failed", format!("Token-2022 metadata mint preparation stopped at {:?}", confirmation.status), )); } return std::result::Result::Ok(signature.0); } 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, token_instructions: std::vec::Vec, policy: ks_lib::ExApiExecutionPolicy, rent_lamports: u64, ) -> ks_lib::ExApiPreparedExecutionPlan { system_plan.instructions.extend(token_instructions); system_plan.executor_name = "ks-pipeline-demo-scenarios.token_2022_metadata_fixture".to_string(); system_plan.operation_code = "spl.token_2022.metadata.prepare_mint".to_string(); system_plan.policy = policy; system_plan.requested_spend_lamports = rent_lamports; return system_plan; } #[cfg(test)] mod tests { #[test] fn metadata_rent_budget_covers_pointer_and_largest_campaign_value() { let mint_space = super::token_2022_metadata_pointer_mint_space() .unwrap_or_else(|error| panic!("metadata pointer mint size failed: {error}")); assert!(mint_space > 82); let budget = super::metadata_campaign_rent_budget_space(mint_space); assert_eq!( budget, mint_space + super::TOKEN_2022_TLV_HEADER_BYTES + super::metadata_campaign_max_value_bytes() ); assert!(budget > mint_space); } }