From 4be74ad496692d371adffcc1b3c868ef6a01cb52 Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 8 Sep 2023 13:52:36 +0200 Subject: [PATCH] refac hw module and add bitbox support --- gui/Cargo.lock | 651 +++++++++++++++++++++++++-- gui/Cargo.toml | 2 +- gui/src/app/cache.rs | 4 + gui/src/app/message.rs | 4 +- gui/src/app/settings.rs | 96 ++++ gui/src/app/state/psbt.rs | 82 ++-- gui/src/app/state/psbts.rs | 10 +- gui/src/app/state/recovery.rs | 18 +- gui/src/app/state/settings/mod.rs | 8 + gui/src/app/state/settings/wallet.rs | 58 +-- gui/src/app/state/spend/mod.rs | 4 + gui/src/app/state/spend/step.rs | 22 +- gui/src/app/view/hw.rs | 6 + gui/src/app/view/psbt.rs | 222 ++++----- gui/src/app/view/settings.rs | 15 +- gui/src/app/wallet.rs | 16 +- gui/src/daemon/mod.rs | 11 +- gui/src/daemon/model.rs | 14 +- gui/src/hw.rs | 628 +++++++++++++++++--------- gui/src/installer/message.rs | 6 +- gui/src/installer/mod.rs | 33 +- gui/src/installer/step/bitcoind.rs | 13 +- gui/src/installer/step/descriptor.rs | 279 ++++++------ gui/src/installer/step/mnemonic.rs | 9 +- gui/src/installer/step/mod.rs | 16 +- gui/src/installer/view.rs | 78 ++-- gui/src/loader.rs | 8 +- gui/ui/src/component/hw.rs | 21 + 28 files changed, 1652 insertions(+), 682 deletions(-) diff --git a/gui/Cargo.lock b/gui/Cargo.lock index e9d6e8c75..edbd18950 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -60,13 +60,48 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "opaque-debug", +] + +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom", + "getrandom 0.2.8", "once_cell", "version_check", ] @@ -95,6 +130,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + [[package]] name = "approx" version = "0.5.1" @@ -133,17 +174,18 @@ dependencies = [ [[package]] name = "async-hwi" -version = "0.0.11" +version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29e54987aab24867f5259b95d5c7f3d46bf69ad8ddfb01dde24a88c00a9e93d" +checksum = "bca4546c8774683d7c15e692be53751f59fcac3b83465332013b3ca673248e15" dependencies = [ "async-trait", "base64 0.13.1", + "bitbox-api", "bitcoin", "futures", "hidapi", "ledger-apdu", - "ledger-transport-hid", + "ledger-transport-hidapi", "ledger_bitcoin_client", "regex", "serialport", @@ -183,6 +225,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base64" version = "0.13.1" @@ -242,6 +290,32 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" +[[package]] +name = "bitbox-api" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f42283322b091f6650ed7cfa17febd100c245bf068fc1a28708e6b3763f2f43" +dependencies = [ + "async-trait", + "base32", + "bitcoin", + "byteorder", + "getrandom 0.2.8", + "hex", + "hidapi", + "noise-protocol", + "noise-rust-crypto", + "num-bigint", + "prost", + "prost-build", + "semver", + "serde", + "serde_json", + "thiserror", + "tokio", + "zeroize", +] + [[package]] name = "bitcoin" version = "0.30.0" @@ -287,12 +361,30 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "block" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.12.0" @@ -386,6 +478,31 @@ dependencies = [ "libc", ] +[[package]] +name = "chacha20" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "checked_int_cast" version = "1.0.0" @@ -407,6 +524,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "clipboard-win" version = "4.5.0" @@ -526,7 +652,7 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" dependencies = [ - "getrandom", + "getrandom 0.2.8", "once_cell", "proc-macro-hack", "tiny-keccak", @@ -600,6 +726,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -681,12 +816,44 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher", +] + [[package]] name = "cty" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + [[package]] name = "cxx" version = "1.0.94" @@ -783,6 +950,26 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "dirs" version = "3.0.2" @@ -920,6 +1107,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "error-code" version = "2.3.1" @@ -977,6 +1191,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fern" version = "0.6.2" @@ -1007,6 +1230,12 @@ dependencies = [ "toml", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.25" @@ -1244,6 +1473,16 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "0.2.3" @@ -1254,6 +1493,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -1267,6 +1517,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gif" version = "0.12.0" @@ -1513,7 +1773,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -1553,6 +1813,12 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "hashlink" version = "0.7.0" @@ -1592,6 +1858,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + [[package]] name = "hex" version = "0.4.3" @@ -1612,9 +1884,9 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "hidapi" -version = "1.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798154e4b6570af74899d71155fb0072d5b17e6aa12f39c8ef22c60fb8ec99e7" +checksum = "723777263b0dcc5730aec947496bd8c3940ba63c15f5633b288cc615f4f6af79" dependencies = [ "cc", "libc", @@ -1934,6 +2206,16 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + [[package]] name = "instant" version = "0.1.12" @@ -1946,12 +2228,32 @@ dependencies = [ "web-sys", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.2", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipnet" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -1975,9 +2277,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -2084,10 +2386,10 @@ dependencies = [ ] [[package]] -name = "ledger-transport-hid" +name = "ledger-transport-hidapi" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ba81a1f5f24396b37211478aff7fbcd605dd4544df8dbed07b9da3c2057aee" +checksum = "e27139d540e4271fa55b67b8cb94c6f100931042dcc663db1c2395fa3ffb8599" dependencies = [ "byteorder", "cfg-if", @@ -2113,13 +2415,13 @@ dependencies = [ [[package]] name = "liana" version = "2.0.0" -source = "git+https://github.com/wizardsardine/liana?branch=master#6869d8554a6214d8e73614adf3c9334c1c6188d4" +source = "git+https://github.com/wizardsardine/liana?branch=master#42578609e2ed340d277d75c747c74449308acbb7" dependencies = [ "backtrace", "bip39", "dirs 5.0.0", "fern", - "getrandom", + "getrandom 0.2.8", "jsonrpc 0.16.0", "log", "miniscript", @@ -2240,6 +2542,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "lock_api" version = "0.4.9" @@ -2463,6 +2771,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + [[package]] name = "mutate_once" version = "0.1.1" @@ -2479,7 +2793,7 @@ dependencies = [ "bitflags", "codespan-reporting", "hexf-parse", - "indexmap", + "indexmap 1.9.3", "log", "num-traits", "rustc-hash", @@ -2495,7 +2809,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom", + "getrandom 0.2.8", ] [[package]] @@ -2614,6 +2928,31 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +[[package]] +name = "noise-protocol" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb474d36dfe51bb4d7e733fee2b0dfd92ee1b95c716030a70e92737dea1a52b" +dependencies = [ + "arrayvec 0.7.2", +] + +[[package]] +name = "noise-rust-crypto" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e7cfeb8e6a63b4a5ccef34ed7a22d084a129b1e53a000c080bbc54c0da6f8c" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "getrandom 0.2.8", + "noise-protocol", + "sha2", + "x25519-dalek", + "zeroize", +] + [[package]] name = "nom" version = "7.1.3" @@ -2634,6 +2973,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2671,7 +3021,7 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] @@ -2750,6 +3100,12 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "ordered-float" version = "3.6.0" @@ -2895,6 +3251,16 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.0.0", +] + [[package]] name = "phf" version = "0.11.1" @@ -2993,12 +3359,45 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3054,6 +3453,60 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74605f360ce573babfe43964cbe520294dcb081afbf8c108fc6e23036b4da2df" +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" +dependencies = [ + "bytes", + "heck", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 1.0.109", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-types" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +dependencies = [ + "prost", +] + [[package]] name = "qoi" version = "0.4.1" @@ -3089,7 +3542,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -3099,7 +3552,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -3108,7 +3570,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.8", ] [[package]] @@ -3176,7 +3638,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e233b642160555c1aa1ff7a78443c6139342f411b6fa6602af2ebbfee9e166bb" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -3203,7 +3665,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom", + "getrandom 0.2.8", "redox_syscall 0.2.16", "thiserror", ] @@ -3372,6 +3834,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.37.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79bef90eb6d984c72722595b5b1348ab39275a5e5123faca6863bf07d75a4e0" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + [[package]] name = "rustls" version = "0.21.6" @@ -3494,6 +3970,12 @@ dependencies = [ "cc", ] +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + [[package]] name = "serde" version = "1.0.186" @@ -3575,6 +4057,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -3767,6 +4260,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "svg_fmt" version = "0.4.1" @@ -3835,6 +4334,20 @@ dependencies = [ "libc", ] +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -4057,7 +4570,7 @@ version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ - "indexmap", + "indexmap 1.9.3", "toml_datetime", "winnow", ] @@ -4149,6 +4662,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -4218,6 +4737,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" @@ -4301,6 +4830,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -4315,9 +4850,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4325,24 +4860,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.29", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -4352,9 +4887,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4362,22 +4897,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-timer" @@ -4611,6 +5146,17 @@ dependencies = [ "wgpu", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "widestring" version = "0.5.1" @@ -4948,6 +5494,17 @@ dependencies = [ "winapi-wsapoll", ] +[[package]] +name = "x25519-dalek" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077" +dependencies = [ + "curve25519-dalek", + "rand_core 0.5.1", + "zeroize", +] + [[package]] name = "xcursor" version = "0.3.4" @@ -4975,6 +5532,26 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/gui/Cargo.toml b/gui/Cargo.toml index 394dae947..89b2c4fb2 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -14,7 +14,7 @@ name = "liana-gui" path = "src/main.rs" [dependencies] -async-hwi = "0.0.11" +async-hwi = "0.0.12" liana = { git = "https://github.com/wizardsardine/liana", branch = "master", default-features = false, features = ["nonblocking_shutdown"] } liana_ui = { path = "ui" } backtrace = "0.3" diff --git a/gui/src/app/cache.rs b/gui/src/app/cache.rs index 4c13ed106..cc714a6f8 100644 --- a/gui/src/app/cache.rs +++ b/gui/src/app/cache.rs @@ -1,8 +1,10 @@ use crate::daemon::model::{Coin, SpendTx}; use liana::miniscript::bitcoin::Network; +use std::path::PathBuf; #[derive(Debug)] pub struct Cache { + pub datadir_path: PathBuf, pub network: Network, pub blockheight: i32, pub coins: Vec, @@ -10,9 +12,11 @@ pub struct Cache { pub rescan_progress: Option, } +/// only used for tests. impl std::default::Default for Cache { fn default() -> Self { Self { + datadir_path: std::path::PathBuf::new(), network: Network::Bitcoin, blockheight: 0, coins: Vec::new(), diff --git a/gui/src/app/message.rs b/gui/src/app/message.rs index ad35271f0..f8256ecb4 100644 --- a/gui/src/app/message.rs +++ b/gui/src/app/message.rs @@ -9,7 +9,7 @@ use liana::{ use crate::{ app::{error::Error, view, wallet::Wallet}, daemon::model::*, - hw::HardwareWallet, + hw::HardwareWalletMessage, }; #[derive(Debug)] @@ -32,7 +32,7 @@ pub enum Message { Updated(Result<(), Error>), Saved(Result<(), Error>), StartRescan(Result<(), Error>), - ConnectedHardwareWallets(Vec), + HardwareWallets(HardwareWalletMessage), HistoryTransactions(Result, Error>), PendingTransactions(Result, Error>), LabelsUpdated(Result>, Error>), diff --git a/gui/src/app/settings.rs b/gui/src/app/settings.rs index ea82ea63c..a2765dd12 100644 --- a/gui/src/app/settings.rs +++ b/gui/src/app/settings.rs @@ -122,3 +122,99 @@ impl std::fmt::Display for SettingsError { } } } + +/// global settings. +pub mod global { + use async_hwi::bitbox::{ConfigError, NoiseConfig, NoiseConfigData}; + use serde::{Deserialize, Serialize}; + use std::io::{Read, Write}; + use std::path::{Path, PathBuf}; + + pub const DEFAULT_FILE_NAME: &str = "global_settings.json"; + + #[derive(Debug, Deserialize, Serialize)] + pub struct Settings { + pub bitbox: Option, + } + + #[derive(Debug, Deserialize, Serialize)] + pub struct BitboxSettings { + pub noise_config: NoiseConfigData, + } + + pub struct PersistedBitboxNoiseConfig { + file_path: PathBuf, + } + + impl async_hwi::bitbox::api::Threading for PersistedBitboxNoiseConfig {} + + impl PersistedBitboxNoiseConfig { + /// Creates a new persisting noise config, which stores the pairing information in "bitbox.json" + /// in the provided directory. + pub fn new(global_datadir: &Path) -> PersistedBitboxNoiseConfig { + PersistedBitboxNoiseConfig { + file_path: global_datadir.join(DEFAULT_FILE_NAME), + } + } + } + + impl NoiseConfig for PersistedBitboxNoiseConfig { + fn read_config(&self) -> Result { + if !self.file_path.exists() { + return Ok(NoiseConfigData::default()); + } + + let mut file = + std::fs::File::open(&self.file_path).map_err(|e| ConfigError(e.to_string()))?; + + let mut contents = String::new(); + file.read_to_string(&mut contents) + .map_err(|e| ConfigError(e.to_string()))?; + + let settings = serde_json::from_str::(&contents) + .map_err(|e| ConfigError(e.to_string()))?; + + Ok(settings + .bitbox + .map(|s| s.noise_config) + .unwrap_or_else(NoiseConfigData::default)) + } + + fn store_config(&self, conf: &NoiseConfigData) -> Result<(), ConfigError> { + let data = if self.file_path.exists() { + let mut file = + std::fs::File::open(&self.file_path).map_err(|e| ConfigError(e.to_string()))?; + + let mut contents = String::new(); + file.read_to_string(&mut contents) + .map_err(|e| ConfigError(e.to_string()))?; + + let mut settings = serde_json::from_str::(&contents) + .map_err(|e| ConfigError(e.to_string()))?; + + settings.bitbox = Some(BitboxSettings { + noise_config: conf.clone(), + }); + + serde_json::to_string_pretty(&settings).map_err(|e| ConfigError(e.to_string()))? + } else { + serde_json::to_string_pretty(&Settings { + bitbox: Some(BitboxSettings { + noise_config: conf.clone(), + }), + }) + .map_err(|e| ConfigError(e.to_string()))? + }; + + let mut file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&self.file_path) + .map_err(|e| ConfigError(e.to_string()))?; + + file.write_all(data.as_bytes()) + .map_err(|e| ConfigError(e.to_string())) + } + } +} diff --git a/gui/src/app/state/psbt.rs b/gui/src/app/state/psbt.rs index 841ff0113..98a421bfe 100644 --- a/gui/src/app/state/psbt.rs +++ b/gui/src/app/state/psbt.rs @@ -1,10 +1,13 @@ use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; use std::sync::Arc; +use iced::Subscription; + use iced::Command; use liana::{ descriptors::LianaPolicy, - miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt}, + miniscript::bitcoin::{bip32::Fingerprint, psbt::Psbt, Network}, }; use liana_ui::{ @@ -25,7 +28,7 @@ use crate::{ model::{LabelItem, Labelled, SpendStatus, SpendTx}, Daemon, }, - hw::{list_hardware_wallets, HardwareWallet}, + hw::{HardwareWallet, HardwareWallets}, }; pub trait Action { @@ -35,6 +38,9 @@ pub trait Action { fn load(&self, _daemon: Arc) -> Command { Command::none() } + fn subscription(&self) -> Subscription { + Subscription::none() + } fn update( &mut self, _daemon: Arc, @@ -69,6 +75,14 @@ impl PsbtState { } } + pub fn subscription(&self) -> Subscription { + if let Some(action) = &self.action { + action.subscription() + } else { + Subscription::none() + } + } + pub fn load(&self, daemon: Arc) -> Command { if let Some(action) = &self.action { action.load(daemon) @@ -80,7 +94,7 @@ impl PsbtState { pub fn update( &mut self, daemon: Arc, - _cache: &Cache, + cache: &Cache, message: Message, ) -> Command { match &message { @@ -92,7 +106,12 @@ impl PsbtState { self.action = Some(Box::::default()); } view::SpendTxMessage::Sign => { - let action = SignAction::new(self.tx.signers(), self.wallet.clone()); + let action = SignAction::new( + self.tx.signers(), + self.wallet.clone(), + cache.datadir_path.clone(), + cache.network, + ); let cmd = action.load(daemon); self.action = Some(Box::new(action)); return cmd; @@ -296,18 +315,23 @@ pub struct SignAction { wallet: Arc, chosen_hw: Option, processing: bool, - hws: Vec, + hws: HardwareWallets, error: Option, signed: HashSet, } impl SignAction { - pub fn new(signed: HashSet, wallet: Arc) -> Self { + pub fn new( + signed: HashSet, + wallet: Arc, + datadir_path: PathBuf, + network: Network, + ) -> Self { Self { - wallet, chosen_hw: None, processing: false, - hws: Vec::new(), + hws: HardwareWallets::new(datadir_path, network).with_wallet(wallet.clone()), + wallet, error: None, signed, } @@ -319,13 +343,10 @@ impl Action for SignAction { self.error.as_ref() } - fn load(&self, _daemon: Arc) -> Command { - let wallet = self.wallet.clone(); - Command::perform( - async move { list_hardware_wallets(&wallet).await }, - Message::ConnectedHardwareWallets, - ) + fn subscription(&self) -> Subscription { + self.hws.refresh().map(Message::HardwareWallets) } + fn update( &mut self, daemon: Arc, @@ -338,7 +359,7 @@ impl Action for SignAction { fingerprint, device, .. - }) = self.hws.get(i) + }) = self.hws.list.get(i) { self.chosen_hw = Some(i); self.processing = true; @@ -372,28 +393,23 @@ impl Action for SignAction { Message::Updated(res) => match res { Ok(()) => { self.processing = false; - tx.sigs = self - .wallet - .main_descriptor - .partial_spend_info(&tx.psbt) - .unwrap(); + match self.wallet.main_descriptor.partial_spend_info(&tx.psbt) { + Ok(sigs) => tx.sigs = sigs, + Err(e) => self.error = Some(Error::Unexpected(e.to_string())), + } } Err(e) => self.error = Some(e), }, - // We add the new hws without dropping the reference of the previous ones. - Message::ConnectedHardwareWallets(hws) => { - for h in hws { - if !self - .hws - .iter() - .any(|hw| hw.fingerprint() == hw.fingerprint() && hw.kind() == h.kind()) - { - self.hws.push(h); - } + + Message::HardwareWallets(msg) => match self.hws.update(msg) { + Ok(cmd) => { + return cmd.map(Message::HardwareWallets); } - } + Err(e) => { + self.error = Some(e.into()); + } + }, Message::View(view::Message::Reload) => { - self.hws = Vec::new(); self.chosen_hw = None; self.error = None; return self.load(daemon); @@ -405,7 +421,7 @@ impl Action for SignAction { fn view(&self) -> Element { view::psbt::sign_action( self.error.as_ref(), - &self.hws, + &self.hws.list, self.wallet.signer.as_ref().map(|s| s.fingerprint()), self.wallet .signer diff --git a/gui/src/app/state/psbts.rs b/gui/src/app/state/psbts.rs index de0bd1ccc..3023ba34b 100644 --- a/gui/src/app/state/psbts.rs +++ b/gui/src/app/state/psbts.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use iced::Command; +use iced::{Command, Subscription}; use liana::miniscript::bitcoin::psbt::Psbt; use liana_ui::{ @@ -109,6 +109,14 @@ impl State for PsbtsPanel { Command::none() } + fn subscription(&self) -> Subscription { + if let Some(psbt) = &self.selected_tx { + psbt.subscription() + } else { + Subscription::none() + } + } + fn load(&self, daemon: Arc) -> Command { let daemon = daemon.clone(); Command::perform( diff --git a/gui/src/app/state/recovery.rs b/gui/src/app/state/recovery.rs index a8113118d..18623837c 100644 --- a/gui/src/app/state/recovery.rs +++ b/gui/src/app/state/recovery.rs @@ -51,6 +51,14 @@ impl RecoveryPanel { } impl State for RecoveryPanel { + fn subscription(&self) -> iced::Subscription { + if let Some(psbt) = &self.generated { + psbt.subscription() + } else { + iced::Subscription::none() + } + } + fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { if let Some(generated) = &self.generated { generated.view(cache) @@ -154,15 +162,7 @@ impl State for RecoveryPanel { .any(|input| input.previous_output == coin.outpoint) }) .collect(); - let sigs = desc.partial_spend_info(&psbt).unwrap(); - Ok(SpendTx::new( - None, - psbt, - coins, - sigs, - desc.max_sat_vbytes(), - network, - )) + Ok(SpendTx::new(None, psbt, coins, &desc, network)) }, Message::Recovery, ); diff --git a/gui/src/app/state/settings/mod.rs b/gui/src/app/state/settings/mod.rs index fdce1ca36..ad88a406b 100644 --- a/gui/src/app/state/settings/mod.rs +++ b/gui/src/app/state/settings/mod.rs @@ -93,6 +93,14 @@ impl State for SettingsState { } } + fn subscription(&self) -> iced::Subscription { + if let Some(setting) = &self.setting { + setting.subscription() + } else { + iced::Subscription::none() + } + } + fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { if let Some(setting) = &self.setting { setting.view(cache) diff --git a/gui/src/app/state/settings/wallet.rs b/gui/src/app/state/settings/wallet.rs index f05802804..8686a9809 100644 --- a/gui/src/app/state/settings/wallet.rs +++ b/gui/src/app/state/settings/wallet.rs @@ -3,7 +3,7 @@ use std::convert::From; use std::path::PathBuf; use std::sync::Arc; -use iced::Command; +use iced::{Command, Subscription}; use liana::miniscript::bitcoin::{bip32::Fingerprint, Network}; @@ -17,7 +17,7 @@ use crate::{ cache::Cache, error::Error, message::Message, settings, state::State, view, wallet::Wallet, }, daemon::Daemon, - hw::{list_hardware_wallets, HardwareWallet, HardwareWalletConfig}, + hw::{HardwareWallet, HardwareWalletConfig, HardwareWallets}, }; pub struct WalletSettingsState { @@ -91,6 +91,14 @@ impl State for WalletSettingsState { } } + fn subscription(&self) -> Subscription { + if let Some(modal) = &self.modal { + modal.subscription() + } else { + Subscription::none() + } + } + fn update( &mut self, daemon: Arc, @@ -160,11 +168,9 @@ impl State for WalletSettingsState { self.modal = Some(RegisterWalletModal::new( self.data_dir.clone(), self.wallet.clone(), + cache.network, )); - self.modal - .as_ref() - .map(|m| m.load(daemon)) - .unwrap_or_else(Command::none) + Command::none() } _ => self .modal @@ -193,23 +199,23 @@ pub struct RegisterWalletModal { wallet: Arc, warning: Option, chosen_hw: Option, - hws: Vec, + hws: HardwareWallets, registered: HashSet, processing: bool, } impl RegisterWalletModal { - pub fn new(data_dir: PathBuf, wallet: Arc) -> Self { + pub fn new(data_dir: PathBuf, wallet: Arc, network: Network) -> Self { let mut registered = HashSet::new(); for hw in &wallet.hardware_wallets { registered.insert(hw.fingerprint); } Self { - data_dir, - wallet, + data_dir: data_dir.clone(), warning: None, chosen_hw: None, - hws: Vec::new(), + hws: HardwareWallets::new(data_dir, network).with_wallet(wallet.clone()), + wallet, processing: false, registered, } @@ -220,30 +226,36 @@ impl RegisterWalletModal { fn view(&self) -> Element { view::settings::register_wallet_modal( self.warning.as_ref(), - &self.hws, + &self.hws.list, self.processing, self.chosen_hw, &self.registered, ) } + fn subscription(&self) -> Subscription { + self.hws.refresh().map(Message::HardwareWallets) + } + fn update( &mut self, - daemon: Arc, + _daemon: Arc, cache: &Cache, message: Message, ) -> Command { match message { Message::View(view::Message::Reload) => { - self.hws = Vec::new(); self.chosen_hw = None; self.warning = None; - self.load(daemon) - } - Message::ConnectedHardwareWallets(hws) => { - self.hws = hws; Command::none() } + Message::HardwareWallets(msg) => match self.hws.update(msg) { + Ok(cmd) => cmd.map(Message::HardwareWallets), + Err(e) => { + self.warning = Some(e.into()); + Command::none() + } + }, Message::WalletRegistered(res) => { self.processing = false; self.chosen_hw = None; @@ -261,7 +273,7 @@ impl RegisterWalletModal { fingerprint, device, .. - }) = self.hws.get(i) + }) = self.hws.list.get(i) { self.chosen_hw = Some(i); self.processing = true; @@ -282,14 +294,6 @@ impl RegisterWalletModal { _ => Command::none(), } } - - fn load(&self, _daemon: Arc) -> Command { - let wallet = self.wallet.clone(); - Command::perform( - async move { list_hardware_wallets(&wallet).await }, - Message::ConnectedHardwareWallets, - ) - } } async fn register_wallet( diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index 3cea189d2..eab826bf6 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -70,6 +70,10 @@ impl State for CreateSpendPanel { self.steps.get(self.current).unwrap().view(cache) } + fn subscription(&self) -> iced::Subscription { + self.steps.get(self.current).unwrap().subscription() + } + fn update( &mut self, daemon: Arc, diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index 99e611f61..0fecbb0dd 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; -use iced::Command; +use iced::{Command, Subscription}; use liana::{ descriptors::LianaDescriptor, miniscript::bitcoin::{ @@ -59,6 +59,9 @@ pub trait Step { ) -> Command; fn apply(&self, _draft: &mut TransactionDraft) {} fn load(&mut self, _draft: &TransactionDraft) {} + fn subscription(&self) -> Subscription { + Subscription::none() + } } pub struct DefineSpend { @@ -508,18 +511,11 @@ impl SaveSpend { impl Step for SaveSpend { fn load(&mut self, draft: &TransactionDraft) { let psbt = draft.generated.clone().unwrap(); - let sigs = self - .wallet - .main_descriptor - .partial_spend_info(&psbt) - .unwrap(); - let mut tx = SpendTx::new( None, psbt, draft.inputs.clone(), - sigs, - self.wallet.main_descriptor.max_sat_vbytes(), + &self.wallet.main_descriptor, draft.network, ); tx.labels = draft.labels.clone(); @@ -540,6 +536,14 @@ impl Step for SaveSpend { self.spend = Some(psbt::PsbtState::new(self.wallet.clone(), tx, false)); } + fn subscription(&self) -> Subscription { + if let Some(spend) = &self.spend { + spend.subscription() + } else { + Subscription::none() + } + } + fn update( &mut self, daemon: Arc, diff --git a/gui/src/app/view/hw.rs b/gui/src/app/view/hw.rs index 312aaca94..e6dbba985 100644 --- a/gui/src/app/view/hw.rs +++ b/gui/src/app/view/hw.rs @@ -43,6 +43,9 @@ pub fn hw_list_view( HardwareWallet::Unsupported { version, kind, .. } => { hw::unsupported_hardware_wallet(&kind.to_string(), version.as_ref()) } + HardwareWallet::Locked { + kind, pairing_code, .. + } => hw::locked_hardware_wallet(kind, pairing_code.as_ref()), }) .style(theme::Button::Border) .width(Length::Fill); @@ -90,6 +93,9 @@ pub fn hw_list_view_for_registration( HardwareWallet::Unsupported { version, kind, .. } => { hw::unsupported_hardware_wallet(&kind.to_string(), version.as_ref()) } + HardwareWallet::Locked { + kind, pairing_code, .. + } => hw::locked_hardware_wallet(kind, pairing_code.as_ref()), }) .style(theme::Button::Border) .width(Length::Fill); diff --git a/gui/src/app/view/psbt.rs b/gui/src/app/view/psbt.rs index 4a4f4e6ba..d14c47fec 100644 --- a/gui/src/app/view/psbt.rs +++ b/gui/src/app/view/psbt.rs @@ -328,8 +328,7 @@ pub fn signatures<'a>( keys_aliases: &'a HashMap, ) -> Element<'a, Message> { Column::new() - .push( - if let Some(sigs) = tx.path_ready() { + .push(if let Some(sigs) = tx.path_ready() { Container::new( scrollable( Row::new() @@ -340,94 +339,95 @@ pub fn signatures<'a>( .push(icon::circle_check_icon().style(color::GREEN)) .push(text("Ready").bold().style(color::GREEN)) .push(text(" signed by")) - .push( - sigs.signed_pubkeys - .keys() - .fold(Row::new().spacing(5), |row, value| { + .push(sigs.signed_pubkeys.keys().fold( + Row::new().spacing(5), + |row, value| { row.push(if let Some(alias) = keys_aliases.get(value) { - Container::new( - tooltip::Tooltip::new( - Container::new(text(alias)) - .padding(10) - .style(theme::Container::Pill(theme::Pill::Simple)), + Container::new( + tooltip::Tooltip::new( + Container::new(text(alias)) + .padding(10) + .style(theme::Container::Pill(theme::Pill::Simple)), value.to_string(), tooltip::Position::Bottom, + ) + .style(theme::Container::Card(theme::Card::Simple)), ) - .style(theme::Container::Card(theme::Card::Simple)), - ) - } else { - Container::new(text(value.to_string())) - .padding(10) - .style(theme::Container::Pill(theme::Pill::Simple)) - }) - }), - ) - ).horizontal_scroll(scrollable::Properties::new().width(2).scroller_width(2)) - ).padding(15) - } else{ - Container::new( - Collapse::new( - move || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .spacing(20) - .push(p1_bold("Status")) - .push(Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(icon::circle_cross_icon().style(color::RED)) - .push(text("Not ready").style(color::RED)) - .width(Length::Fill) - ) - .push(icon::collapse_icon()), - ) - .padding(15) - .width(Length::Fill) - .style(theme::Button::TransparentBorder) - }, - move || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .spacing(20) - .push(p1_bold("Status")) - .push( - Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(icon::circle_cross_icon().style(color::RED)) - .push(text("Not ready").style(color::RED)) - .width(Length::Fill) - ) - .push(icon::collapsed_icon()), + } else { + Container::new(text(value.to_string())) + .padding(10) + .style(theme::Container::Pill(theme::Pill::Simple)) + }) + }, + )), ) - .padding(15) - .width(Length::Fill) - .style(theme::Button::TransparentBorder) - }, - move || { - Into::>::into( + .horizontal_scroll(scrollable::Properties::new().width(2).scroller_width(2)), + ) + .padding(15) + } else { + Container::new(Collapse::new( + move || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .spacing(20) + .push(p1_bold("Status")) + .push( + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(icon::circle_cross_icon().style(color::RED)) + .push(text("Not ready").style(color::RED)) + .width(Length::Fill), + ) + .push(icon::collapse_icon()), + ) + .padding(15) + .width(Length::Fill) + .style(theme::Button::TransparentBorder) + }, + move || { + Button::new( + Row::new() + .align_items(Alignment::Center) + .spacing(20) + .push(p1_bold("Status")) + .push( + Row::new() + .spacing(5) + .align_items(Alignment::Center) + .push(icon::circle_cross_icon().style(color::RED)) + .push(text("Not ready").style(color::RED)) + .width(Length::Fill), + ) + .push(icon::collapsed_icon()), + ) + .padding(15) + .width(Length::Fill) + .style(theme::Button::TransparentBorder) + }, + move || { + Into::>::into( Column::new() .padding(15) .spacing(10) - .push(text(if !tx.sigs.recovery_paths().is_empty() { - "Multiple spending paths are available. Finalizing this transaction requires either:" + .push(text("Finalizing this transaction requires:")) + .push_maybe(if tx.sigs.recovery_paths().is_empty() { + Some(path_view( + desc_info.primary_path(), + tx.sigs.primary_path(), + keys_aliases, + )) } else { - "1 spending path is available. Finalizing this transaction requires:" - })) - .push(path_view( - desc_info.primary_path(), - tx.sigs.primary_path(), - keys_aliases, - )) - .push(tx.sigs.recovery_paths().iter().fold(Column::new().spacing(10), |col, (seq, path)| { - let keys = &desc_info.recovery_paths()[seq]; - col.push(path_view(keys, path, keys_aliases)) - })), - ) - }, - ))}) + tx.sigs.recovery_paths().iter().last().map(|(seq, path)| { + let keys = &desc_info.recovery_paths()[seq]; + path_view(keys, path, keys_aliases) + }) + }), + ) + }, + )) + }) .into() } @@ -473,28 +473,35 @@ pub fn path_view<'a>( .push_maybe(if keys.is_empty() { None } else { - Some(keys.iter().fold(Row::new().spacing(5), |row, value| { - row.push_maybe(if !sigs.signed_pubkeys.contains_key(&value.0) { - Some(if let Some(alias) = key_aliases.get(&value.0) { - Container::new( - tooltip::Tooltip::new( - Container::new(text(alias)) - .padding(10) - .style(theme::Container::Pill(theme::Pill::Simple)), - value.0.to_string(), - tooltip::Position::Bottom, - ) - .style(theme::Container::Card(theme::Card::Simple)), + Some( + keys.iter() + .fold(Row::new().spacing(5), |row, (key_fg, paths)| { + row.push_maybe( + if !sigs.signed_pubkeys.iter().any(|(fg, &total_sigs)| { + fg == key_fg && paths.len() == total_sigs + }) { + Some(if let Some(alias) = key_aliases.get(key_fg) { + Container::new( + tooltip::Tooltip::new( + Container::new(text(alias)).padding(10).style( + theme::Container::Pill(theme::Pill::Simple), + ), + key_fg.to_string(), + tooltip::Position::Bottom, + ) + .style(theme::Container::Card(theme::Card::Simple)), + ) + } else { + Container::new(text(key_fg.to_string())) + .padding(10) + .style(theme::Container::Pill(theme::Pill::Simple)) + }) + } else { + None + }, ) - } else { - Container::new(text(value.0.to_string())) - .padding(10) - .style(theme::Container::Pill(theme::Pill::Simple)) - }) - } else { - None - }) - })) + }), + ) }) .push_maybe(if sigs.signed_pubkeys.is_empty() { None @@ -930,14 +937,9 @@ pub fn sign_action<'a>( .push( Column::new() .push( - Row::new() - .push( - text("Select signing device to sign with:") - .bold() - .width(Length::Fill), - ) - .push(button::secondary(None, "Refresh").on_press(Message::Reload)) - .align_items(Alignment::Center), + text("Select signing device to sign with:") + .bold() + .width(Length::Fill), ) .spacing(10) .push(hws.iter().enumerate().fold( diff --git a/gui/src/app/view/settings.rs b/gui/src/app/view/settings.rs index c8e12dfad..2b896b154 100644 --- a/gui/src/app/view/settings.rs +++ b/gui/src/app/view/settings.rs @@ -656,12 +656,7 @@ pub fn register_wallet_modal<'a>( Column::new() .push( Column::new() - .push( - Row::new() - .push(text("Select device:").bold().width(Length::Fill)) - .push(button::secondary(None, "Refresh").on_press(Message::Reload)) - .align_items(Alignment::Center), - ) + .push(text("Select device:").bold().width(Length::Fill)) .spacing(10) .push(hws.iter().enumerate().fold( Column::new().spacing(10), @@ -673,7 +668,13 @@ pub fn register_wallet_modal<'a>( processing, hw.fingerprint() .map(|f| registered.contains(&f)) - .unwrap_or(false), + .unwrap_or(false) + || if let HardwareWallet::Supported { registered, .. } = hw + { + registered == &Some(true) + } else { + false + }, )) }, )) diff --git a/gui/src/app/wallet.rs b/gui/src/app/wallet.rs index f52f22ed2..02f4593ba 100644 --- a/gui/src/app/wallet.rs +++ b/gui/src/app/wallet.rs @@ -14,6 +14,20 @@ use liana::miniscript::bitcoin::bip32::Fingerprint; pub const DEFAULT_WALLET_NAME: &str = "Liana"; +pub fn wallet_name(main_descriptor: &LianaDescriptor) -> String { + let desc = main_descriptor.to_string(); + let checksum = desc + .split_once('#') + .map(|(_, checksum)| checksum) + .unwrap_or(""); + format!( + "{}{}{}", + DEFAULT_WALLET_NAME, + if checksum.is_empty() { "" } else { "-" }, + checksum + ) +} + #[derive(Debug)] pub struct Wallet { pub name: String, @@ -26,7 +40,7 @@ pub struct Wallet { impl Wallet { pub fn new(main_descriptor: LianaDescriptor) -> Self { Self { - name: DEFAULT_WALLET_NAME.to_string(), + name: wallet_name(&main_descriptor), main_descriptor, keys_aliases: HashMap::new(), hardware_wallets: Vec::new(), diff --git a/gui/src/daemon/mod.rs b/gui/src/daemon/mod.rs index 1b680e303..12e46f3a0 100644 --- a/gui/src/daemon/mod.rs +++ b/gui/src/daemon/mod.rs @@ -100,19 +100,14 @@ pub trait Daemon: Debug { }) .cloned() .collect(); - let sigs = info - .descriptors - .main - .partial_spend_info(&tx.psbt) - .map_err(|e| DaemonError::Unexpected(e.to_string()))?; + spend_txs.push(model::SpendTx::new( tx.updated_at, tx.psbt, coins, - sigs, - info.descriptors.main.max_sat_vbytes(), + &info.descriptors.main, info.network, - )) + )); } load_labels(self, &mut spend_txs)?; spend_txs.sort_by(|a, b| { diff --git a/gui/src/daemon/model.rs b/gui/src/daemon/model.rs index 005ab8516..f18616d94 100644 --- a/gui/src/daemon/model.rs +++ b/gui/src/daemon/model.rs @@ -1,14 +1,17 @@ use std::collections::{HashMap, HashSet}; +use liana::descriptors::LianaDescriptor; pub use liana::{ commands::{ CreateSpendResult, GetAddressResult, GetInfoResult, GetLabelsResult, LabelItem, ListCoinsEntry, ListCoinsResult, ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo, }, - descriptors::{PartialSpendInfo, PathSpendInfo}, + descriptors::{LianaPolicy, PartialSpendInfo, PathSpendInfo}, miniscript::bitcoin::{ - bip32::Fingerprint, psbt::Psbt, Address, Amount, Network, OutPoint, Transaction, Txid, + bip32::{DerivationPath, Fingerprint}, + psbt::Psbt, + Address, Amount, Network, OutPoint, Transaction, Txid, }, }; @@ -57,10 +60,10 @@ impl SpendTx { updated_at: Option, psbt: Psbt, coins: Vec, - sigs: PartialSpendInfo, - max_sat_vbytes: usize, + desc: &LianaDescriptor, network: Network, ) -> Self { + let max_sat_vbytes = desc.max_sat_vbytes(); let mut change_indexes = Vec::new(); let (change_amount, spend_amount) = psbt.unsigned_tx.output.iter().enumerate().fold( (Amount::from_sat(0), Amount::from_sat(0)), @@ -90,6 +93,9 @@ impl SpendTx { } } } + let sigs = desc + .partial_spend_info(&psbt) + .expect("PSBT must be generated by Liana"); Self { labels: HashMap::new(), diff --git a/gui/src/hw.rs b/gui/src/hw.rs index 9b19e993f..de2a21005 100644 --- a/gui/src/hw.rs +++ b/gui/src/hw.rs @@ -1,20 +1,38 @@ -use std::{collections::HashMap, sync::Arc}; +use iced::Command; +use std::{ + collections::HashMap, + path::PathBuf, + sync::{Arc, Mutex}, +}; -use crate::app::wallet::Wallet; -use async_hwi::{ledger, specter, DeviceKind, Error as HWIError, Version, HWI}; -use liana::miniscript::bitcoin::{bip32::Fingerprint, hashes::hex::FromHex}; +use crate::app::{settings, wallet::Wallet}; +use async_hwi::{ + bitbox::{api::runtime, BitBox02, PairingBitbox02}, + ledger, specter, DeviceKind, Error as HWIError, Version, HWI, +}; +use liana::miniscript::bitcoin::{bip32::Fingerprint, hashes::hex::FromHex, Network}; use serde::{Deserialize, Serialize}; use tracing::{debug, warn}; +// Todo drop the Clone, to remove the Mutex on HardwareWallet::Locked #[derive(Debug, Clone)] pub enum HardwareWallet { Unsupported { + id: String, kind: DeviceKind, version: Option, message: String, }, + Locked { + id: String, + // None if the device is currently unlocking in a command. + device: Arc>>, + pairing_code: Option, + kind: DeviceKind, + }, Supported { - device: Arc, + id: String, + device: Arc, kind: DeviceKind, fingerprint: Fingerprint, version: Option, @@ -23,8 +41,19 @@ pub enum HardwareWallet { }, } +pub enum LockedDevice { + BitBox02(PairingBitbox02), +} + +impl std::fmt::Debug for LockedDevice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WaitingConfirmBitBox").finish() + } +} + impl HardwareWallet { async fn new( + id: String, device: Arc, aliases: Option<&HashMap>, ) -> Result { @@ -32,6 +61,7 @@ impl HardwareWallet { let fingerprint = device.get_master_fingerprint().await?; let version = device.get_version().await.ok(); Ok(Self::Supported { + id, device, kind, fingerprint, @@ -41,8 +71,17 @@ impl HardwareWallet { }) } + fn id(&self) -> &String { + match self { + Self::Locked { id, .. } => id, + Self::Unsupported { id, .. } => id, + Self::Supported { id, .. } => id, + } + } + pub fn kind(&self) -> &DeviceKind { match self { + Self::Locked { kind, .. } => kind, Self::Unsupported { kind, .. } => kind, Self::Supported { kind, .. } => kind, } @@ -50,6 +89,7 @@ impl HardwareWallet { pub fn fingerprint(&self) -> Option { match self { + Self::Locked { .. } => None, Self::Unsupported { .. } => None, Self::Supported { fingerprint, .. } => Some(*fingerprint), } @@ -83,256 +123,440 @@ impl HardwareWalletConfig { } } -pub async fn list_hardware_wallets(wallet: &Wallet) -> Vec { - let descriptor = wallet.main_descriptor.to_string(); - let mut hws: Vec = Vec::new(); - match specter::SpecterSimulator::try_connect().await { - Ok(device) => match HardwareWallet::new(Arc::new(device), Some(&wallet.keys_aliases)).await - { - Ok(hw) => hws.push(hw), - Err(e) => { - debug!("{}", e); - } - }, - Err(HWIError::DeviceNotFound) => {} - Err(e) => { - debug!("{}", e); +#[derive(Debug, Clone)] +pub enum HardwareWalletMessage { + Error(String), + List(ConnectedList), + Unlocked(String, Result), +} + +#[derive(Debug, Clone)] +pub struct ConnectedList { + pub new: Vec, + still: Vec, +} + +pub struct HardwareWallets { + network: Network, + pub list: Vec, + pub aliases: HashMap, + wallet: Option>, + datadir_path: PathBuf, +} + +impl std::fmt::Debug for HardwareWallets { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WaitingConfirmBitBox").finish() + } +} + +impl HardwareWallets { + pub fn new(datadir_path: PathBuf, network: Network) -> Self { + Self { + network, + list: Vec::new(), + aliases: HashMap::new(), + wallet: None, + datadir_path, } } - match specter::Specter::enumerate().await { - Ok(devices) => { - for device in devices { - match HardwareWallet::new(Arc::new(device), Some(&wallet.keys_aliases)).await { - Ok(hw) => hws.push(hw), - Err(e) => { - debug!("{}", e); - } + + pub fn with_wallet(mut self, wallet: Arc) -> Self { + self.aliases = wallet.keys_aliases.clone(); + self.wallet = Some(wallet); + self + } + + pub fn set_alias(&mut self, fg: Fingerprint, new_alias: String) { + // remove all (fingerprint, alias) with same alias. + self.aliases.retain(|_, a| *a != new_alias); + for hw in &mut self.list { + if let HardwareWallet::Supported { + fingerprint, alias, .. + } = hw + { + if *fingerprint == fg { + *alias = Some(new_alias.clone()); + } else if alias.as_ref() == Some(&new_alias) { + *alias = None; } } } - Err(e) => warn!("Error while listing specter wallets: {}", e), + self.aliases.insert(fg, new_alias); } - match ledger::LedgerSimulator::try_connect().await { - Ok(mut device) => match device.get_master_fingerprint().await { - Ok(fingerprint) => { - let version = device.get_version().await.ok(); - if ledger_version_supported(version.as_ref()) { - let mut registered = false; - if let Some(cfg) = wallet - .hardware_wallets - .iter() - .find(|cfg| cfg.fingerprint == fingerprint) - { - device = device - .with_wallet(&wallet.name, &descriptor, Some(cfg.token())) - .expect("Configuration must be correct"); - registered = true; + + pub fn load_aliases(&mut self, aliases: HashMap) { + self.aliases = aliases; + } + + pub fn set_network(&mut self, network: Network) { + self.network = network; + } + + pub fn update( + &mut self, + message: HardwareWalletMessage, + ) -> Result, async_hwi::Error> { + match message { + HardwareWalletMessage::Error(e) => Err(async_hwi::Error::Device(e)), + HardwareWalletMessage::List(ConnectedList { still, mut new }) => { + // remove disconnected + self.list.retain(|hw| still.contains(hw.id())); + self.list.append(&mut new); + let mut cmds = Vec::new(); + for hw in &mut self.list { + match hw { + HardwareWallet::Supported { + fingerprint, alias, .. + } => { + *alias = self.aliases.get(fingerprint).cloned(); + } + HardwareWallet::Locked { device, id, .. } => { + if let Some(LockedDevice::BitBox02(bb)) = device.lock().unwrap().take() + { + let id = id.to_string(); + let id_cloned = id.clone(); + let network = self.network; + let wallet = self.wallet.clone(); + cmds.push(Command::perform( + async move { + let paired_bb = bb.wait_confirm().await?; + let mut bitbox2 = + BitBox02::from(paired_bb).with_network(network); + let fingerprint = bitbox2.get_master_fingerprint().await?; + let mut registered = false; + if let Some(wallet) = wallet { + let desc = wallet.main_descriptor.to_string(); + bitbox2 = bitbox2.with_policy(&desc)?; + registered = + bitbox2.is_policy_registered(&desc).await?; + } + Ok(HardwareWallet::Supported { + id: id.clone(), + kind: DeviceKind::BitBox02, + fingerprint, + device: bitbox2.into(), + version: None, + registered: Some(registered), + alias: None, + }) + }, + |res| HardwareWalletMessage::Unlocked(id_cloned, res), + )); + } + } + _ => {} } - hws.push(HardwareWallet::Supported { - kind: device.device_kind(), - fingerprint, - device: Arc::new(device), - version, - registered: Some(registered), - alias: wallet.keys_aliases.get(&fingerprint).cloned(), - }); + } + if cmds.is_empty() { + Ok(Command::none()) } else { - hws.push(HardwareWallet::Unsupported { - kind: device.device_kind(), - version, - message: "Minimal supported app version is 2.1.0".to_string(), - }); + Ok(Command::batch(cmds)) } } - Err(_) => { - hws.push(HardwareWallet::Unsupported { - kind: device.device_kind(), - version: None, - message: "Minimal supported app version is 2.1.0".to_string(), - }); + HardwareWalletMessage::Unlocked(id, res) => { + match res { + Err(_) => { + warn!("Pairing failed with an external device"); + self.list.retain(|hw| hw.id() != &id); + } + Ok(hw) => { + if let Some(h) = self.list.iter_mut().find(|hw1| { + if let HardwareWallet::Locked { id, .. } = hw1 { + id == hw.id() + } else { + false + } + }) { + *h = hw; + if let HardwareWallet::Supported { + fingerprint, alias, .. + } = h + { + *alias = self.aliases.get(fingerprint).cloned(); + } + } + } + } + Ok(Command::none()) + } + } + } + + pub fn refresh(&self) -> iced::Subscription { + iced::subscription::unfold( + format!("refresh-{}", self.network), + State { + keys_aliases: self.aliases.clone(), + wallet: self.wallet.clone(), + connected_supported_hws: Vec::new(), + api: None, + datadir_path: self.datadir_path.clone(), + }, + refresh, + ) + } +} + +struct State { + keys_aliases: HashMap, + wallet: Option>, + connected_supported_hws: Vec, + api: Option, + datadir_path: PathBuf, +} + +async fn refresh(mut state: State) -> (HardwareWalletMessage, State) { + let api = if let Some(api) = &mut state.api { + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + if let Err(e) = api.refresh_devices() { + return (HardwareWalletMessage::Error(e.to_string()), state); + }; + api + } else { + match ledger::HidApi::new() { + Ok(api) => { + state.api = Some(api); + state.api.as_mut().unwrap() + } + Err(e) => { + return (HardwareWalletMessage::Error(e.to_string()), state); } - }, + } + }; + + let mut hws: Vec = Vec::new(); + let mut still: Vec = Vec::new(); + match specter::SpecterSimulator::try_connect().await { + Ok(device) => { + let id = "specter-simulator".to_string(); + if state.connected_supported_hws.contains(&id) { + still.push(id); + } else { + match HardwareWallet::new(id, Arc::new(device), Some(&state.keys_aliases)).await { + Ok(hw) => hws.push(hw), + Err(e) => { + debug!("{}", e); + } + } + } + } Err(HWIError::DeviceNotFound) => {} Err(e) => { debug!("{}", e); } } - match ledger::HidApi::new() { - Err(e) => { - debug!("{}", e); + + match specter::SerialTransport::enumerate_potential_ports() { + Ok(ports) => { + for port in ports { + let id = format!("specter-{}", port); + if state.connected_supported_hws.contains(&id) { + still.push(id); + } else { + let device = specter::Specter::::new(port.clone()); + if device.is_connected().await.is_ok() { + match HardwareWallet::new(id, Arc::new(device), Some(&state.keys_aliases)) + .await + { + Ok(hw) => hws.push(hw), + Err(e) => { + debug!("{}", e); + } + } + } + } + } } - Ok(api) => { - for detected in ledger::Ledger::::enumerate(&api) { - match ledger::Ledger::::connect(&api, detected) { - Ok(mut device) => match device.get_master_fingerprint().await { - Ok(fingerprint) => { - let version = device.get_version().await.ok(); - if ledger_version_supported(version.as_ref()) { - let mut registered = false; - if let Some(cfg) = wallet + Err(e) => warn!("Error while listing specter wallets: {}", e), + } + match ledger::LedgerSimulator::try_connect().await { + Ok(mut device) => { + let id = "ledger-simulator".to_string(); + if state.connected_supported_hws.contains(&id) { + still.push(id); + } else { + match device.get_master_fingerprint().await { + Ok(fingerprint) => { + let version = device.get_version().await.ok(); + if ledger_version_supported(version.as_ref()) { + let mut registered = false; + if let Some(w) = &state.wallet { + if let Some(cfg) = w .hardware_wallets .iter() .find(|cfg| cfg.fingerprint == fingerprint) { device = device - .with_wallet(&wallet.name, &descriptor, Some(cfg.token())) + .with_wallet( + &w.name, + &w.main_descriptor.to_string(), + Some(cfg.token()), + ) .expect("Configuration must be correct"); registered = true; } - hws.push(HardwareWallet::Supported { - kind: device.device_kind(), - fingerprint, - device: Arc::new(device), - version, - registered: Some(registered), - alias: wallet.keys_aliases.get(&fingerprint).cloned(), - }); - } else { - hws.push(HardwareWallet::Unsupported { - kind: device.device_kind(), - version, - message: "Minimal supported app version is 2.1.0".to_string(), - }); } - } - Err(_) => { + hws.push(HardwareWallet::Supported { + id, + kind: device.device_kind(), + fingerprint, + device: Arc::new(device), + version, + registered: Some(registered), + alias: state.keys_aliases.get(&fingerprint).cloned(), + }); + } else { hws.push(HardwareWallet::Unsupported { + id, kind: device.device_kind(), - version: None, + version, message: "Minimal supported app version is 2.1.0".to_string(), }); } - }, - Err(HWIError::DeviceNotFound) => {} - Err(e) => { - debug!("{}", e); + } + Err(_) => { + hws.push(HardwareWallet::Unsupported { + id, + kind: device.device_kind(), + version: None, + message: "Minimal supported app version is 2.1.0".to_string(), + }); } } } } - } - hws -} - -fn ledger_version_supported(version: Option<&Version>) -> bool { - if let Some(version) = version { - if version.major >= 2 { - if version.major == 2 { - version.minor >= 1 - } else { - true - } - } else { - false - } - } else { - false - } -} - -pub async fn list_unregistered_hardware_wallets() -> Vec { - let mut hws: Vec = Vec::new(); - match specter::SpecterSimulator::try_connect().await { - Ok(device) => match HardwareWallet::new(Arc::new(device), None).await { - Ok(hw) => hws.push(hw), - Err(e) => { - debug!("{}", e); - } - }, Err(HWIError::DeviceNotFound) => {} Err(e) => { debug!("{}", e); } } - match specter::Specter::enumerate().await { - Ok(devices) => { - for device in devices { - match HardwareWallet::new(Arc::new(device), None).await { - Ok(hw) => hws.push(hw), - Err(e) => { - debug!("{}", e); - } + + for device_info in api.device_list() { + if async_hwi::bitbox::is_bitbox02(device_info) { + let id = format!( + "bitbox-{:?}-{}-{}", + device_info.path(), + device_info.vendor_id(), + device_info.product_id() + ); + if state.connected_supported_hws.contains(&id) { + still.push(id); + continue; + } + if let Ok(device) = device_info.open_device(api) { + if let Ok(device) = PairingBitbox02::connect( + device, + Some(Box::new(settings::global::PersistedBitboxNoiseConfig::new( + &state.datadir_path, + ))), + ) + .await + { + hws.push(HardwareWallet::Locked { + id, + kind: DeviceKind::BitBox02, + pairing_code: device.pairing_code().map(|s| s.replace('\n', " ")), + device: Arc::new(Mutex::new(Some(LockedDevice::BitBox02(device)))), + }); } } } - Err(e) => warn!("Error while listing specter wallets: {}", e), } - match ledger::LedgerSimulator::try_connect().await { - Ok(device) => match device.get_master_fingerprint().await { - Ok(fingerprint) => { - let version = device.get_version().await.ok(); - if ledger_version_supported(version.as_ref()) { - hws.push(HardwareWallet::Supported { - kind: device.device_kind(), - fingerprint, - device: Arc::new(device), - version, - registered: None, - alias: None, - }); - } else { + for detected in ledger::Ledger::::enumerate(api) { + let id = format!( + "ledger-{:?}-{}-{}", + detected.path(), + detected.vendor_id(), + detected.product_id() + ); + if state.connected_supported_hws.contains(&id) { + still.push(id); + continue; + } + match ledger::Ledger::::connect(api, detected) { + Ok(mut device) => match device.get_master_fingerprint().await { + Ok(fingerprint) => { + let version = device.get_version().await.ok(); + if ledger_version_supported(version.as_ref()) { + let mut registered = false; + if let Some(w) = &state.wallet { + if let Some(cfg) = w + .hardware_wallets + .iter() + .find(|cfg| cfg.fingerprint == fingerprint) + { + device = device + .with_wallet( + &w.name, + &w.main_descriptor.to_string(), + Some(cfg.token()), + ) + .expect("Configuration must be correct"); + registered = true; + } + } + hws.push(HardwareWallet::Supported { + id, + kind: device.device_kind(), + fingerprint, + device: Arc::new(device), + version, + registered: Some(registered), + alias: state.keys_aliases.get(&fingerprint).cloned(), + }); + } else { + hws.push(HardwareWallet::Unsupported { + id, + kind: device.device_kind(), + version, + message: "Minimal supported app version is 2.1.0".to_string(), + }); + } + } + Err(_) => { hws.push(HardwareWallet::Unsupported { + id, kind: device.device_kind(), - version, + version: None, message: "Minimal supported app version is 2.1.0".to_string(), }); } + }, + Err(HWIError::DeviceNotFound) => {} + Err(e) => { + debug!("{}", e); } - Err(_) => { - hws.push(HardwareWallet::Unsupported { - kind: device.device_kind(), - version: None, - message: "Minimal supported app version is 2.1.0".to_string(), - }); - } - }, - Err(HWIError::DeviceNotFound) => {} - Err(e) => { - debug!("{}", e); } } - match ledger::HidApi::new() { - Err(e) => { - debug!("{}", e); - } - Ok(api) => { - for detected in ledger::Ledger::::enumerate(&api) { - match ledger::Ledger::::connect(&api, detected) { - Ok(device) => match device.get_master_fingerprint().await { - Ok(fingerprint) => { - let version = device.get_version().await.ok(); - if ledger_version_supported(version.as_ref()) { - hws.push(HardwareWallet::Supported { - kind: device.device_kind(), - fingerprint, - device: Arc::new(device), - version, - registered: None, - alias: None, - }); - } else { - hws.push(HardwareWallet::Unsupported { - kind: device.device_kind(), - version, - message: "Minimal supported app version is 2.1.0".to_string(), - }); - } - } - Err(_) => { - hws.push(HardwareWallet::Unsupported { - kind: device.device_kind(), - version: None, - message: "Minimal supported app version is 2.1.0".to_string(), - }); - } - }, - Err(HWIError::DeviceNotFound) => {} - Err(e) => { - debug!("{}", e); - } - } + + state.connected_supported_hws = still + .iter() + .chain(hws.iter().filter_map(|hw| match hw { + HardwareWallet::Locked { id, .. } => Some(id), + HardwareWallet::Supported { id, .. } => Some(id), + HardwareWallet::Unsupported { .. } => None, + })) + .cloned() + .collect(); + ( + HardwareWalletMessage::List(ConnectedList { new: hws, still }), + state, + ) +} + +fn ledger_version_supported(version: Option<&Version>) -> bool { + if let Some(version) = version { + if version.major >= 2 { + if version.major == 2 { + version.minor >= 1 + } else { + true } + } else { + false } + } else { + false } - hws } diff --git a/gui/src/installer/message.rs b/gui/src/installer/message.rs index 05b2dd06e..66cb21e73 100644 --- a/gui/src/installer/message.rs +++ b/gui/src/installer/message.rs @@ -5,7 +5,7 @@ use liana::miniscript::{ use std::path::PathBuf; use super::Error; -use crate::{bitcoind::Bitcoind, download::Progress, hw::HardwareWallet}; +use crate::{bitcoind::Bitcoind, download::Progress, hw::HardwareWalletMessage}; use async_hwi::DeviceKind; #[derive(Debug, Clone)] @@ -30,8 +30,8 @@ pub enum Message { InternalBitcoind(InternalBitcoindMsg), DefineBitcoind(DefineBitcoind), DefineDescriptor(DefineDescriptor), - ImportXpub(usize, Result), - ConnectedHardwareWallets(Vec), + ImportXpub(Fingerprint, Result), + HardwareWallets(HardwareWalletMessage), WalletRegistered(Result<(Fingerprint, Option<[u8; 32]>), Error>), MnemonicWord(usize, String), ImportMnemonic(bool), diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index 04ff4e221..d24701cb5 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -17,6 +17,7 @@ use std::sync::{Arc, Mutex}; use crate::{ app::{config as gui_config, settings as gui_settings}, + hw::HardwareWallets, signer::Signer, }; @@ -30,6 +31,7 @@ use step::{ pub struct Installer { current: usize, steps: Vec>, + hws: HardwareWallets, signer: Arc>, /// Context is data passed through each step. @@ -60,6 +62,7 @@ impl Installer { ( Installer { current: 0, + hws: HardwareWallets::new(destination_path.clone(), network), steps: vec![Welcome::default().into()], context: Context::new(network, destination_path), signer: Arc::new(Mutex::new(Signer::generate(network).unwrap())), @@ -69,10 +72,17 @@ impl Installer { } pub fn subscription(&self) -> Subscription { - self.steps - .get(self.current) - .expect("There is always a step") - .subscription() + if self.current > 0 { + Subscription::batch(vec![ + self.hws.refresh().map(Message::HardwareWallets), + self.steps + .get(self.current) + .expect("There is always a step") + .subscription(), + ]) + } else { + Subscription::none() + } } pub fn stop(&mut self) { @@ -163,6 +173,13 @@ impl Installer { ]; self.next() } + Message::HardwareWallets(msg) => match self.hws.update(msg) { + Ok(cmd) => cmd.map(Message::HardwareWallets), + Err(e) => { + error!("{}", e); + Command::none() + } + }, Message::Clibpboard(s) => clipboard::write(s), Message::Next => self.next(), Message::Previous => { @@ -174,7 +191,7 @@ impl Installer { .steps .get_mut(self.current) .expect("There is always a step") - .update(message); + .update(&mut self.hws, message); Command::perform( install(self.context.clone(), self.signer.clone()), Message::Installed, @@ -201,13 +218,13 @@ impl Installer { self.steps .get_mut(self.current) .expect("There is always a step") - .update(Message::Installed(Err(e))) + .update(&mut self.hws, Message::Installed(Err(e))) } _ => self .steps .get_mut(self.current) .expect("There is always a step") - .update(message), + .update(&mut self.hws, message), } } @@ -232,7 +249,7 @@ impl Installer { self.steps .get(self.current) .expect("There is always a step") - .view(self.progress()) + .view(&self.hws, self.progress()) } } diff --git a/gui/src/installer/step/bitcoind.rs b/gui/src/installer/step/bitcoind.rs index abc9da365..2c48abcbe 100644 --- a/gui/src/installer/step/bitcoind.rs +++ b/gui/src/installer/step/bitcoind.rs @@ -24,6 +24,7 @@ use crate::{ Bitcoind, StartInternalBitcoindError, }, download, + hw::HardwareWallets, installer::{ context::Context, message::{self, Message}, @@ -434,7 +435,7 @@ impl SelectBitcoindTypeStep { } impl Step for SelectBitcoindTypeStep { - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { if let Message::SelectBitcoindType(msg) = message { match msg { message::SelectBitcoindTypeMsg::UseExternal(selected) => { @@ -458,7 +459,7 @@ impl Step for SelectBitcoindTypeStep { true } - fn view(&self, progress: (usize, usize)) -> Element { + fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element { view::select_bitcoind_type(progress) } } @@ -510,7 +511,7 @@ impl Step for DefineBitcoind { self.address.value = bitcoind_default_address(&ctx.bitcoin_config.network); } } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { if let Message::DefineBitcoind(msg) = message { match msg { message::DefineBitcoind::PingBitcoind => { @@ -561,7 +562,7 @@ impl Step for DefineBitcoind { } } - fn view(&self, progress: (usize, usize)) -> Element { + fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element { view::define_bitcoin( progress, &self.address, @@ -646,7 +647,7 @@ impl Step for InternalBitcoindStep { } } } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { if let Message::InternalBitcoind(msg) = message { match msg { message::InternalBitcoindMsg::Previous => { @@ -854,7 +855,7 @@ impl Step for InternalBitcoindStep { false } - fn view(&self, progress: (usize, usize)) -> Element { + fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element { view::start_internal_bitcoind( progress, self.exe_path.as_ref(), diff --git a/gui/src/installer/step/descriptor.rs b/gui/src/installer/step/descriptor.rs index 600e1dc8e..7816c4d53 100644 --- a/gui/src/installer/step/descriptor.rs +++ b/gui/src/installer/step/descriptor.rs @@ -27,8 +27,8 @@ use liana_ui::{ use async_hwi::DeviceKind; use crate::{ - app::settings::KeySetting, - hw::{list_unregistered_hardware_wallets, HardwareWallet}, + app::{settings::KeySetting, wallet::wallet_name}, + hw::{HardwareWallet, HardwareWallets}, installer::{ message::{self, Message}, step::{Context, Step}, @@ -41,10 +41,10 @@ pub trait DescriptorEditModal { fn processing(&self) -> bool { false } - fn update(&mut self, _message: Message) -> Command { + fn update(&mut self, _hws: &mut HardwareWallets, _message: Message) -> Command { Command::none() } - fn view(&self) -> Element; + fn view<'a>(&'a self, _hws: &'a HardwareWallets) -> Element<'a, Message>; } pub struct RecoveryPath { @@ -210,14 +210,17 @@ impl DefineDescriptor { 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 { + fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command { let network = self.network; self.error = None; match message { Message::Close => { self.modal = None; } - Message::Network(network) => self.set_network(network), + Message::Network(network) => { + hws.set_network(network); + self.set_network(network) + } Message::DefineDescriptor(message::DefineDescriptor::AddRecoveryPath) => { self.setup_mut().recovery_paths.push(RecoveryPath::new()); } @@ -235,6 +238,7 @@ impl Step for DefineDescriptor { } message::DefineKey::Edited(name, imported_key, kind) => { let fingerprint = imported_key.master_fingerprint(); + hws.set_alias(fingerprint, name.clone()); if let Some(key) = self .setup_mut() .keys @@ -325,6 +329,7 @@ impl Step for DefineDescriptor { } message::DefineKey::Edited(name, imported_key, kind) => { let fingerprint = imported_key.master_fingerprint(); + hws.set_alias(fingerprint, name.clone()); if let Some(key) = self .setup_mut() .keys @@ -391,7 +396,7 @@ impl Step for DefineDescriptor { }, _ => { if let Some(modal) = &mut self.modal { - return modal.update(message); + return modal.update(hws, message); } } }; @@ -498,7 +503,11 @@ impl Step for DefineDescriptor { true } - fn view(&self, progress: (usize, usize)) -> Element { + fn view<'a>( + &'a self, + hws: &'a HardwareWallets, + progress: (usize, usize), + ) -> Element<'a, Message> { let aliases = self.setup[&self.network].keys_aliases(); let content = view::define_descriptor( progress, @@ -542,7 +551,7 @@ impl Step for DefineDescriptor { self.error.as_ref(), ); if let Some(modal) = &self.modal { - Modal::new(content, modal.view()) + Modal::new(content, modal.view(hws)) .on_blur(if modal.processing() { None } else { @@ -627,7 +636,7 @@ impl DescriptorEditModal for EditSequenceModal { false } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { if let Message::DefineDescriptor(message::DefineDescriptor::SequenceModal(msg)) = message { match msg { message::SequenceModal::SequenceEdited(seq) => { @@ -660,7 +669,7 @@ impl DescriptorEditModal for EditSequenceModal { Command::none() } - fn view(&self) -> Element { + fn view(&self, _hws: &HardwareWallets) -> Element { view::edit_sequence_modal(&self.sequence) } } @@ -681,7 +690,6 @@ pub struct EditXpubModal { duplicate_master_fg: bool, keys: Vec, - hws: Vec, hot_signer: Arc>, hot_signer_fingerprint: Fingerprint, chosen_signer: Option<(Fingerprint, Option)>, @@ -729,7 +737,6 @@ impl EditXpubModal { path_index, key_index, processing: false, - hws: Vec::new(), error: None, network, edit_name: false, @@ -740,10 +747,7 @@ impl EditXpubModal { } } fn load(&self) -> Command { - Command::perform( - async move { list_unregistered_hardware_wallets().await }, - Message::ConnectedHardwareWallets, - ) + Command::none() } } @@ -752,7 +756,7 @@ impl DescriptorEditModal for EditXpubModal { self.processing } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command { // Reset these fields. // the fonction will setup them again if something is wrong self.duplicate_master_fg = false; @@ -764,7 +768,7 @@ impl DescriptorEditModal for EditXpubModal { fingerprint, kind, .. - }) = self.hws.get(i) + }) = hws.list.get(i) { self.chosen_signer = Some((*fingerprint, Some(*kind))); self.processing = true; @@ -778,19 +782,7 @@ impl DescriptorEditModal for EditXpubModal { ); } } - Message::ConnectedHardwareWallets(hws) => { - if let Ok(key) = DescriptorPublicKey::from_str(&self.form_xpub.value) { - self.chosen_signer = Some(( - key.master_fingerprint(), - hws.iter() - .find(|hw| hw.fingerprint() == Some(key.master_fingerprint())) - .map(|hw| *hw.kind()), - )); - } - self.hws = hws; - } Message::Reload => { - self.hws = Vec::new(); return self.load(); } Message::UseHotSigner => { @@ -937,11 +929,11 @@ impl DescriptorEditModal for EditXpubModal { }; Command::none() } - fn view(&self) -> Element { + fn view<'a>(&'a self, hws: &'a HardwareWallets) -> Element<'a, Message> { let chosen_signer = self.chosen_signer.map(|s| s.0); view::edit_key_modal( self.network, - self.hws + hws.list .iter() .enumerate() .filter_map(|(i, hw)| { @@ -1032,73 +1024,17 @@ async fn get_extended_pubkey( } pub struct HardwareWalletXpubs { - hw: HardwareWallet, + fingerprint: Fingerprint, xpubs: Vec, processing: bool, error: Option, - next_account: ChildNumber, } impl HardwareWalletXpubs { - fn new(hw: HardwareWallet) -> Self { - Self { - hw, - xpubs: Vec::new(), - processing: false, - error: None, - next_account: ChildNumber::from_hardened_idx(0).unwrap(), - } - } - - fn update(&mut self, res: Result) { - self.processing = false; - match res { - Err(e) => { - self.error = e.into(); - } - Ok(xpub) => { - self.error = None; - self.next_account = self.next_account.increment().unwrap(); - self.xpubs.push(xpub.to_string()); - } - } - } - fn reset(&mut self) { self.error = None; - self.next_account = ChildNumber::from_hardened_idx(0).unwrap(); self.xpubs = Vec::new(); } - - fn select(&mut self, i: usize, network: Network) -> Command { - if let HardwareWallet::Supported { - device, - fingerprint, - .. - } = &self.hw - { - let device = device.clone(); - let fingerprint = *fingerprint; - self.processing = true; - self.error = None; - Command::perform( - async move { (i, get_extended_pubkey(device, fingerprint, network).await) }, - |(i, res)| Message::ImportXpub(i, res), - ) - } else { - Command::none() - } - } - - pub fn view(&self, i: usize) -> Element { - view::hardware_wallet_xpubs( - i, - &self.xpubs, - &self.hw, - self.processing, - self.error.as_ref(), - ) - } } pub struct SignerXpubs { @@ -1125,12 +1061,13 @@ impl SignerXpubs { self.next_account = self.next_account.increment().unwrap(); let signer = self.signer.lock().unwrap(); let derivation_path = default_derivation_path(network); - self.xpubs.push(format!( + // We keep only one for the moment. + self.xpubs = vec![format!( "[{}{}]{}", signer.fingerprint(), derivation_path.to_string().trim_start_matches('m'), signer.get_extended_pubkey(&derivation_path) - )); + )]; } pub fn view(&self) -> Element { @@ -1145,7 +1082,7 @@ pub struct ParticipateXpub { shared: bool, - xpubs_hw: Vec, + hw_xpubs: Vec, xpubs_signer: SignerXpubs, } @@ -1155,7 +1092,7 @@ impl ParticipateXpub { network: Network::Bitcoin, network_valid: true, data_dir: None, - xpubs_hw: Vec::new(), + hw_xpubs: Vec::new(), shared: false, xpubs_signer: SignerXpubs::new(signer), } @@ -1163,7 +1100,7 @@ impl ParticipateXpub { fn set_network(&mut self, network: Network) { if network != self.network { - self.xpubs_hw.iter_mut().for_each(|hw| hw.reset()); + self.hw_xpubs.iter_mut().for_each(|hw| hw.reset()); self.xpubs_signer.reset(); } self.network = network; @@ -1182,40 +1119,67 @@ impl ParticipateXpub { impl Step for ParticipateXpub { // 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 { + fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command { match message { Message::Network(network) => { + hws.set_network(network); self.set_network(network); } Message::UserActionDone(shared) => self.shared = shared, - Message::ImportXpub(i, res) => { - if let Some(hw) = self.xpubs_hw.get_mut(i) { - hw.update(res); + Message::ImportXpub(fg, res) => { + if let Some(hw_xpubs) = self.hw_xpubs.iter_mut().find(|x| x.fingerprint == fg) { + hw_xpubs.processing = false; + match res { + Err(e) => { + hw_xpubs.error = e.into(); + } + Ok(xpub) => { + hw_xpubs.error = None; + // We keep only one for the moment. + hw_xpubs.xpubs = vec![xpub.to_string()]; + } + } } } Message::UseHotSigner => { self.xpubs_signer.select(self.network); } Message::Select(i) => { - if let Some(hw) = self.xpubs_hw.get_mut(i) { - return hw.select(i, self.network); - } - } - Message::ConnectedHardwareWallets(hws) => { - for hw in hws { - if let Some(xpub_hw) = self.xpubs_hw.iter_mut().find(|h| { - h.hw.kind() == hw.kind() - && (h.hw.fingerprint() == hw.fingerprint() || !h.hw.is_supported()) - }) { - xpub_hw.hw = hw; + if let Some(HardwareWallet::Supported { + device, + fingerprint, + .. + }) = hws.list.get(i) + { + let device = device.clone(); + let fingerprint = *fingerprint; + let network = self.network; + if let Some(hw_xpubs) = self + .hw_xpubs + .iter_mut() + .find(|x| x.fingerprint == fingerprint) + { + hw_xpubs.processing = true; + hw_xpubs.error = None; } else { - self.xpubs_hw.push(HardwareWalletXpubs::new(hw)); + self.hw_xpubs.push(HardwareWalletXpubs { + fingerprint, + xpubs: Vec::new(), + processing: true, + error: None, + }); } + return Command::perform( + async move { + ( + fingerprint, + get_extended_pubkey(device, fingerprint, network).await, + ) + }, + |(fingerprint, res)| Message::ImportXpub(fingerprint, res), + ); } } - Message::Reload => { - return self.load(); - } _ => {} }; Command::none() @@ -1226,29 +1190,38 @@ impl Step for ParticipateXpub { self.set_network(ctx.bitcoin_config.network); } - fn load(&self) -> Command { - Command::perform( - list_unregistered_hardware_wallets(), - Message::ConnectedHardwareWallets, - ) - } - fn apply(&mut self, ctx: &mut Context) -> bool { ctx.bitcoin_config.network = self.network; // Drop connections to hardware wallets. - self.xpubs_hw = Vec::new(); + self.hw_xpubs = Vec::new(); true } - fn view(&self, progress: (usize, usize)) -> Element { + fn view<'a>(&'a self, hws: &'a HardwareWallets, progress: (usize, usize)) -> Element { view::participate_xpub( progress, self.network, self.network_valid, - self.xpubs_hw + hws.list .iter() .enumerate() - .map(|(i, hw)| hw.view(i)) + .map(|(i, hw)| { + if let Some(hw_xpubs) = self + .hw_xpubs + .iter() + .find(|h| hw.fingerprint() == Some(h.fingerprint)) + { + view::hardware_wallet_xpubs( + i, + hw, + Some(&hw_xpubs.xpubs), + hw_xpubs.processing, + hw_xpubs.error.as_ref(), + ) + } else { + view::hardware_wallet_xpubs(i, hw, None, false, None) + } + }) .collect(), self.xpubs_signer.view(), self.shared, @@ -1301,7 +1274,7 @@ impl ImportDescriptor { impl Step for ImportDescriptor { // 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 { + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { match message { Message::Network(network) => { self.network = network; @@ -1354,7 +1327,7 @@ impl Step for ImportDescriptor { } } - fn view(&self, progress: (usize, usize)) -> Element { + fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element { view::import_descriptor( progress, self.change_network, @@ -1374,10 +1347,8 @@ impl From for Box { pub struct RegisterDescriptor { descriptor: Option, - keys_aliases: HashMap, processing: bool, chosen_hw: Option, - hws: Vec, hmacs: Vec<(Fingerprint, DeviceKind, Option<[u8; 32]>)>, registered: HashSet, error: Option, @@ -1394,10 +1365,8 @@ impl RegisterDescriptor { Self { created_desc, descriptor: Default::default(), - keys_aliases: Default::default(), processing: Default::default(), chosen_hw: Default::default(), - hws: Default::default(), hmacs: Default::default(), registered: Default::default(), error: Default::default(), @@ -1421,24 +1390,29 @@ impl Step for RegisterDescriptor { for key in ctx.keys.iter().filter(|k| !k.name.is_empty()) { map.insert(key.master_fingerprint, key.name.clone()); } - self.keys_aliases = map; } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command { match message { Message::Select(i) => { if let Some(HardwareWallet::Supported { device, fingerprint, .. - }) = self.hws.get(i) + }) = hws.list.get(i) { if !self.registered.contains(fingerprint) { - let descriptor = self.descriptor.as_ref().unwrap().to_string(); + let descriptor = self.descriptor.as_ref().unwrap(); + let name = wallet_name(descriptor); self.chosen_hw = Some(i); self.processing = true; self.error = None; return Command::perform( - register_wallet(device.clone(), *fingerprint, descriptor), + register_wallet( + device.clone(), + *fingerprint, + name, + descriptor.to_string(), + ), Message::WalletRegistered, ); } @@ -1449,8 +1423,8 @@ impl Step for RegisterDescriptor { self.chosen_hw = None; match res { Ok((fingerprint, hmac)) => { - if let Some(hw_h) = self - .hws + if let Some(hw_h) = hws + .list .iter() .find(|hw_h| hw_h.fingerprint() == Some(fingerprint)) { @@ -1461,11 +1435,7 @@ impl Step for RegisterDescriptor { Err(e) => self.error = Some(e), } } - Message::ConnectedHardwareWallets(hws) => { - self.hws = hws; - } Message::Reload => { - self.hws = Vec::new(); return self.load(); } Message::UserActionDone(done) => { @@ -1485,17 +1455,18 @@ impl Step for RegisterDescriptor { true } fn load(&self) -> Command { - Command::perform( - async move { list_unregistered_hardware_wallets().await }, - Message::ConnectedHardwareWallets, - ) + Command::none() } - fn view(&self, progress: (usize, usize)) -> Element { + fn view<'a>( + &'a self, + hws: &'a HardwareWallets, + progress: (usize, usize), + ) -> Element<'a, Message> { let desc = self.descriptor.as_ref().unwrap(); view::register_descriptor( progress, desc.to_string(), - &self.hws, + &hws.list, &self.registered, self.error.as_ref(), self.processing, @@ -1509,10 +1480,11 @@ impl Step for RegisterDescriptor { async fn register_wallet( hw: std::sync::Arc, fingerprint: Fingerprint, + name: String, descriptor: String, ) -> Result<(Fingerprint, Option<[u8; 32]>), Error> { let hmac = hw - .register_wallet("Liana", &descriptor) + .register_wallet(&name, &descriptor) .await .map_err(Error::from)?; Ok((fingerprint, hmac)) @@ -1531,7 +1503,7 @@ pub struct BackupDescriptor { } impl Step for BackupDescriptor { - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { if let Message::UserActionDone(done) = message { self.done = done; } @@ -1540,7 +1512,7 @@ impl Step for BackupDescriptor { fn load_context(&mut self, ctx: &Context) { self.descriptor = ctx.descriptor.clone(); } - fn view(&self, progress: (usize, usize)) -> Element { + fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element { let desc = self.descriptor.as_ref().unwrap(); view::backup_descriptor(progress, desc.to_string(), self.done) } @@ -1575,11 +1547,12 @@ mod tests { } pub async fn update(&self, message: Message) { - let cmd = self.step.lock().unwrap().update(message); + let mut hws = HardwareWallets::new(PathBuf::from_str("/").unwrap(), Network::Bitcoin); + let cmd = self.step.lock().unwrap().update(&mut hws, message); for action in cmd.actions() { if let Action::Future(f) = action { let msg = f.await; - let _cmd = self.step.lock().unwrap().update(msg); + let _cmd = self.step.lock().unwrap().update(&mut hws, msg); } } } diff --git a/gui/src/installer/step/mnemonic.rs b/gui/src/installer/step/mnemonic.rs index 7198b8340..439cb628b 100644 --- a/gui/src/installer/step/mnemonic.rs +++ b/gui/src/installer/step/mnemonic.rs @@ -7,6 +7,7 @@ use liana::{bip39, signer::HotSigner}; use liana_ui::widget::Element; use crate::{ + hw::HardwareWallets, installer::{context::Context, message::Message, step::Step, view}, signer::Signer, }; @@ -35,7 +36,7 @@ impl From for Box { } impl Step for BackupMnemonic { - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { if let Message::UserActionDone(done) = message { self.done = done; } @@ -50,7 +51,7 @@ impl Step for BackupMnemonic { false } } - fn view(&self, progress: (usize, usize)) -> Element { + fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element { view::backup_mnemonic(progress, &self.words, self.done) } } @@ -86,7 +87,7 @@ impl From for Box { } impl Step for RecoverMnemonic { - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { match message { Message::MnemonicWord(index, value) => { if let Some((word, valid)) = self.words.get_mut(index) { @@ -162,7 +163,7 @@ impl Step for RecoverMnemonic { ctx.recovered_signer = Some(Arc::new(signer)); true } - fn view(&self, progress: (usize, usize)) -> Element { + fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element { view::recover_mnemonic( progress, &self.words, diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs index e6664a9de..3583dbcae 100644 --- a/gui/src/installer/step/mod.rs +++ b/gui/src/installer/step/mod.rs @@ -21,17 +21,23 @@ use liana_ui::widget::*; use crate::{ bitcoind::Bitcoind, + hw::HardwareWallets, installer::{context::Context, message::Message, view}, }; pub trait Step { - fn update(&mut self, _message: Message) -> Command { + fn update(&mut self, _hws: &mut HardwareWallets, _message: Message) -> Command { Command::none() } fn subscription(&self) -> Subscription { Subscription::none() } - fn view(&self, progress: (usize, usize)) -> Element; + fn view<'a>( + &'a self, + _hws: &'a HardwareWallets, + progress: (usize, usize), + ) -> Element<'a, Message>; + fn load_context(&mut self, _ctx: &Context) {} fn load(&self) -> Command { Command::none() @@ -49,7 +55,7 @@ pub trait Step { pub struct Welcome {} impl Step for Welcome { - fn view(&self, _progress: (usize, usize)) -> Element { + fn view(&self, _hws: &HardwareWallets, _progress: (usize, usize)) -> Element { view::welcome() } } @@ -95,7 +101,7 @@ impl Step for Final { Command::none() } } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command { match message { Message::Installed(res) => { self.generating = false; @@ -123,7 +129,7 @@ impl Step for Final { Command::none() } - fn view(&self, progress: (usize, usize)) -> Element { + fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element { view::install( progress, self.generating, diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 35bdd5180..7ac1a6d6d 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -487,8 +487,8 @@ pub fn signer_xpubs(xpubs: &Vec) -> Element { pub fn hardware_wallet_xpubs<'a>( i: usize, - xpubs: &'a Vec, hw: &'a HardwareWallet, + xpubs: Option<&'a Vec>, processing: bool, error: Option<&Error>, ) -> Element<'a, Message> { @@ -509,6 +509,9 @@ pub fn hardware_wallet_xpubs<'a>( HardwareWallet::Unsupported { version, kind, .. } => { hw::unsupported_hardware_wallet(&kind.to_string(), version.as_ref()) } + HardwareWallet::Locked { + kind, pairing_code, .. + } => hw::locked_hardware_wallet(kind, pairing_code.as_ref()), }) .style(theme::Button::Secondary) .width(Length::Fill); @@ -519,15 +522,13 @@ pub fn hardware_wallet_xpubs<'a>( Column::new() .push_maybe(error.map(|e| card::warning(e.to_string()).width(Length::Fill))) .push(bttn) - .push_maybe(if xpubs.is_empty() { + .push_maybe(if xpubs.is_none() { None } else { Some(separation().width(Length::Fill)) }) - .push_maybe(if xpubs.is_empty() { - None - } else { - Some(xpubs.iter().fold(Column::new().padding(15), |col, xpub| { + .push_maybe(xpubs.map(|xpubs| { + xpubs.iter().fold(Column::new().padding(15), |col, xpub| { col.push( Row::new() .spacing(5) @@ -550,8 +551,8 @@ pub fn hardware_wallet_xpubs<'a>( .padding(10), ), ) - })) - }), + }) + })), ) .style(theme::Container::Card(theme::Card::Simple)) .into() @@ -599,17 +600,11 @@ pub fn participate_xpub<'a>( .push( Column::new() .push( - Row::new() - .spacing(10) - .align_items(Alignment::Center) - .push( - Container::new(text("Generate an extended public key by selecting a signing device:").bold()) - .width(Length::Fill), - ) - .push( - button::secondary(Some(icon::reload_icon()), "Refresh") - .on_press(Message::Reload), - ), + Container::new( + text("Generate an extended public key by selecting a signing device:") + .bold(), + ) + .width(Length::Fill), ) .spacing(10) .push(Column::with_children(hws).spacing(10)) @@ -670,25 +665,16 @@ pub fn register_descriptor<'a>( .push( Column::new() .push( - Row::new() - .spacing(10) - .align_items(Alignment::Center) - .push( - Container::new( - if created_desc { - text("Select hardware wallet to register descriptor on:") - .bold() - } else { - text("If necessary, please select the signing device to register descriptor on:") - .bold() - }, - ) - .width(Length::Fill), - ) - .push( - button::secondary(Some(icon::reload_icon()), "Refresh") - .on_press(Message::Reload), - ), + Container::new( + if created_desc { + text("Select hardware wallet to register descriptor on:") + .bold() + } else { + text("If necessary, please select the signing device to register descriptor on:") + .bold() + }, + ) + .width(Length::Fill), ) .spacing(10) .push( @@ -1305,17 +1291,8 @@ pub fn edit_key_modal<'a>( .push( Column::new() .push( - Row::new() - .spacing(10) - .align_items(Alignment::Center) - .push( - Container::new(text("Select a signing device:").bold()) - .width(Length::Fill), - ) - .push( - button::secondary(Some(icon::reload_icon()), "Refresh") - .on_press(Message::Reload), - ), + Container::new(text("Select a signing device:").bold()) + .width(Length::Fill), ) .spacing(10) .push( @@ -1554,6 +1531,9 @@ pub fn hw_list_view( HardwareWallet::Unsupported { version, kind, .. } => { hw::unsupported_hardware_wallet(&kind.to_string(), version.as_ref()) } + HardwareWallet::Locked { + kind, pairing_code, .. + } => hw::locked_hardware_wallet(kind, pairing_code.as_ref()), }) .style(theme::Button::Border) .width(Length::Fill); diff --git a/gui/src/loader.rs b/gui/src/loader.rs index 265f91542..b8825088d 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -316,9 +316,14 @@ pub async fn load_application( ), Error, > { + let wallet = + Wallet::new(info.descriptors.main).load_settings(&gui_config, &datadir_path, network)?; + let coins = daemon.list_coins().map(|res| res.coins)?; let spend_txs = daemon.list_spend_transactions()?; + let cache = Cache { + datadir_path, network: info.network, blockheight: info.block_height, coins, @@ -326,9 +331,6 @@ pub async fn load_application( ..Default::default() }; - let wallet = - Wallet::new(info.descriptors.main).load_settings(&gui_config, &datadir_path, network)?; - Ok((Arc::new(wallet), cache, daemon, internal_bitcoind)) } diff --git a/gui/ui/src/component/hw.rs b/gui/ui/src/component/hw.rs index 1e4c5c981..c393e049f 100644 --- a/gui/ui/src/component/hw.rs +++ b/gui/ui/src/component/hw.rs @@ -6,6 +6,27 @@ use iced::{ use std::borrow::Cow; use std::fmt::Display; +pub fn locked_hardware_wallet<'a, T: 'a, K: Display>( + kind: K, + pairing_code: Option>>, +) -> Container<'a, T> { + Container::new( + column(vec![ + Row::new() + .spacing(5) + .push(text::p1_bold("Locked, check code:")) + .push_maybe(pairing_code.map(|a| text::p1_bold(a))) + .into(), + Row::new() + .spacing(5) + .push(text::caption(kind.to_string())) + .into(), + ]) + .width(Length::Fill), + ) + .padding(10) +} + pub fn supported_hardware_wallet<'a, T: 'a, K: Display, V: Display, F: Display>( kind: K, version: Option,