From 2debb32181f2365fb4e30d9d7e28a4cb7c86cd65 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Thu, 22 Feb 2024 17:42:34 +0100 Subject: [PATCH] Add taproot support to installer descriptor editor step --- gui/src/app/view/hw.rs | 3 +- gui/src/hw.rs | 21 +++++ gui/src/installer/message.rs | 11 ++- gui/src/installer/step/descriptor.rs | 135 ++++++++++++++++++++++----- gui/src/installer/view.rs | 89 +++++++++++++++++- gui/src/signer.rs | 4 +- gui/ui/src/color.rs | 1 + gui/ui/src/component/hw.rs | 5 +- gui/ui/src/theme.rs | 10 ++ 9 files changed, 244 insertions(+), 35 deletions(-) diff --git a/gui/src/app/view/hw.rs b/gui/src/app/view/hw.rs index 65a914c82..20ab14d37 100644 --- a/gui/src/app/view/hw.rs +++ b/gui/src/app/view/hw.rs @@ -33,11 +33,12 @@ pub fn hw_list_view( alias.as_ref(), ) } else if *registered == Some(false) { - hw::unregistered_hardware_wallet( + hw::warning_hardware_wallet( kind, version.as_ref(), fingerprint, alias.as_ref(), + "The wallet descriptor is not registered on the device.\n You can register it in the settings.", ) } else { hw::supported_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref()) diff --git a/gui/src/hw.rs b/gui/src/hw.rs index e2a8f048e..fbc91181e 100644 --- a/gui/src/hw.rs +++ b/gui/src/hw.rs @@ -673,3 +673,24 @@ fn ledger_version_supported(version: Option<&Version>) -> bool { false } } + +// Kind and minimal version of devices supporting tapminiscript. +// We cannot use a lazy_static HashMap yet, because DeviceKind does not implement Hash. +const DEVICES_COMPATIBLE_WITH_TAPMINISCRIPT: [(DeviceKind, Option); 0] = []; + +pub fn is_compatible_with_tapminiscript( + device_kind: &DeviceKind, + version: Option<&Version>, +) -> bool { + DEVICES_COMPATIBLE_WITH_TAPMINISCRIPT + .iter() + .any(|(kind, minimal_version)| { + device_kind == kind + && match (version, minimal_version) { + (Some(v1), Some(v2)) => v1 >= v2, + (None, Some(_)) => false, + (Some(_), None) => true, + (None, None) => true, + } + }) +} diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index d6e8afad4..4c2981017 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -10,7 +10,7 @@ use crate::{ download::Progress, hw::HardwareWalletMessage, }; -use async_hwi::DeviceKind; +use async_hwi::{DeviceKind, Version}; #[derive(Debug, Clone)] pub enum Message { @@ -30,6 +30,7 @@ pub enum Message { UseHotSigner, Installed(Result), Network(Network), + CreateTaprootDescriptor(bool), SelectBitcoindType(SelectBitcoindTypeMsg), InternalBitcoind(InternalBitcoindMsg), DefineBitcoind(DefineBitcoind), @@ -85,12 +86,18 @@ pub enum DefinePath { EditSequence, } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum DefineKey { Delete, Edit, Clipboard(String), - Edited(String, DescriptorPublicKey, Option), + Edited( + String, + DescriptorPublicKey, + Option, + Option, + ), } #[derive(Debug, Clone)] diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index 2426496e5..86fccaf7a 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -24,8 +24,9 @@ use liana_ui::{ widget::Element, }; -use async_hwi::DeviceKind; +use async_hwi::{DeviceKind, Version}; +use crate::hw; use crate::{ app::{settings::KeySetting, wallet::wallet_name}, hw::{HardwareWallet, HardwareWallets}, @@ -72,6 +73,7 @@ impl RecoveryPath { &self, aliases: &HashMap, duplicate_name: &HashSet, + incompatible_with_tapminiscript: &HashSet, ) -> Element { view::recovery_path_view( self.sequence, @@ -85,6 +87,7 @@ impl RecoveryPath { view::defined_descriptor_key( aliases.get(key).unwrap().to_string(), duplicate_name.contains(key), + incompatible_with_tapminiscript.contains(key), ) } else { view::undefined_descriptor_key() @@ -99,6 +102,7 @@ impl RecoveryPath { struct Setup { keys: Vec, duplicate_name: HashSet, + incompatible_with_tapminiscript: HashSet, spending_keys: Vec>, spending_threshold: usize, recovery_paths: Vec, @@ -109,6 +113,7 @@ impl Setup { Self { keys: Vec::new(), duplicate_name: HashSet::new(), + incompatible_with_tapminiscript: HashSet::new(), spending_keys: vec![None], spending_threshold: 1, recovery_paths: vec![RecoveryPath::new()], @@ -120,6 +125,7 @@ impl Setup { && !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() } // Mark as duplicate every defined key that have the same name but not the same fingerprint. @@ -150,6 +156,33 @@ impl Setup { } } + fn check_for_tapminiscript_support(&mut self, must_support_taproot: bool) { + self.incompatible_with_tapminiscript = HashSet::new(); + if must_support_taproot { + for key in &self.keys { + // check if key is used by a path + if !self + .spending_keys + .iter() + .chain(self.recovery_paths.iter().flat_map(|path| &path.keys)) + .any(|k| *k == Some(key.fingerprint)) + { + continue; + } + + // device_kind is none only for HotSigner which is compatible. + if let Some(device_kind) = key.device_kind.as_ref() { + if !hw::is_compatible_with_tapminiscript( + device_kind, + key.device_version.as_ref(), + ) { + self.incompatible_with_tapminiscript.insert(key.fingerprint); + } + } + } + } + } + fn keys_aliases(&self) -> HashMap { let mut map = HashMap::new(); for key in &self.keys { @@ -162,6 +195,7 @@ impl Setup { pub struct DefineDescriptor { network: Network, network_valid: bool, + use_taproot: bool, data_dir: Option, setup: HashMap, @@ -175,6 +209,7 @@ impl DefineDescriptor { pub fn new(signer: Arc>) -> Self { Self { network: Network::Bitcoin, + use_taproot: false, setup: HashMap::from([(Network::Bitcoin, Setup::new())]), data_dir: None, network_valid: true, @@ -204,6 +239,14 @@ impl DefineDescriptor { network_datadir.push(self.network.to_string()); self.network_valid = !network_datadir.exists(); } + self.check_setup() + } + + fn check_setup(&mut self) { + self.setup_mut().check_for_duplicate(); + let use_taproot = self.use_taproot; + self.setup_mut() + .check_for_tapminiscript_support(use_taproot); } } @@ -221,6 +264,10 @@ impl Step for DefineDescriptor { hws.set_network(network); self.set_network(network) } + Message::CreateTaprootDescriptor(use_taproot) => { + self.use_taproot = use_taproot; + self.check_setup(); + } Message::DefineDescriptor(message::DefineDescriptor::AddRecoveryPath) => { self.setup_mut().recovery_paths.push(RecoveryPath::new()); } @@ -236,7 +283,7 @@ impl Step for DefineDescriptor { message::DefineKey::Clipboard(key) => { return Command::perform(async move { key }, Message::Clibpboard); } - message::DefineKey::Edited(name, imported_key, kind) => { + message::DefineKey::Edited(name, imported_key, kind, version) => { let fingerprint = imported_key.master_fingerprint(); hws.set_alias(fingerprint, name.clone()); if let Some(key) = self @@ -252,17 +299,20 @@ impl Step for DefineDescriptor { name, key: imported_key, device_kind: kind, + device_version: version, }); } self.setup_mut().spending_keys[i] = Some(fingerprint); self.modal = None; - self.setup_mut().check_for_duplicate(); + self.check_setup(); } message::DefineKey::Edit => { + let use_taproot = self.use_taproot; 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 @@ -293,7 +343,7 @@ impl Step for DefineDescriptor { { self.setup_mut().spending_threshold -= 1; } - self.setup_mut().check_for_duplicate(); + self.check_setup(); } }, _ => {} @@ -327,7 +377,7 @@ impl Step for DefineDescriptor { message::DefineKey::Clipboard(key) => { return Command::perform(async move { key }, Message::Clibpboard); } - message::DefineKey::Edited(name, imported_key, kind) => { + message::DefineKey::Edited(name, imported_key, kind, version) => { let fingerprint = imported_key.master_fingerprint(); hws.set_alias(fingerprint, name.clone()); if let Some(key) = self @@ -343,17 +393,20 @@ impl Step for DefineDescriptor { name, key: imported_key, device_kind: kind, + device_version: version, }); } self.setup_mut().recovery_paths[i].keys[j] = Some(fingerprint); self.modal = None; - self.setup_mut().check_for_duplicate(); + self.check_setup(); } message::DefineKey::Edit => { + let use_taproot = self.use_taproot; let setup = self.setup_mut(); 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] { @@ -390,7 +443,7 @@ impl Step for DefineDescriptor { { self.setup_mut().recovery_paths.remove(i); } - self.setup_mut().check_for_duplicate(); + self.check_setup(); } }, }, @@ -490,7 +543,11 @@ impl Step for DefineDescriptor { PathInfo::Multi(self.setup[&self.network].spending_threshold, spending_keys) }; - let policy = match LianaPolicy::new(spending_keys, recovery_paths) { + let policy = match if self.use_taproot { + LianaPolicy::new(spending_keys, recovery_paths) + } else { + LianaPolicy::new_legacy(spending_keys, recovery_paths) + } { Ok(policy) => policy, Err(e) => { self.error = Some(e.to_string()); @@ -513,6 +570,7 @@ impl Step for DefineDescriptor { progress, self.network, self.network_valid, + self.use_taproot, self.setup[&self.network] .spending_keys .iter() @@ -522,6 +580,9 @@ impl Step for DefineDescriptor { view::defined_descriptor_key( aliases.get(key).unwrap().to_string(), self.setup[&self.network].duplicate_name.contains(key), + self.setup[&self.network] + .incompatible_with_tapminiscript + .contains(key), ) } else { view::undefined_descriptor_key() @@ -539,12 +600,14 @@ impl Step for DefineDescriptor { .iter() .enumerate() .map(|(i, path)| { - path.view(&aliases, &self.setup[&self.network].duplicate_name) - .map(move |msg| { - Message::DefineDescriptor(message::DefineDescriptor::RecoveryPath( - i, msg, - )) - }) + path.view( + &aliases, + &self.setup[&self.network].duplicate_name, + &self.setup[&self.network].incompatible_with_tapminiscript, + ) + .map(move |msg| { + Message::DefineDescriptor(message::DefineDescriptor::RecoveryPath(i, msg)) + }) }) .collect(), self.valid(), @@ -583,6 +646,7 @@ fn new_multixkey_from_xpub( #[derive(Clone)] pub struct Key { pub device_kind: Option, + pub device_version: Option, pub name: String, pub fingerprint: Fingerprint, pub key: DescriptorPublicKey, @@ -675,6 +739,7 @@ impl DescriptorEditModal for EditSequenceModal { } pub struct EditXpubModal { + device_must_support_tapminiscript: bool, /// None if path is primary path path_index: Option, key_index: usize, @@ -692,12 +757,13 @@ pub struct EditXpubModal { keys: Vec, hot_signer: Arc>, hot_signer_fingerprint: Fingerprint, - chosen_signer: Option<(Fingerprint, Option)>, + chosen_signer: Option<(Fingerprint, Option, Option)>, } impl EditXpubModal { #[allow(clippy::too_many_arguments)] fn new( + device_must_support_tapminiscript: bool, other_path_keys: HashSet, key: Option, path_index: Option, @@ -708,6 +774,7 @@ impl EditXpubModal { ) -> Self { let hot_signer_fingerprint = hot_signer.lock().unwrap().fingerprint(); Self { + device_must_support_tapminiscript, other_path_keys, form_name: form::Value { valid: true, @@ -740,7 +807,7 @@ impl EditXpubModal { error: None, network, edit_name: false, - chosen_signer: key.map(|k| (k, None)), + chosen_signer: key.map(|k| (k, None, None)), hot_signer_fingerprint, hot_signer, duplicate_master_fg: false, @@ -767,10 +834,11 @@ impl DescriptorEditModal for EditXpubModal { device, fingerprint, kind, + version, .. }) = hws.list.get(i) { - self.chosen_signer = Some((*fingerprint, Some(*kind))); + self.chosen_signer = Some((*fingerprint, Some(*kind), version.clone())); self.processing = true; return Command::perform( get_extended_pubkey(device.clone(), *fingerprint, self.network), @@ -787,7 +855,7 @@ impl DescriptorEditModal for EditXpubModal { } Message::UseHotSigner => { let fingerprint = self.hot_signer.lock().unwrap().fingerprint(); - self.chosen_signer = Some((fingerprint, None)); + self.chosen_signer = Some((fingerprint, None, None)); self.form_xpub.valid = true; if let Some(alias) = self .keys @@ -882,7 +950,12 @@ impl DescriptorEditModal for EditXpubModal { if let Ok(key) = DescriptorPublicKey::from_str(&self.form_xpub.value) { let key_index = self.key_index; let name = self.form_name.value.clone(); - let device_kind = self.chosen_signer.and_then(|(_, kind)| kind); + let (device_kind, device_version) = + if let Some((_, kind, version)) = &self.chosen_signer { + (*kind, version.clone()) + } else { + (None, None) + }; if self.other_path_keys.contains(&key.master_fingerprint()) { self.duplicate_master_fg = true; } else if let Some(path_index) = self.path_index { @@ -893,7 +966,12 @@ impl DescriptorEditModal for EditXpubModal { path_index, message::DefinePath::Key( key_index, - message::DefineKey::Edited(name, key, device_kind), + message::DefineKey::Edited( + name, + key, + device_kind, + device_version, + ), ), ) }, @@ -906,7 +984,12 @@ impl DescriptorEditModal for EditXpubModal { message::DefineDescriptor::PrimaryPath( message::DefinePath::Key( key_index, - message::DefineKey::Edited(name, key, device_kind), + message::DefineKey::Edited( + name, + key, + device_kind, + device_version, + ), ), ) }, @@ -917,7 +1000,8 @@ impl DescriptorEditModal for EditXpubModal { } message::ImportKeyModal::SelectKey(i) => { if let Some(key) = self.keys.get(i) { - self.chosen_signer = Some((key.fingerprint, key.device_kind)); + self.chosen_signer = + Some((key.fingerprint, key.device_kind, key.device_version.clone())); self.form_xpub.value = key.key.to_string(); self.form_xpub.valid = true; self.form_name.value = key.name.clone(); @@ -930,7 +1014,7 @@ impl DescriptorEditModal for EditXpubModal { Command::none() } fn view<'a>(&'a self, hws: &'a HardwareWallets) -> Element<'a, Message> { - let chosen_signer = self.chosen_signer.map(|s| s.0); + let chosen_signer = self.chosen_signer.as_ref().map(|s| s.0); view::edit_key_modal( self.network, hws.list @@ -953,6 +1037,7 @@ impl DescriptorEditModal for EditXpubModal { && hw.fingerprint() == chosen_signer && self.form_xpub.valid && !self.form_xpub.value.is_empty(), + self.device_must_support_tapminiscript, )) } }) @@ -969,13 +1054,15 @@ impl DescriptorEditModal for EditXpubModal { &key.name, &key.fingerprint, key.device_kind.as_ref(), + key.device_version.as_ref(), Some(key.fingerprint) == chosen_signer, + self.device_must_support_tapminiscript, )) } }) .collect(), self.error.as_ref(), - self.chosen_signer.map(|s| s.0), + self.chosen_signer.as_ref().map(|s| s.0), &self.hot_signer_fingerprint, self.keys.iter().find_map(|k| { if k.fingerprint == self.hot_signer_fingerprint { diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 60eb4efa9..eb1cd165c 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -23,7 +23,7 @@ use liana_ui::{ use crate::{ bitcoind::{ConfigField, RpcAuthType, RpcAuthValues, StartInternalBitcoindError}, - hw::HardwareWallet, + hw::{is_compatible_with_tapminiscript, HardwareWallet}, installer::{ message::{self, Message}, prompt, @@ -173,11 +173,29 @@ pub fn welcome<'a>() -> Element<'a, Message> { .into() } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DescriptorKind { + Legacy, + Taproot, +} + +const DESCRIPTOR_KINDS: [DescriptorKind; 2] = [DescriptorKind::Legacy, DescriptorKind::Taproot]; + +impl std::fmt::Display for DescriptorKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Legacy => write!(f, "Default"), + Self::Taproot => write!(f, "Taproot"), + } + } +} + #[allow(clippy::too_many_arguments)] pub fn define_descriptor<'a>( progress: (usize, usize), network: bitcoin::Network, network_valid: bool, + use_taproot: bool, spending_keys: Vec>, spending_threshold: usize, recovery_paths: Vec>, @@ -204,6 +222,23 @@ pub fn define_descriptor<'a>( Some(text("A data directory already exists for this network").style(color::RED)) }); + let col_wallet = Column::new() + .spacing(10) + .push(text("Wallet").bold()) + .push(container( + pick_list( + &DESCRIPTOR_KINDS[..], + Some(if use_taproot { + DescriptorKind::Taproot + } else { + DescriptorKind::Legacy + }), + |kind| Message::CreateTaprootDescriptor(kind == DescriptorKind::Taproot), + ) + .style(theme::PickList::Secondary) + .padding(10), + )); + let col_spending_keys = Column::new() .push( Row::new() @@ -266,7 +301,13 @@ pub fn define_descriptor<'a>( .push( Column::new() .width(Length::Fill) - .push(col_network) + .push( + Row::new() + .push(col_network) + .push(Space::with_width(Length::Fixed(100.0))) + .push(col_wallet) + .align_items(Alignment::Start), + ) .push( Column::new() .spacing(25) @@ -707,6 +748,7 @@ pub fn register_descriptor<'a>( hw.fingerprint() .map(|fg| registered.contains(&fg)) .unwrap_or(false), + false, )) }), ) @@ -1287,6 +1329,7 @@ pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> { 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) @@ -1336,6 +1379,21 @@ pub fn defined_descriptor_key<'a>( ) .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) @@ -1594,6 +1652,7 @@ pub fn hw_list_view( chosen: bool, processing: bool, selected: bool, + device_must_support_taproot: bool, ) -> Element { let mut bttn = Button::new(match hw { HardwareWallet::Supported { @@ -1607,6 +1666,16 @@ pub fn hw_list_view( hw::processing_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref()) } else if selected { hw::selected_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref()) + } else if device_must_support_taproot + && !is_compatible_with_tapminiscript(kind, version.as_ref()) + { + hw::warning_hardware_wallet( + kind, + version.as_ref(), + fingerprint, + alias.as_ref(), + "Device firmware version does not support taproot miniscript", + ) } else { hw::supported_hardware_wallet(kind, version.as_ref(), fingerprint, alias.as_ref()) } @@ -1634,19 +1703,31 @@ pub fn key_list_view<'a>( name: &'a str, fingerprint: &'a Fingerprint, kind: Option<&'a DeviceKind>, + version: Option<&'a async_hwi::Version>, chosen: bool, + device_must_support_taproot: bool, ) -> Element<'a, Message> { let bttn = Button::new(if chosen { hw::selected_hardware_wallet( kind.map(|k| k.to_string()).unwrap_or_default(), - None::, + version, fingerprint, Some(name), ) + } else if device_must_support_taproot + && kind.map(|kind| is_compatible_with_tapminiscript(kind, version)) == Some(false) + { + hw::warning_hardware_wallet( + kind.map(|k| k.to_string()).unwrap_or_default(), + version, + fingerprint, + Some(name), + "Device firmware version does not support taproot miniscript", + ) } else { hw::supported_hardware_wallet( kind.map(|k| k.to_string()).unwrap_or_default(), - None::, + version, fingerprint, Some(name), ) diff --git a/gui/src/signer.rs b/gui/src/signer.rs index 4f7009948..dad45dab9 100644 --- a/gui/src/signer.rs +++ b/gui/src/signer.rs @@ -10,7 +10,7 @@ use liana::{ }; pub struct Signer { - curve: secp256k1::Secp256k1, + curve: secp256k1::Secp256k1, key: HotSigner, pub fingerprint: Fingerprint, } @@ -23,7 +23,7 @@ impl std::fmt::Debug for Signer { impl Signer { pub fn new(key: HotSigner) -> Self { - let curve = secp256k1::Secp256k1::signing_only(); + let curve = secp256k1::Secp256k1::new(); let fingerprint = key.fingerprint(&curve); Self { key, diff --git a/gui/ui/src/color.rs b/gui/ui/src/color.rs index e9e8b6691..dcd7726d7 100644 --- a/gui/ui/src/color.rs +++ b/gui/ui/src/color.rs @@ -1,5 +1,6 @@ use iced::Color; pub const BLACK: Color = iced::Color::BLACK; +pub const TRANSPARENT: Color = iced::Color::TRANSPARENT; pub const LIGHT_BLACK: Color = Color::from_rgb( 0x14 as f32 / 255.0, 0x14 as f32 / 255.0, diff --git a/gui/ui/src/component/hw.rs b/gui/ui/src/component/hw.rs index 848935e58..534d4872f 100644 --- a/gui/ui/src/component/hw.rs +++ b/gui/ui/src/component/hw.rs @@ -51,11 +51,12 @@ pub fn supported_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>( .padding(10) } -pub fn unregistered_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>( +pub fn warning_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>( kind: K, version: Option, fingerprint: F, alias: Option>>, + warning: &'static str, ) -> Container<'a, T> { container( row(vec![ @@ -75,7 +76,7 @@ pub fn unregistered_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Displa .into(), column(vec![tooltip::Tooltip::new( icon::warning_icon(), - "The wallet descriptor is not registered on the device.\n You can register it in the settings.", + warning, tooltip::Position::Bottom, ) .style(theme::Container::Card(theme::Card::Simple)) diff --git a/gui/ui/src/theme.rs b/gui/ui/src/theme.rs index e40d9010c..a55767e44 100644 --- a/gui/ui/src/theme.rs +++ b/gui/ui/src/theme.rs @@ -441,6 +441,7 @@ pub enum PickList { #[default] Simple, Invalid, + Secondary, } impl pick_list::StyleSheet for Theme { type Style = PickList; @@ -465,6 +466,15 @@ impl pick_list::StyleSheet for Theme { border_radius: 25.0, text_color: color::RED, }, + PickList::Secondary => pick_list::Appearance { + placeholder_color: color::GREY_3, + handle_color: color::GREY_3, + background: color::TRANSPARENT.into(), + border_width: 1.0, + border_color: color::GREY_3, + border_radius: 25.0, + text_color: color::GREY_3, + }, } }