v0.4.7-pre.013

This commit is contained in:
2026-08-04 17:14:01 +02:00
parent 4d08c75b2b
commit 22228149a3
26 changed files with 2367 additions and 223 deletions

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/lib.rs
// version: 10
// version: 12
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
@@ -9,6 +9,7 @@
mod constants;
mod environment;
mod metaplex_token_metadata_fixture;
mod solana_ata_execution;
mod solana_execution;
mod solana_memo_execution;
@@ -30,6 +31,12 @@ pub use self::environment::initialize_demo_scenario_environment;
pub use self::environment::prepare_demo_devnet_profile_store;
/// Resolves one configured Devnet profile for UI, CLI and test scenarios.
pub use self::environment::resolve_demo_devnet_profile;
/// Options for one Metaplex Create fixture preparation.
pub use self::metaplex_token_metadata_fixture::MetaplexCreateFixturePreparationOptions;
/// Public values generated for one Metaplex Create fixture.
pub use self::metaplex_token_metadata_fixture::MetaplexCreateFixturePreparationSummary;
/// Creates or reuses a classic SPL mint and derives its Metaplex PDAs.
pub use self::metaplex_token_metadata_fixture::prepare_metaplex_create_fixture;
/// Complete request for one Devnet Associated Token Account execution.
pub use self::solana_ata_execution::DevnetSplAssociatedTokenAccountExecutionRequest;
/// Complete result of one Devnet Associated Token Account execution.
@@ -70,7 +77,7 @@ pub use self::solana_memo_execution::DevnetMemoExecutionSummary;
pub use self::solana_memo_execution::execute_devnet_memo;
/// Complete request for one real Devnet Metaplex Token Metadata execution.
pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionRequest;
/// Complete evidence produced by one real Devnet Metaplex Token Metadata execution.
/// Complete evidence produced by one real Devnet Metaplex execution.
pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionSummary;
/// Creates one named creator unverification request without handwritten JSON.
pub use self::solana_metaplex_token_metadata_devnet_execution::devnet_metaplex_creator_unverify_request;
@@ -86,6 +93,10 @@ pub use self::solana_metaplex_token_metadata_devnet_execution::metaplex_token_me
pub use self::solana_metaplex_token_metadata_devnet_execution::simulate_devnet_metaplex_token_metadata;
/// Stable asset family covered by one Metaplex scenario.
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataAssetFamily;
/// Stable fixture graph required by one Metaplex scenario journey.
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataFixtureKind;
/// Stable lifecycle state consumed or produced by one Metaplex scenario journey.
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataFixtureState;
/// One stable Metaplex scenario reusable by automated tests and demo adapters.
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataScenario;
/// Execution mode accepted by one Metaplex scenario.

View File

