v0.0.9 - TUI: real-time per-word BIP-39 validation in seed entry popup

This commit is contained in:
Laan Tungir
2026-08-19 15:17:15 -04:00
parent 2f7f29ed99
commit 5d3cc4e96d
4 changed files with 52 additions and 3 deletions
Generated
+1 -1
View File
@@ -1490,7 +1490,7 @@ dependencies = [
[[package]]
name = "nsigner"
version = "0.0.7"
version = "0.0.8"
dependencies = [
"base64",
"chacha20poly1305",
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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";
+49
View File
@@ -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<Span> = 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",