Merge #706: Add bitbox support

4be74ad496692d371adffcc1b3c868ef6a01cb52 refac hw module and add bitbox support (edouard)

Pull request description:

  - Add bitbox02 as possible signer to liana-gui
  - Introduce global_settings.json in datadir to keep the bitbox noise config.

ACKs for top commit:
  edouardparis:
    Self-ACK 4be74ad496692d371adffcc1b3c868ef6a01cb52

Tree-SHA512: e0615e1903baa7faecc25db717f49ed71c23a6afcec607c41ac2045cfb2cfa9a9aa3d3452cc952aeaf72a735d670c56dbf19b239c8d665466a2ecb09fbe222c5
This commit is contained in:
edouard 2023-10-24 17:46:31 +02:00
commit 1a59d03858
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
28 changed files with 1658 additions and 688 deletions

651
gui/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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<Coin>,
@ -10,9 +12,11 @@ pub struct Cache {
pub rescan_progress: Option<f64>,
}
/// 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(),

View File

@ -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<HardwareWallet>),
HardwareWallets(HardwareWalletMessage),
HistoryTransactions(Result<Vec<HistoryTransaction>, Error>),
PendingTransactions(Result<Vec<HistoryTransaction>, Error>),
LabelsUpdated(Result<HashMap<String, Option<String>>, Error>),

View File

@ -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<BitboxSettings>,
}
#[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<NoiseConfigData, async_hwi::bitbox::api::ConfigError> {
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::<Settings>(&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::<Settings>(&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()))
}
}
}

View File

