853 lines
37 KiB
Rust
853 lines
37 KiB
Rust
// file: kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs
|
|
// version: 1
|
|
|
|
//! Desktop adapters for Metaplex Token Metadata execution demo scenarios.
|
|
|
|
/// UI-safe Metaplex scenario row.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload.ts"
|
|
)]
|
|
pub(crate) struct DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload {
|
|
/// Stable scenario identifier.
|
|
pub id: std::string::String,
|
|
/// Operator-visible scenario label.
|
|
pub label: std::string::String,
|
|
/// Stable asset-family code.
|
|
pub asset_family: std::string::String,
|
|
/// Stable fixture graph code.
|
|
pub fixture_kind: std::string::String,
|
|
/// Required initial lifecycle state.
|
|
pub initial_state: std::string::String,
|
|
/// Expected terminal lifecycle state.
|
|
pub resulting_state: std::string::String,
|
|
/// Stable execution-mode code.
|
|
pub mode: std::string::String,
|
|
/// Ordered operation codes.
|
|
pub operation_codes: std::vec::Vec<std::string::String>,
|
|
/// Whether a collection account is required.
|
|
pub requires_collection: bool,
|
|
/// Whether programmable authorization rules are required.
|
|
pub requires_programmable_rules: bool,
|
|
/// Whether postconditions are mandatory.
|
|
pub requires_postcondition: bool,
|
|
/// Current validation status from the canonical matrix.
|
|
pub validation_status: std::string::String,
|
|
/// Required evidence kinds.
|
|
pub required_evidence: std::vec::Vec<std::string::String>,
|
|
/// Canonical account projections expected when materialization is enabled.
|
|
pub materialization_targets: std::vec::Vec<std::string::String>,
|
|
}
|
|
|
|
/// UI-safe result of one prepared Metaplex Create fixture.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload.ts"
|
|
)]
|
|
pub(crate) struct DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload {
|
|
/// Fixture mint address.
|
|
pub mint: std::string::String,
|
|
/// Canonical metadata PDA.
|
|
pub metadata: std::string::String,
|
|
/// Canonical master-edition PDA.
|
|
pub master_edition: std::string::String,
|
|
/// Wallet authority.
|
|
pub authority: std::string::String,
|
|
/// Generated typed Create operation JSON.
|
|
pub operation_json: std::string::String,
|
|
/// Whether the mint account was newly created.
|
|
pub mint_created: bool,
|
|
}
|
|
|
|
/// UI-safe prepared step for one coherent Metaplex Devnet scenario.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload.ts"
|
|
)]
|
|
pub(crate) struct DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload {
|
|
/// Stable scenario identifier.
|
|
pub scenario_id: std::string::String,
|
|
/// Zero-based step index.
|
|
pub step_index: usize,
|
|
/// Stable operation name accepted by the executor JSON contract.
|
|
pub operation: std::string::String,
|
|
/// Operator-visible step label.
|
|
pub label: std::string::String,
|
|
/// Prepared typed operation JSON.
|
|
pub operation_json: std::string::String,
|
|
/// Automatically generated bounded preflight reads.
|
|
pub preflight_reads_json: std::string::String,
|
|
/// Automatically generated bounded postcondition reads.
|
|
pub postcondition_reads_json: std::string::String,
|
|
/// Whether this step can be simulated immediately.
|
|
pub simulation_ready: bool,
|
|
/// Whether this step can be submitted immediately.
|
|
pub submission_ready: bool,
|
|
/// Fixture state used to prepare the step.
|
|
pub fixture_state: std::string::String,
|
|
}
|
|
|
|
/// Request for one real Devnet Metaplex execution from the desktop panel.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataRequest.ts"
|
|
)]
|
|
pub(crate) struct DemoExecutionMetadataMetaplexTokenMetadataRequest {
|
|
/// Devnet profile selected by the operator.
|
|
pub profile_name: std::string::String,
|
|
/// Stable execution identifier shown in diagnostics.
|
|
pub intent_id: std::string::String,
|
|
/// Serialized typed current Metaplex operation.
|
|
pub operation_json: std::string::String,
|
|
/// Serialized bounded preflight read requests.
|
|
pub preflight_reads_json: std::string::String,
|
|
/// Serialized bounded postcondition read requests.
|
|
pub postcondition_reads_json: std::string::String,
|
|
/// Whether signing and submission are explicitly requested.
|
|
pub submit: bool,
|
|
/// Explicit operator confirmation for submission.
|
|
pub operator_confirmed: bool,
|
|
/// Materializes confirmed Metaplex account snapshots after submission.
|
|
pub materialize_after_confirmation: bool,
|
|
}
|
|
|
|
/// UI-safe result of one real Devnet Metaplex execution.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload.ts"
|
|
)]
|
|
pub(crate) struct DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload {
|
|
/// Selected profile name.
|
|
pub profile_name: std::string::String,
|
|
/// Exact cluster code.
|
|
pub cluster: std::string::String,
|
|
/// Genesis hash observed from RPC.
|
|
pub genesis_hash: std::string::String,
|
|
/// Wallet public key used by the runner.
|
|
pub wallet_public_key: std::string::String,
|
|
/// Wallet balance before execution.
|
|
pub balance_lamports: std::string::String,
|
|
/// Whether the real RPC simulation succeeded.
|
|
pub simulation_success: bool,
|
|
/// Optional submitted signature.
|
|
pub transaction_signature: std::option::Option<std::string::String>,
|
|
/// Optional terminal confirmation status.
|
|
pub confirmation_status: std::option::Option<std::string::String>,
|
|
/// Exact prepared plan as formatted JSON.
|
|
pub plan_json: std::string::String,
|
|
/// Stateful preflight as formatted JSON.
|
|
pub preflight_json: std::string::String,
|
|
/// Real RPC simulation as formatted JSON.
|
|
pub simulation_json: std::string::String,
|
|
/// Before/after state and readiness diagnostics as formatted JSON.
|
|
pub evidence_json: std::string::String,
|
|
/// Whether post-confirmation account materialization was requested.
|
|
pub materialization_requested: bool,
|
|
/// Number of canonical post-confirmation snapshots materialized.
|
|
pub materialized_snapshot_count: usize,
|
|
}
|
|
|
|
/// Executes one real Devnet Metaplex simulation or explicitly confirmed submission.
|
|
pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
|
|
app_handle: tauri::AppHandle,
|
|
state: tauri::State<'_, crate::AppState>,
|
|
request: crate::DemoExecutionMetadataMetaplexTokenMetadataRequest,
|
|
) -> std::result::Result<
|
|
crate::DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload,
|
|
std::string::String,
|
|
> {
|
|
let acquire_result = state.demo_execution_solana_core_running().compare_exchange(
|
|
false,
|
|
true,
|
|
std::sync::atomic::Ordering::AcqRel,
|
|
std::sync::atomic::Ordering::Acquire,
|
|
);
|
|
if acquire_result.is_err() {
|
|
return std::result::Result::Err("a Devnet execution is already running".to_string());
|
|
}
|
|
let _run_guard = crate::DemoExecutionRunGuard {
|
|
running: state.demo_execution_solana_core_running(),
|
|
};
|
|
state
|
|
.demo_execution_solana_core_cancel_requested()
|
|
.store(false, std::sync::atomic::Ordering::Release);
|
|
let profile =
|
|
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let http_pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
let mut execution_request = match kb_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json(
|
|
request.intent_id,
|
|
request.operation_json.as_str(),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
execution_request.preflight_reads =
|
|
match parse_stateful_reads(request.preflight_reads_json.as_str()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
execution_request.postcondition_reads =
|
|
match parse_stateful_reads(request.postcondition_reads_json.as_str()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
execution_request.submit = request.submit;
|
|
execution_request.operator_confirmed = request.operator_confirmed;
|
|
execution_request.materialize_after_confirmation = request.materialize_after_confirmation;
|
|
if let std::result::Result::Err(error) = execution_request.validate() {
|
|
return std::result::Result::Err(error.to_string());
|
|
}
|
|
let observer = crate::DemoExecutionMetadataObserver {
|
|
app_handle,
|
|
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
|
};
|
|
let workspace_root = crate::workspace_root_dir();
|
|
let summary = match kb_pipeline_demo_scenarios::execute_devnet_metaplex_token_metadata(
|
|
&http_pool,
|
|
&profile,
|
|
workspace_root.as_path(),
|
|
&execution_request,
|
|
&observer,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
let transaction_signature =
|
|
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
|
let confirmation_status =
|
|
summary.confirmation.as_ref().map(|value| return format!("{:?}", value.status));
|
|
let evidence_json = crate::pretty_json(&serde_json::json!({
|
|
"before": &summary.before,
|
|
"readiness": &summary.readiness,
|
|
"sendResult": &summary.send_result,
|
|
"confirmation": &summary.confirmation,
|
|
"after": &summary.after,
|
|
"materializationRequested": summary.materialization_requested,
|
|
"materializedSnapshots": &summary.materialized_snapshots
|
|
}));
|
|
return std::result::Result::Ok(
|
|
crate::DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload {
|
|
profile_name: summary.profile_name,
|
|
cluster: format!("{:?}", summary.cluster).to_lowercase(),
|
|
genesis_hash: summary.genesis_hash,
|
|
wallet_public_key: summary.wallet.public_key,
|
|
balance_lamports: summary.balance_lamports.to_string(),
|
|
simulation_success: summary.simulation.success,
|
|
transaction_signature,
|
|
confirmation_status,
|
|
plan_json: crate::pretty_json(&summary.plan),
|
|
preflight_json: crate::pretty_json(&summary.stateful_preflight),
|
|
simulation_json: crate::pretty_json(&summary.simulation),
|
|
evidence_json,
|
|
materialization_requested: summary.materialization_requested,
|
|
materialized_snapshot_count: summary.materialized_snapshots.len(),
|
|
},
|
|
);
|
|
}
|
|
|
|
fn parse_stateful_reads(
|
|
value: &str,
|
|
) -> std::result::Result<
|
|
std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest>,
|
|
std::string::String,
|
|
> {
|
|
if value.trim().is_empty() {
|
|
return std::result::Result::Ok(std::vec::Vec::new());
|
|
}
|
|
return match serde_json::from_str(value) {
|
|
std::result::Result::Ok(parsed) => std::result::Result::Ok(parsed),
|
|
std::result::Result::Err(error) => {
|
|
std::result::Result::Err(format!("invalid Metaplex stateful reads JSON: {error}"))
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Creates or reuses the Devnet mint fixture used by the named Create scenario.
|
|
pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_prepare_create_fixture(
|
|
state: tauri::State<'_, crate::AppState>,
|
|
profile_name: std::string::String,
|
|
) -> std::result::Result<
|
|
crate::DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload,
|
|
std::string::String,
|
|
> {
|
|
let profile = match crate::select_devnet_profile(state.app_config(), profile_name.as_str()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
|
|
let wallet_dir = if configured.is_absolute() {
|
|
configured
|
|
} else {
|
|
crate::workspace_root_dir().join(configured)
|
|
};
|
|
let http_pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
let workspace_root = crate::workspace_root_dir();
|
|
let summary = match kb_pipeline_demo_scenarios::prepare_metaplex_create_fixture(
|
|
&http_pool,
|
|
&profile,
|
|
workspace_root.as_path(),
|
|
&kb_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions::new(wallet_dir),
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
return std::result::Result::Ok(
|
|
crate::DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload {
|
|
mint: summary.mint,
|
|
metadata: summary.metadata,
|
|
master_edition: summary.master_edition,
|
|
authority: summary.authority,
|
|
operation_json: summary.operation_json,
|
|
mint_created: summary.mint_created,
|
|
},
|
|
);
|
|
}
|
|
|
|
fn adapt_create_operation_json(
|
|
operation_json: &str,
|
|
asset_family: kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily,
|
|
) -> std::result::Result<std::string::String, std::string::String> {
|
|
let mut value: serde_json::Value = match serde_json::from_str(operation_json) {
|
|
std::result::Result::Ok(parsed) => parsed,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(format!(
|
|
"invalid generated Metaplex Create JSON: {error}"
|
|
));
|
|
},
|
|
};
|
|
if matches!(
|
|
asset_family,
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft
|
|
| kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible
|
|
) {
|
|
value["master_edition"] = serde_json::Value::Null;
|
|
}
|
|
let create = match value
|
|
.get_mut("create_args")
|
|
.and_then(|args| return args.get_mut("V1"))
|
|
.and_then(serde_json::Value::as_object_mut)
|
|
{
|
|
std::option::Option::Some(found) => found,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
"generated Metaplex Create JSON has no create_args.V1 object".to_string(),
|
|
);
|
|
},
|
|
};
|
|
match asset_family {
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft => {
|
|
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet NFT"));
|
|
create.insert("symbol".to_string(), serde_json::json!("KHNFT"));
|
|
create.insert("token_standard".to_string(), serde_json::json!("NonFungible"));
|
|
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
|
create.insert("decimals".to_string(), serde_json::Value::Null);
|
|
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft => {
|
|
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet SFT"));
|
|
create.insert("symbol".to_string(), serde_json::json!("KHSFT"));
|
|
create.insert("token_standard".to_string(), serde_json::json!("FungibleAsset"));
|
|
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
|
create.insert("decimals".to_string(), serde_json::json!(0));
|
|
create.insert("print_supply".to_string(), serde_json::Value::Null);
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible => {
|
|
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet Fungible"));
|
|
create.insert("symbol".to_string(), serde_json::json!("KHFUN"));
|
|
create.insert("token_standard".to_string(), serde_json::json!("Fungible"));
|
|
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
|
create.insert("decimals".to_string(), serde_json::json!(0));
|
|
create.insert("print_supply".to_string(), serde_json::Value::Null);
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection => {
|
|
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet Collection"));
|
|
create.insert("symbol".to_string(), serde_json::json!("KHCOL"));
|
|
create.insert("token_standard".to_string(), serde_json::json!("NonFungible"));
|
|
create.insert("collection_details".to_string(), serde_json::json!({"V1": {"size": 0}}));
|
|
create.insert("decimals".to_string(), serde_json::Value::Null);
|
|
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft => {
|
|
create.insert("name".to_string(), serde_json::json!("Khadhroony Devnet pNFT"));
|
|
create.insert("symbol".to_string(), serde_json::json!("KHPNFT"));
|
|
create
|
|
.insert("token_standard".to_string(), serde_json::json!("ProgrammableNonFungible"));
|
|
create.insert("collection_details".to_string(), serde_json::Value::Null);
|
|
create.insert("rule_set".to_string(), serde_json::Value::Null);
|
|
create.insert("decimals".to_string(), serde_json::Value::Null);
|
|
create.insert("print_supply".to_string(), serde_json::json!("Zero"));
|
|
},
|
|
}
|
|
return std::result::Result::Ok(crate::pretty_json(&value));
|
|
}
|
|
|
|
/// Prepares one coherent scenario step and all of its stateful reads.
|
|
pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_prepare_step(
|
|
state: tauri::State<'_, crate::AppState>,
|
|
profile_name: std::string::String,
|
|
scenario_id: std::string::String,
|
|
step_index: usize,
|
|
previous_operation_json: std::option::Option<std::string::String>,
|
|
) -> std::result::Result<
|
|
crate::DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload,
|
|
std::string::String,
|
|
> {
|
|
let scenario = kb_pipeline_demo_scenarios::metaplex_token_metadata_devnet_scenarios()
|
|
.into_iter()
|
|
.find(|candidate| return candidate.id == scenario_id);
|
|
let scenario = match scenario {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(format!(
|
|
"unknown Metaplex Devnet scenario `{scenario_id}`"
|
|
));
|
|
},
|
|
};
|
|
let operation_code = match scenario.operation_codes.get(step_index) {
|
|
std::option::Option::Some(value) => value.clone(),
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(format!(
|
|
"scenario `{}` has no step at index {step_index}",
|
|
scenario.id
|
|
));
|
|
},
|
|
};
|
|
let operation_suffix = match operation_code.rsplit('.').next() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => operation_code.as_str(),
|
|
};
|
|
let operation = if operation_suffix == "update" {
|
|
"update_as_update_authority_v2".to_string()
|
|
} else {
|
|
operation_suffix.to_string()
|
|
};
|
|
if operation != "create" && operation != "update_as_update_authority_v2" {
|
|
return std::result::Result::Err(format!(
|
|
"scenario step `{operation}` is declared but its Devnet fixture builder is not implemented yet"
|
|
));
|
|
}
|
|
let (fixture_mint, fixture_metadata, fixture_master_edition, operation_json, fixture_state) =
|
|
if operation == "create" {
|
|
let fixture =
|
|
match crate::demo_execution_metadata_metaplex_token_metadata_prepare_create_fixture(
|
|
state,
|
|
profile_name,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let operation_json = match adapt_create_operation_json(
|
|
fixture.operation_json.as_str(),
|
|
scenario.asset_family,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
(
|
|
fixture.mint,
|
|
fixture.metadata,
|
|
fixture.master_edition,
|
|
operation_json,
|
|
if fixture.mint_created {
|
|
"mint_prepared".to_string()
|
|
} else {
|
|
"mint_reused".to_string()
|
|
},
|
|
)
|
|
} else {
|
|
let previous = match previous_operation_json {
|
|
std::option::Option::Some(value) if !value.trim().is_empty() => value,
|
|
_ => {
|
|
return std::result::Result::Err(
|
|
"prepare and submit the previous NFT lifecycle step before continuing"
|
|
.to_string(),
|
|
);
|
|
},
|
|
};
|
|
let previous_value: serde_json::Value = match serde_json::from_str(previous.as_str()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(format!(
|
|
"invalid previous scenario operation JSON: {error}"
|
|
));
|
|
},
|
|
};
|
|
let string_field = |name: &str| -> std::result::Result<
|
|
std::string::String,
|
|
std::string::String,
|
|
> {
|
|
return match previous_value.get(name).and_then(serde_json::Value::as_str) {
|
|
std::option::Option::Some(value) => std::result::Result::Ok(value.to_string()),
|
|
std::option::Option::None => std::result::Result::Err(format!(
|
|
"previous scenario step does not contain `{name}`"
|
|
)),
|
|
};
|
|
};
|
|
let mint = match string_field("mint") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let metadata = match string_field("metadata") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let authority = match string_field("authority") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let master_edition =
|
|
match previous_value.get("master_edition").and_then(serde_json::Value::as_str) {
|
|
std::option::Option::Some(value) => value.to_string(),
|
|
std::option::Option::None => std::string::String::new(),
|
|
};
|
|
let immutable_transition = scenario.asset_family
|
|
== kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft
|
|
&& step_index == 2;
|
|
let value = serde_json::json!({
|
|
"operation": "update_as_update_authority_v2",
|
|
"authority": authority.clone(),
|
|
"mint": mint.clone(),
|
|
"metadata": metadata.clone(),
|
|
"edition": null,
|
|
"token": null,
|
|
"authorization_rules_program": null,
|
|
"authorization_rules": null,
|
|
"update_args": {
|
|
"AsUpdateAuthorityV2": {
|
|
"new_update_authority": null,
|
|
"data": null,
|
|
"primary_sale_happened": if immutable_transition {
|
|
serde_json::Value::Null
|
|
} else {
|
|
serde_json::json!(true)
|
|
},
|
|
"is_mutable": if immutable_transition {
|
|
serde_json::json!(false)
|
|
} else {
|
|
serde_json::Value::Null
|
|
},
|
|
"collection": "None",
|
|
"collection_details": "None",
|
|
"uses": "None",
|
|
"rule_set": "None",
|
|
"token_standard": null,
|
|
"authorization_data": null
|
|
}
|
|
}
|
|
});
|
|
(
|
|
mint,
|
|
metadata,
|
|
master_edition,
|
|
crate::pretty_json(&value),
|
|
if immutable_transition {
|
|
"metadata_immutable".to_string()
|
|
} else {
|
|
"metadata_created".to_string()
|
|
},
|
|
)
|
|
};
|
|
let metadata_read = serde_json::json!({
|
|
"query_role": "http_queries",
|
|
"account": fixture_metadata.clone(),
|
|
"kind": { "kind": "metadata" },
|
|
"min_context_slot": null,
|
|
"max_data_bytes": 65536
|
|
});
|
|
let edition_read = serde_json::json!({
|
|
"query_role": "http_queries",
|
|
"account": fixture_master_edition.clone(),
|
|
"kind": { "kind": "edition", "mint": fixture_mint.clone() },
|
|
"min_context_slot": null,
|
|
"max_data_bytes": 65536
|
|
});
|
|
let preflight_reads = if operation == "create" {
|
|
serde_json::json!([])
|
|
} else {
|
|
serde_json::json!([metadata_read.clone()])
|
|
};
|
|
let create_reads_edition = matches!(
|
|
scenario.asset_family,
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft
|
|
| kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection
|
|
| kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft
|
|
);
|
|
let postcondition_reads = if operation == "create" && create_reads_edition {
|
|
serde_json::json!([metadata_read, edition_read])
|
|
} else {
|
|
serde_json::json!([metadata_read])
|
|
};
|
|
return std::result::Result::Ok(
|
|
crate::DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload {
|
|
scenario_id: scenario.id,
|
|
step_index,
|
|
operation: operation.clone(),
|
|
label: if scenario.asset_family
|
|
== kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft
|
|
&& step_index == 2
|
|
{
|
|
"Étape 3 — rendre les metadata immutables".to_string()
|
|
} else {
|
|
format!("Étape {} — {}", step_index + 1, operation)
|
|
},
|
|
operation_json,
|
|
preflight_reads_json: crate::pretty_json(&preflight_reads),
|
|
postcondition_reads_json: crate::pretty_json(&postcondition_reads),
|
|
simulation_ready: true,
|
|
submission_ready: true,
|
|
fixture_state,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Returns current operation names accepted by automatic Devnet campaigns.
|
|
pub(crate) fn demo_execution_metadata_metaplex_token_metadata_current_operations()
|
|
-> std::vec::Vec<std::string::String> {
|
|
return kb_pipeline_demo_scenarios::metaplex_token_metadata_current_operation_names()
|
|
.iter()
|
|
.map(|value| return (*value).to_string())
|
|
.collect();
|
|
}
|
|
|
|
/// Returns one generated typed JSON template for a named current operation.
|
|
pub(crate) fn demo_execution_metadata_metaplex_token_metadata_operation_template(
|
|
operation: std::string::String,
|
|
) -> std::result::Result<std::string::String, std::string::String> {
|
|
return match kb_pipeline_demo_scenarios::metaplex_token_metadata_current_operation_json_template(
|
|
operation.as_str(),
|
|
) {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
|
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
|
|
};
|
|
}
|
|
|
|
/// Returns the canonical Metaplex scenario inventory for the desktop panel.
|
|
pub(crate) fn demo_execution_metadata_metaplex_token_metadata_scenarios() -> std::result::Result<
|
|
std::vec::Vec<crate::DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload>,
|
|
std::string::String,
|
|
> {
|
|
let matrix = match kb_pipeline_demo_scenarios::load_metaplex_token_metadata_validation_matrix()
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
let statuses = matrix
|
|
.scenarios
|
|
.into_iter()
|
|
.map(|scenario| return (scenario.id, (scenario.status, scenario.required_evidence)))
|
|
.collect::<std::collections::BTreeMap<
|
|
std::string::String,
|
|
(
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus,
|
|
std::vec::Vec<std::string::String>,
|
|
),
|
|
>>();
|
|
let mut payloads = std::vec::Vec::new();
|
|
for scenario in kb_pipeline_demo_scenarios::metaplex_token_metadata_synthetic_scenarios()
|
|
.into_iter()
|
|
.chain(kb_pipeline_demo_scenarios::metaplex_token_metadata_devnet_scenarios())
|
|
{
|
|
let status = statuses.get(scenario.id.as_str());
|
|
let validation_status = match status {
|
|
std::option::Option::Some((value, _)) => validation_status_code(*value),
|
|
std::option::Option::None => "not_run".to_string(),
|
|
};
|
|
let required_evidence = match status {
|
|
std::option::Option::Some((_, evidence)) => evidence.clone(),
|
|
std::option::Option::None => std::vec::Vec::new(),
|
|
};
|
|
payloads.push(crate::DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload {
|
|
id: scenario.id,
|
|
label: scenario.label,
|
|
asset_family: asset_family_code(scenario.asset_family),
|
|
fixture_kind: fixture_kind_code(scenario.fixture_kind),
|
|
initial_state: fixture_state_code(scenario.initial_state),
|
|
resulting_state: fixture_state_code(scenario.resulting_state),
|
|
mode: scenario_mode_code(scenario.mode),
|
|
operation_codes: scenario.operation_codes,
|
|
requires_collection: scenario.requires_collection,
|
|
requires_programmable_rules: scenario.requires_programmable_rules,
|
|
requires_postcondition: scenario.requires_postcondition,
|
|
validation_status,
|
|
required_evidence,
|
|
materialization_targets: scenario.materialization_targets,
|
|
});
|
|
}
|
|
return std::result::Result::Ok(payloads);
|
|
}
|
|
|
|
fn asset_family_code(
|
|
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily,
|
|
) -> std::string::String {
|
|
return match value {
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft => "nft".to_string(),
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft => "sft".to_string(),
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible => {
|
|
"fungible".to_string()
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection => {
|
|
"collection".to_string()
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft => {
|
|
"programmable_nft".to_string()
|
|
},
|
|
};
|
|
}
|
|
|
|
fn fixture_kind_code(
|
|
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataFixtureKind,
|
|
) -> std::string::String {
|
|
return format!("{value:?}").to_lowercase();
|
|
}
|
|
|
|
fn fixture_state_code(
|
|
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataFixtureState,
|
|
) -> std::string::String {
|
|
return format!("{value:?}").to_lowercase();
|
|
}
|
|
|
|
fn scenario_mode_code(
|
|
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode,
|
|
) -> std::string::String {
|
|
return match value {
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode::Synthetic => {
|
|
"synthetic".to_string()
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode::NetworkSimulation => {
|
|
"network_simulation".to_string()
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataScenarioMode::NetworkSubmission => {
|
|
"network_submission".to_string()
|
|
},
|
|
};
|
|
}
|
|
|
|
fn validation_status_code(
|
|
value: kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus,
|
|
) -> std::string::String {
|
|
return match value {
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::NotRun => {
|
|
"not_run".to_string()
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::SyntheticValidated => {
|
|
"synthetic_validated".to_string()
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Simulated => {
|
|
"simulated".to_string()
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Submitted => {
|
|
"submitted".to_string()
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Confirmed => {
|
|
"confirmed".to_string()
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Unavailable => {
|
|
"unavailable".to_string()
|
|
},
|
|
kb_pipeline_demo_scenarios::MetaplexTokenMetadataValidationStatus::Failed => {
|
|
"failed".to_string()
|
|
},
|
|
};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn desktop_inventory_matches_reusable_scenarios_and_matrix() {
|
|
let payloads = match crate::demo_execution_metadata_metaplex_token_metadata_scenarios() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
assert_eq!(std::option::Option::Some(error), std::option::Option::None);
|
|
return;
|
|
},
|
|
};
|
|
let expected_count =
|
|
kb_pipeline_demo_scenarios::metaplex_token_metadata_synthetic_scenarios().len()
|
|
+ kb_pipeline_demo_scenarios::metaplex_token_metadata_devnet_scenarios().len();
|
|
assert_eq!(payloads.len(), expected_count);
|
|
assert!(payloads.iter().all(|payload| return !payload.operation_codes.is_empty()));
|
|
assert!(payloads.iter().all(|payload| return payload.requires_postcondition));
|
|
assert!(
|
|
payloads
|
|
.iter()
|
|
.all(|payload| return payload.validation_status != "missing_from_matrix")
|
|
);
|
|
assert_eq!(
|
|
payloads
|
|
.iter()
|
|
.filter(|payload| return payload.mode == "network_simulation")
|
|
.count(),
|
|
5
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_stateful_reads_are_accepted() {
|
|
let parsed = super::parse_stateful_reads(" ");
|
|
assert!(parsed.is_ok());
|
|
let values = match parsed {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
assert_eq!(std::option::Option::Some(error), std::option::Option::None);
|
|
return;
|
|
},
|
|
};
|
|
assert_eq!(values.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn metadata_frontend_contains_every_required_runtime_control() {
|
|
let source = std::include_str!("../frontend/demo_execution_metadata.html");
|
|
for id in [
|
|
"metadataProfileSelect",
|
|
"metadataDevnetScenarioSelect",
|
|
"metadataStepSelect",
|
|
"prepareMetadataScenarioStepButton",
|
|
"metadataExecutionLogOutput",
|
|
"metadataProfileHelp",
|
|
"simulateMetadataButton",
|
|
"submitMetadataButton",
|
|
] {
|
|
assert!(source.contains(format!("id=\"{id}\"").as_str()));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn programmable_and_collection_requirements_remain_explicit() {
|
|
let payloads = match crate::demo_execution_metadata_metaplex_token_metadata_scenarios() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
assert_eq!(std::option::Option::Some(error), std::option::Option::None);
|
|
return;
|
|
},
|
|
};
|
|
assert!(payloads.iter().any(|payload| return payload.requires_collection));
|
|
assert!(payloads.iter().any(|payload| return payload.requires_programmable_rules));
|
|
}
|
|
}
|