From 123c5a16a253c1a0ff628c859a770acb1c71aab2 Mon Sep 17 00:00:00 2001 From: c03rad0r Date: Fri, 17 Jul 2026 14:24:13 +0530 Subject: [PATCH 1/2] feat(qml): add per-word validation feedback in seed entry Flag individual words not in the BIP39/Electrum wordlist during seed entry on mobile (QML). Shows a red warning label indicating which word(s) are invalid, without blocking input. Changes: - Add isWordInWordlist() slot to QEBitcoin for word validation - Add validateWords property and per-word check to SeedTextArea.qml - Enable validateWords in WCConfirmSeed (retype seed) and WCHaveSeed (restore from seed) wizard pages Closes #10758 --- .../qml/components/controls/SeedTextArea.qml | 32 +++++++++++++++++++ .../qml/components/wizard/WCConfirmSeed.qml | 1 + .../gui/qml/components/wizard/WCHaveSeed.qml | 1 + electrum/gui/qml/qebitcoin.py | 8 +++++ 4 files changed, 42 insertions(+) diff --git a/electrum/gui/qml/components/controls/SeedTextArea.qml b/electrum/gui/qml/components/controls/SeedTextArea.qml index 9674c008fc2a..a9963e081e81 100644 --- a/electrum/gui/qml/components/controls/SeedTextArea.qml +++ b/electrum/gui/qml/components/controls/SeedTextArea.qml @@ -16,7 +16,13 @@ Pane { property string indicatorText property bool indicatorValid + // Whether to validate individual words against the wordlist. + // When true, invalid words are visually flagged (red underline area). + // Does not block input — user can still type and correct. + property bool validateWords: false + property var _suggestions: [] + property var _invalidWordIndices: [] onTextChanged: { if (seedtextarea.text != text) @@ -63,6 +69,19 @@ Pane { _suggestions = bitcoin.mnemonicsFor(seedtextarea.text.split(' ').pop()) // TODO: cursorPosition only on suggestion apply cursorPosition = text.length + + // per-word validation: flag words not in the wordlist + if (root.validateWords) { + var words = seedtextarea.text.split(' ') + // last word may be incomplete (still being typed), skip it + var invalid = [] + for (var i = 0; i < words.length - 1; i++) { + if (words[i].length > 0 && !bitcoin.isWordInWordlist(words[i])) { + invalid.push(i) + } + } + _invalidWordIndices = invalid + } } Rectangle { @@ -86,6 +105,19 @@ Pane { } } + Label { + id: invalidWordWarning + visible: root.validateWords && _invalidWordIndices.length > 0 + text: _invalidWordIndices.length === 1 + ? qsTr('Word %1 is not in the word list').arg(_invalidWordIndices[0] + 1) + : qsTr('%1 words are not in the word list').arg(_invalidWordIndices.length) + color: Material.color(Material.Red) + font.pixelSize: constants.fontSizeSmall + Layout.fillWidth: true + wrapMode: Text.Wrap + leftPadding: constants.paddingXXSmall + } + Flickable { Layout.preferredWidth: parent.width Layout.minimumHeight: fontMetrics.lineSpacing + 2*constants.paddingXXSmall + 2*constants.paddingXSmall + 2 diff --git a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml index 5aebaf0b7169..0c1aaa6d67f3 100644 --- a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml +++ b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml @@ -45,6 +45,7 @@ WizardComponent { Layout.fillWidth: true Layout.topMargin: constants.paddingSmall placeholderText: qsTr('Enter your seed') + validateWords: true onTextChanged: checkValid() } } diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index 95ce6cd6f53c..4bc9fcb9364d 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -201,6 +201,7 @@ WizardComponent { Layout.topMargin: constants.paddingLarge placeholderText: cosigner ? qsTr('Enter cosigner seed') : qsTr('Enter your seed') + validateWords: true indicatorValid: root._seedValid ? root._seedType == 'bip39' && root._validationMessage diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index 2f1f0df409fb..2e150bf93791 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -125,3 +125,11 @@ def mnemonicsFor(self, fragment): if not self._words: self._words = set(Mnemonic('en').wordlist).union(set(old_wordlist)) return sorted(filter(lambda x: x.startswith(fragment), self._words)) + + @pyqtSlot(str, result=bool) + def isWordInWordlist(self, word): + if not word: + return True + if not self._words: + self._words = set(Mnemonic('en').wordlist).union(set(old_wordlist)) + return word in self._words From d3ab085054b54c420b5d1a95217c9457110f3b1f Mon Sep 17 00:00:00 2001 From: c03rad0r Date: Fri, 17 Jul 2026 15:12:28 +0530 Subject: [PATCH 2/2] fix: replace em-dash with ASCII hyphen in SeedTextArea.qml comment ban_unicode.py CI check bans non-whitelisted unicode characters. --- electrum/gui/qml/components/controls/SeedTextArea.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/SeedTextArea.qml b/electrum/gui/qml/components/controls/SeedTextArea.qml index a9963e081e81..38646b5af351 100644 --- a/electrum/gui/qml/components/controls/SeedTextArea.qml +++ b/electrum/gui/qml/components/controls/SeedTextArea.qml @@ -18,7 +18,7 @@ Pane { // Whether to validate individual words against the wordlist. // When true, invalid words are visually flagged (red underline area). - // Does not block input — user can still type and correct. + // Does not block input - user can still type and correct. property bool validateWords: false property var _suggestions: []