From 62f045b2bba37ddb0fcc71f083e02858980d2e7c Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Fri, 6 Feb 2026 02:28:18 -0500 Subject: [PATCH 1/2] business-settings: display human readable policy in settings --- .../business-settings/src/views/mod.rs | 12 +++ liana-gui/src/app/view/settings/mod.rs | 82 ++++++++++--------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/liana-business/business-settings/src/views/mod.rs b/liana-business/business-settings/src/views/mod.rs index 8643dae77..55e98227b 100644 --- a/liana-business/business-settings/src/views/mod.rs +++ b/liana-business/business-settings/src/views/mod.rs @@ -2,6 +2,7 @@ use iced::widget::{scrollable, Column, Container, Row, Space}; use iced::{Alignment, Length}; +use liana_gui::app::view::settings::display_policy; use liana_ui::{ component::{badge, button, card, separation, text::*}, icon, theme, @@ -40,6 +41,16 @@ pub fn list_view() -> Element<'static, Msg> { pub fn wallet_view(state: &BusinessSettingsUI) -> Element<'_, Msg> { let header = section_header("Wallet"); + // Policy display + let policy = state.wallet.main_descriptor.policy(); + let policy_card = card::simple(display_policy( + policy, + state.wallet.keys_aliases.clone(), + &state.wallet.provider_keys, + )) + .width(Length::Fill); + + // Descriptor display let descriptor = state.wallet.main_descriptor.to_string(); let descriptor_card = card::simple( Column::new() @@ -71,6 +82,7 @@ pub fn wallet_view(state: &BusinessSettingsUI) -> Element<'_, Msg> { .spacing(20) .push(header) .push(descriptor_card) + .push(policy_card) .width(Length::Fill) .into() } diff --git a/liana-gui/src/app/view/settings/mod.rs b/liana-gui/src/app/view/settings/mod.rs index 9a65f2541..85329d739 100644 --- a/liana-gui/src/app/view/settings/mod.rs +++ b/liana-gui/src/app/view/settings/mod.rs @@ -1128,6 +1128,13 @@ pub fn wallet_settings<'a>( ) .width(Length::Fill); + // Convert form values to HashMap for display_policy + let aliases_map: HashMap = keys_aliases + .iter() + .filter(|(_, v)| !v.value.is_empty()) + .map(|(fg, v)| (*fg, v.value.clone())) + .collect(); + dashboard( &Menu::Settings, cache, @@ -1139,7 +1146,7 @@ pub fn wallet_settings<'a>( .push( card::simple(display_policy( descriptor.policy(), - keys_aliases, + aliases_map, provider_keys, )) .width(Length::Fill), @@ -1148,15 +1155,18 @@ pub fn wallet_settings<'a>( ) } -fn display_policy<'a>( +/// Display wallet policy in human-readable format. +/// +/// Shows primary path and recovery paths with thresholds, key names, and timelocks. +pub fn display_policy( policy: LianaPolicy, - keys_aliases: &'a [(Fingerprint, form::Value)], - provider_keys: &'a HashMap, -) -> Element<'a, Message> { + keys_aliases: HashMap, + provider_keys: &HashMap, +) -> Element<'_, M> { let (primary_threshold, primary_keys) = policy.primary_path().thresh_origins(); let recovery_paths = policy.recovery_paths(); - // The iteration over an HashMap keys can have a different order at each refresh + // The iteration over a HashMap keys can have a different order at each refresh let mut primary_keys: Vec = primary_keys.into_keys().collect(); primary_keys.sort(); @@ -1181,22 +1191,19 @@ fn display_policy<'a>( .iter() .enumerate() .fold(Row::new().spacing(5), |row, (i, k)| { - let content = if let Some(alias) = keys_aliases - .iter() - .find(|(fg, a)| fg == k && !a.value.is_empty()) - .map(|(_, f)| &f.value) - { - Container::new( - iced_tooltip::Tooltip::new( - text(alias).bold(), - text(k.to_string()), - iced_tooltip::Position::Bottom, + let content = + if let Some(alias) = keys_aliases.get(k).filter(|a| !a.is_empty()) { + Container::new( + iced_tooltip::Tooltip::new( + text(alias).bold(), + text(k.to_string()), + iced_tooltip::Position::Bottom, + ) + .style(theme::card::simple), ) - .style(theme::card::simple), - ) - } else { - Container::new(text(format!("[{}]", k)).bold()) - }; + } else { + Container::new(text(format!("[{}]", k)).bold()) + }; if primary_keys.len() == 1 || i == primary_keys.len() - 1 { row.push(content) } else if i <= primary_keys.len() - 2 { @@ -1211,7 +1218,7 @@ fn display_policy<'a>( for (i, (sequence, recovery_path)) in recovery_paths.iter().enumerate() { let (threshold, recovery_keys) = recovery_path.thresh_origins(); - // The iteration over an HashMap keys can have a different order at each refresh + // Sort for consistent ordering let mut recovery_keys: Vec = recovery_keys.into_keys().collect(); recovery_keys.sort(); @@ -1234,22 +1241,19 @@ fn display_policy<'a>( .push(recovery_keys.iter().enumerate().fold( Row::new().spacing(5), |row, (i, k)| { - let content = if let Some(alias) = keys_aliases - .iter() - .find(|(fg, a)| fg == k && !a.value.is_empty()) - .map(|(_, f)| &f.value) - { - Container::new( - iced_tooltip::Tooltip::new( - text(alias).bold(), - text(k.to_string()), - iced_tooltip::Position::Bottom, + let content = + if let Some(alias) = keys_aliases.get(k).filter(|a| !a.is_empty()) { + Container::new( + iced_tooltip::Tooltip::new( + text(alias).bold(), + text(k.to_string()), + iced_tooltip::Position::Bottom, + ) + .style(theme::card::simple), ) - .style(theme::card::simple), - ) - } else { - Container::new(text(format!("[{}]", k)).bold()) - }; + } else { + Container::new(text(format!("[{}]", k)).bold()) + }; if recovery_keys.len() == 1 || i == recovery_keys.len() - 1 { row.push(content) } else if i <= recovery_keys.len() - 2 { @@ -1291,8 +1295,8 @@ fn display_policy<'a>( .into() } -/// returns y,m,d -fn expire_message_units(sequence: u32) -> Vec { +/// Converts a block sequence to human-readable time units (y, m, d, h, m). +pub fn expire_message_units(sequence: u32) -> Vec { let mut n_minutes = sequence * 10; let n_years = n_minutes / 525960; n_minutes -= n_years * 525960; From 84b1dc83c05cd6eaaf16ff70eb82923a61877022 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Fri, 6 Feb 2026 04:51:20 -0500 Subject: [PATCH 2/2] business-settings: let user backup descriptor --- .../business-settings/src/message.rs | 23 +++++ liana-business/business-settings/src/ui.rs | 89 +++++++++++++++++-- .../business-settings/src/views/mod.rs | 8 ++ 3 files changed, 114 insertions(+), 6 deletions(-) diff --git a/liana-business/business-settings/src/message.rs b/liana-business/business-settings/src/message.rs index c5ecffa7c..313fb3a50 100644 --- a/liana-business/business-settings/src/message.rs +++ b/liana-business/business-settings/src/message.rs @@ -1,5 +1,7 @@ //! Message types for business settings UI. +use liana_gui::{app::view::Close, export::ImportExportMessage}; + /// Settings section for navigation. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Section { @@ -19,4 +21,25 @@ pub enum Msg { /// Register wallet on selected device. RegisterWallet, + + /// Copy descriptor to clipboard. + CopyDescriptor, + + /// Export encrypted descriptor. + ExportEncryptedDescriptor, + + /// Handle export progress/result. + Export(ImportExportMessage), +} + +impl From for Msg { + fn from(msg: ImportExportMessage) -> Self { + Msg::Export(msg) + } +} + +impl Close for Msg { + fn close() -> Self { + Msg::Export(ImportExportMessage::Close) + } } diff --git a/liana-business/business-settings/src/ui.rs b/liana-business/business-settings/src/ui.rs index 644340d4a..7e9559094 100644 --- a/liana-business/business-settings/src/ui.rs +++ b/liana-business/business-settings/src/ui.rs @@ -2,20 +2,21 @@ use std::sync::Arc; -use iced::{Subscription, Task}; +use iced::{clipboard, Subscription, Task}; use liana_gui::{ app::{ cache::Cache, menu::Menu, message::Message, settings::SettingsUI, - state::{settings::wallet::RegisterWalletModal, State}, + state::{export::ExportModal, settings::wallet::RegisterWalletModal, State}, view, wallet::Wallet, Config, }, daemon::{Daemon, DaemonBackend}, dir::LianaDirectory, + export::{ImportExportMessage, ImportExportType}, }; use liana_ui::widget::{modal, Element}; @@ -30,6 +31,7 @@ pub struct BusinessSettingsUI { #[allow(dead_code)] processing: bool, register_modal: Option, + export_modal: Option, } impl SettingsUI for BusinessSettingsUI { @@ -47,6 +49,7 @@ impl SettingsUI for BusinessSettingsUI { current_section: None, processing: false, register_modal: None, + export_modal: None, }; // TODO: Fetch fiat setting from backend on load (ui, Task::none()) @@ -65,24 +68,65 @@ impl SettingsUI for BusinessSettingsUI { } Msg::SelectSection(section) => self.on_select_section(section), Msg::RegisterWallet => Task::none(), // Handled in State::update() + Msg::CopyDescriptor => { + let descriptor = self.wallet.main_descriptor.to_string(); + clipboard::write(descriptor) + } + Msg::ExportEncryptedDescriptor => { + self.export_modal = Some(ExportModal::new( + None, + ImportExportType::ExportEncryptedDescriptor(Box::new( + self.wallet.main_descriptor.clone(), + )), + )); + self.export_modal + .as_ref() + .map(|m| m.launch(true)) + .unwrap_or(Task::none()) + } + Msg::Export(msg) => { + if matches!(msg, ImportExportMessage::Close) { + self.export_modal = None; + return Task::none(); + } + if let Some(modal) = &mut self.export_modal { + modal.update(msg) + } else { + Task::none() + } + } } } fn view<'a>(&'a self, _cache: &'a Cache) -> Element<'a, Msg> { - match self.current_section { + let content = match self.current_section { None => views::list_view(), Some(Section::Wallet) => views::wallet_view(self), Some(Section::About) => views::about_view(), + }; + + if let Some(export_modal) = &self.export_modal { + export_modal.view(content) + } else { + content } } fn subscription(&self) -> Subscription { - Subscription::none() + if let Some(modal) = &self.export_modal { + modal + .subscription() + .map(|s| s.map(|m| Msg::Export(ImportExportMessage::Progress(m)))) + .unwrap_or(Subscription::none()) + } else { + Subscription::none() + } } fn stop(&mut self) { self.current_section = None; self.register_modal = None; + self.export_modal = None; } fn reload(&mut self, _daemon: Arc, wallet: Arc) -> Task { @@ -112,6 +156,13 @@ impl State for BusinessSettingsUI { view::Message::Settings(view::SettingsMessage::AboutSection) } Msg::RegisterWallet => view::Message::Settings(view::SettingsMessage::RegisterWallet), + Msg::CopyDescriptor => { + view::Message::Clipboard(self.wallet.main_descriptor.to_string()) + } + Msg::ExportEncryptedDescriptor => { + view::Message::Settings(view::SettingsMessage::ExportEncryptedDescriptor) + } + Msg::Export(msg) => view::Message::ImportExport(msg), }); let dashboard = view::dashboard(&Menu::Settings, cache, None, content); @@ -162,6 +213,27 @@ impl State for BusinessSettingsUI { Task::none() } } + Message::View(view::Message::Settings( + view::SettingsMessage::ExportEncryptedDescriptor, + )) => { + SettingsUI::update(self, daemon, cache, Msg::ExportEncryptedDescriptor).map(|m| { + if let Msg::Export(export_msg) = m { + Message::View(view::Message::ImportExport(export_msg)) + } else { + Message::View(view::Message::Close) + } + }) + } + Message::View(view::Message::ImportExport(msg)) => { + SettingsUI::update(self, daemon, cache, Msg::Export(msg)).map(|m| { + if let Msg::Export(export_msg) = m { + Message::View(view::Message::ImportExport(export_msg)) + } else { + Message::View(view::Message::Close) + } + }) + } + Message::View(view::Message::Clipboard(text)) => clipboard::write(text), Message::View(view::Message::Settings(ref settings_msg)) => { let msg = match settings_msg { view::SettingsMessage::EditWalletSettings => { @@ -180,11 +252,16 @@ impl State for BusinessSettingsUI { } fn subscription(&self) -> Subscription { - if let Some(modal) = &self.register_modal { + let register_sub = if let Some(modal) = &self.register_modal { modal.subscription() } else { Subscription::none() - } + }; + let export_sub = SettingsUI::subscription(self).map(|msg| match msg { + Msg::Export(m) => Message::View(view::Message::ImportExport(m)), + _ => Message::View(view::Message::Close), + }); + Subscription::batch([register_sub, export_sub]) } fn reload( diff --git a/liana-business/business-settings/src/views/mod.rs b/liana-business/business-settings/src/views/mod.rs index 55e98227b..3a6056b34 100644 --- a/liana-business/business-settings/src/views/mod.rs +++ b/liana-business/business-settings/src/views/mod.rs @@ -69,6 +69,14 @@ pub fn wallet_view(state: &BusinessSettingsUI) -> Element<'_, Msg> { Row::new() .spacing(10) .push(Space::with_width(Length::Fill)) + .push( + button::secondary(Some(icon::backup_icon()), "Encrypted descriptor") + .on_press(Msg::ExportEncryptedDescriptor), + ) + .push( + button::secondary(Some(icon::clipboard_icon()), "Copy") + .on_press(Msg::CopyDescriptor), + ) .push( button::secondary(Some(icon::chip_icon()), "Register on device") .on_press(Msg::RegisterWallet),