refac hw module and add bitbox support
This commit is contained in:
parent
42578609e2
commit
4be74ad496
651
gui/Cargo.lock
generated
651
gui/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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>),
|
||||
|
||||
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
},
|
||||
))
|
||||
},
|
||||
))
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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| {
|
||||
|
||||
@ -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(),
|
||||
|
||||
640
gui/src/hw.rs
640
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<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
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -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>,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user