CW-723-Add-Monero-support-to-the-Shared-Seed-feature-in-Cake (#2131)

* feat: add exodus style bip39 to monero legacy seed

* feat: restore monero wallet from bip39 and add test

* bug: fix wrong naming in CI

* feat: add monero bip39 UI flow

* fix: monero.dart generation

* fix: skip monero_wallet_service tests till CI is fixed

* ci: copy monero_libwallet2_api_c.so to /usr/lib for testing
ci: reduce timeout for cw_monero tests

* fix: monero wallet creation credentials default to bip39 if mnemonic are set

* fix: do not skip monero wallets services test

* fix: Include non bip39 monero wallets on Wallet Group

* fix: null pointer stemming from missing language selector if seed is selected

* fix: Fixes to Bip39 Creation and restore

- Do not restore from 0 for fresh bip39 wallet
- disallow restoring bip39 wallet without date or height

* fix: Fixes to Bip39 restore

- Refresh height is now getting set correctly
- Add new create monero wallet tests
- Add seed-language English for Bip39 Monero wallets
- Fix seed-type naming

* feat (cw_monero): Store monero wallet after bip39 creation

* feat (cw_monero): remove prints from monero_wallet_service_test.dart

* fix: exception during seed language autodetect

* feat (cw_monero): Add support for passphrases on bip39 seeds

* feat (cw_monero): Add support for passphrases on bip39 seeds

* fix: seed language selection for recovering bip39 wallets

* style: improve readability of isLegacySeedOnly in wallet_keys_view_model.dart

* feat: hide monero seed type selector from advanced settings when creating a child wallet

* fix(cw_monero): use named arguments for bip39_seed tests

---------

Co-authored-by: cyan <cyjan@mrcyjanek.net>
This commit is contained in:
Konstantin Ullrich
2025-04-10 03:31:26 +02:00
committed by GitHub
co-authored by cyan
parent 494207290e
commit f58a5fb8fd
51 changed files with 702 additions and 283 deletions
+8 -17
View File
@@ -1,17 +1,17 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cw_core/enumerable_item.dart';
import 'package:cw_core/wallet_info.dart';
class MoneroSeedType extends EnumerableItem<int> with Serializable<int> {
const MoneroSeedType({required String title, required int raw}) : super(title: title, raw: raw);
static const all = [MoneroSeedType.legacy, MoneroSeedType.polyseed];
static const all = [legacy, polyseed, bip39];
static const defaultSeedType = polyseed;
static const legacy = MoneroSeedType(raw: 0, title: 'Legacy (25 words)');
static const polyseed = MoneroSeedType(raw: 1, title: 'Polyseed (16 words)');
static const wowneroSeed = MoneroSeedType(raw: 2, title: 'Wownero (14 words)');
static const legacy = MoneroSeedType(raw: 0, title: 'Legacy');
static const polyseed = MoneroSeedType(raw: 1, title: 'Polyseed');
static const wowneroSeed = MoneroSeedType(raw: 2, title: 'Wownero');
static const bip39 = MoneroSeedType(raw: 3, title: 'BIP39');
static MoneroSeedType deserialize({required int raw}) {
switch (raw) {
@@ -21,24 +21,15 @@ class MoneroSeedType extends EnumerableItem<int> with Serializable<int> {
return polyseed;
case 2:
return wowneroSeed;
case 3:
return bip39;
default:
throw Exception('Unexpected token: $raw for SeedType deserialize');
}
}
@override
String toString() {
switch (this) {
case MoneroSeedType.legacy:
return S.current.seedtype_legacy;
case MoneroSeedType.polyseed:
return S.current.seedtype_polyseed;
case MoneroSeedType.wowneroSeed:
return S.current.seedtype_wownero;
default:
return '';
}
}
String toString() => title;
}
class BitcoinSeedType extends EnumerableItem<int> with Serializable<int> {
+13 -3
View File
@@ -252,11 +252,21 @@ class CWMonero extends Monero {
WalletCredentials createMoneroNewWalletCredentials({
required String name,
required String language,
required bool isPolyseed,
required int seedType,
required String? passphrase,
String? password}) =>
String? password,
String? mnemonic,
}) =>
MoneroNewWalletCredentials(
name: name, password: password, language: language, isPolyseed: isPolyseed, passphrase: passphrase);
name: name,
password: password,
language: language,
seedType: seedType == 1
? MoneroSeedType.polyseed
: (seedType == 3 ? MoneroSeedType.bip39 : MoneroSeedType.legacy),
passphrase: passphrase,
mnemonic: mnemonic,
);
@override
Map<String, String> getKeys(Object wallet) {
+1 -1
View File
@@ -11,8 +11,8 @@ bool isBIP39Wallet(WalletType walletType) {
case WalletType.bitcoinCash:
case WalletType.nano:
case WalletType.banano:
return true;
case WalletType.monero:
return true;
case WalletType.wownero:
case WalletType.haven:
case WalletType.zano:
@@ -144,7 +144,9 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
),
);
}),
if (widget.privacySettingsViewModel.isMoneroSeedTypeOptionsEnabled)
if (widget
.privacySettingsViewModel.isMoneroSeedTypeOptionsEnabled &&
!widget.isChildWallet)
Observer(builder: (_) {
return SettingsChoicesCell(
ChoicesListItem<MoneroSeedType>(
@@ -305,7 +305,7 @@ class _WalletNameFormState extends State<WalletNameForm> {
),
),
),
if (_walletNewVM.hasLanguageSelector) ...[
if (_walletNewVM.showLanguageSelector) ...[
if (_walletNewVM.hasSeedType) ...[
Observer(
builder: (BuildContext build) => Padding(
@@ -401,7 +401,11 @@ class _WalletNameFormState extends State<WalletNameForm> {
} else {
await _walletNewVM.create(
options: _walletNewVM.hasLanguageSelector
? [_languageSelectorKey.currentState!.selected, isPolyseed]
? [
_languageSelectorKey.currentState?.selected ??
defaultSeedLanguage,
widget._seedSettingsViewModel.moneroSeedType
]
: null);
}
} catch (e) {
@@ -11,13 +11,15 @@ import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:polyseed/polyseed.dart';
class WalletRestoreFromSeedForm extends StatefulWidget {
WalletRestoreFromSeedForm({Key? key,
WalletRestoreFromSeedForm({
Key? key,
required this.displayLanguageSelector,
required this.displayBlockHeightSelector,
required this.type,
@@ -47,21 +49,23 @@ class WalletRestoreFromSeedForm extends StatefulWidget {
@override
WalletRestoreFromSeedFormState createState() =>
WalletRestoreFromSeedFormState('English', displayWalletPassword: displayWalletPassword);
WalletRestoreFromSeedFormState('English',
displayWalletPassword: displayWalletPassword);
}
class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
WalletRestoreFromSeedFormState(this.language, {required bool displayWalletPassword})
WalletRestoreFromSeedFormState(this.language,
{required bool displayWalletPassword})
: seedWidgetStateKey = GlobalKey<SeedWidgetState>(),
blockchainHeightKey = GlobalKey<BlockchainHeightState>(),
formKey = GlobalKey<FormState>(),
languageController = TextEditingController(),
nameTextEditingController = TextEditingController(),
passwordTextEditingController = displayWalletPassword ? TextEditingController() : null,
repeatedPasswordTextEditingController = displayWalletPassword
? TextEditingController()
: null,
seedTypeController = TextEditingController();
passwordTextEditingController =
displayWalletPassword ? TextEditingController() : null,
repeatedPasswordTextEditingController =
displayWalletPassword ? TextEditingController() : null,
seedTypeController = TextEditingController();
final GlobalKey<SeedWidgetState> seedWidgetStateKey;
final GlobalKey<BlockchainHeightState> blockchainHeightKey;
@@ -83,27 +87,30 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
_setLanguageLabel(language);
if (passwordTextEditingController != null) {
passwordListener = () => widget.onPasswordChange?.call(passwordTextEditingController!.text);
passwordListener = () =>
widget.onPasswordChange?.call(passwordTextEditingController!.text);
passwordTextEditingController?.addListener(passwordListener!);
}
if (repeatedPasswordTextEditingController != null) {
repeatedPasswordListener =
() => widget.onRepeatedPasswordChange?.call(repeatedPasswordTextEditingController!.text);
repeatedPasswordTextEditingController?.addListener(repeatedPasswordListener!);
repeatedPasswordListener = () => widget.onRepeatedPasswordChange
?.call(repeatedPasswordTextEditingController!.text);
repeatedPasswordTextEditingController
?.addListener(repeatedPasswordListener!);
}
moneroSeedTypeReaction =
reaction((_) => widget.seedSettingsViewModel.moneroSeedType, (MoneroSeedType item) {
_setSeedType(item);
_changeLanguage('English');
});
reaction((_) => widget.seedSettingsViewModel.moneroSeedType,
(MoneroSeedType item) {
_setSeedType(item);
_changeLanguage('English');
});
super.initState();
}
@override
void dispose() {
void dispose() {
moneroSeedTypeReaction();
if (passwordListener != null) {
@@ -118,16 +125,22 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
}
void onSeedChange(String seed) {
if ((widget.type == WalletType.monero || widget.type == WalletType.wownero) &&
Polyseed.isValidSeed(seed)) {
final lang = PolyseedLang.getByPhrase(seed);
if ([WalletType.monero, WalletType.wownero].contains(widget.type) &&
(seed.split(" ").length == 12 || Polyseed.isValidSeed(seed))) {
try {
final lang = PolyseedLang.getByPhrase(seed);
_changeSeedType(MoneroSeedType.polyseed);
_changeLanguage(lang.nameEnglish);
if (widget.type == WalletType.monero && seed.split(" ").length == 12) {
_changeSeedType(MoneroSeedType.bip39);
} else {
_changeSeedType(MoneroSeedType.polyseed);
}
_changeLanguage(lang.nameEnglish, true);
} catch (e) {
printV(e);
}
}
if (widget.type == WalletType.wownero && seed
.split(" ")
.length == 14) {
if (widget.type == WalletType.wownero && seed.split(" ").length == 14) {
_changeSeedType(MoneroSeedType.wowneroSeed);
_changeLanguage("English");
}
@@ -147,9 +160,7 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
BaseTextFormField(
key: ValueKey('wallet_restore_from_seed_wallet_name_textfield_key'),
controller: nameTextEditingController,
hintText: S
.of(context)
.wallet_name,
hintText: S.of(context).wallet_name,
suffixIcon: IconButton(
key: ValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key'),
onPressed: () async {
@@ -158,17 +169,17 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
setState(() {
nameTextEditingController.text = rName;
nameTextEditingController.selection = TextSelection.fromPosition(
TextPosition(offset: nameTextEditingController.text.length));
nameTextEditingController.selection =
TextSelection.fromPosition(TextPosition(
offset:
nameTextEditingController.text.length));
});
},
icon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
color: Theme
.of(context)
.hintColor,
color: Theme.of(context).hintColor,
),
width: 34,
height: 34,
@@ -194,14 +205,13 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
seedTextFieldKey: ValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key'),
pasteButtonKey: ValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'),
),
if (widget.type == WalletType.monero || widget.type == WalletType.wownero)
if ([WalletType.monero, WalletType.wownero].contains(widget.type))
GestureDetector(
key: ValueKey('wallet_restore_from_seed_seedtype_picker_button_key'),
onTap: () async {
await showPopUp<void>(
context: context,
builder: (_) =>
Picker(
builder: (_) => Picker(
items: _getItems(),
selectedAtIndex: isPolyseed
? 1
@@ -226,33 +236,30 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
),
),
),
if (widget.displayWalletPassword)
...[BaseTextFormField(
if (widget.displayWalletPassword) ...[
BaseTextFormField(
key: ValueKey('password'),
controller: passwordTextEditingController,
hintText: S
.of(context)
.password,
hintText: S.of(context).password,
obscureText: true),
BaseTextFormField(
key: ValueKey('repeat_wallet_password'),
controller: repeatedPasswordTextEditingController,
hintText: S
.of(context)
.repeat_wallet_password,
obscureText: true)
],
BaseTextFormField(
key: ValueKey('repeat_wallet_password'),
controller: repeatedPasswordTextEditingController,
hintText: S.of(context).repeat_wallet_password,
obscureText: true)
],
if (widget.displayLanguageSelector)
if (!seedTypeController.value.text.contains("14") && widget.displayLanguageSelector)
GestureDetector(
onTap: () async {
await showPopUp<void>(
context: context,
builder: (_) =>
SeedLanguagePicker(
builder: (_) => SeedLanguagePicker(
selected: language,
onItemSelected: _changeLanguage,
seedType: isPolyseed ? MoneroSeedType.polyseed : MoneroSeedType.legacy,
onItemSelected: (lang) =>
_changeLanguage(lang, isPolyseed || isBip39),
seedType:
widget.seedSettingsViewModel.moneroSeedType,
));
},
child: Container(
@@ -274,7 +281,8 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
key: blockchainHeightKey,
blockHeightTextFieldKey: ValueKey('wallet_restore_from_seed_blockheight_textfield_key'),
onHeightOrDateEntered: widget.onHeightOrDateEntered,
hasDatePicker: widget.type == WalletType.monero || widget.type == WalletType.wownero,
hasDatePicker:
[WalletType.monero, WalletType.wownero].contains(widget.type),
walletType: widget.type,
),
]));
@@ -282,28 +290,29 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
bool get isPolyseed =>
widget.seedSettingsViewModel.moneroSeedType == MoneroSeedType.polyseed &&
(widget.type == WalletType.monero || widget.type == WalletType.wownero);
[WalletType.monero, WalletType.wownero].contains(widget.type);
Widget get expandIcon =>
Container(
bool get isBip39 =>
widget.seedSettingsViewModel.moneroSeedType == MoneroSeedType.bip39 &&
WalletType.monero == widget.type;
Widget get expandIcon => Container(
padding: EdgeInsets.all(18),
width: 24,
height: 24,
child: Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
height: 8,
color: Theme
.of(context)
.hintColor,
color: Theme.of(context).hintColor,
),
);
void _changeLanguage(String language) {
final setLang = isPolyseed
void _changeLanguage(String language, [bool useBip39Wordlist = false]) {
final setLang = useBip39Wordlist
? "POLYSEED_$language"
: seedTypeController.value.text.contains("14")
? "WOWSEED_" + language
: language;
? "WOWSEED_" + language
: language;
setState(() {
this.language = setLang;
seedWidgetStateKey.currentState!.changeSeedLanguage(setLang);
@@ -312,8 +321,8 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
});
}
void _setLanguageLabel(String language) =>
languageController.text = '${language.replaceAll("POLYSEED_", "")} (Seed language)';
void _setLanguageLabel(String language) => languageController.text =
'${language.replaceAll("POLYSEED_", "")} (Seed language)';
void _changeSeedType(MoneroSeedType item) {
_setSeedType(item);
@@ -328,9 +337,17 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
List<MoneroSeedType> _getItems() {
switch (widget.type) {
case WalletType.monero:
return [MoneroSeedType.legacy, MoneroSeedType.polyseed];
return [
MoneroSeedType.legacy,
MoneroSeedType.polyseed,
MoneroSeedType.bip39
];
case WalletType.wownero:
return [MoneroSeedType.legacy, MoneroSeedType.polyseed, MoneroSeedType.wowneroSeed];
return [
MoneroSeedType.legacy,
MoneroSeedType.polyseed,
MoneroSeedType.wowneroSeed
];
default:
return [MoneroSeedType.legacy];
}
@@ -527,25 +527,42 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
}
bool _isValidSeed() {
final seedPhrase =
walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text;
final seedPhrase = walletRestoreFromSeedFormKey
.currentState!.seedWidgetStateKey.currentState!.text;
if (walletRestoreViewModel.isPolyseed(seedPhrase)) return true;
final seedWords = seedPhrase.split(' ');
if (seedWords.length == 14 && walletRestoreViewModel.type == WalletType.wownero) return true;
if (seedWords.length == 26 && walletRestoreViewModel.type == WalletType.zano) return true;
if (seedWords.length == 14 &&
walletRestoreViewModel.type == WalletType.wownero) return true;
if (seedWords.length == 26 &&
walletRestoreViewModel.type == WalletType.zano) return true;
if ((walletRestoreViewModel.type == WalletType.monero ||
walletRestoreViewModel.type == WalletType.wownero ||
walletRestoreViewModel.type == WalletType.haven) &&
seedWords.length != WalletRestoreViewModelBase.moneroSeedMnemonicLength) {
return false;
if (seedWords.length == 12 &&
walletRestoreViewModel.type == WalletType.monero) {
return walletRestoreFromSeedFormKey
.currentState
?.blockchainHeightKey
.currentState
?.restoreHeightController
.text
.isNotEmpty == true;
}
if ([WalletType.monero, WalletType.wownero, WalletType.haven]
.contains(walletRestoreViewModel.type) &&
seedWords.length ==
WalletRestoreViewModelBase.moneroSeedMnemonicLength) {
return true;
}
// bip39:
final validBip39SeedLengths = [12, 18, 24];
final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven, WalletType.decred];
final nonBip39WalletTypes = [
WalletType.wownero,
WalletType.haven,
WalletType.decred
];
// if it's a bip39 wallet and the length is not valid return false
if (!nonBip39WalletTypes.contains(walletRestoreViewModel.type) &&
!(validBip39SeedLengths.contains(seedWords.length))) {
@@ -558,8 +575,9 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
return false;
}
final words =
walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.words.toSet();
final words = walletRestoreFromSeedFormKey
.currentState!.seedWidgetStateKey.currentState!.words
.toSet();
return seedWords.toSet().difference(words).toSet().isEmpty;
}
+1 -1
View File
@@ -16,7 +16,7 @@ class SeedLanguagePickerOption {
final List<SeedLanguagePickerOption> seedLanguages = [
SeedLanguagePickerOption('English', S.current.seed_language_english,
Image.asset('assets/images/flags/usa.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]),
Image.asset('assets/images/flags/usa.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed, MoneroSeedType.bip39]),
SeedLanguagePickerOption('Chinese (Simplified)', S.current.seed_language_chinese,
Image.asset('assets/images/flags/chn.png'), [MoneroSeedType.legacy, MoneroSeedType.polyseed]),
SeedLanguagePickerOption('Chinese (Traditional)', S.current.seed_language_chinese_traditional,
@@ -6,6 +6,7 @@ import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cake_wallet/wallet_types.g.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
@@ -127,12 +128,16 @@ abstract class WalletGroupsDisplayViewModelBase with Store {
bool isNonSeedWallet = wallet.isNonSeedWallet;
bool isNotMoneroBip39Wallet = wallet.type == WalletType.monero &&
wallet.derivationInfo?.derivationType != DerivationType.bip39;
// Exclude if any of these conditions are true
return isNonBIP39Wallet ||
isNanoDerivationType ||
isElectrumDerivationType ||
isSameTypeAsSelectedWallet ||
isNonSeedWallet;
isNonSeedWallet ||
isNotMoneroBip39Wallet;
});
if (shouldExcludeGroup) continue;
+4 -3
View File
@@ -63,14 +63,15 @@ abstract class WalletKeysViewModelBase with Store {
String get seed => _wallet.seed != null ? _wallet.seed! : '';
bool get isLegacySeedOnly =>
(_wallet.type == WalletType.monero || _wallet.type == WalletType.wownero) &&
[WalletType.monero, WalletType.wownero].contains(_wallet.type) &&
_wallet.seed != null &&
!Polyseed.isValidSeed(_wallet.seed!);
!(Polyseed.isValidSeed(_wallet.seed!) ||
_wallet.seed!.split(' ').length == 12);
String get legacySeed {
if ((_wallet.type == WalletType.monero || _wallet.type == WalletType.wownero) &&
_wallet.seed != null &&
Polyseed.isValidSeed(_wallet.seed!)) {
(Polyseed.isValidSeed(_wallet.seed!) || _wallet.seed!.split(' ').length == 12)) {
final langName = PolyseedLang.getByPhrase(_wallet.seed!).nameEnglish;
if (_wallet.type == WalletType.monero) {
+15 -6
View File
@@ -49,6 +49,9 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
bool get hasLanguageSelector =>
[WalletType.monero, WalletType.haven, WalletType.wownero].contains(type);
bool get showLanguageSelector =>
newWalletArguments?.mnemonic == null && hasLanguageSelector;
int get seedPhraseWordsLength {
switch (type) {
case WalletType.monero:
@@ -81,7 +84,9 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
}
}
bool get hasSeedType => [WalletType.monero, WalletType.wownero].contains(type);
bool get hasSeedType =>
newWalletArguments?.mnemonic == null &&
[WalletType.monero, WalletType.wownero].contains(type);
@override
WalletCredentials getCredentials(dynamic _options) {
@@ -92,11 +97,15 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
switch (type) {
case WalletType.monero:
return monero!.createMoneroNewWalletCredentials(
name: name,
language: options!.first as String,
password: walletPassword,
passphrase: passphrase,
isPolyseed: options.last as bool);
name: name,
language: options!.first as String,
password: walletPassword,
passphrase: passphrase,
seedType: newWalletArguments!.mnemonic != null
? MoneroSeedType.bip39.raw
: (options.last as MoneroSeedType).raw,
mnemonic: newWalletArguments!.mnemonic,
);
case WalletType.bitcoin:
case WalletType.litecoin:
return bitcoin!.createBitcoinNewWalletCredentials(