diff --git a/Cargo.lock b/Cargo.lock index 0fa5c2f..bbafdf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1490,7 +1490,7 @@ dependencies = [ [[package]] name = "nsigner" -version = "0.0.7" +version = "0.0.8" dependencies = [ "base64", "chacha20poly1305", diff --git a/Cargo.toml b/Cargo.toml index e379721..72f7e0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nsigner" -version = "0.0.8" +version = "0.0.9" edition = "2021" license = "MIT" description = "Attended Nostr signing daemon — Rust port of n_signer" diff --git a/src/lib.rs b/src/lib.rs index 9b3f366..3f644fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,4 +31,4 @@ pub mod error; pub use error::NsignerError; /// Version string (matches C NSIGNER_VERSION). -pub const VERSION: &str = "v0.0.8"; +pub const VERSION: &str = "v0.0.9"; diff --git a/src/tui.rs b/src/tui.rs index 4b25d85..dc0c3bd 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -38,6 +38,11 @@ pub const TRANSPORT_HTTP: u8 = 0x08; /// Matches NSIGNER_QREXEC_SERVICE_NAME in n_signer/src/main.c. pub const QREXEC_SERVICE_NAME: &str = "qubes.NsignerRpc"; +/// Check if a word is in the BIP-39 English wordlist. +fn is_valid_bip39_word(word: &str) -> bool { + nips::nip006::bip39_wordlist().binary_search(&word).is_ok() +} + /// Number of focusable commands on the main screen: /// 4 transports + Add + Edit + Delete + Clear + Help + Quit. pub const FOCUS_COUNT: usize = 10; @@ -1293,6 +1298,50 @@ impl App { Style::default(), )), ]; + + // Per-word validation: split the input by whitespace and check + // each completed word against the BIP-39 wordlist. The word + // currently being typed (last fragment without a trailing space) + // is not checked — only space-separated completed words count. + let buffer = &self.unlock_input.buffer; + let has_trailing_space = buffer.ends_with(' '); + let words: Vec<&str> = buffer.split_whitespace().collect(); + let completed_count = if has_trailing_space { + words.len() + } else { + words.len().saturating_sub(1) + }; + + if completed_count > 0 { + let red = Style::default().fg(ratatui::style::Color::Red); + let green = Style::default().fg(ratatui::style::Color::Green); + let mut spans: Vec = Vec::new(); + for (i, w) in words.iter().take(completed_count).enumerate() { + if i > 0 { + spans.push(Span::raw(" ")); + } + if is_valid_bip39_word(w) { + spans.push(Span::styled(format!("✓{}", w), green)); + } else { + spans.push(Span::styled(format!("✗{}", w), red)); + } + } + lines.push(Line::from("")); + lines.push(Line::from(spans)); + } + + // Word count indicator. + let total_words = words.len(); + if total_words > 0 { + let valid_counts = [12, 15, 18, 21, 24]; + let count_note = if valid_counts.contains(&total_words) { + format!("Words: {} (valid count)", total_words) + } else { + format!("Words: {} (need 12/15/18/21/24)", total_words) + }; + lines.push(Line::from(count_note)); + } + if self.seed_attempts > 0 { lines.push(Line::from(format!( "Invalid mnemonic. Attempts: {}/10",