From 71609c34b0d9c106444e1ec8627903432fd3ca95 Mon Sep 17 00:00:00 2001
From: julian <julian@cypherstack.com>
Date: Tue, 10 Dec 2024 14:51:19 -0600
Subject: [PATCH] feat: allow setting the restore/refresh height in xmr/wow
 wallets

---
 .../edit_refresh_height_view.dart             | 238 ++++++++++++++++++
 .../wallet_settings_wallet_settings_view.dart |  38 +++
 .../sub_widgets/wallet_options_button.dart    |  70 +++++-
 lib/route_generator.dart                      |  15 ++
 4 files changed, 360 insertions(+), 1 deletion(-)
 create mode 100644 lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart

diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart
new file mode 100644
index 000000000..61d497452
--- /dev/null
+++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart
@@ -0,0 +1,238 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../../../../notifications/show_flush_bar.dart';
+import '../../../../providers/db/main_db_provider.dart';
+import '../../../../providers/global/wallets_provider.dart';
+import '../../../../themes/stack_colors.dart';
+import '../../../../utilities/constants.dart';
+import '../../../../utilities/text_styles.dart';
+import '../../../../utilities/util.dart';
+import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
+import '../../../../widgets/background.dart';
+import '../../../../widgets/conditional_parent.dart';
+import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
+import '../../../../widgets/desktop/desktop_dialog.dart';
+import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
+import '../../../../widgets/desktop/primary_button.dart';
+import '../../../../widgets/icon_widgets/x_icon.dart';
+import '../../../../widgets/stack_text_field.dart';
+import '../../../../widgets/textfield_icon_button.dart';
+
+class EditRefreshHeightView extends ConsumerStatefulWidget {
+  const EditRefreshHeightView({
+    super.key,
+    required this.walletId,
+  });
+
+  static const String routeName = "/editRefreshHeightView";
+
+  final String walletId;
+
+  @override
+  ConsumerState<EditRefreshHeightView> createState() =>
+      _EditRefreshHeightViewState();
+}
+
+class _EditRefreshHeightViewState extends ConsumerState<EditRefreshHeightView> {
+  late final LibMoneroWallet _wallet;
+  late final TextEditingController _controller;
+  final _focusNode = FocusNode();
+
+  bool _saveLock = false;
+
+  void _save() async {
+    if (_saveLock) return;
+    _saveLock = true;
+    try {
+      String? errMessage;
+      try {
+        final newHeight = int.tryParse(_controller.text);
+        if (newHeight != null && newHeight >= 0) {
+          await _wallet.info.updateRestoreHeight(
+            newRestoreHeight: newHeight,
+            isar: ref.read(mainDBProvider).isar,
+          );
+          _wallet.libMoneroWallet!.setRefreshFromBlockHeight(newHeight);
+        } else {
+          errMessage = "Invalid height: ${_controller.text}";
+        }
+      } catch (e) {
+        errMessage = e.toString();
+      }
+
+      if (mounted) {
+        if (errMessage == null) {
+          Navigator.of(context).pop();
+          unawaited(
+            showFloatingFlushBar(
+              type: FlushBarType.success,
+              message: "Refresh height updated",
+              context: context,
+            ),
+          );
+        } else {
+          unawaited(
+            showFloatingFlushBar(
+              type: FlushBarType.warning,
+              message: errMessage,
+              context: context,
+            ),
+          );
+        }
+      }
+    } finally {
+      _saveLock = false;
+    }
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _wallet = ref.read(pWallets).getWallet(widget.walletId) as LibMoneroWallet;
+    _controller = TextEditingController()
+      ..text = _wallet.libMoneroWallet!.getRefreshFromBlockHeight().toString();
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    _focusNode.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ConditionalParent(
+      condition: Util.isDesktop,
+      builder: (child) {
+        return DesktopDialog(
+          maxWidth: 500,
+          maxHeight: double.infinity,
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            crossAxisAlignment: CrossAxisAlignment.stretch,
+            children: [
+              Row(
+                mainAxisAlignment: MainAxisAlignment.end,
+                children: [
+                  DesktopDialogCloseButton(
+                    onPressedOverride: Navigator.of(
+                      context,
+                      rootNavigator: true,
+                    ).pop,
+                  ),
+                ],
+              ),
+              Padding(
+                padding: const EdgeInsets.symmetric(horizontal: 32),
+                child: child,
+              ),
+              const SizedBox(
+                height: 32,
+              ),
+            ],
+          ),
+        );
+      },
+      child: ConditionalParent(
+        condition: !Util.isDesktop,
+        builder: (child) {
+          return Background(
+            child: Scaffold(
+              backgroundColor:
+                  Theme.of(context).extension<StackColors>()!.background,
+              appBar: AppBar(
+                leading: AppBarBackButton(
+                  onPressed: () {
+                    Navigator.of(context).pop();
+                  },
+                ),
+                title: Text(
+                  "Restore height",
+                  style: STextStyles.navBarTitle(context),
+                ),
+              ),
+              body: Padding(
+                padding: const EdgeInsets.all(16),
+                child: child,
+              ),
+            ),
+          );
+        },
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: [
+            ClipRRect(
+              borderRadius: BorderRadius.circular(
+                Constants.size.circularBorderRadius,
+              ),
+              child: TextField(
+                key: const Key("restoreHeightFieldKey"),
+                controller: _controller,
+                focusNode: _focusNode,
+                style: Util.isDesktop
+                    ? STextStyles.desktopTextMedium(context).copyWith(
+                        height: 2,
+                      )
+                    : STextStyles.field(context),
+                enableSuggestions: false,
+                autocorrect: false,
+                autofocus: true,
+                onSubmitted: (_) => _save(),
+                onChanged: (_) => setState(() {}),
+                decoration: standardInputDecoration(
+                  "Restore height",
+                  _focusNode,
+                  context,
+                ).copyWith(
+                  suffixIcon: _controller.text.isNotEmpty
+                      ? Padding(
+                          padding: const EdgeInsets.only(right: 0),
+                          child: UnconstrainedBox(
+                            child: ConditionalParent(
+                              condition: Util.isDesktop,
+                              builder: (child) => SizedBox(
+                                height: 70,
+                                child: child,
+                              ),
+                              child: Row(
+                                children: [
+                                  TextFieldIconButton(
+                                    child: const XIcon(),
+                                    onTap: () async {
+                                      setState(() {
+                                        _controller.text = "";
+                                      });
+                                    },
+                                  ),
+                                ],
+                              ),
+                            ),
+                          ),
+                        )
+                      : Util.isDesktop
+                          ? const SizedBox(
+                              height: 70,
+                            )
+                          : null,
+                ),
+              ),
+            ),
+            Util.isDesktop
+                ? const SizedBox(
+                    height: 32,
+                  )
+                : const Spacer(),
+            PrimaryButton(
+              label: "Save",
+              onPressed: _save,
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart
index bf41d6261..c689baf30 100644
--- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart
+++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart
@@ -20,6 +20,7 @@ import '../../../../utilities/constants.dart';
 import '../../../../utilities/text_styles.dart';
 import '../../../../wallets/isar/models/wallet_info.dart';
 import '../../../../wallets/isar/providers/wallet_info_provider.dart';
+import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
 import '../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart';
 import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
 import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
@@ -32,6 +33,7 @@ import '../../../../widgets/rounded_white_container.dart';
 import '../../../../widgets/stack_dialog.dart';
 import '../../../pinpad_views/lock_screen_view.dart';
 import 'delete_wallet_warning_view.dart';
+import 'edit_refresh_height_view.dart';
 import 'lelantus_settings_view.dart';
 import 'rbf_settings_view.dart';
 import 'rename_wallet_view.dart';
@@ -354,6 +356,42 @@ class _WalletSettingsWalletSettingsViewState
                       ),
                     ),
                   ),
+                if (wallet is LibMoneroWallet)
+                  const SizedBox(
+                    height: 8,
+                  ),
+                if (wallet is LibMoneroWallet)
+                  RoundedWhiteContainer(
+                    padding: const EdgeInsets.all(0),
+                    child: RawMaterialButton(
+                      shape: RoundedRectangleBorder(
+                        borderRadius: BorderRadius.circular(
+                          Constants.size.circularBorderRadius,
+                        ),
+                      ),
+                      materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+                      onPressed: () {
+                        Navigator.of(context).pushNamed(
+                          EditRefreshHeightView.routeName,
+                          arguments: widget.walletId,
+                        );
+                      },
+                      child: Padding(
+                        padding: const EdgeInsets.symmetric(
+                          horizontal: 12.0,
+                          vertical: 20,
+                        ),
+                        child: Row(
+                          children: [
+                            Text(
+                              "Restore height",
+                              style: STextStyles.titleBold12(context),
+                            ),
+                          ],
+                        ),
+                      ),
+                    ),
+                  ),
                 const SizedBox(
                   height: 8,
                 ),
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart
index e0da6d5e9..f0756370f 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart
@@ -17,6 +17,7 @@ import 'package:flutter_svg/svg.dart';
 
 import '../../../../pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart';
 import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
+import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart';
 import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
 import '../../../../providers/global/wallets_provider.dart';
 import '../../../../route_generator.dart';
@@ -30,6 +31,7 @@ import '../../../../wallets/crypto_currency/coins/firo.dart';
 import '../../../../wallets/crypto_currency/intermediate/frost_currency.dart';
 import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart';
 import '../../../../wallets/isar/providers/wallet_info_provider.dart';
+import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
 import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
 import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
 import '../../../addresses/desktop_wallet_addresses_view.dart';
@@ -44,7 +46,8 @@ enum _WalletOptions {
   showXpub,
   lelantusCoins,
   sparkCoins,
-  frostOptions;
+  frostOptions,
+  refreshFromHeight;
 
   String get prettyName {
     switch (this) {
@@ -62,6 +65,8 @@ enum _WalletOptions {
         return "Spark Coins";
       case _WalletOptions.frostOptions:
         return "FROST settings";
+      case _WalletOptions.refreshFromHeight:
+        return "Refresh height";
     }
   }
 }
@@ -111,6 +116,9 @@ class WalletOptionsButton extends ConsumerWidget {
               onFrostMSWalletOptionsPressed: () async {
                 Navigator.of(context).pop(_WalletOptions.frostOptions);
               },
+              onRefreshHeightPressed: () async {
+                Navigator.of(context).pop(_WalletOptions.refreshFromHeight);
+              },
               walletId: walletId,
             );
           },
@@ -243,6 +251,26 @@ class WalletOptionsButton extends ConsumerWidget {
                 ),
               );
               break;
+
+            case _WalletOptions.refreshFromHeight:
+              if (Util.isDesktop) {
+                unawaited(
+                  showDialog(
+                    context: context,
+                    builder: (context) => EditRefreshHeightView(
+                      walletId: walletId,
+                    ),
+                  ),
+                );
+              } else {
+                unawaited(
+                  Navigator.of(context).pushNamed(
+                    EditRefreshHeightView.routeName,
+                    arguments: walletId,
+                  ),
+                );
+              }
+              break;
           }
         }
       },
