installer: add recover seed step

This commit is contained in:
edouard 2023-02-13 12:56:17 +01:00
parent 037c4d025d
commit c70ac11476
6 changed files with 240 additions and 3 deletions

View File

@ -16,6 +16,7 @@ pub enum Message {
Exit(PathBuf),
Clibpboard(String),
Next,
Skip,
Previous,
Install,
Close,
@ -29,6 +30,8 @@ pub enum Message {
ImportXpub(usize, Result<DescriptorPublicKey, Error>),
ConnectedHardwareWallets(Vec<HardwareWallet>),
WalletRegistered(Result<(Fingerprint, Option<[u8; 32]>), Error>),
MnemonicWord(usize, String),
ImportMnemonic(bool),
}
#[derive(Debug, Clone)]

View File

@ -16,7 +16,7 @@ use crate::app::{config as gui_config, settings as gui_settings};
pub use message::Message;
use step::{
BackupDescriptor, BackupMnemonic, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor,
ParticipateXpub, RegisterDescriptor, Step, Welcome,
ParticipateXpub, RecoverMnemonic, RegisterDescriptor, Step, Welcome,
};
pub struct Installer {
@ -116,6 +116,7 @@ impl Installer {
self.steps = vec![
Welcome::default().into(),
ImportDescriptor::new(true).into(),
RecoverMnemonic::default().into(),
RegisterDescriptor::default().into(),
DefineBitcoind::new().into(),
Final::new().into(),

View File

@ -8,3 +8,4 @@ pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str =
"The alias is applied on all the keys derived from the same seed";
pub const REGISTER_DESCRIPTOR_HELP: &str = "To be used with the wallet, a device needs the descriptor. Registration on a device is not a substitute for backing up the descriptor.";
pub const MNEMONIC_HELP: &str = "A hot key generated on this computer was used for creating this wallet. It needs to be backed up. \n Keep it in a safe place. Never share it with anyone.";
pub const RECOVER_MNEMONIC_HELP: &str = "If you were using a hot key (a key stored on the computer) in your wallet, you will need to recover it from mnemonics to be able to sign transactions again. Otherwise you can directly go the next step.";

View File

@ -1,6 +1,13 @@
use crate::installer::{context::Context, message::Message, step::Step, view};
use std::collections::HashSet;
use std::sync::Arc;
use iced::{Command, Element};
use liana::{bip39, signer::HotSigner};
use crate::{
installer::{context::Context, message::Message, step::Step, view},
signer::Signer,
};
#[derive(Default)]
pub struct BackupMnemonic {
@ -33,3 +40,122 @@ impl Step for BackupMnemonic {
view::backup_mnemonic(progress, &self.words, self.done)
}
}
pub struct RecoverMnemonic {
language: bip39::Language,
words: [(String, bool); 12],
current: usize,
suggestions: Vec<String>,
error: Option<String>,
skip: bool,
recover: bool,
}
impl Default for RecoverMnemonic {
fn default() -> Self {
Self {
language: bip39::Language::English,
words: Default::default(),
current: 0,
suggestions: Vec::new(),
error: None,
skip: false,
recover: false,
}
}
}
impl From<RecoverMnemonic> for Box<dyn Step> {
fn from(s: RecoverMnemonic) -> Box<dyn Step> {
Box::new(s)
}
}
impl Step for RecoverMnemonic {
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::MnemonicWord(index, value) => {
if let Some((word, valid)) = self.words.get_mut(index) {
if value.len() >= 3 {
let suggestions = self.language.words_by_prefix(&value);
if suggestions.contains(&value.as_ref()) {
*valid = true;
self.suggestions = Vec::new();
} else {
self.suggestions = suggestions.iter().map(|s| s.to_string()).collect();
*valid = false;
}
} else {
self.suggestions = Vec::new();
*valid = false;
}
self.current = index;
*word = value;
}
}
Message::ImportMnemonic(recover) => self.recover = recover,
Message::Skip => {
self.skip = true;
return Command::perform(async {}, |_| Message::Next);
}
_ => {}
}
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
if self.skip {
// If the user click previous, we dont want the skip to be set to true.
self.skip = false;
ctx.signer = None;
return true;
}
let words: Vec<String> = self
.words
.iter()
.filter_map(|(s, valid)| if *valid { Some(s.clone()) } else { None })
.collect();
let seed = match HotSigner::from_str(ctx.bitcoin_config.network, &words.join(" ")) {
Ok(seed) => seed,
Err(e) => {
self.error = Some(e.to_string());
return false;
}
};
let signer = Signer::new(seed);
let fingerprint = signer.fingerprint();
if let Some(descriptor) = &ctx.descriptor {
let info = descriptor.info();
let mut descriptor_keys = HashSet::new();
for (fingerprint, _) in info.primary_path().thresh_origins().1.iter() {
descriptor_keys.insert(*fingerprint);
}
for (fingerprint, _) in info.recovery_path().1.thresh_origins().1.iter() {
descriptor_keys.insert(*fingerprint);
}
if !descriptor_keys.contains(&fingerprint) {
self.error =
Some("The descriptor does not use a key derived from this seed".to_string());
return false;
}
}
ctx.signer = Some(Arc::new(signer));
true
}
fn view(&self, progress: (usize, usize)) -> Element<Message> {
view::recover_mnemonic(
progress,
&self.words,
self.current,
&self.suggestions,
self.recover,
self.error.as_ref(),
)
}
}

View File

@ -5,7 +5,7 @@ pub use descriptor::{
BackupDescriptor, DefineDescriptor, ImportDescriptor, ParticipateXpub, RegisterDescriptor,
};
pub use mnemonic::BackupMnemonic;
pub use mnemonic::{BackupMnemonic, RecoverMnemonic};
use std::path::PathBuf;
use std::str::FromStr;

View File

@ -1,5 +1,6 @@
use iced::widget::{
scrollable::Properties, Button, Checkbox, Column, Container, PickList, Row, Scrollable, Space,
TextInput,
};
use iced::{alignment, Alignment, Element, Length};
@ -1435,6 +1436,111 @@ pub fn backup_mnemonic<'a>(
)
}
pub fn recover_mnemonic<'a>(
progress: (usize, usize),
words: &'a [(String, bool); 12],
current: usize,
suggestions: &'a Vec<String>,
recover: bool,
error: Option<&'a String>,
) -> Element<'a, Message> {
layout(
progress,
Column::new()
.push(text("Mnemonics import").bold().size(50))
.push(text(prompt::RECOVER_MNEMONIC_HELP))
.push_maybe(if recover {
Some(
Column::new()
.align_items(Alignment::Center)
.push(
Container::new(if !suggestions.is_empty() {
suggestions.iter().fold(Row::new().spacing(5), |row, sugg| {
row.push(
Button::new(text(sugg))
.style(button::Style::Border.into())
.on_press(Message::MnemonicWord(
current,
sugg.to_string(),
)),
)
})
} else {
Row::new()
})
// Fixed height in order to not move words list
.height(Length::Units(50)),
)
.push(words.iter().enumerate().fold(
Column::new().spacing(5),
|acc, (i, (word, valid))| {
acc.push(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(
Container::new(text(format!("#{}", i + 1)).small())
.width(Length::Units(50)),
)
.push(
Container::new(TextInput::new("", word, move |msg| {
Message::MnemonicWord(i, msg)
}))
.width(Length::Units(100)),
)
.push_maybe(if *valid {
Some(icon::circle_check_icon().style(color::SUCCESS))
} else {
None
}),
)
},
))
.push(Space::with_height(Length::Units(50)))
.push_maybe(error.map(|e| card::invalid(text(e).style(color::ALERT)))),
)
} else {
None
})
.push(if !recover {
Row::new()
.spacing(10)
.push(
button::border(None, "Import mnemonic")
.on_press(Message::ImportMnemonic(true))
.width(Length::Units(200)),
)
.push(
button::primary(None, "Skip")
.on_press(Message::Skip)
.width(Length::Units(200)),
)
} else {
Row::new()
.spacing(10)
.push(
button::border(None, "Cancel")
.on_press(Message::ImportMnemonic(false))
.width(Length::Units(200)),
)
.push(
if words.iter().any(|(_, valid)| !valid) || error.is_some() {
button::primary(None, "Next").width(Length::Units(200))
} else {
button::primary(None, "Next")
.on_press(Message::Next)
.width(Length::Units(200))
},
)
})
.width(Length::Fill)
.height(Length::Fill)
.padding(100)
.spacing(50)
.align_items(Alignment::Center),
)
}
fn layout<'a>(
progress: (usize, usize),
content: impl Into<Element<'a, Message>>,