v0.5.2-pre.006-fix-010

This commit is contained in:
2026-08-11 12:03:35 +02:00
parent 1ee1d0297d
commit 56572cec40
67 changed files with 2484 additions and 539 deletions

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/app_state.rs
// version: 16
// version: 17
//! Shared Tauri application state and startup initialization.
@@ -30,6 +30,8 @@ pub(crate) struct AppState {
demo_decode_replay_campaign_id: std::sync::Mutex<std::option::Option<std::string::String>>,
demo_execution_solana_core_running: std::sync::atomic::AtomicBool,
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool,
demo_execution_wallet_alias_overrides:
std::sync::Mutex<std::collections::BTreeMap<std::string::String, std::string::String>>,
startup_sequence_started: std::sync::atomic::AtomicBool,
}
@@ -115,6 +117,9 @@ impl crate::AppState {
demo_decode_replay_campaign_id: std::sync::Mutex::new(std::option::Option::None),
demo_execution_solana_core_running: std::sync::atomic::AtomicBool::new(false),
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool::new(false),
demo_execution_wallet_alias_overrides: std::sync::Mutex::new(
std::collections::BTreeMap::new(),
),
startup_sequence_started: std::sync::atomic::AtomicBool::new(false),
});
}
@@ -169,6 +174,46 @@ impl crate::AppState {
return &self.active_profile;
}
/// Returns the session-only execution-wallet alias override for one profile.
pub(crate) fn demo_execution_wallet_alias_override(
&self,
profile_name: &str,
) -> std::result::Result<std::option::Option<std::string::String>, std::string::String> {
let lock_result = self.demo_execution_wallet_alias_overrides.lock();
let guard = match lock_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
"demo execution wallet selection lock is poisoned".to_string(),
);
},
};
return std::result::Result::Ok(guard.get(profile_name).cloned());
}
/// Sets or clears the session-only execution-wallet alias override for one profile.
pub(crate) fn set_demo_execution_wallet_alias_override(
&self,
profile_name: std::string::String,
alias: std::option::Option<std::string::String>,
) -> std::result::Result<(), std::string::String> {
let lock_result = self.demo_execution_wallet_alias_overrides.lock();
let mut guard = match lock_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
"demo execution wallet selection lock is poisoned".to_string(),
);
},
};
if let std::option::Option::Some(alias) = alias {
guard.insert(profile_name, alias);
} else {
guard.remove(profile_name.as_str());
}
return std::result::Result::Ok(());
}
/// Returns the configured HTTP endpoint pool.
pub(crate) fn http_pool(&self) -> &ks_onchain_transport::HttpEndpointPool {
return &self.http_pool;

View File

@@ -1,10 +1,12 @@
// file: kb-app-demo-desktop/src/demo_devnet_common.rs
// version: 4
// version: 6
//! Shared Devnet demo UI contracts.
use ts_rs::TS; // rust-rules: derive-import
const DEMO_WALLET_PASSWORD_ENV: &str = "KB_SECRET_DEMO_WALLET_PASSWORD";
/// Readiness report for one Devnet profile PostgreSQL store.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -73,3 +75,58 @@ fn bounded_u32(value: usize) -> u32 {
std::result::Result::Err(_) => u32::MAX,
};
}
/// Returns whether the backend-only demo wallet password is configured.
pub(crate) fn demo_wallet_password_configured() -> bool {
return std::env::var_os(DEMO_WALLET_PASSWORD_ENV).is_some();
}
/// Loads the backend-only demo wallet password without exposing it to Tauri DTOs.
pub(crate) fn demo_wallet_password()
-> std::result::Result<ks_wallet::WalletPassword, std::string::String> {
let password_text = match std::env::var(DEMO_WALLET_PASSWORD_ENV) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(format!(
"backend secret {DEMO_WALLET_PASSWORD_ENV} is not configured as UTF-8"
));
},
};
return match ks_wallet::WalletPassword::new(password_text) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
/// Resolves the temporary or persistent signer selected by one Devnet profile.
pub(crate) async fn resolve_devnet_execution_wallet(
state: &crate::AppState,
profile: &ks_config::ProfileConfig,
) -> std::result::Result<ks_pipeline_demo_scenarios::DevnetExecutionWallet, std::string::String> {
let alias_override = match state.demo_execution_wallet_alias_override(profile.name.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut effective_profile = profile.clone();
if let std::option::Option::Some(alias) = alias_override {
effective_profile.wallet.wallet_alias = std::option::Option::Some(alias);
}
let password = if effective_profile.wallet.wallet_alias.is_some() {
match demo_wallet_password() {
std::result::Result::Ok(value) => std::option::Option::Some(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
} else {
std::option::Option::None
};
return match ks_pipeline_demo_scenarios::resolve_devnet_execution_wallet(
&effective_profile,
crate::workspace_root_dir().as_path(),
password,
)
.await
{
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs
// version: 8
// version: 11
//! Desktop adapters for generic and qualified Metaplex Token Metadata execution workflows.
@@ -235,22 +235,26 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_token_metadata(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
{
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_token_metadata_with_wallet(
&http_pool,
&store,
&profile,
&execution_wallet,
&execution_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let transaction_signature =
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
let confirmation_status =
@@ -417,7 +421,7 @@ async fn prepare_create_fixture_for_family(
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 configured = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let wallet_dir = if configured.is_absolute() {
configured
} else {
@@ -1048,7 +1052,8 @@ pub(crate) async fn demo_execution_metadata_metaplex_execute_campaign(
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 configured_wallet_dir =
std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
@@ -1070,8 +1075,8 @@ pub(crate) async fn demo_execution_metadata_metaplex_execute_campaign(
wallet_dir,
&http_pool,
&store,
&state,
&profile,
workspace_root.as_path(),
&observer,
)
.await
@@ -1133,8 +1138,8 @@ fn execute_campaign_projection<'a, S, O>(
wallet_dir: std::path::PathBuf,
http_pool: &'a ks_onchain_transport::HttpEndpointPool,
store: &'a S,
state: &'a crate::AppState,
profile: &'a ks_config::ProfileConfig,
workspace_root: &'a std::path::Path,
observer: &'a O,
) -> CampaignProjectionFuture<'a>
where
@@ -1150,8 +1155,8 @@ where
wallet_dir,
http_pool,
store,
state,
profile,
workspace_root,
observer,
));
}
@@ -1161,8 +1166,8 @@ async fn execute_campaign_projection_unboxed<S, O>(
wallet_dir: std::path::PathBuf,
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
state: &crate::AppState,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
observer: &O,
) -> ks_core::Result<CampaignProjection>
where
@@ -1174,6 +1179,15 @@ where
{
let base_options =
ks_pipeline_demo_scenarios::MetaplexCreateFixturePreparationOptions::new(wallet_dir);
let execution_wallet = match crate::resolve_devnet_execution_wallet(state, profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"demo_wallet_resolution_failed",
error,
));
},
};
return match campaign_id {
"create_mint_nft" => {
execute_create_mint_projection(
@@ -1184,7 +1198,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1198,7 +1212,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1212,7 +1226,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1226,7 +1240,7 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
@@ -1240,17 +1254,17 @@ where
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
observer,
)
.await
},
"collection_verify" => {
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_collection_verify_campaign(
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_collection_verify_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&base_options,
observer,
).await {
@@ -1285,11 +1299,11 @@ where
},
"print_burn" => {
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_print_burn_campaign(
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_print_burn_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&base_options,
observer,
)
@@ -1327,11 +1341,11 @@ where
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::ProgrammableNft,
);
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_pnft_lifecycle_campaign(
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_pnft_lifecycle_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&options,
observer,
)
@@ -1361,11 +1375,11 @@ where
})
},
"escrow" => {
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_escrow_campaign(
let summary = match ks_pipeline_demo_scenarios::execute_devnet_metaplex_escrow_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&base_options,
observer,
)
@@ -1410,11 +1424,11 @@ where
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
);
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_maintenance_campaign(
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_maintenance_campaign_with_wallet(
http_pool,
store,
profile,
workspace_root,
&execution_wallet,
&options,
observer,
)
@@ -1465,19 +1479,20 @@ where
let options = base_options.clone().with_asset_family(
ks_pipeline_demo_scenarios::MetaplexTokenMetadataAssetFamily::Nft,
);
let summary = match ks_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 summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_use_probe_with_wallet(
http_pool,
store,
profile,
&execution_wallet,
&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),
@@ -1513,7 +1528,7 @@ async fn execute_create_mint_projection<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
wallet: &ks_pipeline_demo_scenarios::DevnetExecutionWallet,
observer: &O,
) -> ks_core::Result<CampaignProjection>
where
@@ -1523,19 +1538,15 @@ where
+ Sync,
O: ks_pipeline_demo_scenarios::SolanaExecutionObserver,
{
let summary = match ks_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 summary =
match ks_pipeline_demo_scenarios::execute_devnet_metaplex_create_mint_campaign_with_wallet(
http_pool, store, profile, wallet, &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),

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_metadata_solana_program.rs
// version: 3
// version: 6
//! Thin desktop adapter for complete Solana Program Metadata Devnet campaigns.
@@ -170,11 +170,14 @@ pub(crate) async fn demo_execution_metadata_solana_program_execute_campaign(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_solana_program_metadata_campaign(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_solana_program_metadata_campaign_with_wallet(
&http_pool,
&profile,
workspace_root.as_path(),
&execution_wallet,
&options,
&observer,
)

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs
// version: 2
// version: 5
//! Thin desktop adapter for the complete Token-2022 Token Metadata Devnet campaign.
@@ -96,7 +96,8 @@ pub(crate) async fn demo_execution_metadata_token_2022_execute_campaign(
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 configured_wallet_dir =
std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let wallet_dir = if configured_wallet_dir.is_absolute() {
configured_wallet_dir
} else {
@@ -109,19 +110,24 @@ pub(crate) async fn demo_execution_metadata_token_2022_execute_campaign(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_token_2022_metadata_campaign(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&options,
&observer,
)
.await
{
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_token_2022_metadata_campaign_with_wallet(
&http_pool,
&store,
&profile,
&execution_wallet,
&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));
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_solana_core.rs
// version: 17
// version: 20
//! Tauri adapter for bounded Solana Core execution on Devnet.
@@ -509,12 +509,15 @@ pub(crate) async fn demo_execution_solana_core_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_system_transfer(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_system_transfer_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
@@ -582,12 +585,15 @@ pub(crate) async fn demo_execution_spl_memo_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_memo(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_memo_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_spl_ata.rs
// version: 13
// version: 16
//! Thin Tauri adapter for ATA execution, derivation and lifecycle journal reads.
@@ -279,21 +279,26 @@ pub(crate) async fn demo_execution_spl_ata_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_associated_token_account(
&http_pool,
&store,
&profile,
crate::workspace_root_dir().as_path(),
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
{
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary =
match ks_pipeline_demo_scenarios::execute_devnet_spl_associated_token_account_with_wallet(
&http_pool,
&store,
&profile,
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
&observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
return std::result::Result::Ok(summary_payload(summary));
}
@@ -346,7 +351,7 @@ pub(crate) async fn demo_spl_ata_journal(
async fn load_profile_wallet(
profile: &ks_config::ProfileConfig,
) -> std::result::Result<ks_wallet::TemporaryWallet, std::string::String> {
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
let configured = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let directory = if configured.is_absolute() {
configured
} else {

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_spl_token.rs
// version: 10
// version: 13
//! Thin Tauri adapter for classic SPL Token execution and materialized journals.
@@ -223,12 +223,15 @@ pub(crate) async fn demo_execution_spl_token_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_spl_token_2022.rs
// version: 15
// version: 18
//! Thin Tauri adapter for independent public Token-2022 Devnet validation scenarios.
@@ -192,12 +192,15 @@ pub(crate) async fn demo_execution_spl_token_2022_execute(
app_handle,
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
};
let workspace_root = crate::workspace_root_dir();
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token_2022(
let execution_wallet = match crate::resolve_devnet_execution_wallet(&state, &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token_2022_with_wallet(
&http_pool,
&store,
&profile,
workspace_root.as_path(),
&execution_wallet,
&pipeline_request,
decoders.as_slice(),
materializers.as_slice(),
@@ -220,7 +223,7 @@ pub(crate) fn demo_spl_token_2022_fixture(
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 configured = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str());
let wallet_dir = if configured.is_absolute() {
configured
} else {

View File

@@ -1,11 +1,12 @@
// file: kb-app-demo-desktop/src/demo_wallet.rs
// version: 5
// version: 7
//! UI-safe wallet management and public on-chain exploration.
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
use ts_rs::TS; // rust-rules: derive-import
const DEMO_WALLET_PASSWORD_ENV: &str = "KB_SECRET_DEMO_WALLET_PASSWORD";
const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
/// UI-safe inventory of native wallets visible to the active profile.
@@ -21,6 +22,10 @@ pub(crate) struct DemoWalletInventoryPayload {
pub(crate) cluster: std::string::String,
/// Profiles available for explicit read-only on-chain exploration.
pub(crate) onchain_profiles: std::vec::Vec<DemoWalletOnchainProfilePayload>,
/// Devnet profiles and their session/config execution-wallet selections.
pub(crate) execution_profiles: std::vec::Vec<DemoWalletExecutionProfilePayload>,
/// Workspace-relative directory used for secret exports from this demo.
pub(crate) export_directory: std::string::String,
/// Secret transfer formats supported by the reusable wallet boundary.
pub(crate) transfer_formats: std::vec::Vec<DemoWalletTransferFormatPayload>,
/// Canonical classic SPL Token program identifier used by the explorer.
@@ -119,7 +124,7 @@ pub(crate) struct DemoWalletImportRequest {
pub(crate) format: std::string::String,
}
/// Request to export one native wallet to an explicit external file.
/// Request to export one native wallet to the application-owned export directory.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
export,
@@ -128,12 +133,42 @@ pub(crate) struct DemoWalletImportRequest {
pub(crate) struct DemoWalletExportRequest {
/// Alias of the native wallet to export.
pub(crate) alias: std::string::String,
/// Explicit destination path created without overwrite.
pub(crate) destination_path: std::string::String,
/// File name created inside the application-owned wallet export directory.
pub(crate) file_name: std::string::String,
/// Stable transfer format code selected by the operator.
pub(crate) format: std::string::String,
}
/// One Devnet profile and its effective desktop-session execution-wallet selection.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExecutionProfilePayload.ts"
)]
pub(crate) struct DemoWalletExecutionProfilePayload {
/// Resolved Devnet profile name.
pub(crate) name: std::string::String,
/// Alias persisted in wallet configuration, when present.
pub(crate) configured_alias: std::option::Option<std::string::String>,
/// Session-only desktop override, when present.
pub(crate) runtime_alias: std::option::Option<std::string::String>,
/// Effective alias used by execution demos, or `None` for the temporary fallback.
pub(crate) effective_alias: std::option::Option<std::string::String>,
}
/// Request to set or clear one session-only execution-wallet selection.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExecutionSelectionRequest.ts"
)]
pub(crate) struct DemoWalletExecutionSelectionRequest {
/// Devnet profile whose execution wallet is selected.
pub(crate) profile: std::string::String,
/// Native wallet alias, or `None` to return to profile configuration.
pub(crate) alias: std::option::Option<std::string::String>,
}
/// Exact SOL balance returned for one public wallet address.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
@@ -226,19 +261,87 @@ pub(crate) async fn demo_wallet_inventory(
for handle in handles {
wallets.push(wallet_identity_payload(&handle, selected_alias.as_deref()));
}
let execution_profiles = match execution_profile_payloads(state) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(DemoWalletInventoryPayload {
profile: profile.name.clone(),
cluster: profile.wallet.cluster.clone(),
onchain_profiles: onchain_profile_payloads(state),
execution_profiles,
export_directory: "data/wallets".to_string(),
transfer_formats: wallet_transfer_format_payloads(),
token_program_id: ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
token_2022_program_id: ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(),
create_password_configured: std::env::var_os(DEMO_WALLET_PASSWORD_ENV).is_some(),
create_password_configured: crate::demo_wallet_password_configured(),
selected_alias,
wallets,
});
}
/// Sets or clears the session-only native wallet used by Devnet execution demos.
pub(crate) async fn demo_wallet_select_execution_wallet(
state: &crate::AppState,
request: crate::DemoWalletExecutionSelectionRequest,
) -> std::result::Result<DemoWalletInventoryPayload, std::string::String> {
let profile = match devnet_execution_profile(state, request.profile.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile_name = profile.name.clone();
if let std::option::Option::Some(alias_text) = request.alias {
let alias = match ks_wallet::WalletAlias::parse(alias_text.trim().to_string()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let password = match crate::demo_wallet_password() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let manager = match wallet_manager_for_profile(profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallet = match manager.unlock(&alias, password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let public_key = wallet.public_key();
wallet.lock();
if let std::result::Result::Err(error) = state.set_demo_execution_wallet_alias_override(
profile_name.clone(),
std::option::Option::Some(alias.as_str().to_string()),
) {
return std::result::Result::Err(error);
}
tracing::info!(
target: crate::TRACING_TARGET,
action = "select_demo_execution_wallet",
profile = profile_name.as_str(),
wallet_alias = alias.as_str(),
public_key = %public_key,
selection_source = "runtime",
"selected authenticated native wallet for desktop execution demos"
);
} else {
if let std::result::Result::Err(error) = state.set_demo_execution_wallet_alias_override(
profile_name.clone(),
std::option::Option::None,
) {
return std::result::Result::Err(error);
}
tracing::info!(
target: crate::TRACING_TARGET,
action = "select_demo_execution_wallet",
profile = profile_name.as_str(),
selection_source = "configuration",
"cleared desktop execution-wallet override"
);
}
return demo_wallet_inventory(state).await;
}
/// Creates one password-protected native wallet without sending the password through Tauri IPC.
pub(crate) async fn demo_wallet_create_native(
state: &crate::AppState,
@@ -248,7 +351,7 @@ pub(crate) async fn demo_wallet_create_native(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let password = match demo_wallet_password() {
let password = match crate::demo_wallet_password() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -309,7 +412,7 @@ pub(crate) async fn demo_wallet_import_file(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = match demo_wallet_password() {
let password = match crate::demo_wallet_password() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -341,7 +444,7 @@ pub(crate) async fn demo_wallet_import_file(
pub(crate) async fn demo_wallet_export_file(
state: &crate::AppState,
request: crate::DemoWalletExportRequest,
) -> std::result::Result<(), std::string::String> {
) -> std::result::Result<std::string::String, std::string::String> {
let alias = match ks_wallet::WalletAlias::parse(request.alias.trim().to_string()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
@@ -350,7 +453,15 @@ pub(crate) async fn demo_wallet_export_file(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = match demo_wallet_password() {
let file_name = match validated_wallet_export_file_name(request.file_name.as_str(), format) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let export_directory = match prepare_wallet_export_directory().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = match crate::demo_wallet_password() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -358,13 +469,21 @@ pub(crate) async fn demo_wallet_export_file(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match manager
.export_file(&alias, password, std::path::PathBuf::from(request.destination_path), format)
.await
let destination = export_directory.join(file_name.as_str());
if let std::result::Result::Err(error) =
manager.export_file(&alias, password, destination, format).await
{
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
return std::result::Result::Err(error.to_string());
}
tracing::info!(
target: crate::TRACING_TARGET,
action = "export_wallet_secret",
wallet_alias = alias.as_str(),
transfer_format = format.code(),
export_file_name = file_name.as_str(),
"exported native wallet secret to application-owned data directory"
);
return std::result::Result::Ok(format!("data/wallets/{file_name}"));
}
/// Inspects one explicitly selected native wallet file and returns only its safe identity.
@@ -489,21 +608,6 @@ pub(crate) async fn demo_wallet_rpc_execute(
});
}
fn demo_wallet_password() -> std::result::Result<ks_wallet::WalletPassword, std::string::String> {
let password_text = match std::env::var(DEMO_WALLET_PASSWORD_ENV) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(format!(
"backend secret {DEMO_WALLET_PASSWORD_ENV} is not configured as UTF-8"
));
},
};
return match ks_wallet::WalletPassword::new(password_text) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
fn wallet_transfer_format_payloads() -> std::vec::Vec<crate::DemoWalletTransferFormatPayload> {
let mut formats = std::vec::Vec::new();
for format in ks_wallet::WalletTransferFormat::supported() {
@@ -528,6 +632,127 @@ fn wallet_transfer_format_from_code(
return std::result::Result::Err(format!("wallet transfer format '{code}' is not supported"));
}
fn execution_profile_payloads(
state: &crate::AppState,
) -> std::result::Result<std::vec::Vec<crate::DemoWalletExecutionProfilePayload>, std::string::String>
{
let mut profiles = std::vec::Vec::new();
for profile in &state.app_config().profiles {
if profile.wallet.cluster != "devnet" {
continue;
}
let runtime_alias = match state.demo_execution_wallet_alias_override(profile.name.as_str())
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let effective_alias = match runtime_alias.as_ref() {
std::option::Option::Some(value) => std::option::Option::Some(value.clone()),
std::option::Option::None => profile.wallet.wallet_alias.clone(),
};
profiles.push(crate::DemoWalletExecutionProfilePayload {
name: profile.name.clone(),
configured_alias: profile.wallet.wallet_alias.clone(),
runtime_alias,
effective_alias,
});
}
return std::result::Result::Ok(profiles);
}
fn devnet_execution_profile<'a>(
state: &'a crate::AppState,
profile_name: &str,
) -> std::result::Result<&'a ks_config::ProfileConfig, std::string::String> {
let profile = match onchain_profile(state, profile_name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if profile.wallet.cluster != "devnet" {
return std::result::Result::Err(
"wallet execution selection requires a Devnet profile".to_string(),
);
}
return std::result::Result::Ok(profile);
}
fn validated_wallet_export_file_name(
file_name: &str,
format: ks_wallet::WalletTransferFormat,
) -> std::result::Result<std::string::String, std::string::String> {
let file_name = file_name.trim();
if file_name.is_empty() || file_name.len() > 128 {
return std::result::Result::Err(
"wallet export file name must contain between 1 and 128 bytes".to_string(),
);
}
if file_name == "." || file_name == ".." || file_name.starts_with('.') {
return std::result::Result::Err("wallet export file name is invalid".to_string());
}
if !file_name.chars().all(|value| {
return value.is_ascii_alphanumeric() || value == '-' || value == '_' || value == '.';
}) {
return std::result::Result::Err(
"wallet export file name contains unsupported characters".to_string(),
);
}
let expected_extension = format.default_extension();
let path = std::path::Path::new(file_name);
if path.extension().is_none() {
return std::result::Result::Ok(format!("{file_name}.{expected_extension}"));
}
if path.extension().and_then(std::ffi::OsStr::to_str)
!= std::option::Option::Some(expected_extension)
{
return std::result::Result::Err(format!(
"wallet export file extension must be '.{expected_extension}' for format '{}'",
format.code()
));
}
return std::result::Result::Ok(file_name.to_string());
}
async fn prepare_wallet_export_directory()
-> std::result::Result<std::path::PathBuf, std::string::String> {
let data_directory = crate::workspace_root_dir().join("data");
if let std::result::Result::Ok(metadata) = tokio::fs::symlink_metadata(&data_directory).await {
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return std::result::Result::Err(
"wallet export data directory must be a regular directory".to_string(),
);
}
}
if let std::result::Result::Err(error) = tokio::fs::create_dir_all(&data_directory).await {
return std::result::Result::Err(format!(
"cannot create wallet export data directory: {error}"
));
}
let export_directory = data_directory.join("wallets");
if let std::result::Result::Ok(metadata) = tokio::fs::symlink_metadata(&export_directory).await
{
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return std::result::Result::Err(
"wallet export directory must be a regular directory".to_string(),
);
}
}
if let std::result::Result::Err(error) = tokio::fs::create_dir_all(&export_directory).await {
return std::result::Result::Err(format!("cannot create wallet export directory: {error}"));
}
#[cfg(unix)]
{
let permissions = std::fs::Permissions::from_mode(0o700);
if let std::result::Result::Err(error) =
tokio::fs::set_permissions(&export_directory, permissions).await
{
return std::result::Result::Err(format!(
"cannot secure wallet export directory permissions: {error}"
));
}
}
return std::result::Result::Ok(export_directory);
}
fn onchain_profile<'a>(
state: &'a crate::AppState,
profile_name: &str,
@@ -676,14 +901,19 @@ mod tests {
"importWalletTransferButton",
"walletExportAliasSelect",
"walletExportFormatSelect",
"walletExportDestinationPathInput",
"walletExportFileNameInput",
"walletExportDirectory",
"exportWalletTransferButton",
"walletExecutionProfileSelect",
"walletExecutionAliasSelect",
"applyWalletExecutionSelectionButton",
"walletExternalPathInput",
"inspectWalletFileButton",
"walletInspectionJson",
] {
assert!(html.contains(required));
}
assert!(script.contains("demo_wallet_select_execution_wallet"));
assert!(script.contains("demo_wallet_create_native"));
assert!(script.contains("demo_wallet_inspect_transfer_file"));
assert!(script.contains("demo_wallet_import_file"));
@@ -700,6 +930,7 @@ mod tests {
assert!(main_html.contains("openDemoWalletLink"));
assert!(main_script.contains("open_demo_wallet_window"));
assert!(tauri_runtime.contains("open_demo_wallet_window"));
assert!(tauri_runtime.contains("demo_wallet_select_execution_wallet"));
assert!(tauri_runtime.contains("demo_wallet_create_native"));
assert!(tauri_runtime.contains("demo_wallet_inspect_transfer_file"));
assert!(tauri_runtime.contains("demo_wallet_import_file"));
@@ -721,6 +952,13 @@ mod tests {
cluster: "devnet".to_string(),
active: false,
}],
execution_profiles: std::vec![super::DemoWalletExecutionProfilePayload {
name: "local_devnet".to_string(),
configured_alias: std::option::Option::None,
runtime_alias: std::option::Option::Some("selected-wallet".to_string()),
effective_alias: std::option::Option::Some("selected-wallet".to_string()),
}],
export_directory: "data/wallets".to_string(),
transfer_formats: super::wallet_transfer_format_payloads(),
token_program_id: ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
token_2022_program_id: ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(),
@@ -752,6 +990,34 @@ mod tests {
}
}
#[test]
fn wallet_export_file_name_is_bounded_and_format_specific() {
let json = super::validated_wallet_export_file_name(
"operator",
ks_wallet::WalletTransferFormat::SolanaCliJson,
);
assert_eq!(json, std::result::Result::Ok("operator.json".to_string()));
let base58 = super::validated_wallet_export_file_name(
"operator.txt",
ks_wallet::WalletTransferFormat::SolanaPrivateKeyBase58,
);
assert_eq!(base58, std::result::Result::Ok("operator.txt".to_string()));
assert!(
super::validated_wallet_export_file_name(
"../operator.json",
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.is_err()
);
assert!(
super::validated_wallet_export_file_name(
"operator.txt",
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.is_err()
);
}
#[test]
fn wallet_rpc_surface_is_read_only_and_bounded() {
for allowed in ["getTokenAccountsByOwner", "getSignaturesForAddress", "getTransaction"] {

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/lib.rs
// version: 41
// version: 43
//! Tauri desktop demo application for `khadhroony-bot3`.
@@ -151,10 +151,16 @@ pub(crate) use self::demo_decode_replay::demo_decode_replay_options;
pub(crate) use self::demo_devnet_common::DemoExecutionDevnetStoreReadinessPayload;
/// Restores the shared single-execution state flag when a Devnet run finishes.
pub(crate) use self::demo_devnet_common::DemoExecutionRunGuard;
/// Loads the backend-only demo wallet password.
pub(crate) use self::demo_devnet_common::demo_wallet_password;
/// Returns whether the backend-only demo wallet password is configured.
pub(crate) use self::demo_devnet_common::demo_wallet_password_configured;
/// Converts the reusable scenario readiness report to the desktop TS-RS payload.
pub(crate) use self::demo_devnet_common::devnet_store_readiness_payload;
/// Serializes a diagnostic value to indented JSON without panicking.
pub(crate) use self::demo_devnet_common::pretty_json;
/// Resolves the temporary or persistent signer selected by one Devnet profile.
pub(crate) use self::demo_devnet_common::resolve_devnet_execution_wallet;
/// Bridges Metadata execution progress to the Metadata desktop window.
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataObserver;
/// Progress event emitted by Metadata execution workflows.
@@ -367,6 +373,10 @@ pub(crate) use self::demo_transport::DemoEndpointRolePayload;
pub(crate) use self::demo_wallet::DemoWalletBalancePayload;
/// Request to create one native wallet from the backend-only demo secret.
pub(crate) use self::demo_wallet::DemoWalletCreateRequest;
/// One Devnet profile and its effective execution-wallet selection.
pub(crate) use self::demo_wallet::DemoWalletExecutionProfilePayload;
/// Request to set or clear one session-only execution-wallet selection.
pub(crate) use self::demo_wallet::DemoWalletExecutionSelectionRequest;
/// Request to export one native wallet secret through the backend-only demo secret.
pub(crate) use self::demo_wallet::DemoWalletExportRequest;
/// UI-safe native wallet identity exposed by the desktop adapter.
@@ -403,6 +413,8 @@ pub(crate) use self::demo_wallet::demo_wallet_inspect_transfer_file;
pub(crate) use self::demo_wallet::demo_wallet_inventory;
/// Executes one bounded read-only wallet explorer RPC through an explicitly selected profile.
pub(crate) use self::demo_wallet::demo_wallet_rpc_execute;
/// Sets or clears the session-only native wallet used by Devnet execution demos.
pub(crate) use self::demo_wallet::demo_wallet_select_execution_wallet;
/// UI-safe WebSocket endpoint snapshot.
pub(crate) use self::demo_ws::DemoWsEndpointPayload;
/// WebSocket execution response payload.

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/tauri.rs
// version: 43
// version: 44
//! Tauri runtime assembly and private command wrappers.
@@ -38,6 +38,7 @@ pub fn run() -> ks_core::Result<()> {
load_demo_config,
open_demo_wallet_window,
demo_wallet_inventory,
demo_wallet_select_execution_wallet,
demo_wallet_create_native,
demo_wallet_inspect_transfer_file,
demo_wallet_import_file,
@@ -258,6 +259,14 @@ async fn demo_wallet_inventory(
return crate::demo_wallet_inventory(state.inner()).await;
}
#[tauri::command]
async fn demo_wallet_select_execution_wallet(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoWalletExecutionSelectionRequest,
) -> std::result::Result<crate::DemoWalletInventoryPayload, std::string::String> {
return crate::demo_wallet_select_execution_wallet(state.inner(), request).await;
}
#[tauri::command]
async fn demo_wallet_create_native(
state: tauri::State<'_, crate::AppState>,
@@ -285,7 +294,7 @@ async fn demo_wallet_import_file(
async fn demo_wallet_export_file(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoWalletExportRequest,
) -> std::result::Result<(), std::string::String> {
) -> std::result::Result<std::string::String, std::string::String> {
return crate::demo_wallet_export_file(state.inner(), request).await;
}