Merge #1098: Set network in launcher

c0e7b63809fd60f4fc6f5e9cd5223c138e25cfde fix launcher wording (edouardparis)
977d8fa2dda00b45178b18ee3120082c0780b5ad Create datadir before install to store installer.log (edouardparis)
dee083c6d1b12eff07edef2f888bc1899c57bb72 Add blue banner for testing network (edouardparis)
be343aec37d6a8d6249285cf64759e23673e85e8 Add previous button to installer first page (edouardparis)
9ae33408cb0c0446e5ce729d46aa23dce3eb614d Move all network setup in launcher (edouardparis)

Pull request description:

  This Pull requests introduce three changes:

  1. Network can only be chosen with the launcher panel. A drop down showing what network user can install is shown to the user when clicking on install a new network.
  2. A previous button is added to the first installer page, so user can be redirected to launcher and the network choices if he has chosen the wrong network. close #306
  3. In order to make it clear for the user which network he is actually using or he is currently installing a blue banner is added on top of the window if the network is different than the bitcoin mainnet.

  ![20240529_13h01m15s_grim](https://github.com/wizardsardine/liana/assets/6933020/54ec0cb2-f358-404e-8e6c-27737ecbf2ef)
  ![20240529_13h01m27s_grim](https://github.com/wizardsardine/liana/assets/6933020/39c25b63-83d8-43ef-9b20-70fec8684cee)
  ![20240529_13h01m02s_grim](https://github.com/wizardsardine/liana/assets/6933020/a96a3062-6608-4d87-b4cf-5ccc59b17b59)
  ![20240529_13h00m35s_grim](https://github.com/wizardsardine/liana/assets/6933020/73fdd803-bbe8-4ee0-9855-050306ecbc1c)

ACKs for top commit:
  jp1ac4:
    Tested ACK c0e7b63809.

Tree-SHA512: 9a0673ac785835da06fe313a861d76bdd14438cff56edb37b108b0564fd473e54028a63e209ad253b25aea318b3eb65a062cc190f76117df1baf35a251ce8356
This commit is contained in:
edouardparis 2024-05-30 11:49:36 +02:00
commit c4c32d2634
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
10 changed files with 389 additions and 410 deletions

View File

@ -20,7 +20,10 @@ use tokio::runtime::Handle;
use tracing::{error, info, warn};
pub use liana::{commands::CoinStatus, config::Config as DaemonConfig, miniscript::bitcoin};
use liana_ui::widget::Element;
use liana_ui::{
component::network_banner,
widget::{Column, Element},
};
pub use config::Config;
pub use message::Message;
@ -318,6 +321,11 @@ impl App {
}
pub fn view(&self) -> Element<Message> {
self.panels.current().view(&self.cache).map(Message::View)
let content = self.panels.current().view(&self.cache).map(Message::View);
if self.cache.network != bitcoin::Network::Bitcoin {
Column::with_children(vec![network_banner(self.cache.network).into(), content]).into()
} else {
content
}
}
}

View File

@ -1,7 +1,4 @@
use liana::miniscript::{
bitcoin::{bip32::Fingerprint, Network},
DescriptorPublicKey,
};
use liana::miniscript::{bitcoin::bip32::Fingerprint, DescriptorPublicKey};
use std::path::PathBuf;
use super::Error;
@ -23,13 +20,13 @@ pub enum Message {
Next,
Skip,
Previous,
BackToLauncher,
Install,
Close,
Reload,
Select(usize),
UseHotSigner,
Installed(Result<PathBuf, Error>),
Network(Network),
CreateTaprootDescriptor(bool),
SelectBitcoindType(SelectBitcoindTypeMsg),
InternalBitcoind(InternalBitcoindMsg),

View File

@ -5,8 +5,11 @@ mod step;
mod view;
use iced::{clipboard, Command, Subscription};
use liana::miniscript::bitcoin;
use liana_ui::widget::Element;
use liana::miniscript::bitcoin::{self, Network};
use liana_ui::{
component::network_banner,
widget::{Column, Element},
};
use tracing::{error, info, warn};
use context::Context;
@ -29,6 +32,7 @@ use step::{
};
pub struct Installer {
network: bitcoin::Network,
current: usize,
steps: Vec<Box<dyn Step>>,
hws: HardwareWallets,
@ -61,6 +65,7 @@ impl Installer {
) -> (Installer, Command<Message>) {
(
Installer {
network,
current: 0,
hws: HardwareWallets::new(destination_path.clone(), network),
steps: vec![Welcome::default().into()],
@ -71,6 +76,10 @@ impl Installer {
)
}
pub fn destination_path(&self) -> PathBuf {
self.context.data_dir.clone()
}
pub fn subscription(&self) -> Subscription<Message> {
if self.current > 0 {
self.steps
@ -135,7 +144,7 @@ impl Installer {
Message::CreateWallet => {
self.steps = vec![
Welcome::default().into(),
DefineDescriptor::new(self.signer.clone()).into(),
DefineDescriptor::new(self.network, self.signer.clone()).into(),
BackupMnemonic::new(self.signer.clone()).into(),
BackupDescriptor::default().into(),
RegisterDescriptor::new_create_wallet().into(),
@ -149,8 +158,8 @@ impl Installer {
Message::ParticipateWallet => {
self.steps = vec![
Welcome::default().into(),
ParticipateXpub::new(self.signer.clone()).into(),
ImportDescriptor::new(false).into(),
ParticipateXpub::new(self.network, self.signer.clone()).into(),
ImportDescriptor::new(self.network).into(),
BackupMnemonic::new(self.signer.clone()).into(),
RegisterDescriptor::new_import_wallet().into(),
SelectBitcoindTypeStep::new().into(),
@ -163,7 +172,7 @@ impl Installer {
Message::ImportWallet => {
self.steps = vec![
Welcome::default().into(),
ImportDescriptor::new(true).into(),
ImportDescriptor::new(self.network).into(),
RecoverMnemonic::default().into(),
RegisterDescriptor::new_import_wallet().into(),
SelectBitcoindTypeStep::new().into(),
@ -246,10 +255,17 @@ impl Installer {
}
pub fn view(&self) -> Element<Message> {
self.steps
let content = self
.steps
.get(self.current)
.expect("There is always a step")
.view(&self.hws, self.progress())
.view(&self.hws, self.progress());
if self.network != Network::Bitcoin {
Column::with_children(vec![network_banner(self.network).into(), content]).into()
} else {
content
}
}
}

View File

@ -1,6 +1,5 @@
use std::collections::{BTreeMap, HashMap, HashSet};
use std::iter::FromIterator;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
@ -196,11 +195,9 @@ impl Setup {
}
pub struct DefineDescriptor {
data_dir: Option<PathBuf>,
setup: HashMap<Network, Setup>,
setup: Setup,
network: Network,
network_valid: bool,
use_taproot: bool,
modal: Option<Box<dyn DescriptorEditModal>>,
@ -210,13 +207,11 @@ pub struct DefineDescriptor {
}
impl DefineDescriptor {
pub fn new(signer: Arc<Mutex<Signer>>) -> Self {
pub fn new(network: Network, signer: Arc<Mutex<Signer>>) -> Self {
Self {
network: Network::Bitcoin,
network,
use_taproot: false,
setup: HashMap::from([(Network::Bitcoin, Setup::new())]),
data_dir: None,
network_valid: true,
setup: Setup::new(),
modal: None,
signer,
error: None,
@ -224,25 +219,10 @@ impl DefineDescriptor {
}
fn valid(&self) -> bool {
self.setup[&self.network].valid()
self.setup.valid()
}
fn setup_mut(&mut self) -> &mut Setup {
self.setup
.get_mut(&self.network)
.expect("There is always one")
}
fn set_network(&mut self, network: Network) {
self.network = network;
if self.setup.get(&self.network).is_none() {
self.setup.insert(self.network, Setup::new());
}
self.signer.lock().unwrap().set_network(network);
if let Some(mut network_datadir) = self.data_dir.clone() {
network_datadir.push(self.network.to_string());
self.network_valid = !network_datadir.exists();
}
self.check_setup()
&mut self.setup
}
fn check_setup(&mut self) {
@ -257,16 +237,11 @@ impl Step for DefineDescriptor {
// 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, hws: &mut HardwareWallets, message: Message) -> Command<Message> {
let network = self.network;
self.error = None;
match message {
Message::Close => {
self.modal = None;
}
Message::Network(network) => {
hws.set_network(network);
self.set_network(network)
}
Message::CreateTaprootDescriptor(use_taproot) => {
self.use_taproot = use_taproot;
self.check_setup();
@ -313,6 +288,7 @@ impl Step for DefineDescriptor {
}
message::DefineKey::Edit => {
let use_taproot = self.use_taproot;
let network = self.network;
let setup = self.setup_mut();
let modal = EditXpubModal::new(
use_taproot,
@ -424,7 +400,7 @@ impl Step for DefineDescriptor {
j,
self.network,
self.signer.clone(),
self.setup[&self.network].keys.clone(),
self.setup.keys.clone(),
);
let cmd = modal.load();
self.modal = Some(Box::new(modal));
@ -459,11 +435,6 @@ impl Step for DefineDescriptor {
Command::none()
}
fn load_context(&mut self, ctx: &Context) {
self.data_dir = Some(ctx.data_dir.clone());
self.set_network(ctx.bitcoin_config.network)
}
fn subscription(&self, hws: &HardwareWallets) -> Subscription<Message> {
if let Some(modal) = &self.modal {
modal.subscription(hws)
@ -478,9 +449,10 @@ impl Step for DefineDescriptor {
let mut hw_is_used = false;
let mut spending_keys: Vec<DescriptorPublicKey> = Vec::new();
let mut key_derivation_index = HashMap::<Fingerprint, usize>::new();
for spending_key in self.setup[&self.network].spending_keys.iter().clone() {
for spending_key in self.setup.spending_keys.iter().clone() {
let fingerprint = spending_key.expect("Must be present at this step");
let key = self.setup[&self.network]
let key = self
.setup
.keys
.iter()
.find(|key| key.key.master_fingerprint() == fingerprint)
@ -506,11 +478,12 @@ impl Step for DefineDescriptor {
let mut recovery_paths = BTreeMap::new();
for path in &self.setup[&self.network].recovery_paths {
for path in &self.setup.recovery_paths {
let mut recovery_keys: Vec<DescriptorPublicKey> = Vec::new();
for recovery_key in path.keys.iter().clone() {
let fingerprint = recovery_key.expect("Must be present at this step");
let key = self.setup[&self.network]
let key = self
.setup
.keys
.iter()
.find(|key| key.key.master_fingerprint() == fingerprint)
@ -544,14 +517,14 @@ impl Step for DefineDescriptor {
recovery_paths.insert(path.sequence, recovery_keys);
}
if !self.network_valid || spending_keys.is_empty() {
if spending_keys.is_empty() {
return false;
}
let spending_keys = if spending_keys.len() == 1 {
PathInfo::Single(spending_keys[0].clone())
} else {
PathInfo::Multi(self.setup[&self.network].spending_threshold, spending_keys)
PathInfo::Multi(self.setup.spending_threshold, spending_keys)
};
let policy = match if self.use_taproot {
@ -576,13 +549,11 @@ impl Step for DefineDescriptor {
hws: &'a HardwareWallets,
progress: (usize, usize),
) -> Element<'a, Message> {
let aliases = self.setup[&self.network].keys_aliases();
let aliases = self.setup.keys_aliases();
let content = view::define_descriptor(
progress,
self.network,
self.network_valid,
self.use_taproot,
self.setup[&self.network]
self.setup
.spending_keys
.iter()
.enumerate()
@ -590,10 +561,8 @@ impl Step for DefineDescriptor {
if let Some(key) = key {
view::defined_descriptor_key(
aliases.get(key).unwrap().to_string(),
self.setup[&self.network].duplicate_name.contains(key),
self.setup[&self.network]
.incompatible_with_tapminiscript
.contains(key),
self.setup.duplicate_name.contains(key),
self.setup.incompatible_with_tapminiscript.contains(key),
)
} else {
view::undefined_descriptor_key()
@ -605,16 +574,16 @@ impl Step for DefineDescriptor {
})
})
.collect(),
self.setup[&self.network].spending_threshold,
self.setup[&self.network]
self.setup.spending_threshold,
self.setup
.recovery_paths
.iter()
.enumerate()
.map(|(i, path)| {
path.view(
&aliases,
&self.setup[&self.network].duplicate_name,
&self.setup[&self.network].incompatible_with_tapminiscript,
&self.setup.duplicate_name,
&self.setup.incompatible_with_tapminiscript,
)
.map(move |msg| {
Message::DefineDescriptor(message::DefineDescriptor::RecoveryPath(i, msg))
@ -1133,13 +1102,6 @@ pub struct HardwareWalletXpubs {
error: Option<Error>,
}
impl HardwareWalletXpubs {
fn reset(&mut self) {
self.error = None;
self.xpubs = Vec::new();
}
}
pub struct SignerXpubs {
signer: Arc<Mutex<Signer>>,
xpubs: Vec<String>,
@ -1155,11 +1117,6 @@ impl SignerXpubs {
}
}
fn reset(&mut self) {
self.xpubs = Vec::new();
self.next_account = ChildNumber::from_hardened_idx(0).unwrap();
}
fn select(&mut self, network: Network) {
self.next_account = self.next_account.increment().unwrap();
let signer = self.signer.lock().unwrap();
@ -1180,8 +1137,6 @@ impl SignerXpubs {
pub struct ParticipateXpub {
network: Network,
network_valid: bool,
data_dir: Option<PathBuf>,
shared: bool,
@ -1190,33 +1145,14 @@ pub struct ParticipateXpub {
}
impl ParticipateXpub {
pub fn new(signer: Arc<Mutex<Signer>>) -> Self {
pub fn new(network: Network, signer: Arc<Mutex<Signer>>) -> Self {
Self {
network: Network::Bitcoin,
network_valid: true,
data_dir: None,
network,
hw_xpubs: Vec::new(),
shared: false,
xpubs_signer: SignerXpubs::new(signer),
}
}
fn set_network(&mut self, network: Network) {
if network != self.network {
self.hw_xpubs.iter_mut().for_each(|hw| hw.reset());
self.xpubs_signer.reset();
}
self.network = network;
self.xpubs_signer
.signer
.lock()
.unwrap()
.set_network(network);
if let Some(mut network_datadir) = self.data_dir.clone() {
network_datadir.push(self.network.to_string());
self.network_valid = !network_datadir.exists();
}
}
}
impl Step for ParticipateXpub {
@ -1224,10 +1160,6 @@ impl Step for ParticipateXpub {
// Verification of the values is happening when the user click on Next button.
fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command<Message> {
match message {
Message::Network(network) => {
hws.set_network(network);
self.set_network(network);
}
Message::UserActionDone(shared) => self.shared = shared,
Message::ImportXpub(fg, res) => {
if let Some(hw_xpubs) = self.hw_xpubs.iter_mut().find(|x| x.fingerprint == fg) {
@ -1292,11 +1224,6 @@ impl Step for ParticipateXpub {
hws.refresh().map(Message::HardwareWallets)
}
fn load_context(&mut self, ctx: &Context) {
self.data_dir = Some(ctx.data_dir.clone());
self.set_network(ctx.bitcoin_config.network);
}
fn apply(&mut self, ctx: &mut Context) -> bool {
ctx.bitcoin_config.network = self.network;
// Drop connections to hardware wallets.
@ -1307,8 +1234,6 @@ impl Step for ParticipateXpub {
fn view<'a>(&'a self, hws: &'a HardwareWallets, progress: (usize, usize)) -> Element<Message> {
view::participate_xpub(
progress,
self.network,
self.network_valid,
hws.list
.iter()
.enumerate()
@ -1344,21 +1269,15 @@ impl From<ParticipateXpub> for Box<dyn Step> {
pub struct ImportDescriptor {
network: Network,
network_valid: bool,
change_network: bool,
data_dir: Option<PathBuf>,
imported_descriptor: form::Value<String>,
wrong_network: bool,
error: Option<String>,
}
impl ImportDescriptor {
pub fn new(change_network: bool) -> Self {
pub fn new(network: Network) -> Self {
Self {
change_network,
network: Network::Bitcoin,
network_valid: true,
data_dir: None,
network,
imported_descriptor: form::Value::default(),
wrong_network: false,
error: None,
@ -1397,32 +1316,13 @@ 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, _hws: &mut HardwareWallets, 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();
self.check_descriptor(self.network);
}
Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) => {
self.imported_descriptor.value = desc;
self.check_descriptor(self.network);
}
_ => {}
};
Command::none()
}
fn load_context(&mut self, ctx: &Context) {
if ctx.bitcoin_config.network != self.network {
self.check_descriptor(ctx.bitcoin_config.network);
if let Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) =
message
{
self.imported_descriptor.value = desc;
self.check_descriptor(self.network);
}
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();
Command::none()
}
fn apply(&mut self, ctx: &mut Context) -> bool {
@ -1441,9 +1341,6 @@ impl Step for ImportDescriptor {
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
view::import_descriptor(
progress,
self.change_network,
self.network,
self.network_valid,
&self.imported_descriptor,
self.wrong_network,
self.error.as_ref(),
@ -1650,6 +1547,7 @@ impl From<BackupDescriptor> for Box<dyn Step> {
mod tests {
use super::*;
use iced_runtime::command::Action;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
pub struct Sandbox<S: Step> {
@ -1686,9 +1584,10 @@ mod tests {
#[tokio::test]
async fn test_define_descriptor_use_hotkey() {
let mut ctx = Context::new(Network::Signet, PathBuf::from_str("/").unwrap());
let sandbox: Sandbox<DefineDescriptor> = Sandbox::new(DefineDescriptor::new(Arc::new(
Mutex::new(Signer::generate(Network::Bitcoin).unwrap()),
)));
let sandbox: Sandbox<DefineDescriptor> = Sandbox::new(DefineDescriptor::new(
Network::Bitcoin,
Arc::new(Mutex::new(Signer::generate(Network::Bitcoin).unwrap())),
));
// Edit primary key
sandbox
@ -1767,9 +1666,10 @@ mod tests {
#[tokio::test]
async fn test_define_descriptor_stores_if_hw_is_used() {
let mut ctx = Context::new(Network::Testnet, PathBuf::from_str("/").unwrap());
let sandbox: Sandbox<DefineDescriptor> = Sandbox::new(DefineDescriptor::new(Arc::new(
Mutex::new(Signer::generate(Network::Testnet).unwrap()),
)));
let sandbox: Sandbox<DefineDescriptor> = Sandbox::new(DefineDescriptor::new(
Network::Testnet,
Arc::new(Mutex::new(Signer::generate(Network::Testnet).unwrap())),
));
sandbox.load(&ctx).await;
let specter_key = message::DefinePath::Key(

View File

@ -32,55 +32,6 @@ use crate::{
},
};
#[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,
_ => Network::Mainnet,
}
}
}
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<'a>() -> Element<'a, Message> {
Container::new(
Column::new()
@ -160,6 +111,11 @@ pub fn welcome<'a>() -> Element<'a, Message> {
.padding(20),
),
)
.push(
button::secondary(Some(icon::previous_icon()), "Change network")
.width(Length::Fixed(200.0))
.on_press(Message::BackToLauncher),
)
.push(Space::with_height(Length::Fixed(100.0)))
.spacing(50)
.align_items(Alignment::Center),
@ -191,31 +147,7 @@ impl std::fmt::Display for DescriptorKind {
}
#[allow(clippy::too_many_arguments)]
pub fn define_descriptor_advanced_settings<'a>(
network: bitcoin::Network,
network_valid: bool,
use_taproot: bool,
) -> Element<'a, Message> {
let col_network = Column::new()
.spacing(10)
.push(text("Network").bold())
.push(container(
pick_list(&NETWORKS[..], Some(Network::from(network)), |net| {
Message::Network(net.into())
})
.style(if network_valid {
theme::PickList::Secondary
} else {
theme::PickList::Invalid
})
.padding(10),
))
.push_maybe(if network_valid {
None
} else {
Some(text("A data directory already exists for this network").style(color::RED))
});
pub fn define_descriptor_advanced_settings<'a>(use_taproot: bool) -> Element<'a, Message> {
let col_wallet = Column::new()
.spacing(10)
.push(text("Descriptor type").bold())
@ -238,12 +170,7 @@ pub fn define_descriptor_advanced_settings<'a>(
.spacing(20)
.push(Space::with_height(0))
.push(separation().width(500))
.push(
Row::new()
.push(col_network)
.push(Space::with_width(100))
.push(col_wallet),
)
.push(Row::new().push(col_wallet))
.push_maybe(if use_taproot {
Some(
p1_regular("Taproot is only supported by Liana version 5.0 and above")
@ -259,8 +186,6 @@ pub fn define_descriptor_advanced_settings<'a>(
#[allow(clippy::too_many_arguments)]
pub fn define_descriptor<'a>(
progress: (usize, usize),
network: bitcoin::Network,
network_valid: bool,
use_taproot: bool,
spending_keys: Vec<Element<'a, Message>>,
spending_threshold: usize,
@ -329,34 +254,29 @@ pub fn define_descriptor<'a>(
progress,
"Create the wallet",
Column::new()
.push(
collapse::Collapse::new(
|| {
Button::new(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(text("Advanced settings").small().bold())
.push(icon::collapse_icon()),
)
.style(theme::Button::Transparent)
},
|| {
Button::new(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(text("Advanced settings").small().bold())
.push(icon::collapsed_icon()),
)
.style(theme::Button::Transparent)
},
move || {
define_descriptor_advanced_settings(network, network_valid, use_taproot)
},
)
.collapsed(!network_valid),
)
.push(collapse::Collapse::new(
|| {
Button::new(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(text("Advanced settings").small().bold())
.push(icon::collapse_icon()),
)
.style(theme::Button::Transparent)
},
|| {
Button::new(
Row::new()
.align_items(Alignment::Center)
.spacing(10)
.push(text("Advanced settings").small().bold())
.push(icon::collapsed_icon()),
)
.style(theme::Button::Transparent)
},
move || define_descriptor_advanced_settings(use_taproot),
))
.push(
Column::new()
.width(Length::Fill)
@ -384,7 +304,7 @@ pub fn define_descriptor<'a>(
))
.width(Length::Fixed(200.0)),
)
.push(if !valid || !network_valid {
.push(if !valid {
button::primary(None, "Next").width(Length::Fixed(200.0))
} else {
button::primary(None, "Next")
@ -455,33 +375,10 @@ pub fn recovery_path_view(
pub fn import_descriptor<'a>(
progress: (usize, usize),
change_network: bool,
network: bitcoin::Network,
network_valid: bool,
imported_descriptor: &form::Value<String>,
wrong_network: bool,
error: Option<&String>,
) -> Element<'a, Message> {
let row_network = Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(text("Network:").bold())
.push(Container::new(
pick_list(&NETWORKS[..], Some(Network::from(network)), |net| {
Message::Network(net.into())
})
.style(if network_valid {
theme::PickList::Simple
} else {
theme::PickList::Invalid
})
.padding(10),
))
.push_maybe(if network_valid {
None
} else {
Some(text("A data directory already exists for this network").style(color::RED))
});
let col_descriptor = Column::new()
.push(text("Descriptor:").bold())
.push(
@ -501,28 +398,13 @@ pub fn import_descriptor<'a>(
progress,
"Import the wallet",
Column::new()
.push(
Column::new()
.spacing(20)
.push_maybe(if change_network {
Some(row_network)
} else {
None
})
.push(col_descriptor)
.push_maybe(if change_network {
// only show message when importing a descriptor
Some(text(
"After creating the wallet, \
.push(Column::new().spacing(20).push(col_descriptor).push(text(
"After creating the wallet, \
you will need to perform a rescan of \
the blockchain in order to see your \
coins and past transactions. This can \
be done in Settings > Bitcoin Core.",
))
} else {
None
}),
)
)))
.push(
if imported_descriptor.value.is_empty() || !imported_descriptor.valid {
button::primary(None, "Next").width(Length::Fixed(200.0))
@ -684,43 +566,14 @@ pub fn hardware_wallet_xpubs<'a>(
pub fn participate_xpub<'a>(
progress: (usize, usize),
network: bitcoin::Network,
network_valid: bool,
hws: Vec<Element<'a, Message>>,
signer: Element<'a, Message>,
shared: bool,
) -> Element<'a, Message> {
let row_network = Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(text("Network:").bold())
.push(Container::new(
pick_list(&NETWORKS[..], Some(Network::from(network)), |net| {
Message::Network(net.into())
})
.style(if network_valid {
theme::PickList::Simple
} else {
theme::PickList::Invalid
})
.padding(10),
))
.push_maybe(if network_valid {
None
} else {
Some(text("A data directory already exists for this network").style(color::RED))
});
layout(
progress,
"Share your public keys",
Column::new()
.push(
Column::new()
.spacing(20)
.width(Length::Fill)
.push(row_network),
)
.push(
Column::new()
.push(
@ -739,7 +592,7 @@ pub fn participate_xpub<'a>(
checkbox("I have shared my extended public key", shared)
.on_toggle(Message::UserActionDone),
)
.push(if shared && network_valid {
.push(if shared {
button::primary(None, "Next")
.width(Length::Fixed(200.0))
.on_press(Message::Next)

View File

@ -2,7 +2,7 @@ use std::path::PathBuf;
use iced::{
alignment::Horizontal,
widget::{tooltip, Space},
widget::{scrollable, tooltip},
Alignment, Command, Length, Subscription,
};
@ -28,33 +28,37 @@ fn wallet_name(network: &Network) -> String {
}
pub struct Launcher {
choices: Vec<Network>,
// true if installed
choices: Vec<(Network, bool)>,
datadir_path: PathBuf,
error: Option<String>,
delete_wallet_modal: Option<DeleteWalletModal>,
collapsed: bool,
}
impl Launcher {
pub fn new(datadir_path: PathBuf) -> Self {
let mut choices = Vec::new();
for network in [
Network::Bitcoin,
Network::Testnet,
Network::Signet,
Network::Regtest,
] {
if datadir_path.join(network.to_string()).exists() {
choices.push(network)
}
}
Self {
choices: [
Network::Bitcoin,
Network::Testnet,
Network::Signet,
Network::Regtest,
]
.iter()
.map(|net| (*net, datadir_path.join(net.to_string()).exists()))
.collect(),
datadir_path,
choices,
error: None,
delete_wallet_modal: None,
collapsed: false,
}
}
fn is_fresh_install(&self) -> bool {
!self.choices.iter().any(|(_, installed)| *installed)
}
pub fn stop(&mut self) {}
pub fn subscription(&self) -> Subscription<Message> {
@ -63,9 +67,15 @@ impl Launcher {
pub fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::View(ViewMessage::StartInstall) => {
Message::View(ViewMessage::ShowUninstalledNetworks) => {
self.collapsed = true;
Command::none()
}
Message::View(ViewMessage::StartInstall(net)) => {
let datadir_path = self.datadir_path.clone();
Command::perform(async move { datadir_path }, Message::Install)
Command::perform(async move { (datadir_path, net) }, |(d, n)| {
Message::Install(d, n)
})
}
Message::View(ViewMessage::Check(network)) => Command::perform(
check_network_datadir(self.datadir_path.clone(), network),
@ -88,11 +98,9 @@ impl Launcher {
}
Message::View(ViewMessage::DeleteWallet(DeleteWalletMessage::Deleted)) => {
if let Some(modal) = &self.delete_wallet_modal {
let choices = self.choices.clone();
self.choices = choices
.into_iter()
.filter(|c| c != &modal.network)
.collect();
if let Some(choice) = self.choices.iter_mut().find(|c| c.0 == modal.network) {
choice.1 = false;
}
}
Command::none()
}
@ -126,7 +134,7 @@ impl Launcher {
}
pub fn view(&self) -> Element<Message> {
let content = Into::<Element<ViewMessage>>::into(
let content = Into::<Element<ViewMessage>>::into(scrollable(
Column::new()
.push(
Container::new(image::liana_brand_grey().width(Length::Fixed(200.0)))
@ -136,17 +144,102 @@ impl Launcher {
Container::new(
Column::new()
.spacing(30)
.push(text("Welcome back").size(50).bold())
.push(if !self.is_fresh_install() {
text("Welcome back").size(50).bold()
} else {
text("Welcome").size(50).bold()
})
.push_maybe(self.error.as_ref().map(|e| card::simple(text(e))))
.push(
self.choices
.iter()
.fold(
Column::new()
.push(text("Select network:").small().bold())
.spacing(10),
|col, choice| {
col.push(
.push(if self.is_fresh_install() {
Column::new()
.spacing(10)
.push(
Button::new(
Row::new()
.spacing(20)
.align_items(Alignment::Center)
.push(
badge::Badge::new(icon::bitcoin_icon())
.style(theme::Badge::Bitcoin),
)
.push(text(format!(
"Create wallet on {}",
wallet_name(&Network::Bitcoin)
))),
)
.on_press(ViewMessage::StartInstall(Network::Bitcoin))
.padding(10)
.width(Length::Fixed(400.0))
.style(theme::Button::Border),
)
.push(if !self.collapsed {
Column::new().push(
Button::new(
Row::new()
.spacing(20)
.align_items(Alignment::Center)
.push(badge::Badge::new(icon::plus_icon()))
.push(text("Create wallet on another network")),
)
.on_press(ViewMessage::ShowUninstalledNetworks)
.padding(10)
.width(Length::Fixed(400.0))
.style(theme::Button::TransparentBorder),
)
} else {
self.choices
.iter()
.filter_map(|(net, installed)| {
if *installed || *net == Network::Bitcoin {
None
} else {
Some(net)
}
})
.fold(Column::new().spacing(10), |col, choice| {
col.push(
Button::new(
Row::new()
.spacing(20)
.align_items(Alignment::Center)
.push(
badge::Badge::new(
icon::bitcoin_icon(),
)
.style(theme::Badge::Standard),
)
.push(text(format!(
"Create wallet on {}",
wallet_name(choice)
))),
)
.on_press(ViewMessage::StartInstall(*choice))
.padding(10)
.width(Length::Fixed(400.0))
.style(theme::Button::Border),
)
})
})
} else {
Column::new()
.spacing(10)
.push(
self.choices
.iter()
.filter_map(
|(net, installed)| {
if *installed {
Some(net)
} else {
None
}
},
)
.fold(
Column::new()
.spacing(10),
|col, choice| {
col.push(
Row::new()
.spacing(10)
.push(
@ -165,11 +258,11 @@ impl Launcher {
_ => theme::Badge::Standard,
}),
)
.push(text(wallet_name(choice))),
.push(text(format!("Open wallet on {}", choice))),
)
.on_press(ViewMessage::Check(*choice))
.padding(10)
.width(Length::Fill)
.width(Length::Fixed(400.0))
.style(theme::Button::Border),
)
.push(tooltip::Tooltip::new(
@ -187,35 +280,84 @@ impl Launcher {
))
.align_items(Alignment::Center),
)
},
},
),
)
.push(
Button::new(
Row::new()
.spacing(20)
.align_items(Alignment::Center)
.push(badge::Badge::new(icon::plus_icon()))
.push(text("Install Liana on another network")),
)
.on_press(ViewMessage::StartInstall)
.padding(10)
.width(Length::Fill)
.style(theme::Button::TransparentBorder),
),
)
.max_width(500)
.align_items(Alignment::Center),
if !self.collapsed
&& self.choices.iter().any(|(_, installed)| !installed)
{
Column::new().push(
Button::new(
Row::new()
.spacing(20)
.align_items(Alignment::Center)
.push(badge::Badge::new(icon::plus_icon()))
.push(text("Create a new wallet")),
)
.on_press(ViewMessage::ShowUninstalledNetworks)
.padding(10)
.width(Length::Fixed(400.0))
.style(theme::Button::TransparentBorder),
)
} else if self.collapsed {
self.choices
.iter()
.filter_map(|(net, installed)| {
if *installed {
None
} else {
Some(net)
}
})
.fold(
Column::new()
.spacing(10),
|col, choice| {
col.push(
Button::new(
Row::new()
.spacing(20)
.align_items(Alignment::Center)
.push(
badge::Badge::new(
icon::bitcoin_icon(),
)
.style(match choice {
Network::Bitcoin => {
theme::Badge::Bitcoin
}
_ => theme::Badge::Standard,
}),
)
.push(text(format!("Create wallet on {}", wallet_name(choice)))),
)
.on_press(ViewMessage::StartInstall(*choice))
.padding(10)
.width(Length::Fixed(400.0))
.style(theme::Button::Border),
)
},
)
} else {
Column::new()
},
)
})
.align_items(if self.is_fresh_install() {
Alignment::Center
} else {
Alignment::Start
})
.max_width(500),
)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y(),
)
.push(Space::with_height(Length::Fixed(100.0))),
)
.center_x(),
),
))
.map(Message::View);
if let Some(modal) = &self.delete_wallet_modal {
Modal::new(content, modal.view())
Modal::new(Container::new(content).height(Length::Fill), modal.view())
.on_blur(Some(Message::View(ViewMessage::DeleteWallet(
DeleteWalletMessage::CloseModal,
))))
@ -229,14 +371,15 @@ impl Launcher {
#[derive(Debug, Clone)]
pub enum Message {
View(ViewMessage),
Install(PathBuf),
Install(PathBuf, Network),
Checked(Result<Network, String>),
Run(PathBuf, app::config::Config, Network),
}
#[derive(Debug, Clone)]
pub enum ViewMessage {
StartInstall,
StartInstall(Network),
ShowUninstalledNetworks,
Check(Network),
DeleteWallet(DeleteWalletMessage),
}

View File

@ -198,13 +198,24 @@ impl Application for GUI {
}
}
(State::Launcher(l), Message::Launch(msg)) => match *msg {
launcher::Message::Install(datadir_path) => {
launcher::Message::Install(datadir_path, network) => {
if !datadir_path.exists() {
// datadir is created right before launching the installer
// so logs can go in <datadir_path>/installer.log
if let Err(e) = create_datadir(&datadir_path) {
error!("Failed to create datadir: {}", e);
} else {
info!(
"Created a fresh data directory at {}",
&datadir_path.to_string_lossy()
);
}
}
self.logger.set_installer_mode(
datadir_path.clone(),
self.log_level.unwrap_or(LevelFilter::INFO),
);
let (install, command) =
Installer::new(datadir_path, bitcoin::Network::Bitcoin);
let (install, command) = Installer::new(datadir_path, network);
self.state = State::Installer(Box::new(install));
command.map(|msg| Message::Install(Box::new(msg)))
}
@ -247,6 +258,10 @@ impl Application for GUI {
);
self.state = State::Loader(Box::new(loader));
command.map(|msg| Message::Load(Box::new(msg)))
} else if let installer::Message::BackToLauncher = *msg {
let launcher = Launcher::new(i.destination_path());
self.state = State::Launcher(Box::new(launcher));
Command::none()
} else {
i.update(*msg).map(|msg| Message::Install(Box::new(msg)))
}
@ -364,13 +379,6 @@ impl Config {
Err(ConfigError::NotFound) => Ok(Config::Install(datadir_path, network)),
Err(e) => Err(format!("Failed to read configuration file: {}", e).into()),
}
} else if !datadir_path.exists()
|| (!datadir_path.join("bitcoin").exists()
&& !datadir_path.join("testnet").exists()
&& !datadir_path.join("signet").exists()
&& !datadir_path.join("regtest").exists())
{
Ok(Config::Install(datadir_path, bitcoin::Network::Bitcoin))
} else {
Ok(Config::Launcher(datadir_path))
}

View File

@ -55,3 +55,9 @@ pub const RED: Color = Color::from_rgb(
pub const ORANGE: Color =
Color::from_rgb(0xFF as f32 / 255.0, 0xa7 as f32 / 255.0, 0x0 as f32 / 255.0);
pub const BLUE: Color = Color::from_rgb(
0x7D as f32 / 255.0,
0xD3 as f32 / 255.0,
0xFC as f32 / 255.0,
);

View File

@ -12,14 +12,41 @@ pub mod text;
pub mod toast;
pub mod tooltip;
use bitcoin::Network;
pub use tooltip::tooltip;
use iced::Length;
use crate::{theme, widget::*};
use self::text::Text;
pub fn separation<'a, T: 'a>() -> Container<'a, T> {
Container::new(Column::new().push(Text::new(" ")))
Container::new(Column::new().push(text::text(" ")))
.style(theme::Container::Border)
.height(Length::Fixed(1.0))
}
pub fn network_banner<'a, T: 'a>(network: Network) -> Container<'a, T> {
Container::new(
Row::new()
.push(super::icon::warning_icon())
.push(text::text("THIS IS A "))
.push(
text::text(match network {
Network::Signet => "SIGNET WALLET",
Network::Testnet => "TESTNET WALLET",
Network::Regtest => "REGTEST WALLET",
_ => unreachable!(),
})
.bold(),
)
.push(text::text(", COINS HAVE "))
.push(text::text("NO VALUE").bold())
.align_items(iced::Alignment::Center),
)
.padding(5)
.width(Length::Fill)
.center_x()
.style(theme::Container::Banner)
}

View File

@ -91,6 +91,7 @@ pub enum Container {
Background,
Foreground,
Border,
Banner,
Card(Card),
Badge(Badge),
Pill(Pill),
@ -142,6 +143,16 @@ impl container::StyleSheet for Theme {
},
..container::Appearance::default()
},
Container::Banner => container::Appearance {
background: Some(color::WHITE.into()),
border: iced::Border {
color: color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
},
text_color: color::LIGHT_BLACK.into(),
..container::Appearance::default()
},
},
Theme::Dark => match style {
Container::Transparent => container::Appearance {
@ -182,6 +193,16 @@ impl container::StyleSheet for Theme {
},
..container::Appearance::default()
},
Container::Banner => container::Appearance {
background: Some(color::BLUE.into()),
border: iced::Border {
color: color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
},
text_color: color::LIGHT_BLACK.into(),
..container::Appearance::default()
},
},
}
}