diff --git a/lib/electrumx_rpc/cached_electrumx.dart b/lib/electrumx_rpc/cached_electrumx.dart index 91b7d1bc8..8366e259f 100644 --- a/lib/electrumx_rpc/cached_electrumx.dart +++ b/lib/electrumx_rpc/cached_electrumx.dart @@ -167,8 +167,8 @@ class CachedElectrumX { Set cachedSerials = _list == null ? {} : List.from(_list).toSet(); - // startNumber is broken currently - final startNumber = 0; // cachedSerials.length; + final startNumber = + cachedSerials.length - 10; // 10 being some arbitrary buffer final serials = await electrumXClient.getUsedCoinSerials( startNumber: startNumber, diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index a1fc66f21..152437c23 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -89,6 +89,11 @@ class _ConfirmTransactionViewState late final FocusNode _noteFocusNode; late final TextEditingController noteController; + + late final FocusNode _onChainNoteFocusNode; + late final TextEditingController onChainNoteController; + + Future _attemptSend(BuildContext context) async { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); @@ -138,6 +143,9 @@ class _ConfirmTransactionViewState txidFuture = (manager.wallet as FiroWallet) .confirmSendPublic(txData: transactionInfo); } else { + if (coin == Coin.epicCash) { + transactionInfo["onChainNote"] = onChainNoteController.text; + } txidFuture = manager.confirmSend(txData: transactionInfo); } } @@ -272,14 +280,21 @@ class _ConfirmTransactionViewState _noteFocusNode = FocusNode(); noteController = TextEditingController(); noteController.text = transactionInfo["note"] as String? ?? ""; + + _onChainNoteFocusNode = FocusNode(); + onChainNoteController = TextEditingController(); + onChainNoteController.text = transactionInfo["onChainNote"] as String? ?? ""; + super.initState(); } @override void dispose() { noteController.dispose(); + onChainNoteController.dispose(); _noteFocusNode.dispose(); + _onChainNoteFocusNode.dispose(); super.dispose(); } @@ -840,8 +855,64 @@ class _ConfirmTransactionViewState mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (coin == Coin.epicCash) + Text( + "On chain Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + if (coin == Coin.epicCash) + const SizedBox( + height: 8, + ), + if (coin == Coin.epicCash) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + maxLength: 256, + controller: onChainNoteController, + focusNode: _onChainNoteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _onChainNoteFocusNode, + context, + ).copyWith( + suffixIcon: onChainNoteController.text.isNotEmpty + ? Padding( + padding: + const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + onChainNoteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + if (coin == Coin.epicCash) + const SizedBox( + height: 12, + ), Text( - "Note (optional)", + (coin == Coin.epicCash) ? "Local Note (optional)" + : "Note (optional)", style: STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of(context) diff --git a/lib/pages/special/firo_rescan_recovery_error_dialog.dart b/lib/pages/special/firo_rescan_recovery_error_dialog.dart new file mode 100644 index 000000000..a543eb6bc --- /dev/null +++ b/lib/pages/special/firo_rescan_recovery_error_dialog.dart @@ -0,0 +1,307 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:tuple/tuple.dart'; + +enum FiroRescanRecoveryErrorViewOption { + retry, + showMnemonic, + deleteWallet; +} + +class FiroRescanRecoveryErrorView extends ConsumerStatefulWidget { + const FiroRescanRecoveryErrorView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/firoRescanRecoveryErrorView"; + + final String walletId; + + @override + ConsumerState createState() => + _FiroRescanRecoveryErrorViewState(); +} + +class _FiroRescanRecoveryErrorViewState + extends ConsumerState { + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => false, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) { + return DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + isCompactHeight: true, + // useSpacers: false, + trailing: Padding( + padding: const EdgeInsets.only(right: 16), + child: CustomTextButton( + text: "Delete wallet", + onTap: () async { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: DesktopDeleteWalletDialog.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: DesktopDeleteWalletDialog.routeName, + arguments: widget.walletId, + ), + ), + ]; + }, + ), + ); + + if (result == true) { + if (context.mounted) { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + } + } + }, + ), + ), + ), + body: SizedBox(width: 328, child: child), + ); + }, + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + automaticallyImplyLeading: false, + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + semanticsLabel: "Delete wallet button. " + "Start process of deleting current wallet.", + key: const Key("walletViewRadioButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension()! + .background, + icon: SvgPicture.asset( + Assets.svg.trash, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () async { + await showDialog( + barrierDismissible: true, + context: context, + builder: (_) => StackDialog( + title: + "Do you want to delete ${ref.read(walletsChangeNotifierProvider).getManager(widget.walletId).walletName}?", + leftButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: + widget.walletId, + showBackButton: true, + routeOnSuccess: + DeleteWalletWarningView.routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to delete wallet", + biometricsAuthenticationTitle: + "Delete wallet", + ), + settings: const RouteSettings( + name: "/deleteWalletLockscreen"), + ), + ); + }, + child: Text( + "Delete", + style: STextStyles.button(context), + ), + ), + ), + ); + }, + ), + ), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!Util.isDesktop) const Spacer(), + Text( + "Failed to rescan firo wallet", + style: STextStyles.pageTitleH2(context), + ), + Util.isDesktop + ? const SizedBox( + height: 60, + ) + : const Spacer(), + BranchedParent( + condition: Util.isDesktop, + conditionBranchBuilder: (children) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ), + otherBranchBuilder: (children) => Row( + children: [ + Expanded(child: children[0]), + children[1], + Expanded(child: children[2]), + ], + ), + children: [ + SecondaryButton( + label: "Show mnemonic", + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (Util.isDesktop) { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: UnlockWalletKeysDesktop.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: UnlockWalletKeysDesktop.routeName, + arguments: widget.walletId, + ), + ) + ]; + }, + ), + ); + } else { + final mnemonic = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + if (mounted) { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: + Tuple2(widget.walletId, mnemonic), + showBackButton: true, + routeOnSuccess: WalletBackupView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to view recovery phrase", + biometricsAuthenticationTitle: + "View recovery phrase", + ), + settings: const RouteSettings( + name: "/viewRecoverPhraseLockscreen"), + ), + ); + } + } + }, + ), + const SizedBox( + width: 16, + height: 16, + ), + PrimaryButton( + label: "Retry", + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: () { + Navigator.of(context).pop( + true, + ); + }, + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 33126e8ac..c9e391320 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -31,6 +31,7 @@ import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; +import 'package:stackwallet/pages/special/firo_rescan_recovery_error_dialog.dart'; import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary.dart'; @@ -119,6 +120,36 @@ class _WalletViewState extends ConsumerState { late StreamSubscription _nodeStatusSubscription; bool _rescanningOnOpen = false; + bool _lelantusRescanRecovery = false; + + Future _firoRescanRecovery() async { + final success = await (ref.read(managerProvider).wallet as FiroWallet) + .firoRescanRecovery(); + + if (success) { + // go into wallet + WidgetsBinding.instance.addPostFrameCallback( + (_) => setState(() { + _rescanningOnOpen = false; + _lelantusRescanRecovery = false; + }), + ); + } else { + // show error message dialog w/ options + if (mounted) { + final shouldRetry = await Navigator.of(context).pushNamed( + FiroRescanRecoveryErrorView.routeName, + arguments: walletId, + ); + + if (shouldRetry is bool && shouldRetry) { + await _firoRescanRecovery(); + } + } else { + return await _firoRescanRecovery(); + } + } + } @override void initState() { @@ -134,7 +165,14 @@ class _WalletViewState extends ConsumerState { _shouldDisableAutoSyncOnLogOut = false; } - if (ref.read(managerProvider).rescanOnOpenVersion == Constants.rescanV1) { + if (ref.read(managerProvider).coin == Coin.firo && + (ref.read(managerProvider).wallet as FiroWallet) + .lelantusCoinIsarRescanRequired) { + _rescanningOnOpen = true; + _lelantusRescanRecovery = true; + _firoRescanRecovery(); + } else if (ref.read(managerProvider).rescanOnOpenVersion == + Constants.rescanV1) { _rescanningOnOpen = true; ref.read(managerProvider).fullRescan(20, 1000).then( (_) => ref.read(managerProvider).resetRescanOnOpen().then( @@ -212,6 +250,10 @@ class _WalletViewState extends ConsumerState { DateTime? _cachedTime; Future _onWillPop() async { + if (_rescanningOnOpen || _lelantusRescanRecovery) { + return false; + } + final now = DateTime.now(); const timeout = Duration(milliseconds: 1500); if (_cachedTime == null || now.difference(_cachedTime!) > timeout) { @@ -434,33 +476,37 @@ class _WalletViewState extends ConsumerState { eventBus: null, textColor: Theme.of(context).extension()!.textDark, - actionButton: SecondaryButton( - label: "Cancel", - onPressed: () async { - await showDialog( - context: context, - builder: (context) => StackDialog( - title: "Warning!", - message: "Skipping this process can completely" - " break your wallet. It is only meant to be done in" - " emergency situations where the migration fails" - " and will not let you continue. Still skip?", - leftButton: SecondaryButton( - label: "Cancel", - onPressed: - Navigator.of(context, rootNavigator: true).pop, - ), - rightButton: SecondaryButton( - label: "Ok", - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - setState(() => _rescanningOnOpen = false); - }, - ), + actionButton: _lelantusRescanRecovery + ? null + : SecondaryButton( + label: "Cancel", + onPressed: () async { + await showDialog( + context: context, + builder: (context) => StackDialog( + title: "Warning!", + message: "Skipping this process can completely" + " break your wallet. It is only meant to be done in" + " emergency situations where the migration fails" + " and will not let you continue. Still skip?", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: + Navigator.of(context, rootNavigator: true) + .pop, + ), + rightButton: SecondaryButton( + label: "Ok", + onPressed: () { + Navigator.of(context, rootNavigator: true) + .pop(); + setState(() => _rescanningOnOpen = false); + }, + ), + ), + ); + }, ), - ); - }, - ), ), ) ], diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index a43b9eca3..0df83088b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -17,6 +17,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; +import 'package:stackwallet/pages/special/firo_rescan_recovery_error_dialog.dart'; import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; @@ -30,6 +31,7 @@ import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; @@ -78,6 +80,7 @@ class _DesktopWalletViewState extends ConsumerState { late final bool _shouldDisableAutoSyncOnLogOut; bool _rescanningOnOpen = false; + bool _lelantusRescanRecovery = false; Future onBackPressed() async { await _logout(); @@ -103,6 +106,38 @@ class _DesktopWalletViewState extends ConsumerState { ref.read(managerProvider.notifier).isActiveWallet = false; } + Future _firoRescanRecovery() async { + final success = await (ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as FiroWallet) + .firoRescanRecovery(); + + if (success) { + // go into wallet + WidgetsBinding.instance.addPostFrameCallback( + (_) => setState(() { + _rescanningOnOpen = false; + _lelantusRescanRecovery = false; + }), + ); + } else { + // show error message dialog w/ options + if (mounted) { + final shouldRetry = await Navigator.of(context).pushNamed( + FiroRescanRecoveryErrorView.routeName, + arguments: widget.walletId, + ); + + if (shouldRetry is bool && shouldRetry) { + await _firoRescanRecovery(); + } + } else { + return await _firoRescanRecovery(); + } + } + } + @override void initState() { controller = TextEditingController(); @@ -124,7 +159,13 @@ class _DesktopWalletViewState extends ConsumerState { _shouldDisableAutoSyncOnLogOut = false; } - if (ref.read(managerProvider).coin != Coin.ethereum && + if (ref.read(managerProvider).coin == Coin.firo && + (ref.read(managerProvider).wallet as FiroWallet) + .lelantusCoinIsarRescanRequired) { + _rescanningOnOpen = true; + _lelantusRescanRecovery = true; + _firoRescanRecovery(); + } else if (ref.read(managerProvider).coin != Coin.ethereum && ref.read(managerProvider).rescanOnOpenVersion == Constants.rescanV1) { _rescanningOnOpen = true; ref.read(managerProvider).fullRescan(20, 1000).then( @@ -172,83 +213,86 @@ class _DesktopWalletViewState extends ConsumerState { subMessage: "This only needs to run once per wallet", eventBus: null, textColor: Theme.of(context).extension()!.textDark, - actionButton: SecondaryButton( - label: "Skip", - buttonHeight: ButtonHeight.l, - onPressed: () async { - await showDialog( - context: context, - builder: (context) => DesktopDialog( - maxWidth: 500, - maxHeight: double.infinity, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + actionButton: _lelantusRescanRecovery + ? null + : SecondaryButton( + label: "Skip", + buttonHeight: ButtonHeight.l, + onPressed: () async { + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 500, + maxHeight: double.infinity, + child: Column( children: [ - Text( - "Warning!", - style: STextStyles.desktopH3(context), + Padding( + padding: const EdgeInsets.only(left: 32), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Warning!", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), ), - const DesktopDialogCloseButton(), - ], - ), - ), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 32), - child: Text( - "Skipping this process can completely" - " break your wallet. It is only meant to be done in" - " emergency situations where the migration fails" - " and will not let you continue. Still skip?", - style: STextStyles.desktopTextSmall(context), - ), - ), - const SizedBox( - height: 32, - ), - Padding( - padding: const EdgeInsets.all(32), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: ButtonHeight.l, - onPressed: Navigator.of(context, - rootNavigator: true) - .pop, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32), + child: Text( + "Skipping this process can completely" + " break your wallet. It is only meant to be done in" + " emergency situations where the migration fails" + " and will not let you continue. Still skip?", + style: + STextStyles.desktopTextSmall(context), ), ), const SizedBox( - width: 16, + height: 32, ), - Expanded( - child: PrimaryButton( - label: "Ok", - buttonHeight: ButtonHeight.l, - onPressed: () { - Navigator.of(context, - rootNavigator: true) - .pop(); - setState( - () => _rescanningOnOpen = false); - }, + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context, + rootNavigator: true) + .pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Ok", + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context, + rootNavigator: true) + .pop(); + setState(() => + _rescanningOnOpen = false); + }, + ), + ), + ], ), - ), + ) ], ), - ) - ], - ), + ), + ); + }, ), - ); - }, - ), ), ) ], diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 0488358a7..3d65eb6e2 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -105,6 +105,7 @@ class _DesktopSendState extends ConsumerState { final _baseFocus = FocusNode(); String? _note; + String? _onChainNote; Amount? _amountToSend; Amount? _cachedAmountToSend; @@ -354,6 +355,9 @@ class _DesktopSendState extends ConsumerState { } else { txData["address"] = _address; txData["note"] = _note ?? ""; + if (coin == Coin.epicCash) { + txData['onChainNote'] = _onChainNote ?? ""; + } } // pop building dialog Navigator.of( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 8916bb008..2b7aa662d 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -118,6 +118,7 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_set import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; +import 'package:stackwallet/pages/special/firo_rescan_recovery_error_dialog.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart'; import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; import 'package:stackwallet/pages/token_view/token_contract_details_view.dart'; @@ -264,6 +265,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case FiroRescanRecoveryErrorView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FiroRescanRecoveryErrorView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case WalletsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 573992318..a4cea6393 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -21,6 +21,7 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; import 'package:lelantus/lelantus.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; @@ -1895,14 +1896,44 @@ class FiroWallet extends CoinServiceAPI await Future.wait([ updateCachedId(walletId), updateCachedIsFavorite(false), + setLelantusCoinIsarRescanRequiredDone(), ]); } + static const String _lelantusCoinIsarRescanRequired = + "lelantusCoinIsarRescanRequired"; + + Future setLelantusCoinIsarRescanRequiredDone() async { + await DB.instance.put( + boxName: walletId, + key: _lelantusCoinIsarRescanRequired, + value: false, + ); + } + + bool get lelantusCoinIsarRescanRequired => + DB.instance.get( + boxName: walletId, + key: _lelantusCoinIsarRescanRequired, + ) as bool? ?? + true; + + Future firoRescanRecovery() async { + try { + await fullRescan(50, 1000); + await setLelantusCoinIsarRescanRequiredDone(); + return true; + } catch (_) { + return false; + } + } + @override Future initializeExisting() async { Logging.instance.log( - "initializeExisting() $_walletId ${coin.prettyName} wallet.", - level: LogLevel.Info); + "initializeExisting() $_walletId ${coin.prettyName} wallet.", + level: LogLevel.Info, + ); if (getCachedId() == null) { throw Exception( @@ -3601,6 +3632,81 @@ class FiroWallet extends CoinServiceAPI txnsData.add(Tuple2(tx, transactionAddress)); + // Master node payment ===================================== + } else if (inputList.length == 1 && + inputList.first["coinbase"] is String) { + List ins = [ + isar_models.Input( + txid: inputList.first["coinbase"] as String, + vout: -1, + scriptSig: null, + scriptSigAsm: null, + isCoinbase: true, + sequence: inputList.first['sequence'] as int?, + innerRedeemScriptAsm: null, + ), + ]; + + // parse outputs + List outs = []; + for (final output in outputList) { + // get value + final value = Amount.fromDecimal( + Decimal.parse(output["value"].toString()), + fractionDigits: coin.decimals, + ); + + // get output address + final address = output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]?["address"] as String?; + if (address != null) { + outputAddresses.add(address); + + // if output was to my wallet, add value to amount received + if (receivingAddresses.contains(address)) { + amountReceivedInWallet += value; + } + } + + outs.add( + isar_models.Output( + scriptPubKey: output['scriptPubKey']?['hex'] as String?, + scriptPubKeyAsm: output['scriptPubKey']?['asm'] as String?, + scriptPubKeyType: output['scriptPubKey']?['type'] as String?, + scriptPubKeyAddress: address ?? "", + value: value.raw.toInt(), + ), + ); + } + + // this is the address initially used to fetch the txid + isar_models.Address transactionAddress = + txObject["address"] as isar_models.Address; + + final tx = isar_models.Transaction( + walletId: walletId, + txid: txObject["txid"] as String, + timestamp: txObject["blocktime"] as int? ?? + (DateTime.now().millisecondsSinceEpoch ~/ 1000), + type: isar_models.TransactionType.incoming, + subType: isar_models.TransactionSubType.none, + // amount may overflow. Deprecated. Use amountString + amount: amountReceivedInWallet.raw.toInt(), + amountString: amountReceivedInWallet.toJsonString(), + fee: 0, + height: txObject["height"] as int?, + isCancelled: false, + isLelantus: false, + slateId: null, + otherData: null, + nonce: null, + inputs: ins, + outputs: outs, + numberOfMessages: null, + ); + + txnsData.add(Tuple2(tx, transactionAddress)); + // Assume non lelantus transaction ===================================== } else { // parse inputs @@ -4098,6 +4204,7 @@ class FiroWallet extends CoinServiceAPI maxNumberOfIndexesToCheck, false, ); + await setLelantusCoinIsarRescanRequiredDone(); await compute( _setTestnetWrapper, diff --git a/pubspec.yaml b/pubspec.yaml index e3fa11745..03eeea12e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.7.17+184 +version: 1.7.17+185 environment: sdk: ">=3.0.2 <4.0.0"