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

662 lines
28 KiB
Rust

// 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<crate::Token2022MetadataFixturePreparationSummary> {
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<usize> {
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<std::string::String> {
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 <solana_pubkey::Pubkey as std::str::FromStr>::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 <solana_pubkey::Pubkey as std::str::FromStr>::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 <solana_pubkey::Pubkey as std::str::FromStr>::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<ks_lib::ExApiPlannedInstruction>,
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);
}
}