diff --git a/.gitignore b/.gitignore index 60cccf155..72e4d3a34 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ test_db* **/.DS_Store explorer.log .gitaipconfig +.claude/worktrees diff --git a/src/context/mod.rs b/src/context/mod.rs index bf371f568..656ab07e7 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -706,12 +706,24 @@ impl AppContext { /// Import an address into the correct Core wallet if it's not already known. /// Uses `core_wallet_name` to target the right wallet on multi-wallet nodes. /// No-op if the address is already watched/mine. + /// + /// In SPV mode this is a no-op: there is no Dash Core node to import into. + /// HD-derived addresses are tracked by the SPV wallet manager watching the + /// BIP44 account derived from the same xprv — `Wallet::register_address` + /// records them in wallet state (`known_addresses`) but does not update the + /// SPV bloom filter directly. Incoming UTXOs for these addresses are + /// populated via the SPV reconciliation path (`reconcile_spv_wallets()`), + /// which is what downstream checks such as + /// `capture_qr_funding_utxo_if_available` observe. pub fn ensure_address_imported( &self, address: &Address, core_wallet_name: Option<&str>, label: Option<&str>, ) -> Result<(), TaskError> { + if self.core_backend_mode() != CoreBackendMode::Rpc { + return Ok(()); + } let client = self.core_client_for_wallet(core_wallet_name)?; let info = client .get_address_info(address) @@ -725,12 +737,18 @@ impl AppContext { } /// Import address into Core, ignoring errors. For best-effort registration. + /// + /// No-ops in SPV mode — mirroring [`Self::ensure_address_imported`] — because there is no + /// RPC client to talk to and every call would fail silently, wasting resources. pub fn try_import_address( &self, address: &Address, core_wallet_name: Option<&str>, label: Option<&str>, ) { + if self.core_backend_mode() != CoreBackendMode::Rpc { + return; + } if let Ok(client) = self.core_client_for_wallet(core_wallet_name) { let _ = client.import_address(address, label, Some(false)); } diff --git a/src/ui/components/password_input.rs b/src/ui/components/password_input.rs index 970700597..96fff94de 100644 --- a/src/ui/components/password_input.rs +++ b/src/ui/components/password_input.rs @@ -1,7 +1,10 @@ use egui::{Rect, Response, Sense, Stroke, Ui, pos2, vec2}; use crate::model::secret::Secret; -use crate::ui::theme::{DashColors, ResponseExt}; +use crate::ui::theme::{DashColors, ResponseExt, Typography}; + +const PASSWORD_INPUT_HORIZONTAL_PADDING: f32 = 8.0; +const PASSWORD_INPUT_REVEAL_ICON_WIDTH: f32 = 28.0; /// Response from [`PasswordInput::show`]. /// @@ -141,7 +144,7 @@ impl PasswordInput { .password(!self.revealing) .hint_text(&self.hint_text) .margin(egui::Margin { - right: 28, + right: PASSWORD_INPUT_REVEAL_ICON_WIDTH as i8, ..egui::Margin::same(4) }); @@ -155,6 +158,20 @@ impl PasswordInput { if let Some(width) = self.desired_width { text_edit = text_edit.desired_width(width); + } else if let Some(limit) = self.char_limit { + let font_id = if self.monospace { + egui::TextStyle::Monospace.resolve(ui.style()) + } else { + egui::TextStyle::Body.resolve(ui.style()) + }; + // Wide glyph upper bound — identical width in monospace, safe upper bound in proportional. + let sample = "W".repeat(limit); + let measured_width = Typography::measure_text_width(ui, sample, font_id); + text_edit = text_edit.desired_width( + measured_width + + PASSWORD_INPUT_HORIZONTAL_PADDING + + PASSWORD_INPUT_REVEAL_ICON_WIDTH, + ); } else { text_edit = text_edit.desired_width(ui.available_width()); } diff --git a/src/ui/identities/keys/add_key_screen.rs b/src/ui/identities/keys/add_key_screen.rs index a48d582e0..344dcc54a 100644 --- a/src/ui/identities/keys/add_key_screen.rs +++ b/src/ui/identities/keys/add_key_screen.rs @@ -76,6 +76,7 @@ impl AddKeyScreen { app_context: app_context.clone(), private_key_input: PasswordInput::new() .with_hint_text("Private key (hex)") + .with_char_limit(64) .with_monospace(), key_type: KeyType::ECDSA_SECP256K1, purpose: Purpose::AUTHENTICATION, @@ -119,6 +120,7 @@ impl AddKeyScreen { app_context: app_context.clone(), private_key_input: PasswordInput::new() .with_hint_text("Private key (hex)") + .with_char_limit(64) .with_monospace(), key_type: KeyType::ECDSA_SECP256K1, purpose: Purpose::ENCRYPTION, @@ -162,6 +164,7 @@ impl AddKeyScreen { app_context: app_context.clone(), private_key_input: PasswordInput::new() .with_hint_text("Private key (hex)") + .with_char_limit(64) .with_monospace(), key_type: KeyType::ECDSA_SECP256K1, purpose: Purpose::DECRYPTION, diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 5f14e5e6c..dcf5cc1a7 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -1,4 +1,6 @@ -use egui::{Button, Color32, CursorIcon, FontFamily, FontId, RichText, Stroke, Vec2, WidgetText}; +use egui::{ + Button, Color32, CursorIcon, FontFamily, FontId, RichText, Stroke, Ui, Vec2, WidgetText, +}; /// Theme mode enumeration #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -602,6 +604,14 @@ impl Typography { pub fn button() -> FontId { FontId::new(Self::SCALE_BASE, FontFamily::Proportional) } + + /// Measure the width of a representative sample using egui's active font metrics. + pub fn measure_text_width(ui: &Ui, sample: impl Into, font_id: FontId) -> f32 { + ui.painter() + .layout_no_wrap(sample.into(), font_id, Color32::TRANSPARENT) + .size() + .x + } } /// Spacing constants for consistent layout