Merge #130: Fix installer wording

59c12cd5844e19f296066465fd34a3be48e26f2e installer: wait process is done before next step (edouard)
0577b971b77c0691f0dbb948956eb7616c4c8614 installer: add progress information (edouard)
0365794f910833e400f2418e52b6259f3ccb9b22 installer: add final step summary (edouard)
437faef1af01d9952f5ed3156ce38d9964ba5efb Add learn more collapse to installer (edouard)
8348ad1afadc2e46994b364d595491cc327d4961 installer: add backup descriptor step (edouard)
b260ac420819cb41f8740fa12f65b3a79d69eed3 installer: keep refresh button to list hws (edouard)
92699c645b38f97bfcde35d30b83981335285bdf installer: create or import wallet (edouard)
c89b8d88f59f4dec556d083b6f39c8263279741c installer: rename network pick_list (edouard)

Pull request description:

  base on #129

ACKs for top commit:
  edouardparis:
    Self-ACK 59c12cd5844e19f296066465fd34a3be48e26f2e

Tree-SHA512: d8016b353b54a744ab69c4d7278f4c04aab18326d97293b1b145bb5c3a46633c4ebbc4ccd3291e88fb508f02d44137d82f88218ce936b4a221335b16be879ff6
This commit is contained in:
edouard 2022-11-29 17:22:22 +01:00
commit 27e170f659
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
10 changed files with 716 additions and 294 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,9 @@ use crate::hw::HardwareWallet;
#[derive(Debug, Clone)]
pub enum Message {
CreateWallet,
ImportWallet,
BackupDone(bool),
Event(iced_native::Event),
Exit(PathBuf),
Clibpboard(String),

View File

@ -1,5 +1,6 @@
mod config;
mod message;
mod prompt;
mod step;
mod view;
@ -16,7 +17,10 @@ use crate::{
};
pub use message::Message;
use step::{Context, DefineBitcoind, DefineDescriptor, Final, RegisterDescriptor, Step, Welcome};
use step::{
BackupDescriptor, Context, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor,
RegisterDescriptor, Step, Welcome,
};
pub struct Installer {
should_exit: bool,
@ -28,12 +32,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 +46,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 +65,62 @@ 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(),
BackupDescriptor::default().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 +133,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.
@ -141,15 +160,19 @@ impl Installer {
self.steps
.get(self.current)
.expect("There is always a step")
.view()
.view((self.current, self.steps.len() - 1))
}
}
pub async fn install(ctx: Context) -> Result<PathBuf, Error> {
let hardware_wallets = ctx
.hw_tokens
.hws
.iter()
.map(|(kind, fingerprint, token)| HardwareWalletConfig::new(kind, fingerprint, token))
.filter_map(|(kind, fingerprint, token)| {
token
.as_ref()
.map(|token| HardwareWalletConfig::new(kind, fingerprint, token))
})
.collect();
let mut cfg: liana::config::Config = ctx

View File

@ -0,0 +1,2 @@
pub const BACKUP_DESCRIPTOR_MESSAGE: &str = "The descriptor is necessary to recover your funds. The backup of your key (via mnemonics, sometimes called 'seed words') is not enough. Please make sure you have backed up both your private key and your descriptor.";
pub const BACKUP_DESCRIPTOR_HELP: &str = "In Bitcoin, the coins are locked using a Script (related to the 'address'). In order to recover your funds you need both to know the Scripts you have participated in (your 'addresses'), and be able to sign a transaction that spends from those. For the ability to sign you backup your private key, this is your mnemonics ('seed words'). For finding the coins that belongs to you you backup a template of your Script ( / 'addresses'), this is your descriptor. Note however the descriptor needs not be as securely stored as the private key. A thief that steals your descriptor but not your private key will not be able to steal your funds.";

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,18 +154,18 @@ impl Step for DefineDescriptor {
}
};
ctx.descriptor = Some(desc);
true
}
ctx.descriptor = Some(desc);
true
}
fn view(&self) -> Element<Message> {
fn view(&self, progress: (usize, usize)) -> Element<Message> {
if let Some(modal) = &self.modal {
modal.view()
} else {
view::define_descriptor(
progress,
self.network,
&self.imported_descriptor,
self.network_valid,
&self.user_xpub,
&self.heir_xpub,
&self.sequence,
@ -341,12 +309,99 @@ 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, progress: (usize, usize)) -> Element<Message> {
view::import_descriptor(
progress,
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>,
processing: bool,
chosen_hw: Option<usize>,
hws: Vec<(HardwareWallet, Option<[u8; 32]>)>,
hws: Vec<(HardwareWallet, Option<[u8; 32]>, bool)>,
error: Option<Error>,
}
@ -357,7 +412,7 @@ impl Step for RegisterDescriptor {
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Select(i) => {
if let Some((hw, hmac)) = self.hws.get(i) {
if let Some((hw, hmac, _)) = self.hws.get(i) {
if hmac.is_none() {
let device = hw.device.clone();
let descriptor = self.descriptor.as_ref().unwrap().to_string();
@ -381,7 +436,8 @@ impl Step for RegisterDescriptor {
.iter_mut()
.find(|hw_h| hw_h.0.fingerprint == fingerprint)
{
hw_h.1 = Some(hmac.unwrap_or([0x00; 32]));
hw_h.1 = hmac;
hw_h.2 = true;
}
}
Err(e) => self.error = Some(e),
@ -392,9 +448,9 @@ impl Step for RegisterDescriptor {
if !self
.hws
.iter()
.any(|(h, _)| h.fingerprint == hw.fingerprint)
.any(|(h, _, _)| h.fingerprint == hw.fingerprint)
{
self.hws.push((hw, None));
self.hws.push((hw, None, false));
}
}
}
@ -406,11 +462,9 @@ impl Step for RegisterDescriptor {
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
for (hw, token) in &self.hws {
if let Some(token) = token {
if *token != [0x00; 32] {
ctx.hw_tokens.push((hw.kind, hw.fingerprint, *token));
}
for (hw, token, registered) in &self.hws {
if *registered {
ctx.hws.push((hw.kind, hw.fingerprint, *token));
}
}
true
@ -421,9 +475,10 @@ impl Step for RegisterDescriptor {
Message::ConnectedHardwareWallets,
)
}
fn view(&self) -> Element<Message> {
fn view(&self, progress: (usize, usize)) -> Element<Message> {
let desc = self.descriptor.as_ref().unwrap();
view::register_descriptor(
progress,
desc.to_string(),
&self.hws,
self.error.as_ref(),
@ -450,3 +505,31 @@ impl From<RegisterDescriptor> for Box<dyn Step> {
Box::new(s)
}
}
#[derive(Default)]
pub struct BackupDescriptor {
done: bool,
descriptor: Option<MultipathDescriptor>,
}
impl Step for BackupDescriptor {
fn update(&mut self, message: Message) -> Command<Message> {
if let Message::BackupDone(done) = message {
self.done = done;
}
Command::none()
}
fn load_context(&mut self, ctx: &Context) {
self.descriptor = ctx.descriptor.clone();
}
fn view(&self, progress: (usize, usize)) -> Element<Message> {
let desc = self.descriptor.as_ref().unwrap();
view::backup_descriptor(progress, desc.to_string(), self.done)
}
}
impl From<BackupDescriptor> for Box<dyn Step> {
fn from(s: BackupDescriptor) -> Box<dyn Step> {
Box::new(s)
}
}

View File

@ -1,5 +1,5 @@
mod descriptor;
pub use descriptor::{DefineDescriptor, RegisterDescriptor};
pub use descriptor::{BackupDescriptor, DefineDescriptor, ImportDescriptor, RegisterDescriptor};
use std::path::PathBuf;
use std::str::FromStr;
@ -24,7 +24,7 @@ pub trait Step {
fn update(&mut self, _message: Message) -> Command<Message> {
Command::none()
}
fn view(&self) -> Element<Message>;
fn view(&self, progress: (usize, usize)) -> Element<Message>;
fn load_context(&mut self, _ctx: &Context) {}
fn load(&self) -> Command<Message> {
Command::none()
@ -42,18 +42,22 @@ pub struct Context {
pub bitcoin_config: BitcoinConfig,
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 hws: Vec<(
DeviceKind,
bitcoin::util::bip32::Fingerprint,
Option<[u8; 32]>,
)>,
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,
poll_interval_secs: Duration::from_secs(30),
},
hw_tokens: Vec::new(),
hws: Vec::new(),
bitcoind_config: None,
descriptor: None,
data_dir,
@ -61,36 +65,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())
fn view(&self, _progress: (usize, usize)) -> Element<Message> {
view::welcome()
}
}
@ -211,8 +191,8 @@ impl Step for DefineBitcoind {
}
}
fn view(&self) -> Element<Message> {
view::define_bitcoin(&self.address, &self.cookie_path)
fn view(&self, progress: (usize, usize)) -> Element<Message> {
view::define_bitcoin(progress, &self.address, &self.cookie_path)
}
}
@ -230,6 +210,7 @@ impl From<DefineBitcoind> for Box<dyn Step> {
pub struct Final {
generating: bool,
context: Option<Context>,
warning: Option<String>,
config_path: Option<PathBuf>,
}
@ -237,6 +218,7 @@ pub struct Final {
impl Final {
pub fn new() -> Self {
Self {
context: None,
generating: false,
warning: None,
config_path: None,
@ -245,6 +227,9 @@ impl Final {
}
impl Step for Final {
fn load_context(&mut self, ctx: &Context) {
self.context = Some(ctx.clone());
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Installed(res) => {
@ -267,8 +252,13 @@ impl Step for Final {
Command::none()
}
fn view(&self) -> Element<Message> {
fn view(&self, progress: (usize, usize)) -> Element<Message> {
let ctx = self.context.as_ref().unwrap();
let desc = ctx.descriptor.as_ref().unwrap().to_string();
view::install(
progress,
ctx,
desc,
self.generating,
self.config_path.as_ref(),
self.warning.as_ref(),

View File

@ -1,4 +1,4 @@
use iced::widget::{Button, Column, Container, PickList, Row, Scrollable};
use iced::widget::{Button, Checkbox, Column, Container, PickList, Row, Scrollable};
use iced::{Alignment, Element, Length};
use liana::miniscript::bitcoin;
@ -7,12 +7,13 @@ use crate::{
hw::HardwareWallet,
installer::{
message::{self, Message},
step::Context,
Error,
},
ui::{
color,
component::{
button, card, container, form,
button, card, collapse, container, form,
text::{text, Text},
},
icon,
@ -20,28 +21,89 @@ use crate::{
},
};
const NETWORKS: [bitcoin::Network; 4] = [
bitcoin::Network::Bitcoin,
bitcoin::Network::Testnet,
bitcoin::Network::Signet,
bitcoin::Network::Regtest,
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Network {
Mainnet,
Testnet,
Regtest,
Signet,
}
impl From<bitcoin::Network> for Network {
fn from(n: bitcoin::Network) -> Self {
match n {
bitcoin::Network::Bitcoin => Network::Mainnet,
bitcoin::Network::Testnet => Network::Testnet,
bitcoin::Network::Regtest => Network::Regtest,
bitcoin::Network::Signet => Network::Signet,
}
}
}
impl From<Network> for bitcoin::Network {
fn from(network: Network) -> bitcoin::Network {
match network {
Network::Mainnet => bitcoin::Network::Bitcoin,
Network::Testnet => bitcoin::Network::Testnet,
Network::Regtest => bitcoin::Network::Regtest,
Network::Signet => bitcoin::Network::Signet,
}
}
}
impl std::fmt::Display for Network {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Mainnet => write!(f, "Bitcoin mainnet"),
Self::Testnet => write!(f, "Bitcoin testnet"),
Self::Regtest => write!(f, "Bitcoin regtest"),
Self::Signet => write!(f, "Bitcoin signet"),
}
}
}
const NETWORKS: [Network; 4] = [
Network::Mainnet,
Network::Testnet,
Network::Signet,
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), message::Message::Network).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)
@ -56,27 +118,34 @@ pub fn welcome(network: &bitcoin::Network, valid: bool) -> Element<Message> {
}
pub fn define_descriptor<'a>(
progress: (usize, usize),
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(
@ -100,7 +169,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(
@ -124,7 +193,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| {
@ -139,22 +208,21 @@ pub fn define_descriptor<'a>(
.spacing(10);
layout(
progress,
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 {
@ -172,30 +240,111 @@ pub fn define_descriptor<'a>(
)
}
pub fn import_descriptor<'a>(
progress: (usize, usize),
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(
progress,
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>(
progress: (usize, usize),
descriptor: String,
hws: &[(HardwareWallet, Option<[u8; 32]>)],
hws: &[(HardwareWallet, Option<[u8; 32]>, bool)],
error: Option<&Error>,
processing: bool,
chosen_hw: Option<usize>,
) -> Element<'a, Message> {
layout(
progress,
Column::new()
.push(text("Register descriptor").bold().size(50))
.push(
.push(card::simple(
Column::new()
.push(text("The descriptor:").small().bold())
.push(text(descriptor.clone()).small())
.push(
button::transparent_border(Some(icon::clipboard_icon()), "Copy")
.on_press(Message::Clibpboard(descriptor)),
Row::new().push(Column::new().width(Length::Fill)).push(
button::transparent_border(Some(icon::clipboard_icon()), "Copy")
.on_press(Message::Clibpboard(descriptor)),
),
)
.spacing(10)
.align_items(Alignment::Center),
)
.max_width(1000),
))
.push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
.push(if !hws.is_empty() {
.push(
Column::new()
.push(text(format!("{} hardware wallets connected", hws.len())).bold())
.push(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(
Container::new(
text(format!("{} hardware wallets connected", hws.len()))
.bold(),
)
.width(Length::Fill),
)
.push(
button::border(Some(icon::reload_icon()), "Refresh")
.on_press(Message::Reload),
),
)
.spacing(10)
.push(
hws.iter()
@ -206,26 +355,19 @@ pub fn register_descriptor<'a>(
&hw.0,
Some(i) == chosen_hw,
processing,
hw.1.is_some(),
hw.2,
))
}),
)
.width(Length::Fill)
.width(Length::Fill),
)
.push(if processing {
button::primary(None, "Next").width(Length::Units(200))
} else {
Column::new().push(card::simple(
Column::new()
.spacing(20)
.push("No hardware wallet connected")
.push(button::primary(None, "Refresh").on_press(Message::Reload))
.align_items(Alignment::Center)
.width(Length::Fill),
))
})
.push(
button::primary(None, "Next")
.on_press(Message::Next)
.width(Length::Units(200)),
)
.width(Length::Units(200))
})
.width(Length::Fill)
.height(Length::Fill)
.padding(100)
@ -234,7 +376,86 @@ pub fn register_descriptor<'a>(
)
}
pub fn backup_descriptor<'a>(
progress: (usize, usize),
descriptor: String,
done: bool,
) -> Element<'a, Message> {
layout(
progress,
Column::new()
.push(
text("Did you backup your wallet descriptor ?")
.bold()
.size(50),
)
.push(
Column::new()
.push(text(super::prompt::BACKUP_DESCRIPTOR_MESSAGE))
.push(collapse::Collapse::new(
|| {
Button::new(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(text("Learn more").small().bold())
.push(icon::collapse_icon()),
)
.style(button::Style::Transparent.into())
},
|| {
Button::new(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(text("Learn more").small().bold())
.push(icon::collapsed_icon()),
)
.style(button::Style::Transparent.into())
},
help_backup,
))
.max_width(1000),
)
.push(card::simple(
Column::new()
.push(text("The descriptor:").small().bold())
.push(text(descriptor.clone()).small())
.push(
Row::new().push(Column::new().width(Length::Fill)).push(
button::transparent_border(Some(icon::clipboard_icon()), "Copy")
.on_press(Message::Clibpboard(descriptor)),
),
)
.spacing(10)
.max_width(1000),
))
.push(Checkbox::new(
done,
"I have backed up my descriptor",
Message::BackupDone,
))
.push(if done {
button::primary(None, "Next")
.on_press(Message::Next)
.width(Length::Units(200))
} else {
button::primary(None, "Next").width(Length::Units(200))
})
.width(Length::Fill)
.height(Length::Fill)
.padding(100)
.spacing(50)
.align_items(Alignment::Center),
)
}
pub fn help_backup<'a>() -> Element<'a, Message> {
text(super::prompt::BACKUP_DESCRIPTOR_HELP).small().into()
}
pub fn define_bitcoin<'a>(
progress: (usize, usize),
address: &form::Value<String>,
cookie_path: &form::Value<String>,
) -> Element<'a, Message> {
@ -263,6 +484,7 @@ pub fn define_bitcoin<'a>(
.spacing(10);
layout(
progress,
Column::new()
.push(
text("Set up connection to the Bitcoin full node")
@ -285,15 +507,88 @@ pub fn define_bitcoin<'a>(
}
pub fn install<'a>(
progress: (usize, usize),
context: &Context,
descriptor: String,
generating: bool,
config_path: Option<&std::path::PathBuf>,
warning: Option<&'a String>,
) -> Element<'a, Message> {
let mut col = Column::new()
.push(
Container::new(
Column::new()
.spacing(10)
.push(
card::simple(
Column::new()
.spacing(5)
.push(text("Descriptor:").small().bold())
.push(text(descriptor).small()),
)
.width(Length::Fill),
)
.push(
card::simple(
Column::new()
.spacing(5)
.push(text("Hardware devices:").small().bold())
.push(context.hws.iter().fold(Column::new(), |acc, hw| {
acc.push(
Row::new()
.spacing(5)
.push(text(hw.0.to_string()).small())
.push(text(format!("(fingerprint: {})", hw.1)).small()),
)
})),
)
.width(Length::Fill),
)
.push(
card::simple(
Column::new()
.push(text("Bitcoind:").small().bold())
.push(
Row::new()
.spacing(5)
.align_items(Alignment::Center)
.push(text("Cookie path:").small())
.push(
text(format!(
"{}",
context
.bitcoind_config
.as_ref()
.unwrap()
.cookie_path
.to_string_lossy()
))
.small(),
),
)
.push(
Row::new()
.spacing(5)
.align_items(Alignment::Center)
.push(text("Address:").small())
.push(
text(format!(
"{}",
context.bitcoind_config.as_ref().unwrap().addr
))
.small(),
),
),
)
.width(Length::Fill),
),
)
.padding(50)
.max_width(1000),
)
.spacing(50)
.width(Length::Fill)
.height(Length::Fill)
.padding(100)
.spacing(50)
.align_items(Alignment::Center);
if let Some(error) = warning {
@ -327,7 +622,7 @@ pub fn install<'a>(
);
}
layout(col)
layout(progress, col)
}
pub fn hardware_wallet_xpubs_modal<'a>(
@ -341,17 +636,32 @@ 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),
)
.push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
.push(if !hws.is_empty() {
.push(
Column::new()
.push(text(format!("{} hardware wallets connected", hws.len())).bold())
.push(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(
Container::new(
text(format!("{} hardware wallets connected", hws.len()))
.bold(),
)
.width(Length::Fill),
)
.push(
button::border(Some(icon::reload_icon()), "Refresh")
.on_press(Message::Reload),
),
)
.spacing(10)
.push(
hws.iter()
@ -366,22 +676,8 @@ pub fn hardware_wallet_xpubs_modal<'a>(
))
}),
)
.width(Length::Fill)
} else {
Column::new()
.push(
card::simple(
Column::new()
.spacing(20)
.width(Length::Fill)
.push("Please connect a hardware wallet")
.push(button::primary(None, "Refresh").on_press(Message::Reload))
.align_items(Alignment::Center),
)
.width(Length::Fill),
)
.width(Length::Fill)
})
.width(Length::Fill),
)
.width(Length::Fill)
.height(Length::Fill)
.padding(100)
@ -435,13 +731,21 @@ fn hw_list_view<'a>(
.into()
}
fn layout<'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
fn layout<'a>(
progress: (usize, usize),
content: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
Container::new(Scrollable::new(
Column::new()
.push(
Container::new(button::transparent(None, "< Previous").on_press(Message::Previous))
.padding(5),
)
.push(
Container::new(text(format!("{}/{}", progress.0, progress.1)))
.width(Length::Fill)
.center_x(),
)
.push(Container::new(content).width(Length::Fill).center_x()),
))
.center_x()

View File

@ -16,6 +16,10 @@ pub fn transparent<'a, T: 'a>(icon: Option<Text<'a>>, t: &'static str) -> button
button::Button::new(content(icon, t)).style(Style::Transparent.into())
}
pub fn border<'a, T: 'a>(icon: Option<Text<'a>>, t: &'static str) -> button::Button<'a, T> {
button::Button::new(content(icon, t)).style(Style::Border.into())
}
pub fn transparent_border<'a, T: 'a>(
icon: Option<Text<'a>>,
t: &'static str,

View File

@ -5,41 +5,42 @@ use iced::{
use iced_lazy::{self, Component};
use std::marker::PhantomData;
use super::button::Style;
pub struct Collapse<'a, M, H, F, C> {
before: H,
after: F,
content: C,
phantom: PhantomData<&'a M>,
}
pub fn collapse<
'a,
impl<'a, Message, T, H, F, C> Collapse<'a, Message, H, F, C>
where
Message: 'a,
T: Into<Message> + Clone + 'a,
H: Fn() -> Element<'a, T> + 'a,
H: Fn() -> Button<'a, Event<T>> + 'a,
F: Fn() -> Button<'a, Event<T>> + 'a,
C: Fn() -> Element<'a, T> + 'a,
>(
header: H,
content: C,
) -> impl Into<Element<'a, Message>> {
Collapse {
header,
content,
phantom: PhantomData,
{
pub fn new(before: H, after: F, content: C) -> Self {
Collapse {
before,
after,
content,
phantom: PhantomData,
}
}
}
struct Collapse<'a, H, C> {
header: H,
content: C,
phantom: PhantomData<&'a H>,
}
#[derive(Debug, Clone, Copy)]
enum Event<T> {
pub enum Event<T> {
Internal(T),
Collapse(bool),
}
impl<'a, Message, T, H, C> Component<Message, iced::Renderer> for Collapse<'a, H, C>
impl<'a, Message, T, H, F, C> Component<Message, iced::Renderer> for Collapse<'a, Message, H, F, C>
where
T: Into<Message> + Clone + 'a,
H: Fn() -> Element<'a, T>,
H: Fn() -> Button<'a, Event<T>>,
F: Fn() -> Button<'a, Event<T>>,
C: Fn() -> Element<'a, T>,
{
type State = bool;
@ -58,35 +59,27 @@ where
fn view(&self, state: &Self::State) -> Element<Self::Event> {
if *state {
Column::new()
.push(
Button::new((self.header)().map(Event::Internal))
.style(Style::TransparentBorder.into())
.padding(10)
.on_press(Event::Collapse(false)),
)
.push((self.after)().on_press(Event::Collapse(false)))
.push((self.content)().map(Event::Internal))
.into()
} else {
Column::new()
.push(
Button::new((self.header)().map(Event::Internal))
.style(Style::TransparentBorder.into())
.padding(10)
.on_press(Event::Collapse(true)),
)
.push((self.before)().on_press(Event::Collapse(true)))
.into()
}
}
}
impl<'a, Message, T, H: 'a, C: 'a> From<Collapse<'a, H, C>> for Element<'a, Message>
impl<'a, Message, T, H: 'a, F: 'a, C: 'a> From<Collapse<'a, Message, H, F, C>>
for Element<'a, Message>
where
Message: 'a,
T: Into<Message> + Clone + 'a,
H: Fn() -> Element<'a, T, iced::Renderer>,
H: Fn() -> Button<'a, Event<T>, iced::Renderer>,
F: Fn() -> Button<'a, Event<T>, iced::Renderer>,
C: Fn() -> Element<'a, T, iced::Renderer>,
{
fn from(c: Collapse<'a, H, C>) -> Self {
fn from(c: Collapse<'a, Message, H, F, C>) -> Self {
iced_lazy::component(c)
}
}

View File

@ -13,6 +13,18 @@ fn icon(unicode: char) -> Text<'static> {
.size(20)
}
pub fn reload_icon() -> Text<'static> {
icon('\u{F130}')
}
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}')
}
@ -178,3 +190,11 @@ pub fn done_icon() -> Text<'static> {
pub fn todo_icon() -> Text<'static> {
icon('\u{F28A}')
}
pub fn collapse_icon() -> Text<'static> {
icon('\u{F284}')
}
pub fn collapsed_icon() -> Text<'static> {
icon('\u{F282}')
}