Edit key name in installer

This commit is contained in:
edouard 2023-01-23 17:40:00 +01:00
parent bcd223f3fb
commit 689f19a4f2
3 changed files with 286 additions and 251 deletions

View File

@ -44,6 +44,7 @@ pub enum DefineDescriptor {
Key(bool, usize, DefineKey),
HWXpubImported(Result<DescriptorPublicKey, Error>),
XPubEdited(String),
NameEdited(String),
SequenceEdited(String),
ThresholdEdited(bool, usize),
ConfirmXpub,
@ -52,8 +53,7 @@ pub enum DefineDescriptor {
#[derive(Debug, Clone)]
pub enum DefineKey {
Delete,
ImportFromHardware,
ImportFromClipboard,
Edit,
Clipboard(String),
Imported(DescriptorPublicKey),
Edited(String, DescriptorPublicKey),
}

View File

@ -7,7 +7,7 @@ use liana::{
descriptors::{LianaDescKeys, MultipathDescriptor},
miniscript::{
bitcoin::{
util::bip32::{DerivationPath, ExtendedPubKey, Fingerprint},
util::bip32::{DerivationPath, Fingerprint},
Network,
},
descriptor::{DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, Wildcard},
@ -48,6 +48,8 @@ pub struct DefineDescriptor {
sequence: form::Value<String>,
modal: Option<Box<dyn DescriptorKeyModal>>,
name_indexes: (usize, usize),
error: Option<String>,
}
@ -61,6 +63,7 @@ impl DefineDescriptor {
spending_threshold: 1,
recovery_keys: vec![DescriptorKey::new("Recovery key 1".to_string())],
recovery_threshold: 1,
name_indexes: (1, 1),
sequence: form::Value::default(),
modal: None,
error: None,
@ -112,12 +115,12 @@ impl DefineDescriptor {
for spending_key in self.spending_keys.iter_mut() {
spending_key.duplicate_name = duplicate_names.contains(&spending_key.name);
if let Some(key) = &spending_key.key {
spending_key.duplicate_key = duplicate_keys.contains(&key);
spending_key.duplicate_key = duplicate_keys.contains(key);
}
}
for recovery_key in self.recovery_keys.iter_mut() {
if let Some(key) = &recovery_key.key {
recovery_key.duplicate_key = duplicate_keys.contains(&key);
recovery_key.duplicate_key = duplicate_keys.contains(key);
}
}
}
@ -161,16 +164,16 @@ impl Step for DefineDescriptor {
}
message::DefineDescriptor::AddKey(is_recovery) => {
if is_recovery {
self.name_indexes.0 += 1;
self.recovery_keys.push(DescriptorKey::new(format!(
"Recovery key {}",
self.recovery_keys.len() + 1
self.name_indexes.0,
)));
self.recovery_threshold += 1;
} else {
self.spending_keys.push(DescriptorKey::new(format!(
"Key {}",
self.spending_keys.len() + 1
)));
self.name_indexes.1 += 1;
self.spending_keys
.push(DescriptorKey::new(format!("Key {}", self.name_indexes.1,)));
self.spending_threshold += 1;
}
}
@ -178,28 +181,51 @@ impl Step for DefineDescriptor {
message::DefineKey::Clipboard(key) => {
return Command::perform(async move { key }, Message::Clibpboard);
}
message::DefineKey::Imported(imported_key) => {
message::DefineKey::Edited(name, imported_key) => {
if is_recovery {
if let Some(recovery_key) = self.recovery_keys.get_mut(i) {
recovery_key.name = name;
recovery_key.key = Some(imported_key);
recovery_key.check_network(self.network);
}
} else if let Some(spending_key) = self.spending_keys.get_mut(i) {
spending_key.name = name;
spending_key.key = Some(imported_key);
spending_key.check_network(self.network);
}
self.modal = None;
self.check_for_duplicate();
}
message::DefineKey::ImportFromClipboard => {
let modal = ImportXpubModal::new(i, is_recovery, self.network);
self.modal = Some(Box::new(modal));
}
message::DefineKey::ImportFromHardware => {
let modal = HardwareXpubModal::new(i, is_recovery, self.network);
let cmd = modal.load();
self.modal = Some(Box::new(modal));
return cmd;
message::DefineKey::Edit => {
if is_recovery {
if let Some(recovery_key) = self.recovery_keys.get(i) {
let name = recovery_key.name.clone();
let key = recovery_key
.key
.as_ref()
.map(|k| {
k.to_string().trim_end_matches("/<0;1>/*").to_string()
})
.unwrap_or_else(|| "".to_string());
let modal =
EditXpubModal::new(name, key, i, is_recovery, self.network);
let cmd = modal.load();
self.modal = Some(Box::new(modal));
return cmd;
}
} else if let Some(spending_key) = self.spending_keys.get(i) {
let name = spending_key.name.clone();
let key = spending_key
.key
.as_ref()
.map(|k| k.to_string().trim_end_matches("/<0;1>/*").to_string())
.unwrap_or_else(|| "".to_string());
let modal =
EditXpubModal::new(name, key, i, is_recovery, self.network);
let cmd = modal.load();
self.modal = Some(Box::new(modal));
return cmd;
}
}
message::DefineKey::Delete => {
if is_recovery {
@ -371,10 +397,13 @@ impl DescriptorKey {
pub fn view(&self) -> Element<message::DefineKey> {
match &self.key {
None => view::undefined_descriptor_key(),
Some(key) => {
view::defined_descriptor_key(key.to_string(), self.valid, self.duplicate_key)
}
None => view::undefined_descriptor_key(&self.name),
Some(_) => view::defined_descriptor_key(
&self.name,
self.valid,
self.duplicate_key,
self.duplicate_name,
),
}
}
}
@ -411,20 +440,37 @@ impl From<DefineDescriptor> for Box<dyn Step> {
}
}
pub struct HardwareXpubModal {
pub struct EditXpubModal {
is_recovery: bool,
key_index: usize,
network: Network,
error: Option<Error>,
processing: bool,
form_name: form::Value<String>,
form_xpub: form::Value<String>,
chosen_hw: Option<usize>,
hws: Vec<HardwareWallet>,
}
impl HardwareXpubModal {
fn new(key_index: usize, is_recovery: bool, network: Network) -> Self {
impl EditXpubModal {
fn new(
name: String,
key: String,
key_index: usize,
is_recovery: bool,
network: Network,
) -> Self {
Self {
form_name: form::Value {
valid: true,
value: name,
},
form_xpub: form::Value {
valid: true,
value: key,
},
is_recovery,
key_index,
chosen_hw: None,
@ -442,7 +488,7 @@ impl HardwareXpubModal {
}
}
impl DescriptorKeyModal for HardwareXpubModal {
impl DescriptorKeyModal for EditXpubModal {
fn processing(&self) -> bool {
self.processing
}
@ -474,61 +520,18 @@ impl DescriptorKeyModal for HardwareXpubModal {
self.processing = false;
match res {
Ok(key) => {
let key_index = self.key_index;
let is_recovery = self.is_recovery;
return Command::perform(
async move { (is_recovery, key_index, key) },
|(is_recovery, key_index, key)| {
message::DefineDescriptor::Key(
is_recovery,
key_index,
message::DefineKey::Imported(key),
)
},
)
.map(Message::DefineDescriptor);
self.form_xpub.value =
key.to_string().trim_end_matches("/<0;1>/*").to_string();
}
Err(e) => {
self.error = Some(e);
}
}
}
_ => {}
};
Command::none()
}
fn view(&self) -> Element<Message> {
view::hardware_wallet_xpubs_modal(
self.is_recovery,
&self.hws,
self.error.as_ref(),
self.processing,
self.chosen_hw,
)
}
}
pub struct ImportXpubModal {
is_recovery: bool,
key_index: usize,
form_xpub: form::Value<String>,
network: Network,
}
impl ImportXpubModal {
fn new(key_index: usize, is_recovery: bool, network: Network) -> Self {
Self {
form_xpub: form::Value::default(),
is_recovery,
key_index,
network,
}
}
}
impl DescriptorKeyModal for ImportXpubModal {
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::DefineDescriptor(message::DefineDescriptor::NameEdited(name)) => {
self.form_name.valid = true;
self.form_name.value = name;
}
Message::DefineDescriptor(message::DefineDescriptor::XPubEdited(s)) => {
self.form_xpub.valid =
DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", s)).is_ok();
@ -540,13 +543,14 @@ impl DescriptorKeyModal for ImportXpubModal {
{
let key_index = self.key_index;
let is_recovery = self.is_recovery;
let name = self.form_name.value.clone();
return Command::perform(
async move { (is_recovery, key_index, key) },
|(is_recovery, key_index, key)| {
message::DefineDescriptor::Key(
is_recovery,
key_index,
message::DefineKey::Imported(key),
message::DefineKey::Edited(name, key),
)
},
)
@ -558,29 +562,15 @@ impl DescriptorKeyModal for ImportXpubModal {
Command::none()
}
fn view(&self) -> Element<Message> {
view::clipboard_xpub_modal(&self.form_xpub, self.network)
}
}
pub struct XKey {
origin: Option<(Fingerprint, DerivationPath)>,
key: ExtendedPubKey,
}
impl std::fmt::Display for XKey {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some((ref master_id, ref master_deriv)) = self.origin {
std::fmt::Formatter::write_str(f, "[")?;
for byte in master_id.into_bytes().iter() {
write!(f, "{:02x}", byte)?;
}
for child in master_deriv {
write!(f, "/{}", child)?;
}
std::fmt::Formatter::write_str(f, "]")?;
}
self.key.fmt(f)?;
Ok(())
view::edit_key_modal(
self.network,
&self.hws,
self.error.as_ref(),
self.processing,
self.chosen_hw,
&self.form_xpub,
&self.form_name,
)
}
}

View File

@ -162,12 +162,14 @@ pub fn define_descriptor<'a>(
Some(card::warning(
"A data directory already exists for this network".to_string(),
))
});
})
.padding(50);
let col_spending_keys = Column::new()
.push(
Row::new()
.spacing(10)
.push(Space::with_width(Length::Units(40)))
.push(text("Primary path:").bold())
.push(tooltip(
super::prompt::DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP,
@ -200,13 +202,13 @@ pub fn define_descriptor<'a>(
.push(
Button::new(
Container::new(icon::plus_icon().size(50))
.width(Length::Units(250))
.height(Length::Units(250))
.width(Length::Units(200))
.height(Length::Units(200))
.align_y(alignment::Vertical::Center)
.align_x(alignment::Horizontal::Center),
)
.width(Length::Units(250))
.height(Length::Units(250))
.width(Length::Units(200))
.height(Length::Units(200))
.style(button::Style::TransparentBorder.into())
.on_press(
Message::DefineDescriptor(
@ -225,7 +227,11 @@ pub fn define_descriptor<'a>(
.spacing(10);
let col_recovery_keys = Column::new()
.push(text("Recovery path:").bold())
.push(
Row::new()
.push(Space::with_width(Length::Units(50)))
.push(text("Recovery path:").bold()),
)
.push(separation().width(Length::Fill))
.push(
Container::new(
@ -253,13 +259,13 @@ pub fn define_descriptor<'a>(
.push(
Button::new(
Container::new(icon::plus_icon().size(50))
.width(Length::Units(250))
.height(Length::Units(250))
.width(Length::Units(200))
.height(Length::Units(200))
.align_y(alignment::Vertical::Center)
.align_x(alignment::Horizontal::Center),
)
.width(Length::Units(250))
.height(Length::Units(250))
.width(Length::Units(200))
.height(Length::Units(200))
.style(button::Style::TransparentBorder.into())
.on_press(
Message::DefineDescriptor(
@ -292,7 +298,7 @@ pub fn define_descriptor<'a>(
)
.push(
Container::new(
form::Form::new("Number of block", sequence, |msg| {
form::Form::new("Number of blocks", sequence, |msg| {
Message::DefineDescriptor(
message::DefineDescriptor::SequenceEdited(msg),
)
@ -313,6 +319,7 @@ pub fn define_descriptor<'a>(
layout(
progress,
Column::new()
.push(Space::with_height(Length::Units(50)))
.push(text("Create the wallet").bold().size(50))
.push(
Column::new()
@ -330,9 +337,9 @@ pub fn define_descriptor<'a>(
.on_press(Message::Next)
})
.push_maybe(error.map(|e| card::error("Failed to create descriptor", e.to_string())))
.push(Space::with_height(Length::Units(20)))
.width(Length::Fill)
.height(Length::Fill)
.padding(100)
.spacing(50)
.align_items(Alignment::Center),
)
@ -451,10 +458,8 @@ pub fn participate_xpub<'a>(
.spacing(10)
.align_items(Alignment::Center)
.push(
Container::new(
text(format!("Select your hardware wallet:")).bold(),
)
.width(Length::Fill),
Container::new(text("Select your hardware wallet:").bold())
.width(Length::Fill),
)
.push(
button::border(Some(icon::reload_icon()), "Refresh")
@ -849,7 +854,7 @@ pub fn install<'a>(
layout(progress, col)
}
pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> {
pub fn undefined_descriptor_key(name: &str) -> Element<message::DefineKey> {
card::simple(
Column::new()
.width(Length::Fill)
@ -857,7 +862,6 @@ pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> {
.push(
Row::new()
.align_items(Alignment::Center)
.push(icon::key_icon())
.push(Space::with_width(Length::Fill))
.push(
Button::new(icon::cross_icon())
@ -868,39 +872,41 @@ pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> {
.push(
Container::new(
Column::new()
.spacing(5)
.spacing(15)
.align_items(Alignment::Center)
.push(
button::border(Some(icon::import_icon()), "from text input")
.on_press(message::DefineKey::ImportFromClipboard),
Scrollable::new(text(name).bold())
.horizontal_scroll(Properties::new().width(2).scroller_width(2)),
)
.push(
button::border(Some(icon::chip_icon()), "from hardware")
.on_press(message::DefineKey::ImportFromHardware),
),
.push(icon::circle_check_icon().style(color::FOREGROUND).size(50)),
)
.height(Length::Fill)
.align_y(alignment::Vertical::Center),
),
)
.push(
button::border(Some(icon::pencil_icon()), "Edit")
.on_press(message::DefineKey::Edit),
)
.push(Space::with_height(Length::Units(5))),
)
.padding(5)
.height(Length::Units(250))
.width(Length::Units(250))
.height(Length::Units(200))
.width(Length::Units(200))
.into()
}
pub fn defined_descriptor_key<'a>(
key: String,
pub fn defined_descriptor_key(
name: &str,
valid: bool,
duplicate: bool,
) -> Element<'a, message::DefineKey> {
duplicate_key: bool,
duplicate_name: bool,
) -> Element<message::DefineKey> {
let col = Column::new()
.spacing(40)
.width(Length::Fill)
.align_items(Alignment::Center)
.push(
Row::new()
.align_items(Alignment::Center)
.push(icon::key_icon())
.push(Space::with_width(Length::Fill))
.push(
Button::new(icon::cross_icon())
@ -914,18 +920,28 @@ pub fn defined_descriptor_key<'a>(
.spacing(5)
.push(
Container::new(
Scrollable::new(Container::new(text(key.clone())))
.height(Length::Units(50))
.horizontal_scroll(Properties::new().width(2).scroller_width(2)),
Column::new()
.spacing(15)
.align_items(Alignment::Center)
.push(
Scrollable::new(text(name).bold()).horizontal_scroll(
Properties::new().width(2).scroller_width(2),
),
)
.push(
icon::circle_check_icon()
.style(color::SUCCESS)
.size(40)
.width(Length::Units(50)),
),
)
.width(Length::Fill)
.height(Length::Fill),
.height(Length::Fill)
.align_y(alignment::Vertical::Center),
)
.push(
button::transparent_border(Some(icon::clipboard_icon()), "Copy")
.on_press(message::DefineKey::Clipboard(key)),
),
);
.height(Length::Fill),
)
.push(button::border(Some(icon::pencil_icon()), "Edit").on_press(message::DefineKey::Edit))
.push(Space::with_height(Length::Units(5)));
if !valid {
Column::new()
@ -933,8 +949,8 @@ pub fn defined_descriptor_key<'a>(
.push(
card::invalid(col)
.padding(5)
.height(Length::Units(250))
.width(Length::Units(250)),
.height(Length::Units(200))
.width(Length::Units(200)),
)
.push(
text("Key is for a different network")
@ -942,134 +958,164 @@ pub fn defined_descriptor_key<'a>(
.style(color::ALERT),
)
.into()
} else if duplicate {
} else if duplicate_key {
Column::new()
.align_items(Alignment::Center)
.push(
card::invalid(col)
.padding(5)
.height(Length::Units(250))
.width(Length::Units(250)),
.height(Length::Units(200))
.width(Length::Units(200)),
)
.push(text("Key is a duplicate").small().style(color::ALERT))
.into()
} else if duplicate_name {
Column::new()
.align_items(Alignment::Center)
.push(
card::invalid(col)
.padding(5)
.height(Length::Units(200))
.width(Length::Units(200)),
)
.push(text("Name is a duplicate").small().style(color::ALERT))
.into()
} else {
card::simple(col)
.padding(5)
.height(Length::Units(250))
.width(Length::Units(250))
.height(Length::Units(200))
.width(Length::Units(200))
.into()
}
}
pub fn hardware_wallet_xpubs_modal<'a>(
is_heir: bool,
pub fn edit_key_modal<'a>(
network: bitcoin::Network,
hws: &[HardwareWallet],
error: Option<&Error>,
processing: bool,
chosen_hw: Option<usize>,
form_xpub: &form::Value<String>,
form_name: &form::Value<String>,
) -> Element<'a, Message> {
card::simple(
Column::new()
.spacing(20)
.push(
text(if is_heir {
"Import the recovery public key:"
} else {
"Import the user public key:"
})
.bold(),
)
.push(separation().width(Length::Fill))
.push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
.push(if !hws.is_empty() {
Column::new()
.push(
Column::new()
.push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
.push(card::simple(
Column::new()
.spacing(25)
.push(
Container::new(
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(5)
.push(icon::pencil_icon())
.push(text("Edit")),
)
.spacing(10)
.push(
hws.iter()
.enumerate()
.fold(Column::new().spacing(10), |col, (i, hw)| {
.width(Length::Fill)
.align_x(alignment::Horizontal::Center),
)
.push(
Column::new()
.spacing(5)
.push(text("Edit name:").bold())
.push(
form::Form::new("Name", form_name, |msg| {
Message::DefineDescriptor(message::DefineDescriptor::NameEdited(
msg,
))
})
.warning("Please enter correct name")
.size(20)
.padding(10),
),
)
.push(
Column::new()
.spacing(5)
.push(text("Enter an extended public key:").bold())
.push(
Row::new()
.push(
form::Form::new("Extended public key", form_xpub, |msg| {
Message::DefineDescriptor(
message::DefineDescriptor::XPubEdited(msg),
)
})
.warning(if network == bitcoin::Network::Bitcoin {
"Please enter correct xpub"
} else {
"Please enter correct tpub"
})
.size(20)
.padding(10),
)
.spacing(10)
.push(Container::new(text("/<0;1>/*")).padding(5)),
),
)
.push(if !hws.is_empty() {
Column::new()
.push(
Row::new()
.spacing(10)
.align_items(Alignment::Center)
.push(
Container::new(text("Or select a hardware wallet:").bold())
.width(Length::Fill),
)
.push(
button::border(Some(icon::reload_icon()), "Refresh")
.on_press(Message::Reload),
),
)
.spacing(10)
.push(hws.iter().enumerate().fold(
Column::new().spacing(10),
|col, (i, hw)| {
col.push(hw_list_view(
i,
hw,
Some(i) == chosen_hw,
processing,
false,
!processing
&& Some(i) == chosen_hw
&& form_xpub.valid
&& !form_xpub.value.is_empty(),
))
}),
)
.width(Length::Fill)
} else {
Column::new()
.push(
Column::new()
.spacing(15)
.width(Length::Fill)
.push("Please connect a hardware wallet")
.push(button::border(None, "Refresh").on_press(Message::Reload))
.align_items(Alignment::Center),
)
.width(Length::Fill)
})
.width(Length::Units(600)),
)
.into()
}
pub fn clipboard_xpub_modal<'a>(
form_xpub: &form::Value<String>,
network: bitcoin::Network,
) -> Element<'a, Message> {
card::simple(
Column::new()
.spacing(10)
.push(text("Input extended public key:").bold())
.push(
Row::new()
.push(
form::Form::new("Extended public key", form_xpub, |msg| {
Message::DefineDescriptor(message::DefineDescriptor::XPubEdited(msg))
})
.warning(if network == bitcoin::Network::Bitcoin {
"Please enter correct xpub"
} else {
"Please enter correct tpub"
})
.size(20)
.padding(10),
)
.spacing(10)
.push(Container::new(text("/<0;1>/*")).padding(5)),
)
.push(
Row::new()
.push(Space::with_width(Length::Fill))
.push(if form_xpub.valid {
button::primary(None, "Apply").on_press(Message::DefineDescriptor(
message::DefineDescriptor::ConfirmXpub,
},
))
} else {
.width(Length::Fill)
} else {
Column::new()
.push(
Row::new()
.spacing(15)
.width(Length::Fill)
.push(
text("Or connect a hardware wallet")
.bold()
.width(Length::Fill),
)
.push(button::border(None, "Refresh").on_press(Message::Reload))
.align_items(Alignment::Center),
)
.width(Length::Fill)
})
.push(
if form_xpub.valid && !form_xpub.value.is_empty() && !form_name.value.is_empty()
{
button::primary(None, "Apply")
}),
),
)
.width(Length::Units(600))
.into()
.on_press(Message::DefineDescriptor(
message::DefineDescriptor::ConfirmXpub,
))
.width(Length::Units(200))
} else {
button::primary(None, "Apply").width(Length::Units(100))
},
)
.align_items(Alignment::Center),
))
.width(Length::Units(600))
.into()
}
fn hw_list_view<'a>(
@ -1217,18 +1263,17 @@ mod threshsold_input {
};
Column::new()
.height(Length::Units(250))
.width(Length::Units(200))
.push(button(icon::up_icon().size(50), Event::IncrementPressed))
.height(Length::Units(200))
.width(Length::Units(100))
.push(button(icon::up_icon().size(40), Event::IncrementPressed))
.push(text("Threshold:").small().bold())
.push(
Container::new(text(format!("{}/{}", self.value, self.max)).size(50))
.height(Length::Fill)
.align_y(alignment::Vertical::Center),
)
.push(button(icon::down_icon().size(50), Event::DecrementPressed))
.push(button(icon::down_icon().size(40), Event::DecrementPressed))
.align_items(Alignment::Center)
.spacing(10)
.into()
}
}