diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8f6aa108..a07d391f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2527,6 +2527,12 @@ name = "keyring" version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c163ef0b9da5ccf44ae4d7c9d24fb1a8750aa1969d484865fc1eedc44b26c09" +dependencies = [ + "byteorder", + "linux-keyutils", + "security-framework", + "windows-sys 0.52.0", +] [[package]] name = "kuchikiki" @@ -2590,6 +2596,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-keyutils" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b0331db2..37b60c59 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -54,7 +54,7 @@ sanitize-filename = "0.5" async-trait = "0.1.81" sysinfo = "0.31.2" log4rs = "1.3.0" -keyring = "3.0.5" +keyring = { version = "3.0.5", features = ["windows-native", "apple-native", "linux-native"] } nix = { version = "0.29.0", features = ["signal"] } # static bind lzma xz2 = { version = "0.1.7", features = ["static"] } diff --git a/src-tauri/src/internal_wallet.rs b/src-tauri/src/internal_wallet.rs index 2fb52d96..ea1ce6a2 100644 --- a/src-tauri/src/internal_wallet.rs +++ b/src-tauri/src/internal_wallet.rs @@ -21,6 +21,7 @@ use tari_core::transactions::key_manager::{ TransactionKeyManagerInterface, }; use tari_key_manager::mnemonic::{Mnemonic, MnemonicLanguage}; +use tari_key_manager::SeedWords; use tari_utilities::hex::Hex; const KEY_MANAGER_COMMS_SECRET_KEY_BRANCH_KEY: &str = "comms"; @@ -80,7 +81,6 @@ impl InternalWallet { }); let seed = CipherSeed::new(); - // TODO: Don't print out the seed words lol let seed_words = seed.to_mnemonic(MnemonicLanguage::English, None).unwrap(); for i in 0..seed_words.len() { dbg!(seed_words.get_word(i).unwrap()); @@ -124,6 +124,17 @@ impl InternalWallet { )) } + pub fn decrypt_seed_words(&self) -> Result { + let entry = Entry::new("com.tari.universe", "internal_wallet")?; + + let passphrase = SafePassword::from(entry.get_password()?); + let seed_binary = Vec::::from_base58(&self.config.seed_words_encrypted_base58) + .map_err(|e| anyhow!(e.to_string()))?; + let seed = CipherSeed::from_enciphered_bytes(&seed_binary, Some(passphrase))?; + let seed_words = seed.to_mnemonic(MnemonicLanguage::English, None)?; + Ok(seed_words) + } + pub fn get_view_key(&self) -> String { self.config.view_key_private_hex.clone() } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 94487562..09579c58 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -253,6 +253,26 @@ async fn set_auto_mining<'r>( Ok(()) } +#[tauri::command] +async fn get_seed_words<'r>( + _window: tauri::Window, + _state: tauri::State<'r, UniverseAppState>, + app: tauri::AppHandle, +) -> Result, String> { + let config_path = app.path_resolver().app_config_dir().unwrap(); + let internal_wallet = InternalWallet::load_or_create(config_path) + .await + .map_err(|e| e.to_string())?; + let seed_words = internal_wallet + .decrypt_seed_words() + .map_err(|e| e.to_string())?; + let mut res = vec![]; + for i in 0..seed_words.len() { + res.push(seed_words.get_word(i).unwrap().clone()); + } + Ok(res) +} + #[tauri::command] async fn start_mining<'r>( window: tauri::Window, @@ -537,7 +557,8 @@ fn main() { stop_mining, set_auto_mining, set_mode, - open_log_dir + open_log_dir, + get_seed_words ]) .build(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/containers/SideBar/components/Settings.tsx b/src/containers/SideBar/components/Settings.tsx index 5fbc0bed..f830fa3b 100644 --- a/src/containers/SideBar/components/Settings.tsx +++ b/src/containers/SideBar/components/Settings.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { CgEye, CgEyeAlt, CgCopy } from 'react-icons/cg'; import { IconButton, Dialog, @@ -10,16 +11,28 @@ import { Box, Typography, Divider, + CircularProgress, + Tooltip, } from '@mui/material'; import { IoSettingsOutline, IoClose } from 'react-icons/io5'; +import { useGetSeedWords } from '../../../hooks/useGetSeedWords'; +import truncateString from '../../../utils/truncateString'; import { invoke } from '@tauri-apps/api/tauri'; const Settings: React.FC = () => { const [open, setOpen] = useState(false); const [formState, setFormState] = useState({ field1: '', field2: '' }); + const [showSeedWords, setShowSeedWords] = useState(false); + const [isCopyTooltipHidden, setIsCopyTooltipHidden] = useState(true); + const { seedWords, getSeedWords, seedWordsFetched, seedWordsFetching } = + useGetSeedWords(); const handleClickOpen = () => setOpen(true); - const handleClose = () => setOpen(false); + const handleClose = () => { + setOpen(false); + setFormState({ field1: '', field2: '' }); + setShowSeedWords(false); + }; const handleChange = (event: React.ChangeEvent) => { const { name, value } = event.target; @@ -47,6 +60,22 @@ const Settings: React.FC = () => { }); }; + const toggleSeedWordsVisibility = async () => { + if (!seedWordsFetched) { + await getSeedWords(); + } + setShowSeedWords((p) => !p); + }; + + const copySeedWords = async () => { + if (!seedWordsFetched) { + await getSeedWords(); + } + setIsCopyTooltipHidden(false); + navigator.clipboard.writeText(seedWords.join(',')); + setTimeout(() => setIsCopyTooltipHidden(true), 1000); + }; + return ( <> @@ -71,7 +100,48 @@ const Settings: React.FC = () => { - + + + Seed Words + + + + {showSeedWords + ? truncateString(seedWords.join(','), 50) + : '****************************************************'} + + {seedWordsFetching ? ( + + ) : ( + <> + + {showSeedWords ? ( + + ) : ( + + )} + + + + + + + + )} + + + + Random { Open logs directory + + + + + diff --git a/src/hooks/useGetSeedWords.ts b/src/hooks/useGetSeedWords.ts new file mode 100644 index 00000000..d604ed62 --- /dev/null +++ b/src/hooks/useGetSeedWords.ts @@ -0,0 +1,26 @@ +import { useCallback, useState } from 'react'; +import { invoke } from '@tauri-apps/api/tauri'; + +export function useGetSeedWords() { + const [seedWords, setSeedWords] = useState([]); + const [seedWordsFetching, setSeedWordsFetching] = useState(false); + + const getSeedWords = useCallback(async () => { + setSeedWordsFetching(true); + try { + const seedWords = await invoke('get_seed_words') as string[]; + setSeedWords(seedWords); + } catch (e) { + console.error('Could not get seed words', e); + } finally { + setSeedWordsFetching(false); + } + }, []); + + return { + seedWords, + getSeedWords, + seedWordsFetched: seedWords.length > 0, + seedWordsFetching, + }; +} diff --git a/src/utils/truncateString.ts b/src/utils/truncateString.ts new file mode 100644 index 00000000..c06e87ed --- /dev/null +++ b/src/utils/truncateString.ts @@ -0,0 +1,6 @@ +const truncateString = (str: string, num: number): string => { + if (str.length <= num) return str; + return str.slice(0, num) + "..."; +}; + +export default truncateString; \ No newline at end of file