From 50ddbd597e1fa13cab5c04c373d1b4f1d0619979 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:47:34 +0100 Subject: [PATCH] fix(wallet): use cryptographically secure entropy for mnemonic generation The mnemonic/wallet generation was using a constant zero-byte array for entropy ([0u8; 32]), making all wallets deterministic based solely on the password. This commit introduces proper random entropy using OsRng and enables users to back up their recovery phrase. Changes: - SeedHolder::new_mnemonic() now uses OsRng for 256-bit random entropy and returns the generated mnemonic - Added SeedHolder::from_mnemonic() to recover a wallet from an existing mnemonic phrase - WalletChainStore::new_storage() returns the mnemonic for user backup - Added WalletChainStore::restore_storage() for recovery from a mnemonic - WalletCore::new_init_storage() now returns the mnemonic - Renamed reset_storage to restore_storage, which accepts a mnemonic for recovery - CLI displays the recovery phrase when a new wallet is created - RestoreKeys command now prompts for the mnemonic phrase via read_mnemonic_from_stdin() Note: The password parameter is retained for future storage encryption but is no longer used in seed derivation (empty passphrase is used instead). This means the mnemonic alone is sufficient to recover accounts. Usage: On first wallet initialization, users will see: IMPORTANT: Write down your recovery phrase and store it securely. This is the only way to recover your wallet if you lose access. Recovery phrase: word1 word2 word3 ... word24 To restore keys: wallet restore-keys --depth 5 Input recovery phrase: <24 words> Input password: --- Cargo.lock | 1 + .../configs/debug/wallet/wallet_config.json | 545 ++++++++++++++++++ integration_tests/src/lib.rs | 2 +- integration_tests/tests/wallet_ffi.rs | 8 +- key_protocol/src/key_management/mod.rs | 19 +- .../src/key_management/secret_holders.rs | 83 ++- key_protocol/src/key_protocol_core/mod.rs | 5 +- wallet-ffi/src/wallet.rs | 2 +- wallet/Cargo.toml | 1 + wallet/src/chain_storage.rs | 41 +- wallet/src/cli/mod.rs | 16 +- wallet/src/config.rs | 4 +- wallet/src/lib.rs | 28 +- wallet/src/main.rs | 10 +- 14 files changed, 725 insertions(+), 40 deletions(-) create mode 100644 integration_tests/configs/debug/wallet/wallet_config.json diff --git a/Cargo.lock b/Cargo.lock index 6e0461fd..6d9f55ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8650,6 +8650,7 @@ dependencies = [ "anyhow", "async-stream", "base58", + "bip39", "clap", "common", "env_logger", diff --git a/integration_tests/configs/debug/wallet/wallet_config.json b/integration_tests/configs/debug/wallet/wallet_config.json new file mode 100644 index 00000000..359e9eb2 --- /dev/null +++ b/integration_tests/configs/debug/wallet/wallet_config.json @@ -0,0 +1,545 @@ +{ + "sequencer_addr": "http://127.0.0.1:3040", + "seq_poll_timeout_millis": 12000, + "seq_tx_poll_max_blocks": 5, + "seq_poll_max_retries": 5, + "seq_block_poll_max_amount": 100, + "initial_accounts": [ + { + "Public": { + "account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy", + "pub_sign_key": [ + 16, + 162, + 106, + 154, + 236, + 125, + 52, + 184, + 35, + 100, + 238, + 174, + 69, + 197, + 41, + 77, + 187, + 10, + 118, + 75, + 0, + 11, + 148, + 238, + 185, + 181, + 133, + 17, + 220, + 72, + 124, + 77 + ] + } + }, + { + "Public": { + "account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw", + "pub_sign_key": [ + 113, + 121, + 64, + 177, + 204, + 85, + 229, + 214, + 178, + 6, + 109, + 191, + 29, + 154, + 63, + 38, + 242, + 18, + 244, + 219, + 8, + 208, + 35, + 136, + 23, + 127, + 207, + 237, + 216, + 169, + 190, + 27 + ] + } + }, + { + "Private": { + "account_id": "3oCG8gqdKLMegw4rRfyaMQvuPHpcASt7xwttsmnZLSkw", + "account": { + "program_owner": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "balance": 10000, + "data": [], + "nonce": 0 + }, + "key_chain": { + "secret_spending_key": [ + 251, + 82, + 235, + 1, + 146, + 96, + 30, + 81, + 162, + 234, + 33, + 15, + 123, + 129, + 116, + 0, + 84, + 136, + 176, + 70, + 190, + 224, + 161, + 54, + 134, + 142, + 154, + 1, + 18, + 251, + 242, + 189 + ], + "private_key_holder": { + "nullifier_secret_key": [ + 29, + 250, + 10, + 187, + 35, + 123, + 180, + 250, + 246, + 97, + 216, + 153, + 44, + 156, + 16, + 93, + 241, + 26, + 174, + 219, + 72, + 84, + 34, + 247, + 112, + 101, + 217, + 243, + 189, + 173, + 75, + 20 + ], + "incoming_viewing_secret_key": [ + 251, + 201, + 22, + 154, + 100, + 165, + 218, + 108, + 163, + 190, + 135, + 91, + 145, + 84, + 69, + 241, + 46, + 117, + 217, + 110, + 197, + 248, + 91, + 193, + 14, + 104, + 88, + 103, + 67, + 153, + 182, + 158 + ], + "outgoing_viewing_secret_key": [ + 25, + 67, + 121, + 76, + 175, + 100, + 30, + 198, + 105, + 123, + 49, + 169, + 75, + 178, + 75, + 210, + 100, + 143, + 210, + 243, + 228, + 243, + 21, + 18, + 36, + 84, + 164, + 186, + 139, + 113, + 214, + 12 + ] + }, + "nullifer_public_key": [ + 63, + 202, + 178, + 231, + 183, + 82, + 237, + 212, + 216, + 221, + 215, + 255, + 153, + 101, + 177, + 161, + 254, + 210, + 128, + 122, + 54, + 190, + 230, + 151, + 183, + 64, + 225, + 229, + 113, + 1, + 228, + 97 + ], + "incoming_viewing_public_key": [ + 3, + 235, + 139, + 131, + 237, + 177, + 122, + 189, + 6, + 177, + 167, + 178, + 202, + 117, + 246, + 58, + 28, + 65, + 132, + 79, + 220, + 139, + 119, + 243, + 187, + 160, + 212, + 121, + 61, + 247, + 116, + 72, + 205 + ] + } + } + }, + { + "Private": { + "account_id": "AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9er6xX", + "account": { + "program_owner": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "balance": 20000, + "data": [], + "nonce": 0 + }, + "key_chain": { + "secret_spending_key": [ + 238, + 171, + 241, + 69, + 111, + 217, + 85, + 64, + 19, + 82, + 18, + 189, + 32, + 91, + 78, + 175, + 107, + 7, + 109, + 60, + 52, + 44, + 243, + 230, + 72, + 244, + 192, + 92, + 137, + 33, + 118, + 254 + ], + "private_key_holder": { + "nullifier_secret_key": [ + 25, + 211, + 215, + 119, + 57, + 223, + 247, + 37, + 245, + 144, + 122, + 29, + 118, + 245, + 83, + 228, + 23, + 9, + 101, + 120, + 88, + 33, + 238, + 207, + 128, + 61, + 110, + 2, + 89, + 62, + 164, + 13 + ], + "incoming_viewing_secret_key": [ + 193, + 181, + 14, + 196, + 142, + 84, + 15, + 65, + 128, + 101, + 70, + 196, + 241, + 47, + 130, + 221, + 23, + 146, + 161, + 237, + 221, + 40, + 19, + 126, + 59, + 15, + 169, + 236, + 25, + 105, + 104, + 231 + ], + "outgoing_viewing_secret_key": [ + 20, + 170, + 220, + 108, + 41, + 23, + 155, + 217, + 247, + 190, + 175, + 168, + 247, + 34, + 105, + 134, + 114, + 74, + 104, + 91, + 211, + 62, + 126, + 13, + 130, + 100, + 241, + 214, + 250, + 236, + 38, + 150 + ] + }, + "nullifer_public_key": [ + 192, + 251, + 166, + 243, + 167, + 236, + 84, + 249, + 35, + 136, + 130, + 172, + 219, + 225, + 161, + 139, + 229, + 89, + 243, + 125, + 194, + 213, + 209, + 30, + 23, + 174, + 100, + 244, + 124, + 74, + 140, + 47 + ], + "incoming_viewing_public_key": [ + 2, + 181, + 98, + 93, + 216, + 241, + 241, + 110, + 58, + 198, + 119, + 174, + 250, + 184, + 1, + 204, + 200, + 173, + 44, + 238, + 37, + 247, + 170, + 156, + 100, + 254, + 116, + 242, + 28, + 183, + 187, + 77, + 255 + ] + } + } + } + ] +} \ No newline at end of file diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 08e7cf9f..6103bdef 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -256,7 +256,7 @@ impl TestContext { let config_overrides = WalletConfigOverrides::default(); let wallet_password = "test_pass".to_owned(); - let wallet = WalletCore::new_init_storage( + let (wallet, _mnemonic) = WalletCore::new_init_storage( config_path, storage_path, Some(config_overrides), diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index dad4c79e..b1d41bb7 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -216,7 +216,13 @@ fn new_wallet_rust_with_default_config(password: &str) -> Result { let config_path = tempdir.path().join("wallet_config.json"); let storage_path = tempdir.path().join("storage.json"); - WalletCore::new_init_storage(config_path, storage_path, None, password.to_owned()) + let (core, _mnemonic) = WalletCore::new_init_storage( + config_path.to_path_buf(), + storage_path.to_path_buf(), + None, + password.to_string(), + )?; + Ok(core) } fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> { diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index dcdaff45..c038c415 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -42,10 +42,10 @@ impl KeyChain { } #[must_use] - pub fn new_mnemonic(passphrase: String) -> Self { + pub fn new_mnemonic(passphrase: &str) -> (Self, bip39::Mnemonic) { // Currently dropping SeedHolder at the end of initialization. // Not entirely sure if we need it in the future. - let seed_holder = SeedHolder::new_mnemonic(passphrase); + let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase); let secret_spending_key = seed_holder.produce_top_secret_key_holder(); let private_key_holder = secret_spending_key.produce_private_key_holder(None); @@ -53,12 +53,15 @@ impl KeyChain { let nullifier_public_key = private_key_holder.generate_nullifier_public_key(); let viewing_public_key = private_key_holder.generate_viewing_public_key(); - Self { - secret_spending_key, - private_key_holder, - nullifier_public_key, - viewing_public_key, - } + ( + Self { + secret_spending_key, + private_key_holder, + nullifier_public_key, + viewing_public_key, + }, + mnemonic, + ) } #[must_use] diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index 45e640da..f2e590c7 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -8,8 +8,6 @@ use rand::{RngCore as _, rngs::OsRng}; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, digest::FixedOutput as _}; -const NSSA_ENTROPY_BYTES: [u8; 32] = [0; 32]; - /// Seed holder. Non-clonable to ensure that different holders use different seeds. /// Produces `TopSecretKeyHolder` objects. #[derive(Debug)] @@ -49,9 +47,23 @@ impl SeedHolder { } #[must_use] - pub fn new_mnemonic(passphrase: String) -> Self { - let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES) - .expect("Enthropy must be a multiple of 32 bytes"); + pub fn new_mnemonic(passphrase: &str) -> (Self, Mnemonic) { + let mut entropy_bytes: [u8; 32] = [0; 32]; + OsRng.fill_bytes(&mut entropy_bytes); + + let mnemonic = + Mnemonic::from_entropy(&entropy_bytes).expect("Entropy must be a multiple of 32 bytes"); + let seed_wide = mnemonic.to_seed(passphrase); + + ( + Self { + seed: seed_wide.to_vec(), + }, + mnemonic, + ) + } + + pub fn from_mnemonic(mnemonic: &Mnemonic, passphrase: &str) -> Self { let seed_wide = mnemonic.to_seed(passphrase); Self { @@ -176,12 +188,63 @@ mod tests { } #[test] - fn two_seeds_generated_same_from_same_mnemonic() { - let mnemonic = "test_pass"; + fn two_seeds_recovered_same_from_same_mnemonic() { + let passphrase = "test_pass"; - let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_owned()); - let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_owned()); + // Generate a mnemonic with random entropy + let (original_seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase); - assert_eq!(seed_holder1.seed, seed_holder2.seed); + // Recover from the same mnemonic + let recovered_seed_holder = SeedHolder::from_mnemonic(&mnemonic, passphrase); + + assert_eq!(original_seed_holder.seed, recovered_seed_holder.seed); + } + + #[test] + fn new_mnemonic_generates_different_seeds_each_time() { + let (seed_holder1, mnemonic1) = SeedHolder::new_mnemonic(""); + let (seed_holder2, mnemonic2) = SeedHolder::new_mnemonic(""); + + // Different entropy should produce different mnemonics and seeds + assert_ne!(mnemonic1.to_string(), mnemonic2.to_string()); + assert_ne!(seed_holder1.seed, seed_holder2.seed); + } + + #[test] + fn new_mnemonic_generates_24_word_phrase() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + // 256 bits of entropy produces a 24-word mnemonic + let word_count = mnemonic.to_string().split_whitespace().count(); + assert_eq!(word_count, 24); + } + + #[test] + fn new_mnemonic_produces_valid_seed_length() { + let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic(""); + + assert_eq!(seed_holder.seed.len(), 64); + } + + #[test] + fn different_passphrases_produce_different_seeds() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + let seed_with_pass_a = SeedHolder::from_mnemonic(&mnemonic, "password_a"); + let seed_with_pass_b = SeedHolder::from_mnemonic(&mnemonic, "password_b"); + + // Same mnemonic but different passphrases should produce different seeds + assert_ne!(seed_with_pass_a.seed, seed_with_pass_b.seed); + } + + #[test] + fn empty_passphrase_is_deterministic() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + let seed1 = SeedHolder::from_mnemonic(&mnemonic, ""); + let seed2 = SeedHolder::from_mnemonic(&mnemonic, ""); + + // Same mnemonic and passphrase should always produce the same seed + assert_eq!(seed1.seed, seed2.seed); } } diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index 8232d9f4..8186865f 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -181,11 +181,12 @@ impl NSSAUserData { impl Default for NSSAUserData { fn default() -> Self { + let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic(""); Self::new_with_accounts( BTreeMap::new(), BTreeMap::new(), - KeyTreePublic::new(&SeedHolder::new_mnemonic("default".to_owned())), - KeyTreePrivate::new(&SeedHolder::new_mnemonic("default".to_owned())), + KeyTreePublic::new(&seed_holder), + KeyTreePrivate::new(&seed_holder), ) .unwrap() } diff --git a/wallet-ffi/src/wallet.rs b/wallet-ffi/src/wallet.rs index 9117d0ee..472320ac 100644 --- a/wallet-ffi/src/wallet.rs +++ b/wallet-ffi/src/wallet.rs @@ -112,7 +112,7 @@ pub unsafe extern "C" fn wallet_ffi_create_new( }; match WalletCore::new_init_storage(config_path, storage_path, None, password) { - Ok(core) => { + Ok((core, _mnemonic)) => { let wrapper = Box::new(WalletWrapper { core: Mutex::new(core), }); diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 2e628790..4617543a 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -15,6 +15,7 @@ key_protocol.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } token_core.workspace = true amm_core.workspace = true +bip39.workspace = true anyhow.workspace = true thiserror.workspace = true diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs index a88e0b8b..1fbbbee0 100644 --- a/wallet/src/chain_storage.rs +++ b/wallet/src/chain_storage.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, HashMap, btree_map::Entry}; use anyhow::Result; +use bip39::Mnemonic; use key_protocol::{ key_management::{ key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex}, @@ -95,7 +96,7 @@ impl WalletChainStore { }) } - pub fn new_storage(config: WalletConfig, password: String) -> Result { + pub fn new_storage(config: WalletConfig, password: String) -> Result<(Self, Mnemonic)> { let mut public_init_acc_map = BTreeMap::new(); let mut private_init_acc_map = BTreeMap::new(); @@ -115,13 +116,43 @@ impl WalletChainStore { } } - let public_tree = KeyTreePublic::new(&SeedHolder::new_mnemonic(password.clone())); - let private_tree = KeyTreePrivate::new(&SeedHolder::new_mnemonic(password)); + // TODO: Use password for storage encryption + let _ = password; + let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + let public_tree = KeyTreePublic::new(&seed_holder); + let private_tree = KeyTreePrivate::new(&seed_holder); + + Ok(( + Self { + user_data: NSSAUserData::new_with_accounts( + public_init_acc_map, + private_init_acc_map, + public_tree, + private_tree, + )?, + wallet_config: config, + labels: HashMap::new(), + }, + mnemonic, + )) + } + + /// Restore storage from an existing mnemonic phrase. + pub fn restore_storage( + config: WalletConfig, + mnemonic: &Mnemonic, + password: &str, + ) -> Result { + // TODO: Use password for storage encryption + let _ = password; + let seed_holder = SeedHolder::from_mnemonic(mnemonic, ""); + let public_tree = KeyTreePublic::new(&seed_holder); + let private_tree = KeyTreePrivate::new(&seed_holder); Ok(Self { user_data: NSSAUserData::new_with_accounts( - public_init_acc_map, - private_init_acc_map, + BTreeMap::new(), + BTreeMap::new(), public_tree, private_tree, )?, diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 85e792cc..edc1f4cf 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -1,6 +1,7 @@ -use std::{io::Write as _, path::PathBuf}; +use std::{io::Write as _, path::PathBuf, str::FromStr}; use anyhow::{Context as _, Result}; +use bip39::Mnemonic; use clap::{Parser, Subcommand}; use common::{HashType, transaction::NSSATransaction}; use futures::TryFutureExt as _; @@ -162,8 +163,9 @@ pub async fn execute_subcommand( config_subcommand.handle_subcommand(wallet_core).await? } Command::RestoreKeys { depth } => { + let mnemonic = read_mnemonic_from_stdin()?; let password = read_password_from_stdin()?; - wallet_core.reset_storage(password)?; + wallet_core.restore_storage(&mnemonic, &password)?; execute_keys_restoration(wallet_core, depth).await?; SubcommandReturnValue::Empty @@ -207,6 +209,16 @@ pub fn read_password_from_stdin() -> Result { Ok(password.trim().to_owned()) } +pub fn read_mnemonic_from_stdin() -> Result { + let mut phrase = String::new(); + + print!("Input recovery phrase: "); + std::io::stdout().flush()?; + std::io::stdin().read_line(&mut phrase)?; + + Mnemonic::from_str(phrase.trim()).context("Invalid mnemonic phrase") +} + pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) -> Result<()> { wallet_core .storage diff --git a/wallet/src/config.rs b/wallet/src/config.rs index 60164e86..91505a37 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -222,10 +222,10 @@ impl Default for WalletConfig { let public_key2 = nssa::PublicKey::new_from_private_key(&pub_sign_key2); let public_account_id2 = nssa::AccountId::from(&public_key2); - let key_chain1 = KeyChain::new_mnemonic("default_private_account_1".to_owned()); + let (key_chain1, _) = KeyChain::new_mnemonic("default_private_account_1"); let private_account_id1 = nssa::AccountId::from(&key_chain1.nullifier_public_key); - let key_chain2 = KeyChain::new_mnemonic("default_private_account_2".to_owned()); + let (key_chain2, _) = KeyChain::new_mnemonic("default_private_account_2"); let private_account_id2 = nssa::AccountId::from(&key_chain2.nullifier_public_key); Self { diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index bf82a1bd..d73729d9 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -11,6 +11,7 @@ use std::path::PathBuf; use anyhow::{Context as _, Result}; +use bip39::Mnemonic; use chain_storage::WalletChainStore; use common::{HashType, transaction::NSSATransaction}; use config::WalletConfig; @@ -116,14 +117,23 @@ impl WalletCore { storage_path: PathBuf, config_overrides: Option, password: String, - ) -> Result { - Self::new( + ) -> Result<(Self, Mnemonic)> { + let mut mnemonic_out = None; + let wallet = Self::new( config_path, storage_path, config_overrides, - |config| WalletChainStore::new_storage(config, password), + |config| { + let (storage, mnemonic) = WalletChainStore::new_storage(config, password)?; + mnemonic_out = Some(mnemonic); + Ok(storage) + }, 0, - ) + )?; + Ok(( + wallet, + mnemonic_out.expect("mnemonic should be set after new_storage"), + )) } fn new( @@ -189,9 +199,13 @@ impl WalletCore { &self.storage } - /// Reset storage. - pub fn reset_storage(&mut self, password: String) -> Result<()> { - self.storage = WalletChainStore::new_storage(self.storage.wallet_config.clone(), password)?; + /// Restore storage from an existing mnemonic phrase. + pub fn restore_storage(&mut self, mnemonic: &Mnemonic, password: &str) -> Result<()> { + self.storage = WalletChainStore::restore_storage( + self.storage.wallet_config.clone(), + mnemonic, + password, + )?; Ok(()) } diff --git a/wallet/src/main.rs b/wallet/src/main.rs index e055bd63..19d9b623 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -46,13 +46,21 @@ async fn main() -> Result<()> { println!("Persistent storage not found, need to execute setup"); let password = read_password_from_stdin()?; - let wallet = WalletCore::new_init_storage( + let (wallet, mnemonic) = WalletCore::new_init_storage( config_path, storage_path, Some(config_overrides), password, )?; + println!(); + println!("IMPORTANT: Write down your recovery phrase and store it securely."); + println!("This is the only way to recover your wallet if you lose access."); + println!(); + println!("Recovery phrase:"); + println!(" {}", mnemonic); + println!(); + wallet.store_persistent_data().await?; wallet };