From 365eafa27aaa636e5e9febb2730afb87608f6b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Papie=C5=BC?= Date: Thu, 8 Aug 2024 16:29:08 +0200 Subject: [PATCH] feat(wallet): export seed words --- src-tauri/src/internal_wallet.rs | 28 +++--- src-tauri/src/main.rs | 23 ++++- .../SideBar/components/Settings.tsx | 92 ++++++++++++++++--- src/hooks/useGetSeedWords.ts | 26 ++++++ src/utils/truncateString.ts | 6 ++ 5 files changed, 151 insertions(+), 24 deletions(-) create mode 100644 src/hooks/useGetSeedWords.ts create mode 100644 src/utils/truncateString.ts diff --git a/src-tauri/src/internal_wallet.rs b/src-tauri/src/internal_wallet.rs index 2fb52d96..0f87d20d 100644 --- a/src-tauri/src/internal_wallet.rs +++ b/src-tauri/src/internal_wallet.rs @@ -1,5 +1,4 @@ use anyhow::anyhow; -use keyring::Entry; use log::{info, warn}; use rand::Rng; use serde::{Deserialize, Serialize}; @@ -21,6 +20,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"; @@ -67,17 +67,10 @@ impl InternalWallet { view_key_private_hex: "".to_string(), seed_words_encrypted_base58: "".to_string(), spend_public_key_hex: "".to_string(), + passphrase: "".to_string(), }; - let entry = Entry::new("com.tari.universe", "internal_wallet")?; - - let passphrase = SafePassword::from(match entry.get_password() { - Ok(pass) => pass, - Err(_) => { - let passphrase = generate_password(32); - entry.set_password(&passphrase)?; - passphrase - } - }); + let passphrase = generate_password(32); + let safe_password = SafePassword::from(passphrase.clone()); let seed = CipherSeed::new(); // TODO: Don't print out the seed words lol @@ -86,7 +79,7 @@ impl InternalWallet { dbg!(seed_words.get_word(i).unwrap()); info!(target: LOG_TARGET, "Seed: {}:{}", i+1, seed_words.get_word(i).unwrap()); } - let seed_file = seed.encipher(Some(passphrase))?; + let seed_file = seed.encipher(Some(safe_password))?; config.seed_words_encrypted_base58 = seed_file.to_base58(); let comms_key_manager = KeyManager::::from( @@ -115,6 +108,7 @@ impl InternalWallet { config.tari_address_base58 = tari_address.to_base58(); config.view_key_private_hex = view_key_private.to_hex(); config.spend_public_key_hex = comms_pub_key.to_hex(); + config.passphrase = passphrase; Ok(( Self { tari_address, @@ -124,6 +118,15 @@ impl InternalWallet { )) } + pub fn decrypt_seed_words(&self) -> Result { + let safe_password = SafePassword::from(self.config.passphrase.clone()); + 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(safe_password))?; + 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() } @@ -154,4 +157,5 @@ pub struct WalletConfig { view_key_private_hex: String, spend_public_key_hex: String, seed_words_encrypted_base58: String, + passphrase: String, } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 64f1f376..2bd6f104 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -122,6 +122,26 @@ async fn setup_application<'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, @@ -376,7 +396,8 @@ fn main() { status, start_mining, stop_mining, - set_mode + set_mode, + 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 f6012d8b..b911ed5d 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,15 +11,27 @@ 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'; 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; @@ -36,6 +49,22 @@ const Settings: React.FC = () => { handleClose(); }; + 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 ( <> @@ -60,7 +89,48 @@ const Settings: React.FC = () => { - + + + Seed Words + + + + {showSeedWords + ? truncateString(seedWords.join(','), 50) + : '****************************************************'} + + {seedWordsFetching ? ( + + ) : ( + <> + + {showSeedWords ? ( + + ) : ( + + )} + + + + + + + + )} + + + + Random { onChange={handleChange} /> - - - - - + + + + + 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