mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-19 05:58:50 +02:00
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:
co-authored by
cyan
parent
494207290e
commit
f58a5fb8fd
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user