diff --git a/electrum/gui/qml/components/controls/SeedTextArea.qml b/electrum/gui/qml/components/controls/SeedTextArea.qml index 9674c008fc2a..38646b5af351 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