v0.4.7-pre.013
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 d’autorité",
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user