@@ -278,6 +306,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
     required this.onFiroShowLelantusCoins,
     required this.onFiroShowSparkCoins,
     required this.onFrostMSWalletOptionsPressed,
+    required this.onRefreshHeightPressed,
     required this.walletId,
   });
 
@@ -288,6 +317,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
   final VoidCallback onFiroShowLelantusCoins;
   final VoidCallback onFiroShowSparkCoins;
   final VoidCallback onFrostMSWalletOptionsPressed;
+  final VoidCallback onRefreshHeightPressed;
   final String walletId;
 
   @override
@@ -307,6 +337,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
     final bool canChangeRep = coin is NanoCurrency;
 
     final bool isFrost = coin is FrostCurrency;
+    final bool isMoneroWow = wallet is LibMoneroWallet;
 
     return Stack(
       children: [
@@ -509,6 +540,43 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
                         ),
                       ),
                     ),
+                  if (isMoneroWow)
+                    const SizedBox(
+                      height: 8,
+                    ),
+                  if (isMoneroWow)
+                    TransparentButton(
+                      onPressed: onRefreshHeightPressed,
+                      child: Padding(
+                        padding: const EdgeInsets.all(8),
+                        child: Row(
+                          mainAxisAlignment: MainAxisAlignment.start,
+                          children: [
+                            SvgPicture.asset(
+                              Assets.svg.addressBookDesktop,
+                              width: 20,
+                              height: 20,
+                              color: Theme.of(context)
+                                  .extension<StackColors>()!
+                                  .textFieldActiveSearchIconLeft,
+                            ),
+                            const SizedBox(width: 14),
+                            Expanded(
+                              child: Text(
+                                _WalletOptions.refreshFromHeight.prettyName,
+                                style: STextStyles.desktopTextExtraExtraSmall(
+                                  context,
+                                ).copyWith(
+                                  color: Theme.of(context)
+                                      .extension<StackColors>()!
+                                      .textDark,
+                                ),
+                              ),
+                            ),
+                          ],
+                        ),
+                      ),
+                    ),
                   if (xpubEnabled)
                     const SizedBox(
                       height: 8,
diff --git a/lib/route_generator.dart b/lib/route_generator.dart
index 52454fb29..bcfffa8cd 100644
--- a/lib/route_generator.dart
+++ b/lib/route_generator.dart
@@ -135,6 +135,7 @@ import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_setting
 import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_view_only_wallet_keys_view.dart';
 import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart';
 import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart';
+import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart';
 import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/lelantus_settings_view.dart';
 import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rbf_settings_view.dart';
 import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart';
@@ -2140,6 +2141,20 @@ class RouteGenerator {
         }
         return _routeError("${settings.name} invalid args: ${args.toString()}");
 
+      case EditRefreshHeightView.routeName:
+        if (args is String) {
+          return getRoute(
+            shouldUseMaterialRoute: useMaterialPageRoute,
+            builder: (_) => EditRefreshHeightView(
+              walletId: args,
+            ),
+            settings: RouteSettings(
+              name: settings.name,
+            ),
+          );
+        }
+        return _routeError("${settings.name} invalid args: ${args.toString()}");
+
       // == Desktop specific routes ============================================
       case CreatePasswordView.routeName:
         if (args is bool) {