0.5.1-pre.002

This commit is contained in:
2026-08-09 19:34:08 +02:00
parent 816eee59a9
commit 6a680767ae
767 changed files with 12257 additions and 12195 deletions

View File

@@ -0,0 +1,152 @@
// file: ks-pipeline-demo-scenarios/src/bin/ks_pipeline_demo_scenarios_cli.rs
// version: 2
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
//! Command-line entry point for reusable Devnet fixture preparation.
macro_rules! ks_try {
($expression:expr) => {
match $expression {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
};
}
#[tokio::main]
async fn main() {
let result = run().await;
if let std::result::Result::Err(error) = result {
eprintln!("{}: {}", error.code(), error.message());
std::process::exit(1);
}
}
async fn run() -> ks_core::Result<()> {
let arguments = std::env::args().skip(1).collect::<std::vec::Vec<_>>();
let command = match arguments.first() {
std::option::Option::Some(value) => value.as_str(),
std::option::Option::None => return usage_error("missing command"),
};
if command != "prepare-token-2022-fixture" {
return usage_error(format!("unknown command {command}").as_str());
}
let options = ks_try!(parse_prepare_options(arguments.as_slice()));
let summary = ks_try!(ks_pipeline_demo_scenarios::prepare_token_2022_fixture(&options).await);
let output = match serde_json::to_string_pretty(&summary) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::json(format!(
"unable to serialize Token-2022 fixture summary: {error}"
)));
},
};
println!("{output}");
return std::result::Result::Ok(());
}
fn parse_prepare_options(
arguments: &[std::string::String],
) -> ks_core::Result<ks_pipeline_demo_scenarios::Token2022FixturePreparationOptions> {
let mut rpc_url = std::option::Option::None;
let mut wallet_path = std::option::Option::None;
let mut wallet_dir = std::option::Option::None;
let mut decimals = 9_u8;
let mut index = 1_usize;
while index < arguments.len() {
let name = arguments[index].as_str();
let value = match arguments.get(index + 1) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return usage_error(format!("missing value for {name}").as_str());
},
};
match name {
"--rpc-url" => rpc_url = std::option::Option::Some(value.clone()),
"--wallet" => wallet_path = std::option::Option::Some(std::path::PathBuf::from(value)),
"--wallet-dir" => {
wallet_dir = std::option::Option::Some(std::path::PathBuf::from(value))
},
"--decimals" => {
decimals = match value.parse::<u8>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(format!(
"invalid --decimals value {value}: {error}"
)));
},
}
},
_ => return usage_error(format!("unknown option {name}").as_str()),
}
index += 2;
}
let rpc_url = match rpc_url.or_else(|| return std::env::var("KB_DEVNET_RPC_URL").ok()) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::config(
"missing --rpc-url or KB_DEVNET_RPC_URL",
));
},
};
let wallet_path = match wallet_path
.or_else(|| return std::env::var("KB_DEVNET_WALLET").ok().map(std::path::PathBuf::from))
{
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::config(
"missing --wallet or KB_DEVNET_WALLET",
));
},
};
let wallet_dir = match wallet_dir {
std::option::Option::Some(value) => value,
std::option::Option::None => match wallet_path.parent() {
std::option::Option::Some(value) => std::path::Path::to_path_buf(value),
std::option::Option::None => std::path::PathBuf::from("."),
},
};
return std::result::Result::Ok(
ks_pipeline_demo_scenarios::Token2022FixturePreparationOptions {
rpc_url,
wallet_path,
wallet_dir,
decimals,
},
);
}
fn usage_error<T>(message: &str) -> ks_core::Result<T> {
return std::result::Result::Err(ks_core::Error::config(format!(
"{message}. Usage: cargo run -p ks-pipeline-demo-scenarios --bin ks-pipeline-demo-scenarios-cli -- prepare-token-2022-fixture --rpc-url URL --wallet PATH [--wallet-dir PATH] [--decimals 9]"
)));
}
#[cfg(test)]
mod tests {
#[test]
fn prepare_options_accept_explicit_values() {
let arguments = vec![
"prepare-token-2022-fixture".to_string(),
"--rpc-url".to_string(),
"https://api.devnet.solana.com".to_string(),
"--wallet".to_string(),
"wallet.json".to_string(),
"--wallet-dir".to_string(),
"wallet-dir".to_string(),
"--decimals".to_string(),
"9".to_string(),
];
let options = super::parse_prepare_options(arguments.as_slice());
assert!(options.is_ok());
let options = match options {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(options.wallet_dir, std::path::PathBuf::from("wallet-dir"));
assert_eq!(options.decimals, 9);
}
}

View File

@@ -0,0 +1,10 @@
// file: ks-pipeline-demo-scenarios/src/constants.rs
// version: 4
//! Demo scenario constants.
/// Canonical tracing target for demo pipeline scenarios.
pub(crate) const TRACING_TARGET: &str = "ks-pipeline-demo-scenarios";
/// Total transactions reserved by one complete Solana Program Metadata campaign.
pub(crate) const SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT: u64 = 11;

View File

@@ -0,0 +1,184 @@
// file: ks-pipeline-demo-scenarios/src/environment.rs
// version: 3
//! Environment initialization for opt-in demonstration scenarios.
/// Loads the workspace `.env` selected by `ks-config`.
pub fn initialize_demo_scenario_environment(
workspace_root: &std::path::Path,
) -> ks_core::Result<ks_config::EnvironmentLoadReport> {
return ks_config::load_workspace_environment(workspace_root);
}
/// Resolves one compatible Devnet profile for demo scenarios.
///
/// An explicit name has priority, followed by `KB_DEVNET_PROFILE`, then the
/// first compatible Devnet profile declared by the configuration.
pub fn resolve_demo_devnet_profile(
config: &ks_config::AppConfig,
requested_profile_name: std::option::Option<&str>,
) -> ks_core::Result<ks_config::ProfileConfig> {
let explicit = requested_profile_name
.map(str::trim)
.filter(|value| return !value.is_empty())
.map(std::string::ToString::to_string);
let selected_name = match explicit {
std::option::Option::Some(value) => std::option::Option::Some(value),
std::option::Option::None => std::env::var("KB_DEVNET_PROFILE")
.ok()
.map(|value| return value.trim().to_string())
.filter(|value| return !value.is_empty()),
};
for profile in &config.profiles {
let compatible = profile.wallet.cluster == "devnet"
&& profile.wallet.temporary_wallet_enabled
&& profile.wallet.temporary_wallet_persist;
if !compatible {
continue;
}
if let std::option::Option::Some(name) = selected_name.as_ref()
&& profile.name != *name
{
continue;
}
return std::result::Result::Ok(profile.clone());
}
return match selected_name {
std::option::Option::Some(name) => std::result::Result::Err(ks_core::Error::config(
format!("compatible Devnet profile '{name}' is unavailable"),
)),
std::option::Option::None => std::result::Result::Err(ks_core::Error::config(
"no compatible Devnet profile is configured",
)),
};
}
/// Readiness report for one Devnet profile PostgreSQL store.
#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DevnetProfileStoreReadiness {
/// Selected Devnet profile.
pub profile_name: std::string::String,
/// Whether schema creation was allowed by configuration.
pub auto_initialize_schema: bool,
/// Number of known tables already present before preparation.
pub existing_tables_before: usize,
/// Number of known tables created during preparation.
pub created_tables: usize,
/// Number of known tables present after preparation.
pub existing_tables_after: usize,
/// Total number of known tables expected by the current store.
pub expected_tables: usize,
}
/// Verifies or initializes the PostgreSQL schema selected by one Devnet profile.
pub async fn prepare_demo_devnet_profile_store(
profile: &ks_config::ProfileConfig,
) -> ks_core::Result<DevnetProfileStoreReadiness> {
if profile.wallet.cluster != "devnet" {
return std::result::Result::Err(ks_core::Error::config(format!(
"profile '{}' is not configured for Devnet",
profile.name
)));
}
if !profile.database.enabled {
return std::result::Result::Err(ks_core::Error::config(format!(
"Devnet profile '{}' has database storage disabled",
profile.name
)));
}
if profile.database.backend != "postgres" {
return std::result::Result::Err(ks_core::Error::config(format!(
"Devnet profile '{}' must use the PostgreSQL backend",
profile.name
)));
}
let options = match ks_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = match ks_store::PostgresStore::connect(options).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let before = match store.known_table_diagnostics().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let existing_tables_before = before.iter().filter(|table| return table.exists).count();
if profile.database.postgres.auto_initialize_schema
&& let std::result::Result::Err(error) = store.initialize_store_schema().await
{
return std::result::Result::Err(error);
}
let after = match store.known_table_diagnostics().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let existing_tables_after = after.iter().filter(|table| return table.exists).count();
if existing_tables_after != after.len() {
let missing = after
.iter()
.filter(|table| return !table.exists)
.map(|table| return table.table_name.clone())
.collect::<std::vec::Vec<_>>();
return std::result::Result::Err(ks_core::Error::config(format!(
"Devnet profile '{}' PostgreSQL schema is incomplete and auto initialization is {}: missing {}",
profile.name,
if profile.database.postgres.auto_initialize_schema {
"enabled"
} else {
"disabled"
},
missing.join(", ")
)));
}
return std::result::Result::Ok(DevnetProfileStoreReadiness {
profile_name: profile.name.clone(),
auto_initialize_schema: profile.database.postgres.auto_initialize_schema,
existing_tables_before,
created_tables: existing_tables_after.saturating_sub(existing_tables_before),
existing_tables_after,
expected_tables: after.len(),
});
}
#[cfg(test)]
mod tests {
fn example_config() -> ks_config::AppConfig {
return match ks_config::parse_config_json(include_str!("../../config/example.config.json"))
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
};
}
#[test]
fn resolver_finds_a_compatible_devnet_profile_without_a_fixed_name() {
let config = example_config();
let profile = match super::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),
};
assert_eq!(profile.wallet.cluster, "devnet");
assert!(profile.wallet.temporary_wallet_enabled);
assert!(profile.wallet.temporary_wallet_persist);
}
#[test]
fn resolver_rejects_an_explicit_non_devnet_profile() {
let config = example_config();
assert!(
super::resolve_demo_devnet_profile(
&config,
std::option::Option::Some("mainnet_research"),
)
.is_err()
);
}
}

View File

@@ -0,0 +1,378 @@
// file: ks-pipeline-demo-scenarios/src/lib.rs
// version: 28
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
//! Reusable demonstration and Devnet validation scenarios built on `ks-pipeline`.
mod constants;
mod environment;
mod metadata;
mod solana;
mod spl;
/// Readiness report for one Devnet profile PostgreSQL store.
pub use self::environment::DevnetProfileStoreReadiness;
/// Loads the workspace environment for opt-in demo and Devnet scenarios.
pub use self::environment::initialize_demo_scenario_environment;
/// Verifies or initializes the PostgreSQL schema selected by one Devnet profile.
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;
/// Complete evidence produced by one collection parent/member verification campaign.
pub use self::metadata::metaplex_token_metadata::collection_verify_campaign::DevnetMetaplexCollectionVerifyCampaignSummary;
/// Collection relation and sized-collection state observed across verification.
pub use self::metadata::metaplex_token_metadata::collection_verify_campaign::DevnetMetaplexCollectionVerifyState;
/// Executes one fresh collection parent/member verification campaign on Devnet.
pub use self::metadata::metaplex_token_metadata::collection_verify_campaign::execute_devnet_metaplex_collection_verify_campaign;
/// Returns the exact current operations qualified by the collection verification campaign.
pub use self::metadata::metaplex_token_metadata::collection_verify_campaign::metaplex_collection_verify_campaign_operation_names;
/// Complete evidence produced by one family-specific Metaplex `Create -> Mint` campaign.
pub use self::metadata::metaplex_token_metadata::create_mint_campaign::DevnetMetaplexCreateMintCampaignSummary;
/// Confirmed SPL state observed around the Metaplex `Mint` operation.
pub use self::metadata::metaplex_token_metadata::create_mint_campaign::DevnetMetaplexCreateMintTokenState;
/// Executes one fresh family-specific Metaplex `Create -> Mint` campaign on Devnet.
pub use self::metadata::metaplex_token_metadata::create_mint_campaign::execute_devnet_metaplex_create_mint_campaign;
/// Returns the exact ordered operations covered by the `Create -> Mint` campaign.
pub use self::metadata::metaplex_token_metadata::create_mint_campaign::metaplex_create_mint_campaign_operation_names;
/// Complete request for one real Devnet Metaplex Token Metadata execution.
pub use self::metadata::metaplex_token_metadata::devnet_execution::DevnetMetaplexTokenMetadataExecutionRequest;
/// Complete evidence produced by one real Devnet Metaplex execution.
pub use self::metadata::metaplex_token_metadata::devnet_execution::DevnetMetaplexTokenMetadataExecutionSummary;
/// Creates one named creator unverification request without handwritten JSON.
pub use self::metadata::metaplex_token_metadata::devnet_execution::devnet_metaplex_creator_unverify_request;
/// Creates one named creator verification request without handwritten JSON.
pub use self::metadata::metaplex_token_metadata::devnet_execution::devnet_metaplex_creator_verify_request;
/// Executes one real Devnet Metaplex simulation or explicitly authorized submission.
pub use self::metadata::metaplex_token_metadata::devnet_execution::execute_devnet_metaplex_token_metadata;
/// Executes one real Devnet Metaplex submission with explicitly authorized extra signers.
pub use self::metadata::metaplex_token_metadata::devnet_execution::execute_devnet_metaplex_token_metadata_with_signers;
/// Returns a formatted JSON template for one named current operation.
pub use self::metadata::metaplex_token_metadata::devnet_execution::metaplex_token_metadata_current_operation_json_template;
/// Returns the current operation names accepted by automatic Devnet campaigns.
pub use self::metadata::metaplex_token_metadata::devnet_execution::metaplex_token_metadata_current_operation_names;
/// Simulates one current Metaplex operation against a real Devnet endpoint.
pub use self::metadata::metaplex_token_metadata::devnet_execution::simulate_devnet_metaplex_token_metadata;
/// Exact SPL state transitions exercised around the Token Owned Escrow operations.
pub use self::metadata::metaplex_token_metadata::escrow_campaign::DevnetMetaplexEscrowCampaignState;
/// Complete evidence retained by one Token Owned Escrow Devnet campaign.
pub use self::metadata::metaplex_token_metadata::escrow_campaign::DevnetMetaplexEscrowCampaignSummary;
/// Executes a fresh Token Owned Escrow lifecycle on Devnet.
pub use self::metadata::metaplex_token_metadata::escrow_campaign::execute_devnet_metaplex_escrow_campaign;
/// Returns the exact ordered Metaplex operations qualified by the escrow campaign.
pub use self::metadata::metaplex_token_metadata::escrow_campaign::metaplex_escrow_campaign_operation_names;
/// Options for one Metaplex Create fixture preparation.
pub use self::metadata::metaplex_token_metadata::fixture::MetaplexCreateFixturePreparationOptions;
/// Public values generated for one Metaplex Create fixture.
pub use self::metadata::metaplex_token_metadata::fixture::MetaplexCreateFixturePreparationSummary;
/// Reads the validated classic SPL mint supply for an internal Metaplex campaign.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::classic_fixture_mint_supply;
/// Reads the validated classic SPL token-account state for an internal Metaplex campaign.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::classic_fixture_token_account_state;
/// Reads the validated classic SPL token-account amount for an internal Metaplex campaign.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::classic_fixture_token_amount;
/// Creates or reuses a classic SPL mint and derives its Metaplex PDAs.
pub use self::metadata::metaplex_token_metadata::fixture::prepare_metaplex_create_fixture;
/// Reads one classic SPL mint account at or after a minimum context slot.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::read_mint_account_at_or_after;
/// Reads one classic SPL token account at or after a minimum context slot.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::read_token_account_at_or_after;
/// Validates one classic SPL mint fixture against an exact expected supply.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::validate_classic_fixture_mint_state;
/// Validates one classic SPL token-account fixture against an exact expected amount.
pub(crate) use self::metadata::metaplex_token_metadata::fixture::validate_classic_fixture_token_account_state;
/// Complete evidence produced by the final Metaplex maintenance campaign.
pub use self::metadata::metaplex_token_metadata::maintenance_campaign::DevnetMetaplexMaintenanceCampaignSummary;
/// Executes the final bounded current maintenance campaign on Devnet.
pub use self::metadata::metaplex_token_metadata::maintenance_campaign::execute_devnet_metaplex_maintenance_campaign;
/// Returns the exact ordered operations exercised by the maintenance campaign.
pub use self::metadata::metaplex_token_metadata::maintenance_campaign::metaplex_maintenance_campaign_operation_names;
/// Complete evidence produced by one pNFT lifecycle campaign.
pub use self::metadata::metaplex_token_metadata::pnft_lifecycle_campaign::DevnetMetaplexPnftLifecycleCampaignSummary;
/// Exact programmable-token state observed across the pNFT lifecycle campaign.
pub use self::metadata::metaplex_token_metadata::pnft_lifecycle_campaign::DevnetMetaplexPnftLifecycleState;
/// Executes one fresh pNFT delegate/lock/unlock/revoke/transfer campaign on Devnet.
pub use self::metadata::metaplex_token_metadata::pnft_lifecycle_campaign::execute_devnet_metaplex_pnft_lifecycle_campaign;
/// Returns the exact ordered current operations exercised by the pNFT lifecycle campaign.
pub use self::metadata::metaplex_token_metadata::pnft_lifecycle_campaign::metaplex_pnft_lifecycle_campaign_operation_names;
/// Complete evidence produced by one printable master NFT `Print -> Burn` campaign.
pub use self::metadata::metaplex_token_metadata::print_burn_campaign::DevnetMetaplexPrintBurnCampaignSummary;
/// Exact printed-edition state transition observed around `Print` and `Burn`.
pub use self::metadata::metaplex_token_metadata::print_burn_campaign::DevnetMetaplexPrintBurnState;
/// Executes one printable master NFT `Print -> Burn` campaign on Devnet.
pub use self::metadata::metaplex_token_metadata::print_burn_campaign::execute_devnet_metaplex_print_burn_campaign;
/// Returns the exact ordered current operations qualified by the print/burn campaign.
pub use self::metadata::metaplex_token_metadata::print_burn_campaign::metaplex_print_burn_campaign_operation_names;
/// Stable asset family covered by one Metaplex scenario.
pub use self::metadata::metaplex_token_metadata::scenarios::MetaplexTokenMetadataAssetFamily;
/// Stable fixture graph required by one Metaplex scenario journey.
pub use self::metadata::metaplex_token_metadata::scenarios::MetaplexTokenMetadataFixtureKind;
/// Stable lifecycle state consumed or produced by one Metaplex scenario journey.
pub use self::metadata::metaplex_token_metadata::scenarios::MetaplexTokenMetadataFixtureState;
/// One stable Metaplex scenario reusable by automated tests and demo adapters.
pub use self::metadata::metaplex_token_metadata::scenarios::MetaplexTokenMetadataScenario;
/// Execution mode accepted by one Metaplex scenario.
pub use self::metadata::metaplex_token_metadata::scenarios::MetaplexTokenMetadataScenarioMode;
/// Returns the ordered Metaplex Devnet simulation scenario inventory.
pub use self::metadata::metaplex_token_metadata::scenarios::metaplex_token_metadata_devnet_scenarios;
/// Returns the complete ordered synthetic Metaplex scenario inventory.
pub use self::metadata::metaplex_token_metadata::scenarios::metaplex_token_metadata_synthetic_scenarios;
/// Runtime availability classification produced by one bounded current `Use` probe.
pub use self::metadata::metaplex_token_metadata::use_campaign::DevnetMetaplexUseProbeStatus;
/// Complete evidence produced by one bounded current `Use` probe.
pub use self::metadata::metaplex_token_metadata::use_campaign::DevnetMetaplexUseProbeSummary;
/// Executes one bounded current `Use` availability probe on Devnet.
pub use self::metadata::metaplex_token_metadata::use_campaign::execute_devnet_metaplex_use_probe;
/// Returns the exact setup and target operations exercised by the `Use` probe.
pub use self::metadata::metaplex_token_metadata::use_campaign::metaplex_use_probe_operation_names;
/// Maximum evidence entries accepted by one Metaplex validation scenario.
pub use self::metadata::metaplex_token_metadata::validation::MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE;
/// One case in the closed Metaplex cross-validation corpus.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataCrossValidationCase;
/// Closed Metaplex cross-validation corpus for `0.4.7-pre.010`.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataCrossValidationMatrix;
/// Family-specific evidence bundle for one current Metaplex Devnet operation.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataDevnetExecutionFamilyEvidence;
/// Closed inventory of every current Metaplex operation requiring Devnet coverage.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataDevnetExecutionMatrix;
/// One current Metaplex operation in the closed Devnet execution matrix.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataDevnetExecutionOperation;
/// Stable failure category exercised by one negative case.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataFailureClass;
/// Expected transaction outcome for one cross-validation case.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataTransactionOutcome;
/// Transaction location covered by one cross-validation case.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataTransactionPath;
/// One bounded Metaplex validation evidence entry.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataValidationEvidence;
/// Canonical Metaplex validation matrix.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataValidationMatrix;
/// One scenario declared by the canonical Metaplex validation matrix.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataValidationMatrixScenario;
/// Exact validation status for one Metaplex scenario.
pub use self::metadata::metaplex_token_metadata::validation::MetaplexTokenMetadataValidationStatus;
/// Loads and validates the closed Metaplex cross-validation corpus.
pub use self::metadata::metaplex_token_metadata::validation::load_metaplex_token_metadata_cross_validation_matrix;
/// Loads and validates the closed Devnet execution matrix.
pub use self::metadata::metaplex_token_metadata::validation::load_metaplex_token_metadata_devnet_execution_matrix;
/// Loads and validates the canonical Metaplex validation matrix.
pub use self::metadata::metaplex_token_metadata::validation::load_metaplex_token_metadata_validation_matrix;
/// Validates the closed Metaplex cross-validation corpus and evidence claims.
pub use self::metadata::metaplex_token_metadata::validation::validate_metaplex_token_metadata_cross_validation_matrix;
/// Validates exact current-operation coverage and conservative network statuses.
pub use self::metadata::metaplex_token_metadata::validation::validate_metaplex_token_metadata_devnet_execution_matrix;
/// Validates the canonical Metaplex validation matrix.
pub use self::metadata::metaplex_token_metadata::validation::validate_metaplex_token_metadata_validation_matrix;
/// One confirmed step retained by the complete Devnet campaign.
pub use self::metadata::solana_program::campaign::DevnetSolanaProgramMetadataCampaignStepSummary;
/// Complete evidence retained by the two-journey Devnet campaign.
pub use self::metadata::solana_program::campaign::DevnetSolanaProgramMetadataCampaignSummary;
/// Prepares fresh accounts and executes the nine stable operations on Devnet.
pub use self::metadata::solana_program::campaign::execute_devnet_solana_program_metadata_campaign;
/// Complete request for one Solana Program Metadata Devnet execution.
pub use self::metadata::solana_program::devnet_execution::DevnetSolanaProgramMetadataExecutionRequest;
/// Complete evidence produced by one Solana Program Metadata Devnet execution.
pub use self::metadata::solana_program::devnet_execution::DevnetSolanaProgramMetadataExecutionSummary;
/// Executes one Solana Program Metadata simulation or authorized submission.
pub use self::metadata::solana_program::devnet_execution::execute_devnet_solana_program_metadata;
/// Simulates one stable Solana Program Metadata operation against Devnet.
pub use self::metadata::solana_program::devnet_execution::simulate_devnet_solana_program_metadata;
/// Bytes reserved for the public Buffer journey before `Trim`.
pub use self::metadata::solana_program::fixture::SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES;
/// Options used to prepare one complete Solana Program Metadata fixture.
pub use self::metadata::solana_program::fixture::SolanaProgramMetadataFixturePreparationOptions;
/// Public fixture values for the two Solana Program Metadata journeys.
pub use self::metadata::solana_program::fixture::SolanaProgramMetadataFixturePreparationSummary;
/// One prepared executable step in a Solana Program Metadata journey.
pub use self::metadata::solana_program::fixture::SolanaProgramMetadataPreparedStep;
/// Creates two unique pre-funded PDAs and the nine typed journey steps.
pub use self::metadata::solana_program::fixture::prepare_solana_program_metadata_fixture;
/// Stable lifecycle state consumed or produced by one scenario step.
pub use self::metadata::solana_program::scenarios::SolanaProgramMetadataFixtureState;
/// One ordered multi-step Solana Program Metadata journey.
pub use self::metadata::solana_program::scenarios::SolanaProgramMetadataScenario;
/// One stable step in a Solana Program Metadata Devnet journey.
pub use self::metadata::solana_program::scenarios::SolanaProgramMetadataScenarioStep;
/// Returns the two ordered Devnet journeys covering all nine stable operations.
pub use self::metadata::solana_program::scenarios::solana_program_metadata_devnet_scenarios;
/// Maximum evidence entries accepted by one operation validation row.
pub use self::metadata::solana_program::validation::MAX_SOLANA_PROGRAM_METADATA_VALIDATION_EVIDENCE;
/// One bounded evidence record.
pub use self::metadata::solana_program::validation::SolanaProgramMetadataValidationEvidence;
/// Closed validation matrix for all nine stable operations.
pub use self::metadata::solana_program::validation::SolanaProgramMetadataValidationMatrix;
/// One operation row in the closed Devnet validation matrix.
pub use self::metadata::solana_program::validation::SolanaProgramMetadataValidationOperation;
/// Exact network validation status.
pub use self::metadata::solana_program::validation::SolanaProgramMetadataValidationStatus;
/// Loads and validates the canonical Solana Program Metadata Devnet matrix.
pub use self::metadata::solana_program::validation::load_solana_program_metadata_validation_matrix;
/// Validates exact inventory, order and conservative evidence claims.
pub use self::metadata::solana_program::validation::validate_solana_program_metadata_validation_matrix;
/// Complete request for one bounded System Program transfer on Devnet.
pub use self::solana::DevnetSystemTransferRequest;
/// Summary returned by one bounded System Program transfer execution.
pub use self::solana::DevnetSystemTransferSummary;
/// No-op observer suitable for CLI tools and opt-in integration tests.
pub use self::solana::NoopSolanaExecutionObserver;
/// Observer notified during Solana execution orchestration.
pub use self::solana::SolanaExecutionObserver;
/// Progress event emitted by Solana execution orchestration.
pub use self::solana::SolanaExecutionProgressEvent;
/// Severity of one Solana execution progress event.
pub use self::solana::SolanaExecutionProgressLevel;
/// Emits one execution progress event for specialized pipeline orchestrators.
pub(crate) use self::solana::emit;
/// Rejects one execution stage when the observer reports cancellation.
pub(crate) use self::solana::ensure_not_cancelled;
/// Executes one bounded System Program transfer on Devnet.
pub use self::solana::execute_devnet_system_transfer;
/// Loads the persistent wallet configured by one execution profile.
pub(crate) use self::solana::load_profile_wallet;
/// Formats one failed simulation without discarding runtime diagnostics.
pub(crate) use self::solana::simulation_failure_message;
/// Formats safety violations for one denied execution plan.
pub(crate) use self::solana::violation_message;
/// Complete request for one Devnet Associated Token Account execution.
pub use self::spl::associated_token_account::DevnetSplAssociatedTokenAccountExecutionRequest;
/// Complete result of one Devnet Associated Token Account execution.
pub use self::spl::associated_token_account::DevnetSplAssociatedTokenAccountExecutionSummary;
/// Executes one Devnet Associated Token Account simulation or authorized submission.
pub use self::spl::associated_token_account::execute_devnet_spl_associated_token_account;
/// Simulates one Devnet Associated Token Account operation after stateful preflight.
pub use self::spl::associated_token_account::simulate_devnet_spl_associated_token_account;
/// Complete request for one SPL Memo v4 Devnet execution.
pub use self::spl::memo::DevnetMemoExecutionRequest;
/// Complete result of one SPL Memo v4 Devnet execution and post-validation.
pub use self::spl::memo::DevnetMemoExecutionSummary;
/// Executes one SPL Memo v4 Devnet simulation or explicitly authorized submission.
pub use self::spl::memo::execute_devnet_memo;
/// Complete request for one Devnet classic SPL Token execution.
pub use self::spl::token::execution::DevnetSplTokenExecutionRequest;
/// Complete result of one Devnet classic SPL Token execution.
pub use self::spl::token::execution::DevnetSplTokenExecutionSummary;
/// Canonical hydration availability helper shared by Token lifecycle orchestration.
pub(crate) use self::spl::token::execution::canonical_available;
/// Decode completion helper shared by Token lifecycle orchestration.
pub(crate) use self::spl::token::execution::decode_completed;
/// Executes one Devnet classic SPL Token simulation or authorized submission.
pub use self::spl::token::execution::execute_devnet_spl_token;
/// Canonical signature hydration helper shared by Token lifecycle orchestration.
pub(crate) use self::spl::token::execution::hydrate_signature;
/// Targeted program replay helper shared by Token lifecycle orchestration.
pub(crate) use self::spl::token::execution::replay_program;
/// Simulates one Devnet classic SPL Token operation after stateful preflight.
pub use self::spl::token::execution::simulate_devnet_spl_token;
/// Complete request to prepare raw accounts for one Devnet SPL Token lifecycle.
pub use self::spl::token::lifecycle::DevnetSplTokenLifecyclePreparationRequest;
/// One prepared raw account step.
pub use self::spl::token::lifecycle::DevnetSplTokenLifecyclePreparationStep;
/// Summary of raw-account preparation for one lifecycle.
pub use self::spl::token::lifecycle::DevnetSplTokenLifecyclePreparationSummary;
/// Complete request for one controlled Devnet SPL Token lifecycle.
pub use self::spl::token::lifecycle::DevnetSplTokenLifecycleRequest;
/// Summary of one lifecycle operation step.
pub use self::spl::token::lifecycle::DevnetSplTokenLifecycleStepSummary;
/// Complete controlled lifecycle summary.
pub use self::spl::token::lifecycle::DevnetSplTokenLifecycleSummary;
/// Executes one controlled Devnet SPL Token lifecycle.
pub use self::spl::token::lifecycle::execute_devnet_spl_token_lifecycle;
/// Prepares raw accounts for one controlled Devnet SPL Token lifecycle.
pub use self::spl::token::lifecycle::prepare_devnet_spl_token_lifecycle_accounts;
/// Complete request for one Devnet Token-2022 execution.
pub use self::spl::token_2022::devnet_execution::DevnetSplToken2022ExecutionRequest;
/// Complete result of one Devnet Token-2022 execution.
pub use self::spl::token_2022::devnet_execution::DevnetSplToken2022ExecutionSummary;
/// Executes one Devnet Token-2022 simulation or authorized submission.
pub use self::spl::token_2022::devnet_execution::execute_devnet_spl_token_2022;
/// Simulates one Devnet Token-2022 operation after stateful preflight.
pub use self::spl::token_2022::devnet_execution::simulate_devnet_spl_token_2022;
/// Stable category of one independent Devnet validation scenario.
pub use self::spl::token_2022::devnet_scenarios::DevnetSplValidationFamily;
/// Current implementation status of one Devnet validation scenario.
pub use self::spl::token_2022::devnet_scenarios::DevnetSplValidationImplementationStatus;
/// One independent Devnet validation scenario exposed to applications.
pub use self::spl::token_2022::devnet_scenarios::DevnetSplValidationScenario;
/// Returns the complete ordered Devnet scenario inventory.
pub use self::spl::token_2022::devnet_scenarios::devnet_spl_validation_scenarios;
/// Default raw amount delegated by the public ApproveChecked scenario.
pub use self::spl::token_2022::fixture::DEFAULT_TOKEN_2022_APPROVE_AMOUNT_RAW;
/// Default raw amount burned by the public BurnChecked scenario.
pub use self::spl::token_2022::fixture::DEFAULT_TOKEN_2022_BURN_AMOUNT_RAW;
/// Default raw amount minted by the public MintToChecked scenario.
pub use self::spl::token_2022::fixture::DEFAULT_TOKEN_2022_MINT_AMOUNT_RAW;
/// Default raw amount transferred by the public TransferChecked scenario.
pub use self::spl::token_2022::fixture::DEFAULT_TOKEN_2022_TRANSFER_AMOUNT_RAW;
/// Command-line options for one Token-2022 fixture preparation.
pub use self::spl::token_2022::fixture::Token2022FixturePreparationOptions;
/// Public values written to one Token-2022 fixture file.
pub use self::spl::token_2022::fixture::Token2022FixturePreparationSummary;
/// Creates or reuses one complete Token-2022 public scenario fixture.
pub use self::spl::token_2022::fixture::prepare_token_2022_fixture;
/// One confirmed step in the five-instruction Token Metadata Devnet campaign.
pub use self::spl::token_2022::metadata::campaign::DevnetToken2022MetadataCampaignStepSummary;
/// Complete evidence retained by the Token Metadata Devnet campaign.
pub use self::spl::token_2022::metadata::campaign::DevnetToken2022MetadataCampaignSummary;
/// Executes all five Token Metadata interface instructions on one fresh Devnet mint.
pub use self::spl::token_2022::metadata::campaign::execute_devnet_token_2022_metadata_campaign;
/// Returns the exact ordered operation names covered by the Token Metadata campaign.
pub use self::spl::token_2022::metadata::campaign::token_2022_metadata_campaign_operation_names;
/// Additional metadata key created and removed by the campaign.
pub use self::spl::token_2022::metadata::fixture::TOKEN_2022_METADATA_CAMPAIGN_KEY;
/// Stable metadata name used by the `0.4.8-pre.012` campaign.
pub use self::spl::token_2022::metadata::fixture::TOKEN_2022_METADATA_CAMPAIGN_NAME;
/// Stable metadata symbol used by the `0.4.8-pre.012` campaign.
pub use self::spl::token_2022::metadata::fixture::TOKEN_2022_METADATA_CAMPAIGN_SYMBOL;
/// Stable opaque URI stored by the `0.4.8-pre.012` campaign.
pub use self::spl::token_2022::metadata::fixture::TOKEN_2022_METADATA_CAMPAIGN_URI;
/// Additional metadata value created and removed by the campaign.
pub use self::spl::token_2022::metadata::fixture::TOKEN_2022_METADATA_CAMPAIGN_VALUE;
/// Options used to prepare one fresh Token-2022 metadata fixture.
pub use self::spl::token_2022::metadata::fixture::Token2022MetadataFixturePreparationOptions;
/// Fresh mint and authorities prepared for one Token Metadata campaign.
pub use self::spl::token_2022::metadata::fixture::Token2022MetadataFixturePreparationSummary;
/// Creates one fresh Token-2022 mint with a self-referential Metadata Pointer.
pub use self::spl::token_2022::metadata::fixture::prepare_token_2022_metadata_fixture;
/// Maximum evidence entries retained by one Token Metadata validation scenario.
pub use self::spl::token_2022::metadata::validation::MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE;
/// One bounded evidence item retained by the Token Metadata validation matrix.
pub use self::spl::token_2022::metadata::validation::Token2022MetadataValidationEvidence;
/// Canonical Token Metadata Devnet validation matrix.
pub use self::spl::token_2022::metadata::validation::Token2022MetadataValidationMatrix;
/// One scenario in the Token Metadata Devnet validation matrix.
pub use self::spl::token_2022::metadata::validation::Token2022MetadataValidationScenario;
/// Exact network-validation status for one Token Metadata scenario.
pub use self::spl::token_2022::metadata::validation::Token2022MetadataValidationStatus;
/// Loads and validates the Token Metadata Devnet validation matrix.
pub use self::spl::token_2022::metadata::validation::load_token_2022_metadata_validation_matrix;
/// Validates exact Token Metadata campaign coverage and evidence claims.
pub use self::spl::token_2022::metadata::validation::validate_token_2022_metadata_validation_matrix;
/// Maximum number of evidence records accepted in one validation report.
pub use self::spl::token_2022::validation::MAX_TOKEN_2022_VALIDATION_EVIDENCE;
/// Required validation environment for one Token-2022 scenario.
pub use self::spl::token_2022::validation::Token2022ValidationEnvironment;
/// One bounded proof attached to a Token-2022 validation scenario.
pub use self::spl::token_2022::validation::Token2022ValidationEvidence;
/// Machine-readable Token-2022 validation matrix.
pub use self::spl::token_2022::validation::Token2022ValidationMatrix;
/// One scenario declared by the canonical validation matrix.
pub use self::spl::token_2022::validation::Token2022ValidationMatrixScenario;
/// Complete bounded Token-2022 validation report.
pub use self::spl::token_2022::validation::Token2022ValidationReport;
/// One declared Token-2022 validation scenario and its observed evidence.
pub use self::spl::token_2022::validation::Token2022ValidationScenario;
/// Exact status of one Token-2022 validation scenario.
pub use self::spl::token_2022::validation::Token2022ValidationStatus;
/// Loads and validates the canonical Token-2022 validation matrix.
pub use self::spl::token_2022::validation::load_token_2022_validation_matrix;
/// Validates one Token-2022 validation matrix.
pub use self::spl::token_2022::validation::validate_token_2022_validation_matrix;
/// Validates one bounded Token-2022 milestone report.
pub use self::spl::token_2022::validation::validate_token_2022_validation_report;
/// Total transactions reserved by one complete Solana Program Metadata campaign.
pub(crate) use self::constants::SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT;
/// Canonical tracing target for demo pipeline scenarios.
pub(crate) use self::constants::TRACING_TARGET;

View File

@@ -0,0 +1,7 @@
// file: ks-pipeline-demo-scenarios/src/metadata.rs
// version: 2
//! Metadata-oriented reusable demo and Devnet validation scenarios.
pub(crate) mod metaplex_token_metadata;
pub(crate) mod solana_program;

View File

@@ -0,0 +1,16 @@
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata.rs
// version: 8
//! Metaplex Token Metadata demo and Devnet validation scenarios.
pub(crate) mod collection_verify_campaign;
pub(crate) mod create_mint_campaign;
pub(crate) mod devnet_execution;
pub(crate) mod escrow_campaign;
pub(crate) mod fixture;
pub(crate) mod maintenance_campaign;
pub(crate) mod pnft_lifecycle_campaign;
pub(crate) mod print_burn_campaign;
pub(crate) mod scenarios;
pub(crate) mod use_campaign;
pub(crate) mod validation;

View File

