installer: let user choose account (derivation path) before fetching an xpub from signing device

This commit is contained in:
pythcoiner 2025-05-15 03:22:03 +02:00
parent 8efc55bcf5
commit ce8e1f3c3e
No known key found for this signature in database
GPG Key ID: C1048AEEDF303B88
7 changed files with 176 additions and 27 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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();
}
}

View File

@ -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

View File

@ -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(),

View File

@ -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))

View File

@ -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)