gui: new module settings
This commit is contained in:
parent
689f19a4f2
commit
bf1e9e4b80
@ -1,6 +1,7 @@
|
||||
use crate::daemon::model::{Coin, SpendTx};
|
||||
use liana::miniscript::bitcoin::Network;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
pub network: Network,
|
||||
pub blockheight: i32,
|
||||
|
||||
@ -11,19 +11,19 @@ pub struct Config {
|
||||
/// Use iced debug feature if true.
|
||||
pub debug: Option<bool>,
|
||||
/// hardware wallets config.
|
||||
#[serde(default)]
|
||||
pub hardware_wallets: Vec<HardwareWalletConfig>,
|
||||
/// LEGACY: Use Settings module instead.
|
||||
pub hardware_wallets: Option<Vec<HardwareWalletConfig>>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_FILE_NAME: &str = "gui.toml";
|
||||
|
||||
impl Config {
|
||||
pub fn new(daemon_config_path: PathBuf, hardware_wallets: Vec<HardwareWalletConfig>) -> Self {
|
||||
pub fn new(daemon_config_path: PathBuf) -> Self {
|
||||
Self {
|
||||
daemon_config_path,
|
||||
log_level: None,
|
||||
debug: None,
|
||||
hardware_wallets,
|
||||
hardware_wallets: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,14 +40,6 @@ impl Config {
|
||||
})?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn default_path() -> Result<PathBuf, ConfigError> {
|
||||
let mut datadir = default_datadir().map_err(|_| {
|
||||
ConfigError::Unexpected("Could not locate the default datadir directory.".to_owned())
|
||||
})?;
|
||||
datadir.push(DEFAULT_FILE_NAME);
|
||||
Ok(datadir)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
|
||||
@ -2,6 +2,7 @@ pub mod cache;
|
||||
pub mod config;
|
||||
pub mod menu;
|
||||
pub mod message;
|
||||
pub mod settings;
|
||||
pub mod state;
|
||||
pub mod view;
|
||||
pub mod wallet;
|
||||
@ -31,14 +32,14 @@ pub struct App {
|
||||
state: Box<dyn State>,
|
||||
cache: Cache,
|
||||
config: Config,
|
||||
wallet: Wallet,
|
||||
wallet: Arc<Wallet>,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(
|
||||
cache: Cache,
|
||||
wallet: Wallet,
|
||||
wallet: Arc<Wallet>,
|
||||
config: Config,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
) -> (App, Command<Message>) {
|
||||
@ -72,22 +73,15 @@ impl App {
|
||||
.into(),
|
||||
menu::Menu::Recovery => RecoveryPanel::new(
|
||||
self.wallet.clone(),
|
||||
self.config.clone(),
|
||||
&self.cache.coins,
|
||||
self.wallet.main_descriptor.timelock_value(),
|
||||
self.cache.blockheight as u32,
|
||||
)
|
||||
.into(),
|
||||
menu::Menu::Receive => ReceivePanel::default().into(),
|
||||
menu::Menu::Spend => SpendPanel::new(
|
||||
self.wallet.clone(),
|
||||
self.config.clone(),
|
||||
&self.cache.spend_txs,
|
||||
)
|
||||
.into(),
|
||||
menu::Menu::Spend => SpendPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(),
|
||||
menu::Menu::CreateSpendTx => CreateSpendPanel::new(
|
||||
self.wallet.clone(),
|
||||
self.config.clone(),
|
||||
&self.cache.coins,
|
||||
self.cache.blockheight as u32,
|
||||
)
|
||||
|
||||
75
gui/src/app/settings.rs
Normal file
75
gui/src/app/settings.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use liana::miniscript::bitcoin::util::bip32::Fingerprint;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::hw::HardwareWalletConfig;
|
||||
|
||||
///! Settings is the module to handle the GUI settings file.
|
||||
///! The settings file is used by the GUI to store useful information.
|
||||
pub const DEFAULT_FILE_NAME: &str = "settings.json";
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Settings {
|
||||
pub wallets: Vec<WalletSetting>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn from_file(path: &Path) -> Result<Self, SettingsError> {
|
||||
let config = std::fs::read(path)
|
||||
.map_err(|e| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => SettingsError::NotFound,
|
||||
_ => SettingsError::ReadingFile(format!("Reading settings file: {}", e)),
|
||||
})
|
||||
.and_then(|file_content| {
|
||||
serde_json::from_slice::<Settings>(&file_content).map_err(|e| {
|
||||
SettingsError::ReadingFile(format!("Parsing settings file: {}", e))
|
||||
})
|
||||
})?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct WalletSetting {
|
||||
pub name: String,
|
||||
pub descriptor_checksum: String,
|
||||
#[serde(default)]
|
||||
pub keys: Vec<KeySetting>,
|
||||
#[serde(default)]
|
||||
pub hardware_wallets: Vec<HardwareWalletConfig>,
|
||||
}
|
||||
|
||||
impl WalletSetting {
|
||||
pub fn keys_aliases(&self) -> HashMap<Fingerprint, String> {
|
||||
let mut map = HashMap::new();
|
||||
for key in self.keys.clone() {
|
||||
map.insert(key.master_fingerprint, key.name);
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct KeySetting {
|
||||
pub name: String,
|
||||
pub master_fingerprint: Fingerprint,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum SettingsError {
|
||||
NotFound,
|
||||
ReadingFile(String),
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SettingsError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NotFound => write!(f, "Settings file not found"),
|
||||
Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
|
||||
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,7 +39,7 @@ pub trait State {
|
||||
}
|
||||
|
||||
pub struct Home {
|
||||
wallet: Wallet,
|
||||
wallet: Arc<Wallet>,
|
||||
balance: Amount,
|
||||
recovery_warning: Option<(Amount, usize)>,
|
||||
recovery_alert: Option<(Amount, usize)>,
|
||||
@ -50,7 +50,7 @@ pub struct Home {
|
||||
}
|
||||
|
||||
impl Home {
|
||||
pub fn new(wallet: Wallet, coins: &[Coin]) -> Self {
|
||||
pub fn new(wallet: Arc<Wallet>, coins: &[Coin]) -> Self {
|
||||
Self {
|
||||
wallet,
|
||||
balance: Amount::from_sat(
|
||||
|
||||
@ -6,7 +6,6 @@ use iced::{Command, Element};
|
||||
use crate::{
|
||||
app::{
|
||||
cache::Cache,
|
||||
config::Config,
|
||||
error::Error,
|
||||
menu::Menu,
|
||||
message::Message,
|
||||
@ -25,8 +24,7 @@ use crate::{
|
||||
use liana::miniscript::bitcoin::{Address, Amount, Network};
|
||||
|
||||
pub struct RecoveryPanel {
|
||||
wallet: Wallet,
|
||||
config: Config,
|
||||
wallet: Arc<Wallet>,
|
||||
locked_coins: (usize, Amount),
|
||||
recoverable_coins: (usize, Amount),
|
||||
warning: Option<Error>,
|
||||
@ -38,13 +36,7 @@ pub struct RecoveryPanel {
|
||||
}
|
||||
|
||||
impl RecoveryPanel {
|
||||
pub fn new(
|
||||
wallet: Wallet,
|
||||
config: Config,
|
||||
coins: &[Coin],
|
||||
timelock: u32,
|
||||
blockheight: u32,
|
||||
) -> Self {
|
||||
pub fn new(wallet: Arc<Wallet>, coins: &[Coin], timelock: u32, blockheight: u32) -> Self {
|
||||
let mut locked_coins = (0, Amount::from_sat(0));
|
||||
let mut recoverable_coins = (0, Amount::from_sat(0));
|
||||
for coin in coins {
|
||||
@ -61,7 +53,6 @@ impl RecoveryPanel {
|
||||
}
|
||||
Self {
|
||||
wallet,
|
||||
config,
|
||||
locked_coins,
|
||||
recoverable_coins,
|
||||
warning: None,
|
||||
@ -123,12 +114,7 @@ impl State for RecoveryPanel {
|
||||
},
|
||||
Message::Recovery(res) => match res {
|
||||
Ok(tx) => {
|
||||
self.generated = Some(detail::SpendTxState::new(
|
||||
self.wallet.clone(),
|
||||
self.config.clone(),
|
||||
tx,
|
||||
false,
|
||||
))
|
||||
self.generated = Some(detail::SpendTxState::new(self.wallet.clone(), tx, false))
|
||||
}
|
||||
Err(e) => self.warning = Some(e),
|
||||
},
|
||||
|
||||
@ -8,8 +8,7 @@ use liana::miniscript::bitcoin::{
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
cache::Cache, config::Config, error::Error, message::Message, view, view::spend::detail,
|
||||
wallet::Wallet,
|
||||
cache::Cache, error::Error, message::Message, view, view::spend::detail, wallet::Wallet,
|
||||
},
|
||||
daemon::{
|
||||
model::{SpendStatus, SpendTx},
|
||||
@ -39,19 +38,17 @@ trait Action {
|
||||
}
|
||||
|
||||
pub struct SpendTxState {
|
||||
wallet: Wallet,
|
||||
config: Config,
|
||||
wallet: Arc<Wallet>,
|
||||
tx: SpendTx,
|
||||
saved: bool,
|
||||
action: Option<Box<dyn Action>>,
|
||||
}
|
||||
|
||||
impl SpendTxState {
|
||||
pub fn new(wallet: Wallet, config: Config, tx: SpendTx, saved: bool) -> Self {
|
||||
pub fn new(wallet: Arc<Wallet>, tx: SpendTx, saved: bool) -> Self {
|
||||
Self {
|
||||
wallet,
|
||||
action: None,
|
||||
config,
|
||||
tx,
|
||||
saved,
|
||||
}
|
||||
@ -80,7 +77,7 @@ impl SpendTxState {
|
||||
self.action = Some(Box::new(DeleteAction::default()));
|
||||
}
|
||||
view::SpendTxMessage::Sign => {
|
||||
let action = SignAction::new(self.config.clone());
|
||||
let action = SignAction::new();
|
||||
let cmd = action.load(&self.wallet, daemon);
|
||||
self.action = Some(Box::new(action));
|
||||
return cmd;
|
||||
@ -252,7 +249,6 @@ impl Action for DeleteAction {
|
||||
}
|
||||
|
||||
pub struct SignAction {
|
||||
config: Config,
|
||||
chosen_hw: Option<usize>,
|
||||
processing: bool,
|
||||
hws: Vec<HardwareWallet>,
|
||||
@ -261,9 +257,8 @@ pub struct SignAction {
|
||||
}
|
||||
|
||||
impl SignAction {
|
||||
pub fn new(config: Config) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
config,
|
||||
chosen_hw: None,
|
||||
processing: false,
|
||||
hws: Vec::new(),
|
||||
@ -279,13 +274,8 @@ impl Action for SignAction {
|
||||
}
|
||||
|
||||
fn load(&self, wallet: &Wallet, _daemon: Arc<dyn Daemon + Sync + Send>) -> Command<Message> {
|
||||
let config = self.config.clone();
|
||||
let desc = wallet.main_descriptor.to_string();
|
||||
let name = wallet.name.clone();
|
||||
Command::perform(
|
||||
list_hws(config, name, desc),
|
||||
Message::ConnectedHardwareWallets,
|
||||
)
|
||||
let wallet = wallet.clone();
|
||||
Command::perform(list_hws(wallet), Message::ConnectedHardwareWallets)
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
@ -350,8 +340,12 @@ impl Action for SignAction {
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_hws(config: Config, wallet_name: String, descriptor: String) -> Vec<HardwareWallet> {
|
||||
list_hardware_wallets(&config.hardware_wallets, Some((&wallet_name, &descriptor))).await
|
||||
async fn list_hws(wallet: Wallet) -> Vec<HardwareWallet> {
|
||||
list_hardware_wallets(
|
||||
&wallet.hardware_wallets,
|
||||
Some((&wallet.name, &wallet.main_descriptor.to_string())),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn sign_psbt(
|
||||
|
||||
@ -8,10 +8,7 @@ use liana::miniscript::bitcoin::{consensus, util::psbt::Psbt};
|
||||
|
||||
use super::{redirect, State};
|
||||
use crate::{
|
||||
app::{
|
||||
cache::Cache, config::Config, error::Error, menu::Menu, message::Message, view,
|
||||
wallet::Wallet,
|
||||
},
|
||||
app::{cache::Cache, error::Error, menu::Menu, message::Message, view, wallet::Wallet},
|
||||
daemon::{
|
||||
model::{Coin, SpendTx},
|
||||
Daemon,
|
||||
@ -20,8 +17,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub struct SpendPanel {
|
||||
wallet: Wallet,
|
||||
config: Config,
|
||||
wallet: Arc<Wallet>,
|
||||
selected_tx: Option<detail::SpendTxState>,
|
||||
spend_txs: Vec<SpendTx>,
|
||||
warning: Option<Error>,
|
||||
@ -29,10 +25,9 @@ pub struct SpendPanel {
|
||||
}
|
||||
|
||||
impl SpendPanel {
|
||||
pub fn new(wallet: Wallet, config: Config, spend_txs: &[SpendTx]) -> Self {
|
||||
pub fn new(wallet: Arc<Wallet>, spend_txs: &[SpendTx]) -> Self {
|
||||
Self {
|
||||
wallet,
|
||||
config,
|
||||
spend_txs: spend_txs.to_vec(),
|
||||
warning: None,
|
||||
selected_tx: None,
|
||||
@ -97,12 +92,7 @@ impl State for SpendPanel {
|
||||
}
|
||||
Message::View(view::Message::Select(i)) => {
|
||||
if let Some(tx) = self.spend_txs.get(i) {
|
||||
let tx = detail::SpendTxState::new(
|
||||
self.wallet.clone(),
|
||||
self.config.clone(),
|
||||
tx.clone(),
|
||||
true,
|
||||
);
|
||||
let tx = detail::SpendTxState::new(self.wallet.clone(), tx.clone(), true);
|
||||
let cmd = tx.load(daemon);
|
||||
self.selected_tx = Some(tx);
|
||||
return cmd;
|
||||
@ -143,7 +133,7 @@ pub struct CreateSpendPanel {
|
||||
}
|
||||
|
||||
impl CreateSpendPanel {
|
||||
pub fn new(wallet: Wallet, config: Config, coins: &[Coin], blockheight: u32) -> Self {
|
||||
pub fn new(wallet: Arc<Wallet>, coins: &[Coin], blockheight: u32) -> Self {
|
||||
let descriptor = wallet.main_descriptor.clone();
|
||||
let timelock = descriptor.timelock_value();
|
||||
Self {
|
||||
@ -157,7 +147,7 @@ impl CreateSpendPanel {
|
||||
timelock,
|
||||
blockheight,
|
||||
)),
|
||||
Box::new(step::SaveSpend::new(wallet, config)),
|
||||
Box::new(step::SaveSpend::new(wallet)),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,8 +12,7 @@ use liana::{
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
cache::Cache, config::Config, error::Error, message::Message, state::spend::detail, view,
|
||||
wallet::Wallet,
|
||||
cache::Cache, error::Error, message::Message, state::spend::detail, view, wallet::Wallet,
|
||||
},
|
||||
daemon::{
|
||||
model::{remaining_sequence, Coin, SpendTx},
|
||||
@ -430,16 +429,14 @@ impl Step for ChooseCoins {
|
||||
}
|
||||
|
||||
pub struct SaveSpend {
|
||||
wallet: Wallet,
|
||||
config: Config,
|
||||
wallet: Arc<Wallet>,
|
||||
spend: Option<detail::SpendTxState>,
|
||||
}
|
||||
|
||||
impl SaveSpend {
|
||||
pub fn new(wallet: Wallet, config: Config) -> Self {
|
||||
pub fn new(wallet: Arc<Wallet>) -> Self {
|
||||
Self {
|
||||
wallet,
|
||||
config,
|
||||
spend: None,
|
||||
}
|
||||
}
|
||||
@ -455,7 +452,6 @@ impl Step for SaveSpend {
|
||||
.unwrap();
|
||||
self.spend = Some(detail::SpendTxState::new(
|
||||
self.wallet.clone(),
|
||||
self.config.clone(),
|
||||
SpendTx::new(psbt, draft.inputs.clone(), sigs),
|
||||
false,
|
||||
));
|
||||
|
||||
@ -1,16 +1,46 @@
|
||||
use liana::descriptors::MultipathDescriptor;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
use crate::hw::HardwareWalletConfig;
|
||||
|
||||
use liana::descriptors::MultipathDescriptor;
|
||||
use liana::miniscript::bitcoin::util::bip32::Fingerprint;
|
||||
|
||||
pub const DEFAULT_WALLET_NAME: &str = "Liana";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Wallet {
|
||||
pub name: String,
|
||||
pub main_descriptor: MultipathDescriptor,
|
||||
pub keys_aliases: HashMap<Fingerprint, String>,
|
||||
pub hardware_wallets: Vec<HardwareWalletConfig>,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
pub fn new(main_descriptor: MultipathDescriptor) -> Self {
|
||||
pub fn new(name: String, main_descriptor: MultipathDescriptor) -> Self {
|
||||
Self {
|
||||
name: "Liana".to_string(),
|
||||
name,
|
||||
main_descriptor,
|
||||
keys_aliases: HashMap::new(),
|
||||
hardware_wallets: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn legacy(main_descriptor: MultipathDescriptor) -> Self {
|
||||
Self {
|
||||
name: DEFAULT_WALLET_NAME.to_string(),
|
||||
main_descriptor,
|
||||
keys_aliases: HashMap::new(),
|
||||
hardware_wallets: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_key_aliases(mut self, aliases: HashMap<Fingerprint, String>) -> Self {
|
||||
self.keys_aliases = aliases;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_harware_wallets(mut self, hardware_wallets: Vec<HardwareWalletConfig>) -> Self {
|
||||
self.hardware_wallets = hardware_wallets;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ use std::convert::TryFrom;
|
||||
|
||||
use liana::config::Config as LianaConfig;
|
||||
|
||||
use super::step::Context;
|
||||
use super::Context;
|
||||
|
||||
pub const DEFAULT_FILE_NAME: &str = "daemon.toml";
|
||||
|
||||
|
||||
87
gui/src/installer/context.rs
Normal file
87
gui/src/installer/context.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
settings::{KeySetting, Settings, WalletSetting},
|
||||
wallet::DEFAULT_WALLET_NAME,
|
||||
},
|
||||
hw::HardwareWalletConfig,
|
||||
};
|
||||
use async_hwi::DeviceKind;
|
||||
use liana::{
|
||||
config::Config,
|
||||
config::{BitcoinConfig, BitcoindConfig},
|
||||
descriptors::MultipathDescriptor,
|
||||
miniscript::bitcoin,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pub bitcoin_config: BitcoinConfig,
|
||||
pub bitcoind_config: Option<BitcoindConfig>,
|
||||
pub descriptor: Option<MultipathDescriptor>,
|
||||
pub keys: Vec<KeySetting>,
|
||||
pub hws: Vec<(
|
||||
DeviceKind,
|
||||
bitcoin::util::bip32::Fingerprint,
|
||||
Option<[u8; 32]>,
|
||||
)>,
|
||||
pub data_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
bitcoin_config: BitcoinConfig {
|
||||
network,
|
||||
poll_interval_secs: Duration::from_secs(30),
|
||||
},
|
||||
hws: Vec::new(),
|
||||
keys: Vec::new(),
|
||||
bitcoind_config: None,
|
||||
descriptor: None,
|
||||
data_dir,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_gui_settings(&self) -> Settings {
|
||||
let hardware_wallets = self
|
||||
.hws
|
||||
.iter()
|
||||
.filter_map(|(kind, fingerprint, token)| {
|
||||
token
|
||||
.as_ref()
|
||||
.map(|token| HardwareWalletConfig::new(kind, fingerprint, token))
|
||||
})
|
||||
.collect();
|
||||
Settings {
|
||||
wallets: vec![WalletSetting {
|
||||
name: DEFAULT_WALLET_NAME.to_string(),
|
||||
descriptor_checksum: self
|
||||
.descriptor
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.split_once('#')
|
||||
.map(|(_, checksum)| checksum)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
keys: self.keys.clone(),
|
||||
hardware_wallets,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_daemon_config(&self) -> Config {
|
||||
Config {
|
||||
#[cfg(unix)]
|
||||
daemon: false,
|
||||
log_level: log::LevelFilter::Info,
|
||||
main_descriptor: self.descriptor.clone().unwrap(),
|
||||
data_dir: Some(self.data_dir.clone()),
|
||||
bitcoin_config: self.bitcoin_config.clone(),
|
||||
bitcoind_config: self.bitcoind_config.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -44,6 +44,7 @@ pub enum DefineDescriptor {
|
||||
Key(bool, usize, DefineKey),
|
||||
HWXpubImported(Result<DescriptorPublicKey, Error>),
|
||||
XPubEdited(String),
|
||||
EditName,
|
||||
NameEdited(String),
|
||||
SequenceEdited(String),
|
||||
ThresholdEdited(bool, usize),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
mod config;
|
||||
mod context;
|
||||
mod message;
|
||||
mod prompt;
|
||||
mod step;
|
||||
@ -7,18 +7,16 @@ mod view;
|
||||
use iced::{clipboard, Command, Element, Subscription};
|
||||
use liana::miniscript::bitcoin;
|
||||
|
||||
use std::convert::TryInto;
|
||||
use context::Context;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
app::config as gui_config, hw::HardwareWalletConfig, installer::config::DEFAULT_FILE_NAME,
|
||||
};
|
||||
use crate::app::{config as gui_config, settings as gui_settings};
|
||||
|
||||
pub use message::Message;
|
||||
use step::{
|
||||
BackupDescriptor, Context, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor,
|
||||
ParticipateXpub, RegisterDescriptor, Step, Welcome,
|
||||
BackupDescriptor, DefineBitcoind, DefineDescriptor, Final, ImportDescriptor, ParticipateXpub,
|
||||
RegisterDescriptor, Step, Welcome,
|
||||
};
|
||||
|
||||
pub struct Installer {
|
||||
@ -164,19 +162,7 @@ impl Installer {
|
||||
}
|
||||
|
||||
pub async fn install(ctx: Context) -> Result<PathBuf, Error> {
|
||||
let hardware_wallets = ctx
|
||||
.hws
|
||||
.iter()
|
||||
.filter_map(|(kind, fingerprint, token)| {
|
||||
token
|
||||
.as_ref()
|
||||
.map(|token| HardwareWalletConfig::new(kind, fingerprint, token))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut cfg: liana::config::Config = ctx
|
||||
.try_into()
|
||||
.expect("Everything should be checked at this point");
|
||||
let mut cfg: liana::config::Config = ctx.extract_daemon_config();
|
||||
// Start Daemon to check correctness of installation
|
||||
let daemon = liana::DaemonHandle::start_default(cfg.clone()).map_err(|e| {
|
||||
Error::Unexpected(format!("Failed to start daemon with entered config: {}", e))
|
||||
@ -191,42 +177,55 @@ pub async fn install(ctx: Context) -> Result<PathBuf, Error> {
|
||||
let mut datadir_path = cfg.data_dir.clone().unwrap();
|
||||
datadir_path.push(cfg.bitcoin_config.network.to_string());
|
||||
|
||||
// create lianad configuration file
|
||||
let mut daemon_config_path = datadir_path.clone();
|
||||
daemon_config_path.push(DEFAULT_FILE_NAME);
|
||||
let mut daemon_config_file = std::fs::File::create(&daemon_config_path)
|
||||
.map_err(|e| Error::CannotCreateFile(e.to_string()))?;
|
||||
|
||||
// Step needed because of ValueAfterTable error in the toml serialize implementation.
|
||||
let daemon_config =
|
||||
toml::Value::try_from(&cfg).expect("daemon::Config has a proper Serialize implementation");
|
||||
|
||||
daemon_config_file
|
||||
.write_all(daemon_config.to_string().as_bytes())
|
||||
.map_err(|e| Error::CannotWriteToFile(e.to_string()))?;
|
||||
// create lianad configuration file
|
||||
let daemon_config_path = create_and_write_file(
|
||||
datadir_path.clone(),
|
||||
"daemon.toml",
|
||||
daemon_config.to_string().as_bytes(),
|
||||
)?;
|
||||
|
||||
// create liana GUI configuration file
|
||||
let mut gui_config_path = datadir_path;
|
||||
gui_config_path.push(gui_config::DEFAULT_FILE_NAME);
|
||||
let mut gui_config_file = std::fs::File::create(&gui_config_path)
|
||||
.map_err(|e| Error::CannotCreateFile(e.to_string()))?;
|
||||
let gui_config_path = create_and_write_file(
|
||||
datadir_path.clone(),
|
||||
gui_config::DEFAULT_FILE_NAME,
|
||||
toml::to_string(&gui_config::Config::new(
|
||||
daemon_config_path.canonicalize().map_err(|e| {
|
||||
Error::Unexpected(format!("Failed to canonicalize daemon config path: {}", e))
|
||||
})?,
|
||||
))
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
gui_config_file
|
||||
.write_all(
|
||||
toml::to_string(&gui_config::Config::new(
|
||||
daemon_config_path.canonicalize().map_err(|e| {
|
||||
Error::Unexpected(format!("Failed to canonicalize daemon config path: {}", e))
|
||||
})?,
|
||||
hardware_wallets,
|
||||
))
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
)
|
||||
.map_err(|e| Error::CannotWriteToFile(e.to_string()))?;
|
||||
// create liana GUI settings file
|
||||
let settings: gui_settings::Settings = ctx.extract_gui_settings();
|
||||
create_and_write_file(
|
||||
datadir_path,
|
||||
gui_settings::DEFAULT_FILE_NAME,
|
||||
serde_json::to_string_pretty(&settings).unwrap().as_bytes(),
|
||||
)?;
|
||||
|
||||
Ok(gui_config_path)
|
||||
}
|
||||
|
||||
pub fn create_and_write_file(
|
||||
mut network_datadir: PathBuf,
|
||||
file_name: &str,
|
||||
data: &[u8],
|
||||
) -> Result<PathBuf, Error> {
|
||||
network_datadir.push(file_name);
|
||||
let path = network_datadir;
|
||||
let mut file =
|
||||
std::fs::File::create(&path).map_err(|e| Error::CannotCreateFile(e.to_string()))?;
|
||||
file.write_all(data)
|
||||
.map_err(|e| Error::CannotWriteToFile(e.to_string()))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
CannotCreateDatadir(String),
|
||||
|
||||
@ -4,3 +4,5 @@ pub const DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP: &str =
|
||||
"This is the keys that can spend received coins immediately,\n with no time restriction.";
|
||||
pub const DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP: &str =
|
||||
"Number of blocks after a coin is received \nfor which the recovery path is not available";
|
||||
pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str =
|
||||
"The alias is applied on all the keys derived from the same seed";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
@ -7,7 +7,7 @@ use liana::{
|
||||
descriptors::{LianaDescKeys, MultipathDescriptor},
|
||||
miniscript::{
|
||||
bitcoin::{
|
||||
util::bip32::{DerivationPath, Fingerprint},
|
||||
util::bip32::{ChildNumber, DerivationPath, Fingerprint},
|
||||
Network,
|
||||
},
|
||||
descriptor::{DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, Wildcard},
|
||||
@ -15,6 +15,7 @@ use liana::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::settings::KeySetting,
|
||||
hw::{list_hardware_wallets, HardwareWallet},
|
||||
installer::{
|
||||
message::{self, Message},
|
||||
@ -24,9 +25,6 @@ use crate::{
|
||||
ui::component::{form, modal::Modal},
|
||||
};
|
||||
|
||||
const LIANA_STANDARD_PATH: &str = "m/48'/0'/0'/2'";
|
||||
const LIANA_TESTNET_STANDARD_PATH: &str = "m/48'/1'/0'/2'";
|
||||
|
||||
pub trait DescriptorKeyModal {
|
||||
fn processing(&self) -> bool {
|
||||
false
|
||||
@ -48,8 +46,6 @@ pub struct DefineDescriptor {
|
||||
sequence: form::Value<String>,
|
||||
modal: Option<Box<dyn DescriptorKeyModal>>,
|
||||
|
||||
name_indexes: (usize, usize),
|
||||
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
@ -59,11 +55,10 @@ impl DefineDescriptor {
|
||||
network: Network::Bitcoin,
|
||||
data_dir: None,
|
||||
network_valid: true,
|
||||
spending_keys: vec![DescriptorKey::new("Key 1".to_string())],
|
||||
spending_keys: vec![DescriptorKey::default()],
|
||||
spending_threshold: 1,
|
||||
recovery_keys: vec![DescriptorKey::new("Recovery key 1".to_string())],
|
||||
recovery_keys: vec![DescriptorKey::default()],
|
||||
recovery_threshold: 1,
|
||||
name_indexes: (1, 1),
|
||||
sequence: form::Value::default(),
|
||||
modal: None,
|
||||
error: None,
|
||||
@ -79,18 +74,22 @@ impl DefineDescriptor {
|
||||
}
|
||||
|
||||
// TODO: Improve algo
|
||||
// Mark as duplicate every defined key that have the same name but not the same fingerprint.
|
||||
// And every undefined_key that have a same name than an other key.
|
||||
fn check_for_duplicate(&mut self) {
|
||||
let mut all_keys = HashSet::new();
|
||||
let mut duplicate_keys = HashSet::new();
|
||||
let mut all_names = HashSet::new();
|
||||
let mut all_names: HashMap<String, Fingerprint> = HashMap::new();
|
||||
let mut duplicate_names = HashSet::new();
|
||||
for spending_key in &self.spending_keys {
|
||||
if all_names.contains(&spending_key.name) {
|
||||
duplicate_names.insert(spending_key.name.clone());
|
||||
} else {
|
||||
all_names.insert(spending_key.name.clone());
|
||||
}
|
||||
if let Some(key) = &spending_key.key {
|
||||
if let Some(fg) = all_names.get(&spending_key.name) {
|
||||
if fg != &key.master_fingerprint() {
|
||||
duplicate_names.insert(spending_key.name.clone());
|
||||
}
|
||||
} else {
|
||||
all_names.insert(spending_key.name.clone(), key.master_fingerprint());
|
||||
}
|
||||
if all_keys.contains(key) {
|
||||
duplicate_keys.insert(key.clone());
|
||||
} else {
|
||||
@ -99,12 +98,14 @@ impl DefineDescriptor {
|
||||
}
|
||||
}
|
||||
for recovery_key in &self.recovery_keys {
|
||||
if all_names.contains(&recovery_key.name) {
|
||||
duplicate_names.insert(recovery_key.name.clone());
|
||||
} else {
|
||||
all_names.insert(recovery_key.name.clone());
|
||||
}
|
||||
if let Some(key) = &recovery_key.key {
|
||||
if let Some(fg) = all_names.get(&recovery_key.name) {
|
||||
if fg != &key.master_fingerprint() {
|
||||
duplicate_names.insert(recovery_key.name.clone());
|
||||
}
|
||||
} else {
|
||||
all_names.insert(recovery_key.name.clone(), key.master_fingerprint());
|
||||
}
|
||||
if all_keys.contains(key) {
|
||||
duplicate_keys.insert(key.clone());
|
||||
} else {
|
||||
@ -124,6 +125,69 @@ impl DefineDescriptor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_alias_for_key_with_same_fingerprint(&mut self, name: String, fingerprint: Fingerprint) {
|
||||
for spending_key in &mut self.spending_keys {
|
||||
if spending_key.key.as_ref().map(|k| k.master_fingerprint()) == Some(fingerprint) {
|
||||
spending_key.name = name.clone();
|
||||
}
|
||||
}
|
||||
for recovery_key in &mut self.recovery_keys {
|
||||
if recovery_key.key.as_ref().map(|k| k.master_fingerprint()) == Some(fingerprint) {
|
||||
recovery_key.name = name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the maximum account index per key fingerprint
|
||||
fn fingerprint_account_index_mappping(&self) -> HashMap<Fingerprint, ChildNumber> {
|
||||
let mut mapping = HashMap::new();
|
||||
let update_mapping =
|
||||
|keys: &[DescriptorKey], mapping: &mut HashMap<Fingerprint, ChildNumber>| {
|
||||
for key in keys {
|
||||
if let Some(DescriptorPublicKey::MultiXPub(key)) = key.key.as_ref() {
|
||||
if let Some((fingerprint, derivation_path)) = key.origin.as_ref() {
|
||||
let index = if derivation_path.len() >= 4 {
|
||||
if derivation_path[0].to_string() == "48'" {
|
||||
Some(derivation_path[2])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(index) = index {
|
||||
if let Some(previous_index) = mapping.get(fingerprint) {
|
||||
if index > *previous_index {
|
||||
mapping.insert(*fingerprint, index);
|
||||
}
|
||||
} else {
|
||||
mapping.insert(*fingerprint, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
update_mapping(&self.spending_keys, &mut mapping);
|
||||
update_mapping(&self.recovery_keys, &mut mapping);
|
||||
mapping
|
||||
}
|
||||
|
||||
fn keys_aliases(&self) -> HashMap<Fingerprint, String> {
|
||||
let mut map = HashMap::new();
|
||||
for spending_key in &self.spending_keys {
|
||||
if let Some(key) = spending_key.key.as_ref() {
|
||||
map.insert(key.master_fingerprint(), spending_key.name.clone());
|
||||
}
|
||||
}
|
||||
for recovery_key in &self.recovery_keys {
|
||||
if let Some(key) = recovery_key.key.as_ref() {
|
||||
map.insert(key.master_fingerprint(), recovery_key.name.clone());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
impl Step for DefineDescriptor {
|
||||
@ -164,16 +228,10 @@ impl Step for DefineDescriptor {
|
||||
}
|
||||
message::DefineDescriptor::AddKey(is_recovery) => {
|
||||
if is_recovery {
|
||||
self.name_indexes.0 += 1;
|
||||
self.recovery_keys.push(DescriptorKey::new(format!(
|
||||
"Recovery key {}",
|
||||
self.name_indexes.0,
|
||||
)));
|
||||
self.recovery_keys.push(DescriptorKey::default());
|
||||
self.recovery_threshold += 1;
|
||||
} else {
|
||||
self.name_indexes.1 += 1;
|
||||
self.spending_keys
|
||||
.push(DescriptorKey::new(format!("Key {}", self.name_indexes.1,)));
|
||||
self.spending_keys.push(DescriptorKey::default());
|
||||
self.spending_threshold += 1;
|
||||
}
|
||||
}
|
||||
@ -182,6 +240,10 @@ impl Step for DefineDescriptor {
|
||||
return Command::perform(async move { key }, Message::Clibpboard);
|
||||
}
|
||||
message::DefineKey::Edited(name, imported_key) => {
|
||||
self.edit_alias_for_key_with_same_fingerprint(
|
||||
name.clone(),
|
||||
imported_key.master_fingerprint(),
|
||||
);
|
||||
if is_recovery {
|
||||
if let Some(recovery_key) = self.recovery_keys.get_mut(i) {
|
||||
recovery_key.name = name;
|
||||
@ -207,8 +269,15 @@ impl Step for DefineDescriptor {
|
||||
k.to_string().trim_end_matches("/<0;1>/*").to_string()
|
||||
})
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let modal =
|
||||
EditXpubModal::new(name, key, i, is_recovery, self.network);
|
||||
let modal = EditXpubModal::new(
|
||||
name,
|
||||
key,
|
||||
i,
|
||||
is_recovery,
|
||||
self.network,
|
||||
self.fingerprint_account_index_mappping(),
|
||||
self.keys_aliases(),
|
||||
);
|
||||
let cmd = modal.load();
|
||||
self.modal = Some(Box::new(modal));
|
||||
return cmd;
|
||||
@ -220,8 +289,15 @@ impl Step for DefineDescriptor {
|
||||
.as_ref()
|
||||
.map(|k| k.to_string().trim_end_matches("/<0;1>/*").to_string())
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let modal =
|
||||
EditXpubModal::new(name, key, i, is_recovery, self.network);
|
||||
let modal = EditXpubModal::new(
|
||||
name,
|
||||
key,
|
||||
i,
|
||||
is_recovery,
|
||||
self.network,
|
||||
self.fingerprint_account_index_mappping(),
|
||||
self.keys_aliases(),
|
||||
);
|
||||
let cmd = modal.load();
|
||||
self.modal = Some(Box::new(modal));
|
||||
return cmd;
|
||||
@ -268,17 +344,36 @@ impl Step for DefineDescriptor {
|
||||
|
||||
fn apply(&mut self, ctx: &mut Context) -> bool {
|
||||
ctx.bitcoin_config.network = self.network;
|
||||
let spending_keys: Vec<DescriptorPublicKey> = self
|
||||
.spending_keys
|
||||
.iter()
|
||||
.filter_map(|k| k.key.clone())
|
||||
.collect();
|
||||
ctx.keys = Vec::new();
|
||||
let mut spending_keys: Vec<DescriptorPublicKey> = Vec::new();
|
||||
for spending_key in self.spending_keys.iter().clone() {
|
||||
if let Some(key) = spending_key.key.as_ref() {
|
||||
if let DescriptorPublicKey::MultiXPub(xpub) = key {
|
||||
if let Some((master_fingerprint, _)) = xpub.origin {
|
||||
ctx.keys.push(KeySetting {
|
||||
master_fingerprint,
|
||||
name: spending_key.name.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
spending_keys.push(key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let recovery_keys: Vec<DescriptorPublicKey> = self
|
||||
.recovery_keys
|
||||
.iter()
|
||||
.filter_map(|k| k.key.clone())
|
||||
.collect();
|
||||
let mut recovery_keys: Vec<DescriptorPublicKey> = Vec::new();
|
||||
for recovery_key in self.recovery_keys.iter().clone() {
|
||||
if let Some(key) = recovery_key.key.as_ref() {
|
||||
if let DescriptorPublicKey::MultiXPub(xpub) = key {
|
||||
if let Some((master_fingerprint, _)) = xpub.origin {
|
||||
ctx.keys.push(KeySetting {
|
||||
master_fingerprint,
|
||||
name: recovery_key.name.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
recovery_keys.push(key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let sequence = self.sequence.value.parse::<u16>();
|
||||
self.sequence.valid = sequence.is_ok();
|
||||
@ -378,17 +473,19 @@ pub struct DescriptorKey {
|
||||
pub duplicate_name: bool,
|
||||
}
|
||||
|
||||
impl DescriptorKey {
|
||||
pub fn new(name: String) -> Self {
|
||||
impl Default for DescriptorKey {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name,
|
||||
name: "".to_string(),
|
||||
valid: true,
|
||||
key: None,
|
||||
duplicate_key: false,
|
||||
duplicate_name: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DescriptorKey {
|
||||
pub fn check_network(&mut self, network: Network) {
|
||||
if let Some(key) = &self.key {
|
||||
self.valid = check_key_network(key, network);
|
||||
@ -397,7 +494,7 @@ impl DescriptorKey {
|
||||
|
||||
pub fn view(&self) -> Element<message::DefineKey> {
|
||||
match &self.key {
|
||||
None => view::undefined_descriptor_key(&self.name),
|
||||
None => view::undefined_descriptor_key(),
|
||||
Some(_) => view::defined_descriptor_key(
|
||||
&self.name,
|
||||
self.valid,
|
||||
@ -447,8 +544,12 @@ pub struct EditXpubModal {
|
||||
error: Option<Error>,
|
||||
processing: bool,
|
||||
|
||||
keys_aliases: HashMap<Fingerprint, String>,
|
||||
account_indexes: HashMap<Fingerprint, ChildNumber>,
|
||||
|
||||
form_name: form::Value<String>,
|
||||
form_xpub: form::Value<String>,
|
||||
edit_name: bool,
|
||||
|
||||
chosen_hw: Option<usize>,
|
||||
hws: Vec<HardwareWallet>,
|
||||
@ -461,6 +562,8 @@ impl EditXpubModal {
|
||||
key_index: usize,
|
||||
is_recovery: bool,
|
||||
network: Network,
|
||||
account_indexes: HashMap<Fingerprint, ChildNumber>,
|
||||
keys_aliases: HashMap<Fingerprint, String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
form_name: form::Value {
|
||||
@ -471,6 +574,8 @@ impl EditXpubModal {
|
||||
valid: true,
|
||||
value: key,
|
||||
},
|
||||
keys_aliases,
|
||||
account_indexes,
|
||||
is_recovery,
|
||||
key_index,
|
||||
chosen_hw: None,
|
||||
@ -478,6 +583,7 @@ impl EditXpubModal {
|
||||
hws: Vec::new(),
|
||||
error: None,
|
||||
network,
|
||||
edit_name: false,
|
||||
}
|
||||
}
|
||||
fn load(&self) -> Command<Message> {
|
||||
@ -500,8 +606,14 @@ impl DescriptorKeyModal for EditXpubModal {
|
||||
let device = hw.device.clone();
|
||||
self.chosen_hw = Some(i);
|
||||
self.processing = true;
|
||||
// If another account n exists, the key is retrieved for the account n+1
|
||||
let account_index = self
|
||||
.account_indexes
|
||||
.get(&hw.fingerprint)
|
||||
.map(|account_index| account_index.increment().unwrap())
|
||||
.unwrap_or_else(|| ChildNumber::from_hardened_idx(0).unwrap());
|
||||
return Command::perform(
|
||||
get_extended_pubkey(device, hw.fingerprint, self.network),
|
||||
get_extended_pubkey(device, hw.fingerprint, self.network, account_index),
|
||||
|res| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::HWXpubImported(
|
||||
res,
|
||||
@ -520,6 +632,14 @@ impl DescriptorKeyModal for EditXpubModal {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok(key) => {
|
||||
if let Some(alias) = self.keys_aliases.get(&key.master_fingerprint()) {
|
||||
self.form_name.valid = true;
|
||||
self.form_name.value = alias.clone();
|
||||
self.edit_name = false;
|
||||
} else {
|
||||
self.edit_name = true;
|
||||
}
|
||||
self.form_xpub.valid = true;
|
||||
self.form_xpub.value =
|
||||
key.to_string().trim_end_matches("/<0;1>/*").to_string();
|
||||
}
|
||||
@ -528,13 +648,32 @@ impl DescriptorKeyModal for EditXpubModal {
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::EditName) => {
|
||||
self.edit_name = true;
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::NameEdited(name)) => {
|
||||
self.form_name.valid = true;
|
||||
self.form_name.value = name;
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::XPubEdited(s)) => {
|
||||
self.form_xpub.valid =
|
||||
DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", s)).is_ok();
|
||||
if let Ok(DescriptorPublicKey::MultiXPub(key)) =
|
||||
DescriptorPublicKey::from_str(&format!("{}/<0;1>/*", s))
|
||||
{
|
||||
if let Some((fingerprint, _)) = key.origin {
|
||||
self.form_xpub.valid = true;
|
||||
if let Some(alias) = self.keys_aliases.get(&fingerprint) {
|
||||
self.form_name.valid = true;
|
||||
self.form_name.value = alias.clone();
|
||||
self.edit_name = false;
|
||||
} else {
|
||||
self.edit_name = true;
|
||||
}
|
||||
} else {
|
||||
self.form_xpub.valid = false;
|
||||
}
|
||||
} else {
|
||||
self.form_xpub.valid = false;
|
||||
}
|
||||
self.form_xpub.value = s;
|
||||
}
|
||||
Message::DefineDescriptor(message::DefineDescriptor::ConfirmXpub) => {
|
||||
@ -570,19 +709,25 @@ impl DescriptorKeyModal for EditXpubModal {
|
||||
self.chosen_hw,
|
||||
&self.form_xpub,
|
||||
&self.form_name,
|
||||
self.edit_name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// LIANA_STANDARD_PATH: m/48'/0'/0'/2';
|
||||
/// LIANA_TESTNET_STANDARD_PATH: m/48'/1'/0'/2';
|
||||
async fn get_extended_pubkey(
|
||||
hw: std::sync::Arc<dyn async_hwi::HWI + Send + Sync>,
|
||||
fingerprint: Fingerprint,
|
||||
network: Network,
|
||||
account_index: ChildNumber,
|
||||
) -> Result<DescriptorPublicKey, Error> {
|
||||
let derivation_path = DerivationPath::from_str(if network == Network::Bitcoin {
|
||||
LIANA_STANDARD_PATH
|
||||
} else {
|
||||
LIANA_TESTNET_STANDARD_PATH
|
||||
let derivation_path = DerivationPath::from_str(&{
|
||||
if network == Network::Bitcoin {
|
||||
format!("m/48'/0'/{}/2'", account_index)
|
||||
} else {
|
||||
format!("m/48'/1'/{}/2'", account_index)
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let xkey = hw
|
||||
@ -667,7 +812,12 @@ impl Step for ParticipateXpub {
|
||||
self.processing = true;
|
||||
self.error = None;
|
||||
return Command::perform(
|
||||
get_extended_pubkey(device, hw.fingerprint, self.network),
|
||||
get_extended_pubkey(
|
||||
device,
|
||||
hw.fingerprint,
|
||||
self.network,
|
||||
ChildNumber::from_hardened_idx(0).unwrap(),
|
||||
),
|
||||
Message::ImportXpub,
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,19 +5,14 @@ pub use descriptor::{
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_hwi::DeviceKind;
|
||||
use iced::{Command, Element};
|
||||
use liana::{
|
||||
config::{BitcoinConfig, BitcoindConfig},
|
||||
descriptors::MultipathDescriptor,
|
||||
miniscript::bitcoin,
|
||||
};
|
||||
use liana::{config::BitcoindConfig, miniscript::bitcoin};
|
||||
|
||||
use crate::ui::component::form;
|
||||
|
||||
use crate::installer::{
|
||||
context::Context,
|
||||
message::{self, Message},
|
||||
view,
|
||||
};
|
||||
@ -39,34 +34,6 @@ pub trait Step {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pub bitcoin_config: BitcoinConfig,
|
||||
pub bitcoind_config: Option<BitcoindConfig>,
|
||||
pub descriptor: Option<MultipathDescriptor>,
|
||||
pub hws: Vec<(
|
||||
DeviceKind,
|
||||
bitcoin::util::bip32::Fingerprint,
|
||||
Option<[u8; 32]>,
|
||||
)>,
|
||||
pub data_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(network: bitcoin::Network, data_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
bitcoin_config: BitcoinConfig {
|
||||
network,
|
||||
poll_interval_secs: Duration::from_secs(30),
|
||||
},
|
||||
hws: Vec::new(),
|
||||
bitcoind_config: None,
|
||||
descriptor: None,
|
||||
data_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Welcome {}
|
||||
|
||||
|
||||
@ -8,9 +8,9 @@ use liana::miniscript::bitcoin;
|
||||
use crate::{
|
||||
hw::HardwareWallet,
|
||||
installer::{
|
||||
context::Context,
|
||||
message::{self, Message},
|
||||
step::Context,
|
||||
Error,
|
||||
prompt, Error,
|
||||
},
|
||||
ui::{
|
||||
color,
|
||||
@ -171,9 +171,7 @@ pub fn define_descriptor<'a>(
|
||||
.spacing(10)
|
||||
.push(Space::with_width(Length::Units(40)))
|
||||
.push(text("Primary path:").bold())
|
||||
.push(tooltip(
|
||||
super::prompt::DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP,
|
||||
)),
|
||||
.push(tooltip(prompt::DEFINE_DESCRIPTOR_PRIMATRY_PATH_TOOLTIP)),
|
||||
)
|
||||
.push(separation().width(Length::Fill))
|
||||
.push(
|
||||
@ -294,7 +292,7 @@ pub fn define_descriptor<'a>(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(text("Blocks before recovery:").bold())
|
||||
.push(tooltip(super::prompt::DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP)),
|
||||
.push(tooltip(prompt::DEFINE_DESCRIPTOR_SEQUENCE_TOOLTIP)),
|
||||
)
|
||||
.push(
|
||||
Container::new(
|
||||
@ -319,7 +317,7 @@ pub fn define_descriptor<'a>(
|
||||
layout(
|
||||
progress,
|
||||
Column::new()
|
||||
.push(Space::with_height(Length::Units(50)))
|
||||
.push(Space::with_height(Length::Units(30)))
|
||||
.push(text("Create the wallet").bold().size(50))
|
||||
.push(
|
||||
Column::new()
|
||||
@ -620,7 +618,7 @@ pub fn backup_descriptor<'a>(
|
||||
)
|
||||
.push(
|
||||
Column::new()
|
||||
.push(text(super::prompt::BACKUP_DESCRIPTOR_MESSAGE))
|
||||
.push(text(prompt::BACKUP_DESCRIPTOR_MESSAGE))
|
||||
.push(collapse::Collapse::new(
|
||||
|| {
|
||||
Button::new(
|
||||
@ -680,7 +678,7 @@ pub fn backup_descriptor<'a>(
|
||||
}
|
||||
|
||||
pub fn help_backup<'a>() -> Element<'a, Message> {
|
||||
text(super::prompt::BACKUP_DESCRIPTOR_HELP).small().into()
|
||||
text(prompt::BACKUP_DESCRIPTOR_HELP).small().into()
|
||||
}
|
||||
|
||||
pub fn define_bitcoin<'a>(
|
||||
@ -854,7 +852,7 @@ pub fn install<'a>(
|
||||
layout(progress, col)
|
||||
}
|
||||
|
||||
pub fn undefined_descriptor_key(name: &str) -> Element<message::DefineKey> {
|
||||
pub fn undefined_descriptor_key<'a>() -> Element<'a, message::DefineKey> {
|
||||
card::simple(
|
||||
Column::new()
|
||||
.width(Length::Fill)
|
||||
@ -875,8 +873,13 @@ pub fn undefined_descriptor_key(name: &str) -> Element<message::DefineKey> {
|
||||
.spacing(15)
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Scrollable::new(text(name).bold())
|
||||
.horizontal_scroll(Properties::new().width(2).scroller_width(2)),
|
||||
Scrollable::new(
|
||||
icon::key_icon()
|
||||
.style(color::DARK_GREY)
|
||||
.size(50)
|
||||
.width(Length::Units(50)),
|
||||
)
|
||||
.horizontal_scroll(Properties::new().width(2).scroller_width(2)),
|
||||
)
|
||||
.push(icon::circle_check_icon().style(color::FOREGROUND).size(50)),
|
||||
)
|
||||
@ -884,8 +887,7 @@ pub fn undefined_descriptor_key(name: &str) -> Element<message::DefineKey> {
|
||||
.align_y(alignment::Vertical::Center),
|
||||
)
|
||||
.push(
|
||||
button::border(Some(icon::pencil_icon()), "Edit")
|
||||
.on_press(message::DefineKey::Edit),
|
||||
button::border(Some(icon::pencil_icon()), "Set").on_press(message::DefineKey::Edit),
|
||||
)
|
||||
.push(Space::with_height(Length::Units(5))),
|
||||
)
|
||||
@ -967,7 +969,7 @@ pub fn defined_descriptor_key(
|
||||
.height(Length::Units(200))
|
||||
.width(Length::Units(200)),
|
||||
)
|
||||
.push(text("Key is a duplicate").small().style(color::ALERT))
|
||||
.push(text("Duplicate key").small().style(color::ALERT))
|
||||
.into()
|
||||
} else if duplicate_name {
|
||||
Column::new()
|
||||
@ -978,7 +980,7 @@ pub fn defined_descriptor_key(
|
||||
.height(Length::Units(200))
|
||||
.width(Length::Units(200)),
|
||||
)
|
||||
.push(text("Name is a duplicate").small().style(color::ALERT))
|
||||
.push(text("Duplicate name").small().style(color::ALERT))
|
||||
.into()
|
||||
} else {
|
||||
card::simple(col)
|
||||
@ -989,6 +991,7 @@ pub fn defined_descriptor_key(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn edit_key_modal<'a>(
|
||||
network: bitcoin::Network,
|
||||
hws: &[HardwareWallet],
|
||||
@ -996,62 +999,14 @@ pub fn edit_key_modal<'a>(
|
||||
processing: bool,
|
||||
chosen_hw: Option<usize>,
|
||||
form_xpub: &form::Value<String>,
|
||||
form_name: &form::Value<String>,
|
||||
form_name: &'a form::Value<String>,
|
||||
edit_name: bool,
|
||||
) -> Element<'a, Message> {
|
||||
Column::new()
|
||||
.push_maybe(error.map(|e| card::error("Failed to import xpub", e.to_string())))
|
||||
.push(card::simple(
|
||||
Column::new()
|
||||
.spacing(25)
|
||||
.push(
|
||||
Container::new(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(icon::pencil_icon())
|
||||
.push(text("Edit")),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.align_x(alignment::Horizontal::Center),
|
||||
)
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(text("Edit name:").bold())
|
||||
.push(
|
||||
form::Form::new("Name", form_name, |msg| {
|
||||
Message::DefineDescriptor(message::DefineDescriptor::NameEdited(
|
||||
msg,
|
||||
))
|
||||
})
|
||||
.warning("Please enter correct name")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
),
|
||||
)
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(text("Enter an extended public key:").bold())
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
form::Form::new("Extended public key", form_xpub, |msg| {
|
||||
Message::DefineDescriptor(
|
||||
message::DefineDescriptor::XPubEdited(msg),
|
||||
)
|
||||
})
|
||||
.warning(if network == bitcoin::Network::Bitcoin {
|
||||
"Please enter correct xpub"
|
||||
} else {
|
||||
"Please enter correct tpub"
|
||||
})
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(10)
|
||||
.push(Container::new(text("/<0;1>/*")).padding(5)),
|
||||
),
|
||||
)
|
||||
.push(if !hws.is_empty() {
|
||||
Column::new()
|
||||
.push(
|
||||
@ -1059,7 +1014,7 @@ pub fn edit_key_modal<'a>(
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.push(
|
||||
Container::new(text("Or select a hardware wallet:").bold())
|
||||
Container::new(text("Select a hardware wallet:").bold())
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.push(
|
||||
@ -1100,6 +1055,75 @@ pub fn edit_key_modal<'a>(
|
||||
)
|
||||
.width(Length::Fill)
|
||||
})
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(text("Or enter an extended public key:").bold())
|
||||
.push(
|
||||
Row::new()
|
||||
.push(
|
||||
form::Form::new("Extended public key", form_xpub, |msg| {
|
||||
Message::DefineDescriptor(
|
||||
message::DefineDescriptor::XPubEdited(msg),
|
||||
)
|
||||
})
|
||||
.warning(if network == bitcoin::Network::Bitcoin {
|
||||
"Please enter correct xpub with origin"
|
||||
} else {
|
||||
"Please enter correct tpub with origin"
|
||||
})
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
.spacing(10)
|
||||
.push(Container::new(text("/<0;1>/*")).padding(5)),
|
||||
),
|
||||
)
|
||||
.push(
|
||||
if !edit_name && !form_xpub.value.is_empty() && form_xpub.valid {
|
||||
Column::new().push(
|
||||
Row::new()
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.width(Length::Fill)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text("Fingerprint alias:").bold())
|
||||
.push(tooltip(
|
||||
prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP,
|
||||
)),
|
||||
)
|
||||
.push(text(&form_name.value)),
|
||||
)
|
||||
.push(button::border(Some(icon::pencil_icon()), "Edit").on_press(
|
||||
Message::DefineDescriptor(message::DefineDescriptor::EditName),
|
||||
)),
|
||||
)
|
||||
} else if !form_xpub.value.is_empty() && form_xpub.valid {
|
||||
Column::new()
|
||||
.spacing(5)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.push(text("Fingerprint alias:").bold())
|
||||
.push(tooltip(prompt::DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP)),
|
||||
)
|
||||
.push(
|
||||
form::Form::new("Alias", form_name, |msg| {
|
||||
Message::DefineDescriptor(
|
||||
message::DefineDescriptor::NameEdited(msg),
|
||||
)
|
||||
})
|
||||
.warning("Please enter correct alias")
|
||||
.size(20)
|
||||
.padding(10),
|
||||
)
|
||||
} else {
|
||||
Column::new()
|
||||
},
|
||||
)
|
||||
.push(
|
||||
if form_xpub.valid && !form_xpub.value.is_empty() && !form_name.value.is_empty()
|
||||
{
|
||||
|
||||
@ -17,7 +17,12 @@ use liana::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::config::{default_datadir, Config as GUIConfig},
|
||||
app::{
|
||||
cache::Cache,
|
||||
config::{default_datadir, Config as GUIConfig},
|
||||
settings::{self, Settings},
|
||||
wallet::Wallet,
|
||||
},
|
||||
daemon::{client, embedded::EmbeddedDaemon, model::*, Daemon, DaemonError},
|
||||
ui::{
|
||||
component::{button, notification, text::*},
|
||||
@ -30,6 +35,7 @@ type Lianad = client::Lianad<client::jsonrpc::JsonRPCClient>;
|
||||
|
||||
pub struct Loader {
|
||||
pub datadir_path: Option<PathBuf>,
|
||||
pub network: bitcoin::Network,
|
||||
pub gui_config: GUIConfig,
|
||||
pub daemon_started: bool,
|
||||
|
||||
@ -47,16 +53,12 @@ pub enum Step {
|
||||
Error(Box<Error>),
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
View(ViewMessage),
|
||||
Syncing(Result<GetInfoResult, DaemonError>),
|
||||
Synced(
|
||||
GetInfoResult,
|
||||
Vec<Coin>,
|
||||
Vec<SpendTx>,
|
||||
Arc<dyn Daemon + Sync + Send>,
|
||||
),
|
||||
Synced(Result<(Arc<Wallet>, Cache, Arc<dyn Daemon + Sync + Send>), Error>),
|
||||
Started(Result<Arc<dyn Daemon + Sync + Send>, Error>),
|
||||
Loaded(Result<Arc<dyn Daemon + Sync + Send>, Error>),
|
||||
Failure(DaemonError),
|
||||
@ -75,6 +77,7 @@ impl Loader {
|
||||
.unwrap();
|
||||
(
|
||||
Loader {
|
||||
network: daemon_config.bitcoin_config.network,
|
||||
datadir_path,
|
||||
daemon_config: daemon_config.clone(),
|
||||
gui_config,
|
||||
@ -141,18 +144,47 @@ impl Loader {
|
||||
Ok(info) => {
|
||||
if (info.sync - 1.0_f64).abs() < f64::EPSILON {
|
||||
let daemon = daemon.clone();
|
||||
let settings_path =
|
||||
settings_path(&self.datadir_path, self.network).unwrap();
|
||||
let gui_config_hws = self
|
||||
.gui_config
|
||||
.hardware_wallets
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
return Command::perform(
|
||||
async move {
|
||||
let coins = daemon
|
||||
.list_coins()
|
||||
.map(|res| res.coins)
|
||||
.unwrap_or_else(|_| Vec::new());
|
||||
let spend_txs = daemon
|
||||
.list_spend_transactions()
|
||||
.unwrap_or_else(|_| Vec::new());
|
||||
(info, coins, spend_txs, daemon)
|
||||
let coins = daemon.list_coins().map(|res| res.coins)?;
|
||||
let spend_txs = daemon.list_spend_transactions()?;
|
||||
let cache = Cache {
|
||||
network: info.network,
|
||||
blockheight: info.block_height,
|
||||
coins,
|
||||
spend_txs,
|
||||
..Default::default()
|
||||
};
|
||||
let wallet = match Settings::from_file(&settings_path) {
|
||||
Ok(settings) => {
|
||||
if let Some(wallet_setting) = settings.wallets.first() {
|
||||
Wallet::legacy(info.descriptors.main)
|
||||
.with_harware_wallets(
|
||||
wallet_setting.hardware_wallets.clone(),
|
||||
)
|
||||
.with_key_aliases(wallet_setting.keys_aliases())
|
||||
} else {
|
||||
Wallet::legacy(info.descriptors.main)
|
||||
.with_harware_wallets(gui_config_hws)
|
||||
}
|
||||
}
|
||||
Err(settings::SettingsError::NotFound) => {
|
||||
Wallet::legacy(info.descriptors.main)
|
||||
.with_harware_wallets(gui_config_hws)
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
Ok((Arc::new(wallet), cache, daemon))
|
||||
},
|
||||
|res| Message::Synced(res.0, res.1, res.2, res.3),
|
||||
Message::Synced,
|
||||
);
|
||||
} else {
|
||||
*progress = info.sync
|
||||
@ -333,6 +365,7 @@ async fn sync(
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Settings(settings::SettingsError),
|
||||
Config(ConfigError),
|
||||
Daemon(DaemonError),
|
||||
}
|
||||
@ -340,12 +373,19 @@ pub enum Error {
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Settings(e) => write!(f, "Settings error: {}", e),
|
||||
Self::Config(e) => write!(f, "Config error: {}", e),
|
||||
Self::Daemon(e) => write!(f, "Liana daemon error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<settings::SettingsError> for Error {
|
||||
fn from(error: settings::SettingsError) -> Self {
|
||||
Error::Settings(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigError> for Error {
|
||||
fn from(error: ConfigError) -> Self {
|
||||
Error::Config(error)
|
||||
@ -372,3 +412,18 @@ fn socket_path(
|
||||
path.push("lianad_rpc");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// default liana settings path is .liana/bitcoin/settings.json
|
||||
fn settings_path(
|
||||
datadir: &Option<PathBuf>,
|
||||
network: bitcoin::Network,
|
||||
) -> Result<PathBuf, ConfigError> {
|
||||
let mut path = if let Some(ref datadir) = datadir {
|
||||
datadir.clone()
|
||||
} else {
|
||||
default_datadir().map_err(|_| ConfigError::DatadirNotFound)?
|
||||
};
|
||||
path.push(network.to_string());
|
||||
path.push(settings::DEFAULT_FILE_NAME);
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
@ -11,9 +11,7 @@ use liana::{config::Config as DaemonConfig, miniscript::bitcoin};
|
||||
use liana_gui::{
|
||||
app::{
|
||||
self,
|
||||
cache::Cache,
|
||||
config::{default_datadir, ConfigError},
|
||||
wallet::Wallet,
|
||||
App,
|
||||
},
|
||||
installer::{self, Installer},
|
||||
@ -205,17 +203,7 @@ impl Application for GUI {
|
||||
)));
|
||||
Command::none()
|
||||
}
|
||||
loader::Message::Synced(info, coins, spend_txs, daemon) => {
|
||||
let cache = Cache {
|
||||
network: info.network,
|
||||
blockheight: info.block_height,
|
||||
coins,
|
||||
spend_txs,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let wallet = Wallet::new(info.descriptors.main);
|
||||
|
||||
loader::Message::Synced(Ok((wallet, cache, daemon))) => {
|
||||
let (app, command) = App::new(cache, wallet, loader.gui_config.clone(), daemon);
|
||||
self.state = State::App(app);
|
||||
command.map(|msg| Message::Run(Box::new(msg)))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user