installer: let user choose account (derivation path) before fetching an xpub from signing device
This commit is contained in:
parent
8efc55bcf5
commit
ce8e1f3c3e
@ -1,5 +1,8 @@
|
||||
use async_hwi::{DeviceKind, Version};
|
||||
use liana::miniscript::{bitcoin::bip32::Fingerprint, descriptor::DescriptorPublicKey};
|
||||
use liana::miniscript::{
|
||||
bitcoin::bip32::{ChildNumber, Fingerprint},
|
||||
descriptor::DescriptorPublicKey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::settings::ProviderKey, hw::is_compatible_with_tapminiscript, services::keys::api::KeyKind,
|
||||
@ -107,6 +110,7 @@ pub struct Key {
|
||||
pub name: String,
|
||||
pub fingerprint: Fingerprint,
|
||||
pub key: DescriptorPublicKey,
|
||||
pub account: Option<ChildNumber>,
|
||||
}
|
||||
|
||||
pub struct Path {
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
use liana::miniscript::{
|
||||
bitcoin::{bip32::Fingerprint, Network},
|
||||
bitcoin::{
|
||||
bip32::{ChildNumber, Fingerprint},
|
||||
Network,
|
||||
},
|
||||
DescriptorPublicKey,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
@ -67,6 +70,7 @@ pub enum Message {
|
||||
ImportBackup,
|
||||
WalletFromBackup((HashMap<Fingerprint, settings::KeySetting>, Backup)),
|
||||
WalletAliasEdited(String),
|
||||
SelectAccount(Fingerprint, ChildNumber),
|
||||
}
|
||||
|
||||
impl Close for Message {
|
||||
@ -81,6 +85,12 @@ impl From<ImportExportMessage> for Message {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Fingerprint, ChildNumber)> for Message {
|
||||
fn from(value: (Fingerprint, ChildNumber)) -> Self {
|
||||
Self::SelectAccount(value.0, value.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SelectBackend {
|
||||
// view messages
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use iced::{Subscription, Task};
|
||||
use liana::miniscript::bitcoin::bip32::Xpub;
|
||||
use liana::miniscript::bitcoin::bip32::{ChildNumber, Xpub};
|
||||
use liana::miniscript::{
|
||||
bitcoin::{
|
||||
bip32::{DerivationPath, Fingerprint},
|
||||
@ -89,6 +89,7 @@ pub struct EditXpubModal {
|
||||
hot_signer_fingerprint: Fingerprint,
|
||||
chosen_signer: Option<Key>,
|
||||
modal: Option<ExportModal>,
|
||||
accounts: HashMap<Fingerprint, ChildNumber>,
|
||||
}
|
||||
|
||||
impl EditXpubModal {
|
||||
@ -104,6 +105,10 @@ impl EditXpubModal {
|
||||
hot_signer_fingerprint: Fingerprint,
|
||||
keys: Vec<Key>,
|
||||
) -> Self {
|
||||
let accounts = keys
|
||||
.iter()
|
||||
.filter_map(|k| k.account.map(|acc| (k.fingerprint, acc)))
|
||||
.collect();
|
||||
Self {
|
||||
device_must_support_tapminiscript,
|
||||
path_kind,
|
||||
@ -139,6 +144,7 @@ impl EditXpubModal {
|
||||
hot_signer,
|
||||
duplicate_master_fg: false,
|
||||
modal: None,
|
||||
accounts,
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,6 +164,10 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
self.duplicate_master_fg = false;
|
||||
self.error = None;
|
||||
match message {
|
||||
Message::SelectAccount(fg, index) => {
|
||||
self.accounts.insert(fg, index);
|
||||
return Task::none();
|
||||
}
|
||||
Message::Select(i) => {
|
||||
if let Some(HardwareWallet::Supported {
|
||||
device,
|
||||
@ -170,6 +180,11 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
self.processing = true;
|
||||
self.form_key_source_kind = None;
|
||||
let device_version = version.clone();
|
||||
let account = self
|
||||
.accounts
|
||||
.get(fingerprint)
|
||||
.copied()
|
||||
.unwrap_or(ChildNumber::from_hardened_idx(0).expect("hardcoded"));
|
||||
let fingerprint = *fingerprint;
|
||||
let device_kind = *kind;
|
||||
let device_cloned = device.clone();
|
||||
@ -181,10 +196,11 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
device_kind,
|
||||
fingerprint,
|
||||
network,
|
||||
get_extended_pubkey(device_cloned, fingerprint, network).await,
|
||||
get_extended_pubkey(device_cloned, fingerprint, network, account)
|
||||
.await,
|
||||
)
|
||||
},
|
||||
|(device_version, device_kind, fingerprint, network, res)| {
|
||||
move |(device_version, device_kind, fingerprint, network, res)| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::KeyModal(
|
||||
message::ImportKeyModal::FetchedKey(match res {
|
||||
Err(e) => Err(e),
|
||||
@ -198,6 +214,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
fingerprint,
|
||||
name: "".to_string(),
|
||||
key,
|
||||
account: Some(account),
|
||||
})
|
||||
} else {
|
||||
Err(Error::Unexpected(
|
||||
@ -233,6 +250,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
fingerprint,
|
||||
name: "".to_string(),
|
||||
key: DescriptorPublicKey::from_str(&key_str).unwrap(),
|
||||
account: None,
|
||||
});
|
||||
self.form_name.value = self
|
||||
.keys
|
||||
@ -273,7 +291,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
message::ImportKeyModal::FetchedKey(res) => {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok(key) => {
|
||||
Ok(mut key) => {
|
||||
// If it is a provider key that has just been fetched, do some additional sanity checks.
|
||||
if let Some(key_kind) = key.source.provider_key_kind() {
|
||||
// We don't need to check key's status as redeemed keys are not returned.
|
||||
@ -302,6 +320,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
};
|
||||
self.form_token.valid = self.form_token_warning.is_none();
|
||||
}
|
||||
key.account = self.accounts.get(&key.fingerprint).copied();
|
||||
// User can set name for key if it is not a provider key or is a valid provider key.
|
||||
if key.source.provider_key().is_none() || self.form_token.valid {
|
||||
self.form_name.valid = key.name.is_empty()
|
||||
@ -374,6 +393,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
fingerprint,
|
||||
name: "".to_string(),
|
||||
key: DescriptorPublicKey::XPub(key),
|
||||
account: None,
|
||||
});
|
||||
self.form_name.value = "".to_string();
|
||||
self.form_name.valid = true;
|
||||
@ -429,6 +449,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
key.kind
|
||||
),
|
||||
key: key.xpub.clone(),
|
||||
account: None,
|
||||
}),
|
||||
}),
|
||||
))
|
||||
@ -503,6 +524,8 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
hw.fingerprint() == chosen_signer,
|
||||
None,
|
||||
self.device_must_support_tapminiscript,
|
||||
Some(&self.accounts),
|
||||
true,
|
||||
))
|
||||
}
|
||||
})
|
||||
@ -528,6 +551,7 @@ impl super::DescriptorEditModal for EditXpubModal {
|
||||
key.source.device_version(),
|
||||
Some(key.fingerprint) == chosen_signer,
|
||||
self.device_must_support_tapminiscript,
|
||||
&self.accounts,
|
||||
))
|
||||
}
|
||||
})
|
||||
@ -576,14 +600,31 @@ pub fn default_derivation_path(network: Network) -> DerivationPath {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn derivation_path(network: Network, account: ChildNumber) -> DerivationPath {
|
||||
assert!(account.is_hardened());
|
||||
let network = if network == Network::Bitcoin {
|
||||
ChildNumber::Hardened { index: 0 }
|
||||
} else {
|
||||
ChildNumber::Hardened { index: 1 }
|
||||
};
|
||||
vec![
|
||||
ChildNumber::Hardened { index: 48 },
|
||||
network,
|
||||
account,
|
||||
ChildNumber::Hardened { index: 2 },
|
||||
]
|
||||
.into()
|
||||
}
|
||||
|
||||
/// LIANA_STANDARD_PATH: m/48'/0'/0'/2';
|
||||
/// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2';
|
||||
pub async fn get_extended_pubkey(
|
||||
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
|
||||
fingerprint: Fingerprint,
|
||||
network: Network,
|
||||
account: ChildNumber,
|
||||
) -> Result<DescriptorPublicKey, Error> {
|
||||
let derivation_path = default_derivation_path(network);
|
||||
let derivation_path = derivation_path(network, account);
|
||||
let xkey = hw
|
||||
.get_extended_pubkey(&derivation_path)
|
||||
.await
|
||||
@ -619,4 +660,30 @@ mod tests {
|
||||
"48'/1'/0'/2'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derivation_path() {
|
||||
assert_eq!(
|
||||
derivation_path(Network::Bitcoin, ChildNumber::Hardened { index: 0 }).to_string(),
|
||||
"48'/0'/0'/2'"
|
||||
);
|
||||
assert_eq!(
|
||||
derivation_path(Network::Regtest, ChildNumber::Hardened { index: 0 }).to_string(),
|
||||
"48'/1'/0'/2'"
|
||||
);
|
||||
assert_eq!(
|
||||
derivation_path(Network::Bitcoin, ChildNumber::Hardened { index: 1 }).to_string(),
|
||||
"48'/0'/1'/2'"
|
||||
);
|
||||
assert_eq!(
|
||||
derivation_path(Network::Regtest, ChildNumber::Hardened { index: 1 }).to_string(),
|
||||
"48'/1'/1'/2'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn unhardened_derivation_path() {
|
||||
derivation_path(Network::Bitcoin, ChildNumber::Normal { index: 0 }).to_string();
|
||||
}
|
||||
}
|
||||
|
||||
@ -764,6 +764,7 @@ mod tests {
|
||||
fingerprint: key.master_fingerprint(),
|
||||
key,
|
||||
source: KeySource::Device(async_hwi::DeviceKind::Specter, None),
|
||||
account: None,
|
||||
};
|
||||
|
||||
// Use Specter device for primary key
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use iced::{Subscription, Task};
|
||||
use liana::miniscript::bitcoin::{
|
||||
@ -73,6 +76,7 @@ pub struct ShareXpubs {
|
||||
hw_xpubs: Vec<HardwareWalletXpubs>,
|
||||
xpubs_signer: SignerXpubs,
|
||||
modal: Option<ExportModal>,
|
||||
accounts: HashMap<Fingerprint, ChildNumber>,
|
||||
}
|
||||
|
||||
impl ShareXpubs {
|
||||
@ -82,6 +86,7 @@ impl ShareXpubs {
|
||||
hw_xpubs: Vec::new(),
|
||||
xpubs_signer: SignerXpubs::new(signer),
|
||||
modal: None,
|
||||
accounts: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,6 +96,10 @@ impl Step for ShareXpubs {
|
||||
// Verification of the values is happening when the user click on Next button.
|
||||
fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::SelectAccount(fg, index) => {
|
||||
self.accounts.insert(fg, index);
|
||||
return Task::none();
|
||||
}
|
||||
Message::ImportXpub(fg, res) => {
|
||||
if let Some(hw_xpubs) = self.hw_xpubs.iter_mut().find(|x| x.fingerprint == fg) {
|
||||
hw_xpubs.processing = false;
|
||||
@ -138,6 +147,11 @@ impl Step for ShareXpubs {
|
||||
}) = hws.list.get(i)
|
||||
{
|
||||
let device = device.clone();
|
||||
let account = self
|
||||
.accounts
|
||||
.get(fingerprint)
|
||||
.copied()
|
||||
.unwrap_or(ChildNumber::from_hardened_idx(0).expect("hardcoded"));
|
||||
let fingerprint = *fingerprint;
|
||||
let network = self.network;
|
||||
if let Some(hw_xpubs) = self
|
||||
@ -159,7 +173,7 @@ impl Step for ShareXpubs {
|
||||
async move {
|
||||
(
|
||||
fingerprint,
|
||||
get_extended_pubkey(device, fingerprint, network).await,
|
||||
get_extended_pubkey(device, fingerprint, network, account).await,
|
||||
)
|
||||
},
|
||||
|(fingerprint, res)| Message::ImportXpub(fingerprint, res),
|
||||
@ -212,9 +226,10 @@ impl Step for ShareXpubs {
|
||||
Some(&hw_xpubs.xpubs),
|
||||
hw_xpubs.processing,
|
||||
hw_xpubs.error.as_ref(),
|
||||
&self.accounts,
|
||||
)
|
||||
} else {
|
||||
view::hardware_wallet_xpubs(i, hw, None, false, None)
|
||||
view::hardware_wallet_xpubs(i, hw, None, false, None, &self.accounts)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
pub mod template;
|
||||
|
||||
use iced::widget::{container, pick_list, scrollable, slider, Button, Space};
|
||||
use iced::widget::{self, container, pick_list, scrollable, slider, Button, Space};
|
||||
use iced::{Alignment, Length};
|
||||
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
@ -353,6 +353,18 @@ pub fn edit_key_modal<'a>(
|
||||
duplicate_master_fg: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let xpub_valid = form_xpub.valid && !form_xpub.value.is_empty();
|
||||
let info = Column::new()
|
||||
.push(Space::with_height(5))
|
||||
.push(widget::tooltip::Tooltip::new(
|
||||
icon::tooltip_icon(),
|
||||
"Switch account if you already use the same hardware in other configurations",
|
||||
widget::tooltip::Position::Bottom,
|
||||
));
|
||||
let source = Row::new()
|
||||
.push(p1_regular("Select the source of your key").bold())
|
||||
.push(Space::with_width(10))
|
||||
.push(info)
|
||||
.push(Space::with_width(Length::Fill));
|
||||
let content = Column::new()
|
||||
.padding(25)
|
||||
.push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
|
||||
@ -367,7 +379,7 @@ pub fn edit_key_modal<'a>(
|
||||
)
|
||||
.push(
|
||||
Column::new()
|
||||
.push(p1_regular("Select the source of your key"))
|
||||
.push(source)
|
||||
.spacing(10)
|
||||
.push(Column::with_children(hws).spacing(10))
|
||||
.push(Column::with_children(keys).spacing(10))
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
pub mod editor;
|
||||
|
||||
use async_hwi::utils::extract_keys_and_template;
|
||||
use iced::widget::{checkbox, radio, scrollable, scrollable::Scrollbar, Button, Space, TextInput};
|
||||
use iced::widget::{
|
||||
checkbox, radio, scrollable, scrollable::Scrollbar, tooltip, Button, Space, TextInput,
|
||||
};
|
||||
use iced::{
|
||||
alignment,
|
||||
widget::{progress_bar, tooltip as iced_tooltip},
|
||||
@ -9,6 +11,7 @@ use iced::{
|
||||
};
|
||||
|
||||
use async_hwi::DeviceKind;
|
||||
use liana::miniscript::bitcoin::bip32::ChildNumber;
|
||||
use liana_ui::component::text::{self, p2_regular};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
@ -473,6 +476,7 @@ pub fn hardware_wallet_xpubs<'a>(
|
||||
xpubs: Option<&'a Vec<String>>,
|
||||
processing: bool,
|
||||
error: Option<&Error>,
|
||||
accounts: &HashMap<Fingerprint, ChildNumber>,
|
||||
) -> Element<'a, Message> {
|
||||
let mut bttn = Button::new(match hw {
|
||||
HardwareWallet::Supported {
|
||||
@ -485,7 +489,14 @@ pub fn hardware_wallet_xpubs<'a>(
|
||||
if processing {
|
||||
hw::processing_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref())
|
||||
} else {
|
||||
hw::supported_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref())
|
||||
hw::supported_hardware_wallet_with_account(
|
||||
kind,
|
||||
version.as_ref(),
|
||||
*fingerprint,
|
||||
alias.as_ref(),
|
||||
accounts.get(fingerprint).cloned(),
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
HardwareWallet::Unsupported {
|
||||
@ -563,17 +574,24 @@ pub fn share_xpubs<'a>(
|
||||
hws: Vec<Element<'a, Message>>,
|
||||
signer: Element<'a, Message>,
|
||||
) -> Element<'a, Message> {
|
||||
let info = Column::new()
|
||||
.push(Space::with_height(5))
|
||||
.push(tooltip::Tooltip::new(
|
||||
icon::tooltip_icon(),
|
||||
"Switch account if you already use the same hardware in other configurations",
|
||||
tooltip::Position::Bottom,
|
||||
));
|
||||
let title = Row::new()
|
||||
.push(text("Import an extended public key by selecting a signing device:").bold())
|
||||
.push(Space::with_width(10))
|
||||
.push(info)
|
||||
.push(Space::with_width(Length::Fill));
|
||||
layout(
|
||||
(0, 0),
|
||||
email,
|
||||
"Share your public keys (Xpubs)",
|
||||
Column::new()
|
||||
.push(
|
||||
Container::new(
|
||||
text("Import an extended public key by selecting a signing device:").bold(),
|
||||
)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(title)
|
||||
.push_maybe(if hws.is_empty() {
|
||||
Some(p1_regular("No signing device connected").style(theme::text::secondary))
|
||||
} else {
|
||||
@ -718,6 +736,8 @@ pub fn register_descriptor<'a>(
|
||||
.unwrap_or(false),
|
||||
Some(descriptor),
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
))
|
||||
}),
|
||||
)
|
||||
@ -1655,6 +1675,7 @@ pub fn defined_sequence<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn hw_list_view<'a>(
|
||||
i: usize,
|
||||
hw: &'a HardwareWallet,
|
||||
@ -1663,6 +1684,8 @@ pub fn hw_list_view<'a>(
|
||||
selected: bool,
|
||||
descriptor: Option<&'a LianaDescriptor>,
|
||||
device_must_support_taproot: bool,
|
||||
accounts: Option<&HashMap<Fingerprint, ChildNumber>>,
|
||||
display_account: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let mut unrelated = false;
|
||||
let mut bttn = Button::new(match hw {
|
||||
@ -1684,6 +1707,9 @@ pub fn hw_list_view<'a>(
|
||||
} else if chosen && processing {
|
||||
hw::processing_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref())
|
||||
} else if selected {
|
||||
let acc = accounts
|
||||
.as_ref()
|
||||
.and_then(|map| map.get(fingerprint).cloned());
|
||||
hw::selected_hardware_wallet(
|
||||
kind,
|
||||
version.as_ref(),
|
||||
@ -1696,8 +1722,8 @@ pub fn hw_list_view<'a>(
|
||||
None
|
||||
}
|
||||
},
|
||||
None,
|
||||
true,
|
||||
acc,
|
||||
display_account,
|
||||
)
|
||||
} else if not_tapminiscript {
|
||||
hw::warning_hardware_wallet(
|
||||
@ -1707,8 +1733,17 @@ pub fn hw_list_view<'a>(
|
||||
alias.as_ref(),
|
||||
"Device firmware version does not support taproot miniscript",
|
||||
)
|
||||
} else if let Some(accounts) = accounts {
|
||||
hw::supported_hardware_wallet_with_account(
|
||||
kind,
|
||||
version.as_ref(),
|
||||
*fingerprint,
|
||||
alias.as_ref(),
|
||||
accounts.get(fingerprint).cloned(),
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
hw::supported_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref())
|
||||
hw::supported_hardware_wallet(kind, version.as_ref(), *fingerprint, alias.as_ref())
|
||||
}
|
||||
}
|
||||
HardwareWallet::Unsupported {
|
||||
@ -1744,6 +1779,7 @@ pub fn hw_list_view<'a>(
|
||||
bttn.into()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn key_list_view<'a>(
|
||||
i: usize,
|
||||
name: &'a str,
|
||||
@ -1752,7 +1788,9 @@ pub fn key_list_view<'a>(
|
||||
version: Option<&'a async_hwi::Version>,
|
||||
chosen: bool,
|
||||
device_must_support_taproot: bool,
|
||||
accounts: &HashMap<Fingerprint, ChildNumber>,
|
||||
) -> Element<'a, Message> {
|
||||
let account = accounts.get(fingerprint).copied();
|
||||
Button::new(if chosen {
|
||||
hw::selected_hardware_wallet(
|
||||
kind.map(|k| k.to_string()).unwrap_or_default(),
|
||||
@ -1766,7 +1804,7 @@ pub fn key_list_view<'a>(
|
||||
} else {
|
||||
None
|
||||
},
|
||||
None,
|
||||
account,
|
||||
true,
|
||||
)
|
||||
} else if device_must_support_taproot
|
||||
@ -1780,11 +1818,13 @@ pub fn key_list_view<'a>(
|
||||
"Device firmware version does not support taproot miniscript",
|
||||
)
|
||||
} else {
|
||||
hw::supported_hardware_wallet(
|
||||
hw::supported_hardware_wallet_with_account(
|
||||
kind.map(|k| k.to_string()).unwrap_or_default(),
|
||||
version,
|
||||
fingerprint,
|
||||
*fingerprint,
|
||||
Some(name),
|
||||
account,
|
||||
false,
|
||||
)
|
||||
})
|
||||
.style(theme::button::secondary)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user