@@ -0,0 +1,747 @@
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs
// version: 2
//! Confirmed Devnet collection parent/member `Verify -> Unverify` campaign.
/// Collection relation and sized-collection state observed across verification.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DevnetMetaplexCollectionVerifyState {
/// Member collection flag before `Verify`.
pub verified_before: bool,
/// Member collection flag after `Verify`.
pub verified_after_verify: bool,
/// Member collection flag after `Unverify`.
pub verified_after_unverify: bool,
/// Collection size before `Verify`.
pub collection_size_before: u64,
/// Collection size after `Verify`.
pub collection_size_after_verify: u64,
/// Collection size after `Unverify`.
pub collection_size_after_unverify: u64,
}
/// Complete evidence produced by one collection parent/member verification campaign.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexCollectionVerifyCampaignSummary {
/// Fully qualified collection parent fixture and `Create -> Mint` evidence.
pub parent: crate::DevnetMetaplexCreateMintCampaignSummary,
/// Fully qualified unverified member fixture and `Create -> Mint` evidence.
pub member: crate::DevnetMetaplexCreateMintCampaignSummary,
/// Confirmed collection verification evidence.
pub verify: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Confirmed collection unverification evidence.
pub unverify: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Exact collection relation and size transitions.
pub state: crate::DevnetMetaplexCollectionVerifyState,
}
/// Returns the exact current Metaplex operations qualified by this campaign after fixture setup.
pub fn metaplex_collection_verify_campaign_operation_names() -> &'static [&'static str; 2] {
return &["verify", "unverify"];
}
/// Executes one fresh collection parent/member `Verify -> Unverify` campaign on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_collection_verify_campaign<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> ks_core::Result<crate::DevnetMetaplexCollectionVerifyCampaignSummary>
where
S: ks_store::RawTransactionStore
+ ks_store::CoreExtractionStore
+ ks_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if options.collection_mint.is_some() {
return std::result::Result::Err(ks_core::Error::config(
"collection verification campaign base options must not preselect a collection mint",
));
}
let mut parent_options = options.clone();
parent_options.asset_family = crate::MetaplexTokenMetadataAssetFamily::Collection;
parent_options.collection_mint = std::option::Option::None;
let parent = match crate::execute_devnet_metaplex_create_mint_campaign(
http_pool,
store,
profile,
workspace_root,
&parent_options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut member_options = options.clone();
member_options.asset_family = crate::MetaplexTokenMetadataAssetFamily::Nft;
member_options.collection_mint = std::option::Option::Some(parent.fixture.mint.clone());
let member = match crate::execute_devnet_metaplex_create_mint_campaign(
http_pool,
store,
profile,
workspace_root,
&member_options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verified_before = match collection_member_verified(
member.mint.after.as_slice(),
member.fixture.metadata.as_str(),
parent.fixture.mint.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection_size_before =
match collection_size(parent.mint.after.as_slice(), parent.fixture.metadata.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if verified_before || collection_size_before != 0 {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_verify_initial_state_invalid",
format!(
"fresh collection member must start unverified with parent size 0; verified={verified_before}, size={collection_size_before}"
),
));
}
let parent_mint_slot = match confirmed_slot("parent mint", &parent.mint) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let member_mint_slot = match confirmed_slot("member mint", &member.mint) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture_slot = std::cmp::max(parent_mint_slot, member_mint_slot);
let decoders: std::vec::Vec<std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(ks_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>> = std::vec![
std::sync::Arc::new(ks_lib::MtAdminMaterializer),
std::sync::Arc::new(ks_lib::MtComplianceAuditMaterializer),
std::sync::Arc::new(ks_lib::MtLifecycleMaterializer),
std::sync::Arc::new(ks_lib::MtMetadataMetaplexTokenMetadataMaterializer),
std::sync::Arc::new(ks_lib::MtRiskMaterializer),
];
let verify_operation = ks_lib::ExMetaplexTokenMetadataOperation::Verify {
authority: ks_lib::MdPubkey(parent.fixture.authority.clone()),
delegate_record: std::option::Option::None,
metadata: ks_lib::MdPubkey(member.fixture.metadata.clone()),
collection_mint: std::option::Option::Some(ks_lib::MdPubkey(parent.fixture.mint.clone())),
collection_metadata: std::option::Option::Some(ks_lib::MdPubkey(
parent.fixture.metadata.clone(),
)),
collection_master_edition: std::option::Option::Some(ks_lib::MdPubkey(
parent.fixture.master_edition.clone(),
)),
verification_args: mpl_token_metadata::types::VerificationArgs::CollectionV1,
};
let mut verify_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-collection-verify-{}", uuid::Uuid::new_v4()),
verify_operation,
);
verify_request.query_role = options.query_role.clone();
verify_request.transaction_role = options.transaction_role.clone();
verify_request.submit = true;
verify_request.operator_confirmed = true;
verify_request.materialize_after_confirmation = true;
verify_request.post_validation_max_retries = 20;
verify_request.preflight_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(fixture_slot),
);
verify_request.postcondition_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(fixture_slot),
);
let verify = match crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&verify_request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_slot = match validate_confirmed_execution("verify", &verify) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verified_after_verify = match collection_member_verified(
verify.after.as_slice(),
member.fixture.metadata.as_str(),
parent.fixture.mint.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection_size_after_verify =
match collection_size(verify.after.as_slice(), parent.fixture.metadata.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !verified_after_verify || collection_size_after_verify != 1 {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_verify_postcondition_failed",
format!(
"confirmed collection Verify must set member verified=true and parent size=1; verified={verified_after_verify}, size={collection_size_after_verify}"
),
));
}
let unverify_operation = ks_lib::ExMetaplexTokenMetadataOperation::Unverify {
authority: ks_lib::MdPubkey(parent.fixture.authority.clone()),
delegate_record: std::option::Option::None,
metadata: ks_lib::MdPubkey(member.fixture.metadata.clone()),
collection_mint: std::option::Option::Some(ks_lib::MdPubkey(parent.fixture.mint.clone())),
collection_metadata: std::option::Option::Some(ks_lib::MdPubkey(
parent.fixture.metadata.clone(),
)),
verification_args: mpl_token_metadata::types::VerificationArgs::CollectionV1,
};
let mut unverify_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-collection-unverify-{}", uuid::Uuid::new_v4()),
unverify_operation,
);
unverify_request.query_role = options.query_role.clone();
unverify_request.transaction_role = options.transaction_role.clone();
unverify_request.submit = true;
unverify_request.operator_confirmed = true;
unverify_request.materialize_after_confirmation = true;
unverify_request.post_validation_max_retries = 20;
unverify_request.preflight_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(verify_slot),
);
unverify_request.postcondition_reads = collection_reads(
&parent.fixture,
&member.fixture,
options.query_role.as_str(),
std::option::Option::Some(verify_slot),
);
let unverify = match crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&unverify_request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unverify_slot = match validate_confirmed_execution("unverify", &unverify) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verified_after_unverify = match collection_member_verified(
unverify.after.as_slice(),
member.fixture.metadata.as_str(),
parent.fixture.mint.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection_size_after_unverify =
match collection_size(unverify.after.as_slice(), parent.fixture.metadata.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if verified_after_unverify || collection_size_after_unverify != 0 {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_unverify_postcondition_failed",
format!(
"confirmed collection Unverify at slot {unverify_slot} must restore verified=false and parent size=0; verified={verified_after_unverify}, size={collection_size_after_unverify}"
),
));
}
return std::result::Result::Ok(crate::DevnetMetaplexCollectionVerifyCampaignSummary {
parent,
member,
verify,
unverify,
state: crate::DevnetMetaplexCollectionVerifyState {
verified_before,
verified_after_verify,
verified_after_unverify,
collection_size_before,
collection_size_after_verify,
collection_size_after_unverify,
},
});
}
fn collection_reads(
parent: &crate::MetaplexCreateFixturePreparationSummary,
member: &crate::MetaplexCreateFixturePreparationSummary,
query_role: &str,
min_context_slot: std::option::Option<u64>,
) -> std::vec::Vec<ks_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
return vec![
ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(member.metadata.clone()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
},
ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(parent.metadata.clone()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
},
ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(parent.master_edition.clone()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Edition {
mint: ks_lib::MdPubkey(parent.mint.clone()),
},
min_context_slot,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
},
];
}
fn confirmed_slot(
step: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> ks_core::Result<u64> {
return match execution.confirmation.as_ref().and_then(|value| return value.slot) {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_fixture_confirmation_slot_missing",
format!("Metaplex {step} has no confirmed slot"),
)),
};
}
fn validate_confirmed_execution(
step: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> ks_core::Result<u64> {
if !execution.simulation.success {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_simulation_failed",
format!("Metaplex collection {step} simulation failed"),
));
}
let confirmation = match execution.confirmation.as_ref() {
std::option::Option::Some(value)
if matches!(
value.status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
) =>
{
value
},
std::option::Option::Some(value) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_confirmation_incomplete",
format!("Metaplex collection {step} stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_confirmation_missing",
format!("Metaplex collection {step} has no confirmation evidence"),
));
},
};
let diagnostic = match execution.post_execution.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_post_execution_missing",
format!("Metaplex collection {step} has no post-execution diagnostic"),
));
},
};
if !diagnostic.canonical_inserted
|| !diagnostic.core_extracted
|| !diagnostic.decode_replayed
|| !diagnostic.materialized
|| execution.materializations.is_empty()
|| execution.materialized_snapshots.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_post_execution_incomplete",
format!(
"Metaplex collection {step} post-execution incomplete: signature={}, slot={:?}, canonical_inserted={}, core_extracted={}, decode_replayed={}, materialized={}, materialization_rows={}, materialized_snapshots={}, diagnostics={:?}",
confirmation.signature.0,
confirmation.slot,
diagnostic.canonical_inserted,
diagnostic.core_extracted,
diagnostic.decode_replayed,
diagnostic.materialized,
execution.materializations.len(),
execution.materialized_snapshots.len(),
diagnostic.diagnostics
),
));
}
let second_replay = match execution.idempotence_replay.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_idempotence_missing",
format!("Metaplex collection {step} has no idempotence replay evidence"),
));
},
};
if second_replay.failed_inputs != 0
|| second_replay.processing_error_inputs != 0
|| second_replay.processors.iter().any(|processor| {
return processor.failed != 0
|| processor.processing_errors != 0
|| processor.materialized_outputs != 0
|| processor.materialization_refused != 0;
})
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_idempotence_failed",
format!("Metaplex collection {step} second replay is not idempotent"),
));
}
return match confirmation.slot {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_confirmation_slot_missing",
format!("Metaplex collection {step} confirmation has no slot"),
)),
};
}
fn metadata_payload<'a>(
snapshots: &'a [ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
metadata: &str,
) -> ks_core::Result<&'a serde_json::Value> {
let snapshot = snapshots.iter().find(|value| {
return value.snapshot.account.0.as_str() == metadata
&& value.snapshot.account_kind == "metadata";
});
return match snapshot {
std::option::Option::Some(value) => std::result::Result::Ok(&value.snapshot.payload_json),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_metadata_snapshot_missing",
format!("Metaplex metadata snapshot {metadata} is missing"),
)),
};
}
fn collection_member_verified(
snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
member_metadata: &str,
expected_collection_mint: &str,
) -> ks_core::Result<bool> {
let payload = match metadata_payload(snapshots, member_metadata) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collection = match payload.get("collection") {
std::option::Option::Some(value) if value.is_object() => value,
_ => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_relation_missing",
"member metadata does not contain a collection relation",
));
},
};
let key = collection.get("key").and_then(serde_json::Value::as_str);
let verified = collection.get("verified").and_then(serde_json::Value::as_bool);
if key != std::option::Option::Some(expected_collection_mint) || verified.is_none() {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_relation_invalid",
format!(
"member collection relation mismatch: expected key={expected_collection_mint}, got key={key:?}, verified={verified:?}"
),
));
}
return match verified {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_verified_flag_missing",
"member collection relation does not expose a verified flag",
)),
};
}
fn collection_size(
snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
collection_metadata: &str,
) -> ks_core::Result<u64> {
let payload = match metadata_payload(snapshots, collection_metadata) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let size = payload
.get("collection_details")
.and_then(|value| return value.get("V1"))
.and_then(|value| return value.get("size"))
.and_then(serde_json::Value::as_u64);
return match size {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_collection_size_missing",
"collection metadata does not expose CollectionDetails::V1 size",
)),
};
}
#[cfg(test)]
mod tests {
#[test]
fn collection_campaign_contract_is_exactly_verify_then_unverify() {
assert_eq!(
crate::metaplex_collection_verify_campaign_operation_names(),
&["verify", "unverify"]
);
}
#[test]
fn collection_payload_contract_tracks_relation_and_size() {
let collection = solana_pubkey::Pubkey::new_unique().to_string();
let relation = serde_json::json!({"collection": {"verified": false, "key": collection}});
let details = serde_json::json!({"collection_details": {"V1": {"size": 0}}});
assert_eq!(
relation.pointer("/collection/verified").and_then(serde_json::Value::as_bool),
std::option::Option::Some(false)
);
assert_eq!(
details
.pointer("/collection_details/V1/size")
.and_then(serde_json::Value::as_u64),
std::option::Option::Some(0)
);
}
#[test]
fn metadata_serde_contract_uses_stateful_collection_keys() {
let metadata = mpl_token_metadata::accounts::Metadata {
key: mpl_token_metadata::types::Key::MetadataV1,
update_authority: mpl_token_metadata::ID,
mint: mpl_token_metadata::ID,
name: "Collection".to_string(),
symbol: "COL".to_string(),
uri: "https://example.invalid/collection.json".to_string(),
seller_fee_basis_points: 0,
creators: std::option::Option::None,
primary_sale_happened: false,
is_mutable: true,
edition_nonce: std::option::Option::None,
token_standard: std::option::Option::Some(
mpl_token_metadata::types::TokenStandard::NonFungible,
),
collection: std::option::Option::None,
uses: std::option::Option::None,
collection_details: std::option::Option::Some(
mpl_token_metadata::types::CollectionDetails::V1 { size: 7 },
),
programmable_config: std::option::Option::None,
};
let payload = match serde_json::to_value(metadata) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("metadata serialization failed: {error}"),
};
assert_eq!(
payload
.pointer("/collection_details/V1/size")
.and_then(serde_json::Value::as_u64),
std::option::Option::Some(7)
);
assert!(payload.get("collectionDetails").is_none());
}
fn execution_evidence_json(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> serde_json::Value {
let confirmation = execution.confirmation.as_ref();
let confirmation_status = confirmation
.map(|value| return format!("{:?}", value.status))
.unwrap_or_else(|| return "missing".to_string());
let confirmation_slot = confirmation.and_then(|value| return value.slot);
let signature = confirmation
.map(|value| return value.signature.0.clone())
.unwrap_or_else(|| return "missing".to_string());
let diagnostic = execution.post_execution.as_ref();
let idempotence_clean = execution.idempotence_replay.as_ref().is_some_and(|replay| {
return replay.failed_inputs == 0
&& replay.processing_error_inputs == 0
&& replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
});
return serde_json::json!({
"operation": execution.plan.operation_code.as_str(),
"cluster": execution.cluster,
"genesisHash": execution.genesis_hash.as_str(),
"simulationContextSlot": execution.simulation_context_slot,
"simulationSuccess": execution.simulation.success,
"simulationLogCount": execution.simulation.logs.len(),
"messageHash": execution.readiness.message_hash.as_str(),
"feeLamports": execution.fee.fee_lamports,
"signature": signature,
"confirmationStatus": confirmation_status,
"confirmationSlot": confirmation_slot,
"beforeSnapshots": execution.before.len(),
"afterSnapshots": execution.after.len(),
"canonicalHydration": diagnostic.is_some_and(|value| return value.canonical_inserted),
"coreExtraction": diagnostic.is_some_and(|value| return value.core_extracted),
"decodeReplay": diagnostic.is_some_and(|value| return value.decode_replayed),
"materialized": diagnostic.is_some_and(|value| return value.materialized),
"instructionMaterializations": execution.materializations.len(),
"materializedSnapshots": execution.materialized_snapshots.len(),
"idempotenceReplayClean": idempotence_clean,
});
}
#[tokio::test]
async fn optional_devnet_metaplex_collection_verify_campaign_from_env() {
if std::env::var("KB_DEVNET_METAPLEX_COLLECTION_VERIFY_CAMPAIGN_TEST")
.ok()
.as_deref()
!= std::option::Option::Some("1")
{
return;
}
if std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref()
!= std::option::Option::Some("1")
{
panic!("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 is required for the spending campaign");
}
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
};
let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") {
std::result::Result::Ok(value) => {
let path = std::path::PathBuf::from(value);
if path.is_absolute() { path } else { workspace_root.join(path) }
},
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
};
let config = ks_config::read_config_json_file_with_environment(
config_path.as_path(),
workspace_root,
)
.unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}"));
let profile_name = std::env::var("KB_DEVNET_PROFILE").ok();
let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref())
.unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}"));
let database_url = std::env::var("KB_POSTGRES_TEST_URL")
.unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}"));
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
let pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}"));
let store_options = ks_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
)
.unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}"));
let store = ks_store::PostgresStore::connect(store_options)
.await
.unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}"));
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("PostgreSQL schema initialization failed: {error}");
}
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
workspace_root.join(configured_wallet_dir)
};
let options = crate::MetaplexCreateFixturePreparationOptions::new(wallet_dir);
let summary = crate::execute_devnet_metaplex_collection_verify_campaign(
&pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| {
panic!("Metaplex collection Verify -> Unverify Devnet campaign failed: {error}")
});
let verify_confirmation = summary
.verify
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("Verify confirmation missing"));
let unverify_confirmation = summary
.unverify
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("Unverify confirmation missing"));
println!(
"METAPLEX_COLLECTION_VERIFY_FIXTURE parent_mint={} parent_metadata={} parent_master_edition={} member_mint={} member_metadata={} member_master_edition={}",
summary.parent.fixture.mint,
summary.parent.fixture.metadata,
summary.parent.fixture.master_edition,
summary.member.fixture.mint,
summary.member.fixture.metadata,
summary.member.fixture.master_edition,
);
println!(
"METAPLEX_COLLECTION_VERIFY_STEP operation={} signature={} slot={:?} materializations={}",
summary.verify.plan.operation_code,
verify_confirmation.signature.0,
verify_confirmation.slot,
summary.verify.materializations.len(),
);
println!(
"METAPLEX_COLLECTION_VERIFY_STEP operation={} signature={} slot={:?} materializations={}",
summary.unverify.plan.operation_code,
unverify_confirmation.signature.0,
unverify_confirmation.slot,
summary.unverify.materializations.len(),
);
println!(
"METAPLEX_COLLECTION_VERIFY_STATE verified_before={} verified_after_verify={} verified_after_unverify={} size_before={} size_after_verify={} size_after_unverify={}",
summary.state.verified_before,
summary.state.verified_after_verify,
summary.state.verified_after_unverify,
summary.state.collection_size_before,
summary.state.collection_size_after_verify,
summary.state.collection_size_after_unverify,
);
println!(
"METAPLEX_COLLECTION_VERIFY_EVIDENCE verify={} unverify={}",
execution_evidence_json(&summary.verify),
execution_evidence_json(&summary.unverify),
);
}
}

View File

