Add wallet id for settings filtering

This commit is contained in:
edouardparis 2025-05-07 15:23:36 +02:00
parent b4062b28a9
commit 8ddcece820
8 changed files with 176 additions and 166 deletions

View File

@ -26,6 +26,24 @@ pub struct Settings {
pub wallets: Vec<WalletSettings>,
}
impl Settings {
pub fn from_file(network_dir: &NetworkDirectory) -> Result<Settings, SettingsError> {
let mut path = network_dir.path().to_path_buf();
path.push(DEFAULT_FILE_NAME);
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))
})
})
}
}
pub async fn update_settings_file<F>(
network_dir: &NetworkDirectory,
updater: F,
@ -107,6 +125,7 @@ impl AuthConfig {
pub struct WalletSettings {
pub name: String,
pub descriptor_checksum: String,
pub pinned_at: Option<i64>,
// if wallet is using remote backend, then this information is stored on the remote backend
// wallet metadata
#[serde(default)]
@ -129,20 +148,7 @@ impl WalletSettings {
where
F: FnMut(&WalletSettings) -> bool,
{
let mut path = network_dir.path().to_path_buf();
path.push(DEFAULT_FILE_NAME);
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))
})
})
.map(|cache| cache.wallets.into_iter().find(selecter))
Settings::from_file(network_dir).map(|cache| cache.wallets.into_iter().find(selecter))
}
pub fn keys_aliases(&self) -> HashMap<Fingerprint, String> {
@ -183,6 +189,14 @@ impl WalletSettings {
.collect();
}
}
pub fn wallet_id(&self) -> String {
if let Some(t) = self.pinned_at {
format!("{}-{}", self.descriptor_checksum, t)
} else {
self.descriptor_checksum.clone()
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]

View File

@ -428,12 +428,12 @@ async fn register_wallet(
if daemon.backend() != DaemonBackend::RemoteBackend {
let network_dir = data_dir.network_directory(network);
let checksum = wallet.descriptor_checksum();
let wallet_id = wallet.id();
update_settings_file(&network_dir, |mut settings| {
if let Some(wallet_setting) = settings
.wallets
.iter_mut()
.find(|w| w.descriptor_checksum == checksum)
.find(|w| w.wallet_id() == wallet_id)
{
if let Some(hw_config) = wallet_setting
.hardware_wallets
@ -479,12 +479,12 @@ pub async fn update_keys_aliases(
) -> Result<Arc<Wallet>, Error> {
if daemon.backend() != DaemonBackend::RemoteBackend {
let network_dir = data_dir.network_directory(network);
let checksum = wallet.descriptor_checksum();
let wallet_id = wallet.id();
update_settings_file(&network_dir, |mut settings| {
if let Some(wallet_setting) = settings
.wallets
.iter_mut()
.find(|w| w.descriptor_checksum == checksum)
.find(|w| w.wallet_id() == wallet_id)
{
wallet_setting.keys = keys_aliases
.iter()

View File

@ -1,7 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use crate::dir::{LianaDirectory, NetworkDirectory};
use crate::dir::LianaDirectory;
use crate::{
app::settings, daemon::DaemonBackend, hw::HardwareWalletConfig, node::NodeType, signer::Signer,
};
@ -11,6 +11,8 @@ use liana::{miniscript::bitcoin, signer::HotSigner};
use liana::descriptors::LianaDescriptor;
use liana::miniscript::bitcoin::bip32::Fingerprint;
use super::settings::WalletSettings;
const DEFAULT_WALLET_NAME: &str = "Liana";
pub fn wallet_name(main_descriptor: &LianaDescriptor) -> String {
@ -31,6 +33,8 @@ pub fn wallet_name(main_descriptor: &LianaDescriptor) -> String {
pub struct Wallet {
pub name: String,
pub main_descriptor: LianaDescriptor,
pub descriptor_checksum: String,
pub pinned_at: Option<i64>,
// TODO: We could replace these two fields with `keys: HashMap<Fingerprint, settings::KeySetting>`.
pub keys_aliases: HashMap<Fingerprint, String>,
pub provider_keys: HashMap<Fingerprint, settings::ProviderKey>,
@ -42,6 +46,13 @@ impl Wallet {
pub fn new(main_descriptor: LianaDescriptor) -> Self {
Self {
name: wallet_name(&main_descriptor),
descriptor_checksum: main_descriptor
.to_string()
.split_once('#')
.map(|(_, checksum)| checksum)
.unwrap()
.to_string(),
pinned_at: None,
main_descriptor,
keys_aliases: HashMap::new(),
provider_keys: HashMap::new(),
@ -55,6 +66,20 @@ impl Wallet {
self
}
pub fn with_pin_date(mut self, pinned_at: Option<i64>) -> Self {
self.pinned_at = pinned_at;
self
}
// To match with WalletSettings.wallet_id
pub fn id(&self) -> String {
if let Some(t) = self.pinned_at {
format!("{}-{}", self.descriptor_checksum, t)
} else {
self.descriptor_checksum.clone()
}
}
pub fn with_key_aliases(mut self, aliases: HashMap<Fingerprint, String>) -> Self {
self.keys_aliases = aliases;
self
@ -92,26 +117,15 @@ impl Wallet {
descriptor_keys
}
pub fn descriptor_checksum(&self) -> String {
self.main_descriptor
.to_string()
.split_once('#')
.map(|(_, checksum)| checksum)
.unwrap()
.to_string()
}
pub fn load_from_settings(self, dir: &NetworkDirectory) -> Result<Self, WalletError> {
if let Some(wallet_settings) = settings::WalletSettings::from_file(dir, |w| {
w.descriptor_checksum == self.descriptor_checksum()
})? {
pub fn load_from_settings(self, wallet_settings: WalletSettings) -> Result<Self, WalletError> {
if wallet_settings.descriptor_checksum != self.descriptor_checksum {
Err(WalletError::WrongWalletLoaded)
} else {
Ok(self
.with_key_aliases(wallet_settings.keys_aliases())
.with_provider_keys(wallet_settings.provider_keys())
.with_name(wallet_settings.name)
.with_hardware_wallets(wallet_settings.hardware_wallets))
} else {
Ok(self)
}
}
@ -172,6 +186,7 @@ impl Wallet {
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum WalletError {
WrongWalletLoaded,
Settings(settings::SettingsError),
HotSigner(String),
}
@ -179,6 +194,7 @@ pub enum WalletError {
impl std::fmt::Display for WalletError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::WrongWalletLoaded => write!(f, "Wrong wallet was loaded"),
Self::Settings(e) => write!(f, "Failed to load settings: {}", e),
Self::HotSigner(e) => write!(f, "Failed to load hot signer: {}", e),
}

View File

@ -199,10 +199,9 @@ impl Backup {
let keys = wallet.keys();
let network_dir = datadir.network_directory(network);
if let Some(settings) = WalletSettings::from_file(&network_dir, |settings| {
wallet.descriptor_checksum() == settings.descriptor_checksum
})
.map_err(|_| Error::SettingsFromFile)?
if let Some(settings) =
WalletSettings::from_file(&network_dir, |settings| wallet.id() == settings.wallet_id())
.map_err(|_| Error::SettingsFromFile)?
{
if let Ok(settings) = serde_json::to_value(settings) {
proprietary.insert(SETTINGS_KEY.to_string(), settings);

View File

@ -668,6 +668,7 @@ pub async fn extract_remote_gui_settings(ctx: &Context, backend: &BackendWalletC
wallets: vec![WalletSettings {
name: wallet_name(descriptor),
descriptor_checksum,
pinned_at: Some(chrono::Utc::now().timestamp()),
keys: Vec::new(),
hardware_wallets: Vec::new(),
remote_backend_auth: Some(AuthConfig::new(
@ -704,6 +705,7 @@ pub fn extract_local_gui_settings(ctx: &Context) -> Settings {
Settings {
wallets: vec![WalletSettings {
name: wallet_name(descriptor),
pinned_at: Some(chrono::Utc::now().timestamp()),
descriptor_checksum,
keys: ctx.keys.values().cloned().collect(),
hardware_wallets,

View File

@ -13,7 +13,7 @@ use liana_ui::{
use lianad::config::ConfigError;
use crate::{
app,
app::{self, settings::WalletSettings},
dir::{LianaDirectory, NetworkDirectory},
installer::UserFlow,
};
@ -28,11 +28,7 @@ const NETWORKS: [Network; 4] = [
#[derive(Debug, Clone)]
pub enum State {
Unchecked,
Wallet {
name: Option<String>,
email: Option<String>,
checksum: Option<String>,
},
Wallets(Vec<WalletSettings>),
NoWallet,
}
@ -133,20 +129,26 @@ impl Launcher {
Task::none()
}
},
Message::View(ViewMessage::Run) => {
if matches!(self.state, State::Wallet { .. }) {
let datadir_path = self.datadir_path.clone();
let mut path = self
.datadir_path
.network_directory(self.network)
.path()
.to_path_buf();
path.push(app::config::DEFAULT_FILE_NAME);
let cfg = app::Config::from_file(&path).expect("Already checked");
let network = self.network;
Task::perform(async move { (datadir_path.clone(), cfg, network) }, |m| {
Message::Run(m.0, m.1, m.2)
})
Message::View(ViewMessage::Run(index)) => {
if let State::Wallets(wallets) = &self.state {
if let Some(settings) = wallets.get(index) {
let datadir_path = self.datadir_path.clone();
let mut path = self
.datadir_path
.network_directory(self.network)
.path()
.to_path_buf();
path.push(app::config::DEFAULT_FILE_NAME);
let cfg = app::Config::from_file(&path).expect("Already checked");
let network = self.network;
let settings = settings.clone();
Task::perform(
async move { (datadir_path.clone(), cfg, network, settings) },
|m| Message::Run(m.0, m.1, m.2, m.3),
)
} else {
Task::none()
}
} else {
Task::none()
}
@ -191,7 +193,7 @@ impl Launcher {
Column::new()
.align_x(Alignment::Center)
.spacing(30)
.push(if matches!(self.state, State::Wallet { .. }) {
.push(if matches!(self.state, State::Wallets { .. }) {
text("Welcome back").size(50).bold()
} else {
text("Welcome").size(50).bold()
@ -199,63 +201,14 @@ impl Launcher {
.push_maybe(self.error.as_ref().map(|e| card::simple(text(e))))
.push(match &self.state {
State::Unchecked => Column::new(),
State::Wallet {
email, checksum, ..
} => Column::new().push(
Row::new()
.align_y(Alignment::Center)
.spacing(20)
.push(
Container::new(
Button::new(
Column::new()
.push(p1_bold(format!(
"My Liana {} wallet",
match self.network {
Network::Bitcoin => "Bitcoin",
Network::Signet => "Signet",
Network::Testnet => "Testnet",
Network::Regtest => "Regtest",
_ => "",
}
)))
.push_maybe(checksum.as_ref().map(
|checksum| {
p1_regular(format!(
"Liana-{}",
checksum
))
.style(theme::text::secondary)
},
))
.push_maybe(email.as_ref().map(|email| {
Row::new()
.push(Space::with_width(
Length::Fill,
))
.push(
p1_regular(email).style(
theme::text::secondary,
),
)
})),
)
.on_press(ViewMessage::Run)
.padding(15)
.style(theme::button::container_border)
.width(Length::Fill),
)
.style(theme::card::simple),
)
.push(
Button::new(icon::trash_icon())
.style(theme::button::secondary)
.padding(10)
.on_press(ViewMessage::DeleteWallet(
DeleteWalletMessage::ShowModal,
)),
),
),
State::Wallets(wallets) => {
Column::new().push(wallets.iter().enumerate().fold(
Column::new().spacing(20),
|col, (i, settings)| {
col.push(wallets_list_item(self.network, settings, i))
},
))
}
State::NoWallet => Column::new()
.push(
Row::new()
@ -338,12 +291,63 @@ impl Launcher {
}
}
fn wallets_list_item(
network: Network,
settings: &WalletSettings,
i: usize,
) -> Element<ViewMessage> {
Container::new(
Row::new()
.align_y(Alignment::Center)
.spacing(20)
.push(
Container::new(
Button::new(
Column::new()
.push(p1_bold(format!(
"My Liana {} wallet",
match network {
Network::Bitcoin => "Bitcoin",
Network::Signet => "Signet",
Network::Testnet => "Testnet",
Network::Regtest => "Regtest",
_ => "",
}
)))
.push(
p1_regular(format!("Liana-{}", settings.descriptor_checksum))
.style(theme::text::secondary),
)
.push_maybe(settings.remote_backend_auth.as_ref().map(|auth| {
Row::new()
.push(Space::with_width(Length::Fill))
.push(p1_regular(&auth.email).style(theme::text::secondary))
})),
)
.on_press(ViewMessage::Run(i))
.padding(15)
.style(theme::button::container_border)
.width(Length::Fill),
)
.style(theme::card::simple),
)
.push(
Button::new(icon::trash_icon())
.style(theme::button::secondary)
.padding(10)
.on_press(ViewMessage::DeleteWallet(DeleteWalletMessage::ShowModal)),
),
)
.into()
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
pub enum Message {
View(ViewMessage),
Install(LianaDirectory, Network, UserFlow),
Checked(Result<State, String>),
Run(LianaDirectory, app::config::Config, Network),
Run(LianaDirectory, app::config::Config, Network, WalletSettings),
}
#[derive(Debug, Clone)]
@ -354,7 +358,7 @@ pub enum ViewMessage {
SelectNetwork(Network),
StartInstall(Network),
Check,
Run,
Run(usize),
DeleteWallet(DeleteWalletMessage),
}
@ -513,16 +517,7 @@ async fn check_network_datadir(path: NetworkDirectory) -> Result<State, String>
})?;
}
if let Ok(Some(wallet)) = app::settings::WalletSettings::from_file(&path, |_w| true) {
return Ok(State::Wallet {
name: Some(wallet.name),
checksum: Some(wallet.descriptor_checksum),
email: wallet.remote_backend_auth.map(|auth| auth.email),
});
}
Ok(State::Wallet {
name: None,
checksum: None,
email: None,
})
app::settings::Settings::from_file(&path)
.map(|s| State::Wallets(s.wallets))
.map_err(|e| e.to_string())
}

View File

@ -59,7 +59,7 @@ pub struct Loader {
pub internal_bitcoind: Option<Bitcoind>,
pub waiting_daemon_bitcoind: bool,
pub backup: Option<Backup>,
pub wallet_setting: Option<WalletSettings>,
pub wallet_settings: WalletSettings,
step: Step,
}
@ -119,7 +119,7 @@ impl Loader {
network: bitcoin::Network,
internal_bitcoind: Option<Bitcoind>,
backup: Option<Backup>,
wallet_setting: Option<WalletSettings>,
wallet_settings: WalletSettings,
) -> (Self, Task<Message>) {
let path = socket_path(&datadir_path, network);
(
@ -131,7 +131,7 @@ impl Loader {
daemon_started: false,
internal_bitcoind,
waiting_daemon_bitcoind: false,
wallet_setting,
wallet_settings,
backup,
},
Task::perform(connect(path), Message::Loaded),
@ -141,11 +141,7 @@ impl Loader {
fn start_bitcoind(&self) -> bool {
if self.internal_bitcoind.is_some() {
false
} else if let Some(start) = self
.wallet_setting
.as_ref()
.and_then(|setting| setting.start_internal_bitcoind)
{
} else if let Some(start) = self.wallet_settings.start_internal_bitcoind {
start
} else {
self.gui_config.start_internal_bitcoind
@ -162,6 +158,7 @@ impl Loader {
if info.block_height > 0 {
return Task::perform(
load_application(
self.wallet_settings.clone(),
daemon,
info,
self.datadir_path.clone(),
@ -256,6 +253,7 @@ impl Loader {
if (info.sync - 1.0_f64).abs() < f64::EPSILON {
return Task::perform(
load_application(
self.wallet_settings.clone(),
daemon.clone(),
info,
self.datadir_path.clone(),
@ -309,7 +307,7 @@ impl Loader {
self.network,
self.internal_bitcoind.clone(),
self.backup.clone(),
self.wallet_setting.clone(),
self.wallet_settings.clone(),
);
*self = loader;
cmd
@ -406,6 +404,7 @@ fn get_bitcoind_log(log_path: PathBuf) -> impl Stream<Item = Option<String>> {
}
pub async fn load_application(
wallet_settings: WalletSettings,
daemon: Arc<dyn Daemon + Sync + Send>,
info: GetInfoResult,
datadir_path: LianaDirectory,
@ -422,9 +421,8 @@ pub async fn load_application(
),
Error,
> {
let network_dir = datadir_path.network_directory(network);
let wallet = Wallet::new(info.descriptors.main)
.load_from_settings(&network_dir)?
.load_from_settings(wallet_settings)?
.load_hotsigners(&datadir_path, network)?;
let coins = coins_to_cache(daemon.clone()).await.map(|res| res.coins)?;

View File

@ -200,34 +200,21 @@ impl GUI {
self.state = State::Installer(Box::new(install));
command.map(|msg| Message::Install(Box::new(msg)))
}
launcher::Message::Run(datadir_path, cfg, network) => {
launcher::Message::Run(datadir_path, cfg, network, settings) => {
self.logger.set_running_mode(
datadir_path.clone(),
network,
self.log_level
.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)),
);
let network_dir = datadir_path.network_directory(network);
if let Ok(settings) =
app::settings::WalletSettings::from_file(&network_dir, |_w| true)
{
if let Some(setting) = settings
.as_ref()
.and_then(|w| w.remote_backend_auth.clone())
{
let (login, command) =
login::LianaLiteLogin::new(datadir_path, network, setting);
self.state = State::Login(Box::new(login));
command.map(|msg| Message::Login(Box::new(msg)))
} else {
let (loader, command) =
Loader::new(datadir_path, cfg, network, None, None, settings);
self.state = State::Loader(Box::new(loader));
command.map(|msg| Message::Load(Box::new(msg)))
}
if let Some(setting) = settings.remote_backend_auth {
let (login, command) =
login::LianaLiteLogin::new(datadir_path, network, setting);
self.state = State::Login(Box::new(login));
command.map(|msg| Message::Login(Box::new(msg)))
} else {
let (loader, command) =
Loader::new(datadir_path, cfg, network, None, None, None);
Loader::new(datadir_path, cfg, network, None, None, settings);
self.state = State::Loader(Box::new(loader));
command.map(|msg| Message::Load(Box::new(msg)))
}
@ -281,12 +268,11 @@ impl GUI {
(State::Installer(i), Message::Install(msg)) => {
if let installer::Message::Exit(path, internal_bitcoind, remove_log) = *msg {
let network_dir = i.datadir.network_directory(i.network);
// We get the first one created.
let settings = app::settings::WalletSettings::from_file(&network_dir, |_| true)
.expect("A settings file was created");
if let Some(setting) = settings
.as_ref()
.and_then(|w| w.remote_backend_auth.clone())
{
.expect("A settings file was created")
.expect("A wallet was created");
if let Some(setting) = settings.remote_backend_auth {
let (login, command) =
login::LianaLiteLogin::new(i.datadir.clone(), i.network, setting);
self.state = State::Login(Box::new(login));