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 async_hwi::{DeviceKind, Version};
use liana::miniscript::{bitcoin::bip32::Fingerprint, descriptor::DescriptorPublicKey}; use liana::miniscript::{
bitcoin::bip32::{ChildNumber, Fingerprint},
descriptor::DescriptorPublicKey,
};
use crate::{ use crate::{
app::settings::ProviderKey, hw::is_compatible_with_tapminiscript, services::keys::api::KeyKind, app::settings::ProviderKey, hw::is_compatible_with_tapminiscript, services::keys::api::KeyKind,
@ -107,6 +110,7 @@ pub struct Key {
pub name: String, pub name: String,
pub fingerprint: Fingerprint, pub fingerprint: Fingerprint,
pub key: DescriptorPublicKey, pub key: DescriptorPublicKey,
pub account: Option<ChildNumber>,
} }
pub struct Path { pub struct Path {

View File

@ -1,5 +1,8 @@
use liana::miniscript::{ use liana::miniscript::{
bitcoin::{bip32::Fingerprint, Network}, bitcoin::{
bip32::{ChildNumber, Fingerprint},
Network,
},
DescriptorPublicKey, DescriptorPublicKey,
}; };
use std::collections::HashMap; use std::collections::HashMap;
@ -67,6 +70,7 @@ pub enum Message {
ImportBackup, ImportBackup,
WalletFromBackup((HashMap<Fingerprint, settings::KeySetting>, Backup)), WalletFromBackup((HashMap<Fingerprint, settings::KeySetting>, Backup)),
WalletAliasEdited(String), WalletAliasEdited(String),
SelectAccount(Fingerprint, ChildNumber),
} }
impl Close for Message { 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)] #[derive(Debug, Clone)]
pub enum SelectBackend { pub enum SelectBackend {
// view messages // view messages

View File

@ -1,9 +1,9 @@
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use iced::{Subscription, Task}; use iced::{Subscription, Task};
use liana::miniscript::bitcoin::bip32::Xpub; use liana::miniscript::bitcoin::bip32::{ChildNumber, Xpub};
use liana::miniscript::{ use liana::miniscript::{
bitcoin::{ bitcoin::{
bip32::{DerivationPath, Fingerprint}, bip32::{DerivationPath, Fingerprint},
@ -89,6 +89,7 @@ pub struct EditXpubModal {
hot_signer_fingerprint: Fingerprint, hot_signer_fingerprint: Fingerprint,
chosen_signer: Option<Key>, chosen_signer: Option<Key>,
modal: Option<ExportModal>, modal: Option<ExportModal>,
accounts: HashMap<Fingerprint, ChildNumber>,
} }
impl EditXpubModal { impl EditXpubModal {
@ -104,6 +105,10 @@ impl EditXpubModal {
hot_signer_fingerprint: Fingerprint, hot_signer_fingerprint: Fingerprint,
keys: Vec<Key>, keys: Vec<Key>,
) -> Self { ) -> Self {
let accounts = keys
.iter()
.filter_map(|k| k.account.map(|acc| (k.fingerprint, acc)))
.collect();
Self { Self {
device_must_support_tapminiscript, device_must_support_tapminiscript,
path_kind, path_kind,
@ -139,6 +144,7 @@ impl EditXpubModal {
hot_signer, hot_signer,
duplicate_master_fg: false, duplicate_master_fg: false,
modal: None, modal: None,
accounts,
} }
} }
@ -158,6 +164,10 @@ impl super::DescriptorEditModal for EditXpubModal {
self.duplicate_master_fg = false; self.duplicate_master_fg = false;
self.error = None; self.error = None;
match message { match message {
Message::SelectAccount(fg, index) => {
self.accounts.insert(fg, index);
return Task::none();
}
Message::Select(i) => { Message::Select(i) => {
if let Some(HardwareWallet::Supported { if let Some(HardwareWallet::Supported {
device, device,
@ -170,6 +180,11 @@ impl super::DescriptorEditModal for EditXpubModal {
self.processing = true; self.processing = true;
self.form_key_source_kind = None; self.form_key_source_kind = None;
let device_version = version.clone(); 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 fingerprint = *fingerprint;
let device_kind = *kind; let device_kind = *kind;
let device_cloned = device.clone(); let device_cloned = device.clone();
@ -181,10 +196,11 @@ impl super::DescriptorEditModal for EditXpubModal {
device_kind, device_kind,
fingerprint, fingerprint,
network, 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::DefineDescriptor(message::DefineDescriptor::KeyModal(
message::ImportKeyModal::FetchedKey(match res { message::ImportKeyModal::FetchedKey(match res {
Err(e) => Err(e), Err(e) => Err(e),
@ -198,6 +214,7 @@ impl super::DescriptorEditModal for EditXpubModal {
fingerprint, fingerprint,
name: "".to_string(), name: "".to_string(),
key, key,
account: Some(account),
}) })
} else { } else {
Err(Error::Unexpected( Err(Error::Unexpected(
@ -233,6 +250,7 @@ impl super::DescriptorEditModal for EditXpubModal {
fingerprint, fingerprint,
name: "".to_string(), name: "".to_string(),
key: DescriptorPublicKey::from_str(&key_str).unwrap(), key: DescriptorPublicKey::from_str(&key_str).unwrap(),
account: None,
}); });
self.form_name.value = self self.form_name.value = self
.keys .keys
@ -273,7 +291,7 @@ impl super::DescriptorEditModal for EditXpubModal {
message::ImportKeyModal::FetchedKey(res) => { message::ImportKeyModal::FetchedKey(res) => {
self.processing = false; self.processing = false;
match res { match res {
Ok(key) => { Ok(mut key) => {
// If it is a provider key that has just been fetched, do some additional sanity checks. // 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() { 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. // 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(); 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. // 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 { if key.source.provider_key().is_none() || self.form_token.valid {
self.form_name.valid = key.name.is_empty() self.form_name.valid = key.name.is_empty()
@ -374,6 +393,7 @@ impl super::DescriptorEditModal for EditXpubModal {
fingerprint, fingerprint,
name: "".to_string(), name: "".to_string(),
key: DescriptorPublicKey::XPub(key), key: DescriptorPublicKey::XPub(key),
account: None,
}); });
self.form_name.value = "".to_string(); self.form_name.value = "".to_string();
self.form_name.valid = true; self.form_name.valid = true;
@ -429,6 +449,7 @@ impl super::DescriptorEditModal for EditXpubModal {
key.kind key.kind
), ),
key: key.xpub.clone(), key: key.xpub.clone(),
account: None,
}), }),
}), }),
)) ))
@ -503,6 +524,8 @@ impl super::DescriptorEditModal for EditXpubModal {
hw.fingerprint() == chosen_signer, hw.fingerprint() == chosen_signer,
None, None,
self.device_must_support_tapminiscript, self.device_must_support_tapminiscript,
Some(&self.accounts),
true,
)) ))
} }
}) })
@ -528,6 +551,7 @@ impl super::DescriptorEditModal for EditXpubModal {
key.source.device_version(), key.source.device_version(),
Some(key.fingerprint) == chosen_signer, Some(key.fingerprint) == chosen_signer,
self.device_must_support_tapminiscript, self.device_must_support_tapminiscript,
&self.accounts,
)) ))
} }
}) })
@ -576,14 +600,31 @@ pub fn default_derivation_path(network: Network) -> DerivationPath {
.unwrap() .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_STANDARD_PATH: m/48'/0'/0'/2';
/// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2'; /// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2';
pub async fn get_extended_pubkey( pub async fn get_extended_pubkey(
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>, hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
fingerprint: Fingerprint, fingerprint: Fingerprint,
network: Network, network: Network,
account: ChildNumber,
) -> Result<DescriptorPublicKey, Error> { ) -> Result<DescriptorPublicKey, Error> {
let derivation_path = default_derivation_path(network); let derivation_path = derivation_path(network, account);
let xkey = hw let xkey = hw
.get_extended_pubkey(&derivation_path) .get_extended_pubkey(&derivation_path)
.await .await
@ -619,4 +660,30 @@ mod tests {
"48'/1'/0'/2'" "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(), fingerprint: key.master_fingerprint(),
key, key,
source: KeySource::Device(async_hwi::DeviceKind::Specter, None), source: KeySource::Device(async_hwi::DeviceKind::Specter, None),
account: None,
}; };
// Use Specter device for primary key // 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 iced::{Subscription, Task};
use liana::miniscript::bitcoin::{ use liana::miniscript::bitcoin::{
@ -73,6 +76,7 @@ pub struct ShareXpubs {
hw_xpubs: Vec<HardwareWalletXpubs>, hw_xpubs: Vec<HardwareWalletXpubs>,
xpubs_signer: SignerXpubs, xpubs_signer: SignerXpubs,
modal: Option<ExportModal>, modal: Option<ExportModal>,
accounts: HashMap<Fingerprint, ChildNumber>,
} }
impl ShareXpubs { impl ShareXpubs {
@ -82,6 +86,7 @@ impl ShareXpubs {
hw_xpubs: Vec::new(), hw_xpubs: Vec::new(),
xpubs_signer: SignerXpubs::new(signer), xpubs_signer: SignerXpubs::new(signer),
modal: None, 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. // Verification of the values is happening when the user click on Next button.
fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Task<Message> { fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Task<Message> {
match message { match message {
Message::SelectAccount(fg, index) => {
self.accounts.insert(fg, index);
return Task::none();
}
Message::ImportXpub(fg, res) => { Message::ImportXpub(fg, res) => {
if let Some(hw_xpubs) = self.hw_xpubs.iter_mut().find(|x| x.fingerprint == fg) { if let Some(hw_xpubs) = self.hw_xpubs.iter_mut().find(|x| x.fingerprint == fg) {
hw_xpubs.processing = false; hw_xpubs.processing = false;
@ -138,6 +147,11 @@ impl Step for ShareXpubs {
}) = hws.list.get(i) }) = hws.list.get(i)
{ {
let device = device.clone(); let device = device.clone();
let account = self
.accounts
.get(fingerprint)
.copied()
.unwrap_or(ChildNumber::from_hardened_idx(0).expect("hardcoded"));
let fingerprint = *fingerprint; let fingerprint = *fingerprint;
let network = self.network; let network = self.network;
if let Some(hw_xpubs) = self if let Some(hw_xpubs) = self
@ -159,7 +173,7 @@ impl Step for ShareXpubs {
async move { async move {
( (
fingerprint, fingerprint,
get_extended_pubkey(device, fingerprint, network).await, get_extended_pubkey(device, fingerprint, network, account).await,
) )
}, },
|(fingerprint, res)| Message::ImportXpub(fingerprint, res), |(fingerprint, res)| Message::ImportXpub(fingerprint, res),
@ -212,9 +226,10 @@ impl Step for ShareXpubs {
Some(&hw_xpubs.xpubs), Some(&hw_xpubs.xpubs),
hw_xpubs.processing, hw_xpubs.processing,
hw_xpubs.error.as_ref(), hw_xpubs.error.as_ref(),
&self.accounts,
) )
} else { } else {
view::hardware_wallet_xpubs(i, hw, None, false, None) view::hardware_wallet_xpubs(i, hw, None, false, None, &self.accounts)
} }
}) })
.collect(), .collect(),

View File

@ -2,7 +2,7 @@
pub mod template; 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 iced::{Alignment, Length};
use liana::miniscript::bitcoin::Network; use liana::miniscript::bitcoin::Network;
@ -353,6 +353,18 @@ pub fn edit_key_modal<'a>(
duplicate_master_fg: bool, duplicate_master_fg: bool,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let xpub_valid = form_xpub.valid && !form_xpub.value.is_empty(); 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() let content = Column::new()
.padding(25) .padding(25)
.push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string()))) .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( .push(
Column::new() Column::new()
.push(p1_regular("Select the source of your key")) .push(source)
.spacing(10) .spacing(10)
.push(Column::with_children(hws).spacing(10)) .push(Column::with_children(hws).spacing(10))
.push(Column::with_children(keys).spacing(10)) .push(Column::with_children(keys).spacing(10))

View File

@ -1,7 +1,9 @@
pub mod editor; pub mod editor;
use async_hwi::utils::extract_keys_and_template; 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::{ use iced::{
alignment, alignment,
widget::{progress_bar, tooltip as iced_tooltip}, widget::{progress_bar, tooltip as iced_tooltip},
@ -9,6 +11,7 @@ use iced::{
}; };
use async_hwi::DeviceKind; use async_hwi::DeviceKind;
use liana::miniscript::bitcoin::bip32::ChildNumber;
use liana_ui::component::text::{self, p2_regular}; use liana_ui::component::text::{self, p2_regular};
use std::collections::HashMap; use std::collections::HashMap;
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
@ -473,6 +476,7 @@ pub fn hardware_wallet_xpubs<'a>(
xpubs: Option<&'a Vec<String>>, xpubs: Option<&'a Vec<String>>,
processing: bool, processing: bool,
error: Option<&Error>, error: Option<&Error>,
accounts: &HashMap<Fingerprint, ChildNumber>,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let mut bttn = Button::new(match hw { let mut bttn = Button::new(match hw {
HardwareWallet::Supported { HardwareWallet::Supported {
@ -485,7 +489,14 @@ pub fn hardware_wallet_xpubs<'a>(
if processing { if processing {
hw::processing_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref()) hw::processing_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref())
} else { } 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 { HardwareWallet::Unsupported {
@ -563,17 +574,24 @@ pub fn share_xpubs<'a>(
hws: Vec<Element<'a, Message>>, hws: Vec<Element<'a, Message>>,
signer: Element<'a, Message>, signer: Element<'a, Message>,
) -> 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( layout(
(0, 0), (0, 0),
email, email,
"Share your public keys (Xpubs)", "Share your public keys (Xpubs)",
Column::new() Column::new()
.push( .push(title)
Container::new(
text("Import an extended public key by selecting a signing device:").bold(),
)
.width(Length::Fill),
)
.push_maybe(if hws.is_empty() { .push_maybe(if hws.is_empty() {
Some(p1_regular("No signing device connected").style(theme::text::secondary)) Some(p1_regular("No signing device connected").style(theme::text::secondary))
} else { } else {
@ -718,6 +736,8 @@ pub fn register_descriptor<'a>(
.unwrap_or(false), .unwrap_or(false),
Some(descriptor), Some(descriptor),
false, false,
None,
false,
)) ))
}), }),
) )
@ -1655,6 +1675,7 @@ pub fn defined_sequence<'a>(
.into() .into()
} }
#[allow(clippy::too_many_arguments)]
pub fn hw_list_view<'a>( pub fn hw_list_view<'a>(
i: usize, i: usize,
hw: &'a HardwareWallet, hw: &'a HardwareWallet,
@ -1663,6 +1684,8 @@ pub fn hw_list_view<'a>(
selected: bool, selected: bool,
descriptor: Option<&'a LianaDescriptor>, descriptor: Option<&'a LianaDescriptor>,
device_must_support_taproot: bool, device_must_support_taproot: bool,
accounts: Option<&HashMap<Fingerprint, ChildNumber>>,
display_account: bool,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let mut unrelated = false; let mut unrelated = false;
let mut bttn = Button::new(match hw { let mut bttn = Button::new(match hw {
@ -1684,6 +1707,9 @@ pub fn hw_list_view<'a>(
} else if chosen && processing { } else if chosen && processing {
hw::processing_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref()) hw::processing_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref())
} else if selected { } else if selected {
let acc = accounts
.as_ref()
.and_then(|map| map.get(fingerprint).cloned());
hw::selected_hardware_wallet( hw::selected_hardware_wallet(
kind, kind,
version.as_ref(), version.as_ref(),
@ -1696,8 +1722,8 @@ pub fn hw_list_view<'a>(
None None
} }
}, },
None, acc,
true, display_account,
) )
} else if not_tapminiscript { } else if not_tapminiscript {
hw::warning_hardware_wallet( hw::warning_hardware_wallet(
@ -1707,8 +1733,17 @@ pub fn hw_list_view<'a>(
alias.as_ref(), alias.as_ref(),
"Device firmware version does not support taproot miniscript", "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 { } 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 { HardwareWallet::Unsupported {
@ -1744,6 +1779,7 @@ pub fn hw_list_view<'a>(
bttn.into() bttn.into()
} }
#[allow(clippy::too_many_arguments)]
pub fn key_list_view<'a>( pub fn key_list_view<'a>(
i: usize, i: usize,
name: &'a str, name: &'a str,
@ -1752,7 +1788,9 @@ pub fn key_list_view<'a>(
version: Option<&'a async_hwi::Version>, version: Option<&'a async_hwi::Version>,
chosen: bool, chosen: bool,
device_must_support_taproot: bool, device_must_support_taproot: bool,
accounts: &HashMap<Fingerprint, ChildNumber>,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let account = accounts.get(fingerprint).copied();
Button::new(if chosen { Button::new(if chosen {
hw::selected_hardware_wallet( hw::selected_hardware_wallet(
kind.map(|k| k.to_string()).unwrap_or_default(), kind.map(|k| k.to_string()).unwrap_or_default(),
@ -1766,7 +1804,7 @@ pub fn key_list_view<'a>(
} else { } else {
None None
}, },
None, account,
true, true,
) )
} else if device_must_support_taproot } else if device_must_support_taproot
@ -1780,11 +1818,13 @@ pub fn key_list_view<'a>(
"Device firmware version does not support taproot miniscript", "Device firmware version does not support taproot miniscript",
) )
} else { } else {
hw::supported_hardware_wallet( hw::supported_hardware_wallet_with_account(
kind.map(|k| k.to_string()).unwrap_or_default(), kind.map(|k| k.to_string()).unwrap_or_default(),
version, version,
fingerprint, *fingerprint,
Some(name), Some(name),
account,
false,
) )
}) })
.style(theme::button::secondary) .style(theme::button::secondary)