@@ -0,0 +1,922 @@
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/create_mint_campaign.rs
// version: 10
//! Confirmed Devnet `Create -> Mint` campaign for one Metaplex asset family.
/// Confirmed SPL state observed before and after the Metaplex `Mint` operation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DevnetMetaplexCreateMintTokenState {
/// Raw amount minted by the campaign.
pub expected_amount_raw: u64,
/// SPL mint supply observed after `Create` and before `Mint`.
pub supply_before_mint: u64,
/// Operator token-account amount observed after `Create` and before `Mint`.
pub token_amount_before_mint: u64,
/// Raw classic SPL token-account state observed before `Mint`.
pub token_account_state_before_mint: u8,
/// SPL mint supply observed at or after the confirmed `Mint` slot.
pub supply_after_mint: u64,
/// Operator token-account amount observed at or after the confirmed `Mint` slot.
pub token_amount_after_mint: u64,
/// Raw classic SPL token-account state observed after `Mint`.
pub token_account_state_after_mint: u8,
}
/// Complete evidence produced by one family-specific Metaplex `Create -> Mint` campaign.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexCreateMintCampaignSummary {
/// Asset family exercised by this campaign.
pub asset_family: crate::MetaplexTokenMetadataAssetFamily,
/// Fresh classic SPL mint and ATA fixture.
pub fixture: crate::MetaplexCreateFixturePreparationSummary,
/// Confirmed `Create` execution evidence.
pub create: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Confirmed `Mint` execution evidence.
pub mint: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Exact classic SPL supply and token-account transition caused by `Mint`.
pub token_state: crate::DevnetMetaplexCreateMintTokenState,
}
/// Returns the exact ordered operation names exercised by this campaign.
pub fn metaplex_create_mint_campaign_operation_names() -> &'static [&'static str; 2] {
return &["create", "mint"];
}
/// Executes one fresh family-specific `Create -> Mint` campaign on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_create_mint_campaign<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> ks_core::Result<crate::DevnetMetaplexCreateMintCampaignSummary>
where
S: ks_store::RawTransactionStore
+ ks_store::CoreExtractionStore
+ ks_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if !profile.wallet.devnet_send_enabled {
return std::result::Result::Err(ks_core::Error::config(
"Metaplex Create -> Mint Devnet campaign requires devnet_send_enabled=true",
));
}
let fixture =
match crate::prepare_metaplex_create_fixture(http_pool, profile, workspace_root, options)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let decoders: std::vec::Vec<std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(ks_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>> = std::vec![
std::sync::Arc::new(ks_lib::MtAdminMaterializer),
std::sync::Arc::new(ks_lib::MtComplianceAuditMaterializer),
std::sync::Arc::new(ks_lib::MtLifecycleMaterializer),
std::sync::Arc::new(ks_lib::MtMetadataMetaplexTokenMetadataMaterializer),
std::sync::Arc::new(ks_lib::MtRiskMaterializer),
];
let mut create_request =
match crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
format!("metaplex-create-mint-create-{}", uuid::Uuid::new_v4()),
fixture.operation_json.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
create_request.query_role = options.query_role.clone();
create_request.transaction_role = options.transaction_role.clone();
create_request.submit = true;
create_request.operator_confirmed = true;
create_request.materialize_after_confirmation = true;
create_request.post_validation_max_retries = 20;
create_request.postcondition_reads = postcondition_reads(
&fixture,
options.query_role.as_str(),
std::option::Option::None,
false,
);
let create = match crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&create_request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let create_slot = match validate_confirmed_execution("create", &create) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let create_signature = match create.confirmation.as_ref() {
std::option::Option::Some(value) => value.signature.0.as_str(),
std::option::Option::None => "<missing-signature>",
};
if let std::result::Result::Err(error) =
validate_expected_stateful_accounts(&fixture, &create.after, false)
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_post_create_stateful_invalid",
format!(
"confirmed Metaplex Create {create_signature} at slot {create_slot} left invalid expected stateful accounts: {error}"
),
));
}
let mint_before = match crate::read_mint_account_at_or_after(
http_pool,
options.query_role.as_str(),
fixture.mint.as_str(),
std::option::Option::Some(create_slot),
)
.await
{
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_mint_missing_before_mint",
format!(
"classic SPL mint is unavailable after confirmed Metaplex Create {create_signature} at slot {create_slot}"
),
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let expected_mint_authority = expected_mint_authority_after_create(&fixture);
if let std::result::Result::Err(error) = crate::validate_classic_fixture_mint_state(
&mint_before,
expected_mint_authority,
fixture.mint_decimals,
0,
) {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_post_create_spl_state_invalid",
format!(
"confirmed Metaplex Create {} at slot {} left an invalid classic mint state; expected mint/freeze authority {}; {error}",
create_signature, create_slot, expected_mint_authority
),
));
}
let token_before = match crate::read_token_account_at_or_after(
http_pool,
options.query_role.as_str(),
fixture.token_account.as_str(),
std::option::Option::Some(create_slot),
)
.await
{
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_token_missing_before_mint",
format!(
"classic SPL token account is unavailable after confirmed Metaplex Create {create_signature} at slot {create_slot}"
),
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = crate::validate_classic_fixture_token_account_state(
&token_before,
fixture.mint.as_str(),
fixture.authority.as_str(),
0,
spl_token_interface::state::AccountState::Initialized,
) {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_post_create_token_state_invalid",
format!(
"confirmed Metaplex Create {create_signature} at slot {create_slot} left an invalid classic token-account state: {error}"
),
));
}
let supply_before_mint = match crate::classic_fixture_mint_supply(&mint_before) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let token_amount_before_mint = match crate::classic_fixture_token_amount(&token_before) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let token_account_state_before_mint =
match crate::classic_fixture_token_account_state(&token_before) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut mint_request =
match crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
format!("metaplex-create-mint-mint-{}", uuid::Uuid::new_v4()),
fixture.mint_operation_json.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
mint_request.query_role = options.query_role.clone();
mint_request.transaction_role = options.transaction_role.clone();
mint_request.submit = true;
mint_request.operator_confirmed = true;
mint_request.materialize_after_confirmation = true;
mint_request.post_validation_max_retries = 20;
mint_request.preflight_reads = postcondition_reads(
&fixture,
options.query_role.as_str(),
std::option::Option::Some(create_slot),
false,
);
mint_request.postcondition_reads = postcondition_reads(
&fixture,
options.query_role.as_str(),
std::option::Option::Some(create_slot),
true,
);
let mint = match crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&mint_request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint_slot = match validate_confirmed_execution("mint", &mint) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint_signature = match mint.confirmation.as_ref() {
std::option::Option::Some(value) => value.signature.0.as_str(),
std::option::Option::None => "<missing-signature>",
};
if let std::result::Result::Err(error) =
validate_expected_stateful_accounts(&fixture, &mint.after, true)
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_post_mint_stateful_invalid",
format!(
"confirmed Metaplex Mint {mint_signature} at slot {mint_slot} left invalid expected stateful accounts: {error}"
),
));
}
let mint_after = match crate::read_mint_account_at_or_after(
http_pool,
options.query_role.as_str(),
fixture.mint.as_str(),
std::option::Option::Some(mint_slot),
)
.await
{
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_mint_missing_after_mint",
format!(
"classic SPL mint is unavailable after confirmed Metaplex Mint {mint_signature} at slot {mint_slot}"
),
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = crate::validate_classic_fixture_mint_state(
&mint_after,
expected_mint_authority,
fixture.mint_decimals,
fixture.mint_amount_raw,
) {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_post_mint_spl_state_invalid",
format!(
"confirmed Metaplex Mint {} at slot {} left an invalid classic mint state; expected mint/freeze authority {}; {error}",
mint_signature, mint_slot, expected_mint_authority
),
));
}
let token_after = match crate::read_token_account_at_or_after(
http_pool,
options.query_role.as_str(),
fixture.token_account.as_str(),
std::option::Option::Some(mint_slot),
)
.await
{
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_token_missing_after_mint",
format!(
"classic SPL token account is unavailable after confirmed Metaplex Mint {mint_signature} at slot {mint_slot}"
),
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let expected_token_account_state_after_mint =
expected_token_account_state_after_mint(options.asset_family);
if let std::result::Result::Err(error) = crate::validate_classic_fixture_token_account_state(
&token_after,
fixture.mint.as_str(),
fixture.authority.as_str(),
fixture.mint_amount_raw,
expected_token_account_state_after_mint,
) {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_post_mint_token_state_invalid",
format!(
"confirmed Metaplex Mint {mint_signature} at slot {mint_slot} left an invalid classic token-account state: {error}"
),
));
}
let supply_after_mint = match crate::classic_fixture_mint_supply(&mint_after) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let token_amount_after_mint = match crate::classic_fixture_token_amount(&token_after) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let token_account_state_after_mint =
match crate::classic_fixture_token_account_state(&token_after) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::DevnetMetaplexCreateMintCampaignSummary {
asset_family: options.asset_family,
token_state: crate::DevnetMetaplexCreateMintTokenState {
expected_amount_raw: fixture.mint_amount_raw,
supply_before_mint,
token_amount_before_mint,
token_account_state_before_mint,
supply_after_mint,
token_amount_after_mint,
token_account_state_after_mint,
},
fixture,
create,
mint,
});
}
fn expected_mint_authority_after_create(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
) -> &str {
if fixture.master_edition_requested {
return fixture.master_edition.as_str();
}
return fixture.authority.as_str();
}
fn expected_token_account_state_after_mint(
asset_family: crate::MetaplexTokenMetadataAssetFamily,
) -> spl_token_interface::state::AccountState {
if asset_family == crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft {
return spl_token_interface::state::AccountState::Frozen;
}
return spl_token_interface::state::AccountState::Initialized;
}
fn postcondition_reads(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
query_role: &str,
min_context_slot: std::option::Option<u64>,
include_token_record: bool,
) -> std::vec::Vec<ks_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
let mint = ks_lib::MdPubkey(fixture.mint.clone());
let mut reads = vec![ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(fixture.metadata.clone()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
}];
if fixture.master_edition_requested {
reads.push(ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(fixture.master_edition.clone()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Edition { mint: mint.clone() },
min_context_slot,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
});
}
if include_token_record {
if let std::option::Option::Some(token_record) = fixture.token_record.as_ref() {
reads.push(ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(token_record.clone()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::TokenRecord {
mint,
token: ks_lib::MdPubkey(fixture.token_account.clone()),
},
min_context_slot,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
});
}
}
return reads;
}
fn validate_confirmed_execution(
step: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> ks_core::Result<u64> {
if !execution.simulation.success {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_simulation_failed",
format!("Metaplex {step} simulation failed"),
));
}
let confirmation = match execution.confirmation.as_ref() {
std::option::Option::Some(value)
if matches!(
value.status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
) =>
{
value
},
std::option::Option::Some(value) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_confirmation_incomplete",
format!("Metaplex {step} stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_confirmation_missing",
format!("Metaplex {step} has no confirmation evidence"),
));
},
};
let diagnostic = match execution.post_execution.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_post_execution_missing",
format!("Metaplex {step} has no post-execution diagnostic"),
));
},
};
if !diagnostic.canonical_inserted
|| !diagnostic.core_extracted
|| !diagnostic.decode_replayed
|| !diagnostic.materialized
|| execution.materializations.is_empty()
|| execution.materialized_snapshots.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_post_execution_incomplete",
format!(
"Metaplex {step} post-execution incomplete: signature={}, slot={:?}, canonical_inserted={}, core_extracted={}, decode_replayed={}, materialized={}, materialization_rows={}, materialized_snapshots={}, diagnostics={:?}",
confirmation.signature.0,
confirmation.slot,
diagnostic.canonical_inserted,
diagnostic.core_extracted,
diagnostic.decode_replayed,
diagnostic.materialized,
execution.materializations.len(),
execution.materialized_snapshots.len(),
diagnostic.diagnostics
),
));
}
let second_replay = match execution.idempotence_replay.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_idempotence_missing",
format!("Metaplex {step} has no idempotence replay evidence"),
));
},
};
if second_replay.failed_inputs != 0
|| second_replay.processing_error_inputs != 0
|| second_replay.processors.iter().any(|processor| {
return processor.failed != 0
|| processor.processing_errors != 0
|| processor.materialized_outputs != 0
|| processor.materialization_refused != 0;
})
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_idempotence_failed",
format!("Metaplex {step} second replay is not idempotent"),
));
}
return match confirmation.slot {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_confirmation_slot_missing",
format!("Metaplex {step} confirmation has no slot"),
)),
};
}
fn validate_expected_stateful_accounts(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
include_token_record: bool,
) -> ks_core::Result<()> {
let metadata = snapshots.iter().any(|value| {
return value.snapshot.account.0.as_str() == fixture.metadata.as_str()
&& value.snapshot.account_kind == "metadata"
&& value
.snapshot
.mint
.as_ref()
.is_some_and(|mint| return mint.0.as_str() == fixture.mint.as_str());
});
let edition = snapshots.iter().any(|value| {
return value.snapshot.account.0.as_str() == fixture.master_edition.as_str()
&& value.snapshot.account_kind == "edition"
&& value
.snapshot
.mint
.as_ref()
.is_some_and(|mint| return mint.0.as_str() == fixture.mint.as_str());
});
let token_record = fixture.token_record.as_ref().is_some_and(|expected| {
return snapshots.iter().any(|value| {
return value.snapshot.account.0.as_str() == expected.as_str()
&& value.snapshot.account_kind == "token_record"
&& value
.snapshot
.mint
.as_ref()
.is_some_and(|mint| return mint.0.as_str() == fixture.mint.as_str());
});
});
if !metadata
|| fixture.master_edition_requested != edition
|| (include_token_record && fixture.token_record.is_some() && !token_record)
|| (!include_token_record && token_record)
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_create_mint_stateful_postcondition_failed",
format!(
"Metaplex stateful accounts mismatch: metadata={metadata}, edition={edition}, token_record={token_record}, master_edition_requested={}, token_record_expected={}",
fixture.master_edition_requested,
include_token_record && fixture.token_record.is_some()
),
));
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
fn fixture(
family: crate::MetaplexTokenMetadataAssetFamily,
master_edition_requested: bool,
token_record: std::option::Option<&str>,
) -> crate::MetaplexCreateFixturePreparationSummary {
return crate::MetaplexCreateFixturePreparationSummary {
mint_keypair_path: std::path::PathBuf::from("mint.json"),
asset_family: family,
mint: "11111111111111111111111111111111".to_string(),
mint_decimals: if family == crate::MetaplexTokenMetadataAssetFamily::Fungible {
9
} else {
0
},
metadata: "SysvarC1ock11111111111111111111111111111111".to_string(),
master_edition: "SysvarRent111111111111111111111111111111111".to_string(),
master_edition_requested,
token_account: "SysvarS1otHashes111111111111111111111111111".to_string(),
token_record: token_record.map(|value| return value.to_string()),
collection_mint: std::option::Option::None,
authority: "11111111111111111111111111111111".to_string(),
mint_amount_raw: if family == crate::MetaplexTokenMetadataAssetFamily::Fungible {
1_000_000_000
} else {
1
},
operation_json: "{}".to_string(),
mint_operation_json: "{}".to_string(),
mint_created: true,
preparation_signature: std::option::Option::Some("signature".to_string()),
};
}
#[test]
fn campaign_contract_is_exactly_create_then_mint() {
assert_eq!(crate::metaplex_create_mint_campaign_operation_names(), &["create", "mint"],);
}
#[test]
fn stateful_read_inventory_tracks_master_edition_and_pnft_token_record() {
let nft =
fixture(crate::MetaplexTokenMetadataAssetFamily::Nft, true, std::option::Option::None);
let nft_create =
super::postcondition_reads(&nft, "rpc", std::option::Option::Some(7), false);
assert_eq!(nft_create.len(), 2);
assert!(
nft_create
.iter()
.all(|value| return value.min_context_slot == std::option::Option::Some(7))
);
assert!(nft_create.iter().all(|value| {
return value.max_data_bytes == ks_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES;
}));
let pnft = fixture(
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
true,
std::option::Option::Some("Stake11111111111111111111111111111111111111"),
);
let pnft_mint =
super::postcondition_reads(&pnft, "rpc", std::option::Option::Some(9), true);
assert_eq!(pnft_mint.len(), 3);
assert!(matches!(
&pnft_mint[2].kind,
ks_pipeline::MetaplexTokenMetadataAccountKind::TokenRecord { .. }
));
}
#[test]
fn campaign_supports_all_five_fixture_families() {
for family in [
crate::MetaplexTokenMetadataAssetFamily::Nft,
crate::MetaplexTokenMetadataAssetFamily::Sft,
crate::MetaplexTokenMetadataAssetFamily::Fungible,
crate::MetaplexTokenMetadataAssetFamily::Collection,
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
] {
let options = crate::MetaplexCreateFixturePreparationOptions::new(
std::path::PathBuf::from("wallets"),
)
.with_asset_family(family);
assert_eq!(options.asset_family, family);
}
}
fn family_from_env(value: &str) -> crate::MetaplexTokenMetadataAssetFamily {
return match value {
"nft" => crate::MetaplexTokenMetadataAssetFamily::Nft,
"sft" => crate::MetaplexTokenMetadataAssetFamily::Sft,
"fungible" => crate::MetaplexTokenMetadataAssetFamily::Fungible,
"collection" => crate::MetaplexTokenMetadataAssetFamily::Collection,
"programmable_nft" | "pnft" => crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
_ => panic!("unsupported KB_DEVNET_METAPLEX_CREATE_MINT_FAMILY `{value}`"),
};
}
fn execution_evidence_json(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> serde_json::Value {
let confirmation = execution.confirmation.as_ref();
let confirmation_status = confirmation
.map(|value| return format!("{:?}", value.status))
.unwrap_or_else(|| return "missing".to_string());
let confirmation_slot = confirmation.and_then(|value| return value.slot);
let signature = confirmation
.map(|value| return value.signature.0.clone())
.unwrap_or_else(|| return "missing".to_string());
let diagnostic = execution.post_execution.as_ref();
let idempotence_clean = execution.idempotence_replay.as_ref().is_some_and(|replay| {
return replay.failed_inputs == 0
&& replay.processing_error_inputs == 0
&& replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
});
return serde_json::json!({
"operation": execution.plan.operation_code.as_str(),
"cluster": execution.cluster,
"genesisHash": execution.genesis_hash.as_str(),
"simulationContextSlot": execution.simulation_context_slot,
"simulationSuccess": execution.simulation.success,
"simulationLogCount": execution.simulation.logs.len(),
"messageHash": execution.readiness.message_hash.as_str(),
"feeLamports": execution.fee.fee_lamports,
"signature": signature,
"confirmationStatus": confirmation_status,
"confirmationSlot": confirmation_slot,
"beforeSnapshots": execution.before.len(),
"afterSnapshots": execution.after.len(),
"canonicalHydration": diagnostic.is_some_and(|value| return value.canonical_inserted),
"coreExtraction": diagnostic.is_some_and(|value| return value.core_extracted),
"decodeReplay": diagnostic.is_some_and(|value| return value.decode_replayed),
"materialized": diagnostic.is_some_and(|value| return value.materialized),
"instructionMaterializations": execution.materializations.len(),
"materializedSnapshots": execution.materialized_snapshots.len(),
"idempotenceReplayClean": idempotence_clean,
});
}
#[test]
fn post_create_mint_authority_tracks_master_edition_only_for_nft_families() {
let authority = solana_pubkey::Pubkey::new_unique().to_string();
let master_edition = solana_pubkey::Pubkey::new_unique().to_string();
let families = [
(crate::MetaplexTokenMetadataAssetFamily::Nft, true),
(crate::MetaplexTokenMetadataAssetFamily::Sft, false),
(crate::MetaplexTokenMetadataAssetFamily::Fungible, false),
(crate::MetaplexTokenMetadataAssetFamily::Collection, true),
(crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft, true),
];
for (asset_family, master_edition_requested) in families {
let fixture = crate::MetaplexCreateFixturePreparationSummary {
mint_keypair_path: std::path::PathBuf::from("fixture.json"),
asset_family,
mint: solana_pubkey::Pubkey::new_unique().to_string(),
mint_decimals: 0,
metadata: solana_pubkey::Pubkey::new_unique().to_string(),
master_edition: master_edition.clone(),
master_edition_requested,
token_account: solana_pubkey::Pubkey::new_unique().to_string(),
token_record: std::option::Option::None,
collection_mint: std::option::Option::None,
authority: authority.clone(),
mint_amount_raw: 1,
operation_json: "{}".to_string(),
mint_operation_json: "{}".to_string(),
mint_created: true,
preparation_signature: std::option::Option::None,
};
assert_eq!(
super::expected_mint_authority_after_create(&fixture),
if master_edition_requested {
master_edition.as_str()
} else {
authority.as_str()
}
);
}
}
#[test]
fn post_mint_token_account_state_is_frozen_only_for_programmable_nft() {
let families = [
(
crate::MetaplexTokenMetadataAssetFamily::Nft,
spl_token_interface::state::AccountState::Initialized,
),
(
crate::MetaplexTokenMetadataAssetFamily::Sft,
spl_token_interface::state::AccountState::Initialized,
),
(
crate::MetaplexTokenMetadataAssetFamily::Fungible,
spl_token_interface::state::AccountState::Initialized,
),
(
crate::MetaplexTokenMetadataAssetFamily::Collection,
spl_token_interface::state::AccountState::Initialized,
),
(
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
spl_token_interface::state::AccountState::Frozen,
),
];
for (asset_family, expected_state) in families {
assert_eq!(
super::expected_token_account_state_after_mint(asset_family),
expected_state
);
}
}
#[tokio::test]
async fn optional_devnet_metaplex_create_mint_campaign_from_env() {
if std::env::var("KB_DEVNET_METAPLEX_CREATE_MINT_CAMPAIGN_TEST").ok().as_deref()
!= std::option::Option::Some("1")
{
return;
}
if std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref()
!= std::option::Option::Some("1")
{
panic!("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 is required for the spending campaign");
}
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
};
let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") {
std::result::Result::Ok(value) => {
let path = std::path::PathBuf::from(value);
if path.is_absolute() { path } else { workspace_root.join(path) }
},
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
};
let config = ks_config::read_config_json_file_with_environment(
config_path.as_path(),
workspace_root,
)
.unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}"));
let profile_name = std::env::var("KB_DEVNET_PROFILE").ok();
let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref())
.unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}"));
let database_url = std::env::var("KB_POSTGRES_TEST_URL")
.unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}"));
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
let pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}"));
let store_options = ks_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
)
.unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}"));
let store = ks_store::PostgresStore::connect(store_options)
.await
.unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}"));
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("PostgreSQL schema initialization failed: {error}");
}
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
workspace_root.join(configured_wallet_dir)
};
let family = std::env::var("KB_DEVNET_METAPLEX_CREATE_MINT_FAMILY")
.map(|value| return family_from_env(value.as_str()))
.unwrap_or(crate::MetaplexTokenMetadataAssetFamily::Nft);
let options = crate::MetaplexCreateFixturePreparationOptions::new(wallet_dir)
.with_asset_family(family);
let summary = crate::execute_devnet_metaplex_create_mint_campaign(
&pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| panic!("Metaplex Create -> Mint Devnet campaign failed: {error}"));
let create_confirmation = summary
.create
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("Create confirmation missing"));
let mint_confirmation = summary
.mint
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("Mint confirmation missing"));
println!(
"METAPLEX_CREATE_MINT_FIXTURE family={:?} mint={} metadata={} master_edition={} token_account={} token_record={} preparation_signature={}",
summary.asset_family,
summary.fixture.mint,
summary.fixture.metadata,
summary.fixture.master_edition,
summary.fixture.token_account,
summary.fixture.token_record.as_deref().unwrap_or("none"),
summary.fixture.preparation_signature.as_deref().unwrap_or("missing"),
);
println!(
"METAPLEX_CREATE_MINT_STEP operation={} signature={} slot={:?} materializations={}",
summary.create.plan.operation_code,
create_confirmation.signature.0,
create_confirmation.slot,
summary.create.materializations.len(),
);
println!(
"METAPLEX_CREATE_MINT_STEP operation={} signature={} slot={:?} materializations={} supply_before={} supply_after={} token_before={} token_after={} token_state_before={} token_state_after={}",
summary.mint.plan.operation_code,
mint_confirmation.signature.0,
mint_confirmation.slot,
summary.mint.materializations.len(),
summary.token_state.supply_before_mint,
summary.token_state.supply_after_mint,
summary.token_state.token_amount_before_mint,
summary.token_state.token_amount_after_mint,
summary.token_state.token_account_state_before_mint,
summary.token_state.token_account_state_after_mint,
);
println!(
"METAPLEX_CREATE_MINT_EVIDENCE family={:?} create={} mint={} token_state={}",
summary.asset_family,
execution_evidence_json(&summary.create),
execution_evidence_json(&summary.mint),
serde_json::json!({
"expectedAmountRaw": summary.token_state.expected_amount_raw,
"supplyBeforeMint": summary.token_state.supply_before_mint,
"supplyAfterMint": summary.token_state.supply_after_mint,
"tokenAmountBeforeMint": summary.token_state.token_amount_before_mint,
"tokenAmountAfterMint": summary.token_state.token_amount_after_mint,
"tokenAccountStateBeforeMint": summary.token_state.token_account_state_before_mint,
"tokenAccountStateAfterMint": summary.token_state.token_account_state_after_mint,
}),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,730 @@
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/maintenance_campaign.rs
// version: 4
//! Final bounded Devnet qualification campaign for current Metaplex maintenance operations.
const METAPLEX_FEE_DESTINATION: &str = "2fb1TjRrJQLy9BkYfBjcYgibV7LUsr9cf6QxvyRZyuXn";
const METAPLEX_OWNERLESS_CLOSE_DESTINATION: &str = "GxCXYtrnaU6JXeAza8Ugn4EE6QiFinpfn8t3Lo4UkBDX";
/// Complete evidence produced by the final bounded Metaplex maintenance campaign.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexMaintenanceCampaignSummary {
/// Fresh classic NFT used by the campaign.
pub fixture: crate::DevnetMetaplexCreateMintCampaignSummary,
/// Confirmed `Update` execution evidence.
pub update: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Simulation-only `Resize` evidence for a fresh already-resized NFT.
pub resize_probe: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Simulation-only `Migrate` evidence.
pub migrate_probe: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Simulation-only `Collect` evidence.
pub collect_probe: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Simulation-only `CloseAccounts` evidence.
pub close_accounts_probe: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Primary-sale flag before the current `Update` execution.
pub primary_sale_before_update: bool,
/// Primary-sale flag after the current `Update` execution.
pub primary_sale_after_update: bool,
}
/// Returns the exact ordered operations exercised by the maintenance campaign.
pub fn metaplex_maintenance_campaign_operation_names() -> &'static [&'static str; 7] {
return &["create", "mint", "update", "resize", "migrate", "collect", "close_accounts"];
}
/// Executes the final bounded Metaplex maintenance qualification campaign on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_maintenance_campaign<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> ks_core::Result<crate::DevnetMetaplexMaintenanceCampaignSummary>
where
S: ks_store::RawTransactionStore
+ ks_store::CoreExtractionStore
+ ks_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if options.asset_family != crate::MetaplexTokenMetadataAssetFamily::Nft {
return std::result::Result::Err(ks_core::Error::config(
"Metaplex maintenance campaign requires the classic NFT fixture family",
));
}
let fixture = match crate::execute_devnet_metaplex_create_mint_campaign(
http_pool,
store,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let primary_sale_before_update = match metadata_primary_sale(
fixture.mint.after.as_slice(),
fixture.fixture.metadata.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if primary_sale_before_update {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_initial_primary_sale_invalid",
"fresh maintenance NFT already has primary_sale_happened=true",
));
}
let update_operation = match update_operation(&fixture.fixture) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let update = match execute_confirmed_operation(
"update",
update_operation,
metadata_reads(&fixture.fixture, options.query_role.as_str()),
http_pool,
store,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let _update_slot = match validate_confirmed_execution("update", &update) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let primary_sale_after_update =
match metadata_primary_sale(update.after.as_slice(), fixture.fixture.metadata.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !primary_sale_after_update {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_update_state_invalid",
"confirmed Update did not flip primary_sale_happened to true",
));
}
let resize_operation = ks_lib::ExMetaplexTokenMetadataOperation::Resize {
metadata: ks_lib::MdPubkey(fixture.fixture.metadata.clone()),
edition: ks_lib::MdPubkey(fixture.fixture.master_edition.clone()),
mint: ks_lib::MdPubkey(fixture.fixture.mint.clone()),
payer: ks_lib::MdPubkey(fixture.fixture.authority.clone()),
authority: std::option::Option::None,
token: std::option::Option::Some(ks_lib::MdPubkey(fixture.fixture.token_account.clone())),
system_program: ks_lib::MdPubkey(ks_program_ids::SYSTEM_PROGRAM_ID.to_string()),
};
let resize_probe = match simulate_expected_unavailable(
"resize",
resize_operation,
201,
http_pool,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let migrate_probe = match simulate_expected_unavailable(
"migrate",
migrate_operation(&fixture.fixture),
75,
http_pool,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let collect_probe = match simulate_expected_unavailable(
"collect",
ks_lib::ExMetaplexTokenMetadataOperation::Collect {
authority: ks_lib::MdPubkey(fixture.fixture.authority.clone()),
recipient: ks_lib::MdPubkey(METAPLEX_FEE_DESTINATION.to_string()),
},
7,
http_pool,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let close_accounts_probe = match simulate_expected_unavailable(
"close_accounts",
ks_lib::ExMetaplexTokenMetadataOperation::CloseAccounts {
metadata: ks_lib::MdPubkey(fixture.fixture.metadata.clone()),
edition: ks_lib::MdPubkey(fixture.fixture.master_edition.clone()),
mint: ks_lib::MdPubkey(fixture.fixture.mint.clone()),
authority: ks_lib::MdPubkey(fixture.fixture.authority.clone()),
destination: ks_lib::MdPubkey(METAPLEX_OWNERLESS_CLOSE_DESTINATION.to_string()),
},
188,
http_pool,
profile,
workspace_root,
options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::DevnetMetaplexMaintenanceCampaignSummary {
fixture,
update,
resize_probe,
migrate_probe,
collect_probe,
close_accounts_probe,
primary_sale_before_update,
primary_sale_after_update,
});
}
fn update_operation(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
) -> ks_core::Result<ks_lib::ExMetaplexTokenMetadataOperation> {
let value = serde_json::json!({
"operation": "update_as_update_authority_v2",
"authority": fixture.authority.as_str(),
"mint": fixture.mint.as_str(),
"metadata": fixture.metadata.as_str(),
"edition": fixture.master_edition.as_str(),
"token": fixture.token_account.as_str(),
"authorization_rules_program": null,
"authorization_rules": null,
"update_args": {
"AsUpdateAuthorityV2": {
"new_update_authority": null,
"data": null,
"primary_sale_happened": true,
"is_mutable": null,
"collection": "None",
"collection_details": "None",
"uses": "None",
"rule_set": "None",
"token_standard": null,
"authorization_data": null
}
}
});
return match serde_json::from_value::<ks_lib::ExMetaplexTokenMetadataOperation>(value) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::config(
format!("Metaplex maintenance Update operation is invalid: {error}"),
)),
};
}
fn migrate_operation(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
) -> ks_lib::ExMetaplexTokenMetadataOperation {
return ks_lib::ExMetaplexTokenMetadataOperation::Migrate {
metadata: ks_lib::MdPubkey(fixture.metadata.clone()),
edition: ks_lib::MdPubkey(fixture.master_edition.clone()),
token: ks_lib::MdPubkey(fixture.token_account.clone()),
token_owner: ks_lib::MdPubkey(fixture.authority.clone()),
mint: ks_lib::MdPubkey(fixture.mint.clone()),
payer: ks_lib::MdPubkey(fixture.authority.clone()),
authority: ks_lib::MdPubkey(fixture.authority.clone()),
collection_metadata: ks_lib::MdPubkey(fixture.metadata.clone()),
delegate_record: ks_lib::MdPubkey(fixture.metadata.clone()),
token_record: ks_lib::MdPubkey(fixture.metadata.clone()),
system_program: ks_lib::MdPubkey(ks_program_ids::SYSTEM_PROGRAM_ID.to_string()),
sysvar_instructions: ks_lib::MdPubkey(
ks_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID.to_string(),
),
spl_token_program: ks_lib::MdPubkey(ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
authorization_rules_program: std::option::Option::None,
authorization_rules: std::option::Option::None,
};
}
#[allow(clippy::too_many_arguments)]
async fn execute_confirmed_operation<S, O>(
label: &str,
operation: ks_lib::ExMetaplexTokenMetadataOperation,
reads: std::vec::Vec<ks_pipeline::MetaplexTokenMetadataStatefulReadRequest>,
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> ks_core::Result<crate::DevnetMetaplexTokenMetadataExecutionSummary>
where
S: ks_store::RawTransactionStore
+ ks_store::CoreExtractionStore
+ ks_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
let decoders: std::vec::Vec<std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(ks_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>> = std::vec![
std::sync::Arc::new(ks_lib::MtAdminMaterializer),
std::sync::Arc::new(ks_lib::MtComplianceAuditMaterializer),
std::sync::Arc::new(ks_lib::MtLifecycleMaterializer),
std::sync::Arc::new(ks_lib::MtMetadataMetaplexTokenMetadataMaterializer),
std::sync::Arc::new(ks_lib::MtRiskMaterializer),
];
let mut request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-maintenance-{label}-{}", uuid::Uuid::new_v4()),
operation,
);
request.query_role = options.query_role.clone();
request.transaction_role = options.transaction_role.clone();
request.preflight_reads = reads.clone();
request.postcondition_reads = reads;
request.submit = true;
request.operator_confirmed = true;
request.materialize_after_confirmation = true;
request.post_validation_max_retries = 20;
return crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await;
}
#[allow(clippy::too_many_arguments)]
async fn simulate_expected_unavailable<O>(
label: &str,
operation: ks_lib::ExMetaplexTokenMetadataOperation,
expected_custom_error: u64,
http_pool: &ks_onchain_transport::HttpEndpointPool,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> ks_core::Result<crate::DevnetMetaplexTokenMetadataExecutionSummary>
where
O: crate::SolanaExecutionObserver,
{
let mut request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-maintenance-{label}-probe-{}", uuid::Uuid::new_v4()),
operation,
);
request.query_role = options.query_role.clone();
request.transaction_role = options.transaction_role.clone();
let summary = match crate::simulate_devnet_metaplex_token_metadata(
http_pool,
profile,
workspace_root,
&request,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if summary.simulation.success || summary.send_result.is_some() || summary.confirmation.is_some()
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_unavailable_probe_unexpected_success",
format!("Metaplex {label} probe unexpectedly became submit-capable or confirmed"),
));
}
let observed_custom_error = simulation_custom_error(summary.simulation.error.as_deref());
if observed_custom_error != std::option::Option::Some(expected_custom_error) {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_unavailable_probe_error_mismatch",
format!(
"Metaplex {label} probe expected custom error {expected_custom_error}; got custom_error={observed_custom_error:?}, error={:?}, logs={:?}",
summary.simulation.error, summary.simulation.logs
),
));
}
return std::result::Result::Ok(summary);
}
fn simulation_custom_error(error: std::option::Option<&str>) -> std::option::Option<u64> {
let error = match error {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let value = match serde_json::from_str::<serde_json::Value>(error) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let instruction_error =
match value.get("InstructionError").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) if value.len() == 2 => value,
_ => return std::option::Option::None,
};
let detail = match instruction_error.get(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
return detail.get("Custom").and_then(serde_json::Value::as_u64);
}
fn metadata_reads(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
query_role: &str,
) -> std::vec::Vec<ks_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
return std::vec![ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(fixture.metadata.clone()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot: std::option::Option::None,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
}];
}
fn metadata_primary_sale(
snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
metadata: &str,
) -> ks_core::Result<bool> {
let snapshot = match snapshots.iter().find(|value| {
return value.snapshot.account.0.as_str() == metadata
&& value.snapshot.account_kind == "metadata";
}) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_metadata_snapshot_missing",
format!("metadata snapshot {metadata} is missing"),
));
},
};
let value = snapshot
.snapshot
.payload_json
.get("primary_sale_happened")
.or_else(|| return snapshot.snapshot.payload_json.get("primarySaleHappened"));
return match value.and_then(serde_json::Value::as_bool) {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_primary_sale_missing",
"metadata snapshot has no boolean primary-sale field",
)),
};
}
fn validate_confirmed_execution(
label: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> ks_core::Result<u64> {
let confirmation = match execution.confirmation.as_ref() {
std::option::Option::Some(value)
if matches!(
value.status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
) =>
{
value
},
std::option::Option::Some(value) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_confirmation_incomplete",
format!("Metaplex {label} stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_confirmation_missing",
format!("Metaplex {label} has no confirmation evidence"),
));
},
};
let diagnostic = match execution.post_execution.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_post_execution_missing",
format!("Metaplex {label} has no post-execution diagnostic"),
));
},
};
if !diagnostic.canonical_inserted
|| !diagnostic.core_extracted
|| !diagnostic.decode_replayed
|| !diagnostic.materialized
|| execution.materializations.is_empty()
|| execution.materialized_snapshots.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_post_execution_incomplete",
format!(
"Metaplex {label} post-execution incomplete: signature={}, slot={:?}, canonical_inserted={}, core_extracted={}, decode_replayed={}, materialized={}, materialization_rows={}, materialized_snapshots={}, diagnostics={:?}",
confirmation.signature.0,
confirmation.slot,
diagnostic.canonical_inserted,
diagnostic.core_extracted,
diagnostic.decode_replayed,
diagnostic.materialized,
execution.materializations.len(),
execution.materialized_snapshots.len(),
diagnostic.diagnostics
),
));
}
let idempotence = match execution.idempotence_replay.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_idempotence_missing",
format!("Metaplex {label} has no idempotence replay evidence"),
));
},
};
if idempotence.failed_inputs != 0
|| idempotence.processing_error_inputs != 0
|| idempotence.processors.iter().any(|processor| {
return processor.failed != 0
|| processor.processing_errors != 0
|| processor.materialized_outputs != 0
|| processor.materialization_refused != 0;
})
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_idempotence_failed",
format!("Metaplex {label} second replay is not idempotent"),
));
}
return match confirmation.slot {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_maintenance_confirmation_slot_missing",
format!("Metaplex {label} confirmation has no slot"),
)),
};
}
#[cfg(test)]
mod tests {
#[test]
fn maintenance_campaign_contract_is_exact() {
assert_eq!(
crate::metaplex_maintenance_campaign_operation_names(),
&["create", "mint", "update", "resize", "migrate", "collect", "close_accounts"],
);
for custom_error in [201_u64, 75_u64, 7_u64, 188_u64] {
let error = format!(r#"{{"InstructionError":[0,{{"Custom":{custom_error}}}]}}"#);
assert_eq!(
super::simulation_custom_error(std::option::Option::Some(error.as_str())),
std::option::Option::Some(custom_error),
);
}
assert_eq!(
super::simulation_custom_error(std::option::Option::Some(
r#"{"InstructionError":[0,"InvalidInstructionData"]}"#,
)),
std::option::Option::None,
);
}
#[test]
fn maintenance_reserved_destinations_are_exact() {
assert_eq!(super::METAPLEX_FEE_DESTINATION, "2fb1TjRrJQLy9BkYfBjcYgibV7LUsr9cf6QxvyRZyuXn");
assert_eq!(
super::METAPLEX_OWNERLESS_CLOSE_DESTINATION,
"GxCXYtrnaU6JXeAza8Ugn4EE6QiFinpfn8t3Lo4UkBDX",
);
}
#[tokio::test]
async fn optional_devnet_metaplex_maintenance_campaign_from_env() {
if std::env::var("KB_DEVNET_METAPLEX_MAINTENANCE_CAMPAIGN_TEST").ok().as_deref()
!= std::option::Option::Some("1")
{
return;
}
assert_eq!(
std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref(),
std::option::Option::Some("1"),
"set KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 before the maintenance campaign"
);
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
};
let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") {
std::result::Result::Ok(value) => {
let path = std::path::PathBuf::from(value);
if path.is_absolute() { path } else { workspace_root.join(path) }
},
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
};
let config = ks_config::read_config_json_file_with_environment(
config_path.as_path(),
workspace_root,
)
.unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}"));
let profile_name = std::env::var("KB_DEVNET_PROFILE").ok();
let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref())
.unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}"));
let database_url = std::env::var("KB_POSTGRES_TEST_URL")
.unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}"));
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
let http_pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool initialization failed: {error}"));
let store_options = ks_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
)
.unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}"));
let store = ks_store::PostgresStore::connect(store_options)
.await
.unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}"));
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("PostgreSQL schema initialization failed: {error}");
}
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
workspace_root.join(configured_wallet_dir)
};
let options = crate::MetaplexCreateFixturePreparationOptions::new(wallet_dir)
.with_asset_family(crate::MetaplexTokenMetadataAssetFamily::Nft);
let summary = crate::execute_devnet_metaplex_maintenance_campaign(
&http_pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| panic!("Metaplex maintenance Devnet campaign failed: {error}"));
println!(
"METAPLEX_MAINTENANCE_FIXTURE mint={} metadata={} master_edition={} token={}",
summary.fixture.fixture.mint,
summary.fixture.fixture.metadata,
summary.fixture.fixture.master_edition,
summary.fixture.fixture.token_account,
);
print_confirmed_step("update", &summary.update);
println!(
"METAPLEX_MAINTENANCE_STATE primary_sale_before={} primary_sale_after={}",
summary.primary_sale_before_update, summary.primary_sale_after_update,
);
print_probe("resize", 201, &summary.resize_probe);
print_probe("migrate", 75, &summary.migrate_probe);
print_probe("collect", 7, &summary.collect_probe);
print_probe("close_accounts", 188, &summary.close_accounts_probe);
println!(
"METAPLEX_MAINTENANCE_EVIDENCE update={}",
execution_evidence_json(&summary.update),
);
}
fn print_confirmed_step(
label: &str,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) {
let confirmation = execution
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("{label} confirmation is missing"));
println!(
"METAPLEX_MAINTENANCE_STEP label={} operation={} signature={} slot={:?} materializations={}",
label,
execution.plan.operation_code,
confirmation.signature.0,
confirmation.slot,
execution.materializations.len(),
);
}
fn print_probe(
label: &str,
expected_custom_error: u64,
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) {
println!(
"METAPLEX_MAINTENANCE_PROBE label={} operation={} success={} context_slot={} expected_custom_error={} error={} logs={}",
label,
execution.plan.operation_code,
execution.simulation.success,
execution.simulation_context_slot,
expected_custom_error,
serde_json::to_string(&execution.simulation.error).unwrap_or_else(|error| panic!(
"{label} probe error serialization failed: {error}"
)),
execution.simulation.logs.len(),
);
println!(
"METAPLEX_MAINTENANCE_PROBE_LOGS label={} {}",
label,
serde_json::to_string(&execution.simulation.logs)
.unwrap_or_else(|error| panic!("{label} probe log serialization failed: {error}")),
);
}
fn execution_evidence_json(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> serde_json::Value {
let confirmation = execution.confirmation.as_ref();
let post_execution = execution.post_execution.as_ref();
let idempotence_clean = execution.idempotence_replay.as_ref().is_some_and(|replay| {
return replay.failed_inputs == 0
&& replay.processing_error_inputs == 0
&& replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
});
return serde_json::json!({
"operation": execution.plan.operation_code.as_str(),
"cluster": execution.cluster,
"genesisHash": execution.genesis_hash.as_str(),
"simulationContextSlot": execution.simulation_context_slot,
"simulationSuccess": execution.simulation.success,
"simulationLogCount": execution.simulation.logs.len(),
"messageHash": execution.readiness.message_hash.as_str(),
"feeLamports": execution.fee.fee_lamports,
"signature": confirmation.map(|value| return value.signature.0.clone()),
"confirmationStatus": confirmation.map(|value| return format!("{:?}", value.status)),
"confirmationSlot": confirmation.and_then(|value| return value.slot),
"beforeSnapshots": execution.before.len(),
"afterSnapshots": execution.after.len(),
"canonicalHydration": post_execution.is_some_and(|value| return value.canonical_inserted),
"coreExtraction": post_execution.is_some_and(|value| return value.core_extracted),
"decodeReplay": post_execution.is_some_and(|value| return value.decode_replayed),
"materialized": post_execution.is_some_and(|value| return value.materialized),
"instructionMaterializations": execution.materializations.len(),
"materializedSnapshots": execution.materialized_snapshots.len(),
"idempotenceReplayClean": idempotence_clean,
});
}
}

View File

@@ -0,0 +1,404 @@
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/scenarios.rs
// version: 9
//! 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,
/// Metadata is immutable after a confirmed update-authority transition.
MetadataImmutable,
/// 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![
ks_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
ks_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
ks_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![
ks_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
ks_lib::EX_METAPLEX_TOKEN_METADATA_MINT_OPERATION.to_string(),
ks_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
],
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![
ks_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
ks_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::CollectionMemberUnverified,
vec![
ks_lib::EX_METAPLEX_TOKEN_METADATA_VERIFY_OPERATION.to_string(),
ks_lib::EX_METAPLEX_TOKEN_METADATA_UNVERIFY_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![
ks_lib::EX_METAPLEX_TOKEN_METADATA_DELEGATE_OPERATION.to_string(),
ks_lib::EX_METAPLEX_TOKEN_METADATA_LOCK_OPERATION.to_string(),
ks_lib::EX_METAPLEX_TOKEN_METADATA_UNLOCK_OPERATION.to_string(),
ks_lib::EX_METAPLEX_TOKEN_METADATA_TRANSFER_OPERATION.to_string(),
ks_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 definitions = [
(
"metaplex_nft_lifecycle_devnet",
"NFT classique : création, mise à jour puis immutabilité",
crate::MetaplexTokenMetadataAssetFamily::Nft,
crate::MetaplexTokenMetadataFixtureKind::ClassicNftMint,
false,
false,
&["metadata_state", "edition_state"][..],
),
(
"metaplex_sft_create_update_devnet",
"SFT : création confirmée puis mise à jour",
crate::MetaplexTokenMetadataAssetFamily::Sft,
crate::MetaplexTokenMetadataFixtureKind::SemiFungibleMint,
false,
false,
&["metadata_state"][..],
),
(
"metaplex_fungible_create_update_devnet",
"Fungible : création confirmée puis mise à jour",
crate::MetaplexTokenMetadataAssetFamily::Fungible,
crate::MetaplexTokenMetadataFixtureKind::FungibleMint,
false,
false,
&["metadata_state"][..],
),
(
"metaplex_collection_create_update_devnet",
"Collection parent : création confirmée puis mise à jour",
crate::MetaplexTokenMetadataAssetFamily::Collection,
crate::MetaplexTokenMetadataFixtureKind::CollectionParentAndMember,
true,
false,
&["metadata_state", "edition_state"][..],
),
(
"metaplex_pnft_create_update_devnet",
"pNFT sans rule set : création confirmée puis mise à jour",
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
crate::MetaplexTokenMetadataFixtureKind::ProgrammableNftTokenRecord,
false,
false,
&["metadata_state", "edition_state"][..],
),
];
return definitions
.into_iter()
.map(
|(
id,
label,
asset_family,
fixture_kind,
requires_collection,
requires_programmable_rules,
materialization_targets,
)| {
let mut value = scenario(
id,
label,
asset_family,
fixture_kind,
crate::MetaplexTokenMetadataFixtureState::MintPrepared,
if asset_family == crate::MetaplexTokenMetadataAssetFamily::Nft {
crate::MetaplexTokenMetadataFixtureState::MetadataImmutable
} else {
crate::MetaplexTokenMetadataFixtureState::MetadataCreated
},
if asset_family == crate::MetaplexTokenMetadataAssetFamily::Nft {
vec![
ks_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
ks_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
ks_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
]
} else {
vec![
ks_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
ks_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION.to_string(),
]
},
requires_collection,
requires_programmable_rules,
materialization_targets,
);
value.mode = crate::MetaplexTokenMetadataScenarioMode::NetworkSimulation;
return value;
},
)
.collect();
}
#[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_journey_uses_only_current_verify_and_unverify_operations() {
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());
if let std::option::Option::Some(scenario) = collection {
assert_eq!(
scenario.operation_codes,
vec![
ks_lib::EX_METAPLEX_TOKEN_METADATA_VERIFY_OPERATION.to_string(),
ks_lib::EX_METAPLEX_TOKEN_METADATA_UNVERIFY_OPERATION.to_string(),
]
);
assert_eq!(
scenario.resulting_state,
crate::MetaplexTokenMetadataFixtureState::CollectionMemberUnverified
);
}
}
#[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(), 5);
let families = devnet
.iter()
.map(|scenario| return scenario.asset_family)
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(families.len(), 5);
for scenario in devnet {
let expected_len =
if scenario.asset_family == crate::MetaplexTokenMetadataAssetFamily::Nft {
3
} else {
2
};
assert_eq!(scenario.operation_codes.len(), expected_len);
assert_eq!(
scenario.operation_codes[0],
ks_lib::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION
);
assert!(
scenario.operation_codes[1..].iter().all(|operation| return operation
== ks_lib::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION)
);
assert_eq!(scenario.mode, crate::MetaplexTokenMetadataScenarioMode::NetworkSimulation);
}
}
}

View File

@@ -0,0 +1,574 @@
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/use_campaign.rs
// version: 2
//! Bounded Devnet availability probe for the current Metaplex `Use` surface.
/// Outcome of one bounded Devnet `Use` availability probe.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DevnetMetaplexUseProbeStatus {
/// The current `Use` instruction was confirmed with complete pipeline evidence.
Confirmed,
/// The exact current `Use` instruction was rejected by Devnet simulation.
RuntimeUnavailable,
}
/// Complete evidence produced by one current Metaplex `Use` availability probe.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMetaplexUseProbeSummary {
/// Fresh classic NFT prepared with two bounded `Multiple` uses.
pub fixture: crate::DevnetMetaplexCreateMintCampaignSummary,
/// Exact simulation evidence for the current `Use` instruction.
pub simulation: crate::DevnetMetaplexTokenMetadataExecutionSummary,
/// Confirmed execution evidence when the runtime accepts the current instruction.
pub execution: std::option::Option<crate::DevnetMetaplexTokenMetadataExecutionSummary>,
/// Runtime availability classification produced by this probe.
pub status: crate::DevnetMetaplexUseProbeStatus,
/// Remaining uses observed immediately before the `Use` probe.
pub uses_before: u64,
/// Remaining uses observed after confirmed execution, when available.
pub uses_after: std::option::Option<u64>,
/// Exact bounded simulation failure diagnostic when runtime-unavailable.
pub unavailable_reason: std::option::Option<std::string::String>,
}
/// Returns the exact ordered operations exercised by the bounded `Use` probe.
pub fn metaplex_use_probe_operation_names() -> &'static [&'static str; 3] {
return &["create", "mint", "use"];
}
/// Executes one fresh classic NFT `Use` availability probe on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_metaplex_use_probe<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::MetaplexCreateFixturePreparationOptions,
observer: &O,
) -> ks_core::Result<crate::DevnetMetaplexUseProbeSummary>
where
S: ks_store::RawTransactionStore
+ ks_store::CoreExtractionStore
+ ks_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if options.asset_family != crate::MetaplexTokenMetadataAssetFamily::Nft {
return std::result::Result::Err(ks_core::Error::config(
"Metaplex Use probe requires the classic NFT fixture family",
));
}
let fixture_options = options.clone().with_multiple_uses(2);
let fixture = match crate::execute_devnet_metaplex_create_mint_campaign(
http_pool,
store,
profile,
workspace_root,
&fixture_options,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let uses_before = match metadata_remaining_multiple_uses(
fixture.mint.after.as_slice(),
fixture.fixture.metadata.as_str(),
2,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if uses_before != 2 {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_initial_uses_invalid",
format!("Metaplex Use probe requires remaining=2 before execution; got {uses_before}"),
));
}
let operation = use_operation(&fixture.fixture);
let mut simulation_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-use-probe-simulation-{}", uuid::Uuid::new_v4()),
operation.clone(),
);
simulation_request.query_role = fixture_options.query_role.clone();
simulation_request.transaction_role = fixture_options.transaction_role.clone();
let simulation = match crate::simulate_devnet_metaplex_token_metadata(
http_pool,
profile,
workspace_root,
&simulation_request,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !simulation.simulation.success {
return std::result::Result::Ok(crate::DevnetMetaplexUseProbeSummary {
fixture,
unavailable_reason: std::option::Option::Some(crate::simulation_failure_message(
&simulation.simulation,
)),
simulation,
execution: std::option::Option::None,
status: crate::DevnetMetaplexUseProbeStatus::RuntimeUnavailable,
uses_before,
uses_after: std::option::Option::None,
});
}
let decoders: std::vec::Vec<std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(ks_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>> = std::vec![
std::sync::Arc::new(ks_lib::MtAdminMaterializer),
std::sync::Arc::new(ks_lib::MtComplianceAuditMaterializer),
std::sync::Arc::new(ks_lib::MtLifecycleMaterializer),
std::sync::Arc::new(ks_lib::MtMetadataMetaplexTokenMetadataMaterializer),
std::sync::Arc::new(ks_lib::MtRiskMaterializer),
];
let mut execution_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
format!("metaplex-use-probe-submit-{}", uuid::Uuid::new_v4()),
operation,
);
execution_request.query_role = fixture_options.query_role.clone();
execution_request.transaction_role = fixture_options.transaction_role.clone();
execution_request.submit = true;
execution_request.operator_confirmed = true;
execution_request.materialize_after_confirmation = true;
execution_request.post_validation_max_retries = 20;
execution_request.postcondition_reads =
metadata_read(fixture.fixture.metadata.as_str(), fixture_options.query_role.as_str());
let execution = match crate::execute_devnet_metaplex_token_metadata(
http_pool,
store,
profile,
workspace_root,
&execution_request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmation_slot = match validate_confirmed_use_execution(&execution) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let uses_after = match metadata_remaining_multiple_uses(
execution.after.as_slice(),
fixture.fixture.metadata.as_str(),
2,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if uses_after != 1 {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_remaining_uses_invalid",
format!(
"confirmed Metaplex Use must decrement remaining uses from 2 to 1; got {uses_after}"
),
));
}
let token_after = match crate::read_token_account_at_or_after(
http_pool,
fixture_options.query_role.as_str(),
fixture.fixture.token_account.as_str(),
std::option::Option::Some(confirmation_slot),
)
.await
{
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_token_missing",
"classic NFT token account disappeared after confirmed Use",
));
},
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = crate::validate_classic_fixture_token_account_state(
&token_after,
fixture.fixture.mint.as_str(),
fixture.fixture.authority.as_str(),
1,
spl_token_interface::state::AccountState::Initialized,
) {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_token_state_invalid",
format!("confirmed Metaplex Use left invalid NFT token state: {error}"),
));
}
return std::result::Result::Ok(crate::DevnetMetaplexUseProbeSummary {
fixture,
simulation,
execution: std::option::Option::Some(execution),
status: crate::DevnetMetaplexUseProbeStatus::Confirmed,
uses_before,
uses_after: std::option::Option::Some(uses_after),
unavailable_reason: std::option::Option::None,
});
}
fn use_operation(
fixture: &crate::MetaplexCreateFixturePreparationSummary,
) -> ks_lib::ExMetaplexTokenMetadataOperation {
return ks_lib::ExMetaplexTokenMetadataOperation::Use {
authority: ks_lib::MdPubkey(fixture.authority.clone()),
delegate_record: std::option::Option::None,
token: std::option::Option::Some(ks_lib::MdPubkey(fixture.token_account.clone())),
mint: ks_lib::MdPubkey(fixture.mint.clone()),
metadata: ks_lib::MdPubkey(fixture.metadata.clone()),
edition: std::option::Option::Some(ks_lib::MdPubkey(fixture.master_edition.clone())),
payer: ks_lib::MdPubkey(fixture.authority.clone()),
system_program: ks_lib::MdPubkey(ks_program_ids::SYSTEM_PROGRAM_ID.to_string()),
sysvar_instructions: ks_lib::MdPubkey(
ks_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID.to_string(),
),
spl_token_program: std::option::Option::Some(ks_lib::MdPubkey(
ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
)),
authorization_rules_program: std::option::Option::None,
authorization_rules: std::option::Option::None,
args: mpl_token_metadata::types::UseArgs::V1 {
authorization_data: std::option::Option::None,
},
};
}
fn metadata_read(
metadata: &str,
query_role: &str,
) -> std::vec::Vec<ks_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
return std::vec![ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account: ks_lib::MdPubkey(metadata.to_string()),
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
min_context_slot: std::option::Option::None,
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
}];
}
fn metadata_remaining_multiple_uses(
snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
metadata: &str,
expected_total: u64,
) -> ks_core::Result<u64> {
let snapshot = match snapshots.iter().find(|value| {
return value.snapshot.account.0.as_str() == metadata
&& value.snapshot.account_kind == "metadata";
}) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_metadata_snapshot_missing",
format!("metadata snapshot {metadata} is missing from the Use probe"),
));
},
};
let uses_value = match snapshot.snapshot.payload_json.get("uses") {
std::option::Option::Some(value) if !value.is_null() => value.clone(),
_ => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_uses_missing",
"metadata snapshot has no Uses state",
));
},
};
let uses = match serde_json::from_value::<mpl_token_metadata::types::Uses>(uses_value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_uses_decode_failed",
format!("metadata Uses projection is invalid: {error}"),
));
},
};
if uses.use_method != mpl_token_metadata::types::UseMethod::Multiple
|| uses.total != expected_total
|| uses.remaining > uses.total
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_uses_state_invalid",
format!(
"metadata Uses state must remain Multiple with total={expected_total}; got method={:?}, remaining={}, total={}",
uses.use_method, uses.remaining, uses.total
),
));
}
return std::result::Result::Ok(uses.remaining);
}
fn validate_confirmed_use_execution(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> ks_core::Result<u64> {
let confirmation = match execution.confirmation.as_ref() {
std::option::Option::Some(value)
if matches!(
value.status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
) =>
{
value
},
std::option::Option::Some(value) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_confirmation_incomplete",
format!("Metaplex Use stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_confirmation_missing",
"Metaplex Use has no confirmation evidence",
));
},
};
let diagnostic = match execution.post_execution.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_post_execution_missing",
"Metaplex Use has no post-execution diagnostic",
));
},
};
if !diagnostic.canonical_inserted
|| !diagnostic.core_extracted
|| !diagnostic.decode_replayed
|| !diagnostic.materialized
|| execution.materializations.is_empty()
|| execution.materialized_snapshots.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_post_execution_incomplete",
format!(
"Metaplex Use post-execution incomplete: signature={}, slot={:?}, canonical_inserted={}, core_extracted={}, decode_replayed={}, materialized={}, materialization_rows={}, materialized_snapshots={}, diagnostics={:?}",
confirmation.signature.0,
confirmation.slot,
diagnostic.canonical_inserted,
diagnostic.core_extracted,
diagnostic.decode_replayed,
diagnostic.materialized,
execution.materializations.len(),
execution.materialized_snapshots.len(),
diagnostic.diagnostics
),
));
}
let idempotence = match execution.idempotence_replay.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_idempotence_missing",
"Metaplex Use has no idempotence replay evidence",
));
},
};
if idempotence.failed_inputs != 0
|| idempotence.processing_error_inputs != 0
|| idempotence.processors.iter().any(|processor| {
return processor.failed != 0
|| processor.processing_errors != 0
|| processor.materialized_outputs != 0
|| processor.materialization_refused != 0;
})
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_idempotence_failed",
"Metaplex Use second replay is not idempotent",
));
}
return match confirmation.slot {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
"metaplex_use_probe_confirmation_slot_missing",
"Metaplex Use confirmation has no slot",
)),
};
}
#[cfg(test)]
mod tests {
#[test]
fn use_probe_contract_is_exactly_create_mint_then_use() {
assert_eq!(crate::metaplex_use_probe_operation_names(), &["create", "mint", "use"]);
assert_ne!(
crate::DevnetMetaplexUseProbeStatus::Confirmed,
crate::DevnetMetaplexUseProbeStatus::RuntimeUnavailable,
);
}
#[tokio::test]
async fn optional_devnet_metaplex_use_probe_from_env() {
if std::env::var("KB_DEVNET_METAPLEX_USE_PROBE_TEST").ok().as_deref()
!= std::option::Option::Some("1")
{
return;
}
assert_eq!(
std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref(),
std::option::Option::Some("1"),
"set KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 before submitting an accepted Use probe"
);
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
};
let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") {
std::result::Result::Ok(value) => {
let path = std::path::PathBuf::from(value);
if path.is_absolute() { path } else { workspace_root.join(path) }
},
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
};
let config = ks_config::read_config_json_file_with_environment(
config_path.as_path(),
workspace_root,
)
.unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}"));
let profile_name = std::env::var("KB_DEVNET_PROFILE").ok();
let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref())
.unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}"));
let database_url = std::env::var("KB_POSTGRES_TEST_URL")
.unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}"));
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
let http_pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool initialization failed: {error}"));
let store_options = ks_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
)
.unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}"));
let store = ks_store::PostgresStore::connect(store_options)
.await
.unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}"));
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("PostgreSQL schema initialization failed: {error}");
}
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
workspace_root.join(configured_wallet_dir)
};
let options = crate::MetaplexCreateFixturePreparationOptions::new(wallet_dir)
.with_asset_family(crate::MetaplexTokenMetadataAssetFamily::Nft);
let summary = crate::execute_devnet_metaplex_use_probe(
&http_pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| panic!("Metaplex Use Devnet probe failed: {error}"));
println!(
"METAPLEX_USE_PROBE_FIXTURE mint={} metadata={} master_edition={} token={} uses_before={}",
summary.fixture.fixture.mint,
summary.fixture.fixture.metadata,
summary.fixture.fixture.master_edition,
summary.fixture.fixture.token_account,
summary.uses_before,
);
println!(
"METAPLEX_USE_PROBE_SIMULATION success={} context_slot={} logs={} error={}",
summary.simulation.simulation.success,
summary.simulation.simulation_context_slot,
summary.simulation.simulation.logs.len(),
serde_json::to_string(&summary.simulation.simulation.error).unwrap_or_else(
|error| panic!("Use simulation error serialization failed: {error}")
),
);
match summary.status {
crate::DevnetMetaplexUseProbeStatus::RuntimeUnavailable => {
println!(
"METAPLEX_USE_PROBE_STATUS status=runtime_unavailable uses_before={} uses_after=null reason={}",
summary.uses_before,
serde_json::to_string(&summary.unavailable_reason).unwrap_or_else(
|error| panic!("Use unavailable reason serialization failed: {error}")
),
);
println!(
"METAPLEX_USE_PROBE_LOGS {}",
serde_json::to_string(&summary.simulation.simulation.logs).unwrap_or_else(
|error| panic!("Use simulation log serialization failed: {error}")
),
);
},
crate::DevnetMetaplexUseProbeStatus::Confirmed => {
let execution = summary
.execution
.as_ref()
.unwrap_or_else(|| panic!("confirmed Use probe execution is missing"));
let confirmation = execution
.confirmation
.as_ref()
.unwrap_or_else(|| panic!("confirmed Use probe confirmation is missing"));
println!(
"METAPLEX_USE_PROBE_STATUS status=confirmed uses_before={} uses_after={}",
summary.uses_before,
summary
.uses_after
.unwrap_or_else(|| panic!("confirmed Use uses_after is missing")),
);
println!(
"METAPLEX_USE_PROBE_STEP operation={} signature={} slot={:?} materializations={}",
execution.plan.operation_code,
confirmation.signature.0,
confirmation.slot,
execution.materializations.len(),
);
println!("METAPLEX_USE_PROBE_EVIDENCE {}", execution_evidence_json(execution),);
},
}
}
fn execution_evidence_json(
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
) -> serde_json::Value {
let confirmation = execution.confirmation.as_ref();
let post_execution = execution.post_execution.as_ref();
let idempotence_clean = execution.idempotence_replay.as_ref().is_some_and(|replay| {
return replay.failed_inputs == 0
&& replay.processing_error_inputs == 0
&& replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
});
return serde_json::json!({
"operation": execution.plan.operation_code.as_str(),
"cluster": execution.cluster,
"genesisHash": execution.genesis_hash.as_str(),
"simulationContextSlot": execution.simulation_context_slot,
"simulationSuccess": execution.simulation.success,
"simulationLogCount": execution.simulation.logs.len(),
"messageHash": execution.readiness.message_hash.as_str(),
"feeLamports": execution.fee.fee_lamports,
"signature": confirmation.map(|value| return value.signature.0.clone()),
"confirmationStatus": confirmation.map(|value| return format!("{:?}", value.status)),
"confirmationSlot": confirmation.and_then(|value| return value.slot),
"beforeSnapshots": execution.before.len(),
"afterSnapshots": execution.after.len(),
"canonicalHydration": post_execution.is_some_and(|value| return value.canonical_inserted),
"coreExtraction": post_execution.is_some_and(|value| return value.core_extracted),
"decodeReplay": post_execution.is_some_and(|value| return value.decode_replayed),
"materialized": post_execution.is_some_and(|value| return value.materialized),
"instructionMaterializations": execution.materializations.len(),
"materializedSnapshots": execution.materialized_snapshots.len(),
"idempotenceReplayClean": idempotence_clean,
});
}
}

