installer: refac setup as a list of paths

This commit is contained in:
edouardparis 2024-10-01 15:46:37 +02:00
parent 5fabd987e8
commit d0ec811bef
5 changed files with 905 additions and 1015 deletions

View File

@ -110,8 +110,7 @@ pub enum InternalBitcoindMsg {
#[derive(Debug, Clone)]
pub enum DefineDescriptor {
ImportDescriptor(String),
PrimaryPath(DefinePath),
RecoveryPath(usize, DefinePath),
Path(usize, DefinePath),
AddRecoveryPath,
KeyModal(ImportKeyModal),
SequenceModal(SequenceModal),

View File

@ -73,8 +73,7 @@ pub fn check_key_network(key: &DescriptorPublicKey, network: Network) -> bool {
pub struct EditXpubModal {
device_must_support_tapminiscript: bool,
/// None if path is primary path
path_index: Option<usize>,
path_index: usize,
key_index: usize,
network: Network,
error: Option<Error>,
@ -99,7 +98,7 @@ impl EditXpubModal {
device_must_support_tapminiscript: bool,
other_path_keys: HashSet<Fingerprint>,
key: Option<Fingerprint>,
path_index: Option<usize>,
path_index: usize,
key_index: usize,
network: Network,
hot_signer: Arc<Mutex<Signer>>,
@ -292,11 +291,12 @@ impl super::DescriptorEditModal for EditXpubModal {
};
if self.other_path_keys.contains(&key.master_fingerprint()) {
self.duplicate_master_fg = true;
} else if let Some(path_index) = self.path_index {
} else {
let path_index = self.path_index;
return Command::perform(
async move { (path_index, key_index, key) },
move |(path_index, key_index, key)| {
message::DefineDescriptor::RecoveryPath(
message::DefineDescriptor::Path(
path_index,
message::DefinePath::Key(
key_index,
@ -311,24 +311,6 @@ impl super::DescriptorEditModal for EditXpubModal {
},
)
.map(Message::DefineDescriptor);
} else {
return Command::perform(
async move { (key_index, key) },
move |(key_index, key)| {
message::DefineDescriptor::PrimaryPath(
message::DefinePath::Key(
key_index,
message::DefineKey::Edited(
name,
key,
device_kind,
device_version,
),
),
)
},
)
.map(Message::DefineDescriptor);
}
}
}
@ -354,7 +336,7 @@ impl super::DescriptorEditModal for EditXpubModal {
fn view<'a>(&'a self, hws: &'a HardwareWallets) -> Element<'a, Message> {
let chosen_signer = self.chosen_signer.as_ref().map(|s| s.0);
view::edit_key_modal(
view::editor::edit_key_modal(
self.network,
hws.list
.iter()

View File

@ -31,7 +31,7 @@ use crate::{
signer::Signer,
};
use key::{check_key_network, new_multixkey_from_xpub, EditXpubModal, Key};
use key::{new_multixkey_from_xpub, EditXpubModal, Key};
pub trait DescriptorEditModal {
fn processing(&self) -> bool {
@ -46,19 +46,28 @@ pub trait DescriptorEditModal {
}
}
pub struct RecoveryPath {
pub struct Path {
keys: Vec<Option<Fingerprint>>,
threshold: usize,
sequence: u16,
sequence: Option<u16>,
duplicate_sequence: bool,
}
impl RecoveryPath {
pub fn new() -> Self {
impl Path {
pub fn new_primary_path() -> Self {
Self {
keys: vec![None],
threshold: 1,
sequence: u16::MAX,
sequence: None,
duplicate_sequence: false,
}
}
pub fn new_recovery_path() -> Self {
Self {
keys: vec![None],
threshold: 1,
sequence: Some(u16::MAX),
duplicate_sequence: false,
}
}
@ -73,57 +82,92 @@ impl RecoveryPath {
duplicate_name: &HashSet<Fingerprint>,
incompatible_with_tapminiscript: &HashSet<Fingerprint>,
) -> Element<message::DefinePath> {
view::recovery_path_view(
self.sequence,
self.duplicate_sequence,
self.threshold,
self.keys
.iter()
.enumerate()
.map(|(i, key)| {
if let Some(key) = key {
view::defined_descriptor_key(
aliases.get(key).unwrap().to_string(),
duplicate_name.contains(key),
incompatible_with_tapminiscript.contains(key),
)
} else {
view::undefined_descriptor_key()
}
.map(move |msg| message::DefinePath::Key(i, msg))
})
.collect(),
)
if let Some(sequence) = self.sequence {
view::editor::recovery_path_view(
sequence,
self.duplicate_sequence,
self.threshold,
self.keys
.iter()
.enumerate()
.map(|(i, key)| {
if let Some(key) = key {
view::editor::defined_descriptor_key(
aliases.get(key).unwrap().to_string(),
duplicate_name.contains(key),
incompatible_with_tapminiscript.contains(key),
)
} else {
view::editor::undefined_descriptor_key()
}
.map(move |msg| message::DefinePath::Key(i, msg))
})
.collect(),
)
} else {
view::editor::primary_path_view(
self.threshold,
self.keys
.iter()
.enumerate()
.map(|(i, key)| {
if let Some(key) = key {
view::editor::defined_descriptor_key(
aliases.get(key).unwrap().to_string(),
duplicate_name.contains(key),
incompatible_with_tapminiscript.contains(key),
)
} else {
view::editor::undefined_descriptor_key()
}
.map(move |msg| message::DefinePath::Key(i, msg))
})
.collect(),
)
}
}
}
struct Setup {
pub struct DefineDescriptor {
network: Network,
use_taproot: bool,
modal: Option<Box<dyn DescriptorEditModal>>,
signer: Arc<Mutex<Signer>>,
signer_fingerprint: Fingerprint,
keys: Vec<Key>,
duplicate_name: HashSet<Fingerprint>,
incompatible_with_tapminiscript: HashSet<Fingerprint>,
spending_keys: Vec<Option<Fingerprint>>,
spending_threshold: usize,
recovery_paths: Vec<RecoveryPath>,
paths: Vec<Path>,
error: Option<String>,
}
impl Setup {
fn new() -> Self {
impl DefineDescriptor {
pub fn new(network: Network, signer: Arc<Mutex<Signer>>) -> Self {
let signer_fingerprint = signer.lock().unwrap().fingerprint();
Self {
network,
use_taproot: false,
modal: None,
signer_fingerprint,
signer,
error: None,
keys: Vec::new(),
duplicate_name: HashSet::new(),
incompatible_with_tapminiscript: HashSet::new(),
spending_keys: vec![None],
spending_threshold: 1,
recovery_paths: vec![RecoveryPath::new()],
paths: vec![Path::new_primary_path(), Path::new_recovery_path()],
}
}
fn valid(&self) -> bool {
!self.spending_keys.is_empty()
&& !self.spending_keys.iter().any(|k| k.is_none())
&& !self.recovery_paths.iter().any(|path| !path.valid())
&& self.duplicate_name.is_empty()
&& self.incompatible_with_tapminiscript.is_empty()
fn keys_aliases(&self) -> HashMap<Fingerprint, String> {
let mut map = HashMap::new();
for key in &self.keys {
map.insert(key.key.master_fingerprint(), key.name.clone());
}
map
}
// Mark as duplicate every defined key that have the same name but not the same fingerprint.
@ -140,17 +184,21 @@ impl Setup {
}
let mut all_sequence = HashSet::new();
let mut duplicate_sequence = HashSet::new();
for path in &mut self.recovery_paths {
if all_sequence.contains(&path.sequence) {
duplicate_sequence.insert(path.sequence);
} else {
all_sequence.insert(path.sequence);
let mut duplicate_sequences = HashSet::new();
for path in &mut self.paths {
if let Some(sequence) = path.sequence {
if all_sequence.contains(&sequence) {
duplicate_sequences.insert(sequence);
} else {
all_sequence.insert(sequence);
}
}
}
for path in &mut self.recovery_paths {
path.duplicate_sequence = duplicate_sequence.contains(&path.sequence);
for path in &mut self.paths {
if let Some(sequence) = path.sequence {
path.duplicate_sequence = duplicate_sequences.contains(&sequence);
}
}
}
@ -160,9 +208,9 @@ impl Setup {
for key in &self.keys {
// check if key is used by a path
if !self
.spending_keys
.paths
.iter()
.chain(self.recovery_paths.iter().flat_map(|path| &path.keys))
.flat_map(|path| &path.keys)
.any(|k| *k == Some(key.fingerprint))
{
continue;
@ -181,54 +229,17 @@ impl Setup {
}
}
fn keys_aliases(&self) -> HashMap<Fingerprint, String> {
let mut map = HashMap::new();
for key in &self.keys {
map.insert(key.key.master_fingerprint(), key.name.clone());
}
map
}
}
pub struct DefineDescriptor {
setup: Setup,
network: Network,
use_taproot: bool,
modal: Option<Box<dyn DescriptorEditModal>>,
signer: Arc<Mutex<Signer>>,
signer_fingerprint: Fingerprint,
error: Option<String>,
}
impl DefineDescriptor {
pub fn new(network: Network, signer: Arc<Mutex<Signer>>) -> Self {
let signer_fingerprint = signer.lock().unwrap().fingerprint();
Self {
network,
use_taproot: false,
setup: Setup::new(),
modal: None,
signer_fingerprint,
signer,
error: None,
}
}
fn valid(&self) -> bool {
self.setup.valid()
}
fn setup_mut(&mut self) -> &mut Setup {
&mut self.setup
!self.paths.iter().any(|path| !path.valid())
&& self.duplicate_name.is_empty()
&& self.incompatible_with_tapminiscript.is_empty()
&& self.paths.len() >= 2
}
fn check_setup(&mut self) {
self.setup_mut().check_for_duplicate();
self.check_for_duplicate();
let use_taproot = self.use_taproot;
self.setup_mut()
.check_for_tapminiscript_support(use_taproot);
self.check_for_tapminiscript_support(use_taproot);
}
}
@ -246,110 +257,30 @@ impl Step for DefineDescriptor {
self.check_setup();
}
Message::DefineDescriptor(message::DefineDescriptor::AddRecoveryPath) => {
self.setup_mut().recovery_paths.push(RecoveryPath::new());
self.paths.push(Path::new_recovery_path());
}
Message::DefineDescriptor(message::DefineDescriptor::PrimaryPath(msg)) => match msg {
Message::DefineDescriptor(message::DefineDescriptor::Path(i, msg)) => match msg {
message::DefinePath::ThresholdEdited(value) => {
self.setup_mut().spending_threshold = value;
}
message::DefinePath::AddKey => {
self.setup_mut().spending_keys.push(None);
self.setup_mut().spending_threshold += 1;
}
message::DefinePath::Key(i, msg) => match msg {
message::DefineKey::Clipboard(key) => {
return Command::perform(async move { key }, Message::Clibpboard);
}
message::DefineKey::Edited(name, imported_key, kind, version) => {
let fingerprint = imported_key.master_fingerprint();
let is_hot_signer = self.signer_fingerprint == fingerprint;
hws.set_alias(fingerprint, name.clone());
if let Some(key) = self
.setup_mut()
.keys
.iter_mut()
.find(|k| k.fingerprint == fingerprint)
{
key.name = name;
} else {
self.setup_mut().keys.push(Key {
is_hot_signer,
fingerprint,
name,
key: imported_key,
device_kind: kind,
device_version: version,
});
}
self.setup_mut().spending_keys[i] = Some(fingerprint);
self.modal = None;
self.check_setup();
}
message::DefineKey::Edit => {
let use_taproot = self.use_taproot;
let network = self.network;
let setup = self.setup_mut();
let modal = EditXpubModal::new(
use_taproot,
HashSet::from_iter(setup.spending_keys.iter().filter_map(|key| {
if key.is_some() && key != &setup.spending_keys[i] {
*key
} else {
None
}
})),
self.setup_mut().spending_keys[i],
None,
i,
network,
self.signer.clone(),
self.signer_fingerprint,
self.setup_mut()
.keys
.iter()
.filter(|k| check_key_network(&k.key, network))
.cloned()
.collect(),
);
let cmd = modal.load();
self.modal = Some(Box::new(modal));
return cmd;
}
message::DefineKey::Delete => {
self.setup_mut().spending_keys.remove(i);
if self.setup_mut().spending_threshold
> self.setup_mut().spending_keys.len()
{
self.setup_mut().spending_threshold -= 1;
}
self.check_setup();
}
},
_ => {}
},
Message::DefineDescriptor(message::DefineDescriptor::RecoveryPath(i, msg)) => match msg
{
message::DefinePath::ThresholdEdited(value) => {
if let Some(path) = self.setup_mut().recovery_paths.get_mut(i) {
if let Some(path) = self.paths.get_mut(i) {
path.threshold = value;
}
}
message::DefinePath::SequenceEdited(seq) => {
self.modal = None;
if let Some(path) = self.setup_mut().recovery_paths.get_mut(i) {
path.sequence = seq;
if let Some(path) = self.paths.get_mut(i) {
path.sequence = Some(seq);
}
self.setup_mut().check_for_duplicate();
self.check_for_duplicate();
}
message::DefinePath::EditSequence => {
if let Some(path) = self.setup_mut().recovery_paths.get(i) {
self.modal = Some(Box::new(EditSequenceModal::new(i, path.sequence)));
if let Some(path) = self.paths.get(i) {
if let Some(sequence) = path.sequence {
self.modal = Some(Box::new(EditSequenceModal::new(i, sequence)));
}
}
}
message::DefinePath::AddKey => {
if let Some(path) = self.setup_mut().recovery_paths.get_mut(i) {
if let Some(path) = self.paths.get_mut(i) {
path.keys.push(None);
path.threshold += 1;
}
@ -362,18 +293,15 @@ impl Step for DefineDescriptor {
let fingerprint = imported_key.master_fingerprint();
let is_hot_signer = self.signer_fingerprint == fingerprint;
hws.set_alias(fingerprint, name.clone());
if let Some(key) = self
.setup_mut()
.keys
.iter_mut()
.find(|k| k.fingerprint == fingerprint)
if let Some(key) =
self.keys.iter_mut().find(|k| k.fingerprint == fingerprint)
{
key.name = name;
key.is_hot_signer = is_hot_signer;
key.device_kind = kind;
key.device_version = version;
} else {
self.setup_mut().keys.push(Key {
self.keys.push(Key {
fingerprint,
is_hot_signer,
name,
@ -383,52 +311,49 @@ impl Step for DefineDescriptor {
});
}
self.setup_mut().recovery_paths[i].keys[j] = Some(fingerprint);
self.paths[i].keys[j] = Some(fingerprint);
self.modal = None;
self.check_setup();
}
message::DefineKey::Edit => {
let use_taproot = self.use_taproot;
let setup = self.setup_mut();
let path = &self.paths[i];
let modal = EditXpubModal::new(
use_taproot,
HashSet::from_iter(setup.recovery_paths[i].keys.iter().filter_map(
|key| {
if key.is_some() && key != &setup.recovery_paths[i].keys[j] {
*key
} else {
None
}
},
)),
setup.recovery_paths[i].keys[j],
Some(i),
HashSet::from_iter(path.keys.iter().filter_map(|key| {
if key.is_some() && key != &path.keys[j] {
*key
} else {
None
}
})),
path.keys[j],
i,
j,
self.network,
self.signer.clone(),
self.signer_fingerprint,
self.setup.keys.clone(),
self.keys.clone(),
);
let cmd = modal.load();
self.modal = Some(Box::new(modal));
return cmd;
}
message::DefineKey::Delete => {
if let Some(path) = self.setup_mut().recovery_paths.get_mut(i) {
if let Some(path) = self.paths.get_mut(i) {
path.keys.remove(j);
if path.threshold > path.keys.len() {
path.threshold -= 1;
}
}
if self
.setup_mut()
.recovery_paths
.paths
.get(i)
.map(|path| path.keys.is_empty())
.unwrap_or(false)
{
self.setup_mut().recovery_paths.remove(i);
self.paths.remove(i);
}
self.check_setup();
}
@ -452,15 +377,18 @@ impl Step for DefineDescriptor {
}
fn apply(&mut self, ctx: &mut Context) -> bool {
if self.paths.len() < 2 {
return false;
}
ctx.bitcoin_config.network = self.network;
ctx.keys = Vec::new();
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.spending_keys.iter().clone() {
for spending_key in self.paths[0].keys.iter().clone() {
let fingerprint = spending_key.expect("Must be present at this step");
let key = self
.setup
.keys
.iter()
.find(|key| key.key.master_fingerprint() == fingerprint)
@ -486,12 +414,11 @@ impl Step for DefineDescriptor {
let mut recovery_paths = BTreeMap::new();
for path in &self.setup.recovery_paths {
for path in &self.paths[1..] {
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
.keys
.iter()
.find(|key| key.key.master_fingerprint() == fingerprint)
@ -522,7 +449,11 @@ impl Step for DefineDescriptor {
PathInfo::Multi(path.threshold, recovery_keys)
};
recovery_paths.insert(path.sequence, recovery_keys);
recovery_paths.insert(
path.sequence
.expect("Must be a recovery path with a sequence"),
recovery_keys,
);
}
if spending_keys.is_empty() {
@ -532,7 +463,7 @@ impl Step for DefineDescriptor {
let spending_keys = if spending_keys.len() == 1 {
PathInfo::Single(spending_keys[0].clone())
} else {
PathInfo::Multi(self.setup.spending_threshold, spending_keys)
PathInfo::Multi(self.paths[0].threshold, spending_keys)
};
let policy = match if self.use_taproot {
@ -558,45 +489,22 @@ impl Step for DefineDescriptor {
progress: (usize, usize),
email: Option<&'a str>,
) -> Element<'a, Message> {
let aliases = self.setup.keys_aliases();
let content = view::define_descriptor(
let aliases = self.keys_aliases();
let content = view::editor::define_descriptor(
progress,
email,
self.use_taproot,
self.setup
.spending_keys
.iter()
.enumerate()
.map(|(i, key)| {
if let Some(key) = key {
view::defined_descriptor_key(
aliases.get(key).unwrap().to_string(),
self.setup.duplicate_name.contains(key),
self.setup.incompatible_with_tapminiscript.contains(key),
)
} else {
view::undefined_descriptor_key()
}
.map(move |msg| {
Message::DefineDescriptor(message::DefineDescriptor::PrimaryPath(
message::DefinePath::Key(i, msg),
))
})
})
.collect(),
self.setup.spending_threshold,
self.setup
.recovery_paths
self.paths
.iter()
.enumerate()
.map(|(i, path)| {
path.view(
&aliases,
&self.setup.duplicate_name,
&self.setup.incompatible_with_tapminiscript,
&self.duplicate_name,
&self.incompatible_with_tapminiscript,
)
.map(move |msg| {
Message::DefineDescriptor(message::DefineDescriptor::RecoveryPath(i, msg))
Message::DefineDescriptor(message::DefineDescriptor::Path(i, msg))
})
})
.collect(),
@ -663,7 +571,7 @@ impl DescriptorEditModal for EditSequenceModal {
return Command::perform(
async move { (path_index, sequence) },
|(path_index, sequence)| {
message::DefineDescriptor::RecoveryPath(
message::DefineDescriptor::Path(
path_index,
message::DefinePath::SequenceEdited(sequence),
)
@ -679,7 +587,7 @@ impl DescriptorEditModal for EditSequenceModal {
}
fn view(&self, _hws: &HardwareWallets) -> Element<Message> {
view::edit_sequence_modal(&self.sequence)
view::editor::edit_sequence_modal(&self.sequence)
}
}
@ -735,12 +643,10 @@ mod tests {
// Edit primary key
sandbox
.update(Message::DefineDescriptor(
message::DefineDescriptor::PrimaryPath(message::DefinePath::Key(
0,
message::DefineKey::Edit,
)),
))
.update(Message::DefineDescriptor(message::DefineDescriptor::Path(
0,
message::DefinePath::Key(0, message::DefineKey::Edit),
)))
.await;
sandbox.check(|step| assert!(step.modal.is_some()));
sandbox.update(Message::UseHotSigner).await;
@ -760,22 +666,18 @@ mod tests {
// Edit sequence
sandbox
.update(Message::DefineDescriptor(
message::DefineDescriptor::RecoveryPath(
0,
message::DefinePath::SequenceEdited(1000),
),
))
.update(Message::DefineDescriptor(message::DefineDescriptor::Path(
1,
message::DefinePath::SequenceEdited(1000),
)))
.await;
// Edit recovery key
sandbox
.update(Message::DefineDescriptor(
message::DefineDescriptor::RecoveryPath(
0,
message::DefinePath::Key(0, message::DefineKey::Edit),
),
))
.update(Message::DefineDescriptor(message::DefineDescriptor::Path(
1,
message::DefinePath::Key(0, message::DefineKey::Edit),
)))
.await;
sandbox.check(|step| assert!(step.modal.is_some()));
sandbox.update(Message::DefineDescriptor(
@ -832,19 +734,18 @@ mod tests {
// Use Specter device for primary key
sandbox
.update(Message::DefineDescriptor(
message::DefineDescriptor::PrimaryPath(specter_key.clone()),
))
.update(Message::DefineDescriptor(message::DefineDescriptor::Path(
0,
specter_key.clone(),
)))
.await;
// Edit recovery key
sandbox
.update(Message::DefineDescriptor(
message::DefineDescriptor::RecoveryPath(
0,
message::DefinePath::Key(0, message::DefineKey::Edit),
),
))
.update(Message::DefineDescriptor(message::DefineDescriptor::Path(
1,
message::DefinePath::Key(0, message::DefineKey::Edit),
)))
.await;
sandbox.check(|step| assert!(step.modal.is_some()));
sandbox.update(Message::DefineDescriptor(
@ -872,12 +773,10 @@ mod tests {
// Now edit primary key to use hot signer instead of Specter device
sandbox
.update(Message::DefineDescriptor(
message::DefineDescriptor::PrimaryPath(message::DefinePath::Key(
0,
message::DefineKey::Edit,
)),
))
.update(Message::DefineDescriptor(message::DefineDescriptor::Path(
0,
message::DefinePath::Key(0, message::DefineKey::Edit),
)))
.await;
sandbox.check(|step| assert!(step.modal.is_some()));
sandbox.update(Message::UseHotSigner).await;
@ -901,9 +800,10 @@ mod tests {
// Now edit the recovery key to use Specter device
sandbox
.update(Message::DefineDescriptor(
message::DefineDescriptor::RecoveryPath(0, specter_key.clone()),
))
.update(Message::DefineDescriptor(message::DefineDescriptor::Path(
1,
specter_key.clone(),
)))
.await;
sandbox.check(|step| {
assert!((step).apply(&mut ctx));

View File

@ -0,0 +1,710 @@
use iced::widget::{
container, pick_list, scrollable, scrollable::Properties, slider, Button, Space,
};
use iced::{alignment, Alignment, Length};
use liana_ui::component::text;
use std::str::FromStr;
use liana::miniscript::bitcoin::{self, bip32::Fingerprint};
use liana_ui::{
color,
component::{
button, card, collapse, form, hw, separation,
text::{p1_regular, text, Text},
tooltip,
},
icon, image, theme,
widget::*,
};
use crate::installer::{
message::{self, Message},
prompt,
view::{defined_sequence, layout},
Error,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DescriptorKind {
P2WSH,
Taproot,
}
const DESCRIPTOR_KINDS: [DescriptorKind; 2] = [DescriptorKind::P2WSH, DescriptorKind::Taproot];
impl std::fmt::Display for DescriptorKind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::P2WSH => write!(f, "P2WSH"),
Self::Taproot => write!(f, "Taproot"),
}
}
}
#[allow(clippy::too_many_arguments)]
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())
.push(container(
pick_list(
&DESCRIPTOR_KINDS[..],
Some(if use_taproot {
DescriptorKind::Taproot
} else {
DescriptorKind::P2WSH
}),
|kind| Message::CreateTaprootDescriptor(kind == DescriptorKind::Taproot),
)
.style(theme::PickList::Secondary)
.padding(10),
));
container(
Column::new()
.spacing(20)
.push(Space::with_height(0))
.push(separation().width(500))
.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")
.style(color::GREY_2),
)
} else {
None
}),
)
.into()
}
#[allow(clippy::too_many_arguments)]
pub fn define_descriptor<'a>(
progress: (usize, usize),
email: Option<&'a str>,
use_taproot: bool,
paths: Vec<Element<'a, Message>>,
valid: bool,
error: Option<&String>,
) -> Element<'a, Message> {
layout(
progress,
email,
"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(use_taproot),
))
.push(
Column::new()
.width(Length::Fill)
.push(
Column::new()
.spacing(25)
.push(Column::with_children(paths).spacing(10))
.push(tooltip(prompt::DEFINE_DESCRIPTOR_RECOVERY_PATH_TOOLTIP)),
)
.spacing(25),
)
.push(
Row::new()
.spacing(10)
.push(
button::secondary(Some(icon::plus_icon()), "Add a recovery path")
.on_press(Message::DefineDescriptor(
message::DefineDescriptor::AddRecoveryPath,
))
.width(Length::Fixed(200.0)),
)
.push(if !valid {
button::primary(None, "Next").width(Length::Fixed(200.0))
} else {
button::primary(None, "Next")
.width(Length::Fixed(200.0))
.on_press(Message::Next)
}),
)
.push_maybe(error.map(|e| card::error("Failed to create descriptor", e.to_string())))
.push(Space::with_height(Length::Fixed(20.0)))
.spacing(50),
false,
Some(Message::Previous),
)
}
pub fn primary_path_view(
primary_threshold: usize,
primary_keys: Vec<Element<message::DefinePath>>,
) -> Element<message::DefinePath> {
Container::new(
Column::new().push(
Row::new()
.align_items(Alignment::Center)
.push_maybe(if primary_keys.len() > 1 {
Some(threshsold_input::threshsold_input(
primary_threshold,
primary_keys.len(),
message::DefinePath::ThresholdEdited,
))
} else {
None
})
.push(
scrollable(
Row::new()
.spacing(5)
.align_items(Alignment::Center)
.push(Row::with_children(primary_keys).spacing(5))
.push(
Button::new(
Container::new(icon::plus_icon().size(50))
.width(Length::Fixed(150.0))
.height(Length::Fixed(150.0))
.align_y(alignment::Vertical::Center)
.align_x(alignment::Horizontal::Center),
)
.width(Length::Fixed(150.0))
.height(Length::Fixed(150.0))
.style(theme::Button::TransparentBorder)
.on_press(message::DefinePath::AddKey),
)
.padding(5),
)
.direction(scrollable::Direction::Horizontal(
Properties::new().width(3).scroller_width(3),
)),
),
),
)
.padding(5)
.style(theme::Container::Card(theme::Card::Border))
.into()
}
pub fn recovery_path_view(
sequence: u16,
duplicate_sequence: bool,
recovery_threshold: usize,
recovery_keys: Vec<Element<message::DefinePath>>,
) -> Element<message::DefinePath> {
Container::new(
Column::new()
.push(defined_sequence(sequence, duplicate_sequence))
.push(
Row::new()
.align_items(Alignment::Center)
.push_maybe(if recovery_keys.len() > 1 {
Some(threshsold_input::threshsold_input(
recovery_threshold,
recovery_keys.len(),
message::DefinePath::ThresholdEdited,
))
} else {
None
})
.push(
scrollable(
Row::new()
.spacing(5)
.align_items(Alignment::Center)
.push(Row::with_children(recovery_keys).spacing(5))
.push(
Button::new(
Container::new(icon::plus_icon().size(50))
.width(Length::Fixed(150.0))
.height(Length::Fixed(150.0))
.align_y(alignment::Vertical::Center)
.align_x(alignment::Horizontal::Center),
)
.width(Length::Fixed(150.0))
.height(Length::Fixed(150.0))
.style(theme::Button::TransparentBorder)
.on_press(message::DefinePath::AddKey),
)
.padding(5),
)
.direction(scrollable::Direction::Horizontal(
Properties::new().width(3).scroller_width(3),
)),
),
),
)
.padding(5)
.style(theme::Container::Card(theme::Card::Border))
.into()
}
pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> {
card::simple(
Column::new()
.width(Length::Fill)
.align_items(Alignment::Center)
.push(
Row::new()
.align_items(Alignment::Center)
.push(Space::with_width(Length::Fill))
.push(
Button::new(icon::cross_icon())
.style(theme::Button::Transparent)
.on_press(message::DefineKey::Delete),
),
)
.push(
Container::new(
Column::new()
.spacing(15)
.align_items(Alignment::Center)
.push(image::key_mark_icon().width(Length::Fixed(30.0))),
)
.height(Length::Fill)
.align_y(alignment::Vertical::Center),
)
.push(
button::secondary(Some(icon::pencil_icon()), "Set")
.on_press(message::DefineKey::Edit),
)
.push(Space::with_height(Length::Fixed(5.0))),
)
.padding(5)
.height(Length::Fixed(150.0))
.width(Length::Fixed(150.0))
.into()
}
pub fn defined_descriptor_key<'a>(
name: String,
duplicate_name: bool,
incompatible_with_tapminiscript: bool,
) -> Element<'a, message::DefineKey> {
let col = Column::new()
.width(Length::Fill)
.align_items(Alignment::Center)
.push(
Row::new()
.align_items(Alignment::Center)
.push(Space::with_width(Length::Fill))
.push(
Button::new(icon::cross_icon())
.style(theme::Button::Transparent)
.on_press(message::DefineKey::Delete),
),
)
.push(
Container::new(
Column::new()
.spacing(10)
.align_items(Alignment::Center)
.push(
scrollable(
Column::new()
.push(text(name).bold())
.push(Space::with_height(Length::Fixed(5.0))),
)
.direction(scrollable::Direction::Horizontal(
Properties::new().width(5).scroller_width(5),
)),
)
.push(image::success_mark_icon().width(Length::Fixed(50.0)))
.push(Space::with_width(Length::Fixed(1.0))),
)
.height(Length::Fill)
.align_y(alignment::Vertical::Center),
)
.push(
button::secondary(Some(icon::pencil_icon()), "Edit").on_press(message::DefineKey::Edit),
)
.push(Space::with_height(Length::Fixed(5.0)));
if duplicate_name {
Column::new()
.align_items(Alignment::Center)
.push(
card::invalid(col)
.padding(5)
.height(Length::Fixed(150.0))
.width(Length::Fixed(150.0)),
)
.push(text("Duplicate name").small().style(color::RED))
.into()
} else if incompatible_with_tapminiscript {
Column::new()
.align_items(Alignment::Center)
.push(
card::invalid(col)
.padding(5)
.height(Length::Fixed(150.0))
.width(Length::Fixed(150.0)),
)
.push(
text("Taproot is not supported\nby this key device")
.small()
.style(color::RED),
)
.into()
} else {
card::simple(col)
.padding(5)
.height(Length::Fixed(150.0))
.width(Length::Fixed(150.0))
.into()
}
}
#[allow(clippy::too_many_arguments)]
pub fn edit_key_modal<'a>(
network: bitcoin::Network,
hws: Vec<Element<'a, Message>>,
keys: Vec<Element<'a, Message>>,
error: Option<&Error>,
chosen_signer: Option<Fingerprint>,
hot_signer_fingerprint: &Fingerprint,
signer_alias: Option<&'a String>,
form_xpub: &form::Value<String>,
form_name: &'a form::Value<String>,
edit_name: bool,
duplicate_master_fg: bool,
) -> Element<'a, Message> {
Column::new()
.push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
.push(card::simple(
Column::new()
.spacing(25)
.push(
Column::new()
.push(
Container::new(text("Select a signing device:").bold())
.width(Length::Fill),
)
.spacing(10)
.push(
Column::with_children(hws).spacing(10)
)
.push(
Column::with_children(keys).spacing(10)
)
.push(
Button::new(if Some(*hot_signer_fingerprint) == chosen_signer {
hw::selected_hot_signer(hot_signer_fingerprint, signer_alias)
} else {
hw::unselected_hot_signer(hot_signer_fingerprint, signer_alias)
})
.width(Length::Fill)
.on_press(Message::UseHotSigner)
.style(theme::Button::Border),
)
.width(Length::Fill),
)
.push(
Column::new()
.spacing(5)
.push(text("Or enter an extended public key:").bold())
.push(
Row::new()
.push(
form::Form::new_trimmed(
&format!(
"[aabbccdd/42'/0']{}pub6DAkq8LWw91WGgUGnkR5Sbzjev5JCsXaTVZQ9MwsPV4BkNFKygtJ8GHodfDVx1udR723nT7JASqGPpKvz7zQ25pUTW6zVEBdiWoaC4aUqik",
if network == bitcoin::Network::Bitcoin {
"x"
} else {
"t"
}
),
form_xpub, |msg| {
Message::DefineDescriptor(
message::DefineDescriptor::KeyModal(
message::ImportKeyModal::XPubEdited(msg),),)
})
.warning(if network == bitcoin::Network::Bitcoin {
"Please enter correct xpub with origin and without appended derivation path"
} else {
"Please enter correct tpub with origin and without appended derivation path"
})
.size(text::P1_SIZE)
.padding(10),
)
.spacing(10)
),
)
.push(
if !edit_name && !form_xpub.value.is_empty() && form_xpub.valid {
Column::new().push(
Row::new()
.push(
Column::new()
.spacing(5)
.width(Length::Fill)
.push(
Row::new()
.spacing(5)
.push(text("Fingerprint alias:").bold())
.push(tooltip(
prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP,
)),
)
.push(text(&form_name.value)),
)
.push(
button::secondary(Some(icon::pencil_icon()), "Edit").on_press(
Message::DefineDescriptor(
message::DefineDescriptor::KeyModal(
message::ImportKeyModal::EditName,
),
),
),
),
)
} else if !form_xpub.value.is_empty() && form_xpub.valid {
Column::new()
.spacing(5)
.push(
Row::new()
.spacing(5)
.push(text("Fingerprint alias:").bold())
.push(tooltip(prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP)),
)
.push(
form::Form::new("Alias", form_name, |msg| {
Message::DefineDescriptor(message::DefineDescriptor::KeyModal(
message::ImportKeyModal::NameEdited(msg),
))
})
.warning("Please enter correct alias")
.size(text::P1_SIZE)
.padding(10),
)
} else {
Column::new()
},
)
.push_maybe(
if duplicate_master_fg {
Some(text("A single signing device may not be used more than once per path. (It can still be used in other paths.)").style(color::RED))
} else {
None
}
)
.push(
if form_xpub.valid && !form_xpub.value.is_empty() && !form_name.value.is_empty() && !duplicate_master_fg
{
button::primary(None, "Apply")
.on_press(Message::DefineDescriptor(
message::DefineDescriptor::KeyModal(
message::ImportKeyModal::ConfirmXpub,
),
))
.width(Length::Fixed(200.0))
} else {
button::primary(None, "Apply").width(Length::Fixed(100.0))
},
)
.align_items(Alignment::Center),
))
.width(Length::Fixed(600.0))
.into()
}
/// returns y,m,d,h,m
pub fn duration_from_sequence(sequence: u16) -> (u32, u32, u32, u32, u32) {
let mut n_minutes = sequence as u32 * 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;
n_minutes -= n_days * 1440;
let n_hours = n_minutes / 60;
n_minutes -= n_hours * 60;
(n_years, n_months, n_days, n_hours, n_minutes)
}
pub fn edit_sequence_modal<'a>(sequence: &form::Value<String>) -> Element<'a, Message> {
let mut col = Column::new()
.width(Length::Fill)
.spacing(20)
.align_items(Alignment::Center)
.push(text("Activate recovery path after:"))
.push(
Row::new()
.push(
Container::new(
form::Form::new_trimmed("ex: 1000", sequence, |v| {
Message::DefineDescriptor(message::DefineDescriptor::SequenceModal(
message::SequenceModal::SequenceEdited(v),
))
})
.warning("Sequence must be superior to 0 and inferior to 65535"),
)
.width(Length::Fixed(200.0)),
)
.spacing(10)
.push(text("blocks").bold()),
);
if sequence.valid {
if let Ok(sequence) = u16::from_str(&sequence.value) {
let (n_years, n_months, n_days, n_hours, n_minutes) = duration_from_sequence(sequence);
col = col
.push(
[
(n_years, "year"),
(n_months, "month"),
(n_days, "day"),
(n_hours, "hour"),
(n_minutes, "minute"),
]
.iter()
.fold(Row::new().spacing(5), |row, (n, unit)| {
row.push_maybe(if *n > 0 {
Some(
text(format!("{} {}{}", n, unit, if *n > 1 { "s" } else { "" }))
.bold(),
)
} else {
None
})
}),
)
.push(
Container::new(
slider(1..=u16::MAX, sequence, |v| {
Message::DefineDescriptor(message::DefineDescriptor::SequenceModal(
message::SequenceModal::SequenceEdited(v.to_string()),
))
})
.step(144_u16), // 144 blocks per day
)
.width(Length::Fixed(500.0)),
);
}
}
card::simple(col.push(if sequence.valid {
button::primary(None, "Apply")
.on_press(Message::DefineDescriptor(
message::DefineDescriptor::SequenceModal(message::SequenceModal::ConfirmSequence),
))
.width(Length::Fixed(200.0))
} else {
button::primary(None, "Apply").width(Length::Fixed(200.0))
}))
.width(Length::Fixed(800.0))
.into()
}
mod threshsold_input {
use iced::alignment::{self, Alignment};
use iced::widget::{component, Component};
use iced::Length;
use liana_ui::{component::text::*, icon, theme, widget::*};
pub struct ThresholdInput<Message> {
value: usize,
max: usize,
on_change: Box<dyn Fn(usize) -> Message>,
}
pub fn threshsold_input<Message>(
value: usize,
max: usize,
on_change: impl Fn(usize) -> Message + 'static,
) -> ThresholdInput<Message> {
ThresholdInput::new(value, max, on_change)
}
#[derive(Debug, Clone)]
pub enum Event {
IncrementPressed,
DecrementPressed,
}
impl<Message> ThresholdInput<Message> {
pub fn new(
value: usize,
max: usize,
on_change: impl Fn(usize) -> Message + 'static,
) -> Self {
Self {
value,
max,
on_change: Box::new(on_change),
}
}
}
impl<Message> Component<Message, theme::Theme> for ThresholdInput<Message> {
type State = ();
type Event = Event;
fn update(&mut self, _state: &mut Self::State, event: Event) -> Option<Message> {
match event {
Event::IncrementPressed => {
if self.value < self.max {
Some((self.on_change)(self.value.saturating_add(1)))
} else {
None
}
}
Event::DecrementPressed => {
if self.value > 1 {
Some((self.on_change)(self.value.saturating_sub(1)))
} else {
None
}
}
}
}
fn view(&self, _state: &Self::State) -> Element<Self::Event> {
let button = |label, on_press| {
Button::new(label)
.style(theme::Button::Transparent)
.width(Length::Fixed(50.0))
.on_press(on_press)
};
Column::new()
.width(Length::Fixed(150.0))
.push(button(icon::up_icon().size(30), Event::IncrementPressed))
.push(text("Threshold:").small().bold())
.push(
Container::new(text(format!("{}/{}", self.value, self.max)).size(30))
.align_y(alignment::Vertical::Center),
)
.push(button(icon::down_icon().size(30), Event::DecrementPressed))
.align_items(Alignment::Center)
.into()
}
}
impl<'a, Message> From<ThresholdInput<Message>> for Element<'a, Message>
where
Message: 'a,
{
fn from(numeric_input: ThresholdInput<Message>) -> Self {
component(numeric_input)
}
}
}

View File

@ -1,8 +1,7 @@
pub mod editor;
use async_hwi::utils::extract_keys_and_template;
use iced::widget::{
checkbox, container, pick_list, radio, scrollable, scrollable::Properties, slider, Button,
Space, TextInput,
};
use iced::widget::{checkbox, radio, scrollable, scrollable::Properties, Button, Space, TextInput};
use iced::{
alignment,
widget::{progress_bar, tooltip as iced_tooltip},
@ -25,9 +24,8 @@ use liana_ui::{
component::{
button, card, collapse, form, hw, separation,
text::{h2, h3, h4_bold, h5_regular, p1_regular, text, Text},
tooltip,
},
icon, image, theme,
icon, theme,
widget::*,
};
@ -37,6 +35,7 @@ use crate::{
message::{self, DefineBitcoind, DefineNode, Message},
prompt,
step::{DownloadState, InstallState},
view::editor::duration_from_sequence,
Error,
},
node::{
@ -45,252 +44,6 @@ use crate::{
},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DescriptorKind {
P2WSH,
Taproot,
}
const DESCRIPTOR_KINDS: [DescriptorKind; 2] = [DescriptorKind::P2WSH, DescriptorKind::Taproot];
impl std::fmt::Display for DescriptorKind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::P2WSH => write!(f, "P2WSH"),
Self::Taproot => write!(f, "Taproot"),
}
}
}
#[allow(clippy::too_many_arguments)]
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())
.push(container(
pick_list(
&DESCRIPTOR_KINDS[..],
Some(if use_taproot {
DescriptorKind::Taproot
} else {
DescriptorKind::P2WSH
}),
|kind| Message::CreateTaprootDescriptor(kind == DescriptorKind::Taproot),
)
.style(theme::PickList::Secondary)
.padding(10),
));
container(
Column::new()
.spacing(20)
.push(Space::with_height(0))
.push(separation().width(500))
.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")
.style(color::GREY_2),
)
} else {
None
}),
)
.into()
}
#[allow(clippy::too_many_arguments)]
pub fn define_descriptor<'a>(
progress: (usize, usize),
email: Option<&'a str>,
use_taproot: bool,
spending_keys: Vec<Element<'a, Message>>,
spending_threshold: usize,
recovery_paths: Vec<Element<'a, Message>>,
valid: bool,
error: Option<&String>,
) -> Element<'a, Message> {
let col_spending_keys = Column::new()
.push(
Row::new()
.spacing(10)
.push(text("Primary path:").bold())
.push(tooltip(prompt::DEFINE_DESCRIPTOR_PRIMARY_PATH_TOOLTIP)),
)
.push(Container::new(
Row::new()
.align_items(Alignment::Center)
.push_maybe(if spending_keys.len() > 1 {
Some(threshsold_input::threshsold_input(
spending_threshold,
spending_keys.len(),
|value| {
Message::DefineDescriptor(message::DefineDescriptor::PrimaryPath(
message::DefinePath::ThresholdEdited(value),
))
},
))
} else {
None
})
.push(
scrollable(
Row::new()
.spacing(5)
.align_items(Alignment::Center)
.push(Row::with_children(spending_keys).spacing(5))
.push(
Button::new(
Container::new(icon::plus_icon().size(50))
.width(Length::Fixed(150.0))
.height(Length::Fixed(150.0))
.align_y(alignment::Vertical::Center)
.align_x(alignment::Horizontal::Center),
)
.width(Length::Fixed(150.0))
.height(Length::Fixed(150.0))
.style(theme::Button::TransparentBorder)
.on_press(
Message::DefineDescriptor(
message::DefineDescriptor::PrimaryPath(
message::DefinePath::AddKey,
),
),
),
)
.padding(5),
)
.direction(scrollable::Direction::Horizontal(
Properties::new().width(3).scroller_width(3),
)),
),
))
.spacing(10);
layout(
progress,
email,
"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(use_taproot),
))
.push(
Column::new()
.width(Length::Fill)
.push(
Column::new()
.spacing(25)
.push(col_spending_keys)
.push(
Row::new()
.spacing(10)
.push(text("Recovery paths:").bold())
.push(tooltip(prompt::DEFINE_DESCRIPTOR_RECOVERY_PATH_TOOLTIP)),
)
.push(Column::with_children(recovery_paths).spacing(10)),
)
.spacing(25),
)
.push(
Row::new()
.spacing(10)
.push(
button::secondary(Some(icon::plus_icon()), "Add a recovery path")
.on_press(Message::DefineDescriptor(
message::DefineDescriptor::AddRecoveryPath,
))
.width(Length::Fixed(200.0)),
)
.push(if !valid {
button::secondary(None, "Next").width(Length::Fixed(200.0))
} else {
button::secondary(None, "Next")
.width(Length::Fixed(200.0))
.on_press(Message::Next)
}),
)
.push_maybe(error.map(|e| card::error("Failed to create descriptor", e.to_string())))
.push(Space::with_height(Length::Fixed(20.0)))
.spacing(50),
false,
Some(Message::Previous),
)
}
pub fn recovery_path_view(
sequence: u16,
duplicate_sequence: bool,
recovery_threshold: usize,
recovery_keys: Vec<Element<message::DefinePath>>,
) -> Element<message::DefinePath> {
Container::new(
Column::new()
.push(defined_sequence(sequence, duplicate_sequence))
.push(
Row::new()
.align_items(Alignment::Center)
.push_maybe(if recovery_keys.len() > 1 {
Some(threshsold_input::threshsold_input(
recovery_threshold,
recovery_keys.len(),
message::DefinePath::ThresholdEdited,
))
} else {
None
})
.push(
scrollable(
Row::new()
.spacing(5)
.align_items(Alignment::Center)
.push(Row::with_children(recovery_keys).spacing(5))
.push(
Button::new(
Container::new(icon::plus_icon().size(50))
.width(Length::Fixed(150.0))
.height(Length::Fixed(150.0))
.align_y(alignment::Vertical::Center)
.align_x(alignment::Horizontal::Center),
)
.width(Length::Fixed(150.0))
.height(Length::Fixed(150.0))
.style(theme::Button::TransparentBorder)
.on_press(message::DefinePath::AddKey),
)
.padding(5),
)
.direction(scrollable::Direction::Horizontal(
Properties::new().width(3).scroller_width(3),
)),
),
),
)
.padding(5)
.style(theme::Container::Card(theme::Card::Border))
.into()
}
pub fn import_wallet_or_descriptor<'a>(
progress: (usize, usize),
email: Option<&'a str>,
@ -1722,365 +1475,6 @@ pub fn defined_sequence<'a>(
.into()
}
pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> {
card::simple(
Column::new()
.width(Length::Fill)
.align_items(Alignment::Center)
.push(
Row::new()
.align_items(Alignment::Center)
.push(Space::with_width(Length::Fill))
.push(
Button::new(icon::cross_icon())
.style(theme::Button::Transparent)
.on_press(message::DefineKey::Delete),
),
)
.push(
Container::new(
Column::new()
.spacing(15)
.align_items(Alignment::Center)
.push(image::key_mark_icon().width(Length::Fixed(30.0))),
)
.height(Length::Fill)
.align_y(alignment::Vertical::Center),
)
.push(
button::secondary(Some(icon::pencil_icon()), "Set")
.on_press(message::DefineKey::Edit),
)
.push(Space::with_height(Length::Fixed(5.0))),
)
.padding(5)
.height(Length::Fixed(150.0))
.width(Length::Fixed(150.0))
.into()
}
pub fn defined_descriptor_key<'a>(
name: String,
duplicate_name: bool,
incompatible_with_tapminiscript: bool,
) -> Element<'a, message::DefineKey> {
let col = Column::new()
.width(Length::Fill)
.align_items(Alignment::Center)
.push(
Row::new()
.align_items(Alignment::Center)
.push(Space::with_width(Length::Fill))
.push(
Button::new(icon::cross_icon())
.style(theme::Button::Transparent)
.on_press(message::DefineKey::Delete),
),
)
.push(
Container::new(
Column::new()
.spacing(10)
.align_items(Alignment::Center)
.push(
scrollable(
Column::new()
.push(text(name).bold())
.push(Space::with_height(Length::Fixed(5.0))),
)
.direction(scrollable::Direction::Horizontal(
Properties::new().width(5).scroller_width(5),
)),
)
.push(image::success_mark_icon().width(Length::Fixed(50.0)))
.push(Space::with_width(Length::Fixed(1.0))),
)
.height(Length::Fill)
.align_y(alignment::Vertical::Center),
)
.push(
button::secondary(Some(icon::pencil_icon()), "Edit").on_press(message::DefineKey::Edit),
)
.push(Space::with_height(Length::Fixed(5.0)));
if duplicate_name {
Column::new()
.align_items(Alignment::Center)
.push(
card::invalid(col)
.padding(5)
.height(Length::Fixed(150.0))
.width(Length::Fixed(150.0)),
)
.push(text("Duplicate name").small().style(color::RED))
.into()
} else if incompatible_with_tapminiscript {
Column::new()
.align_items(Alignment::Center)
.push(
card::invalid(col)
.padding(5)
.height(Length::Fixed(150.0))
.width(Length::Fixed(150.0)),
)
.push(
text("Taproot is not supported\nby this key device")
.small()
.style(color::RED),
)
.into()
} else {
card::simple(col)
.padding(5)
.height(Length::Fixed(150.0))
.width(Length::Fixed(150.0))
.into()
}
}
#[allow(clippy::too_many_arguments)]
pub fn edit_key_modal<'a>(
network: bitcoin::Network,
hws: Vec<Element<'a, Message>>,
keys: Vec<Element<'a, Message>>,
error: Option<&Error>,
chosen_signer: Option<Fingerprint>,
hot_signer_fingerprint: &Fingerprint,
signer_alias: Option<&'a String>,
form_xpub: &form::Value<String>,
form_name: &'a form::Value<String>,
edit_name: bool,
duplicate_master_fg: bool,
) -> Element<'a, Message> {
Column::new()
.push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
.push(card::simple(
Column::new()
.spacing(25)
.push(
Column::new()
.push(
Container::new(text("Select a signing device:").bold())
.width(Length::Fill),
)
.spacing(10)
.push(
Column::with_children(hws).spacing(10)
)
.push(
Column::with_children(keys).spacing(10)
)
.push(
Button::new(if Some(*hot_signer_fingerprint) == chosen_signer {
hw::selected_hot_signer(hot_signer_fingerprint, signer_alias)
} else {
hw::unselected_hot_signer(hot_signer_fingerprint, signer_alias)
})
.width(Length::Fill)
.on_press(Message::UseHotSigner)
.style(theme::Button::Border),
)
.width(Length::Fill),
)
.push(
Column::new()
.spacing(5)
.push(text("Or enter an extended public key:").bold())
.push(
Row::new()
.push(
form::Form::new_trimmed(
&format!(
"[aabbccdd/42'/0']{}pub6DAkq8LWw91WGgUGnkR5Sbzjev5JCsXaTVZQ9MwsPV4BkNFKygtJ8GHodfDVx1udR723nT7JASqGPpKvz7zQ25pUTW6zVEBdiWoaC4aUqik",
if network == bitcoin::Network::Bitcoin {
"x"
} else {
"t"
}
),
form_xpub, |msg| {
Message::DefineDescriptor(
message::DefineDescriptor::KeyModal(
message::ImportKeyModal::XPubEdited(msg),),)
})
.warning(if network == bitcoin::Network::Bitcoin {
"Please enter correct xpub with origin and without appended derivation path"
} else {
"Please enter correct tpub with origin and without appended derivation path"
})
.size(text::P1_SIZE)
.padding(10),
)
.spacing(10)
),
)
.push(
if !edit_name && !form_xpub.value.is_empty() && form_xpub.valid {
Column::new().push(
Row::new()
.push(
Column::new()
.spacing(5)
.width(Length::Fill)
.push(
Row::new()
.spacing(5)
.push(text("Fingerprint alias:").bold())
.push(tooltip(
prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP,
)),
)
.push(text(&form_name.value)),
)
.push(
button::secondary(Some(icon::pencil_icon()), "Edit").on_press(
Message::DefineDescriptor(
message::DefineDescriptor::KeyModal(
message::ImportKeyModal::EditName,
),
),
),
),
)
} else if !form_xpub.value.is_empty() && form_xpub.valid {
Column::new()
.spacing(5)
.push(
Row::new()
.spacing(5)
.push(text("Fingerprint alias:").bold())
.push(tooltip(prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP)),
)
.push(
form::Form::new("Alias", form_name, |msg| {
Message::DefineDescriptor(message::DefineDescriptor::KeyModal(
message::ImportKeyModal::NameEdited(msg),
))
})
.warning("Please enter correct alias")
.size(text::P1_SIZE)
.padding(10),
)
} else {
Column::new()
},
)
.push_maybe(
if duplicate_master_fg {
Some(text("A single signing device may not be used more than once per path. (It can still be used in other paths.)").style(color::RED))
} else {
None
}
)
.push(
if form_xpub.valid && !form_xpub.value.is_empty() && !form_name.value.is_empty() && !duplicate_master_fg
{
button::secondary(None, "Apply")
.on_press(Message::DefineDescriptor(
message::DefineDescriptor::KeyModal(
message::ImportKeyModal::ConfirmXpub,
),
))
.width(Length::Fixed(200.0))
} else {
button::secondary(None, "Apply").width(Length::Fixed(100.0))
},
)
.align_items(Alignment::Center),
))
.width(Length::Fixed(600.0))
.into()
}
/// returns y,m,d,h,m
fn duration_from_sequence(sequence: u16) -> (u32, u32, u32, u32, u32) {
let mut n_minutes = sequence as u32 * 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;
n_minutes -= n_days * 1440;
let n_hours = n_minutes / 60;
n_minutes -= n_hours * 60;
(n_years, n_months, n_days, n_hours, n_minutes)
}
pub fn edit_sequence_modal<'a>(sequence: &form::Value<String>) -> Element<'a, Message> {
let mut col = Column::new()
.width(Length::Fill)
.spacing(20)
.align_items(Alignment::Center)
.push(text("Activate recovery path after:"))
.push(
Row::new()
.push(
Container::new(
form::Form::new_trimmed("ex: 1000", sequence, |v| {
Message::DefineDescriptor(message::DefineDescriptor::SequenceModal(
message::SequenceModal::SequenceEdited(v),
))
})
.warning("Sequence must be superior to 0 and inferior to 65535"),
)
.width(Length::Fixed(200.0)),
)
.spacing(10)
.push(text("blocks").bold()),
);
if sequence.valid {
if let Ok(sequence) = u16::from_str(&sequence.value) {
let (n_years, n_months, n_days, n_hours, n_minutes) = duration_from_sequence(sequence);
col = col
.push(
[
(n_years, "year"),
(n_months, "month"),
(n_days, "day"),
(n_hours, "hour"),
(n_minutes, "minute"),
]
.iter()
.fold(Row::new().spacing(5), |row, (n, unit)| {
row.push_maybe(if *n > 0 {
Some(
text(format!("{} {}{}", n, unit, if *n > 1 { "s" } else { "" }))
.bold(),
)
} else {
None
})
}),
)
.push(
Container::new(
slider(1..=u16::MAX, sequence, |v| {
Message::DefineDescriptor(message::DefineDescriptor::SequenceModal(
message::SequenceModal::SequenceEdited(v.to_string()),
))
})
.step(144_u16), // 144 blocks per day
)
.width(Length::Fixed(500.0)),
);
}
}
card::simple(col.push(if sequence.valid {
button::secondary(None, "Apply")
.on_press(Message::DefineDescriptor(
message::DefineDescriptor::SequenceModal(message::SequenceModal::ConfirmSequence),
))
.width(Length::Fixed(200.0))
} else {
button::secondary(None, "Apply").width(Length::Fixed(200.0))
}))
.width(Length::Fixed(800.0))
.into()
}
pub fn hw_list_view(
i: usize,
hw: &HardwareWallet,
@ -2589,98 +1983,3 @@ fn layout<'a>(
.style(theme::Container::Background)
.into()
}
mod threshsold_input {
use iced::alignment::{self, Alignment};
use iced::widget::{component, Component};
use iced::Length;
use liana_ui::{component::text::*, icon, theme, widget::*};
pub struct ThresholdInput<Message> {
value: usize,
max: usize,
on_change: Box<dyn Fn(usize) -> Message>,
}
pub fn threshsold_input<Message>(
value: usize,
max: usize,
on_change: impl Fn(usize) -> Message + 'static,
) -> ThresholdInput<Message> {
ThresholdInput::new(value, max, on_change)
}
#[derive(Debug, Clone)]
pub enum Event {
IncrementPressed,
DecrementPressed,
}
impl<Message> ThresholdInput<Message> {
pub fn new(
value: usize,
max: usize,
on_change: impl Fn(usize) -> Message + 'static,
) -> Self {
Self {
value,
max,
on_change: Box::new(on_change),
}
}
}
impl<Message> Component<Message, theme::Theme> for ThresholdInput<Message> {
type State = ();
type Event = Event;
fn update(&mut self, _state: &mut Self::State, event: Event) -> Option<Message> {
match event {
Event::IncrementPressed => {
if self.value < self.max {
Some((self.on_change)(self.value.saturating_add(1)))
} else {
None
}
}
Event::DecrementPressed => {
if self.value > 1 {
Some((self.on_change)(self.value.saturating_sub(1)))
} else {
None
}
}
}
}
fn view(&self, _state: &Self::State) -> Element<Self::Event> {
let button = |label, on_press| {
Button::new(label)
.style(theme::Button::Transparent)
.width(Length::Fixed(50.0))
.on_press(on_press)
};
Column::new()
.width(Length::Fixed(150.0))
.push(button(icon::up_icon().size(30), Event::IncrementPressed))
.push(text("Threshold:").small().bold())
.push(
Container::new(text(format!("{}/{}", self.value, self.max)).size(30))
.align_y(alignment::Vertical::Center),
)
.push(button(icon::down_icon().size(30), Event::DecrementPressed))
.align_items(Alignment::Center)
.into()
}
}
impl<'a, Message> From<ThresholdInput<Message>> for Element<'a, Message>
where
Message: 'a,
{
fn from(numeric_input: ThresholdInput<Message>) -> Self {
component(numeric_input)
}
}
}