Add inheritance and custom templates
This commit is contained in:
parent
5889e60dc2
commit
bd03cc9cff
@ -45,8 +45,9 @@ impl RemoteBackend {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DescriptorTemplate {
|
||||
#[default]
|
||||
SimpleInheritance,
|
||||
Custom,
|
||||
}
|
||||
@ -78,7 +79,7 @@ impl Context {
|
||||
remote_backend: RemoteBackend,
|
||||
) -> Self {
|
||||
Self {
|
||||
descriptor_template: DescriptorTemplate::Custom,
|
||||
descriptor_template: DescriptorTemplate::default(),
|
||||
bitcoin_config: BitcoinConfig {
|
||||
network,
|
||||
poll_interval_secs: Duration::from_secs(30),
|
||||
|
||||
@ -8,13 +8,13 @@ use super::{context, Error};
|
||||
use crate::{
|
||||
download::Progress,
|
||||
hw::HardwareWalletMessage,
|
||||
installer::step::descriptor::editor::key::Key,
|
||||
lianalite::client::{auth::AuthClient, backend::api},
|
||||
node::{
|
||||
bitcoind::{Bitcoind, ConfigField, RpcAuthType},
|
||||
electrum, NodeType,
|
||||
},
|
||||
};
|
||||
use async_hwi::{DeviceKind, Version};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
@ -108,13 +108,15 @@ pub enum InternalBitcoindMsg {
|
||||
Start,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DefineDescriptor {
|
||||
ChangeTemplate(context::DescriptorTemplate),
|
||||
ImportDescriptor(String),
|
||||
Path(usize, DefinePath),
|
||||
AddRecoveryPath,
|
||||
KeyModal(ImportKeyModal),
|
||||
SequenceModal(SequenceModal),
|
||||
ThresholdSequenceModal(ThresholdSequenceModal),
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@ -125,6 +127,7 @@ pub enum DefinePath {
|
||||
ThresholdEdited(usize),
|
||||
SequenceEdited(u16),
|
||||
EditSequence,
|
||||
EditThreshold,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@ -133,26 +136,22 @@ pub enum DefineKey {
|
||||
Delete,
|
||||
Edit,
|
||||
Clipboard(String),
|
||||
Edited(
|
||||
String,
|
||||
DescriptorPublicKey,
|
||||
Option<DeviceKind>,
|
||||
Option<Version>,
|
||||
),
|
||||
Edited(Key),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ImportKeyModal {
|
||||
HWXpubImported(Result<DescriptorPublicKey, Error>),
|
||||
FetchedKey(Result<Key, Error>),
|
||||
XPubEdited(String),
|
||||
EditName,
|
||||
NameEdited(String),
|
||||
ManuallyImportXpub,
|
||||
ConfirmXpub,
|
||||
SelectKey(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SequenceModal {
|
||||
pub enum ThresholdSequenceModal {
|
||||
ThresholdEdited(usize),
|
||||
SequenceEdited(String),
|
||||
ConfirmSequence,
|
||||
Confirm,
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ use liana_ui::{component::form, widget::Element};
|
||||
use async_hwi::{DeviceKind, Version};
|
||||
|
||||
use crate::{
|
||||
hw::{HardwareWallet, HardwareWallets},
|
||||
hw::{is_compatible_with_tapminiscript, HardwareWallet, HardwareWallets},
|
||||
installer::{
|
||||
message::{self, Message},
|
||||
view, Error,
|
||||
@ -41,7 +41,7 @@ pub fn new_multixkey_from_xpub(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Key {
|
||||
pub device_kind: Option<DeviceKind>,
|
||||
pub is_hot_signer: bool,
|
||||
@ -49,6 +49,7 @@ pub struct Key {
|
||||
pub name: String,
|
||||
pub fingerprint: Fingerprint,
|
||||
pub key: DescriptorPublicKey,
|
||||
pub is_compatible_taproot: bool,
|
||||
}
|
||||
|
||||
pub fn check_key_network(key: &DescriptorPublicKey, network: Network) -> bool {
|
||||
@ -81,7 +82,7 @@ pub struct EditXpubModal {
|
||||
|
||||
form_name: form::Value<String>,
|
||||
form_xpub: form::Value<String>,
|
||||
edit_name: bool,
|
||||
manually_imported_xpub: bool,
|
||||
|
||||
other_path_keys: HashSet<Fingerprint>,
|
||||
duplicate_master_fg: bool,
|
||||
@ -89,7 +90,7 @@ pub struct EditXpubModal {
|
||||
keys: Vec<Key>,
|
||||
hot_signer: Arc<Mutex<Signer>>,
|
||||
hot_signer_fingerprint: Fingerprint,
|
||||
chosen_signer: Option<(Fingerprint, Option<DeviceKind>, Option<Version>)>,
|
||||
chosen_signer: Option<Key>,
|
||||
}
|
||||
|
||||
impl EditXpubModal {
|
||||
@ -97,7 +98,7 @@ impl EditXpubModal {
|
||||
pub fn new(
|
||||
device_must_support_tapminiscript: bool,
|
||||
other_path_keys: HashSet<Fingerprint>,
|
||||
key: Option<Fingerprint>,
|
||||
key: Option<Key>,
|
||||
path_index: usize,
|
||||
key_index: usize,
|
||||
network: Network,
|
||||
@ -105,41 +106,34 @@ impl EditXpubModal {
|
||||
hot_signer_fingerprint: Fingerprint,
|
||||
keys: Vec<Key>,
|
||||
) -> Self {
|
||||
// The xpub is manually imported if the key is neither from a device or the hot signer.
|
||||
let manually_imported_xpub = key
|
||||
.as_ref()
|
||||
.map(|k| !k.is_hot_signer && k.device_kind.is_none())
|
||||
.unwrap_or(false);
|
||||
Self {
|
||||
device_must_support_tapminiscript,
|
||||
other_path_keys,
|
||||
form_name: form::Value {
|
||||
valid: true,
|
||||
value: key
|
||||
.map(|fg| {
|
||||
keys.iter()
|
||||
.find(|k| k.fingerprint == fg)
|
||||
.expect("must be stored")
|
||||
.name
|
||||
.clone()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
value: key.as_ref().map(|k| k.name.clone()).unwrap_or_default(),
|
||||
},
|
||||
form_xpub: form::Value {
|
||||
valid: true,
|
||||
value: key
|
||||
.map(|fg| {
|
||||
keys.iter()
|
||||
.find(|k| k.fingerprint == fg)
|
||||
.expect("must be stored")
|
||||
.key
|
||||
.to_string()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
value: if manually_imported_xpub {
|
||||
key.as_ref().map(|k| k.key.to_string()).unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
},
|
||||
manually_imported_xpub,
|
||||
keys,
|
||||
path_index,
|
||||
key_index,
|
||||
processing: false,
|
||||
error: None,
|
||||
network,
|
||||
edit_name: false,
|
||||
chosen_signer: key.map(|k| (k, None, None)),
|
||||
chosen_signer: key,
|
||||
hot_signer_fingerprint,
|
||||
hot_signer,
|
||||
duplicate_master_fg: false,
|
||||
@ -171,13 +165,41 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
..
|
||||
}) = hws.list.get(i)
|
||||
{
|
||||
self.chosen_signer = Some((*fingerprint, Some(*kind), version.clone()));
|
||||
self.processing = true;
|
||||
self.manually_imported_xpub = false;
|
||||
let device_version = version.clone();
|
||||
let fingerprint = *fingerprint;
|
||||
let device_kind = *kind;
|
||||
let network = self.network;
|
||||
return Command::perform(
|
||||
get_extended_pubkey(device.clone(), *fingerprint, self.network),
|
||||
|res| {
|
||||
get_extended_pubkey(device.clone(), fingerprint, self.network),
|
||||
move |res| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeyModal(
|
||||
message::ImportKeyModal::HWXpubImported(res),
|
||||
message::ImportKeyModal::FetchedKey(match res {
|
||||
Err(e) => Err(e),
|
||||
Ok(key) => {
|
||||
if check_key_network(&key, network) {
|
||||
Ok(Key {
|
||||
is_hot_signer: false,
|
||||
fingerprint,
|
||||
name: "".to_string(),
|
||||
key,
|
||||
is_compatible_taproot:
|
||||
is_compatible_with_tapminiscript(
|
||||
&device_kind,
|
||||
device_version.as_ref(),
|
||||
),
|
||||
device_kind: Some(device_kind),
|
||||
device_version,
|
||||
})
|
||||
} else {
|
||||
Err(Error::Unexpected(
|
||||
"Fetched key does not have the correct network"
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}),
|
||||
))
|
||||
},
|
||||
);
|
||||
@ -187,24 +209,10 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
return self.load();
|
||||
}
|
||||
Message::UseHotSigner => {
|
||||
self.manually_imported_xpub = false;
|
||||
let fingerprint = self.hot_signer.lock().unwrap().fingerprint();
|
||||
self.chosen_signer = Some((fingerprint, None, None));
|
||||
self.form_xpub.valid = true;
|
||||
if let Some(alias) = self
|
||||
.keys
|
||||
.iter()
|
||||
.find(|key| key.fingerprint == fingerprint)
|
||||
.map(|k| k.name.clone())
|
||||
{
|
||||
self.form_name.valid = true;
|
||||
self.form_name.value = alias;
|
||||
self.edit_name = false;
|
||||
} else {
|
||||
self.edit_name = true;
|
||||
self.form_name.value = String::new();
|
||||
}
|
||||
let derivation_path = default_derivation_path(self.network);
|
||||
self.form_xpub.value = format!(
|
||||
let key_str = format!(
|
||||
"[{}{}]{}",
|
||||
fingerprint,
|
||||
derivation_path.to_string().trim_start_matches('m'),
|
||||
@ -213,27 +221,36 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
.unwrap()
|
||||
.get_extended_pubkey(&derivation_path)
|
||||
);
|
||||
self.chosen_signer = Some(Key {
|
||||
is_hot_signer: true,
|
||||
fingerprint,
|
||||
name: "".to_string(),
|
||||
key: DescriptorPublicKey::from_str(&key_str).unwrap(),
|
||||
is_compatible_taproot: true,
|
||||
device_kind: None,
|
||||
device_version: None,
|
||||
});
|
||||
self.form_name.value = self
|
||||
.keys
|
||||
.iter()
|
||||
.find_map(|k| {
|
||||
if k.fingerprint == fingerprint {
|
||||
Some(k.name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
self.form_name.valid = true;
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeyModal(msg)) => match msg {
|
||||
message::ImportKeyModal::HWXpubImported(res) => {
|
||||
message::ImportKeyModal::FetchedKey(res) => {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok(key) => {
|
||||
if let Some(alias) = self
|
||||
.keys
|
||||
.iter()
|
||||
.find(|k| k.fingerprint == key.master_fingerprint())
|
||||
.map(|k| k.name.clone())
|
||||
{
|
||||
self.form_name.valid = true;
|
||||
self.form_name.value = alias;
|
||||
self.edit_name = false;
|
||||
} else {
|
||||
self.edit_name = true;
|
||||
self.form_name.value = String::new();
|
||||
}
|
||||
self.form_xpub.valid = check_key_network(&key, self.network);
|
||||
self.form_xpub.value = key.to_string();
|
||||
self.form_name.valid = true;
|
||||
self.form_name.value.clone_from(&key.name);
|
||||
self.chosen_signer = Some(key);
|
||||
}
|
||||
Err(e) => {
|
||||
self.chosen_signer = None;
|
||||
@ -241,11 +258,16 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
}
|
||||
}
|
||||
}
|
||||
message::ImportKeyModal::EditName => {
|
||||
self.edit_name = true;
|
||||
message::ImportKeyModal::ManuallyImportXpub => {
|
||||
self.chosen_signer = None;
|
||||
self.manually_imported_xpub = true;
|
||||
self.form_xpub = form::Value::default();
|
||||
}
|
||||
message::ImportKeyModal::NameEdited(name) => {
|
||||
self.form_name.valid = true;
|
||||
self.form_name.valid = !self.keys.iter().any(|k| {
|
||||
Some(&k.fingerprint) != self.chosen_signer.as_ref().map(|s| &s.fingerprint)
|
||||
&& name == k.name
|
||||
});
|
||||
self.form_name.value = name;
|
||||
}
|
||||
message::ImportKeyModal::XPubEdited(s) => {
|
||||
@ -259,17 +281,18 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
} else {
|
||||
key.xkey.network == Network::Testnet
|
||||
};
|
||||
if let Some(alias) = self
|
||||
.keys
|
||||
.iter()
|
||||
.find(|k| k.fingerprint == fingerprint)
|
||||
.map(|k| k.name.clone())
|
||||
{
|
||||
if self.form_xpub.valid {
|
||||
self.chosen_signer = Some(Key {
|
||||
is_hot_signer: false,
|
||||
fingerprint,
|
||||
name: "".to_string(),
|
||||
key: DescriptorPublicKey::XPub(key),
|
||||
is_compatible_taproot: true,
|
||||
device_kind: None,
|
||||
device_version: None,
|
||||
});
|
||||
self.form_name.value = "".to_string();
|
||||
self.form_name.valid = true;
|
||||
self.form_name.value = alias;
|
||||
self.edit_name = false;
|
||||
} else {
|
||||
self.edit_name = true;
|
||||
}
|
||||
} else {
|
||||
self.form_xpub.valid = false;
|
||||
@ -280,16 +303,10 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
self.form_xpub.value = s;
|
||||
}
|
||||
message::ImportKeyModal::ConfirmXpub => {
|
||||
if let Ok(key) = DescriptorPublicKey::from_str(&self.form_xpub.value) {
|
||||
if let Some(mut key) = self.chosen_signer.clone() {
|
||||
let key_index = self.key_index;
|
||||
let name = self.form_name.value.clone();
|
||||
let (device_kind, device_version) =
|
||||
if let Some((_, kind, version)) = &self.chosen_signer {
|
||||
(*kind, version.clone())
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
if self.other_path_keys.contains(&key.master_fingerprint()) {
|
||||
key.name.clone_from(&self.form_name.value);
|
||||
if self.other_path_keys.contains(&key.fingerprint) {
|
||||
self.duplicate_master_fg = true;
|
||||
} else {
|
||||
let path_index = self.path_index;
|
||||
@ -300,12 +317,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
path_index,
|
||||
message::DefinePath::Key(
|
||||
key_index,
|
||||
message::DefineKey::Edited(
|
||||
name,
|
||||
key,
|
||||
device_kind,
|
||||
device_version,
|
||||
),
|
||||
message::DefineKey::Edited(key),
|
||||
),
|
||||
)
|
||||
},
|
||||
@ -316,10 +328,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
}
|
||||
message::ImportKeyModal::SelectKey(i) => {
|
||||
if let Some(key) = self.keys.get(i) {
|
||||
self.chosen_signer =
|
||||
Some((key.fingerprint, key.device_kind, key.device_version.clone()));
|
||||
self.form_xpub.value = key.key.to_string();
|
||||
self.form_xpub.valid = true;
|
||||
self.chosen_signer = Some(key.clone());
|
||||
self.form_name.value.clone_from(&key.name);
|
||||
self.form_name.valid = true;
|
||||
}
|
||||
@ -335,8 +344,13 @@ 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);
|
||||
let chosen_signer = self.chosen_signer.as_ref().map(|s| s.fingerprint);
|
||||
view::editor::edit_key_modal(
|
||||
if self.path_index > 0 {
|
||||
"Set your key"
|
||||
} else {
|
||||
"Set your primary key"
|
||||
},
|
||||
self.network,
|
||||
hws.list
|
||||
.iter()
|
||||
@ -354,10 +368,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
hw,
|
||||
hw.fingerprint() == chosen_signer,
|
||||
self.processing,
|
||||
!self.processing
|
||||
&& hw.fingerprint() == chosen_signer
|
||||
&& self.form_xpub.valid
|
||||
&& !self.form_xpub.value.is_empty(),
|
||||
hw.fingerprint() == chosen_signer,
|
||||
self.device_must_support_tapminiscript,
|
||||
))
|
||||
}
|
||||
@ -383,7 +394,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
})
|
||||
.collect(),
|
||||
self.error.as_ref(),
|
||||
self.chosen_signer.as_ref().map(|s| s.0),
|
||||
self.chosen_signer.as_ref().map(|s| s.fingerprint),
|
||||
&self.hot_signer_fingerprint,
|
||||
self.keys.iter().find_map(|k| {
|
||||
if k.fingerprint == self.hot_signer_fingerprint {
|
||||
@ -392,9 +403,9 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
None
|
||||
}
|
||||
}),
|
||||
&self.form_xpub,
|
||||
&self.form_name,
|
||||
self.edit_name,
|
||||
&self.form_xpub,
|
||||
self.manually_imported_xpub,
|
||||
self.duplicate_master_fg,
|
||||
)
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ use liana_ui::{
|
||||
widget::Element,
|
||||
};
|
||||
|
||||
use crate::hw;
|
||||
use crate::installer::context::DescriptorTemplate;
|
||||
use crate::{
|
||||
app::settings::KeySetting,
|
||||
hw::HardwareWallets,
|
||||
@ -50,7 +50,8 @@ pub trait DescriptorEditModal {
|
||||
pub struct Path {
|
||||
keys: Vec<Option<Fingerprint>>,
|
||||
threshold: usize,
|
||||
sequence: Option<u16>,
|
||||
// sequence is 0 if it is a primary path.
|
||||
sequence: u16,
|
||||
duplicate_sequence: bool,
|
||||
}
|
||||
|
||||
@ -59,7 +60,7 @@ impl Path {
|
||||
Self {
|
||||
keys: vec![None],
|
||||
threshold: 1,
|
||||
sequence: None,
|
||||
sequence: 0,
|
||||
duplicate_sequence: false,
|
||||
}
|
||||
}
|
||||
@ -68,7 +69,7 @@ impl Path {
|
||||
Self {
|
||||
keys: vec![None],
|
||||
threshold: 1,
|
||||
sequence: Some(u16::MAX),
|
||||
sequence: u16::MAX,
|
||||
duplicate_sequence: false,
|
||||
}
|
||||
}
|
||||
@ -76,57 +77,6 @@ impl Path {
|
||||
fn valid(&self) -> bool {
|
||||
!self.keys.is_empty() && !self.keys.iter().any(|k| k.is_none()) && !self.duplicate_sequence
|
||||
}
|
||||
|
||||
fn view(
|
||||
&self,
|
||||
aliases: &HashMap<Fingerprint, String>,
|
||||
duplicate_name: &HashSet<Fingerprint>,
|
||||
incompatible_with_tapminiscript: &HashSet<Fingerprint>,
|
||||
) -> Element<message::DefinePath> {
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DefineDescriptor {
|
||||
@ -137,10 +87,9 @@ pub struct DefineDescriptor {
|
||||
signer: Arc<Mutex<Signer>>,
|
||||
signer_fingerprint: Fingerprint,
|
||||
|
||||
keys: Vec<Key>,
|
||||
duplicate_name: HashSet<Fingerprint>,
|
||||
incompatible_with_tapminiscript: HashSet<Fingerprint>,
|
||||
keys: HashMap<Fingerprint, Key>,
|
||||
paths: Vec<Path>,
|
||||
descriptor_template: DescriptorTemplate,
|
||||
|
||||
error: Option<String>,
|
||||
}
|
||||
@ -156,95 +105,78 @@ impl DefineDescriptor {
|
||||
|
||||
signer,
|
||||
error: None,
|
||||
keys: Vec::new(),
|
||||
duplicate_name: HashSet::new(),
|
||||
incompatible_with_tapminiscript: HashSet::new(),
|
||||
paths: vec![Path::new_primary_path(), Path::new_recovery_path()],
|
||||
keys: HashMap::new(),
|
||||
descriptor_template: DescriptorTemplate::default(),
|
||||
paths: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
// And every undefined_key that have a same name than an other key.
|
||||
fn check_for_duplicate(&mut self) {
|
||||
self.duplicate_name = HashSet::new();
|
||||
for a in &self.keys {
|
||||
for b in &self.keys {
|
||||
if a.name == b.name && a.fingerprint != b.fingerprint {
|
||||
self.duplicate_name.insert(a.fingerprint);
|
||||
self.duplicate_name.insert(b.fingerprint);
|
||||
fn path_keys<'a>(&'a self, p: &Path) -> Vec<Option<&'a Key>> {
|
||||
p.keys
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if let Some(f) = f {
|
||||
self.keys.get(f)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn check_for_duplicate(&mut self) {
|
||||
let mut all_sequence = HashSet::new();
|
||||
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);
|
||||
}
|
||||
if all_sequence.contains(&path.sequence) {
|
||||
duplicate_sequences.insert(path.sequence);
|
||||
} else {
|
||||
all_sequence.insert(path.sequence);
|
||||
}
|
||||
}
|
||||
|
||||
for path in &mut self.paths {
|
||||
if let Some(sequence) = path.sequence {
|
||||
path.duplicate_sequence = duplicate_sequences.contains(&sequence);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_for_tapminiscript_support(&mut self, must_support_taproot: bool) {
|
||||
self.incompatible_with_tapminiscript = HashSet::new();
|
||||
if must_support_taproot {
|
||||
for key in &self.keys {
|
||||
// check if key is used by a path
|
||||
if !self
|
||||
.paths
|
||||
.iter()
|
||||
.flat_map(|path| &path.keys)
|
||||
.any(|k| *k == Some(key.fingerprint))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// device_kind is none only for HotSigner which is compatible.
|
||||
if let Some(device_kind) = key.device_kind.as_ref() {
|
||||
if !hw::is_compatible_with_tapminiscript(
|
||||
device_kind,
|
||||
key.device_version.as_ref(),
|
||||
) {
|
||||
self.incompatible_with_tapminiscript.insert(key.fingerprint);
|
||||
}
|
||||
}
|
||||
}
|
||||
path.duplicate_sequence = duplicate_sequences.contains(&path.sequence);
|
||||
}
|
||||
}
|
||||
|
||||
fn valid(&self) -> bool {
|
||||
!self.paths.iter().any(|path| !path.valid())
|
||||
&& self.duplicate_name.is_empty()
|
||||
&& self.incompatible_with_tapminiscript.is_empty()
|
||||
&& self.paths.len() >= 2
|
||||
!self.paths.iter().any(|path| {
|
||||
!path.valid()
|
||||
|| (self.use_taproot
|
||||
&& path.keys.iter().any(|k| {
|
||||
if let Some(k) = k.and_then(|k| self.keys.get(&k)) {
|
||||
!k.is_compatible_taproot
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}))
|
||||
}) && self.paths.len() >= 2
|
||||
}
|
||||
|
||||
fn check_setup(&mut self) {
|
||||
self.check_for_duplicate();
|
||||
let use_taproot = self.use_taproot;
|
||||
self.check_for_tapminiscript_support(use_taproot);
|
||||
}
|
||||
|
||||
fn load_template(&mut self, template: DescriptorTemplate) {
|
||||
if self.descriptor_template != template || self.paths.is_empty() {
|
||||
match template {
|
||||
DescriptorTemplate::SimpleInheritance => {
|
||||
self.paths = vec![Path::new_primary_path(), Path::new_recovery_path()];
|
||||
}
|
||||
DescriptorTemplate::Custom => {
|
||||
self.paths = vec![Path::new_primary_path(), Path::new_recovery_path()];
|
||||
}
|
||||
}
|
||||
}
|
||||
self.descriptor_template = template;
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for DefineDescriptor {
|
||||
fn load_context(&mut self, ctx: &Context) {
|
||||
self.load_template(ctx.descriptor_template)
|
||||
}
|
||||
// form value is set as valid each time it is edited.
|
||||
// Verification of the values is happening when the user click on Next button.
|
||||
fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command<Message> {
|
||||
@ -257,29 +189,40 @@ impl Step for DefineDescriptor {
|
||||
self.use_taproot = use_taproot;
|
||||
self.check_setup();
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::ChangeTemplate(template)) => {
|
||||
self.descriptor_template = template;
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::AddRecoveryPath) => {
|
||||
self.paths.push(Path::new_recovery_path());
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::Path(i, msg)) => match msg {
|
||||
message::DefinePath::ThresholdEdited(value) => {
|
||||
if let Some(path) = self.paths.get_mut(i) {
|
||||
path.threshold = value;
|
||||
}
|
||||
}
|
||||
message::DefinePath::SequenceEdited(seq) => {
|
||||
self.modal = None;
|
||||
if let Some(path) = self.paths.get_mut(i) {
|
||||
path.sequence = Some(seq);
|
||||
path.sequence = seq;
|
||||
}
|
||||
self.check_for_duplicate();
|
||||
}
|
||||
message::DefinePath::EditSequence => {
|
||||
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::ThresholdEdited(t) => {
|
||||
self.modal = None;
|
||||
if let Some(path) = self.paths.get_mut(i) {
|
||||
path.threshold = t;
|
||||
}
|
||||
}
|
||||
message::DefinePath::EditSequence => {
|
||||
if let Some(path) = self.paths.get(i) {
|
||||
self.modal = Some(Box::new(EditSequenceModal::new(i, path.sequence)));
|
||||
}
|
||||
}
|
||||
message::DefinePath::EditThreshold => {
|
||||
if let Some(path) = self.paths.get(i) {
|
||||
self.modal = Some(Box::new(EditThresholdModal::new(
|
||||
i,
|
||||
(path.threshold, path.keys.len()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
message::DefinePath::AddKey => {
|
||||
if let Some(path) = self.paths.get_mut(i) {
|
||||
path.keys.push(None);
|
||||
@ -290,30 +233,10 @@ impl Step for DefineDescriptor {
|
||||
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.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.keys.push(Key {
|
||||
fingerprint,
|
||||
is_hot_signer,
|
||||
name,
|
||||
key: imported_key,
|
||||
device_kind: kind,
|
||||
device_version: version,
|
||||
});
|
||||
}
|
||||
|
||||
self.paths[i].keys[j] = Some(fingerprint);
|
||||
|
||||
message::DefineKey::Edited(key) => {
|
||||
hws.set_alias(key.fingerprint, key.name.clone());
|
||||
self.paths[i].keys[j] = Some(key.fingerprint);
|
||||
self.keys.insert(key.fingerprint, key);
|
||||
self.modal = None;
|
||||
self.check_setup();
|
||||
}
|
||||
@ -329,13 +252,13 @@ impl Step for DefineDescriptor {
|
||||
None
|
||||
}
|
||||
})),
|
||||
path.keys[j],
|
||||
path.keys[j].and_then(|f| self.keys.get(&f)).cloned(),
|
||||
i,
|
||||
j,
|
||||
self.network,
|
||||
self.signer.clone(),
|
||||
self.signer_fingerprint,
|
||||
self.keys.clone(),
|
||||
self.keys.values().cloned().collect(),
|
||||
);
|
||||
let cmd = modal.load();
|
||||
self.modal = Some(Box::new(modal));
|
||||
@ -348,11 +271,13 @@ impl Step for DefineDescriptor {
|
||||
path.threshold -= 1;
|
||||
}
|
||||
}
|
||||
if self
|
||||
.paths
|
||||
.get(i)
|
||||
.map(|path| path.keys.is_empty())
|
||||
.unwrap_or(false)
|
||||
// Only delete recovery paths.
|
||||
if i > 0
|
||||
&& self
|
||||
.paths
|
||||
.get(i)
|
||||
.map(|path| path.keys.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.paths.remove(i);
|
||||
}
|
||||
@ -391,8 +316,7 @@ impl Step for DefineDescriptor {
|
||||
let fingerprint = spending_key.expect("Must be present at this step");
|
||||
let key = self
|
||||
.keys
|
||||
.iter()
|
||||
.find(|key| key.key.master_fingerprint() == fingerprint)
|
||||
.get(&fingerprint)
|
||||
.expect("Must be present at this step");
|
||||
if let DescriptorPublicKey::XPub(xpub) = &key.key {
|
||||
if let Some((master_fingerprint, _)) = xpub.origin {
|
||||
@ -421,8 +345,7 @@ impl Step for DefineDescriptor {
|
||||
let fingerprint = recovery_key.expect("Must be present at this step");
|
||||
let key = self
|
||||
.keys
|
||||
.iter()
|
||||
.find(|key| key.key.master_fingerprint() == fingerprint)
|
||||
.get(&fingerprint)
|
||||
.expect("Must be present at this step");
|
||||
if let DescriptorPublicKey::XPub(xpub) = &key.key {
|
||||
if let Some((master_fingerprint, _)) = xpub.origin {
|
||||
@ -450,11 +373,7 @@ impl Step for DefineDescriptor {
|
||||
PathInfo::Multi(path.threshold, recovery_keys)
|
||||
};
|
||||
|
||||
recovery_paths.insert(
|
||||
path.sequence
|
||||
.expect("Must be a recovery path with a sequence"),
|
||||
recovery_keys,
|
||||
);
|
||||
recovery_paths.insert(path.sequence, recovery_keys);
|
||||
}
|
||||
|
||||
if spending_keys.is_empty() {
|
||||
@ -488,30 +407,43 @@ impl Step for DefineDescriptor {
|
||||
&'a self,
|
||||
hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
email: Option<&'a str>,
|
||||
_email: Option<&'a str>,
|
||||
) -> Element<'a, Message> {
|
||||
let aliases = self.keys_aliases();
|
||||
let content = view::editor::define_descriptor(
|
||||
progress,
|
||||
email,
|
||||
self.use_taproot,
|
||||
self.paths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, path)| {
|
||||
path.view(
|
||||
&aliases,
|
||||
&self.duplicate_name,
|
||||
&self.incompatible_with_tapminiscript,
|
||||
)
|
||||
.map(move |msg| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::Path(i, msg))
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
self.valid(),
|
||||
self.error.as_ref(),
|
||||
);
|
||||
let content = match self.descriptor_template {
|
||||
DescriptorTemplate::SimpleInheritance => {
|
||||
view::editor::template::inheritance::inheritance_template(
|
||||
progress,
|
||||
self.use_taproot,
|
||||
self.paths[0].keys[0]
|
||||
.as_ref()
|
||||
.and_then(|f| self.keys.get(f)),
|
||||
self.paths[1].keys[0]
|
||||
.as_ref()
|
||||
.and_then(|f| self.keys.get(f)),
|
||||
self.paths[1].sequence,
|
||||
self.valid(),
|
||||
)
|
||||
}
|
||||
DescriptorTemplate::Custom => view::editor::template::custom::custom_template(
|
||||
progress,
|
||||
self.use_taproot,
|
||||
view::editor::template::custom::Path {
|
||||
keys: self.path_keys(&self.paths[0]),
|
||||
sequence: self.paths[0].sequence,
|
||||
duplicate_sequence: self.paths[0].duplicate_sequence,
|
||||
threshold: self.paths[0].threshold,
|
||||
},
|
||||
&mut self.paths[1..]
|
||||
.iter()
|
||||
.map(|p| view::editor::template::custom::Path {
|
||||
sequence: p.sequence,
|
||||
duplicate_sequence: p.duplicate_sequence,
|
||||
threshold: p.threshold,
|
||||
keys: self.path_keys(p),
|
||||
}),
|
||||
self.valid(),
|
||||
),
|
||||
};
|
||||
if let Some(modal) = &self.modal {
|
||||
Modal::new(content, modal.view(hws))
|
||||
.on_blur(if modal.processing() {
|
||||
@ -555,9 +487,11 @@ impl DescriptorEditModal for EditSequenceModal {
|
||||
}
|
||||
|
||||
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
|
||||
if let Message::DefineDescriptor(message::DefineDescriptor::SequenceModal(msg)) = message {
|
||||
if let Message::DefineDescriptor(message::DefineDescriptor::ThresholdSequenceModal(msg)) =
|
||||
message
|
||||
{
|
||||
match msg {
|
||||
message::SequenceModal::SequenceEdited(seq) => {
|
||||
message::ThresholdSequenceModal::SequenceEdited(seq) => {
|
||||
if let Ok(s) = u16::from_str(&seq) {
|
||||
self.sequence.valid = s != 0
|
||||
} else {
|
||||
@ -565,7 +499,7 @@ impl DescriptorEditModal for EditSequenceModal {
|
||||
}
|
||||
self.sequence.value = seq;
|
||||
}
|
||||
message::SequenceModal::ConfirmSequence => {
|
||||
message::ThresholdSequenceModal::Confirm => {
|
||||
if self.sequence.valid {
|
||||
if let Ok(sequence) = u16::from_str(&self.sequence.value) {
|
||||
let path_index = self.path_index;
|
||||
@ -582,6 +516,7 @@ impl DescriptorEditModal for EditSequenceModal {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
@ -592,6 +527,60 @@ impl DescriptorEditModal for EditSequenceModal {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditThresholdModal {
|
||||
threshold: (usize, usize),
|
||||
path_index: usize,
|
||||
}
|
||||
|
||||
impl EditThresholdModal {
|
||||
pub fn new(path_index: usize, threshold: (usize, usize)) -> Self {
|
||||
Self {
|
||||
threshold,
|
||||
path_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DescriptorEditModal for EditThresholdModal {
|
||||
fn processing(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
|
||||
if let Message::DefineDescriptor(message::DefineDescriptor::ThresholdSequenceModal(msg)) =
|
||||
message
|
||||
{
|
||||
match msg {
|
||||
message::ThresholdSequenceModal::ThresholdEdited(threshold) => {
|
||||
if threshold <= self.threshold.1 {
|
||||
self.threshold.0 = threshold;
|
||||
}
|
||||
}
|
||||
message::ThresholdSequenceModal::Confirm => {
|
||||
let path_index = self.path_index;
|
||||
let threshold = self.threshold.0;
|
||||
return Command::perform(
|
||||
async move { (path_index, threshold) },
|
||||
|(path_index, threshold)| {
|
||||
message::DefineDescriptor::Path(
|
||||
path_index,
|
||||
message::DefinePath::ThresholdEdited(threshold),
|
||||
)
|
||||
},
|
||||
)
|
||||
.map(Message::DefineDescriptor);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn view(&self, _hws: &HardwareWallets) -> Element<Message> {
|
||||
view::editor::edit_threshold_modal(self.threshold)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -638,9 +627,10 @@ mod tests {
|
||||
crate::installer::context::RemoteBackend::None,
|
||||
);
|
||||
let sandbox: Sandbox<DefineDescriptor> = Sandbox::new(DefineDescriptor::new(
|
||||
Network::Bitcoin,
|
||||
Network::Signet,
|
||||
Arc::new(Mutex::new(Signer::generate(Network::Bitcoin).unwrap())),
|
||||
));
|
||||
sandbox.load(&ctx).await;
|
||||
|
||||
// Edit primary key
|
||||
sandbox
|
||||
@ -723,14 +713,18 @@ mod tests {
|
||||
));
|
||||
sandbox.load(&ctx).await;
|
||||
|
||||
let key = DescriptorPublicKey::from_str("[4df3f0e3/84'/0'/0']tpubDDRs9DnRUiJc4hq92PSJKhfzQBgHJUrDo7T2i48smsDfLsQcm3Vh7JhuGqJv8zozVkNFin8YPgpmn2NWNmpRaE3GW2pSxbmAzYf2juy7LeW").unwrap();
|
||||
let specter_key = message::DefinePath::Key(
|
||||
0,
|
||||
message::DefineKey::Edited(
|
||||
"My Specter key".to_string(),
|
||||
DescriptorPublicKey::from_str("[4df3f0e3/84'/0'/0']tpubDDRs9DnRUiJc4hq92PSJKhfzQBgHJUrDo7T2i48smsDfLsQcm3Vh7JhuGqJv8zozVkNFin8YPgpmn2NWNmpRaE3GW2pSxbmAzYf2juy7LeW").unwrap(),
|
||||
Some(async_hwi::DeviceKind::Specter),
|
||||
None,
|
||||
),
|
||||
message::DefineKey::Edited(Key {
|
||||
name: "My Specter key".to_string(),
|
||||
fingerprint: key.master_fingerprint(),
|
||||
key,
|
||||
device_kind: Some(async_hwi::DeviceKind::Specter),
|
||||
device_version: None,
|
||||
is_compatible_taproot: false,
|
||||
is_hot_signer: false,
|
||||
}),
|
||||
);
|
||||
|
||||
// Use Specter device for primary key
|
||||
|
||||
@ -12,18 +12,11 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ChooseDescriptorTemplate {
|
||||
template: DescriptorTemplate,
|
||||
}
|
||||
|
||||
impl Default for ChooseDescriptorTemplate {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
template: DescriptorTemplate::Custom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChooseDescriptorTemplate> for Box<dyn Step> {
|
||||
fn from(s: ChooseDescriptorTemplate) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
@ -54,18 +47,11 @@ impl Step for ChooseDescriptorTemplate {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DescriptorTemplateDescription {
|
||||
template: DescriptorTemplate,
|
||||
}
|
||||
|
||||
impl Default for DescriptorTemplateDescription {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
template: DescriptorTemplate::Custom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DescriptorTemplateDescription> for Box<dyn Step> {
|
||||
fn from(s: DescriptorTemplateDescription) -> Box<dyn Step> {
|
||||
Box::new(s)
|
||||
@ -77,16 +63,19 @@ impl Step for DescriptorTemplateDescription {
|
||||
self.template = ctx.descriptor_template;
|
||||
}
|
||||
|
||||
fn skip(&self, ctx: &Context) -> bool {
|
||||
ctx.descriptor_template == DescriptorTemplate::Custom
|
||||
}
|
||||
|
||||
fn view<'a>(
|
||||
&'a self,
|
||||
_hws: &'a HardwareWallets,
|
||||
progress: (usize, usize),
|
||||
_email: Option<&'a str>,
|
||||
) -> Element<Message> {
|
||||
view::editor::template::inheritance::inheritance_template_description(progress)
|
||||
match self.template {
|
||||
DescriptorTemplate::SimpleInheritance => {
|
||||
view::editor::template::inheritance::inheritance_template_description(progress)
|
||||
}
|
||||
DescriptorTemplate::Custom { .. } => {
|
||||
view::editor::template::custom::custom_template_description(progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod descriptor;
|
||||
|
||||
mod backend;
|
||||
mod descriptor;
|
||||
mod mnemonic;
|
||||
mod node;
|
||||
mod share_xpubs;
|
||||
|
||||
@ -1,32 +1,34 @@
|
||||
pub mod template;
|
||||
|
||||
use iced::widget::{
|
||||
container, pick_list, scrollable, scrollable::Properties, slider, Button, Space,
|
||||
};
|
||||
use iced::{alignment, Alignment, Length};
|
||||
use iced::widget::{container, pick_list, slider, Button, Space};
|
||||
use iced::{Alignment, Length};
|
||||
|
||||
use liana_ui::component::text;
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
use liana_ui::component::text::{self, h3, p1_bold, p2_regular, H3_SIZE};
|
||||
use liana_ui::image;
|
||||
use std::str::FromStr;
|
||||
|
||||
use liana::miniscript::bitcoin::{self, bip32::Fingerprint};
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{
|
||||
button, card, collapse, form, hw, separation,
|
||||
button, card, form, hw, separation,
|
||||
text::{p1_regular, text, Text},
|
||||
tooltip,
|
||||
},
|
||||
icon, image, theme,
|
||||
icon, theme,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::installer::{
|
||||
message::{self, Message},
|
||||
prompt,
|
||||
view::{defined_sequence, layout},
|
||||
view::defined_sequence,
|
||||
Error,
|
||||
};
|
||||
|
||||
use super::defined_threshold;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DescriptorKind {
|
||||
P2WSH,
|
||||
@ -81,300 +83,141 @@ pub fn define_descriptor_advanced_settings<'a>(use_taproot: bool) -> Element<'a,
|
||||
.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",
|
||||
pub fn path(
|
||||
color: iced::Color,
|
||||
title: Option<String>,
|
||||
sequence: u16,
|
||||
duplicate_sequence: bool,
|
||||
threshold: usize,
|
||||
keys: Vec<Element<message::DefinePath>>,
|
||||
fixed: bool,
|
||||
) -> Element<message::DefinePath> {
|
||||
let keys_len = keys.len();
|
||||
Container::new(
|
||||
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),
|
||||
))
|
||||
.spacing(10)
|
||||
.push_maybe(title.map(p1_bold))
|
||||
.push(defined_sequence(sequence, duplicate_sequence))
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center)
|
||||
.push(Column::with_children(keys).spacing(5)),
|
||||
)
|
||||
.push_maybe(if fixed {
|
||||
if keys_len == 1 {
|
||||
None
|
||||
} else {
|
||||
Some(Row::new().push(defined_threshold(color, fixed, (threshold, keys_len))))
|
||||
}
|
||||
} else {
|
||||
Some(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(defined_threshold(color, fixed, (threshold, keys_len)))
|
||||
.push(
|
||||
button::secondary(Some(icon::plus_icon()), "Add key")
|
||||
.on_press(message::DefinePath::AddKey),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.padding(10)
|
||||
.style(theme::Container::Card(theme::Card::Border))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn defined_key<'a>(
|
||||
alias: &'a str,
|
||||
color: iced::Color,
|
||||
title: &'static str,
|
||||
warning: Option<&'static str>,
|
||||
fixed: bool,
|
||||
) -> Element<'a, message::DefineKey> {
|
||||
card::simple(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.push(icon::round_key_icon().size(H3_SIZE).style(color))
|
||||
.push(
|
||||
Column::new()
|
||||
.width(Length::Fill)
|
||||
.spacing(5)
|
||||
.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),
|
||||
.spacing(10)
|
||||
.push(p1_regular(title).style(color::GREY_2))
|
||||
.push(p1_bold(alias)),
|
||||
)
|
||||
.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_maybe(warning.map(|w| p2_regular(w).style(color::RED))),
|
||||
)
|
||||
.push_maybe(if warning.is_none() {
|
||||
Some(icon::check_icon().style(color::GREEN))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.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")
|
||||
button::secondary(Some(icon::pencil_icon()), "Edit")
|
||||
.on_press(message::DefineKey::Edit),
|
||||
)
|
||||
.push(Space::with_height(Length::Fixed(5.0))),
|
||||
.push_maybe(if fixed {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Button::new(icon::trash_icon())
|
||||
.style(theme::Button::Secondary)
|
||||
.padding(5)
|
||||
.on_press(message::DefineKey::Delete),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.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,
|
||||
pub fn undefined_key<'a>(
|
||||
color: iced::Color,
|
||||
title: &'static str,
|
||||
active: bool,
|
||||
fixed: 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(
|
||||
card::simple(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.push(icon::round_key_icon().size(H3_SIZE).style(color))
|
||||
.push(
|
||||
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))),
|
||||
.width(Length::Fill)
|
||||
.spacing(5)
|
||||
.push(p1_bold(title)),
|
||||
)
|
||||
.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()
|
||||
}
|
||||
.push_maybe(if active {
|
||||
Some(
|
||||
button::primary(Some(icon::pencil_icon()), "Set")
|
||||
.on_press(message::DefineKey::Edit),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push_maybe(if fixed {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Button::new(icon::trash_icon())
|
||||
.style(theme::Button::Secondary)
|
||||
.padding(5)
|
||||
.on_press(message::DefineKey::Delete),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn edit_key_modal<'a>(
|
||||
title: &'a str,
|
||||
network: bitcoin::Network,
|
||||
hws: Vec<Element<'a, Message>>,
|
||||
keys: Vec<Element<'a, Message>>,
|
||||
@ -382,22 +225,24 @@ pub fn edit_key_modal<'a>(
|
||||
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,
|
||||
form_xpub: &form::Value<String>,
|
||||
manually_imported_xpub: bool,
|
||||
duplicate_master_fg: bool,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.padding(25)
|
||||
.push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
|
||||
.push(card::simple(
|
||||
.push(card::modal(
|
||||
Column::new()
|
||||
.spacing(25)
|
||||
.push(Row::new()
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(button::transparent(Some(icon::cross_icon()), "").on_press(Message::Close)))
|
||||
.push(
|
||||
Column::new()
|
||||
.push(
|
||||
Container::new(text("Select a signing device:").bold())
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(h3(title))
|
||||
.push(p1_regular("Select the signing device for your key"))
|
||||
.spacing(10)
|
||||
.push(
|
||||
Column::with_children(hws).spacing(10)
|
||||
@ -415,90 +260,79 @@ pub fn edit_key_modal<'a>(
|
||||
.on_press(Message::UseHotSigner)
|
||||
.style(theme::Button::Border),
|
||||
)
|
||||
.push(if manually_imported_xpub {
|
||||
card::simple(Column::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(p1_regular("Enter an extended public key:").width(Length::Fill))
|
||||
.push(image::success_mark_icon().width(Length::Fixed(50.0)))
|
||||
)
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
form::Form::new_trimmed(
|
||||
&example_xpub(network),
|
||||
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)
|
||||
))
|
||||
} else {
|
||||
Container::new(
|
||||
Button::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10)
|
||||
.push(icon::import_icon())
|
||||
.push(p1_regular("Enter an extended public key"))
|
||||
)
|
||||
.padding(20)
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::KeyModal(message::ImportKeyModal::ManuallyImportXpub)
|
||||
))
|
||||
.style(theme::Button::Secondary),
|
||||
)
|
||||
}
|
||||
)
|
||||
.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_maybe(
|
||||
if chosen_signer.is_some() {
|
||||
Some(card::simple(Column::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text("Fingerprint alias:").bold())
|
||||
.push(text("Key name:").bold())
|
||||
.push(tooltip(prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP)),
|
||||
)
|
||||
.push(p1_regular("Give this key a friendly name. It helps you identify it later").style(color::GREY_2))
|
||||
.push(
|
||||
form::Form::new("Alias", form_name, |msg| {
|
||||
form::Form::new("Name", form_name, |msg| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeyModal(
|
||||
message::ImportKeyModal::NameEdited(msg),
|
||||
))
|
||||
})
|
||||
.warning("Please enter correct alias")
|
||||
.warning("Two different keys cannot have the same name")
|
||||
.padding(10)
|
||||
.size(text::P1_SIZE)
|
||||
.padding(10),
|
||||
)
|
||||
)))
|
||||
} else {
|
||||
Column::new()
|
||||
},
|
||||
None
|
||||
}
|
||||
)
|
||||
.push_maybe(
|
||||
if duplicate_master_fg {
|
||||
@ -508,25 +342,30 @@ pub fn edit_key_modal<'a>(
|
||||
}
|
||||
)
|
||||
.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(
|
||||
button::primary(None, "Apply")
|
||||
.on_press_maybe(if !duplicate_master_fg
|
||||
&& (!manually_imported_xpub || form_xpub.valid)
|
||||
&& !form_name.value.is_empty() && form_name.valid {
|
||||
Some(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::KeyModal(
|
||||
message::ImportKeyModal::ConfirmXpub,
|
||||
),
|
||||
))
|
||||
.width(Length::Fixed(200.0))
|
||||
} else {
|
||||
button::primary(None, "Apply").width(Length::Fixed(100.0))
|
||||
},
|
||||
} else {None})
|
||||
.width(Length::Fixed(200.0))
|
||||
)
|
||||
.align_items(Alignment::Center),
|
||||
))
|
||||
.width(Length::Fixed(600.0))
|
||||
.width(Length::Fixed(800.0))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn example_xpub(network: Network) -> String {
|
||||
format!("[aabbccdd/42'/0']{}pub6DAkq8LWw91WGgUGnkR5Sbzjev5JCsXaTVZQ9MwsPV4BkNFKygtJ8GHodfDVx1udR723nT7JASqGPpKvz7zQ25pUTW6zVEBdiWoaC4aUqik",
|
||||
if network == bitcoin::Network::Bitcoin { "x" } else { "t" }
|
||||
)
|
||||
}
|
||||
|
||||
/// 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;
|
||||
@ -547,15 +386,17 @@ pub fn edit_sequence_modal<'a>(sequence: &form::Value<String>) -> Element<'a, Me
|
||||
.width(Length::Fill)
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
.push(text("Activate recovery path after:"))
|
||||
.push(text("Keys can move the funds after inactivity of:"))
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
Container::new(
|
||||
form::Form::new_trimmed("ex: 1000", sequence, |v| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::SequenceModal(
|
||||
message::SequenceModal::SequenceEdited(v),
|
||||
))
|
||||
Message::DefineDescriptor(
|
||||
message::DefineDescriptor::ThresholdSequenceModal(
|
||||
message::ThresholdSequenceModal::SequenceEdited(v),
|
||||
),
|
||||
)
|
||||
})
|
||||
.warning("Sequence must be superior to 0 and inferior to 65535"),
|
||||
)
|
||||
@ -592,9 +433,11 @@ pub fn edit_sequence_modal<'a>(sequence: &form::Value<String>) -> Element<'a, Me
|
||||
.push(
|
||||
Container::new(
|
||||
slider(1..=u16::MAX, sequence, |v| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::SequenceModal(
|
||||
message::SequenceModal::SequenceEdited(v.to_string()),
|
||||
))
|
||||
Message::DefineDescriptor(
|
||||
message::DefineDescriptor::ThresholdSequenceModal(
|
||||
message::ThresholdSequenceModal::SequenceEdited(v.to_string()),
|
||||
),
|
||||
)
|
||||
})
|
||||
.step(144_u16), // 144 blocks per day
|
||||
)
|
||||
@ -603,10 +446,12 @@ pub fn edit_sequence_modal<'a>(sequence: &form::Value<String>) -> Element<'a, Me
|
||||
}
|
||||
}
|
||||
|
||||
card::simple(col.push(if sequence.valid {
|
||||
card::modal(col.push(if sequence.valid {
|
||||
button::primary(None, "Apply")
|
||||
.on_press(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::SequenceModal(message::SequenceModal::ConfirmSequence),
|
||||
message::DefineDescriptor::ThresholdSequenceModal(
|
||||
message::ThresholdSequenceModal::Confirm,
|
||||
),
|
||||
))
|
||||
.width(Length::Fixed(200.0))
|
||||
} else {
|
||||
@ -616,6 +461,35 @@ pub fn edit_sequence_modal<'a>(sequence: &form::Value<String>) -> Element<'a, Me
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn edit_threshold_modal<'a>(threshold: (usize, usize)) -> Element<'a, Message> {
|
||||
card::modal(
|
||||
Column::new()
|
||||
.width(Length::Fill)
|
||||
.spacing(20)
|
||||
.align_items(Alignment::Center)
|
||||
.push(threshsold_input::threshsold_input(
|
||||
threshold.0,
|
||||
threshold.1,
|
||||
|v| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::ThresholdSequenceModal(
|
||||
message::ThresholdSequenceModal::ThresholdEdited(v),
|
||||
))
|
||||
},
|
||||
))
|
||||
.push(
|
||||
button::primary(None, "Apply")
|
||||
.on_press(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::ThresholdSequenceModal(
|
||||
message::ThresholdSequenceModal::Confirm,
|
||||
),
|
||||
))
|
||||
.width(Length::Fixed(200.0)),
|
||||
),
|
||||
)
|
||||
.width(Length::Fixed(800.0))
|
||||
.into()
|
||||
}
|
||||
|
||||
mod threshsold_input {
|
||||
use iced::alignment::{self, Alignment};
|
||||
use iced::widget::{component, Component};
|
||||
|
||||
206
gui/src/installer/view/editor/template/custom.rs
Normal file
206
gui/src/installer/view/editor/template/custom.rs
Normal file
@ -0,0 +1,206 @@
|
||||
use iced::{alignment, widget::Space, Alignment, Length};
|
||||
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{
|
||||
button, collapse,
|
||||
text::{h3, p1_regular, text, Text},
|
||||
},
|
||||
icon, image, theme,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::installer::{
|
||||
message::{self, Message},
|
||||
prompt,
|
||||
step::descriptor::editor::key::Key,
|
||||
view::{
|
||||
editor::{define_descriptor_advanced_settings, defined_key, path, undefined_key},
|
||||
layout,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn custom_template_description(progress: (usize, usize)) -> Element<'static, Message> {
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
"Introduction",
|
||||
Column::new()
|
||||
.align_items(Alignment::Start)
|
||||
.push(h3("Custom wallet"))
|
||||
.max_width(800.0)
|
||||
.push(Container::new(
|
||||
p1_regular("Through this setup you can choose how many keys you want to use. For security reasons, we suggest you use Hardware Wallets to store them.")
|
||||
.style(color::GREY_2)
|
||||
.horizontal_alignment(alignment::Horizontal::Left)
|
||||
).align_x(alignment::Horizontal::Left).width(Length::Fill))
|
||||
.push(Container::new(
|
||||
p1_regular("For this Custom wallet you will need to define your Primary and Recovery Sets of Keys.")
|
||||
.style(color::GREY_2)
|
||||
.horizontal_alignment(alignment::Horizontal::Left)
|
||||
).align_x(alignment::Horizontal::Left).width(Length::Fill))
|
||||
.push(image::custom_template_description().width(Length::Fill))
|
||||
.push(Container::new(
|
||||
p1_regular("The Primary set of Keys will always be able to spend. Your Recovery set(s) of Keys will activate only after a defined time of wallet inactivity, allowing for secure recovery and advanced spending policies. You can define more than one set of Recovery Keys activating at different times.")
|
||||
.style(color::GREY_2)
|
||||
.horizontal_alignment(alignment::Horizontal::Left)
|
||||
).align_x(alignment::Horizontal::Left).width(Length::Fill))
|
||||
.push(Row::new().push(Space::with_width(Length::Fill)).push(button::primary(None, "Select").width(Length::Fixed(200.0)).on_press(Message::Next)))
|
||||
.spacing(20),
|
||||
true,
|
||||
Some(Message::Previous),
|
||||
)
|
||||
}
|
||||
|
||||
pub struct Path<'a> {
|
||||
pub keys: Vec<Option<&'a Key>>,
|
||||
pub sequence: u16,
|
||||
pub duplicate_sequence: bool,
|
||||
pub threshold: usize,
|
||||
}
|
||||
|
||||
pub fn custom_template<'a>(
|
||||
progress: (usize, usize),
|
||||
use_taproot: bool,
|
||||
primary_path: Path<'a>,
|
||||
recovery_paths: &mut dyn Iterator<Item = Path<'a>>,
|
||||
valid: bool,
|
||||
) -> Element<'a, Message> {
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
"Set keys",
|
||||
Column::new()
|
||||
.align_items(Alignment::Start)
|
||||
.max_width(1000.0)
|
||||
.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(p1_regular(prompt::DEFINE_DESCRIPTOR_PRIMARY_PATH_TOOLTIP).style(color::GREY_2))
|
||||
.push(
|
||||
path(
|
||||
color::GREEN,
|
||||
Some("Primary spending option:".to_string()),
|
||||
primary_path.sequence,
|
||||
primary_path.duplicate_sequence,
|
||||
primary_path.threshold,
|
||||
primary_path
|
||||
.keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, primary_key)| {
|
||||
if let Some(key) = primary_key {
|
||||
defined_key(
|
||||
&key.name,
|
||||
color::GREEN,
|
||||
"Primary key",
|
||||
if use_taproot && !key.is_compatible_taproot {
|
||||
Some("Key is not compatible with taproot")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
i == 0,
|
||||
)
|
||||
} else {
|
||||
undefined_key(
|
||||
color::GREEN,
|
||||
"Primary key",
|
||||
!primary_path.keys[0..i].iter().any(|k| k.is_none()),
|
||||
i == 0,
|
||||
)
|
||||
}
|
||||
.map(move |msg| message::DefinePath::Key(i, msg))
|
||||
})
|
||||
.collect(),
|
||||
false,
|
||||
)
|
||||
.map(|msg| Message::DefineDescriptor(message::DefineDescriptor::Path(0, msg))),
|
||||
)
|
||||
.push(p1_regular(prompt::DEFINE_DESCRIPTOR_RECOVERY_PATH_TOOLTIP).style(color::GREY_2))
|
||||
.push(recovery_paths.into_iter().enumerate().fold(
|
||||
Column::new().spacing(20),
|
||||
|col, (i, p)| {
|
||||
col.push(
|
||||
path(
|
||||
color::ORANGE,
|
||||
Some(format!("Recovery option #{}:", i + 1)),
|
||||
p.sequence,
|
||||
p.duplicate_sequence,
|
||||
p.threshold,
|
||||
p.keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(j, recovery_key)| {
|
||||
if let Some(key) = recovery_key {
|
||||
defined_key(
|
||||
&key.name,
|
||||
color::ORANGE,
|
||||
"Recovery key",
|
||||
if use_taproot && !key.is_compatible_taproot {
|
||||
Some("Key is not compatible with Taproot")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
undefined_key(
|
||||
color::ORANGE,
|
||||
"Recovery key",
|
||||
!p.keys[0..j].iter().any(|k| k.is_none()),
|
||||
false,
|
||||
)
|
||||
}
|
||||
.map(move |msg| message::DefinePath::Key(j, msg))
|
||||
})
|
||||
.collect(),
|
||||
false,
|
||||
)
|
||||
.map(move |msg| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::Path(i + 1, msg))
|
||||
}),
|
||||
)
|
||||
},
|
||||
))
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
button::secondary(Some(icon::plus_icon()), "Add recovery option")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::AddRecoveryPath,
|
||||
)),
|
||||
)
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(
|
||||
button::primary(None, "Continue")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press_maybe(if valid { Some(Message::Next) } else { None }),
|
||||
),
|
||||
)
|
||||
.push(Space::with_height(100.0))
|
||||
.spacing(20),
|
||||
true,
|
||||
Some(Message::Previous),
|
||||
)
|
||||
}
|
||||
@ -3,14 +3,22 @@ use iced::{alignment, widget::Space, Alignment, Length};
|
||||
use liana_ui::{
|
||||
color,
|
||||
component::{
|
||||
button,
|
||||
text::{h3, p1_regular},
|
||||
button, collapse,
|
||||
text::{h3, p1_regular, text, Text},
|
||||
},
|
||||
image,
|
||||
icon, image, theme,
|
||||
widget::*,
|
||||
};
|
||||
|
||||
use crate::installer::{message::Message, view::layout};
|
||||
use crate::installer::{
|
||||
context,
|
||||
message::{self, Message},
|
||||
step::descriptor::editor::key::Key,
|
||||
view::{
|
||||
editor::{define_descriptor_advanced_settings, defined_key, path, undefined_key},
|
||||
layout,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn inheritance_template_description(progress: (usize, usize)) -> Element<'static, Message> {
|
||||
layout(
|
||||
@ -22,19 +30,14 @@ pub fn inheritance_template_description(progress: (usize, usize)) -> Element<'st
|
||||
.push(h3("Inheritance wallet"))
|
||||
.max_width(800.0)
|
||||
.push(Container::new(
|
||||
p1_regular("In this current setup you will need 2 Keys for your wallet. For security reasons, we suggest you to use 2 Hardware Wallets to store them.")
|
||||
.style(color::GREY_3)
|
||||
.horizontal_alignment(alignment::Horizontal::Left)
|
||||
).align_x(alignment::Horizontal::Left).width(Length::Fill))
|
||||
.push(Container::new(
|
||||
p1_regular("For this Inheritance wallet you will need 2 Keys: Your Primary Key and an Inheritance Key to be given to a chosen relative.")
|
||||
.style(color::GREY_3)
|
||||
p1_regular("For this Inheritance wallet you will need 2 Keys: Your Primary Key and an Inheritance Key to be given to a chosen heir. For security reasons, we suggest you use 2 Hardware Wallets to store them.")
|
||||
.style(color::GREY_2)
|
||||
.horizontal_alignment(alignment::Horizontal::Left)
|
||||
).align_x(alignment::Horizontal::Left).width(Length::Fill))
|
||||
.push(image::inheritance_template_description().width(Length::Fill))
|
||||
.push(Container::new(
|
||||
p1_regular("Your relative’s Inheritance Key will become active only if you don’t move the coins in your wallet for the defined period of time, enabling him/her to recover your funds while not being able to access them before that.")
|
||||
.style(color::GREY_3)
|
||||
.style(color::GREY_2)
|
||||
.horizontal_alignment(alignment::Horizontal::Left)
|
||||
).align_x(alignment::Horizontal::Left).width(Length::Fill))
|
||||
.push(Row::new().push(Space::with_width(Length::Fill)).push(button::primary(None, "Select").width(Length::Fixed(200.0)).on_press(Message::Next)))
|
||||
@ -43,3 +46,119 @@ pub fn inheritance_template_description(progress: (usize, usize)) -> Element<'st
|
||||
Some(Message::Previous),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn inheritance_template<'a>(
|
||||
progress: (usize, usize),
|
||||
use_taproot: bool,
|
||||
primary_key: Option<&'a Key>,
|
||||
recovery_key: Option<&'a Key>,
|
||||
sequence: u16,
|
||||
valid: bool,
|
||||
) -> Element<'a, Message> {
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
"Set keys",
|
||||
Column::new()
|
||||
.align_items(Alignment::Start)
|
||||
.max_width(1000.0)
|
||||
.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(
|
||||
path(
|
||||
color::GREEN,
|
||||
None,
|
||||
0,
|
||||
false,
|
||||
1,
|
||||
vec![if let Some(key) = primary_key {
|
||||
defined_key(
|
||||
&key.name,
|
||||
color::GREEN,
|
||||
"Primary key",
|
||||
if use_taproot && !key.is_compatible_taproot {
|
||||
Some("Key is not compatible with Taproot")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
undefined_key(color::GREEN, "Primary key", true, true)
|
||||
}
|
||||
.map(|msg| message::DefinePath::Key(0, msg))],
|
||||
true,
|
||||
)
|
||||
.map(|msg| Message::DefineDescriptor(message::DefineDescriptor::Path(0, msg))),
|
||||
)
|
||||
.push(
|
||||
path(
|
||||
color::WHITE,
|
||||
None,
|
||||
sequence,
|
||||
false,
|
||||
1,
|
||||
vec![if let Some(key) = recovery_key {
|
||||
defined_key(
|
||||
&key.name,
|
||||
color::WHITE,
|
||||
"Inheritance key",
|
||||
if use_taproot && !key.is_compatible_taproot {
|
||||
Some("Key is not compatible with taproot")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
undefined_key(color::WHITE, "Inheritance key", primary_key.is_some(), true)
|
||||
}
|
||||
.map(|msg| message::DefinePath::Key(0, msg))],
|
||||
true,
|
||||
)
|
||||
.map(|msg| Message::DefineDescriptor(message::DefineDescriptor::Path(1, msg))),
|
||||
)
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
button::secondary(None, "Customize")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press(Message::DefineDescriptor(
|
||||
message::DefineDescriptor::ChangeTemplate(
|
||||
context::DescriptorTemplate::Custom,
|
||||
),
|
||||
)),
|
||||
)
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(
|
||||
button::primary(None, "Continue")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press_maybe(if valid { Some(Message::Next) } else { None }),
|
||||
),
|
||||
)
|
||||
.spacing(20),
|
||||
true,
|
||||
Some(Message::Previous),
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
pub mod custom;
|
||||
pub mod inheritance;
|
||||
|
||||
use iced::{alignment, Alignment, Length};
|
||||
@ -18,13 +19,13 @@ pub fn choose_descriptor_template(progress: (usize, usize)) -> Element<'static,
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
"Choose Wallet Type",
|
||||
"Choose wallet type",
|
||||
Column::new()
|
||||
.max_width(800.0)
|
||||
.align_items(Alignment::Start)
|
||||
.push(Container::new(
|
||||
p1_regular("What do you want your wallet for? Also consider this depends on the amount of funds you have, the more funds, higher the security should be. Not sure about the wallet type? We can help you.")
|
||||
.style(color::GREY_3)
|
||||
p1_regular("What do you want your wallet for? This depends on the amount of funds you have, the more funds, the higher the security should be. Not sure about the wallet type? We can help you.")
|
||||
.style(color::GREY_2)
|
||||
.horizontal_alignment(alignment::Horizontal::Left)
|
||||
).align_x(alignment::Horizontal::Left).width(Length::Fill))
|
||||
.push(
|
||||
@ -35,7 +36,7 @@ pub fn choose_descriptor_template(progress: (usize, usize)) -> Element<'static,
|
||||
Column::new()
|
||||
.align_items(Alignment::Start)
|
||||
.push(h3("Simple inheritance"))
|
||||
.push(p2_regular("Two keys required, one for yourself to spend and another for your heir.").style(color::GREY_3))
|
||||
.push(p2_regular("Two keys required, one for yourself to spend and another for your heir.").style(color::GREY_2))
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.push(button::secondary(None, "Select").on_press(
|
||||
@ -54,12 +55,12 @@ pub fn choose_descriptor_template(progress: (usize, usize)) -> Element<'static,
|
||||
Column::new()
|
||||
.align_items(Alignment::Start)
|
||||
.push(h3("Custom (choose your own)"))
|
||||
.push(p2_regular("Create a custom set up that fits all your need").style(color::GREY_3))
|
||||
.push(p2_regular("Create a custom setup that fits all your needs").style(color::GREY_2))
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.push(button::secondary(None, "Select").on_press(
|
||||
Message::SelectDescriptorTemplate(
|
||||
context::DescriptorTemplate::Custom,
|
||||
context::DescriptorTemplate::Custom ,
|
||||
),
|
||||
)),
|
||||
)
|
||||
|
||||
@ -1268,10 +1268,6 @@ pub fn start_internal_bitcoind<'a>(
|
||||
install_state: Option<&InstallState>,
|
||||
) -> Element<'a, Message> {
|
||||
let version = crate::node::bitcoind::VERSION;
|
||||
let mut next_button = button::secondary(None, "Next").width(Length::Fixed(200.0));
|
||||
if let Some(Ok(_)) = started {
|
||||
next_button = next_button.on_press(Message::Next);
|
||||
};
|
||||
layout(
|
||||
progress,
|
||||
None,
|
||||
@ -1364,11 +1360,7 @@ pub fn start_internal_bitcoind<'a>(
|
||||
}
|
||||
})
|
||||
.spacing(50)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(Row::new().spacing(10).push(next_button)),
|
||||
)
|
||||
.push(Row::new())
|
||||
.push_maybe(error.map(|e| card::invalid(text(e)))),
|
||||
true,
|
||||
Some(message::Message::InternalBitcoind(
|
||||
@ -1415,6 +1407,57 @@ pub fn install<'a>(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn defined_threshold<'a>(
|
||||
color: iced::Color,
|
||||
fixed: bool,
|
||||
threshold: (usize, usize),
|
||||
) -> Element<'a, message::DefinePath> {
|
||||
if !fixed && threshold.1 > 1 {
|
||||
Button::new(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push((0..threshold.1).fold(Row::new(), |row, i| {
|
||||
if i < threshold.0 {
|
||||
row.push(icon::round_key_icon().style(color))
|
||||
} else {
|
||||
row.push(icon::round_key_icon())
|
||||
}
|
||||
}))
|
||||
.push(text(format!(
|
||||
"{} out of {} key{}",
|
||||
threshold.0,
|
||||
threshold.1,
|
||||
if threshold.0 > 1 { "s" } else { "" },
|
||||
)))
|
||||
.push(icon::pencil_icon()),
|
||||
)
|
||||
.padding(10)
|
||||
.on_press(message::DefinePath::EditThreshold)
|
||||
.style(theme::Button::Secondary)
|
||||
.into()
|
||||
} else {
|
||||
card::simple(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push((0..threshold.1).fold(Row::new(), |row, i| {
|
||||
if i < threshold.0 {
|
||||
row.push(icon::round_key_icon().style(color))
|
||||
} else {
|
||||
row.push(icon::round_key_icon())
|
||||
}
|
||||
}))
|
||||
.push(text(format!(
|
||||
"{} out of {} key{}",
|
||||
threshold.0,
|
||||
threshold.1,
|
||||
if threshold.0 > 1 { "s" } else { "" },
|
||||
))),
|
||||
)
|
||||
.padding(10)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn defined_sequence<'a>(
|
||||
sequence: u16,
|
||||
duplicate_sequence: bool,
|
||||
@ -1423,53 +1466,66 @@ pub fn defined_sequence<'a>(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(if sequence != 0 {
|
||||
Row::new().align_items(Alignment::Center).push(
|
||||
Container::new(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(5)
|
||||
.push(
|
||||
text::p1_regular("Available after inactivity of ~")
|
||||
.style(color::GREY_2),
|
||||
)
|
||||
.push(
|
||||
Button::new(
|
||||
Row::new()
|
||||
.padding(5)
|
||||
.spacing(5)
|
||||
.align_items(Alignment::Center)
|
||||
.push(text(
|
||||
[
|
||||
(n_years, "y"),
|
||||
(n_months, "m"),
|
||||
(n_days, "d"),
|
||||
(n_hours, "h"),
|
||||
(n_minutes, "mn"),
|
||||
]
|
||||
.iter()
|
||||
.filter_map(|(n, unit)| {
|
||||
if *n > 0 {
|
||||
Some(format!("{}{}", n, unit))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
))
|
||||
.push(icon::pencil_icon()),
|
||||
)
|
||||
.style(theme::Button::Secondary)
|
||||
.on_press(message::DefinePath::EditSequence),
|
||||
),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(5)
|
||||
.align_y(alignment::Vertical::Center),
|
||||
)
|
||||
} else {
|
||||
Row::new()
|
||||
.push(p1_regular("Able to move the funds at any time.").style(color::GREY_2))
|
||||
.padding(5)
|
||||
})
|
||||
.push_maybe(if duplicate_sequence {
|
||||
Some(
|
||||
text("No two recovery paths may become available at the very same date.")
|
||||
text("No two recovery options may become available at the very same date.")
|
||||
.small()
|
||||
.style(color::RED),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.push(
|
||||
Row::new()
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(text(format!("Available after {} blocks", sequence)).bold())
|
||||
.push(
|
||||
[
|
||||
(n_years, "y"),
|
||||
(n_months, "m"),
|
||||
(n_days, "d"),
|
||||
(n_hours, "h"),
|
||||
(n_minutes, "mn"),
|
||||
]
|
||||
.iter()
|
||||
.fold(
|
||||
Row::new().spacing(5),
|
||||
|row, (n, unit)| {
|
||||
row.push_maybe(if *n > 0 {
|
||||
Some(text(format!("{}{}", n, unit,)))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.padding(5)
|
||||
.align_y(alignment::Vertical::Center),
|
||||
)
|
||||
.push(
|
||||
button::secondary(Some(icon::pencil_icon()), "Edit")
|
||||
.on_press(message::DefinePath::EditSequence),
|
||||
)
|
||||
.spacing(15),
|
||||
),
|
||||
.spacing(15),
|
||||
)
|
||||
.padding(5)
|
||||
.into()
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
use crate::{color, component::text::text, icon, theme, widget::*};
|
||||
|
||||
pub fn modal<'a, T: 'a, C: Into<Element<'a, T>>>(content: C) -> Container<'a, T> {
|
||||
Container::new(content)
|
||||
.padding(15)
|
||||
.style(theme::Container::Card(theme::Card::Modal))
|
||||
}
|
||||
|
||||
pub fn simple<'a, T: 'a, C: Into<Element<'a, T>>>(content: C) -> Container<'a, T> {
|
||||
Container::new(content)
|
||||
.padding(15)
|
||||
|
||||
@ -115,6 +115,14 @@ pub fn previous_icon() -> Text<'static> {
|
||||
bootstrap_icon('\u{F284}')
|
||||
}
|
||||
|
||||
pub fn check_icon() -> Text<'static> {
|
||||
bootstrap_icon('\u{F633}')
|
||||
}
|
||||
|
||||
pub fn round_key_icon() -> Text<'static> {
|
||||
bootstrap_icon('\u{F44E}')
|
||||
}
|
||||
|
||||
const ICONEX_ICONS: Font = Font::with_name("Untitled1");
|
||||
|
||||
fn iconex_icon(unicode: char) -> Text<'static> {
|
||||
|
||||
@ -67,3 +67,11 @@ pub fn inheritance_template_description() -> Svg {
|
||||
let h = Handle::from_memory(INHERITANCE_TEMPLATE_DESC.to_vec());
|
||||
Svg::new(h)
|
||||
}
|
||||
|
||||
const CUSTOM_TEMPLATE_DESC: &[u8] =
|
||||
include_bytes!("../static/images/custom_template_description.svg");
|
||||
|
||||
pub fn custom_template_description() -> Svg {
|
||||
let h = Handle::from_memory(CUSTOM_TEMPLATE_DESC.to_vec());
|
||||
Svg::new(h)
|
||||
}
|
||||
|
||||
@ -288,6 +288,7 @@ impl Notification {
|
||||
pub enum Card {
|
||||
#[default]
|
||||
Simple,
|
||||
Modal,
|
||||
Border,
|
||||
Invalid,
|
||||
Warning,
|
||||
@ -302,10 +303,14 @@ impl Card {
|
||||
background: Some(color::GREY_2.into()),
|
||||
..container::Appearance::default()
|
||||
},
|
||||
Card::Modal => container::Appearance {
|
||||
background: Some(color::GREY_2.into()),
|
||||
..container::Appearance::default()
|
||||
},
|
||||
Card::Border => container::Appearance {
|
||||
background: Some(iced::Color::TRANSPARENT.into()),
|
||||
border: iced::Border {
|
||||
color: color::GREY_2,
|
||||
color: color::GREY_7,
|
||||
width: 1.0,
|
||||
radius: 10.0.into(),
|
||||
},
|
||||
@ -347,10 +352,19 @@ impl Card {
|
||||
},
|
||||
..container::Appearance::default()
|
||||
},
|
||||
Card::Modal => container::Appearance {
|
||||
background: Some(color::LIGHT_BLACK.into()),
|
||||
border: iced::Border {
|
||||
color: color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 25.0.into(),
|
||||
},
|
||||
..container::Appearance::default()
|
||||
},
|
||||
Card::Border => container::Appearance {
|
||||
background: Some(iced::Color::TRANSPARENT.into()),
|
||||
border: iced::Border {
|
||||
color: color::GREY_5,
|
||||
color: color::GREY_7,
|
||||
width: 1.0,
|
||||
radius: 25.0.into(),
|
||||
},
|
||||
@ -790,8 +804,11 @@ impl button::StyleSheet for Theme {
|
||||
}
|
||||
}
|
||||
fn disabled(&self, style: &Self::Style) -> button::Appearance {
|
||||
let active = self.active(style);
|
||||
|
||||
let active = if let Button::Primary = style {
|
||||
self.active(&Button::Secondary)
|
||||
} else {
|
||||
self.active(style)
|
||||
};
|
||||
button::Appearance {
|
||||
shadow_offset: iced::Vector::default(),
|
||||
background: Some(color::TRANSPARENT.into()),
|
||||
|
||||
28
gui/ui/static/images/custom_template_description.svg
Normal file
28
gui/ui/static/images/custom_template_description.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 125 KiB |
Loading…
x
Reference in New Issue
Block a user