Files
khadhroony-bot3/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/fixture.rs
2026-08-10 23:14:17 +02:00

1546 lines
63 KiB
Rust

// 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<std::string::String>,
/// Optional limited print supply reserved for one printable NFT master edition.
pub print_supply_limit: std::option::Option<u64>,
/// Optional bounded `Multiple` uses metadata reserved for the current Use probe.
pub uses: std::option::Option<mpl_token_metadata::types::Uses>,
}
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<std::string::String>,
) -> 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<std::string::String>,
/// Optional collection parent mint linked to this asset as initially unverified.
pub collection_mint: std::option::Option<std::string::String>,
/// 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<std::string::String>,
}
/// 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<crate::MetaplexCreateFixturePreparationSummary> {
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 <solana_pubkey::Pubkey as std::str::FromStr>::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 <solana_pubkey::Pubkey as std::str::FromStr>::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 <solana_pubkey::Pubkey as std::str::FromStr>::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 <solana_pubkey::Pubkey as std::str::FromStr>::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<NativeFixturePreparationEvidence> {
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 <solana_pubkey::Pubkey as std::str::FromStr>::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 <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 native fixture mint public key: {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 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<std::option::Option<ks_onchain_transport::AccountInfoValue>> {
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<u64>,
) -> ks_core::Result<std::option::Option<ks_onchain_transport::AccountInfoValue>> {
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<u64> {
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<std::option::Option<std::string::String>> {
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<u64>,
) -> ks_core::Result<std::option::Option<ks_onchain_transport::AccountInfoValue>> {
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<u8> {
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<u64> {
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<solana_pubkey::Pubkey> {
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<u64>,
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<std::string::String>,
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::<ks_lib::ExMetaplexTokenMetadataOperation>(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::<ks_lib::ExMetaplexTokenMetadataOperation>(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::<ks_lib::ExMetaplexTokenMetadataOperation>(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::<ks_lib::ExMetaplexTokenMetadataOperation>(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 <solana_pubkey::Pubkey as std::str::FromStr>::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::<ks_lib::ExMetaplexTokenMetadataOperation>(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
));
}
}
}