Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,42 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
super.dispose();
}

// The number of leading characters CryptoNote (Monero/Wownero/Salvium)
// mnemonics use when matching words. Words are compared by this unique
// prefix rather than by their full spelling, so valid seeds may contain
// truncations or inflections that are not verbatim wordlist entries.
// English and the other supported languages use a prefix length of 3.
static const int _cryptonotePrefixLength = 3;

String _cryptonotePrefix(String word) =>
word.length <= _cryptonotePrefixLength
? word
: word.substring(0, _cryptonotePrefixLength);

bool _isValidCryptonoteWord(String word, List<String> wordList) {
// Fast path: exact match.
if (wordList.contains(word)) {
return true;
}
// CryptoNote matches words by their unique prefix, not the full word.
final prefix = _cryptonotePrefix(word);
return wordList.any((w) => _cryptonotePrefix(w) == prefix);
}

// TODO: check for wownero wordlist?
bool _isValidMnemonicWord(String word) {
// TODO: get the actual language
if (widget.coin is Monero || widget.coin is Salvium) {
// Salvium use's Monero's wordlists.
switch (widget.seedWordsLength) {
case 25:
return csMonero.getMoneroWordList("English").contains(word);
return _isValidCryptonoteWord(
word,
csMonero.getMoneroWordList("English"),
);
case 16:
// The 16 word seed is a BIP39 style (Polyseed) wordlist whose words
// must match exactly, so the CryptoNote prefix rule does not apply.
return Monero.sixteenWordsWordList.contains(word);
default:
return false;
Expand All @@ -194,6 +221,11 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
"English",
widget.seedWordsLength,
);
// Only the 25 word seed uses the CryptoNote prefix wordlist. The 14 word
// seed is a BIP39 style (Polyseed) wordlist requiring an exact match.
if (widget.seedWordsLength == 25) {
return _isValidCryptonoteWord(word, wowneroWordList);
}
return wowneroWordList.contains(word);
}
if (widget.coin is Xelis) {
Expand All @@ -219,6 +251,24 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
}
mnemonic = mnemonic.trim();

// Verify word count matches expected seed length.
final wordCount = mnemonic.split(" ").length;
if (wordCount != _seedWordCount) {
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message:
"Expected $_seedWordCount words but got $wordCount. "
"Please fill in all fields.",
context: context,
),
);
setState(() => _hideSeedWords = false);
}
return;
}

int height = widget.restoreBlockHeight;
String? otherDataJsonString;

Expand Down Expand Up @@ -887,6 +937,20 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
i * 4 + j - 1 == 1
? textSelectionControls
: null,
validator: (value) {
if (value == null ||
value.trim().isEmpty) {
return "Required";
}
if (!_isValidMnemonicWord(
value
.trim()
.toLowerCase(),
)) {
return "Invalid word";
}
return null;
},
// focusNode:
// _focusNodes[i * 4 + j - 1],
onChanged: (value) {
Expand Down Expand Up @@ -1032,6 +1096,20 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
selectionControls: i == 1
? textSelectionControls
: null,
validator: (value) {
if (value == null ||
value.trim().isEmpty) {
return "Required";
}
if (!_isValidMnemonicWord(
value
.trim()
.toLowerCase(),
)) {
return "Invalid word";
}
return null;
},
onChanged: (value) {
final FormInputStatus
formInputStatus;
Expand Down Expand Up @@ -1169,6 +1247,18 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
selectionControls: i == 1
? textSelectionControls
: null,
validator: (value) {
if (value == null ||
value.trim().isEmpty) {
return "Required";
}
if (!_isValidMnemonicWord(
value.trim().toLowerCase(),
)) {
return "Invalid word";
}
return null;
},
// focusNode: _focusNodes[i - 1],
onChanged: (value) {
final FormInputStatus formInputStatus;
Expand Down
2 changes: 1 addition & 1 deletion lib/wallets/wallet/intermediate/lib_monero_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,7 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
height: height,
);

if (this.wallet == null) {
if (this.wallet != null) {
await exit();
}
this.wallet = wallet;
Expand Down
2 changes: 1 addition & 1 deletion lib/wallets/wallet/intermediate/lib_wownero_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1555,7 +1555,7 @@ abstract class LibWowneroWallet<T extends CryptonoteCurrency>
height: height,
);

if (this.wallet == null) {
if (this.wallet != null) {
await exit();
}
this.wallet = wallet;
Expand Down
Loading