installer: refac setup as a list of paths
This commit is contained in:
parent
5fabd987e8
commit
d0ec811bef
@ -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),
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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));
|
||||
|
||||
710
gui/src/installer/view/editor.rs
Normal file
710
gui/src/installer/view/editor.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user