v0.5.2-pre.006
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/demo_config.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Configuration demo payloads with explicit public and bounded diagnostic projections.
|
||||
|
||||
@@ -203,6 +203,8 @@ pub(crate) struct DemoConfigWalletDiagnosticPayload {
|
||||
pub(crate) wallet_directory_state: std::string::String,
|
||||
/// Wallet cluster.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Persistent native wallet alias selected by configuration, when present.
|
||||
pub(crate) wallet_alias: std::option::Option<std::string::String>,
|
||||
/// Whether the managed temporary wallet is enabled.
|
||||
pub(crate) temporary_wallet_enabled: bool,
|
||||
/// Temporary wallet alias, which is internal but not secret.
|
||||
@@ -387,6 +389,7 @@ fn diagnostic_config_payload(state: &crate::AppState) -> DemoConfigDiagnosticPay
|
||||
wallet: DemoConfigWalletDiagnosticPayload {
|
||||
wallet_directory_state: configured_state(profile.wallet.wallet_dir.as_str()),
|
||||
cluster: profile.wallet.cluster.clone(),
|
||||
wallet_alias: profile.wallet.wallet_alias.clone(),
|
||||
temporary_wallet_enabled: profile.wallet.temporary_wallet_enabled,
|
||||
temporary_wallet_alias: profile.wallet.temporary_wallet_alias.clone(),
|
||||
temporary_wallet_persist: profile.wallet.temporary_wallet_persist,
|
||||
@@ -547,6 +550,7 @@ mod tests {
|
||||
wallet: super::DemoConfigWalletDiagnosticPayload {
|
||||
wallet_directory_state: super::configured_state(profile.wallet.wallet_dir.as_str()),
|
||||
cluster: profile.wallet.cluster.clone(),
|
||||
wallet_alias: profile.wallet.wallet_alias.clone(),
|
||||
temporary_wallet_enabled: profile.wallet.temporary_wallet_enabled,
|
||||
temporary_wallet_alias: profile.wallet.temporary_wallet_alias.clone(),
|
||||
temporary_wallet_persist: profile.wallet.temporary_wallet_persist,
|
||||
|
||||
559
kb-app-demo-desktop/src/demo_wallet.rs
Normal file
559
kb-app-demo-desktop/src/demo_wallet.rs
Normal file
@@ -0,0 +1,559 @@
|
||||
// file: kb-app-demo-desktop/src/demo_wallet.rs
|
||||
// version: 4
|
||||
|
||||
//! UI-safe wallet management and public on-chain exploration.
|
||||
|
||||
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.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletInventoryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWalletInventoryPayload {
|
||||
/// Active resolved application profile.
|
||||
pub(crate) profile: std::string::String,
|
||||
/// Active wallet cluster.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Profiles available for explicit read-only on-chain exploration.
|
||||
pub(crate) onchain_profiles: std::vec::Vec<DemoWalletOnchainProfilePayload>,
|
||||
/// Canonical classic SPL Token program identifier used by the explorer.
|
||||
pub(crate) token_program_id: std::string::String,
|
||||
/// Canonical Token-2022 program identifier used by the explorer.
|
||||
pub(crate) token_2022_program_id: std::string::String,
|
||||
/// Whether the backend-only demo password environment variable is present.
|
||||
pub(crate) create_password_configured: bool,
|
||||
/// Persistent alias selected by configuration, when present.
|
||||
pub(crate) selected_alias: std::option::Option<std::string::String>,
|
||||
/// Structurally valid native wallet identities discovered in the configured directory.
|
||||
pub(crate) wallets: std::vec::Vec<DemoWalletIdentityPayload>,
|
||||
}
|
||||
|
||||
/// One UI-safe native wallet identity.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletIdentityPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWalletIdentityPayload {
|
||||
/// Non-sensitive wallet alias.
|
||||
pub(crate) alias: std::string::String,
|
||||
/// Public key declared by the native wallet header.
|
||||
pub(crate) public_key: std::string::String,
|
||||
/// Native wallet format version.
|
||||
pub(crate) format_version: u16,
|
||||
/// Whether this alias is selected by the active profile.
|
||||
pub(crate) selected: bool,
|
||||
}
|
||||
|
||||
/// Request to create one native wallet in the active profile store.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletCreateRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWalletCreateRequest {
|
||||
/// Alias of the new persistent wallet.
|
||||
pub(crate) alias: std::string::String,
|
||||
}
|
||||
|
||||
/// Exact SOL balance returned for one public wallet address.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletBalancePayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWalletBalancePayload {
|
||||
/// Explicitly selected resolved application profile.
|
||||
pub(crate) profile: std::string::String,
|
||||
/// Cluster selected by that profile.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Public address queried on-chain.
|
||||
pub(crate) address: std::string::String,
|
||||
/// Exact lamport balance represented as a decimal string.
|
||||
pub(crate) lamports: std::string::String,
|
||||
/// Exact SOL balance represented without floating-point conversion.
|
||||
pub(crate) sol: std::string::String,
|
||||
/// Endpoint selected for the balance query.
|
||||
pub(crate) endpoint_name: std::string::String,
|
||||
/// Provider selected for the balance query.
|
||||
pub(crate) provider: std::string::String,
|
||||
}
|
||||
|
||||
/// One selectable profile for read-only wallet on-chain exploration.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletOnchainProfilePayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWalletOnchainProfilePayload {
|
||||
/// Resolved profile name.
|
||||
pub(crate) name: std::string::String,
|
||||
/// Cluster selected by that profile.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Whether this is the application-wide active profile.
|
||||
pub(crate) active: bool,
|
||||
}
|
||||
|
||||
/// Request for one wallet-scoped public JSON-RPC read on an explicitly selected profile.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWalletRpcRequest {
|
||||
/// Resolved profile name used for transport selection.
|
||||
pub(crate) profile: std::string::String,
|
||||
/// Allowed read-only Solana JSON-RPC method.
|
||||
pub(crate) method: std::string::String,
|
||||
/// Complete positional JSON-RPC parameters encoded as one JSON array.
|
||||
pub(crate) params_json: std::string::String,
|
||||
}
|
||||
|
||||
/// Result of one wallet-scoped public JSON-RPC read.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletRpcExecutionPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWalletRpcExecutionPayload {
|
||||
/// Resolved profile name used by the request.
|
||||
pub(crate) profile: std::string::String,
|
||||
/// Cluster selected by that profile.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Selected endpoint name.
|
||||
pub(crate) endpoint_name: std::string::String,
|
||||
/// Selected provider code.
|
||||
pub(crate) provider: std::string::String,
|
||||
/// JSON-RPC method name.
|
||||
pub(crate) method: std::string::String,
|
||||
/// Pretty JSON response text.
|
||||
pub(crate) response_json: std::string::String,
|
||||
}
|
||||
|
||||
/// Returns the native wallet inventory visible to the active profile without secret material.
|
||||
pub(crate) async fn demo_wallet_inventory(
|
||||
state: &crate::AppState,
|
||||
) -> std::result::Result<DemoWalletInventoryPayload, std::string::String> {
|
||||
let profile = state.active_profile();
|
||||
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 handles = match manager.scan().await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let selected_alias = profile.wallet.wallet_alias.clone();
|
||||
let mut wallets = std::vec::Vec::with_capacity(handles.len());
|
||||
for handle in handles {
|
||||
wallets.push(wallet_identity_payload(&handle, selected_alias.as_deref()));
|
||||
}
|
||||
return std::result::Result::Ok(DemoWalletInventoryPayload {
|
||||
profile: profile.name.clone(),
|
||||
cluster: profile.wallet.cluster.clone(),
|
||||
onchain_profiles: onchain_profile_payloads(state),
|
||||
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(),
|
||||
selected_alias,
|
||||
wallets,
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates one password-protected native wallet without sending the password through Tauri IPC.
|
||||
pub(crate) async fn demo_wallet_create_native(
|
||||
state: &crate::AppState,
|
||||
request: crate::DemoWalletCreateRequest,
|
||||
) -> std::result::Result<DemoWalletIdentityPayload, 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()),
|
||||
};
|
||||
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"
|
||||
));
|
||||
},
|
||||
};
|
||||
let password = match ks_wallet::WalletPassword::new(password_text) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let profile = state.active_profile();
|
||||
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.create(alias, password).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let identity = wallet.identity();
|
||||
let payload = DemoWalletIdentityPayload {
|
||||
selected: profile.wallet.wallet_alias.as_deref()
|
||||
== std::option::Option::Some(identity.alias.as_str()),
|
||||
alias: identity.alias.as_str().to_string(),
|
||||
public_key: identity.public_key,
|
||||
format_version: 1,
|
||||
};
|
||||
wallet.lock();
|
||||
return std::result::Result::Ok(payload);
|
||||
}
|
||||
|
||||
/// Inspects one explicitly selected native wallet file and returns only its safe identity.
|
||||
pub(crate) async fn demo_wallet_inspect_file(
|
||||
state: &crate::AppState,
|
||||
path: std::string::String,
|
||||
) -> std::result::Result<DemoWalletIdentityPayload, std::string::String> {
|
||||
let profile = state.active_profile();
|
||||
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 handle = match manager.inspect_file(std::path::PathBuf::from(path)).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(wallet_identity_payload(
|
||||
&handle,
|
||||
profile.wallet.wallet_alias.as_deref(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Loads the exact SOL balance of one public address through an explicitly selected profile.
|
||||
pub(crate) async fn demo_wallet_balance(
|
||||
state: &crate::AppState,
|
||||
profile: std::string::String,
|
||||
address: std::string::String,
|
||||
) -> std::result::Result<crate::DemoWalletBalancePayload, std::string::String> {
|
||||
let selected_profile = match onchain_profile(state, &profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pool = match ks_onchain_transport::HttpEndpointPool::from_profile(selected_profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let address = address.trim().to_string();
|
||||
if address.is_empty() {
|
||||
return std::result::Result::Err("wallet address must not be empty".to_string());
|
||||
}
|
||||
let role = match best_http_role_for_method(&pool, "getBalance") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let client = match pool.select_client_for_role_and_method(&role, "getBalance") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let result = match client
|
||||
.get_balance(
|
||||
&ks_lib::MdPubkey(address.clone()),
|
||||
&ks_onchain_transport::GetBalanceConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(crate::DemoWalletBalancePayload {
|
||||
profile: selected_profile.name.clone(),
|
||||
cluster: selected_profile.wallet.cluster.clone(),
|
||||
address,
|
||||
lamports: result.lamports.to_string(),
|
||||
sol: format_lamports_as_sol(result.lamports),
|
||||
endpoint_name: client.endpoint_name().to_string(),
|
||||
provider: client.provider().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Executes one bounded public wallet explorer RPC through an explicitly selected profile.
|
||||
pub(crate) async fn demo_wallet_rpc_execute(
|
||||
state: &crate::AppState,
|
||||
request: crate::DemoWalletRpcRequest,
|
||||
) -> std::result::Result<crate::DemoWalletRpcExecutionPayload, std::string::String> {
|
||||
let selected_profile = match onchain_profile(state, &request.profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let method = request.method.trim().to_string();
|
||||
if !wallet_rpc_method_allowed(&method) {
|
||||
return std::result::Result::Err(format!(
|
||||
"wallet explorer RPC method '{method}' is not allowed"
|
||||
));
|
||||
}
|
||||
let params = match serde_json::from_str::<std::vec::Vec<serde_json::Value>>(
|
||||
request.params_json.as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"wallet explorer params must be a JSON array: {error}"
|
||||
));
|
||||
},
|
||||
};
|
||||
let pool = match ks_onchain_transport::HttpEndpointPool::from_profile(selected_profile) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let role = match best_http_role_for_method(&pool, &method) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let client = match pool.select_client_for_role_and_method(&role, &method) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let response = match client.execute_json_rpc_result_raw(method.clone(), params).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let response_json = match serde_json::to_string_pretty(&response) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return std::result::Result::Ok(crate::DemoWalletRpcExecutionPayload {
|
||||
profile: selected_profile.name.clone(),
|
||||
cluster: selected_profile.wallet.cluster.clone(),
|
||||
endpoint_name: client.endpoint_name().to_string(),
|
||||
provider: client.provider().to_string(),
|
||||
method,
|
||||
response_json,
|
||||
});
|
||||
}
|
||||
|
||||
fn onchain_profile<'a>(
|
||||
state: &'a crate::AppState,
|
||||
profile_name: &str,
|
||||
) -> std::result::Result<&'a ks_config::ProfileConfig, std::string::String> {
|
||||
let profile_name = profile_name.trim();
|
||||
if profile_name.is_empty() {
|
||||
return std::result::Result::Err("wallet on-chain profile must not be empty".to_string());
|
||||
}
|
||||
for profile in &state.app_config().profiles {
|
||||
if profile.name == profile_name {
|
||||
return std::result::Result::Ok(profile);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err(format!(
|
||||
"wallet on-chain profile '{profile_name}' is not configured"
|
||||
));
|
||||
}
|
||||
|
||||
fn onchain_profile_payloads(
|
||||
state: &crate::AppState,
|
||||
) -> std::vec::Vec<crate::DemoWalletOnchainProfilePayload> {
|
||||
let mut profiles = std::vec::Vec::new();
|
||||
for profile in &state.app_config().profiles {
|
||||
let has_enabled_http =
|
||||
profile.solana.http_endpoints.iter().any(|endpoint| return endpoint.enabled);
|
||||
if !has_enabled_http {
|
||||
continue;
|
||||
}
|
||||
profiles.push(crate::DemoWalletOnchainProfilePayload {
|
||||
name: profile.name.clone(),
|
||||
cluster: profile.wallet.cluster.clone(),
|
||||
active: profile.name == state.active_profile().name,
|
||||
});
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
fn wallet_rpc_method_allowed(method: &str) -> bool {
|
||||
return method == "getTokenAccountsByOwner"
|
||||
|| method == "getSignaturesForAddress"
|
||||
|| method == "getTransaction";
|
||||
}
|
||||
|
||||
fn wallet_manager_for_profile(
|
||||
profile: &ks_config::ProfileConfig,
|
||||
) -> std::result::Result<ks_wallet::WalletManager, std::string::String> {
|
||||
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
|
||||
let directory = if configured.is_absolute() {
|
||||
configured
|
||||
} else {
|
||||
crate::workspace_root_dir().join(configured)
|
||||
};
|
||||
return match ks_wallet::WalletManager::new(directory) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
fn wallet_identity_payload(
|
||||
handle: &ks_wallet::WalletFileHandle,
|
||||
selected_alias: std::option::Option<&str>,
|
||||
) -> crate::DemoWalletIdentityPayload {
|
||||
return crate::DemoWalletIdentityPayload {
|
||||
selected: selected_alias == std::option::Option::Some(handle.alias().as_str()),
|
||||
alias: handle.alias().as_str().to_string(),
|
||||
public_key: handle.public_key().to_string(),
|
||||
format_version: handle.format_version(),
|
||||
};
|
||||
}
|
||||
|
||||
fn best_http_role_for_method(
|
||||
pool: &ks_onchain_transport::HttpEndpointPool,
|
||||
method: &str,
|
||||
) -> std::result::Result<std::string::String, std::string::String> {
|
||||
let request_kind = ks_onchain_transport::request_kind_from_method(method);
|
||||
let mut selected = std::option::Option::<(u32, std::string::String)>::None;
|
||||
for endpoint in pool.snapshot() {
|
||||
for role in endpoint.roles {
|
||||
if !role.enabled {
|
||||
continue;
|
||||
}
|
||||
let supports_kind = role
|
||||
.request_kinds
|
||||
.iter()
|
||||
.any(|kind| return kind == &request_kind || kind == "*");
|
||||
if !supports_kind {
|
||||
continue;
|
||||
}
|
||||
let replace = match &selected {
|
||||
std::option::Option::Some((priority, selected_role)) => {
|
||||
role.priority < *priority
|
||||
|| (role.priority == *priority && role.role < *selected_role)
|
||||
},
|
||||
std::option::Option::None => true,
|
||||
};
|
||||
if replace {
|
||||
selected = std::option::Option::Some((role.priority, role.role));
|
||||
}
|
||||
}
|
||||
}
|
||||
return match selected {
|
||||
std::option::Option::Some((_, role)) => std::result::Result::Ok(role),
|
||||
std::option::Option::None => std::result::Result::Err(format!(
|
||||
"active HTTP profile has no role for request kind '{request_kind}'"
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
fn format_lamports_as_sol(lamports: u64) -> std::string::String {
|
||||
let whole = lamports / LAMPORTS_PER_SOL;
|
||||
let fraction = lamports % LAMPORTS_PER_SOL;
|
||||
if fraction == 0 {
|
||||
return whole.to_string();
|
||||
}
|
||||
let fraction_text = format!("{fraction:09}");
|
||||
let trimmed = fraction_text.trim_end_matches('0');
|
||||
return format!("{whole}.{trimmed}");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn wallet_frontend_is_wired_to_management_and_onchain_read_commands() {
|
||||
let html = include_str!("../frontend/demo_wallet.html");
|
||||
let script = include_str!("../frontend/ts/demo_wallet.ts");
|
||||
let main_html = include_str!("../frontend/main.html");
|
||||
let main_script = include_str!("../frontend/ts/main.ts");
|
||||
let tauri_runtime = include_str!("tauri.rs");
|
||||
let capability = include_str!("../capabilities/default.json");
|
||||
let vite_config = include_str!("../vite.config.ts");
|
||||
for required in [
|
||||
"createWalletAliasInput",
|
||||
"createWalletButton",
|
||||
"refreshWalletInventoryButton",
|
||||
"walletInventoryTableBody",
|
||||
"walletOnchainProfileSelect",
|
||||
"walletAddressInput",
|
||||
"loadWalletOnchainButton",
|
||||
"walletTokensTableBody",
|
||||
"walletTransactionsTableBody",
|
||||
"walletTransactionJson",
|
||||
"walletExternalPathInput",
|
||||
"inspectWalletFileButton",
|
||||
"walletInspectionJson",
|
||||
] {
|
||||
assert!(html.contains(required));
|
||||
}
|
||||
assert!(script.contains("demo_wallet_create_native"));
|
||||
assert!(script.contains("demo_wallet_balance"));
|
||||
assert!(script.contains("demo_wallet_inventory"));
|
||||
assert!(script.contains("demo_wallet_rpc_execute"));
|
||||
assert!(script.contains("demo_wallet_inspect_file"));
|
||||
assert!(!script.contains("demo_http_execute_request"));
|
||||
assert!(script.contains("getTokenAccountsByOwner"));
|
||||
assert!(script.contains("getSignaturesForAddress"));
|
||||
assert!(script.contains("getTransaction"));
|
||||
assert!(!html.to_ascii_lowercase().contains("type=\"password\""));
|
||||
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_create_native"));
|
||||
assert!(tauri_runtime.contains("demo_wallet_balance"));
|
||||
assert!(tauri_runtime.contains("demo_wallet_rpc_execute"));
|
||||
assert!(tauri_runtime.contains("demo_wallet.html"));
|
||||
assert!(capability.contains("\"demo_wallet\""));
|
||||
assert!(vite_config.contains("frontend/demo_wallet.html"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_inventory_payload_contains_only_explicit_safe_fields() {
|
||||
let payload = super::DemoWalletInventoryPayload {
|
||||
profile: "mainnet_research".to_string(),
|
||||
cluster: "mainnet-beta".to_string(),
|
||||
onchain_profiles: std::vec![super::DemoWalletOnchainProfilePayload {
|
||||
name: "local_devnet".to_string(),
|
||||
cluster: "devnet".to_string(),
|
||||
active: false,
|
||||
}],
|
||||
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: true,
|
||||
selected_alias: std::option::Option::Some("selected-wallet".to_string()),
|
||||
wallets: std::vec![super::DemoWalletIdentityPayload {
|
||||
alias: "selected-wallet".to_string(),
|
||||
public_key: "11111111111111111111111111111111".to_string(),
|
||||
format_version: 1,
|
||||
selected: true,
|
||||
}],
|
||||
};
|
||||
let serialized = match serde_json::to_string(&payload) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("wallet inventory must serialize: {error}"),
|
||||
};
|
||||
for forbidden in [
|
||||
"canary-wallet-password",
|
||||
"ciphertext",
|
||||
"nonce",
|
||||
"salt",
|
||||
"wallet_path",
|
||||
"storage_path",
|
||||
".kswallet",
|
||||
"http://",
|
||||
"https://",
|
||||
] {
|
||||
assert!(!serialized.contains(forbidden));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_rpc_surface_is_read_only_and_bounded() {
|
||||
for allowed in ["getTokenAccountsByOwner", "getSignaturesForAddress", "getTransaction"] {
|
||||
assert!(super::wallet_rpc_method_allowed(allowed));
|
||||
}
|
||||
for forbidden in
|
||||
["requestAirdrop", "sendTransaction", "simulateTransaction", "getProgramAccounts"]
|
||||
{
|
||||
assert!(!super::wallet_rpc_method_allowed(forbidden));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lamport_formatting_is_exact_without_float_conversion() {
|
||||
assert_eq!(super::format_lamports_as_sol(0), "0");
|
||||
assert_eq!(super::format_lamports_as_sol(1), "0.000000001");
|
||||
assert_eq!(super::format_lamports_as_sol(1_500_000_000), "1.5");
|
||||
assert_eq!(super::format_lamports_as_sol(u64::MAX), "18446744073.709551615");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/lib.rs
|
||||
// version: 37
|
||||
// version: 40
|
||||
|
||||
//! Tauri desktop demo application for `khadhroony-bot3`.
|
||||
|
||||
@@ -30,6 +30,7 @@ mod demo_sql_pg_core;
|
||||
mod demo_sql_pg_raw;
|
||||
mod demo_sql_replay_candidates;
|
||||
mod demo_transport;
|
||||
mod demo_wallet;
|
||||
mod demo_ws;
|
||||
mod desktop_config;
|
||||
mod frontend_log;
|
||||
@@ -362,6 +363,30 @@ pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_programs;
|
||||
pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_transactions;
|
||||
/// UI-safe endpoint role shared by HTTP and WebSocket diagnostics.
|
||||
pub(crate) use self::demo_transport::DemoEndpointRolePayload;
|
||||
/// Exact public SOL balance projected by the wallet demo.
|
||||
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;
|
||||
/// UI-safe native wallet identity exposed by the desktop adapter.
|
||||
pub(crate) use self::demo_wallet::DemoWalletIdentityPayload;
|
||||
/// UI-safe native wallet inventory exposed by the desktop adapter.
|
||||
pub(crate) use self::demo_wallet::DemoWalletInventoryPayload;
|
||||
/// One selectable profile for read-only wallet on-chain exploration.
|
||||
pub(crate) use self::demo_wallet::DemoWalletOnchainProfilePayload;
|
||||
/// Result of one wallet-scoped public JSON-RPC read.
|
||||
pub(crate) use self::demo_wallet::DemoWalletRpcExecutionPayload;
|
||||
/// Request for one wallet-scoped public JSON-RPC read.
|
||||
pub(crate) use self::demo_wallet::DemoWalletRpcRequest;
|
||||
/// Loads the exact SOL balance of one public wallet address.
|
||||
pub(crate) use self::demo_wallet::demo_wallet_balance;
|
||||
/// Creates one password-protected native wallet without exposing the password to Tauri IPC.
|
||||
pub(crate) use self::demo_wallet::demo_wallet_create_native;
|
||||
/// Inspects an explicitly selected native wallet file without exposing its path in the result.
|
||||
pub(crate) use self::demo_wallet::demo_wallet_inspect_file;
|
||||
/// Returns native wallet identities without paths, passwords or protected payloads.
|
||||
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;
|
||||
/// UI-safe WebSocket endpoint snapshot.
|
||||
pub(crate) use self::demo_ws::DemoWsEndpointPayload;
|
||||
/// WebSocket execution response payload.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/src/tauri.rs
|
||||
// version: 38
|
||||
// version: 42
|
||||
|
||||
//! Tauri runtime assembly and private command wrappers.
|
||||
|
||||
@@ -36,6 +36,12 @@ pub fn run() -> ks_core::Result<()> {
|
||||
load_project_readme,
|
||||
open_demo_config_window,
|
||||
load_demo_config,
|
||||
open_demo_wallet_window,
|
||||
demo_wallet_inventory,
|
||||
demo_wallet_create_native,
|
||||
demo_wallet_balance,
|
||||
demo_wallet_inspect_file,
|
||||
demo_wallet_rpc_execute,
|
||||
open_demo_http_window,
|
||||
demo_http_list_pool_clients,
|
||||
demo_http_options,
|
||||
@@ -235,6 +241,53 @@ fn load_demo_config(state: tauri::State<'_, crate::AppState>) -> crate::DemoConf
|
||||
return crate::demo_config_payload(state.inner());
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_demo_wallet_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
return open_or_focus_demo_window(app_handle, "demo_wallet", "demo_wallet.html", "Wallets");
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_wallet_inventory(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::DemoWalletInventoryPayload, std::string::String> {
|
||||
return crate::demo_wallet_inventory(state.inner()).await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_wallet_create_native(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoWalletCreateRequest,
|
||||
) -> std::result::Result<crate::DemoWalletIdentityPayload, std::string::String> {
|
||||
return crate::demo_wallet_create_native(state.inner(), request).await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_wallet_balance(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
profile: std::string::String,
|
||||
address: std::string::String,
|
||||
) -> std::result::Result<crate::DemoWalletBalancePayload, std::string::String> {
|
||||
return crate::demo_wallet_balance(state.inner(), profile, address).await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_wallet_inspect_file(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
path: std::string::String,
|
||||
) -> std::result::Result<crate::DemoWalletIdentityPayload, std::string::String> {
|
||||
return crate::demo_wallet_inspect_file(state.inner(), path).await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn demo_wallet_rpc_execute(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoWalletRpcRequest,
|
||||
) -> std::result::Result<crate::DemoWalletRpcExecutionPayload, std::string::String> {
|
||||
return crate::demo_wallet_rpc_execute(state.inner(), request).await;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_demo_backfill_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
|
||||
Reference in New Issue
Block a user