650 lines
23 KiB
Rust
650 lines
23 KiB
Rust
#[tauri::command]
|
|
async fn current_block_height(
|
|
state: State<'_, WalletState>,
|
|
broadcast_node: String,
|
|
) -> Result<u32, String> {
|
|
ensure_gui_settings_file().await?;
|
|
|
|
let socket_address = active_broadcast_socket(&broadcast_node)?;
|
|
let (_short_address, public_key, private_key) = active_wallet_rpc_parts(&state)?;
|
|
let response = serialized_wallet_rpc(
|
|
&state,
|
|
socket_address,
|
|
String::new(),
|
|
RPC_BLOCK_HEIGHT as usize,
|
|
public_key,
|
|
private_key,
|
|
"Block height lookup failed",
|
|
)
|
|
.await?;
|
|
|
|
if let Some(error) = parse_error_response(&response) {
|
|
return Err(error);
|
|
}
|
|
if response.len() != 4 {
|
|
return Err("Block height response had an unexpected byte length.".to_string());
|
|
}
|
|
|
|
Ok(u32::from_le_bytes(response[0..4].try_into().map_err(
|
|
|_| "Failed to parse block height response.".to_string(),
|
|
)?))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn network_info(
|
|
state: State<'_, WalletState>,
|
|
broadcast_node: String,
|
|
) -> Result<NetworkInfoView, String> {
|
|
ensure_gui_settings_file().await?;
|
|
|
|
let socket_address = active_broadcast_socket(&broadcast_node)?;
|
|
let (_short_address, public_key, private_key) = active_wallet_rpc_parts(&state)?;
|
|
let response = serialized_wallet_rpc(
|
|
&state,
|
|
socket_address,
|
|
String::new(),
|
|
RPC_NETWORK_INFO as usize,
|
|
public_key,
|
|
private_key,
|
|
"Network info lookup failed",
|
|
)
|
|
.await?;
|
|
|
|
parse_network_info_response(response)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn test_broadcast_node_time(
|
|
state: State<'_, WalletState>,
|
|
broadcast_node: String,
|
|
) -> Result<u32, String> {
|
|
ensure_gui_settings_file().await?;
|
|
|
|
let socket_address = active_broadcast_socket(&broadcast_node)?;
|
|
let (_short_address, public_key, private_key) = active_wallet_rpc_parts(&state)?;
|
|
let response = serialized_wallet_rpc(
|
|
&state,
|
|
socket_address,
|
|
String::new(),
|
|
RPC_TIME as usize,
|
|
public_key,
|
|
private_key,
|
|
"Node time lookup failed",
|
|
)
|
|
.await?;
|
|
|
|
if let Some(error) = parse_error_response(&response) {
|
|
return Err(error);
|
|
}
|
|
if response.len() != 4 {
|
|
return Err("Node time response had an unexpected byte length.".to_string());
|
|
}
|
|
|
|
Ok(u32::from_le_bytes(
|
|
response[0..4]
|
|
.try_into()
|
|
.map_err(|_| "Failed to parse node time response.".to_string())?,
|
|
))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn read_local_transaction_cache(state: State<'_, WalletState>) -> Result<String, String> {
|
|
let _cache_guard = state.history_cache_io.lock().await;
|
|
let cache_path = active_wallet_cache_path(&state)?;
|
|
match read(&cache_path).await {
|
|
Ok(bytes) => String::from_utf8(bytes)
|
|
.map_err(|_| "Local transaction cache is not valid UTF-8.".to_string()),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok("{}".to_string()),
|
|
Err(err) => Err(format!("Unable to read local transaction cache: {err}")),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn write_local_transaction_cache(
|
|
state: State<'_, WalletState>,
|
|
cache_json: String,
|
|
) -> Result<(), String> {
|
|
let _cache_guard = state.history_cache_io.lock().await;
|
|
let cache_path = active_wallet_cache_path(&state)?;
|
|
let parsed: JsonValue = serde_json::from_str(&cache_json)
|
|
.map_err(|err| format!("Transaction cache must be valid JSON: {err}"))?;
|
|
let normalized = serde_json::to_string_pretty(&parsed)
|
|
.map_err(|err| format!("Unable to serialize transaction cache: {err}"))?;
|
|
|
|
contractless::tokio::fs::write(&cache_path, normalized)
|
|
.await
|
|
.map_err(|err| format!("Unable to write local transaction cache: {err}"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn read_local_transaction_index(state: State<'_, WalletState>) -> Result<String, String> {
|
|
let _cache_guard = state.history_cache_io.lock().await;
|
|
let history_dir = active_wallet_history_dir(&state)?;
|
|
let index_path = history_dir.join("index.json");
|
|
match read(&index_path).await {
|
|
Ok(bytes) => String::from_utf8(bytes)
|
|
.map_err(|_| "Local transaction index is not valid UTF-8.".to_string()),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok("{}".to_string()),
|
|
Err(err) => Err(format!("Unable to read local transaction index: {err}")),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn write_local_transaction_index(
|
|
state: State<'_, WalletState>,
|
|
index_json: String,
|
|
) -> Result<(), String> {
|
|
let _cache_guard = state.history_cache_io.lock().await;
|
|
let history_dir = active_wallet_history_dir(&state)?;
|
|
create_dir_all(&history_dir)
|
|
.await
|
|
.map_err(|err| format!("Unable to create transaction history directory: {err}"))?;
|
|
let parsed: JsonValue = serde_json::from_str(&index_json)
|
|
.map_err(|err| format!("Transaction index must be valid JSON: {err}"))?;
|
|
let normalized = serde_json::to_string_pretty(&parsed)
|
|
.map_err(|err| format!("Unable to serialize transaction index: {err}"))?;
|
|
|
|
contractless::tokio::fs::write(history_dir.join("index.json"), normalized)
|
|
.await
|
|
.map_err(|err| format!("Unable to write local transaction index: {err}"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn read_local_transaction_record(
|
|
state: State<'_, WalletState>,
|
|
file_name: String,
|
|
) -> Result<String, String> {
|
|
let _cache_guard = state.history_cache_io.lock().await;
|
|
let history_dir = active_wallet_history_dir(&state)?;
|
|
let file_name = validate_transaction_record_file(&file_name)?;
|
|
match read(history_dir.join(file_name)).await {
|
|
Ok(bytes) => String::from_utf8(bytes)
|
|
.map_err(|_| "Local transaction record is not valid UTF-8.".to_string()),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok("{}".to_string()),
|
|
Err(err) => Err(format!("Unable to read local transaction record: {err}")),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn write_local_transaction_record(
|
|
state: State<'_, WalletState>,
|
|
file_name: String,
|
|
record_json: String,
|
|
) -> Result<(), String> {
|
|
let _cache_guard = state.history_cache_io.lock().await;
|
|
let history_dir = active_wallet_history_dir(&state)?;
|
|
create_dir_all(&history_dir)
|
|
.await
|
|
.map_err(|err| format!("Unable to create transaction history directory: {err}"))?;
|
|
let file_name = validate_transaction_record_file(&file_name)?;
|
|
let parsed: JsonValue = serde_json::from_str(&record_json)
|
|
.map_err(|err| format!("Transaction record must be valid JSON: {err}"))?;
|
|
let normalized = serde_json::to_string_pretty(&parsed)
|
|
.map_err(|err| format!("Unable to serialize transaction record: {err}"))?;
|
|
|
|
contractless::tokio::fs::write(history_dir.join(file_name), normalized)
|
|
.await
|
|
.map_err(|err| format!("Unable to write local transaction record: {err}"))
|
|
}
|
|
|
|
fn validate_nft_cache_key(nft_name: &str, series: u32) -> Result<String, String> {
|
|
let name = nft_name.trim().to_ascii_lowercase();
|
|
if name.is_empty() {
|
|
return Err("NFT cache name cannot be empty.".to_string());
|
|
}
|
|
if !name
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
|
|
{
|
|
return Err("NFT cache name contains unsupported characters.".to_string());
|
|
}
|
|
Ok(format!("{name}_{series}"))
|
|
}
|
|
|
|
fn nft_cache_record_from_parts(
|
|
detail: JsonValue,
|
|
metadata: JsonValue,
|
|
image_bytes: Option<Vec<u8>>,
|
|
image_mime: Option<String>,
|
|
) -> NftCacheRecordView {
|
|
let image_data_url = image_bytes.map(|bytes| {
|
|
let mime = image_mime
|
|
.filter(|value| value.starts_with("image/"))
|
|
.unwrap_or_else(|| "image/png".to_string());
|
|
format!("data:{mime};base64,{}", STANDARD.encode(bytes))
|
|
});
|
|
|
|
NftCacheRecordView {
|
|
detail,
|
|
metadata,
|
|
image_data_url,
|
|
}
|
|
}
|
|
|
|
async fn read_cached_nft_record(record_dir: &Path) -> Result<NftCacheRecordView, String> {
|
|
let record_path = record_dir.join("record.json");
|
|
let record_bytes = read(&record_path)
|
|
.await
|
|
.map_err(|err| format!("Unable to read NFT cache record: {err}"))?;
|
|
let record: JsonValue = serde_json::from_slice(&record_bytes)
|
|
.map_err(|err| format!("NFT cache record is not valid JSON: {err}"))?;
|
|
let detail = record.get("detail").cloned().unwrap_or(JsonValue::Null);
|
|
let metadata = record.get("metadata").cloned().unwrap_or(JsonValue::Null);
|
|
let image_mime = record
|
|
.get("image_mime")
|
|
.and_then(|value| value.as_str())
|
|
.map(|value| value.to_string());
|
|
let image_bytes = match read(record_dir.join("image.bin")).await {
|
|
Ok(bytes) => Some(bytes),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
|
|
Err(err) => return Err(format!("Unable to read NFT cached image: {err}")),
|
|
};
|
|
|
|
Ok(nft_cache_record_from_parts(
|
|
detail,
|
|
metadata,
|
|
image_bytes,
|
|
image_mime,
|
|
))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn read_nft_cache(
|
|
state: State<'_, WalletState>,
|
|
nft_name: String,
|
|
series: u32,
|
|
) -> Result<Option<NftCacheRecordView>, String> {
|
|
let _cache_guard = state.nft_cache_io.lock().await;
|
|
let cache_dir = active_wallet_nft_dir(&state)?;
|
|
let key = validate_nft_cache_key(&nft_name, series)?;
|
|
let record_dir = cache_dir.join(key);
|
|
match read_cached_nft_record(&record_dir).await {
|
|
Ok(record) => Ok(Some(record)),
|
|
Err(err) if err.contains("os error 2") || err.contains("not find") => Ok(None),
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn write_nft_cache(
|
|
state: State<'_, WalletState>,
|
|
nft_name: String,
|
|
series: u32,
|
|
detail_json: String,
|
|
metadata_json: String,
|
|
image_url: String,
|
|
) -> Result<NftCacheRecordView, String> {
|
|
let _cache_guard = state.nft_cache_io.lock().await;
|
|
let cache_dir = active_wallet_nft_dir(&state)?;
|
|
let key = validate_nft_cache_key(&nft_name, series)?;
|
|
let record_dir = cache_dir.join(key);
|
|
create_dir_all(&record_dir)
|
|
.await
|
|
.map_err(|err| format!("Unable to create NFT cache directory: {err}"))?;
|
|
|
|
let detail: JsonValue = serde_json::from_str(&detail_json)
|
|
.map_err(|err| format!("NFT detail cache must be valid JSON: {err}"))?;
|
|
let metadata: JsonValue = serde_json::from_str(&metadata_json)
|
|
.map_err(|err| format!("NFT metadata cache must be valid JSON: {err}"))?;
|
|
|
|
let mut image_mime = None;
|
|
let mut image_cached = false;
|
|
let image_url = image_url.trim();
|
|
if image_url.starts_with("http://")
|
|
|| image_url.starts_with("https://")
|
|
|| image_url.starts_with("data:image/")
|
|
{
|
|
if image_url.starts_with("data:image/") {
|
|
if let Some((header, encoded)) = image_url.split_once(',') {
|
|
image_mime = header
|
|
.strip_prefix("data:")
|
|
.and_then(|header| header.split_once(';').map(|(mime, _)| mime.to_string()));
|
|
if let Ok(bytes) = base64::Engine::decode(&STANDARD, encoded) {
|
|
if bytes.len() <= 10_000_000 {
|
|
contractless::tokio::fs::write(record_dir.join("image.bin"), bytes)
|
|
.await
|
|
.map_err(|err| format!("Unable to write NFT cached image: {err}"))?;
|
|
image_cached = true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
let client = reqwest::Client::builder()
|
|
.timeout(Duration::from_secs(20))
|
|
.build()
|
|
.map_err(|err| format!("Failed to prepare NFT image request: {err}"))?;
|
|
let response = client
|
|
.get(image_url)
|
|
.send()
|
|
.await
|
|
.map_err(|err| format!("Failed to fetch NFT image: {err}"))?
|
|
.error_for_status()
|
|
.map_err(|err| format!("NFT image gateway returned an error: {err}"))?;
|
|
image_mime = response
|
|
.headers()
|
|
.get(reqwest::header::CONTENT_TYPE)
|
|
.and_then(|value| value.to_str().ok())
|
|
.map(|value| value.split(';').next().unwrap_or(value).to_string());
|
|
let bytes = response
|
|
.bytes()
|
|
.await
|
|
.map_err(|err| format!("Failed to read NFT image response: {err}"))?;
|
|
if bytes.len() <= 10_000_000 {
|
|
contractless::tokio::fs::write(record_dir.join("image.bin"), &bytes)
|
|
.await
|
|
.map_err(|err| format!("Unable to write NFT cached image: {err}"))?;
|
|
image_cached = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
let record = serde_json::json!({
|
|
"detail": detail,
|
|
"metadata": metadata,
|
|
"image_mime": image_mime,
|
|
"image_cached": image_cached,
|
|
});
|
|
let normalized = serde_json::to_string_pretty(&record)
|
|
.map_err(|err| format!("Unable to serialize NFT cache record: {err}"))?;
|
|
contractless::tokio::fs::write(record_dir.join("record.json"), normalized)
|
|
.await
|
|
.map_err(|err| format!("Unable to write NFT cache record: {err}"))?;
|
|
|
|
read_cached_nft_record(&record_dir).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn prune_nft_cache(
|
|
state: State<'_, WalletState>,
|
|
keep_keys: Vec<String>,
|
|
) -> Result<(), String> {
|
|
let _cache_guard = state.nft_cache_io.lock().await;
|
|
let cache_dir = active_wallet_nft_dir(&state)?;
|
|
let keep: std::collections::HashSet<String> = keep_keys
|
|
.into_iter()
|
|
.map(|key| key.trim().to_ascii_lowercase())
|
|
.filter(|key| !key.is_empty())
|
|
.collect();
|
|
|
|
let mut entries = match read_dir(&cache_dir).await {
|
|
Ok(entries) => entries,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
|
Err(err) => return Err(format!("Unable to read NFT cache directory: {err}")),
|
|
};
|
|
|
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
|
let file_type = match entry.file_type().await {
|
|
Ok(file_type) => file_type,
|
|
Err(_) => continue,
|
|
};
|
|
if !file_type.is_dir() {
|
|
continue;
|
|
}
|
|
let key = entry.file_name().to_string_lossy().to_ascii_lowercase();
|
|
if !keep.contains(&key) {
|
|
contractless::tokio::fs::remove_dir_all(entry.path())
|
|
.await
|
|
.map_err(|err| format!("Unable to remove stale NFT cache entry: {err}"))?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn remove_file_if_exists(path: &Path) -> Result<(), String> {
|
|
match remove_file(path).await {
|
|
Ok(_) => Ok(()),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
|
Err(err) => Err(format!("Unable to remove {}: {err}", path.display())),
|
|
}
|
|
}
|
|
|
|
async fn remove_dir_if_exists(path: &Path) -> Result<(), String> {
|
|
match remove_dir_all(path).await {
|
|
Ok(_) => Ok(()),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
|
Err(err) => Err(format!("Unable to remove {}: {err}", path.display())),
|
|
}
|
|
}
|
|
|
|
async fn clear_nft_cache_files(cache_dir: &Path, file_name: &str) -> Result<(), String> {
|
|
let mut entries = match read_dir(cache_dir).await {
|
|
Ok(entries) => entries,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
|
Err(err) => return Err(format!("Unable to read NFT cache directory: {err}")),
|
|
};
|
|
|
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
|
let file_type = match entry.file_type().await {
|
|
Ok(file_type) => file_type,
|
|
Err(_) => continue,
|
|
};
|
|
if file_type.is_dir() {
|
|
remove_file_if_exists(&entry.path().join(file_name)).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn clear_wallet_cache(state: State<'_, WalletState>, cache_kind: String) -> Result<(), String> {
|
|
match cache_kind.trim().to_ascii_lowercase().as_str() {
|
|
"history" => {
|
|
let _cache_guard = state.history_cache_io.lock().await;
|
|
remove_file_if_exists(&active_wallet_cache_path(&state)?).await?;
|
|
remove_dir_if_exists(&active_wallet_history_dir(&state)?).await
|
|
}
|
|
"nft" | "nft_images" | "nft_media" => {
|
|
let _cache_guard = state.nft_cache_io.lock().await;
|
|
clear_nft_cache_files(&active_wallet_nft_dir(&state)?, "image.bin").await
|
|
}
|
|
"nft_details" => {
|
|
let _cache_guard = state.nft_cache_io.lock().await;
|
|
clear_nft_cache_files(&active_wallet_nft_dir(&state)?, "record.json").await
|
|
}
|
|
"address_book" => {
|
|
let _cache_guard = state.address_book_io.lock().await;
|
|
remove_file_if_exists(&active_wallet_address_book_path(&state)?).await
|
|
}
|
|
_ => Err("Unknown cache type.".to_string()),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn read_address_book(state: State<'_, WalletState>) -> Result<String, String> {
|
|
let _cache_guard = state.address_book_io.lock().await;
|
|
let address_book_path = active_wallet_address_book_path(&state)?;
|
|
match read(&address_book_path).await {
|
|
Ok(bytes) => String::from_utf8(bytes)
|
|
.map_err(|_| "Address book is not valid UTF-8.".to_string()),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
Ok("{\"entries\":{}}".to_string())
|
|
}
|
|
Err(err) => Err(format!("Unable to read address book: {err}")),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn write_address_book(
|
|
state: State<'_, WalletState>,
|
|
address_book_json: String,
|
|
) -> Result<(), String> {
|
|
let _cache_guard = state.address_book_io.lock().await;
|
|
let address_book_path = active_wallet_address_book_path(&state)?;
|
|
let parsed: JsonValue = serde_json::from_str(&address_book_json)
|
|
.map_err(|err| format!("Address book must be valid JSON: {err}"))?;
|
|
let normalized = serde_json::to_string_pretty(&parsed)
|
|
.map_err(|err| format!("Unable to serialize address book: {err}"))?;
|
|
|
|
contractless::tokio::fs::write(&address_book_path, normalized)
|
|
.await
|
|
.map_err(|err| format!("Unable to write address book: {err}"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn read_storage_labels(state: State<'_, WalletState>) -> Result<String, String> {
|
|
let labels_path = active_wallet_storage_labels_path(&state)?;
|
|
match read(&labels_path).await {
|
|
Ok(bytes) => String::from_utf8(bytes)
|
|
.map_err(|_| "Storage labels file is not valid UTF-8.".to_string()),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
Ok("{\"labels\":{}}".to_string())
|
|
}
|
|
Err(err) => Err(format!("Unable to read storage labels: {err}")),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn write_storage_labels(
|
|
state: State<'_, WalletState>,
|
|
storage_labels_json: String,
|
|
) -> Result<(), String> {
|
|
let labels_path = active_wallet_storage_labels_path(&state)?;
|
|
let parsed: JsonValue = serde_json::from_str(&storage_labels_json)
|
|
.map_err(|err| format!("Storage labels must be valid JSON: {err}"))?;
|
|
let normalized = serde_json::to_string_pretty(&parsed)
|
|
.map_err(|err| format!("Unable to serialize storage labels: {err}"))?;
|
|
|
|
contractless::tokio::fs::write(&labels_path, normalized)
|
|
.await
|
|
.map_err(|err| format!("Unable to write storage labels: {err}"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn lookup_address_vanity(
|
|
state: State<'_, WalletState>,
|
|
broadcast_node: String,
|
|
address: String,
|
|
) -> Result<Option<String>, String> {
|
|
ensure_gui_settings_file().await?;
|
|
|
|
let owner_address = address.trim();
|
|
if owner_address.is_empty() {
|
|
return Err("Address cannot be empty.".to_string());
|
|
}
|
|
|
|
let socket_address = active_broadcast_socket(&broadcast_node)?;
|
|
let (_short_address, public_key, private_key) = active_wallet_rpc_parts(&state)?;
|
|
let response = serialized_wallet_rpc(
|
|
&state,
|
|
socket_address,
|
|
owner_address.to_string(),
|
|
RPC_VANITY_LOOKUP as usize,
|
|
public_key,
|
|
private_key,
|
|
"Vanity address lookup failed",
|
|
)
|
|
.await?;
|
|
|
|
if let Some(error) = parse_error_response(&response) {
|
|
return Err(error);
|
|
}
|
|
|
|
let vanity = String::from_utf8_lossy(&response).trim().to_string();
|
|
if vanity.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
Ok(Some(vanity))
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn check_vanity_address_availability(
|
|
state: State<'_, WalletState>,
|
|
broadcast_node: String,
|
|
vanity_name: String,
|
|
) -> Result<VanityAvailabilityView, String> {
|
|
ensure_gui_settings_file().await?;
|
|
|
|
let socket_address = active_broadcast_socket(&broadcast_node)?;
|
|
let (short_address, public_key, private_key) = active_wallet_rpc_parts(&state)?;
|
|
let vanity_address = vanity_address_for_wallet(&vanity_name, &short_address)?;
|
|
let display_vanity = display_vanity_address(&vanity_address)
|
|
.ok_or_else(|| "Failed to build vanity address display.".to_string())?;
|
|
|
|
let response = serialized_wallet_rpc(
|
|
&state,
|
|
socket_address,
|
|
vanity_address,
|
|
RPC_VANITY_OWNER_LOOKUP as usize,
|
|
public_key,
|
|
private_key,
|
|
"Vanity availability lookup failed",
|
|
)
|
|
.await?;
|
|
|
|
if let Some(error) = parse_error_response(&response) {
|
|
return Err(error);
|
|
}
|
|
|
|
let owner = String::from_utf8_lossy(&response).trim().to_string();
|
|
if owner.is_empty() {
|
|
Ok(VanityAvailabilityView {
|
|
vanity_address: display_vanity,
|
|
available: true,
|
|
owner: None,
|
|
})
|
|
} else {
|
|
Ok(VanityAvailabilityView {
|
|
vanity_address: display_vanity,
|
|
available: false,
|
|
owner: Some(owner),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn save_history_export(
|
|
default_file_name: String,
|
|
format: String,
|
|
contents: String,
|
|
) -> Result<Option<String>, String> {
|
|
let extension = match format.trim().to_ascii_lowercase().as_str() {
|
|
"json" => "json",
|
|
"xml" => "xml",
|
|
"csv" => "csv",
|
|
_ => return Err("Export format must be json, xml, or csv.".to_string()),
|
|
};
|
|
|
|
let mut file_name = default_file_name.trim().to_string();
|
|
if file_name.is_empty() {
|
|
file_name = format!("contractless-history.{extension}");
|
|
}
|
|
if !file_name
|
|
.to_ascii_lowercase()
|
|
.ends_with(&format!(".{extension}"))
|
|
{
|
|
file_name.push('.');
|
|
file_name.push_str(extension);
|
|
}
|
|
|
|
let selected = FileDialog::new()
|
|
.set_filename(&file_name)
|
|
.add_filter(
|
|
match extension {
|
|
"json" => "JSON",
|
|
"xml" => "XML",
|
|
"csv" => "CSV",
|
|
_ => "Export",
|
|
},
|
|
&[extension],
|
|
)
|
|
.show_save_single_file()
|
|
.map_err(|err| format!("Unable to open export save dialog: {err}"))?;
|
|
|
|
let Some(mut path) = selected else {
|
|
return Ok(None);
|
|
};
|
|
|
|
if path.extension().is_none() {
|
|
path.set_extension(extension);
|
|
}
|
|
|
|
contractless::tokio::fs::write(&path, contents)
|
|
.await
|
|
.map_err(|err| format!("Unable to save history export: {err}"))?;
|
|
|
|
Ok(Some(path.to_string_lossy().into_owned()))
|
|
}
|