Merge #71: gui: import xpubs from hws

1d8527ed95e8e8454ffb989aded266224ec753e3 minisafe: bump miniscript (edouard)
258aeb57feba3b84e09d5bf3e863209cea06113a hw: load wallet from config (edouard)
6500381059c232023c29eb251e66cdb68bbe8148 installer: add clipboard (edouard)
c2ed30961c4c49826e97f6ab59eb0952ef1a9d2c installer: register wallet (edouard)
a2021ca326d614d7a604e3e6cad9f568e29a2d20 installer: refac context (edouard)
56be997d6bdbecc4d8c5ae7cded322c6c6936ed5 gui: fix ci add missing deps (edouard)
8ad37f18a88a57748a7c3f53f1ecb6bb8347194a gui: import xpubs from hws (wip) (edouard)

Pull request description:

ACKs for top commit:
  edouardparis:
    self-ACK 1d8527ed95e8e8454ffb989aded266224ec753e3

Tree-SHA512: 21b669915388ec60ff37dbb5be3a5f635571c60387ebb696689eddcc7f41cd3d900aff8aa3c5f11fd60860dc19e87dac7fd39e7cbbe6a8eae0ea8baba643354a
This commit is contained in:
edouard 2022-11-03 16:10:02 +01:00
commit ecd5962831
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
16 changed files with 1277 additions and 304 deletions

View File

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

2
Cargo.lock generated
View File

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

335
gui/Cargo.lock generated
View File

@ -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=master#14b29f820910132d9b8f799d030bfed69ac67239"
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#efe26ed9fcfe0676c33141ad4777fd3786f7d539"
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"
@ -1389,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",
@ -1409,6 +1617,7 @@ dependencies = [
name = "minisafe-gui"
version = "0.0.1"
dependencies = [
"async-hwi",
"backtrace",
"chrono",
"dirs",
@ -1427,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",
@ -1455,6 +1663,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 +1766,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 +1790,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 +2186,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 +2373,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 +2494,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 +2656,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 +2684,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"

View File

@ -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 = "master" }
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"

View File

@ -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<String>,
/// Use iced debug feature if true.
pub debug: Option<bool>,
/// hardware wallets config.
#[serde(default)]
pub hardware_wallets: Vec<HardwareWalletConfig>,
}
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<HardwareWalletConfig>,
) -> Self {
Self {
minisafed_config_path,
log_level: None,
debug: None,
hardware_wallets,
}
}

125
gui/src/hw.rs Normal file
View File

@ -0,0 +1,125 @@
use std::sync::Arc;
use async_hwi::{ledger, specter, DeviceKind, Error as HWIError, HWI};
use log::debug;
use minisafe::miniscript::bitcoin::{
hashes::hex::{FromHex, ToHex},
util::bip32::Fingerprint,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct HardwareWallet {
pub device: Arc<dyn HWI + Send + Sync>,
pub kind: DeviceKind,
pub fingerprint: Fingerprint,
}
impl HardwareWallet {
async fn new(device: Arc<dyn HWI + Send + Sync>) -> Result<Self, HWIError> {
let kind = device.device_kind();
let fingerprint = device.get_master_fingerprint().await?;
Ok(Self {
device,
kind,
fingerprint,
})
}
}
#[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<HardwareWallet> {
let mut hws: Vec<HardwareWallet> = 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(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);
}
},
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
}

View File