View File

@@ -0,0 +1,918 @@
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/validation.rs
// version: 18
//! Machine-readable Metaplex Token Metadata validation contract.
/// Maximum evidence entries accepted by one Metaplex validation scenario.
pub const MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE: usize = 64;
/// Exact validation status for one Metaplex scenario.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MetaplexTokenMetadataValidationStatus {
/// Scenario has not been executed.
NotRun,
/// Scenario passed deterministic synthetic validation.
SyntheticValidated,
/// Scenario was simulated on Devnet or Testnet.
Simulated,
/// Scenario was submitted but confirmation evidence is incomplete.
Submitted,
/// Scenario was confirmed and all postconditions passed.
Confirmed,
/// Scenario is unavailable in the selected environment.
Unavailable,
/// Scenario ran and failed.
Failed,
}
/// One bounded validation evidence entry.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MetaplexTokenMetadataValidationEvidence {
/// Stable evidence kind.
pub kind: std::string::String,
/// Bounded evidence value.
pub value: std::string::String,
}
/// One scenario declared by the canonical Metaplex validation matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MetaplexTokenMetadataValidationMatrixScenario {
/// Stable scenario identifier.
pub id: std::string::String,
/// Required scenario mode.
pub mode: crate::MetaplexTokenMetadataScenarioMode,
/// Exact validation status.
pub status: crate::MetaplexTokenMetadataValidationStatus,
/// Evidence kinds required before confirmation.
pub required_evidence: std::vec::Vec<std::string::String>,
/// Observed evidence.
#[serde(default)]
pub evidence: std::vec::Vec<crate::MetaplexTokenMetadataValidationEvidence>,
}
/// Canonical Metaplex validation matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MetaplexTokenMetadataValidationMatrix {
/// Matrix schema version.
pub matrix_version: u32,
/// Owning milestone.
pub milestone: std::string::String,
/// Ordered validation scenarios.
pub scenarios: std::vec::Vec<crate::MetaplexTokenMetadataValidationMatrixScenario>,
}
/// Transaction location covered by one cross-validation case.
#[derive(
Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize,
)]
#[serde(rename_all = "snake_case")]
pub enum MetaplexTokenMetadataTransactionPath {
/// Top-level instruction.
Outer,
/// Cross-program invocation.
Cpi,
}
/// Expected transaction outcome for one cross-validation case.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MetaplexTokenMetadataTransactionOutcome {
/// Successful transaction path.
Success,
/// Failed or rejected transaction path.
Failed,
}
/// Stable failure category exercised by one negative case.
#[derive(
Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize,
)]
#[serde(rename_all = "snake_case")]
pub enum MetaplexTokenMetadataFailureClass {
/// Transaction failed before committed observations could be emitted.
TransactionFailed,
/// Program ID mismatch.
ProgramId,
/// Account owner mismatch.
Owner,
/// Program-derived address mismatch.
Pda,
/// Positional account or flag mismatch.
Accounts,
/// Truncated instruction payload.
PayloadTruncated,
/// Forbidden trailing instruction bytes.
PayloadSuffix,
/// Unknown instruction discriminant.
UnknownDiscriminant,
/// Conflicting Metaplex and Token-2022 metadata sources.
MetadataSourceConflict,
}
/// One case in the closed Metaplex cross-validation corpus.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MetaplexTokenMetadataCrossValidationCase {
/// Stable case identifier.
pub id: std::string::String,
/// Outer or CPI transaction path.
pub path: crate::MetaplexTokenMetadataTransactionPath,
/// Expected transaction outcome.
pub outcome: crate::MetaplexTokenMetadataTransactionOutcome,
/// Optional asset family for successful corpus cases.
pub asset_family: std::option::Option<crate::MetaplexTokenMetadataAssetFamily>,
/// Optional stable negative-case category.
pub failure_class: std::option::Option<crate::MetaplexTokenMetadataFailureClass>,
/// Whether status changes require actual RPC evidence.
pub requires_network_evidence: bool,
/// Exact observed validation status.
pub status: crate::MetaplexTokenMetadataValidationStatus,
/// Evidence kinds required before the declared status is accepted.
pub required_evidence: std::vec::Vec<std::string::String>,
/// Bounded observed evidence.
#[serde(default)]
pub evidence: std::vec::Vec<crate::MetaplexTokenMetadataValidationEvidence>,
}
/// Closed Metaplex cross-validation corpus for `0.4.7-pre.010`.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MetaplexTokenMetadataCrossValidationMatrix {
/// Matrix schema version.
pub matrix_version: u32,
/// Exact owning prerelease.
pub milestone: std::string::String,
/// Ordered validation cases.
pub cases: std::vec::Vec<crate::MetaplexTokenMetadataCrossValidationCase>,
}
/// Loads and validates the closed Metaplex cross-validation corpus.
pub fn load_metaplex_token_metadata_cross_validation_matrix()
-> ks_core::Result<crate::MetaplexTokenMetadataCrossValidationMatrix> {
let parsed = match serde_json::from_str::<crate::MetaplexTokenMetadataCrossValidationMatrix>(
include_str!(
"../../../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_CROSS_VALIDATION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_cross_validation_matrix_invalid_json",
format!("Metaplex cross-validation matrix JSON is invalid: {error}"),
));
},
};
if let std::result::Result::Err(error) =
validate_metaplex_token_metadata_cross_validation_matrix(&parsed)
{
return std::result::Result::Err(error);
}
return std::result::Result::Ok(parsed);
}
/// Validates the closed Metaplex cross-validation corpus and evidence claims.
pub fn validate_metaplex_token_metadata_cross_validation_matrix(
matrix: &crate::MetaplexTokenMetadataCrossValidationMatrix,
) -> ks_core::Result<()> {
if matrix.matrix_version != 1 || matrix.milestone != "0.4.7-pre.010" {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_cross_validation_matrix_contract_mismatch",
"Metaplex cross-validation matrix must use version 1 for 0.4.7-pre.010",
));
}
let mut ids = std::collections::BTreeSet::<&str>::new();
for case in &matrix.cases {
if !ids.insert(case.id.as_str()) || case.required_evidence.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_cross_validation_case_invalid",
"Metaplex cross-validation cases must be unique and declare evidence",
));
}
if case.evidence.len() > crate::MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_cross_validation_evidence_limit_exceeded",
"Metaplex cross-validation evidence exceeds the compiled bound",
));
}
if case.outcome == crate::MetaplexTokenMetadataTransactionOutcome::Success
&& case.asset_family.is_none()
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_cross_validation_success_without_asset_family",
"Successful Metaplex corpus cases must identify an asset family",
));
}
if case.outcome == crate::MetaplexTokenMetadataTransactionOutcome::Failed
&& case.failure_class.is_none()
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_cross_validation_failure_without_class",
"Failed Metaplex corpus cases must identify a failure class",
));
}
if matches!(
case.status,
crate::MetaplexTokenMetadataValidationStatus::NotRun
| crate::MetaplexTokenMetadataValidationStatus::Unavailable
) && !case.evidence.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_cross_validation_unobserved_with_evidence",
"Not-run or unavailable cross-validation cases must not contain evidence",
));
}
if case.requires_network_evidence
&& matches!(
case.status,
crate::MetaplexTokenMetadataValidationStatus::SyntheticValidated
)
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_cross_validation_network_overdeclared",
"Network cases cannot be declared validated from synthetic evidence",
));
}
if matches!(
case.status,
crate::MetaplexTokenMetadataValidationStatus::Simulated
| crate::MetaplexTokenMetadataValidationStatus::Submitted
| crate::MetaplexTokenMetadataValidationStatus::Confirmed
) {
let observed = case
.evidence
.iter()
.map(|evidence| return evidence.kind.as_str())
.collect::<std::collections::BTreeSet<&str>>();
if !case
.required_evidence
.iter()
.all(|kind| return observed.contains(kind.as_str()))
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_cross_validation_status_without_required_evidence",
"Observed Metaplex network status lacks required evidence",
));
}
}
}
return std::result::Result::Ok(());
}
/// Loads and validates the canonical Metaplex validation matrix.
pub fn load_metaplex_token_metadata_validation_matrix()
-> ks_core::Result<crate::MetaplexTokenMetadataValidationMatrix> {
let parsed = match serde_json::from_str::<crate::MetaplexTokenMetadataValidationMatrix>(
include_str!(
"../../../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_VALIDATION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_validation_matrix_invalid_json",
format!("Metaplex validation matrix JSON is invalid: {error}"),
));
},
};
if let std::result::Result::Err(error) =
validate_metaplex_token_metadata_validation_matrix(&parsed)
{
return std::result::Result::Err(error);
}
return std::result::Result::Ok(parsed);
}
/// Validates the canonical Metaplex validation matrix.
pub fn validate_metaplex_token_metadata_validation_matrix(
matrix: &crate::MetaplexTokenMetadataValidationMatrix,
) -> ks_core::Result<()> {
if matrix.matrix_version != 1 || matrix.milestone != "0.4.7" {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_validation_matrix_contract_mismatch",
"Metaplex validation matrix must use version 1 for milestone 0.4.7",
));
}
let expected = crate::metaplex_token_metadata_synthetic_scenarios()
.iter()
.map(|scenario| return scenario.id.clone())
.collect::<std::vec::Vec<std::string::String>>();
let actual = matrix
.scenarios
.iter()
.map(|scenario| return scenario.id.clone())
.collect::<std::vec::Vec<std::string::String>>();
if actual != expected {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_validation_matrix_inventory_mismatch",
"Metaplex validation matrix inventory differs from the compiled scenario inventory",
));
}
let mut ids = std::collections::BTreeSet::<&str>::new();
for scenario in &matrix.scenarios {
if !ids.insert(scenario.id.as_str()) || scenario.required_evidence.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_validation_matrix_scenario_invalid",
"Metaplex validation scenarios must be unique and declare required evidence",
));
}
if scenario.evidence.len() > crate::MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_validation_evidence_limit_exceeded",
"Metaplex validation evidence exceeds the compiled bound",
));
}
if matches!(
scenario.status,
crate::MetaplexTokenMetadataValidationStatus::NotRun
| crate::MetaplexTokenMetadataValidationStatus::Unavailable
) && !scenario.evidence.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_validation_unobserved_with_evidence",
"Not-run or unavailable Metaplex scenarios must not contain evidence",
));
}
if scenario.status == crate::MetaplexTokenMetadataValidationStatus::Confirmed {
let observed = scenario
.evidence
.iter()
.map(|evidence| return evidence.kind.as_str())
.collect::<std::collections::BTreeSet<&str>>();
if !scenario
.required_evidence
.iter()
.all(|kind| return observed.contains(kind.as_str()))
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_token_metadata_validation_confirmed_without_required_evidence",
"Confirmed Metaplex scenario lacks required evidence",
));
}
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
fn canonical_matrix_is_machine_readable_and_matches_synthetic_inventory() {
let result = crate::load_metaplex_token_metadata_validation_matrix();
assert!(result.is_ok());
if let std::result::Result::Ok(matrix) = result {
assert_eq!(matrix.scenarios.len(), 5);
}
}
#[test]
fn confirmed_scenario_requires_every_declared_evidence_kind() {
let result = crate::load_metaplex_token_metadata_validation_matrix();
assert!(result.is_ok());
if let std::result::Result::Ok(mut matrix) = result {
matrix.scenarios[0].status = crate::MetaplexTokenMetadataValidationStatus::Confirmed;
assert!(crate::validate_metaplex_token_metadata_validation_matrix(&matrix).is_err());
matrix.scenarios[0].evidence = matrix.scenarios[0]
.required_evidence
.iter()
.map(|kind| {
return crate::MetaplexTokenMetadataValidationEvidence {
kind: kind.clone(),
value: "synthetic-proof".to_string(),
};
})
.collect();
assert!(crate::validate_metaplex_token_metadata_validation_matrix(&matrix).is_ok());
}
}
#[test]
fn cross_validation_corpus_is_closed_and_covers_every_required_dimension() {
let result = crate::load_metaplex_token_metadata_cross_validation_matrix();
assert!(result.is_ok());
if let std::result::Result::Ok(matrix) = result {
assert_eq!(matrix.cases.len(), 19);
let paths = matrix
.cases
.iter()
.map(|case| return case.path)
.collect::<std::collections::BTreeSet<crate::MetaplexTokenMetadataTransactionPath>>(
);
assert_eq!(paths.len(), 2);
let families = matrix
.cases
.iter()
.filter_map(|case| return case.asset_family)
.collect::<std::collections::BTreeSet<crate::MetaplexTokenMetadataAssetFamily>>(
);
assert_eq!(families.len(), 5);
let failures = matrix
.cases
.iter()
.filter_map(|case| return case.failure_class)
.collect::<std::collections::BTreeSet<crate::MetaplexTokenMetadataFailureClass>>(
);
assert_eq!(failures.len(), 9);
assert_eq!(
matrix.cases.iter().filter(|case| return case.requires_network_evidence).count(),
3
);
}
}
#[test]
fn network_cases_cannot_be_promoted_from_synthetic_or_incomplete_evidence() {
let result = crate::load_metaplex_token_metadata_cross_validation_matrix();
assert!(result.is_ok());
if let std::result::Result::Ok(mut matrix) = result {
let network =
matrix.cases.iter_mut().find(|case| return case.requires_network_evidence);
assert!(network.is_some());
if let std::option::Option::Some(case) = network {
case.status = crate::MetaplexTokenMetadataValidationStatus::SyntheticValidated;
}
assert!(
crate::validate_metaplex_token_metadata_cross_validation_matrix(&matrix).is_err()
);
}
}
}
/// One family-specific evidence bundle in the closed Devnet execution matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MetaplexTokenMetadataDevnetExecutionFamilyEvidence {
/// Asset family exercised by this evidence bundle.
pub family: crate::MetaplexTokenMetadataAssetFamily,
/// Complete evidence observed for this family.
pub evidence: std::vec::Vec<crate::MetaplexTokenMetadataValidationEvidence>,
}
/// One current Metaplex operation in the closed Devnet execution matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MetaplexTokenMetadataDevnetExecutionOperation {
/// Stable instruction discriminator.
pub discriminator: u8,
/// Official instruction name.
pub name: std::string::String,
/// Stable executor operation code.
pub operation_code: std::string::String,
/// Whether the operation is deprecated.
pub deprecated: bool,
/// Prerelease responsible for the campaign.
pub campaign_prerelease: std::string::String,
/// Implementation status of the reusable runner.
pub runner_status: std::string::String,
/// Exact observed network status.
pub network_status: std::string::String,
/// Asset families on which this operation has completed its declared network status.
#[serde(default)]
pub observed_families: std::vec::Vec<crate::MetaplexTokenMetadataAssetFamily>,
/// Evidence required after simulation.
pub required_evidence: std::vec::Vec<std::string::String>,
/// Additional evidence required after submission.
pub submission_evidence: std::vec::Vec<std::string::String>,
/// Operation-level evidence reserved for non-family states such as unavailability.
#[serde(default)]
pub evidence: std::vec::Vec<crate::MetaplexTokenMetadataValidationEvidence>,
/// Family-specific evidence bundles for simulated or confirmed operation paths.
#[serde(default)]
pub family_evidence: std::vec::Vec<crate::MetaplexTokenMetadataDevnetExecutionFamilyEvidence>,
}
/// Closed inventory of every current Metaplex operation requiring Devnet coverage.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MetaplexTokenMetadataDevnetExecutionMatrix {
/// Matrix schema version.
pub matrix_version: u32,
/// Owning prerelease.
pub milestone: std::string::String,
/// Canonical Token Metadata program ID.
pub program_id: std::string::String,
/// Ordered current operations.
pub operations: std::vec::Vec<crate::MetaplexTokenMetadataDevnetExecutionOperation>,
}
/// Loads and validates the closed Devnet execution matrix.
pub fn load_metaplex_token_metadata_devnet_execution_matrix()
-> ks_core::Result<crate::MetaplexTokenMetadataDevnetExecutionMatrix> {
let matrix = match serde_json::from_str::<crate::MetaplexTokenMetadataDevnetExecutionMatrix>(
include_str!(
"../../../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_invalid_json",
format!("Metaplex Devnet execution matrix JSON is invalid: {error}"),
));
},
};
if let std::result::Result::Err(error) =
validate_metaplex_token_metadata_devnet_execution_matrix(&matrix)
{
return std::result::Result::Err(error);
}
return std::result::Result::Ok(matrix);
}
/// Validates exact current-operation coverage and conservative network statuses.
pub fn validate_metaplex_token_metadata_devnet_execution_matrix(
matrix: &crate::MetaplexTokenMetadataDevnetExecutionMatrix,
) -> ks_core::Result<()> {
if matrix.matrix_version != 3
|| matrix.milestone != "0.4.8-pre.013"
|| matrix.program_id != ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
|| matrix.operations.len() != 20
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_contract_mismatch",
"Metaplex Devnet matrix must use version 3 and declare exactly 20 current operations for 0.4.8-pre.013",
));
}
let expected = [
"metadata.metaplex_token_metadata.create_escrow_account",
"metadata.metaplex_token_metadata.close_escrow_account",
"metadata.metaplex_token_metadata.transfer_out_of_escrow",
"metadata.metaplex_token_metadata.burn",
"metadata.metaplex_token_metadata.create",
"metadata.metaplex_token_metadata.mint",
"metadata.metaplex_token_metadata.delegate",
"metadata.metaplex_token_metadata.revoke",
"metadata.metaplex_token_metadata.lock",
"metadata.metaplex_token_metadata.unlock",
"metadata.metaplex_token_metadata.migrate",
"metadata.metaplex_token_metadata.transfer",
"metadata.metaplex_token_metadata.update",
"metadata.metaplex_token_metadata.use",
"metadata.metaplex_token_metadata.verify",
"metadata.metaplex_token_metadata.unverify",
"metadata.metaplex_token_metadata.collect",
"metadata.metaplex_token_metadata.print",
"metadata.metaplex_token_metadata.resize",
"metadata.metaplex_token_metadata.close_accounts",
]
.into_iter()
.collect::<std::collections::BTreeSet<&str>>();
let observed = matrix
.operations
.iter()
.map(|operation| return operation.operation_code.as_str())
.collect::<std::collections::BTreeSet<&str>>();
if observed != expected {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_operation_mismatch",
"Metaplex Devnet matrix must cover the exact 20 current operation codes",
));
}
let required_evidence = [
"cluster",
"genesis_hash",
"simulation_slot",
"simulation_logs",
"message_hash",
"fee_lamports",
]
.into_iter()
.collect::<std::collections::BTreeSet<&str>>();
let submission_evidence = [
"signature",
"confirmation_status",
"confirmation_slot",
"before_state",
"after_state",
"canonical_hydration",
"core_extraction",
"decode_replay",
"materialization",
"idempotence_replay",
]
.into_iter()
.collect::<std::collections::BTreeSet<&str>>();
for operation in &matrix.operations {
let observed_required = operation
.required_evidence
.iter()
.map(std::string::String::as_str)
.collect::<std::collections::BTreeSet<&str>>();
let observed_submission = operation
.submission_evidence
.iter()
.map(std::string::String::as_str)
.collect::<std::collections::BTreeSet<&str>>();
if operation.deprecated
|| operation.runner_status != "implemented"
|| operation.campaign_prerelease != "0.4.8-pre.013"
|| observed_required != required_evidence
|| observed_submission != submission_evidence
|| !matches!(
operation.network_status.as_str(),
"not_run" | "simulated" | "confirmed" | "unavailable"
)
|| operation.evidence.len() > crate::MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_entry_invalid",
"Metaplex Devnet entries must be current, implemented, evidenced and conservatively classified",
));
}
let observed_families = operation
.observed_families
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>();
if observed_families.len() != operation.observed_families.len() {
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_duplicate_family",
"Metaplex Devnet observed families must be unique",
));
}
let mut observed_evidence = std::collections::BTreeSet::<&str>::new();
for evidence in &operation.evidence {
if evidence.kind.trim().is_empty()
|| evidence.value.trim().is_empty()
|| !observed_evidence.insert(evidence.kind.as_str())
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_evidence_invalid",
"Metaplex Devnet operation-level evidence kinds and values must be non-empty and unique",
));
}
}
let mut observed_family_evidence =
std::collections::BTreeSet::<crate::MetaplexTokenMetadataAssetFamily>::new();
for family_evidence in &operation.family_evidence {
if !observed_family_evidence.insert(family_evidence.family)
|| family_evidence.evidence.len()
> crate::MAX_METAPLEX_TOKEN_METADATA_VALIDATION_EVIDENCE
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_family_evidence_invalid",
"Metaplex Devnet family evidence bundles must be unique and bounded",
));
}
let mut family_kinds = std::collections::BTreeSet::<&str>::new();
for evidence in &family_evidence.evidence {
if evidence.kind.trim().is_empty()
|| evidence.value.trim().is_empty()
|| !family_kinds.insert(evidence.kind.as_str())
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_family_evidence_entry_invalid",
"Metaplex Devnet family evidence kinds and values must be non-empty and unique",
));
}
}
if operation.network_status == "simulated"
&& operation
.required_evidence
.iter()
.any(|kind| return !family_kinds.contains(kind.as_str()))
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_family_simulation_evidence_incomplete",
"Simulated Metaplex Devnet family evidence must contain every required simulation evidence kind",
));
}
if operation.network_status == "confirmed"
&& operation
.required_evidence
.iter()
.chain(operation.submission_evidence.iter())
.any(|kind| return !family_kinds.contains(kind.as_str()))
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_family_confirmation_evidence_incomplete",
"Confirmed Metaplex Devnet family evidence must contain every simulation and submission evidence kind",
));
}
}
match operation.network_status.as_str() {
"not_run" => {
if !operation.observed_families.is_empty()
|| !operation.evidence.is_empty()
|| !operation.family_evidence.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_not_run_with_evidence",
"Not-run Metaplex Devnet operations cannot claim families or evidence",
));
}
},
"simulated" | "confirmed" => {
if operation.observed_families.is_empty()
|| !operation.evidence.is_empty()
|| observed_family_evidence != observed_families
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_family_evidence_mismatch",
"Simulated or confirmed Metaplex Devnet operations require exactly one complete evidence bundle per observed family and no aggregate operation evidence",
));
}
},
"unavailable" => {
if !operation.observed_families.is_empty()
|| !operation.family_evidence.is_empty()
|| !observed_evidence.contains("unavailable_reason")
{
return std::result::Result::Err(ks_core::Error::new(
"metaplex_devnet_execution_matrix_unavailable_without_reason",
"Unavailable Metaplex Devnet operations require an unavailable_reason and no observed family evidence",
));
}
},
_ => {},
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod devnet_execution_matrix_tests {
#[test]
fn devnet_execution_matrix_is_reopened_for_pre_013_and_conservative() {
let result = crate::load_metaplex_token_metadata_devnet_execution_matrix();
assert!(result.is_ok());
let matrix = if let std::result::Result::Ok(value) = result {
value
} else {
return;
};
assert_eq!(matrix.matrix_version, 3);
assert_eq!(matrix.operations.len(), 20);
assert!(
matrix
.operations
.iter()
.all(|operation| return operation.campaign_prerelease == "0.4.8-pre.013")
);
assert!(matrix.operations.iter().all(|operation| return !operation.deprecated));
assert!(
matrix
.operations
.iter()
.all(|operation| return operation.runner_status == "implemented")
);
let confirmed = matrix
.operations
.iter()
.filter(|operation| return operation.network_status == "confirmed")
.collect::<std::vec::Vec<_>>();
assert_eq!(confirmed.len(), 15);
assert_eq!(
confirmed
.iter()
.map(|operation| return operation.operation_code.as_str())
.collect::<std::collections::BTreeSet<_>>(),
[
"metadata.metaplex_token_metadata.burn",
"metadata.metaplex_token_metadata.close_escrow_account",
"metadata.metaplex_token_metadata.create",
"metadata.metaplex_token_metadata.create_escrow_account",
"metadata.metaplex_token_metadata.delegate",
"metadata.metaplex_token_metadata.lock",
"metadata.metaplex_token_metadata.mint",
"metadata.metaplex_token_metadata.print",
"metadata.metaplex_token_metadata.revoke",
"metadata.metaplex_token_metadata.transfer",
"metadata.metaplex_token_metadata.transfer_out_of_escrow",
"metadata.metaplex_token_metadata.unlock",
"metadata.metaplex_token_metadata.update",
"metadata.metaplex_token_metadata.verify",
"metadata.metaplex_token_metadata.unverify",
]
.into_iter()
.collect::<std::collections::BTreeSet<_>>()
);
let create_mint_families = vec![
crate::MetaplexTokenMetadataAssetFamily::Nft,
crate::MetaplexTokenMetadataAssetFamily::Sft,
crate::MetaplexTokenMetadataAssetFamily::Fungible,
crate::MetaplexTokenMetadataAssetFamily::Collection,
crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
];
for operation in &confirmed {
let expected_families = if matches!(
operation.operation_code.as_str(),
"metadata.metaplex_token_metadata.create" | "metadata.metaplex_token_metadata.mint"
) {
create_mint_families.as_slice()
} else if matches!(
operation.operation_code.as_str(),
"metadata.metaplex_token_metadata.print"
| "metadata.metaplex_token_metadata.burn"
| "metadata.metaplex_token_metadata.create_escrow_account"
| "metadata.metaplex_token_metadata.close_escrow_account"
| "metadata.metaplex_token_metadata.transfer_out_of_escrow"
| "metadata.metaplex_token_metadata.update"
) {
&[crate::MetaplexTokenMetadataAssetFamily::Nft]
} else if matches!(
operation.operation_code.as_str(),
"metadata.metaplex_token_metadata.delegate"
| "metadata.metaplex_token_metadata.revoke"
| "metadata.metaplex_token_metadata.lock"
| "metadata.metaplex_token_metadata.unlock"
| "metadata.metaplex_token_metadata.transfer"
) {
&[crate::MetaplexTokenMetadataAssetFamily::ProgrammableNft]
} else {
&[crate::MetaplexTokenMetadataAssetFamily::Collection]
};
assert_eq!(operation.observed_families.as_slice(), expected_families);
assert_eq!(
operation
.family_evidence
.iter()
.map(|family_evidence| return family_evidence.family)
.collect::<std::collections::BTreeSet<_>>(),
operation
.observed_families
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>()
);
}
assert_eq!(
matrix
.operations
.iter()
.filter(|operation| return operation.network_status == "not_run")
.count(),
0
);
let unavailable = matrix
.operations
.iter()
.filter(|operation| return operation.network_status == "unavailable")
.collect::<std::vec::Vec<_>>();
assert_eq!(unavailable.len(), 5);
let use_operation = unavailable.iter().find(|operation| {
return operation.operation_code == "metadata.metaplex_token_metadata.use";
});
assert!(use_operation.is_some());
if let std::option::Option::Some(operation) = use_operation {
assert!(operation.evidence.iter().any(|evidence| {
return evidence.kind == "unavailable_reason"
&& evidence.value.contains("InvalidInstructionData");
}));
}
let resize_operation = unavailable.iter().find(|operation| {
return operation.operation_code == "metadata.metaplex_token_metadata.resize";
});
assert!(resize_operation.is_some());
if let std::option::Option::Some(operation) = resize_operation {
assert!(operation.evidence.iter().any(|evidence| {
return evidence.kind == "unavailable_reason"
&& evidence.value.contains("AlreadyResized")
&& evidence.value.contains("201");
}));
}
for (operation_code, expected_fragment) in [
("metadata.metaplex_token_metadata.migrate", "Removed"),
("metadata.metaplex_token_metadata.collect", "fee authority"),
("metadata.metaplex_token_metadata.close_accounts", "close authority"),
] {
let operation = unavailable
.iter()
.find(|operation| return operation.operation_code == operation_code);
assert!(operation.is_some());
if let std::option::Option::Some(operation) = operation {
assert!(operation.evidence.iter().any(|evidence| {
return evidence.kind == "unavailable_reason"
&& evidence.value.contains(expected_fragment);
}));
}
}
let update_operation = confirmed.iter().find(|operation| {
return operation.operation_code == "metadata.metaplex_token_metadata.update";
});
assert!(update_operation.is_some());
if let std::option::Option::Some(operation) = update_operation {
assert_eq!(
operation.observed_families.as_slice(),
&[crate::MetaplexTokenMetadataAssetFamily::Nft]
);
}
let mut incomplete = matrix.clone();
let create = incomplete.operations.iter_mut().find(|operation| {
return operation.operation_code == "metadata.metaplex_token_metadata.create";
});
assert!(create.is_some());
if let std::option::Option::Some(operation) = create {
if let std::option::Option::Some(family_evidence) =
operation.family_evidence.first_mut()
{
family_evidence.evidence.clear();
}
}
assert!(
crate::validate_metaplex_token_metadata_devnet_execution_matrix(&incomplete).is_err()
);
}
}

View File

@@ -0,0 +1,10 @@
// file: ks-pipeline-demo-scenarios/src/metadata/solana_program.rs
// version: 2
//! Solana Program Metadata demo and Devnet validation scenarios.
pub(crate) mod campaign;
pub(crate) mod devnet_execution;
pub(crate) mod fixture;
pub(crate) mod scenarios;
pub(crate) mod validation;

View File

@@ -0,0 +1,144 @@
// file: ks-pipeline-demo-scenarios/src/metadata/solana_program/campaign.rs
// version: 4
//! Ordered confirmed Devnet campaign for all Solana Program Metadata operations.
/// One confirmed step retained by the complete Devnet campaign.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetSolanaProgramMetadataCampaignStepSummary {
/// Stable scenario identifier.
pub scenario_id: std::string::String,
/// Stable scenario-step identifier.
pub step_id: std::string::String,
/// Zero-based position inside the scenario.
pub step_index: usize,
/// Complete simulation, submission and stateful evidence.
pub execution: crate::DevnetSolanaProgramMetadataExecutionSummary,
}
/// Complete evidence retained by the two-journey Devnet campaign.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetSolanaProgramMetadataCampaignSummary {
/// Fresh pre-funded fixture used by the campaign.
pub fixture: crate::SolanaProgramMetadataFixturePreparationSummary,
/// Nine confirmed steps in deterministic journey order.
pub steps: std::vec::Vec<crate::DevnetSolanaProgramMetadataCampaignStepSummary>,
}
/// Prepares fresh accounts and executes the nine stable operations on Devnet.
///
/// The campaign always submits transactions. The caller must explicitly confirm
/// both fixture prefunding and all subsequent state mutations.
pub async fn execute_devnet_solana_program_metadata_campaign<O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
observer: &O,
) -> ks_core::Result<crate::DevnetSolanaProgramMetadataCampaignSummary>
where
O: crate::SolanaExecutionObserver,
{
if !options.operator_confirmed {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata Devnet campaign requires explicit operator confirmation",
));
}
let fixture = match crate::prepare_solana_program_metadata_fixture(
http_pool,
profile,
workspace_root,
options,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut steps = std::vec::Vec::with_capacity(fixture.prepared_steps.len());
for prepared in &fixture.prepared_steps {
let mut request = prepared
.execution_request(format!("spm-devnet-{}-{}", prepared.scenario_id, prepared.step_id));
request.query_role = options.query_role.clone();
request.transaction_role = options.transaction_role.clone();
request.submit = true;
request.operator_confirmed = true;
let execution = match crate::execute_devnet_solana_program_metadata(
http_pool,
profile,
workspace_root,
&request,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmed = match execution.confirmation.as_ref() {
std::option::Option::Some(value) => matches!(
value.status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
),
std::option::Option::None => false,
};
if !confirmed || execution.post_execution.is_none() {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_campaign_step_not_confirmed",
format!(
"campaign step {} did not reach confirmed stateful completion",
prepared.step_id
),
));
}
if prepared.operation.operation_code() != ks_lib::EX_METADATA_SPM_CLOSE_OPERATION
&& execution.materialized_snapshots.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_campaign_snapshot_missing",
format!(
"campaign step {} did not produce its required materialized snapshot",
prepared.step_id
),
));
}
steps.push(crate::DevnetSolanaProgramMetadataCampaignStepSummary {
scenario_id: prepared.scenario_id.clone(),
step_id: prepared.step_id.clone(),
step_index: prepared.step_index,
execution,
});
}
return std::result::Result::Ok(crate::DevnetSolanaProgramMetadataCampaignSummary {
fixture,
steps,
});
}
#[cfg(test)]
mod tests {
#[test]
fn campaign_contract_requires_exactly_nine_ordered_steps() {
let scenarios = crate::solana_program_metadata_devnet_scenarios();
let step_ids = scenarios
.iter()
.flat_map(|scenario| return scenario.steps.iter())
.map(|step| return step.id.as_str())
.collect::<std::vec::Vec<_>>();
assert_eq!(
step_ids,
vec![
"spm_buffer_allocate",
"spm_buffer_extend",
"spm_buffer_write",
"spm_buffer_set_authority",
"spm_buffer_trim",
"spm_buffer_close",
"spm_metadata_initialize",
"spm_metadata_set_data",
"spm_metadata_set_immutable",
]
);
}
}

View File

