v0.4.7-pre.012
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/lib.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -72,8 +72,16 @@ pub use self::solana_memo_execution::execute_devnet_memo;
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionRequest;
|
||||
/// Complete evidence produced by one real Devnet Metaplex Token Metadata execution.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionSummary;
|
||||
/// Creates one named creator unverification request without handwritten JSON.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::devnet_metaplex_creator_unverify_request;
|
||||
/// Creates one named creator verification request without handwritten JSON.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::devnet_metaplex_creator_verify_request;
|
||||
/// Executes one real Devnet Metaplex simulation or explicitly authorized submission.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::execute_devnet_metaplex_token_metadata;
|
||||
/// Returns a formatted JSON template for one named current operation.
|
||||
pub use self::solana_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::solana_metaplex_token_metadata_devnet_execution::metaplex_token_metadata_current_operation_names;
|
||||
/// Simulates one current Metaplex operation against a real Devnet endpoint.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::simulate_devnet_metaplex_token_metadata;
|
||||
/// Stable asset family covered by one Metaplex scenario.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_devnet_execution.rs
|
||||
// version: 1
|
||||
// version: 4
|
||||
|
||||
//! Real Devnet simulation and submission for current Metaplex Token Metadata operations.
|
||||
|
||||
@@ -42,6 +42,40 @@ impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a conservative request from one serialized typed Metaplex operation.
|
||||
///
|
||||
/// This entry point lets automated Devnet campaigns and the desktop reuse the
|
||||
/// complete executor surface without duplicating one Rust constructor per
|
||||
/// operation. The JSON must deserialize to `ExMetaplexTokenMetadataOperation`.
|
||||
pub fn from_operation_json(
|
||||
intent_id: impl std::convert::Into<std::string::String>,
|
||||
operation_json: &str,
|
||||
) -> kb_core::Result<Self> {
|
||||
if operation_json.contains("\"operation\":\"...\"")
|
||||
|| operation_json.contains("\"operation\": \"...\"")
|
||||
|| operation_json.trim() == "..."
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Metaplex operation JSON still contains the documentation placeholder `...`; use metaplex_token_metadata_current_operation_json_template or one of the named request constructors",
|
||||
));
|
||||
}
|
||||
let operation = match serde_json::from_str::<kb_lib::ExMetaplexTokenMetadataOperation>(
|
||||
operation_json,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"invalid typed Metaplex operation JSON: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let request = Self::new(intent_id, operation);
|
||||
if let std::result::Result::Err(error) = request.validate() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(request);
|
||||
}
|
||||
|
||||
/// Validates request-local bounds and rejects deprecated operations.
|
||||
pub fn validate(&self) -> kb_core::Result<()> {
|
||||
if self.intent_id.trim().is_empty() {
|
||||
@@ -73,6 +107,164 @@ impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current operation names accepted by automatic Devnet campaigns.
|
||||
pub fn metaplex_token_metadata_current_operation_names() -> &'static [&'static str] {
|
||||
return &[
|
||||
"create",
|
||||
"update_as_update_authority_v2",
|
||||
"verify",
|
||||
"unverify",
|
||||
"burn",
|
||||
"delegate",
|
||||
"revoke",
|
||||
"lock",
|
||||
"unlock",
|
||||
"transfer",
|
||||
"close_accounts",
|
||||
"create_escrow_account",
|
||||
"close_escrow_account",
|
||||
"transfer_out_of_escrow",
|
||||
"mint",
|
||||
"migrate",
|
||||
"use",
|
||||
"collect",
|
||||
"print",
|
||||
"resize",
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns a formatted JSON template for the first named Devnet campaign operations.
|
||||
///
|
||||
/// Placeholders beginning with `<` must be replaced before parsing or execution.
|
||||
pub fn metaplex_token_metadata_current_operation_json_template(
|
||||
operation: &str,
|
||||
) -> kb_core::Result<std::string::String> {
|
||||
let value = match operation {
|
||||
"create" => serde_json::json!({
|
||||
"operation": "create",
|
||||
"metadata": "<METADATA_PDA>",
|
||||
"master_edition": "<MASTER_EDITION_PDA_OR_NULL>",
|
||||
"mint": "<MINT>",
|
||||
"mint_as_signer": false,
|
||||
"authority": "<MINT_AUTHORITY>",
|
||||
"update_authority": "<UPDATE_AUTHORITY>",
|
||||
"update_authority_as_signer": true,
|
||||
"spl_token_program": null,
|
||||
"create_args": {
|
||||
"V1": {
|
||||
"name": "Khadhroony Devnet Asset",
|
||||
"symbol": "KHDEV",
|
||||
"uri": "https://example.invalid/khadhroony-devnet.json",
|
||||
"seller_fee_basis_points": 0,
|
||||
"creators": null,
|
||||
"primary_sale_happened": false,
|
||||
"is_mutable": true,
|
||||
"token_standard": "NonFungible",
|
||||
"collection": null,
|
||||
"uses": null,
|
||||
"collection_details": null,
|
||||
"rule_set": null,
|
||||
"decimals": null,
|
||||
"print_supply": null
|
||||
}
|
||||
}
|
||||
}),
|
||||
"update_as_update_authority_v2" => serde_json::json!({
|
||||
"operation": "update_as_update_authority_v2",
|
||||
"authority": "<UPDATE_AUTHORITY>",
|
||||
"mint": "<MINT>",
|
||||
"metadata": "<METADATA_PDA>",
|
||||
"edition": null,
|
||||
"token": null,
|
||||
"authorization_rules_program": null,
|
||||
"authorization_rules": null,
|
||||
"update_args": {
|
||||
"AsUpdateAuthorityV2": {
|
||||
"new_update_authority": null,
|
||||
"data": null,
|
||||
"primary_sale_happened": null,
|
||||
"is_mutable": null,
|
||||
"collection": null,
|
||||
"collection_details": null,
|
||||
"uses": null,
|
||||
"rule_set": null,
|
||||
"authorization_data": null
|
||||
}
|
||||
}
|
||||
}),
|
||||
"verify_creator" | "verify" => serde_json::json!({
|
||||
"operation": "verify",
|
||||
"authority": "<CREATOR_AUTHORITY>",
|
||||
"delegate_record": null,
|
||||
"metadata": "<METADATA_PDA>",
|
||||
"collection_mint": null,
|
||||
"collection_metadata": null,
|
||||
"collection_master_edition": null,
|
||||
"verification_args": "CreatorV1"
|
||||
}),
|
||||
"unverify_creator" | "unverify" => serde_json::json!({
|
||||
"operation": "unverify",
|
||||
"authority": "<CREATOR_AUTHORITY>",
|
||||
"delegate_record": null,
|
||||
"metadata": "<METADATA_PDA>",
|
||||
"collection_mint": null,
|
||||
"collection_metadata": null,
|
||||
"verification_args": "CreatorV1"
|
||||
}),
|
||||
_ => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"no named JSON template for `{operation}`; current operations: {}",
|
||||
metaplex_token_metadata_current_operation_names().join(", ")
|
||||
)));
|
||||
},
|
||||
};
|
||||
return match serde_json::to_string_pretty(&value) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(format!(
|
||||
"unable to serialize Metaplex operation template: {error}"
|
||||
))),
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates one named creator verification request without handwritten operation JSON.
|
||||
pub fn devnet_metaplex_creator_verify_request(
|
||||
intent_id: impl std::convert::Into<std::string::String>,
|
||||
authority: kb_lib::MdPubkey,
|
||||
metadata: kb_lib::MdPubkey,
|
||||
) -> crate::DevnetMetaplexTokenMetadataExecutionRequest {
|
||||
return crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
|
||||
intent_id,
|
||||
kb_lib::ExMetaplexTokenMetadataOperation::Verify {
|
||||
authority,
|
||||
delegate_record: std::option::Option::None,
|
||||
metadata,
|
||||
collection_mint: std::option::Option::None,
|
||||
collection_metadata: std::option::Option::None,
|
||||
collection_master_edition: std::option::Option::None,
|
||||
verification_args: mpl_token_metadata::types::VerificationArgs::CreatorV1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates one named creator unverification request without handwritten operation JSON.
|
||||
pub fn devnet_metaplex_creator_unverify_request(
|
||||
intent_id: impl std::convert::Into<std::string::String>,
|
||||
authority: kb_lib::MdPubkey,
|
||||
metadata: kb_lib::MdPubkey,
|
||||
) -> crate::DevnetMetaplexTokenMetadataExecutionRequest {
|
||||
return crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
|
||||
intent_id,
|
||||
kb_lib::ExMetaplexTokenMetadataOperation::Unverify {
|
||||
authority,
|
||||
delegate_record: std::option::Option::None,
|
||||
metadata,
|
||||
collection_mint: std::option::Option::None,
|
||||
collection_metadata: std::option::Option::None,
|
||||
verification_args: mpl_token_metadata::types::VerificationArgs::CreatorV1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Complete evidence produced by one real Devnet Metaplex execution.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetMetaplexTokenMetadataExecutionSummary {
|
||||
@@ -583,6 +775,34 @@ fn validate_profile_wallet_signers(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn example_devnet_profile() -> kb_config::ProfileConfig {
|
||||
let config =
|
||||
match kb_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}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn optional_stateful_reads_from_env(
|
||||
variable: &str,
|
||||
) -> std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
|
||||
let value = match std::env::var(variable) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::vec::Vec::new(),
|
||||
};
|
||||
return match serde_json::from_str::<
|
||||
std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest>,
|
||||
>(value.as_str())
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("invalid {variable}: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn request_rejects_deprecated_operations_and_empty_ids() {
|
||||
@@ -598,6 +818,133 @@ mod tests {
|
||||
assert!(empty.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_operation_json_rejects_invalid_and_deprecated_payloads() {
|
||||
assert!(
|
||||
crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
|
||||
"invalid-json",
|
||||
"{}",
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let deprecated = serde_json::json!({
|
||||
"operation": "puff_metadata",
|
||||
"metadata": "11111111111111111111111111111111"
|
||||
});
|
||||
assert!(
|
||||
crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
|
||||
"deprecated-operation",
|
||||
deprecated.to_string().as_str(),
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_templates_are_json_and_placeholder_is_rejected_explicitly() {
|
||||
for operation in ["create", "update_as_update_authority_v2", "verify", "unverify"] {
|
||||
let template =
|
||||
super::metaplex_token_metadata_current_operation_json_template(operation);
|
||||
assert!(template.is_ok());
|
||||
let template = match template {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(serde_json::from_str::<serde_json::Value>(template.as_str()).is_ok());
|
||||
}
|
||||
let placeholder = crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
|
||||
"placeholder",
|
||||
r#"{"operation":"..."}"#,
|
||||
);
|
||||
assert_eq!(
|
||||
placeholder.as_ref().err().map(kb_core::Error::code),
|
||||
std::option::Option::Some("config"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_creator_requests_are_conservative_and_current() {
|
||||
let authority = kb_lib::MdPubkey("11111111111111111111111111111111".to_string());
|
||||
let metadata = kb_lib::MdPubkey("SysvarC1ock11111111111111111111111111111111".to_string());
|
||||
let verify = super::devnet_metaplex_creator_verify_request(
|
||||
"verify-creator",
|
||||
authority.clone(),
|
||||
metadata.clone(),
|
||||
);
|
||||
let unverify = super::devnet_metaplex_creator_unverify_request(
|
||||
"unverify-creator",
|
||||
authority,
|
||||
metadata,
|
||||
);
|
||||
assert!(!verify.submit);
|
||||
assert!(!verify.operator_confirmed);
|
||||
assert!(verify.validate().is_ok());
|
||||
assert!(unverify.validate().is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn optional_devnet_current_operation_from_env() {
|
||||
if std::env::var("KB_DEVNET_METAPLEX_EXECUTION_TEST").ok().as_deref()
|
||||
!= std::option::Option::Some("1")
|
||||
{
|
||||
return;
|
||||
}
|
||||
let operation_json = match std::env::var("KB_DEVNET_METAPLEX_OPERATION_JSON") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("KB_DEVNET_METAPLEX_OPERATION_JSON is required: {error}");
|
||||
},
|
||||
};
|
||||
let mut profile = example_devnet_profile();
|
||||
if let std::result::Result::Ok(directory) = std::env::var("KB_DEVNET_WALLET_DIR") {
|
||||
profile.wallet.wallet_dir = directory;
|
||||
}
|
||||
let pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"),
|
||||
};
|
||||
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 mut request =
|
||||
match crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
|
||||
format!("devnet-metaplex-test-{}", uuid::Uuid::new_v4()),
|
||||
operation_json.as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("Metaplex request parsing failed: {error}")
|
||||
},
|
||||
};
|
||||
request.preflight_reads =
|
||||
optional_stateful_reads_from_env("KB_DEVNET_METAPLEX_PREFLIGHT_READS_JSON");
|
||||
request.postcondition_reads =
|
||||
optional_stateful_reads_from_env("KB_DEVNET_METAPLEX_POSTCONDITION_READS_JSON");
|
||||
request.submit = std::env::var("KB_DEVNET_METAPLEX_SUBMIT").ok().as_deref()
|
||||
== std::option::Option::Some("1");
|
||||
request.operator_confirmed = request.submit
|
||||
&& std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref()
|
||||
== std::option::Option::Some("1");
|
||||
let summary = match crate::execute_devnet_metaplex_token_metadata(
|
||||
&pool,
|
||||
&profile,
|
||||
workspace_root,
|
||||
&request,
|
||||
&crate::NoopSolanaExecutionObserver,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("Devnet Metaplex execution failed: {error}"),
|
||||
};
|
||||
assert!(summary.simulation.success, "{:#?}", summary.simulation);
|
||||
if request.submit {
|
||||
assert!(summary.send_result.is_some());
|
||||
assert!(summary.confirmation.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signer_guard_rejects_external_authorities() {
|
||||
assert!(super::validate_profile_wallet_signers(&["wallet".to_string()], "wallet").is_ok());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Machine-readable Metaplex Token Metadata validation contract.
|
||||
|
||||
@@ -506,13 +506,13 @@ pub fn validate_metaplex_token_metadata_devnet_execution_matrix(
|
||||
matrix: &crate::MetaplexTokenMetadataDevnetExecutionMatrix,
|
||||
) -> kb_core::Result<()> {
|
||||
if matrix.matrix_version != 1
|
||||
|| matrix.milestone != "0.4.7-pre.011"
|
||||
|| matrix.milestone != "0.4.7-pre.012"
|
||||
|| matrix.program_id != kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
|
||||
|| matrix.operations.len() != 20
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_devnet_execution_matrix_contract_mismatch",
|
||||
"Metaplex Devnet matrix must declare exactly 20 current operations for pre.011",
|
||||
"Metaplex Devnet matrix must declare exactly 20 current operations for pre.012",
|
||||
));
|
||||
}
|
||||
let expected = [
|
||||
@@ -552,6 +552,8 @@ pub fn validate_metaplex_token_metadata_devnet_execution_matrix(
|
||||
}
|
||||
for operation in &matrix.operations {
|
||||
if operation.deprecated
|
||||
|| operation.runner_status != "implemented"
|
||||
|| !matches!(operation.campaign_prerelease.as_str(), "0.4.7-pre.011" | "0.4.7-pre.012")
|
||||
|| operation.required_evidence.is_empty()
|
||||
|| operation.submission_evidence.is_empty()
|
||||
|| !matches!(
|
||||
@@ -561,7 +563,7 @@ pub fn validate_metaplex_token_metadata_devnet_execution_matrix(
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_devnet_execution_matrix_entry_invalid",
|
||||
"Metaplex Devnet entries must be current, evidenced and conservatively classified",
|
||||
"Metaplex Devnet entries must be current, implemented, evidenced and conservatively classified",
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -588,7 +590,21 @@ mod devnet_execution_matrix_tests {
|
||||
.count(),
|
||||
4,
|
||||
);
|
||||
assert_eq!(
|
||||
matrix
|
||||
.operations
|
||||
.iter()
|
||||
.filter(|operation| return operation.campaign_prerelease == "0.4.7-pre.012")
|
||||
.count(),
|
||||
16,
|
||||
);
|
||||
assert!(matrix.operations.iter().all(|operation| return !operation.deprecated));
|
||||
assert!(
|
||||
matrix
|
||||
.operations
|
||||
.iter()
|
||||
.all(|operation| return operation.runner_status == "implemented")
|
||||
);
|
||||
assert!(
|
||||
matrix
|
||||
.operations
|
||||
|
||||
Reference in New Issue
Block a user