diff --git a/lib/wallet/seed.dart b/lib/wallet/seed.dart index 6b81df07..90094784 100644 --- a/lib/wallet/seed.dart +++ b/lib/wallet/seed.dart @@ -1,4 +1,5 @@ import 'package:f_logs/f_logs.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; import 'package:provider/provider.dart'; @@ -20,6 +21,8 @@ class Seed extends StatefulWidget { class _SeedState extends State { bool checked = false; bool visibility = false; + final controller = TextEditingController(); + bool valid = false; // initialise the phrase with empty words - in order for the widget to not throw // an error while waiting for the rust api. Seems to be the easiest way of handling @@ -31,6 +34,11 @@ class _SeedState extends State { void initState() { _callGetSeedPhrase(); super.initState(); + controller.addListener(() { + setState(() { + valid = controller.text.trim().split(" ").where((word) => word.isNotEmpty).length == 12; + }); + }); } Future _callGetSeedPhrase() async { @@ -121,6 +129,46 @@ class _SeedState extends State { ), ), ), + Container( + margin: const EdgeInsets.only(bottom: 50), + child: Visibility( + // only show restore in dev mode for now. + visible: kDebugMode, + child: Row( + children: [ + Expanded( + child: TextField( + controller: controller, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: 'Enter your recovery BIP39 seed phrase'), + ), + ), + const SizedBox(width: 15), + ElevatedButton( + onPressed: !valid + ? null + : () async { + try { + await api.restore(mnemonic: controller.text); + setState(() { + phrase = controller.text + .split(" ") + .where((word) => word.isNotEmpty) + .toList(); + }); + } on FfiException catch (error) { + FLog.error(text: "Failed to restore seed.", exception: error); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + backgroundColor: Colors.red, + content: Text("Failed to restore seed."), + )); + } + }, + child: const Text("Recover")) + ], + )), + ), Center( child: Column( mainAxisAlignment: MainAxisAlignment.end, diff --git a/rust/src/api.rs b/rust/src/api.rs index aef83e35..a08f5db6 100644 --- a/rust/src/api.rs +++ b/rust/src/api.rs @@ -88,6 +88,10 @@ pub fn get_balance() -> Result { wallet::get_balance() } +pub fn restore(mnemonic: String) -> Result<()> { + wallet::restore(mnemonic) +} + pub fn get_address() -> Result
{ Ok(Address::new(wallet::get_address()?.to_string())) } diff --git a/rust/src/seed.rs b/rust/src/seed.rs index 3f43ece5..67e2f3a7 100644 --- a/rust/src/seed.rs +++ b/rust/src/seed.rs @@ -1,4 +1,5 @@ -use std::path::Path; +use std::path::PathBuf; +use std::str::FromStr; use anyhow::bail; use anyhow::Result; @@ -12,23 +13,24 @@ use sha2::Sha256; #[derive(Clone)] pub struct Bip39Seed { + path: PathBuf, mnemonic: Mnemonic, } impl Bip39Seed { - pub fn new() -> Result { + pub fn new(path: PathBuf) -> Result { let mut rng = rand::thread_rng(); let mnemonic = Mnemonic::generate_in_with(&mut rng, Language::English, 12)?; - Ok(Self { mnemonic }) + Ok(Self { mnemonic, path }) } /// Initialise a [`Seed`] from a path. /// Generates new seed if there was no seed found in the given path - pub fn initialize(seed_file: &Path) -> Result { + pub fn initialize(seed_file: PathBuf) -> Result { let seed = if !seed_file.exists() { tracing::info!("No seed found. Generating new seed"); - let seed = Self::new()?; - seed.write_to(seed_file)?; + let seed = Self::new(seed_file)?; + seed.write(false)?; seed } else { Bip39Seed::read_from(seed_file)? @@ -42,6 +44,14 @@ impl Bip39Seed { self.mnemonic.to_seed_normalized("") } + pub fn restore(&mut self, mnemonic: String) -> Result<()> { + // TODO: we should probably think about creating a backup of the existing seed instead of + // simply overwriting it. + self.mnemonic = Mnemonic::from_str(&mnemonic)?; + self.write(true)?; + Ok(()) + } + pub fn derive_extended_priv_key(&self, network: Network) -> Result { let mut ext_priv_key_seed = [0u8; 64]; @@ -58,34 +68,24 @@ impl Bip39Seed { } // Read the entropy used to generate Mnemonic from disk - fn read_from(path: &Path) -> Result { - let bytes = std::fs::read(path)?; - - let seed: Bip39Seed = TryInto::try_into(bytes) - .map_err(|_| anyhow::anyhow!("Cannot read the stored entropy"))?; - Ok(seed) + fn read_from(path: PathBuf) -> Result { + let bytes = std::fs::read(path.clone())?; + let mnemonic = Mnemonic::from_entropy(&bytes)?; + Ok(Bip39Seed { mnemonic, path }) } // Store the entropy used to generate Mnemonic on disk - fn write_to(&self, path: &Path) -> Result<()> { - if path.exists() { - let path = path.display(); + fn write(&self, restore: bool) -> Result<()> { + if self.path.exists() && !restore { + let path = self.path.display(); bail!("Refusing to overwrite file at {path}") } - std::fs::write(path, &self.mnemonic.to_entropy())?; + std::fs::write(self.path.as_path(), &self.mnemonic.to_entropy())?; Ok(()) } } -impl TryFrom> for Bip39Seed { - type Error = anyhow::Error; - fn try_from(bytes: Vec) -> Result { - let mnemonic = Mnemonic::from_entropy(&bytes)?; - Ok(Bip39Seed { mnemonic }) - } -} - #[cfg(test)] mod tests { use std::env::temp_dir; @@ -94,7 +94,8 @@ mod tests { #[test] fn create_bip39_seed() { - let seed = Bip39Seed::new().expect("seed to be generated"); + let path = temp_dir(); + let seed = Bip39Seed::new(path).expect("seed to be generated"); let phrase = seed.get_seed_phrase(); assert_eq!(12, phrase.len()); } @@ -103,8 +104,8 @@ mod tests { fn reinitialised_seed_is_the_same() { let mut path = temp_dir(); path.push("seed"); - let seed_1 = Bip39Seed::initialize(&path).unwrap(); - let seed_2 = Bip39Seed::initialize(&path).unwrap(); + let seed_1 = Bip39Seed::initialize(path.clone()).unwrap(); + let seed_2 = Bip39Seed::initialize(path).unwrap(); assert_eq!( seed_1.mnemonic, seed_2.mnemonic, "Reinitialised wallet should contain the same mnemonic" diff --git a/rust/src/wallet.rs b/rust/src/wallet.rs index 4660e140..9f131b77 100644 --- a/rust/src/wallet.rs +++ b/rust/src/wallet.rs @@ -82,7 +82,7 @@ impl Wallet { .context(format!("Could not create data dir for {network}"))?; } let seed_path = data_dir.join("seed"); - let seed = Bip39Seed::initialize(&seed_path)?; + let seed = Bip39Seed::initialize(seed_path)?; let ext_priv_key = seed.derive_extended_priv_key(network)?; let client = Client::new(&electrum_str)?; @@ -335,6 +335,11 @@ pub fn init_wallet(data_dir: &Path) -> Result<()> { Ok(()) } +pub fn restore(mnemonic: String) -> Result<()> { + tracing::info!("Restoring wallet from mnemonic"); + get_wallet()?.seed.restore(mnemonic) +} + pub async fn run_ldk() -> Result { let wallet = { (*get_wallet()?).clone() }; wallet.run_ldk().await