@@ -0,0 +1,715 @@
// file: ks-pipeline-demo-scenarios/src/metadata/solana_program/devnet_execution.rs
// version: 3
//! Real Devnet simulation and controlled submission for Solana Program Metadata.
/// Complete request for one Solana Program Metadata Devnet execution.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetSolanaProgramMetadataExecutionRequest {
/// Stable caller-provided execution identifier.
pub intent_id: std::string::String,
/// Endpoint role used for reads, balance, blockhash and fee queries.
pub query_role: std::string::String,
/// Endpoint role used for simulation, submission and confirmation.
pub transaction_role: std::string::String,
/// Exact stable Solana Program Metadata operation.
pub operation: ks_lib::ExMetadataSpmOperation,
/// Bounded state reads required before execution.
pub preflight_reads: std::vec::Vec<ks_pipeline::SolanaProgramMetadataStatefulReadRequest>,
/// Explicit rent observations required by allocation and growth operations.
pub rent_observations: std::vec::Vec<ks_pipeline::SolanaProgramMetadataRentObservation>,
/// Bounded state reads repeated after confirmed submission.
pub postcondition_reads: std::vec::Vec<ks_pipeline::SolanaProgramMetadataStatefulReadRequest>,
/// Explicit approval for overwrite, destructive or irreversible operations.
pub allow_destructive_operation: bool,
/// Explicitly authorizes signing and submission after successful simulation.
pub submit: bool,
/// Explicit operator confirmation required for submission.
pub operator_confirmed: bool,
/// Materializes confirmed account snapshots after submission.
pub materialize_after_confirmation: bool,
}
impl crate::DevnetSolanaProgramMetadataExecutionRequest {
/// Creates a conservative simulation-only request.
pub fn new(
intent_id: impl std::convert::Into<std::string::String>,
operation: ks_lib::ExMetadataSpmOperation,
) -> Self {
return Self {
intent_id: intent_id.into(),
query_role: "http_queries".to_string(),
transaction_role: "http_transactions".to_string(),
operation,
preflight_reads: std::vec::Vec::new(),
rent_observations: std::vec::Vec::new(),
postcondition_reads: std::vec::Vec::new(),
allow_destructive_operation: false,
submit: false,
operator_confirmed: false,
materialize_after_confirmation: false,
};
}
/// Creates one conservative request from serialized typed operation JSON.
pub fn from_operation_json(
intent_id: impl std::convert::Into<std::string::String>,
operation_json: &str,
) -> ks_core::Result<Self> {
if operation_json.contains('<') || operation_json.trim() == "..." {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata operation JSON still contains a placeholder",
));
}
let operation = match serde_json::from_str::<ks_lib::ExMetadataSpmOperation>(operation_json)
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(format!(
"invalid typed Solana Program Metadata operation JSON: {error}"
)));
},
};
return std::result::Result::Ok(Self::new(intent_id, operation));
}
/// Validates request-local bounds independently from one profile.
pub fn validate(&self) -> ks_core::Result<()> {
if self.intent_id.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata Devnet intent id must not be empty",
));
}
if self.query_role.trim().is_empty() || self.transaction_role.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata endpoint roles must not be empty",
));
}
if self.preflight_reads.is_empty()
|| self.preflight_reads.len()
> ks_pipeline::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS
|| self.postcondition_reads.len()
> ks_pipeline::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS
{
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata stateful read inventory is empty or above the compiled bound",
));
}
if self.operation.requires_explicit_approval() && !self.allow_destructive_operation {
return std::result::Result::Err(ks_core::Error::new(
"execution_metadata_spm_destructive_approval_required",
"destructive Solana Program Metadata execution requires explicit approval",
));
}
if self.submit && self.postcondition_reads.is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"submitted Solana Program Metadata execution requires postcondition reads",
));
}
if self.materialize_after_confirmation && self.postcondition_reads.is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata materialization requires postcondition reads",
));
}
return std::result::Result::Ok(());
}
}
/// Complete evidence produced by one Solana Program Metadata Devnet execution.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetSolanaProgramMetadataExecutionSummary {
/// Profile used by the orchestration.
pub profile_name: std::string::String,
/// Exact classified cluster.
pub cluster: ks_lib::ExApiExecutionCluster,
/// Genesis hash returned by the selected endpoint.
pub genesis_hash: std::string::String,
/// Non-secret persistent wallet description.
pub wallet: ks_wallet::WalletSummary,
/// Wallet balance observed before planning.
pub balance_lamports: u64,
/// Stateful snapshots observed before simulation.
pub before: std::vec::Vec<ks_pipeline::SolanaProgramMetadataStatefulReadResult>,
/// Stateful preflight report bound to the exact plan.
pub stateful_preflight: ks_pipeline::SolanaProgramMetadataPreflightReport,
/// Exact prepared plan.
pub plan: ks_lib::ExApiPreparedExecutionPlan,
/// Recent blockhash used by the exact transaction.
pub latest_blockhash: ks_onchain_transport::LatestBlockhashResult,
/// Fee estimate for the exact compiled message.
pub fee: ks_onchain_transport::FeeForMessageResult,
/// Exact real RPC simulation result.
pub simulation: ks_lib::ExApiExecutionSimulationResult,
/// Readiness report proving exact-message simulation and signer resolution.
pub readiness: ks_pipeline::SolanaProgramMetadataExecutionReadinessReport,
/// Submission result when explicitly authorized.
pub send_result: std::option::Option<ks_lib::ExApiExecutionSendResult>,
/// Confirmation result when submitted.
pub confirmation: std::option::Option<ks_lib::ExApiExecutionConfirmationResult>,
/// Stateful snapshots observed after confirmed submission.
pub after: std::vec::Vec<ks_pipeline::SolanaProgramMetadataStatefulReadResult>,
/// Stateful postcondition report after confirmed submission.
pub post_execution: std::option::Option<ks_pipeline::SolanaProgramMetadataPostExecutionReport>,
/// Canonical projections emitted from confirmed account snapshots.
pub materialized_snapshots: std::vec::Vec<ks_lib::MtApiMaterializedOutput>,
}
struct PreparedSolanaProgramMetadataExecution {
wallet: ks_wallet::TemporaryWallet,
unsigned: ks_lib::ExSolanaUnsignedTransaction,
evidence: ks_lib::ExSolanaSimulationEvidence,
summary: crate::DevnetSolanaProgramMetadataExecutionSummary,
}
/// Simulates one stable Solana Program Metadata operation against Devnet.
pub async fn simulate_devnet_solana_program_metadata<O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
observer: &O,
) -> ks_core::Result<crate::DevnetSolanaProgramMetadataExecutionSummary>
where
O: crate::SolanaExecutionObserver,
{
if request.submit {
return std::result::Result::Err(ks_core::Error::config(
"simulate_devnet_solana_program_metadata requires submit=false",
));
}
let prepared =
match prepare_execution(http_pool, profile, workspace_root, request, observer).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(prepared.summary);
}
/// Executes one Solana Program Metadata simulation or authorized submission.
pub async fn execute_devnet_solana_program_metadata<O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
observer: &O,
) -> ks_core::Result<crate::DevnetSolanaProgramMetadataExecutionSummary>
where
O: crate::SolanaExecutionObserver,
{
let prepared =
match prepare_execution(http_pool, profile, workspace_root, request, observer).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !request.submit {
return std::result::Result::Ok(prepared.summary);
}
if !prepared.summary.simulation.success {
return std::result::Result::Err(ks_core::Error::new(
"execution_simulation_failed",
crate::simulation_failure_message(&prepared.summary.simulation),
));
}
if let std::result::Result::Err(error) = validate_profile_wallet_signers(
prepared.unsigned.required_signer_pubkeys(),
prepared.summary.wallet.public_key.as_str(),
) {
return std::result::Result::Err(error);
}
let send_evaluation = match ks_lib::ExSafetyChecker
.evaluate_send(&prepared.summary.plan, &prepared.summary.simulation)
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if send_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"execution_send_denied",
crate::violation_message(send_evaluation.violations.as_slice()),
));
}
let signed = match prepared
.unsigned
.sign_after_simulation(&prepared.evidence, &[prepared.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 mut summary = prepared.summary;
let send_config = match ks_onchain_transport::SendTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(summary.latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let sent = match http_pool
.send_transaction_for_role(
request.transaction_role.as_str(),
signed.transaction_base64().as_str(),
&signature,
&send_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
summary.send_result =
std::option::Option::Some(sent.to_execution_result(ks_lib::ExApiExecutionCluster::Devnet));
let confirmation_config =
match ks_onchain_transport::ConfirmTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(summary.latest_blockhash.last_valid_block_height),
std::option::Option::Some(summary.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(
request.transaction_role.as_str(),
request.query_role.as_str(),
ks_lib::ExApiExecutionCluster::Devnet,
&signature,
&confirmation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmed = matches!(
confirmation.status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
);
summary.confirmation = std::option::Option::Some(confirmation);
if confirmed {
summary.after = match read_snapshots(http_pool, &request.postcondition_reads).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let report = match ks_pipeline::inspect_solana_program_metadata_post_execution(
&ks_pipeline::SolanaProgramMetadataPostExecutionRequest {
operation: request.operation.clone(),
before: summary.before.clone(),
after: summary.after.clone(),
},
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if report.status == ks_pipeline::SolanaProgramMetadataPostconditionStatus::Contradicted {
return std::result::Result::Err(ks_core::Error::new(
"execution_metadata_spm_postcondition_contradicted",
"confirmed Solana Program Metadata transaction contradicted its stateful postcondition",
));
}
summary.post_execution = std::option::Option::Some(report);
if request.materialize_after_confirmation {
summary.materialized_snapshots =
materialize_confirmed_snapshots(summary.after.as_slice());
}
}
return std::result::Result::Ok(summary);
}
async fn prepare_execution<O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
observer: &O,
) -> ks_core::Result<PreparedSolanaProgramMetadataExecution>
where
O: crate::SolanaExecutionObserver,
{
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = validate_profile(profile, request) {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) =
crate::ensure_not_cancelled(observer, "solana_program_metadata_validate")
{
return std::result::Result::Err(error);
}
let genesis = match http_pool.get_genesis_hash_for_role(request.query_role.as_str()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if genesis.classified_cluster
!= std::option::Option::Some(ks_lib::ExApiExecutionCluster::Devnet)
{
return std::result::Result::Err(ks_core::Error::new(
"execution_cluster_mismatch",
format!(
"expected Devnet genesis hash but endpoint returned {} classified as {:?}",
genesis.genesis_hash, genesis.classified_cluster
),
));
}
let wallet = 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 wallet_summary = wallet.summary();
let fee_payer = ks_lib::MdPubkey(wallet_summary.public_key.clone());
let balance = match http_pool
.get_balance_for_role(
request.query_role.as_str(),
&fee_payer,
&ks_onchain_transport::GetBalanceConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if balance.lamports < profile.execution.max_fee_lamports {
return std::result::Result::Err(ks_core::Error::new(
"execution_balance_insufficient",
"Devnet wallet balance is below the configured fee ceiling",
));
}
let intent = build_intent(profile, request, fee_payer.clone());
let plan = match ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
&ks_lib::ExMetadataSolanaProgramMetadataExecutor,
&intent,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let plan_evaluation = match ks_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if plan_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"execution_metadata_spm_plan_denied",
crate::violation_message(plan_evaluation.violations.as_slice()),
));
}
let before = match read_snapshots(http_pool, &request.preflight_reads).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let stateful_preflight = match ks_pipeline::inspect_solana_program_metadata_preflight(
&ks_pipeline::SolanaProgramMetadataPreflightRequest {
intent: intent.clone(),
plan: plan.clone(),
before: before.clone(),
rent_observations: request.rent_observations.clone(),
},
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let latest_blockhash = match http_pool
.get_latest_blockhash_for_role(
request.query_role.as_str(),
&ks_onchain_transport::GetLatestBlockhashConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned = match ks_lib::executor_solana_build_legacy_transaction(
&plan,
latest_blockhash.blockhash.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = validate_profile_wallet_signers(
unsigned.required_signer_pubkeys(),
wallet_summary.public_key.as_str(),
) {
return std::result::Result::Err(error);
}
let fee = match http_pool
.get_fee_for_message_for_role(
request.query_role.as_str(),
unsigned.message_base64().as_str(),
&ks_onchain_transport::GetFeeForMessageConfig::new(
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
std::option::Option::Some(latest_blockhash.context.slot),
),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned_base64 = match unsigned.transaction_base64() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_config = match ks_onchain_transport::SimulateTransactionConfig::new(
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
false,
false,
std::option::Option::Some(latest_blockhash.context.slot),
true,
std::option::Option::None,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::emit(
observer,
crate::SolanaExecutionProgressLevel::Info,
"solana_program_metadata_simulation",
format!("simulating exact Solana Program Metadata message {}", unsigned.message_hash()),
std::option::Option::None,
);
let simulation_rpc = match http_pool
.simulate_transaction_for_role(
request.transaction_role.as_str(),
unsigned_base64.as_str(),
&simulation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation = simulation_rpc.to_execution_result(
ks_lib::ExApiExecutionCluster::Devnet,
ks_lib::ExApiExecutionBlockhashKind::Latest,
std::option::Option::Some(
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(&fee),
);
let readiness = match ks_pipeline::validate_solana_program_metadata_execution_readiness(
&ks_pipeline::SolanaProgramMetadataExecutionReadinessRequest {
plan: plan.clone(),
preflight: stateful_preflight.clone(),
message_hash: unsigned.message_hash().to_string(),
simulated_message_hash: unsigned.message_hash().to_string(),
simulated: true,
simulation_succeeded: simulation.success,
resolved_signers: vec![fee_payer],
submit: request.submit,
operator_confirmed: request.operator_confirmed,
},
) {
std::result::Result::Ok(value) => value,
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(ks_core::Error::new(
error.code(),
format!("{}; simulation={simulation_json}", error.message()),
));
},
};
let evidence = unsigned.bind_simulation(simulation.clone());
return std::result::Result::Ok(PreparedSolanaProgramMetadataExecution {
wallet,
unsigned,
evidence,
summary: crate::DevnetSolanaProgramMetadataExecutionSummary {
profile_name: profile.name.clone(),
cluster: ks_lib::ExApiExecutionCluster::Devnet,
genesis_hash: genesis.genesis_hash,
wallet: wallet_summary,
balance_lamports: balance.lamports,
before,
stateful_preflight,
plan,
latest_blockhash,
fee,
simulation,
readiness,
send_result: std::option::Option::None,
confirmation: std::option::Option::None,
after: std::vec::Vec::new(),
post_execution: std::option::Option::None,
materialized_snapshots: std::vec::Vec::new(),
},
});
}
fn build_intent(
profile: &ks_config::ProfileConfig,
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
fee_payer: ks_lib::MdPubkey,
) -> ks_lib::ExMetadataSpmExecutionIntent {
return ks_lib::ExMetadataSpmExecutionIntent {
intent_id: request.intent_id.clone(),
fee_payer: fee_payer.clone(),
policy: ks_lib::ExApiExecutionPolicy {
cluster: ks_lib::ExApiExecutionClusterPolicy {
expected_cluster: ks_lib::ExApiExecutionCluster::Devnet,
allow_mainnet: false,
mainnet_confirmation: false,
},
simulation: ks_lib::ExApiExecutionSimulationPolicy::Required,
blockhash: ks_lib::ExApiExecutionBlockhashPolicy {
kind: ks_lib::ExApiExecutionBlockhashKind::Latest,
max_age_slots: std::option::Option::Some(
profile.execution.recent_blockhash_max_age_slots,
),
nonce_account: std::option::Option::None,
nonce_authority: std::option::Option::None,
},
cost_limit: ks_lib::ExApiExecutionCostLimit {
max_spend_lamports: std::option::Option::Some(
profile.execution.devnet_max_spend_lamports,
),
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
max_compute_unit_price_micro_lamports: std::option::Option::Some(
profile.execution.max_compute_unit_price_micro_lamports,
),
},
authorized_signers: vec![fee_payer],
dry_run: !request.submit,
post_execution_validation: ks_lib::ExApiPostExecutionValidationPolicy {
canonical_insert_required: true,
core_extraction_required: true,
decode_replay_required: true,
materialization_required: true,
},
},
allow_destructive_operation: request.allow_destructive_operation,
operation: request.operation.clone(),
};
}
async fn read_snapshots(
http_pool: &ks_onchain_transport::HttpEndpointPool,
requests: &[ks_pipeline::SolanaProgramMetadataStatefulReadRequest],
) -> ks_core::Result<std::vec::Vec<ks_pipeline::SolanaProgramMetadataStatefulReadResult>> {
let mut results = std::vec::Vec::with_capacity(requests.len());
for request in requests {
let value =
match ks_pipeline::read_solana_program_metadata_stateful_snapshot(http_pool, request)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
results.push(value);
}
return std::result::Result::Ok(results);
}
fn materialize_confirmed_snapshots(
snapshots: &[ks_pipeline::SolanaProgramMetadataStatefulReadResult],
) -> std::vec::Vec<ks_lib::MtApiMaterializedOutput> {
return snapshots
.iter()
.filter_map(|result| return result.materialized_output.clone())
.collect();
}
fn validate_profile(
profile: &ks_config::ProfileConfig,
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
) -> ks_core::Result<()> {
if profile.wallet.cluster != "devnet" {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata orchestration requires a Devnet wallet profile",
));
}
if !profile.wallet.temporary_wallet_enabled || !profile.wallet.temporary_wallet_persist {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata orchestration requires an enabled persistent temporary wallet",
));
}
if !profile.execution.require_simulation {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata orchestration requires simulation",
));
}
if request.submit && !profile.wallet.devnet_send_enabled {
return std::result::Result::Err(ks_core::Error::config(
"Devnet transaction submission is disabled by the wallet profile",
));
}
if request.submit
&& profile.execution.require_operator_confirmation
&& !request.operator_confirmed
{
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata submission requires explicit operator confirmation",
));
}
return std::result::Result::Ok(());
}
fn validate_profile_wallet_signers(
required_signers: &[std::string::String],
wallet_pubkey: &str,
) -> ks_core::Result<()> {
if required_signers.iter().any(|value| return value.as_str() != wallet_pubkey) {
return std::result::Result::Err(ks_core::Error::new(
"execution_metadata_spm_external_signer_unavailable",
format!(
"the Devnet Solana Program Metadata orchestrator can sign only with profile wallet {}; required signers are {}",
wallet_pubkey,
required_signers.join(",")
),
));
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
fn wallet() -> ks_lib::MdPubkey {
return ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([7_u8; 32]).to_string());
}
#[test]
fn request_rejects_destructive_operations_without_approval() {
let wallet = wallet();
let mut request = crate::DevnetSolanaProgramMetadataExecutionRequest::new(
"write",
ks_lib::ExMetadataSpmOperation::Write {
buffer: wallet.clone(),
authority: wallet,
offset: 0,
source: ks_lib::ExMetadataSpmWriteSource::Inline { data: vec![1] },
},
);
request
.preflight_reads
.push(ks_pipeline::SolanaProgramMetadataStatefulReadRequest {
query_role: "http_queries".to_string(),
account: request.operation_target_for_test(),
expected_state: ks_pipeline::SolanaProgramMetadataExpectedAccountState::Buffer,
min_context_slot: std::option::Option::None,
max_data_bytes: ks_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES,
});
assert!(request.validate().is_err());
request.allow_destructive_operation = true;
assert!(request.validate().is_ok());
}
trait OperationTargetForTest {
fn operation_target_for_test(&self) -> ks_lib::MdPubkey;
}
impl OperationTargetForTest for crate::DevnetSolanaProgramMetadataExecutionRequest {
fn operation_target_for_test(&self) -> ks_lib::MdPubkey {
return match &self.operation {
ks_lib::ExMetadataSpmOperation::Write { buffer, .. } => buffer.clone(),
_ => ks_lib::MdPubkey(ks_program_ids::SYSTEM_PROGRAM_ID.to_string()),
};
}
}
}

View File

@@ -0,0 +1,962 @@
// file: ks-pipeline-demo-scenarios/src/metadata/solana_program/fixture.rs
// version: 7
//! Native Solana Program Metadata fixture preparation for Devnet journeys.
/// Bytes reserved for the public Buffer journey before `Trim`.
pub const SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES: u16 = 128;
/// Options used to prepare one complete Solana Program Metadata fixture.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SolanaProgramMetadataFixturePreparationOptions {
/// Endpoint role used for account, rent, balance, blockhash and fee calls.
pub query_role: std::string::String,
/// Endpoint role used for simulation, submission and confirmation.
pub transaction_role: std::string::String,
/// Explicit operator confirmation for the two prefunding transfers.
pub operator_confirmed: bool,
}
impl crate::SolanaProgramMetadataFixturePreparationOptions {
/// Creates conservative defaults matching the Devnet execution profiles.
pub fn new() -> Self {
return Self {
query_role: "http_queries".to_string(),
transaction_role: "http_transactions".to_string(),
operator_confirmed: false,
};
}
}
impl std::default::Default for crate::SolanaProgramMetadataFixturePreparationOptions {
fn default() -> Self {
return Self::new();
}
}
/// One prepared executable step in a Solana Program Metadata journey.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SolanaProgramMetadataPreparedStep {
/// Stable scenario identifier.
pub scenario_id: std::string::String,
/// Stable scenario-step identifier.
pub step_id: std::string::String,
/// Zero-based step position inside the scenario.
pub step_index: usize,
/// Exact typed operation.
pub operation: ks_lib::ExMetadataSpmOperation,
/// Bounded state reads required before execution.
pub preflight_reads: std::vec::Vec<ks_pipeline::SolanaProgramMetadataStatefulReadRequest>,
/// Explicit rent observations required by the operation.
pub rent_observations: std::vec::Vec<ks_pipeline::SolanaProgramMetadataRentObservation>,
/// Bounded state reads required after confirmation.
pub postcondition_reads: std::vec::Vec<ks_pipeline::SolanaProgramMetadataStatefulReadRequest>,
/// Whether explicit destructive approval is required.
pub allow_destructive_operation: bool,
}
impl crate::SolanaProgramMetadataPreparedStep {
/// Builds one simulation-only execution request from the prepared fixture step.
pub fn execution_request(
&self,
intent_id: impl std::convert::Into<std::string::String>,
) -> crate::DevnetSolanaProgramMetadataExecutionRequest {
let mut request = crate::DevnetSolanaProgramMetadataExecutionRequest::new(
intent_id,
self.operation.clone(),
);
request.preflight_reads = self.preflight_reads.clone();
request.rent_observations = self.rent_observations.clone();
request.postcondition_reads = self.postcondition_reads.clone();
request.allow_destructive_operation = self.allow_destructive_operation;
request.materialize_after_confirmation = true;
return request;
}
}
/// Public fixture values for the two Solana Program Metadata journeys.
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SolanaProgramMetadataFixturePreparationSummary {
/// Profile used by the fixture preparation.
pub profile_name: std::string::String,
/// Profile-wallet authority.
pub authority: std::string::String,
/// Executable program described by the non-canonical metadata PDA.
pub described_program: std::string::String,
/// Exact 16-byte Buffer seed as UTF-8 text.
pub buffer_seed: std::string::String,
/// Non-canonical Buffer PDA.
pub buffer: std::string::String,
/// Lamports transferred to pre-fund the Buffer PDA.
pub buffer_prefund_lamports: u64,
/// Confirmed Buffer prefunding signature.
pub buffer_prefund_signature: std::string::String,
/// Exact 16-byte Metadata seed as UTF-8 text.
pub metadata_seed: std::string::String,
/// Non-canonical Metadata PDA.
pub metadata: std::string::String,
/// Lamports transferred to pre-fund the Metadata PDA.
pub metadata_prefund_lamports: u64,
/// Confirmed Metadata prefunding signature.
pub metadata_prefund_signature: std::string::String,
/// Ordered executable steps for both journeys.
pub prepared_steps: std::vec::Vec<crate::SolanaProgramMetadataPreparedStep>,
}
/// Creates two unique pre-funded PDAs and the nine typed journey steps.
pub async fn prepare_solana_program_metadata_fixture(
http_pool: &ks_onchain_transport::HttpEndpointPool,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
) -> ks_core::Result<crate::SolanaProgramMetadataFixturePreparationSummary> {
if options.query_role.trim().is_empty() || options.transaction_role.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata fixture endpoint roles must not be empty",
));
}
if profile.wallet.cluster != "devnet"
|| !profile.wallet.devnet_send_enabled
|| !profile.wallet.temporary_wallet_enabled
|| !profile.wallet.temporary_wallet_persist
{
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata fixture requires one persistent send-enabled Devnet profile",
));
}
if !options.operator_confirmed {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata fixture prefunding requires explicit operator confirmation",
));
}
let genesis = match http_pool.get_genesis_hash_for_role(options.query_role.as_str()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if genesis.classified_cluster
!= std::option::Option::Some(ks_lib::ExApiExecutionCluster::Devnet)
{
return std::result::Result::Err(ks_core::Error::new(
"execution_cluster_mismatch",
"Solana Program Metadata fixture preparation requires a Devnet endpoint",
));
}
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 authority = ks_lib::MdPubkey(operator.public_key());
let described_program = ks_lib::MdPubkey(ks_program_ids::SYSTEM_PROGRAM_ID.to_string());
let unique = uuid::Uuid::new_v4().simple().to_string();
let buffer_seed = format!("buf{}", &unique[0..13]);
let metadata_seed = format!("met{}", &unique[13..26]);
let buffer = match ks_lib::executor_metadata_solana_program_metadata_derive_non_canonical_pda(
&described_program,
&authority,
buffer_seed.as_bytes(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let metadata = match ks_lib::executor_metadata_solana_program_metadata_derive_non_canonical_pda(
&described_program,
&authority,
metadata_seed.as_bytes(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = ensure_fixture_addresses_are_missing(
http_pool,
options,
&[buffer.clone(), metadata.clone()],
)
.await
{
return std::result::Result::Err(error);
}
let buffer_header = match u64::try_from(ks_lib::DC_METADATA_SPM_BUFFER_HEADER_BYTES) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(error.to_string()));
},
};
let metadata_header = match u64::try_from(ks_lib::DC_METADATA_SPM_METADATA_HEADER_BYTES) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(error.to_string()));
},
};
let buffer_target_space = buffer_header
.saturating_add(u64::from(crate::SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES));
let initial_metadata_bytes = br#"{"name":"Khadhroony Program Metadata","version":1}"#.to_vec();
let updated_metadata_bytes =
br#"{"name":"Khadhroony Program Metadata","version":2,"validated":true}"#.to_vec();
let initial_metadata_data_len = match u64::try_from(initial_metadata_bytes.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(error.to_string()));
},
};
let updated_metadata_data_len = match u64::try_from(updated_metadata_bytes.len()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(error.to_string()));
},
};
let initial_metadata_space = metadata_header.saturating_add(initial_metadata_data_len);
let updated_metadata_space = metadata_header.saturating_add(updated_metadata_data_len);
let buffer_header_rent =
match rent_for_space(http_pool, options.query_role.as_str(), buffer_header).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let buffer_target_rent =
match rent_for_space(http_pool, options.query_role.as_str(), buffer_target_space).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let initial_metadata_rent = match rent_for_space(
http_pool,
options.query_role.as_str(),
initial_metadata_space,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let updated_metadata_rent = match rent_for_space(
http_pool,
options.query_role.as_str(),
updated_metadata_space,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let buffer_prefund_lamports = buffer_target_rent.max(buffer_header_rent);
let metadata_prefund_lamports = updated_metadata_rent.max(initial_metadata_rent);
let total =
match buffer_prefund_lamports
.checked_add(metadata_prefund_lamports)
.and_then(|value| {
return value.checked_add(
profile
.execution
.max_fee_lamports
.saturating_mul(crate::SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT),
);
}) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::config(
"Solana Program Metadata fixture funding requirement overflowed u64",
));
},
};
let balance = match http_pool
.get_balance_for_role(
options.query_role.as_str(),
&authority,
&ks_onchain_transport::GetBalanceConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if balance.lamports < total {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_fixture_balance_insufficient",
format!(
"Devnet fixture wallet has {} lamports but requires at least {}; fund or airdrop the profile wallet first",
balance.lamports, total
),
));
}
let buffer_prefund_signature = match submit_prefund_transfer(
http_pool,
profile,
options,
&operator,
buffer.clone(),
buffer_prefund_lamports,
"buffer",
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let metadata_prefund_signature = match submit_prefund_transfer(
http_pool,
profile,
options,
&operator,
metadata.clone(),
metadata_prefund_lamports,
"metadata",
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let prepared_steps = prepared_steps(
options.query_role.as_str(),
authority.clone(),
described_program.clone(),
buffer.clone(),
buffer_seed.as_bytes(),
metadata.clone(),
metadata_seed.as_bytes(),
initial_metadata_bytes,
updated_metadata_bytes,
buffer_prefund_lamports,
buffer_header,
buffer_header_rent,
buffer_target_space,
buffer_target_rent,
metadata_prefund_lamports,
initial_metadata_space,
initial_metadata_rent,
updated_metadata_space,
updated_metadata_rent,
);
tracing::info!(
target: crate::TRACING_TARGET,
action = "prepare_solana_program_metadata_fixture",
profile = profile.name.as_str(),
authority = authority.0.as_str(),
buffer = buffer.0.as_str(),
metadata = metadata.0.as_str(),
"prepared pre-funded Solana Program Metadata Devnet fixture"
);
return std::result::Result::Ok(crate::SolanaProgramMetadataFixturePreparationSummary {
profile_name: profile.name.clone(),
authority: authority.0,
described_program: described_program.0,
buffer_seed,
buffer: buffer.0,
buffer_prefund_lamports,
buffer_prefund_signature,
metadata_seed,
metadata: metadata.0,
metadata_prefund_lamports,
metadata_prefund_signature,
prepared_steps,
});
}
#[allow(clippy::too_many_arguments)]
fn prepared_steps(
query_role: &str,
authority: ks_lib::MdPubkey,
described_program: ks_lib::MdPubkey,
buffer: ks_lib::MdPubkey,
buffer_seed: &[u8],
metadata: ks_lib::MdPubkey,
metadata_seed: &[u8],
initial_metadata_bytes: std::vec::Vec<u8>,
updated_metadata_bytes: std::vec::Vec<u8>,
buffer_observed_lamports: u64,
buffer_header_space: u64,
buffer_header_rent: u64,
buffer_target_space: u64,
buffer_target_rent: u64,
metadata_observed_lamports: u64,
initial_metadata_space: u64,
initial_metadata_rent: u64,
updated_metadata_space: u64,
updated_metadata_rent: u64,
) -> std::vec::Vec<crate::SolanaProgramMetadataPreparedStep> {
let context = ks_lib::ExMetadataSpmProgramContext {
program: described_program.clone(),
program_data: std::option::Option::None,
};
let buffer_vacant = read(
query_role,
buffer.clone(),
ks_pipeline::SolanaProgramMetadataExpectedAccountState::Vacant,
);
let buffer_state = read(
query_role,
buffer.clone(),
ks_pipeline::SolanaProgramMetadataExpectedAccountState::Buffer,
);
let buffer_missing = read(
query_role,
buffer.clone(),
ks_pipeline::SolanaProgramMetadataExpectedAccountState::Missing,
);
let metadata_vacant = read(
query_role,
metadata.clone(),
ks_pipeline::SolanaProgramMetadataExpectedAccountState::Vacant,
);
let metadata_state = read(
query_role,
metadata.clone(),
ks_pipeline::SolanaProgramMetadataExpectedAccountState::Metadata,
);
return vec![
prepared_step(
"spm_buffer_lifecycle",
"spm_buffer_allocate",
0,
ks_lib::ExMetadataSpmOperation::Allocate {
buffer: buffer.clone(),
authority: authority.clone(),
seed: std::option::Option::Some(buffer_seed.to_vec()),
program_context: std::option::Option::Some(context.clone()),
canonical: false,
allocate_account: true,
},
vec![buffer_vacant],
vec![rent(
buffer.clone(),
buffer_header_space,
buffer_observed_lamports,
buffer_header_rent,
)],
vec![buffer_state.clone()],
false,
),
prepared_step(
"spm_buffer_lifecycle",
"spm_buffer_extend",
1,
ks_lib::ExMetadataSpmOperation::Extend {
account: buffer.clone(),
authority: authority.clone(),
length: crate::SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES,
program_context: std::option::Option::None,
},
vec![buffer_state.clone()],
vec![rent(
buffer.clone(),
buffer_target_space,
buffer_observed_lamports,
buffer_target_rent,
)],
vec![buffer_state.clone()],
false,
),
prepared_step(
"spm_buffer_lifecycle",
"spm_buffer_write",
2,
ks_lib::ExMetadataSpmOperation::Write {
buffer: buffer.clone(),
authority: authority.clone(),
offset: 0,
source: ks_lib::ExMetadataSpmWriteSource::Inline {
data: b"Khadhroony Solana Program Metadata Buffer fixture".to_vec(),
},
},
vec![buffer_state.clone()],
std::vec::Vec::new(),
vec![buffer_state.clone()],
true,
),
prepared_step(
"spm_buffer_lifecycle",
"spm_buffer_set_authority",
3,
ks_lib::ExMetadataSpmOperation::SetAuthority {
account: buffer.clone(),
authority: authority.clone(),
new_authority: std::option::Option::Some(authority.clone()),
program_context: std::option::Option::None,
},
vec![buffer_state.clone()],
std::vec::Vec::new(),
vec![buffer_state.clone()],
true,
),
prepared_step(
"spm_buffer_lifecycle",
"spm_buffer_trim",
4,
ks_lib::ExMetadataSpmOperation::Trim {
account: buffer.clone(),
authority: authority.clone(),
destination: authority.clone(),
program_context: std::option::Option::None,
},
vec![buffer_state.clone()],
std::vec::Vec::new(),
vec![buffer_state.clone()],
true,
),
prepared_step(
"spm_buffer_lifecycle",
"spm_buffer_close",
5,
ks_lib::ExMetadataSpmOperation::Close {
account: buffer.clone(),
authority: authority.clone(),
destination: authority.clone(),
program_context: std::option::Option::None,
},
vec![buffer_state.clone()],
std::vec::Vec::new(),
vec![buffer_missing],
true,
),
prepared_step(
"spm_metadata_lifecycle",
"spm_metadata_initialize",
0,
ks_lib::ExMetadataSpmOperation::Initialize {
metadata: metadata.clone(),
authority: authority.clone(),
program: described_program,
program_data: std::option::Option::None,
canonical: false,
seed: metadata_seed.to_vec(),
encoding: ks_lib::ExMetadataSpmEncoding::Utf8,
compression: ks_lib::ExMetadataSpmCompression::None,
format: ks_lib::ExMetadataSpmFormat::Json,
data_source: ks_lib::ExMetadataSpmDataSource::Direct,
data: std::option::Option::Some(ks_lib::ExMetadataSpmDataInput::Direct {
bytes: initial_metadata_bytes,
}),
allocate_account: true,
},
vec![metadata_vacant],
vec![rent(
metadata.clone(),
initial_metadata_space,
metadata_observed_lamports,
initial_metadata_rent,
)],
vec![metadata_state.clone()],
false,
),
prepared_step(
"spm_metadata_lifecycle",
"spm_metadata_set_data",
1,
ks_lib::ExMetadataSpmOperation::SetData {
metadata: metadata.clone(),
authority: authority.clone(),
encoding: ks_lib::ExMetadataSpmEncoding::Utf8,
compression: ks_lib::ExMetadataSpmCompression::None,
format: ks_lib::ExMetadataSpmFormat::Json,
source: ks_lib::ExMetadataSpmSetDataSource::Inline {
data: ks_lib::ExMetadataSpmDataInput::Direct { bytes: updated_metadata_bytes },
},
program_context: std::option::Option::None,
},
vec![metadata_state.clone()],
vec![rent(
metadata.clone(),
updated_metadata_space,
metadata_observed_lamports,
updated_metadata_rent,
)],
vec![metadata_state.clone()],
true,
),
prepared_step(
"spm_metadata_lifecycle",
"spm_metadata_set_immutable",
2,
ks_lib::ExMetadataSpmOperation::SetImmutable {
metadata: metadata.clone(),
authority,
program_context: std::option::Option::None,
},
vec![metadata_state.clone()],
std::vec::Vec::new(),
vec![metadata_state],
true,
),
];
}
fn prepared_step(
scenario_id: &str,
step_id: &str,
step_index: usize,
operation: ks_lib::ExMetadataSpmOperation,
preflight_reads: std::vec::Vec<ks_pipeline::SolanaProgramMetadataStatefulReadRequest>,
rent_observations: std::vec::Vec<ks_pipeline::SolanaProgramMetadataRentObservation>,
postcondition_reads: std::vec::Vec<ks_pipeline::SolanaProgramMetadataStatefulReadRequest>,
allow_destructive_operation: bool,
) -> crate::SolanaProgramMetadataPreparedStep {
return crate::SolanaProgramMetadataPreparedStep {
scenario_id: scenario_id.to_string(),
step_id: step_id.to_string(),
step_index,
operation,
preflight_reads,
rent_observations,
postcondition_reads,
allow_destructive_operation,
};
}
fn read(
query_role: &str,
account: ks_lib::MdPubkey,
expected_state: ks_pipeline::SolanaProgramMetadataExpectedAccountState,
) -> ks_pipeline::SolanaProgramMetadataStatefulReadRequest {
return ks_pipeline::SolanaProgramMetadataStatefulReadRequest {
query_role: query_role.to_string(),
account,
expected_state,
min_context_slot: std::option::Option::None,
max_data_bytes: ks_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES,
};
}
fn rent(
account: ks_lib::MdPubkey,
target_space: u64,
observed_lamports: u64,
required_lamports: u64,
) -> ks_pipeline::SolanaProgramMetadataRentObservation {
return ks_pipeline::SolanaProgramMetadataRentObservation {
account,
target_space,
observed_lamports,
required_lamports,
};
}
async fn ensure_fixture_addresses_are_missing(
http_pool: &ks_onchain_transport::HttpEndpointPool,
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
accounts: &[ks_lib::MdPubkey],
) -> ks_core::Result<()> {
for account in accounts {
let value = match http_pool
.get_account_info_for_role(
options.query_role.as_str(),
account,
&ks_onchain_transport::GetAccountInfoConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if value.account.is_some() {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_fixture_address_collision",
format!("generated fixture address {} already exists", account.0),
));
}
}
return std::result::Result::Ok(());
}
async fn rent_for_space(
http_pool: &ks_onchain_transport::HttpEndpointPool,
query_role: &str,
space: u64,
) -> ks_core::Result<u64> {
let result = match http_pool
.get_minimum_balance_for_rent_exemption_for_role(
query_role,
space,
&ks_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(result.minimum_balance_lamports);
}
async fn submit_prefund_transfer(
http_pool: &ks_onchain_transport::HttpEndpointPool,
profile: &ks_config::ProfileConfig,
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
operator: &ks_wallet::TemporaryWallet,
recipient: ks_lib::MdPubkey,
lamports: u64,
label: &str,
) -> ks_core::Result<std::string::String> {
let payer = ks_lib::MdPubkey(operator.public_key());
let policy = ks_lib::ExApiExecutionPolicy {
cluster: ks_lib::ExApiExecutionClusterPolicy {
expected_cluster: ks_lib::ExApiExecutionCluster::Devnet,
allow_mainnet: false,
mainnet_confirmation: false,
},
simulation: ks_lib::ExApiExecutionSimulationPolicy::Required,
blockhash: ks_lib::ExApiExecutionBlockhashPolicy {
kind: ks_lib::ExApiExecutionBlockhashKind::Latest,
max_age_slots: std::option::Option::Some(
profile.execution.recent_blockhash_max_age_slots,
),
nonce_account: std::option::Option::None,
nonce_authority: std::option::Option::None,
},
cost_limit: ks_lib::ExApiExecutionCostLimit {
max_spend_lamports: std::option::Option::Some(
profile.execution.devnet_max_spend_lamports,
),
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
max_compute_unit_price_micro_lamports: std::option::Option::Some(
profile.execution.max_compute_unit_price_micro_lamports,
),
},
authorized_signers: vec![payer.clone()],
dry_run: false,
post_execution_validation: ks_lib::ExApiPostExecutionValidationPolicy {
canonical_insert_required: false,
core_extraction_required: false,
decode_replay_required: false,
materialization_required: false,
},
};
let intent = ks_lib::ExSolanaCoreExecutionIntent {
intent_id: format!("spm-fixture-prefund-{label}-{}", recipient.0),
fee_payer: payer.clone(),
policy,
operation: ks_lib::ExSolanaCoreOperation::SystemTransfer {
from: payer,
to: recipient,
lamports,
},
};
let plan = match ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
&ks_lib::ExSolanaCoreExecutor,
&intent,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let plan_evaluation = match ks_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if plan_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_fixture_prefund_plan_denied",
crate::violation_message(plan_evaluation.violations.as_slice()),
));
}
let latest_blockhash = match http_pool
.get_latest_blockhash_for_role(
options.query_role.as_str(),
&ks_onchain_transport::GetLatestBlockhashConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned = match ks_lib::executor_solana_build_legacy_transaction(
&plan,
latest_blockhash.blockhash.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fee = match http_pool
.get_fee_for_message_for_role(
options.query_role.as_str(),
unsigned.message_base64().as_str(),
&ks_onchain_transport::GetFeeForMessageConfig::new(
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
std::option::Option::Some(latest_blockhash.context.slot),
),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned_base64 = match unsigned.transaction_base64() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_config = match ks_onchain_transport::SimulateTransactionConfig::new(
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
false,
false,
std::option::Option::Some(latest_blockhash.context.slot),
true,
std::option::Option::None,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_rpc = match http_pool
.simulate_transaction_for_role(
options.transaction_role.as_str(),
unsigned_base64.as_str(),
&simulation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation = simulation_rpc.to_execution_result(
ks_lib::ExApiExecutionCluster::Devnet,
ks_lib::ExApiExecutionBlockhashKind::Latest,
std::option::Option::Some(
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(&fee),
);
if !simulation.success {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_fixture_prefund_simulation_failed",
crate::simulation_failure_message(&simulation),
));
}
let send_evaluation = match ks_lib::ExSafetyChecker.evaluate_send(&plan, &simulation) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if send_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_fixture_prefund_send_denied",
crate::violation_message(send_evaluation.violations.as_slice()),
));
}
let evidence = unsigned.bind_simulation(simulation.clone());
let signed = match unsigned.sign_after_simulation(&evidence, &[operator.as_signer()]) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = signed.verify_signatures() {
return std::result::Result::Err(error);
}
let signature = signed.primary_signature().clone();
let send_config = match ks_onchain_transport::SendTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = http_pool
.send_transaction_for_role(
options.transaction_role.as_str(),
signed.transaction_base64().as_str(),
&signature,
&send_config,
)
.await
{
return std::result::Result::Err(error);
}
let confirmation_config =
match ks_onchain_transport::ConfirmTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(latest_blockhash.last_valid_block_height),
std::option::Option::Some(latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmation = match http_pool
.confirm_transaction_for_roles(
options.transaction_role.as_str(),
options.query_role.as_str(),
ks_lib::ExApiExecutionCluster::Devnet,
&signature,
&confirmation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !matches!(
confirmation.status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
) {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_fixture_prefund_confirmation_failed",
format!("prefunding transaction stopped at {:?}", confirmation.status),
));
}
return std::result::Result::Ok(signature.0);
}
#[cfg(test)]
mod tests {
#[test]
fn fixture_seed_shapes_and_journey_inventory_are_exact() {
let unique = uuid::Uuid::nil().simple().to_string();
let buffer_seed = format!("buf{}", &unique[0..13]);
let metadata_seed = format!("met{}", &unique[13..26]);
assert_eq!(buffer_seed.len(), ks_lib::DC_METADATA_SPM_SEED_BYTES);
assert_eq!(metadata_seed.len(), ks_lib::DC_METADATA_SPM_SEED_BYTES);
let authority =
ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([1_u8; 32]).to_string());
let buffer =
ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([2_u8; 32]).to_string());
let metadata =
ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([3_u8; 32]).to_string());
let steps = super::prepared_steps(
"custom_queries",
authority,
ks_lib::MdPubkey(ks_program_ids::SYSTEM_PROGRAM_ID.to_string()),
buffer,
buffer_seed.as_bytes(),
metadata,
metadata_seed.as_bytes(),
br#"{"version":1}"#.to_vec(),
br#"{"version":2}"#.to_vec(),
1_000_000,
96,
1_000_000,
224,
1_000_000,
1_000_000,
109,
1_000_000,
109,
1_000_000,
);
let actual = steps
.iter()
.map(|step| return step.operation.operation_code())
.collect::<std::vec::Vec<_>>();
let scenarios = crate::solana_program_metadata_devnet_scenarios();
let expected = scenarios
.iter()
.flat_map(|scenario| return scenario.steps.iter())
.map(|step| return step.operation_code.as_str())
.collect::<std::vec::Vec<_>>();
assert_eq!(actual, expected);
let expected_buffer =
ks_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([2_u8; 32]).to_string());
let set_authority = steps.iter().find(|step| {
return step.operation.operation_code()
== ks_lib::EX_METADATA_SPM_SET_AUTHORITY_OPERATION;
});
assert!(matches!(
set_authority.map(|step| return &step.operation),
std::option::Option::Some(ks_lib::ExMetadataSpmOperation::SetAuthority {
account,
..
}) if account == &expected_buffer
));
assert!(steps.iter().all(|step| {
return step
.preflight_reads
.iter()
.chain(step.postcondition_reads.iter())
.all(|read| return read.query_role == "custom_queries");
}));
}
}

