From 774cd3c70ec920acb65833813487de2975d0d6aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 26 Feb 2026 21:55:17 -0500 Subject: [PATCH 1/3] Add xpub announce UI and peer xpubs display to desktop --- keep-desktop/src/app.rs | 119 +++++++++++++++++++++- keep-desktop/src/frost.rs | 8 ++ keep-desktop/src/message.rs | 23 +++++ keep-desktop/src/screen/wallet.rs | 160 ++++++++++++++++++++++++++++++ 4 files changed, 308 insertions(+), 2 deletions(-) diff --git a/keep-desktop/src/app.rs b/keep-desktop/src/app.rs index 7475c775..28e2834b 100644 --- a/keep-desktop/src/app.rs +++ b/keep-desktop/src/app.rs @@ -39,7 +39,8 @@ use crate::screen::shares::{ShareEntry, ShareListScreen}; use crate::screen::signing_audit::{AuditDisplayEntry, ChainStatus, SigningAuditScreen}; use crate::screen::unlock::UnlockScreen; use crate::screen::wallet::{ - DescriptorProgress, SetupPhase, SetupState, TierConfig, WalletEntry, WalletScreen, + AnnounceState, DescriptorProgress, SetupPhase, SetupState, TierConfig, WalletEntry, + WalletScreen, }; use crate::screen::Screen; use crate::theme; @@ -709,7 +710,14 @@ impl App { | Message::WalletBeginCoordination | Message::WalletCancelSetup | Message::WalletSessionStarted(..) - | Message::WalletDescriptorProgress(..) => self.handle_wallet_message(message), + | Message::WalletDescriptorProgress(..) + | Message::WalletStartAnnounce + | Message::WalletAnnounceXpubChanged(..) + | Message::WalletAnnounceFingerprintChanged(..) + | Message::WalletAnnounceLabelChanged(..) + | Message::WalletCancelAnnounce + | Message::WalletSubmitAnnounce + | Message::WalletAnnounceResult(..) => self.handle_wallet_message(message), Message::RelayUrlChanged(..) | Message::ConnectPasswordChanged(..) @@ -1420,6 +1428,113 @@ impl App { } Task::none() } + Message::WalletStartAnnounce => { + if let Screen::Wallet(s) = &mut self.screen { + s.announce = Some(AnnounceState { + xpub: String::new(), + fingerprint: String::new(), + label: String::new(), + error: None, + submitting: false, + }); + } + Task::none() + } + Message::WalletAnnounceXpubChanged(v) => { + if let Screen::Wallet(WalletScreen { + announce: Some(a), .. + }) = &mut self.screen + { + a.xpub = v; + } + Task::none() + } + Message::WalletAnnounceFingerprintChanged(v) => { + if let Screen::Wallet(WalletScreen { + announce: Some(a), .. + }) = &mut self.screen + { + let filtered: String = v + .chars() + .filter(|c| c.is_ascii_hexdigit()) + .take(8) + .collect(); + a.fingerprint = filtered; + } + Task::none() + } + Message::WalletAnnounceLabelChanged(v) => { + if let Screen::Wallet(WalletScreen { + announce: Some(a), .. + }) = &mut self.screen + { + a.label = v; + } + Task::none() + } + Message::WalletCancelAnnounce => { + if let Screen::Wallet(s) = &mut self.screen { + s.announce = None; + } + Task::none() + } + Message::WalletSubmitAnnounce => { + let (xpub, fingerprint, label) = if let Screen::Wallet(WalletScreen { + announce: Some(a), + .. + }) = &mut self.screen + { + a.submitting = true; + a.error = None; + ( + a.xpub.trim().to_string(), + a.fingerprint.trim().to_string(), + a.label.trim().to_string(), + ) + } else { + return Task::none(); + }; + + let Some(node) = self.get_frost_node() else { + if let Screen::Wallet(WalletScreen { + announce: Some(a), .. + }) = &mut self.screen + { + a.error = Some("Relay not connected".into()); + a.submitting = false; + } + return Task::none(); + }; + + let announced = keep_frost_net::AnnouncedXpub { + xpub, + fingerprint, + label: if label.is_empty() { None } else { Some(label) }, + }; + + Task::perform( + async move { + node.announce_xpubs(vec![announced]) + .await + .map_err(|e| format!("{e}")) + }, + Message::WalletAnnounceResult, + ) + } + Message::WalletAnnounceResult(result) => { + if let Screen::Wallet(s) = &mut self.screen { + match result { + Ok(()) => s.announce = None, + Err(e) => { + if let Some(a) = &mut s.announce { + a.error = Some(e); + a.submitting = false; + } + } + } + } + Task::none() + } _ => Task::none(), } } diff --git a/keep-desktop/src/frost.rs b/keep-desktop/src/frost.rs index 855283ab..287d2069 100644 --- a/keep-desktop/src/frost.rs +++ b/keep-desktop/src/frost.rs @@ -819,6 +819,14 @@ impl App { count = recovery_xpubs.len(), "Received recovery xpub announcement" ); + if let Screen::Wallet(ws) = &mut self.screen { + let entry = ws.peer_xpubs.entry(share_index).or_default(); + for xpub in recovery_xpubs { + if !entry.iter().any(|x| x.xpub == xpub.xpub) { + entry.push(xpub); + } + } + } } FrostNodeMsg::HealthCheckComplete { responsive, diff --git a/keep-desktop/src/message.rs b/keep-desktop/src/message.rs index 478af9b1..dc48032b 100644 --- a/keep-desktop/src/message.rs +++ b/keep-desktop/src/message.rs @@ -202,6 +202,13 @@ pub enum Message { WalletCancelSetup, WalletSessionStarted(Result<([u8; 32], [u8; 32], String, usize), String>), WalletDescriptorProgress(DescriptorProgress, Option<[u8; 32]>), + WalletStartAnnounce, + WalletAnnounceXpubChanged(String), + WalletAnnounceFingerprintChanged(String), + WalletAnnounceLabelChanged(String), + WalletCancelAnnounce, + WalletSubmitAnnounce, + WalletAnnounceResult(Result<(), String>), // Relay / FROST RelayUrlChanged(String), ConnectPasswordChanged(Zeroizing), @@ -527,6 +534,22 @@ impl fmt::Debug for Message { .field(&r.as_ref().map(|_| "ok").map_err(|e| e.as_str())) .finish(), Self::WalletDescriptorProgress(..) => f.write_str("WalletDescriptorProgress"), + Self::WalletStartAnnounce => f.write_str("WalletStartAnnounce"), + Self::WalletAnnounceXpubChanged(_) => f.write_str("WalletAnnounceXpubChanged(***)"), + Self::WalletAnnounceFingerprintChanged(fp) => f + .debug_tuple("WalletAnnounceFingerprintChanged") + .field(fp) + .finish(), + Self::WalletAnnounceLabelChanged(l) => f + .debug_tuple("WalletAnnounceLabelChanged") + .field(l) + .finish(), + Self::WalletCancelAnnounce => f.write_str("WalletCancelAnnounce"), + Self::WalletSubmitAnnounce => f.write_str("WalletSubmitAnnounce"), + Self::WalletAnnounceResult(r) => f + .debug_tuple("WalletAnnounceResult") + .field(&r.as_ref().map(|_| "ok").map_err(|e| e.as_str())) + .finish(), Self::RelayUrlChanged(u) => f.debug_tuple("RelayUrlChanged").field(u).finish(), Self::ConnectPasswordChanged(_) => f.write_str("ConnectPasswordChanged(***)"), Self::AddRelay => f.write_str("AddRelay"), diff --git a/keep-desktop/src/screen/wallet.rs b/keep-desktop/src/screen/wallet.rs index ab82fb9c..693969a6 100644 --- a/keep-desktop/src/screen/wallet.rs +++ b/keep-desktop/src/screen/wallet.rs @@ -1,9 +1,12 @@ // SPDX-FileCopyrightText: © 2026 PrivKey LLC // SPDX-License-Identifier: AGPL-3.0-or-later +use std::collections::HashMap; + use chrono::{DateTime, Utc}; use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; use iced::{Alignment, Element, Length}; +use keep_frost_net::AnnouncedXpub; use crate::message::Message; use crate::screen::shares::ShareEntry; @@ -69,10 +72,20 @@ pub enum SetupPhase { Coordinating(DescriptorProgress), } +pub struct AnnounceState { + pub xpub: String, + pub fingerprint: String, + pub label: String, + pub error: Option, + pub submitting: bool, +} + pub struct WalletScreen { pub descriptors: Vec, pub expanded: Option, pub setup: Option, + pub announce: Option, + pub peer_xpubs: HashMap>, } pub struct SetupState { @@ -91,10 +104,16 @@ impl WalletScreen { descriptors, expanded: None, setup: None, + announce: None, + peer_xpubs: HashMap::new(), } } pub fn view_content(&self) -> Element<'_, Message> { + if let Some(announce) = &self.announce { + return self.view_announce(announce); + } + if let Some(setup) = &self.setup { return self.view_setup(setup); } @@ -104,11 +123,18 @@ impl WalletScreen { .padding([theme::space::SM, theme::space::LG]) .on_press(Message::WalletStartSetup); + let announce_btn = button(text("Announce Recovery Keys").size(theme::size::BODY)) + .style(theme::secondary_button) + .padding([theme::space::SM, theme::space::LG]) + .on_press(Message::WalletStartAnnounce); + let title_row = row![ theme::heading("Wallet Descriptors"), Space::new().width(Length::Fill), + announce_btn, setup_btn, ] + .spacing(theme::space::SM) .align_y(Alignment::Center); let mut content = column![title_row].spacing(theme::space::MD); @@ -138,6 +164,10 @@ impl WalletScreen { content = content.push(scrollable(list).height(Length::Fill)); } + if !self.peer_xpubs.is_empty() { + content = content.push(self.peer_xpubs_card()); + } + container(content) .padding(theme::space::XL) .width(Length::Fill) @@ -477,4 +507,134 @@ impl WalletScreen { .width(Length::Fill) .into() } + + fn view_announce<'a>(&self, state: &'a AnnounceState) -> Element<'a, Message> { + let back_btn = button(text("< Back").size(theme::size::BODY)) + .on_press(Message::WalletCancelAnnounce) + .style(theme::text_button) + .padding([theme::space::XS, theme::space::SM]); + + let title_text = text("Announce Recovery Key") + .size(theme::size::HEADING) + .color(theme::color::TEXT); + + let header = row![back_btn, Space::new().width(theme::space::SM), title_text] + .align_y(Alignment::Center); + + let subtitle = text("Share a recovery xpub with your FROST group peers") + .size(theme::size::SMALL) + .color(theme::color::TEXT_MUTED); + + let xpub_input = text_input("tpub...", &state.xpub) + .on_input(Message::WalletAnnounceXpubChanged) + .padding(theme::space::SM); + + let fp_input = text_input("8 hex chars", &state.fingerprint) + .on_input(Message::WalletAnnounceFingerprintChanged) + .padding(theme::space::SM) + .width(120); + + let label_input = text_input("e.g. coldcard-backup", &state.label) + .on_input(Message::WalletAnnounceLabelChanged) + .padding(theme::space::SM); + + let valid_prefixes = [ + "xpub", "tpub", "ypub", "zpub", "upub", "vpub", "Ypub", "Zpub", "Upub", "Vpub", + ]; + let xpub_valid = valid_prefixes.iter().any(|p| state.xpub.starts_with(p)); + let fp_valid = state.fingerprint.len() == 8 + && state.fingerprint.chars().all(|c| c.is_ascii_hexdigit()); + let can_submit = xpub_valid && fp_valid && !state.submitting; + + let mut submit_btn = button(text("Announce").size(theme::size::BODY)) + .style(theme::primary_button) + .padding([theme::space::SM, theme::space::LG]); + if can_submit { + submit_btn = submit_btn.on_press(Message::WalletSubmitAnnounce); + } + + let mut content = column![ + header, + subtitle, + Space::new().height(theme::space::SM), + theme::label("Extended Public Key"), + xpub_input, + Space::new().height(theme::space::XS), + theme::label("Fingerprint"), + fp_input, + Space::new().height(theme::space::XS), + theme::label("Label (optional)"), + label_input, + Space::new().height(theme::space::MD), + submit_btn, + ] + .spacing(theme::space::XS); + + if let Some(err) = &state.error { + content = content.push(theme::error_text(err.as_str())); + } + + container(scrollable(content)) + .padding(theme::space::XL) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn peer_xpubs_card(&self) -> Element<'_, Message> { + let title = text("Announced Recovery Keys") + .size(theme::size::HEADING) + .color(theme::color::TEXT); + + let mut content = column![title].spacing(theme::space::SM); + + let mut indices: Vec<_> = self.peer_xpubs.keys().copied().collect(); + indices.sort(); + + for idx in indices { + if let Some(xpubs) = self.peer_xpubs.get(&idx) { + let share_label = text(format!("Share {idx}")) + .size(theme::size::BODY) + .color(theme::color::TEXT); + + let mut share_col = column![share_label].spacing(theme::space::XS); + + for xpub in xpubs { + let truncated = if xpub.xpub.len() > 32 { + format!("{}...", &xpub.xpub[..32]) + } else { + xpub.xpub.clone() + }; + + let mut xpub_row = row![ + text(truncated) + .size(theme::size::TINY) + .color(theme::color::TEXT_DIM), + text(&xpub.fingerprint) + .size(theme::size::TINY) + .color(theme::color::TEXT_MUTED), + ] + .spacing(theme::space::SM); + + if let Some(label) = &xpub.label { + xpub_row = xpub_row.push( + text(label) + .size(theme::size::TINY) + .color(theme::color::TEXT_MUTED), + ); + } + + share_col = share_col.push(xpub_row); + } + + content = content.push(share_col); + } + } + + container(content) + .style(theme::card_style) + .padding(theme::space::LG) + .width(Length::Fill) + .into() + } } From a2eef973e4ff47e0ca472f26dc84ba0ad2229e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 26 Feb 2026 22:07:17 -0500 Subject: [PATCH 2/3] fix: xpub announce security and UX issues --- keep-desktop/src/app.rs | 12 +++++++++--- keep-desktop/src/frost.rs | 16 ++++++++++------ keep-desktop/src/screen/wallet.rs | 30 +++++++++++++++--------------- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/keep-desktop/src/app.rs b/keep-desktop/src/app.rs index 28e2834b..58abd257 100644 --- a/keep-desktop/src/app.rs +++ b/keep-desktop/src/app.rs @@ -45,6 +45,7 @@ use crate::screen::wallet::{ use crate::screen::Screen; use crate::theme; use crate::tray::{TrayEvent, TrayState}; +use keep_frost_net::protocol::{MAX_XPUB_LABEL_LENGTH, MAX_XPUB_LENGTH}; static PENDING_NOSTRCONNECT: OnceLock>> = OnceLock::new(); @@ -158,6 +159,7 @@ pub struct App { pub(crate) pin_mismatch_confirm: bool, pub(crate) bunker_cert_pin_failed: bool, pub(crate) active_coordinations: HashMap<[u8; 32], ActiveCoordination>, + pub(crate) peer_xpubs: HashMap>, import_return_to_nsec: bool, cached_share_count: usize, cached_nsec_count: usize, @@ -568,6 +570,7 @@ impl App { tray_last_bunker: false, scanner_rx: None, active_coordinations: HashMap::new(), + peer_xpubs: HashMap::new(), import_return_to_nsec: false, cached_share_count: 0, cached_nsec_count: 0, @@ -959,7 +962,9 @@ impl App { Message::WalletsLoaded(result) => { match result { Ok(entries) => { - self.screen = Screen::Wallet(WalletScreen::new(entries)); + let mut ws = WalletScreen::new(entries); + ws.peer_xpubs = self.peer_xpubs.clone(); + self.screen = Screen::Wallet(ws); } Err(e) => { self.set_toast(e, ToastKind::Error); @@ -1445,7 +1450,7 @@ impl App { announce: Some(a), .. }) = &mut self.screen { - a.xpub = v; + a.xpub = v.chars().take(MAX_XPUB_LENGTH).collect(); } Task::none() } @@ -1468,7 +1473,7 @@ impl App { announce: Some(a), .. }) = &mut self.screen { - a.label = v; + a.label = v.chars().take(MAX_XPUB_LABEL_LENGTH).collect(); } Task::none() } @@ -3487,6 +3492,7 @@ impl App { tray_last_bunker: false, scanner_rx: None, active_coordinations: HashMap::new(), + peer_xpubs: HashMap::new(), import_return_to_nsec: false, cached_share_count: 0, cached_nsec_count: 0, diff --git a/keep-desktop/src/frost.rs b/keep-desktop/src/frost.rs index 287d2069..b5fc08e8 100644 --- a/keep-desktop/src/frost.rs +++ b/keep-desktop/src/frost.rs @@ -819,14 +819,18 @@ impl App { count = recovery_xpubs.len(), "Received recovery xpub announcement" ); - if let Screen::Wallet(ws) = &mut self.screen { - let entry = ws.peer_xpubs.entry(share_index).or_default(); - for xpub in recovery_xpubs { - if !entry.iter().any(|x| x.xpub == xpub.xpub) { - entry.push(xpub); - } + let entry = self.peer_xpubs.entry(share_index).or_default(); + for xpub in recovery_xpubs { + if entry.len() >= keep_frost_net::protocol::MAX_RECOVERY_XPUBS { + break; + } + if !entry.iter().any(|x| x.xpub == xpub.xpub) { + entry.push(xpub); } } + if let Screen::Wallet(ws) = &mut self.screen { + ws.peer_xpubs = self.peer_xpubs.clone(); + } } FrostNodeMsg::HealthCheckComplete { responsive, diff --git a/keep-desktop/src/screen/wallet.rs b/keep-desktop/src/screen/wallet.rs index 693969a6..25edb427 100644 --- a/keep-desktop/src/screen/wallet.rs +++ b/keep-desktop/src/screen/wallet.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use chrono::{DateTime, Utc}; use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; use iced::{Alignment, Element, Length}; +use keep_frost_net::protocol::VALID_XPUB_PREFIXES; use keep_frost_net::AnnouncedXpub; use crate::message::Message; @@ -140,7 +141,7 @@ impl WalletScreen { let mut content = column![title_row].spacing(theme::space::MD); if self.descriptors.is_empty() { - let empty = column![ + let mut inner = column![ text("No wallet descriptors yet") .size(theme::size::BODY) .color(theme::color::TEXT_MUTED), @@ -151,23 +152,22 @@ impl WalletScreen { .align_x(Alignment::Center) .spacing(theme::space::SM); - content = content.push( - container(empty) - .center_x(Length::Fill) - .center_y(Length::Fill), - ); + if !self.peer_xpubs.is_empty() { + inner = inner.push(self.peer_xpubs_card()); + } + + content = content.push(scrollable(inner).height(Length::Fill)); } else { let mut list = column![].spacing(theme::space::SM); for (i, entry) in self.descriptors.iter().enumerate() { list = list.push(self.wallet_card(i, entry)); } + if !self.peer_xpubs.is_empty() { + list = list.push(self.peer_xpubs_card()); + } content = content.push(scrollable(list).height(Length::Fill)); } - if !self.peer_xpubs.is_empty() { - content = content.push(self.peer_xpubs_card()); - } - container(content) .padding(theme::space::XL) .width(Length::Fill) @@ -538,10 +538,9 @@ impl WalletScreen { .on_input(Message::WalletAnnounceLabelChanged) .padding(theme::space::SM); - let valid_prefixes = [ - "xpub", "tpub", "ypub", "zpub", "upub", "vpub", "Ypub", "Zpub", "Upub", "Vpub", - ]; - let xpub_valid = valid_prefixes.iter().any(|p| state.xpub.starts_with(p)); + let xpub_valid = VALID_XPUB_PREFIXES + .iter() + .any(|p| state.xpub.starts_with(p)); let fp_valid = state.fingerprint.len() == 8 && state.fingerprint.chars().all(|c| c.is_ascii_hexdigit()); let can_submit = xpub_valid && fp_valid && !state.submitting; @@ -601,7 +600,8 @@ impl WalletScreen { for xpub in xpubs { let truncated = if xpub.xpub.len() > 32 { - format!("{}...", &xpub.xpub[..32]) + let prefix: String = xpub.xpub.chars().take(32).collect(); + format!("{prefix}...") } else { xpub.xpub.clone() }; From 3c129b944f3f800695c5050cd46cf866dce751db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 26 Feb 2026 22:15:28 -0500 Subject: [PATCH 3/3] Harden xpub announce: upsert, redact debug, input limits, layout fixes --- keep-desktop/src/app.rs | 63 ++++++----------- keep-desktop/src/frost.rs | 18 +++-- keep-desktop/src/message.rs | 14 ++-- keep-desktop/src/screen/wallet.rs | 112 +++++++++++++++--------------- 4 files changed, 100 insertions(+), 107 deletions(-) diff --git a/keep-desktop/src/app.rs b/keep-desktop/src/app.rs index 58abd257..586dd8c2 100644 --- a/keep-desktop/src/app.rs +++ b/keep-desktop/src/app.rs @@ -45,7 +45,7 @@ use crate::screen::wallet::{ use crate::screen::Screen; use crate::theme; use crate::tray::{TrayEvent, TrayState}; -use keep_frost_net::protocol::{MAX_XPUB_LABEL_LENGTH, MAX_XPUB_LENGTH}; +use keep_frost_net::{MAX_XPUB_LABEL_LENGTH, MAX_XPUB_LENGTH}; static PENDING_NOSTRCONNECT: OnceLock>> = OnceLock::new(); @@ -1446,33 +1446,23 @@ impl App { Task::none() } Message::WalletAnnounceXpubChanged(v) => { - if let Screen::Wallet(WalletScreen { - announce: Some(a), .. - }) = &mut self.screen - { + if let Some(a) = self.announce_state_mut() { a.xpub = v.chars().take(MAX_XPUB_LENGTH).collect(); } Task::none() } Message::WalletAnnounceFingerprintChanged(v) => { - if let Screen::Wallet(WalletScreen { - announce: Some(a), .. - }) = &mut self.screen - { - let filtered: String = v + if let Some(a) = self.announce_state_mut() { + a.fingerprint = v .chars() .filter(|c| c.is_ascii_hexdigit()) .take(8) .collect(); - a.fingerprint = filtered; } Task::none() } Message::WalletAnnounceLabelChanged(v) => { - if let Screen::Wallet(WalletScreen { - announce: Some(a), .. - }) = &mut self.screen - { + if let Some(a) = self.announce_state_mut() { a.label = v.chars().take(MAX_XPUB_LABEL_LENGTH).collect(); } Task::none() @@ -1484,27 +1474,17 @@ impl App { Task::none() } Message::WalletSubmitAnnounce => { - let (xpub, fingerprint, label) = if let Screen::Wallet(WalletScreen { - announce: Some(a), - .. - }) = &mut self.screen - { - a.submitting = true; - a.error = None; - ( - a.xpub.trim().to_string(), - a.fingerprint.trim().to_string(), - a.label.trim().to_string(), - ) - } else { + let Some(a) = self.announce_state_mut() else { return Task::none(); }; + a.submitting = true; + a.error = None; + let xpub = a.xpub.trim().to_string(); + let fingerprint = a.fingerprint.trim().to_string(); + let label = a.label.trim().to_string(); let Some(node) = self.get_frost_node() else { - if let Screen::Wallet(WalletScreen { - announce: Some(a), .. - }) = &mut self.screen - { + if let Some(a) = self.announce_state_mut() { a.error = Some("Relay not connected".into()); a.submitting = false; } @@ -1527,14 +1507,16 @@ impl App { ) } Message::WalletAnnounceResult(result) => { - if let Screen::Wallet(s) = &mut self.screen { - match result { - Ok(()) => s.announce = None, - Err(e) => { - if let Some(a) = &mut s.announce { - a.error = Some(e); - a.submitting = false; - } + match result { + Ok(()) => { + if let Screen::Wallet(s) = &mut self.screen { + s.announce = None; + } + } + Err(e) => { + if let Some(a) = self.announce_state_mut() { + a.error = Some(e); + a.submitting = false; } } } @@ -1899,6 +1881,7 @@ impl App { let clear_clipboard = self.clipboard_clear_at.take().is_some(); self.active_share_hex = None; self.active_coordinations.clear(); + self.peer_xpubs.clear(); self.identities.clear(); self.cached_share_count = 0; self.cached_nsec_count = 0; diff --git a/keep-desktop/src/frost.rs b/keep-desktop/src/frost.rs index b5fc08e8..9ccdb7b7 100644 --- a/keep-desktop/src/frost.rs +++ b/keep-desktop/src/frost.rs @@ -662,6 +662,16 @@ impl App { self.frost_node.lock().ok()?.clone() } + pub(crate) fn announce_state_mut( + &mut self, + ) -> Option<&mut crate::screen::wallet::AnnounceState> { + if let Screen::Wallet(ws) = &mut self.screen { + ws.announce.as_mut() + } else { + None + } + } + pub(crate) fn update_wallet_setup( &mut self, session_id: &[u8; 32], @@ -821,10 +831,10 @@ impl App { ); let entry = self.peer_xpubs.entry(share_index).or_default(); for xpub in recovery_xpubs { - if entry.len() >= keep_frost_net::protocol::MAX_RECOVERY_XPUBS { - break; - } - if !entry.iter().any(|x| x.xpub == xpub.xpub) { + if let Some(existing) = entry.iter_mut().find(|x| x.xpub == xpub.xpub) { + existing.fingerprint = xpub.fingerprint; + existing.label = xpub.label; + } else if entry.len() < keep_frost_net::MAX_RECOVERY_XPUBS { entry.push(xpub); } } diff --git a/keep-desktop/src/message.rs b/keep-desktop/src/message.rs index dc48032b..40c59515 100644 --- a/keep-desktop/src/message.rs +++ b/keep-desktop/src/message.rs @@ -536,14 +536,12 @@ impl fmt::Debug for Message { Self::WalletDescriptorProgress(..) => f.write_str("WalletDescriptorProgress"), Self::WalletStartAnnounce => f.write_str("WalletStartAnnounce"), Self::WalletAnnounceXpubChanged(_) => f.write_str("WalletAnnounceXpubChanged(***)"), - Self::WalletAnnounceFingerprintChanged(fp) => f - .debug_tuple("WalletAnnounceFingerprintChanged") - .field(fp) - .finish(), - Self::WalletAnnounceLabelChanged(l) => f - .debug_tuple("WalletAnnounceLabelChanged") - .field(l) - .finish(), + Self::WalletAnnounceFingerprintChanged(_) => { + f.write_str("WalletAnnounceFingerprintChanged()") + } + Self::WalletAnnounceLabelChanged(_) => { + f.write_str("WalletAnnounceLabelChanged()") + } Self::WalletCancelAnnounce => f.write_str("WalletCancelAnnounce"), Self::WalletSubmitAnnounce => f.write_str("WalletSubmitAnnounce"), Self::WalletAnnounceResult(r) => f diff --git a/keep-desktop/src/screen/wallet.rs b/keep-desktop/src/screen/wallet.rs index 25edb427..d82de18f 100644 --- a/keep-desktop/src/screen/wallet.rs +++ b/keep-desktop/src/screen/wallet.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; use chrono::{DateTime, Utc}; use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; use iced::{Alignment, Element, Length}; -use keep_frost_net::protocol::VALID_XPUB_PREFIXES; use keep_frost_net::AnnouncedXpub; +use keep_frost_net::VALID_XPUB_PREFIXES; use crate::message::Message; use crate::screen::shares::ShareEntry; @@ -140,34 +140,37 @@ impl WalletScreen { let mut content = column![title_row].spacing(theme::space::MD); - if self.descriptors.is_empty() { - let mut inner = column![ - text("No wallet descriptors yet") - .size(theme::size::BODY) - .color(theme::color::TEXT_MUTED), - text("Use Setup Wallet to coordinate a descriptor with your peers.") - .size(theme::size::SMALL) - .color(theme::color::TEXT_DIM), - ] - .align_x(Alignment::Center) - .spacing(theme::space::SM); - - if !self.peer_xpubs.is_empty() { - inner = inner.push(self.peer_xpubs_card()); - } + let mut list = column![].spacing(theme::space::SM); - content = content.push(scrollable(inner).height(Length::Fill)); + if self.descriptors.is_empty() { + list = list.push( + container( + column![ + text("No wallet descriptors yet") + .size(theme::size::BODY) + .color(theme::color::TEXT_MUTED), + text("Use Setup Wallet to coordinate a descriptor with your peers.") + .size(theme::size::SMALL) + .color(theme::color::TEXT_DIM), + ] + .align_x(Alignment::Center) + .spacing(theme::space::SM), + ) + .center_x(Length::Fill) + .center_y(Length::Fill), + ); } else { - let mut list = column![].spacing(theme::space::SM); for (i, entry) in self.descriptors.iter().enumerate() { list = list.push(self.wallet_card(i, entry)); } - if !self.peer_xpubs.is_empty() { - list = list.push(self.peer_xpubs_card()); - } - content = content.push(scrollable(list).height(Length::Fill)); } + if !self.peer_xpubs.is_empty() { + list = list.push(self.peer_xpubs_card()); + } + + content = content.push(scrollable(list).height(Length::Fill)); + container(content) .padding(theme::space::XL) .width(Length::Fill) @@ -591,44 +594,43 @@ impl WalletScreen { indices.sort(); for idx in indices { - if let Some(xpubs) = self.peer_xpubs.get(&idx) { - let share_label = text(format!("Share {idx}")) - .size(theme::size::BODY) - .color(theme::color::TEXT); - - let mut share_col = column![share_label].spacing(theme::space::XS); - - for xpub in xpubs { - let truncated = if xpub.xpub.len() > 32 { - let prefix: String = xpub.xpub.chars().take(32).collect(); - format!("{prefix}...") - } else { - xpub.xpub.clone() - }; - - let mut xpub_row = row![ - text(truncated) - .size(theme::size::TINY) - .color(theme::color::TEXT_DIM), - text(&xpub.fingerprint) + let xpubs = &self.peer_xpubs[&idx]; + let share_label = text(format!("Share {idx}")) + .size(theme::size::BODY) + .color(theme::color::TEXT); + + let mut share_col = column![share_label].spacing(theme::space::XS); + + for xpub in xpubs { + let display = if xpub.xpub.chars().count() > 32 { + let prefix: String = xpub.xpub.chars().take(32).collect(); + format!("{prefix}...") + } else { + xpub.xpub.clone() + }; + + let mut xpub_row = row![ + text(display) + .size(theme::size::TINY) + .color(theme::color::TEXT_DIM), + text(&xpub.fingerprint) + .size(theme::size::TINY) + .color(theme::color::TEXT_MUTED), + ] + .spacing(theme::space::SM); + + if let Some(label) = &xpub.label { + xpub_row = xpub_row.push( + text(label) .size(theme::size::TINY) .color(theme::color::TEXT_MUTED), - ] - .spacing(theme::space::SM); - - if let Some(label) = &xpub.label { - xpub_row = xpub_row.push( - text(label) - .size(theme::size::TINY) - .color(theme::color::TEXT_MUTED), - ); - } - - share_col = share_col.push(xpub_row); + ); } - content = content.push(share_col); + share_col = share_col.push(xpub_row); } + + content = content.push(share_col); } container(content)