Merge #1236: Display details of wallet policy in installer

59e7131aeef4478933c6923155ed59535e218c96 Display wallet policy in wallet settings (edouardparis)
52755ed8129041433e3d9695a3c5f93b094c80ed Display details of wallet policy in installer (edouardparis)

Pull request description:

  ![20240827_14h53m19s_grim](https://github.com/user-attachments/assets/e639813c-9fcb-4623-8ed7-a656a7af4154)
  ![20240827_14h56m31s_grim](https://github.com/user-attachments/assets/ac683af1-2c77-4276-8be3-6cd90383cfbc)

  close #620

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

Tree-SHA512: 2c272b22cbed496648d8c16c8f5639f6dfa174ca6c5891036d91e9a568e0ada4c5db69576f6d1bc08f44fbae172ba66714e6278ed87fd3cf51e95f00135e63b6
This commit is contained in:
edouardparis 2024-08-29 18:34:33 +02:00
commit f2e8e11a12
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
4 changed files with 489 additions and 112 deletions

View File

@ -5,7 +5,10 @@ use std::sync::Arc;
use iced::{Command, Subscription};
use liana::miniscript::bitcoin::{bip32::Fingerprint, Network};
use liana::{
descriptors::LianaDescriptor,
miniscript::bitcoin::{bip32::Fingerprint, Network},
};
use liana_ui::{
component::{form, modal},
@ -23,7 +26,7 @@ use crate::{
pub struct WalletSettingsState {
data_dir: PathBuf,
warning: Option<Error>,
descriptor: String,
descriptor: LianaDescriptor,
keys_aliases: Vec<(Fingerprint, form::Value<String>)>,
wallet: Arc<Wallet>,
modal: Option<RegisterWalletModal>,
@ -35,7 +38,7 @@ impl WalletSettingsState {
pub fn new(data_dir: PathBuf, wallet: Arc<Wallet>) -> Self {
WalletSettingsState {
data_dir,
descriptor: wallet.main_descriptor.to_string(),
descriptor: wallet.main_descriptor.clone(),
keys_aliases: Self::keys_aliases(&wallet),
wallet,
warning: None,
@ -177,7 +180,7 @@ impl State for WalletSettingsState {
daemon: Arc<dyn Daemon + Sync + Send>,
wallet: Arc<Wallet>,
) -> Command<Message> {
self.descriptor = wallet.main_descriptor.to_string();
self.descriptor = wallet.main_descriptor.clone();
self.keys_aliases = Self::keys_aliases(&wallet);
self.wallet = wallet;
Command::perform(

View File

@ -3,12 +3,13 @@ use std::str::FromStr;
use iced::{
alignment,
widget::{radio, scrollable, Space},
widget::{radio, scrollable, tooltip as iced_tooltip, Space},
Alignment, Length,
};
use liana::{
config::BitcoindRpcAuth,
descriptors::{LianaDescriptor, LianaPolicy},
miniscript::bitcoin::{bip32::Fingerprint, Network},
};
@ -685,8 +686,8 @@ fn is_ok_and<T, E>(res: &Result<T, E>, f: impl FnOnce(&T) -> bool) -> bool {
pub fn wallet_settings<'a>(
cache: &'a Cache,
warning: Option<&Error>,
descriptor: &'a str,
keys_aliases: &[(Fingerprint, form::Value<String>)],
descriptor: &'a LianaDescriptor,
keys_aliases: &'a [(Fingerprint, form::Value<String>)],
processing: bool,
updated: bool,
) -> Element<'a, Message> {
@ -712,88 +713,276 @@ pub fn wallet_settings<'a>(
.on_press(Message::Settings(SettingsMessage::EditWalletSettings)),
),
)
.push(card::simple(
Column::new()
.push(text("Wallet descriptor:").bold())
.push(
scrollable(
Column::new()
.push(text(descriptor.to_owned()).small())
.push(Space::with_height(Length::Fixed(5.0))),
)
.direction(scrollable::Direction::Horizontal(
scrollable::Properties::new().width(5).scroller_width(5),
)),
)
.push(
Row::new()
.spacing(10)
.push(Column::new().width(Length::Fill))
.push(
button::secondary(Some(icon::clipboard_icon()), "Copy")
.on_press(Message::Clipboard(descriptor.to_owned())),
.push(
card::simple(
Column::new()
.push(text("Wallet descriptor:").bold())
.push(
scrollable(
Column::new()
.push(text(descriptor.to_string()).small())
.push(Space::with_height(Length::Fixed(5.0))),
)
.push(
button::primary(
Some(icon::chip_icon()),
"Register on hardware device",
)
.on_press(Message::Settings(SettingsMessage::RegisterWallet)),
.direction(
scrollable::Direction::Horizontal(
scrollable::Properties::new().width(5).scroller_width(5),
),
),
)
.spacing(10),
))
.push(card::simple(
Column::new()
.push(text("Fingerprint aliases:").bold())
.push(keys_aliases.iter().fold(
Column::new().spacing(10),
|col, (fingerprint, name)| {
let fg = *fingerprint;
col.push(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(text(fg.to_string()).bold().width(Length::Fixed(100.0)))
.push(
form::Form::new("Alias", name, move |msg| {
Message::Settings(
SettingsMessage::FingerprintAliasEdited(fg, msg),
)
})
.warning("Please enter correct alias")
.size(P1_SIZE)
.padding(10),
),
)
},
))
.push(
Row::new()
.align_items(Alignment::Center)
.push(Space::with_width(Length::Fill))
.push_maybe(if updated {
Some(
Row::new()
.align_items(Alignment::Center)
.push(icon::circle_check_icon().style(color::GREEN))
.push(text("Updated").style(color::GREEN)),
)
.push(
Row::new()
.spacing(10)
.push(Column::new().width(Length::Fill))
.push(
button::secondary(Some(icon::clipboard_icon()), "Copy")
.on_press(Message::Clipboard(descriptor.to_string())),
)
} else {
None
})
.push(if !processing {
button::primary(None, "Update")
.on_press(Message::Settings(SettingsMessage::Save))
} else {
button::primary(None, "Updating")
}),
)
.spacing(10),
)),
.push(
button::primary(
Some(icon::chip_icon()),
"Register on hardware device",
)
.on_press(Message::Settings(SettingsMessage::RegisterWallet)),
),
)
.spacing(10),
)
.width(Length::Fill),
)
.push(
card::simple(display_policy(descriptor.policy(), keys_aliases)).width(Length::Fill),
)
.push(
card::simple(
Column::new()
.push(text("Fingerprint aliases:").bold())
.push(keys_aliases.iter().fold(
Column::new().spacing(10),
|col, (fingerprint, name)| {
let fg = *fingerprint;
col.push(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(
text(fg.to_string()).bold().width(Length::Fixed(100.0)),
)
.push(
form::Form::new("Alias", name, move |msg| {
Message::Settings(
SettingsMessage::FingerprintAliasEdited(
fg, msg,
),
)
})
.warning("Please enter correct alias")
.size(P1_SIZE)
.padding(10),
),
)
},
))
.push(
Row::new()
.align_items(Alignment::Center)
.push(Space::with_width(Length::Fill))
.push_maybe(if updated {
Some(
Row::new()
.align_items(Alignment::Center)
.push(icon::circle_check_icon().style(color::GREEN))
.push(text("Updated").style(color::GREEN)),
)
} else {
None
})
.push(if !processing {
button::primary(None, "Update")
.on_press(Message::Settings(SettingsMessage::Save))
} else {
button::primary(None, "Updating")
}),
)
.spacing(10),
)
.width(Length::Fill),
),
)
}
fn display_policy(
policy: LianaPolicy,
keys_aliases: &[(Fingerprint, form::Value<String>)],
) -> Element<'_, Message> {
let (primary_threshold, primary_keys) = policy.primary_path().thresh_origins();
let recovery_paths = policy.recovery_paths();
// The iteration over an HashMap keys can have a different order at each refresh
let mut primary_keys: Vec<Fingerprint> = primary_keys.into_keys().collect();
primary_keys.sort();
let mut col = Column::new().push(
Row::new()
.spacing(5)
.push(
text(format!(
"{} signature{}",
primary_threshold,
if primary_threshold > 1 { "s" } else { "" }
))
.bold(),
)
.push(if primary_keys.len() > 1 {
text(format!("out of {} by", primary_keys.len()))
} else {
text("by")
})
.push(
primary_keys
.iter()
.enumerate()
.fold(Row::new().spacing(5), |row, (i, k)| {
let content = if let Some(alias) = keys_aliases
.iter()
.find(|(fg, _)| fg == k)
.map(|(_, f)| &f.value)
{
Container::new(
iced_tooltip::Tooltip::new(
text(alias).bold(),
text(k.to_string()),
iced_tooltip::Position::Bottom,
)
.style(theme::Container::Card(theme::Card::Simple)),
)
} else {
Container::new(text(k.to_string()))
.padding(10)
.style(theme::Container::Pill(theme::Pill::Simple))
};
if primary_keys.len() == 1 || i == primary_keys.len() - 1 {
row.push(content)
} else if i <= primary_keys.len() - 2 {
row.push(content).push(text("and"))
} else {
row.push(content).push(text(","))
}
}),
)
.push(text("can always spend this wallet's funds (Primary path)")),
);
for (i, (sequence, recovery_path)) in recovery_paths.iter().enumerate() {
let (threshold, recovery_keys) = recovery_path.thresh_origins();
// The iteration over an HashMap keys can have a different order at each refresh
let mut recovery_keys: Vec<Fingerprint> = recovery_keys.into_keys().collect();
recovery_keys.sort();
col = col.push(
Row::new()
.spacing(5)
.push(
text(format!(
"{} signature{}",
threshold,
if threshold > 1 { "s" } else { "" }
))
.bold(),
)
.push(if recovery_keys.len() > 1 {
text(format!("out of {} by", recovery_keys.len()))
} else {
text("by")
})
.push(recovery_keys.iter().enumerate().fold(
Row::new().spacing(5),
|row, (i, k)| {
let content = if let Some(alias) = keys_aliases
.iter()
.find(|(fg, _)| fg == k)
.map(|(_, f)| &f.value)
{
Container::new(
iced_tooltip::Tooltip::new(
text(alias).bold(),
text(k.to_string()),
iced_tooltip::Position::Bottom,
)
.style(theme::Container::Card(theme::Card::Simple)),
)
} else {
Container::new(text(k.to_string()))
.padding(10)
.style(theme::Container::Pill(theme::Pill::Simple))
};
if recovery_keys.len() == 1 || i == recovery_keys.len() - 1 {
row.push(content)
} else if i <= recovery_keys.len() - 2 {
row.push(content).push(text("and"))
} else {
row.push(content).push(text(","))
}
},
))
.push(text("can spend coins inactive for"))
.push(
text(format!(
"{} blocks (~{})",
sequence,
expire_message_units(*sequence as u32).join(",")
))
.bold(),
)
.push(text(format!("(Recovery path #{})", i + 1))),
);
}
Column::new()
.spacing(10)
.push(text("The wallet policy:").bold())
.push(scrollable(col).direction(scrollable::Direction::Horizontal(
scrollable::Properties::new().width(5).scroller_width(5),
)))
.into()
}
/// returns y,m,d
fn expire_message_units(sequence: u32) -> Vec<String> {
let mut n_minutes = sequence * 10;
let n_years = n_minutes / 525960;
n_minutes -= n_years * 525960;
let n_months = n_minutes / 43830;
n_minutes -= n_months * 43830;
let n_days = n_minutes / 1440;
#[allow(clippy::nonminimal_bool)]
if n_years != 0 || n_months != 0 || n_days != 0 {
[(n_years, "y"), (n_months, "m"), (n_days, "d")]
.iter()
.filter_map(|(n, u)| {
if *n != 0 {
Some(format!("{}{}", n, u))
} else {
None
}
})
.collect()
} else {
n_minutes -= n_days * 1440;
let n_hours = n_minutes / 60;
n_minutes -= n_hours * 60;
[(n_hours, "h"), (n_minutes, "m")]
.iter()
.filter_map(|(n, u)| {
if *n != 0 {
Some(format!("{}{}", n, u))
} else {
None
}
})
.collect()
}
}
pub fn register_wallet_modal<'a>(
warning: Option<&Error>,
hws: &'a [HardwareWallet],

View File

@ -1363,6 +1363,7 @@ impl From<RegisterDescriptor> for Box<dyn Step> {
pub struct BackupDescriptor {
done: bool,
descriptor: Option<LianaDescriptor>,
key_aliases: HashMap<Fingerprint, String>,
}
impl Step for BackupDescriptor {
@ -1377,6 +1378,12 @@ impl Step for BackupDescriptor {
self.descriptor.clone_from(&ctx.descriptor);
self.done = false;
}
self.key_aliases = ctx
.keys
.iter()
.cloned()
.map(|k| (k.master_fingerprint, k.name))
.collect()
}
fn view<'a>(
&'a self,
@ -1384,8 +1391,13 @@ impl Step for BackupDescriptor {
progress: (usize, usize),
email: Option<&'a str>,
) -> Element<Message> {
let desc = self.descriptor.as_ref().unwrap();
view::backup_descriptor(progress, email, desc.to_string(), self.done)
view::backup_descriptor(
progress,
email,
self.descriptor.as_ref().expect("Must be a descriptor"),
&self.key_aliases,
self.done,
)
}
}

View File

@ -3,15 +3,23 @@ use iced::widget::{
checkbox, container, pick_list, radio, scrollable, scrollable::Properties, slider, Button,
Space, TextInput,
};
use iced::{alignment, widget::progress_bar, Alignment, Length};
use iced::{
alignment,
widget::{progress_bar, tooltip as iced_tooltip},
Alignment, Length,
};
use async_hwi::DeviceKind;
use liana_ui::component::text;
use std::collections::HashMap;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::path::PathBuf;
use std::{collections::HashSet, str::FromStr};
use liana::miniscript::bitcoin::{self, bip32::Fingerprint};
use liana::{
descriptors::{LianaDescriptor, LianaPolicy},
miniscript::bitcoin::{self, bip32::Fingerprint},
};
use liana_ui::{
color,
component::{
@ -1007,12 +1015,13 @@ pub fn register_descriptor<'a>(
)
}
pub fn backup_descriptor(
pub fn backup_descriptor<'a>(
progress: (usize, usize),
email: Option<&str>,
descriptor: String,
email: Option<&'a str>,
descriptor: &'a LianaDescriptor,
keys_aliases: &'a HashMap<Fingerprint, String>,
done: bool,
) -> Element<'_, Message> {
) -> Element<'a, Message> {
layout(
progress,
email,
@ -1046,28 +1055,37 @@ pub fn backup_descriptor(
))
.max_width(1000),
)
.push(card::simple(
Column::new()
.push(text("The descriptor:").small().bold())
.push(
scrollable(
Column::new()
.push(text(descriptor.to_owned()).small())
.push(Space::with_height(Length::Fixed(5.0))),
.push(
card::simple(
Column::new()
.push(text("The descriptor:").small().bold())
.push(
scrollable(
Column::new()
.push(text(descriptor.to_string()).small())
.push(Space::with_height(Length::Fixed(5.0))),
)
.direction(
scrollable::Direction::Horizontal(
scrollable::Properties::new().width(5).scroller_width(5),
),
),
)
.direction(scrollable::Direction::Horizontal(
scrollable::Properties::new().width(5).scroller_width(5),
)),
)
.push(
Row::new().push(Column::new().width(Length::Fill)).push(
button::secondary(Some(icon::clipboard_icon()), "Copy")
.on_press(Message::Clibpboard(descriptor)),
),
)
.spacing(10)
.max_width(1000),
))
.push(
Row::new().push(Column::new().width(Length::Fill)).push(
button::secondary(Some(icon::clipboard_icon()), "Copy")
.on_press(Message::Clibpboard(descriptor.to_string())),
),
)
.spacing(10),
)
.max_width(1500),
)
.push(
card::simple(display_policy(descriptor.policy(), keys_aliases))
.width(Length::Fill)
.max_width(1500),
)
.push(
checkbox("I have backed up my descriptor", done).on_toggle(Message::UserActionDone),
)
@ -1084,6 +1102,161 @@ pub fn backup_descriptor(
)
}
fn display_policy(
policy: LianaPolicy,
keys_aliases: &HashMap<Fingerprint, String>,
) -> Element<'_, Message> {
let (primary_threshold, primary_keys) = policy.primary_path().thresh_origins();
let recovery_paths = policy.recovery_paths();
let mut col = Column::new().push(
Row::new()
.spacing(5)
.push(
text(format!(
"{} signature{}",
primary_threshold,
if primary_threshold > 1 { "s" } else { "" }
))
.bold(),
)
.push(if primary_keys.len() > 1 {
text(format!("out of {} by", primary_keys.len()))
} else {
text("by")
})
.push(
primary_keys
.keys()
.enumerate()
.fold(Row::new().spacing(5), |row, (i, k)| {
let content = if let Some(alias) = keys_aliases.get(k) {
Container::new(
iced_tooltip::Tooltip::new(
text(alias).bold(),
text(k.to_string()),
iced_tooltip::Position::Bottom,
)
.style(theme::Container::Card(theme::Card::Simple)),
)
} else {
Container::new(text(k.to_string()))
.padding(10)
.style(theme::Container::Pill(theme::Pill::Simple))
};
if primary_keys.len() == 1 || i == primary_keys.len() - 1 {
row.push(content)
} else if i <= primary_keys.len() - 2 {
row.push(content).push(text("and"))
} else {
row.push(content).push(text(","))
}
}),
)
.push(text("can always spend this wallet's funds (Primary path)")),
);
for (i, (sequence, recovery_path)) in recovery_paths.iter().enumerate() {
let (threshold, recovery_keys) = recovery_path.thresh_origins();
col = col.push(
Row::new()
.spacing(5)
.push(
text(format!(
"{} signature{}",
threshold,
if threshold > 1 { "s" } else { "" }
))
.bold(),
)
.push(if recovery_keys.len() > 1 {
text(format!("out of {} by", recovery_keys.len()))
} else {
text("by")
})
.push(recovery_keys.keys().enumerate().fold(
Row::new().spacing(5),
|row, (i, k)| {
let content = if let Some(alias) = keys_aliases.get(k) {
Container::new(
iced_tooltip::Tooltip::new(
text(alias).bold(),
text(k.to_string()),
iced_tooltip::Position::Bottom,
)
.style(theme::Container::Card(theme::Card::Simple)),
)
} else {
Container::new(text(k.to_string()))
.padding(10)
.style(theme::Container::Pill(theme::Pill::Simple))
};
if recovery_keys.len() == 1 || i == recovery_keys.len() - 1 {
row.push(content)
} else if i <= recovery_keys.len() - 2 {
row.push(content).push(text("and"))
} else {
row.push(content).push(text(","))
}
},
))
.push(text("can spend coins inactive for"))
.push(
text(format!(
"{} blocks (~{})",
sequence,
expire_message_units(*sequence as u32).join(",")
))
.bold(),
)
.push(text(format!("(Recovery path #{})", i + 1))),
);
}
Column::new()
.spacing(10)
.push(text("The wallet policy:").bold())
.push(scrollable(col).direction(scrollable::Direction::Horizontal(
scrollable::Properties::new().width(5).scroller_width(5),
)))
.into()
}
/// returns y,m,d
fn expire_message_units(sequence: u32) -> Vec<String> {
let mut n_minutes = sequence * 10;
let n_years = n_minutes / 525960;
n_minutes -= n_years * 525960;
let n_months = n_minutes / 43830;
n_minutes -= n_months * 43830;
let n_days = n_minutes / 1440;
#[allow(clippy::nonminimal_bool)]
if n_years != 0 || n_months != 0 || n_days != 0 {
[(n_years, "y"), (n_months, "m"), (n_days, "d")]
.iter()
.filter_map(|(n, u)| {
if *n != 0 {
Some(format!("{}{}", n, u))
} else {
None
}
})
.collect()
} else {
n_minutes -= n_days * 1440;
let n_hours = n_minutes / 60;
n_minutes -= n_hours * 60;
[(n_hours, "h"), (n_minutes, "m")]
.iter()
.filter_map(|(n, u)| {
if *n != 0 {
Some(format!("{}{}", n, u))
} else {
None
}
})
.collect()
}
}
pub fn help_backup<'a>() -> Element<'a, Message> {
text(prompt::BACKUP_DESCRIPTOR_HELP).small().into()
}