297 lines
12 KiB
Rust
297 lines
12 KiB
Rust
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_scenarios.rs
|
||
// version: 4
|
||
|
||
//! Coherent Metaplex Token Metadata scenario journeys for tests and Devnet campaigns.
|
||
|
||
/// Stable asset family covered by one Metaplex scenario.
|
||
#[derive(
|
||
Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize,
|
||
)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum MetaplexTokenMetadataAssetFamily {
|
||
/// Non-fungible token.
|
||
Nft,
|
||
/// Semi-fungible token.
|
||
Sft,
|
||
/// Fungible token with Metaplex metadata.
|
||
Fungible,
|
||
/// 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")]
|
||
pub enum MetaplexTokenMetadataScenarioMode {
|
||
/// Purely deterministic scenario using synthetic accounts and plans.
|
||
Synthetic,
|
||
/// Devnet or Testnet simulation without submission.
|
||
NetworkSimulation,
|
||
/// Explicitly authorized Devnet or Testnet submission.
|
||
NetworkSubmission,
|
||
}
|
||
|
||
/// 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.
|
||
pub operation_codes: std::vec::Vec<std::string::String>,
|
||
/// Whether the scenario requires a collection account.
|
||
pub requires_collection: bool,
|
||
/// Whether the scenario requires programmable authorization rules.
|
||
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>,
|
||
}
|
||
|
||
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![
|
||
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(),
|
||
],
|
||
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(),
|
||
],
|
||
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(),
|
||
],
|
||
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(),
|
||
],
|
||
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(),
|
||
],
|
||
false,
|
||
true,
|
||
&["metadata_state", "programmable_token_state", "delegate_admin_state"],
|
||
),
|
||
];
|
||
}
|
||
|
||
/// Returns the ordered Metaplex journeys intended for opt-in Devnet simulation.
|
||
pub fn metaplex_token_metadata_devnet_scenarios()
|
||
-> 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 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
|
||
.iter()
|
||
.map(|scenario| return scenario.id.as_str())
|
||
.collect::<std::collections::BTreeSet<&str>>();
|
||
assert_eq!(ids.len(), scenarios.len());
|
||
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 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 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!(
|
||
scenario.operation_codes[0],
|
||
kb_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION
|
||
);
|
||
assert_eq!(
|
||
scenario.operation_codes[1],
|
||
kb_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION
|
||
);
|
||
assert_eq!(scenario.mode, crate::MetaplexTokenMetadataScenarioMode::NetworkSimulation);
|
||
}
|
||
}
|