@@ -0,0 +1,703 @@
// file: kb-pipeline-demo-scenarios/src/metaplex_token_metadata_fixture.rs
// version: 8
//! Native Metaplex Token Metadata fixture preparation for Devnet demos.
/// Options used to create or reuse one 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,
}
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,
};
}
}
/// 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,
/// Classic SPL mint address.
pub mint: std::string::String,
/// Canonical Metaplex metadata PDA.
pub metadata: std::string::String,
/// Canonical Metaplex master-edition PDA.
pub master_edition: std::string::String,
/// Operator public key used as mint and update authority.
pub authority: std::string::String,
/// Ready-to-edit typed Create operation JSON.
pub operation_json: std::string::String,
/// Whether the mint account was created during this call.
pub mint_created: bool,
/// Confirmed signature of native mint preparation when newly created.
pub preparation_signature: std::option::Option<std::string::String>,
}
/// Creates or reuses a zero-decimal classic SPL mint and derives its Metaplex PDAs.
pub async fn prepare_metaplex_create_fixture(
http_pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
) -> kb_core::Result<crate::MetaplexCreateFixturePreparationSummary> {
if options.query_role.trim().is_empty() || options.transaction_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex fixture endpoint roles must not be empty",
));
}
if !profile.wallet.devnet_send_enabled {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex fixture preparation requires devnet_send_enabled=true",
));
}
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_dir = options.wallet_dir.join("metaplex_token_metadata_validation");
let fixture_store = match kb_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_millis(),
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"unable to create a unique Metaplex fixture identifier: {error}"
)));
},
};
let fixture_alias =
match kb_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.load_or_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 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),
};
let preparation_signature = if let std::option::Option::Some(account) = existing {
if let std::result::Result::Err(error) =
validate_classic_nft_mint(&account, authority.as_str())
{
return std::result::Result::Err(error);
}
std::option::Option::None
} else {
let signature = match create_native_classic_nft_mint(
http_pool,
profile,
options,
&operator,
&mint_wallet,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let created =
match read_mint_account(http_pool, options.query_role.as_str(), mint.as_str()).await {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_fixture_mint_missing_after_confirmation",
"native Metaplex fixture mint is unavailable after confirmation",
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) =
validate_classic_nft_mint(&created, authority.as_str())
{
return std::result::Result::Err(error);
}
std::option::Option::Some(signature)
};
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(kb_core::Error::config(format!(
"invalid generated mint public key: {error}"
)));
},
};
let program_id = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_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 operation = create_operation_json(
mint_pubkey.to_string(),
metadata.to_string(),
master_edition.to_string(),
authority.clone(),
);
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(kb_core::Error::new(
"metaplex_fixture_serialization_failed",
format!("unable to serialize Metaplex Create fixture: {error}"),
));
},
};
return std::result::Result::Ok(crate::MetaplexCreateFixturePreparationSummary {
mint_keypair_path,
mint: mint_pubkey.to_string(),
metadata: metadata.to_string(),
master_edition: master_edition.to_string(),
authority,
operation_json,
mint_created: preparation_signature.is_some(),
preparation_signature,
});
}
async fn create_native_classic_nft_mint(
http_pool: &kb_onchain_transport::HttpEndpointPool,
profile: &kb_config::ProfileConfig,
options: &crate::MetaplexCreateFixturePreparationOptions,
operator: &kb_wallet::TemporaryWallet,
mint_wallet: &kb_wallet::TemporaryWallet,
) -> kb_core::Result<std::string::String> {
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,
&kb_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let payer = kb_lib::MdPubkey(operator.public_key());
let mint = kb_lib::MdPubkey(mint_wallet.public_key());
let policy =
fixture_policy(profile, payer.clone(), mint.clone(), rent.minimum_balance_lamports);
let system_intent = kb_lib::ExSolanaCoreExecutionIntent {
intent_id: format!("metaplex-fixture-create-account-{}", mint.0),
fee_payer: payer.clone(),
policy: policy.clone(),
operation: kb_lib::ExSolanaCoreOperation::SystemCreateAccount {
from: payer.clone(),
new_account: mint.clone(),
lamports: rent.minimum_balance_lamports,
space: MINT_SPACE,
owner: kb_lib::MdPubkey(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
},
};
let system_plan = match kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
&kb_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(
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_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(kb_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(kb_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),
0,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_fixture_initialize_mint_build_failed",
error.to_string(),
));
},
};
let plan = merge_fixture_instruction(
system_plan,
planned_instruction("spl.token.initialize_mint2", &token_instruction),
policy,
rent.minimum_balance_lamports,
);
let plan_evaluation = match kb_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 == kb_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(kb_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(),
&kb_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 kb_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(),
&kb_onchain_transport::GetFeeForMessageConfig::new(
kb_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 kb_onchain_transport::SimulateTransactionConfig::new(
kb_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(
kb_lib::ExApiExecutionCluster::Devnet,
kb_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(kb_core::Error::new(
"metaplex_fixture_simulation_failed",
crate::simulation_failure_message(&simulation),
));
}
let send_evaluation = match kb_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 == kb_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(kb_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 kb_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 kb_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(),
kb_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,
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
) {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_fixture_confirmation_failed",
format!("native mint preparation stopped at {:?}", confirmation.status),
));
}
tracing::info!(
target: "kb-pipeline-demo-scenarios.metaplex_fixture",
action = "prepare_native_metaplex_mint",
mint = mint_wallet.public_key(),
authority = operator.public_key(),
signature = signature.0.as_str(),
"created and initialized native classic SPL mint for Metaplex fixture"
);
return std::result::Result::Ok(signature.0);
}
fn planned_instruction(
operation_code: &str,
instruction: &solana_instruction::Instruction,
) -> kb_lib::ExApiPlannedInstruction {
return kb_lib::ExApiPlannedInstruction {
program_id: kb_lib::MdProgramId(instruction.program_id.to_string()),
operation_code: operation_code.to_string(),
accounts: instruction
.accounts
.iter()
.map(|account| {
return kb_lib::ExApiPlannedAccount {
pubkey: kb_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: &kb_config::ProfileConfig,
payer: kb_lib::MdPubkey,
mint: kb_lib::MdPubkey,
_rent_lamports: u64,
) -> kb_lib::ExApiExecutionPolicy {
return kb_lib::ExApiExecutionPolicy {
cluster: kb_lib::ExApiExecutionClusterPolicy {
expected_cluster: kb_lib::ExApiExecutionCluster::Devnet,
allow_mainnet: false,
mainnet_confirmation: false,
},
simulation: kb_lib::ExApiExecutionSimulationPolicy::Required,
blockhash: kb_lib::ExApiExecutionBlockhashPolicy {
kind: kb_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: kb_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: kb_lib::ExApiPostExecutionValidationPolicy {
canonical_insert_required: false,
core_extraction_required: false,
decode_replay_required: false,
materialization_required: false,
},
};
}
fn merge_fixture_instruction(
mut system_plan: kb_lib::ExApiPreparedExecutionPlan,
token_instruction: kb_lib::ExApiPlannedInstruction,
policy: kb_lib::ExApiExecutionPolicy,
rent_lamports: u64,
) -> kb_lib::ExApiPreparedExecutionPlan {
system_plan.instructions.push(token_instruction);
system_plan.executor_name = "kb-pipeline-demo-scenarios.metaplex_fixture".to_string();
system_plan.operation_code = "metadata.metaplex_token_metadata.prepare_native_mint".to_string();
system_plan.policy = policy;
system_plan.requested_spend_lamports = rent_lamports;
return system_plan;
}
async fn read_mint_account(
http_pool: &kb_onchain_transport::HttpEndpointPool,
query_role: &str,
mint: &str,
) -> kb_core::Result<std::option::Option<kb_onchain_transport::AccountInfoValue>> {
let config = match kb_onchain_transport::GetAccountInfoConfig::confirmed_with_data(82) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint_pubkey = kb_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_nft_mint(
account: &kb_onchain_transport::AccountInfoValue,
expected_authority: &str,
) -> kb_core::Result<()> {
if account.owner.0 != kb_program_ids::SPL_TOKEN_PROGRAM_ID || account.executable {
return std::result::Result::Err(kb_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(kb_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_bytes = match <[u8; 8]>::try_from(&bytes[36..44]) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(kb_core::Error::config(
"invalid fixture mint supply bytes",
));
},
};
let supply = u64::from_le_bytes(supply_bytes);
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 != 0 || supply != 0 || !initialized {
return std::result::Result::Err(kb_core::Error::config(format!(
"Metaplex NFT fixture mint requires initialized=true, decimals=0 and supply=0, 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(kb_core::Error::config(format!(
"Metaplex NFT 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,
) -> kb_core::Result<std::option::Option<std::string::String>> {
if bytes.len() != 36 {
return std::result::Result::Err(kb_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(kb_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(kb_core::Error::config(format!(
"invalid {label} public key bytes"
)));
},
};
return std::result::Result::Ok(std::option::Option::Some(key.to_string()));
}
fn create_operation_json(
mint: std::string::String,
metadata: std::string::String,
master_edition: std::string::String,
authority: std::string::String,
) -> serde_json::Value {
return serde_json::json!({
"operation": "create",
"metadata": metadata,
"master_edition": master_edition,
"mint": mint,
"mint_as_signer": false,
"authority": authority,
"update_authority": authority,
"update_authority_as_signer": true,
"spl_token_program": kb_program_ids::SPL_TOKEN_PROGRAM_ID,
"create_args": {"V1": {
"name": "Khadhroony Devnet Asset",
"symbol": "KHDEV",
"uri": "https://example.invalid/khadhroony-devnet.json",
"seller_fee_basis_points": 0,
"creators": null,
"primary_sale_happened": false,
"is_mutable": true,
"token_standard": "NonFungible",
"collection": null,
"uses": null,
"collection_details": null,
"rule_set": null,
"decimals": null,
"print_supply": "Zero"
}}
});
}
#[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 create_contract_uses_classic_token_program_and_zero_print_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(),
);
assert_eq!(
operation.get("spl_token_program").and_then(serde_json::Value::as_str),
std::option::Option::Some(kb_program_ids::SPL_TOKEN_PROGRAM_ID),
);
assert_eq!(
operation
.pointer("/create_args/V1/print_supply")
.and_then(serde_json::Value::as_str),
std::option::Option::Some("Zero"),
);
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_devnet_execution.rs
// version: 4
// version: 6
//! Real Devnet simulation and submission for current Metaplex Token Metadata operations.
@@ -22,6 +22,8 @@ pub struct DevnetMetaplexTokenMetadataExecutionRequest {
pub submit: bool,
/// Explicit operator confirmation required for submission.
pub operator_confirmed: bool,
/// Materializes canonical account snapshots after confirmed submission.
pub materialize_after_confirmation: bool,
}
impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
@@ -39,6 +41,7 @@ impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
postcondition_reads: std::vec::Vec::new(),
submit: false,
operator_confirmed: false,
materialize_after_confirmation: false,
};
}
@@ -96,6 +99,12 @@ impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
"Devnet Metaplex execution stateful read limit exceeded",
));
}
if self.submit && self.materialize_after_confirmation && self.postcondition_reads.is_empty()
{
return std::result::Result::Err(kb_core::Error::config(
"Metaplex materialization requires at least one postcondition account read",
));
}
let operation_code = self.operation.operation_code();
if kb_lib::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES.contains(&operation_code) {
return std::result::Result::Err(kb_core::Error::new(
@@ -149,7 +158,7 @@ pub fn metaplex_token_metadata_current_operation_json_template(
"authority": "<MINT_AUTHORITY>",
"update_authority": "<UPDATE_AUTHORITY>",
"update_authority_as_signer": true,
"spl_token_program": null,
"spl_token_program": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"create_args": {
"V1": {
"name": "Khadhroony Devnet Asset",
@@ -165,7 +174,7 @@ pub fn metaplex_token_metadata_current_operation_json_template(
"collection_details": null,
"rule_set": null,
"decimals": null,
"print_supply": null
"print_supply": "Zero"
}
}
}),
@@ -298,6 +307,10 @@ pub struct DevnetMetaplexTokenMetadataExecutionSummary {
pub confirmation: std::option::Option<kb_lib::ExApiExecutionConfirmationResult>,
/// Stateful snapshots observed after confirmed submission.
pub after: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadResult>,
/// Whether canonical account materialization was requested.
pub materialization_requested: bool,
/// Canonical projections emitted from confirmed account snapshots.
pub materialized_snapshots: std::vec::Vec<serde_json::Value>,
}
struct PreparedMetaplexExecution {
@@ -440,6 +453,10 @@ where
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if request.materialize_after_confirmation {
summary.materialized_snapshots =
materialize_confirmed_snapshots(summary.after.as_slice());
}
}
return std::result::Result::Ok(summary);
}
@@ -625,7 +642,16 @@ where
},
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
std::result::Result::Err(error) => {
let simulation_json = match serde_json::to_string_pretty(&simulation) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => "<simulation serialization unavailable>".to_string(),
};
return std::result::Result::Err(kb_core::Error::new(
error.code(),
format!("{}; simulation={simulation_json}", error.message()),
));
},
};
let evidence = unsigned.bind_simulation(simulation.clone());
return std::result::Result::Ok(PreparedMetaplexExecution {
@@ -648,10 +674,34 @@ where
send_result: std::option::Option::None,
confirmation: std::option::Option::None,
after: std::vec::Vec::new(),
materialization_requested: request.materialize_after_confirmation,
materialized_snapshots: std::vec::Vec::new(),
},
});
}
fn materialize_confirmed_snapshots(
snapshots: &[kb_pipeline::MetaplexTokenMetadataStatefulReadResult],
) -> std::vec::Vec<serde_json::Value> {
return snapshots
.iter()
.map(|result| {
return serde_json::json!({
"projectionVersion": 1,
"domain": "metaplex_token_metadata",
"projectionSemantics": "confirmed_bounded_account_snapshot",
"account": &result.snapshot.account,
"accountKind": &result.snapshot.account_kind,
"mint": &result.snapshot.mint,
"slot": result.snapshot.slot.to_string(),
"commitment": &result.commitment,
"state": &result.snapshot.payload_json,
"externalUriFetched": false
});
})
.collect();
}
async fn read_snapshots(
http_pool: &kb_onchain_transport::HttpEndpointPool,
requests: &[kb_pipeline::MetaplexTokenMetadataStatefulReadRequest],
@@ -709,7 +759,7 @@ fn build_plan(
canonical_insert_required: request.submit,
core_extraction_required: request.submit,
decode_replay_required: request.submit,
materialization_required: request.submit,
materialization_required: request.submit && request.materialize_after_confirmation,
},
},
allow_deprecated_operation: false,
@@ -850,7 +900,24 @@ mod tests {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert!(serde_json::from_str::<serde_json::Value>(template.as_str()).is_ok());
let parsed = serde_json::from_str::<serde_json::Value>(template.as_str());
assert!(parsed.is_ok());
if operation == "create" {
let parsed = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(
parsed
.pointer("/create_args/V1/print_supply")
.and_then(serde_json::Value::as_str),
std::option::Option::Some("Zero"),
);
assert_eq!(
parsed.get("spl_token_program").and_then(serde_json::Value::as_str),
std::option::Option::Some("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",),
);
}
}
let placeholder = crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
"placeholder",

View File

@@ -1,7 +1,7 @@
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_scenarios.rs
// version: 2
// version: 4
//! Deterministic Metaplex Token Metadata scenario inventory for tests and Devnet campaigns.
//! Coherent Metaplex Token Metadata scenario journeys for tests and Devnet campaigns.
/// Stable asset family covered by one Metaplex scenario.
#[derive(
@@ -15,12 +15,52 @@ pub enum MetaplexTokenMetadataAssetFamily {
Sft,
/// Fungible token with Metaplex metadata.
Fungible,
/// Collection metadata and verification flow.
/// Collection parent and member relationship.
Collection,
/// Programmable non-fungible token.
ProgrammableNft,
}
/// Stable fixture graph required by one scenario journey.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MetaplexTokenMetadataFixtureKind {
/// Fresh zero-decimal classic SPL mint with profile-owned authorities.
ClassicNftMint,
/// Fresh SFT mint and token account.
SemiFungibleMint,
/// Fresh fungible mint and token account.
FungibleMint,
/// Collection parent plus one unverified member.
CollectionParentAndMember,
/// Programmable NFT with token record and optional rule set.
ProgrammableNftTokenRecord,
}
/// Stable lifecycle state consumed or produced by one scenario journey.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MetaplexTokenMetadataFixtureState {
/// Mint exists but metadata does not.
MintPrepared,
/// Metadata exists and remains mutable.
MetadataCreated,
/// Master edition exists.
MasterEditionCreated,
/// Collection member exists but is not verified.
CollectionMemberUnverified,
/// Collection member is verified.
CollectionMemberVerified,
/// Programmable token record exists.
ProgrammableTokenRecordCreated,
/// Programmable asset is delegated.
Delegated,
/// Programmable asset is locked.
Locked,
/// Scenario has reached its terminal validated state.
Validated,
}
/// Execution mode accepted by one scenario.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
@@ -33,13 +73,21 @@ pub enum MetaplexTokenMetadataScenarioMode {
NetworkSubmission,
}
/// One stable Metaplex scenario reusable by automated tests and demo adapters.
/// One stable Metaplex journey reusable by automated tests and demo adapters.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MetaplexTokenMetadataScenario {
/// Stable scenario identifier.
pub id: std::string::String,
/// Operator-visible scenario label.
pub label: std::string::String,
/// Asset family exercised by the scenario.
pub asset_family: crate::MetaplexTokenMetadataAssetFamily,
/// Fixture graph required by the scenario.
pub fixture_kind: crate::MetaplexTokenMetadataFixtureKind,
/// Required initial lifecycle state.
pub initial_state: crate::MetaplexTokenMetadataFixtureState,
/// Expected terminal lifecycle state.
pub resulting_state: crate::MetaplexTokenMetadataFixtureState,
/// Execution mode.
pub mode: crate::MetaplexTokenMetadataScenarioMode,
/// Stable operation codes executed in order.
@@ -50,93 +98,155 @@ pub struct MetaplexTokenMetadataScenario {
pub requires_programmable_rules: bool,
/// Whether a confirmed postcondition is mandatory.
pub requires_postcondition: bool,
/// Canonical account projections expected when materialization is enabled.
pub materialization_targets: std::vec::Vec<std::string::String>,
}
/// Returns the complete ordered synthetic Metaplex scenario inventory.
fn scenario(
id: &str,
label: &str,
asset_family: crate::MetaplexTokenMetadataAssetFamily,
fixture_kind: crate::MetaplexTokenMetadataFixtureKind,
initial_state: crate::MetaplexTokenMetadataFixtureState,
resulting_state: crate::MetaplexTokenMetadataFixtureState,
operation_codes: std::vec::Vec<std::string::String>,
requires_collection: bool,
requires_programmable_rules: bool,
materialization_targets: &[&str],
) -> crate::MetaplexTokenMetadataScenario {
return crate::MetaplexTokenMetadataScenario {
id: id.to_string(),
label: label.to_string(),
asset_family,
fixture_kind,
initial_state,
resulting_state,
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
operation_codes,
requires_collection,
requires_programmable_rules,
requires_postcondition: true,
materialization_targets: materialization_targets
.iter()
.map(|value| return (*value).to_string())
.collect(),
};
}
/// Returns the complete ordered synthetic Metaplex journey inventory.
pub fn metaplex_token_metadata_synthetic_scenarios()
-> std::vec::Vec<crate::MetaplexTokenMetadataScenario> {
return vec![
crate::MetaplexTokenMetadataScenario {
id: "metaplex_nft_create_update".to_string(),
asset_family: crate::MetaplexTokenMetadataAssetFamily::Nft,
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
operation_codes: vec![
scenario(
"metaplex_nft_create_update",
"NFT classique : création, mise à jour et édition maître",
crate::MetaplexTokenMetadataAssetFamily::Nft,
crate::MetaplexTokenMetadataFixtureKind::ClassicNftMint,
crate::MetaplexTokenMetadataFixtureState::MintPrepared,
crate::MetaplexTokenMetadataFixtureState::MasterEditionCreated,
vec![
kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION.to_string(),
],
requires_collection: false,
requires_programmable_rules: false,
requires_postcondition: true,
},
crate::MetaplexTokenMetadataScenario {
id: "metaplex_sft_create_mint".to_string(),
asset_family: crate::MetaplexTokenMetadataAssetFamily::Sft,
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
operation_codes: vec![
false,
false,
&["metadata_state", "edition_state"],
),
scenario(
"metaplex_sft_create_mint",
"SFT : création des metadata puis émission de supply",
crate::MetaplexTokenMetadataAssetFamily::Sft,
crate::MetaplexTokenMetadataFixtureKind::SemiFungibleMint,
crate::MetaplexTokenMetadataFixtureState::MintPrepared,
crate::MetaplexTokenMetadataFixtureState::Validated,
vec![
kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_MINT_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
],
requires_collection: false,
requires_programmable_rules: false,
requires_postcondition: true,
},
crate::MetaplexTokenMetadataScenario {
id: "metaplex_fungible_create_update".to_string(),
asset_family: crate::MetaplexTokenMetadataAssetFamily::Fungible,
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
operation_codes: vec![
false,
false,
&["metadata_state"],
),
scenario(
"metaplex_fungible_create_update",
"Fungible : création et transitions dautorité",
crate::MetaplexTokenMetadataAssetFamily::Fungible,
crate::MetaplexTokenMetadataFixtureKind::FungibleMint,
crate::MetaplexTokenMetadataFixtureState::MintPrepared,
crate::MetaplexTokenMetadataFixtureState::Validated,
vec![
kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
],
requires_collection: false,
requires_programmable_rules: false,
requires_postcondition: true,
},
crate::MetaplexTokenMetadataScenario {
id: "metaplex_collection_verify_unverify".to_string(),
asset_family: crate::MetaplexTokenMetadataAssetFamily::Collection,
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
operation_codes: vec![
false,
false,
&["metadata_state"],
),
scenario(
"metaplex_collection_verify_unverify",
"Collection : membre non vérifié, vérification puis révocation",
crate::MetaplexTokenMetadataAssetFamily::Collection,
crate::MetaplexTokenMetadataFixtureKind::CollectionParentAndMember,
crate::MetaplexTokenMetadataFixtureState::CollectionMemberUnverified,
crate::MetaplexTokenMetadataFixtureState::CollectionMemberVerified,
vec![
kb_lib::EX_METAPLEX_TOKEN_METADATA_VERIFY_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_UNVERIFY_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_SET_COLLECTION_SIZE_OPERATION.to_string(),
],
requires_collection: true,
requires_programmable_rules: false,
requires_postcondition: true,
},
crate::MetaplexTokenMetadataScenario {
id: "metaplex_pnft_delegate_transfer_revoke".to_string(),
asset_family: crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
mode: crate::MetaplexTokenMetadataScenarioMode::Synthetic,
operation_codes: vec![
true,
false,
&["metadata_state", "edition_state"],
),
scenario(
"metaplex_pnft_delegate_transfer_revoke",
"pNFT : token record, délégation, verrouillage, transfert et révocation",
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
crate::MetaplexTokenMetadataFixtureKind::ProgrammableNftTokenRecord,
crate::MetaplexTokenMetadataFixtureState::ProgrammableTokenRecordCreated,
crate::MetaplexTokenMetadataFixtureState::Validated,
vec![
kb_lib::EX_METAPLEX_TOKEN_METADATA_DELEGATE_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_LOCK_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_UNLOCK_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_TRANSFER_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_REVOKE_OPERATION.to_string(),
],
requires_collection: false,
requires_programmable_rules: true,
requires_postcondition: true,
},
false,
true,
&["metadata_state", "programmable_token_state", "delegate_admin_state"],
),
];
}
/// Returns the ordered Metaplex scenarios intended for opt-in Devnet simulation.
/// Returns the ordered Metaplex journeys intended for opt-in Devnet simulation.
pub fn metaplex_token_metadata_devnet_scenarios()
-> std::vec::Vec<crate::MetaplexTokenMetadataScenario> {
return crate::metaplex_token_metadata_synthetic_scenarios()
.into_iter()
.map(|mut scenario| {
scenario.id = format!("{}_devnet_simulation", scenario.id);
scenario.mode = crate::MetaplexTokenMetadataScenarioMode::NetworkSimulation;
return scenario;
})
.collect::<std::vec::Vec<crate::MetaplexTokenMetadataScenario>>();
let mut scenario = scenario(
"metaplex_nft_create_update_devnet",
"NFT classique : création confirmée puis mise à jour",
crate::MetaplexTokenMetadataAssetFamily::Nft,
crate::MetaplexTokenMetadataFixtureKind::ClassicNftMint,
crate::MetaplexTokenMetadataFixtureState::MintPrepared,
crate::MetaplexTokenMetadataFixtureState::MetadataCreated,
vec![
kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
],
false,
false,
&["metadata_state", "edition_state"],
);
scenario.mode = crate::MetaplexTokenMetadataScenarioMode::NetworkSimulation;
return vec![scenario];
}
#[cfg(test)]
mod tests {
#[test]
fn synthetic_inventory_is_exact_unique_and_covers_every_required_asset_family() {
fn inventory_is_exact_unique_and_uses_coherent_fixture_graphs() {
let scenarios = crate::metaplex_token_metadata_synthetic_scenarios();
assert_eq!(scenarios.len(), 5);
let ids = scenarios
@@ -144,56 +254,43 @@ mod tests {
.map(|scenario| return scenario.id.as_str())
.collect::<std::collections::BTreeSet<&str>>();
assert_eq!(ids.len(), scenarios.len());
let families = scenarios
.iter()
.map(|scenario| return scenario.asset_family)
.collect::<std::collections::BTreeSet<crate::MetaplexTokenMetadataAssetFamily>>(
);
assert_eq!(families.len(), 5);
}
#[test]
fn synthetic_scenarios_are_simulation_safe_and_require_postconditions() {
for scenario in crate::metaplex_token_metadata_synthetic_scenarios() {
assert_eq!(scenario.mode, crate::MetaplexTokenMetadataScenarioMode::Synthetic);
for scenario in scenarios {
assert!(!scenario.label.trim().is_empty());
assert!(!scenario.operation_codes.is_empty());
assert!(!scenario.materialization_targets.is_empty());
assert!(scenario.requires_postcondition);
}
}
#[test]
fn devnet_inventory_mirrors_every_asset_family_as_network_simulation() {
let synthetic = crate::metaplex_token_metadata_synthetic_scenarios();
let devnet = crate::metaplex_token_metadata_devnet_scenarios();
assert_eq!(devnet.len(), synthetic.len());
assert!(devnet.iter().all(|scenario| return scenario.mode
== crate::MetaplexTokenMetadataScenarioMode::NetworkSimulation));
assert!(devnet.iter().all(|scenario| return scenario.id.ends_with("_devnet_simulation")));
let families = devnet
.iter()
.map(|scenario| return scenario.asset_family)
.collect::<std::collections::BTreeSet<crate::MetaplexTokenMetadataAssetFamily>>(
);
assert_eq!(families.len(), 5);
fn collection_and_programmable_journeys_declare_special_requirements() {
let scenarios = crate::metaplex_token_metadata_synthetic_scenarios();
let collection = scenarios.iter().find(|scenario| {
return scenario.asset_family == crate::MetaplexTokenMetadataAssetFamily::Collection;
});
assert!(collection.is_some_and(|scenario| return scenario.requires_collection));
let programmable = scenarios.iter().find(|scenario| {
return scenario.asset_family
== crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft;
});
assert!(programmable.is_some_and(|scenario| return scenario.requires_programmable_rules));
}
#[test]
fn programmable_and_collection_requirements_are_declared_only_where_needed() {
let scenarios = crate::metaplex_token_metadata_synthetic_scenarios();
let programmable = scenarios
.iter()
.filter(|scenario| return scenario.requires_programmable_rules)
.collect::<std::vec::Vec<&crate::MetaplexTokenMetadataScenario>>();
assert_eq!(programmable.len(), 1);
fn devnet_inventory_exposes_only_fully_preparable_journeys() {
let devnet = crate::metaplex_token_metadata_devnet_scenarios();
assert_eq!(devnet.len(), 1);
let scenario = &devnet[0];
assert_eq!(scenario.asset_family, crate::MetaplexTokenMetadataAssetFamily::Nft);
assert_eq!(scenario.operation_codes.len(), 2);
assert_eq!(
programmable[0].asset_family,
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft
scenario.operation_codes[0],
kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION
);
let collection = scenarios
.iter()
.filter(|scenario| return scenario.requires_collection)
.collect::<std::vec::Vec<&crate::MetaplexTokenMetadataScenario>>();
assert_eq!(collection.len(), 1);
assert_eq!(collection[0].asset_family, crate::MetaplexTokenMetadataAssetFamily::Collection);
assert_eq!(
scenario.operation_codes[1],
kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION
);
assert_eq!(scenario.mode, crate::MetaplexTokenMetadataScenarioMode::NetworkSimulation);
}
}