From 8ad37f18a88a57748a7c3f53f1ecb6bb8347194a Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 21 Oct 2022 17:30:55 +0200 Subject: [PATCH 1/7] gui: import xpubs from hws (wip) --- gui/Cargo.lock | 332 ++++++++++++++++++++++++++- gui/Cargo.toml | 5 +- gui/src/hw.rs | 76 ++++++ gui/src/installer/message.rs | 8 + gui/src/installer/mod.rs | 30 ++- gui/src/installer/step/descriptor.rs | 286 +++++++++++++++++++++++ gui/src/installer/step/mod.rs | 158 ++----------- gui/src/installer/view.rs | 206 ++++++++++++++--- gui/src/lib.rs | 1 + gui/src/ui/component/card.rs | 35 ++- gui/src/ui/icon.rs | 4 + 11 files changed, 943 insertions(+), 198 deletions(-) create mode 100644 gui/src/hw.rs create mode 100644 gui/src/installer/step/descriptor.rs diff --git a/gui/Cargo.lock b/gui/Cargo.lock index 9fde9e67..281ff269 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -2,6 +2,27 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "CoreFoundation-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e9889e6db118d49d88d84728d0e964d973a5680befb5f85f55141beea5c20b" +dependencies = [ + "libc", + "mach 0.1.2", +] + +[[package]] +name = "IOKit-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99696c398cbaf669d2368076bdb3d627fb0ce51a26899d7c61228c5c0af3bf4a" +dependencies = [ + "CoreFoundation-sys", + "libc", + "mach 0.1.2", +] + [[package]] name = "ab_glyph" version = "0.2.15" @@ -39,6 +60,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "ahash" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" + [[package]] name = "ahash" version = "0.7.6" @@ -50,6 +77,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +dependencies = [ + "memchr", +] + [[package]] name = "approx" version = "0.5.1" @@ -86,6 +122,36 @@ dependencies = [ "libloading", ] +[[package]] +name = "async-hwi" +version = "0.0.1" +source = "git+https://github.com/revault/async-hwi?branch=add-ledger#0eace00eca1ece4e4c49f2cd4d6fbffa2d527ce5" +dependencies = [ + "async-trait", + "base64", + "bitcoin", + "futures", + "hidapi", + "ledger-apdu", + "ledger-transport-hid", + "ledger_bitcoin_client", + "regex", + "serialport", + "tokio", + "tokio-serial", +] + +[[package]] +name = "async-trait" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -149,8 +215,11 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cb36de3b18ad25f396f9168302e36fb7e1e8923298ab3127da252d288d5af9d" dependencies = [ + "base64", "bech32", "bitcoin_hashes", + "core2", + "hashbrown 0.8.2", "secp256k1", "serde", ] @@ -161,6 +230,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" dependencies = [ + "core2", "serde", ] @@ -208,6 +278,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" + [[package]] name = "calloop" version = "0.9.3" @@ -431,6 +507,15 @@ dependencies = [ "objc", ] +[[package]] +name = "core2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" +dependencies = [ + "memchr", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -590,6 +675,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "downcast-rs" version = "1.2.0" @@ -940,13 +1031,23 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "hashbrown" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" +dependencies = [ + "ahash 0.3.8", + "autocfg", +] + [[package]] name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash", + "ahash 0.7.6", ] [[package]] @@ -964,6 +1065,12 @@ dependencies = [ "hashbrown 0.11.2", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -973,12 +1080,29 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hexf-parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hidapi" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d26e1151deaab68f34fbfd16d491a2a0170cf98d69d3efa23873b567a4199e1" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "iced" version = "0.4.2" @@ -1216,6 +1340,52 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "ledger-apdu" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe435806c197dfeaa5efcded5e623c4b8230fd28fdf1e91e7a86e40ef2acbf90" +dependencies = [ + "arrayref", + "no-std-compat", + "snafu", +] + +[[package]] +name = "ledger-transport" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1117f2143d92c157197785bf57711d7b02f2cfa101e162f8ca7900fb7f976321" +dependencies = [ + "async-trait", + "ledger-apdu", +] + +[[package]] +name = "ledger-transport-hid" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ba81a1f5f24396b37211478aff7fbcd605dd4544df8dbed07b9da3c2057aee" +dependencies = [ + "byteorder", + "cfg-if 1.0.0", + "hex", + "hidapi", + "ledger-transport", + "libc", + "log", + "thiserror", +] + +[[package]] +name = "ledger_bitcoin_client" +version = "0.1.0" +source = "git+https://github.com/edouardparis/app-bitcoin-new/?branch=bitcoin_client_rs#4f7951ce7c464db80f241ca8cd46c45770d6eec7" +dependencies = [ + "async-trait", + "bitcoin", +] + [[package]] name = "libc" version = "0.2.126" @@ -1243,6 +1413,26 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1318,6 +1508,24 @@ dependencies = [ "lyon_path", ] +[[package]] +name = "mach" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd13ee2dd61cc82833ba05ade5a30bb3d63f7ced605ef827063c63078302de9" +dependencies = [ + "libc", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1409,6 +1617,7 @@ dependencies = [ name = "minisafe-gui" version = "0.0.1" dependencies = [ + "async-hwi", "backtrace", "chrono", "dirs", @@ -1455,6 +1664,19 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mio-serial" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e0f6dc55a5aa7b8d320407c5c4ced464e23815c60f6a1e6d9e225d2b45905" +dependencies = [ + "log", + "mio", + "nix 0.23.1", + "serialport", + "winapi", +] + [[package]] name = "mutate_once" version = "0.1.1" @@ -1545,6 +1767,19 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset", +] + [[package]] name = "nix" version = "0.24.2" @@ -1556,6 +1791,12 @@ dependencies = [ "libc", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.1" @@ -1946,6 +2187,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -2116,6 +2374,23 @@ dependencies = [ "serde", ] +[[package]] +name = "serialport" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab92efb5cf60ad310548bc3f16fa6b0d950019cb7ed8ff41968c3d03721cf12" +dependencies = [ + "CoreFoundation-sys", + "IOKit-sys", + "bitflags", + "cfg-if 1.0.0", + "libudev", + "mach 0.3.2", + "nix 0.24.2", + "regex", + "winapi", +] + [[package]] name = "sid" version = "0.6.1" @@ -2220,6 +2495,38 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "snafu" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ba99b054b22972ee794cf04e5ef572da1229e33b65f3c57abbff0525a454" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e79cdebbabaebb06a9bdbaedc7f159b410461f63611d4d0e3fb0fab8fed850" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "spirv" version = "0.2.0+1.5.4" @@ -2350,17 +2657,19 @@ dependencies = [ [[package]] name = "tokio" -version = "1.20.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", + "bytes", "libc", + "memchr", "mio", "num_cpus", - "once_cell", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", "winapi", ] @@ -2376,6 +2685,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-serial" +version = "5.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5488e0c75c70e880823aebc3ad4ac0a6da6f48d95bc1b9a52bd3200d6f1e724" +dependencies = [ + "cfg-if 1.0.0", + "futures", + "log", + "mio-serial", + "tokio", +] + [[package]] name = "toml" version = "0.5.9" @@ -2403,7 +2725,7 @@ version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "rand 0.8.5", "static_assertions", ] diff --git a/gui/Cargo.toml b/gui/Cargo.toml index a30e9808..1c597ec3 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -14,13 +14,14 @@ name = "minisafe-gui" path = "src/main.rs" [dependencies] -minisafe = { git = "https://github.com/revault/minisafe", branch = "master", default-features = false} +async-hwi = { git = "https://github.com/revault/async-hwi", branch = "add-ledger" } +minisafe = { git = "https://github.com/revault/minisafe", branch = "master", default-features = false } backtrace = "0.3" iced = { version = "0.4", default-features= false, features = ["tokio", "wgpu", "svg", "qr_code", "pure"] } iced_native = "0.5" -tokio = {version = "1.9.0", features = ["signal"]} +tokio = {version = "1.21.0", features = ["signal"]} serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/gui/src/hw.rs b/gui/src/hw.rs new file mode 100644 index 00000000..cdbe26d4 --- /dev/null +++ b/gui/src/hw.rs @@ -0,0 +1,76 @@ +use async_hwi::{ledger, specter, DeviceKind, Error as HWIError, HWI}; +use log::debug; +use minisafe::miniscript::bitcoin::util::bip32::Fingerprint; +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct HardwareWallet { + pub device: Arc, + pub kind: DeviceKind, + pub fingerprint: Fingerprint, +} + +impl HardwareWallet { + async fn new(device: Arc) -> Result { + let kind = device.device_kind(); + let fingerprint = device.get_master_fingerprint().await?; + Ok(Self { + device, + kind, + fingerprint, + }) + } +} + +pub async fn list_hardware_wallets() -> Vec { + let mut hws: Vec = Vec::new(); + match specter::SpecterSimulator::try_connect().await { + Ok(device) => match HardwareWallet::new(Arc::new(device)).await { + Ok(hw) => hws.push(hw), + Err(e) => { + debug!("{}", e); + } + }, + Err(HWIError::DeviceNotFound) => {} + Err(e) => { + debug!("{}", e); + } + } + match specter::Specter::try_connect_serial().await { + Ok(device) => match HardwareWallet::new(Arc::new(device)).await { + Ok(hw) => hws.push(hw), + Err(e) => { + debug!("{}", e); + } + }, + Err(HWIError::DeviceNotFound) => {} + Err(e) => { + debug!("{}", e); + } + } + match ledger::LedgerSimulator::try_connect().await { + Ok(device) => match HardwareWallet::new(Arc::new(device)).await { + Ok(hw) => hws.push(hw), + Err(e) => { + debug!("{}", e); + } + }, + Err(HWIError::DeviceNotFound) => {} + Err(e) => { + debug!("{}", e); + } + } + match ledger::Ledger::try_connect_hid() { + Ok(device) => match HardwareWallet::new(Arc::new(device)).await { + Ok(hw) => hws.push(hw), + Err(e) => { + debug!("{}", e); + } + }, + Err(HWIError::DeviceNotFound) => {} + Err(e) => { + debug!("{}", e); + } + } + hws +} diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 537ca1c4..1ac412c7 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -2,6 +2,7 @@ use minisafe::miniscript::bitcoin; use std::path::PathBuf; use super::Error; +use crate::hw::HardwareWallet; #[derive(Debug, Clone)] pub enum Message { @@ -10,10 +11,14 @@ pub enum Message { Next, Previous, Install, + Close, + Reload, + Select(usize), Installed(Result), Network(bitcoin::Network), DefineBitcoind(DefineBitcoind), DefineDescriptor(DefineDescriptor), + ConnectedHardwareWallets(Vec), } #[derive(Debug, Clone)] @@ -25,6 +30,9 @@ pub enum DefineBitcoind { #[derive(Debug, Clone)] pub enum DefineDescriptor { ImportDescriptor(String), + ImportUserHWXpub, + ImportHeirHWXpub, + XpubImported(Result), UserXpubEdited(String), HeirXpubEdited(String), SequenceEdited(String), diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 864407f7..b67e947a 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -100,32 +100,32 @@ impl Installer { .expect("There is always a step"); current_step.load_context(&self.context); } + Command::none() } Message::Previous => { self.previous(); + Command::none() } Message::Install => { self.steps .get_mut(self.current) .expect("There is always a step") .update(message); - return Command::perform( + Command::perform( install(self.context.clone(), self.config.clone()), Message::Installed, - ); + ) } Message::Event(Event::Window(window::Event::CloseRequested)) => { self.stop(); - return Command::none(); + Command::none() } - _ => { - self.steps - .get_mut(self.current) - .expect("There is always a step") - .update(message); - } - }; - Command::none() + _ => self + .steps + .get_mut(self.current) + .expect("There is always a step") + .update(message), + } } pub fn view(&self) -> Element { @@ -196,6 +196,13 @@ pub enum Error { CannotCreateFile(String), CannotWriteToFile(String), Unexpected(String), + HardwareWallet(async_hwi::Error), +} + +impl From for Error { + fn from(error: async_hwi::Error) -> Self { + Error::HardwareWallet(error) + } } impl std::fmt::Display for Error { @@ -205,6 +212,7 @@ impl std::fmt::Display for Error { Self::CannotWriteToFile(e) => write!(f, "Failed to write to file: {}", e), Self::CannotCreateFile(e) => write!(f, "Failed to create file: {}", e), Self::Unexpected(e) => write!(f, "Unexpected: {}", e), + Self::HardwareWallet(e) => write!(f, "Hardware Wallet: {}", e), } } } diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs new file mode 100644 index 00000000..c7d0987a --- /dev/null +++ b/gui/src/installer/step/descriptor.rs @@ -0,0 +1,286 @@ +use std::str::FromStr; + +use iced::{pure::Element, Command}; +use minisafe::{ + descriptors::InheritanceDescriptor, + miniscript::{ + bitcoin::util::bip32::{DerivationPath, Fingerprint}, + descriptor::{Descriptor, DescriptorPublicKey, DescriptorXKey, Wildcard}, + }, +}; + +use crate::{ + hw::{list_hardware_wallets, HardwareWallet}, + installer::{ + config, + message::{self, Message}, + step::{Context, Step}, + view, Error, + }, + ui::component::form, +}; + +pub struct DefineDescriptor { + imported_descriptor: form::Value, + user_xpub: form::Value, + heir_xpub: form::Value, + sequence: form::Value, + modal: Option, + + error: Option, +} + +impl DefineDescriptor { + pub fn new() -> Self { + Self { + imported_descriptor: form::Value::default(), + user_xpub: form::Value::default(), + heir_xpub: form::Value::default(), + sequence: form::Value::default(), + modal: None, + error: None, + } + } +} + +impl Step for DefineDescriptor { + // form value is set as valid each time it is edited. + // Verification of the values is happening when the user click on Next button. + fn update(&mut self, message: Message) -> Command { + match message { + Message::Close => { + self.modal = None; + } + Message::DefineDescriptor(msg) => { + match msg { + message::DefineDescriptor::ImportDescriptor(desc) => { + self.imported_descriptor.value = desc; + self.imported_descriptor.valid = true; + } + message::DefineDescriptor::UserXpubEdited(xpub) => { + self.user_xpub.value = xpub; + self.user_xpub.valid = true; + self.modal = None; + } + message::DefineDescriptor::HeirXpubEdited(xpub) => { + self.heir_xpub.value = xpub; + self.heir_xpub.valid = true; + self.modal = None; + } + message::DefineDescriptor::SequenceEdited(seq) => { + self.sequence.valid = true; + if seq.is_empty() || seq.parse::().is_ok() { + self.sequence.value = seq; + } + } + message::DefineDescriptor::ImportUserHWXpub => { + let modal = GetHardwareWalletXpubModal::new(false); + let cmd = modal.load(); + self.modal = Some(modal); + return cmd; + } + message::DefineDescriptor::ImportHeirHWXpub => { + let modal = GetHardwareWalletXpubModal::new(true); + let cmd = modal.load(); + self.modal = Some(modal); + return cmd; + } + _ => { + if let Some(modal) = &mut self.modal { + return modal.update(Message::DefineDescriptor(msg)); + } + } + }; + } + _ => { + if let Some(modal) = &mut self.modal { + return modal.update(message); + } + } + }; + Command::none() + } + + fn apply(&mut self, _ctx: &mut Context, config: &mut config::Config) -> bool { + // descriptor forms for import or creation cannot be both empty or filled. + if self.imported_descriptor.value.is_empty() + == (self.user_xpub.value.is_empty() + || self.heir_xpub.value.is_empty() + || self.sequence.value.is_empty()) + { + if !self.user_xpub.value.is_empty() { + self.user_xpub.valid = DescriptorPublicKey::from_str(&self.user_xpub.value).is_ok(); + } + if !self.heir_xpub.value.is_empty() { + self.heir_xpub.valid = DescriptorPublicKey::from_str(&self.heir_xpub.value).is_ok(); + } + if !self.sequence.value.is_empty() { + self.sequence.valid = self.sequence.value.parse::().is_ok(); + } + if !self.imported_descriptor.value.is_empty() { + self.imported_descriptor.valid = + Descriptor::::from_str(&self.imported_descriptor.value) + .is_ok(); + } + false + } else if !self.imported_descriptor.value.is_empty() { + if let Ok(desc) = InheritanceDescriptor::from_str(&self.imported_descriptor.value) { + config.main_descriptor = Some(desc); + true + } else { + self.imported_descriptor.valid = false; + false + } + } else { + let user_key = DescriptorPublicKey::from_str(&self.user_xpub.value); + self.user_xpub.valid = user_key.is_ok(); + + let heir_key = DescriptorPublicKey::from_str(&self.heir_xpub.value); + self.user_xpub.valid = user_key.is_ok(); + + let sequence = self.sequence.value.parse::(); + self.sequence.valid = sequence.is_ok(); + + if !self.user_xpub.valid || !self.heir_xpub.valid || !self.sequence.valid { + return false; + } + + match InheritanceDescriptor::new( + user_key.unwrap(), + heir_key.unwrap(), + sequence.unwrap(), + ) { + Ok(desc) => { + config.main_descriptor = Some(desc); + true + } + Err(e) => { + self.error = Some(e.to_string()); + false + } + } + } + } + + fn view(&self) -> Element { + if let Some(modal) = &self.modal { + modal.view() + } else { + view::define_descriptor( + &self.imported_descriptor, + &self.user_xpub, + &self.heir_xpub, + &self.sequence, + self.error.as_ref(), + ) + } + } +} + +impl Default for DefineDescriptor { + fn default() -> Self { + Self::new() + } +} + +impl From for Box { + fn from(s: DefineDescriptor) -> Box { + Box::new(s) + } +} + +pub struct GetHardwareWalletXpubModal { + is_heir: bool, + chosen_hw: Option, + processing: bool, + hws: Vec, + error: Option, +} + +impl GetHardwareWalletXpubModal { + fn new(is_heir: bool) -> Self { + Self { + is_heir, + chosen_hw: None, + processing: false, + hws: Vec::new(), + error: None, + } + } + fn load(&self) -> Command { + Command::perform(list_hardware_wallets(), Message::ConnectedHardwareWallets) + } + 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(); + self.chosen_hw = Some(i); + self.processing = true; + return Command::perform(get_extended_pubkey(device, hw.fingerprint), |res| { + Message::DefineDescriptor(message::DefineDescriptor::XpubImported( + res.map(|key| key.to_string()), + )) + }); + } + } + Message::ConnectedHardwareWallets(hws) => { + self.hws = hws; + } + Message::Reload => { + return self.load(); + } + Message::DefineDescriptor(message::DefineDescriptor::XpubImported(res)) => { + self.processing = false; + match res { + Ok(key) => { + if self.is_heir { + return Command::perform( + async move { key }, + message::DefineDescriptor::HeirXpubEdited, + ) + .map(Message::DefineDescriptor); + } else { + return Command::perform( + async move { key }, + message::DefineDescriptor::UserXpubEdited, + ) + .map(Message::DefineDescriptor); + } + } + Err(e) => { + self.error = Some(e); + } + } + } + _ => {} + }; + Command::none() + } + fn view(&self) -> Element { + view::hardware_wallet_xpubs_modal( + self.is_heir, + &self.hws, + self.error.as_ref(), + self.processing, + self.chosen_hw, + ) + } +} + +async fn get_extended_pubkey( + hw: std::sync::Arc, + fingerprint: Fingerprint, +) -> Result { + let derivation_path = DerivationPath::master(); + let xkey = hw + .get_extended_pubkey(&derivation_path, true) + .await + .map_err(Error::from)?; + Ok(DescriptorPublicKey::XPub(DescriptorXKey { + origin: Some((fingerprint, derivation_path)), + derivation_path: DerivationPath::master(), + xkey, + wildcard: Wildcard::Unhardened, + })) +} diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index 931ff224..20d55249 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -1,14 +1,11 @@ +mod descriptor; +pub use descriptor::DefineDescriptor; + use std::path::PathBuf; use std::str::FromStr; -use iced::pure::Element; -use minisafe::{ - descriptors::InheritanceDescriptor, - miniscript::{ - bitcoin, - descriptor::{Descriptor, DescriptorPublicKey}, - }, -}; +use iced::{pure::Element, Command}; +use minisafe::miniscript::bitcoin; use crate::ui::component::form; @@ -19,7 +16,9 @@ use crate::installer::{ }; pub trait Step { - fn update(&mut self, message: Message); + fn update(&mut self, _message: Message) -> Command { + Command::none() + } fn view(&self) -> Element; fn load_context(&mut self, _ctx: &Context) {} fn skip(&self, _ctx: &Context) -> bool { @@ -58,10 +57,11 @@ impl Welcome { } impl Step for Welcome { - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command { if let message::Message::Network(network) = message { self.network = network; } + Command::none() } fn apply(&mut self, ctx: &mut Context, config: &mut config::Config) -> bool { ctx.network = self.network; @@ -85,138 +85,6 @@ impl From for Box { } } -pub struct DefineDescriptor { - imported_descriptor: form::Value, - user_xpub: form::Value, - heir_xpub: form::Value, - sequence: form::Value, - error: Option, -} - -impl DefineDescriptor { - pub fn new() -> Self { - Self { - imported_descriptor: form::Value::default(), - user_xpub: form::Value::default(), - heir_xpub: form::Value::default(), - sequence: form::Value::default(), - error: None, - } - } -} - -impl Step for DefineDescriptor { - // form value is set as valid each time it is edited. - // Verification of the values is happening when the user click on Next button. - fn update(&mut self, message: Message) { - if let Message::DefineDescriptor(msg) = message { - match msg { - message::DefineDescriptor::ImportDescriptor(desc) => { - self.imported_descriptor.value = desc; - self.imported_descriptor.valid = true; - } - message::DefineDescriptor::UserXpubEdited(xpub) => { - self.user_xpub.value = xpub; - self.user_xpub.valid = true; - } - message::DefineDescriptor::HeirXpubEdited(xpub) => { - self.heir_xpub.value = xpub; - self.heir_xpub.valid = true; - } - message::DefineDescriptor::SequenceEdited(seq) => { - self.sequence.valid = true; - if seq.is_empty() || seq.parse::().is_ok() { - self.sequence.value = seq; - } - } - }; - }; - } - - fn apply(&mut self, _ctx: &mut Context, config: &mut config::Config) -> bool { - // descriptor forms for import or creation cannot be both empty or filled. - if self.imported_descriptor.value.is_empty() - == (self.user_xpub.value.is_empty() - || self.heir_xpub.value.is_empty() - || self.sequence.value.is_empty()) - { - if !self.user_xpub.value.is_empty() { - self.user_xpub.valid = DescriptorPublicKey::from_str(&self.user_xpub.value).is_ok(); - } - if !self.heir_xpub.value.is_empty() { - self.heir_xpub.valid = DescriptorPublicKey::from_str(&self.heir_xpub.value).is_ok(); - } - if !self.sequence.value.is_empty() { - self.sequence.valid = self.sequence.value.parse::().is_ok(); - } - if !self.imported_descriptor.value.is_empty() { - self.imported_descriptor.valid = - Descriptor::::from_str(&self.imported_descriptor.value) - .is_ok(); - } - false - } else if !self.imported_descriptor.value.is_empty() { - if let Ok(desc) = InheritanceDescriptor::from_str(&self.imported_descriptor.value) { - config.main_descriptor = Some(desc); - true - } else { - self.imported_descriptor.valid = false; - false - } - } else { - let user_key = DescriptorPublicKey::from_str(&self.user_xpub.value); - self.user_xpub.valid = user_key.is_ok(); - - let heir_key = DescriptorPublicKey::from_str(&self.heir_xpub.value); - self.user_xpub.valid = user_key.is_ok(); - - let sequence = self.sequence.value.parse::(); - self.sequence.valid = sequence.is_ok(); - - if !self.user_xpub.valid || !self.heir_xpub.valid || !self.sequence.valid { - return false; - } - - match InheritanceDescriptor::new( - user_key.unwrap(), - heir_key.unwrap(), - sequence.unwrap(), - ) { - Ok(desc) => { - config.main_descriptor = Some(desc); - true - } - Err(e) => { - self.error = Some(e.to_string()); - false - } - } - } - } - - fn view(&self) -> Element { - view::define_descriptor( - &self.imported_descriptor, - &self.user_xpub, - &self.heir_xpub, - &self.sequence, - self.error.as_ref(), - ) - } -} - -impl Default for DefineDescriptor { - fn default() -> Self { - Self::new() - } -} - -impl From for Box { - fn from(s: DefineDescriptor) -> Box { - Box::new(s) - } -} - pub struct DefineBitcoind { cookie_path: form::Value, address: form::Value, @@ -283,7 +151,7 @@ impl Step for DefineBitcoind { self.address.value = bitcoind_default_address(&ctx.network); } } - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command { if let Message::DefineBitcoind(msg) = message { match msg { message::DefineBitcoind::AddressEdited(address) => { @@ -296,6 +164,7 @@ impl Step for DefineBitcoind { } }; }; + Command::none() } fn apply(&mut self, _ctx: &mut Context, config: &mut config::Config) -> bool { @@ -358,7 +227,7 @@ impl Final { } impl Step for Final { - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command { match message { Message::Installed(res) => { self.generating = false; @@ -377,6 +246,7 @@ impl Step for Final { } _ => {} }; + Command::none() } fn view(&self) -> Element { diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 58a6bdf2..ba176938 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -1,18 +1,25 @@ -use iced::pure::{column, container, pick_list, row, scrollable, Element}; +use iced::pure::{column, container, pick_list, row, scrollable, widget, Element}; use iced::{Alignment, Length}; use minisafe::miniscript::bitcoin; -use crate::ui::{ - component::{ - button, form, - text::{text, Text}, +use crate::{ + hw::HardwareWallet, + installer::{ + message::{self, Message}, + Error, + }, + ui::{ + color, + component::{ + button, card, form, + text::{text, Text}, + }, + icon, + util::Collection, }, - util::Collection, }; -use crate::installer::message::{self, Message}; - const NETWORKS: [bitcoin::Network; 4] = [ bitcoin::Network::Bitcoin, bitcoin::Network::Testnet, @@ -66,36 +73,55 @@ pub fn define_descriptor<'a>( let col_user_xpub = column() .push(text("Your xpub:").bold()) .push( - form::Form::new("Xpub", user_xpub, |msg| { - Message::DefineDescriptor(message::DefineDescriptor::UserXpubEdited(msg)) - }) - .warning("Please enter correct xpub") - .size(20) - .padding(10), + row() + .push( + form::Form::new("Xpub", user_xpub, |msg| { + Message::DefineDescriptor(message::DefineDescriptor::UserXpubEdited(msg)) + }) + .warning("Please enter correct xpub") + .size(20) + .padding(10), + ) + .push(button::primary(Some(icon::chip_icon()), "Import").on_press( + Message::DefineDescriptor(message::DefineDescriptor::ImportUserHWXpub), + )) + .spacing(5) + .align_items(Alignment::Center), ) .spacing(10); let col_heir_xpub = column() .push(text("Heir xpub:").bold()) .push( - form::Form::new("Xpub", heir_xpub, |msg| { - Message::DefineDescriptor(message::DefineDescriptor::HeirXpubEdited(msg)) - }) - .warning("Please enter correct xpub") - .size(20) - .padding(10), + row() + .push( + form::Form::new("Xpub", heir_xpub, |msg| { + Message::DefineDescriptor(message::DefineDescriptor::HeirXpubEdited(msg)) + }) + .warning("Please enter correct xpub") + .size(20) + .padding(10), + ) + .push(button::primary(Some(icon::chip_icon()), "Import").on_press( + Message::DefineDescriptor(message::DefineDescriptor::ImportHeirHWXpub), + )) + .spacing(5) + .align_items(Alignment::Center), ) .spacing(10); let col_sequence = column() - .push(text("Number of block").bold()) + .push(text("Number of block:").bold()) .push( - form::Form::new("Number of block", sequence, |msg| { - Message::DefineDescriptor(message::DefineDescriptor::SequenceEdited(msg)) - }) - .warning("Please enter correct block number") - .size(20) - .padding(10), + container( + form::Form::new("Number of block", sequence, |msg| { + Message::DefineDescriptor(message::DefineDescriptor::SequenceEdited(msg)) + }) + .warning("Please enter correct block number") + .size(20) + .padding(10), + ) + .width(Length::Units(150)), ) .spacing(10); @@ -105,12 +131,8 @@ pub fn define_descriptor<'a>( .push( column() .push(col_user_xpub) - .push( - row() - .push(col_sequence.width(Length::FillPortion(1))) - .push(col_heir_xpub.width(Length::FillPortion(4))) - .spacing(20), - ) + .push(col_sequence) + .push(col_heir_xpub) .spacing(20), ) .push(text("or import it").bold().size(25)) @@ -128,7 +150,7 @@ pub fn define_descriptor<'a>( .on_press(Message::Next) }, ) - .push_maybe(error.map(|e| text(e).size(15))) + .push_maybe(error.map(|e| card::error("Failed to create descriptor", e))) .width(Length::Fill) .height(Length::Fill) .padding(100) @@ -233,6 +255,92 @@ pub fn install<'a>( layout(col) } +pub fn hardware_wallet_xpubs_modal<'a>( + is_heir: bool, + hws: &[HardwareWallet], + error: Option<&Error>, + processing: bool, + chosen_hw: Option, +) -> Element<'a, Message> { + modal( + column() + .push( + text(if is_heir { + "Import the Heir xpub" + } else { + "Import the user xpub" + }) + .bold() + .size(50), + ) + .push_maybe(error.map(|e| card::error("Failed to import xpub", &e.to_string()))) + .push(if !hws.is_empty() { + column() + .push(text(&format!("{} hardware wallets connected", hws.len())).bold()) + .spacing(10) + .push( + hws.iter() + .enumerate() + .fold(column().spacing(10), |col, (i, hw)| { + col.push(hw_list_view(i, hw, Some(i) == chosen_hw, processing)) + }), + ) + .width(Length::Fill) + } else { + column().push(card::simple( + column() + .spacing(10) + .push("Please connect a hardware wallet") + .push(button::primary(None, "Refresh").on_press(Message::Reload)) + .align_items(Alignment::Center), + )) + }) + .width(Length::Fill) + .height(Length::Fill) + .padding(100) + .spacing(50) + .align_items(Alignment::Center), + ) +} + +fn hw_list_view<'a>( + i: usize, + hw: &HardwareWallet, + chosen: bool, + processing: bool, +) -> Element<'a, Message> { + let mut bttn = iced::pure::button( + row() + .push( + column() + .push(text(&format!("{}", hw.kind)).bold()) + .push(text(&format!("fingerprint: {}", hw.fingerprint)).small()) + .spacing(5) + .width(Length::Fill), + ) + .push_maybe(if chosen && processing { + Some( + column() + .push(text("Processing...")) + .push(text("Please check your device").small()), + ) + } else { + None + }) + .width(Length::Fill), + ) + .padding(10) + .style(button::Style::TransparentBorder) + .width(Length::Fill); + if !processing { + bttn = bttn.on_press(Message::Select(i)); + } + container(bttn) + .width(Length::Fill) + .style(card::SimpleCardStyle) + .into() +} + fn layout<'a>(content: impl Into>) -> Element<'a, Message> { container(scrollable( column() @@ -247,3 +355,33 @@ fn layout<'a>(content: impl Into>) -> Element<'a, Message> .width(Length::Fill) .into() } + +fn modal<'a>(content: impl Into>) -> Element<'a, Message> { + container(scrollable( + column() + .push( + row().push(column().width(Length::Fill)).push( + container( + button::primary(Some(icon::cross_icon()), "Close").on_press(Message::Close), + ) + .padding(10), + ), + ) + .push(container(content).width(Length::Fill).center_x()), + )) + .center_x() + .height(Length::Fill) + .width(Length::Fill) + .style(ModalStyle) + .into() +} + +pub struct ModalStyle; +impl widget::container::StyleSheet for ModalStyle { + fn style(&self) -> widget::container::Style { + widget::container::Style { + background: color::BACKGROUND.into(), + ..widget::container::Style::default() + } + } +} diff --git a/gui/src/lib.rs b/gui/src/lib.rs index 883f95ce..66125b3f 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -1,5 +1,6 @@ pub mod app; pub mod daemon; +pub mod hw; pub mod installer; pub mod loader; pub mod ui; diff --git a/gui/src/ui/component/card.rs b/gui/src/ui/component/card.rs index 87d57dc3..dccc26ee 100644 --- a/gui/src/ui/component/card.rs +++ b/gui/src/ui/component/card.rs @@ -1,6 +1,6 @@ -use iced::pure::{container, widget, Element}; +use iced::pure::{container, row, tooltip, widget, Element}; -use crate::ui::color; +use crate::ui::{color, component::text::text, icon}; pub fn simple<'a, T: 'a, C: Into>>(content: C) -> widget::Container<'a, T> { container(content).padding(15).style(SimpleCardStyle) @@ -16,3 +16,34 @@ impl widget::container::StyleSheet for SimpleCardStyle { } } } + +/// display an error card with the message and the error in a tooltip. +pub fn error<'a, T: 'a>(message: &str, error: &str) -> widget::Container<'a, T> { + container( + tooltip( + row() + .spacing(20) + .align_items(iced::Alignment::Center) + .push(icon::block_icon().color(color::ALERT)) + .push(text(message).color(color::ALERT)), + error, + widget::tooltip::Position::Bottom, + ) + .style(ErrorCardStyle), + ) + .padding(15) + .style(ErrorCardStyle) +} + +pub struct ErrorCardStyle; +impl widget::container::StyleSheet for ErrorCardStyle { + fn style(&self) -> widget::container::Style { + widget::container::Style { + border_radius: 10.0, + border_color: color::ALERT, + border_width: 1.5, + background: color::FOREGROUND.into(), + ..widget::container::Style::default() + } + } +} diff --git a/gui/src/ui/icon.rs b/gui/src/ui/icon.rs index f06196c8..52e27587 100644 --- a/gui/src/ui/icon.rs +++ b/gui/src/ui/icon.rs @@ -134,6 +134,10 @@ pub fn warning_icon() -> Text { icon('\u{F33B}') } +pub fn chip_icon() -> Text { + icon('\u{F2D6}') +} + pub fn trash_icon() -> Text { icon('\u{F5DE}') } From 56be997d6bdbecc4d8c5ae7cded322c6c6936ed5 Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 24 Oct 2022 21:56:57 +0200 Subject: [PATCH 2/7] gui: fix ci add missing deps --- .github/workflows/main.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c02e3c7..3b7e9e9f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -85,5 +85,8 @@ jobs: if: matrix.os == 'windows-latest' run: cd gui && cargo test --verbose --no-default-features - name: Test on Rust ${{ matrix.toolchain }} (non Windows) - if: matrix.os != 'windows-latest' + if: matrix.os == 'macOS-latest' run: cd gui && cargo test --verbose --color always -- --nocapture + - name: Test on Rust ${{ matrix.toolchain }} (non Windows) + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get update & sudo apt-get install --allow-downgrades libudev1=245.4-4ubuntu3 libudev-dev=245.4-4ubuntu3 pkg-config libxkbcommon-dev libvulkan-dev && cd gui && cargo test --verbose --color always -- --nocapture From a2021ca326d614d7a604e3e6cad9f568e29a2d20 Mon Sep 17 00:00:00 2001 From: edouard Date: Tue, 25 Oct 2022 15:52:52 +0200 Subject: [PATCH 3/7] installer: refac context --- gui/src/installer/config.rs | 74 ++++------------------------ gui/src/installer/mod.rs | 29 +++++------ gui/src/installer/step/descriptor.rs | 19 ++++--- gui/src/installer/step/mod.rs | 51 +++++++++++-------- 4 files changed, 62 insertions(+), 111 deletions(-) diff --git a/gui/src/installer/config.rs b/gui/src/installer/config.rs index 9ee70f65..5d5ec8fb 100644 --- a/gui/src/installer/config.rs +++ b/gui/src/installer/config.rs @@ -1,80 +1,26 @@ use std::convert::TryFrom; -use minisafe::{ - config::{BitcoinConfig, BitcoindConfig, Config as MinisafeConfig}, - descriptors::InheritanceDescriptor, - miniscript::bitcoin::Network, -}; +use minisafe::config::Config as MinisafeConfig; -use serde::Serialize; -use std::{net::SocketAddr, path::PathBuf, time::Duration}; +use super::step::Context; -/// Static informations we require to operate -/// fields with default values are not present, see minisafe::config. -#[derive(Debug, Clone, Serialize)] -pub struct Config { - #[serde(serialize_with = "serialize_option_to_string")] - pub main_descriptor: Option, - pub bitcoin_config: BitcoinConfig, - /// Everything we need to know to talk to bitcoind - pub bitcoind_config: BitcoindConfig, - /// An optional custom data directory - pub data_dir: Option, -} +pub const DEFAULT_FILE_NAME: &str = "daemon.toml"; -impl Config { - pub const DEFAULT_FILE_NAME: &'static str = "daemon.toml"; - /// returns a minisafed config with empty or dummy values - pub fn new() -> Config { - Self { - main_descriptor: None, - bitcoin_config: BitcoinConfig { - network: Network::Bitcoin, - poll_interval_secs: Duration::from_secs(30), - }, - bitcoind_config: BitcoindConfig { - cookie_path: PathBuf::new(), - addr: SocketAddr::new( - std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), - 8080, - ), - }, - data_dir: None, - } - } -} - -impl Default for Config { - fn default() -> Self { - Self::new() - } -} - -pub fn serialize_option_to_string( - field: &Option, - s: S, -) -> Result { - match field { - Some(field) => s.serialize_str(&field.to_string()), - None => s.serialize_none(), - } -} - -impl TryFrom for MinisafeConfig { +impl TryFrom for MinisafeConfig { type Error = &'static str; - fn try_from(cfg: Config) -> Result { - if cfg.main_descriptor.is_none() { + fn try_from(ctx: Context) -> Result { + if ctx.descriptor.is_none() { return Err("config does not have a main Descriptor"); } Ok(MinisafeConfig { #[cfg(unix)] daemon: false, log_level: log::LevelFilter::Info, - main_descriptor: cfg.main_descriptor.unwrap(), - data_dir: cfg.data_dir, - bitcoin_config: cfg.bitcoin_config, - bitcoind_config: Some(cfg.bitcoind_config), + main_descriptor: ctx.descriptor.unwrap(), + data_dir: ctx.data_dir, + bitcoin_config: ctx.bitcoin_config, + bitcoind_config: ctx.bitcoind_config, }) } } diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index b67e947a..1bc8a2f1 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -12,7 +12,7 @@ use std::convert::TryInto; use std::io::Write; use std::path::PathBuf; -use crate::{app::config as gui_config, installer::config::Config as DaemonConfig}; +use crate::{app::config as gui_config, installer::config::DEFAULT_FILE_NAME}; pub use message::Message; use step::{Context, DefineBitcoind, DefineDescriptor, Final, Step, Welcome}; @@ -24,7 +24,6 @@ pub struct Installer { /// Context is data passed through each step. context: Context, - config: DaemonConfig, } impl Installer { @@ -44,12 +43,9 @@ impl Installer { destination_path: PathBuf, network: bitcoin::Network, ) -> (Installer, Command) { - let mut config = DaemonConfig::new(); - config.data_dir = Some(destination_path); ( Installer { should_exit: false, - config, current: 0, steps: vec![ Welcome::new(network).into(), @@ -57,7 +53,7 @@ impl Installer { DefineBitcoind::new().into(), Final::new().into(), ], - context: Context::new(network), + context: Context::new(network, Some(destination_path)), }, Command::none(), ) @@ -82,7 +78,7 @@ impl Installer { .steps .get_mut(self.current) .expect("There is always a step"); - if current_step.apply(&mut self.context, &mut self.config) { + if current_step.apply(&mut self.context) { self.next(); // skip the step according to the current context. while self @@ -111,10 +107,7 @@ impl Installer { .get_mut(self.current) .expect("There is always a step") .update(message); - Command::perform( - install(self.context.clone(), self.config.clone()), - Message::Installed, - ) + Command::perform(install(self.context.clone()), Message::Installed) } Message::Event(Event::Window(window::Event::CloseRequested)) => { self.stop(); @@ -136,12 +129,14 @@ impl Installer { } } -pub async fn install(_ctx: Context, mut cfg: DaemonConfig) -> Result { +pub async fn install(ctx: Context) -> Result { + let mut cfg: minisafe::config::Config = ctx + .try_into() + .expect("Everything should be checked at this point"); // Start Daemon to check correctness of installation - let daemon = - minisafe::DaemonHandle::start_default(cfg.clone().try_into().unwrap()).map_err(|e| { - Error::Unexpected(format!("Failed to start daemon with entered config: {}", e)) - })?; + let daemon = minisafe::DaemonHandle::start_default(cfg.clone()).map_err(|e| { + Error::Unexpected(format!("Failed to start daemon with entered config: {}", e)) + })?; daemon.shutdown(); cfg.data_dir = @@ -154,7 +149,7 @@ pub async fn install(_ctx: Context, mut cfg: DaemonConfig) -> Result bool { + fn apply(&mut self, ctx: &mut Context) -> bool { // descriptor forms for import or creation cannot be both empty or filled. if self.imported_descriptor.value.is_empty() == (self.user_xpub.value.is_empty() @@ -125,7 +124,7 @@ impl Step for DefineDescriptor { false } else if !self.imported_descriptor.value.is_empty() { if let Ok(desc) = InheritanceDescriptor::from_str(&self.imported_descriptor.value) { - config.main_descriptor = Some(desc); + ctx.descriptor = Some(desc); true } else { self.imported_descriptor.valid = false; @@ -145,20 +144,20 @@ impl Step for DefineDescriptor { return false; } - match InheritanceDescriptor::new( + let desc = match InheritanceDescriptor::new( user_key.unwrap(), heir_key.unwrap(), sequence.unwrap(), ) { - Ok(desc) => { - config.main_descriptor = Some(desc); - true - } + Ok(desc) => desc, Err(e) => { self.error = Some(e.to_string()); - false + return false; } - } + }; + + ctx.descriptor = Some(desc); + true } } diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index 20d55249..91955994 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -3,14 +3,18 @@ pub use descriptor::DefineDescriptor; use std::path::PathBuf; use std::str::FromStr; +use std::time::Duration; use iced::{pure::Element, Command}; -use minisafe::miniscript::bitcoin; +use minisafe::{ + config::{BitcoinConfig, BitcoindConfig}, + descriptors::InheritanceDescriptor, + miniscript::bitcoin, +}; use crate::ui::component::form; use crate::installer::{ - config, message::{self, Message}, view, }; @@ -24,25 +28,30 @@ pub trait Step { fn skip(&self, _ctx: &Context) -> bool { false } - fn apply(&mut self, _ctx: &mut Context, _config: &mut config::Config) -> bool { + fn apply(&mut self, _ctx: &mut Context) -> bool { true } } #[derive(Clone)] pub struct Context { - pub network: bitcoin::Network, + pub bitcoin_config: BitcoinConfig, + pub bitcoind_config: Option, + pub descriptor: Option, + pub data_dir: Option, } impl Context { - pub fn new(network: bitcoin::Network) -> Self { - Self { network } - } -} - -impl Default for Context { - fn default() -> Self { - Self::new(bitcoin::Network::Bitcoin) + pub fn new(network: bitcoin::Network, data_dir: Option) -> Self { + Self { + bitcoin_config: BitcoinConfig { + network, + poll_interval_secs: Duration::from_secs(30), + }, + bitcoind_config: None, + descriptor: None, + data_dir, + } } } @@ -63,9 +72,8 @@ impl Step for Welcome { } Command::none() } - fn apply(&mut self, ctx: &mut Context, config: &mut config::Config) -> bool { - ctx.network = self.network; - config.bitcoin_config.network = self.network; + fn apply(&mut self, ctx: &mut Context) -> bool { + ctx.bitcoin_config.network = self.network; true } fn view(&self) -> Element { @@ -145,10 +153,11 @@ impl DefineBitcoind { impl Step for DefineBitcoind { fn load_context(&mut self, ctx: &Context) { if self.cookie_path.value.is_empty() { - self.cookie_path.value = bitcoind_default_cookie_path(&ctx.network).unwrap_or_default() + self.cookie_path.value = + bitcoind_default_cookie_path(&ctx.bitcoin_config.network).unwrap_or_default() } if self.address.value.is_empty() { - self.address.value = bitcoind_default_address(&ctx.network); + self.address.value = bitcoind_default_address(&ctx.bitcoin_config.network); } } fn update(&mut self, message: Message) -> Command { @@ -167,7 +176,7 @@ impl Step for DefineBitcoind { Command::none() } - fn apply(&mut self, _ctx: &mut Context, config: &mut config::Config) -> bool { + fn apply(&mut self, ctx: &mut Context) -> bool { match ( PathBuf::from_str(&self.cookie_path.value), std::net::SocketAddr::from_str(&self.address.value), @@ -186,8 +195,10 @@ impl Step for DefineBitcoind { false } (Ok(path), Ok(addr)) => { - config.bitcoind_config.cookie_path = path; - config.bitcoind_config.addr = addr; + ctx.bitcoind_config = Some(BitcoindConfig { + cookie_path: path, + addr, + }); true } } From c2ed30961c4c49826e97f6ab59eb0952ef1a9d2c Mon Sep 17 00:00:00 2001 From: edouard Date: Tue, 25 Oct 2022 17:23:19 +0200 Subject: [PATCH 4/7] installer: register wallet --- gui/Cargo.lock | 6 +- gui/src/installer/message.rs | 5 +- gui/src/installer/mod.rs | 4 +- gui/src/installer/step/descriptor.rs | 100 +++++++++++++++++++++++++++ gui/src/installer/step/mod.rs | 5 +- gui/src/installer/view.rs | 95 ++++++++++++++++++++++--- 6 files changed, 197 insertions(+), 18 deletions(-) diff --git a/gui/Cargo.lock b/gui/Cargo.lock index 281ff269..a4833a42 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -125,7 +125,7 @@ dependencies = [ [[package]] name = "async-hwi" version = "0.0.1" -source = "git+https://github.com/revault/async-hwi?branch=add-ledger#0eace00eca1ece4e4c49f2cd4d6fbffa2d527ce5" +source = "git+https://github.com/revault/async-hwi?branch=add-ledger#de5615ba2e2e2d3ded6a058f0193f8787d3873dc" dependencies = [ "async-trait", "base64", @@ -1380,7 +1380,7 @@ dependencies = [ [[package]] name = "ledger_bitcoin_client" version = "0.1.0" -source = "git+https://github.com/edouardparis/app-bitcoin-new/?branch=bitcoin_client_rs#4f7951ce7c464db80f241ca8cd46c45770d6eec7" +source = "git+https://github.com/edouardparis/app-bitcoin-new/?branch=bitcoin_client_rs#95ec68b546d49a77eee274880d2450fdb08f840c" dependencies = [ "async-trait", "bitcoin", @@ -2725,7 +2725,7 @@ version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 0.1.10", "rand 0.8.5", "static_assertions", ] diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 1ac412c7..291c144e 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -1,4 +1,4 @@ -use minisafe::miniscript::bitcoin; +use minisafe::miniscript::bitcoin::{util::bip32::Fingerprint, Network}; use std::path::PathBuf; use super::Error; @@ -15,10 +15,11 @@ pub enum Message { Reload, Select(usize), Installed(Result), - Network(bitcoin::Network), + Network(Network), DefineBitcoind(DefineBitcoind), DefineDescriptor(DefineDescriptor), ConnectedHardwareWallets(Vec), + WalletRegistered(Result<(Fingerprint, Option<[u8; 32]>), Error>), } #[derive(Debug, Clone)] diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 1bc8a2f1..6199d624 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -15,7 +15,7 @@ use std::path::PathBuf; use crate::{app::config as gui_config, installer::config::DEFAULT_FILE_NAME}; pub use message::Message; -use step::{Context, DefineBitcoind, DefineDescriptor, Final, Step, Welcome}; +use step::{Context, DefineBitcoind, DefineDescriptor, Final, RegisterDescriptor, Step, Welcome}; pub struct Installer { should_exit: bool, @@ -50,6 +50,7 @@ impl Installer { steps: vec![ Welcome::new(network).into(), DefineDescriptor::new().into(), + RegisterDescriptor::default().into(), DefineBitcoind::new().into(), Final::new().into(), ], @@ -95,6 +96,7 @@ impl Installer { .get_mut(self.current) .expect("There is always a step"); current_step.load_context(&self.context); + return current_step.load(); } Command::none() } diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index 33bfe555..fb409948 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -283,3 +283,103 @@ async fn get_extended_pubkey( wildcard: Wildcard::Unhardened, })) } + +#[derive(Default)] +pub struct RegisterDescriptor { + descriptor: Option, + processing: bool, + chosen_hw: Option, + hws: Vec<(HardwareWallet, Option<[u8; 32]>)>, + error: Option, +} + +impl Step for RegisterDescriptor { + fn load_context(&mut self, ctx: &Context) { + self.descriptor = ctx.descriptor.clone(); + } + fn update(&mut self, message: Message) -> Command { + match message { + Message::Select(i) => { + if let Some((hw, 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), + Message::WalletRegistered, + ); + } + } + } + Message::WalletRegistered(res) => { + self.processing = false; + self.chosen_hw = None; + match res { + Ok((fingerprint, hmac)) => { + if let Some(hw_h) = self + .hws + .iter_mut() + .find(|hw_h| hw_h.0.fingerprint == fingerprint) + { + hw_h.1 = Some(hmac.unwrap_or([0x00; 32])); + } + } + Err(e) => self.error = Some(e), + } + } + Message::ConnectedHardwareWallets(hws) => { + for hw in hws { + if !self + .hws + .iter() + .any(|(h, _)| h.fingerprint == hw.fingerprint) + { + self.hws.push((hw, None)); + } + } + } + Message::Reload => { + return self.load(); + } + _ => {} + }; + Command::none() + } + fn apply(&mut self, ctx: &mut Context) -> bool { + true + } + fn load(&self) -> Command { + Command::perform(list_hardware_wallets(), Message::ConnectedHardwareWallets) + } + fn view(&self) -> Element { + let desc = self.descriptor.as_ref().unwrap(); + view::register_descriptor( + &desc.to_string(), + &self.hws, + self.error.as_ref(), + self.processing, + self.chosen_hw, + ) + } +} + +async fn register_wallet( + hw: std::sync::Arc, + fingerprint: Fingerprint, + descriptor: String, +) -> Result<(Fingerprint, Option<[u8; 32]>), Error> { + let hmac = hw + .register_wallet("Minisafe", &descriptor) + .await + .map_err(Error::from)?; + Ok((fingerprint, hmac)) +} + +impl From for Box { + fn from(s: RegisterDescriptor) -> Box { + Box::new(s) + } +} diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index 91955994..e9e738d8 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -1,5 +1,5 @@ mod descriptor; -pub use descriptor::DefineDescriptor; +pub use descriptor::{DefineDescriptor, RegisterDescriptor}; use std::path::PathBuf; use std::str::FromStr; @@ -25,6 +25,9 @@ pub trait Step { } fn view(&self) -> Element; fn load_context(&mut self, _ctx: &Context) {} + fn load(&self) -> Command { + Command::none() + } fn skip(&self, _ctx: &Context) -> bool { false } diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index ba176938..ed057902 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -159,6 +159,59 @@ pub fn define_descriptor<'a>( ) } +pub fn register_descriptor<'a>( + descriptor: &str, + hws: &[(HardwareWallet, Option<[u8; 32]>)], + error: Option<&Error>, + processing: bool, + chosen_hw: Option, +) -> Element<'a, Message> { + layout( + column() + .push(text("Register descriptor").bold().size(50)) + .push(text(descriptor).small()) + .push_maybe(error.map(|e| card::error("Failed to import xpub", &e.to_string()))) + .push(if !hws.is_empty() { + column() + .push(text(&format!("{} hardware wallets connected", hws.len())).bold()) + .spacing(10) + .push( + hws.iter() + .enumerate() + .fold(column().spacing(10), |col, (i, hw)| { + col.push(hw_list_view( + i, + &hw.0, + Some(i) == chosen_hw, + processing, + hw.1.is_some(), + )) + }), + ) + .width(Length::Fill) + } else { + column().push(card::simple( + column() + .spacing(20) + .push("No hardware wallet connected") + .push(button::primary(None, "Refresh").on_press(Message::Reload)) + .align_items(Alignment::Center) + .width(Length::Fill), + )) + }) + .push( + button::primary(None, "Next") + .on_press(Message::Next) + .width(Length::Units(200)), + ) + .width(Length::Fill) + .height(Length::Fill) + .padding(100) + .spacing(50) + .align_items(Alignment::Center), + ) +} + pub fn define_bitcoin<'a>( address: &form::Value, cookie_path: &form::Value, @@ -282,18 +335,30 @@ pub fn hardware_wallet_xpubs_modal<'a>( hws.iter() .enumerate() .fold(column().spacing(10), |col, (i, hw)| { - col.push(hw_list_view(i, hw, Some(i) == chosen_hw, processing)) + col.push(hw_list_view( + i, + hw, + Some(i) == chosen_hw, + processing, + false, + )) }), ) .width(Length::Fill) } else { - column().push(card::simple( - column() - .spacing(10) - .push("Please connect a hardware wallet") - .push(button::primary(None, "Refresh").on_press(Message::Reload)) - .align_items(Alignment::Center), - )) + column() + .push( + card::simple( + column() + .spacing(20) + .width(Length::Fill) + .push("Please connect a hardware wallet") + .push(button::primary(None, "Refresh").on_press(Message::Reload)) + .align_items(Alignment::Center), + ) + .width(Length::Fill), + ) + .width(Length::Fill) }) .width(Length::Fill) .height(Length::Fill) @@ -308,6 +373,7 @@ fn hw_list_view<'a>( hw: &HardwareWallet, chosen: bool, processing: bool, + registered: bool, ) -> Element<'a, Message> { let mut bttn = iced::pure::button( row() @@ -327,6 +393,12 @@ fn hw_list_view<'a>( } else { None }) + .push_maybe(if registered { + Some(column().push(icon::circle_check_icon().color(color::SUCCESS))) + } else { + None + }) + .align_items(Alignment::Center) .width(Length::Fill), ) .padding(10) @@ -353,6 +425,7 @@ fn layout<'a>(content: impl Into>) -> Element<'a, Message> .center_x() .height(Length::Fill) .width(Length::Fill) + .style(BackgroundStyle) .into() } @@ -372,12 +445,12 @@ fn modal<'a>(content: impl Into>) -> Element<'a, Message> { .center_x() .height(Length::Fill) .width(Length::Fill) - .style(ModalStyle) + .style(BackgroundStyle) .into() } -pub struct ModalStyle; -impl widget::container::StyleSheet for ModalStyle { +pub struct BackgroundStyle; +impl widget::container::StyleSheet for BackgroundStyle { fn style(&self) -> widget::container::Style { widget::container::Style { background: color::BACKGROUND.into(), From 6500381059c232023c29eb251e66cdb68bbe8148 Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 26 Oct 2022 13:16:35 +0200 Subject: [PATCH 5/7] installer: add clipboard --- gui/src/installer/message.rs | 1 + gui/src/installer/mod.rs | 3 ++- gui/src/installer/step/descriptor.rs | 2 +- gui/src/installer/view.rs | 11 ++++++++++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 291c144e..0c8a5ba2 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -8,6 +8,7 @@ use crate::hw::HardwareWallet; pub enum Message { Event(iced_native::Event), Exit(PathBuf), + Clibpboard(String), Next, Previous, Install, diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 6199d624..f75a9304 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -4,7 +4,7 @@ mod step; mod view; use iced::pure::Element; -use iced::{Command, Subscription}; +use iced::{clipboard, Command, Subscription}; use iced_native::{window, Event}; use minisafe::miniscript::bitcoin; @@ -74,6 +74,7 @@ impl Installer { pub fn update(&mut self, message: Message) -> Command { match message { + Message::Clibpboard(s) => clipboard::write(s), Message::Next => { let current_step = self .steps diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index fb409948..286a63f5 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -348,7 +348,7 @@ impl Step for RegisterDescriptor { }; Command::none() } - fn apply(&mut self, ctx: &mut Context) -> bool { + fn apply(&mut self, _ctx: &mut Context) -> bool { true } fn load(&self) -> Command { diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index ed057902..93047802 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -169,7 +169,16 @@ pub fn register_descriptor<'a>( layout( column() .push(text("Register descriptor").bold().size(50)) - .push(text(descriptor).small()) + .push( + column() + .push(text(descriptor).small()) + .push( + button::transparent_border(Some(icon::clipboard_icon()), "Copy") + .on_press(Message::Clibpboard(descriptor.to_string())), + ) + .spacing(10) + .align_items(Alignment::Center), + ) .push_maybe(error.map(|e| card::error("Failed to import xpub", &e.to_string()))) .push(if !hws.is_empty() { column() From 258aeb57feba3b84e09d5bf3e863209cea06113a Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 31 Oct 2022 14:49:39 +0100 Subject: [PATCH 6/7] hw: load wallet from config --- gui/Cargo.lock | 9 ++--- gui/Cargo.toml | 2 +- gui/src/app/config.rs | 10 ++++- gui/src/hw.rs | 59 +++++++++++++++++++++++++--- gui/src/installer/mod.rs | 11 +++++- gui/src/installer/step/descriptor.rs | 36 ++++++++++++----- gui/src/installer/step/mod.rs | 7 +++- gui/src/utils/mock.rs | 2 +- 8 files changed, 110 insertions(+), 26 deletions(-) diff --git a/gui/Cargo.lock b/gui/Cargo.lock index a4833a42..ec0ec9d5 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -125,7 +125,7 @@ dependencies = [ [[package]] name = "async-hwi" version = "0.0.1" -source = "git+https://github.com/revault/async-hwi?branch=add-ledger#de5615ba2e2e2d3ded6a058f0193f8787d3873dc" +source = "git+https://github.com/revault/async-hwi?branch=master#14b29f820910132d9b8f799d030bfed69ac67239" dependencies = [ "async-trait", "base64", @@ -1380,7 +1380,7 @@ dependencies = [ [[package]] name = "ledger_bitcoin_client" version = "0.1.0" -source = "git+https://github.com/edouardparis/app-bitcoin-new/?branch=bitcoin_client_rs#95ec68b546d49a77eee274880d2450fdb08f840c" +source = "git+https://github.com/edouardparis/app-bitcoin-new/?branch=bitcoin_client_rs#efe26ed9fcfe0676c33141ad4777fd3786f7d539" dependencies = [ "async-trait", "bitcoin", @@ -1597,7 +1597,7 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "minisafe" version = "0.0.1" -source = "git+https://github.com/revault/minisafe?branch=master#790d283e77063c019f54690eb1c483562e12338c" +source = "git+https://github.com/revault/minisafe?branch=master#8b129fe3e51ac41a78c282a018e70bd86e2ab24f" dependencies = [ "backtrace", "base64", @@ -1636,8 +1636,7 @@ dependencies = [ [[package]] name = "miniscript" version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4975078076f0b7b914a3044ad7432d2a7fcec38edb855afdc672e24ca35b69" +source = "git+https://github.com/darosior/rust-miniscript?branch=multipath_descriptors_on_8.0#a63d5a263a9006b4d29342012133a3bc919765ba" dependencies = [ "bitcoin", "serde", diff --git a/gui/Cargo.toml b/gui/Cargo.toml index 1c597ec3..a8b34dcd 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -14,7 +14,7 @@ name = "minisafe-gui" path = "src/main.rs" [dependencies] -async-hwi = { git = "https://github.com/revault/async-hwi", branch = "add-ledger" } +async-hwi = { git = "https://github.com/revault/async-hwi", branch = "master" } minisafe = { git = "https://github.com/revault/minisafe", branch = "master", default-features = false } backtrace = "0.3" diff --git a/gui/src/app/config.rs b/gui/src/app/config.rs index bbfc1970..c0467ed1 100644 --- a/gui/src/app/config.rs +++ b/gui/src/app/config.rs @@ -1,3 +1,4 @@ +use crate::hw::HardwareWalletConfig; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -9,16 +10,23 @@ pub struct Config { pub log_level: Option, /// Use iced debug feature if true. pub debug: Option, + /// hardware wallets config. + #[serde(default)] + pub hardware_wallets: Vec, } pub const DEFAULT_FILE_NAME: &str = "gui.toml"; impl Config { - pub fn new(minisafed_config_path: PathBuf) -> Self { + pub fn new( + minisafed_config_path: PathBuf, + hardware_wallets: Vec, + ) -> Self { Self { minisafed_config_path, log_level: None, debug: None, + hardware_wallets, } } diff --git a/gui/src/hw.rs b/gui/src/hw.rs index cdbe26d4..ee6eb3cb 100644 --- a/gui/src/hw.rs +++ b/gui/src/hw.rs @@ -1,7 +1,12 @@ +use std::sync::Arc; + use async_hwi::{ledger, specter, DeviceKind, Error as HWIError, HWI}; use log::debug; -use minisafe::miniscript::bitcoin::util::bip32::Fingerprint; -use std::sync::Arc; +use minisafe::miniscript::bitcoin::{ + hashes::hex::{FromHex, ToHex}, + util::bip32::Fingerprint, +}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone)] pub struct HardwareWallet { @@ -22,7 +27,33 @@ impl HardwareWallet { } } -pub async fn list_hardware_wallets() -> Vec { +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct HardwareWalletConfig { + pub kind: String, + pub fingerprint: String, + pub token: String, +} + +impl HardwareWalletConfig { + pub fn new(kind: &async_hwi::DeviceKind, fingerprint: &Fingerprint, token: &[u8; 32]) -> Self { + Self { + kind: kind.to_string(), + fingerprint: fingerprint.to_string(), + token: token.to_hex(), + } + } + + fn token(&self) -> [u8; 32] { + let mut res = [0x00; 32]; + res.copy_from_slice(&Vec::from_hex(&self.token).unwrap()); + res + } +} + +pub async fn list_hardware_wallets( + cfg: &[HardwareWalletConfig], + wallet: Option<(&str, &str)>, +) -> Vec { let mut hws: Vec = Vec::new(); match specter::SpecterSimulator::try_connect().await { Ok(device) => match HardwareWallet::new(Arc::new(device)).await { @@ -49,8 +80,26 @@ pub async fn list_hardware_wallets() -> Vec { } } match ledger::LedgerSimulator::try_connect().await { - Ok(device) => match HardwareWallet::new(Arc::new(device)).await { - Ok(hw) => hws.push(hw), + Ok(mut device) => match device.get_master_fingerprint().await { + Ok(fingerprint) => { + if let Some((name, descriptor)) = wallet { + device + .load_wallet( + name, + descriptor, + cfg.iter() + .find(|cfg| cfg.fingerprint == fingerprint.to_string()) + .map(|cfg| cfg.token()), + ) + .expect("Configuration must be correct"); + } + + hws.push(HardwareWallet { + kind: device.device_kind(), + fingerprint, + device: Arc::new(device), + }); + } Err(e) => { debug!("{}", e); } diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index f75a9304..60d52124 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -12,7 +12,9 @@ use std::convert::TryInto; use std::io::Write; use std::path::PathBuf; -use crate::{app::config as gui_config, installer::config::DEFAULT_FILE_NAME}; +use crate::{ + app::config as gui_config, hw::HardwareWalletConfig, installer::config::DEFAULT_FILE_NAME, +}; pub use message::Message; use step::{Context, DefineBitcoind, DefineDescriptor, Final, RegisterDescriptor, Step, Welcome}; @@ -133,6 +135,12 @@ impl Installer { } pub async fn install(ctx: Context) -> Result { + let hardware_wallets = ctx + .hw_tokens + .iter() + .map(|(kind, fingerprint, token)| HardwareWalletConfig::new(kind, fingerprint, token)) + .collect(); + let mut cfg: minisafe::config::Config = ctx .try_into() .expect("Everything should be checked at this point"); @@ -179,6 +187,7 @@ pub async fn install(ctx: Context) -> Result { e )) })?, + hardware_wallets, )) .unwrap() .as_bytes(), diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index 286a63f5..daf02b1d 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -2,10 +2,10 @@ use std::str::FromStr; use iced::{pure::Element, Command}; use minisafe::{ - descriptors::InheritanceDescriptor, + descriptors::MultipathDescriptor, miniscript::{ bitcoin::util::bip32::{DerivationPath, Fingerprint}, - descriptor::{Descriptor, DescriptorPublicKey, DescriptorXKey, Wildcard}, + descriptor::{Descriptor, DescriptorMultiXKey, DescriptorPublicKey, Wildcard}, }, }; @@ -123,7 +123,7 @@ impl Step for DefineDescriptor { } false } else if !self.imported_descriptor.value.is_empty() { - if let Ok(desc) = InheritanceDescriptor::from_str(&self.imported_descriptor.value) { + if let Ok(desc) = MultipathDescriptor::from_str(&self.imported_descriptor.value) { ctx.descriptor = Some(desc); true } else { @@ -144,7 +144,7 @@ impl Step for DefineDescriptor { return false; } - let desc = match InheritanceDescriptor::new( + let desc = match MultipathDescriptor::new( user_key.unwrap(), heir_key.unwrap(), sequence.unwrap(), @@ -207,7 +207,10 @@ impl GetHardwareWalletXpubModal { } } fn load(&self) -> Command { - Command::perform(list_hardware_wallets(), Message::ConnectedHardwareWallets) + Command::perform( + list_hardware_wallets(&[], None), + Message::ConnectedHardwareWallets, + ) } fn update(&mut self, message: Message) -> Command { match message { @@ -276,9 +279,12 @@ async fn get_extended_pubkey( .get_extended_pubkey(&derivation_path, true) .await .map_err(Error::from)?; - Ok(DescriptorPublicKey::XPub(DescriptorXKey { + Ok(DescriptorPublicKey::MultiXPub(DescriptorMultiXKey { origin: Some((fingerprint, derivation_path)), - derivation_path: DerivationPath::master(), + derivation_paths: vec![ + DerivationPath::from_str("m/0").unwrap(), + DerivationPath::from_str("m/1").unwrap(), + ], xkey, wildcard: Wildcard::Unhardened, })) @@ -286,7 +292,7 @@ async fn get_extended_pubkey( #[derive(Default)] pub struct RegisterDescriptor { - descriptor: Option, + descriptor: Option, processing: bool, chosen_hw: Option, hws: Vec<(HardwareWallet, Option<[u8; 32]>)>, @@ -348,11 +354,21 @@ impl Step for RegisterDescriptor { }; Command::none() } - fn apply(&mut self, _ctx: &mut Context) -> bool { + fn apply(&mut self, ctx: &mut Context) -> bool { + for (hw, token) in &self.hws { + if let Some(token) = token { + if *token != [0x00; 32] { + ctx.hw_tokens.push((hw.kind, hw.fingerprint, *token)); + } + } + } true } fn load(&self) -> Command { - Command::perform(list_hardware_wallets(), Message::ConnectedHardwareWallets) + Command::perform( + list_hardware_wallets(&[], None), + Message::ConnectedHardwareWallets, + ) } fn view(&self) -> Element { let desc = self.descriptor.as_ref().unwrap(); diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index e9e738d8..f8add589 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -5,10 +5,11 @@ use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; +use async_hwi::DeviceKind; use iced::{pure::Element, Command}; use minisafe::{ config::{BitcoinConfig, BitcoindConfig}, - descriptors::InheritanceDescriptor, + descriptors::MultipathDescriptor, miniscript::bitcoin, }; @@ -40,7 +41,8 @@ pub trait Step { pub struct Context { pub bitcoin_config: BitcoinConfig, pub bitcoind_config: Option, - pub descriptor: Option, + pub descriptor: Option, + pub hw_tokens: Vec<(DeviceKind, bitcoin::util::bip32::Fingerprint, [u8; 32])>, pub data_dir: Option, } @@ -51,6 +53,7 @@ impl Context { network, poll_interval_secs: Duration::from_secs(30), }, + hw_tokens: Vec::new(), bitcoind_config: None, descriptor: None, data_dir, diff --git a/gui/src/utils/mock.rs b/gui/src/utils/mock.rs index 43e9e06b..453703ec 100644 --- a/gui/src/utils/mock.rs +++ b/gui/src/utils/mock.rs @@ -81,7 +81,7 @@ pub fn fake_daemon_config() -> Config { toml::from_str( r#" data_dir = "/home/edouard/code/revault/demo/minisafe/datadir" -main_descriptor = "wsh(or_d(pk(tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/*),and_v(v:pkh(tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S/*),older(100))))#459t6xxr" +main_descriptor = "wsh(or_d(pk(tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/<0;1>/*),and_v(v:pkh(tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S/<0;1>/*),older(100))))#9sx3g3pv" [bitcoin_config] network = "regtest" From 1d8527ed95e8e8454ffb989aded266224ec753e3 Mon Sep 17 00:00:00 2001 From: edouard Date: Thu, 3 Nov 2022 15:00:41 +0100 Subject: [PATCH 7/7] minisafe: bump miniscript --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 12079160..7559b509 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -256,7 +256,7 @@ dependencies = [ [[package]] name = "miniscript" version = "8.0.0" -source = "git+https://github.com/darosior/rust-miniscript?branch=multipath_descriptors_on_8.0#7d756f2ab066d85d299f711f953ebda15f14e832" +source = "git+https://github.com/darosior/rust-miniscript?branch=multipath_descriptors_on_8.0#a63d5a263a9006b4d29342012133a3bc919765ba" dependencies = [ "bitcoin", "serde",