diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index f17ed06ff..91aa555f6 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -322,7 +322,7 @@ class _NameYourWalletViewState extends ConsumerState { ) : Semantics( label: - "Generate Random Wallet Name Button. Generates A Random Name For Wallet.", + "Clear Wallet Name Field Button. Clears the wallet name field.", excludeSemantics: true, child: XIcon( width: isDesktop ? 21 : 18, diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 384cacb0c..a7fe47d6e 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -10,8 +10,10 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:logger/logger.dart'; import 'package:tuple/tuple.dart'; import '../../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; @@ -20,6 +22,7 @@ import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/format.dart'; +import '../../../../utilities/logger.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; @@ -27,13 +30,16 @@ import '../../../../wallets/crypto_currency/interfaces/view_only_option_currency import '../../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart'; import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../../widgets/custom_buttons/checkbox_text_button.dart'; import '../../../../widgets/date_picker/date_picker.dart'; import '../../../../widgets/desktop/desktop_app_bar.dart'; import '../../../../widgets/desktop/desktop_scaffold.dart'; import '../../../../widgets/expandable.dart'; +import '../../../../widgets/icon_widgets/x_icon.dart'; import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_text_field.dart'; +import '../../../../widgets/textfield_icon_button.dart'; import '../../../../widgets/toggle.dart'; import '../../create_or_restore_wallet_view/sub_widgets/coin_image.dart'; import '../restore_view_only_wallet_view.dart'; @@ -44,6 +50,8 @@ import 'sub_widgets/restore_from_date_picker.dart'; import 'sub_widgets/restore_options_next_button.dart'; import 'sub_widgets/restore_options_platform_layout.dart'; +import 'package:cs_monero/src/deprecated/get_height_by_date.dart' as cs_monero_deprecated; + class RestoreOptionsView extends ConsumerStatefulWidget { const RestoreOptionsView({ super.key, @@ -66,6 +74,9 @@ class _RestoreOptionsViewState extends ConsumerState { late final bool isDesktop; late TextEditingController _dateController; + late TextEditingController _blockHeightController; + late FocusNode _blockHeightFocusNode; + final ValueNotifier _isUsingDateNotifier = ValueNotifier(true); late FocusNode textFieldFocusNode; late final FocusNode passwordFocusNode; late final TextEditingController passwordController; @@ -89,6 +100,9 @@ class _RestoreOptionsViewState extends ConsumerState { textFieldFocusNode = FocusNode(); passwordController = TextEditingController(); passwordFocusNode = FocusNode(); + _blockHeightController = TextEditingController(); + _blockHeightFocusNode = FocusNode(); + _isUsingDateNotifier.value = true; super.initState(); } @@ -96,6 +110,7 @@ class _RestoreOptionsViewState extends ConsumerState { @override void dispose() { _dateController.dispose(); + _blockHeightController.dispose(); textFieldFocusNode.dispose(); passwordController.dispose(); passwordFocusNode.dispose(); @@ -116,6 +131,12 @@ class _RestoreOptionsViewState extends ConsumerState { } if (mounted) { + int height = 0; + if (_isUsingDateNotifier.value) { + height = getBlockHeightFromDate(_restoreFromDate); + } else { + height = int.tryParse(_blockHeightController.text) ?? 0; + } if (!_showViewOnlyOption) { await Navigator.of(context).pushNamed( RestoreWalletView.routeName, @@ -123,7 +144,7 @@ class _RestoreOptionsViewState extends ConsumerState { walletName, coin, ref.read(mnemonicWordCountStateProvider.state).state, - _restoreFromDate, + height, passwordController.text, enableLelantusScanning, ), @@ -134,7 +155,7 @@ class _RestoreOptionsViewState extends ConsumerState { arguments: ( walletName: walletName, coin: coin, - restoreFromDate: _restoreFromDate, + restoreBlockHeight: height, enableLelantusScanning: enableLelantusScanning, ), ); @@ -186,6 +207,48 @@ class _RestoreOptionsViewState extends ConsumerState { ); } + int getBlockHeightFromDate(DateTime? date) { + try { + int height = 0; + if (date != null) { + if (widget.coin is Monero) { + height = cs_monero_deprecated.getMoneroHeightByDate( + date: date, + ); + } + if (widget.coin is Wownero) { + height = cs_monero_deprecated.getWowneroHeightByDate( + date: date, + ); + } + if (height < 0) { + height = 0; + } + + if (widget.coin is Epiccash) { + final int secondsSinceEpoch = + date.millisecondsSinceEpoch ~/ 1000; + const int epicCashFirstBlock = 1565370278; + const double overestimateSecondsPerBlock = 61; + final int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; + final int approximateHeight = + chosenSeconds ~/ overestimateSecondsPerBlock; + + height = approximateHeight; + if (height < 0) { + height = 0; + } + } + } else { + height = 0; + } + return height; + } catch (e) { + Logging.instance.log(Level.info, "Error getting block height from date: $e"); + return 0; + } + } + bool _showViewOnlyOption = false; @override @@ -281,10 +344,16 @@ class _RestoreOptionsViewState extends ConsumerState { dateController: _dateController, dateChooserFunction: isDesktop ? chooseDesktopDate : chooseDate, + blockHeightController: _blockHeightController, + blockHeightFocusNode: _blockHeightFocusNode, + isUsingDateNotifier: _isUsingDateNotifier, ) : SeedRestoreOption( coin: coin, dateController: _dateController, + blockHeightController: _blockHeightController, + blockHeightFocusNode: _blockHeightFocusNode, + isUsingDateNotifier: _isUsingDateNotifier, pwController: passwordController, pwFocusNode: passwordFocusNode, supportsMnemonicPassphrase: supportsMnemonicPassphrase, @@ -324,6 +393,9 @@ class SeedRestoreOption extends ConsumerStatefulWidget { super.key, required this.coin, required this.dateController, + required this.blockHeightController, + required this.blockHeightFocusNode, + required this.isUsingDateNotifier, required this.pwController, required this.pwFocusNode, required this.supportsMnemonicPassphrase, @@ -334,6 +406,9 @@ class SeedRestoreOption extends ConsumerStatefulWidget { final CryptoCurrency coin; final TextEditingController dateController; + final TextEditingController blockHeightController; + final FocusNode blockHeightFocusNode; + final ValueNotifier isUsingDateNotifier; final TextEditingController pwController; final FocusNode pwFocusNode; final bool supportsMnemonicPassphrase; @@ -350,6 +425,7 @@ class _SeedRestoreOptionState extends ConsumerState { bool _hidePassword = true; bool _expandedAdvanced = false; bool _enableLelantusScanning = false; + bool _blockFieldEmpty = true; @override Widget build(BuildContext context) { @@ -363,25 +439,94 @@ class _SeedRestoreOptionState extends ConsumerState { return Column( children: [ if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) - Text( - "Choose start date", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ) - : STextStyles.smallMed12(context), - textAlign: TextAlign.left, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.isUsingDateNotifier.value + ? "Choose start date" + : "Block height", + style: Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: widget.isUsingDateNotifier.value + ? "Use block height" + : "Use date", + onTap: () { + setState(() { + widget.isUsingDateNotifier.value = + !widget.isUsingDateNotifier.value; + }); + }, + ), + ], ), if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) SizedBox( height: Util.isDesktop ? 16 : 8, ), if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) + widget.isUsingDateNotifier.value ? RestoreFromDatePicker( onTap: widget.dateChooserFunction, controller: widget.dateController, - ), + ) : + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + focusNode: widget.blockHeightFocusNode, + controller: widget.blockHeightController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + textInputAction: TextInputAction.done, + style: Util.isDesktop + ? STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ) + : STextStyles.field(context), + onChanged: (value) { + setState(() { + _blockFieldEmpty = value.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Start scanning from...", + widget.blockHeightFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: TextFieldIconButton(child: + Semantics( + label: "Clear Block Height Field Button. Clears the block height field", + excludeSemantics: true, + child: !_blockFieldEmpty + ? XIcon( + width: Util.isDesktop ? 24 : 16, + height: Util.isDesktop ? 24 : 16, + ) + : const SizedBox.shrink(), + ), + onTap: () { + widget.blockHeightController.text = ""; + setState(() { + _blockFieldEmpty = true; + }); + }, + ), + ), + ), + ), + ), if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) const SizedBox( height: 8, @@ -390,7 +535,9 @@ class _SeedRestoreOptionState extends ConsumerState { RoundedWhiteContainer( child: Center( child: Text( - "Choose the date you made the wallet (approximate is fine)", + widget.isUsingDateNotifier.value + ? "Choose the date you made the wallet (approximate is fine)" + : "Enter the initial block height of the wallet", style: Util.isDesktop ? STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of(context) @@ -635,6 +782,13 @@ class _SeedRestoreOptionState extends ConsumerState { ], ); } + + @override + void initState() { + super.initState(); + + _blockFieldEmpty = widget.blockHeightController.text.isEmpty; + } } class ViewOnlyRestoreOption extends StatefulWidget { @@ -643,10 +797,16 @@ class ViewOnlyRestoreOption extends StatefulWidget { required this.coin, required this.dateController, required this.dateChooserFunction, + required this.blockHeightController, + required this.blockHeightFocusNode, + required this.isUsingDateNotifier, }); final CryptoCurrency coin; final TextEditingController dateController; + final TextEditingController blockHeightController; + final FocusNode blockHeightFocusNode; + final ValueNotifier isUsingDateNotifier; final Future Function() dateChooserFunction; @@ -655,30 +815,102 @@ class ViewOnlyRestoreOption extends StatefulWidget { } class _ViewOnlyRestoreOptionState extends State { + bool _blockFieldEmpty = true; + @override Widget build(BuildContext context) { final showDateOption = widget.coin is CryptonoteCurrency; return Column( children: [ if (showDateOption) - Text( - "Choose start date", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ) - : STextStyles.smallMed12(context), - textAlign: TextAlign.left, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.isUsingDateNotifier.value + ? "Choose start date" + : "Block height", + style: Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: widget.isUsingDateNotifier.value + ? "Use block height" + : "Use date", + onTap: () { + setState(() { + widget.isUsingDateNotifier.value = + !widget.isUsingDateNotifier.value; + }); + }, + ), + ], ), if (showDateOption) SizedBox( height: Util.isDesktop ? 16 : 8, ), if (showDateOption) - RestoreFromDatePicker( - onTap: widget.dateChooserFunction, - controller: widget.dateController, + widget.isUsingDateNotifier.value + ? RestoreFromDatePicker( + onTap: widget.dateChooserFunction, + controller: widget.dateController, + ) + : ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + focusNode: widget.blockHeightFocusNode, + controller: widget.blockHeightController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + textInputAction: TextInputAction.done, + style: Util.isDesktop + ? STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ) + : STextStyles.field(context), + onChanged: (value) { + setState(() { + _blockFieldEmpty = value.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Start scanning from...", + widget.blockHeightFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: TextFieldIconButton( + child: Semantics( + label: + "Clear Block Height Field Button. Clears the block height field", + excludeSemantics: true, + child: !_blockFieldEmpty + ? XIcon( + width: Util.isDesktop ? 24 : 16, + height: Util.isDesktop ? 24 : 16, + ) + : const SizedBox.shrink(), + ), + onTap: () { + widget.blockHeightController.text = ""; + setState(() { + _blockFieldEmpty = true; + }); + }, + ), + ), + ), + ), ), if (showDateOption) const SizedBox( @@ -688,7 +920,9 @@ class _ViewOnlyRestoreOptionState extends State { RoundedWhiteContainer( child: Center( child: Text( - "Choose the date you made the wallet (approximate is fine)", + widget.isUsingDateNotifier.value + ? "Choose the date you made the wallet (approximate is fine)" + : "Enter the initial block height of the wallet", style: Util.isDesktop ? STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of(context) @@ -708,4 +942,11 @@ class _ViewOnlyRestoreOptionState extends State { ], ); } + + @override + void initState() { + super.initState(); + + _blockFieldEmpty = widget.blockHeightController.text.isEmpty; + } } diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart index 9123b2f46..b4b7d03e3 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart @@ -51,7 +51,7 @@ class RestoreViewOnlyWalletView extends ConsumerStatefulWidget { super.key, required this.walletName, required this.coin, - required this.restoreFromDate, + required this.restoreBlockHeight, this.enableLelantusScanning = false, this.barcodeScanner = const BarcodeScannerWrapper(), this.clipboard = const ClipboardWrapper(), @@ -61,7 +61,7 @@ class RestoreViewOnlyWalletView extends ConsumerStatefulWidget { final String walletName; final CryptoCurrency coin; - final DateTime? restoreFromDate; + final int restoreBlockHeight; final bool enableLelantusScanning; final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @@ -114,7 +114,6 @@ class _RestoreViewOnlyWalletViewState } Future _attemptRestore() async { - int height = 0; final Map otherDataJson = { WalletInfoKeys.isViewOnlyKey: true, }; @@ -134,20 +133,6 @@ class _RestoreViewOnlyWalletViewState ? ViewOnlyWalletType.addressOnly : ViewOnlyWalletType.xPub; } else if (widget.coin is CryptonoteCurrency) { - if (widget.restoreFromDate != null) { - if (widget.coin is Monero) { - height = cs_monero_deprecated.getMoneroHeightByDate( - date: widget.restoreFromDate!, - ); - } - if (widget.coin is Wownero) { - height = cs_monero_deprecated.getWowneroHeightByDate( - date: widget.restoreFromDate!, - ); - } - if (height < 0) height = 0; - } - viewOnlyWalletType = ViewOnlyWalletType.cryptonote; } else { throw Exception( @@ -163,7 +148,7 @@ class _RestoreViewOnlyWalletViewState final info = WalletInfo.createNew( coin: widget.coin, name: widget.walletName, - restoreHeight: height, + restoreHeight: widget.restoreBlockHeight, otherDataJsonString: jsonEncode(otherDataJson), ); diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index 972012c00..59d479112 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -17,8 +17,6 @@ import 'dart:math'; import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/src/wordlists/english.dart' as bip39wordlist; import 'package:cs_monero/cs_monero.dart' as lib_monero; -import 'package:cs_monero/src/deprecated/get_height_by_date.dart' - as cs_monero_deprecated; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -79,7 +77,7 @@ class RestoreWalletView extends ConsumerStatefulWidget { required this.coin, required this.seedWordsLength, required this.mnemonicPassphrase, - required this.restoreFromDate, + required this.restoreBlockHeight, this.enableLelantusScanning = false, this.barcodeScanner = const BarcodeScannerWrapper(), this.clipboard = const ClipboardWrapper(), @@ -91,7 +89,7 @@ class RestoreWalletView extends ConsumerStatefulWidget { final CryptoCurrency coin; final String mnemonicPassphrase; final int seedWordsLength; - final DateTime? restoreFromDate; + final int restoreBlockHeight; final bool enableLelantusScanning; final BarcodeScannerInterface barcodeScanner; @@ -233,42 +231,11 @@ class _RestoreWalletViewState extends ConsumerState { } mnemonic = mnemonic.trim(); - int height = 0; + final int height = widget.restoreBlockHeight; String? otherDataJsonString; - if (widget.restoreFromDate != null) { - if (widget.coin is Monero) { - height = cs_monero_deprecated.getMoneroHeightByDate( - date: widget.restoreFromDate!, - ); - } - if (widget.coin is Wownero) { - height = cs_monero_deprecated.getWowneroHeightByDate( - date: widget.restoreFromDate!, - ); - } - if (height < 0) { - height = 0; - } - } - // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index if (widget.coin is Epiccash) { - if (widget.restoreFromDate != null) { - final int secondsSinceEpoch = - widget.restoreFromDate!.millisecondsSinceEpoch ~/ 1000; - const int epicCashFirstBlock = 1565370278; - const double overestimateSecondsPerBlock = 61; - final int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; - final int approximateHeight = - chosenSeconds ~/ overestimateSecondsPerBlock; - - height = approximateHeight; - } - if (height < 0) { - height = 0; - } - otherDataJsonString = jsonEncode( { WalletInfoKeys.epiccashData: jsonEncode( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 6695a53b8..eaa8c1c06 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -1554,14 +1554,14 @@ class RouteGenerator { case RestoreWalletView.routeName: if (args - is Tuple6) { + is Tuple6) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => RestoreWalletView( walletName: args.item1, coin: args.item2, seedWordsLength: args.item3, - restoreFromDate: args.item4, + restoreBlockHeight: args.item4, mnemonicPassphrase: args.item5, enableLelantusScanning: args.item6 ?? false, ), @@ -1576,7 +1576,7 @@ class RouteGenerator { if (args is ({ String walletName, CryptoCurrency coin, - DateTime? restoreFromDate, + int restoreBlockHeight, bool enableLelantusScanning, })) { return getRoute( @@ -1584,7 +1584,7 @@ class RouteGenerator { builder: (_) => RestoreViewOnlyWalletView( walletName: args.walletName, coin: args.coin, - restoreFromDate: args.restoreFromDate, + restoreBlockHeight: args.restoreBlockHeight, enableLelantusScanning: args.enableLelantusScanning, ), settings: RouteSettings(