View File

@@ -0,0 +1,237 @@
// file: ks-pipeline-demo-scenarios/src/metadata/solana_program/scenarios.rs
// version: 5
//! Coherent Solana Program Metadata journeys for Devnet validation.
/// Stable lifecycle state consumed or produced by one scenario step.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SolanaProgramMetadataFixtureState {
/// Pre-funded but uninitialized PDA.
Prefunded,
/// Initialized Buffer account.
Buffer,
/// Initialized mutable Metadata account.
MutableMetadata,
/// Initialized immutable Metadata account.
ImmutableMetadata,
/// Closed account.
Closed,
}
/// One stable step in a Solana Program Metadata Devnet journey.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SolanaProgramMetadataScenarioStep {
/// Stable scenario-step identifier.
pub id: std::string::String,
/// Operator-visible label.
pub label: std::string::String,
/// Stable executor operation code.
pub operation_code: std::string::String,
/// Expected state before the operation.
pub initial_state: crate::SolanaProgramMetadataFixtureState,
/// Expected state after confirmed execution.
pub resulting_state: crate::SolanaProgramMetadataFixtureState,
/// Whether the operation requires explicit destructive approval.
pub requires_explicit_approval: bool,
/// Required evidence kinds.
pub required_evidence: std::vec::Vec<std::string::String>,
}
/// One ordered multi-step Solana Program Metadata journey.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SolanaProgramMetadataScenario {
/// Stable scenario identifier.
pub id: std::string::String,
/// Operator-visible label.
pub label: std::string::String,
/// Fixture account selected by the journey.
pub fixture_account: std::string::String,
/// Ordered scenario steps.
pub steps: std::vec::Vec<crate::SolanaProgramMetadataScenarioStep>,
}
/// Returns the two ordered Devnet journeys covering all nine stable operations.
pub fn solana_program_metadata_devnet_scenarios()
-> std::vec::Vec<crate::SolanaProgramMetadataScenario> {
return vec![
crate::SolanaProgramMetadataScenario {
id: "spm_buffer_lifecycle".to_string(),
label: "Buffer : allocation, extension, écriture, autorité, trim et fermeture"
.to_string(),
fixture_account: "buffer".to_string(),
steps: vec![
step(
"spm_buffer_allocate",
"Allouer le Buffer préfinancé",
ks_lib::EX_METADATA_SPM_ALLOCATE_OPERATION,
crate::SolanaProgramMetadataFixtureState::Prefunded,
crate::SolanaProgramMetadataFixtureState::Buffer,
false,
),
step(
"spm_buffer_extend",
"Étendre la capacité du Buffer",
ks_lib::EX_METADATA_SPM_EXTEND_OPERATION,
crate::SolanaProgramMetadataFixtureState::Buffer,
crate::SolanaProgramMetadataFixtureState::Buffer,
false,
),
step(
"spm_buffer_write",
"Écrire les données inline",
ks_lib::EX_METADATA_SPM_WRITE_OPERATION,
crate::SolanaProgramMetadataFixtureState::Buffer,
crate::SolanaProgramMetadataFixtureState::Buffer,
true,
),
step(
"spm_buffer_set_authority",
"Réaffirmer lautorité du Buffer non canonique",
ks_lib::EX_METADATA_SPM_SET_AUTHORITY_OPERATION,
crate::SolanaProgramMetadataFixtureState::Buffer,
crate::SolanaProgramMetadataFixtureState::Buffer,
true,
),
step(
"spm_buffer_trim",
"Réduire le Buffer et restituer lexcédent",
ks_lib::EX_METADATA_SPM_TRIM_OPERATION,
crate::SolanaProgramMetadataFixtureState::Buffer,
crate::SolanaProgramMetadataFixtureState::Buffer,
true,
),
step(
"spm_buffer_close",
"Fermer le Buffer",
ks_lib::EX_METADATA_SPM_CLOSE_OPERATION,
crate::SolanaProgramMetadataFixtureState::Buffer,
crate::SolanaProgramMetadataFixtureState::Closed,
true,
),
],
},
crate::SolanaProgramMetadataScenario {
id: "spm_metadata_lifecycle".to_string(),
label: "Metadata : initialisation, données et immutabilité".to_string(),
fixture_account: "metadata".to_string(),
steps: vec![
step(
"spm_metadata_initialize",
"Initialiser les metadata non-canoniques",
ks_lib::EX_METADATA_SPM_INITIALIZE_OPERATION,
crate::SolanaProgramMetadataFixtureState::Prefunded,
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
false,
),
step(
"spm_metadata_set_data",
"Mettre à jour les données JSON",
ks_lib::EX_METADATA_SPM_SET_DATA_OPERATION,
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
true,
),
step(
"spm_metadata_set_immutable",
"Rendre les metadata immuables",
ks_lib::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION,
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
crate::SolanaProgramMetadataFixtureState::ImmutableMetadata,
true,
),
],
},
];
}
fn step(
id: &str,
label: &str,
operation_code: &str,
initial_state: crate::SolanaProgramMetadataFixtureState,
resulting_state: crate::SolanaProgramMetadataFixtureState,
requires_explicit_approval: bool,
) -> crate::SolanaProgramMetadataScenarioStep {
return crate::SolanaProgramMetadataScenarioStep {
id: id.to_string(),
label: label.to_string(),
operation_code: operation_code.to_string(),
initial_state,
resulting_state,
requires_explicit_approval,
required_evidence: required_evidence(resulting_state),
};
}
fn required_evidence(
resulting_state: crate::SolanaProgramMetadataFixtureState,
) -> std::vec::Vec<std::string::String> {
let terminal_evidence = match resulting_state {
crate::SolanaProgramMetadataFixtureState::Closed => "account_absence",
_ => "materialized_snapshot",
};
return vec![
"stateful_preflight".to_string(),
"rpc_simulation".to_string(),
"confirmed_signature".to_string(),
"stateful_postcondition".to_string(),
terminal_evidence.to_string(),
];
}
#[cfg(test)]
mod tests {
#[test]
fn journeys_cover_each_stable_operation_exactly_once() {
let scenarios = crate::solana_program_metadata_devnet_scenarios();
assert_eq!(scenarios.len(), 2);
let operations = scenarios
.iter()
.flat_map(|scenario| return scenario.steps.iter())
.map(|step| return step.operation_code.as_str())
.collect::<std::vec::Vec<_>>();
assert_eq!(operations.len(), ks_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES.len());
for operation in ks_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES {
assert_eq!(
operations.iter().filter(|candidate| return **candidate == *operation).count(),
1
);
}
}
#[test]
fn set_authority_targets_the_non_canonical_buffer_not_metadata() {
let scenarios = crate::solana_program_metadata_devnet_scenarios();
let set_authority_steps = scenarios
.iter()
.flat_map(|scenario| {
return scenario
.steps
.iter()
.map(move |step| return (scenario.fixture_account.as_str(), step));
})
.filter(|(_, step)| {
return step.operation_code == ks_lib::EX_METADATA_SPM_SET_AUTHORITY_OPERATION;
})
.collect::<std::vec::Vec<_>>();
assert_eq!(set_authority_steps.len(), 1);
assert_eq!(set_authority_steps[0].0, "buffer");
assert_eq!(set_authority_steps[0].1.id, "spm_buffer_set_authority");
}
#[test]
fn destructive_flags_match_the_executor_contract() {
for scenario in crate::solana_program_metadata_devnet_scenarios() {
for step in scenario.steps {
assert_eq!(
step.requires_explicit_approval,
ks_lib::EX_METADATA_SPM_DESTRUCTIVE_OPERATION_CODES
.contains(&step.operation_code.as_str())
);
}
}
}
}

View File

@@ -0,0 +1,178 @@
// file: ks-pipeline-demo-scenarios/src/metadata/solana_program/validation.rs
// version: 5
//! Conservative Solana Program Metadata validation status and evidence contracts.
/// Maximum evidence entries accepted by one operation validation row.
pub const MAX_SOLANA_PROGRAM_METADATA_VALIDATION_EVIDENCE: usize = 16;
/// Exact network validation status.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SolanaProgramMetadataValidationStatus {
/// No network execution was attempted.
NotRun,
/// Exact transaction simulated successfully.
Simulated,
/// Transaction submitted but not yet confirmed.
Submitted,
/// Transaction confirmed and postconditions validated.
Confirmed,
/// Required network capability was unavailable.
Unavailable,
/// Validation failed with retained diagnostics.
Failed,
}
/// One bounded evidence record.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SolanaProgramMetadataValidationEvidence {
/// Stable evidence kind.
pub kind: std::string::String,
/// Non-secret evidence value.
pub value: std::string::String,
}
/// One operation row in the closed Devnet validation matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SolanaProgramMetadataValidationOperation {
/// Stable operation code.
pub operation_code: std::string::String,
/// Scenario step that exercises the operation.
pub scenario_step_id: std::string::String,
/// Exact current validation status.
pub status: crate::SolanaProgramMetadataValidationStatus,
/// Whether a destructive approval is required.
pub requires_explicit_approval: bool,
/// Evidence required for confirmed status.
pub required_evidence: std::vec::Vec<std::string::String>,
/// Bounded observed evidence.
#[serde(default)]
pub evidence: std::vec::Vec<crate::SolanaProgramMetadataValidationEvidence>,
}
/// Closed validation matrix for all nine stable operations.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SolanaProgramMetadataValidationMatrix {
/// Matrix schema version.
pub matrix_version: u32,
/// Exact owning prerelease.
pub milestone: std::string::String,
/// Ordered stable operations.
pub operations: std::vec::Vec<crate::SolanaProgramMetadataValidationOperation>,
}
/// Loads and validates the canonical Solana Program Metadata Devnet matrix.
pub fn load_solana_program_metadata_validation_matrix()
-> ks_core::Result<crate::SolanaProgramMetadataValidationMatrix> {
let parsed = match serde_json::from_str::<crate::SolanaProgramMetadataValidationMatrix>(
include_str!(
"../../../../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_DEVNET_VALIDATION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_validation_matrix_invalid_json",
format!("Solana Program Metadata validation matrix is invalid: {error}"),
));
},
};
if let std::result::Result::Err(error) =
validate_solana_program_metadata_validation_matrix(&parsed)
{
return std::result::Result::Err(error);
}
return std::result::Result::Ok(parsed);
}
/// Validates exact inventory, order and conservative evidence claims.
pub fn validate_solana_program_metadata_validation_matrix(
matrix: &crate::SolanaProgramMetadataValidationMatrix,
) -> ks_core::Result<()> {
if matrix.matrix_version != 1 || matrix.milestone != "0.4.8-pre.009" {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_validation_matrix_contract_mismatch",
"Solana Program Metadata validation matrix must use version 1 for 0.4.8-pre.009",
));
}
let compiled_steps = crate::solana_program_metadata_devnet_scenarios()
.into_iter()
.flat_map(|scenario| return scenario.steps)
.collect::<std::vec::Vec<_>>();
let expected = compiled_steps
.iter()
.map(|step| return step.operation_code.as_str())
.collect::<std::vec::Vec<_>>();
let actual = matrix
.operations
.iter()
.map(|operation| return operation.operation_code.as_str())
.collect::<std::vec::Vec<_>>();
if actual != expected {
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_validation_matrix_inventory_mismatch",
"Solana Program Metadata validation matrix differs from the compiled journey order",
));
}
for (operation, step) in matrix.operations.iter().zip(compiled_steps.iter()) {
if operation.scenario_step_id != step.id
|| operation.requires_explicit_approval != step.requires_explicit_approval
|| operation.required_evidence != step.required_evidence
|| operation.evidence.len() > crate::MAX_SOLANA_PROGRAM_METADATA_VALIDATION_EVIDENCE
{
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_validation_matrix_row_invalid",
"Solana Program Metadata validation row differs from the compiled scenario contract",
));
}
if matches!(
operation.status,
crate::SolanaProgramMetadataValidationStatus::NotRun
| crate::SolanaProgramMetadataValidationStatus::Unavailable
) && !operation.evidence.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_validation_unobserved_with_evidence",
"not-run or unavailable validation rows must not contain observed evidence",
));
}
if operation.status == crate::SolanaProgramMetadataValidationStatus::Confirmed {
let observed = operation
.evidence
.iter()
.map(|evidence| return evidence.kind.as_str())
.collect::<std::collections::BTreeSet<_>>();
if !operation
.required_evidence
.iter()
.all(|kind| return observed.contains(kind.as_str()))
{
return std::result::Result::Err(ks_core::Error::new(
"solana_program_metadata_validation_confirmed_without_evidence",
"confirmed validation row lacks required evidence",
));
}
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
fn canonical_matrix_matches_the_compiled_two_journey_inventory() {
let matrix = match crate::load_solana_program_metadata_validation_matrix() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("matrix load failed: {error}"),
};
assert_eq!(matrix.operations.len(), 9);
assert!(matrix.operations.iter().all(|operation| {
return operation.status == crate::SolanaProgramMetadataValidationStatus::Confirmed
&& operation.evidence.len() == 10;
}));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
// file: ks-pipeline-demo-scenarios/src/spl.rs
// version: 2
//! SPL-oriented reusable demo and Devnet validation scenarios.
pub(crate) mod associated_token_account;
pub(crate) mod memo;
pub(crate) mod token;
pub(crate) mod token_2022;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,908 @@
// file: ks-pipeline-demo-scenarios/src/spl/memo.rs
// version: 7
//! Devnet SPL Memo v4 execution with canonical post-validation.
/// Complete request for one SPL Memo v4 Devnet execution.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DevnetMemoExecutionRequest {
/// Stable caller-provided execution identifier.
pub intent_id: std::string::String,
/// Endpoint role used for cluster, balance, blockhash, fee and hydration calls.
pub query_role: std::string::String,
/// Endpoint role used for simulation, submission and confirmation polling.
pub transaction_role: std::string::String,
/// Exact UTF-8 Memo payload.
pub message: std::string::String,
/// Supplies the persistent Devnet fee payer as a Memo signer account.
pub include_wallet_as_memo_signer: bool,
/// Explicitly authorizes signing and submission after successful simulation.
pub submit: bool,
/// Explicit operator confirmation required by the active profile.
pub operator_confirmed: bool,
/// Number of `getTransaction` retries after the first hydration attempt.
pub post_validation_max_retries: u32,
/// Replaces existing core and decode outputs for the submitted signature.
pub force_post_validation_replay: bool,
}
impl DevnetMemoExecutionRequest {
/// Creates a conservative simulation-only Memo v4 request.
pub fn new(
intent_id: impl std::convert::Into<std::string::String>,
message: impl std::convert::Into<std::string::String>,
) -> Self {
return Self {
intent_id: intent_id.into(),
query_role: "http_queries".to_string(),
transaction_role: "http_transactions".to_string(),
message: message.into(),
include_wallet_as_memo_signer: true,
submit: false,
operator_confirmed: false,
post_validation_max_retries: 10,
force_post_validation_replay: false,
};
}
/// Validates request-local bounds independently from one profile.
pub fn validate(&self) -> ks_core::Result<()> {
if self.intent_id.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Devnet Memo execution intent id must not be empty",
));
}
if self.query_role.trim().is_empty() || self.transaction_role.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Devnet Memo execution endpoint roles must not be empty",
));
}
if self.message.len() > ks_lib::EX_SPL_MEMO_MAX_MESSAGE_BYTES {
return std::result::Result::Err(ks_core::Error::config(format!(
"Devnet Memo payload length {} exceeds the executor limit {}",
self.message.len(),
ks_lib::EX_SPL_MEMO_MAX_MESSAGE_BYTES
)));
}
if self.post_validation_max_retries > 20 {
return std::result::Result::Err(ks_core::Error::config(
"post-execution getTransaction retries must not exceed 20",
));
}
return std::result::Result::Ok(());
}
}
/// Complete result of one SPL Memo v4 Devnet execution.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetMemoExecutionSummary {
/// Profile used by the orchestration.
pub profile_name: std::string::String,
/// Exact classified cluster.
pub cluster: ks_lib::ExApiExecutionCluster,
/// Genesis hash returned by the selected endpoint.
pub genesis_hash: std::string::String,
/// Non-secret persistent wallet description.
pub wallet: ks_wallet::WalletSummary,
/// Wallet balance observed before planning.
pub balance_lamports: u64,
/// Exact prepared Memo plan.
pub plan: ks_lib::ExApiPreparedExecutionPlan,
/// Recent blockhash used by the exact transaction.
pub latest_blockhash: ks_onchain_transport::LatestBlockhashResult,
/// Fee estimate for the exact compiled message.
pub fee: ks_onchain_transport::FeeForMessageResult,
/// Exact simulation result bound to the compiled message.
pub simulation: ks_lib::ExApiExecutionSimulationResult,
/// Submission result when explicitly authorized.
pub send_result: std::option::Option<ks_lib::ExApiExecutionSendResult>,
/// Confirmation result when submitted.
pub confirmation: std::option::Option<ks_lib::ExApiExecutionConfirmationResult>,
/// Canonical hydration result for the exact signature.
pub backfill: std::option::Option<ks_pipeline::BackfillSummary>,
/// Core extraction result for the exact signature.
pub core_extraction: std::option::Option<ks_pipeline::CoreExtractionSummary>,
/// First Memo decode and materialization replay.
pub decode_replay: std::option::Option<ks_pipeline::DecodeReplaySummary>,
/// Second replay proving that the same decoder version and input are idempotent.
pub idempotence_replay: std::option::Option<ks_pipeline::DecodeReplaySummary>,
/// Exact persisted transaction annotation rows for the submitted signature.
pub annotations: std::vec::Vec<ks_store::MaterializedEventQueryRow>,
/// Aggregated post-execution validation diagnostic.
pub post_execution: std::option::Option<ks_lib::ExApiPostExecutionDiagnostic>,
}
/// Executes one Memo v4 Devnet simulation or explicitly authorized submission.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_memo<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetMemoExecutionRequest,
decoders: &[std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>],
materializers: &[std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>],
observer: &O,
) -> ks_core::Result<crate::DevnetMemoExecutionSummary>
where
S: ks_store::RawTransactionStore
+ ks_store::CoreExtractionStore
+ ks_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = validate_profile(profile, request) {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = crate::ensure_not_cancelled(observer, "validate") {
return std::result::Result::Err(error);
}
let genesis = match http_pool.get_genesis_hash_for_role(request.query_role.as_str()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if genesis.classified_cluster
!= std::option::Option::Some(ks_lib::ExApiExecutionCluster::Devnet)
{
return std::result::Result::Err(ks_core::Error::new(
"execution_cluster_mismatch",
format!(
"expected Devnet genesis hash but endpoint returned {} classified as {:?}",
genesis.genesis_hash, genesis.classified_cluster
),
));
}
let wallet = 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 wallet_summary = wallet.summary();
let fee_payer = ks_lib::MdPubkey(wallet_summary.public_key.clone());
let balance = match http_pool
.get_balance_for_role(
request.query_role.as_str(),
&fee_payer,
&ks_onchain_transport::GetBalanceConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if balance.lamports < profile.execution.max_fee_lamports {
return std::result::Result::Err(ks_core::Error::new(
"execution_balance_insufficient",
format!(
"Devnet wallet balance {} is below the configured fee ceiling {}",
balance.lamports, profile.execution.max_fee_lamports
),
));
}
let plan = match build_plan(profile, request, fee_payer.clone()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let plan_evaluation = match ks_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if plan_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"execution_plan_denied",
crate::violation_message(plan_evaluation.violations.as_slice()),
));
}
let latest_blockhash = match http_pool
.get_latest_blockhash_for_role(
request.query_role.as_str(),
&ks_onchain_transport::GetLatestBlockhashConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned = match ks_lib::executor_solana_build_legacy_transaction(
&plan,
latest_blockhash.blockhash.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let message_base64 = unsigned.message_base64();
let fee = match http_pool
.get_fee_for_message_for_role(
request.query_role.as_str(),
message_base64.as_str(),
&ks_onchain_transport::GetFeeForMessageConfig::new(
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
std::option::Option::Some(latest_blockhash.context.slot),
),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if fee.fee_lamports.is_none() {
return std::result::Result::Err(ks_core::Error::new(
"execution_fee_unavailable",
"getFeeForMessage returned null for the selected recent blockhash",
));
}
let unsigned_base64 = match unsigned.transaction_base64() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_config = match ks_onchain_transport::SimulateTransactionConfig::new(
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
false,
false,
std::option::Option::Some(latest_blockhash.context.slot),
true,
std::option::Option::None,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::emit(
observer,
crate::SolanaExecutionProgressLevel::Info,
"memo_simulation",
format!("simulating exact Memo message {}", unsigned.message_hash()),
std::option::Option::None,
);
let simulation_rpc = match http_pool
.simulate_transaction_for_role(
request.transaction_role.as_str(),
unsigned_base64.as_str(),
&simulation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation = simulation_rpc.to_execution_result(
ks_lib::ExApiExecutionCluster::Devnet,
ks_lib::ExApiExecutionBlockhashKind::Latest,
std::option::Option::Some(
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(&fee),
);
let evidence = unsigned.bind_simulation(simulation.clone());
let mut summary = crate::DevnetMemoExecutionSummary {
profile_name: profile.name.clone(),
cluster: ks_lib::ExApiExecutionCluster::Devnet,
genesis_hash: genesis.genesis_hash,
wallet: wallet_summary,
balance_lamports: balance.lamports,
plan,
latest_blockhash,
fee,
simulation,
send_result: std::option::Option::None,
confirmation: std::option::Option::None,
backfill: std::option::Option::None,
core_extraction: std::option::Option::None,
decode_replay: std::option::Option::None,
idempotence_replay: std::option::Option::None,
annotations: std::vec::Vec::new(),
post_execution: std::option::Option::None,
};
if !request.submit {
return std::result::Result::Ok(summary);
}
if !summary.simulation.success {
return std::result::Result::Err(ks_core::Error::new(
"execution_simulation_failed",
crate::simulation_failure_message(&summary.simulation),
));
}
let send_evaluation =
match ks_lib::ExSafetyChecker.evaluate_send(&summary.plan, &summary.simulation) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if send_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"execution_send_denied",
crate::violation_message(send_evaluation.violations.as_slice()),
));
}
let signed = match unsigned.sign_after_simulation(&evidence, &[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 mut diagnostic = ks_lib::ExApiPostExecutionDiagnostic {
signature: signature.clone(),
canonical_inserted: false,
core_extracted: false,
decode_replayed: false,
materialized: false,
diagnostics: std::vec::Vec::new(),
};
let send_config = match ks_onchain_transport::SendTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(summary.latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let signed_base64 = signed.transaction_base64();
let sent = match http_pool
.send_transaction_for_role(
request.transaction_role.as_str(),
signed_base64.as_str(),
&signature,
&send_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Memo submission failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
summary.send_result =
std::option::Option::Some(sent.to_execution_result(ks_lib::ExApiExecutionCluster::Devnet));
let confirmation_config =
match ks_onchain_transport::ConfirmTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(summary.latest_blockhash.last_valid_block_height),
std::option::Option::Some(summary.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(
request.transaction_role.as_str(),
request.query_role.as_str(),
ks_lib::ExApiExecutionCluster::Devnet,
&signature,
&confirmation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Memo confirmation failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
let confirmation_status = confirmation.status;
summary.confirmation = std::option::Option::Some(confirmation);
if !matches!(
confirmation_status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
) {
diagnostic.diagnostics.push(format!(
"Memo post-validation stopped at confirmation status {confirmation_status:?}"
));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
let backfill =
match hydrate_signature(http_pool, store, profile, request, observer, &signature).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Memo hydration failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
diagnostic.canonical_inserted = canonical_available(&backfill);
summary.backfill = std::option::Option::Some(backfill);
if !diagnostic.canonical_inserted {
diagnostic
.diagnostics
.push("confirmed Memo transaction was unavailable for canonical hydration".to_string());
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
let extraction = match ks_pipeline::execute_core_extraction(
store,
&ks_pipeline::CoreExtractionRequest {
source: ks_pipeline::CoreExtractionSource::Signatures(std::vec![signature.0.clone()]),
limit: 1,
max_concurrent_extractions: 1,
force_replay: request.force_post_validation_replay,
},
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Memo core extraction failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
diagnostic.core_extracted = extraction.failed == 0
&& !extraction.cancelled
&& extraction.selected == 1
&& extraction.extracted.saturating_add(extraction.skipped) >= 1;
summary.core_extraction = std::option::Option::Some(extraction);
if !diagnostic.core_extracted {
diagnostic.diagnostics.push("Memo core extraction did not complete".to_string());
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
let first_replay =
match replay_memo(store, request, &signature, false, decoders, materializers, observer)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Memo decode replay failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
diagnostic.decode_replayed = decode_completed(&first_replay);
summary.decode_replay = std::option::Option::Some(first_replay);
let filter = match ks_store::MaterializedEventFilter::new(
std::option::Option::Some("materializer.transaction.annotations".to_string()),
std::option::Option::Some("transaction_annotation".to_string()),
std::option::Option::Some(signature.0.clone()),
8,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
summary.annotations =
match ks_store::DecodePipelineStore::list_materialized_events(store, &filter).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Memo annotation query failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
diagnostic.materialized = diagnostic.decode_replayed
&& summary
.annotations
.iter()
.any(|row| return row.signature.as_str() == signature.0.as_str());
let second_replay = match replay_memo(
store,
request,
&signature,
true,
decoders,
materializers,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Memo idempotence replay failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
let idempotent = second_replay.failed_inputs == 0
&& second_replay.processing_error_inputs == 0
&& second_replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
summary.idempotence_replay = std::option::Option::Some(second_replay);
if !idempotent {
diagnostic
.diagnostics
.push("second Memo replay did not prove a clean idempotent skip".to_string());
} else if diagnostic.canonical_inserted
&& diagnostic.core_extracted
&& diagnostic.decode_replayed
&& diagnostic.materialized
{
diagnostic.diagnostics.push(
"Memo completed canonical hydration, core extraction, decode, annotation projection and idempotence validation"
.to_string(),
);
}
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
fn validate_profile(
profile: &ks_config::ProfileConfig,
request: &crate::DevnetMemoExecutionRequest,
) -> ks_core::Result<()> {
if profile.wallet.cluster != "devnet" {
return std::result::Result::Err(ks_core::Error::config(
"Memo Devnet orchestration requires a Devnet wallet profile",
));
}
if !profile.wallet.temporary_wallet_enabled || !profile.wallet.temporary_wallet_persist {
return std::result::Result::Err(ks_core::Error::config(
"Memo Devnet orchestration requires an enabled persistent temporary wallet",
));
}
if !profile.execution.require_simulation {
return std::result::Result::Err(ks_core::Error::config(
"Memo Devnet orchestration requires simulation",
));
}
if request.submit && !profile.wallet.devnet_send_enabled {
return std::result::Result::Err(ks_core::Error::config(
"Devnet transaction submission is disabled by the wallet profile",
));
}
if request.submit
&& profile.execution.require_operator_confirmation
&& !request.operator_confirmed
{
return std::result::Result::Err(ks_core::Error::config(
"Memo Devnet submission requires explicit operator confirmation",
));
}
return std::result::Result::Ok(());
}
fn build_plan(
profile: &ks_config::ProfileConfig,
request: &crate::DevnetMemoExecutionRequest,
fee_payer: ks_lib::MdPubkey,
) -> ks_core::Result<ks_lib::ExApiPreparedExecutionPlan> {
let signers = if request.include_wallet_as_memo_signer {
std::vec![ks_lib::ExSplMemoSigner { pubkey: fee_payer.clone() }]
} else {
std::vec::Vec::new()
};
let intent = ks_lib::ExSplMemoExecutionIntent {
intent_id: request.intent_id.clone(),
fee_payer: fee_payer.clone(),
policy: ks_lib::ExApiExecutionPolicy {
cluster: ks_lib::ExApiExecutionClusterPolicy {
expected_cluster: ks_lib::ExApiExecutionCluster::Devnet,
allow_mainnet: false,
mainnet_confirmation: false,
},
simulation: ks_lib::ExApiExecutionSimulationPolicy::Required,
blockhash: ks_lib::ExApiExecutionBlockhashPolicy {
kind: ks_lib::ExApiExecutionBlockhashKind::Latest,
max_age_slots: std::option::Option::Some(
profile.execution.recent_blockhash_max_age_slots,
),
nonce_account: std::option::Option::None,
nonce_authority: std::option::Option::None,
},
cost_limit: ks_lib::ExApiExecutionCostLimit {
max_spend_lamports: std::option::Option::Some(0),
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![fee_payer.clone()],
dry_run: !request.submit,
post_execution_validation: ks_lib::ExApiPostExecutionValidationPolicy {
canonical_insert_required: true,
core_extraction_required: true,
decode_replay_required: true,
materialization_required: true,
},
},
operation: ks_lib::ExSplMemoOperation::AddMemo {
generation: ks_lib::ExSplMemoGeneration::V4,
message: request.message.clone(),
signers,
},
};
return ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
&ks_lib::ExSplMemoExecutor,
&intent,
);
}
async fn hydrate_signature<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
request: &crate::DevnetMemoExecutionRequest,
observer: &O,
signature: &ks_lib::MdSignature,
) -> ks_core::Result<ks_pipeline::BackfillSummary>
where
S: ks_store::RawTransactionStore + Sync,
O: crate::SolanaExecutionObserver,
{
let mut retry = 0_u32;
loop {
let result = match ks_pipeline::execute_http_backfill(
http_pool,
store,
&ks_pipeline::BackfillRequest {
role: request.query_role.clone(),
commitment: "confirmed".to_string(),
source: ks_pipeline::BackfillSource::ExplicitSignatures(std::vec![
signature.0.clone()
]),
page_size: 1,
max_pages: 1,
max_concurrent_requests: 1,
max_retries: 0,
},
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if canonical_available(&result)
|| retry >= request.post_validation_max_retries
|| observer.is_execution_cancelled()
{
return std::result::Result::Ok(result);
}
retry = retry.saturating_add(1);
tokio::time::sleep(std::time::Duration::from_millis(std::cmp::max(
profile.execution.confirmation_poll_interval_ms,
500,
)))
.await;
}
}
fn canonical_available(summary: &ks_pipeline::BackfillSummary) -> bool {
return summary.failed == 0
&& summary.missing == 0
&& summary.candidates_completed == 1
&& summary.candidates_cancelled == 0
&& summary.candidates_not_started == 0
&& summary
.canonical_inserted
.saturating_add(summary.canonical_skipped)
.saturating_add(summary.existing_skipped)
>= 1;
}
async fn replay_memo<S, O>(
store: &S,
request: &crate::DevnetMemoExecutionRequest,
signature: &ks_lib::MdSignature,
include_materialized_state: bool,
decoders: &[std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>],
materializers: &[std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>],
observer: &O,
) -> ks_core::Result<ks_pipeline::DecodeReplaySummary>
where
S: ks_store::DecodePipelineStore + Sync,
O: crate::SolanaExecutionObserver,
{
let mut states = std::vec![
ks_store::CoreInstructionProcessingState::Pending,
ks_store::CoreInstructionProcessingState::Failed,
ks_store::CoreInstructionProcessingState::ReplayRequested,
];
if include_materialized_state {
states.push(ks_store::CoreInstructionProcessingState::Materialized);
}
let selection = match ks_store::DecodeSelectionFilter::new(
std::vec![signature.0.clone()],
states,
std::option::Option::None,
std::option::Option::None,
std::vec![ks_program_ids::SPL_MEMO_V4_PROGRAM_ID.to_string()],
std::vec::Vec::new(),
false,
8,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return ks_pipeline::execute_decode_replay(
store,
&ks_pipeline::DecodeReplayRequest {
campaign_id: ks_pipeline::new_decode_campaign_id(),
selection,
decoder_names: std::vec::Vec::new(),
dispatch_policy: ks_pipeline::DecodeDispatchPolicy::HighestPriority,
max_concurrent_inputs: 1,
force_replay: if include_materialized_state {
false
} else {
request.force_post_validation_replay
},
force_replay_all_matching: false,
materialize_after_decode: true,
},
decoders,
materializers,
observer,
)
.await;
}
fn decode_completed(summary: &ks_pipeline::DecodeReplaySummary) -> bool {
return summary.failed_inputs == 0
&& summary.processing_error_inputs == 0
&& summary.unmatched == 0
&& !summary.cancelled
&& summary.completed >= 1
&& summary.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.unsupported == 0
&& processor.materialization_refused == 0;
})
&& summary.processors.iter().map(|processor| return processor.decoded).sum::<u64>() >= 1;
}
#[cfg(test)]
mod tests {
fn example_devnet_profile() -> ks_config::ProfileConfig {
let config =
match ks_config::parse_config_json(include_str!("../../../config/example.config.json"))
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
};
return match crate::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(profile) => profile,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),
};
}
#[test]
fn request_is_simulation_only_and_bounded_by_default() {
let request = crate::DevnetMemoExecutionRequest::new("memo-1", "audit annotation");
assert!(!request.submit);
assert!(request.include_wallet_as_memo_signer);
assert!(request.validate().is_ok());
let oversized = "x".repeat(ks_lib::EX_SPL_MEMO_MAX_MESSAGE_BYTES + 1);
assert!(crate::DevnetMemoExecutionRequest::new("memo-2", oversized).validate().is_err());
}
#[test]
fn exact_v4_plan_has_zero_spend_and_wallet_signer() {
let profile = example_devnet_profile();
let fee_payer = ks_lib::MdPubkey(ks_program_ids::SYSTEM_PROGRAM_ID.to_string());
let mut request = crate::DevnetMemoExecutionRequest::new("memo-3", "hello");
request.submit = true;
request.operator_confirmed = true;
let plan = match super::build_plan(&profile, &request, fee_payer.clone()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("Memo plan failed: {error}"),
};
assert_eq!(plan.requested_spend_lamports, 0);
assert_eq!(plan.fee_payer, fee_payer);
assert_eq!(plan.instructions.len(), 1);
assert_eq!(plan.instructions[0].program_id.0, ks_program_ids::SPL_MEMO_V4_PROGRAM_ID);
assert_eq!(plan.instructions[0].accounts.len(), 1);
assert!(plan.instructions[0].accounts[0].is_signer);
assert!(!plan.instructions[0].accounts[0].is_writable);
assert!(ks_lib::ExSafetyChecker.evaluate_prepared_plan(&plan).is_ok());
}
#[test]
fn submission_requires_profile_enablement_and_confirmation() {
let profile = example_devnet_profile();
let mut request = crate::DevnetMemoExecutionRequest::new("memo-4", "hello");
assert!(super::validate_profile(&profile, &request).is_ok());
request.submit = true;
assert!(super::validate_profile(&profile, &request).is_err());
request.operator_confirmed = true;
assert!(super::validate_profile(&profile, &request).is_ok());
}
#[tokio::test]
async fn optional_devnet_memo_execution_from_env() {
if std::env::var("KB_DEVNET_MEMO_EXECUTION_TEST").ok().as_deref()
!= std::option::Option::Some("1")
{
return;
}
let database_url = match std::env::var("KB_POSTGRES_TEST_URL") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("KB_POSTGRES_TEST_URL is required: {error}");
},
};
let mut profile = example_devnet_profile();
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
if let std::result::Result::Ok(directory) = std::env::var("KB_DEVNET_WALLET_DIR") {
profile.wallet.wallet_dir = directory;
}
let pool = match ks_onchain_transport::HttpEndpointPool::from_profile(&profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"),
};
let store_options = match ks_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("PostgreSQL options failed: {error}"),
};
let store = match ks_store::PostgresStore::connect(store_options).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("PostgreSQL connection failed: {error}"),
};
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("PostgreSQL schema initialization failed: {error}");
}
let mut request = crate::DevnetMemoExecutionRequest::new(
format!("devnet-memo-test-{}", uuid::Uuid::new_v4()),
format!("khadhroony-bot3 memo validation {}", uuid::Uuid::new_v4()),
);
request.post_validation_max_retries = 20;
if std::env::var("KB_DEVNET_MEMO_SUBMIT").ok().as_deref() == std::option::Option::Some("1")
{
request.submit = true;
request.operator_confirmed = true;
}
let decoders: std::vec::Vec<std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(ks_lib::DcSplMemoDecoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>> =
std::vec![std::sync::Arc::new(ks_lib::MtTransactionAnnotationMaterializer,)];
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
};
let summary = match crate::execute_devnet_memo(
&pool,
&store,
&profile,
workspace_root,
&request,
decoders.as_slice(),
materializers.as_slice(),
&crate::NoopSolanaExecutionObserver,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("Devnet Memo execution failed: {error}"),
};
assert!(summary.simulation.success);
if request.submit {
let post_execution = match summary.post_execution {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("Memo post-execution diagnostic missing"),
};
assert!(post_execution.canonical_inserted);
assert!(post_execution.core_extracted);
assert!(post_execution.decode_replayed);
assert!(post_execution.materialized);
assert!(!summary.annotations.is_empty());
let idempotence = match summary.idempotence_replay {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("Memo idempotence replay missing"),
};
assert_eq!(
idempotence
.processors
.iter()
.map(|processor| return processor.materialized_outputs)
.sum::<u64>(),
0
);
}
}
}

View File

@@ -0,0 +1,7 @@
// file: ks-pipeline-demo-scenarios/src/spl/token.rs
// version: 2
//! Classic SPL Token demo and Devnet validation scenarios.
pub(crate) mod execution;
pub(crate) mod lifecycle;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022.rs
// version: 2
//! SPL Token-2022 demo and Devnet validation scenarios.
pub(crate) mod devnet_execution;
pub(crate) mod devnet_scenarios;
pub(crate) mod fixture;
pub(crate) mod metadata;
pub(crate) mod validation;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,180 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/devnet_scenarios.rs
// version: 9
//! Stable Devnet validation scenarios required to close milestone 0.4.6.
/// Stable category of one independent Devnet validation scenario.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DevnetSplValidationFamily {
/// Public Token-2022 state mutations without cryptographic proofs.
Token2022Public,
/// ElGamal Registry lifecycle kept technically separate from Token-2022.
ElGamalRegistry,
/// Token-2022 confidential operations requiring proof material.
Token2022Confidential,
}
/// Stable status of one scenario in the application validation workflow.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DevnetSplValidationImplementationStatus {
/// The scenario can be simulated and submitted through the application.
Executable,
/// The typed backend exists but the application execution path remains to be connected.
BackendReady,
/// The scenario requires externally prepared proof material before execution.
ProofFixtureRequired,
}
/// One independent Devnet validation scenario exposed to applications.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct DevnetSplValidationScenario {
/// Stable identifier used by scripts, tests and the frontend.
pub id: std::string::String,
/// Operator-visible label.
pub label: std::string::String,
/// Scenario family.
pub family: crate::DevnetSplValidationFamily,
/// Stable executor operation code.
pub operation_code: std::string::String,
/// Current implementation status.
pub implementation_status: crate::DevnetSplValidationImplementationStatus,
/// Whether this scenario mutates on-chain state.
pub destructive: bool,
/// Whether explicit operator confirmation is mandatory before submission.
pub operator_confirmation_required: bool,
/// Whether cryptographic proof material is required.
pub proof_required: bool,
/// Ordered fixture variables required by the scenario.
pub required_fixture_variables: std::vec::Vec<std::string::String>,
}
/// Returns the complete ordered Devnet scenario inventory for milestone 0.4.6.
pub fn devnet_spl_validation_scenarios() -> std::vec::Vec<crate::DevnetSplValidationScenario> {
return vec![
public_scenario("token_2022_mint_to_checked", "Token-2022 MintToChecked", ks_lib::EX_SPL_TOKEN_2022_MINT_TO_CHECKED_OPERATION, &["TOKEN_2022_MINT", "TOKEN_2022_SOURCE", "TOKEN_2022_AUTHORITY", "TOKEN_2022_DECIMALS", "TOKEN_2022_MINT_AMOUNT_RAW"]),
public_scenario("token_2022_transfer_checked", "Token-2022 TransferChecked", ks_lib::EX_SPL_TOKEN_2022_TRANSFER_CHECKED_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_DESTINATION", "TOKEN_2022_AUTHORITY", "TOKEN_2022_DECIMALS", "TOKEN_2022_TRANSFER_AMOUNT_RAW"]),
public_scenario("token_2022_approve_checked", "Token-2022 ApproveChecked", ks_lib::EX_SPL_TOKEN_2022_APPROVE_CHECKED_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_DELEGATE", "TOKEN_2022_AUTHORITY", "TOKEN_2022_DECIMALS", "TOKEN_2022_APPROVE_AMOUNT_RAW"]),
public_scenario("token_2022_revoke", "Token-2022 Revoke", ks_lib::EX_SPL_TOKEN_2022_REVOKE_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_AUTHORITY"]),
public_scenario("token_2022_burn_checked", "Token-2022 BurnChecked", ks_lib::EX_SPL_TOKEN_2022_BURN_CHECKED_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_AUTHORITY", "TOKEN_2022_DECIMALS", "TOKEN_2022_BURN_AMOUNT_RAW"]),
public_scenario("token_2022_freeze_account", "Token-2022 FreezeAccount", ks_lib::EX_SPL_TOKEN_2022_FREEZE_ACCOUNT_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_FREEZE_AUTHORITY"]),
public_scenario("token_2022_thaw_account", "Token-2022 ThawAccount", ks_lib::EX_SPL_TOKEN_2022_THAW_ACCOUNT_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_FREEZE_AUTHORITY"]),
public_scenario("token_2022_close_destination", "Token-2022 CloseAccount destination", ks_lib::EX_SPL_TOKEN_2022_CLOSE_ACCOUNT_OPERATION, &["TOKEN_2022_DESTINATION", "KB_DEVNET_WALLET_ADDRESS", "TOKEN_2022_AUTHORITY"]),
registry_scenario("elgamal_registry_create", "ElGamal Registry CreateRegistry", "spl.elgamal_registry.create_registry", &["ELGAMAL_REGISTRY_ADDRESS", "ELGAMAL_PUBKEY_BASE64", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
registry_scenario("elgamal_registry_update", "ElGamal Registry UpdateRegistry", "spl.elgamal_registry.update_registry", &["ELGAMAL_REGISTRY_ADDRESS", "ELGAMAL_PUBKEY_BASE64", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
confidential_scenario("token_2022_configure_confidential_account", "Token-2022 ConfigureAccountWithRegistry", ks_lib::EX_SPL_TOKEN_2022_CONFIGURE_CONFIDENTIAL_TRANSFER_ACCOUNT_WITH_REGISTRY_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "ELGAMAL_REGISTRY_ADDRESS", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
];
}
fn public_scenario(
id: &str,
label: &str,
operation_code: &str,
variables: &[&str],
) -> crate::DevnetSplValidationScenario {
return scenario(
id,
label,
crate::DevnetSplValidationFamily::Token2022Public,
operation_code,
crate::DevnetSplValidationImplementationStatus::Executable,
true,
false,
variables,
);
}
fn registry_scenario(
id: &str,
label: &str,
operation_code: &str,
variables: &[&str],
) -> crate::DevnetSplValidationScenario {
return scenario(
id,
label,
crate::DevnetSplValidationFamily::ElGamalRegistry,
operation_code,
crate::DevnetSplValidationImplementationStatus::ProofFixtureRequired,
true,
true,
variables,
);
}
fn confidential_scenario(
id: &str,
label: &str,
operation_code: &str,
variables: &[&str],
) -> crate::DevnetSplValidationScenario {
return scenario(
id,
label,
crate::DevnetSplValidationFamily::Token2022Confidential,
operation_code,
crate::DevnetSplValidationImplementationStatus::ProofFixtureRequired,
true,
true,
variables,
);
}
fn scenario(
id: &str,
label: &str,
family: crate::DevnetSplValidationFamily,
operation_code: &str,
implementation_status: crate::DevnetSplValidationImplementationStatus,
destructive: bool,
proof_required: bool,
variables: &[&str],
) -> crate::DevnetSplValidationScenario {
return crate::DevnetSplValidationScenario {
id: id.to_string(),
label: label.to_string(),
family,
operation_code: operation_code.to_string(),
implementation_status,
destructive,
operator_confirmation_required: destructive,
proof_required,
required_fixture_variables: variables
.iter()
.map(|value| return (*value).to_string())
.collect(),
};
}
#[cfg(test)]
mod tests {
#[test]
fn milestone_inventory_is_stable_unique_and_keeps_registry_separate() {
let scenarios = crate::devnet_spl_validation_scenarios();
assert_eq!(scenarios.len(), 11);
let ids = scenarios
.iter()
.map(|scenario| return scenario.id.as_str())
.collect::<std::collections::BTreeSet<&str>>();
assert_eq!(ids.len(), scenarios.len());
assert!(
scenarios.iter().any(|scenario| return scenario.family
== crate::DevnetSplValidationFamily::ElGamalRegistry)
);
assert!(
scenarios
.iter()
.filter(|scenario| return scenario.family
== crate::DevnetSplValidationFamily::Token2022Public)
.all(|scenario| return !scenario.proof_required)
);
}
#[test]
fn every_mutating_scenario_requires_operator_confirmation() {
assert!(crate::devnet_spl_validation_scenarios().iter().all(|scenario| {
return !scenario.destructive || scenario.operator_confirmation_required;
}));
}
}

View File

@@ -0,0 +1,462 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/fixture.rs
// version: 4
//! Idempotent Token-2022 public scenario fixture preparation.
/// Default raw amount minted by the public MintToChecked scenario.
pub const DEFAULT_TOKEN_2022_MINT_AMOUNT_RAW: u64 = 10_000_000_000;
/// Default raw amount transferred by the public TransferChecked scenario.
pub const DEFAULT_TOKEN_2022_TRANSFER_AMOUNT_RAW: u64 = 1_000_000_000;
/// Default raw amount delegated by the public ApproveChecked scenario.
pub const DEFAULT_TOKEN_2022_APPROVE_AMOUNT_RAW: u64 = 2_000_000_000;
/// Default raw amount burned by the public BurnChecked scenario.
pub const DEFAULT_TOKEN_2022_BURN_AMOUNT_RAW: u64 = 1_000_000_000;
macro_rules! ks_try {
($expression:expr) => {
match $expression {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
};
}
/// Command-line options for one Token-2022 fixture preparation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Token2022FixturePreparationOptions {
/// Devnet RPC URL used by Solana CLI commands.
pub rpc_url: std::string::String,
/// Persistent operator wallet keypair path.
pub wallet_path: std::path::PathBuf,
/// Wallet directory receiving public fixture files and temporary keypairs.
pub wallet_dir: std::path::PathBuf,
/// Mint decimals used by the public scenarios.
pub decimals: u8,
}
/// Public values written to one Token-2022 fixture file.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct Token2022FixturePreparationSummary {
/// Generated fixture path.
pub fixture_path: std::path::PathBuf,
/// Canonical Token-2022 Program ID.
pub program_id: std::string::String,
/// Token-2022 mint address.
pub mint: std::string::String,
/// Source Token-2022 account address.
pub source: std::string::String,
/// Destination Token-2022 account address.
pub destination: std::string::String,
/// Empty Token-2022 account reserved for CloseAccount.
pub close_account: std::string::String,
/// Delegate public key.
pub delegate: std::string::String,
/// Wallet authority public key.
pub authority: std::string::String,
/// Freeze authority public key.
pub freeze_authority: std::string::String,
/// Mint decimals.
pub decimals: u8,
/// Whether the fixture file was replaced during this invocation.
pub fixture_written: bool,
}
/// Creates or reuses one complete Token-2022 public scenario fixture.
pub async fn prepare_token_2022_fixture(
options: &crate::Token2022FixturePreparationOptions,
) -> ks_core::Result<crate::Token2022FixturePreparationSummary> {
ks_try!(validate_options(options));
let fixture_dir = options.wallet_dir.join("spl_token_2022_validation");
if let std::result::Result::Err(error) = tokio::fs::create_dir_all(&fixture_dir).await {
return std::result::Result::Err(ks_core::Error::io(format!(
"unable to create Token-2022 fixture directory {}: {error}",
fixture_dir.display()
)));
}
let wallet_path = ks_try!(path_text(&options.wallet_path));
let authority =
ks_try!(command_stdout("solana-keygen", &["pubkey", wallet_path.as_str()]).await);
let mint_keypair = fixture_dir.join("mint.json");
let source_keypair = fixture_dir.join("source-account.json");
let destination_keypair = fixture_dir.join("destination-account.json");
let close_account_keypair = fixture_dir.join("close-account.json");
let delegate_keypair = fixture_dir.join("delegate.json");
ks_try!(ensure_keypair(&mint_keypair).await);
ks_try!(ensure_keypair(&source_keypair).await);
ks_try!(ensure_keypair(&destination_keypair).await);
ks_try!(ensure_keypair(&close_account_keypair).await);
ks_try!(ensure_keypair(&delegate_keypair).await);
let mint = ks_try!(keypair_pubkey(&mint_keypair).await);
let source = ks_try!(keypair_pubkey(&source_keypair).await);
let destination = ks_try!(keypair_pubkey(&destination_keypair).await);
let close_account = ks_try!(keypair_pubkey(&close_account_keypair).await);
let delegate = ks_try!(keypair_pubkey(&delegate_keypair).await);
let mint_exists = ks_try!(account_exists(options.rpc_url.as_str(), mint.as_str()).await);
if !mint_exists {
let decimals = options.decimals.to_string();
let wallet_path = ks_try!(path_text(&options.wallet_path));
let mint_keypair_path = ks_try!(path_text(&mint_keypair));
ks_try!(
run_command(
"spl-token",
&[
"--url",
options.rpc_url.as_str(),
"--program-id",
ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
"create-token",
"--enable-freeze",
"--decimals",
decimals.as_str(),
"--fee-payer",
wallet_path.as_str(),
"--mint-authority",
authority.as_str(),
mint_keypair_path.as_str()
]
)
.await
);
}
ks_try!(
ensure_token_account(
options,
mint.as_str(),
authority.as_str(),
&source_keypair,
source.as_str()
)
.await
);
ks_try!(
ensure_token_account(
options,
mint.as_str(),
authority.as_str(),
&destination_keypair,
destination.as_str()
)
.await
);
ks_try!(
ensure_token_account(
options,
mint.as_str(),
authority.as_str(),
&close_account_keypair,
close_account.as_str()
)
.await
);
let fixture_path = fixture_dir.join("fixture.env");
let summary = crate::Token2022FixturePreparationSummary {
fixture_path: fixture_path.clone(),
program_id: ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(),
mint,
source,
destination,
close_account,
delegate,
authority: authority.clone(),
freeze_authority: authority,
decimals: options.decimals,
fixture_written: true,
};
let contents = render_fixture(&summary);
let fixture_written =
ks_try!(write_fixture_atomically(&fixture_path, contents.as_bytes()).await);
return std::result::Result::Ok(crate::Token2022FixturePreparationSummary {
fixture_path,
program_id: summary.program_id,
mint: summary.mint,
source: summary.source,
destination: summary.destination,
close_account: summary.close_account,
delegate: summary.delegate,
authority: summary.authority,
freeze_authority: summary.freeze_authority,
decimals: summary.decimals,
fixture_written,
});
}
fn validate_options(options: &crate::Token2022FixturePreparationOptions) -> ks_core::Result<()> {
if options.rpc_url.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 fixture RPC URL must not be empty",
));
}
if !options.wallet_path.is_file() {
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 fixture wallet does not exist: {}",
options.wallet_path.display()
)));
}
if options.decimals > 18 {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 fixture decimals must be at most 18",
));
}
return std::result::Result::Ok(());
}
async fn ensure_keypair(path: &std::path::Path) -> ks_core::Result<()> {
if path.is_file() {
return std::result::Result::Ok(());
}
let keypair_path = ks_try!(path_text(path));
ks_try!(
run_command(
"solana-keygen",
&[
"new",
"--no-bip39-passphrase",
"--force",
"--silent",
"--outfile",
keypair_path.as_str()
]
)
.await
);
ks_try!(set_private_permissions(path).await);
return std::result::Result::Ok(());
}
async fn set_private_permissions(path: &std::path::Path) -> ks_core::Result<()> {
#[cfg(unix)]
{
let metadata = match tokio::fs::metadata(path).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::io(format!(
"unable to read keypair metadata {}: {error}",
path.display()
)));
},
};
let mut permissions = metadata.permissions();
std::os::unix::fs::PermissionsExt::set_mode(&mut permissions, 0o600);
if let std::result::Result::Err(error) = tokio::fs::set_permissions(path, permissions).await
{
return std::result::Result::Err(ks_core::Error::io(format!(
"unable to protect keypair {}: {error}",
path.display()
)));
}
}
return std::result::Result::Ok(());
}
async fn keypair_pubkey(path: &std::path::Path) -> ks_core::Result<std::string::String> {
let keypair_path = ks_try!(path_text(path));
return command_stdout("solana-keygen", &["pubkey", keypair_path.as_str()]).await;
}
async fn account_exists(rpc_url: &str, address: &str) -> ks_core::Result<bool> {
let output = match tokio::process::Command::new("solana")
.args(["account", address, "--url", rpc_url, "--output", "json"])
.output()
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::io(format!(
"unable to execute solana account: {error}"
)));
},
};
return std::result::Result::Ok(output.status.success());
}
async fn ensure_token_account(
options: &crate::Token2022FixturePreparationOptions,
mint: &str,
authority: &str,
keypair_path: &std::path::Path,
address: &str,
) -> ks_core::Result<()> {
let exists = ks_try!(account_exists(options.rpc_url.as_str(), address).await);
if exists {
return std::result::Result::Ok(());
}
let token_account_keypair = ks_try!(path_text(keypair_path));
let wallet_path = ks_try!(path_text(&options.wallet_path));
ks_try!(
run_command(
"spl-token",
&[
"--url",
options.rpc_url.as_str(),
"--program-id",
ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
"create-account",
mint,
token_account_keypair.as_str(),
"--owner",
authority,
"--fee-payer",
wallet_path.as_str()
]
)
.await
);
return std::result::Result::Ok(());
}
async fn command_stdout(program: &str, arguments: &[&str]) -> ks_core::Result<std::string::String> {
let output = match tokio::process::Command::new(program).args(arguments).output().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::io(format!(
"unable to execute {program}: {error}"
)));
},
};
if !output.status.success() {
return std::result::Result::Err(command_error(program, arguments, &output));
}
let value = match std::string::String::from_utf8(output.stdout) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::invalid_state(format!(
"{program} returned non-UTF-8 output: {error}"
)));
},
};
return std::result::Result::Ok(value.trim().to_string());
}
async fn run_command(program: &str, arguments: &[&str]) -> ks_core::Result<()> {
let output = match tokio::process::Command::new(program).args(arguments).output().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::io(format!(
"unable to execute {program}: {error}"
)));
},
};
if !output.status.success() {
return std::result::Result::Err(command_error(program, arguments, &output));
}
return std::result::Result::Ok(());
}
fn command_error(
program: &str,
arguments: &[&str],
output: &std::process::Output,
) -> ks_core::Error {
let stderr = std::string::String::from_utf8_lossy(output.stderr.as_slice())
.trim()
.to_string();
let stdout = std::string::String::from_utf8_lossy(output.stdout.as_slice())
.trim()
.to_string();
let detail = if stderr.is_empty() { stdout } else { stderr };
return ks_core::Error::new(
"token_2022_fixture_command_failed",
format!(
"command {program} {} failed with status {}: {detail}",
arguments.join(" "),
output.status
),
);
}
fn path_text(path: &std::path::Path) -> ks_core::Result<std::string::String> {
return match path.to_str() {
std::option::Option::Some(value) => std::result::Result::Ok(value.to_string()),
std::option::Option::None => std::result::Result::Err(ks_core::Error::config(format!(
"path is not valid UTF-8: {}",
path.display()
))),
};
}
fn render_fixture(summary: &crate::Token2022FixturePreparationSummary) -> std::string::String {
return format!(
"export TOKEN_2022_PROGRAM={}\nexport TOKEN_2022_MINT={}\nexport TOKEN_2022_SOURCE={}\nexport TOKEN_2022_DESTINATION={}\nexport TOKEN_2022_CLOSE_ACCOUNT={}\nexport TOKEN_2022_DELEGATE={}\nexport TOKEN_2022_AUTHORITY={}\nexport TOKEN_2022_FREEZE_AUTHORITY={}\nexport TOKEN_2022_DECIMALS={}\nexport TOKEN_2022_MINT_AMOUNT_RAW={}\nexport TOKEN_2022_TRANSFER_AMOUNT_RAW={}\nexport TOKEN_2022_APPROVE_AMOUNT_RAW={}\nexport TOKEN_2022_BURN_AMOUNT_RAW={}\nexport KB_DEVNET_WALLET_ADDRESS={}\n",
summary.program_id,
summary.mint,
summary.source,
summary.destination,
summary.close_account,
summary.delegate,
summary.authority,
summary.freeze_authority,
summary.decimals,
crate::DEFAULT_TOKEN_2022_MINT_AMOUNT_RAW,
crate::DEFAULT_TOKEN_2022_TRANSFER_AMOUNT_RAW,
crate::DEFAULT_TOKEN_2022_APPROVE_AMOUNT_RAW,
crate::DEFAULT_TOKEN_2022_BURN_AMOUNT_RAW,
summary.authority
);
}
async fn write_fixture_atomically(
path: &std::path::Path,
contents: &[u8],
) -> ks_core::Result<bool> {
if let std::result::Result::Ok(existing) = tokio::fs::read(path).await {
if existing == contents {
return std::result::Result::Ok(false);
}
}
let temporary = path.with_extension("env.tmp");
if let std::result::Result::Err(error) = tokio::fs::write(&temporary, contents).await {
return std::result::Result::Err(ks_core::Error::io(format!(
"unable to write temporary Token-2022 fixture {}: {error}",
temporary.display()
)));
}
if let std::result::Result::Err(error) = tokio::fs::rename(&temporary, path).await {
return std::result::Result::Err(ks_core::Error::io(format!(
"unable to replace Token-2022 fixture {}: {error}",
path.display()
)));
}
return std::result::Result::Ok(true);
}
#[cfg(test)]
mod tests {
#[test]
fn rendered_fixture_contains_every_public_scenario_variable() {
let summary = super::Token2022FixturePreparationSummary {
fixture_path: std::path::PathBuf::from("fixture.env"),
program_id: ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(),
mint: "mint".to_string(),
source: "source".to_string(),
destination: "destination".to_string(),
close_account: "close".to_string(),
delegate: "delegate".to_string(),
authority: "authority".to_string(),
freeze_authority: "authority".to_string(),
decimals: 9,
fixture_written: true,
};
let fixture = super::render_fixture(&summary);
for scenario in crate::devnet_spl_validation_scenarios().into_iter().filter(|scenario| {
return scenario.family == crate::DevnetSplValidationFamily::Token2022Public;
}) {
for variable in scenario.required_fixture_variables {
assert!(fixture.lines().any(|line| return line.starts_with(format!("export {variable}=").as_str())), "missing {variable}");
}
}
assert!(fixture.contains("export TOKEN_2022_CLOSE_ACCOUNT=close\n"));
assert!(fixture.contains("export KB_DEVNET_WALLET_ADDRESS=authority\n"));
}
#[test]
fn options_reject_excessive_decimals() {
let options = super::Token2022FixturePreparationOptions {
rpc_url: "https://api.devnet.solana.com".to_string(),
wallet_path: std::path::PathBuf::from("missing.json"),
wallet_dir: std::path::PathBuf::from("wallets"),
decimals: 19,
};
let error = super::validate_options(&options);
assert!(error.is_err());
}
}

