Files
khadhroony-bot3/kb-app-demo-desktop/src/demo_wallet.rs

1052 lines
44 KiB
Rust

// 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<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.
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,
}
/// 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<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(
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()));
}
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<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,
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 = 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<DemoWalletTransferInspectionPayload, std::string::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 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<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 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<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()),
};
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<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 wallet_transfer_format_payloads() -> std::vec::Vec<crate::DemoWalletTransferFormatPayload> {
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<ks_wallet::WalletTransferFormat, std::string::String> {
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::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,
) -> 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",
"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");
}
}