Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

feat: restore seed from bip39 seed phrase #385

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions lib/wallet/seed.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -20,6 +21,8 @@ class Seed extends StatefulWidget {
class _SeedState extends State<Seed> {
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
Expand All @@ -31,6 +34,11 @@ class _SeedState extends State<Seed> {
void initState() {
_callGetSeedPhrase();
super.initState();
controller.addListener(() {
setState(() {
valid = controller.text.trim().split(" ").where((word) => word.isNotEmpty).length == 12;
});
});
}

Future<void> _callGetSeedPhrase() async {
Expand Down Expand Up @@ -121,6 +129,46 @@ class _SeedState extends State<Seed> {
),
),
),
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,
Expand Down
4 changes: 4 additions & 0 deletions rust/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ pub fn get_balance() -> Result<Balance> {
wallet::get_balance()
}

pub fn restore(mnemonic: String) -> Result<()> {
wallet::restore(mnemonic)
}

pub fn get_address() -> Result<Address> {
Ok(Address::new(wallet::get_address()?.to_string()))
}
Expand Down
55 changes: 28 additions & 27 deletions rust/src/seed.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;

use anyhow::bail;
use anyhow::Result;
Expand All @@ -12,23 +13,24 @@ use sha2::Sha256;

#[derive(Clone)]
pub struct Bip39Seed {
path: PathBuf,
mnemonic: Mnemonic,
}

impl Bip39Seed {
pub fn new() -> Result<Self> {
pub fn new(path: PathBuf) -> Result<Self> {
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<Self> {
pub fn initialize(seed_file: PathBuf) -> Result<Self> {
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)?
Expand All @@ -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<ExtendedPrivKey> {
let mut ext_priv_key_seed = [0u8; 64];

Expand All @@ -58,34 +68,24 @@ impl Bip39Seed {
}

// Read the entropy used to generate Mnemonic from disk
fn read_from(path: &Path) -> Result<Self> {
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<Self> {
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<Vec<u8>> for Bip39Seed {
type Error = anyhow::Error;
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
let mnemonic = Mnemonic::from_entropy(&bytes)?;
Ok(Bip39Seed { mnemonic })
}
}

#[cfg(test)]
mod tests {
use std::env::temp_dir;
Expand All @@ -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());
}
Expand All @@ -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"
Expand Down
7 changes: 6 additions & 1 deletion rust/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down Expand Up @@ -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<BackgroundProcessor> {
let wallet = { (*get_wallet()?).clone() };
wallet.run_ldk().await
Expand Down