installer: create or import wallet

This commit is contained in:
edouard 2022-11-24 15:16:59 +01:00
parent c89b8d88f5
commit 92699c645b
7 changed files with 324 additions and 188 deletions

View File

@ -18,7 +18,7 @@ impl TryFrom<Context> for LianaConfig {
daemon: false,
log_level: log::LevelFilter::Info,
main_descriptor: ctx.descriptor.unwrap(),
data_dir: ctx.data_dir,
data_dir: Some(ctx.data_dir),
bitcoin_config: ctx.bitcoin_config,
bitcoind_config: ctx.bitcoind_config,
})

View File

@ -6,6 +6,8 @@ use crate::hw::HardwareWallet;
#[derive(Debug, Clone)]
pub enum Message {
CreateWallet,
ImportWallet,
Event(iced_native::Event),
Exit(PathBuf),
Clibpboard(String),

View File

@ -16,7 +16,10 @@ use crate::{
};
pub use message::Message;
use step::{Context, DefineBitcoind, DefineDescriptor, Final, RegisterDescriptor, Step, Welcome};
use step::{
Context, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, RegisterDescriptor, Step,
Welcome,
};
pub struct Installer {
should_exit: bool,
@ -28,12 +31,6 @@ pub struct Installer {
}
impl Installer {
fn next(&mut self) {
if self.current < self.steps.len() - 1 {
self.current += 1;
}
}
fn previous(&mut self) {
if self.current > 0 {
self.current -= 1;
@ -48,14 +45,8 @@ impl Installer {
Installer {
should_exit: false,
current: 0,
steps: vec![
Welcome::new(network, destination_path.clone()).into(),
DefineDescriptor::new().into(),
RegisterDescriptor::default().into(),
DefineBitcoind::new().into(),
Final::new().into(),
],
context: Context::new(network, Some(destination_path)),
steps: vec![Welcome::default().into()],
context: Context::new(network, destination_path),
},
Command::none(),
)
@ -73,35 +64,61 @@ impl Installer {
self.should_exit = true;
}
fn next(&mut self) -> Command<Message> {
let current_step = self
.steps
.get_mut(self.current)
.expect("There is always a step");
if current_step.apply(&mut self.context) {
if self.current < self.steps.len() - 1 {
self.current += 1;
}
// skip the step according to the current context.
while self
.steps
.get(self.current)
.expect("There is always a step")
.skip(&self.context)
{
if self.current < self.steps.len() - 1 {
self.current += 1;
}
}
// calculate new current_step.
let current_step = self
.steps
.get_mut(self.current)
.expect("There is always a step");
current_step.load_context(&self.context);
return current_step.load();
}
Command::none()
}
pub fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Clibpboard(s) => clipboard::write(s),
Message::Next => {
let current_step = self
.steps
.get_mut(self.current)
.expect("There is always a step");
if current_step.apply(&mut self.context) {
self.next();
// skip the step according to the current context.
while self
.steps
.get(self.current)
.expect("There is always a step")
.skip(&self.context)
{
self.next();
}
// calculate new current_step.
let current_step = self
.steps
.get_mut(self.current)
.expect("There is always a step");
current_step.load_context(&self.context);
return current_step.load();
}
Command::none()
Message::CreateWallet => {
self.steps = vec![
Welcome::default().into(),
DefineDescriptor::new().into(),
RegisterDescriptor::default().into(),
DefineBitcoind::new().into(),
Final::new().into(),
];
self.next()
}
Message::ImportWallet => {
self.steps = vec![
Welcome::default().into(),
ImportDescriptor::new().into(),
RegisterDescriptor::default().into(),
DefineBitcoind::new().into(),
Final::new().into(),
];
self.next()
}
Message::Clibpboard(s) => clipboard::write(s),
Message::Next => self.next(),
Message::Previous => {
self.previous();
Command::none()
@ -114,7 +131,7 @@ impl Installer {
Command::perform(install(self.context.clone()), Message::Installed)
}
Message::Installed(Err(e)) => {
let mut data_dir = self.context.data_dir.clone().unwrap();
let mut data_dir = self.context.data_dir.clone();
data_dir.push(self.context.bitcoin_config.network.to_string());
// In case of failure during install, block the thread to
// deleted the data_dir/network directory in order to start clean again.

View File

@ -1,3 +1,4 @@
use std::path::PathBuf;
use std::str::FromStr;
use iced::{Command, Element};
@ -8,7 +9,7 @@ use liana::{
util::bip32::{DerivationPath, Fingerprint},
Network,
},
descriptor::{Descriptor, DescriptorMultiXKey, DescriptorPublicKey, Wildcard},
descriptor::{DescriptorMultiXKey, DescriptorPublicKey, Wildcard},
},
};
@ -24,7 +25,8 @@ use crate::{
pub struct DefineDescriptor {
network: Network,
imported_descriptor: form::Value<String>,
network_valid: bool,
data_dir: Option<PathBuf>,
user_xpub: form::Value<String>,
heir_xpub: form::Value<String>,
sequence: form::Value<String>,
@ -37,7 +39,8 @@ impl DefineDescriptor {
pub fn new() -> Self {
Self {
network: Network::Bitcoin,
imported_descriptor: form::Value::default(),
data_dir: None,
network_valid: true,
user_xpub: form::Value::default(),
heir_xpub: form::Value::default(),
sequence: form::Value::default(),
@ -55,12 +58,14 @@ impl Step for DefineDescriptor {
Message::Close => {
self.modal = None;
}
Message::Network(network) => {
self.network = network;
let mut network_datadir = self.data_dir.clone().unwrap();
network_datadir.push(self.network.to_string());
self.network_valid = !network_datadir.exists();
}
Message::DefineDescriptor(msg) => {
match msg {
message::DefineDescriptor::ImportDescriptor(desc) => {
self.imported_descriptor.value = desc;
self.imported_descriptor.valid = true;
}
message::DefineDescriptor::UserXpubEdited(xpub) => {
self.user_xpub.value = xpub;
self.user_xpub.valid = true;
@ -107,78 +112,41 @@ impl Step for DefineDescriptor {
fn load_context(&mut self, ctx: &Context) {
self.network = ctx.bitcoin_config.network;
self.data_dir = Some(ctx.data_dir.clone());
let mut network_datadir = ctx.data_dir.clone();
network_datadir.push(self.network.to_string());
self.network_valid = !network_datadir.exists();
}
fn apply(&mut self, ctx: &mut Context) -> bool {
ctx.bitcoin_config.network = self.network;
// descriptor forms for import or creation cannot be both empty or filled.
if self.imported_descriptor.value.is_empty()
== (self.user_xpub.value.is_empty()
|| self.heir_xpub.value.is_empty()
|| self.sequence.value.is_empty())
let user_key = DescriptorPublicKey::from_str(&self.user_xpub.value);
self.user_xpub.valid = user_key.is_ok();
if let Ok(key) = &user_key {
self.user_xpub.valid = check_key_network(key, self.network);
}
let heir_key = DescriptorPublicKey::from_str(&self.heir_xpub.value);
self.heir_xpub.valid = heir_key.is_ok();
if let Ok(key) = &heir_key {
self.heir_xpub.valid = check_key_network(key, self.network);
}
let sequence = self.sequence.value.parse::<u16>();
self.sequence.valid = sequence.is_ok();
if !self.network_valid
|| !self.user_xpub.valid
|| !self.heir_xpub.valid
|| !self.sequence.valid
{
if !self.user_xpub.value.is_empty() {
let key = DescriptorPublicKey::from_str(&self.user_xpub.value);
self.user_xpub.valid = key.is_ok();
// Check the Network
if let Ok(key) = &key {
self.user_xpub.valid = check_key_network(key, ctx.bitcoin_config.network);
}
}
return false;
}
if !self.heir_xpub.value.is_empty() {
let key = DescriptorPublicKey::from_str(&self.heir_xpub.value);
self.heir_xpub.valid = key.is_ok();
// Check the Network
if let Ok(key) = &key {
self.heir_xpub.valid = check_key_network(key, ctx.bitcoin_config.network);
}
}
if !self.sequence.value.is_empty() {
self.sequence.valid = self.sequence.value.parse::<u32>().is_ok();
} else {
self.sequence.valid = false;
}
if !self.imported_descriptor.value.is_empty() {
self.imported_descriptor.valid =
Descriptor::<DescriptorPublicKey>::from_str(&self.imported_descriptor.value)
.is_ok();
}
false
} else if !self.imported_descriptor.value.is_empty() {
if let Ok(desc) = MultipathDescriptor::from_str(&self.imported_descriptor.value) {
ctx.descriptor = Some(desc);
true
} else {
self.imported_descriptor.valid = false;
false
}
} else {
let user_key = DescriptorPublicKey::from_str(&self.user_xpub.value);
self.user_xpub.valid = user_key.is_ok();
if let Ok(key) = &user_key {
self.user_xpub.valid = check_key_network(key, ctx.bitcoin_config.network);
}
let heir_key = DescriptorPublicKey::from_str(&self.heir_xpub.value);
self.heir_xpub.valid = heir_key.is_ok();
if let Ok(key) = &heir_key {
self.heir_xpub.valid = check_key_network(key, ctx.bitcoin_config.network);
}
let sequence = self.sequence.value.parse::<u16>();
self.sequence.valid = sequence.is_ok();
if !self.user_xpub.valid || !self.heir_xpub.valid || !self.sequence.valid {
return false;
}
let desc = match MultipathDescriptor::new(
user_key.unwrap(),
heir_key.unwrap(),
sequence.unwrap(),
) {
let desc =
match MultipathDescriptor::new(user_key.unwrap(), heir_key.unwrap(), sequence.unwrap())
{
Ok(desc) => desc,
Err(e) => {
self.error = Some(e.to_string());
@ -186,9 +154,8 @@ impl Step for DefineDescriptor {
}
};
ctx.descriptor = Some(desc);
true
}
ctx.descriptor = Some(desc);
true
}
fn view(&self) -> Element<Message> {
@ -197,7 +164,7 @@ impl Step for DefineDescriptor {
} else {
view::define_descriptor(
self.network,
&self.imported_descriptor,
self.network_valid,
&self.user_xpub,
&self.heir_xpub,
&self.sequence,
@ -341,6 +308,92 @@ async fn get_extended_pubkey(
}))
}
pub struct ImportDescriptor {
network: Network,
network_valid: bool,
data_dir: Option<PathBuf>,
imported_descriptor: form::Value<String>,
error: Option<String>,
}
impl ImportDescriptor {
pub fn new() -> Self {
Self {
network: Network::Bitcoin,
network_valid: true,
data_dir: None,
imported_descriptor: form::Value::default(),
error: None,
}
}
}
impl Step for ImportDescriptor {
// form value is set as valid each time it is edited.
// Verification of the values is happening when the user click on Next button.
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Network(network) => {
self.network = network;
let mut network_datadir = self.data_dir.clone().unwrap();
network_datadir.push(self.network.to_string());
self.network_valid = !network_datadir.exists();
}
Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) => {
self.imported_descriptor.value = desc;
self.imported_descriptor.valid = true;
}
_ => {}
};
Command::none()
}
fn load_context(&mut self, ctx: &Context) {
self.network = ctx.bitcoin_config.network;
self.data_dir = Some(ctx.data_dir.clone());
let mut network_datadir = ctx.data_dir.clone();
network_datadir.push(self.network.to_string());
self.network_valid = !network_datadir.exists();
}
fn apply(&mut self, ctx: &mut Context) -> bool {
ctx.bitcoin_config.network = self.network;
// descriptor forms for import or creation cannot be both empty or filled.
if !self.imported_descriptor.value.is_empty() {
if let Ok(desc) = MultipathDescriptor::from_str(&self.imported_descriptor.value) {
ctx.descriptor = Some(desc);
true
} else {
self.imported_descriptor.valid = false;
false
}
} else {
false
}
}
fn view(&self) -> Element<Message> {
view::import_descriptor(
self.network,
self.network_valid,
&self.imported_descriptor,
self.error.as_ref(),
)
}
}
impl Default for ImportDescriptor {
fn default() -> Self {
Self::new()
}
}
impl From<ImportDescriptor> for Box<dyn Step> {
fn from(s: ImportDescriptor) -> Box<dyn Step> {
Box::new(s)
}
}
#[derive(Default)]
pub struct RegisterDescriptor {
descriptor: Option<MultipathDescriptor>,

View File

@ -1,5 +1,5 @@
mod descriptor;
pub use descriptor::{DefineDescriptor, RegisterDescriptor};
pub use descriptor::{DefineDescriptor, ImportDescriptor, RegisterDescriptor};
use std::path::PathBuf;
use std::str::FromStr;
@ -43,11 +43,11 @@ pub struct Context {
pub bitcoind_config: Option<BitcoindConfig>,
pub descriptor: Option<MultipathDescriptor>,
pub hw_tokens: Vec<(DeviceKind, bitcoin::util::bip32::Fingerprint, [u8; 32])>,
pub data_dir: Option<PathBuf>,
pub data_dir: PathBuf,
}
impl Context {
pub fn new(network: bitcoin::Network, data_dir: Option<PathBuf>) -> Self {
pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self {
Self {
bitcoin_config: BitcoinConfig {
network,
@ -61,36 +61,12 @@ impl Context {
}
}
pub struct Welcome {
network: bitcoin::Network,
data_dir: PathBuf,
}
impl Welcome {
pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self {
Self { network, data_dir }
}
fn valid(&self) -> bool {
let mut network_datadir = self.data_dir.clone();
network_datadir.push(self.network.to_string());
!network_datadir.exists()
}
}
#[derive(Default)]
pub struct Welcome {}
impl Step for Welcome {
fn update(&mut self, message: Message) -> Command<Message> {
if let message::Message::Network(network) = message {
self.network = network;
}
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
ctx.bitcoin_config.network = self.network;
true
}
fn view(&self) -> Element<Message> {
view::welcome(&self.network, self.valid())
view::welcome()
}
}

View File

@ -68,24 +68,41 @@ const NETWORKS: [Network; 4] = [
Network::Regtest,
];
pub fn welcome(network: &bitcoin::Network, valid: bool) -> Element<Message> {
pub fn welcome<'a>() -> Element<'a, Message> {
Container::new(Container::new(
Column::new()
.push(Container::new(
PickList::new(&NETWORKS[..], Some(Network::from(*network)), |net| {
Message::Network(net.into())
})
.padding(10),
))
.push(if valid {
Container::new(
button::primary(None, "Start the install")
.on_press(Message::Next)
.width(Length::Units(200)),
)
} else {
card::warning("A data directory already exists for this network".to_string())
})
.push(
Row::new()
.spacing(20)
.push(
Button::new(
Container::new(
Column::new()
.width(Length::Units(200))
.push(icon::wallet_icon().size(50).width(Length::Units(100)))
.push(text("Create new wallet"))
.align_items(Alignment::Center),
)
.padding(50),
)
.style(button::Style::Border.into())
.on_press(Message::CreateWallet),
)
.push(
Button::new(
Container::new(
Column::new()
.width(Length::Units(200))
.push(icon::import_icon().size(50).width(Length::Units(100)))
.push(text("Import wallet"))
.align_items(Alignment::Center),
)
.padding(50),
)
.style(button::Style::Border.into())
.on_press(Message::ImportWallet),
),
)
.width(Length::Fill)
.height(Length::Fill)
.padding(100)
@ -101,26 +118,32 @@ pub fn welcome(network: &bitcoin::Network, valid: bool) -> Element<Message> {
pub fn define_descriptor<'a>(
network: bitcoin::Network,
imported_descriptor: &form::Value<String>,
network_valid: bool,
user_xpub: &form::Value<String>,
heir_xpub: &form::Value<String>,
sequence: &form::Value<String>,
error: Option<&String>,
) -> Element<'a, Message> {
let col_descriptor = Column::new()
.push(text("Descriptor:").bold())
.push(
form::Form::new("Descriptor", imported_descriptor, |msg| {
Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(msg))
let row_network = Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(text("Network:").bold())
.push(Container::new(
PickList::new(&NETWORKS[..], Some(Network::from(network)), |net| {
Message::Network(net.into())
})
.warning("Please enter correct descriptor")
.size(20)
.padding(10),
)
.spacing(10);
))
.push_maybe(if network_valid {
None
} else {
Some(card::warning(
"A data directory already exists for this network".to_string(),
))
});
let col_user_xpub = Column::new()
.push(text("Your xpub:").bold())
.push(text("Your public key:").bold())
.push(
Row::new()
.push(
@ -144,7 +167,7 @@ pub fn define_descriptor<'a>(
.spacing(10);
let col_heir_xpub = Column::new()
.push(text("Heir xpub:").bold())
.push(text("Public key of the recovery key:").bold())
.push(
Row::new()
.push(
@ -168,7 +191,7 @@ pub fn define_descriptor<'a>(
.spacing(10);
let col_sequence = Column::new()
.push(text("Number of block:").bold())
.push(text("Number of block before enabling recovery:").bold())
.push(
Container::new(
form::Form::new("Number of block", sequence, |msg| {
@ -184,21 +207,19 @@ pub fn define_descriptor<'a>(
layout(
Column::new()
.push(text("Create the descriptor").bold().size(50))
.push(text("Create the wallet").bold().size(50))
.push(
Column::new()
.push(row_network)
.push(col_user_xpub)
.push(col_sequence)
.push(col_heir_xpub)
.spacing(20),
)
.push(text("or import it").bold().size(25))
.push(col_descriptor)
.push(
if !imported_descriptor.value.is_empty()
&& (!user_xpub.value.is_empty()
|| !heir_xpub.value.is_empty()
|| !sequence.value.is_empty())
if !user_xpub.value.is_empty()
|| !heir_xpub.value.is_empty()
|| !sequence.value.is_empty()
{
button::primary(None, "Next").width(Length::Units(200))
} else {
@ -216,6 +237,65 @@ pub fn define_descriptor<'a>(
)
}
pub fn import_descriptor<'a>(
network: bitcoin::Network,
network_valid: bool,
imported_descriptor: &form::Value<String>,
error: Option<&String>,
) -> Element<'a, Message> {
let row_network = Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(text("Network:").bold())
.push(Container::new(
PickList::new(&NETWORKS[..], Some(Network::from(network)), |net| {
Message::Network(net.into())
})
.padding(10),
))
.push_maybe(if network_valid {
None
} else {
Some(card::warning(
"A data directory already exists for this network".to_string(),
))
});
let col_descriptor = Column::new()
.push(text("Descriptor:").bold())
.push(
form::Form::new("Descriptor", imported_descriptor, |msg| {
Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(msg))
})
.warning("Please enter correct descriptor")
.size(20)
.padding(10),
)
.spacing(10);
layout(
Column::new()
.push(text("Import the wallet").bold().size(50))
.push(
Column::new()
.spacing(20)
.push(row_network)
.push(col_descriptor),
)
.push(if !imported_descriptor.value.is_empty() {
button::primary(None, "Next").width(Length::Units(200))
} else {
button::primary(None, "Next")
.width(Length::Units(200))
.on_press(Message::Next)
})
.push_maybe(error.map(|e| card::error("Invalid descriptor", e.to_string())))
.width(Length::Fill)
.height(Length::Fill)
.padding(100)
.spacing(50)
.align_items(Alignment::Center),
)
}
pub fn register_descriptor<'a>(
descriptor: String,
hws: &[(HardwareWallet, Option<[u8; 32]>)],
@ -385,9 +465,9 @@ pub fn hardware_wallet_xpubs_modal<'a>(
Column::new()
.push(
text(if is_heir {
"Import the Heir xpub"
"Import the recovery public key"
} else {
"Import the user xpub"
"Import the user public key"
})
.bold()
.size(50),

View File

@ -13,6 +13,14 @@ fn icon(unicode: char) -> Text<'static> {
.size(20)
}
pub fn import_icon() -> Text<'static> {
icon('\u{F30A}')
}
pub fn wallet_icon() -> Text<'static> {
icon('\u{F615}')
}
pub fn hourglass_icon() -> Text<'static> {
icon('\u{F41F}')
}