diff --git a/gui/Cargo.lock b/gui/Cargo.lock index 3b1e5343..e58bc908 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "async-hwi" -version = "0.0.2" +version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6da8b5e813fa1393db34de31dd4ce826d7603e81867bccd64f5ee216209ee2" +checksum = "5df769ed2ee66f45f290602647bfb48f7c652d6d8bb9dd8eb104fdfb0ac48919" dependencies = [ "async-trait", "base64", @@ -1629,9 +1629,9 @@ dependencies = [ [[package]] name = "ledger_bitcoin_client" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8bb8c131c03c33df548b5c45ccc5c20e3f89aa973b4c8bdd539132af7f48f" +checksum = "9a8f2e27af48417f8786467d4bd7e0e279c3320ab756391329450eedec59eab6" dependencies = [ "async-trait", "bitcoin", diff --git a/gui/Cargo.toml b/gui/Cargo.toml index 6f0f16c5..cb79fc76 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -14,7 +14,7 @@ name = "liana-gui" path = "src/main.rs" [dependencies] -async-hwi = "0.0.2" +async-hwi = "0.0.3" liana = { git = "https://github.com/wizardsardine/liana", branch = "master", default-features = false } backtrace = "0.3" base64 = "0.13" diff --git a/gui/src/app/state/spend/detail.rs b/gui/src/app/state/spend/detail.rs index 81479859..eacb53c4 100644 --- a/gui/src/app/state/spend/detail.rs +++ b/gui/src/app/state/spend/detail.rs @@ -297,13 +297,17 @@ impl Action for SignAction { ) -> Command { match message { Message::View(view::Message::Spend(view::SpendTxMessage::SelectHardwareWallet(i))) => { - if let Some(hw) = self.hws.get(i) { - let device = hw.device.clone(); + if let Some(HardwareWallet::Supported { + fingerprint, + device, + .. + }) = self.hws.get(i) + { self.chosen_hw = Some(i); self.processing = true; let psbt = tx.psbt.clone(); return Command::perform( - sign_psbt(device, hw.fingerprint, psbt), + sign_psbt(device.clone(), *fingerprint, psbt), Message::Signed, ); } @@ -331,7 +335,11 @@ impl Action for SignAction { // We add the new hws without dropping the reference of the previous ones. Message::ConnectedHardwareWallets(hws) => { for h in hws { - if !self.hws.iter().any(|hw| hw.fingerprint == h.fingerprint) { + if !self + .hws + .iter() + .any(|hw| hw.fingerprint() == hw.fingerprint() && hw.kind() == h.kind()) + { self.hws.push(h); } } diff --git a/gui/src/app/view/hw.rs b/gui/src/app/view/hw.rs index 72cef21a..c32b8357 100644 --- a/gui/src/app/view/hw.rs +++ b/gui/src/app/view/hw.rs @@ -1,5 +1,5 @@ use iced::{ - widget::{Button, Column, Container, Row}, + widget::{tooltip, Button, Column, Container, Row}, Alignment, Element, Length, }; @@ -17,19 +17,49 @@ use crate::{ }, }; -pub fn hw_list_view<'a>( +pub fn hw_list_view( i: usize, hw: &HardwareWallet, chosen: bool, processing: bool, signed: bool, -) -> Element<'a, Message> { +) -> Element { let mut bttn = Button::new( Row::new() .push( Column::new() - .push(text(format!("{}", hw.kind)).bold()) - .push(text(format!("fingerprint: {}", hw.fingerprint)).small()) + .push(text(format!("{}", hw.kind())).bold()) + .push(match hw { + HardwareWallet::Supported { + fingerprint, + version, + .. + } => Row::new() + .spacing(5) + .push(text(format!("fingerprint: {}", fingerprint)).small()) + .push_maybe( + version + .as_ref() + .map(|v| text(format!("version: {}", v)).small()), + ), + HardwareWallet::Unsupported { + version, message, .. + } => Row::new() + .spacing(5) + .push_maybe( + version + .as_ref() + .map(|v| text(format!("version: {}", v)).small()), + ) + .push( + tooltip::Tooltip::new( + icon::warning_icon(), + message, + tooltip::Position::Bottom, + ) + .style(card::SimpleCardStyle), + ), + }) .spacing(5) .width(Length::Fill), ) @@ -60,7 +90,7 @@ pub fn hw_list_view<'a>( .padding(10) .style(button::Style::Border.into()) .width(Length::Fill); - if !processing { + if !processing && hw.is_supported() { bttn = bttn.on_press(Message::Spend(SpendTxMessage::SelectHardwareWallet(i))); } Container::new(bttn) diff --git a/gui/src/app/view/spend/detail.rs b/gui/src/app/view/spend/detail.rs index a1a8267c..defa4d51 100644 --- a/gui/src/app/view/spend/detail.rs +++ b/gui/src/app/view/spend/detail.rs @@ -672,7 +672,7 @@ pub fn inputs_and_outputs_view<'a>( pub fn sign_action<'a>( warning: Option<&Error>, - hws: &[HardwareWallet], + hws: &'a [HardwareWallet], processing: bool, chosen_hw: Option, signed: &[Fingerprint], @@ -702,7 +702,9 @@ pub fn sign_action<'a>( hw, Some(i) == chosen_hw, processing, - signed.contains(&hw.fingerprint), + hw.fingerprint() + .map(|f| signed.contains(&f)) + .unwrap_or(false), )) }, )) diff --git a/gui/src/hw.rs b/gui/src/hw.rs index ddca12d2..0ca5c8cc 100644 --- a/gui/src/hw.rs +++ b/gui/src/hw.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use async_hwi::{ledger, specter, DeviceKind, Error as HWIError, HWI}; +use async_hwi::{ledger, specter, DeviceKind, Error as HWIError, Version, HWI}; use liana::miniscript::bitcoin::{ hashes::hex::{FromHex, ToHex}, util::bip32::Fingerprint, @@ -9,22 +9,50 @@ use log::debug; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone)] -pub struct HardwareWallet { - pub device: Arc, - pub kind: DeviceKind, - pub fingerprint: Fingerprint, +pub enum HardwareWallet { + Unsupported { + kind: DeviceKind, + version: Option, + message: String, + }, + Supported { + device: Arc, + kind: DeviceKind, + fingerprint: Fingerprint, + version: Option, + }, } impl HardwareWallet { async fn new(device: Arc) -> Result { let kind = device.device_kind(); let fingerprint = device.get_master_fingerprint().await?; - Ok(Self { + let version = device.get_version().await.ok(); + Ok(Self::Supported { device, kind, fingerprint, + version, }) } + + pub fn kind(&self) -> &DeviceKind { + match self { + Self::Unsupported { kind, .. } => kind, + Self::Supported { kind, .. } => kind, + } + } + + pub fn fingerprint(&self) -> Option { + match self { + Self::Unsupported { .. } => None, + Self::Supported { fingerprint, .. } => Some(*fingerprint), + } + } + + pub fn is_supported(&self) -> bool { + matches!(self, Self::Supported { .. }) + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -94,14 +122,28 @@ pub async fn list_hardware_wallets( .expect("Configuration must be correct"); } - hws.push(HardwareWallet { - kind: device.device_kind(), - fingerprint, - device: Arc::new(device), - }); + let version = device.get_version().await.ok(); + if ledger_version_supported(version.as_ref()) { + hws.push(HardwareWallet::Supported { + kind: device.device_kind(), + fingerprint, + device: Arc::new(device), + version, + }); + } else { + hws.push(HardwareWallet::Unsupported { + kind: device.device_kind(), + version, + message: "Minimal supported app version is 2.1.0".to_string(), + }); + } } - Err(e) => { - debug!("{}", e); + Err(_) => { + hws.push(HardwareWallet::Unsupported { + kind: device.device_kind(), + version: None, + message: "Minimal supported app version is 2.1.0".to_string(), + }); } }, Err(HWIError::DeviceNotFound) => {} @@ -124,14 +166,28 @@ pub async fn list_hardware_wallets( .expect("Configuration must be correct"); } - hws.push(HardwareWallet { - kind: device.device_kind(), - fingerprint, - device: Arc::new(device), - }); + let version = device.get_version().await.ok(); + if ledger_version_supported(version.as_ref()) { + hws.push(HardwareWallet::Supported { + kind: device.device_kind(), + fingerprint, + device: Arc::new(device), + version, + }); + } else { + hws.push(HardwareWallet::Unsupported { + kind: device.device_kind(), + version, + message: "Minimal supported app version is 2.1.0".to_string(), + }); + } } - Err(e) => { - debug!("{}", e); + Err(_) => { + hws.push(HardwareWallet::Unsupported { + kind: device.device_kind(), + version: None, + message: "Minimal supported app version is 2.1.0".to_string(), + }); } }, Err(HWIError::DeviceNotFound) => {} @@ -141,3 +197,19 @@ pub async fn list_hardware_wallets( } hws } + +fn ledger_version_supported(version: Option<&Version>) -> bool { + if let Some(version) = version { + if version.major >= 2 { + if version.major == 2 { + version.minor >= 1 + } else { + true + } + } else { + false + } + } else { + false + } +} diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index bf4048e4..23eabb88 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -602,18 +602,27 @@ impl DescriptorKeyModal for EditXpubModal { fn update(&mut self, message: Message) -> Command { match message { Message::Select(i) => { - if let Some(hw) = self.hws.get(i) { - let device = hw.device.clone(); + if let Some(HardwareWallet::Supported { + device, + fingerprint, + .. + }) = self.hws.get(i) + { self.chosen_hw = Some(i); self.processing = true; // If another account n exists, the key is retrieved for the account n+1 let account_index = self .account_indexes - .get(&hw.fingerprint) + .get(fingerprint) .map(|account_index| account_index.increment().unwrap()) .unwrap_or_else(|| ChildNumber::from_hardened_idx(0).unwrap()); return Command::perform( - get_extended_pubkey(device, hw.fingerprint, self.network, account_index), + get_extended_pubkey( + device.clone(), + *fingerprint, + self.network, + account_index, + ), |res| { Message::DefineDescriptor(message::DefineDescriptor::HWXpubImported( res, @@ -781,20 +790,29 @@ impl HardwareWalletXpubs { } fn select(&mut self, i: usize, network: Network) -> Command { - let device = self.hw.device.clone(); - self.processing = true; - self.error = None; - let fingerprint = self.hw.fingerprint; - let next_account = self.next_account; - Command::perform( - async move { - ( - i, - get_extended_pubkey(device, fingerprint, network, next_account).await, - ) - }, - |(i, res)| Message::ImportXpub(i, res), - ) + if let HardwareWallet::Supported { + device, + fingerprint, + .. + } = &self.hw + { + let device = device.clone(); + let fingerprint = *fingerprint; + self.processing = true; + self.error = None; + let next_account = self.next_account; + Command::perform( + async move { + ( + i, + get_extended_pubkey(device, fingerprint, network, next_account).await, + ) + }, + |(i, res)| Message::ImportXpub(i, res), + ) + } else { + Command::none() + } } pub fn view(&self, i: usize) -> Element { @@ -854,11 +872,9 @@ impl Step for ParticipateXpub { } Message::ConnectedHardwareWallets(hws) => { for hw in hws { - if let Some(xpub_hw) = self - .xpubs_hw - .iter_mut() - .find(|h| h.hw.fingerprint == hw.fingerprint) - { + if let Some(xpub_hw) = self.xpubs_hw.iter_mut().find(|h| { + h.hw.fingerprint() == hw.fingerprint() && h.hw.kind() == hw.kind() + }) { xpub_hw.hw = hw; } else { self.xpubs_hw.push(HardwareWalletXpubs::new(hw)); @@ -1021,15 +1037,23 @@ impl Step for RegisterDescriptor { fn update(&mut self, message: Message) -> Command { match message { Message::Select(i) => { - if let Some((hw, hmac, _)) = self.hws.get(i) { + if let Some(( + HardwareWallet::Supported { + device, + fingerprint, + .. + }, + hmac, + _, + )) = self.hws.get(i) + { if hmac.is_none() { - let device = hw.device.clone(); let descriptor = self.descriptor.as_ref().unwrap().to_string(); self.chosen_hw = Some(i); self.processing = true; self.error = None; return Command::perform( - register_wallet(device, hw.fingerprint, descriptor), + register_wallet(device.clone(), *fingerprint, descriptor), Message::WalletRegistered, ); } @@ -1043,7 +1067,7 @@ impl Step for RegisterDescriptor { if let Some(hw_h) = self .hws .iter_mut() - .find(|hw_h| hw_h.0.fingerprint == fingerprint) + .find(|hw_h| hw_h.0.fingerprint() == Some(fingerprint)) { hw_h.1 = hmac; hw_h.2 = true; @@ -1054,11 +1078,9 @@ impl Step for RegisterDescriptor { } Message::ConnectedHardwareWallets(hws) => { for hw in hws { - if !self - .hws - .iter() - .any(|(h, _, _)| h.fingerprint == hw.fingerprint) - { + if !self.hws.iter().any(|(h, _, _)| { + h.fingerprint() == hw.fingerprint() && h.kind() == hw.kind() + }) { self.hws.push((hw, None, false)); } } @@ -1073,7 +1095,8 @@ impl Step for RegisterDescriptor { fn apply(&mut self, ctx: &mut Context) -> bool { for (hw, token, registered) in &self.hws { if *registered { - ctx.hws.push((hw.kind, hw.fingerprint, *token)); + ctx.hws + .push((*hw.kind(), hw.fingerprint().unwrap(), *token)); } } true diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 31857563..bedaaf07 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -412,7 +412,7 @@ pub fn import_descriptor<'a>( pub fn hardware_wallet_xpubs<'a>( i: usize, xpubs: &'a Vec, - hw: &HardwareWallet, + hw: &'a HardwareWallet, processing: bool, error: Option<&Error>, ) -> Element<'a, Message> { @@ -421,8 +421,38 @@ pub fn hardware_wallet_xpubs<'a>( .align_items(Alignment::Center) .push( Column::new() - .push(text(format!("{}", hw.kind)).bold()) - .push(text(format!("fingerprint: {}", hw.fingerprint)).small()) + .push(text(format!("{}", hw.kind())).bold()) + .push(match hw { + HardwareWallet::Supported { + fingerprint, + version, + .. + } => Row::new() + .spacing(5) + .push(text(format!("fingerprint: {}", fingerprint)).small()) + .push_maybe( + version + .as_ref() + .map(|v| text(format!("version: {}", v)).small()), + ), + HardwareWallet::Unsupported { + version, message, .. + } => Row::new() + .spacing(5) + .push_maybe( + version + .as_ref() + .map(|v| text(format!("version: {}", v)).small()), + ) + .push( + iced::widget::tooltip::Tooltip::new( + icon::warning_icon(), + message, + iced::widget::tooltip::Position::Bottom, + ) + .style(card::SimpleCardStyle), + ), + }) .spacing(5) .width(Length::Fill), ) @@ -442,7 +472,7 @@ pub fn hardware_wallet_xpubs<'a>( .padding(10) .style(button::Style::TransparentBorder.into()) .width(Length::Fill); - if !processing { + if !processing && hw.is_supported() { bttn = bttn.on_press(Message::Select(i)); } Container::new( @@ -576,7 +606,7 @@ pub fn participate_xpub( pub fn register_descriptor<'a>( progress: (usize, usize), descriptor: String, - hws: &[(HardwareWallet, Option<[u8; 32]>, bool)], + hws: &'a [(HardwareWallet, Option<[u8; 32]>, bool)], error: Option<&Error>, processing: bool, chosen_hw: Option, @@ -1039,7 +1069,7 @@ pub fn defined_descriptor_key( #[allow(clippy::too_many_arguments)] pub fn edit_key_modal<'a>( network: bitcoin::Network, - hws: &[HardwareWallet], + hws: &'a [HardwareWallet], error: Option<&Error>, processing: bool, chosen_hw: Option, @@ -1187,19 +1217,49 @@ pub fn edit_key_modal<'a>( .into() } -fn hw_list_view<'a>( +fn hw_list_view( i: usize, hw: &HardwareWallet, chosen: bool, processing: bool, registered: bool, -) -> Element<'a, Message> { +) -> Element { let mut bttn = Button::new( Row::new() .push( Column::new() - .push(text(format!("{}", hw.kind)).bold()) - .push(text(format!("fingerprint: {}", hw.fingerprint)).small()) + .push(text(format!("{}", hw.kind())).bold()) + .push(match hw { + HardwareWallet::Supported { + fingerprint, + version, + .. + } => Row::new() + .spacing(5) + .push(text(format!("fingerprint: {}", fingerprint)).small()) + .push_maybe( + version + .as_ref() + .map(|v| text(format!("version: {}", v)).small()), + ), + HardwareWallet::Unsupported { + version, message, .. + } => Row::new() + .spacing(5) + .push_maybe( + version + .as_ref() + .map(|v| text(format!("version: {}", v)).small()), + ) + .push( + iced::widget::tooltip::Tooltip::new( + icon::warning_icon(), + message, + iced::widget::tooltip::Position::Bottom, + ) + .style(card::SimpleCardStyle), + ), + }) .spacing(5) .width(Length::Fill), ) @@ -1223,7 +1283,7 @@ fn hw_list_view<'a>( .padding(10) .style(button::Style::TransparentBorder.into()) .width(Length::Fill); - if !processing { + if !processing && hw.is_supported() { bttn = bttn.on_press(Message::Select(i)); } Container::new(bttn)