@ -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<InheritanceDescriptor>,
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<PathBuf>,
}
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<T: std::fmt::Display, S: serde::Serializer>(
field: &Option<T>,
s: S,
) -> Result<S::Ok, S::Error> {
match field {
Some(field) => s.serialize_str(&field.to_string()),
None => s.serialize_none(),
}
}
impl TryFrom<Config> for MinisafeConfig {
impl TryFrom<Context> for MinisafeConfig {
type Error = &'static str;
fn try_from(cfg: Config) -> Result<Self, Self::Error> {
if cfg.main_descriptor.is_none() {
fn try_from(ctx: Context) -> Result<Self, Self::Error> {
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,
})
}
}

View File

@ -1,19 +1,26 @@
use minisafe::miniscript::bitcoin;
use minisafe::miniscript::bitcoin::{util::bip32::Fingerprint, Network};
use std::path::PathBuf;
use super::Error;
use crate::hw::HardwareWallet;
#[derive(Debug, Clone)]
pub enum Message {
Event(iced_native::Event),
Exit(PathBuf),
Clibpboard(String),
Next,
Previous,
Install,
Close,
Reload,
Select(usize),
Installed(Result<PathBuf, Error>),
Network(bitcoin::Network),
Network(Network),
DefineBitcoind(DefineBitcoind),
DefineDescriptor(DefineDescriptor),
ConnectedHardwareWallets(Vec<HardwareWallet>),
WalletRegistered(Result<(Fingerprint, Option<[u8; 32]>), Error>),
}
#[derive(Debug, Clone)]
@ -25,6 +32,9 @@ pub enum DefineBitcoind {
#[derive(Debug, Clone)]
pub enum DefineDescriptor {
ImportDescriptor(String),
ImportUserHWXpub,
ImportHeirHWXpub,
XpubImported(Result<String, Error>),
UserXpubEdited(String),
HeirXpubEdited(String),
SequenceEdited(String),

View File

@ -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;
@ -12,10 +12,12 @@ 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, hw::HardwareWalletConfig, 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,
@ -24,7 +26,6 @@ pub struct Installer {
/// Context is data passed through each step.
context: Context,
config: DaemonConfig,
}
impl Installer {
@ -44,20 +45,18 @@ impl Installer {
destination_path: PathBuf,
network: bitcoin::Network,
) -> (Installer, Command<Message>) {
let mut config = DaemonConfig::new();
config.data_dir = Some(destination_path);
(
Installer {
should_exit: false,
config,
current: 0,
steps: vec![
Welcome::new(network).into(),
DefineDescriptor::new().into(),
RegisterDescriptor::default().into(),
DefineBitcoind::new().into(),
Final::new().into(),
],
context: Context::new(network),
context: Context::new(network, Some(destination_path)),
},
Command::none(),
)
@ -77,12 +76,13 @@ impl Installer {
pub fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Clibpboard(s) => clipboard::write(s),
Message::Next => {
let current_step = self
.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
@ -99,33 +99,31 @@ impl Installer {
.get_mut(self.current)
.expect("There is always a step");
current_step.load_context(&self.context);
return current_step.load();
}
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(
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();
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<Message> {
@ -136,12 +134,20 @@ impl Installer {
}
}
pub async fn install(_ctx: Context, mut cfg: DaemonConfig) -> Result<PathBuf, Error> {
pub async fn install(ctx: Context) -> Result<PathBuf, Error> {
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");
// 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 +160,7 @@ pub async fn install(_ctx: Context, mut cfg: DaemonConfig) -> Result<PathBuf, Er
// create minisafed configuration file
let mut minisafed_config_path = datadir_path.clone();
minisafed_config_path.push(DaemonConfig::DEFAULT_FILE_NAME);
minisafed_config_path.push(DEFAULT_FILE_NAME);
let mut minisafed_config_file = std::fs::File::create(&minisafed_config_path)
.map_err(|e| Error::CannotCreateFile(e.to_string()))?;
@ -181,6 +187,7 @@ pub async fn install(_ctx: Context, mut cfg: DaemonConfig) -> Result<PathBuf, Er
e
))
})?,
hardware_wallets,
))
.unwrap()
.as_bytes(),
@ -196,6 +203,13 @@ pub enum Error {
CannotCreateFile(String),
CannotWriteToFile(String),
Unexpected(String),
HardwareWallet(async_hwi::Error),
}
impl From<async_hwi::Error> for Error {
fn from(error: async_hwi::Error) -> Self {
Error::HardwareWallet(error)
}
}
impl std::fmt::Display for Error {
@ -205,6 +219,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),
}
}
}

View File

@ -0,0 +1,401 @@
use std::str::FromStr;
use iced::{pure::Element, Command};
use minisafe::{
descriptors::MultipathDescriptor,
miniscript::{
bitcoin::util::bip32::{DerivationPath, Fingerprint},
descriptor::{Descriptor, DescriptorMultiXKey, DescriptorPublicKey, Wildcard},
},
};
use crate::{
hw::{list_hardware_wallets, HardwareWallet},
installer::{
message::{self, Message},
step::{Context, Step},
view, Error,
},
ui::component::form,
};
pub struct DefineDescriptor {
imported_descriptor: form::Value<String>,
user_xpub: form::Value<String>,
heir_xpub: form::Value<String>,
sequence: form::Value<String>,
modal: Option<GetHardwareWalletXpubModal>,
error: Option<String>,
}
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<Message> {
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::<u16>().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) -> 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::<u32>().is_ok();
}
if !self.imported_descriptor.value.is_empty() {
self.imported_descriptor.valid =
Descriptor::<DescriptorPublicKey>::from_str(&self.imported_descriptor.value)
.is_ok();
}
false
} else if !self.imported_descriptor.value.is_empty() {
if let Ok(desc) = MultipathDescriptor::from_str(&self.imported_descriptor.value) {
ctx.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::<u16>();
self.sequence.valid = sequence.is_ok();
if !self.user_xpub.valid || !self.heir_xpub.valid || !self.sequence.valid {
return false;
}
let desc = match MultipathDescriptor::new(
user_key.unwrap(),
heir_key.unwrap(),
sequence.unwrap(),
) {
Ok(desc) => desc,
Err(e) => {
self.error = Some(e.to_string());
return false;
}
};
ctx.descriptor = Some(desc);
true
}
}
fn view(&self) -> Element<Message> {
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<DefineDescriptor> for Box<dyn Step> {
fn from(s: DefineDescriptor) -> Box<dyn Step> {
Box::new(s)
}
}
pub struct GetHardwareWalletXpubModal {
is_heir: bool,
chosen_hw: Option<usize>,
processing: bool,
hws: Vec<HardwareWallet>,
error: Option<Error>,
}
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<Message> {
Command::perform(
list_hardware_wallets(&[], None),
Message::ConnectedHardwareWallets,
)
}
fn update(&mut self, message: Message) -> Command<Message> {
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<Message> {
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<dyn async_hwi::HWI + Send + Sync>,
fingerprint: Fingerprint,
) -> Result<DescriptorPublicKey, Error> {
let derivation_path = DerivationPath::master();
let xkey = hw
.get_extended_pubkey(&derivation_path, true)
.await
.map_err(Error::from)?;
Ok(DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
origin: Some((fingerprint, derivation_path)),
derivation_paths: vec![
DerivationPath::from_str("m/0").unwrap(),
DerivationPath::from_str("m/1").unwrap(),
],
xkey,
wildcard: Wildcard::Unhardened,
}))
}
#[derive(Default)]
pub struct RegisterDescriptor {
descriptor: Option<MultipathDescriptor>,
processing: bool,
chosen_hw: Option<usize>,
hws: Vec<(HardwareWallet, Option<[u8; 32]>)>,
error: Option<Error>,
}
impl Step for RegisterDescriptor {
fn load_context(&mut self, ctx: &Context) {
self.descriptor = ctx.descriptor.clone();
}
fn update(&mut self, message: Message) -> Command<Message> {
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 {
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<Message> {
Command::perform(
list_hardware_wallets(&[], None),
Message::ConnectedHardwareWallets,
)
}
fn view(&self) -> Element<Message> {
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<dyn async_hwi::HWI + Send + Sync>,
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<RegisterDescriptor> for Box<dyn Step> {
fn from(s: RegisterDescriptor) -> Box<dyn Step> {
Box::new(s)
}
}

View File

@ -1,49 +1,63 @@
mod descriptor;
pub use descriptor::{DefineDescriptor, RegisterDescriptor};
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use iced::pure::Element;
use async_hwi::DeviceKind;
use iced::{pure::Element, Command};
use minisafe::{
descriptors::InheritanceDescriptor,
miniscript::{
bitcoin,
descriptor::{Descriptor, DescriptorPublicKey},
},
config::{BitcoinConfig, BitcoindConfig},
descriptors::MultipathDescriptor,
miniscript::bitcoin,
};
use crate::ui::component::form;
use crate::installer::{
config,
message::{self, Message},
view,
};
pub trait Step {
fn update(&mut self, message: Message);
fn update(&mut self, _message: Message) -> Command<Message> {
Command::none()
}
fn view(&self) -> Element<Message>;
fn load_context(&mut self, _ctx: &Context) {}
fn load(&self) -> Command<Message> {
Command::none()
}
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<BitcoindConfig>,
pub descriptor: Option<MultipathDescriptor>,
pub hw_tokens: Vec<(DeviceKind, bitcoin::util::bip32::Fingerprint, [u8; 32])>,
pub data_dir: Option<PathBuf>,
}
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<PathBuf>) -> Self {
Self {
bitcoin_config: BitcoinConfig {
network,
poll_interval_secs: Duration::from_secs(30),
},
hw_tokens: Vec::new(),
bitcoind_config: None,
descriptor: None,
data_dir,
}
}
}
@ -58,14 +72,14 @@ impl Welcome {
}
impl Step for Welcome {
fn update(&mut self, message: Message) {
fn update(&mut self, message: Message) -> Command<Message> {
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;
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<Message> {
@ -85,138 +99,6 @@ impl From<Welcome> for Box<dyn Step> {
}
}
pub struct DefineDescriptor {
imported_descriptor: form::Value<String>,
user_xpub: form::Value<String>,
heir_xpub: form::Value<String>,
sequence: form::Value<String>,
error: Option<String>,
}
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::<u16>().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::<u32>().is_ok();
}
if !self.imported_descriptor.value.is_empty() {
self.imported_descriptor.valid =
Descriptor::<DescriptorPublicKey>::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::<u16>();
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<Message> {
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<DefineDescriptor> for Box<dyn Step> {
fn from(s: DefineDescriptor) -> Box<dyn Step> {
Box::new(s)
}
}
pub struct DefineBitcoind {
cookie_path: form::Value<String>,
address: form::Value<String>,
@ -277,13 +159,14 @@ 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) {
fn update(&mut self, message: Message) -> Command<Message> {
if let Message::DefineBitcoind(msg) = message {
match msg {
message::DefineBitcoind::AddressEdited(address) => {
@ -296,9 +179,10 @@ 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),
@ -317,8 +201,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
}
}
@ -358,7 +244,7 @@ impl Final {
}
impl Step for Final {
fn update(&mut self, message: Message) {
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Installed(res) => {
self.generating = false;
@ -377,6 +263,7 @@ impl Step for Final {
}
_ => {}
};
Command::none()
}
fn view(&self) -> Element<Message> {

View File

@ -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,69 @@ 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)
.spacing(50)
.align_items(Alignment::Center),
)
}
pub fn register_descriptor<'a>(
descriptor: &str,
hws: &[(HardwareWallet, Option<[u8; 32]>)],
error: Option<&Error>,
processing: bool,
chosen_hw: Option<usize>,
) -> Element<'a, Message> {
layout(
column()
.push(text("Register descriptor").bold().size(50))
.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()
.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)
@ -233,6 +317,111 @@ 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<usize>,
) -> 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,
false,
))
}),
)
.width(Length::Fill)
} else {
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)
.padding(100)
.spacing(50)
.align_items(Alignment::Center),
)
}
fn hw_list_view<'a>(
i: usize,
hw: &HardwareWallet,
chosen: bool,
processing: bool,
registered: 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
})
.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)
.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>>) -> Element<'a, Message> {
container(scrollable(
column()
@ -245,5 +434,36 @@ fn layout<'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message>
.center_x()
.height(Length::Fill)
.width(Length::Fill)
.style(BackgroundStyle)
.into()
}
fn modal<'a>(content: impl Into<Element<'a, Message>>) -> 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(BackgroundStyle)
.into()
}
pub struct BackgroundStyle;
impl widget::container::StyleSheet for BackgroundStyle {
fn style(&self) -> widget::container::Style {
widget::container::Style {
background: color::BACKGROUND.into(),
..widget::container::Style::default()
}
}
}

View File

@ -1,5 +1,6 @@
pub mod app;
pub mod daemon;
pub mod hw;
pub mod installer;
pub mod loader;
pub mod ui;

View File

@ -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<Element<'a, T>>>(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()
}
}
}

View File

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

View File

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