View File

@@ -0,0 +1,8 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata.rs
// version: 2
//! Token-2022 embedded Token Metadata fixtures, campaigns and validation.
pub(crate) mod campaign;
pub(crate) mod fixture;
pub(crate) mod validation;

View File

@@ -0,0 +1,586 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/campaign.rs
// version: 5
//! Ordered confirmed Devnet campaign for the five Token-2022 Token Metadata instructions.
/// One confirmed Token Metadata campaign step and its authoritative postcondition.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetToken2022MetadataCampaignStepSummary {
/// Stable step identifier.
pub step_id: std::string::String,
/// Canonical operation code built by `ks-lib`.
pub operation_code: std::string::String,
/// Complete simulation, submission, replay and instruction-materialization evidence.
pub execution: crate::DevnetSplToken2022ExecutionSummary,
/// Authoritative mint snapshot read at or after the confirmed transaction slot.
pub stateful_snapshot: ks_pipeline::Token2022StatefulReadResult,
/// Validated return-data evidence for the `Emit` step only.
pub emit_evidence: std::option::Option<ks_pipeline::Token2022MetadataEmitEvidence>,
/// Explicit stateful or return-data postcondition.
pub postcondition: ks_pipeline::Token2022ExecutionPostcondition,
}
/// Complete evidence retained by the five-step Token Metadata campaign.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetToken2022MetadataCampaignSummary {
/// Fresh Token-2022 mint prepared for this campaign.
pub fixture: crate::Token2022MetadataFixturePreparationSummary,
/// Five confirmed steps in interface order.
pub steps: std::vec::Vec<crate::DevnetToken2022MetadataCampaignStepSummary>,
}
/// Returns the exact ordered operation names exercised by the Devnet campaign.
pub fn token_2022_metadata_campaign_operation_names() -> &'static [&'static str; 5] {
return &[
"initialize_token_metadata",
"update_token_metadata_field",
"emit_token_metadata",
"remove_token_metadata_key",
"update_token_metadata_authority",
];
}
/// Prepares a fresh mint and executes all five Token Metadata interface instructions on Devnet.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_token_2022_metadata_campaign<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::Token2022MetadataFixturePreparationOptions,
observer: &O,
) -> ks_core::Result<crate::DevnetToken2022MetadataCampaignSummary>
where
S: ks_store::RawTransactionStore
+ ks_store::CoreExtractionStore
+ ks_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
if !options.operator_confirmed {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 Token Metadata Devnet campaign requires explicit operator confirmation",
));
}
let fixture = match crate::prepare_token_2022_metadata_fixture(
http_pool,
profile,
workspace_root,
options,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let operations = campaign_operations(&fixture);
let decoders: std::vec::Vec<std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(ks_lib::DcSplToken2022Decoder)];
let materializers: std::vec::Vec<std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>> =
std::vec![std::sync::Arc::new(ks_lib::MtMetadataToken2022Materializer)];
let mut steps = std::vec::Vec::with_capacity(operations.len());
for (index, (step_id, operation)) in operations.into_iter().enumerate() {
let operation_code = operation.operation_code().to_string();
let mut request = crate::DevnetSplToken2022ExecutionRequest::new(
format!("token-2022-metadata-devnet-{step_id}-{}", uuid::Uuid::new_v4()),
operation.clone(),
);
request.query_role = options.query_role.clone();
request.transaction_role = options.transaction_role.clone();
request.submit = true;
request.operator_confirmed = true;
request.post_validation_max_retries = 20;
let execution = match crate::execute_devnet_spl_token_2022(
http_pool,
store,
profile,
workspace_root,
&request,
decoders.as_slice(),
materializers.as_slice(),
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmation = match execution.confirmation.as_ref() {
std::option::Option::Some(value)
if matches!(
value.status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
) =>
{
value
},
std::option::Option::Some(value) => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_campaign_step_not_confirmed",
format!("campaign step {step_id} stopped at {:?}", value.status),
));
},
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_campaign_confirmation_missing",
format!("campaign step {step_id} has no confirmation evidence"),
));
},
};
let diagnostic = match execution.post_execution.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_campaign_post_execution_missing",
format!("campaign step {step_id} has no post-execution diagnostic"),
));
},
};
if !execution.simulation.success
|| !diagnostic.canonical_inserted
|| !diagnostic.core_extracted
|| !diagnostic.decode_replayed
{
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_campaign_replay_incomplete",
format!(
"campaign step {step_id} did not complete simulation and canonical decode replay"
),
));
}
let idempotence_replay = match execution.idempotence_replay.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_campaign_idempotence_missing",
format!("campaign step {step_id} has no idempotence replay evidence"),
));
},
};
if idempotence_replay.failed_inputs != 0
|| idempotence_replay.processing_error_inputs != 0
|| idempotence_replay.processors.iter().any(|processor| {
return processor.failed != 0
|| processor.processing_errors != 0
|| processor.materialized_outputs != 0
|| processor.materialization_refused != 0;
})
{
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_campaign_idempotence_failed",
format!("campaign step {step_id} did not produce a clean second replay"),
));
}
if !execution.plan.policy.post_execution_validation.materialization_required
&& operation_code != ks_lib::EX_SPL_TOKEN_2022_EMIT_TOKEN_METADATA_OPERATION
{
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_campaign_materialization_policy_invalid",
format!("campaign step {step_id} unexpectedly disabled materialization"),
));
}
if execution.plan.policy.post_execution_validation.materialization_required
&& execution.materializations.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_campaign_materialization_missing",
format!("campaign step {step_id} produced no metadata materialization"),
));
}
let stateful_snapshot = match ks_pipeline::read_token_2022_stateful_snapshot(
http_pool,
&ks_pipeline::Token2022StatefulReadRequest {
query_role: options.query_role.clone(),
account: fixture.mint.clone(),
kind: ks_lib::DcToken2022StateKind::Mint,
min_context_slot: confirmation.slot,
max_data_bytes: ks_pipeline::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES,
context: ks_pipeline::Token2022StatefulContext::default(),
},
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let emit_evidence = match &operation {
ks_lib::ExSplToken2022Operation::Instruction { value } => match value.as_ref() {
ks_lib::ExSplTokenSingleOperation::EmitTokenMetadata { start, end, .. } => {
match ks_pipeline::inspect_token_2022_metadata_emit_simulation(
&execution.simulation,
*start,
*end,
) {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
},
_ => std::option::Option::None,
},
ks_lib::ExSplToken2022Operation::Batch { instructions: _ } => std::option::Option::None,
};
let postcondition = campaign_postcondition(
index,
&fixture,
&stateful_snapshot.snapshot,
emit_evidence.as_ref(),
);
if postcondition.status != ks_pipeline::Token2022ExecutionPostconditionStatus::Confirmed {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_campaign_postcondition_failed",
format!("campaign step {step_id}: {}", postcondition.diagnostic),
));
}
steps.push(crate::DevnetToken2022MetadataCampaignStepSummary {
step_id: step_id.to_string(),
operation_code,
execution,
stateful_snapshot,
emit_evidence,
postcondition,
});
}
return std::result::Result::Ok(crate::DevnetToken2022MetadataCampaignSummary {
fixture,
steps,
});
}
fn campaign_operations(
fixture: &crate::Token2022MetadataFixturePreparationSummary,
) -> std::vec::Vec<(&'static str, ks_lib::ExSplToken2022Operation)> {
let metadata = fixture.mint.clone();
let authority = fixture.initial_authority.clone();
return std::vec![
(
"token_2022_metadata_initialize",
instruction(ks_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
metadata: metadata.clone(),
update_authority: authority.clone(),
mint: metadata.clone(),
mint_authority: authority.clone(),
name: crate::TOKEN_2022_METADATA_CAMPAIGN_NAME.to_string(),
symbol: crate::TOKEN_2022_METADATA_CAMPAIGN_SYMBOL.to_string(),
uri: crate::TOKEN_2022_METADATA_CAMPAIGN_URI.to_string(),
}),
),
(
"token_2022_metadata_update_field",
instruction(ks_lib::ExSplTokenSingleOperation::UpdateTokenMetadataField {
metadata: metadata.clone(),
update_authority: authority.clone(),
field: ks_lib::ExSplTokenMetadataField::Key(
crate::TOKEN_2022_METADATA_CAMPAIGN_KEY.to_string(),
),
value: crate::TOKEN_2022_METADATA_CAMPAIGN_VALUE.to_string(),
}),
),
(
"token_2022_metadata_emit",
instruction(ks_lib::ExSplTokenSingleOperation::EmitTokenMetadata {
metadata: metadata.clone(),
start: std::option::Option::None,
end: std::option::Option::None,
}),
),
(
"token_2022_metadata_remove_key",
instruction(ks_lib::ExSplTokenSingleOperation::RemoveTokenMetadataKey {
metadata: metadata.clone(),
update_authority: authority.clone(),
key: crate::TOKEN_2022_METADATA_CAMPAIGN_KEY.to_string(),
idempotent: false,
}),
),
(
"token_2022_metadata_update_authority",
instruction(ks_lib::ExSplTokenSingleOperation::UpdateTokenMetadataAuthority {
metadata,
current_authority: authority,
new_authority: std::option::Option::Some(fixture.final_authority.clone()),
}),
),
];
}
fn instruction(value: ks_lib::ExSplTokenSingleOperation) -> ks_lib::ExSplToken2022Operation {
return ks_lib::ExSplToken2022Operation::Instruction { value: std::boxed::Box::new(value) };
}
fn campaign_postcondition(
step_index: usize,
fixture: &crate::Token2022MetadataFixturePreparationSummary,
snapshot: &ks_pipeline::Token2022StatefulSnapshotBundle,
emit_evidence: std::option::Option<&ks_pipeline::Token2022MetadataEmitEvidence>,
) -> ks_pipeline::Token2022ExecutionPostcondition {
if step_index == 4 {
return ks_pipeline::inspect_token_2022_metadata_authority_postcondition(
&fixture.mint,
std::option::Option::Some(&fixture.final_authority),
snapshot,
);
}
let value_fields = metadata_value_fields(snapshot);
let confirmed = match step_index {
0 => value_fields.is_some_and(|value| {
return required_fields_match(value, fixture.initial_authority.0.as_str())
&& !contains_campaign_pair(value);
}),
1 => value_fields.is_some_and(|value| {
return required_fields_match(value, fixture.initial_authority.0.as_str())
&& contains_campaign_pair(value);
}),
2 => {
let snapshot_matches = value_fields.is_some_and(|value| {
return required_fields_match(value, fixture.initial_authority.0.as_str())
&& contains_campaign_pair(value);
});
let emit_matches = emit_evidence
.and_then(|evidence| return evidence.decoded_metadata.as_ref())
.is_some_and(|value| {
return required_fields_match(value, fixture.initial_authority.0.as_str())
&& contains_campaign_pair(value);
});
snapshot_matches && emit_matches
},
3 => value_fields.is_some_and(|value| {
return required_fields_match(value, fixture.initial_authority.0.as_str())
&& !contains_campaign_pair(value);
}),
_ => false,
};
return ks_pipeline::Token2022ExecutionPostcondition {
role: "metadata".to_string(),
account: fixture.mint.clone(),
status: if confirmed {
ks_pipeline::Token2022ExecutionPostconditionStatus::Confirmed
} else {
ks_pipeline::Token2022ExecutionPostconditionStatus::Contradicted
},
diagnostic: if confirmed {
format!("Token-2022 metadata campaign postcondition {step_index} is confirmed")
} else {
format!("Token-2022 metadata campaign postcondition {step_index} is contradicted")
},
};
}
fn metadata_value_fields(
snapshot: &ks_pipeline::Token2022StatefulSnapshotBundle,
) -> std::option::Option<&serde_json::Value> {
return snapshot.outputs.iter().find_map(|output| {
if output.family != ks_lib::MdMaterializedEventFamily::Metadata
|| output.payload_json.get("projectionKind").and_then(serde_json::Value::as_str)
!= std::option::Option::Some("token_metadata")
{
return std::option::Option::None;
}
return output.payload_json.get("valueFields");
});
}
fn required_fields_match(value: &serde_json::Value, authority: &str) -> bool {
return value.get("updateAuthority").and_then(serde_json::Value::as_str)
== std::option::Option::Some(authority)
&& value.get("name").and_then(serde_json::Value::as_str)
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_NAME)
&& value.get("symbol").and_then(serde_json::Value::as_str)
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_SYMBOL)
&& value.get("uri").and_then(serde_json::Value::as_str)
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_URI);
}
fn contains_campaign_pair(value: &serde_json::Value) -> bool {
return value
.get("additionalMetadata")
.and_then(serde_json::Value::as_array)
.is_some_and(|pairs| {
return pairs.iter().any(|pair| {
return pair.get("key").and_then(serde_json::Value::as_str)
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_KEY)
&& pair.get("value").and_then(serde_json::Value::as_str)
== std::option::Option::Some(crate::TOKEN_2022_METADATA_CAMPAIGN_VALUE);
});
});
}
#[cfg(test)]
mod tests {
fn pubkey(byte: u8) -> ks_lib::MdPubkey {
return ks_lib::MdPubkey(bs58::encode([byte; 32]).into_string());
}
fn fixture() -> crate::Token2022MetadataFixturePreparationSummary {
return crate::Token2022MetadataFixturePreparationSummary {
mint_keypair_path: std::path::PathBuf::from("mint.json"),
final_authority_keypair_path: std::path::PathBuf::from("authority.json"),
mint: pubkey(1),
initial_authority: pubkey(2),
final_authority: pubkey(3),
mint_space: 234,
rent_budget_space: 512,
rent_reserve_lamports: 1,
preparation_signature: "signature".to_string(),
initial_snapshot: ks_pipeline::Token2022StatefulReadResult {
commitment: "confirmed".to_string(),
context_slot: 1,
snapshot: ks_pipeline::Token2022StatefulSnapshotBundle {
account_key: pubkey(1).0,
slot: 1,
state_kind: "mint".to_string(),
extension_names: vec!["metadata_pointer".to_string()],
outputs: std::vec::Vec::new(),
},
},
};
}
#[test]
fn campaign_contract_covers_five_interface_operations_once_in_order() {
let operations = super::campaign_operations(&fixture());
assert_eq!(
operations
.iter()
.map(|(_, operation)| return operation.operation_code())
.collect::<std::vec::Vec<_>>(),
vec![
ks_lib::EX_SPL_TOKEN_2022_INITIALIZE_TOKEN_METADATA_OPERATION,
ks_lib::EX_SPL_TOKEN_2022_UPDATE_TOKEN_METADATA_FIELD_OPERATION,
ks_lib::EX_SPL_TOKEN_2022_EMIT_TOKEN_METADATA_OPERATION,
ks_lib::EX_SPL_TOKEN_2022_REMOVE_TOKEN_METADATA_KEY_OPERATION,
ks_lib::EX_SPL_TOKEN_2022_UPDATE_TOKEN_METADATA_AUTHORITY_OPERATION,
]
);
assert_eq!(
crate::token_2022_metadata_campaign_operation_names(),
&[
"initialize_token_metadata",
"update_token_metadata_field",
"emit_token_metadata",
"remove_token_metadata_key",
"update_token_metadata_authority",
]
);
}
#[test]
fn campaign_pair_detection_is_exact_and_removal_is_observable() {
let with_pair = serde_json::json!({
"additionalMetadata": [{
"key": crate::TOKEN_2022_METADATA_CAMPAIGN_KEY,
"value": crate::TOKEN_2022_METADATA_CAMPAIGN_VALUE
}]
});
let without_pair = serde_json::json!({"additionalMetadata": []});
assert!(super::contains_campaign_pair(&with_pair));
assert!(!super::contains_campaign_pair(&without_pair));
}
#[tokio::test]
async fn optional_devnet_token_2022_metadata_campaign_from_env() {
if std::env::var("KB_DEVNET_TOKEN_2022_METADATA_CAMPAIGN_TEST").ok().as_deref()
!= std::option::Option::Some("1")
{
return;
}
if std::env::var("KB_DEVNET_TOKEN_2022_METADATA_OPERATOR_CONFIRMED")
.ok()
.as_deref()
!= std::option::Option::Some("1")
{
panic!(
"KB_DEVNET_TOKEN_2022_METADATA_OPERATOR_CONFIRMED=1 is required for the spending campaign"
);
}
let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("workspace root cannot be resolved"),
};
let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") {
std::result::Result::Ok(value) => {
let path = std::path::PathBuf::from(value);
if path.is_absolute() { path } else { workspace_root.join(path) }
},
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
};
let config = ks_config::read_config_json_file_with_environment(
config_path.as_path(),
workspace_root,
)
.unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}"));
let profile_name = std::env::var("KB_DEVNET_PROFILE").ok();
let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref())
.unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}"));
let database_url = std::env::var("KB_POSTGRES_TEST_URL")
.unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}"));
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
let pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile)
.unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}"));
let store_options = ks_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
)
.unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}"));
let store = ks_store::PostgresStore::connect(store_options)
.await
.unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}"));
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
panic!("PostgreSQL schema initialization failed: {error}");
}
let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
workspace_root.join(configured_wallet_dir)
};
let mut options = crate::Token2022MetadataFixturePreparationOptions::new(wallet_dir);
options.operator_confirmed = true;
let summary = crate::execute_devnet_token_2022_metadata_campaign(
&pool,
&store,
&profile,
workspace_root,
&options,
&crate::NoopSolanaExecutionObserver,
)
.await
.unwrap_or_else(|error| {
panic!("Token-2022 Token Metadata Devnet campaign failed: {error}")
});
assert_eq!(summary.steps.len(), 5);
assert!(summary.steps.iter().all(|step| {
return step.postcondition.status
== ks_pipeline::Token2022ExecutionPostconditionStatus::Confirmed;
}));
println!(
"TOKEN_2022_METADATA_FIXTURE mint={} preparation_signature={} initial_authority={} final_authority={}",
summary.fixture.mint.0,
summary.fixture.preparation_signature,
summary.fixture.initial_authority.0,
summary.fixture.final_authority.0,
);
for step in &summary.steps {
let signature = step
.execution
.confirmation
.as_ref()
.map(|value| return value.signature.0.as_str())
.unwrap_or("missing");
let slot = step.execution.confirmation.as_ref().and_then(|value| return value.slot);
println!(
"TOKEN_2022_METADATA_STEP id={} operation={} signature={} slot={:?} postcondition={:?} materializations={} emit_bytes={}",
step.step_id,
step.operation_code,
signature,
slot,
step.postcondition.status,
step.execution.materializations.len(),
step.emit_evidence.as_ref().map(|value| return value.data.len()).unwrap_or(0),
);
}
}
}

