Display details of wallet policy in installer

This commit is contained in:
edouardparis 2024-08-26 17:47:32 +02:00
parent 969046c25b
commit 52755ed812
2 changed files with 214 additions and 29 deletions

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()
}