// file: kb-app-demo-desktop/src/demo_wallet.rs // 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 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, /// Devnet profiles and their session/config execution-wallet selections. pub(crate) execution_profiles: std::vec::Vec, /// 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, /// 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, /// Structurally valid native wallet identities discovered in the configured directory. pub(crate) wallets: std::vec::Vec, } /// 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, } /// One transfer format selectable by the wallet demo. #[derive(Clone, Debug, serde::Serialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferFormatPayload.ts" )] pub(crate) struct DemoWalletTransferFormatPayload { /// Stable machine-readable transfer format code. pub(crate) code: std::string::String, /// Human-readable transfer format label. pub(crate) label: std::string::String, /// Conventional extension suggested for explicit export paths. pub(crate) default_extension: std::string::String, } /// Request to inspect one external keypair transfer file without importing it. #[derive(Clone, Debug, serde::Deserialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionRequest.ts" )] pub(crate) struct DemoWalletTransferInspectionRequest { /// Explicit path of the external transfer file. pub(crate) source_path: std::string::String, /// Stable transfer format code selected by the operator. pub(crate) format: std::string::String, } /// Safe public identity extracted from one external keypair transfer file. #[derive(Clone, Debug, serde::Serialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletTransferInspectionPayload.ts" )] pub(crate) struct DemoWalletTransferInspectionPayload { /// Public key derived from the validated secret keypair. pub(crate) public_key: std::string::String, /// Stable transfer format code used for validation. pub(crate) format: std::string::String, } /// Request to import one external keypair into the active native wallet store. #[derive(Clone, Debug, serde::Deserialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletImportRequest.ts" )] pub(crate) struct DemoWalletImportRequest { /// Alias assigned to the newly created native wallet. pub(crate) alias: std::string::String, /// Explicit path of the external source file. pub(crate) source_path: std::string::String, /// Stable transfer format code selected by the operator. pub(crate) format: std::string::String, } /// Request to export one native wallet to the application-owned export directory. #[derive(Clone, Debug, serde::Deserialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_wallet/DemoWalletExportRequest.ts" )] pub(crate) struct DemoWalletExportRequest { /// Alias of the native wallet to export. pub(crate) alias: 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, /// Session-only desktop override, when present. pub(crate) runtime_alias: std::option::Option, /// Effective alias used by execution demos, or `None` for the temporary fallback. pub(crate) effective_alias: std::option::Option, } /// 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, } /// 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 { 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())); } 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: 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 { 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, request: crate::DemoWalletCreateRequest, ) -> std::result::Result { 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 = match crate::demo_wallet_password() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; 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 external transfer file and returns only the derived public identity. pub(crate) async fn demo_wallet_inspect_transfer_file( request: crate::DemoWalletTransferInspectionRequest, ) -> std::result::Result { let format = match wallet_transfer_format_from_code(request.format.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let inspection = match ks_wallet::inspect_transfer_file( std::path::PathBuf::from(request.source_path), format, ) .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(DemoWalletTransferInspectionPayload { public_key: inspection.public_key().to_string(), format: inspection.format().code().to_string(), }); } /// Imports one external keypair into a new password-protected native wallet. pub(crate) async fn demo_wallet_import_file( state: &crate::AppState, request: crate::DemoWalletImportRequest, ) -> std::result::Result { 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 format = match wallet_transfer_format_from_code(request.format.as_str()) { 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), }; 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 .import_file(alias, password, std::path::PathBuf::from(request.source_path), format) .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); } /// Exports one native wallet after backend-only password authentication. pub(crate) async fn demo_wallet_export_file( state: &crate::AppState, request: crate::DemoWalletExportRequest, ) -> std::result::Result { 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 format = match wallet_transfer_format_from_code(request.format.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; 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), }; let manager = match wallet_manager_for_profile(state.active_profile()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let destination = export_directory.join(file_name.as_str()); if let std::result::Result::Err(error) = manager.export_file(&alias, password, destination, format).await { 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. pub(crate) async fn demo_wallet_inspect_file( state: &crate::AppState, path: std::string::String, ) -> std::result::Result { 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 { 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 { 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::>( 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 wallet_transfer_format_payloads() -> std::vec::Vec { let mut formats = std::vec::Vec::new(); for format in ks_wallet::WalletTransferFormat::supported() { formats.push(crate::DemoWalletTransferFormatPayload { code: format.code().to_string(), label: format.label().to_string(), default_extension: format.default_extension().to_string(), }); } return formats; } fn wallet_transfer_format_from_code( code: &str, ) -> std::result::Result { let code = code.trim(); for format in ks_wallet::WalletTransferFormat::supported() { if format.code() == code { return std::result::Result::Ok(format); } } return std::result::Result::Err(format!("wallet transfer format '{code}' is not supported")); } fn execution_profile_payloads( state: &crate::AppState, ) -> std::result::Result, 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 { 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 { 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, ) -> 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 { 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 { 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 { 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", "walletImportSourcePathInput", "walletImportFormatSelect", "walletImportAliasInput", "inspectWalletTransferButton", "importWalletTransferButton", "walletExportAliasSelect", "walletExportFormatSelect", "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")); assert!(script.contains("demo_wallet_export_file")); 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_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")); assert!(tauri_runtime.contains("demo_wallet_export_file")); 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, }], 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(), 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_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"] { assert!(super::wallet_rpc_method_allowed(allowed)); } for forbidden in ["requestAirdrop", "sendTransaction", "simulateTransaction", "getProgramAccounts"] { assert!(!super::wallet_rpc_method_allowed(forbidden)); } } #[test] fn wallet_transfer_format_inventory_matches_reusable_wallet_boundary() { let formats = super::wallet_transfer_format_payloads(); assert_eq!(formats.len(), 2); assert_eq!(formats[0].code, "solana_cli_json"); assert_eq!(formats[1].code, "solana_private_key_base58"); assert!(super::wallet_transfer_format_from_code("solana_cli_json").is_ok()); assert!(super::wallet_transfer_format_from_code("solana_private_key_base58").is_ok()); assert!(super::wallet_transfer_format_from_code("unknown").is_err()); } #[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"); } }