View File

@@ -0,0 +1,661 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/fixture.rs
// version: 4
//! Native Token-2022 mint preparation for the embedded Token Metadata Devnet campaign.
/// Stable metadata name used by the `0.4.8-pre.012` campaign.
pub const TOKEN_2022_METADATA_CAMPAIGN_NAME: &str = "Khadhroony Token-2022 Metadata";
/// Stable metadata symbol used by the `0.4.8-pre.012` campaign.
pub const TOKEN_2022_METADATA_CAMPAIGN_SYMBOL: &str = "KBTM";
/// Opaque URI stored on-chain by the `0.4.8-pre.012` campaign.
pub const TOKEN_2022_METADATA_CAMPAIGN_URI: &str =
"https://example.invalid/khadhroony/token-2022-metadata.json";
/// Additional metadata key created and removed by the campaign.
pub const TOKEN_2022_METADATA_CAMPAIGN_KEY: &str = "campaign";
/// Additional metadata value created and removed by the campaign.
pub const TOKEN_2022_METADATA_CAMPAIGN_VALUE: &str = "0.4.8-pre.012";
const TOKEN_2022_TLV_HEADER_BYTES: usize = 4;
/// Options used to prepare one fresh Token-2022 mint for the metadata campaign.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Token2022MetadataFixturePreparationOptions {
/// Endpoint role used for state, rent, balance, blockhash and fee calls.
pub query_role: std::string::String,
/// Endpoint role used for simulation, submission and confirmation polling.
pub transaction_role: std::string::String,
/// Directory receiving persistent fixture keypairs.
pub wallet_dir: std::path::PathBuf,
/// Explicit operator authorization for the lamport-spending fixture transaction.
pub operator_confirmed: bool,
}
impl crate::Token2022MetadataFixturePreparationOptions {
/// Creates conservative defaults matching Devnet execution profiles.
pub fn new(wallet_dir: std::path::PathBuf) -> Self {
return Self {
query_role: "http_queries".to_string(),
transaction_role: "http_transactions".to_string(),
wallet_dir,
operator_confirmed: false,
};
}
}
/// Fresh Token-2022 mint and authority identities used by the metadata campaign.
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
pub struct Token2022MetadataFixturePreparationSummary {
/// Persistent fixture mint keypair path.
pub mint_keypair_path: std::path::PathBuf,
/// Persistent final metadata-authority keypair path.
pub final_authority_keypair_path: std::path::PathBuf,
/// Fresh Token-2022 mint address. Embedded metadata uses this same account.
pub mint: ks_lib::MdPubkey,
/// Profile wallet used as mint and initial metadata update authority.
pub initial_authority: ks_lib::MdPubkey,
/// Fresh public key installed by the final `UpdateAuthority` campaign step.
pub final_authority: ks_lib::MdPubkey,
/// Initial mint account bytes containing only the fixed Metadata Pointer extension.
pub mint_space: usize,
/// Account-size budget used to pre-fund rent for the largest campaign metadata state.
pub rent_budget_space: usize,
/// Rent reserve deposited when the mint is created.
pub rent_reserve_lamports: u64,
/// Confirmed signature that created the Token-2022 mint fixture.
pub preparation_signature: std::string::String,
/// Authoritative snapshot after mint creation and before Token Metadata initialization.
pub initial_snapshot: ks_pipeline::Token2022StatefulReadResult,
}
/// Creates a fresh Token-2022 mint with Metadata Pointer initialized to the mint itself.
///
/// The mint account is created with the fixed Metadata Pointer size but is pre-funded
/// for the largest state reached by this campaign so Token Metadata reallocations
/// remain rent-exempt without introducing an additional payer instruction.
pub async fn prepare_token_2022_metadata_fixture(
http_pool: &ks_onchain_transport::HttpEndpointPool,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
options: &crate::Token2022MetadataFixturePreparationOptions,
) -> ks_core::Result<crate::Token2022MetadataFixturePreparationSummary> {
if options.query_role.trim().is_empty() || options.transaction_role.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 metadata fixture endpoint roles must not be empty",
));
}
if profile.wallet.cluster != "devnet"
|| !profile.wallet.temporary_wallet_enabled
|| !profile.wallet.temporary_wallet_persist
{
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 metadata fixture requires a persistent Devnet wallet profile",
));
}
if !profile.wallet.devnet_send_enabled || !options.operator_confirmed {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 metadata fixture requires enabled Devnet submission and explicit operator confirmation",
));
}
let operator = match crate::load_profile_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture_store = match ks_wallet::TemporaryWalletStore::new(
options.wallet_dir.join("token_2022_metadata_validation"),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture_epoch = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
std::result::Result::Ok(value) => value.as_millis(),
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(format!(
"unable to create a unique Token-2022 metadata fixture identifier: {error}"
)));
},
};
let mint_alias = match ks_wallet::WalletAlias::parse(
format!("token-2022-metadata-mint-{fixture_epoch}").as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let final_authority_alias = match ks_wallet::WalletAlias::parse(
format!("token-2022-metadata-authority-{fixture_epoch}").as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint_wallet = match fixture_store.load_or_create(mint_alias.clone()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let final_authority_wallet =
match fixture_store.load_or_create(final_authority_alias.clone()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint = ks_lib::MdPubkey(mint_wallet.public_key());
let initial_authority = ks_lib::MdPubkey(operator.public_key());
let final_authority = ks_lib::MdPubkey(final_authority_wallet.public_key());
let mint_space = match token_2022_metadata_pointer_mint_space() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rent_budget_space = metadata_campaign_rent_budget_space(mint_space);
let preparation_signature = match create_native_token_2022_metadata_mint(
http_pool,
profile,
options,
&operator,
&mint_wallet,
mint_space,
rent_budget_space,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let initial_snapshot = match ks_pipeline::read_token_2022_stateful_snapshot(
http_pool,
&ks_pipeline::Token2022StatefulReadRequest {
query_role: options.query_role.clone(),
account: mint.clone(),
kind: ks_lib::DcToken2022StateKind::Mint,
min_context_slot: std::option::Option::None,
max_data_bytes: ks_pipeline::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES,
context: ks_pipeline::Token2022StatefulContext::default(),
},
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !initial_snapshot
.snapshot
.extension_names
.iter()
.any(|value| return value == "metadata_pointer")
|| initial_snapshot
.snapshot
.extension_names
.iter()
.any(|value| return value == "token_metadata")
|| !metadata_pointer_snapshot_matches(&initial_snapshot.snapshot, &mint, &initial_authority)
{
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_fixture_state_invalid",
"fresh metadata fixture must contain the expected self-referential Metadata Pointer and no Token Metadata extension",
));
}
let rent = match http_pool
.get_minimum_balance_for_rent_exemption_for_role(
options.query_role.as_str(),
match u64::try_from(rent_budget_space) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 metadata rent budget does not fit u64: {error}"
)));
},
},
&ks_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::Token2022MetadataFixturePreparationSummary {
mint_keypair_path: fixture_store.wallet_path(&mint_alias),
final_authority_keypair_path: fixture_store.wallet_path(&final_authority_alias),
mint,
initial_authority,
final_authority,
mint_space,
rent_budget_space,
rent_reserve_lamports: rent.minimum_balance_lamports,
preparation_signature,
initial_snapshot,
});
}
fn metadata_pointer_snapshot_matches(
snapshot: &ks_pipeline::Token2022StatefulSnapshotBundle,
mint: &ks_lib::MdPubkey,
authority: &ks_lib::MdPubkey,
) -> bool {
return snapshot.outputs.iter().any(|output| {
return output.family == ks_lib::MdMaterializedEventFamily::Admin
&& output.payload_json.get("extensionName").and_then(serde_json::Value::as_str)
== std::option::Option::Some("metadata_pointer")
&& output
.payload_json
.get("valueFields")
.and_then(|value| return value.get("address"))
.and_then(serde_json::Value::as_str)
== std::option::Option::Some(mint.0.as_str())
&& output
.payload_json
.get("valueFields")
.and_then(|value| return value.get("authority"))
.and_then(serde_json::Value::as_str)
== std::option::Option::Some(authority.0.as_str());
});
}
fn token_2022_metadata_pointer_mint_space() -> ks_core::Result<usize> {
return match spl_token_2022_interface::extension::ExtensionType::try_calculate_account_len::<
spl_token_2022_interface::pod::PodMint,
>(&[spl_token_2022_interface::extension::ExtensionType::MetadataPointer])
{
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_fixture_space_failed",
format!("unable to calculate Metadata Pointer mint space: {error}"),
)),
};
}
fn metadata_campaign_rent_budget_space(mint_space: usize) -> usize {
return mint_space
.saturating_add(TOKEN_2022_TLV_HEADER_BYTES)
.saturating_add(metadata_campaign_max_value_bytes());
}
fn metadata_campaign_max_value_bytes() -> usize {
return 64_usize
.saturating_add(4)
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_NAME.len())
.saturating_add(4)
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_SYMBOL.len())
.saturating_add(4)
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_URI.len())
.saturating_add(4)
.saturating_add(4)
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_KEY.len())
.saturating_add(4)
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_VALUE.len());
}
#[allow(clippy::too_many_arguments)]
async fn create_native_token_2022_metadata_mint(
http_pool: &ks_onchain_transport::HttpEndpointPool,
profile: &ks_config::ProfileConfig,
options: &crate::Token2022MetadataFixturePreparationOptions,
operator: &ks_wallet::TemporaryWallet,
mint_wallet: &ks_wallet::TemporaryWallet,
mint_space: usize,
rent_budget_space: usize,
) -> ks_core::Result<std::string::String> {
let rent_space = match u64::try_from(rent_budget_space) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 metadata rent budget does not fit u64: {error}"
)));
},
};
let account_space = match u64::try_from(mint_space) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 Metadata Pointer mint space does not fit u64: {error}"
)));
},
};
let rent = match http_pool
.get_minimum_balance_for_rent_exemption_for_role(
options.query_role.as_str(),
rent_space,
&ks_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let payer = ks_lib::MdPubkey(operator.public_key());
let mint = ks_lib::MdPubkey(mint_wallet.public_key());
let policy =
fixture_policy(profile, payer.clone(), mint.clone(), rent.minimum_balance_lamports);
let system_intent = ks_lib::ExSolanaCoreExecutionIntent {
intent_id: format!("token-2022-metadata-fixture-create-account-{}", mint.0),
fee_payer: payer.clone(),
policy: policy.clone(),
operation: ks_lib::ExSolanaCoreOperation::SystemCreateAccount {
from: payer.clone(),
new_account: mint.clone(),
lamports: rent.minimum_balance_lamports,
space: account_space,
owner: ks_lib::MdPubkey(ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()),
},
};
let system_plan = match ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
&ks_lib::ExSolanaCoreExecutor,
&system_intent,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let token_program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(format!(
"invalid Token-2022 Program ID: {error}"
)));
},
};
let mint_pubkey = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(mint.0.as_str())
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(format!(
"invalid Token-2022 metadata fixture mint: {error}"
)));
},
};
let authority_pubkey =
match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(payer.0.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(format!(
"invalid Token-2022 metadata fixture authority: {error}"
)));
},
};
let pointer_instruction =
match spl_token_2022_interface::extension::metadata_pointer::instruction::initialize(
&token_program,
&mint_pubkey,
std::option::Option::Some(authority_pubkey),
std::option::Option::Some(mint_pubkey),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_fixture_pointer_build_failed",
error.to_string(),
));
},
};
let initialize_mint = match spl_token_2022_interface::instruction::initialize_mint2(
&token_program,
&mint_pubkey,
&authority_pubkey,
std::option::Option::Some(&authority_pubkey),
0,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_fixture_initialize_mint_build_failed",
error.to_string(),
));
},
};
let plan = merge_fixture_instructions(
system_plan,
std::vec![
planned_instruction("spl.token_2022.initialize_metadata_pointer", &pointer_instruction,),
planned_instruction("spl.token_2022.initialize_mint2", &initialize_mint),
],
policy,
rent.minimum_balance_lamports,
);
let plan_evaluation = match ks_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if plan_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_fixture_plan_denied",
crate::violation_message(plan_evaluation.violations.as_slice()),
));
}
let latest_blockhash = match http_pool
.get_latest_blockhash_for_role(
options.query_role.as_str(),
&ks_onchain_transport::GetLatestBlockhashConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned = match ks_lib::executor_solana_build_legacy_transaction(
&plan,
latest_blockhash.blockhash.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fee = match http_pool
.get_fee_for_message_for_role(
options.query_role.as_str(),
unsigned.message_base64().as_str(),
&ks_onchain_transport::GetFeeForMessageConfig::new(
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
std::option::Option::Some(latest_blockhash.context.slot),
),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned_base64 = match unsigned.transaction_base64() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_config = match ks_onchain_transport::SimulateTransactionConfig::new(
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
false,
false,
std::option::Option::Some(latest_blockhash.context.slot),
true,
std::option::Option::None,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_rpc = match http_pool
.simulate_transaction_for_role(
options.transaction_role.as_str(),
unsigned_base64.as_str(),
&simulation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation = simulation_rpc.to_execution_result(
ks_lib::ExApiExecutionCluster::Devnet,
ks_lib::ExApiExecutionBlockhashKind::Latest,
std::option::Option::Some(
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(&fee),
);
if !simulation.success {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_fixture_simulation_failed",
crate::simulation_failure_message(&simulation),
));
}
let send_evaluation = match ks_lib::ExSafetyChecker.evaluate_send(&plan, &simulation) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if send_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_fixture_send_denied",
crate::violation_message(send_evaluation.violations.as_slice()),
));
}
let evidence = unsigned.bind_simulation(simulation);
let signed = match unsigned
.sign_after_simulation(&evidence, &[operator.as_signer(), mint_wallet.as_signer()])
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = signed.verify_signatures() {
return std::result::Result::Err(error);
}
let signature = signed.primary_signature().clone();
let send_config = match ks_onchain_transport::SendTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = http_pool
.send_transaction_for_role(
options.transaction_role.as_str(),
signed.transaction_base64().as_str(),
&signature,
&send_config,
)
.await
{
return std::result::Result::Err(error);
}
let confirmation_config =
match ks_onchain_transport::ConfirmTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(latest_blockhash.last_valid_block_height),
std::option::Option::Some(latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmation = match http_pool
.confirm_transaction_for_roles(
options.transaction_role.as_str(),
options.query_role.as_str(),
ks_lib::ExApiExecutionCluster::Devnet,
&signature,
&confirmation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !matches!(
confirmation.status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
) {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_fixture_confirmation_failed",
format!("Token-2022 metadata mint preparation stopped at {:?}", confirmation.status),
));
}
return std::result::Result::Ok(signature.0);
}
fn planned_instruction(
operation_code: &str,
instruction: &solana_instruction::Instruction,
) -> ks_lib::ExApiPlannedInstruction {
return ks_lib::ExApiPlannedInstruction {
program_id: ks_lib::MdProgramId(instruction.program_id.to_string()),
operation_code: operation_code.to_string(),
accounts: instruction
.accounts
.iter()
.map(|account| {
return ks_lib::ExApiPlannedAccount {
pubkey: ks_lib::MdPubkey(account.pubkey.to_string()),
is_signer: account.is_signer,
is_writable: account.is_writable,
};
})
.collect(),
data: instruction.data.clone(),
};
}
fn fixture_policy(
profile: &ks_config::ProfileConfig,
payer: ks_lib::MdPubkey,
mint: ks_lib::MdPubkey,
_rent_lamports: u64,
) -> ks_lib::ExApiExecutionPolicy {
return ks_lib::ExApiExecutionPolicy {
cluster: ks_lib::ExApiExecutionClusterPolicy {
expected_cluster: ks_lib::ExApiExecutionCluster::Devnet,
allow_mainnet: false,
mainnet_confirmation: false,
},
simulation: ks_lib::ExApiExecutionSimulationPolicy::Required,
blockhash: ks_lib::ExApiExecutionBlockhashPolicy {
kind: ks_lib::ExApiExecutionBlockhashKind::Latest,
max_age_slots: std::option::Option::Some(
profile.execution.recent_blockhash_max_age_slots,
),
nonce_account: std::option::Option::None,
nonce_authority: std::option::Option::None,
},
cost_limit: ks_lib::ExApiExecutionCostLimit {
max_spend_lamports: std::option::Option::Some(
profile.execution.devnet_max_spend_lamports,
),
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
max_compute_unit_price_micro_lamports: std::option::Option::Some(
profile.execution.max_compute_unit_price_micro_lamports,
),
},
authorized_signers: std::vec![payer, mint],
dry_run: false,
post_execution_validation: ks_lib::ExApiPostExecutionValidationPolicy {
canonical_insert_required: false,
core_extraction_required: false,
decode_replay_required: false,
materialization_required: false,
},
};
}
fn merge_fixture_instructions(
mut system_plan: ks_lib::ExApiPreparedExecutionPlan,
token_instructions: std::vec::Vec<ks_lib::ExApiPlannedInstruction>,
policy: ks_lib::ExApiExecutionPolicy,
rent_lamports: u64,
) -> ks_lib::ExApiPreparedExecutionPlan {
system_plan.instructions.extend(token_instructions);
system_plan.executor_name =
"ks-pipeline-demo-scenarios.token_2022_metadata_fixture".to_string();
system_plan.operation_code = "spl.token_2022.metadata.prepare_mint".to_string();
system_plan.policy = policy;
system_plan.requested_spend_lamports = rent_lamports;
return system_plan;
}
#[cfg(test)]
mod tests {
#[test]
fn metadata_rent_budget_covers_pointer_and_largest_campaign_value() {
let mint_space = super::token_2022_metadata_pointer_mint_space()
.unwrap_or_else(|error| panic!("metadata pointer mint size failed: {error}"));
assert!(mint_space > 82);
let budget = super::metadata_campaign_rent_budget_space(mint_space);
assert_eq!(
budget,
mint_space
+ super::TOKEN_2022_TLV_HEADER_BYTES
+ super::metadata_campaign_max_value_bytes()
);
assert!(budget > mint_space);
}
}

View File

@@ -0,0 +1,210 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/validation.rs
// version: 4
//! Conservative validation matrix for the `0.4.8-pre.012` Token Metadata campaign.
/// Maximum evidence items retained by one campaign scenario.
pub const MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE: usize = 16;
/// Exact network-validation status for one Token Metadata scenario.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Token2022MetadataValidationStatus {
/// No real Devnet execution has been recorded.
NotRun,
/// The scenario completed with all declared evidence.
Confirmed,
/// The scenario is unavailable for an explicit recorded reason.
Unavailable,
}
/// One bounded evidence item retained by the validation matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Token2022MetadataValidationEvidence {
/// Stable evidence kind.
pub kind: std::string::String,
/// Bounded evidence value such as a signature, slot or diagnostic.
pub value: std::string::String,
}
/// One ordered Token Metadata scenario in the canonical Devnet matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Token2022MetadataValidationScenario {
/// Stable campaign step identifier.
pub id: std::string::String,
/// Short Token Metadata interface operation name.
pub operation: std::string::String,
/// Current evidence-qualified network status.
pub status: crate::Token2022MetadataValidationStatus,
/// Evidence kinds required before promotion to `confirmed`.
pub required_evidence: std::vec::Vec<std::string::String>,
/// Bounded observed evidence.
pub evidence: std::vec::Vec<crate::Token2022MetadataValidationEvidence>,
}
/// Canonical `0.4.8-pre.012` Token Metadata Devnet validation matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Token2022MetadataValidationMatrix {
/// Matrix schema version.
pub matrix_version: u32,
/// Milestone owning the campaign.
pub milestone: std::string::String,
/// Token-2022 program ID.
pub program_id: std::string::String,
/// Five scenarios in campaign order.
pub scenarios: std::vec::Vec<crate::Token2022MetadataValidationScenario>,
}
/// Loads and validates the canonical Token Metadata Devnet matrix.
pub fn load_token_2022_metadata_validation_matrix()
-> ks_core::Result<crate::Token2022MetadataValidationMatrix> {
let matrix = match serde_json::from_str::<crate::Token2022MetadataValidationMatrix>(
include_str!(
"../../../../../test-fixtures/contract-matrices/SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json"
),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_metadata_validation_matrix_invalid_json",
error.to_string(),
));
},
};
return match crate::validate_token_2022_metadata_validation_matrix(&matrix) {
std::result::Result::Ok(()) => std::result::Result::Ok(matrix),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Validates campaign coverage and prevents promotion without complete evidence.
pub fn validate_token_2022_metadata_validation_matrix(
matrix: &crate::Token2022MetadataValidationMatrix,
) -> ks_core::Result<()> {
if matrix.matrix_version != 1
|| matrix.milestone != "0.4.8-pre.012"
|| matrix.program_id != ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID
{
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 metadata validation matrix identity is invalid",
));
}
let expected = crate::token_2022_metadata_campaign_operation_names();
if matrix.scenarios.len() != expected.len() {
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 metadata validation matrix requires exactly {} scenarios",
expected.len()
)));
}
let mut ids = std::collections::BTreeSet::new();
for (index, scenario) in matrix.scenarios.iter().enumerate() {
if !ids.insert(scenario.id.as_str()) {
return std::result::Result::Err(ks_core::Error::config(format!(
"duplicate Token-2022 metadata validation scenario {}",
scenario.id
)));
}
if scenario.operation != expected[index] {
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 metadata scenario {} expected operation {}, got {}",
scenario.id, expected[index], scenario.operation
)));
}
if scenario.required_evidence.is_empty()
|| scenario.required_evidence.len() > MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE
|| scenario.evidence.len() > MAX_TOKEN_2022_METADATA_VALIDATION_EVIDENCE
{
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 metadata scenario {} has invalid evidence bounds",
scenario.id
)));
}
let mut required = std::collections::BTreeSet::new();
if scenario
.required_evidence
.iter()
.any(|kind| return kind.trim().is_empty() || !required.insert(kind.as_str()))
{
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 metadata scenario {} has empty or duplicate required evidence",
scenario.id
)));
}
let mut observed = std::collections::BTreeSet::new();
for evidence in &scenario.evidence {
if evidence.kind.trim().is_empty()
|| evidence.value.trim().is_empty()
|| !observed.insert(evidence.kind.as_str())
{
return std::result::Result::Err(ks_core::Error::config(format!(
"Token-2022 metadata scenario {} has invalid observed evidence",
scenario.id
)));
}
}
match scenario.status {
crate::Token2022MetadataValidationStatus::NotRun => {
if !scenario.evidence.is_empty() {
return std::result::Result::Err(ks_core::Error::config(format!(
"not-run Token-2022 metadata scenario {} cannot claim evidence",
scenario.id
)));
}
},
crate::Token2022MetadataValidationStatus::Confirmed => {
if scenario
.required_evidence
.iter()
.any(|kind| return !observed.contains(kind.as_str()))
{
return std::result::Result::Err(ks_core::Error::config(format!(
"confirmed Token-2022 metadata scenario {} is missing required evidence",
scenario.id
)));
}
},
crate::Token2022MetadataValidationStatus::Unavailable => {
if !observed.contains("unavailable_reason") {
return std::result::Result::Err(ks_core::Error::config(format!(
"unavailable Token-2022 metadata scenario {} requires unavailable_reason",
scenario.id
)));
}
},
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
fn canonical_matrix_is_closed_ordered_and_confirmed_after_network_execution() {
let matrix = crate::load_token_2022_metadata_validation_matrix()
.unwrap_or_else(|error| panic!("Token-2022 metadata matrix failed: {error}"));
assert_eq!(matrix.scenarios.len(), 5);
assert!(matrix.scenarios.iter().all(|scenario| {
let observed = scenario
.evidence
.iter()
.map(|evidence| return evidence.kind.as_str())
.collect::<std::collections::BTreeSet<_>>();
return scenario.status == crate::Token2022MetadataValidationStatus::Confirmed
&& scenario
.required_evidence
.iter()
.all(|kind| return observed.contains(kind.as_str()));
}));
assert_eq!(
matrix
.scenarios
.iter()
.map(|scenario| return scenario.operation.as_str())
.collect::<std::vec::Vec<_>>(),
crate::token_2022_metadata_campaign_operation_names().as_slice()
);
}
}

View File

@@ -0,0 +1,440 @@
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/validation.rs
// version: 8
//! Machine-readable validation evidence contract for the Token-2022 milestone.
/// Maximum number of evidence records accepted in one validation report.
pub const MAX_TOKEN_2022_VALIDATION_EVIDENCE: usize = 64;
/// Required validation environment for one scenario.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Token2022ValidationEnvironment {
/// Deterministic offline fixtures and builder comparisons.
Offline,
/// A local validator under operator control.
Localnet,
/// Solana Devnet.
Devnet,
/// Scenario may run on Localnet or Devnet according to deployment availability.
LocalnetOrDevnet,
/// Mainnet observations without mutable execution.
MainnetObservation,
/// PostgreSQL persistence and replay validation.
Postgres,
/// Tauri application smoke validation.
Tauri,
}
/// Exact status of one validation scenario.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Token2022ValidationStatus {
/// Scenario is declared but has not been run.
NotRun,
/// Scenario was simulated without submission.
Simulated,
/// Scenario was submitted but confirmation evidence is incomplete.
Submitted,
/// Scenario was confirmed and its required postconditions passed.
Confirmed,
/// Scenario is not available in the selected environment.
Unavailable,
/// Scenario ran and failed.
Failed,
}
/// One bounded proof attached to a validation scenario.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022ValidationEvidence {
/// Stable evidence kind such as `signature`, `test_suite`, or `replay`.
pub kind: std::string::String,
/// Bounded evidence value.
pub value: std::string::String,
}
/// One declared Token-2022 validation scenario and its observed evidence.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022ValidationScenario {
/// Stable scenario identifier.
pub id: std::string::String,
/// Validation environment.
pub environment: crate::Token2022ValidationEnvironment,
/// Exact observed status.
pub status: crate::Token2022ValidationStatus,
/// Whether canonical hydration is required.
pub requires_canonical_hydration: bool,
/// Whether core extraction is required.
pub requires_core_extraction: bool,
/// Whether decode replay is required.
pub requires_decode_replay: bool,
/// Whether materialization is required.
pub requires_materialization: bool,
/// Whether a second idempotent replay is required.
pub requires_second_replay: bool,
/// Ordered evidence records.
pub evidence: std::vec::Vec<crate::Token2022ValidationEvidence>,
}
/// Machine-readable validation matrix loaded from the canonical JSON document.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Token2022ValidationMatrix {
/// Matrix schema version.
pub matrix_version: u32,
/// Milestone owning this matrix.
pub milestone: std::string::String,
/// Aggregate matrix status.
pub status: std::string::String,
/// Exact accepted status vocabulary.
pub status_vocabulary: std::vec::Vec<std::string::String>,
/// Scenarios in stable roadmap order.
pub scenarios: std::vec::Vec<crate::Token2022ValidationMatrixScenario>,
}
/// One scenario declared by the canonical validation matrix.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Token2022ValidationMatrixScenario {
/// Stable scenario identifier.
pub id: std::string::String,
/// Required execution environment.
pub environment: crate::Token2022ValidationEnvironment,
/// Exact observed status.
pub status: crate::Token2022ValidationStatus,
/// Evidence kinds required before this scenario may be confirmed.
pub required_evidence: std::vec::Vec<std::string::String>,
/// Observed bounded evidence.
#[serde(default)]
pub evidence: std::vec::Vec<crate::Token2022ValidationEvidence>,
}
/// Loads and validates the canonical Token-2022 validation matrix.
pub fn load_token_2022_validation_matrix() -> ks_core::Result<crate::Token2022ValidationMatrix> {
let parsed = match serde_json::from_str::<crate::Token2022ValidationMatrix>(include_str!(
"../../../../test-fixtures/contract-matrices/SPL_TOKEN_2022_VALIDATION_MATRIX.json"
)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_matrix_invalid_json",
format!("Token-2022 validation matrix JSON is invalid: {error}"),
));
},
};
if let std::result::Result::Err(error) = validate_token_2022_validation_matrix(&parsed) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(parsed);
}
/// Validates schema, scenario inventory, statuses, and observed evidence.
pub fn validate_token_2022_validation_matrix(
matrix: &crate::Token2022ValidationMatrix,
) -> ks_core::Result<()> {
if matrix.matrix_version != 2 || matrix.milestone != "0.4.6" {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_matrix_contract_mismatch",
"Token-2022 validation matrix must use version 2 for milestone 0.4.6",
));
}
let expected_statuses =
["not_run", "simulated", "submitted", "confirmed", "unavailable", "failed"];
let actual_statuses = matrix
.status_vocabulary
.iter()
.map(|status| return status.as_str())
.collect::<std::vec::Vec<&str>>();
if actual_statuses.as_slice() != expected_statuses {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_matrix_status_vocabulary_mismatch",
"Token-2022 validation status vocabulary differs from the compiled contract",
));
}
let expected_ids = [
"offline_full_regression",
"token_2022_public_devnet",
"elgamal_registry_devnet",
"confidential_transfer_localnet_or_devnet",
"confidential_mint_burn_localnet_or_devnet",
"permissioned_confidential_burn_localnet_or_devnet",
"mainnet_observation_corpus",
"postgres_double_replay",
"tauri_smoke",
];
let actual_ids = matrix
.scenarios
.iter()
.map(|scenario| return scenario.id.as_str())
.collect::<std::vec::Vec<&str>>();
if actual_ids.as_slice() != expected_ids {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_matrix_scenario_inventory_mismatch",
"Token-2022 validation scenario inventory or order differs from the compiled contract",
));
}
for scenario in &matrix.scenarios {
if scenario.required_evidence.is_empty()
|| scenario.required_evidence.len() > crate::MAX_TOKEN_2022_VALIDATION_EVIDENCE
{
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_matrix_required_evidence_invalid",
"Every Token-2022 validation scenario must declare bounded required evidence",
));
}
let mut required = std::collections::BTreeSet::<&str>::new();
for evidence_kind in &scenario.required_evidence {
if evidence_kind.trim().is_empty() || !required.insert(evidence_kind.as_str()) {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_matrix_required_evidence_invalid",
"Required Token-2022 validation evidence must be non-empty and unique",
));
}
}
if scenario.status == crate::Token2022ValidationStatus::Confirmed {
let observed = scenario
.evidence
.iter()
.map(|evidence| return evidence.kind.as_str())
.collect::<std::collections::BTreeSet<&str>>();
if !required.iter().all(|kind| return observed.contains(kind)) {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_matrix_confirmed_without_required_evidence",
format!(
"Confirmed Token-2022 validation scenario {} lacks required evidence",
scenario.id
),
));
}
} else if matches!(
scenario.status,
crate::Token2022ValidationStatus::NotRun
| crate::Token2022ValidationStatus::Unavailable
) && !scenario.evidence.is_empty()
{
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_matrix_unobserved_with_evidence",
"Not-run or unavailable Token-2022 scenarios must not retain observed evidence",
));
}
}
return std::result::Result::Ok(());
}
/// Complete validation report checked before milestone closure.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Token2022ValidationReport {
/// Scenarios in stable roadmap order.
pub scenarios: std::vec::Vec<crate::Token2022ValidationScenario>,
}
/// Validates that a milestone report is bounded, unique, and does not overclaim evidence.
pub fn validate_token_2022_validation_report(
report: &crate::Token2022ValidationReport,
) -> ks_core::Result<()> {
if report.scenarios.is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 validation report must contain at least one scenario",
));
}
let mut ids = std::collections::BTreeSet::<std::string::String>::new();
for scenario in &report.scenarios {
if scenario.id.trim().is_empty() || !ids.insert(scenario.id.clone()) {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_scenario_identity_invalid",
"Token-2022 validation scenario ids must be non-empty and unique",
));
}
if scenario.evidence.len() > crate::MAX_TOKEN_2022_VALIDATION_EVIDENCE {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_evidence_limit_exceeded",
format!(
"Token-2022 validation accepts at most {} evidence records per scenario",
crate::MAX_TOKEN_2022_VALIDATION_EVIDENCE
),
));
}
for evidence in &scenario.evidence {
if evidence.kind.trim().is_empty() || evidence.value.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_evidence_invalid",
"Token-2022 validation evidence kind and value must be non-empty",
));
}
}
let completed = matches!(
scenario.status,
crate::Token2022ValidationStatus::Simulated
| crate::Token2022ValidationStatus::Submitted
| crate::Token2022ValidationStatus::Confirmed
| crate::Token2022ValidationStatus::Failed
);
if completed && scenario.evidence.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_completed_without_evidence",
"A completed Token-2022 validation scenario must retain evidence",
));
}
if scenario.status == crate::Token2022ValidationStatus::Confirmed
&& (scenario.requires_canonical_hydration
|| scenario.requires_core_extraction
|| scenario.requires_decode_replay
|| scenario.requires_materialization
|| scenario.requires_second_replay)
&& !has_pipeline_evidence(scenario)
{
return std::result::Result::Err(ks_core::Error::new(
"token_2022_validation_confirmed_without_pipeline_evidence",
"A confirmed end-to-end Token-2022 scenario must retain hydration, extraction, replay, materialization, and idempotence evidence",
));
}
}
return std::result::Result::Ok(());
}
fn has_pipeline_evidence(scenario: &crate::Token2022ValidationScenario) -> bool {
let kinds = scenario
.evidence
.iter()
.map(|evidence| return evidence.kind.as_str())
.collect::<std::collections::BTreeSet<&str>>();
if scenario.requires_canonical_hydration && !kinds.contains("canonical_hydration") {
return false;
}
if scenario.requires_core_extraction && !kinds.contains("core_extraction") {
return false;
}
if scenario.requires_decode_replay && !kinds.contains("decode_replay") {
return false;
}
if scenario.requires_materialization && !kinds.contains("materialization") {
return false;
}
if scenario.requires_second_replay && !kinds.contains("second_replay") {
return false;
}
return true;
}
#[cfg(test)]
mod tests {
fn scenario(status: crate::Token2022ValidationStatus) -> crate::Token2022ValidationScenario {
return crate::Token2022ValidationScenario {
id: "confidential_transfer_devnet".to_string(),
environment: crate::Token2022ValidationEnvironment::Devnet,
status,
requires_canonical_hydration: true,
requires_core_extraction: true,
requires_decode_replay: true,
requires_materialization: true,
requires_second_replay: true,
evidence: vec![
crate::Token2022ValidationEvidence {
kind: "signature".to_string(),
value: "signature".to_string(),
},
crate::Token2022ValidationEvidence {
kind: "canonical_hydration".to_string(),
value: "1".to_string(),
},
crate::Token2022ValidationEvidence {
kind: "core_extraction".to_string(),
value: "1".to_string(),
},
crate::Token2022ValidationEvidence {
kind: "decode_replay".to_string(),
value: "1".to_string(),
},
crate::Token2022ValidationEvidence {
kind: "materialization".to_string(),
value: "1".to_string(),
},
crate::Token2022ValidationEvidence {
kind: "second_replay".to_string(),
value: "0_new_outputs".to_string(),
},
],
};
}
#[test]
fn confirmed_end_to_end_scenario_requires_complete_pipeline_evidence() {
let report = crate::Token2022ValidationReport {
scenarios: vec![scenario(crate::Token2022ValidationStatus::Confirmed)],
};
assert!(crate::validate_token_2022_validation_report(&report).is_ok());
let mut incomplete = scenario(crate::Token2022ValidationStatus::Confirmed);
incomplete.evidence.retain(|evidence| return evidence.kind != "second_replay");
assert!(
crate::validate_token_2022_validation_report(&crate::Token2022ValidationReport {
scenarios: vec![incomplete]
})
.is_err()
);
}
#[test]
fn not_run_and_unavailable_scenarios_do_not_invent_evidence() {
let mut not_run = scenario(crate::Token2022ValidationStatus::NotRun);
not_run.evidence.clear();
let mut unavailable = scenario(crate::Token2022ValidationStatus::Unavailable);
unavailable.id = "permissioned_burn_devnet".to_string();
unavailable.evidence.clear();
assert!(
crate::validate_token_2022_validation_report(&crate::Token2022ValidationReport {
scenarios: vec![not_run, unavailable]
})
.is_ok()
);
}
#[test]
fn duplicate_ids_empty_evidence_and_completed_without_evidence_fail_closed() {
let first = scenario(crate::Token2022ValidationStatus::Confirmed);
let duplicate = first.clone();
assert!(
crate::validate_token_2022_validation_report(&crate::Token2022ValidationReport {
scenarios: vec![first, duplicate]
})
.is_err()
);
let mut failed = scenario(crate::Token2022ValidationStatus::Failed);
failed.evidence.clear();
assert!(
crate::validate_token_2022_validation_report(&crate::Token2022ValidationReport {
scenarios: vec![failed]
})
.is_err()
);
}
#[test]
fn canonical_matrix_matches_compiled_inventory_and_observed_offline_evidence() {
let matrix = crate::load_token_2022_validation_matrix();
assert!(matrix.is_ok());
let matrix = match matrix {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected matrix error: {error}"),
};
assert_eq!(matrix.scenarios.len(), 9);
assert_eq!(matrix.scenarios[0].status, crate::Token2022ValidationStatus::Confirmed);
assert_eq!(matrix.scenarios[0].evidence.len(), 4);
assert!(
matrix.scenarios[1..]
.iter()
.all(|scenario| return scenario.status == crate::Token2022ValidationStatus::NotRun)
);
}
#[test]
fn confirmed_matrix_scenario_requires_every_declared_evidence_kind() {
let matrix = crate::load_token_2022_validation_matrix();
assert!(matrix.is_ok());
let mut matrix = match matrix {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected matrix error: {error}"),
};
matrix.scenarios[0].evidence.retain(|evidence| return evidence.kind != "clippy");
assert!(crate::validate_token_2022_validation_matrix(&matrix).is_err());
}
}