@ -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<dyn Daemon + Sync + Send>) -> Command<Message> {
Command::none()
}
fn subscription(&self) -> Subscription<Message> {
Subscription::none()
}
fn update(
&mut self,
_daemon: Arc<dyn Daemon + Sync + Send>,
@ -69,6 +75,14 @@ impl PsbtState {
}
}
pub fn subscription(&self) -> Subscription<Message> {
if let Some(action) = &self.action {
action.subscription()
} else {
Subscription::none()
}
}
pub fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
if let Some(action) = &self.action {
action.load(daemon)
@ -80,7 +94,7 @@ impl PsbtState {
pub fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
_cache: &Cache,
cache: &Cache,
message: Message,
) -> Command<Message> {
match &message {
@ -92,7 +106,12 @@ impl PsbtState {
self.action = Some(Box::<DeleteAction>::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<Wallet>,
chosen_hw: Option<usize>,
processing: bool,
hws: Vec<HardwareWallet>,
hws: HardwareWallets,
error: Option<Error>,
signed: HashSet<Fingerprint>,
}
impl SignAction {
pub fn new(signed: HashSet<Fingerprint>, wallet: Arc<Wallet>) -> Self {
pub fn new(
signed: HashSet<Fingerprint>,
wallet: Arc<Wallet>,
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<dyn Daemon + Sync + Send>) -> Command<Message> {
let wallet = self.wallet.clone();
Command::perform(
async move { list_hardware_wallets(&wallet).await },
Message::ConnectedHardwareWallets,
)
fn subscription(&self) -> Subscription<Message> {
self.hws.refresh().map(Message::HardwareWallets)
}
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
@ -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::Message> {
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

View File

@ -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<Message> {
if let Some(psbt) = &self.selected_tx {
psbt.subscription()
} else {
Subscription::none()
}
}
fn load(&self, daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
let daemon = daemon.clone();
Command::perform(

View File

@ -51,6 +51,14 @@ impl RecoveryPanel {
}
impl State for RecoveryPanel {
fn subscription(&self) -> iced::Subscription<Message> {
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,
);

View File

@ -93,6 +93,14 @@ impl State for SettingsState {
}
}
fn subscription(&self) -> iced::Subscription<Message> {
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)

View File

@ -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<Message> {
if let Some(modal) = &self.modal {
modal.subscription()
} else {
Subscription::none()
}
}
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
@ -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<Wallet>,
warning: Option<Error>,
chosen_hw: Option<usize>,
hws: Vec<HardwareWallet>,
hws: HardwareWallets,
registered: HashSet<Fingerprint>,
processing: bool,
}
impl RegisterWalletModal {
pub fn new(data_dir: PathBuf, wallet: Arc<Wallet>) -> Self {
pub fn new(data_dir: PathBuf, wallet: Arc<Wallet>, 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::Message> {
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<Message> {
self.hws.refresh().map(Message::HardwareWallets)
}
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
_daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
message: Message,
) -> Command<Message> {
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<dyn Daemon + Sync + Send>) -> Command<Message> {
let wallet = self.wallet.clone();
Command::perform(
async move { list_hardware_wallets(&wallet).await },
Message::ConnectedHardwareWallets,
)
}
}
async fn register_wallet(

View File

@ -70,6 +70,10 @@ impl State for CreateSpendPanel {
self.steps.get(self.current).unwrap().view(cache)
}
fn subscription(&self) -> iced::Subscription<Message> {
self.steps.get(self.current).unwrap().subscription()
}
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,

View File

@ -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<Message>;
fn apply(&self, _draft: &mut TransactionDraft) {}
fn load(&mut self, _draft: &TransactionDraft) {}
fn subscription(&self) -> Subscription<Message> {
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<Message> {
if let Some(spend) = &self.spend {
spend.subscription()
} else {
Subscription::none()
}
}
fn update(
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,

View File

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

View File

@ -328,8 +328,7 @@ pub fn signatures<'a>(
keys_aliases: &'a HashMap<Fingerprint, String>,
) -> 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))
})
}),
} 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()),
)
).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::<Element<'a, Message>>::into(
.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::<Element<'a, Message>>::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(

View File

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

View File

@ -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(),

View File

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

View File

@ -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<u32>,
psbt: Psbt,
coins: Vec<Coin>,
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(),

View File

@ -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<Version>,
message: String,
},
Locked {
id: String,
// None if the device is currently unlocking in a command.
device: Arc<Mutex<Option<LockedDevice>>>,
pairing_code: Option<String>,
kind: DeviceKind,
},
Supported {
device: Arc<dyn HWI + Send + Sync>,
id: String,
device: Arc<dyn HWI + Sync + Send>,
kind: DeviceKind,
fingerprint: Fingerprint,
version: Option<Version>,
@ -23,8 +41,19 @@ pub enum HardwareWallet {
},
}
pub enum LockedDevice {
BitBox02(PairingBitbox02<runtime::TokioRuntime>),
}
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<dyn HWI + Send + Sync>,
aliases: Option<&HashMap<Fingerprint, String>>,
) -> Result<Self, HWIError> {
@ -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<Fingerprint> {
match self {
Self::Locked { .. } => None,
Self::Unsupported { .. } => None,
Self::Supported { fingerprint, .. } => Some(*fingerprint),
}
@ -83,26 +123,217 @@ impl HardwareWalletConfig {
}
}
pub async fn list_hardware_wallets(wallet: &Wallet) -> Vec<HardwareWallet> {
let descriptor = wallet.main_descriptor.to_string();
let mut hws: Vec<HardwareWallet> = 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<HardwareWallet, async_hwi::Error>),
}
#[derive(Debug, Clone)]
pub struct ConnectedList {
pub new: Vec<HardwareWallet>,
still: Vec<String>,
}
pub struct HardwareWallets {
network: Network,
pub list: Vec<HardwareWallet>,
pub aliases: HashMap<Fingerprint, String>,
wallet: Option<Arc<Wallet>>,
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 {
pub fn with_wallet(mut self, wallet: Arc<Wallet>) -> 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;
}
}
}
self.aliases.insert(fg, new_alias);
}
pub fn load_aliases(&mut self, aliases: HashMap<Fingerprint, String>) {
self.aliases = aliases;
}
pub fn set_network(&mut self, network: Network) {
self.network = network;
}
pub fn update(
&mut self,
message: HardwareWalletMessage,
) -> Result<Command<HardwareWalletMessage>, 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),
));
}
}
_ => {}
}
}
if cmds.is_empty() {
Ok(Command::none())
} else {
Ok(Command::batch(cmds))
}
}
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<HardwareWalletMessage> {
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<Fingerprint, String>,
wallet: Option<Arc<Wallet>>,
connected_supported_hws: Vec<String>,
api: Option<ledger::HidApi>,
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<HardwareWallet> = Vec::new();
let mut still: Vec<String> = 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);
@ -110,108 +341,208 @@ pub async fn list_hardware_wallets(wallet: &Wallet) -> Vec<HardwareWallet> {
}
}
}
Err(e) => warn!("Error while listing specter wallets: {}", e),
}
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;
}
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::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);
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::<specter::SerialTransport>::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::<ledger::TransportHID>::enumerate(&api) {
match ledger::Ledger::<ledger::TransportHID>::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::Unsupported {
hws.push(HardwareWallet::Supported {
id,
kind: device.device_kind(),
version: None,
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(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(),
});
}
}
}
}
Err(HWIError::DeviceNotFound) => {}
Err(e) => {
debug!("{}", e);
}
}
hws
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)))),
});
}
}
}
}
for detected in ledger::Ledger::<ledger::TransportHID>::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::<ledger::TransportHID>::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: 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 {
@ -229,110 +560,3 @@ fn ledger_version_supported(version: Option<&Version>) -> bool {
false
}
}
pub async fn list_unregistered_hardware_wallets() -> Vec<HardwareWallet> {
let mut hws: Vec<HardwareWallet> = 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);
}
}
}
}
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 {
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);
}
}
match ledger::HidApi::new() {
Err(e) => {
debug!("{}", e);
}
Ok(api) => {
for detected in ledger::Ledger::<ledger::TransportHID>::enumerate(&api) {
match ledger::Ledger::<ledger::TransportHID>::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);
}
}
}
}
}
hws
}

View File

@ -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<DescriptorPublicKey, Error>),
ConnectedHardwareWallets(Vec<HardwareWallet>),
ImportXpub(Fingerprint, Result<DescriptorPublicKey, Error>),
HardwareWallets(HardwareWalletMessage),
WalletRegistered(Result<(Fingerprint, Option<[u8; 32]>), Error>),
MnemonicWord(usize, String),
ImportMnemonic(bool),

