v0.4.8-pre.014
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// file: kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs
|
||||
// version: 4
|
||||
// version: 6
|
||||
|
||||
//! Desktop adapters for Metaplex Token Metadata execution demo scenarios.
|
||||
//! Desktop adapters for generic and qualified Metaplex Token Metadata execution workflows.
|
||||
|
||||
/// UI-safe Metaplex scenario row.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
||||
@@ -815,6 +815,876 @@ fn validation_status_code(
|
||||
};
|
||||
}
|
||||
|
||||
// Qualified Metaplex Devnet campaigns.
|
||||
|
||||
/// UI-safe descriptor for one qualified Metaplex Devnet campaign.
|
||||
#[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/DemoExecutionMetadataMetaplexCampaignOptionPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataMetaplexCampaignOptionPayload {
|
||||
/// Stable campaign identifier accepted by the desktop executor.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible campaign label.
|
||||
pub label: std::string::String,
|
||||
/// Primary asset family exercised by the campaign.
|
||||
pub asset_family: std::string::String,
|
||||
/// Target operations qualified by the campaign after any fixture setup.
|
||||
pub operation_names: std::vec::Vec<std::string::String>,
|
||||
/// Stable qualification code shown by the desktop.
|
||||
pub qualification: std::string::String,
|
||||
/// Human-readable fixture/setup contract.
|
||||
pub setup: std::string::String,
|
||||
}
|
||||
|
||||
/// Request for one complete qualified Metaplex Devnet campaign.
|
||||
#[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/DemoExecutionMetadataMetaplexCampaignRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataMetaplexCampaignRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub profile_name: std::string::String,
|
||||
/// Stable campaign identifier returned by the campaign inventory.
|
||||
pub campaign_id: std::string::String,
|
||||
/// Explicit authorization for fixture creation and every campaign submission.
|
||||
pub operator_confirmed: bool,
|
||||
}
|
||||
|
||||
/// UI-safe result of one qualified Metaplex Devnet campaign.
|
||||
#[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/DemoExecutionMetadataMetaplexCampaignSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataMetaplexCampaignSummaryPayload {
|
||||
/// Profile used by the campaign.
|
||||
pub profile_name: std::string::String,
|
||||
/// Stable campaign identifier.
|
||||
pub campaign_id: std::string::String,
|
||||
/// Operator-visible campaign label.
|
||||
pub label: std::string::String,
|
||||
/// Number of Metaplex operation executions or probes retained in this run.
|
||||
pub operation_count: usize,
|
||||
/// Number of Metaplex operations reaching confirmed or finalized state.
|
||||
pub confirmed_operation_count: usize,
|
||||
/// Number of simulation-only runtime-unavailable probes retained in this run.
|
||||
pub unavailable_operation_count: usize,
|
||||
/// Whether the complete campaign reached its expected terminal contract.
|
||||
pub completed: bool,
|
||||
/// Fixture/setup description as formatted JSON.
|
||||
pub fixture_json: std::string::String,
|
||||
/// Ordered Metaplex execution evidence as formatted JSON.
|
||||
pub executions_json: std::string::String,
|
||||
/// Exact campaign postconditions as formatted JSON.
|
||||
pub postconditions_json: std::string::String,
|
||||
}
|
||||
|
||||
struct CampaignProjection {
|
||||
label: std::string::String,
|
||||
fixture: serde_json::Value,
|
||||
executions: std::vec::Vec<serde_json::Value>,
|
||||
postconditions: serde_json::Value,
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
type CampaignProjectionFuture<'a> = std::pin::Pin<
|
||||
std::boxed::Box<
|
||||
dyn std::future::Future<Output = kb_core::Result<CampaignProjection>>
|
||||
+ std::marker::Send
|
||||
+ 'a,
|
||||
>,
|
||||
>;
|
||||
|
||||
/// Returns the qualified Metaplex Devnet campaign inventory exposed by the desktop.
|
||||
pub(crate) fn demo_execution_metadata_metaplex_campaigns()
|
||||
-> std::vec::Vec<crate::DemoExecutionMetadataMetaplexCampaignOptionPayload> {
|
||||
let mut values = std::vec::Vec::new();
|
||||
for (id, label, family) in [
|
||||
("create_mint_nft", "Create → Mint — NFT", "nft"),
|
||||
("create_mint_sft", "Create → Mint — SFT", "sft"),
|
||||
("create_mint_fungible", "Create → Mint — Fungible", "fungible"),
|
||||
("create_mint_collection", "Create → Mint — Collection", "collection"),
|
||||
("create_mint_programmable_nft", "Create → Mint — pNFT", "programmable_nft"),
|
||||
] {
|
||||
values.push(crate::DemoExecutionMetadataMetaplexCampaignOptionPayload {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
asset_family: family.to_string(),
|
||||
operation_names:
|
||||
kb_pipeline_demo_scenarios::metaplex_create_mint_campaign_operation_names()
|
||||
.iter()
|
||||
.map(|value| return (*value).to_string())
|
||||
.collect(),
|
||||
qualification: "confirmed".to_string(),
|
||||
setup: "fresh classic SPL mint + operator ATA".to_string(),
|
||||
});
|
||||
}
|
||||
values.extend([
|
||||
campaign_option(
|
||||
"collection_verify",
|
||||
"Collection — Verify → Unverify",
|
||||
"collection",
|
||||
kb_pipeline_demo_scenarios::metaplex_collection_verify_campaign_operation_names(),
|
||||
"confirmed",
|
||||
"fresh collection parent + fresh unverified member",
|
||||
),
|
||||
campaign_option(
|
||||
"print_burn",
|
||||
"NFT imprimable — Print → Burn",
|
||||
"nft",
|
||||
kb_pipeline_demo_scenarios::metaplex_print_burn_campaign_operation_names(),
|
||||
"confirmed",
|
||||
"fresh limited-supply master NFT + fresh printed edition",
|
||||
),
|
||||
campaign_option(
|
||||
"pnft_lifecycle",
|
||||
"pNFT — Delegate → Lock → Unlock → Revoke → Delegate → Transfer",
|
||||
"programmable_nft",
|
||||
kb_pipeline_demo_scenarios::metaplex_pnft_lifecycle_campaign_operation_names(),
|
||||
"confirmed",
|
||||
"fresh pNFT + delegate wallet + destination owner",
|
||||
),
|
||||
campaign_option(
|
||||
"escrow",
|
||||
"Token Owned Escrow — Create → TransferOut → Close",
|
||||
"nft",
|
||||
kb_pipeline_demo_scenarios::metaplex_escrow_campaign_operation_names(),
|
||||
"confirmed",
|
||||
"fresh NFT parent + fresh fungible attribute + escrow ATA deposit",
|
||||
),
|
||||
campaign_option(
|
||||
"maintenance",
|
||||
"Maintenance — Update + probes réservés/legacy",
|
||||
"nft",
|
||||
kb_pipeline_demo_scenarios::metaplex_maintenance_campaign_operation_names(),
|
||||
"mixed_confirmed_unavailable",
|
||||
"fresh NFT; Update submitted; Resize/Migrate/Collect/CloseAccounts simulation-only probes",
|
||||
),
|
||||
campaign_option(
|
||||
"use_probe",
|
||||
"Use — probe runtime",
|
||||
"nft",
|
||||
kb_pipeline_demo_scenarios::metaplex_use_probe_operation_names(),
|
||||
"runtime_unavailable_probe",
|
||||
"fresh NFT with two bounded Multiple uses; Use is simulation-only when rejected",
|
||||
),
|
||||
]);
|
||||
return values;
|
||||
}
|
||||
|
||||
fn campaign_option<const N: usize>(
|
||||
id: &str,
|
||||
label: &str,
|
||||
asset_family: &str,
|
||||
operation_names: &'static [&'static str; N],
|
||||
qualification: &str,
|
||||
setup: &str,
|
||||
) -> crate::DemoExecutionMetadataMetaplexCampaignOptionPayload {
|
||||
return crate::DemoExecutionMetadataMetaplexCampaignOptionPayload {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
asset_family: asset_family.to_string(),
|
||||
operation_names: operation_names.iter().map(|value| return (*value).to_string()).collect(),
|
||||
qualification: qualification.to_string(),
|
||||
setup: setup.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one complete qualified Metaplex Devnet campaign.
|
||||
pub(crate) async fn demo_execution_metadata_metaplex_execute_campaign(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataMetaplexCampaignRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataMetaplexCampaignSummaryPayload,
|
||||
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);
|
||||
if !request.operator_confirmed {
|
||||
return std::result::Result::Err(
|
||||
"Metaplex qualified campaign requires explicit operator confirmation".to_string(),
|
||||
);
|
||||
}
|
||||
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),
|
||||
};
|
||||
if !profile.wallet.devnet_send_enabled {
|
||||
return std::result::Result::Err(
|
||||
"selected profile does not authorize Devnet submissions required by Metaplex fixtures"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
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 store = match crate::connect_postgres_store(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let workspace_root = crate::workspace_root_dir();
|
||||
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 observer = crate::DemoExecutionMetadataObserver {
|
||||
app_handle,
|
||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||
};
|
||||
tracing::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "execute_metadata_metaplex_campaign",
|
||||
campaign_id = %request.campaign_id,
|
||||
phase = "campaign_start",
|
||||
"start qualified Metaplex desktop campaign"
|
||||
);
|
||||
let projection = match execute_campaign_projection(
|
||||
request.campaign_id.as_str(),
|
||||
wallet_dir,
|
||||
&http_pool,
|
||||
&store,
|
||||
&profile,
|
||||
workspace_root.as_path(),
|
||||
&observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
tracing::error!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "execute_metadata_metaplex_campaign",
|
||||
campaign_id = %request.campaign_id,
|
||||
phase = "campaign_failed",
|
||||
error = %error,
|
||||
"qualified Metaplex desktop campaign failed"
|
||||
);
|
||||
return std::result::Result::Err(error.to_string());
|
||||
},
|
||||
};
|
||||
tracing::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "execute_metadata_metaplex_campaign",
|
||||
campaign_id = %request.campaign_id,
|
||||
phase = "campaign_completed",
|
||||
completed = projection.completed,
|
||||
execution_count = projection.executions.len(),
|
||||
"qualified Metaplex desktop campaign completed"
|
||||
);
|
||||
let confirmed_operation_count = projection
|
||||
.executions
|
||||
.iter()
|
||||
.filter(|value| {
|
||||
return value.get("classification").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some("confirmed");
|
||||
})
|
||||
.count();
|
||||
let unavailable_operation_count = projection
|
||||
.executions
|
||||
.iter()
|
||||
.filter(|value| {
|
||||
return value.get("classification").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some("unavailable");
|
||||
})
|
||||
.count();
|
||||
return std::result::Result::Ok(crate::DemoExecutionMetadataMetaplexCampaignSummaryPayload {
|
||||
profile_name: request.profile_name,
|
||||
campaign_id: request.campaign_id,
|
||||
label: projection.label,
|
||||
operation_count: projection.executions.len(),
|
||||
confirmed_operation_count,
|
||||
unavailable_operation_count,
|
||||
completed: projection.completed,
|
||||
fixture_json: crate::pretty_json(&projection.fixture),
|
||||
executions_json: crate::pretty_json(&projection.executions),
|
||||
postconditions_json: crate::pretty_json(&projection.postconditions),
|
||||
});
|
||||
}
|
||||
|
||||
fn execute_campaign_projection<'a, S, O>(
|
||||
campaign_id: &'a str,
|
||||
wallet_dir: std::path::PathBuf,
|
||||
http_pool: &'a kb_onchain_transport::HttpEndpointPool,
|
||||
store: &'a S,
|
||||
profile: &'a kb_config::ProfileConfig,
|
||||
workspace_root: &'a std::path::Path,
|
||||
observer: &'a O,
|
||||
) -> CampaignProjectionFuture<'a>
|
||||
where
|
||||
S: kb_store::RawTransactionStore
|
||||
+ kb_store::CoreExtractionStore
|
||||
+ kb_store::DecodePipelineStore
|
||||
+ Sync
|
||||
+ 'a,
|
||||
O: kb_pipeline_demo_scenarios::SolanaExecutionObserver + 'a,
|
||||
{
|
||||
return std::boxed::Box::pin(execute_campaign_projection_unboxed(
|
||||
campaign_id,
|
||||
wallet_dir,
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
));
|
||||
}
|
||||
|
||||
async fn execute_campaign_projection_unboxed<S, O>(
|
||||
campaign_id: &str,
|
||||
wallet_dir: std::path::PathBuf,
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<CampaignProjection>
|
||||
where
|
||||
S: kb_store::RawTransactionStore
|
||||
+ kb_store::CoreExtractionStore
|
||||
+ kb_store::DecodePipelineStore
|
||||
+ Sync,
|
||||
O: kb_pipeline_demo_scenarios::SolanaExecutionObserver,
|
||||
{
|
||||
let base_options =
|
||||
kb_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions::new(wallet_dir);
|
||||
return match campaign_id {
|
||||
"create_mint_nft" => {
|
||||
execute_create_mint_projection(
|
||||
"Create → Mint — NFT",
|
||||
base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
|
||||
),
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"create_mint_sft" => {
|
||||
execute_create_mint_projection(
|
||||
"Create → Mint — SFT",
|
||||
base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Sft,
|
||||
),
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"create_mint_fungible" => {
|
||||
execute_create_mint_projection(
|
||||
"Create → Mint — Fungible",
|
||||
base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Fungible,
|
||||
),
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"create_mint_collection" => {
|
||||
execute_create_mint_projection(
|
||||
"Create → Mint — Collection",
|
||||
base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Collection,
|
||||
),
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"create_mint_programmable_nft" => {
|
||||
execute_create_mint_projection(
|
||||
"Create → Mint — pNFT",
|
||||
base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
|
||||
),
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
},
|
||||
"collection_verify" => {
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_metaplex_collection_verify_campaign(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&base_options,
|
||||
observer,
|
||||
).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let executions = vec![
|
||||
metaplex_execution_json("parent.create", &summary.parent.create),
|
||||
metaplex_execution_json("parent.mint", &summary.parent.mint),
|
||||
metaplex_execution_json("member.create", &summary.member.create),
|
||||
metaplex_execution_json("member.mint", &summary.member.mint),
|
||||
metaplex_execution_json("verify", &summary.verify),
|
||||
metaplex_execution_json("unverify", &summary.unverify),
|
||||
];
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "Collection — Verify → Unverify".to_string(),
|
||||
fixture: serde_json::json!({
|
||||
"parent": &summary.parent.fixture,
|
||||
"member": &summary.member.fixture
|
||||
}),
|
||||
completed: executions.iter().all(execution_is_confirmed_json),
|
||||
executions,
|
||||
postconditions: serde_json::json!({
|
||||
"verifiedBefore": summary.state.verified_before,
|
||||
"verifiedAfterVerify": summary.state.verified_after_verify,
|
||||
"verifiedAfterUnverify": summary.state.verified_after_unverify,
|
||||
"collectionSizeBefore": summary.state.collection_size_before,
|
||||
"collectionSizeAfterVerify": summary.state.collection_size_after_verify,
|
||||
"collectionSizeAfterUnverify": summary.state.collection_size_after_unverify
|
||||
}),
|
||||
})
|
||||
},
|
||||
"print_burn" => {
|
||||
let summary =
|
||||
match kb_pipeline_demo_scenarios::execute_devnet_metaplex_print_burn_campaign(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&base_options,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let executions = vec![
|
||||
metaplex_execution_json("master.create", &summary.master.create),
|
||||
metaplex_execution_json("master.mint", &summary.master.mint),
|
||||
metaplex_execution_json("print", &summary.print),
|
||||
metaplex_execution_json("burn", &summary.burn),
|
||||
];
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "NFT imprimable — Print → Burn".to_string(),
|
||||
fixture: serde_json::json!({
|
||||
"master": &summary.master.fixture,
|
||||
"editionMint": &summary.edition_mint,
|
||||
"editionMetadata": &summary.edition_metadata,
|
||||
"edition": &summary.edition,
|
||||
"editionTokenAccount": &summary.edition_token_account,
|
||||
"editionMarker": &summary.edition_marker
|
||||
}),
|
||||
completed: executions.iter().all(execution_is_confirmed_json)
|
||||
&& summary.state.edition_metadata_closed_after_burn
|
||||
&& !summary.state.edition_exists_after_burn
|
||||
&& !summary.state.edition_token_exists_after_burn,
|
||||
executions,
|
||||
postconditions: print_burn_state_json(&summary.state),
|
||||
})
|
||||
},
|
||||
"pnft_lifecycle" => {
|
||||
let options = base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
|
||||
);
|
||||
let summary =
|
||||
match kb_pipeline_demo_scenarios::execute_devnet_metaplex_pnft_lifecycle_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 executions = vec![
|
||||
metaplex_execution_json("pnft.create", &summary.pnft.create),
|
||||
metaplex_execution_json("pnft.mint", &summary.pnft.mint),
|
||||
metaplex_execution_json("delegate_staking", &summary.delegate_staking),
|
||||
metaplex_execution_json("lock", &summary.lock),
|
||||
metaplex_execution_json("unlock", &summary.unlock),
|
||||
metaplex_execution_json("revoke", &summary.revoke),
|
||||
metaplex_execution_json("delegate_transfer", &summary.delegate_transfer),
|
||||
metaplex_execution_json("transfer", &summary.transfer),
|
||||
];
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "pNFT lifecycle".to_string(),
|
||||
fixture: serde_json::json!({"pnft": &summary.pnft.fixture}),
|
||||
completed: executions.iter().all(execution_is_confirmed_json)
|
||||
&& summary.state.staking_delegate_revoked
|
||||
&& summary.state.destination_delegate_cleared,
|
||||
executions,
|
||||
postconditions: pnft_state_json(&summary.state),
|
||||
})
|
||||
},
|
||||
"escrow" => {
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_metaplex_escrow_campaign(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&base_options,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let executions = vec![
|
||||
metaplex_execution_json("parent.create", &summary.parent_fixture.create),
|
||||
metaplex_execution_json("parent.mint", &summary.parent_fixture.mint),
|
||||
metaplex_execution_json("attribute.create", &summary.attribute_fixture.create),
|
||||
metaplex_execution_json("attribute.mint", &summary.attribute_fixture.mint),
|
||||
metaplex_execution_json("create_escrow", &summary.create_escrow),
|
||||
metaplex_execution_json("transfer_out", &summary.transfer_out),
|
||||
metaplex_execution_json("close_escrow", &summary.close_escrow),
|
||||
];
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "Token Owned Escrow".to_string(),
|
||||
fixture: serde_json::json!({
|
||||
"parent": &summary.parent_fixture.fixture,
|
||||
"attribute": &summary.attribute_fixture.fixture,
|
||||
"escrow": &summary.escrow,
|
||||
"escrowAttributeTokenAccount": &summary.escrow_attribute_token_account,
|
||||
"setup": {
|
||||
"ataCreationConfirmed": execution_confirmation_is_confirmed(&summary.escrow_attribute_ata_creation.confirmation),
|
||||
"attributeDepositConfirmed": execution_confirmation_is_confirmed(&summary.attribute_deposit.confirmation),
|
||||
"ataMaterializations": summary.escrow_attribute_ata_creation.materializations.len(),
|
||||
"depositMaterializations": summary.attribute_deposit.materializations.len()
|
||||
}
|
||||
}),
|
||||
completed: executions.iter().all(execution_is_confirmed_json)
|
||||
&& summary.state.escrow_attribute_account_closed
|
||||
&& summary.state.escrow_account_closed
|
||||
&& summary.state.parent_token_amount_after_close == 1,
|
||||
executions,
|
||||
postconditions: escrow_state_json(&summary.state),
|
||||
})
|
||||
},
|
||||
"maintenance" => {
|
||||
let options = base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
|
||||
);
|
||||
let summary =
|
||||
match kb_pipeline_demo_scenarios::execute_devnet_metaplex_maintenance_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 executions = vec![
|
||||
metaplex_execution_json("fixture.create", &summary.fixture.create),
|
||||
metaplex_execution_json("fixture.mint", &summary.fixture.mint),
|
||||
metaplex_execution_json("update", &summary.update),
|
||||
metaplex_execution_json("resize_probe", &summary.resize_probe),
|
||||
metaplex_execution_json("migrate_probe", &summary.migrate_probe),
|
||||
metaplex_execution_json("collect_probe", &summary.collect_probe),
|
||||
metaplex_execution_json("close_accounts_probe", &summary.close_accounts_probe),
|
||||
];
|
||||
let classifications = executions
|
||||
.iter()
|
||||
.filter_map(|value| {
|
||||
return value.get("classification").and_then(serde_json::Value::as_str);
|
||||
})
|
||||
.collect::<std::vec::Vec<&str>>();
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "Maintenance — Update + probes".to_string(),
|
||||
fixture: serde_json::json!({"fixture": &summary.fixture.fixture}),
|
||||
completed: classifications
|
||||
== vec![
|
||||
"confirmed",
|
||||
"confirmed",
|
||||
"confirmed",
|
||||
"unavailable",
|
||||
"unavailable",
|
||||
"unavailable",
|
||||
"unavailable",
|
||||
]
|
||||
&& !summary.primary_sale_before_update
|
||||
&& summary.primary_sale_after_update,
|
||||
executions,
|
||||
postconditions: serde_json::json!({
|
||||
"primarySaleBeforeUpdate": summary.primary_sale_before_update,
|
||||
"primarySaleAfterUpdate": summary.primary_sale_after_update,
|
||||
"probePolicy": "simulation_only_no_submission_on_failure"
|
||||
}),
|
||||
})
|
||||
},
|
||||
"use_probe" => {
|
||||
let options = base_options.clone().with_asset_family(
|
||||
kb_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
|
||||
);
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_metaplex_use_probe(
|
||||
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 mut executions = vec![
|
||||
metaplex_execution_json("fixture.create", &summary.fixture.create),
|
||||
metaplex_execution_json("fixture.mint", &summary.fixture.mint),
|
||||
metaplex_execution_json("use_probe", &summary.simulation),
|
||||
];
|
||||
if let std::option::Option::Some(execution) = summary.execution.as_ref() {
|
||||
executions.push(metaplex_execution_json("use_execution", execution));
|
||||
}
|
||||
let status = format!("{:?}", summary.status).to_lowercase();
|
||||
std::result::Result::Ok(CampaignProjection {
|
||||
label: "Use — probe runtime".to_string(),
|
||||
fixture: serde_json::json!({"fixture": &summary.fixture.fixture}),
|
||||
completed: executions[0..2].iter().all(execution_is_confirmed_json)
|
||||
&& (status == "runtimeunavailable" || status == "confirmed"),
|
||||
executions,
|
||||
postconditions: serde_json::json!({
|
||||
"status": status,
|
||||
"usesBefore": summary.uses_before,
|
||||
"usesAfter": summary.uses_after,
|
||||
"unavailableReason": summary.unavailable_reason
|
||||
}),
|
||||
})
|
||||
},
|
||||
_ => std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"unknown qualified Metaplex desktop campaign `{campaign_id}`"
|
||||
))),
|
||||
};
|
||||
}
|
||||
|
||||
async fn execute_create_mint_projection<S, O>(
|
||||
label: &str,
|
||||
options: kb_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions,
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<CampaignProjection>
|
||||
where
|
||||
S: kb_store::RawTransactionStore
|
||||
+ kb_store::CoreExtractionStore
|
||||
+ kb_store::DecodePipelineStore
|
||||
+ Sync,
|
||||
O: kb_pipeline_demo_scenarios::SolanaExecutionObserver,
|
||||
{
|
||||
let summary = match kb_pipeline_demo_scenarios::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 executions = vec![
|
||||
metaplex_execution_json("create", &summary.create),
|
||||
metaplex_execution_json("mint", &summary.mint),
|
||||
];
|
||||
return std::result::Result::Ok(CampaignProjection {
|
||||
label: label.to_string(),
|
||||
fixture: serde_json::json!({"fixture": &summary.fixture}),
|
||||
completed: executions.iter().all(execution_is_confirmed_json)
|
||||
&& summary.token_state.supply_after_mint == summary.token_state.expected_amount_raw
|
||||
&& summary.token_state.token_amount_after_mint
|
||||
== summary.token_state.expected_amount_raw,
|
||||
executions,
|
||||
postconditions: create_mint_state_json(&summary.token_state),
|
||||
});
|
||||
}
|
||||
|
||||
fn metaplex_execution_json(
|
||||
label: &str,
|
||||
summary: &kb_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionSummary,
|
||||
) -> serde_json::Value {
|
||||
let classification = if execution_confirmation_is_confirmed(&summary.confirmation) {
|
||||
"confirmed"
|
||||
} else if !summary.simulation.success && summary.send_result.is_none() {
|
||||
"unavailable"
|
||||
} else {
|
||||
"incomplete"
|
||||
};
|
||||
return serde_json::json!({
|
||||
"label": label,
|
||||
"classification": classification,
|
||||
"profileName": &summary.profile_name,
|
||||
"cluster": format!("{:?}", summary.cluster).to_lowercase(),
|
||||
"genesisHash": &summary.genesis_hash,
|
||||
"walletPublicKey": &summary.wallet.public_key,
|
||||
"balanceLamports": summary.balance_lamports.to_string(),
|
||||
"plan": &summary.plan,
|
||||
"statefulPreflight": &summary.stateful_preflight,
|
||||
"simulation": &summary.simulation,
|
||||
"simulationContextSlot": summary.simulation_context_slot,
|
||||
"readiness": &summary.readiness,
|
||||
"sendResult": &summary.send_result,
|
||||
"confirmation": &summary.confirmation,
|
||||
"before": &summary.before,
|
||||
"after": &summary.after,
|
||||
"materializationRequested": summary.materialization_requested,
|
||||
"materializedSnapshots": &summary.materialized_snapshots,
|
||||
"instructionMaterializations": &summary.materializations,
|
||||
"postExecution": &summary.post_execution,
|
||||
"pipelineEvidence": {
|
||||
"canonicalHydration": summary.backfill.is_some(),
|
||||
"coreExtraction": summary.core_extraction.is_some(),
|
||||
"decodeReplay": summary.decode_replay.is_some(),
|
||||
"idempotenceReplay": summary.idempotence_replay.is_some()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn execution_confirmation_is_confirmed(
|
||||
confirmation: &std::option::Option<kb_lib::ExApiExecutionConfirmationResult>,
|
||||
) -> bool {
|
||||
return confirmation.as_ref().is_some_and(|value| {
|
||||
return matches!(
|
||||
value.status,
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn execution_is_confirmed_json(value: &serde_json::Value) -> bool {
|
||||
return value.get("classification").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some("confirmed");
|
||||
}
|
||||
|
||||
fn create_mint_state_json(
|
||||
state: &kb_pipeline_demo_scenarios::DevnetMetaplexCreateMintTokenState,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"expectedAmountRaw": state.expected_amount_raw,
|
||||
"supplyBeforeMint": state.supply_before_mint,
|
||||
"tokenAmountBeforeMint": state.token_amount_before_mint,
|
||||
"tokenAccountStateBeforeMint": state.token_account_state_before_mint,
|
||||
"supplyAfterMint": state.supply_after_mint,
|
||||
"tokenAmountAfterMint": state.token_amount_after_mint,
|
||||
"tokenAccountStateAfterMint": state.token_account_state_after_mint
|
||||
});
|
||||
}
|
||||
|
||||
fn print_burn_state_json(
|
||||
state: &kb_pipeline_demo_scenarios::DevnetMetaplexPrintBurnState,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"editionNumber": state.edition_number,
|
||||
"masterSupplyBeforePrint": state.master_supply_before_print,
|
||||
"masterSupplyAfterPrint": state.master_supply_after_print,
|
||||
"masterSupplyAfterBurn": state.master_supply_after_burn,
|
||||
"masterMaxSupply": state.master_max_supply,
|
||||
"editionSupplyAfterPrint": state.edition_supply_after_print,
|
||||
"editionSupplyAfterBurn": state.edition_supply_after_burn,
|
||||
"editionTokenAmountAfterPrint": state.edition_token_amount_after_print,
|
||||
"editionTokenStateAfterPrint": state.edition_token_state_after_print,
|
||||
"editionTakenAfterPrint": state.edition_taken_after_print,
|
||||
"editionMetadataExistsAfterBurn": state.edition_metadata_exists_after_burn,
|
||||
"editionMetadataFeeTombstoneAfterBurn": state.edition_metadata_fee_tombstone_after_burn,
|
||||
"editionMetadataClosedAfterBurn": state.edition_metadata_closed_after_burn,
|
||||
"editionExistsAfterBurn": state.edition_exists_after_burn,
|
||||
"editionTokenExistsAfterBurn": state.edition_token_exists_after_burn,
|
||||
"editionMarkerExistsAfterBurn": state.edition_marker_exists_after_burn
|
||||
});
|
||||
}
|
||||
|
||||
fn pnft_state_json(
|
||||
state: &kb_pipeline_demo_scenarios::DevnetMetaplexPnftLifecycleState,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"delegate": &state.delegate,
|
||||
"destinationOwner": &state.destination_owner,
|
||||
"destinationTokenAccount": &state.destination_token_account,
|
||||
"destinationTokenRecord": &state.destination_token_record,
|
||||
"initialState": &state.initial_state,
|
||||
"stakingDelegateRole": &state.staking_delegate_role,
|
||||
"lockedState": &state.locked_state,
|
||||
"unlockedState": &state.unlocked_state,
|
||||
"stakingDelegateRevoked": state.staking_delegate_revoked,
|
||||
"transferDelegateRole": &state.transfer_delegate_role,
|
||||
"sourceTokenAmountAfterTransfer": state.source_token_amount_after_transfer,
|
||||
"sourceTokenStateAfterTransfer": &state.source_token_state_after_transfer,
|
||||
"destinationTokenAmountAfterTransfer": state.destination_token_amount_after_transfer,
|
||||
"destinationTokenStateAfterTransfer": &state.destination_token_state_after_transfer,
|
||||
"destinationTokenRecordState": &state.destination_token_record_state,
|
||||
"destinationDelegateCleared": state.destination_delegate_cleared
|
||||
});
|
||||
}
|
||||
|
||||
fn escrow_state_json(
|
||||
state: &kb_pipeline_demo_scenarios::DevnetMetaplexEscrowCampaignState,
|
||||
) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"depositAmountRaw": state.deposit_amount_raw,
|
||||
"operatorAttributeAmountBeforeDeposit": state.operator_attribute_amount_before_deposit,
|
||||
"operatorAttributeAmountAfterDeposit": state.operator_attribute_amount_after_deposit,
|
||||
"escrowAttributeAmountAfterDeposit": state.escrow_attribute_amount_after_deposit,
|
||||
"operatorAttributeAmountAfterTransferOut": state.operator_attribute_amount_after_transfer_out,
|
||||
"escrowAttributeAccountClosed": state.escrow_attribute_account_closed,
|
||||
"escrowAccountClosed": state.escrow_account_closed,
|
||||
"parentTokenAmountAfterClose": state.parent_token_amount_after_close
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
@@ -865,13 +1735,16 @@ mod tests {
|
||||
let source = std::include_str!("../frontend/demo_execution_metadata.html");
|
||||
for id in [
|
||||
"metadataProfileSelect",
|
||||
"metadataDevnetScenarioSelect",
|
||||
"metadataStepSelect",
|
||||
"prepareMetadataScenarioStepButton",
|
||||
"metadataSyntheticScenarioSelect",
|
||||
"metadataMetaplexCampaignSelect",
|
||||
"metadataMetaplexCampaignOperatorConfirmed",
|
||||
"executeMetadataMetaplexCampaignButton",
|
||||
"metadataExecutionLogOutput",
|
||||
"metadataProfileHelp",
|
||||
"simulateMetadataButton",
|
||||
"submitMetadataButton",
|
||||
"metadataExecutionDetailsAccordion",
|
||||
"metadataPreflightCollapse",
|
||||
"metadataSimulationCollapse",
|
||||
"metadataPostconditionsCollapse",
|
||||
] {
|
||||
assert!(source.contains(format!("id=\"{id}\"").as_str()));
|
||||
}
|
||||
@@ -890,3 +1763,42 @@ mod tests {
|
||||
assert!(payloads.iter().any(|payload| return payload.requires_programmable_rules));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod campaign_tests {
|
||||
#[test]
|
||||
fn qualified_campaign_inventory_is_exact_and_keeps_unavailable_operations_inside_probes() {
|
||||
let campaigns = crate::demo_execution_metadata_metaplex_campaigns();
|
||||
assert_eq!(campaigns.len(), 11);
|
||||
let ids = campaigns
|
||||
.iter()
|
||||
.map(|value| return value.id.as_str())
|
||||
.collect::<std::collections::BTreeSet<&str>>();
|
||||
assert_eq!(ids.len(), campaigns.len());
|
||||
let maintenance = campaigns.iter().find(|value| return value.id == "maintenance");
|
||||
assert!(
|
||||
maintenance
|
||||
.is_some_and(|value| return value.qualification == "mixed_confirmed_unavailable")
|
||||
);
|
||||
let use_probe = campaigns.iter().find(|value| return value.id == "use_probe");
|
||||
assert!(
|
||||
use_probe
|
||||
.is_some_and(|value| return value.qualification == "runtime_unavailable_probe")
|
||||
);
|
||||
let source = std::include_str!("demo_execution_metadata_metaplex_token_metadata.rs");
|
||||
assert!(source.contains("Box::pin(execute_campaign_projection_unboxed("));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_frontend_uses_qualified_metaplex_campaign_controls() {
|
||||
let source = std::include_str!("../frontend/demo_execution_metadata.html");
|
||||
for id in [
|
||||
"metadataMetaplexCampaignSelect",
|
||||
"metadataMetaplexCampaignOutput",
|
||||
"metadataMetaplexCampaignOperatorConfirmed",
|
||||
"executeMetadataMetaplexCampaignButton",
|
||||
] {
|
||||
assert!(source.contains(format!("id=\"{id}\"").as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
224
kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs
Normal file
224
kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
// file: kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs
|
||||
// version: 1
|
||||
|
||||
//! Thin desktop adapter for the complete Token-2022 Token Metadata Devnet campaign.
|
||||
|
||||
/// Request for one complete Token-2022 Token Metadata Devnet campaign.
|
||||
#[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/DemoExecutionMetadataToken2022CampaignRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataToken2022CampaignRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub profile_name: std::string::String,
|
||||
/// Explicit authorization for fixture creation and all five metadata mutations.
|
||||
pub operator_confirmed: bool,
|
||||
}
|
||||
|
||||
/// UI-safe result of one complete Token-2022 Token Metadata Devnet campaign.
|
||||
#[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/DemoExecutionMetadataToken2022CampaignSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMetadataToken2022CampaignSummaryPayload {
|
||||
/// Profile used by the campaign.
|
||||
pub profile_name: std::string::String,
|
||||
/// Fresh Token-2022 mint containing the embedded metadata extension.
|
||||
pub mint: std::string::String,
|
||||
/// Initial update authority, equal to the selected profile wallet.
|
||||
pub initial_authority: std::string::String,
|
||||
/// Fresh authority installed by the final campaign step.
|
||||
pub final_authority: std::string::String,
|
||||
/// Confirmed signature that created the Token-2022 fixture mint.
|
||||
pub preparation_signature: std::string::String,
|
||||
/// Number of Token Metadata interface operations executed.
|
||||
pub step_count: usize,
|
||||
/// Number of operations reaching confirmed or finalized state.
|
||||
pub confirmed_step_count: usize,
|
||||
/// Total number of instruction materializations produced by the five operations.
|
||||
pub materialization_count: usize,
|
||||
/// Whether the complete five-step campaign and every postcondition succeeded.
|
||||
pub completed: bool,
|
||||
/// Complete fixture description as formatted JSON.
|
||||
pub fixture_json: std::string::String,
|
||||
/// Ordered execution and postcondition evidence as formatted JSON.
|
||||
pub steps_json: std::string::String,
|
||||
}
|
||||
|
||||
/// Executes the complete Token-2022 Token Metadata Devnet campaign.
|
||||
pub(crate) async fn demo_execution_metadata_token_2022_execute_campaign(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataToken2022CampaignRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataToken2022CampaignSummaryPayload,
|
||||
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);
|
||||
if !request.operator_confirmed {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 Token Metadata campaign requires explicit operator confirmation"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
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 store = match crate::connect_postgres_store(&profile).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let workspace_root = crate::workspace_root_dir();
|
||||
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 =
|
||||
kb_pipeline_demo_scenarios::Token2022MetadataFixturePreparationOptions::new(wallet_dir);
|
||||
options.operator_confirmed = true;
|
||||
let observer = crate::DemoExecutionMetadataObserver {
|
||||
app_handle,
|
||||
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
||||
};
|
||||
let summary = match kb_pipeline_demo_scenarios::execute_devnet_token_2022_metadata_campaign(
|
||||
&http_pool,
|
||||
&store,
|
||||
&profile,
|
||||
workspace_root.as_path(),
|
||||
&options,
|
||||
&observer,
|
||||
)
|
||||
.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(campaign_summary_payload(request.profile_name, summary));
|
||||
}
|
||||
|
||||
fn campaign_summary_payload(
|
||||
profile_name: std::string::String,
|
||||
summary: kb_pipeline_demo_scenarios::DevnetToken2022MetadataCampaignSummary,
|
||||
) -> crate::DemoExecutionMetadataToken2022CampaignSummaryPayload {
|
||||
let mut confirmed_step_count = 0_usize;
|
||||
let mut materialization_count = 0_usize;
|
||||
let mut step_values = std::vec::Vec::with_capacity(summary.steps.len());
|
||||
for step in &summary.steps {
|
||||
let confirmed = match step.execution.confirmation.as_ref() {
|
||||
std::option::Option::Some(value) => matches!(
|
||||
value.status,
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||
),
|
||||
std::option::Option::None => false,
|
||||
};
|
||||
if confirmed {
|
||||
confirmed_step_count = confirmed_step_count.saturating_add(1);
|
||||
}
|
||||
materialization_count =
|
||||
materialization_count.saturating_add(step.execution.materializations.len());
|
||||
step_values.push(serde_json::json!({
|
||||
"stepId": &step.step_id,
|
||||
"operationCode": &step.operation_code,
|
||||
"simulation": &step.execution.simulation,
|
||||
"sendResult": &step.execution.send_result,
|
||||
"confirmation": &step.execution.confirmation,
|
||||
"statefulPreflight": &step.execution.stateful_preflight,
|
||||
"statefulSnapshot": &step.stateful_snapshot,
|
||||
"emitEvidence": &step.emit_evidence,
|
||||
"postcondition": &step.postcondition,
|
||||
"postExecution": &step.execution.post_execution,
|
||||
"materializations": &step.execution.materializations
|
||||
}));
|
||||
}
|
||||
let expected_step_count =
|
||||
kb_pipeline_demo_scenarios::token_2022_metadata_campaign_operation_names().len();
|
||||
let completed = summary.steps.len() == expected_step_count
|
||||
&& confirmed_step_count == expected_step_count
|
||||
&& summary.steps.iter().all(|step| {
|
||||
return step.postcondition.status
|
||||
== kb_pipeline::Token2022ExecutionPostconditionStatus::Confirmed;
|
||||
});
|
||||
let fixture_json = crate::pretty_json(&serde_json::json!({
|
||||
"mint": &summary.fixture.mint,
|
||||
"initialAuthority": &summary.fixture.initial_authority,
|
||||
"finalAuthority": &summary.fixture.final_authority,
|
||||
"mintSpace": summary.fixture.mint_space,
|
||||
"rentBudgetSpace": summary.fixture.rent_budget_space,
|
||||
"rentReserveLamports": summary.fixture.rent_reserve_lamports.to_string(),
|
||||
"preparationSignature": &summary.fixture.preparation_signature,
|
||||
"initialSnapshot": &summary.fixture.initial_snapshot
|
||||
}));
|
||||
return crate::DemoExecutionMetadataToken2022CampaignSummaryPayload {
|
||||
profile_name,
|
||||
mint: summary.fixture.mint.0.clone(),
|
||||
initial_authority: summary.fixture.initial_authority.0.clone(),
|
||||
final_authority: summary.fixture.final_authority.0.clone(),
|
||||
preparation_signature: summary.fixture.preparation_signature.clone(),
|
||||
step_count: summary.steps.len(),
|
||||
confirmed_step_count,
|
||||
materialization_count,
|
||||
completed,
|
||||
fixture_json,
|
||||
steps_json: crate::pretty_json(&step_values),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn desktop_campaign_exposes_the_exact_five_token_metadata_operations() {
|
||||
assert_eq!(
|
||||
kb_pipeline_demo_scenarios::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 metadata_frontend_contains_token_2022_campaign_controls() {
|
||||
let source = std::include_str!("../frontend/demo_execution_metadata.html");
|
||||
for id in [
|
||||
"token2022MetadataProfileSelect",
|
||||
"token2022MetadataCampaignOutput",
|
||||
"token2022MetadataOperatorConfirmed",
|
||||
"executeToken2022MetadataCampaignButton",
|
||||
] {
|
||||
assert!(source.contains(format!("id=\"{id}\"").as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/lib.rs
|
||||
// version: 31
|
||||
// version: 34
|
||||
|
||||
//! Tauri desktop demo application for `khadhroony-bot3`.
|
||||
|
||||
@@ -17,6 +17,7 @@ mod demo_devnet_common;
|
||||
mod demo_execution_metadata;
|
||||
mod demo_execution_metadata_metaplex_token_metadata;
|
||||
mod demo_execution_metadata_solana_program;
|
||||
mod demo_execution_metadata_token_2022;
|
||||
mod demo_execution_solana_core;
|
||||
mod demo_execution_spl;
|
||||
mod demo_http;
|
||||
@@ -137,6 +138,12 @@ pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataObserver;
|
||||
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataProgressPayload;
|
||||
/// Returns Devnet profile options shared by the Metadata execution panel.
|
||||
pub(crate) use self::demo_execution_metadata::demo_execution_metadata_options;
|
||||
/// UI-safe descriptor for one qualified Metaplex Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexCampaignOptionPayload;
|
||||
/// Request for one complete qualified Metaplex Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexCampaignRequest;
|
||||
/// UI-safe result of one complete qualified Metaplex Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexCampaignSummaryPayload;
|
||||
/// UI-safe result of one prepared Metaplex Token Metadata Create fixture.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexTokenMetadataCreateFixturePayload;
|
||||
/// Prepared coherent Metaplex Token Metadata scenario step.
|
||||
@@ -147,6 +154,10 @@ pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecut
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload;
|
||||
/// UI-safe result of one real Devnet Metaplex Token Metadata execution.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload;
|
||||
/// Returns the qualified Metaplex Devnet campaign inventory.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::demo_execution_metadata_metaplex_campaigns;
|
||||
/// Executes one complete qualified Metaplex Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::demo_execution_metadata_metaplex_execute_campaign;
|
||||
/// Returns current Metaplex Token Metadata operation names accepted by Devnet campaigns.
|
||||
pub(crate) use self::demo_execution_metadata_metaplex_token_metadata::demo_execution_metadata_metaplex_token_metadata_current_operations;
|
||||
/// Executes one real Devnet Metaplex Token Metadata simulation or submission.
|
||||
@@ -171,6 +182,12 @@ pub(crate) use self::demo_execution_metadata_solana_program::DemoExecutionMetada
|
||||
pub(crate) use self::demo_execution_metadata_solana_program::demo_execution_metadata_solana_program_execute_campaign;
|
||||
/// Returns the two canonical Solana Program Metadata Devnet journeys.
|
||||
pub(crate) use self::demo_execution_metadata_solana_program::demo_execution_metadata_solana_program_scenarios;
|
||||
/// Request for the complete Token-2022 Token Metadata Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_token_2022::DemoExecutionMetadataToken2022CampaignRequest;
|
||||
/// UI-safe result of the complete Token-2022 Token Metadata Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_token_2022::DemoExecutionMetadataToken2022CampaignSummaryPayload;
|
||||
/// Executes the complete Token-2022 Token Metadata Devnet campaign.
|
||||
pub(crate) use self::demo_execution_metadata_token_2022::demo_execution_metadata_token_2022_execute_campaign;
|
||||
/// Request sent by the Memo v4 Devnet execution panel.
|
||||
pub(crate) use self::demo_execution_solana_core::DemoExecutionMemoRequest;
|
||||
/// UI-safe result of one Memo v4 Devnet execution orchestration.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/tauri.rs
|
||||
// version: 32
|
||||
// version: 34
|
||||
|
||||
//! Tauri runtime assembly and private command wrappers.
|
||||
|
||||
@@ -90,6 +90,8 @@ pub fn run() -> kb_core::Result<()> {
|
||||
demo_spl_token_2022_fixture,
|
||||
open_demo_execution_metadata_window,
|
||||
demo_execution_metadata_metaplex_token_metadata_scenarios,
|
||||
demo_execution_metadata_metaplex_campaigns,
|
||||
demo_execution_metadata_metaplex_execute_campaign,
|
||||
demo_execution_metadata_options,
|
||||
demo_execution_metadata_metaplex_token_metadata_current_operations,
|
||||
demo_execution_metadata_metaplex_token_metadata_operation_template,
|
||||
@@ -98,6 +100,7 @@ pub fn run() -> kb_core::Result<()> {
|
||||
demo_execution_metadata_metaplex_token_metadata_execute,
|
||||
demo_execution_metadata_solana_program_scenarios,
|
||||
demo_execution_metadata_solana_program_execute_campaign,
|
||||
demo_execution_metadata_token_2022_execute_campaign,
|
||||
]);
|
||||
builder = builder.on_window_event(|window, event| {
|
||||
if window.label() != "main" || !matches!(event, tauri::WindowEvent::Destroyed) {
|
||||
@@ -638,6 +641,25 @@ fn demo_execution_metadata_metaplex_token_metadata_scenarios() -> std::result::R
|
||||
return crate::demo_execution_metadata_metaplex_token_metadata_scenarios();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_metaplex_campaigns()
|
||||
-> std::vec::Vec<crate::DemoExecutionMetadataMetaplexCampaignOptionPayload> {
|
||||
return crate::demo_execution_metadata_metaplex_campaigns();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_metadata_metaplex_execute_campaign(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataMetaplexCampaignRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataMetaplexCampaignSummaryPayload,
|
||||
std::string::String,
|
||||
> {
|
||||
return crate::demo_execution_metadata_metaplex_execute_campaign(app_handle, state, request)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn demo_execution_metadata_metaplex_token_metadata_current_operations()
|
||||
-> std::vec::Vec<std::string::String> {
|
||||
@@ -723,6 +745,19 @@ async fn demo_execution_metadata_solana_program_execute_campaign(
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_metadata_token_2022_execute_campaign(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoExecutionMetadataToken2022CampaignRequest,
|
||||
) -> std::result::Result<
|
||||
crate::DemoExecutionMetadataToken2022CampaignSummaryPayload,
|
||||
std::string::String,
|
||||
> {
|
||||
return crate::demo_execution_metadata_token_2022_execute_campaign(app_handle, state, request)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_execution_spl_token_execute(
|
||||
app_handle: tauri::AppHandle,
|
||||
|
||||
Reference in New Issue
Block a user