View File

@ -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<Box<dyn Step>>,
hws: HardwareWallets,
signer: Arc<Mutex<Signer>>,
/// 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<Message> {
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())
}
}

View File

@ -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<Message> {
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
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<Message> {
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
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<Message> {
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
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<Message> {
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
view::define_bitcoin(
progress,
&self.address,
@ -646,7 +647,7 @@ impl Step for InternalBitcoindStep {
}
}
}
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
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<Message> {
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
view::start_internal_bitcoind(
progress,
self.exe_path.as_ref(),

View File

@ -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<Message> {
fn update(&mut self, _hws: &mut HardwareWallets, _message: Message) -> Command<Message> {
Command::none()
}
fn view(&self) -> Element<Message>;
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<Message> {
fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command<Message> {
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<Message> {
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<Message> {
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
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<Message> {
fn view(&self, _hws: &HardwareWallets) -> Element<Message> {
view::edit_sequence_modal(&self.sequence)
}
}
@ -681,7 +690,6 @@ pub struct EditXpubModal {
duplicate_master_fg: bool,
keys: Vec<Key>,
hws: Vec<HardwareWallet>,
hot_signer: Arc<Mutex<Signer>>,
hot_signer_fingerprint: Fingerprint,
chosen_signer: Option<(Fingerprint, Option<DeviceKind>)>,
@ -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<Message> {
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<Message> {
fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command<Message> {
// 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<Message> {
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<String>,
processing: bool,
error: Option<Error>,
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<DescriptorPublicKey, Error>) {
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<Message> {
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<Message> {
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<Message> {
@ -1145,7 +1082,7 @@ pub struct ParticipateXpub {
shared: bool,
xpubs_hw: Vec<HardwareWalletXpubs>,
hw_xpubs: Vec<HardwareWalletXpubs>,
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<Message> {
fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command<Message> {
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<Message> {
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<Message> {
fn view<'a>(&'a self, hws: &'a HardwareWallets, progress: (usize, usize)) -> Element<Message> {
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<Message> {
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
match message {
Message::Network(network) => {
self.network = network;
@ -1354,7 +1327,7 @@ impl Step for ImportDescriptor {
}
}
fn view(&self, progress: (usize, usize)) -> Element<Message> {
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
view::import_descriptor(
progress,
self.change_network,
@ -1374,10 +1347,8 @@ impl From<ImportDescriptor> for Box<dyn Step> {
pub struct RegisterDescriptor {
descriptor: Option<LianaDescriptor>,
keys_aliases: HashMap<Fingerprint, String>,
processing: bool,
chosen_hw: Option<usize>,
hws: Vec<HardwareWallet>,
hmacs: Vec<(Fingerprint, DeviceKind, Option<[u8; 32]>)>,
registered: HashSet<Fingerprint>,
error: Option<Error>,
@ -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<Message> {
fn update(&mut self, hws: &mut HardwareWallets, message: Message) -> Command<Message> {
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<Message> {
Command::perform(
async move { list_unregistered_hardware_wallets().await },
Message::ConnectedHardwareWallets,
)
Command::none()
}
fn view(&self, progress: (usize, usize)) -> Element<Message> {
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<dyn async_hwi::HWI + Send + Sync>,
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<Message> {
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
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<Message> {
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
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);
}
}
}

View File

@ -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<BackupMnemonic> for Box<dyn Step> {
}
impl Step for BackupMnemonic {
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
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<Message> {
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
view::backup_mnemonic(progress, &self.words, self.done)
}
}
@ -86,7 +87,7 @@ impl From<RecoverMnemonic> for Box<dyn Step> {
}
impl Step for RecoverMnemonic {
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
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<Message> {
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
view::recover_mnemonic(
progress,
&self.words,

View File

@ -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<Message> {
fn update(&mut self, _hws: &mut HardwareWallets, _message: Message) -> Command<Message> {
Command::none()
}
fn subscription(&self) -> Subscription<Message> {
Subscription::none()
}
fn view(&self, progress: (usize, usize)) -> Element<Message>;
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<Message> {
Command::none()
@ -49,7 +55,7 @@ pub trait Step {
pub struct Welcome {}
impl Step for Welcome {
fn view(&self, _progress: (usize, usize)) -> Element<Message> {
fn view(&self, _hws: &HardwareWallets, _progress: (usize, usize)) -> Element<Message> {
view::welcome()
}
}
@ -95,7 +101,7 @@ impl Step for Final {
Command::none()
}
}
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Command<Message> {
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<Message> {
fn view(&self, _hws: &HardwareWallets, progress: (usize, usize)) -> Element<Message> {
view::install(
progress,
self.generating,

View File

@ -487,8 +487,8 @@ pub fn signer_xpubs(xpubs: &Vec<String>) -> Element<Message> {
pub fn hardware_wallet_xpubs<'a>(
i: usize,
xpubs: &'a Vec<String>,
hw: &'a HardwareWallet,
xpubs: Option<&'a Vec<String>>,
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);

View File

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

View File

@ -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<impl Into<Cow<'a, str>>>,
) -> 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<V>,