From 4300526661e6ef31c1416564a86ed335b408e1e8 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 21 Oct 2025 12:19:04 -0600 Subject: [PATCH 01/50] dart format --- .../create_backup_view.dart | 372 +++++++++--------- 1 file changed, 181 insertions(+), 191 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index a04f0de247..57a8017733 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -117,8 +117,9 @@ class _RestoreFromFileViewState extends State { builder: (child) { return Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -168,12 +169,12 @@ class _RestoreFromFileViewState extends State { padding: const EdgeInsets.only(bottom: 10), child: Text( "Choose file location", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ), + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, + ), ), ), child, @@ -191,31 +192,26 @@ class _RestoreFromFileViewState extends State { child: TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: - Platform.isAndroid || Platform.isIOS - ? null - : () async { - try { - await stackFileSystem.prepareStorage(); - - if (mounted) { - await stackFileSystem.pickDir(context); - } + onTap: Platform.isAndroid || Platform.isIOS + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); - } - } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); + if (mounted) { + await stackFileSystem.pickDir(context); } - }, + + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance.e("", error: e, stackTrace: s); + } + }, controller: fileLocationController, style: STextStyles.field(context), decoration: InputDecoration( @@ -227,10 +223,9 @@ class _RestoreFromFileViewState extends State { const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: - Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), @@ -263,12 +258,12 @@ class _RestoreFromFileViewState extends State { padding: const EdgeInsets.only(bottom: 10.0), child: Text( "Create a passphrase", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ), + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, + ), textAlign: TextAlign.left, ), ), @@ -284,41 +279,44 @@ class _RestoreFromFileViewState extends State { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "Create passphrase", - passwordFocusNode, - context, - ).copyWith( - labelStyle: - isDesktop ? STextStyles.fieldLabel(context) : null, - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox(width: 16), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: - Theme.of( + decoration: + standardInputDecoration( + "Create passphrase", + passwordFocusNode, + context, + ).copyWith( + labelStyle: isDesktop + ? STextStyles.fieldLabel(context) + : null, + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of( context, ).extension()!.textDark3, - width: 16, - height: 16, - ), + width: 16, + height: 16, + ), + ), + const SizedBox(width: 12), + ], ), - const SizedBox(width: 12), - ], + ), ), - ), - ), onChanged: (newValue) { if (newValue.isEmpty) { setState(() { @@ -365,13 +363,12 @@ class _RestoreFromFileViewState extends State { right: 12, top: passwordFeedback.isNotEmpty ? 4 : 0, ), - child: - passwordFeedback.isNotEmpty - ? Text( - passwordFeedback, - style: STextStyles.infoSmall(context), - ) - : null, + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), + ) + : null, ), if (passwordFocusNode.hasFocus || passwordRepeatFocusNode.hasFocus || @@ -382,22 +379,20 @@ class _RestoreFromFileViewState extends State { key: const Key("createStackBackUpProgressBar"), width: MediaQuery.of(context).size.width - 32 - 24, height: 5, - fillColor: - passwordStrength < 0.51 - ? Theme.of( - context, - ).extension()!.accentColorRed - : passwordStrength < 1 - ? Theme.of( - context, - ).extension()!.accentColorYellow - : Theme.of( - context, - ).extension()!.accentColorGreen, - backgroundColor: - Theme.of( - context, - ).extension()!.buttonBackSecondary, + fillColor: passwordStrength < 0.51 + ? Theme.of( + context, + ).extension()!.accentColorRed + : passwordStrength < 1 + ? Theme.of( + context, + ).extension()!.accentColorYellow + : Theme.of( + context, + ).extension()!.accentColorGreen, + backgroundColor: Theme.of( + context, + ).extension()!.buttonBackSecondary, percent: passwordStrength < 0.25 ? 0.03 : passwordStrength, ), ), @@ -414,41 +409,44 @@ class _RestoreFromFileViewState extends State { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "Confirm passphrase", - passwordRepeatFocusNode, - context, - ).copyWith( - labelStyle: - isDesktop ? STextStyles.fieldLabel(context) : null, - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox(width: 16), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: - Theme.of( + decoration: + standardInputDecoration( + "Confirm passphrase", + passwordRepeatFocusNode, + context, + ).copyWith( + labelStyle: isDesktop + ? STextStyles.fieldLabel(context) + : null, + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of( context, ).extension()!.textDark3, - width: 16, - height: 16, - ), + width: 16, + height: 16, + ), + ), + const SizedBox(width: 12), + ], ), - const SizedBox(width: 12), - ], + ), ), - ), - ), onChanged: (newValue) { setState(() {}); // TODO: ? check if passwords match? @@ -459,20 +457,18 @@ class _RestoreFromFileViewState extends State { if (!isDesktop) const Spacer(), !isDesktop ? Consumer( - builder: (context, ref, __) { - return TextButton( - style: - shouldEnableCreate - ? Theme.of(context) + builder: (context, ref, __) { + return TextButton( + style: shouldEnableCreate + ? Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) + : Theme.of(context) .extension()! .getPrimaryDisabledButtonStyle(context), - onPressed: - !shouldEnableCreate - ? null - : () async { + onPressed: !shouldEnableCreate + ? null + : () async { final String pathToSave = fileLocationController.text; final String passphrase = @@ -525,11 +521,10 @@ class _RestoreFromFileViewState extends State { showDialog( context: context, barrierDismissible: false, - builder: - (_) => const StackDialog( - title: "Encrypting backup", - message: "This shouldn't take long", - ), + builder: (_) => const StackDialog( + title: "Encrypting backup", + message: "This shouldn't take long", + ), ), ); // make sure the dialog is able to be displayed for at least 1 second @@ -560,17 +555,15 @@ class _RestoreFromFileViewState extends State { await showDialog( context: context, barrierDismissible: false, - builder: - (_) => - Platform.isAndroid - ? StackOkDialog( - title: "Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: - "Backup creation succeeded", - ), + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: "Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: + "Backup creation succeeded", + ), ); passwordController.text = ""; passwordRepeatController.text = ""; @@ -579,34 +572,32 @@ class _RestoreFromFileViewState extends State { await showDialog( context: context, barrierDismissible: false, - builder: - (_) => const StackOkDialog( - title: "Backup creation failed", - ), + builder: (_) => const StackOkDialog( + title: "Backup creation failed", + ), ); } } }, - child: Text( - "Create backup", - style: STextStyles.button(context), - ), - ); - }, - ) + child: Text( + "Create backup", + style: STextStyles.button(context), + ), + ); + }, + ) : Row( - children: [ - Consumer( - builder: (context, ref, __) { - return PrimaryButton( - width: 183, - buttonHeight: ButtonHeight.m, - label: "Create backup", - enabled: shouldEnableCreate, - onPressed: - !shouldEnableCreate - ? null - : () async { + children: [ + Consumer( + builder: (context, ref, __) { + return PrimaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Create backup", + enabled: shouldEnableCreate, + onPressed: !shouldEnableCreate + ? null + : () async { final String pathToSave = fileLocationController.text; final String passphrase = @@ -831,26 +822,25 @@ class _RestoreFromFileViewState extends State { await showDialog( context: context, barrierDismissible: false, - builder: - (_) => const StackOkDialog( - title: "Backup creation failed", - ), + builder: (_) => const StackOkDialog( + title: "Backup creation failed", + ), ); } } }, - ); - }, - ), - const SizedBox(width: 16), - SecondaryButton( - width: 183, - buttonHeight: ButtonHeight.m, - label: "Cancel", - onPressed: () {}, - ), - ], - ), + ); + }, + ), + const SizedBox(width: 16), + SecondaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Cancel", + onPressed: () {}, + ), + ], + ), ], ), ), From 5c37b6b26b2e3fb51f5972b80f193d61b1f6e42e Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 21 Oct 2025 13:50:13 -0600 Subject: [PATCH 02/50] separate swb file saving from encryption call, enable android swb file location selection, and clean up some of the swb ui code a bit --- .../create_auto_backup_view.dart | 587 ++++++-------- .../create_backup_view.dart | 508 ++++-------- .../edit_auto_backup_view.dart | 591 +++++++------- .../helpers/restore_create_backup.dart | 117 ++- .../restore_from_encrypted_string_view.dart | 256 +++--- .../restore_from_file_view.dart | 333 ++++---- ...forgotten_passphrase_restore_from_swb.dart | 159 ++-- .../create_auto_backup.dart | 731 +++++++----------- lib/services/auto_swb_service.dart | 29 +- 9 files changed, 1360 insertions(+), 1951 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index 5616ccd9d2..eae292ff77 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -8,6 +8,7 @@ * */ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -19,7 +20,6 @@ import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:zxcvbn/zxcvbn.dart'; import '../../../../app_config.dart'; -import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/secure_store_provider.dart'; import '../../../../themes/stack_colors.dart'; @@ -28,6 +28,7 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; import '../../../../utilities/format.dart'; import '../../../../utilities/logger.dart'; +import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../widgets/background.dart'; @@ -77,6 +78,108 @@ class _EnableAutoBackupViewState extends ConsumerState { passwordRepeatController.text.isNotEmpty; } + Future _createEnableAutoBackup() async { + final String pathToSave = fileLocationController.text; + final String passphrase = passwordController.text; + final String repeatPassphrase = passwordRepeatController.text; + + if (validateFail(context, pathToSave, passphrase, repeatPassphrase)) return; + + if (mounted) { + final now = DateTime.now(); + Exception? ex; + final savedPath = await showLoading( + whileFuture: () async { + String adkString; + int adkVersion; + try { + final adk = await compute(generateAdk, passphrase); + adkString = Format.uint8listToString(adk.item2); + adkVersion = adk.item1; + } on Exception catch (e, s) { + final String err = getErrorMessageFromSWBException(e); + Logging.instance.e(err, error: e, stackTrace: s); + rethrow; + } + + await secureStore.write(key: "auto_adk_string", value: adkString); + await secureStore.write( + key: "auto_adk_version_string", + value: adkVersion.toString(), + ); + + final fileToSavePath = createAutoBackupFilename(pathToSave, now); + + final backup = await SWB.createStackWalletJSON( + secureStorage: secureStore, + ); + + final encryptedDataString = await SWB.encryptStackWalletWithADK( + adkString, + jsonEncode(backup), + adkVersion, + ); + + if (Platform.isAndroid) { + // TODO SAF + File( + fileToSavePath, + ).writeAsStringSync(encryptedDataString, flush: true); + } else { + File( + fileToSavePath, + ).writeAsStringSync(encryptedDataString, flush: true); + } + + return fileToSavePath; + }(), + context: context, + message: "Encrypting initial backup", + subMessage: "This shouldn't take long", + delay: const Duration(seconds: 1), + onException: (e) => ex = e, + ); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (savedPath != null) { + ref.read(prefsChangeNotifierProvider).autoBackupLocation = savedPath; + ref.read(prefsChangeNotifierProvider).lastAutoBackup = now; + + ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled = true; + + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => StackOkDialog( + title: "${AppConfig.prefix} Auto Backup enabled and saved to:", + message: savedPath, + ), + ); + if (mounted) { + passwordController.text = ""; + passwordRepeatController.text = ""; + + Navigator.of( + context, + ).popUntil(ModalRoute.withName(AutoBackupView.routeName)); + } + } else { + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => StackOkDialog( + title: "Failed to enable Auto Backup", + message: ex?.toString(), + ), + ); + } + } + } + } + @override void initState() { secureStore = ref.read(secureStoreProvider); @@ -88,7 +191,7 @@ class _EnableAutoBackupViewState extends ConsumerState { passwordFocusNode = FocusNode(); passwordRepeatFocusNode = FocusNode(); - if (Platform.isAndroid || Platform.isIOS) { + if (Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -151,38 +254,36 @@ class _EnableAutoBackupViewState extends ConsumerState { style: STextStyles.smallMed12(context), ), const SizedBox(height: 10), - if (!Platform.isAndroid && !Platform.isIOS) + if (!Platform.isIOS) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: - Platform.isAndroid || Platform.isIOS - ? null - : () async { - try { - await stackFileSystem - .prepareStorage(); - - if (mounted) { - await stackFileSystem.pickDir( - context, - ); - } - - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); - } - } catch (e, s) { - Logging.instance.e( - "$e\n$s", - error: e, - stackTrace: s, + onTap: Platform.isIOS + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); + + if (mounted) { + await stackFileSystem.pickDir( + context, ); } - }, + + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance.e( + "$e\n$s", + error: e, + stackTrace: s, + ); + } + }, controller: fileLocationController, style: STextStyles.field(context), decoration: InputDecoration( @@ -194,10 +295,9 @@ class _EnableAutoBackupViewState extends ConsumerState { const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: - Theme.of(context) - .extension()! - .textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), @@ -218,8 +318,7 @@ class _EnableAutoBackupViewState extends ConsumerState { ), onChanged: (newValue) {}, ), - if (!Platform.isAndroid && !Platform.isIOS) - const SizedBox(height: 10), + if (!Platform.isIOS) const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -232,41 +331,41 @@ class _EnableAutoBackupViewState extends ConsumerState { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "Create passphrase", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox(width: 16), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: - Theme.of(context) + decoration: + standardInputDecoration( + "Create passphrase", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) .extension()! .textDark3, - width: 16, - height: 16, - ), + width: 16, + height: 16, + ), + ), + const SizedBox(width: 12), + ], ), - const SizedBox(width: 12), - ], + ), ), - ), - ), onChanged: (newValue) { if (newValue.isEmpty) { setState(() { @@ -317,13 +416,12 @@ class _EnableAutoBackupViewState extends ConsumerState { right: 12, top: passwordFeedback.isNotEmpty ? 4 : 0, ), - child: - passwordFeedback.isNotEmpty - ? Text( - passwordFeedback, - style: STextStyles.infoSmall(context), - ) - : null, + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), + ) + : null, ), if (passwordFocusNode.hasFocus || passwordRepeatFocusNode.hasFocus || @@ -339,26 +437,23 @@ class _EnableAutoBackupViewState extends ConsumerState { width: MediaQuery.of(context).size.width - 32 - 24, height: 5, - fillColor: - passwordStrength < 0.51 - ? Theme.of(context) - .extension()! - .accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension()! - .accentColorYellow - : Theme.of(context) - .extension()! - .accentColorGreen, - backgroundColor: - Theme.of(context) - .extension()! - .buttonBackSecondary, - percent: - passwordStrength < 0.25 - ? 0.03 - : passwordStrength, + fillColor: passwordStrength < 0.51 + ? Theme.of( + context, + ).extension()!.accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension()! + .accentColorYellow + : Theme.of(context) + .extension()! + .accentColorGreen, + backgroundColor: Theme.of( + context, + ).extension()!.buttonBackSecondary, + percent: passwordStrength < 0.25 + ? 0.03 + : passwordStrength, ), ), const SizedBox(height: 10), @@ -374,41 +469,41 @@ class _EnableAutoBackupViewState extends ConsumerState { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "Confirm passphrase", - passwordRepeatFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox(width: 16), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: - Theme.of(context) + decoration: + standardInputDecoration( + "Confirm passphrase", + passwordRepeatFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) .extension()! .textDark3, - width: 16, - height: 16, - ), + width: 16, + height: 16, + ), + ), + const SizedBox(width: 12), + ], ), - const SizedBox(width: 12), - ], + ), ), - ), - ), onChanged: (newValue) { setState(() {}); // TODO: ? check if passwords match? @@ -425,17 +520,17 @@ class _EnableAutoBackupViewState extends ConsumerState { children: [ TextField( autocorrect: Util.isDesktop ? false : true, - enableSuggestions: - Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop + ? false + : true, readOnly: true, textInputAction: TextInputAction.none, ), Positioned.fill( child: RawMaterialButton( - splashColor: - Theme.of( - context, - ).extension()!.highlight, + splashColor: Theme.of( + context, + ).extension()!.highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -450,9 +545,8 @@ class _EnableAutoBackupViewState extends ConsumerState { top: Radius.circular(20), ), ), - builder: - (_) => - const BackupFrequencyTypeSelectSheet(), + builder: (_) => + const BackupFrequencyTypeSelectSheet(), ); }, child: Padding( @@ -466,10 +560,11 @@ class _EnableAutoBackupViewState extends ConsumerState { Text( Format.prettyFrequencyType( ref.watch( - prefsChangeNotifierProvider.select( - (value) => - value.backupFrequencyType, - ), + prefsChangeNotifierProvider + .select( + (value) => value + .backupFrequencyType, + ), ), ), style: STextStyles.itemSubtitle12( @@ -482,10 +577,9 @@ class _EnableAutoBackupViewState extends ConsumerState { ), child: SvgPicture.asset( Assets.svg.chevronDown, - color: - Theme.of(context) - .extension()! - .textSubtitle2, + color: Theme.of(context) + .extension()! + .textSubtitle2, width: 12, height: 6, ), @@ -500,207 +594,16 @@ class _EnableAutoBackupViewState extends ConsumerState { const Spacer(), const SizedBox(height: 10), TextButton( - style: - shouldEnableCreate - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - onPressed: - !shouldEnableCreate - ? null - : () async { - final String pathToSave = - fileLocationController.text; - final String passphrase = - passwordController.text; - final String repeatPassphrase = - passwordRepeatController.text; - - if (pathToSave.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - ); - return; - } - if (!(await Directory( - pathToSave, - ).exists())) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - ); - return; - } - if (passphrase.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - ); - return; - } - if (passphrase != repeatPassphrase) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - ); - return; - } - - showDialog( - context: context, - barrierDismissible: false, - builder: - (_) => const StackDialog( - title: - "Encrypting initial backup", - message: - "This shouldn't take long", - ), - ); - - // make sure the dialog is able to be displayed for at least some time - final fut = Future.delayed( - const Duration(milliseconds: 300), - ); - - String adkString; - int adkVersion; - try { - final adk = await compute( - generateAdk, - passphrase, - ); - adkString = Format.uint8listToString( - adk.item2, - ); - adkVersion = adk.item1; - } on Exception catch (e, s) { - final String err = - getErrorMessageFromSWBException(e); - Logging.instance.e( - "$err\n$s", - error: e, - stackTrace: s, - ); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: err, - context: context, - ); - return; - } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: "$e", - context: context, - ); - return; - } - - await secureStore.write( - key: "auto_adk_string", - value: adkString, - ); - await secureStore.write( - key: "auto_adk_version_string", - value: adkVersion.toString(), - ); - - final DateTime now = DateTime.now(); - final String fileToSave = - createAutoBackupFilename( - pathToSave, - now, - ); - - final backup = await SWB - .createStackWalletJSON( - secureStorage: secureStore, - ); - - final bool result = await SWB - .encryptStackWalletWithADK( - fileToSave, - adkString, - jsonEncode(backup), - adkVersion, - ); - - // this future should already be complete unless there was an error encrypting - await Future.wait([fut]); - - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); - - if (result) { - ref - .read(prefsChangeNotifierProvider) - .autoBackupLocation = pathToSave; - ref - .read(prefsChangeNotifierProvider) - .lastAutoBackup = now; - - ref - .read(prefsChangeNotifierProvider) - .isAutoBackupEnabled = true; - - await showDialog( - context: context, - barrierDismissible: false, - builder: - (_) => - Platform.isAndroid - ? StackOkDialog( - title: - "${AppConfig.prefix} Auto Backup enabled and saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: - "${AppConfig.prefix} Auto Backup enabled!", - ), - ); - if (mounted) { - passwordController.text = ""; - passwordRepeatController.text = ""; - - Navigator.of(context).popUntil( - ModalRoute.withName( - AutoBackupView.routeName, - ), - ); - } - } else { - await showDialog( - context: context, - barrierDismissible: false, - builder: - (_) => const StackOkDialog( - title: - "Failed to enable Auto Backup", - ), - ); - } - } - }, + style: shouldEnableCreate + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + onPressed: !shouldEnableCreate + ? null + : _createEnableAutoBackup, child: Text( "Enable Auto Backup", style: STextStyles.button(context), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 57a8017733..f2d2bc23d1 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -18,12 +18,12 @@ import 'package:flutter_svg/svg.dart'; import 'package:zxcvbn/zxcvbn.dart'; import '../../../../app_config.dart'; -import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/global/secure_store_provider.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/logger.dart'; +import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../widgets/background.dart'; @@ -38,16 +38,16 @@ import '../../../../widgets/stack_text_field.dart'; import 'helpers/restore_create_backup.dart'; import 'helpers/swb_file_system.dart'; -class CreateBackupView extends StatefulWidget { +class CreateBackupView extends ConsumerStatefulWidget { const CreateBackupView({super.key}); static const String routeName = "/createBackup"; @override - State createState() => _RestoreFromFileViewState(); + ConsumerState createState() => _RestoreFromFileViewState(); } -class _RestoreFromFileViewState extends State { +class _RestoreFromFileViewState extends ConsumerState { late final TextEditingController fileLocationController; late final TextEditingController passwordController; late final TextEditingController passwordRepeatController; @@ -72,6 +72,121 @@ class _RestoreFromFileViewState extends State { passwordRepeatController.text.isNotEmpty; } + Future _createBackup() async { + final String pathToSave = fileLocationController.text; + final String passphrase = passwordController.text; + final String repeatPassphrase = passwordRepeatController.text; + + if (validateFail(context, pathToSave, passphrase, repeatPassphrase)) return; + + if (mounted) { + Exception? ex; + final savedPath = await showLoading( + whileFuture: () async { + final DateTime now = DateTime.now(); + final String fileToSavePath = + "$pathToSave/stackbackup" + "_${now.year}" + "_${now.month}" + "_${now.day}" + "_${now.hour}" + "_${now.minute}" + "_${now.second}.swb"; + + final backup = await SWB.createStackWalletJSON( + secureStorage: ref.read(secureStoreProvider), + ); + + final encryptedDataString = await SWB + .encryptStackWalletWithPassphrase(passphrase, jsonEncode(backup)); + + if (Platform.isAndroid) { + // TODO SAF + File( + fileToSavePath, + ).writeAsStringSync(encryptedDataString, flush: true); + } else { + File( + fileToSavePath, + ).writeAsStringSync(encryptedDataString, flush: true); + } + + return fileToSavePath; + }(), + context: context, + message: "Encrypting backup", + subMessage: "This shouldn't take long", + delay: const Duration(seconds: 1), + onException: (e) => ex = e, + ); + + if (mounted) { + if (savedPath != null) { + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => !Util.isDesktop + ? StackOkDialog(title: "Backup saved to:", message: savedPath) + : DesktopDialog( + maxHeight: double.infinity, + maxWidth: 500, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 26), + Text( + "${AppConfig.prefix} backup saved to: \n", + style: STextStyles.desktopH3(context), + ), + Text( + savedPath, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 40), + Row( + children: [ + Expanded( + child: PrimaryButton( + label: "Ok", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ), + ), + ); + passwordController.text = ""; + passwordRepeatController.text = ""; + if (mounted) { + setState(() {}); + } + } else { + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => StackOkDialog( + title: "Backup creation failed", + message: ex?.toString() ?? "Unexpected error", + ), + ); + } + } + } + } + @override void initState() { stackFileSystem = SWBFileSystem(); @@ -82,7 +197,7 @@ class _RestoreFromFileViewState extends State { passwordFocusNode = FocusNode(); passwordRepeatFocusNode = FocusNode(); - if (Platform.isAndroid || Platform.isIOS) { + if (Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -129,7 +244,7 @@ class _RestoreFromFileViewState extends State { const Duration(milliseconds: 75), ); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -184,7 +299,7 @@ class _RestoreFromFileViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (!Platform.isAndroid && !Platform.isIOS) + if (!Platform.isIOS) Consumer( builder: (context, ref, __) { return Container( @@ -192,13 +307,13 @@ class _RestoreFromFileViewState extends State { child: TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid || Platform.isIOS + onTap: Platform.isIOS ? null : () async { try { await stackFileSystem.prepareStorage(); - if (mounted) { + if (context.mounted) { await stackFileSystem.pickDir(context); } @@ -456,135 +571,19 @@ class _RestoreFromFileViewState extends State { const SizedBox(height: 16), if (!isDesktop) const Spacer(), !isDesktop - ? Consumer( - builder: (context, ref, __) { - return TextButton( - style: shouldEnableCreate - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - onPressed: !shouldEnableCreate - ? null - : () async { - final String pathToSave = - fileLocationController.text; - final String passphrase = - passwordController.text; - final String repeatPassphrase = - passwordRepeatController.text; - - if (pathToSave.isEmpty) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - ), - ); - return; - } - if (!(await Directory(pathToSave).exists())) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - ), - ); - return; - } - if (passphrase.isEmpty) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - ), - ); - return; - } - if (passphrase != repeatPassphrase) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - ), - ); - return; - } - - unawaited( - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting backup", - message: "This shouldn't take long", - ), - ), - ); - // make sure the dialog is able to be displayed for at least 1 second - await Future.delayed( - const Duration(seconds: 1), - ); - - final DateTime now = DateTime.now(); - final String fileToSave = - "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; - - final backup = await SWB.createStackWalletJSON( - secureStorage: ref.read(secureStoreProvider), - ); - - final bool result = await SWB - .encryptStackWalletWithPassphrase( - fileToSave, - passphrase, - jsonEncode(backup), - ); - - if (mounted) { - // pop encryption progress dialog - if (!isDesktop) Navigator.of(context).pop(); - - if (result) { - await showDialog( - context: context, - barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: "Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: - "Backup creation succeeded", - ), - ); - passwordController.text = ""; - passwordRepeatController.text = ""; - setState(() {}); - } else { - await showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Backup creation failed", - ), - ); - } - } - }, - child: Text( - "Create backup", - style: STextStyles.button(context), - ), - ); - }, + ? TextButton( + style: shouldEnableCreate + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + onPressed: !shouldEnableCreate ? null : _createBackup, + child: Text( + "Create backup", + style: STextStyles.button(context), + ), ) : Row( children: [ @@ -597,238 +596,7 @@ class _RestoreFromFileViewState extends State { enabled: shouldEnableCreate, onPressed: !shouldEnableCreate ? null - : () async { - final String pathToSave = - fileLocationController.text; - final String passphrase = - passwordController.text; - final String repeatPassphrase = - passwordRepeatController.text; - - if (pathToSave.isEmpty) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - ), - ); - return; - } - if (!(await Directory( - pathToSave, - ).exists())) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - ), - ); - return; - } - if (passphrase.isEmpty) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - ), - ); - return; - } - if (passphrase != repeatPassphrase) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - ), - ); - return; - } - - unawaited( - showDialog( - context: context, - barrierDismissible: false, - builder: (_) { - if (Util.isDesktop) { - return DesktopDialog( - maxHeight: double.infinity, - maxWidth: 450, - child: Padding( - padding: const EdgeInsets.all( - 32, - ), - child: Column( - mainAxisSize: - MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Encrypting initial backup", - style: - STextStyles.desktopH3( - context, - ), - ), - const SizedBox(height: 40), - Text( - "This shouldn't take long", - style: - STextStyles.desktopTextExtraExtraSmall( - context, - ), - ), - ], - ), - ), - ); - } else { - return const StackDialog( - title: - "Encrypting initial backup", - message: - "This shouldn't take long", - ); - } - }, - ), - ); - - await Future.delayed( - const Duration(seconds: 1), - ); - - // make sure the dialog is able to be displayed for at least 1 second - final fut = Future.delayed( - const Duration(seconds: 1), - ); - - final DateTime now = DateTime.now(); - final String fileToSave = - "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; - - final backup = await SWB - .createStackWalletJSON( - secureStorage: ref.read( - secureStoreProvider, - ), - ); - - final bool result = await SWB - .encryptStackWalletWithPassphrase( - fileToSave, - passphrase, - jsonEncode(backup), - ); - - await Future.wait([fut]); - - if (mounted) { - // pop encryption progress dialog - if (!isDesktop) - Navigator.of(context).pop(); - - if (result) { - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) { - if (Platform.isAndroid) { - return StackOkDialog( - title: "Backup saved to:", - message: fileToSave, - ); - } else if (isDesktop) { - return DesktopDialog( - maxHeight: double.infinity, - maxWidth: 500, - child: Padding( - padding: - const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Column( - mainAxisSize: - MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - const SizedBox( - height: 26, - ), - Text( - "${AppConfig.prefix} backup saved to: \n", - style: - STextStyles.desktopH3( - context, - ), - ), - Text( - fileToSave, - style: - STextStyles.desktopTextExtraExtraSmall( - context, - ), - ), - const SizedBox( - height: 40, - ), - Row( - children: [ - // const Spacer(), - Expanded( - child: PrimaryButton( - label: "Ok", - buttonHeight: - ButtonHeight - .l, - onPressed: () { - int count = 0; - Navigator.of( - context, - ).popUntil( - (_) => - count++ >= - 2, - ); - }, - ), - ), - ], - ), - ], - ), - ), - ); - } else { - return const StackOkDialog( - title: - "Backup creation succeeded", - ); - } - }, - ); - passwordController.text = ""; - passwordRepeatController.text = ""; - setState(() {}); - } else { - await showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Backup creation failed", - ), - ); - } - } - }, + : _createBackup, ); }, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 5ad7eab467..e1d5fd30bd 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -8,7 +8,6 @@ * */ -import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -21,7 +20,6 @@ import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:zxcvbn/zxcvbn.dart'; import '../../../../app_config.dart'; -import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/secure_store_provider.dart'; import '../../../../themes/stack_colors.dart'; @@ -31,6 +29,7 @@ import '../../../../utilities/enums/backup_frequency_type.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; import '../../../../utilities/format.dart'; import '../../../../utilities/logger.dart'; +import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../widgets/background.dart'; @@ -95,158 +94,101 @@ class _EditAutoBackupViewState extends ConsumerState { final String passphrase = passwordController.text; final String repeatPassphrase = passwordRepeatController.text; - if (pathToSave.isEmpty) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - ), - ); - return; - } - if (!(await Directory(pathToSave).exists())) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - ), - ); - return; - } - if (passphrase.isEmpty) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - ), - ); - return; - } - if (passphrase != repeatPassphrase) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - ), - ); - return; - } + if (validateFail(context, pathToSave, passphrase, repeatPassphrase)) return; - unawaited( - showDialog( - context: context, - barrierDismissible: false, - builder: - (_) => const StackDialog( - title: "Updating Auto Backup", - message: "This shouldn't take long", - ), - ), - ); - // make sure the dialog is able to be displayed for at least 1 second - final fut = Future.delayed(const Duration(seconds: 1)); - - String adkString; - int adkVersion; - try { - final adk = await compute(generateAdk, passphrase); - adkString = Format.uint8listToString(adk.item2); - adkVersion = adk.item1; - } on Exception catch (e, s) { - final String err = getErrorMessageFromSWBException(e); - Logging.instance.e("$err\n$s", error: e, stackTrace: s); - // pop encryption progress dialog - Navigator.of(context).pop(); - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: err, - context: context, - ), - ); - return; - } catch (e, s) { - Logging.instance.e("$e\n$s", error: e, stackTrace: s); - // pop encryption progress dialog - Navigator.of(context).pop(); - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "$e", - context: context, - ), - ); - return; - } + if (mounted) { + final now = DateTime.now(); + Exception? ex; + final savedPath = await showLoading( + whileFuture: () async { + String adkString; + int adkVersion; + try { + final adk = await compute(generateAdk, passphrase); + adkString = Format.uint8listToString(adk.item2); + adkVersion = adk.item1; + } on Exception catch (e, s) { + final String err = getErrorMessageFromSWBException(e); + Logging.instance.e(err, error: e, stackTrace: s); + rethrow; + } - await secureStore.write(key: "auto_adk_string", value: adkString); - await secureStore.write( - key: "auto_adk_version_string", - value: adkVersion.toString(), - ); + await secureStore.write(key: "auto_adk_string", value: adkString); + await secureStore.write( + key: "auto_adk_version_string", + value: adkVersion.toString(), + ); + + final fileToSavePath = createAutoBackupFilename(pathToSave, now); + + final backup = await SWB.createStackWalletJSON( + secureStorage: secureStore, + ); + + final encryptedDataString = await SWB.encryptStackWalletWithADK( + adkString, + jsonEncode(backup), + adkVersion, + ); + + if (Platform.isAndroid) { + // TODO SAF + File( + fileToSavePath, + ).writeAsStringSync(encryptedDataString, flush: true); + } else { + File( + fileToSavePath, + ).writeAsStringSync(encryptedDataString, flush: true); + } - final DateTime now = DateTime.now(); - final String fileToSave = createAutoBackupFilename(pathToSave, now); + return fileToSavePath; + }(), + context: context, + message: "Updating Auto Backup", + subMessage: "This shouldn't take long", + delay: const Duration(seconds: 1), + onException: (e) => ex = e, + ); - final backup = await SWB.createStackWalletJSON( - secureStorage: ref.read(secureStoreProvider), - ); + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); - final bool result = await SWB.encryptStackWalletWithADK( - fileToSave, - adkString, - jsonEncode(backup), - adkVersion, - ); + if (savedPath != null) { + ref.read(prefsChangeNotifierProvider).autoBackupLocation = pathToSave; + ref.read(prefsChangeNotifierProvider).lastAutoBackup = now; - // this future should already be complete unless there was an error encrypting - await Future.wait([fut]); + ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled = true; - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); - - if (result) { - ref.read(prefsChangeNotifierProvider).autoBackupLocation = pathToSave; - ref.read(prefsChangeNotifierProvider).lastAutoBackup = now; - - ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled = true; - - await showDialog( - context: context, - barrierDismissible: false, - builder: - (_) => - Platform.isAndroid - ? StackOkDialog( - title: "${AppConfig.prefix} Auto Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: "${AppConfig.prefix} Auto Backup saved", - ), - ); - if (mounted) { - passwordController.text = ""; - passwordRepeatController.text = ""; + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => StackOkDialog( + title: "${AppConfig.prefix} Auto Backup saved to:", + message: savedPath, + ), + ); + if (mounted) { + passwordController.text = ""; + passwordRepeatController.text = ""; - if (!Util.isDesktop) { - Navigator.of( - context, - ).popUntil(ModalRoute.withName(AutoBackupView.routeName)); + if (!Util.isDesktop) { + Navigator.of( + context, + ).popUntil(ModalRoute.withName(AutoBackupView.routeName)); + } } + } else { + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => StackOkDialog( + title: "Failed to update Auto Backup", + message: ex?.toString(), + ), + ); } - } else { - await showDialog( - context: context, - barrierDismissible: false, - builder: - (_) => const StackOkDialog(title: "Failed to update Auto Backup"), - ); } } } @@ -262,13 +204,14 @@ class _EditAutoBackupViewState extends ConsumerState { fileLocationController.text = ref.read(prefsChangeNotifierProvider).autoBackupLocation ?? ""; - _currentDropDownValue = - ref.read(prefsChangeNotifierProvider).backupFrequencyType; + _currentDropDownValue = ref + .read(prefsChangeNotifierProvider) + .backupFrequencyType; passwordFocusNode = FocusNode(); passwordRepeatFocusNode = FocusNode(); - if (Platform.isAndroid || Platform.isIOS) { + if (Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -302,44 +245,45 @@ class _EditAutoBackupViewState extends ConsumerState { return ConditionalParent( condition: !isDesktop, - builder: - (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Edit Auto Backup", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight(child: child), - ), - ); - }, - ), - ), + builder: (child) => Background( + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Edit Auto Backup", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), + ), + ); + }, ), ), ), + ), + ), child: Column( - crossAxisAlignment: - isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.stretch, + crossAxisAlignment: isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ if (!isDesktop) Text("Create your backup", style: STextStyles.smallMed12(context)), @@ -352,31 +296,30 @@ class _EditAutoBackupViewState extends ConsumerState { textAlign: TextAlign.left, ), const SizedBox(height: 10), - if (!Platform.isAndroid && !Platform.isIOS) + if (!Platform.isIOS) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: - Platform.isAndroid || Platform.isIOS - ? null - : () async { - try { - await stackFileSystem.prepareStorage(); - - if (mounted) { - await stackFileSystem.pickDir(context); - } - - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); - } - } catch (e, s) { - Logging.instance.e("$e\n$s", error: e, stackTrace: s); + onTap: Platform.isIOS + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); + + if (context.mounted) { + await stackFileSystem.pickDir(context); + } + + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); } - }, + } catch (e, s) { + Logging.instance.e("$e\n$s", error: e, stackTrace: s); + } + }, controller: fileLocationController, style: STextStyles.field(context), decoration: InputDecoration( @@ -388,10 +331,9 @@ class _EditAutoBackupViewState extends ConsumerState { const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: - Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), @@ -419,8 +361,7 @@ class _EditAutoBackupViewState extends ConsumerState { ), textAlign: TextAlign.left, ), - if (!Platform.isAndroid && !Platform.isIOS) - const SizedBox(height: 10), + if (!Platform.isIOS) const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -433,40 +374,44 @@ class _EditAutoBackupViewState extends ConsumerState { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "Create passphrase", - passwordFocusNode, - context, - ).copyWith( - labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox(width: 16), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: - Theme.of( + decoration: + standardInputDecoration( + "Create passphrase", + passwordFocusNode, + context, + ).copyWith( + labelStyle: isDesktop + ? STextStyles.fieldLabel(context) + : null, + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of( context, ).extension()!.textDark3, - width: 16, - height: 16, - ), + width: 16, + height: 16, + ), + ), + const SizedBox(width: 12), + ], ), - const SizedBox(width: 12), - ], + ), ), - ), - ), onChanged: (newValue) { if (newValue.isEmpty) { setState(() { @@ -513,13 +458,12 @@ class _EditAutoBackupViewState extends ConsumerState { right: 12, top: passwordFeedback.isNotEmpty ? 4 : 0, ), - child: - passwordFeedback.isNotEmpty - ? Text( - passwordFeedback, - style: STextStyles.infoSmall(context), - ) - : null, + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), + ) + : null, ), if (passwordFocusNode.hasFocus || passwordRepeatFocusNode.hasFocus || @@ -528,27 +472,22 @@ class _EditAutoBackupViewState extends ConsumerState { padding: const EdgeInsets.only(left: 12, right: 12, top: 10), child: ProgressBar( key: const Key("createStackBackUpProgressBar"), - width: - isDesktop - ? 492 - : MediaQuery.of(context).size.width - 32 - 24, + width: isDesktop + ? 492 + : MediaQuery.of(context).size.width - 32 - 24, height: 5, - fillColor: - passwordStrength < 0.51 - ? Theme.of( - context, - ).extension()!.accentColorRed - : passwordStrength < 1 - ? Theme.of( - context, - ).extension()!.accentColorYellow - : Theme.of( - context, - ).extension()!.accentColorGreen, - backgroundColor: - Theme.of( - context, - ).extension()!.buttonBackSecondary, + fillColor: passwordStrength < 0.51 + ? Theme.of(context).extension()!.accentColorRed + : passwordStrength < 1 + ? Theme.of( + context, + ).extension()!.accentColorYellow + : Theme.of( + context, + ).extension()!.accentColorGreen, + backgroundColor: Theme.of( + context, + ).extension()!.buttonBackSecondary, percent: passwordStrength < 0.25 ? 0.03 : passwordStrength, ), ), @@ -565,40 +504,44 @@ class _EditAutoBackupViewState extends ConsumerState { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "Confirm passphrase", - passwordRepeatFocusNode, - context, - ).copyWith( - labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox(width: 16), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: - Theme.of( + decoration: + standardInputDecoration( + "Confirm passphrase", + passwordRepeatFocusNode, + context, + ).copyWith( + labelStyle: isDesktop + ? STextStyles.fieldLabel(context) + : null, + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of( context, ).extension()!.textDark3, - width: 16, - height: 16, - ), + width: 16, + height: 16, + ), + ), + const SizedBox(width: 12), + ], ), - const SizedBox(width: 12), - ], + ), ), - ), - ), onChanged: (newValue) { setState(() {}); // TODO: ? check if passwords match? @@ -608,13 +551,13 @@ class _EditAutoBackupViewState extends ConsumerState { SizedBox(height: isDesktop ? 24 : 32), Text( "Auto Backup frequency", - style: - isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ) - : STextStyles.smallMed12(context), + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), ), const SizedBox(height: 10), if (isDesktop) @@ -653,8 +596,9 @@ class _EditAutoBackupViewState extends ConsumerState { .backupFrequencyType != value) { ref - .read(prefsChangeNotifierProvider) - .backupFrequencyType = value; + .read(prefsChangeNotifierProvider) + .backupFrequencyType = + value; } setState(() { _currentDropDownValue = value; @@ -666,18 +610,18 @@ class _EditAutoBackupViewState extends ConsumerState { Assets.svg.chevronDown, width: 10, height: 5, - color: - Theme.of(context).extension()!.textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, ), ), dropdownStyleData: DropdownStyleData( offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -699,8 +643,9 @@ class _EditAutoBackupViewState extends ConsumerState { ), Positioned.fill( child: RawMaterialButton( - splashColor: - Theme.of(context).extension()!.highlight, + splashColor: Theme.of( + context, + ).extension()!.highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -737,10 +682,9 @@ class _EditAutoBackupViewState extends ConsumerState { padding: const EdgeInsets.only(right: 4.0), child: SvgPicture.asset( Assets.svg.chevronDown, - color: - Theme.of( - context, - ).extension()!.textSubtitle2, + color: Theme.of( + context, + ).extension()!.textSubtitle2, width: 12, height: 6, ), @@ -777,14 +721,13 @@ class _EditAutoBackupViewState extends ConsumerState { ), if (!isDesktop) TextButton( - style: - shouldEnableCreate - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), + style: shouldEnableCreate + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), onPressed: !shouldEnableCreate ? null : onSavePressed, child: Text("Save", style: STextStyles.button(context)), ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 355fc6643a..58333ed743 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -13,6 +13,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:flutter/material.dart'; import 'package:isar_community/isar.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:tuple/tuple.dart'; @@ -31,6 +32,7 @@ import '../../../../../models/node_model.dart'; import '../../../../../models/stack_restoring_ui_state.dart'; import '../../../../../models/trade_wallet_lookup.dart'; import '../../../../../models/wallet_restore_state.dart'; +import '../../../../../notifications/show_flush_bar.dart'; import '../../../../../services/address_book_service.dart'; import '../../../../../services/node_service.dart'; import '../../../../../services/trade_notes_service.dart'; @@ -92,6 +94,35 @@ String createAutoBackupFilename(String dirPath, DateTime date) { "_${date.minute}_${date.second}.swb"; } +bool validateFail( + BuildContext context, + String pathToSave, + String passphrase, + String repeatPassphrase, +) { + for (final e in [ + [pathToSave.isEmpty, "Directory not chosen"], + [!(Directory(pathToSave).existsSync()), "Directory does not exist"], + [passphrase.isEmpty, "A passphrase is required"], + [passphrase != repeatPassphrase, "Passphrase does not match"], + ]) { + if (e[0] as bool) { + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: e[1] as String, + context: context, + ), + ); + } + return true; + } + } + + return false; +} + abstract class SWB { static Completer? _cancelCompleter; @@ -131,88 +162,42 @@ abstract class SWB { } } - static Future encryptStackWalletWithPassphrase( - String fileToSave, + static Future encryptStackWalletWithPassphrase( String passphrase, String plaintext, ) async { - try { - final File backupFile = File(fileToSave); - if (!backupFile.existsSync()) { - final String jsonBackup = plaintext; - final Uint8List content = Uint8List.fromList(utf8.encode(jsonBackup)); - final Uint8List encryptedContent = await encryptWithPassphrase( - passphrase, - content, - ); - backupFile.writeAsStringSync( - Format.uint8listToString(encryptedContent), - ); - } - Logging.instance.d(backupFile.absolute); - return true; - } catch (e, s) { - Logging.instance.e("$e\n$s", error: e, stackTrace: s); - return false; - } + final String jsonBackup = plaintext; + final Uint8List content = Uint8List.fromList(utf8.encode(jsonBackup)); + final Uint8List encryptedContent = await encryptWithPassphrase( + passphrase, + content, + ); + return Format.uint8listToString(encryptedContent); } - static Future encryptStackWalletWithADK( - String fileToSave, + static Future encryptStackWalletWithADK( String adk, String plaintext, int adkVersion, ) async { - try { - final File backupFile = File(fileToSave); - if (!backupFile.existsSync()) { - final String jsonBackup = plaintext; - final Uint8List content = Uint8List.fromList(utf8.encode(jsonBackup)); - final Uint8List encryptedContent = await encryptWithAdk( - Format.stringToUint8List(adk), - content, - version: adkVersion, - ); - backupFile.writeAsStringSync( - Format.uint8listToString(encryptedContent), - ); - } - Logging.instance.d(backupFile.absolute); - return true; - } catch (e, s) { - Logging.instance.e("$e\n$s", error: e, stackTrace: s); - return false; - } - } - - static Future decryptStackWalletWithPassphrase( - Tuple2 data, - ) async { - try { - final String fileToRestore = data.item1; - final String passphrase = data.item2; - final File backupFile = File(fileToRestore); - final String encryptedText = await backupFile.readAsString(); - return await decryptStackWalletStringWithPassphrase( - Tuple2(encryptedText, passphrase), - ); - } catch (e, s) { - Logging.instance.e("$e\n$s", error: e, stackTrace: s); - return null; - } + final String jsonBackup = plaintext; + final Uint8List content = Uint8List.fromList(utf8.encode(jsonBackup)); + final Uint8List encryptedContent = await encryptWithAdk( + Format.stringToUint8List(adk), + content, + version: adkVersion, + ); + return Format.uint8listToString(encryptedContent); } static Future decryptStackWalletStringWithPassphrase( - Tuple2 data, + ({String passphrase, String encryptedText}) data, ) async { try { - final encryptedText = data.item1; - final passphrase = data.item2; - - final encryptedBytes = Format.stringToUint8List(encryptedText); + final encryptedBytes = Format.stringToUint8List(data.encryptedText); final decryptedContent = await decryptWithPassphrase( - passphrase, + data.passphrase, encryptedBytes, ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart index 5aded8ec62..77ed42e0f5 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart @@ -12,7 +12,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:tuple/tuple.dart'; import '../../../../app_config.dart'; import '../../../../notifications/show_flush_bar.dart'; @@ -73,8 +72,9 @@ class _RestoreFromEncryptedStringViewState onWillPop: _onWillPop, child: Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -120,41 +120,41 @@ class _RestoreFromEncryptedStringViewState obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "Enter password", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox(width: 16), - GestureDetector( - key: const Key( - "restoreFromFilePasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: - Theme.of(context) + decoration: + standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "restoreFromFilePasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) .extension()! .textDark3, - width: 16, - height: 16, - ), + width: 16, + height: 16, + ), + ), + const SizedBox(width: 12), + ], ), - const SizedBox(width: 12), - ], + ), ), - ), - ), onChanged: (newValue) { setState(() {}); }, @@ -163,114 +163,108 @@ class _RestoreFromEncryptedStringViewState const SizedBox(height: 16), const Spacer(), TextButton( - style: - passwordController.text.isEmpty - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle( - context, - ), - onPressed: - passwordController.text.isEmpty - ? null - : () async { - final String passphrase = - passwordController.text; + style: passwordController.text.isEmpty + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + onPressed: passwordController.text.isEmpty + ? null + : () async { + final String passphrase = + passwordController.text; - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); - } + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } - bool shouldPop = false; - showDialog( - barrierDismissible: false, - context: context, - builder: - (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment - .stretch, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting ${AppConfig.prefix} backup file", - style: STextStyles.pageTitleH2( - context, - ).copyWith( - color: - Theme.of( - context, - ) - .extension< - StackColors - >()! - .textWhite, - ), + bool shouldPop = false; + showDialog( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting ${AppConfig.prefix} backup file", + style: + STextStyles.pageTitleH2( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textWhite, ), - ), - ), - const SizedBox(height: 64), - const Center( - child: LoadingIndicator( - width: 100, - ), - ), - ], + ), ), ), - ); - - final String? - jsonString = await compute( - SWB.decryptStackWalletStringWithPassphrase, - Tuple2(widget.encrypted, passphrase), - debugLabel: - "stack wallet decryption compute", - ); + const SizedBox(height: 64), + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], + ), + ), + ); - if (mounted) { - // pop LoadingIndicator - shouldPop = true; - Navigator.of(context).pop(); + final String? jsonString = await compute( + SWB.decryptStackWalletStringWithPassphrase, + ( + encryptedText: widget.encrypted, + passphrase: passphrase, + ), + debugLabel: + "stack wallet decryption compute", + ); - passwordController.text = ""; + if (mounted) { + // pop LoadingIndicator + shouldPop = true; + Navigator.of(context).pop(); - if (jsonString == null) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: - "Failed to decrypt backup file", - context: context, - ); - return; - } + passwordController.text = ""; - Navigator.of(context).push( - RouteGenerator.getRoute( - builder: - (_) => - StackRestoreProgressView( - jsonString: jsonString, - fromFile: true, - ), - ), + if (jsonString == null) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: + "Failed to decrypt backup file", + context: context, ); + return; } - }, + + Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => + StackRestoreProgressView( + jsonString: jsonString, + fromFile: true, + ), + ), + ); + } + }, child: Text( "Restore", style: STextStyles.button(context), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index d0ce73db15..3a7224aba1 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -15,7 +15,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:tuple/tuple.dart'; import '../../../../app_config.dart'; import '../../../../notifications/show_flush_bar.dart'; @@ -89,8 +88,9 @@ class _RestoreFromFileViewState extends ConsumerState { builder: (child) { return Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -140,12 +140,12 @@ class _RestoreFromFileViewState extends ConsumerState { padding: const EdgeInsets.only(bottom: 10.0), child: Text( "Choose file location", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ), + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, + ), textAlign: TextAlign.left, ), ), @@ -187,10 +187,9 @@ class _RestoreFromFileViewState extends ConsumerState { const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: - Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), @@ -215,12 +214,12 @@ class _RestoreFromFileViewState extends ConsumerState { padding: const EdgeInsets.only(bottom: 10.0), child: Text( "Enter passphrase", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ), + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, + ), textAlign: TextAlign.left, ), ), @@ -236,41 +235,44 @@ class _RestoreFromFileViewState extends ConsumerState { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "Enter passphrase", - passwordFocusNode, - context, - ).copyWith( - labelStyle: - isDesktop ? STextStyles.fieldLabel(context) : null, - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox(width: 16), - GestureDetector( - key: const Key( - "restoreFromFilePasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: - Theme.of( + decoration: + standardInputDecoration( + "Enter passphrase", + passwordFocusNode, + context, + ).copyWith( + labelStyle: isDesktop + ? STextStyles.fieldLabel(context) + : null, + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "restoreFromFilePasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of( context, ).extension()!.textDark3, - width: 16, - height: 16, - ), + width: 16, + height: 16, + ), + ), + const SizedBox(width: 12), + ], ), - const SizedBox(width: 12), - ], + ), ), - ), - ), onChanged: (newValue) { setState(() {}); }, @@ -280,20 +282,20 @@ class _RestoreFromFileViewState extends ConsumerState { if (!isDesktop) const Spacer(), !isDesktop ? TextButton( - style: - passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? Theme.of(context) + style: + passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? Theme.of(context) .extension()! .getPrimaryDisabledButtonStyle(context) - : Theme.of(context) + : Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context), - onPressed: - passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? null - : () async { + onPressed: + passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? null + : () async { final String fileToRestore = fileLocationController.text; final String passphrase = passwordController.text; @@ -319,48 +321,51 @@ class _RestoreFromFileViewState extends ConsumerState { showDialog( barrierDismissible: false, context: context, - builder: - (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting ${AppConfig.prefix} backup file", - style: STextStyles.pageTitleH2( + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting ${AppConfig.prefix} backup file", + style: + STextStyles.pageTitleH2( context, ).copyWith( - color: - Theme.of(context) - .extension< - StackColors - >()! - .textWhite, + color: Theme.of(context) + .extension()! + .textWhite, ), - ), - ), - ), - const SizedBox(height: 64), - const Center( - child: LoadingIndicator(width: 100), ), - ], + ), ), - ), + const SizedBox(height: 64), + const Center( + child: LoadingIndicator(width: 100), + ), + ], + ), + ), ), ); + final encryptedText = await File( + fileToRestore, + ).readAsString(); + final String? jsonString = await compute( - SWB.decryptStackWalletWithPassphrase, - Tuple2(fileToRestore, passphrase), + SWB.decryptStackWalletStringWithPassphrase, + ( + encryptedText: encryptedText, + passphrase: passphrase, + ), debugLabel: "stack wallet decryption compute", ); @@ -382,31 +387,30 @@ class _RestoreFromFileViewState extends ConsumerState { await Navigator.of(context).push( RouteGenerator.getRoute( - builder: - (_) => StackRestoreProgressView( - jsonString: jsonString, - shouldPushToHome: true, - ), + builder: (_) => StackRestoreProgressView( + jsonString: jsonString, + shouldPushToHome: true, + ), ), ); } }, - child: Text("Restore", style: STextStyles.button(context)), - ) + child: Text("Restore", style: STextStyles.button(context)), + ) : Row( - children: [ - PrimaryButton( - width: 183, - buttonHeight: ButtonHeight.m, - label: "Restore", - enabled: - !(passwordController.text.isEmpty || - fileLocationController.text.isEmpty), - onPressed: - passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? null - : () async { + children: [ + PrimaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Restore", + enabled: + !(passwordController.text.isEmpty || + fileLocationController.text.isEmpty), + onPressed: + passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? null + : () async { final String fileToRestore = fileLocationController.text; final String passphrase = @@ -433,55 +437,58 @@ class _RestoreFromFileViewState extends ConsumerState { showDialog( barrierDismissible: false, context: context, - builder: - (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting ${AppConfig.prefix} backup file", - style: - STextStyles.pageTitleH2( - context, - ).copyWith( - color: - Theme.of(context) - .extension< - StackColors - >()! - .textWhite, - ), - ), - ), - ), - const SizedBox(height: 64), - const Center( - child: LoadingIndicator( - width: 100, - ), + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting ${AppConfig.prefix} backup file", + style: + STextStyles.pageTitleH2( + context, + ).copyWith( + color: Theme.of(context) + .extension< + StackColors + >()! + .textWhite, + ), ), - ], + ), ), - ), + const SizedBox(height: 64), + const Center( + child: LoadingIndicator(width: 100), + ), + ], + ), + ), ), ); + final encryptedText = await File( + fileToRestore, + ).readAsString(); + final String? jsonString = await compute( - SWB.decryptStackWalletWithPassphrase, - Tuple2(fileToRestore, passphrase), + SWB.decryptStackWalletStringWithPassphrase, + ( + encryptedText: encryptedText, + passphrase: passphrase, + ), debugLabel: "stack wallet decryption compute", ); - if (mounted) { + if (context.mounted) { // pop LoadingIndicator shouldPop = true; Navigator.of( @@ -571,16 +578,16 @@ class _RestoreFromFileViewState extends ConsumerState { ); } }, - ), - const SizedBox(width: 16), - SecondaryButton( - width: 183, - buttonHeight: ButtonHeight.m, - label: "Cancel", - onPressed: () {}, - ), - ], - ), + ), + const SizedBox(width: 16), + SecondaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Cancel", + onPressed: () {}, + ), + ], + ), ], ), ), diff --git a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart index 161459e439..1d672c51b2 100644 --- a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart +++ b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart @@ -15,7 +15,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:tuple/tuple.dart'; import '../../app_config.dart'; import '../../db/hive/db.dart'; @@ -96,29 +95,26 @@ class _ForgottenPassphraseRestoreFromSWBState child: Text( "Decrypting ${AppConfig.prefix} backup file", style: STextStyles.pageTitleH2(context).copyWith( - color: - Theme.of(context).extension()!.textWhite, + color: Theme.of( + context, + ).extension()!.textWhite, ), ), ), ), - const SizedBox( - height: 64, - ), - const Center( - child: LoadingIndicator( - width: 100, - ), - ), + const SizedBox(height: 64), + const Center(child: LoadingIndicator(width: 100)), ], ), ), ), ); + final content = await File(fileToRestore).readAsString(); + final String? jsonString = await compute( - SWB.decryptStackWalletWithPassphrase, - Tuple2(fileToRestore, passphrase), + SWB.decryptStackWalletStringWithPassphrase, + (encryptedText: content, passphrase: passphrase), debugLabel: "${AppConfig.appName} decryption compute", ); @@ -161,9 +157,7 @@ class _ForgottenPassphraseRestoreFromSWBState ), ], ), - const SizedBox( - height: 44, - ), + const SizedBox(height: 44), Flexible( child: StackRestoreProgressView( jsonString: jsonString, @@ -220,8 +214,9 @@ class _ForgottenPassphraseRestoreFromSWBState ref.refresh(storageCryptoHandlerProvider); await DB.instance.init(); if (mounted) { - Navigator.of(context) - .popUntil(ModalRoute.withName(CreatePasswordView.routeName)); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(CreatePasswordView.routeName)); Navigator.of(context).pop(); } }, @@ -241,21 +236,17 @@ class _ForgottenPassphraseRestoreFromSWBState "Restore from backup", style: STextStyles.desktopH1(context), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), Text( "Use your ${AppConfig.prefix} backup file to restore your wallets, address book, and wallet preferences.", textAlign: TextAlign.center, style: STextStyles.desktopTextSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: Theme.of( + context, + ).extension()!.textSubtitle1, ), ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), GestureDetector( onTap: () async { try { @@ -290,20 +281,16 @@ class _ForgottenPassphraseRestoreFromSWBState child: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 24, - ), + const SizedBox(width: 24), SvgPicture.asset( Assets.svg.folder, - color: Theme.of(context) - .extension()! - .textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, width: 24, height: 24, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), @@ -321,16 +308,14 @@ class _ForgottenPassphraseRestoreFromSWBState setState(() { _enableButton = passwordController.text.isNotEmpty && - fileLocationController.text.isNotEmpty; + fileLocationController.text.isNotEmpty; }); }, ), ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -339,67 +324,63 @@ class _ForgottenPassphraseRestoreFromSWBState key: const Key("restoreFromFilePasswordFieldKey"), focusNode: passwordFocusNode, controller: passwordController, - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), + style: STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2), obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "Enter passphrase", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: SizedBox( - height: 70, - child: Row( - children: [ - const SizedBox( - width: 24, - ), - GestureDetector( - key: const Key( - "restoreFromFilePasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, - width: 24, - height: 24, + decoration: + standardInputDecoration( + "Enter passphrase", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox(width: 24), + GestureDetector( + key: const Key( + "restoreFromFilePasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of( + context, + ).extension()!.textDark3, + width: 24, + height: 24, + ), + ), ), - ), - ), - const SizedBox( - width: 12, + const SizedBox(width: 12), + ], ), - ], + ), ), ), - ), - ), onChanged: (newValue) { setState(() { - _enableButton = passwordController.text.isNotEmpty && + _enableButton = + passwordController.text.isNotEmpty && fileLocationController.text.isNotEmpty; }); }, ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), PrimaryButton( label: "Restore", enabled: _enableButton, @@ -407,9 +388,7 @@ class _ForgottenPassphraseRestoreFromSWBState restore(); }, ), - const SizedBox( - height: kDesktopAppBarHeight, - ), + const SizedBox(height: kDesktopAppBarHeight), ], ), ), diff --git a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart index 3ffea65084..ce1916714a 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart @@ -21,7 +21,6 @@ import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:zxcvbn/zxcvbn.dart'; import '../../../../app_config.dart'; -import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import '../../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; import '../../../../providers/global/prefs_provider.dart'; @@ -33,6 +32,7 @@ import '../../../../utilities/enums/backup_frequency_type.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; import '../../../../utilities/format.dart'; import '../../../../utilities/logger.dart'; +import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; @@ -44,9 +44,7 @@ import '../../../../widgets/stack_dialog.dart'; import '../../../../widgets/stack_text_field.dart'; class CreateAutoBackup extends ConsumerStatefulWidget { - const CreateAutoBackup({ - super.key, - }); + const CreateAutoBackup({super.key}); @override ConsumerState createState() => _CreateAutoBackup(); @@ -89,6 +87,146 @@ class _CreateAutoBackup extends ConsumerState { BackupFrequencyType.afterClosingAWallet, ]; + Future _enableAutoBackup() async { + final String pathToSave = fileLocationController.text; + final String passphrase = passphraseController.text; + final String repeatPassphrase = passphraseRepeatController.text; + + if (validateFail(context, pathToSave, passphrase, repeatPassphrase)) return; + + if (mounted) { + final now = DateTime.now(); + Exception? ex; + final savedPath = await showLoading( + whileFuture: () async { + String adkString; + int adkVersion; + try { + final adk = await compute(generateAdk, passphrase); + adkString = Format.uint8listToString(adk.item2); + adkVersion = adk.item1; + } on Exception catch (e, s) { + final String err = getErrorMessageFromSWBException(e); + Logging.instance.e(err, error: e, stackTrace: s); + rethrow; + } + + await secureStore.write(key: "auto_adk_string", value: adkString); + await secureStore.write( + key: "auto_adk_version_string", + value: adkVersion.toString(), + ); + + final fileToSavePath = createAutoBackupFilename(pathToSave, now); + + final backup = await SWB.createStackWalletJSON( + secureStorage: secureStore, + ); + + final encryptedDataString = await SWB.encryptStackWalletWithADK( + adkString, + jsonEncode(backup), + adkVersion, + ); + + if (Platform.isAndroid) { + // TODO SAF + File( + fileToSavePath, + ).writeAsStringSync(encryptedDataString, flush: true); + } else { + File( + fileToSavePath, + ).writeAsStringSync(encryptedDataString, flush: true); + } + + return fileToSavePath; + }(), + context: context, + message: "Encrypting initial backup", + subMessage: "This shouldn't take long", + delay: const Duration(seconds: 1), + onException: (e) => ex = e, + ); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (savedPath != null) { + ref.read(prefsChangeNotifierProvider).autoBackupLocation = pathToSave; + ref.read(prefsChangeNotifierProvider).lastAutoBackup = now; + + ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled = true; + + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxHeight: double.infinity, + maxWidth: 500, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${AppConfig.prefix} Auto Backup enabled!", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox(height: 40), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + label: "Ok", + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + if (mounted) { + passphraseController.text = ""; + passphraseRepeatController.text = ""; + + Navigator.of(context).pop(); + } + } else { + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => StackOkDialog( + title: "Failed to enable Auto Backup", + message: ex?.toString(), + ), + ); + } + } + } + } + @override void initState() { secureStore = ref.read(secureStoreProvider); @@ -101,7 +239,7 @@ class _CreateAutoBackup extends ConsumerState { passphraseFocusNode = FocusNode(); passphraseRepeatFocusNode = FocusNode(); - if (Platform.isAndroid || Platform.isIOS) { + if (Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -154,9 +292,7 @@ class _CreateAutoBackup extends ConsumerState { const DesktopDialogCloseButton(), ], ), - const SizedBox( - height: 30, - ), + const SizedBox(height: 30), Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.only(left: 32), @@ -168,15 +304,13 @@ class _CreateAutoBackup extends ConsumerState { textAlign: TextAlign.left, ), ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (!Platform.isAndroid && !Platform.isIOS) + if (!Platform.isIOS) Consumer( builder: (context, ref, __) { return Container( @@ -184,7 +318,7 @@ class _CreateAutoBackup extends ConsumerState { child: TextField( autocorrect: false, enableSuggestions: false, - onTap: Platform.isAndroid || Platform.isIOS + onTap: Platform.isIOS ? null : () async { try { @@ -216,20 +350,16 @@ class _CreateAutoBackup extends ConsumerState { suffixIcon: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: Theme.of(context) - .extension()! - .textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), @@ -249,21 +379,18 @@ class _CreateAutoBackup extends ConsumerState { ); }, ), - if (!Platform.isAndroid && !Platform.isIOS) - const SizedBox( - height: 24, - ), + if (!Platform.isIOS) const SizedBox(height: 24), if (isDesktop) Padding( padding: const EdgeInsets.only(bottom: 10.0), child: Text( "Create a passphrase", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, + ), textAlign: TextAlign.left, ), ), @@ -279,46 +406,44 @@ class _CreateAutoBackup extends ConsumerState { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "Create passphrase", - passphraseFocusNode, - context, - ).copyWith( - labelStyle: - isDesktop ? STextStyles.fieldLabel(context) : null, - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, + decoration: + standardInputDecoration( + "Create passphrase", + passphraseFocusNode, + context, + ).copyWith( + labelStyle: isDesktop + ? STextStyles.fieldLabel(context) + : null, + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of( + context, + ).extension()!.textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox(width: 12), + ], ), - ], + ), ), - ), - ), onChanged: (newValue) { if (newValue.isEmpty) { setState(() { @@ -386,26 +511,25 @@ class _CreateAutoBackup extends ConsumerState { width: 512, height: 5, fillColor: passwordStrength < 0.51 - ? Theme.of(context) - .extension()! - .accentColorRed + ? Theme.of( + context, + ).extension()!.accentColorRed : passwordStrength < 1 - ? Theme.of(context) - .extension()! - .accentColorYellow - : Theme.of(context) - .extension()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension()! - .buttonBackSecondary, - percent: - passwordStrength < 0.25 ? 0.03 : passwordStrength, + ? Theme.of( + context, + ).extension()!.accentColorYellow + : Theme.of( + context, + ).extension()!.accentColorGreen, + backgroundColor: Theme.of( + context, + ).extension()!.buttonBackSecondary, + percent: passwordStrength < 0.25 + ? 0.03 + : passwordStrength, ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -418,45 +542,42 @@ class _CreateAutoBackup extends ConsumerState { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "Confirm passphrase", - passphraseRepeatFocusNode, - context, - ).copyWith( - labelStyle: STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, + decoration: + standardInputDecoration( + "Confirm passphrase", + passphraseRepeatFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of( + context, + ).extension()!.textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox(width: 12), + ], ), - ], + ), ), - ), - ), onChanged: (newValue) { setState(() {}); // TODO: ? check if passwords match? @@ -466,9 +587,7 @@ class _CreateAutoBackup extends ConsumerState { ], ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.only(left: 32), @@ -480,47 +599,39 @@ class _CreateAutoBackup extends ConsumerState { textAlign: TextAlign.left, ), ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(left: 32, right: 32), child: isDesktop ? DropdownButtonHideUnderline( child: DropdownButton2( isExpanded: true, value: _currentDropDownValue, items: [ - ..._dropDownItems.map( - (e) { - String message = ""; - switch (e) { - case BackupFrequencyType.everyTenMinutes: - message = "Every 10 minutes"; - break; - case BackupFrequencyType.everyAppStart: - message = "Every app startup"; - break; - case BackupFrequencyType.afterClosingAWallet: - message = - "After closing a cryptocurrency wallet"; - break; - } - - return DropdownMenuItem( - value: e, - child: Text( - message, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ), + ..._dropDownItems.map((e) { + String message = ""; + switch (e) { + case BackupFrequencyType.everyTenMinutes: + message = "Every 10 minutes"; + break; + case BackupFrequencyType.everyAppStart: + message = "Every app startup"; + break; + case BackupFrequencyType.afterClosingAWallet: + message = "After closing a cryptocurrency wallet"; + break; + } + + return DropdownMenuItem( + value: e, + child: Text( + message, + style: STextStyles.desktopTextExtraExtraSmall( + context, ), - ); - }, - ), + ), + ); + }), ], onChanged: (value) { if (value is BackupFrequencyType) { @@ -529,8 +640,9 @@ class _CreateAutoBackup extends ConsumerState { .backupFrequencyType != value) { ref - .read(prefsChangeNotifierProvider) - .backupFrequencyType = value; + .read(prefsChangeNotifierProvider) + .backupFrequencyType = + value; } setState(() { _currentDropDownValue = value; @@ -542,18 +654,18 @@ class _CreateAutoBackup extends ConsumerState { Assets.svg.chevronDown, width: 10, height: 5, - color: Theme.of(context) - .extension()! - .textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, ), ), dropdownStyleData: DropdownStyleData( offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -581,292 +693,13 @@ class _CreateAutoBackup extends ConsumerState { onPressed: Navigator.of(context).pop, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( buttonHeight: ButtonHeight.l, label: "Enable Auto Backup", enabled: shouldEnableCreate, - onPressed: !shouldEnableCreate - ? null - : () async { - final String pathToSave = - fileLocationController.text; - final String passphrase = passphraseController.text; - final String repeatPassphrase = - passphraseRepeatController.text; - - if (pathToSave.isEmpty) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - ), - ); - return; - } - if (!(await Directory(pathToSave).exists())) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - ), - ); - return; - } - if (passphrase.isEmpty) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - ), - ); - return; - } - if (passphrase != repeatPassphrase) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - ), - ); - return; - } - - unawaited( - showDialog( - context: context, - barrierDismissible: false, - builder: (_) { - if (Util.isDesktop) { - return DesktopDialog( - maxHeight: double.infinity, - maxWidth: 450, - child: Padding( - padding: const EdgeInsets.all( - 32, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Encrypting initial backup", - style: STextStyles.desktopH3( - context, - ), - ), - const SizedBox( - height: 40, - ), - Text( - "This shouldn't take long", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ), - ), - ], - ), - ), - ); - } else { - return const StackDialog( - title: "Encrypting initial backup", - message: "This shouldn't take long", - ); - } - }, - ), - ); - - // make sure the dialog is able to be displayed for at least some time - final fut = Future.delayed( - const Duration(milliseconds: 300), - ); - - String adkString; - int adkVersion; - try { - final adk = - await compute(generateAdk, passphrase); - adkString = Format.uint8listToString(adk.item2); - adkVersion = adk.item1; - } on Exception catch (e, s) { - final String err = - getErrorMessageFromSWBException(e); - Logging.instance.e( - err, - error: e, - stackTrace: s, - ); - // pop encryption progress dialog - Navigator.of(context).pop(); - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: err, - context: context, - ), - ); - return; - } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); - // pop encryption progress dialog - Navigator.of(context).pop(); - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "$e", - context: context, - ), - ); - return; - } - - await secureStore.write( - key: "auto_adk_string", - value: adkString, - ); - await secureStore.write( - key: "auto_adk_version_string", - value: adkVersion.toString(), - ); - - final DateTime now = DateTime.now(); - final String fileToSave = - createAutoBackupFilename(pathToSave, now); - - final backup = await SWB.createStackWalletJSON( - secureStorage: secureStore, - ); - - final bool result = - await SWB.encryptStackWalletWithADK( - fileToSave, - adkString, - jsonEncode(backup), - adkVersion, - ); - - // this future should already be complete unless there was an error encrypting - await Future.wait([fut]); - - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); - - if (result) { - ref - .read(prefsChangeNotifierProvider) - .autoBackupLocation = pathToSave; - ref - .read(prefsChangeNotifierProvider) - .lastAutoBackup = now; - - ref - .read(prefsChangeNotifierProvider) - .isAutoBackupEnabled = true; - - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) { - if (Platform.isAndroid) { - return StackOkDialog( - title: - "${AppConfig.prefix} Auto Backup enabled and saved to:", - message: fileToSave, - ); - } else if (Util.isDesktop) { - return DesktopDialog( - maxHeight: double.infinity, - maxWidth: 500, - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text( - "${AppConfig.prefix} Auto Backup enabled!", - style: - STextStyles.desktopH3( - context, - ), - ), - const DesktopDialogCloseButton(), - ], - ), - const SizedBox( - height: 40, - ), - Row( - children: [ - const Spacer(), - Expanded( - child: PrimaryButton( - label: "Ok", - buttonHeight: - ButtonHeight.l, - onPressed: () { - Navigator.of(context) - .pop(); - }, - ), - ), - ], - ), - ], - ), - ), - ); - } else { - return const StackOkDialog( - title: - "${AppConfig.prefix} Auto Backup enabled!", - ); - } - }, - ); - if (mounted) { - passphraseController.text = ""; - passphraseRepeatController.text = ""; - - Navigator.of(context).pop(); - } - } else { - await showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Failed to enable Auto Backup", - ), - ); - } - } - }, + onPressed: !shouldEnableCreate ? null : _enableAutoBackup, ), ), ], diff --git a/lib/services/auto_swb_service.dart b/lib/services/auto_swb_service.dart index 24f58d0f78..39b080a336 100644 --- a/lib/services/auto_swb_service.dart +++ b/lib/services/auto_swb_service.dart @@ -20,11 +20,7 @@ import '../utilities/flutter_secure_storage_interface.dart'; import '../utilities/logger.dart'; import '../utilities/prefs.dart'; -enum AutoSWBStatus { - idle, - backingUp, - error, -} +enum AutoSWBStatus { idle, backingUp, error } class AutoSWBService extends ChangeNotifier { Timer? _timer; @@ -74,27 +70,28 @@ class AutoSWBService extends ChangeNotifier { ); final jsonString = jsonEncode(json); - final adkString = - await secureStorageInterface.read(key: "auto_adk_string"); + final adkString = await secureStorageInterface.read( + key: "auto_adk_string", + ); - final adkVersionString = - await secureStorageInterface.read(key: "auto_adk_version_string"); + final adkVersionString = await secureStorageInterface.read( + key: "auto_adk_version_string", + ); final int adkVersion = int.parse(adkVersionString!); final DateTime now = DateTime.now(); - final String fileToSave = - createAutoBackupFilename(autoBackupDirectoryPath, now); + final String fileToSave = createAutoBackupFilename( + autoBackupDirectoryPath, + now, + ); - final result = await SWB.encryptStackWalletWithADK( - fileToSave, + final content = await SWB.encryptStackWalletWithADK( adkString!, jsonString, adkVersion, ); - if (!result) { - throw Exception("stack auto backup service failed to create a backup"); - } + await File(fileToSave).writeAsString(content, flush: true); Prefs.instance.lastAutoBackup = now; From 1311e238b1e9e45319650407373a146eab4ca118 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 22 Oct 2025 14:31:29 -0600 Subject: [PATCH 03/50] use SAF (lol) for some android stuff --- lib/pages/monkey/monkey_view.dart | 587 +++++++++--------- lib/pages/ordinals/ordinal_details_view.dart | 53 +- .../create_auto_backup_view.dart | 36 +- .../create_backup_view.dart | 37 +- .../edit_auto_backup_view.dart | 32 +- .../helpers/restore_create_backup.dart | 3 +- .../helpers/swb_file_system.dart | 81 +-- .../restore_from_file_view.dart | 13 +- .../desktop_ordinal_details_view.dart | 5 +- ...forgotten_passphrase_restore_from_swb.dart | 13 +- .../create_auto_backup.dart | 34 +- lib/utilities/fs.dart | 46 ++ lib/utilities/stack_file_system.dart | 14 - pubspec.lock | 27 +- .../templates/pubspec.template.yaml | 7 +- 15 files changed, 502 insertions(+), 486 deletions(-) create mode 100644 lib/utilities/fs.dart diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 49329cc986..e16f8d6353 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -6,6 +6,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; +import 'package:saf_stream/saf_stream.dart'; +import 'package:saf_util/saf_util.dart'; import '../../notifications/show_flush_bar.dart'; import '../../providers/global/wallets_provider.dart'; @@ -13,8 +15,8 @@ import '../../services/monkey_service.dart'; import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; +import '../../utilities/fs.dart'; import '../../utilities/show_loading.dart'; -import '../../utilities/stack_file_system.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; @@ -51,13 +53,13 @@ class _MonkeyViewState extends ConsumerState { .updateMonkeyImageBytes(monKeyBytes.toList()); } - Future _getDocsDir() async { + Future _getDocsDir() async { try { if (Platform.isAndroid) { - return await StackFileSystem.wtfAndroidDocumentsPath(); + return await FS.pickDirectory(); } - return await getApplicationDocumentsDirectory(); + return (await getApplicationDocumentsDirectory()).path; } catch (_) { return null; } @@ -70,27 +72,40 @@ class _MonkeyViewState extends ConsumerState { bool isPNG = false, bool overwrite = false, }) async { - final dir = await _getDocsDir(); - if (dir == null) { - throw Exception("Failed to get documents directory to save monKey image"); + final dirPath = await _getDocsDir(); + if (dirPath == null) { + throw Exception("Failed to get directory path to save monKey image"); } - final address = - await ref - .read(pWallets) - .getWallet(walletId) - .getCurrentReceivingAddress(); - String filePath = path.join(dir.path, "monkey_${address?.value}"); + final address = await ref + .read(pWallets) + .getWallet(walletId) + .getCurrentReceivingAddress(); - filePath += isPNG ? ".png" : ".svg"; + final fileName = "monkey_${address?.value}${isPNG ? ".png" : ".svg"}"; + final filePath = path.join(dirPath, fileName); - final File imgFile = File(filePath); + if (Platform.isAndroid) { + if (!overwrite && await SafUtil().exists(filePath, false)) { + throw Exception("File already exists"); + } + + await SafStream().writeFileBytes( + dirPath, + fileName, + isPNG ? "png" : "svg", + bytes, + ); + } else { + final File imgFile = File(filePath); + + if (imgFile.existsSync() && !overwrite) { + throw Exception("File already exists"); + } - if (imgFile.existsSync() && !overwrite) { - throw Exception("File already exists"); + await imgFile.writeAsBytes(bytes); } - await imgFile.writeAsBytes(bytes); _monkeyPath = filePath; } @@ -113,313 +128,296 @@ class _MonkeyViewState extends ConsumerState { return Background( child: ConditionalParent( condition: isDesktop, - builder: - (child) => DesktopScaffold( - appBar: DesktopAppBar( - background: Theme.of(context).extension()!.popupBG, - leading: Expanded( - child: Row( - children: [ - const SizedBox(width: 32), - AppBarIconButton( - size: 32, - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.arrowLeft, - width: 18, - height: 18, - color: - Theme.of( - context, - ).extension()!.topNavIconPrimary, - ), - onPressed: Navigator.of(context).pop, - ), - const SizedBox(width: 15), - SvgPicture.asset( - Assets.svg.monkey, - width: 32, - height: 32, - color: - Theme.of( - context, - ).extension()!.textSubtitle1, - ), - const SizedBox(width: 12), - Text("MonKey", style: STextStyles.desktopH3(context)), - ], + builder: (child) => DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: Row( + children: [ + const SizedBox(width: 32), + AppBarIconButton( + size: 32, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of( + context, + ).extension()!.topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, ), - ), - trailing: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(1000), + const SizedBox(width: 15), + SvgPicture.asset( + Assets.svg.monkey, + width: 32, + height: 32, + color: Theme.of( + context, + ).extension()!.textSubtitle1, ), - onPressed: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return DesktopDialog( - maxHeight: double.infinity, - child: Column( + const SizedBox(width: 12), + Text("MonKey", style: STextStyles.desktopH3(context)), + ], + ), + ), + trailing: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), + ), + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "About MonKeys", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Text( - "A MonKey is a visual representation of your Banano address.", - style: STextStyles.desktopTextMedium( - context, - ).copyWith( - color: - Theme.of( - context, - ).extension()!.textDark3, + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "About MonKeys", + style: STextStyles.desktopH3(context), ), ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.all(32), - child: PrimaryButton( - width: 272.5, - label: "OK", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - ], + const DesktopDialogCloseButton(), + ], + ), + Text( + "A MonKey is a visual representation of your Banano address.", + style: STextStyles.desktopTextMedium(context) + .copyWith( + color: Theme.of( + context, + ).extension()!.textDark3, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: PrimaryButton( + width: 272.5, + label: "OK", + onPressed: () { + Navigator.of(context).pop(); + }, + ), ), ], ), - ); - }, + ], + ), ); }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 19, - horizontal: 32, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 19, + horizontal: 32, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circleQuestion, + width: 20, + height: 20, + color: Theme.of( + context, + ).extension()!.customTextButtonEnabledText, ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.circleQuestion, - width: 20, - height: 20, - color: - Theme.of(context) - .extension()! - .customTextButtonEnabledText, - ), - const SizedBox(width: 8), - Text( - "What is MonKey?", - style: STextStyles.desktopMenuItemSelected( - context, - ).copyWith( - color: - Theme.of(context) - .extension()! - .customTextButtonEnabledText, + const SizedBox(width: 8), + Text( + "What is MonKey?", + style: STextStyles.desktopMenuItemSelected(context) + .copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, ), - ), - ], ), - ), + ], ), - useSpacers: false, - isCompactHeight: true, ), - body: child, ), + useSpacers: false, + isCompactHeight: true, + ), + body: child, + ), child: ConditionalParent( condition: !isDesktop, - builder: - (child) => Scaffold( - appBar: AppBar( - leading: AppBarBackButton( + builder: (child) => Scaffold( + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text("MonKey", style: STextStyles.navBarTitle(context)), + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + icon: SvgPicture.asset(Assets.svg.circleQuestion), onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "MonKey", - style: STextStyles.navBarTitle(context), - ), - actions: [ - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - icon: SvgPicture.asset(Assets.svg.circleQuestion), - onPressed: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const StackOkDialog( - title: "About MonKeys", - message: - "A MonKey is a visual representation of your Banano address.", - ); - }, + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const StackOkDialog( + title: "About MonKeys", + message: + "A MonKey is a visual representation of your Banano address.", ); }, - ), - ), - ], + ); + }, + ), ), - body: SafeArea(child: child), - ), + ], + ), + body: SafeArea(child: child), + ), child: ConditionalParent( condition: isDesktop, builder: (child) => SizedBox(width: 318, child: child), child: ConditionalParent( condition: imageBytes != null, - builder: - (_) => Column( - children: [ - isDesktop - ? const SizedBox(height: 50) - : const Spacer(flex: 1), - if (imageBytes != null) - SizedBox( - width: 300, - height: 300, - child: SvgPicture.memory( - Uint8List.fromList(imageBytes!), - ), - ), - isDesktop - ? const SizedBox(height: 50) - : const Spacer(flex: 1), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - SecondaryButton( - label: "Save as SVG", - onPressed: () async { - bool didError = false; - await showLoading( - whileFuture: Future.wait([ - _saveMonKeyToFile( - bytes: Uint8List.fromList( - (wallet as BananoWallet) - .getMonkeyImageBytes()!, - ), - ), - Future.delayed( - const Duration(seconds: 2), - ), - ]), + builder: (_) => Column( + children: [ + isDesktop + ? const SizedBox(height: 50) + : const Spacer(flex: 1), + if (imageBytes != null) + SizedBox( + width: 300, + height: 300, + child: SvgPicture.memory(Uint8List.fromList(imageBytes!)), + ), + isDesktop + ? const SizedBox(height: 50) + : const Spacer(flex: 1), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SecondaryButton( + label: "Save as SVG", + onPressed: () async { + bool didError = false; + await showLoading( + whileFuture: Future.wait([ + _saveMonKeyToFile( + bytes: Uint8List.fromList( + (wallet as BananoWallet) + .getMonkeyImageBytes()!, + ), + ), + Future.delayed( + const Duration(seconds: 2), + ), + ]), + context: context, + rootNavigator: Util.isDesktop, + message: "Saving MonKey svg", + onException: (e) { + didError = true; + String msg = e.toString(); + while (msg.isNotEmpty && + msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, context: context, - rootNavigator: Util.isDesktop, - message: "Saving MonKey svg", - onException: (e) { - didError = true; - String msg = e.toString(); - while (msg.isNotEmpty && - msg.startsWith("Exception:")) { - msg = msg.substring(10).trim(); - } - showFloatingFlushBar( - type: FlushBarType.warning, - message: msg, - context: context, - ); - }, ); + }, + ); - if (!didError && mounted) { - await showFloatingFlushBar( - type: FlushBarType.success, - message: - "SVG MonKey image saved to $_monkeyPath", - context: context, - ); + if (!didError && mounted) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: + "SVG MonKey image saved to $_monkeyPath", + context: context, + ); + } + }, + ), + const SizedBox(height: 12), + SecondaryButton( + label: "Download as PNG", + onPressed: () async { + bool didError = false; + await showLoading( + whileFuture: Future.wait([ + wallet.getCurrentReceivingAddress().then( + (address) async => await ref + .read(pMonKeyService) + .fetchMonKey( + address: address!.value, + png: true, + ) + .then( + (monKeyBytes) async => + await _saveMonKeyToFile( + bytes: monKeyBytes, + isPNG: true, + ), + ), + ), + Future.delayed( + const Duration(seconds: 2), + ), + ]), + context: context, + rootNavigator: Util.isDesktop, + message: "Downloading MonKey png", + onException: (e) { + didError = true; + String msg = e.toString(); + while (msg.isNotEmpty && + msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); } - }, - ), - const SizedBox(height: 12), - SecondaryButton( - label: "Download as PNG", - onPressed: () async { - bool didError = false; - await showLoading( - whileFuture: Future.wait([ - wallet.getCurrentReceivingAddress().then( - (address) async => await ref - .read(pMonKeyService) - .fetchMonKey( - address: address!.value, - png: true, - ) - .then( - (monKeyBytes) async => - await _saveMonKeyToFile( - bytes: monKeyBytes, - isPNG: true, - ), - ), - ), - Future.delayed( - const Duration(seconds: 2), - ), - ]), + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, context: context, - rootNavigator: Util.isDesktop, - message: "Downloading MonKey png", - onException: (e) { - didError = true; - String msg = e.toString(); - while (msg.isNotEmpty && - msg.startsWith("Exception:")) { - msg = msg.substring(10).trim(); - } - showFloatingFlushBar( - type: FlushBarType.warning, - message: msg, - context: context, - ); - }, ); - - if (!didError && mounted) { - await showFloatingFlushBar( - type: FlushBarType.success, - message: - "PNG MonKey image saved to $_monkeyPath", - context: context, - ); - } }, - ), - ], + ); + + if (!didError && mounted) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: + "PNG MonKey image saved to $_monkeyPath", + context: context, + ); + } + }, ), - ), - // child, - ], + ], + ), ), + // child, + ], + ), child: Column( children: [ isDesktop @@ -440,10 +438,9 @@ class _MonkeyViewState extends ConsumerState { Text( "You do not have a MonKey yet. \nFetch yours now!", style: STextStyles.smallMed14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, ), textAlign: TextAlign.center, ), @@ -489,8 +486,8 @@ class _MonkeyViewState extends ConsumerState { }, ); - imageBytes = - (wallet as BananoWallet).getMonkeyImageBytes(); + imageBytes = (wallet as BananoWallet) + .getMonkeyImageBytes(); if (imageBytes != null) { setState(() {}); diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index 996f20db67..7ea7c2d342 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -7,6 +7,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; +import 'package:saf_stream/saf_stream.dart'; +import 'package:saf_util/saf_util.dart'; import '../../app_config.dart'; import '../../models/isar/models/blockchain_data/utxo.dart'; @@ -21,8 +23,8 @@ import '../../utilities/amount/amount.dart'; import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; +import '../../utilities/fs.dart'; import '../../utilities/show_loading.dart'; -import '../../utilities/stack_file_system.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../widgets/background.dart'; @@ -210,6 +212,18 @@ class _OrdinalImageGroup extends ConsumerWidget { static const _spacing = 12.0; + Future _getDocsDir() async { + try { + if (Platform.isAndroid) { + return await FS.pickDirectory(); + } + + return (await getApplicationDocumentsDirectory()).path; + } catch (_) { + return null; + } + } + Future _savePngToFile(WidgetRef ref) async { final HTTP client = HTTP(); @@ -230,21 +244,36 @@ class _OrdinalImageGroup extends ConsumerWidget { final bytes = response.bodyBytes; - final dir = Platform.isAndroid - ? await StackFileSystem.wtfAndroidDocumentsPath() - : await getApplicationDocumentsDirectory(); - final filePath = path.join( - dir.path, - "ordinal_${ordinal.inscriptionNumber}.png", - ); + final dirPath = await _getDocsDir(); + if (dirPath == null) { + throw Exception("Failed to get directory path to save ordinal image"); + } + + final fileName = "ordinal_${ordinal.inscriptionNumber}.png"; + + final filePath = path.join(dirPath, fileName); + + if (Platform.isAndroid) { + if (await SafUtil().exists(filePath, false)) { + throw Exception("File already exists"); + } + + await SafStream().writeFileBytes( + dirPath, + fileName, + "png", + Uint8List.fromList(bytes), + ); + } else { + final File imgFile = File(filePath); - final File imgFile = File(filePath); + if (imgFile.existsSync()) { + throw Exception("File already exists"); + } - if (imgFile.existsSync()) { - throw Exception("File already exists"); + await imgFile.writeAsBytes(bytes); } - await imgFile.writeAsBytes(bytes); return filePath; } diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index eae292ff77..c1bd1b2edb 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -27,6 +27,7 @@ import '../../../../utilities/assets.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; import '../../../../utilities/format.dart'; +import '../../../../utilities/fs.dart'; import '../../../../utilities/logger.dart'; import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; @@ -120,16 +121,11 @@ class _EnableAutoBackupViewState extends ConsumerState { adkVersion, ); - if (Platform.isAndroid) { - // TODO SAF - File( - fileToSavePath, - ).writeAsStringSync(encryptedDataString, flush: true); - } else { - File( - fileToSavePath, - ).writeAsStringSync(encryptedDataString, flush: true); - } + await FS.writeStringToFile( + encryptedDataString, + pathToSave, + fileToSavePath.split("/").last, + ); return fileToSavePath; }(), @@ -263,18 +259,16 @@ class _EnableAutoBackupViewState extends ConsumerState { : () async { try { await stackFileSystem.prepareStorage(); - - if (mounted) { - await stackFileSystem.pickDir( - context, - ); - } - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); + final filePath = await stackFileSystem + .openFile(); + + if (mounted) { + setState(() { + fileLocationController.text = + filePath ?? ""; + }); + } } } catch (e, s) { Logging.instance.e( diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index f2d2bc23d1..0df1b215ca 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -22,6 +22,7 @@ import '../../../../providers/global/secure_store_provider.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/constants.dart'; +import '../../../../utilities/fs.dart'; import '../../../../utilities/logger.dart'; import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; @@ -100,16 +101,11 @@ class _RestoreFromFileViewState extends ConsumerState { final encryptedDataString = await SWB .encryptStackWalletWithPassphrase(passphrase, jsonEncode(backup)); - if (Platform.isAndroid) { - // TODO SAF - File( - fileToSavePath, - ).writeAsStringSync(encryptedDataString, flush: true); - } else { - File( - fileToSavePath, - ).writeAsStringSync(encryptedDataString, flush: true); - } + await FS.writeStringToFile( + encryptedDataString, + pathToSave, + fileToSavePath.split("/").last, + ); return fileToSavePath; }(), @@ -312,16 +308,16 @@ class _RestoreFromFileViewState extends ConsumerState { : () async { try { await stackFileSystem.prepareStorage(); - - if (context.mounted) { - await stackFileSystem.pickDir(context); - } - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); + final filePath = await stackFileSystem + .openFile(); + + if (mounted) { + setState(() { + fileLocationController.text = + filePath ?? ""; + }); + } } } catch (e, s) { Logging.instance.e("", error: e, stackTrace: s); @@ -366,8 +362,7 @@ class _RestoreFromFileViewState extends ConsumerState { ); }, ), - if (!Platform.isAndroid && !Platform.isIOS) - SizedBox(height: !isDesktop ? 8 : 24), + if (!Platform.isIOS) SizedBox(height: !isDesktop ? 8 : 24), if (isDesktop) Padding( padding: const EdgeInsets.only(bottom: 10.0), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index e1d5fd30bd..81d7b9d0dd 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -28,6 +28,7 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/enums/backup_frequency_type.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; import '../../../../utilities/format.dart'; +import '../../../../utilities/fs.dart'; import '../../../../utilities/logger.dart'; import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; @@ -131,16 +132,11 @@ class _EditAutoBackupViewState extends ConsumerState { adkVersion, ); - if (Platform.isAndroid) { - // TODO SAF - File( - fileToSavePath, - ).writeAsStringSync(encryptedDataString, flush: true); - } else { - File( - fileToSavePath, - ).writeAsStringSync(encryptedDataString, flush: true); - } + await FS.writeStringToFile( + encryptedDataString, + pathToSave, + fileToSavePath.split("/").last, + ); return fileToSavePath; }(), @@ -305,16 +301,14 @@ class _EditAutoBackupViewState extends ConsumerState { : () async { try { await stackFileSystem.prepareStorage(); - - if (context.mounted) { - await stackFileSystem.pickDir(context); - } - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); + final filePath = await stackFileSystem.openFile(); + + if (mounted) { + setState(() { + fileLocationController.text = filePath ?? ""; + }); + } } } catch (e, s) { Logging.instance.e("$e\n$s", error: e, stackTrace: s); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 58333ed743..a681186019 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -102,7 +102,8 @@ bool validateFail( ) { for (final e in [ [pathToSave.isEmpty, "Directory not chosen"], - [!(Directory(pathToSave).existsSync()), "Directory does not exist"], + if (!pathToSave.startsWith("content://")) + [!(Directory(pathToSave).existsSync()), "Directory does not exist"], [passphrase.isEmpty, "A passphrase is required"], [passphrase != repeatPassphrase, "Passphrase does not match"], ]) { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart index 9954cb0b7f..c02f91d45a 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart @@ -11,94 +11,63 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import '../../../../../app_config.dart'; -import '../../../../../utilities/stack_file_system.dart'; -import '../../../../../utilities/util.dart'; +import '../../../../../utilities/fs.dart'; class SWBFileSystem { - Directory? rootPath; - Directory? startPath; - - String? filePath; - String? dirPath; - - final bool isDesktop = Util.isDesktop; + Directory? _startPath; Future prepareStorage() async { - if (Platform.isAndroid) { - rootPath = await StackFileSystem.wtfAndroidDocumentsPath(); - } else { - rootPath = await getApplicationDocumentsDirectory(); - } - //todo: check if print needed - // debugPrint(rootPath!.absolute.toString()); + if (_startPath != null) _startPath; + + final _rootPath = await getApplicationDocumentsDirectory(); late Directory sampleFolder; const dirName = "${AppConfig.prefix}_backup"; if (Platform.isIOS) { - sampleFolder = Directory(rootPath!.path); + sampleFolder = Directory(_rootPath.path); } else if (Platform.isAndroid || Platform.isLinux || Platform.isWindows || Platform.isMacOS) { - sampleFolder = Directory(path.join(rootPath!.path, dirName)); + sampleFolder = Directory(path.join(_rootPath.path, dirName)); } - try { - if (!sampleFolder.existsSync()) { - sampleFolder.createSync(recursive: true); - } - } catch (e, s) { - // todo: come back to this - debugPrint("$e $s"); + if (!sampleFolder.existsSync()) { + sampleFolder.createSync(recursive: true); } File sampleFile = File('${sampleFolder.path}/Backups_Go_Here.info'); if (Platform.isIOS) { - sampleFile = File('${rootPath!.path}/Backups_Go_Here.info'); + sampleFile = File('${_rootPath.path}/Backups_Go_Here.info'); } - try { - if (!sampleFile.existsSync()) { - sampleFile.createSync(); - } - } catch (e, s) { - // todo: come back to this - debugPrint("$e $s"); + if (!sampleFile.existsSync()) { + sampleFile.createSync(); } - startPath = sampleFolder; + + _startPath = sampleFolder; return sampleFolder; } - Future pickDir(BuildContext context) async { - final String? chosenPath; - if (Platform.isIOS) { - chosenPath = startPath?.path; - } else { - final String path = - Platform.isWindows - ? startPath!.path.replaceAll("/", "\\") - : startPath!.path; - chosenPath = await FilePicker.platform.getDirectoryPath( - dialogTitle: "Choose Backup location", - initialDirectory: path, - lockParentWindow: true, - ); - } - dirPath = chosenPath; + Future pickDir() { + return FS.pickDirectory( + initialDirectory: Platform.isWindows + ? _startPath?.path.replaceAll("/", "\\") + : _startPath?.path, + ); } - Future openFile(BuildContext context) async { + Future openFile() async { FilePickerResult? result; if (Platform.isAndroid) { result = await FilePicker.platform.pickFiles( dialogTitle: "Load backup file", - initialDirectory: startPath!.path, + initialDirectory: _startPath!.path, type: FileType.any, allowCompression: false, lockParentWindow: true, @@ -106,7 +75,7 @@ class SWBFileSystem { } else if (Platform.isIOS) { result = await FilePicker.platform.pickFiles( dialogTitle: "Load backup file", - initialDirectory: startPath!.path, + initialDirectory: _startPath!.path, type: FileType.any, allowCompression: false, lockParentWindow: true, @@ -114,7 +83,7 @@ class SWBFileSystem { } else { result = await FilePicker.platform.pickFiles( dialogTitle: "Load backup file", - initialDirectory: startPath!.path, + initialDirectory: _startPath!.path, type: FileType.custom, allowedExtensions: ['bin', 'swb'], allowCompression: false, @@ -122,6 +91,6 @@ class SWBFileSystem { ); } - filePath = result?.paths.first; + return result?.paths.first; } } diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index 3a7224aba1..1b8f4283be 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -163,14 +163,13 @@ class _RestoreFromFileViewState extends ConsumerState { try { await stackFileSystem.prepareStorage(); if (mounted) { - await stackFileSystem.openFile(context); - } + final filePath = await stackFileSystem.openFile(); - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.filePath ?? ""; - }); + if (mounted) { + setState(() { + fileLocationController.text = filePath ?? ""; + }); + } } } catch (e, s) { Logging.instance.e("$e\n$s", error: e, stackTrace: s); diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index 6c2a12e1d3..f503d0bee3 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -22,7 +22,6 @@ import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/prefs.dart'; import '../../utilities/show_loading.dart'; -import '../../utilities/stack_file_system.dart'; import '../../utilities/text_styles.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/desktop_app_bar.dart'; @@ -73,9 +72,7 @@ class _DesktopOrdinalDetailsViewState final bytes = response.bodyBytes; - final dir = Platform.isAndroid - ? await StackFileSystem.wtfAndroidDocumentsPath() - : await getApplicationDocumentsDirectory(); + final dir = await getApplicationDocumentsDirectory(); final filePath = path.join( dir.path, diff --git a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart index 1d672c51b2..f4f4de39e8 100644 --- a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart +++ b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart @@ -252,14 +252,13 @@ class _ForgottenPassphraseRestoreFromSWBState try { await stackFileSystem.prepareStorage(); if (mounted) { - await stackFileSystem.openFile(context); - } + final filePath = await stackFileSystem.openFile(); - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.filePath ?? ""; - }); + if (mounted) { + setState(() { + fileLocationController.text = filePath ?? ""; + }); + } } } catch (e, s) { Logging.instance.e("$e\n$s", error: e, stackTrace: s); diff --git a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart index ce1916714a..929c3c214f 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart @@ -31,6 +31,7 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/enums/backup_frequency_type.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; import '../../../../utilities/format.dart'; +import '../../../../utilities/fs.dart'; import '../../../../utilities/logger.dart'; import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; @@ -129,16 +130,11 @@ class _CreateAutoBackup extends ConsumerState { adkVersion, ); - if (Platform.isAndroid) { - // TODO SAF - File( - fileToSavePath, - ).writeAsStringSync(encryptedDataString, flush: true); - } else { - File( - fileToSavePath, - ).writeAsStringSync(encryptedDataString, flush: true); - } + await FS.writeStringToFile( + encryptedDataString, + pathToSave, + fileToSavePath.split("/").last, + ); return fileToSavePath; }(), @@ -323,16 +319,16 @@ class _CreateAutoBackup extends ConsumerState { : () async { try { await stackFileSystem.prepareStorage(); - - if (mounted) { - await stackFileSystem.pickDir(context); - } - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); + final filePath = await stackFileSystem + .openFile(); + + if (mounted) { + setState(() { + fileLocationController.text = + filePath ?? ""; + }); + } } } catch (e, s) { Logging.instance.e( diff --git a/lib/utilities/fs.dart b/lib/utilities/fs.dart new file mode 100644 index 0000000000..b1c1b84a35 --- /dev/null +++ b/lib/utilities/fs.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:path/path.dart'; +import 'package:saf_stream/saf_stream.dart'; +import 'package:saf_util/saf_util.dart'; + +abstract final class FS { + static Future pickDirectory({String? initialDirectory}) async { + final String? path; + if (Platform.isAndroid) { + final dir = await SafUtil().pickDirectory( + writePermission: true, + persistablePermission: true, + initialUri: initialDirectory, + ); + + path = dir?.uri; + } else { + path = await FilePicker.platform.getDirectoryPath( + lockParentWindow: true, + initialDirectory: initialDirectory, + ); + } + + return path; + } + + static Future writeStringToFile( + String content, + String dirPath, + String fileName, + ) { + if (Platform.isAndroid && dirPath.startsWith("content://")) { + return SafStream().writeFileBytes( + dirPath, + fileName, + "txt", + utf8.encode(content), + ); + } else { + return File(join(dirPath, fileName)).writeAsString(content, flush: true); + } + } +} diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index e135577392..292dda1368 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -238,18 +238,4 @@ abstract class StackFileSystem { return logsDir; } - - static Future wtfAndroidDocumentsPath() async { - const base = "/storage/emulated/"; - final rootDir = await applicationRootDirectory(); - final parts = rootDir.path.replaceFirst("/data/user/", "").split("/"); - if (parts.isNotEmpty) { - final id = int.tryParse(parts.first); - - if (id != null) { - return Directory(path.join(base, id.toString(), "Documents")); - } - } - throw Exception("Unsupported Android flavor"); - } } diff --git a/pubspec.lock b/pubspec.lock index fd2dde3709..bcd7d7bf7f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -872,12 +872,11 @@ packages: file_picker: dependency: "direct main" description: - path: "." - ref: b2849e63e1d418ad8d943c886cd3f4ed20d0ff23 - resolved-ref: b2849e63e1d418ad8d943c886cd3f4ed20d0ff23 - url: "https://github.com/cypherstack/flutter_file_picker.git" - source: git - version: "8.3.1" + name: file_picker + sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f + url: "https://pub.dev" + source: hosted + version: "10.3.3" fixnum: dependency: "direct main" description: @@ -1901,6 +1900,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + saf_stream: + dependency: "direct main" + description: + name: saf_stream + sha256: c05449997698c481a03e428162a999f93b1ee1bcc0349d651899a59f7b10230a + url: "https://pub.dev" + source: hosted + version: "0.12.3" + saf_util: + dependency: "direct main" + description: + name: saf_util + sha256: "219f983e5f17b28998335158cdc97add9d52af9884e38b5a43f10dcc070510ec" + url: "https://pub.dev" + source: hosted + version: "0.11.0" sec: dependency: transitive description: diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index 5649487960..d29dea1f41 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -171,10 +171,7 @@ dependencies: pointycastle: ^3.6.0 package_info_plus: ^8.0.2 lottie: ^2.3.2 - file_picker: - git: - url: https://github.com/cypherstack/flutter_file_picker.git - ref: b2849e63e1d418ad8d943c886cd3f4ed20d0ff23 + file_picker: ^10.3.3 connectivity_plus: ^4.0.1 isar_community: 3.3.0-dev.2 isar_community_flutter_libs: 3.3.0-dev.2 @@ -246,6 +243,8 @@ dependencies: path: ^1.9.1 mweb_client: ^0.2.0 fixnum: ^1.1.1 + saf_util: ^0.11.0 + saf_stream: ^0.12.3 dev_dependencies: flutter_test: From 2fa0f5b735d3826250b6cb9d0c6667c6ce65c739 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 22 Oct 2025 14:44:02 -0600 Subject: [PATCH 04/50] ensure eth wallet is fully synced/refreshed before first refresh of a token wallet hack --- .../impl/sub_wallets/eth_token_wallet.dart | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart index e45babf9f9..d101552300 100644 --- a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart +++ b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart @@ -110,10 +110,9 @@ class EthTokenWallet extends Wallet { inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), version: -1, - type: - addressTo == myAddress - ? TransactionType.sentToSelf - : TransactionType.outgoing, + type: addressTo == myAddress + ? TransactionType.sentToSelf + : TransactionType.outgoing, subType: TransactionSubType.ethToken, otherData: jsonEncode(otherData), ); @@ -131,6 +130,18 @@ class EthTokenWallet extends Wallet { FilterOperation? get receivingAddressFilterOperation => ethWallet.receivingAddressFilterOperation; + bool _unverifiedAndUntestedHackFlagThatMightFixAnIssue = true; + + @override + Future refresh() async { + if (_unverifiedAndUntestedHackFlagThatMightFixAnIssue) { + await ethWallet.refresh(); + _unverifiedAndUntestedHackFlagThatMightFixAnIssue = false; + } + + return super.refresh(); + } + @override Future init() async { try { @@ -217,11 +228,10 @@ class EthTokenWallet extends Wallet { // double check balance after internalSharedPrepareSend call to ensure // balance is up to date - final info = - await mainDB.isar.tokenWalletInfo - .where() - .walletIdTokenAddressEqualTo(walletId, tokenContract.address) - .findFirst(); + final info = await mainDB.isar.tokenWalletInfo + .where() + .walletIdTokenAddressEqualTo(walletId, tokenContract.address) + .findFirst(); final availableBalance = info?.getCachedBalance().spendable ?? Amount.zeroWith(fractionDigits: tokenContract.decimals); @@ -302,11 +312,10 @@ class EthTokenWallet extends Wallet { @override Future updateBalance() async { try { - final info = - await mainDB.isar.tokenWalletInfo - .where() - .walletIdTokenAddressEqualTo(walletId, tokenContract.address) - .findFirst(); + final info = await mainDB.isar.tokenWalletInfo + .where() + .walletIdTokenAddressEqualTo(walletId, tokenContract.address) + .findFirst(); final response = await EthereumAPI.getWalletTokenBalance( address: (await getCurrentReceivingAddress())!.value, contractAddress: tokenContract.address, From 8d48d7ad0fc31171dc0818ab89130bda6c225755 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Oct 2025 10:13:20 -0600 Subject: [PATCH 05/50] WIP: basic electrum mnemonic utils --- lib/utilities/electrum_seed_utils.dart | 1098 +++++++++++++++++ pubspec.lock | 2 +- .../templates/pubspec.template.yaml | 1 + test/utilities/electrum_seed_utils_test.dart | 302 +++++ 4 files changed, 1402 insertions(+), 1 deletion(-) create mode 100644 lib/utilities/electrum_seed_utils.dart create mode 100644 test/utilities/electrum_seed_utils_test.dart diff --git a/lib/utilities/electrum_seed_utils.dart b/lib/utilities/electrum_seed_utils.dart new file mode 100644 index 0000000000..337987ad89 --- /dev/null +++ b/lib/utilities/electrum_seed_utils.dart @@ -0,0 +1,1098 @@ +import 'dart:typed_data'; + +import 'package:pointycastle/export.dart'; +import 'package:unorm_dart/unorm_dart.dart'; + +import 'extensions/extensions.dart'; + +abstract class ElectrumSeedUtils { + static const kSeedPrefix = "01"; // standard + static const kSeedPrefixSegwit = "100"; // segwit + static const kSeedPrefix2fa = "101"; // 2FA standard + static const kSeedPrefix2faSegwit = "102"; // 2FA segwit + + static Uint8List electrumMnemonicToSeedBytes( + final String mnemonic, { + final String passphrase = "", + }) { + final salt = Uint8List.fromList([ + ..."electrum".toUint8ListFromUtf8, + ...normalize(passphrase).toUint8ListFromUtf8, + ]); + + final kdf = PBKDF2KeyDerivator(HMac.withDigest(SHA512Digest())) + ..init(Pbkdf2Parameters(salt, 2048, 64)); + + return kdf.process(normalize(mnemonic).toUint8ListFromUtf8); + } + + // based on https://electrum.readthedocs.io/en/latest/seedphrase.html#version-number + static String electrumMnemonicVersion( + final String mnemonic, { + final String passphrase = "", + }) { + final normalized = normalize(mnemonic).toUint8ListFromUtf8; + + final hash = _hmacHex(normalized); + + final length = int.parse(hash[0], radix: 16) + 2; + + return hash.substring(0, length); + } + + static bool isNewSeed(final String mnemonic, {String prefix = kSeedPrefix}) { + final normalized = normalize(mnemonic).toUint8ListFromUtf8; + final hash = _hmacHex(normalized); + return hash.startsWith(prefix); + } + + static String normalize(final String mnemonic) { + final characters = String.fromCharCodes( + nfkd( + mnemonic, + ).toLowerCase().runes.where((e) => !_kNonZeroCCCCodeUnits.contains(e)), + ).split(RegExp(r"\s+")).join(" ").trim().split(""); + + final buffer = StringBuffer(); + + for (int i = 0; i < characters.length; i++) { + final char = characters[i]; + final isSpace = RegExp(r"\s").hasMatch(char); + assert(char.runes.length == 1); + + if (isSpace && i > 0 && i < characters.length - 1) { + final prev = characters[i - 1]; + final next = characters[i + 1]; + if (_isCJK(prev.runes.first) && _isCJK(next.runes.first)) { + continue; + } + } + + buffer.write(char); + } + + return buffer.toString(); + } + + static String _hmacHex(Uint8List message) => + (HMac.withDigest(SHA512Digest()) + ..init(KeyParameter("Seed version".toUint8ListFromUtf8))) + .process(message) + .toHex; + + static bool _isCJK(int code) { + for (final (min, max, _) in _kCjkIntervals) { + if (min <= code && code <= max) { + return true; + } + } + return false; + } +} + +// https://www.unicode.org/reports/tr44/tr44-34.html#Canonical_Combining_Class_Values +// generated from https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt +const _kNonZeroCCCCodeUnits = { + 768, + 769, + 770, + 771, + 772, + 773, + 774, + 775, + 776, + 777, + 778, + 779, + 780, + 781, + 782, + 783, + 784, + 785, + 786, + 787, + 788, + 789, + 790, + 791, + 792, + 793, + 794, + 795, + 796, + 797, + 798, + 799, + 800, + 801, + 802, + 803, + 804, + 805, + 806, + 807, + 808, + 809, + 810, + 811, + 812, + 813, + 814, + 815, + 816, + 817, + 818, + 819, + 820, + 821, + 822, + 823, + 824, + 825, + 826, + 827, + 828, + 829, + 830, + 831, + 832, + 833, + 834, + 835, + 836, + 837, + 838, + 839, + 840, + 841, + 842, + 843, + 844, + 845, + 846, + 848, + 849, + 850, + 851, + 852, + 853, + 854, + 855, + 856, + 857, + 858, + 859, + 860, + 861, + 862, + 863, + 864, + 865, + 866, + 867, + 868, + 869, + 870, + 871, + 872, + 873, + 874, + 875, + 876, + 877, + 878, + 879, + 1155, + 1156, + 1157, + 1158, + 1159, + 1425, + 1426, + 1427, + 1428, + 1429, + 1430, + 1431, + 1432, + 1433, + 1434, + 1435, + 1436, + 1437, + 1438, + 1439, + 1440, + 1441, + 1442, + 1443, + 1444, + 1445, + 1446, + 1447, + 1448, + 1449, + 1450, + 1451, + 1452, + 1453, + 1454, + 1455, + 1456, + 1457, + 1458, + 1459, + 1460, + 1461, + 1462, + 1463, + 1464, + 1465, + 1466, + 1467, + 1468, + 1469, + 1471, + 1473, + 1474, + 1476, + 1477, + 1479, + 1552, + 1553, + 1554, + 1555, + 1556, + 1557, + 1558, + 1559, + 1560, + 1561, + 1562, + 1611, + 1612, + 1613, + 1614, + 1615, + 1616, + 1617, + 1618, + 1619, + 1620, + 1621, + 1622, + 1623, + 1624, + 1625, + 1626, + 1627, + 1628, + 1629, + 1630, + 1631, + 1648, + 1750, + 1751, + 1752, + 1753, + 1754, + 1755, + 1756, + 1759, + 1760, + 1761, + 1762, + 1763, + 1764, + 1767, + 1768, + 1770, + 1771, + 1772, + 1773, + 1809, + 1840, + 1841, + 1842, + 1843, + 1844, + 1845, + 1846, + 1847, + 1848, + 1849, + 1850, + 1851, + 1852, + 1853, + 1854, + 1855, + 1856, + 1857, + 1858, + 1859, + 1860, + 1861, + 1862, + 1863, + 1864, + 1865, + 1866, + 2027, + 2028, + 2029, + 2030, + 2031, + 2032, + 2033, + 2034, + 2035, + 2045, + 2070, + 2071, + 2072, + 2073, + 2075, + 2076, + 2077, + 2078, + 2079, + 2080, + 2081, + 2082, + 2083, + 2085, + 2086, + 2087, + 2089, + 2090, + 2091, + 2092, + 2093, + 2137, + 2138, + 2139, + 2199, + 2200, + 2201, + 2202, + 2203, + 2204, + 2205, + 2206, + 2207, + 2250, + 2251, + 2252, + 2253, + 2254, + 2255, + 2256, + 2257, + 2258, + 2259, + 2260, + 2261, + 2262, + 2263, + 2264, + 2265, + 2266, + 2267, + 2268, + 2269, + 2270, + 2271, + 2272, + 2273, + 2275, + 2276, + 2277, + 2278, + 2279, + 2280, + 2281, + 2282, + 2283, + 2284, + 2285, + 2286, + 2287, + 2288, + 2289, + 2290, + 2291, + 2292, + 2293, + 2294, + 2295, + 2296, + 2297, + 2298, + 2299, + 2300, + 2301, + 2302, + 2303, + 2364, + 2381, + 2385, + 2386, + 2387, + 2388, + 2492, + 2509, + 2558, + 2620, + 2637, + 2748, + 2765, + 2876, + 2893, + 3021, + 3132, + 3149, + 3157, + 3158, + 3260, + 3277, + 3387, + 3388, + 3405, + 3530, + 3640, + 3641, + 3642, + 3656, + 3657, + 3658, + 3659, + 3768, + 3769, + 3770, + 3784, + 3785, + 3786, + 3787, + 3864, + 3865, + 3893, + 3895, + 3897, + 3953, + 3954, + 3956, + 3962, + 3963, + 3964, + 3965, + 3968, + 3970, + 3971, + 3972, + 3974, + 3975, + 4038, + 4151, + 4153, + 4154, + 4237, + 4957, + 4958, + 4959, + 5908, + 5909, + 5940, + 6098, + 6109, + 6313, + 6457, + 6458, + 6459, + 6679, + 6680, + 6752, + 6773, + 6774, + 6775, + 6776, + 6777, + 6778, + 6779, + 6780, + 6783, + 6832, + 6833, + 6834, + 6835, + 6836, + 6837, + 6838, + 6839, + 6840, + 6841, + 6842, + 6843, + 6844, + 6845, + 6847, + 6848, + 6849, + 6850, + 6851, + 6852, + 6853, + 6854, + 6855, + 6856, + 6857, + 6858, + 6859, + 6860, + 6861, + 6862, + 6863, + 6864, + 6865, + 6866, + 6867, + 6868, + 6869, + 6870, + 6871, + 6872, + 6873, + 6874, + 6875, + 6876, + 6877, + 6880, + 6881, + 6882, + 6883, + 6884, + 6885, + 6886, + 6887, + 6888, + 6889, + 6890, + 6891, + 6964, + 6980, + 7019, + 7020, + 7021, + 7022, + 7023, + 7024, + 7025, + 7026, + 7027, + 7082, + 7083, + 7142, + 7154, + 7155, + 7223, + 7376, + 7377, + 7378, + 7380, + 7381, + 7382, + 7383, + 7384, + 7385, + 7386, + 7387, + 7388, + 7389, + 7390, + 7391, + 7392, + 7394, + 7395, + 7396, + 7397, + 7398, + 7399, + 7400, + 7405, + 7412, + 7416, + 7417, + 7616, + 7617, + 7618, + 7619, + 7620, + 7621, + 7622, + 7623, + 7624, + 7625, + 7626, + 7627, + 7628, + 7629, + 7630, + 7631, + 7632, + 7633, + 7634, + 7635, + 7636, + 7637, + 7638, + 7639, + 7640, + 7641, + 7642, + 7643, + 7644, + 7645, + 7646, + 7647, + 7648, + 7649, + 7650, + 7651, + 7652, + 7653, + 7654, + 7655, + 7656, + 7657, + 7658, + 7659, + 7660, + 7661, + 7662, + 7663, + 7664, + 7665, + 7666, + 7667, + 7668, + 7669, + 7670, + 7671, + 7672, + 7673, + 7674, + 7675, + 7676, + 7677, + 7678, + 7679, + 8400, + 8401, + 8402, + 8403, + 8404, + 8405, + 8406, + 8407, + 8408, + 8409, + 8410, + 8411, + 8412, + 8417, + 8421, + 8422, + 8423, + 8424, + 8425, + 8426, + 8427, + 8428, + 8429, + 8430, + 8431, + 8432, + 11503, + 11504, + 11505, + 11647, + 11744, + 11745, + 11746, + 11747, + 11748, + 11749, + 11750, + 11751, + 11752, + 11753, + 11754, + 11755, + 11756, + 11757, + 11758, + 11759, + 11760, + 11761, + 11762, + 11763, + 11764, + 11765, + 11766, + 11767, + 11768, + 11769, + 11770, + 11771, + 11772, + 11773, + 11774, + 11775, + 12330, + 12331, + 12332, + 12333, + 12334, + 12335, + 12441, + 12442, + 42607, + 42612, + 42613, + 42614, + 42615, + 42616, + 42617, + 42618, + 42619, + 42620, + 42621, + 42654, + 42655, + 42736, + 42737, + 43014, + 43052, + 43204, + 43232, + 43233, + 43234, + 43235, + 43236, + 43237, + 43238, + 43239, + 43240, + 43241, + 43242, + 43243, + 43244, + 43245, + 43246, + 43247, + 43248, + 43249, + 43307, + 43308, + 43309, + 43347, + 43443, + 43456, + 43696, + 43698, + 43699, + 43700, + 43703, + 43704, + 43710, + 43711, + 43713, + 43766, + 44013, + 64286, + 65056, + 65057, + 65058, + 65059, + 65060, + 65061, + 65062, + 65063, + 65064, + 65065, + 65066, + 65067, + 65068, + 65069, + 65070, + 65071, + 66045, + 66272, + 66422, + 66423, + 66424, + 66425, + 66426, + 68109, + 68111, + 68152, + 68153, + 68154, + 68159, + 68325, + 68326, + 68900, + 68901, + 68902, + 68903, + 68969, + 68970, + 68971, + 68972, + 68973, + 69291, + 69292, + 69370, + 69371, + 69373, + 69374, + 69375, + 69446, + 69447, + 69448, + 69449, + 69450, + 69451, + 69452, + 69453, + 69454, + 69455, + 69456, + 69506, + 69507, + 69508, + 69509, + 69702, + 69744, + 69759, + 69817, + 69818, + 69888, + 69889, + 69890, + 69939, + 69940, + 70003, + 70080, + 70090, + 70197, + 70198, + 70377, + 70378, + 70459, + 70460, + 70477, + 70502, + 70503, + 70504, + 70505, + 70506, + 70507, + 70508, + 70512, + 70513, + 70514, + 70515, + 70516, + 70606, + 70607, + 70608, + 70722, + 70726, + 70750, + 70850, + 70851, + 71103, + 71104, + 71231, + 71350, + 71351, + 71467, + 71737, + 71738, + 71997, + 71998, + 72003, + 72160, + 72244, + 72263, + 72345, + 72767, + 73026, + 73028, + 73029, + 73111, + 73537, + 73538, + 90415, + 92912, + 92913, + 92914, + 92915, + 92916, + 92976, + 92977, + 92978, + 92979, + 92980, + 92981, + 92982, + 94192, + 94193, + 113822, + 119141, + 119142, + 119143, + 119144, + 119145, + 119149, + 119150, + 119151, + 119152, + 119153, + 119154, + 119163, + 119164, + 119165, + 119166, + 119167, + 119168, + 119169, + 119170, + 119173, + 119174, + 119175, + 119176, + 119177, + 119178, + 119179, + 119210, + 119211, + 119212, + 119213, + 119362, + 119363, + 119364, + 122880, + 122881, + 122882, + 122883, + 122884, + 122885, + 122886, + 122888, + 122889, + 122890, + 122891, + 122892, + 122893, + 122894, + 122895, + 122896, + 122897, + 122898, + 122899, + 122900, + 122901, + 122902, + 122903, + 122904, + 122907, + 122908, + 122909, + 122910, + 122911, + 122912, + 122913, + 122915, + 122916, + 122918, + 122919, + 122920, + 122921, + 122922, + 123023, + 123184, + 123185, + 123186, + 123187, + 123188, + 123189, + 123190, + 123566, + 123628, + 123629, + 123630, + 123631, + 124140, + 124141, + 124142, + 124143, + 124398, + 124399, + 124643, + 124646, + 124654, + 124655, + 124661, + 125136, + 125137, + 125138, + 125139, + 125140, + 125141, + 125142, + 125252, + 125253, + 125254, + 125255, + 125256, + 125257, + 125258, +}; + +// see https://github.com/spesmilo/electrum/blob/master/electrum/mnemonic.py#L39-L70 +// which references http://www.asahi-net.or.jp/~ax2s-kmtn/ref/unicode/e_asia.html +const _kCjkIntervals = [ + (0x4E00, 0x9FFF, "CJK Unified Ideographs"), + (0x3400, 0x4DBF, "CJK Unified Ideographs Extension A"), + (0x20000, 0x2A6DF, "CJK Unified Ideographs Extension B"), + (0x2A700, 0x2B73F, "CJK Unified Ideographs Extension C"), + (0x2B740, 0x2B81F, "CJK Unified Ideographs Extension D"), + (0xF900, 0xFAFF, "CJK Compatibility Ideographs"), + (0x2F800, 0x2FA1D, "CJK Compatibility Ideographs Supplement"), + (0x3190, 0x319F, "Kanbun"), + (0x2E80, 0x2EFF, "CJK Radicals Supplement"), + (0x2F00, 0x2FDF, "CJK Radicals"), + (0x31C0, 0x31EF, "CJK Strokes"), + (0x2FF0, 0x2FFF, "Ideographic Description Characters"), + (0xE0100, 0xE01EF, "Variation Selectors Supplement"), + (0x3100, 0x312F, "Bopomofo"), + (0x31A0, 0x31BF, "Bopomofo Extended"), + (0xFF00, 0xFFEF, "Halfwidth and Fullwidth Forms"), + (0x3040, 0x309F, "Hiragana"), + (0x30A0, 0x30FF, "Katakana"), + (0x31F0, 0x31FF, "Katakana Phonetic Extensions"), + (0x1B000, 0x1B0FF, "Kana Supplement"), + (0xAC00, 0xD7AF, "Hangul Syllables"), + (0x1100, 0x11FF, "Hangul Jamo"), + (0xA960, 0xA97F, "Hangul Jamo Extended A"), + (0xD7B0, 0xD7FF, "Hangul Jamo Extended B"), + (0x3130, 0x318F, "Hangul Compatibility Jamo"), + (0xA4D0, 0xA4FF, "Lisu"), + (0x16F00, 0x16F9F, "Miao"), + (0xA000, 0xA48F, "Yi Syllables"), + (0xA490, 0xA4CF, "Yi Radicals"), +]; diff --git a/pubspec.lock b/pubspec.lock index bcd7d7bf7f..3a81a8c7c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2254,7 +2254,7 @@ packages: source: hosted version: "2.2.2" unorm_dart: - dependency: transitive + dependency: "direct main" description: name: unorm_dart sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b" diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index d29dea1f41..742f44e600 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -245,6 +245,7 @@ dependencies: fixnum: ^1.1.1 saf_util: ^0.11.0 saf_stream: ^0.12.3 + unorm_dart: ^0.2.0 dev_dependencies: flutter_test: diff --git a/test/utilities/electrum_seed_utils_test.dart b/test/utilities/electrum_seed_utils_test.dart new file mode 100644 index 0000000000..a726cbb7c0 --- /dev/null +++ b/test/utilities/electrum_seed_utils_test.dart @@ -0,0 +1,302 @@ +import 'package:coinlib_flutter/coinlib_flutter.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/utilities/electrum_seed_utils.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; + +class _TestCase { + final String words, bip32Seed, seedVersion; + final String? lang, wordsHex, passphrase, passphraseHex; + + const _TestCase({ + required this.words, + required this.bip32Seed, + this.seedVersion = ElectrumSeedUtils.kSeedPrefix, + this.lang, + this.wordsHex, + this.passphrase, + this.passphraseHex, + }); +} + +// horror data sourced from https://github.com/spesmilo/electrum/blob/master/tests/test_wallet_vertical.py#L31-L33 +const kUnicodeHorror = + "₿ 😀 😈 う けたま わる w͢͢͝h͡o͢͡ ̸͢k̵͟n̴͘ǫw̸̛s͘ ̀́w͘͢ḩ̵a҉̡͢t ̧̕h́o̵r͏̵rors̡ ̶͡͠lį̶e͟͟ ̶͝in͢ ͏t̕h̷̡͟e ͟͟d̛a͜r̕͡k̢̨ ͡h̴e͏a̷̢̡rt́͏ ̴̷͠ò̵̶f̸ u̧͘ní̛͜c͢͏o̷͏d̸͢e̡͝?͞"; +const kUnicodeHorrorHex = + "e282bf20f09f988020f09f98882020202020e3818620e38191e3819fe381be20e3828fe382" + "8b2077cda2cda2cd9d68cda16fcda2cda120ccb8cda26bccb5cd9f6eccb4cd98c7ab77ccb8" + "cc9b73cd9820cc80cc8177cd98cda2e1b8a9ccb561d289cca1cda27420cca7cc9568cc816f" + "ccb572cd8fccb5726f7273cca120ccb6cda1cda06cc4afccb665cd9fcd9f20ccb6cd9d696e" + "cda220cd8f74cc9568ccb7cca1cd9f6520cd9fcd9f64cc9b61cd9c72cc95cda16bcca2cca8" + "20cda168ccb465cd8f61ccb7cca2cca17274cc81cd8f20ccb4ccb7cda0c3b2ccb5ccb666cc" + "b82075cca7cd986ec3adcc9bcd9c63cda2cd8f6fccb7cd8f64ccb8cda265cca1cd9d3fcd9e"; + +// test cases sourced from https://github.com/spesmilo/electrum/blob/master/tests/test_mnemonic.py +const kTestCases = { + "english": _TestCase( + words: + "wild father tree among universe such" + " mobile favorite target dynamic credit identify", + seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit, + bip32Seed: + "aac2a6302e48577ab4b46f23dbae0774e2e62c796f797d0a1b5faeb528301e3064342d" + "afb79069e7c4c6b8c38ae11d7a973bec0d4f70626f8cc5184a8d0b0756", + ), + "english_with_passphrase": _TestCase( + words: + "wild father tree among universe such" + " mobile favorite target dynamic credit identify", + seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit, + passphrase: "Did you ever hear the tragedy of Darth Plagueis the Wise?", + bip32Seed: + "4aa29f2aeb0127efb55138ab9e7be83b36750358751906f86c662b21a1ea1370f949e6" + "d1a12fa56d3d93cadda93038c76ac8118597364e46f5156fde6183c82f", + ), + "japanese": _TestCase( + lang: "ja", + words: "なのか ひろい しなん まなぶ つぶす さがす おしゃれ かわく おいかける けさき かいとう さたん", + wordsHex: + "e381aae381aee3818b20e381b2e3828de3818420e38197e381aae3829320e381bee381" + "aae381b5e3829920e381a4e381b5e38299e3819920e38195e3818be38299e38199" + "20e3818ae38197e38283e3828c20e3818be3828fe3818f20e3818ae38184e3818b" + "e38191e3828b20e38191e38195e3818d20e3818be38184e381a8e3818620e38195" + "e3819fe38293", + bip32Seed: + "d3eaf0e44ddae3a5769cb08a26918e8b308258bcb057bb704c6f69713245c0b35cb92c" + "03df9c9ece5eff826091b4e74041e010b701d44d610976ce8bfb66a8ad", + ), + "japanese_with_passphrase": _TestCase( + lang: "ja", + words: "なのか ひろい しなん まなぶ つぶす さがす おしゃれ かわく おいかける けさき かいとう さたん", + wordsHex: + "e381aae381aee3818b20e381b2e3828de3818420e38197e381aae3829320e381bee381" + "aae381b5e3829920e381a4e381b5e38299e3819920e38195e3818be38299e38199" + "20e3818ae38197e38283e3828c20e3818be3828fe3818f20e3818ae38184e3818b" + "e38191e3828b20e38191e38195e3818d20e3818be38184e381a8e3818620e38195" + "e3819fe38293", + passphrase: kUnicodeHorror, + passphraseHex: kUnicodeHorrorHex, + bip32Seed: + "251ee6b45b38ba0849e8f40794540f7e2c6d9d604c31d68d3ac50c034f8b64e4bc037c" + "5e1e985a2fed8aad23560e690b03b120daf2e84dceb1d7857dda042457", + ), + "chinese": _TestCase( + lang: "zh", + words: "眼 悲 叛 改 节 跃 衡 响 疆 股 遂 冬", + wordsHex: + "e79cbc20e682b220e58f9b20e694b920e88a8220e8b78320e8a1a120e5938d20e79686" + "20e882a120e9818220e586ac", + seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit, + bip32Seed: + "0b9077db7b5a50dbb6f61821e2d35e255068a5847e221138048a20e12d80b673ce306b" + "6fe7ac174ebc6751e11b7037be6ee9f17db8040bb44f8466d519ce2abf", + ), + "chinese_with_passphrase": _TestCase( + lang: "zh", + words: "眼 悲 叛 改 节 跃 衡 响 疆 股 遂 冬", + wordsHex: + "e79cbc20e682b220e58f9b20e694b920e88a8220e8b78320e8a1a120e5938d20e79686" + "20e882a120e9818220e586ac", + seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit, + passphrase: "给我一些测试向量谷歌", + passphraseHex: + "e7bb99e68891e4b880e4ba9be6b58be8af95e59091e9878fe8b0b7e6ad8c", + bip32Seed: + "6c03dd0615cf59963620c0af6840b52e867468cc64f20a1f4c8155705738e87b8edb0f" + "c8a6cee4085776cb3a629ff88bb1a38f37085efdbf11ce9ec5a7fa5f71", + ), + "spanish": _TestCase( + lang: "es", + words: + "almíbar tibio superar vencer hacha peatón" + " príncipe matar consejo polen vehículo odisea", + wordsHex: + "616c6d69cc8162617220746962696f20737570657261722076656e6365722068616368" + "6120706561746fcc816e20707269cc816e63697065206d6174617220636f6e7365" + "6a6f20706f6c656e2076656869cc8163756c6f206f6469736561", + bip32Seed: + "18bffd573a960cc775bbd80ed60b7dc00bc8796a186edebe7fc7cf1f316da0fe937852" + "a969c5c79ded8255cdf54409537a16339fbe33fb9161af793ea47faa7a", + ), + "spanish_with_passphrase": _TestCase( + lang: "es", + words: + "almíbar tibio superar vencer hacha peatón " + "príncipe matar consejo polen vehículo odisea", + wordsHex: + "616c6d69cc8162617220746962696f20737570657261722076656e6365722068616368" + "6120706561746fcc816e20707269cc816e63697065206d6174617220636f6e73656a6f" + "20706f6c656e2076656869cc8163756c6f206f6469736561", + passphrase: "araña difícil solución término cárcel", + passphraseHex: + "6172616ecc83612064696669cc8163696c20736f6c7563696fcc816e207465cc81726d" + "696e6f206361cc817263656c", + bip32Seed: + "363dec0e575b887cfccebee4c84fca5a3a6bed9d0e099c061fa6b85020b031f8fe3636" + "d9af187bf432d451273c625e20f24f651ada41aae2c4ea62d87e9fa44c", + ), + "spanish2": _TestCase( + lang: "es", + words: + "equipo fiar auge langosta hacha calor " + "trance cubrir carro pulmón oro áspero", + wordsHex: + "65717569706f20666961722061756765206c616e676f7374612068616368612063616c" + "6f72207472616e63652063756272697220636172726f2070756c6d6fcc816e206f" + "726f2061cc81737065726f", + seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit, + bip32Seed: + "001ebce6bfde5851f28a0d44aae5ae0c762b600daf3b33fc8fc630aee0d207646b6f98" + "b18e17dfe3be0a5efe2753c7cdad95860adbbb62cecad4dedb88e02a64", + ), + "spanish3": _TestCase( + lang: "es", + words: + "vidrio jabón muestra pájaro capucha" + " eludir feliz rotar fogata pez rezar oír", + wordsHex: + "76696472696f206a61626fcc816e206d756573747261207061cc816a61726f20636170" + "7563686120656c756469722066656c697a20726f74617220666f67617461207065" + "7a2072657a6172206f69cc8172", + seedVersion: ElectrumSeedUtils.kSeedPrefixSegwit, + passphrase: + "¡Viva España! repiten veinte pueblos y al hablar dan fe " + "del ánimo español... ¡Marquen arado martillo y clarín", + passphraseHex: + "c2a1566976612045737061c3b16121207265706974656e207665696e74652070756562" + "6c6f73207920616c206861626c61722064616e2066652064656c20c3a16e696d6f" + "2065737061c3b16f6c2e2e2e20c2a14d61727175656e20617261646f206d617274" + "696c6c6f207920636c6172c3ad6e", + bip32Seed: + "c274665e5453c72f82b8444e293e048d700c59bf000cacfba597629d202dcf3aab1cf9" + "c00ba8d3456b7943428541fed714d01d8a0a4028fc3a9bb33d981cb49f", + ), +}; + +void main() { + const kElectrumMnemonic = + "party reward jealous build maze tunnel eternal candy recipe february kid animal"; + + test( + "standard seed prefix", + () => expect(ElectrumSeedUtils.kSeedPrefix, "01"), + ); + test( + "segwit seed prefix", + () => expect(ElectrumSeedUtils.kSeedPrefixSegwit, "100"), + ); + test( + "2fa standard seed prefix", + () => expect(ElectrumSeedUtils.kSeedPrefix2fa, "101"), + ); + test( + "2fa segwit seed prefix", + () => expect(ElectrumSeedUtils.kSeedPrefix2faSegwit, "102"), + ); + + group("electrum mnemonic to seed tests", () { + for (final entry in kTestCases.entries) { + final name = entry.key; + final testCase = entry.value; + + if (testCase.wordsHex != null) { + test("$name: mnemonic to bytes to hex", () { + expect(testCase.wordsHex, testCase.words.toUint8ListFromUtf8.toHex); + }); + } + + if (testCase.passphraseHex != null && testCase.passphrase != null) { + test("$name: passphrase to bytes to hex", () { + expect( + testCase.passphraseHex, + testCase.passphrase!.toUint8ListFromUtf8.toHex, + ); + }); + } + + test("$name: isNewSeed", () { + expect( + ElectrumSeedUtils.isNewSeed( + testCase.words, + prefix: testCase.seedVersion, + ), + true, + ); + }); + test("$name: electrumMnemonicToSeedBytes", () { + expect( + ElectrumSeedUtils.electrumMnemonicToSeedBytes( + testCase.words, + passphrase: testCase.passphrase ?? "", + ).toHex, + testCase.bip32Seed, + ); + }); + } + }); + + test("test segwit version", () async { + expect( + ElectrumSeedUtils.electrumMnemonicVersion(kElectrumMnemonic), + ElectrumSeedUtils.kSeedPrefixSegwit, + ); + }); + + group("test group requires coinlib", () { + setUpAll(() => loadCoinlib()); + + test("test master electrum fingerprint", () async { + final bytes = ElectrumSeedUtils.electrumMnemonicToSeedBytes( + kElectrumMnemonic, + ); + final hd = HDPrivateKey.fromSeed(bytes); + expect(BigInt.from(hd.fingerprint).toHex, "ec8d82aa"); + }); + + test("test root zpub", () async { + final bytes = ElectrumSeedUtils.electrumMnemonicToSeedBytes( + kElectrumMnemonic, + ); + final hd = HDPrivateKey.fromSeed(bytes); + final master = hd.derivePath("m/0'"); + + const zpubHDVersion = + 0x04b24746; // https://github.com/satoshilabs/slips/blob/master/slip-0132.md + expect( + master.hdPublicKey.encode(zpubHDVersion), + "zpub6oHsSqJH7vSzDJTFB8NR4YpzFU13XRmkJaVW9jQTePrnf5BPHHAQXxBMiBot12Z7DqfuTykmyPxGowrQfNa7M8xiAdEvQG47V5jhx5Tk158", + ); + }); + + test("test first receiving address", () async { + final bytes = ElectrumSeedUtils.electrumMnemonicToSeedBytes( + kElectrumMnemonic, + ); + final hd = HDPrivateKey.fromSeed(bytes); + final master = hd.derivePath("m/0'"); + + expect( + P2WPKHAddress.fromHash( + hash160(master.derivePath("0/0").publicKey.data), + hrp: "bc", + ).toString(), + "bc1qgfjuzurxzhl9vdalmjgw68s680lj5q933k37h5", + ); + }); + + test("test 9th change address", () async { + final bytes = ElectrumSeedUtils.electrumMnemonicToSeedBytes( + kElectrumMnemonic, + ); + final hd = HDPrivateKey.fromSeed(bytes); + final master = hd.derivePath("m/0'"); + + expect( + P2WPKHAddress.fromHash( + hash160(master.derivePath("1/8").publicKey.data), + hrp: "bc", + ).toString(), + "bc1qzz0mvhza5sdd2fy77klh3w8h5z238avztvqjdx", + ); + }); + }); +} From cb198ed50ea4468726853ffdc34b9e80454d760b Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Oct 2025 15:32:17 -0600 Subject: [PATCH 06/50] dart autoformat --- .../electrumx_interface.dart | 341 +++++++++--------- 1 file changed, 168 insertions(+), 173 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 26106e6778..0af68f2a3c 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -152,9 +152,9 @@ mixin ElectrumXInterface if (txData.type == TxType.mweb || txData.type == TxType.mwebPegOut) { if (utxos == null) { final db = Drift.get(walletId); - final mwebUtxos = - await (db.select(db.mwebUtxos) - ..where((e) => e.used.equals(false))).get(); + final mwebUtxos = await (db.select( + db.mwebUtxos, + )..where((e) => e.used.equals(false))).get(); availableOutputs = mwebUtxos.map((e) => MwebInput(e)).toList(); } else { @@ -172,23 +172,22 @@ mixin ElectrumXInterface final canCPFP = this is CpfpInterface && coinControl; - final spendableOutputs = - availableOutputs.where((e) { - if (e is StandardInput) { - return !e.utxo.isBlocked && - (e.utxo.used != true) && - (canCPFP || - e.utxo.isConfirmed( - currentChainHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - )); - } else if (e is MwebInput) { - return !e.utxo.blocked && !e.utxo.used; - } else { - return false; - } - }).toList(); + final spendableOutputs = availableOutputs.where((e) { + if (e is StandardInput) { + return !e.utxo.isBlocked && + (e.utxo.used != true) && + (canCPFP || + e.utxo.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + )); + } else if (e is MwebInput) { + return !e.utxo.blocked && !e.utxo.used; + } else { + return false; + } + }).toList(); final spendableSatoshiValue = spendableOutputs.fold( BigInt.zero, (p, e) => p + e.value, @@ -296,16 +295,15 @@ mixin ElectrumXInterface final int vSizeForOneOutput; try { - vSizeForOneOutput = - (await buildTransaction( - inputsWithKeys: inputsWithKeys, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress], - [satoshisBeingUsed - BigInt.one], - ), - ), - )).vSize!; + vSizeForOneOutput = (await buildTransaction( + inputsWithKeys: inputsWithKeys, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshisBeingUsed - BigInt.one], + ), + ), + )).vSize!; } catch (e, s) { Logging.instance.e("vSizeForOneOutput: $e", error: e, stackTrace: s); rethrow; @@ -316,22 +314,21 @@ mixin ElectrumXInterface BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b; try { - vSizeForTwoOutPuts = - (await buildTransaction( - inputsWithKeys: inputsWithKeys, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress, (await changeAddress()).value], - [ - satoshiAmountToSend, - maxBI( - BigInt.zero, - satoshisBeingUsed - (satoshiAmountToSend + BigInt.one), - ), - ], + vSizeForTwoOutPuts = (await buildTransaction( + inputsWithKeys: inputsWithKeys, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress, (await changeAddress()).value], + [ + satoshiAmountToSend, + maxBI( + BigInt.zero, + satoshisBeingUsed - (satoshiAmountToSend + BigInt.one), ), - ), - )).vSize!; + ], + ), + ), + )).vSize!; } catch (e, s) { Logging.instance.e("vSizeForTwoOutPuts: $e", error: e, stackTrace: s); rethrow; @@ -344,9 +341,9 @@ mixin ElectrumXInterface satsPerVByte != null ? (satsPerVByte * vSizeForOneOutput) : estimateTxFee( - vSize: vSizeForOneOutput, - feeRatePerKB: selectedTxFeeRate, - ), + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ), ); // Assume 2 outputs, one for recipient and one for change final feeForTwoOutputs = @@ -355,9 +352,9 @@ mixin ElectrumXInterface satsPerVByte != null ? (satsPerVByte * vSizeForTwoOutPuts) : estimateTxFee( - vSize: vSizeForTwoOutPuts, - feeRatePerKB: selectedTxFeeRate, - ), + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ), ); Logging.instance.d("feeForTwoOutputs: $feeForTwoOutputs"); @@ -513,28 +510,30 @@ mixin ElectrumXInterface BigInt feeForOneOutput; if (overrideFeeAmount == null) { - final int vSizeForOneOutput = - (await buildTransaction( - inputsWithKeys: inputsWithKeys, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress], - [satoshisBeingUsed - BigInt.one], - ), - ), - )).vSize!; + final int vSizeForOneOutput = (await buildTransaction( + inputsWithKeys: inputsWithKeys, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshisBeingUsed - BigInt.one], + ), + ), + )).vSize!; feeForOneOutput = BigInt.from( satsPerVByte != null ? (satsPerVByte * vSizeForOneOutput) : estimateTxFee( - vSize: vSizeForOneOutput, - feeRatePerKB: feeRatePerKB, - ), + vSize: vSizeForOneOutput, + feeRatePerKB: feeRatePerKB, + ), ); if (satsPerVByte == null) { - final roughEstimate = - roughFeeEstimate(inputsWithKeys.length, 1, feeRatePerKB).raw; + final roughEstimate = roughFeeEstimate( + inputsWithKeys.length, + 1, + feeRatePerKB, + ).raw; if (feeForOneOutput < roughEstimate) { feeForOneOutput = roughEstimate; } @@ -604,8 +603,8 @@ mixin ElectrumXInterface final code = await (this as PaynymInterface) .paymentCodeStringByKey(address.otherData!); - final bip47base = - await (this as PaynymInterface).getBip47BaseNode(); + final bip47base = await (this as PaynymInterface) + .getBip47BaseNode(); final privateKey = await (this as PaynymInterface) .getPrivateKeyForPaynymReceivingAddress( @@ -664,10 +663,9 @@ mixin ElectrumXInterface ); // TODO: [prio=high]: check this opt in rbf - final sequence = - this is RbfInterface && (this as RbfInterface).flagOptInRBF - ? 0xffffffff - 10 - : 0xffffffff - 1; + final sequence = this is RbfInterface && (this as RbfInterface).flagOptInRBF + ? 0xffffffff - 10 + : 0xffffffff - 1; bool isMweb = false; bool hasNonWitnessInput = false; @@ -907,44 +905,43 @@ mixin ElectrumXInterface raw: clTx.toHex(), // dirty shortcut for peercoin's weirdness vSize: this is PeercoinWallet ? clTx.size : clTx.vSize(), - tempTx: - txData.type == TxType.mwebPegIn - ? null - : txData.type.isMweb() - ? TransactionV2( - walletId: walletId, - blockHash: null, - hash: clTx.hashHex, - txid: clTx.txid, - height: null, - timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, - inputs: List.unmodifiable(tempInputs), - outputs: List.unmodifiable(tempOutputs), - version: clTx.version, - type: TransactionType.outgoing, - subType: TransactionSubType.mweb, - otherData: null, - ) - : TransactionV2( - walletId: walletId, - blockHash: null, - hash: clTx.hashHex, - txid: clTx.txid, - height: null, - timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, - inputs: List.unmodifiable(tempInputs), - outputs: List.unmodifiable(tempOutputs), - version: clTx.version, - type: - tempOutputs - .map((e) => e.walletOwns) - .fold(true, (p, e) => p &= e) && - txData.paynymAccountLite == null - ? TransactionType.sentToSelf - : TransactionType.outgoing, - subType: TransactionSubType.none, - otherData: null, - ), + tempTx: txData.type == TxType.mwebPegIn + ? null + : txData.type.isMweb() + ? TransactionV2( + walletId: walletId, + blockHash: null, + hash: clTx.hashHex, + txid: clTx.txid, + height: null, + timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(tempInputs), + outputs: List.unmodifiable(tempOutputs), + version: clTx.version, + type: TransactionType.outgoing, + subType: TransactionSubType.mweb, + otherData: null, + ) + : TransactionV2( + walletId: walletId, + blockHash: null, + hash: clTx.hashHex, + txid: clTx.txid, + height: null, + timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(tempInputs), + outputs: List.unmodifiable(tempOutputs), + version: clTx.version, + type: + tempOutputs + .map((e) => e.walletOwns) + .fold(true, (p, e) => p &= e) && + txData.paynymAccountLite == null + ? TransactionType.sentToSelf + : TransactionType.outgoing, + subType: TransactionSubType.none, + otherData: null, + ), ); } @@ -1023,21 +1020,20 @@ mixin ElectrumXInterface } Future updateElectrumX() async { - final failovers = - nodeService - .failoverNodesFor(currency: cryptoCurrency) - .map( - (e) => ElectrumXNode( - address: e.host, - port: e.port, - name: e.name, - id: e.id, - useSSL: e.useSSL, - torEnabled: e.torEnabled, - clearnetEnabled: e.clearnetEnabled, - ), - ) - .toList(); + final failovers = nodeService + .failoverNodesFor(currency: cryptoCurrency) + .map( + (e) => ElectrumXNode( + address: e.host, + port: e.port, + name: e.name, + id: e.id, + useSSL: e.useSSL, + torEnabled: e.torEnabled, + clearnetEnabled: e.clearnetEnabled, + ), + ) + .toList(); final newNode = await _getCurrentElectrumXNode(); try { @@ -1118,10 +1114,12 @@ mixin ElectrumXInterface publicKey: keys.publicKey.data, type: addressData.addressType, derivationIndex: index + j, - derivationPath: - isViewOnly ? null : (DerivationPath()..value = derivePath), - subType: - chain == 0 ? AddressSubType.receiving : AddressSubType.change, + derivationPath: isViewOnly + ? null + : (DerivationPath()..value = derivePath), + subType: chain == 0 + ? AddressSubType.receiving + : AddressSubType.change, ); addressArray.add(address); @@ -1199,8 +1197,9 @@ mixin ElectrumXInterface publicKey: keys.publicKey.data, type: addressData.addressType, derivationIndex: index, - derivationPath: - isViewOnly ? null : (DerivationPath()..value = derivePath), + derivationPath: isViewOnly + ? null + : (DerivationPath()..value = derivePath), subType: chain == 0 ? AddressSubType.receiving : AddressSubType.change, ); @@ -1391,21 +1390,18 @@ mixin ElectrumXInterface numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: - Amount.fromDecimal( - fast, - fractionDigits: info.coin.fractionDigits, - ).raw, - medium: - Amount.fromDecimal( - medium, - fractionDigits: info.coin.fractionDigits, - ).raw, - slow: - Amount.fromDecimal( - slow, - fractionDigits: info.coin.fractionDigits, - ).raw, + fast: Amount.fromDecimal( + fast, + fractionDigits: info.coin.fractionDigits, + ).raw, + medium: Amount.fromDecimal( + medium, + fractionDigits: info.coin.fractionDigits, + ).raw, + slow: Amount.fromDecimal( + slow, + fractionDigits: info.coin.fractionDigits, + ).raw, ); Logging.instance.d("fetched fees: $feeObject"); @@ -1691,8 +1687,8 @@ mixin ElectrumXInterface await mainDB.updateOrPutAddresses(addressesToStore); if (this is PaynymInterface) { - final notificationAddress = - await (this as PaynymInterface).getMyNotificationAddress(); + final notificationAddress = await (this as PaynymInterface) + .getMyNotificationAddress(); await (this as BitcoinWallet).updateTransactions( overrideAddresses: [notificationAddress], @@ -1824,19 +1820,18 @@ mixin ElectrumXInterface Logging.instance.d("Sent txHash: $txHash"); txData = txData.copyWith( - usedUTXOs: - txData.usedUTXOs!.map((e) { - if (e is StandardInput) { - return StandardInput( - e.utxo.copyWith(used: true), - derivePathType: e.derivePathType, - ); - } else if (e is MwebInput) { - return MwebInput(e.utxo.copyWith(used: true)); - } else { - return e; - } - }).toList(), + usedUTXOs: txData.usedUTXOs!.map((e) { + if (e is StandardInput) { + return StandardInput( + e.utxo.copyWith(used: true), + derivePathType: e.derivePathType, + ); + } else if (e is MwebInput) { + return MwebInput(e.utxo.copyWith(used: true)); + } else { + return e; + } + }).toList(), // TODO revisit setting these both txHash: txHash, @@ -1870,8 +1865,8 @@ mixin ElectrumXInterface final balance = txData.type == TxType.mweb || txData.type == TxType.mwebPegOut - ? info.cachedBalanceSecondary - : info.cachedBalance; + ? info.cachedBalanceSecondary + : info.cachedBalance; final feeRateType = txData.feeRateType; final customSatsPerVByte = txData.satsPerVByte; final feeRateAmount = txData.feeRateAmount; @@ -2173,11 +2168,11 @@ mixin ElectrumXInterface receiveFutures.add( canBatch ? checkGapsBatched( - txCountBatchSize, - root, - type, - receiveChain, - ) + txCountBatchSize, + root, + type, + receiveChain, + ) : checkGapsLinearly(root, type, receiveChain), ); } @@ -2197,11 +2192,11 @@ mixin ElectrumXInterface changeFutures.add( canBatch ? checkGapsBatched( - txCountBatchSize, - root, - type, - changeChain, - ) + txCountBatchSize, + root, + type, + changeChain, + ) : checkGapsLinearly(root, type, changeChain), ); } From ff9e8f0b4c1b28378a1e6ce26af14f3ee1d0bcd4 Mon Sep 17 00:00:00 2001 From: julian Date: Sat, 25 Oct 2025 16:13:27 -0600 Subject: [PATCH 07/50] ensure tor singleton --- .../TOR_tor_service_impl.template.dart | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tool/wl_templates/TOR_tor_service_impl.template.dart b/tool/wl_templates/TOR_tor_service_impl.template.dart index 3e26cafa2c..63ee6ef0c5 100644 --- a/tool/wl_templates/TOR_tor_service_impl.template.dart +++ b/tool/wl_templates/TOR_tor_service_impl.template.dart @@ -21,10 +21,15 @@ FusionTorService _getFusionInterface() => throw Exception("TOR not enabled!"); //END_OFF //ON -TorService _getInterface() => _TorServiceImpl(); -FusionTorService _getFusionInterface() => _FusionTorServiceImpl(); +TorService _getInterface() => _TorServiceImpl.instance; +FusionTorService _getFusionInterface() => _FusionTorServiceImpl.instance; class _TorServiceImpl extends TorService { + static _TorServiceImpl? _instance; + static _TorServiceImpl get instance => _instance ??= _TorServiceImpl._(); + + _TorServiceImpl._(); + Tor? _tor; String? _torDataDirPath; TorConnectionStatus _status = TorConnectionStatus.disconnected; @@ -131,6 +136,12 @@ class _TorServiceImpl extends TorService { } class _FusionTorServiceImpl extends FusionTorService { + static _FusionTorServiceImpl? _instance; + static _FusionTorServiceImpl get instance => + _instance ??= _FusionTorServiceImpl._(); + + _FusionTorServiceImpl._(); + Tor? _tor; String? _torDataDirPath; From 28cacaab8d2148cb42bbe0e518135b6c56f8abf6 Mon Sep 17 00:00:00 2001 From: Julian Date: Thu, 30 Oct 2025 08:16:46 -0600 Subject: [PATCH 08/50] fix: mwc list position --- scripts/app_config/configure_stack_wallet.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/app_config/configure_stack_wallet.sh b/scripts/app_config/configure_stack_wallet.sh index cbcead4841..6a4708d785 100755 --- a/scripts/app_config/configure_stack_wallet.sh +++ b/scripts/app_config/configure_stack_wallet.sh @@ -91,11 +91,11 @@ final List _supportedCoins = List.unmodifiable([ Dogecoin(CryptoCurrencyNetwork.main), Ecash(CryptoCurrencyNetwork.main), Epiccash(CryptoCurrencyNetwork.main), - if (!Platform.isMacOS) Mimblewimblecoin(CryptoCurrencyNetwork.main), Ethereum(CryptoCurrencyNetwork.main), Fact0rn(CryptoCurrencyNetwork.main), Firo(CryptoCurrencyNetwork.main), Litecoin(CryptoCurrencyNetwork.main), + if (!Platform.isMacOS) Mimblewimblecoin(CryptoCurrencyNetwork.main), Nano(CryptoCurrencyNetwork.main), Namecoin(CryptoCurrencyNetwork.main), Particl(CryptoCurrencyNetwork.main), From 1a30f6fe23c620ad04b99692d8451ac71c9e69b9 Mon Sep 17 00:00:00 2001 From: Julian Date: Thu, 30 Oct 2025 08:22:03 -0600 Subject: [PATCH 09/50] fix(win/linux): mwc var in cmake templates --- scripts/app_config/templates/linux/CMakeLists.txt | 2 +- scripts/app_config/templates/windows/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/app_config/templates/linux/CMakeLists.txt b/scripts/app_config/templates/linux/CMakeLists.txt index 4b0e69d628..cd44acde4b 100644 --- a/scripts/app_config/templates/linux/CMakeLists.txt +++ b/scripts/app_config/templates/linux/CMakeLists.txt @@ -10,7 +10,7 @@ set(BINARY_NAME "place_holder") set(APPLICATION_ID "com.place.holder") set(INCLUDE_EPIC_SO INCLUDE_EPIC_SO_FLAG) -set(INCLUDE_MWC_SO INCLUDE_EPIC_SO_FLAG) +set(INCLUDE_MWC_SO INCLUDE_MWC_SO_FLAG) # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/scripts/app_config/templates/windows/CMakeLists.txt b/scripts/app_config/templates/windows/CMakeLists.txt index edc7b5a5b7..a33fe23bb5 100644 --- a/scripts/app_config/templates/windows/CMakeLists.txt +++ b/scripts/app_config/templates/windows/CMakeLists.txt @@ -7,7 +7,7 @@ project(place_holder LANGUAGES CXX) set(BINARY_NAME "place_holder") set(INCLUDE_EPIC_SO INCLUDE_EPIC_SO_FLAG) -set(INCLUDE_MWC_SO INCLUDE_EPIC_SO_FLAG) +set(INCLUDE_MWC_SO INCLUDE_MWC_SO_FLAG) # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. From 3bc921f8ca8f97bba6d1a5556634509f0f02f0e5 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 31 Oct 2025 09:27:48 -0600 Subject: [PATCH 10/50] ensure if a certain app coin is not found that the price call still works for the others --- lib/services/price.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/services/price.dart b/lib/services/price.dart index a71c7e201e..e20d96a2a3 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -159,9 +159,19 @@ class PriceAPI { for (final map in coinGeckoData) { final String coinName = map["name"] as String; - final coin = AppConfig.getCryptoCurrencyByPrettyName( - coinName == "Factor" ? "Fact0rn" : coinName, - ); + late CryptoCurrency coin; + try { + coin = AppConfig.getCryptoCurrencyByPrettyName( + coinName == "Factor" ? "Fact0rn" : coinName, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to find matching app coin for $coinName. Moving on", + error: e, + stackTrace: s, + ); + continue; + } try { final price = Decimal.parse(map["current_price"].toString()); From 30730b89e76dff1247a7fbecb8339cd00054b882 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 31 Oct 2025 15:21:48 -0600 Subject: [PATCH 11/50] standalone mwebd for windows --- .gitignore | 3 + docs/building.md | 11 +- lib/app_config.dart | 2 + lib/services/mwebd_service.dart | 12 +- lib/utilities/dynamic_object.dart | 25 ++++ .../interfaces/mwebd_server_interface.dart | 57 +-------- pubspec.lock | 12 +- scripts/app_config/configure_campfire.sh | 2 + scripts/app_config/configure_stack_duo.sh | 2 + scripts/app_config/configure_stack_wallet.sh | 12 ++ .../templates/pubspec.template.yaml | 4 + test/utilities/dynamic_object_test.dart | 23 ++++ tool/build_standalone_mwebd_windows.dart | 99 ++++++++++++++++ ..._mwebd_server_interface_impl.template.dart | 108 +++++++++++++----- 14 files changed, 269 insertions(+), 103 deletions(-) create mode 100644 lib/utilities/dynamic_object.dart create mode 100644 test/utilities/dynamic_object_test.dart create mode 100644 tool/build_standalone_mwebd_windows.dart diff --git a/.gitignore b/.gitignore index 0a8db4dcc3..04721dd096 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,6 @@ lib/wl_gen/generated/ /linux/flutter/generated_plugins.cmake /windows/flutter/generated_plugins.cmake /macos/Flutter/GeneratedPluginRegistrant.swift + +/assets/windows/mwebd.exe +/tool/build diff --git a/docs/building.md b/docs/building.md index 6aa647e53b..41df95be09 100644 --- a/docs/building.md +++ b/docs/building.md @@ -4,7 +4,7 @@ Here you will find instructions on how to install the necessary tools for buildi ## Prerequisites -- The only OS supported for building Android and Linux desktop is Ubuntu 20.04. Windows builds require using Ubuntu 20.04 on WSL2. macOS builds for itself and iOS. Advanced users may also be able to build on other Debian-based distributions like Linux Mint. +- The only OS supported for building Android and Linux desktop is Ubuntu 24.04. Windows builds require using Ubuntu 24.04 on WSL2. macOS builds for itself and iOS. Advanced users may also be able to build on other Debian-based distributions like Linux Mint. - Android setup ([Android Studio](https://developer.android.com/studio) and subsequent dependencies) - 100 GB of storage - Install go: [https://go.dev/doc/install](https://go.dev/doc/install) @@ -77,12 +77,12 @@ pip3 install --upgrade meson==0.64.1 markdown==3.4.1 markupsafe==2.1.1 jinja2==3 ``` ### Flutter -Install Flutter 3.29.2 by [following their guide](https://docs.flutter.dev/get-started/install/linux/desktop?tab=download#install-the-flutter-sdk). You can also clone https://github.com/flutter/flutter, check out the `3.29.2` tag, and add its `flutter/bin` folder to your PATH as in +Install Flutter 3.35.7 by [following their guide](https://docs.flutter.dev/get-started/install/linux/desktop?tab=download#install-the-flutter-sdk). You can also clone https://github.com/flutter/flutter, check out the `3.35.7` tag, and add its `flutter/bin` folder to your PATH as in ```sh FLUTTER_DIR="$HOME/development/flutter" git clone https://github.com/flutter/flutter.git "$FLUTTER_DIR" cd "$FLUTTER_DIR" -git checkout 3.29.2 +git checkout 3.35.7 echo 'export PATH="$PATH:'"$FLUTTER_DIR"'/bin"' >> "$HOME/.profile" source "$HOME/.profile" flutter precache @@ -165,6 +165,7 @@ cd scripts/windows ``` install go in WSL [https://go.dev/doc/install](https://go.dev/doc/install) (follow linux instructions) and ensure you have `x86_64-w64-mingw32-gcc` +go version should be at least 1.24 and use `scripts/build_app.sh` to build plugins: ``` @@ -292,13 +293,13 @@ If the DLLs were built on the WSL filesystem instead of on Windows, copy the res Frostdart will be built by the Windows host later. ### Install Flutter on Windows host -Install Flutter 3.29.2 on your Windows host (not in WSL2) by [following their guide](https://docs.flutter.dev/get-started/install/windows/desktop?tab=download#install-the-flutter-sdk) or by cloning https://github.com/flutter/flutter, checking out the `3.29.2` tag, and adding its `flutter/bin` folder to your PATH as in +Install Flutter 3.35.7 on your Windows host (not in WSL2) by [following their guide](https://docs.flutter.dev/get-started/install/windows/desktop?tab=download#install-the-flutter-sdk) or by cloning https://github.com/flutter/flutter, checking out the `3.35.7` tag, and adding its `flutter/bin` folder to your PATH as in ```bat @echo off set "FLUTTER_DIR=%USERPROFILE%\development\flutter" git clone https://github.com/flutter/flutter.git "%FLUTTER_DIR%" cd /d "%FLUTTER_DIR%" -git checkout 3.29.2 +git checkout 3.35.7 setx PATH "%PATH%;%FLUTTER_DIR%\bin" echo Flutter setup completed. Please restart your command prompt. ``` diff --git a/lib/app_config.dart b/lib/app_config.dart index 3004413d2a..ab5d1da283 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -16,6 +16,8 @@ abstract class AppConfig { static const emptyWalletsMessage = _emptyWalletsMessage; + static const windowsMwebdExeHash = _mwebdExeHash; + static String get appDefaultDataDirName => _appDataDirName; static String get shortDescriptionText => _shortDescriptionText; static String get commitHash => _commitHash; diff --git a/lib/services/mwebd_service.dart b/lib/services/mwebd_service.dart index 2462257505..a3c219470a 100644 --- a/lib/services/mwebd_service.dart +++ b/lib/services/mwebd_service.dart @@ -6,6 +6,7 @@ import 'dart:math'; import 'package:mutex/mutex.dart'; import 'package:mweb_client/mweb_client.dart'; +import '../utilities/dynamic_object.dart'; import '../utilities/logger.dart'; import '../utilities/prefs.dart'; import '../utilities/stack_file_system.dart'; @@ -24,10 +25,7 @@ final class MwebdService { CryptoCurrencyNetwork.test4 => throw UnimplementedError(), }; - final Map< - CryptoCurrencyNetwork, - ({OpaqueMwebdServer server, MwebClient client}) - > + final Map _map = {}; late final StreamSubscription @@ -178,9 +176,9 @@ final class MwebdService { } /// Get server status. Returns null if no server was initialized. - Future getServerStatus(CryptoCurrencyNetwork net) { - return _updateLock.protect(() { - return mwebdServerInterface.getServerStatus(_map[net]?.server); + Future getServerStatus(CryptoCurrencyNetwork net) { + return _updateLock.protect(() async { + return _map[net]?.client.status(StatusRequest()); }); } diff --git a/lib/utilities/dynamic_object.dart b/lib/utilities/dynamic_object.dart new file mode 100644 index 0000000000..bbb7728097 --- /dev/null +++ b/lib/utilities/dynamic_object.dart @@ -0,0 +1,25 @@ +class DynamicObjectTypeException implements Exception { + final Type actual, expected; + + DynamicObjectTypeException({required this.actual, required this.expected}); + + @override + String toString() => + "DynamicObjectException: Found $actual, expected $expected"; +} + +class DynamicObject { + final Object _value; + + DynamicObject(this._value); + + T get() { + if (_value is T) return _value as T; + throw DynamicObjectTypeException(actual: _value.runtimeType, expected: T); + } + + T? getIfMatch() { + if (_value is T) return _value as T; + return null; + } +} diff --git a/lib/wl_gen/interfaces/mwebd_server_interface.dart b/lib/wl_gen/interfaces/mwebd_server_interface.dart index 8b597d0771..451a96d471 100644 --- a/lib/wl_gen/interfaces/mwebd_server_interface.dart +++ b/lib/wl_gen/interfaces/mwebd_server_interface.dart @@ -1,3 +1,4 @@ +import '../../utilities/dynamic_object.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; export '../generated/mwebd_server_interface_impl.dart'; @@ -5,7 +6,7 @@ export '../generated/mwebd_server_interface_impl.dart'; abstract class MwebdServerInterface { const MwebdServerInterface(); - Future<({OpaqueMwebdServer server, int port})> createAndStartServer( + Future<({DynamicObject server, int port})> createAndStartServer( CryptoCurrencyNetwork net, { required String chain, required String dataDir, @@ -15,58 +16,6 @@ abstract class MwebdServerInterface { }); Future<({String chain, String dataDir, String peer})> stopServer( - OpaqueMwebdServer server, + DynamicObject server, ); - - Future getServerStatus(OpaqueMwebdServer? server); -} - -// local copy -class Status { - final int blockHeaderHeight; - final int mwebHeaderHeight; - final int mwebUtxosHeight; - final int blockTime; - - Status({ - required this.blockHeaderHeight, - required this.mwebHeaderHeight, - required this.mwebUtxosHeight, - required this.blockTime, - }); - - @override - String toString() { - return 'Status(' - 'blockHeaderHeight: $blockHeaderHeight, ' - 'mwebHeaderHeight: $mwebHeaderHeight, ' - 'mwebUtxosHeight: $mwebUtxosHeight, ' - 'blockTime: $blockTime' - ')'; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Status && - blockHeaderHeight == other.blockHeaderHeight && - mwebHeaderHeight == other.mwebHeaderHeight && - mwebUtxosHeight == other.mwebUtxosHeight && - blockTime == other.blockTime; - - @override - int get hashCode => Object.hash( - blockHeaderHeight, - mwebHeaderHeight, - mwebUtxosHeight, - blockTime, - ); -} - -final class OpaqueMwebdServer { - final Object _value; - - const OpaqueMwebdServer(this._value); - - T get() => _value as T; } diff --git a/pubspec.lock b/pubspec.lock index 8dca524365..d30893855b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -623,10 +623,10 @@ packages: dependency: "direct main" description: name: cs_wownero_flutter_libs - sha256: "68b6c682c6cce0915418aa6b01b137ef77652932f8b3fb1bc868bfd20d15c562" + sha256: ba1156d015a9f75c841f927ff2ce6565cd7cd37f15aaedd9aaf36703453a9884 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.3" cs_wownero_flutter_libs_android: dependency: transitive description: @@ -663,18 +663,18 @@ packages: dependency: transitive description: name: cs_wownero_flutter_libs_ios - sha256: f80dd0164902565d4fd0058da5be446a3ea5eaee2ca8651289a35d7fc93f3ca4 + sha256: "9ffd158469a0a45668d89ce56b90e846dd823ffd44ee6997b50b76129e6f613c" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" cs_wownero_flutter_libs_linux: dependency: transitive description: name: cs_wownero_flutter_libs_linux - sha256: b60a700f0ef676405bfa67793fb7431f7d2ffbaccc1f6ad69001da0c2e0a07b0 + sha256: "441c9a7b28e28434942709915e6a54ea2392b3261f90c116e03b27b02fce7492" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.0" cs_wownero_flutter_libs_macos: dependency: transitive description: diff --git a/scripts/app_config/configure_campfire.sh b/scripts/app_config/configure_campfire.sh index 04054b9ec4..e12697b35e 100755 --- a/scripts/app_config/configure_campfire.sh +++ b/scripts/app_config/configure_campfire.sh @@ -63,6 +63,8 @@ const _appDataDirName = "campfire"; const _shortDescriptionText = "Your privacy. Your wallet. Your Firo."; const _commitHash = "$BUILT_COMMIT_HASH"; +const _mwebdExeHash = ""; + const Set _features = { AppFeature.tor, AppFeature.swap diff --git a/scripts/app_config/configure_stack_duo.sh b/scripts/app_config/configure_stack_duo.sh index 148775aa3e..c410a18417 100755 --- a/scripts/app_config/configure_stack_duo.sh +++ b/scripts/app_config/configure_stack_duo.sh @@ -59,6 +59,8 @@ const _appDataDirName = "stackduo"; const _shortDescriptionText = "An open-source, multicoin wallet for everyone"; const _commitHash = "$BUILT_COMMIT_HASH"; +const _mwebdExeHash = ""; + const Set _features = { AppFeature.themeSelection, AppFeature.buy, diff --git a/scripts/app_config/configure_stack_wallet.sh b/scripts/app_config/configure_stack_wallet.sh index 0ec22f544c..3de44a6aaf 100755 --- a/scripts/app_config/configure_stack_wallet.sh +++ b/scripts/app_config/configure_stack_wallet.sh @@ -50,6 +50,16 @@ dart "${APP_PROJECT_ROOT_DIR}/tool/gen_interfaces.dart" \ XEL \ FROST + +MWEBD_EXE_SHA256="" +if [[ "$1" == "windows" ]]; then + dart "${APP_PROJECT_ROOT_DIR}/tool/build_standalone_mwebd_windows.dart" + MWEBD_EXE_SHA256="$(sha256sum "${APP_PROJECT_ROOT_DIR}/assets/windows/mwebd.exe" | awk '{print $1}')" + dart "${APP_PROJECT_ROOT_DIR}/tool/process_pubspec_deps.dart" \ + "${PUBSPEC_FILE}" MWEBDEXE +fi + + export INCLUDE_EPIC_SO="ON" export INCLUDE_MWC_SO="ON" @@ -73,6 +83,8 @@ const _appDataDirName = "stackwallet"; const _shortDescriptionText = "An open-source, multicoin wallet for everyone"; const _commitHash = "$BUILT_COMMIT_HASH"; +const _mwebdExeHash = "$MWEBD_EXE_SHA256"; + const Set _features = { AppFeature.themeSelection, AppFeature.buy, diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index e225a20e7d..b6277798b2 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -388,6 +388,10 @@ flutter: # default themes_testing - assets/default_themes/ +# %%ENABLE_MWEBDEXE%% +# - assets/windows/mwebd.exe +# %%END_ENABLE_MWEBDEXE%% + import_sorter: comments: false # Optional, defaults to true ignored_files: # Optional, defaults to [] diff --git a/test/utilities/dynamic_object_test.dart b/test/utilities/dynamic_object_test.dart new file mode 100644 index 0000000000..9999bf0167 --- /dev/null +++ b/test/utilities/dynamic_object_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/utilities/dynamic_object.dart'; + +void main() { + test("DynamicObject get success", () { + final object = DynamicObject(1); + expect(object.get(), isA()); + }); + + test("DynamicObject get failure", () { + final object = DynamicObject(1); + expect(object.get(), throwsA(isA())); + }); + test("DynamicObject get if match success", () { + final object = DynamicObject(1); + expect(object.getIfMatch(), isA()); + }); + + test("DynamicObject get if match failure", () { + final object = DynamicObject(1); + expect(object.getIfMatch(), isNull); + }); +} diff --git a/tool/build_standalone_mwebd_windows.dart b/tool/build_standalone_mwebd_windows.dart new file mode 100644 index 0000000000..b956e4f5e3 --- /dev/null +++ b/tool/build_standalone_mwebd_windows.dart @@ -0,0 +1,99 @@ +import 'dart:io'; + +Future main() async { + final projectToolDir = File(() { + String path = Platform.script.path; + if (Platform.isWindows) { + while (!path.startsWith("C:")) { + path = path.substring(1); + } + } + return path; + }()).parent; + + // setup temp build dir + final tempBuildDir = Directory( + "${projectToolDir.path}" + "${Platform.pathSeparator}build", + ); + if (await tempBuildDir.exists()) { + await tempBuildDir.delete(recursive: true); + } + await tempBuildDir.create(); + + // change working dir and clone mwebd + Directory.current = tempBuildDir; + final clone = await Process.start("git", [ + "clone", + "https://www.github.com/ltcmweb/mwebd.git", + "--branch", + "v0.1.8", + ], runInShell: true); + await _waitForProcess(clone); + + // change working dir and build mwebd.exe + Directory.current = Directory( + "${tempBuildDir.path}" + "${Platform.pathSeparator}mwebd", + ); + final wslBuild = Platform.isWindows + ? await Process.start("wsl", [ + "bash", + "-l", + "-c", + "GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc " + "go build -o ../mwebd.exe github.com/ltcmweb/mwebd/cmd/mwebd", + ], runInShell: true) + : await Process.start( + "go", + ["build", "-o", "../mwebd.exe", "github.com/ltcmweb/mwebd/cmd/mwebd"], + environment: { + "GOOS": "windows", + "GOARCH": "amd64", + "CGO_ENABLED": "1", + "CC": "x86_64-w64-mingw32-gcc", + }, + runInShell: true, + ); + await _waitForProcess(wslBuild); + + // create assets/windows dir if needed + final winAssetsDir = Directory( + "${Directory.current.parent.parent.parent.path}" + "${Platform.pathSeparator}assets" + "${Platform.pathSeparator}windows", + ); + if (!(await winAssetsDir.exists())) { + await winAssetsDir.create(); + } + + // copy the build mwebd.exe to assets/windows + final copy = Platform.isWindows + ? await Process.start("cmd", [ + "/C", + "copy", + "${Directory.current.parent.path}" + "${Platform.pathSeparator}mwebd.exe", + "${winAssetsDir.path}" + "${Platform.pathSeparator}mwebd.exe", + ]) + : await Process.start("cp", [ + "${Directory.current.parent.path}" + "${Platform.pathSeparator}mwebd.exe", + "${winAssetsDir.path}" + "${Platform.pathSeparator}mwebd.exe", + ]); + await _waitForProcess(copy); + + // cleanup + Directory.current = projectToolDir; + await tempBuildDir.delete(recursive: true); +} + +Future _waitForProcess(Process process) async { + final exitCode = await process.exitCode; + if (exitCode != 0) { + print("Exited process with code=$exitCode\n${StackTrace.current}"); + exit(exitCode); + } +} diff --git a/tool/wl_templates/MWEBD_mwebd_server_interface_impl.template.dart b/tool/wl_templates/MWEBD_mwebd_server_interface_impl.template.dart index 55ba2c630a..4098f8adcf 100644 --- a/tool/wl_templates/MWEBD_mwebd_server_interface_impl.template.dart +++ b/tool/wl_templates/MWEBD_mwebd_server_interface_impl.template.dart @@ -1,7 +1,17 @@ //ON -import 'package:flutter_mwebd/flutter_mwebd.dart' hide Status; +import 'dart:async'; +import 'dart:io'; +import 'package:crypto/crypto.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mwebd/flutter_mwebd.dart'; +import 'package:path/path.dart'; + +import '../../app_config.dart'; //END_ON +import '../../utilities/dynamic_object.dart'; +import '../../utilities/extensions/extensions.dart'; +import '../../utilities/stack_file_system.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../interfaces/mwebd_server_interface.dart'; @@ -14,15 +24,36 @@ MwebdServerInterface _getInterface() => throw Exception("MWEBD not enabled!"); //ON MwebdServerInterface _getInterface() => const _MwebdServerInterfaceImpl(); -extension _OpaqueMwebdServerExt on OpaqueMwebdServer { - MwebdServer get value => get(); -} - class _MwebdServerInterfaceImpl extends MwebdServerInterface { const _MwebdServerInterfaceImpl(); + static const _kExe = "mwebd.exe"; + + Future _prepareWindowsExeDirPath() async { + final dir = (await StackFileSystem.applicationMwebdDirectory( + "dummy", + )).parent.path; + final exe = File(join(dir, _kExe)); + + if (!(await exe.exists())) { + final bytes = await rootBundle.load("assets/windows/mwebd.exe"); + await exe.writeAsBytes( + bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes), + flush: true, + ); + } + + final hash = await sha256.bind(exe.openRead()).first; + final hexHash = Uint8List.fromList(hash.bytes).toHex; + if (AppConfig.windowsMwebdExeHash != hexHash) { + throw Exception("Windows mwebd.exe sha256 has mismatch!!!"); + } + + return exe.parent.path; + } + @override - Future<({OpaqueMwebdServer server, int port})> createAndStartServer( + Future<({DynamicObject server, int port})> createAndStartServer( CryptoCurrencyNetwork net, { required String chain, required String dataDir, @@ -37,36 +68,51 @@ class _MwebdServerInterfaceImpl extends MwebdServerInterface { proxy: proxy, serverPort: serverPort, ); - await newServer.createServer(); - await newServer.startServer(); - return (server: OpaqueMwebdServer(newServer), port: newServer.serverPort); + + if (Platform.isWindows) { + final exeDirPath = await _prepareWindowsExeDirPath(); + final process = await Process.start(join(exeDirPath, _kExe), [ + "-c", + chain, + "-d", + chain, + "-l", + "127.0.0.1:$serverPort", + "-p", + peer, + "-proxy", + proxy, + ], workingDirectory: exeDirPath); + return (server: DynamicObject((process, newServer)), port: serverPort); + } else { + await newServer.createServer(); + await newServer.startServer(); + return (server: DynamicObject(newServer), port: newServer.serverPort); + } } @override Future<({String chain, String dataDir, String peer})> stopServer( - OpaqueMwebdServer server, + DynamicObject server, ) async { - final actual = server.value; - final data = ( - chain: actual.chain, - dataDir: actual.dataDir, - peer: actual.peer, - ); - await actual.stopServer(); - return data; - } - - @override - Future getServerStatus(OpaqueMwebdServer? server) async { - final status = await server?.value.getStatus(); - if (status == null) return null; - - return Status( - blockHeaderHeight: status.blockHeaderHeight, - mwebHeaderHeight: status.mwebHeaderHeight, - mwebUtxosHeight: status.mwebUtxosHeight, - blockTime: status.blockTime, - ); + if (server.get() is (Process, MwebdServer)) { + final actual = server.get<(Process, MwebdServer)>(); + actual.$1.kill(); + return ( + chain: actual.$2.chain, + dataDir: actual.$2.dataDir, + peer: actual.$2.peer, + ); + } else { + final actual = server.get(); + final data = ( + chain: actual.chain, + dataDir: actual.dataDir, + peer: actual.peer, + ); + await actual.stopServer(); + return data; + } } } From 8a0f78e6e69c0c9e1456d90c50362e55054c1873 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 4 Nov 2025 13:54:36 -0600 Subject: [PATCH 12/50] basic message signing --- .../receive_view/addresses/address_card.dart | 270 +++++++++--------- lib/pages/signing/signing_view.dart | 89 ++++++ .../signing/sub_widgets/address_list.dart | 218 ++++++++++++++ .../signing/sub_widgets/sign_message_tab.dart | 267 +++++++++++++++++ .../sub_widgets/verify_message_tab.dart | 202 +++++++++++++ lib/pages/wallet_view/wallet_view.dart | 20 ++ .../sub_widgets/desktop_wallet_features.dart | 38 +++ lib/route_generator.dart | 34 ++- lib/utilities/if_not_already.dart | 38 +++ .../crypto_currency/coins/dogecoin.dart | 4 +- lib/wallets/crypto_currency/coins/firo.dart | 4 +- .../crypto_currency/coins/namecoin.dart | 11 +- .../electrumx_interface.dart | 71 ++++- .../sign_verify_interface.dart | 14 + lib/widgets/detail_item.dart | 110 +++---- .../textfields/adaptive_text_field.dart | 174 +++++++++++ 16 files changed, 1360 insertions(+), 204 deletions(-) create mode 100644 lib/pages/signing/signing_view.dart create mode 100644 lib/pages/signing/sub_widgets/address_list.dart create mode 100644 lib/pages/signing/sub_widgets/sign_message_tab.dart create mode 100644 lib/pages/signing/sub_widgets/verify_message_tab.dart create mode 100644 lib/utilities/if_not_already.dart create mode 100644 lib/wallets/wallet/wallet_mixin_interfaces/sign_verify_interface.dart create mode 100644 lib/widgets/textfields/adaptive_text_field.dart diff --git a/lib/pages/receive_view/addresses/address_card.dart b/lib/pages/receive_view/addresses/address_card.dart index 901cd578be..f8b6ec8067 100644 --- a/lib/pages/receive_view/addresses/address_card.dart +++ b/lib/pages/receive_view/addresses/address_card.dart @@ -50,6 +50,7 @@ class AddressCard extends ConsumerStatefulWidget { required this.coin, this.onPressed, this.clipboard = const ClipboardWrapper(), + this.compact = false, }); final int addressId; @@ -57,6 +58,7 @@ class AddressCard extends ConsumerStatefulWidget { final CryptoCurrency coin; final ClipboardInterface clipboard; final VoidCallback? onPressed; + final bool compact; @override ConsumerState createState() => _AddressCardState(); @@ -142,11 +144,10 @@ class _AddressCardState extends ConsumerState { @override void initState() { - address = - MainDB.instance.isar.addresses - .where() - .idEqualTo(widget.addressId) - .findFirstSync()!; + address = MainDB.instance.isar.addresses + .where() + .idEqualTo(widget.addressId) + .findFirstSync()!; label = MainDB.instance.getAddressLabelSync(widget.walletId, address.value); Id? id = label?.id; @@ -155,12 +156,11 @@ class _AddressCardState extends ConsumerState { walletId: widget.walletId, addressString: address.value, value: "", - tags: - address.subType == AddressSubType.receiving - ? ["receiving"] - : address.subType == AddressSubType.change - ? ["change"] - : null, + tags: address.subType == AddressSubType.receiving + ? ["receiving"] + : address.subType == AddressSubType.change + ? ["change"] + : null, ); id = MainDB.instance.putAddressLabelSync(label!); } @@ -181,20 +181,19 @@ class _AddressCardState extends ConsumerState { } return ConditionalParent( - condition: isDesktop, - builder: - (child) => Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.file( - File(ref.watch(coinIconProvider(widget.coin))), - width: 32, - height: 32, - ), - const SizedBox(width: 12), - Expanded(child: child), - ], + condition: isDesktop && !widget.compact, + builder: (child) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.file( + File(ref.watch(coinIconProvider(widget.coin))), + width: 32, + height: 32, ), + const SizedBox(width: 12), + Expanded(child: child), + ], + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -230,129 +229,124 @@ class _AddressCardState extends ConsumerState { ), ], ), - const SizedBox(height: 10), - Row( - children: [ - CustomTextButton( - text: "Copy address", - onTap: () { - widget.clipboard - .setData(ClipboardData(text: address.value)) - .then((value) { - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), - ); - } - }); - }, - ), - const SizedBox(width: 16), - CustomTextButton( - text: "Show QR code", - onTap: () async { - await showDialog( - context: context, - builder: (_) { - return StackDialogBase( - child: Column( - children: [ - if (label!.value.isNotEmpty) - Text( - label!.value, - style: STextStyles.w600_18(context), + if (!widget.compact) const SizedBox(height: 10), + if (!widget.compact) + Row( + children: [ + CustomTextButton( + text: "Copy address", + onTap: () { + widget.clipboard + .setData(ClipboardData(text: address.value)) + .then((value) { + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, ), - if (label!.value.isNotEmpty) - const SizedBox(height: 8), - Text( - address.value, - style: STextStyles.w500_16( - context, - ).copyWith( - color: - Theme.of(context) - .extension()! - .textSubtitle1, + ); + } + }); + }, + ), + const SizedBox(width: 16), + CustomTextButton( + text: "Show QR code", + onTap: () async { + await showDialog( + context: context, + builder: (_) { + return StackDialogBase( + child: Column( + children: [ + if (label!.value.isNotEmpty) + Text( + label!.value, + style: STextStyles.w600_18(context), + ), + if (label!.value.isNotEmpty) + const SizedBox(height: 8), + Text( + address.value, + style: STextStyles.w500_16(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), ), - ), - const SizedBox(height: 16), - Center( - child: RepaintBoundary( - key: _qrKey, - child: QR( - data: AddressUtils.buildUriString( - widget.coin.uriScheme, - address.value, - {}, + const SizedBox(height: 16), + Center( + child: RepaintBoundary( + key: _qrKey, + child: QR( + data: AddressUtils.buildUriString( + widget.coin.uriScheme, + address.value, + {}, + ), + size: 220, ), - size: 220, ), ), - ), - const SizedBox(height: 16), - Row( - children: [ - if (!isDesktop) - Expanded( - child: SecondaryButton( - label: "Share", - buttonHeight: - isDesktop - ? ButtonHeight.l - : null, - icon: SvgPicture.asset( - Assets.svg.share, - width: 14, - height: 14, - color: - Theme.of(context) - .extension()! - .buttonTextSecondary, + const SizedBox(height: 16), + Row( + children: [ + if (!isDesktop) + Expanded( + child: SecondaryButton( + label: "Share", + buttonHeight: isDesktop + ? ButtonHeight.l + : null, + icon: SvgPicture.asset( + Assets.svg.share, + width: 14, + height: 14, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () async { + await _capturePng(false); + }, ), - onPressed: () async { - await _capturePng(false); - }, ), - ), - if (isDesktop) - Expanded( - child: PrimaryButton( - buttonHeight: - isDesktop - ? ButtonHeight.l - : null, - onPressed: () async { - // TODO: add save functionality instead of share - // save works on linux at the moment - await _capturePng(true); - }, - label: "Save", - icon: SvgPicture.asset( - Assets.svg.arrowDown, - width: 20, - height: 20, - color: - Theme.of(context) - .extension()! - .buttonTextPrimary, + if (isDesktop) + Expanded( + child: PrimaryButton( + buttonHeight: isDesktop + ? ButtonHeight.l + : null, + onPressed: () async { + // TODO: add save functionality instead of share + // save works on linux at the moment + await _capturePng(true); + }, + label: "Save", + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .buttonTextPrimary, + ), ), ), - ), - ], - ), - ], - ), - ); - }, - ); - }, - ), - ], - ), + ], + ), + ], + ), + ); + }, + ); + }, + ), + ], + ), // if (label!.tags != null && label!.tags!.isNotEmpty) // Wrap( // spacing: 10, diff --git a/lib/pages/signing/signing_view.dart b/lib/pages/signing/signing_view.dart new file mode 100644 index 0000000000..9afaf36d20 --- /dev/null +++ b/lib/pages/signing/signing_view.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../themes/stack_colors.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/custom_tab_view.dart'; +import '../../widgets/stack_dialog.dart'; +import 'sub_widgets/sign_message_tab.dart'; +import 'sub_widgets/verify_message_tab.dart'; + +class SigningView extends ConsumerStatefulWidget { + const SigningView({super.key, required this.walletId}); + + final String walletId; + + static const String routeName = "/signingView"; + + @override + ConsumerState createState() => _SigningViewState(); +} + +class _SigningViewState extends ConsumerState { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + // keep auto dispose providers alive + ref.listen(pSignIsValid, (_, __) {}); + ref.listen(pVerifyIsValid, (_, __) {}); + + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Sign / Verify", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea(child: child), + ), + ), + child: CustomTabView( + titles: const ["Sign message", "Verify message"], + children: [ + SignMessageForm( + key: const Key("_SignMessageFormKey"), + walletId: widget.walletId, + ), + VerifyMessageForm( + key: const Key("_VerifyMessageFormKey"), + walletId: widget.walletId, + ), + ], + ), + ); + } +} + +Future showSignVerifyError(Exception e, {required BuildContext context}) { + String message = e.toString().trim(); + const exceptionPrefix = "Exception:"; + while (message.startsWith(exceptionPrefix) && + message.length > exceptionPrefix.length) { + message = message.substring(exceptionPrefix.length).trim(); + } + return showDialog( + context: context, + builder: (context) => StackOkDialog( + title: "Error", + message: message, + maxWidth: Util.isDesktop ? 400 : null, + desktopPopRootNavigator: Util.isDesktop, + ), + ); +} diff --git a/lib/pages/signing/sub_widgets/address_list.dart b/lib/pages/signing/sub_widgets/address_list.dart new file mode 100644 index 0000000000..66d4635e67 --- /dev/null +++ b/lib/pages/signing/sub_widgets/address_list.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar_community/isar.dart'; + +import '../../../models/isar/models/address_label.dart'; +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../widgets/background.dart'; +import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../receive_view/addresses/address_card.dart'; + +class AddressList extends ConsumerStatefulWidget { + const AddressList({super.key, required this.walletId}); + + final String walletId; + + @override + ConsumerState createState() => _AddressListState(); +} + +class _AddressListState extends ConsumerState { + String _searchString = ""; + + late final TextEditingController _searchController; + final searchFieldFocusNode = FocusNode(); + + List _search(String term) { + if (term.isEmpty) { + return ref + .read(mainDBProvider) + .getAddresses(widget.walletId) + .filter() + .group( + (q) => q + .subTypeEqualTo(AddressSubType.change) + .or() + .subTypeEqualTo(AddressSubType.receiving) + .or() + .subTypeEqualTo(AddressSubType.paynymReceive) + .or() + .subTypeEqualTo(AddressSubType.paynymNotification), + ) + .and() + .not() + .typeEqualTo(AddressType.nonWallet) + .and() + .group( + (q) => q + .group( + (q2) => q2 + .typeEqualTo(AddressType.frostMS) + .and() + .zSafeFrostEqualTo(true), + ) + .or() + .not() + .typeEqualTo(AddressType.frostMS), + ) + .sortByDerivationIndex() + .idProperty() + .findAllSync(); + } + + final labels = ref + .read(mainDBProvider) + .getAddressLabels(widget.walletId) + .filter() + .group( + (q) => q + .valueContains(term, caseSensitive: false) + .or() + .addressStringContains(term, caseSensitive: false) + .or() + .group( + (q) => q.tagsIsNotNull().and().tagsElementContains( + term, + caseSensitive: false, + ), + ), + ) + .findAllSync(); + + if (labels.isEmpty) { + return []; + } + + return ref + .read(mainDBProvider) + .getAddresses(widget.walletId) + .filter() + .anyOf( + labels, + (q, e) => q.valueEqualTo(e.addressString), + ) + .group( + (q) => q + .subTypeEqualTo(AddressSubType.change) + .or() + .subTypeEqualTo(AddressSubType.receiving) + .or() + .subTypeEqualTo(AddressSubType.paynymReceive) + .or() + .subTypeEqualTo(AddressSubType.paynymNotification), + ) + .and() + .not() + .typeEqualTo(AddressType.nonWallet) + .and() + .group( + (q) => q + .group( + (q2) => q2 + .typeEqualTo(AddressType.frostMS) + .and() + .zSafeFrostEqualTo(true), + ) + .or() + .not() + .typeEqualTo(AddressType.frostMS), + ) + .sortByDerivationIndex() + .idProperty() + .findAllSync(); + } + + @override + void initState() { + _searchController = TextEditingController(); + + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final coin = ref.watch(pWalletCoin(widget.walletId)); + + final ids = _search(_searchString); + + return ListView.separated( + shrinkWrap: true, + itemCount: ids.length, + separatorBuilder: (_, __) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Util.isDesktop + ? Container( + height: 1, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, + ) + : const SizedBox(height: 2), + ), + itemBuilder: (_, index) => Padding( + padding: const EdgeInsets.all(4), + child: AddressCard( + key: Key("addressCardDesktop_key_${ids[index]}"), + walletId: widget.walletId, + compact: true, + addressId: ids[index], + coin: coin, + onPressed: () => Navigator.of( + context, + ).pop(ref.read(mainDBProvider).isar.addresses.getSync(ids[index])!), + ), + ), + ); + } +} + +class CompactAddressListView extends StatelessWidget { + const CompactAddressListView({super.key, required this.walletId}); + + final String walletId; + + static const routeName = "/compactAddressListView"; + + @override + Widget build(BuildContext context) { + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Choose address", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding( + padding: EdgeInsets.only( + bottom: Constants.size.standardPadding, + left: Constants.size.standardPadding, + right: Constants.size.standardPadding, + ), + child: AddressList(walletId: walletId), + ), + ), + ), + ); + } +} diff --git a/lib/pages/signing/sub_widgets/sign_message_tab.dart b/lib/pages/signing/sub_widgets/sign_message_tab.dart new file mode 100644 index 0000000000..a4b0d9241f --- /dev/null +++ b/lib/pages/signing/sub_widgets/sign_message_tab.dart @@ -0,0 +1,267 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../models/isar/models/isar_models.dart'; +import '../../../providers/global/wallets_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/if_not_already.dart'; +import '../../../utilities/show_loading.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/sign_verify_interface.dart'; +import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/custom_buttons/blue_text_button.dart'; +import '../../../widgets/custom_buttons/simple_copy_button.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/detail_item.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/rounded_container.dart'; +import '../../../widgets/textfields/adaptive_text_field.dart'; +import '../signing_view.dart'; +import 'address_list.dart'; + +final class _SignState { + final String message, signature; + final Address? address; + + _SignState({ + required this.address, + required this.message, + required this.signature, + }); + + bool get isValid => message.isNotEmpty && address != null; + + _SignState copyWith({String? message, String? signature}) { + return _SignState( + address: address, + message: message ?? this.message, + signature: signature ?? this.signature, + ); + } + + _SignState copyWithAddress(Address? address) { + return _SignState(address: address, message: message, signature: signature); + } + + @override + String toString() => + "_SignState(address: $address, message: $message, signature: $signature)"; +} + +final _pSignState = StateProvider.autoDispose((ref) { + return _SignState(address: null, message: "", signature: ""); +}); + +final pSignIsValid = Provider.autoDispose( + (ref) => ref.watch(_pSignState).isValid, +); + +class SignMessageForm extends ConsumerStatefulWidget { + const SignMessageForm({super.key, required this.walletId}); + + final String walletId; + + @override + ConsumerState createState() => _SignMessageFormState(); +} + +class _SignMessageFormState extends ConsumerState { + final messageController = TextEditingController(); + + late final VoidCallback _chooseAddress; + late final VoidCallback _sign; + + TextStyle _getStyle(BuildContext context) { + return Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, + ) + : STextStyles.smallMed12(context); + } + + @override + void initState() { + super.initState(); + + messageController.text = ref.read(_pSignState).message; + + _chooseAddress = IfNotAlreadyAsync(() async { + final Address? address; + + if (Util.isDesktop) { + address = await showDialog
( + context: context, + builder: (context) { + return SDialog( + contentCanScroll: false, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => SizedBox(width: 600, child: child), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (Util.isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Choose address", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => Padding( + padding: const EdgeInsets.only( + top: 10, + left: 32, + right: 32, + bottom: 32, + ), + child: RoundedContainer( + padding: EdgeInsets.zero, + color: Colors.transparent, + borderColor: Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: child, + ), + ), + + child: AddressList(walletId: widget.walletId), + ), + ), + ], + ), + ), + ); + }, + ); + } else { + address = await Navigator.of(context).pushNamed
( + CompactAddressListView.routeName, + arguments: widget.walletId, + ); + } + + if (address != null && + address.value != ref.read(_pSignState).address?.value && + mounted) { + ref.read(_pSignState.notifier).state = ref + .read(_pSignState) + .copyWithAddress(address) + .copyWith(signature: ""); + } + }).execute; + + _sign = IfNotAlreadyAsync(() async { + Exception? ex; + + final state = ref.read(_pSignState); + final signature = await showLoading( + whileFuture: + (ref.read(pWallets).getWallet(widget.walletId) + as SignVerifyInterface) + .signMessage(state.message, address: state.address!), + context: context, + message: "Signing...", + delay: const Duration(seconds: 1), + onException: (e) => ex = e, + ); + + if (mounted && ex != null) { + await showSignVerifyError(ex!, context: context); + } else if (signature != null && mounted) { + ref.read(_pSignState.notifier).state = state.copyWith( + signature: signature, + ); + } + }).execute; + } + + @override + void dispose() { + messageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Padding( + padding: EdgeInsets.all(Constants.size.standardPadding), + child: child, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: Util.isDesktop ? 20 : 12), + SelectableText("Message", style: _getStyle(context)), + SizedBox(height: Util.isDesktop ? 10 : 8), + AdaptiveTextField( + controller: messageController, + showPasteClearButton: true, + maxLines: 1, + onChangedComprehensive: (_) { + if (mounted) { + ref.read(_pSignState.notifier).state = ref + .read(_pSignState) + .copyWith(message: messageController.text, signature: ""); + } + }, + ), + SizedBox(height: Util.isDesktop ? 20 : 12), + + DetailItem( + title: "Address", + titleStyle: _getStyle(context), + detail: + ref.watch(_pSignState.select((s) => s.address))?.value ?? "", + showEmptyDetail: true, + detailPlaceholder: "n/a", + noPadding: Util.isDesktop, + button: CustomTextButton( + text: "Choose address", + onTap: _chooseAddress, + ), + ), + SizedBox(height: Util.isDesktop ? 20 : 12), + + DetailItem( + title: "Signature", + titleStyle: _getStyle(context), + detail: ref.watch(_pSignState.select((s) => s.signature)), + showEmptyDetail: true, + detailPlaceholder: "n/a", + noPadding: Util.isDesktop, + button: ref.watch(_pSignState.select((s) => s.signature)).isEmpty + ? null + : SimpleCopyButton(data: ref.read(_pSignState).signature), + ), + + const SizedBox(height: 32), + + PrimaryButton( + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + label: "Sign", + enabled: ref.watch(pSignIsValid), + onPressed: ref.watch(pSignIsValid) ? _sign : null, + ), + ], + ), + ); + } +} diff --git a/lib/pages/signing/sub_widgets/verify_message_tab.dart b/lib/pages/signing/sub_widgets/verify_message_tab.dart new file mode 100644 index 0000000000..08b4e59158 --- /dev/null +++ b/lib/pages/signing/sub_widgets/verify_message_tab.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../providers/global/wallets_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/if_not_already.dart'; +import '../../../utilities/show_loading.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/sign_verify_interface.dart'; +import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../../../widgets/textfields/adaptive_text_field.dart'; +import '../signing_view.dart'; + +final class _VerifyState { + final String address, message, signature; + + _VerifyState({ + required this.address, + required this.message, + required this.signature, + }); + + bool get isValid => + message.isNotEmpty && signature.isNotEmpty && address.isNotEmpty; + + _VerifyState copyWith({String? address, String? message, String? signature}) { + return _VerifyState( + address: address ?? this.address, + message: message ?? this.message, + signature: signature ?? this.signature, + ); + } + + @override + String toString() => + "_VerifyState(address: $address, message: $message, signature: $signature)"; +} + +final _pVerifyState = StateProvider.autoDispose((ref) { + return _VerifyState(address: "", message: "", signature: ""); +}); + +final pVerifyIsValid = Provider.autoDispose( + (ref) => ref.watch(_pVerifyState).isValid, +); + +class VerifyMessageForm extends ConsumerStatefulWidget { + const VerifyMessageForm({super.key, required this.walletId}); + + final String walletId; + + @override + ConsumerState createState() => _VerifyMessageFormState(); +} + +class _VerifyMessageFormState extends ConsumerState { + final messageController = TextEditingController(); + final addressController = TextEditingController(); + final signatureController = TextEditingController(); + + late final VoidCallback _verify; + + TextStyle _getStyle(BuildContext context) { + return Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, + ) + : STextStyles.smallMed12(context); + } + + @override + void initState() { + super.initState(); + + addressController.text = ref.read(_pVerifyState).address; + messageController.text = ref.read(_pVerifyState).message; + signatureController.text = ref.read(_pVerifyState).signature; + + _verify = IfNotAlreadyAsync(() async { + Exception? ex; + + final verified = await showLoading( + whileFuture: + (ref.read(pWallets).getWallet(widget.walletId) + as SignVerifyInterface) + .verifyMessage( + messageController.text, + address: addressController.text, + signature: signatureController.text, + ), + context: context, + message: "Verifying...", + delay: const Duration(seconds: 1), + onException: (e) => ex = e, + ); + + if (mounted) { + if (ex != null) { + await showSignVerifyError(ex!, context: context); + } else { + await showDialog( + context: context, + builder: (context) => StackOkDialog( + title: verified == true + ? "Verification succeeded" + : "Verification failed", + maxWidth: Util.isDesktop ? 400 : null, + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } + }).execute; + } + + @override + void dispose() { + messageController.dispose(); + addressController.dispose(); + signatureController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Padding( + padding: EdgeInsets.all(Constants.size.standardPadding), + child: child, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: Util.isDesktop ? 20 : 12), + + SelectableText("Message", style: _getStyle(context)), + SizedBox(height: Util.isDesktop ? 10 : 8), + AdaptiveTextField( + controller: messageController, + showPasteClearButton: true, + maxLines: 1, + onChangedComprehensive: (_) { + if (mounted) { + ref.read(_pVerifyState.notifier).state = ref + .read(_pVerifyState) + .copyWith(message: messageController.text); + } + }, + ), + SizedBox(height: Util.isDesktop ? 20 : 12), + + SelectableText("Address", style: _getStyle(context)), + SizedBox(height: Util.isDesktop ? 10 : 8), + AdaptiveTextField( + controller: addressController, + showPasteClearButton: true, + maxLines: 1, + onChangedComprehensive: (_) { + if (mounted) { + ref.read(_pVerifyState.notifier).state = ref + .read(_pVerifyState) + .copyWith(address: addressController.text); + } + }, + ), + SizedBox(height: Util.isDesktop ? 20 : 12), + + SelectableText("Signature", style: _getStyle(context)), + SizedBox(height: Util.isDesktop ? 10 : 8), + AdaptiveTextField( + controller: signatureController, + showPasteClearButton: true, + maxLines: 1, + onChangedComprehensive: (_) { + if (mounted) { + ref.read(_pVerifyState.notifier).state = ref + .read(_pVerifyState) + .copyWith(signature: signatureController.text); + } + }, + ), + + const SizedBox(height: 32), + + PrimaryButton( + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + label: "Verify", + enabled: ref.watch(pVerifyIsValid), + onPressed: ref.watch(pVerifyIsValid) ? _verify : null, + ), + ], + ), + ); + } +} diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index b7929f1329..6483be9fc6 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -60,6 +60,7 @@ import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/sign_verify_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../widgets/background.dart'; @@ -103,6 +104,7 @@ import '../send_view/frost_ms/frost_send_view.dart'; import '../send_view/send_view.dart'; import '../settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import '../settings_views/wallet_settings_view/wallet_settings_view.dart'; +import '../signing/signing_view.dart'; import '../spark_names/spark_names_home_view.dart'; import '../token_view/my_tokens_view.dart'; import 'sub_widgets/transactions_list.dart'; @@ -1129,6 +1131,24 @@ class _WalletViewState extends ConsumerState { ); }, ), + if (wallet is SignVerifyInterface && !viewOnly) + WalletNavigationBarItemData( + icon: SvgPicture.asset( + Assets.svg.pencil, + height: 20, + width: 20, + color: Theme.of( + context, + ).extension()!.bottomNavIconIcon, + ), + label: "Sign/Verify", + onTap: () { + Navigator.of(context).pushNamed( + SigningView.routeName, + arguments: widget.walletId, + ); + }, + ), if (wallet is CoinControlInterface && ref.watch( prefsChangeNotifierProvider.select( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index 052569af24..4a709a0bc6 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -25,6 +25,7 @@ import '../../../../pages/namecoin_names/namecoin_names_home_view.dart'; import '../../../../pages/paynym/paynym_claim_view.dart'; import '../../../../pages/paynym/paynym_home_view.dart'; import '../../../../pages/salvium_stake/salvium_create_stake_view.dart'; +import '../../../../pages/signing/signing_view.dart'; import '../../../../pages/spark_names/spark_names_home_view.dart'; import '../../../../providers/desktop/current_desktop_menu_item.dart'; import '../../../../providers/global/paynym_api_provider.dart'; @@ -51,6 +52,7 @@ import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/sign_verify_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../../widgets/custom_loading_overlay.dart'; @@ -88,6 +90,7 @@ enum WalletFeature { namecoinName("Domains", "Namecoin DNS"), sparkNames("Names", "Spark names"), salviumStaking("Staking", "Staking"), + sign("Sign/Verify", "Sign / Verify messages"), // special cases clearSparkCache("", ""), @@ -417,6 +420,38 @@ class _DesktopWalletFeaturesState extends ConsumerState { ); } + Future _onSignPressed() async { + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Sign/Verify", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: SigningView(walletId: widget.walletId), + ), + const SizedBox(height: 32), + ], + ), + ), + ); + } + List<(WalletFeature, String, FutureOr Function())> _getOptions( Wallet wallet, bool showExchange, @@ -455,6 +490,9 @@ class _DesktopWalletFeaturesState extends ConsumerState { _onSalviumStakePressed, ), + if (wallet is SignVerifyInterface && !isViewOnly) + (WalletFeature.sign, Assets.svg.pencil, _onSignPressed), + if (showCoinControl) ( WalletFeature.coinControl, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index b44550bafc..e7565bc7ce 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -152,6 +152,8 @@ import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_setting import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_info.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; +import 'pages/signing/signing_view.dart'; +import 'pages/signing/sub_widgets/address_list.dart'; import 'pages/spark_names/buy_spark_name_view.dart'; import 'pages/spark_names/confirm_spark_name_transaction_view.dart'; import 'pages/spark_names/spark_names_home_view.dart'; @@ -427,6 +429,26 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case SigningView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SigningView(walletId: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case CompactAddressListView.routeName: + if (args is String) { + return getRoute
( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => CompactAddressListView(walletId: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case CreateNewFrostMsWalletView.routeName: if (args is ({String walletName, FrostCurrency frostCurrency})) { return getRoute( @@ -2513,7 +2535,7 @@ class RouteGenerator { } } - static Route getRoute({ + static Route getRoute({ bool shouldUseMaterialRoute = useMaterialPageRoute, required Widget Function(BuildContext) builder, String? title, @@ -2522,14 +2544,14 @@ class RouteGenerator { bool fullscreenDialog = false, }) { if (shouldUseMaterialRoute) { - return MaterialPageRoute( + return MaterialPageRoute( builder: builder, settings: settings, maintainState: maintainState, fullscreenDialog: fullscreenDialog, ); } else { - return CupertinoPageRoute( + return CupertinoPageRoute( builder: builder, settings: settings, title: title, @@ -2539,7 +2561,7 @@ class RouteGenerator { } } - static Route createSlideTransitionRoute(Widget viewToInsert) { + static Route createSlideTransitionRoute(Widget viewToInsert) { return PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => viewToInsert, transitionsBuilder: (context, animation, secondaryAnimation, child) { @@ -2557,7 +2579,7 @@ class RouteGenerator { ); } - static Route _routeError(String message) { + static Route _routeError(String message) { // Replace with robust ErrorView page final Widget errorView = Scaffold( appBar: AppBar( @@ -2571,7 +2593,7 @@ class RouteGenerator { ), ); - return getRoute( + return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => errorView, ); diff --git a/lib/utilities/if_not_already.dart b/lib/utilities/if_not_already.dart new file mode 100644 index 0000000000..664fc9aad6 --- /dev/null +++ b/lib/utilities/if_not_already.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +class IfNotAlready { + final void Function() _function; + + bool _locked = false; + + IfNotAlready(this._function); + + void execute() { + if (_locked) return; + _locked = true; + try { + _function(); + } finally { + _locked = false; + } + } +} + +class IfNotAlreadyAsync { + final Future Function() _function; + + bool _locked = false; + + IfNotAlreadyAsync(this._function); + + Future execute() async { + if (!_locked) { + _locked = true; + try { + await _function(); + } finally { + _locked = false; + } + } + } +} diff --git a/lib/wallets/crypto_currency/coins/dogecoin.dart b/lib/wallets/crypto_currency/coins/dogecoin.dart index 1d281f5bba..375ce6bf8e 100644 --- a/lib/wallets/crypto_currency/coins/dogecoin.dart +++ b/lib/wallets/crypto_currency/coins/dogecoin.dart @@ -137,7 +137,7 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { privHDPrefix: 0x02fac398, pubHDPrefix: 0x02facafd, bech32Hrp: "doge", - messagePrefix: '\x18Dogecoin Signed Message:\n', + messagePrefix: '\x19Dogecoin Signed Message:\n', minFee: BigInt.from(1), // Not used in stack wallet currently minOutput: dustLimit.raw, // Not used in stack wallet currently feePerKb: BigInt.from(1), // Not used in stack wallet currently @@ -150,7 +150,7 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { privHDPrefix: 0x04358394, pubHDPrefix: 0x043587cf, bech32Hrp: "tdge", - messagePrefix: "\x18Dogecoin Signed Message:\n", + messagePrefix: "\x19Dogecoin Signed Message:\n", minFee: BigInt.from(1), // Not used in stack wallet currently minOutput: dustLimit.raw, // Not used in stack wallet currently feePerKb: BigInt.from(1), // Not used in stack wallet currently diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index f432bd77bc..21d57b6a6a 100644 --- a/lib/wallets/crypto_currency/coins/firo.dart +++ b/lib/wallets/crypto_currency/coins/firo.dart @@ -107,7 +107,7 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { privHDPrefix: 0x0488ade4, pubHDPrefix: 0x0488b21e, bech32Hrp: "bc", - messagePrefix: '\x18Zcoin Signed Message:\n', + messagePrefix: '\x16Zcoin Signed Message:\n', minFee: BigInt.from(1), // Not used in stack wallet currently minOutput: dustLimit.raw, // Not used in stack wallet currently feePerKb: BigInt.from(1), // Not used in stack wallet currently @@ -120,7 +120,7 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { privHDPrefix: 0x04358394, pubHDPrefix: 0x043587cf, bech32Hrp: "tb", - messagePrefix: "\x18Zcoin Signed Message:\n", + messagePrefix: "\x16Zcoin Signed Message:\n", minFee: BigInt.from(1), // Not used in stack wallet currently minOutput: dustLimit.raw, // Not used in stack wallet currently feePerKb: BigInt.from(1), // Not used in stack wallet currently diff --git a/lib/wallets/crypto_currency/coins/namecoin.dart b/lib/wallets/crypto_currency/coins/namecoin.dart index 7945e8bca7..464eb0b75c 100644 --- a/lib/wallets/crypto_currency/coins/namecoin.dart +++ b/lib/wallets/crypto_currency/coins/namecoin.dart @@ -148,11 +148,10 @@ class Namecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { return (address: addr, addressType: AddressType.p2pkh); case DerivePathType.bip49: - final p2wpkhScript = - coinlib.P2WPKHAddress.fromPublicKey( - publicKey, - hrp: networkParams.bech32Hrp, - ).program.script; + final p2wpkhScript = coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ).program.script; final addr = coinlib.P2SHAddress.fromRedeemScript( p2wpkhScript, @@ -186,7 +185,7 @@ class Namecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { privHDPrefix: 0x0488ade4, pubHDPrefix: 0x0488b21e, bech32Hrp: "nc", - messagePrefix: '\x18Namecoin Signed Message:\n', + messagePrefix: '\x19Namecoin Signed Message:\n', minFee: BigInt.from(1), // Not used in stack wallet currently minOutput: dustLimit.raw, // Not used in stack wallet currently feePerKb: BigInt.from(1), // Not used in stack wallet currently diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 0af68f2a3c..05170c94bc 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; @@ -36,11 +37,12 @@ import 'cpfp_interface.dart'; import 'mweb_interface.dart'; import 'paynym_interface.dart'; import 'rbf_interface.dart'; +import 'sign_verify_interface.dart'; import 'view_only_option_interface.dart'; mixin ElectrumXInterface on Bip39HDWallet - implements ViewOnlyOptionInterface { + implements ViewOnlyOptionInterface, SignVerifyInterface { late ElectrumXClient electrumXClient; late CachedElectrumXClient electrumXCachedClient; @@ -2033,6 +2035,57 @@ mixin ElectrumXInterface } } + @override + Future signMessage( + final String message, { + required final Address address, + }) async { + if (isViewOnly) { + throw Exception("Cannot sign a message in a view only wallet"); + } + + final root = await getRootHDNode(); + final keyPair = root.derivePath(address.derivationPath!.value); + + final signed = coinlib.MessageSignature.sign( + key: keyPair.privateKey, + message: message, + prefix: _cleanEncodedPrefixLength( + cryptoCurrency.networkParams.messagePrefix, + ), + ); + + return base64Encode(signed.signature.compact); + } + + @override + Future verifyMessage( + final String message, { + required final String address, + required final String signature, + }) async { + final signed = coinlib.MessageSignature.fromBase64(signature); + + coinlib.Address clAddress; + try { + clAddress = coinlib.Address.fromString( + normalizeAddress(address), + cryptoCurrency.networkParams, + ); + } catch (e, s) { + Logging.instance.i("$e\n$s"); + return false; + } + + return signed.verifyAddress( + address: clAddress, + message: message, + prefix: _cleanEncodedPrefixLength( + cryptoCurrency.networkParams.messagePrefix, + ), + ); + } + // =========================================================================== // ========== Interface functions ============================================ @@ -2054,6 +2107,22 @@ mixin ElectrumXInterface // =========================================================================== // ========== private helpers ================================================ + String _cleanEncodedPrefixLength(String prefix) { + final messagePrefixBytes = + cryptoCurrency.networkParams.messagePrefix.toUint8ListFromUtf8; + // Check if prefix already has length encoded and remove as coinlib + // recalculates it. Really not ideal.... + // TODO: clean up cryptoCurrency.networkParams.messagePrefix once its + // determined that every usage of messagePrefix does not expect the length + // prefixed. + final ignoreFirstByte = + messagePrefixBytes.first == messagePrefixBytes.length - 1; + return (ignoreFirstByte + ? messagePrefixBytes.sublist(1) + : messagePrefixBytes) + .toUtf8String; + } + List _spendableUTXOs(List utxos) { return utxos .where( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/sign_verify_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/sign_verify_interface.dart new file mode 100644 index 0000000000..fdceb34615 --- /dev/null +++ b/lib/wallets/wallet/wallet_mixin_interfaces/sign_verify_interface.dart @@ -0,0 +1,14 @@ +import '../../../models/isar/models/blockchain_data/address.dart'; + +mixin SignVerifyInterface { + Future signMessage( + final String message, { + required final Address address, + }); + + Future verifyMessage( + final String message, { + required final String address, + required final String signature, + }); +} diff --git a/lib/widgets/detail_item.dart b/lib/widgets/detail_item.dart index 91a662538d..b45a36d5ff 100644 --- a/lib/widgets/detail_item.dart +++ b/lib/widgets/detail_item.dart @@ -12,12 +12,15 @@ class DetailItem extends StatelessWidget { required this.title, required this.detail, this.button, + this.titleStyle, this.overrideDetailTextColor, this.showEmptyDetail = true, this.horizontal = false, this.disableSelectableText = false, this.borderColor, this.expandDetail = false, + this.detailPlaceholder, + this.noPadding = false, }); final String title; @@ -29,6 +32,9 @@ class DetailItem extends StatelessWidget { final Color? overrideDetailTextColor; final Color? borderColor; final bool expandDetail; + final String? detailPlaceholder; + final TextStyle? titleStyle; + final bool noPadding; @override Widget build(BuildContext context) { @@ -41,7 +47,7 @@ class DetailItem extends StatelessWidget { } if (detail.isEmpty && showEmptyDetail) { - _detail = "$title will appear here"; + _detail = detailPlaceholder ?? "$title will appear here"; detailStyle = detailStyle.copyWith( color: Theme.of(context).extension()!.textSubtitle3, ); @@ -51,14 +57,17 @@ class DetailItem extends StatelessWidget { horizontal: horizontal, borderColor: borderColor, expandDetail: expandDetail, - title: - disableSelectableText - ? Text(title, style: STextStyles.itemSubtitle(context)) - : SelectableText(title, style: STextStyles.itemSubtitle(context)), - detail: - disableSelectableText - ? Text(_detail, style: detailStyle) - : SelectableText(_detail, style: detailStyle), + noPadding: noPadding, + title: disableSelectableText + ? Text(title, style: titleStyle ?? STextStyles.itemSubtitle(context)) + : SelectableText( + title, + style: titleStyle ?? STextStyles.itemSubtitle(context), + ), + detail: disableSelectableText + ? Text(_detail, style: detailStyle) + : SelectableText(_detail, style: detailStyle), + button: button, ); } } @@ -72,6 +81,7 @@ class DetailItemBase extends StatelessWidget { this.horizontal = false, this.borderColor, this.expandDetail = false, + this.noPadding = false, }); final Widget title; @@ -80,53 +90,55 @@ class DetailItemBase extends StatelessWidget { final bool horizontal; final Color? borderColor; final bool expandDetail; + final bool noPadding; @override Widget build(BuildContext context) { return ConditionalParent( condition: !Util.isDesktop || borderColor != null, - builder: - (child) => RoundedWhiteContainer( - padding: - Util.isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - borderColor: borderColor, - child: child, - ), + builder: (child) => RoundedWhiteContainer( + padding: noPadding + ? EdgeInsets.zero + : Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + borderColor: borderColor, + child: child, + ), child: ConditionalParent( condition: Util.isDesktop && borderColor == null, - builder: - (child) => Padding(padding: const EdgeInsets.all(16), child: child), - child: - horizontal - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - title, - if (expandDetail) const SizedBox(width: 16), - ConditionalParent( - condition: expandDetail, - builder: (child) => Expanded(child: child), - child: detail, - ), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [title, button ?? Container()], - ), - const SizedBox(height: 5), - ConditionalParent( - condition: expandDetail, - builder: (child) => Expanded(child: child), - child: detail, - ), - ], - ), + builder: (child) => Padding( + padding: noPadding ? EdgeInsets.zero : const EdgeInsets.all(16), + child: child, + ), + child: horizontal + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + title, + if (expandDetail) const SizedBox(width: 16), + ConditionalParent( + condition: expandDetail, + builder: (child) => Expanded(child: child), + child: detail, + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [title, button ?? Container()], + ), + const SizedBox(height: 5), + ConditionalParent( + condition: expandDetail, + builder: (child) => Expanded(child: child), + child: detail, + ), + ], + ), ), ); } diff --git a/lib/widgets/textfields/adaptive_text_field.dart b/lib/widgets/textfields/adaptive_text_field.dart new file mode 100644 index 0000000000..e57746a80f --- /dev/null +++ b/lib/widgets/textfields/adaptive_text_field.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../utilities/constants.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../icon_widgets/clipboard_icon.dart'; +import '../icon_widgets/x_icon.dart'; +import '../stack_text_field.dart'; +import '../textfield_icon_button.dart'; + +class AdaptiveTextField extends StatefulWidget { + const AdaptiveTextField({ + super.key, + this.labelText, + this.controller, + this.focusNode, + this.autocorrect, + this.readOnly = false, + this.enableSuggestions = true, + this.onChanged, + this.onChangedComprehensive, + this.onSubmitted, + this.suffixIcons, + this.contentPadding, + this.minLines, + this.maxLines, + this.showPasteClearButton = false, + }); + + final String? labelText; + + final TextEditingController? controller; + final FocusNode? focusNode; + final bool? autocorrect; + final EdgeInsets? contentPadding; + final int? minLines; + final int? maxLines; + + final bool readOnly; + final bool enableSuggestions; + + final void Function(String)? onChanged; + final void Function(String)? onChangedComprehensive; + final void Function(String)? onSubmitted; + + /// This will be ignored if [suffixIcons] is not null! + final bool showPasteClearButton; + + /// If this is not null, [showPasteClearButton] will be ignored. + final List? suffixIcons; + + @override + State createState() => _AdaptiveTextFieldState(); +} + +class _AdaptiveTextFieldState extends State { + late final FocusNode _focusNode; + late final bool _focusFlag; + + TextEditingController? _controller; + TextEditingController get controller => widget.controller ?? _controller!; + + String _cache = ""; + + @override + void initState() { + super.initState(); + + if (widget.controller == null) { + _controller = TextEditingController(); + } else if (widget.onChangedComprehensive != null) { + widget.controller!.addListener(() { + if (widget.controller!.text != _cache) { + _cache = widget.controller!.text; + widget.onChangedComprehensive!.call(_cache); + } + }); + } + + if (widget.focusNode == null) { + _focusFlag = true; + _focusNode = FocusNode(); + } else { + _focusFlag = false; + _focusNode = widget.focusNode!; + } + } + + @override + void dispose() { + if (_focusFlag) _focusNode.dispose(); + _controller?.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), + child: TextField( + minLines: widget.minLines, + maxLines: widget.maxLines, + style: Util.isDesktop + ? STextStyles.field(context).copyWith(fontSize: 16) + : STextStyles.field(context), + controller: controller, + focusNode: _focusNode, + onChanged: widget.onChanged, + readOnly: widget.readOnly, + autocorrect: widget.autocorrect, + enableSuggestions: widget.enableSuggestions, + onSubmitted: widget.onSubmitted, + decoration: + standardInputDecoration( + widget.labelText, + _focusNode, + context, + ).copyWith( + contentPadding: + widget.contentPadding ?? + (Util.isDesktop + ? const EdgeInsets.only( + left: 12, + top: 11, + bottom: 12, + right: 5, + ) + : const EdgeInsets.only( + left: 10, + top: 12, + bottom: 8, + right: 5, + )), + suffixIcon: widget.suffixIcons?.isNotEmpty == true + ? Padding( + padding: controller.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: widget.suffixIcons!, + ), + ), + ) + : widget.showPasteClearButton + ? TextFieldIconButton( + onTap: () async { + if (controller.text.isEmpty) { + final ClipboardData? data = await Clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && data!.text!.isNotEmpty) { + final content = data.text!.trim(); + controller.text = content; + } + } else { + controller.text = ""; + } + + if (mounted) setState(() {}); + }, + child: controller.text.isNotEmpty + ? const XIcon() + : const ClipboardIcon(), + ) + : null, + ), + ), + ); + } +} From dd67c8586991cb25b3e1d0c36491e7fbd9f0e7b3 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 4 Nov 2025 16:21:21 -0600 Subject: [PATCH 13/50] optimize/cleanup electrumx_interface wallet addresses saved on recover/rescan --- lib/wallets/wallet/impl/firo_wallet.dart | 151 ++++-------- .../electrumx_interface.dart | 121 +++------- .../mweb_interface.dart | 216 +++++++----------- 3 files changed, 161 insertions(+), 327 deletions(-) diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index e55052e903..3bd04d80ca 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:math'; import 'package:decimal/decimal.dart'; import 'package:isar_community/isar.dart'; @@ -81,17 +80,15 @@ class FiroWallet extends Bip39HDWallet final List
allAddressesOld = await fetchAddressesForElectrumXScan(); - final Set receivingAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => convertAddressString(e.value)) + .toSet(); - final Set changeAddresses = - allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => convertAddressString(e.value)) + .toSet(); final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -99,23 +96,21 @@ class FiroWallet extends Bip39HDWallet allAddressesSet, ); - final sparkCoins = - await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .findAll(); + final sparkCoins = await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .findAll(); final List> allTransactions = []; // some lelantus transactions aren't fetched via wallet addresses so they // will never show as confirmed in the gui. - final unconfirmedTransactions = - await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .heightIsNull() - .findAll(); + final unconfirmedTransactions = await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .heightIsNull() + .findAll(); for (final tx in unconfirmedTransactions) { final txn = await electrumXCachedClient.getTransaction( txHash: tx.txid, @@ -154,13 +149,12 @@ class FiroWallet extends Bip39HDWallet final currentHeight = await chainHeight; for (final txHash in allTxHashes) { - final storedTx = - await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(txHash["tx_hash"] as String) - .findFirst(); + final storedTx = await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(txHash["tx_hash"] as String) + .findFirst(); if (storedTx?.isConfirmed( currentHeight, @@ -214,8 +208,9 @@ class FiroWallet extends Bip39HDWallet bool isSparkMint = false; final bool isSparkSpend = txData["type"] == 9 && txData["version"] == 3; final bool isMySpark = sparkTxids.contains(txData["txid"] as String); - final bool isMySpentSpark = - missing.where((e) => e.txid == txData["txid"]).isNotEmpty; + final bool isMySpentSpark = missing + .where((e) => e.txid == txData["txid"]) + .isNotEmpty; final sparkCoinsInvolvedReceived = sparkCoins.where( (e) => @@ -298,19 +293,17 @@ class FiroWallet extends Bip39HDWallet if (output.addresses.isEmpty && output.scriptPubKeyHex.length >= 488) { // likely spark related - final opByte = - output.scriptPubKeyHex - .substring(0, 2) - .toUint8ListFromHex - .first; + final opByte = output.scriptPubKeyHex + .substring(0, 2) + .toUint8ListFromHex + .first; if (opByte == OP_SPARKMINT || opByte == OP_SPARKSMINT) { final serCoin = base64Encode( output.scriptPubKeyHex.substring(2, 488).toUint8ListFromHex, ); - final coin = - sparkCoinsInvolvedReceived - .where((e) => e.serializedCoinB64!.startsWith(serCoin)) - .firstOrNull; + final coin = sparkCoinsInvolvedReceived + .where((e) => e.serializedCoinB64!.startsWith(serCoin)) + .firstOrNull; if (coin == null) { // not ours @@ -403,10 +396,9 @@ class FiroWallet extends Bip39HDWallet txid: txData["txid"] as String, network: cryptoCurrency.network, ); - spentSparkCoins = - sparkCoinsInvolvedSpent - .where((e) => tags.contains(e.lTagHash)) - .toList(); + spentSparkCoins = sparkCoinsInvolvedSpent + .where((e) => tags.contains(e.lTagHash)) + .toList(); } else if (isSparkSpend) { parseAnonFees(); } else if (isSparkMint) { @@ -490,11 +482,10 @@ class FiroWallet extends Bip39HDWallet if (usedCoins.isNotEmpty) { input = input.copyWith( addresses: usedCoins.map((e) => e.address).toList(), - valueStringSats: - usedCoins - .map((e) => e.value) - .reduce((value, element) => value += element) - .toString(), + valueStringSats: usedCoins + .map((e) => e.value) + .reduce((value, element) => value += element) + .toString(), walletOwns: true, ); wasSentFromThisWallet = true; @@ -505,11 +496,10 @@ class FiroWallet extends Bip39HDWallet spentSparkCoins.isNotEmpty) { input = input.copyWith( addresses: spentSparkCoins.map((e) => e.address).toList(), - valueStringSats: - spentSparkCoins - .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + e) - .toString(), + valueStringSats: spentSparkCoins + .map((e) => e.value) + .fold(BigInt.zero, (p, e) => p + e) + .toString(), walletOwns: true, ); wasSentFromThisWallet = true; @@ -755,53 +745,10 @@ class FiroWallet extends Bip39HDWallet Future.wait(changeFutures), ]); - final receiveResults = futuresResult[0]; - final changeResults = futuresResult[1]; - - final List
addressesToStore = []; - - int highestReceivingIndexWithHistory = 0; - - for (final tuple in receiveResults) { - if (tuple.addresses.isEmpty) { - if (info.otherData[WalletInfoKeys.reuseAddress] != true) { - await checkReceivingAddressForTransactions(); - } - } else { - highestReceivingIndexWithHistory = max( - tuple.index, - highestReceivingIndexWithHistory, - ); - addressesToStore.addAll(tuple.addresses); - } - } - - int highestChangeIndexWithHistory = 0; - // If restoring a wallet that never sent any funds with change, then set changeArray - // manually. If we didn't do this, it'd store an empty array. - for (final tuple in changeResults) { - if (tuple.addresses.isEmpty) { - await checkChangeAddressForTransactions(); - } else { - highestChangeIndexWithHistory = max( - tuple.index, - highestChangeIndexWithHistory, - ); - addressesToStore.addAll(tuple.addresses); - } - } - - // remove extra addresses to help minimize risk of creating a large gap - addressesToStore.removeWhere( - (e) => - e.subType == AddressSubType.change && - e.derivationIndex > highestChangeIndexWithHistory, - ); - addressesToStore.removeWhere( - (e) => - e.subType == AddressSubType.receiving && - e.derivationIndex > highestReceivingIndexWithHistory, - ); + final List
addressesToStore = processGapCheckResults([ + ...futuresResult[0], + ...futuresResult[1], + ]); await mainDB.updateOrPutAddresses(addressesToStore); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 05170c94bc..0aa674bfb4 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1072,7 +1072,7 @@ mixin ElectrumXInterface ) async { final List
addressArray = []; int gapCounter = 0; - int highestIndexWithHistory = 0; + int highestIndexWithHistory = -1; for ( int index = 0; @@ -1163,6 +1163,7 @@ mixin ElectrumXInterface final List
addressArray = []; int gapCounter = 0; int index = 0; + int highestIndexWithHistory = -1; for (; gapCounter < cryptoCurrency.maxUnusedAddressGap; index++) { Logging.instance.d( @@ -1212,10 +1213,11 @@ mixin ElectrumXInterface ), ); + addressArray.add(address); + // check and add appropriate addresses if (count > 0) { - // add address to array - addressArray.add(address); + highestIndexWithHistory = index; // reset counter gapCounter = 0; // add info to derivations @@ -1225,7 +1227,7 @@ mixin ElectrumXInterface } } - return (addresses: addressArray, index: index); + return (addresses: addressArray, index: highestIndexWithHistory); } Future>> fetchHistory( @@ -1640,51 +1642,10 @@ mixin ElectrumXInterface Future.wait(changeFutures), ]); - final receiveResults = futuresResult[0]; - final changeResults = futuresResult[1]; - - final List
addressesToStore = []; - - int highestReceivingIndexWithHistory = 0; - - for (final tuple in receiveResults) { - if (tuple.addresses.isEmpty) { - await checkReceivingAddressForTransactions(); - } else { - highestReceivingIndexWithHistory = max( - tuple.index, - highestReceivingIndexWithHistory, - ); - addressesToStore.addAll(tuple.addresses); - } - } - - int highestChangeIndexWithHistory = 0; - // If restoring a wallet that never sent any funds with change, then set changeArray - // manually. If we didn't do this, it'd store an empty array. - for (final tuple in changeResults) { - if (tuple.addresses.isEmpty) { - await checkChangeAddressForTransactions(); - } else { - highestChangeIndexWithHistory = max( - tuple.index, - highestChangeIndexWithHistory, - ); - addressesToStore.addAll(tuple.addresses); - } - } - - // remove extra addresses to help minimize risk of creating a large gap - addressesToStore.removeWhere( - (e) => - e.subType == AddressSubType.change && - e.derivationIndex > highestChangeIndexWithHistory, - ); - addressesToStore.removeWhere( - (e) => - e.subType == AddressSubType.receiving && - e.derivationIndex > highestReceivingIndexWithHistory, - ); + final List
addressesToStore = processGapCheckResults([ + ...futuresResult[0], + ...futuresResult[1], + ]); await mainDB.updateOrPutAddresses(addressesToStore); @@ -2177,6 +2138,24 @@ mixin ElectrumXInterface return address; } + List
processGapCheckResults( + List<({int index, List
addresses})> results, + ) { + final List
result = []; + for (final tuple in results) { + if (tuple.addresses.isNotEmpty) { + int highestIndexWithHistory = -1; + highestIndexWithHistory = max(tuple.index, highestIndexWithHistory); + + result.addAll( + tuple.addresses.where( + (e) => e.derivationIndex <= highestIndexWithHistory, + ), + ); + } + } + return result; + } // ============== View only ================================================== @override @@ -2277,48 +2256,8 @@ mixin ElectrumXInterface Future.wait(changeFutures), ]); - final receiveResults = futuresResult[0]; - final changeResults = futuresResult[1]; - - int highestReceivingIndexWithHistory = 0; - - for (final tuple in receiveResults) { - if (tuple.addresses.isEmpty) { - await checkReceivingAddressForTransactions(); - } else { - highestReceivingIndexWithHistory = max( - tuple.index, - highestReceivingIndexWithHistory, - ); - addressesToStore.addAll(tuple.addresses); - } - } - - int highestChangeIndexWithHistory = 0; - // If restoring a wallet that never sent any funds with change, then set changeArray - // manually. If we didn't do this, it'd store an empty array. - for (final tuple in changeResults) { - if (tuple.addresses.isEmpty) { - await checkChangeAddressForTransactions(); - } else { - highestChangeIndexWithHistory = max( - tuple.index, - highestChangeIndexWithHistory, - ); - addressesToStore.addAll(tuple.addresses); - } - } - - // remove extra addresses to help minimize risk of creating a large gap - addressesToStore.removeWhere( - (e) => - e.subType == AddressSubType.change && - e.derivationIndex > highestChangeIndexWithHistory, - ); - addressesToStore.removeWhere( - (e) => - e.subType == AddressSubType.receiving && - e.derivationIndex > highestReceivingIndexWithHistory, + addressesToStore.addAll( + processGapCheckResults([...futuresResult[0], ...futuresResult[1]]), ); } else { final clAddress = coinlib.Address.fromString( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart index 82f45ff347..3ee2f89538 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -212,9 +212,9 @@ mixin MwebInterface try { await db.transaction(() async { final prev = - await (db.select(db.mwebUtxos)..where( - (e) => e.outputId.equals(utxo.outputId), - )).getSingleOrNull(); + await (db.select(db.mwebUtxos) + ..where((e) => e.outputId.equals(utxo.outputId))) + .getSingleOrNull(); if (prev == null) { final newUtxo = MwebUtxosCompanion( @@ -254,10 +254,9 @@ mixin MwebInterface blockHash: null, // ?? hash: "", txid: fakeTxid, - timestamp: - utxo.height < 1 - ? DateTime.now().millisecondsSinceEpoch ~/ 1000 - : utxo.blockTime, + timestamp: utxo.height < 1 + ? DateTime.now().millisecondsSinceEpoch ~/ 1000 + : utxo.blockTime, height: utxo.height, inputs: [], outputs: [ @@ -272,13 +271,11 @@ mixin MwebInterface type: TransactionType.incoming, subType: TransactionSubType.mweb, otherData: jsonEncode({ - TxV2OdKeys.overrideFee: - Amount( - rawValue: - BigInt - .zero, // TODO fill in correctly when we have a real txid - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + TxV2OdKeys.overrideFee: Amount( + rawValue: BigInt + .zero, // TODO fill in correctly when we have a real txid + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), }), ); @@ -359,19 +356,18 @@ mixin MwebInterface } Future checkMwebSpends() async { - final pending = - await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .heightIsNull() - .and() - .blockHashIsNull() - .and() - .subTypeEqualTo(TransactionSubType.mweb) - .and() - .typeEqualTo(TransactionType.outgoing) - .findAll(); + final pending = await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .heightIsNull() + .and() + .blockHashIsNull() + .and() + .subTypeEqualTo(TransactionSubType.mweb) + .and() + .typeEqualTo(TransactionType.outgoing) + .findAll(); Logging.instance.f(pending); @@ -391,11 +387,10 @@ mixin MwebInterface // dummy to show tx as confirmed. Need a better way to handle this as its kind of stupid, resulting in terrible UX final dummyHeight = await chainHeight; - TransactionV2? transaction = - await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(tx.txid, walletId) - .findFirst(); + TransactionV2? transaction = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(tx.txid, walletId) + .findFirst(); if (transaction == null || transaction.height == null) { transaction = (transaction ?? tx).copyWith(height: dummyHeight); @@ -504,19 +499,18 @@ mixin MwebInterface Logging.instance.d("Sent txHash: $txHash"); txData = txData.copyWith( - usedUTXOs: - txData.usedUTXOs!.map((e) { - if (e is StandardInput) { - return StandardInput( - e.utxo.copyWith(used: true), - derivePathType: e.derivePathType, - ); - } else if (e is MwebInput) { - return MwebInput(e.utxo.copyWith(used: true)); - } else { - return e; - } - }).toList(), + usedUTXOs: txData.usedUTXOs!.map((e) { + if (e is StandardInput) { + return StandardInput( + e.utxo.copyWith(used: true), + derivePathType: e.derivePathType, + ); + } else if (e is MwebInput) { + return MwebInput(e.utxo.copyWith(used: true)); + } else { + return e; + } + }).toList(), txHash: txHash, txid: txHash, ); @@ -530,8 +524,10 @@ mixin MwebInterface ); // Update used mweb utxos as used in database - final usedMwebUtxos = - txData.usedUTXOs!.whereType().map((e) => e.utxo).toList(); + final usedMwebUtxos = txData.usedUTXOs! + .whereType() + .map((e) => e.utxo) + .toList(); Logging.instance.i("Used mweb inputs: $usedMwebUtxos"); @@ -557,10 +553,9 @@ mixin MwebInterface @override Future prepareSend({required TxData txData}) async { - final hasMwebOutputs = - txData.recipients! - .where((e) => e.addressType == AddressType.mweb) - .isNotEmpty; + final hasMwebOutputs = txData.recipients! + .where((e) => e.addressType == AddressType.mweb) + .isNotEmpty; if (hasMwebOutputs) { // assume pegin tx txData = txData.copyWith(type: TxType.mwebPegIn); @@ -571,10 +566,9 @@ mixin MwebInterface /// prepare mweb transaction where spending mweb outputs Future prepareSendMweb({required TxData txData}) async { - final hasMwebOutputs = - txData.recipients! - .where((e) => e.addressType == AddressType.mweb) - .isNotEmpty; + final hasMwebOutputs = txData.recipients! + .where((e) => e.addressType == AddressType.mweb) + .isNotEmpty; final type = hasMwebOutputs ? TxType.mweb : TxType.mwebPegOut; @@ -594,25 +588,23 @@ mixin MwebInterface try { final currentHeight = await chainHeight; - final spendableUtxos = - await mainDB.isar.utxos - .where() - .walletIdEqualTo(walletId) - .filter() - .isBlockedEqualTo(false) - .and() - .group((q) => q.usedEqualTo(false).or().usedIsNull()) - .and() - .valueGreaterThan(0) - .findAll(); + final spendableUtxos = await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .isBlockedEqualTo(false) + .and() + .group((q) => q.usedEqualTo(false).or().usedIsNull()) + .and() + .valueGreaterThan(0) + .findAll(); spendableUtxos.removeWhere( - (e) => - !e.isConfirmed( - currentHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - ), + (e) => !e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), ); if (spendableUtxos.isEmpty) { @@ -713,9 +705,9 @@ mixin MwebInterface try { final currentHeight = await chainHeight; final db = Drift.get(walletId); - final mwebUtxos = - await (db.select(db.mwebUtxos) - ..where((e) => e.used.equals(false))).get(); + final mwebUtxos = await (db.select( + db.mwebUtxos, + )..where((e) => e.used.equals(false))).get(); Amount satoshiBalanceTotal = Amount( rawValue: BigInt.zero, @@ -871,53 +863,10 @@ mixin MwebInterface Future.wait(changeFutures), ]); - final receiveResults = futuresResult[0]; - final changeResults = futuresResult[1]; - - final List
addressesToStore = []; - - int highestReceivingIndexWithHistory = 0; - - for (final tuple in receiveResults) { - if (tuple.addresses.isEmpty) { - if (info.otherData[WalletInfoKeys.reuseAddress] != true) { - await checkReceivingAddressForTransactions(); - } - } else { - highestReceivingIndexWithHistory = math.max( - tuple.index, - highestReceivingIndexWithHistory, - ); - addressesToStore.addAll(tuple.addresses); - } - } - - int highestChangeIndexWithHistory = 0; - // If restoring a wallet that never sent any funds with change, then set changeArray - // manually. If we didn't do this, it'd store an empty array. - for (final tuple in changeResults) { - if (tuple.addresses.isEmpty) { - await checkChangeAddressForTransactions(); - } else { - highestChangeIndexWithHistory = math.max( - tuple.index, - highestChangeIndexWithHistory, - ); - addressesToStore.addAll(tuple.addresses); - } - } - - // remove extra addresses to help minimize risk of creating a large gap - addressesToStore.removeWhere( - (e) => - e.subType == AddressSubType.change && - e.derivationIndex > highestChangeIndexWithHistory, - ); - addressesToStore.removeWhere( - (e) => - e.subType == AddressSubType.receiving && - e.derivationIndex > highestReceivingIndexWithHistory, - ); + final List
addressesToStore = processGapCheckResults([ + ...futuresResult[0], + ...futuresResult[1], + ]); await mainDB.updateOrPutAddresses(addressesToStore); }); @@ -979,18 +928,17 @@ mixin MwebInterface ); BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b; - final posUtxos = - utxos - .where( - (utxo) => processedTx.inputs.any( - (input) => - input.prevOut.hash.toHex == - Uint8List.fromList( - utxo.id.toUint8ListFromHex.reversed.toList(), - ).toHex, - ), - ) - .toList(); + final posUtxos = utxos + .where( + (utxo) => processedTx.inputs.any( + (input) => + input.prevOut.hash.toHex == + Uint8List.fromList( + utxo.id.toUint8ListFromHex.reversed.toList(), + ).toHex, + ), + ) + .toList(); final posOutputSum = processedTx.outputs.fold( BigInt.zero, From 3e6291c5a45fe93302a278d09873936e4229b1aa Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 5 Nov 2025 09:34:03 -0600 Subject: [PATCH 14/50] bitcoin legacy address toggle option --- lib/pages/receive_view/receive_view.dart | 367 +++++++++--------- .../wallet_settings_wallet_settings_view.dart | 82 ++++ .../sub_widgets/desktop_receive.dart | 116 +++--- .../sub_widgets/desktop_wallet_features.dart | 7 +- .../more_features/more_features_dialog.dart | 104 +++-- lib/wallets/isar/models/wallet_info.dart | 29 +- 6 files changed, 420 insertions(+), 285 deletions(-) diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 5fa78b359a..6d7f7e10d8 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -112,12 +112,10 @@ class _ReceiveViewState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: - (context) => StackOkDialog( - title: "Slatepack receive error", - message: - ex?.toString() ?? "Unexpected result without exception", - ), + builder: (context) => StackOkDialog( + title: "Slatepack receive error", + message: ex?.toString() ?? "Unexpected result without exception", + ), ); } return; @@ -127,27 +125,25 @@ class _ReceiveViewState extends ConsumerState { final response = await showDialog<({String responseSlatepack, bool wasEncrypted})>( context: context, - builder: - (context) => SDialog( - child: MwcSlatepackImportDialog( - walletId: widget.walletId, - clipboard: widget.clipboard, - rawSlatepack: result.raw, - decoded: result.result, - slatepackType: result.type, - ), - ), + builder: (context) => SDialog( + child: MwcSlatepackImportDialog( + walletId: widget.walletId, + clipboard: widget.clipboard, + rawSlatepack: result.raw, + decoded: result.result, + slatepackType: result.type, + ), + ), ); if (mounted && response != null) { await showDialog( context: context, barrierDismissible: false, - builder: - (context) => SlatepackResponseDialog( - responseSlatepack: response.responseSlatepack, - wasEncrypted: response.wasEncrypted, - ), + builder: (context) => SlatepackResponseDialog( + responseSlatepack: response.responseSlatepack, + wasEncrypted: response.wasEncrypted, + ), ); } } @@ -300,6 +296,32 @@ class _ReceiveViewState extends ConsumerState { } } + StreamSubscription _sub(AddressType type) { + return ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(type) + .and() + .subTypeEqualTo(AddressSubType.receiving) + .sortByDerivationIndexDesc() + .findFirst() + .asStream() + .listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[type] = + event?.value ?? _addressMap[type] ?? "[No address yet]"; + }); + } + }); + }); + } + @override void initState() { walletId = widget.walletId; @@ -341,7 +363,9 @@ class _ReceiveViewState extends ConsumerState { } } - if (_walletAddressTypes.length > 1 && wallet is BitcoinWallet) { + if (_walletAddressTypes.length > 1 && + wallet is BitcoinWallet && + !wallet.info.isLegacyAddressesEnabled) { _walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh); } @@ -351,30 +375,7 @@ class _ReceiveViewState extends ConsumerState { if (_showMultiType) { for (final type in _walletAddressTypes) { - _addressSubMap[type] = ref - .read(mainDBProvider) - .isar - .addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .typeEqualTo(type) - .and() - .not() - .subTypeEqualTo(AddressSubType.change) - .sortByDerivationIndexDesc() - .findFirst() - .asStream() - .listen((event) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _addressMap[type] = - event?.value ?? _addressMap[type] ?? "[No address yet]"; - }); - } - }); - }); + _addressSubMap[type] = _sub(type); } } @@ -399,42 +400,40 @@ class _ReceiveViewState extends ConsumerState { if (prev?.isMwebEnabled != next.isMwebEnabled) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { + const type = AddressType.mweb; setState(() { supportsMweb = next.isMwebEnabled; - if (supportsMweb && - !_walletAddressTypes.contains(AddressType.mweb)) { - _walletAddressTypes.insert(0, AddressType.mweb); - _addressSubMap[AddressType.mweb] = ref - .read(mainDBProvider) - .isar - .addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .typeEqualTo(AddressType.mweb) - .and() - .not() - .subTypeEqualTo(AddressSubType.change) - .sortByDerivationIndexDesc() - .findFirst() - .asStream() - .listen((event) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _addressMap[AddressType.mweb] = - event?.value ?? - _addressMap[AddressType.mweb] ?? - "[No address yet]"; - }); - } - }); - }); + if (supportsMweb && !_walletAddressTypes.contains(type)) { + _walletAddressTypes.insert(0, type); + + _addressSubMap[type] = _sub(type); + } else { + _walletAddressTypes.remove(type); + _addressSubMap[type]?.cancel(); + _addressSubMap.remove(type); + } + + if (_currentIndex >= _walletAddressTypes.length) { + _currentIndex = _walletAddressTypes.length - 1; + } + }); + } + }); + } + + if (prev?.isLegacyAddressesEnabled != next.isLegacyAddressesEnabled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + const type = AddressType.p2pkh; + setState(() { + if (!_walletAddressTypes.contains(type)) { + _walletAddressTypes.insert(0, type); + _addressSubMap[type] = _sub(type); } else { - _walletAddressTypes.remove(AddressType.mweb); - _addressSubMap[AddressType.mweb]?.cancel(); - _addressSubMap.remove(AddressType.mweb); + _walletAddressTypes.remove(type); + _addressSubMap[type]?.cancel(); + _addressSubMap.remove(type); } if (_currentIndex >= _walletAddressTypes.length) { @@ -494,10 +493,9 @@ class _ReceiveViewState extends ConsumerState { color: Theme.of(context).extension()!.background, icon: SvgPicture.asset( Assets.svg.verticalEllipsis, - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -514,10 +512,9 @@ class _ReceiveViewState extends ConsumerState { right: 10, child: Container( decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.popupBG, + color: Theme.of( + context, + ).extension()!.popupBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -580,100 +577,94 @@ class _ReceiveViewState extends ConsumerState { children: [ ConditionalParent( condition: _showMultiType, - builder: - (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Address type", - style: STextStyles.w500_14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.infoItemLabel, - ), - ), - const SizedBox(height: 10), - DropdownButtonHideUnderline( - child: DropdownButton2( - value: _currentIndex, - items: [ - for ( - int i = 0; - i < _walletAddressTypes.length; - i++ - ) - DropdownMenuItem( - value: i, - child: Text( - _supportsSpark && - _walletAddressTypes[i] == - AddressType.p2pkh - ? "Transparent address" - : "${_walletAddressTypes[i].readableName} address", - style: STextStyles.w500_14(context), - ), - ), - ], - onChanged: (value) { - if (value != null && - value != _currentIndex) { - setState(() { - _currentIndex = value; - }); - } - }, - isExpanded: true, - iconStyleData: IconStyleData( - icon: Padding( - padding: const EdgeInsets.only(right: 10), - child: SvgPicture.asset( - Assets.svg.chevronDown, - width: 12, - height: 6, - color: - Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Address type", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ), + const SizedBox(height: 10), + DropdownButtonHideUnderline( + child: DropdownButton2( + value: _currentIndex, + items: [ + for ( + int i = 0; + i < _walletAddressTypes.length; + i++ + ) + DropdownMenuItem( + value: i, + child: Text( + _supportsSpark && + _walletAddressTypes[i] == + AddressType.p2pkh + ? "Transparent address" + : "${_walletAddressTypes[i].readableName} address", + style: STextStyles.w500_14(context), ), ), - buttonStyleData: ButtonStyleData( - decoration: BoxDecoration( - color: - Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), + ], + onChanged: (value) { + if (value != null && value != _currentIndex) { + setState(() { + _currentIndex = value; + }); + } + }, + isExpanded: true, + iconStyleData: IconStyleData( + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, -10), - elevation: 0, - decoration: BoxDecoration( - color: - Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), + ), + ), + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), + ), + ), + dropdownStyleData: DropdownStyleData( + offset: const Offset(0, -10), + elevation: 0, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), ), ), - const SizedBox(height: 12), - child, - ], + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), ), + const SizedBox(height: 12), + child, + ], + ), child: GestureDetector( onTap: () { HapticFeedback.lightImpact(); @@ -701,10 +692,9 @@ class _ReceiveViewState extends ConsumerState { Assets.svg.copy, width: 10, height: 10, - color: - Theme.of(context) - .extension()! - .infoItemIcons, + color: Theme.of(context) + .extension()! + .infoItemIcons, ), const SizedBox(width: 4), Text( @@ -753,14 +743,14 @@ class _ReceiveViewState extends ConsumerState { label: "Generate new address", onPressed: supportsMweb && - _walletAddressTypes[_currentIndex] == - AddressType.mweb - ? generateNewMwebAddress - : _supportsSpark && - _walletAddressTypes[_currentIndex] == - AddressType.spark - ? generateNewSparkAddress - : generateNewAddress, + _walletAddressTypes[_currentIndex] == + AddressType.mweb + ? generateNewMwebAddress + : _supportsSpark && + _walletAddressTypes[_currentIndex] == + AddressType.spark + ? generateNewSparkAddress + : generateNewAddress, ), // MWC Slatepack import button. if (coin is Mimblewimblecoin) ...[ @@ -794,11 +784,10 @@ class _ReceiveViewState extends ConsumerState { RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: - (_) => GenerateUriQrCodeView( - coin: coin, - receivingAddress: address, - ), + builder: (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: address, + ), settings: const RouteSettings( name: GenerateUriQrCodeView.routeName, ), 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 f030856a83..04907952e5 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 @@ -22,6 +22,7 @@ import '../../../../utilities/logger.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../wallets/isar/models/wallet_info.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../../wallets/wallet/impl/bitcoin_wallet.dart'; import '../../../../wallets/wallet/intermediate/cryptonote_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; @@ -87,6 +88,37 @@ class _WalletSettingsWalletSettingsViewState } } + bool _switchLegacyToggledLock = false; // Mutex. + Future _switchLegacyToggled() async { + if (_switchLegacyToggledLock) { + return; + } + _switchLegacyToggledLock = true; // Lock mutex. + + try { + // Toggle enableLegacyAddresses in wallet info. + await ref + .read(pWalletInfo(widget.walletId)) + .updateOtherData( + newEntries: { + WalletInfoKeys.enableLegacyAddresses: !ref + .read(pWalletInfo(widget.walletId)) + .isLegacyAddressesEnabled, + }, + isar: ref.read(mainDBProvider).isar, + ); + } catch (e, s) { + Logging.instance.f( + "Failed to update enableLegacyAddresses for wallet", + error: e, + stackTrace: s, + ); + } finally { + // ensure _switchLegacyToggledLock is set to false no matter what + _switchLegacyToggledLock = false; + } + } + bool _switchReuseAddressToggledLock = false; // Mutex. Future _switchReuseAddressToggled() async { if (_switchReuseAddressToggledLock) { @@ -475,6 +507,56 @@ class _WalletSettingsWalletSettingsViewState ), ), ), + if (wallet is BitcoinWallet) const SizedBox(height: 8), + if (wallet is BitcoinWallet) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: _switchLegacyToggled, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable legacy addresses", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: IgnorePointer( + child: DraggableSwitch( + value: + ref.watch( + pWalletInfo( + widget.walletId, + ).select( + (value) => value.otherData, + ), + )[WalletInfoKeys + .enableLegacyAddresses] + as bool? ?? + false, + onChanged: (_) => (), + ), + ), + ), + ], + ), + ), + ), + ), if (wallet is SparkInterface && !wallet.isViewOnly) const SizedBox(height: 8), if (wallet is SparkInterface && !wallet.isViewOnly) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 607bf3f9c5..30d92b55f6 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -312,6 +312,32 @@ class _DesktopReceiveState extends ConsumerState { } } + StreamSubscription _sub(AddressType type) { + return ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(type) + .and() + .subTypeEqualTo(AddressSubType.receiving) + .sortByDerivationIndexDesc() + .findFirst() + .asStream() + .listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[type] = + event?.value ?? _addressMap[type] ?? "[No address yet]"; + }); + } + }); + }); + } + @override void initState() { _receiveSlateController = TextEditingController(); @@ -356,7 +382,9 @@ class _DesktopReceiveState extends ConsumerState { } } - if (_walletAddressTypes.length > 1 && wallet is BitcoinWallet) { + if (_walletAddressTypes.length > 1 && + wallet is BitcoinWallet && + !wallet.info.isLegacyAddressesEnabled) { _walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh); } @@ -366,30 +394,7 @@ class _DesktopReceiveState extends ConsumerState { if (showMultiType) { for (final type in _walletAddressTypes) { - _addressSubMap[type] = ref - .read(mainDBProvider) - .isar - .addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .typeEqualTo(type) - .and() - .not() - .subTypeEqualTo(AddressSubType.change) - .sortByDerivationIndexDesc() - .findFirst() - .asStream() - .listen((event) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _addressMap[type] = - event?.value ?? _addressMap[type] ?? "[No address yet]"; - }); - } - }); - }); + _addressSubMap[type] = _sub(type); } } @@ -413,42 +418,39 @@ class _DesktopReceiveState extends ConsumerState { if (prev?.isMwebEnabled != next.isMwebEnabled) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { + const type = AddressType.mweb; setState(() { supportsMweb = next.isMwebEnabled; - if (supportsMweb && - !_walletAddressTypes.contains(AddressType.mweb)) { - _walletAddressTypes.insert(0, AddressType.mweb); - _addressSubMap[AddressType.mweb] = ref - .read(mainDBProvider) - .isar - .addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .typeEqualTo(AddressType.mweb) - .and() - .not() - .subTypeEqualTo(AddressSubType.change) - .sortByDerivationIndexDesc() - .findFirst() - .asStream() - .listen((event) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _addressMap[AddressType.mweb] = - event?.value ?? - _addressMap[AddressType.mweb] ?? - "[No address yet]"; - }); - } - }); - }); + if (supportsMweb && !_walletAddressTypes.contains(type)) { + _walletAddressTypes.insert(0, type); + _addressSubMap[type] = _sub(type); + } else { + _walletAddressTypes.remove(type); + _addressSubMap[type]?.cancel(); + _addressSubMap.remove(type); + } + + if (_currentIndex >= _walletAddressTypes.length) { + _currentIndex = _walletAddressTypes.length - 1; + } + }); + } + }); + } + + if (prev?.isLegacyAddressesEnabled != next.isLegacyAddressesEnabled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + const type = AddressType.p2pkh; + setState(() { + if (!_walletAddressTypes.contains(type)) { + _walletAddressTypes.insert(0, type); + _addressSubMap[type] = _sub(type); } else { - _walletAddressTypes.remove(AddressType.mweb); - _addressSubMap[AddressType.mweb]?.cancel(); - _addressSubMap.remove(AddressType.mweb); + _walletAddressTypes.remove(type); + _addressSubMap[type]?.cancel(); + _addressSubMap.remove(type); } if (_currentIndex >= _walletAddressTypes.length) { diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index 4a709a0bc6..dd8869c5ab 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -40,6 +40,7 @@ import '../../../../utilities/logger.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../wallets/crypto_currency/coins/banano.dart'; import '../../../../wallets/crypto_currency/coins/firo.dart'; +import '../../../../wallets/wallet/impl/bitcoin_wallet.dart'; import '../../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../../../wallets/wallet/intermediate/cryptonote_wallet.dart'; @@ -96,7 +97,8 @@ enum WalletFeature { clearSparkCache("", ""), rbf("", ""), reuseAddress("", ""), - enableMweb("", ""); + enableMweb("", ""), + enableLegacyAddresses("", ""); final String label; final String description; @@ -582,6 +584,9 @@ class _DesktopWalletFeaturesState extends ConsumerState { if (wallet is RbfInterface) (WalletFeature.rbf, Assets.svg.key, () => ()), + if (wallet is BitcoinWallet) + (WalletFeature.enableLegacyAddresses, Assets.svg.key, () => ()), + if (canGen) (WalletFeature.reuseAddress, Assets.svg.key, () => ()), if (showMwebOption) (WalletFeature.enableMweb, Assets.svg.key, () => ()), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index 20f2524a68..74be0581c4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart @@ -69,6 +69,27 @@ class _MoreFeaturesDialogState extends ConsumerState { } } + bool _switchLegacyToggledLock = false; // Mutex. + Future _switchLegacyToggled(bool newValue) async { + if (_switchLegacyToggledLock) { + return; + } + _switchLegacyToggledLock = true; // Lock mutex. + + try { + // Toggle enableLegacyAddresses in wallet info. + await ref + .read(pWalletInfo(widget.walletId)) + .updateOtherData( + newEntries: {WalletInfoKeys.enableLegacyAddresses: newValue}, + isar: ref.read(mainDBProvider).isar, + ); + } finally { + // ensure _switchLegacyToggledLock is set to false no matter what + _switchLegacyToggledLock = false; + } + } + late final DSBController _switchControllerAddressReuse; late final DSBController _switchControllerMwebToggle; @@ -382,6 +403,40 @@ class _MoreFeaturesDialogState extends ConsumerState { ), ); + case WalletFeature.enableLegacyAddresses: + return _MoreFeaturesItemBase( + child: Row( + children: [ + const SizedBox(width: 3), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select((value) => value.otherData), + )[WalletInfoKeys.enableLegacyAddresses] + as bool? ?? + false, + onValueChanged: _switchLegacyToggled, + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enable legacy (P2PKH) address generation", + style: STextStyles.w600_20(context), + ), + ], + ), + ], + ), + ); + case WalletFeature.reuseAddress: return _MoreFeaturesItemBase( onPressed: _switchReuseAddressToggled, @@ -525,26 +580,23 @@ class _MoreFeaturesItemState extends State<_MoreFeaturesItem> { height: _MoreFeaturesItem.iconSizeBG, radiusMultiplier: _MoreFeaturesItem.iconSizeBG, child: Center( - child: - widget.isSvgFile - ? SvgPicture.file( - File(widget.iconAsset), - width: _MoreFeaturesItem.iconSize, - height: _MoreFeaturesItem.iconSize, - color: - Theme.of( - context, - ).extension()!.settingsIconIcon, - ) - : SvgPicture.asset( - widget.iconAsset, - width: _MoreFeaturesItem.iconSize, - height: _MoreFeaturesItem.iconSize, - color: - Theme.of( - context, - ).extension()!.settingsIconIcon, - ), + child: widget.isSvgFile + ? SvgPicture.file( + File(widget.iconAsset), + width: _MoreFeaturesItem.iconSize, + height: _MoreFeaturesItem.iconSize, + color: Theme.of( + context, + ).extension()!.settingsIconIcon, + ) + : SvgPicture.asset( + widget.iconAsset, + width: _MoreFeaturesItem.iconSize, + height: _MoreFeaturesItem.iconSize, + color: Theme.of( + context, + ).extension()!.settingsIconIcon, + ), ), ), const SizedBox(width: 16), @@ -576,8 +628,9 @@ class _MoreFeaturesItemBase extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 32), child: RoundedContainer( color: Colors.transparent, - borderColor: - Theme.of(context).extension()!.textFieldDefaultBG, + borderColor: Theme.of( + context, + ).extension()!.textFieldDefaultBG, onPressed: onPressed, child: child, ), @@ -636,10 +689,9 @@ class _MoreFeaturesClearSparkCacheItemState Assets.svg.x, width: _MoreFeaturesItem.iconSize, height: _MoreFeaturesItem.iconSize, - color: - Theme.of( - context, - ).extension()!.settingsIconIcon, + color: Theme.of( + context, + ).extension()!.settingsIconIcon, ), ), ), diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index 5b2d6569c8..d3d762ac55 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -114,10 +114,9 @@ class WalletInfo implements IsarId { } @ignore - Map get otherData => - otherDataJsonString == null - ? {} - : Map.from(jsonDecode(otherDataJsonString!) as Map); + Map get otherData => otherDataJsonString == null + ? {} + : Map.from(jsonDecode(otherDataJsonString!) as Map); @ignore bool get isViewOnly => @@ -143,6 +142,10 @@ class WalletInfo implements IsarId { bool get isMwebEnabled => otherData[WalletInfoKeys.mwebEnabled] as bool? ?? false; + @ignore + bool get isLegacyAddressesEnabled => + otherData[WalletInfoKeys.enableLegacyAddresses] as bool? ?? false; + //============================================================================ //============= Updaters ================================================ @@ -248,12 +251,11 @@ class WalletInfo implements IsarId { if (customIndexOverride != null) { index = customIndexOverride; } else if (flag) { - final highest = - await isar.walletInfo - .where() - .sortByFavouriteOrderIndexDesc() - .favouriteOrderIndexProperty() - .findFirst(); + final highest = await isar.walletInfo + .where() + .sortByFavouriteOrderIndexDesc() + .favouriteOrderIndexProperty() + .findFirst(); index = (highest ?? 0) + 1; } else { index = -1; @@ -336,8 +338,10 @@ class WalletInfo implements IsarId { /// Can be dangerous. Don't use unless you know the consequences Future setMnemonicVerified({required Isar isar}) async { - final meta = - await isar.walletInfoMeta.where().walletIdEqualTo(walletId).findFirst(); + final meta = await isar.walletInfoMeta + .where() + .walletIdEqualTo(walletId) + .findFirst(); if (meta == null) { await isar.writeTxn(() async { await isar.walletInfoMeta.put( @@ -524,4 +528,5 @@ abstract class WalletInfoKeys { static const String mwebScanHeight = "mwebScanHeightKey"; static const String firoSparkUsedTagsCacheResetVersion = "firoSparkUsedTagsCacheResetVersionKey"; + static const String enableLegacyAddresses = "enableLegacyAddressesKey"; } From 8ae4f2e0cfe1deb5686f23208ce7bba1c8885aee Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 5 Nov 2025 12:12:19 -0600 Subject: [PATCH 15/50] replace mobile_scanner with an older library that does not use MLKit on android --- .../restore/restore_frost_ms_wallet_view.dart | 3 +- .../restore_wallet_view.dart | 2 +- .../new_contact_address_entry_form.dart | 320 +++---- lib/pages/buy_view/buy_form.dart | 866 +++++++++--------- .../exchange_step_views/step_2_view.dart | 588 ++++++------ lib/pages/finalize_view/finalize_view.dart | 188 ++-- .../sub_widgets/transfer_option_widget.dart | 244 ++--- .../paynym/add_new_paynym_follow_view.dart | 286 +++--- .../sub_widgets/slatepack_entry_dialog.dart | 126 ++- lib/pages/send_view/frost_ms/recipient.dart | 194 ++-- lib/pages/send_view/send_view.dart | 5 +- lib/pages/send_view/token_send_view.dart | 429 +++++---- .../add_edit_node_view.dart | 2 +- lib/utilities/barcode_scanner_interface.dart | 48 +- lib/widgets/qr_scanner.dart | 77 +- lib/widgets/textfields/frost_step_field.dart | 97 +- pubspec.lock | 16 +- .../templates/pubspec.template.yaml | 2 +- 18 files changed, 1751 insertions(+), 1742 deletions(-) diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index 33eddc54b8..0c756d0828 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -215,8 +215,9 @@ class _RestoreFrostMsWalletViewState } final qrResult = await ref.read(pBarcodeScanner).scan(context: context); + if (qrResult.rawContent == null) return; - configFieldController.text = qrResult.rawContent; + configFieldController.text = qrResult.rawContent!; setState(() { _configEmpty = configFieldController.text.isEmpty; 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 e87305fc24..1627c3cc93 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 @@ -622,7 +622,7 @@ class _RestoreWalletViewState extends ConsumerState { try { final qrResult = await ref.read(pBarcodeScanner).scan(context: context); - final results = AddressUtils.decodeQRSeedData(qrResult.rawContent); + final results = AddressUtils.decodeQRSeedData(qrResult.rawContent ?? ""); if (results["mnemonic"] != null) { final list = (results["mnemonic"] as List) diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index b6dcd7bded..1be9783995 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -71,6 +71,7 @@ class _NewContactAddressEntryFormState // .state) // .state = false; final qrResult = await ref.read(pBarcodeScanner).scan(context: context); + if (qrResult.rawContent == null) return; // Future.delayed( // const Duration(seconds: 2), @@ -82,7 +83,7 @@ class _NewContactAddressEntryFormState // ); final paymentData = AddressUtils.parsePaymentUri( - qrResult.rawContent, + qrResult.rawContent!, logging: Logging.instance, ); @@ -93,18 +94,19 @@ class _NewContactAddressEntryFormState addressLabelController.text = paymentData.label ?? addressLabelController.text; - ref.read(addressEntryDataProvider(widget.id)).addressLabel = - addressLabelController.text.isEmpty - ? null - : addressLabelController.text; + ref + .read(addressEntryDataProvider(widget.id)) + .addressLabel = addressLabelController.text.isEmpty + ? null + : addressLabelController.text; // now check for non standard encoded basic address } else if (ref.read(addressEntryDataProvider(widget.id)).coin != null) { if (ref .read(addressEntryDataProvider(widget.id)) .coin! - .validateAddress(qrResult.rawContent)) { - addressController.text = qrResult.rawContent; + .validateAddress(qrResult.rawContent!)) { + addressController.text = qrResult.rawContent!; ref.read(addressEntryDataProvider(widget.id)).address = qrResult.rawContent; } @@ -140,13 +142,10 @@ class _NewContactAddressEntryFormState @override void initState() { - addressLabelController = - TextEditingController() - ..text = - ref.read(addressEntryDataProvider(widget.id)).addressLabel ?? ""; - addressController = - TextEditingController() - ..text = ref.read(addressEntryDataProvider(widget.id)).address ?? ""; + addressLabelController = TextEditingController() + ..text = ref.read(addressEntryDataProvider(widget.id)).addressLabel ?? ""; + addressController = TextEditingController() + ..text = ref.read(addressEntryDataProvider(widget.id)).address ?? ""; addressLabelFocusNode = FocusNode(); addressFocusNode = FocusNode(); coins = [...AppConfig.coins]; @@ -177,15 +176,15 @@ class _NewContactAddressEntryFormState coins = [...AppConfig.coins]; coins.removeWhere((e) => e is Firo && e.network.isTestNet); - final showTestNet = - ref.read(prefsChangeNotifierProvider).showTestNetCoins; + final showTestNet = ref + .read(prefsChangeNotifierProvider) + .showTestNetCoins; if (showTestNet) { coins = coins.toList(); } else { - coins = - coins - .where((e) => e.network != CryptoCurrencyNetwork.test) - .toList(); + coins = coins + .where((e) => e.network != CryptoCurrencyNetwork.test) + .toList(); } } @@ -202,10 +201,9 @@ class _NewContactAddressEntryFormState offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -249,14 +247,14 @@ class _NewContactAddressEntryFormState const SizedBox(width: 12), Text( coin.prettyName, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of( + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( context, ).extension()!.textDark, - ), + ), ), ], ), @@ -279,8 +277,9 @@ class _NewContactAddressEntryFormState child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: RawMaterialButton( - splashColor: - Theme.of(context).extension()!.highlight, + splashColor: Theme.of( + context, + ).extension()!.highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -308,48 +307,47 @@ class _NewContactAddressEntryFormState ) == null ? Text( - "Select cryptocurrency", - style: STextStyles.fieldLabel(context), - ) + "Select cryptocurrency", + style: STextStyles.fieldLabel(context), + ) : Row( - children: [ - SvgPicture.file( - File( - ref.watch( - coinIconProvider( - ref.watch( + children: [ + SvgPicture.file( + File( + ref.watch( + coinIconProvider( + ref.watch( + addressEntryDataProvider( + widget.id, + ).select((value) => value.coin), + )!, + ), + ), + ), + height: 20, + width: 20, + ), + const SizedBox(width: 12), + Text( + ref + .watch( addressEntryDataProvider( widget.id, ).select((value) => value.coin), - )!, - ), - ), + )! + .prettyName, + style: STextStyles.itemSubtitle12(context), ), - height: 20, - width: 20, - ), - const SizedBox(width: 12), - Text( - ref - .watch( - addressEntryDataProvider( - widget.id, - ).select((value) => value.coin), - )! - .prettyName, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), + ], + ), if (!isDesktop) SvgPicture.asset( Assets.svg.chevronDown, width: 8, height: 4, - color: - Theme.of( - context, - ).extension()!.textSubtitle2, + color: Theme.of( + context, + ).extension()!.textSubtitle2, ), ], ), @@ -369,33 +367,35 @@ class _NewContactAddressEntryFormState focusNode: addressLabelFocusNode, controller: addressLabelController, style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter address label", - addressLabelFocusNode, - context, - ).copyWith( - labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, - suffixIcon: - addressLabelController.text.isNotEmpty + decoration: + standardInputDecoration( + "Enter address label", + addressLabelFocusNode, + context, + ).copyWith( + labelStyle: isDesktop + ? STextStyles.fieldLabel(context) + : null, + suffixIcon: addressLabelController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - addressLabelController.text = ""; - }); - }, - ), - ], + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + addressLabelController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) + ) : null, - ), + ), onChanged: (newValue) { ref.read(addressEntryDataProvider(widget.id)).addressLabel = newValue; @@ -413,76 +413,87 @@ class _NewContactAddressEntryFormState focusNode: addressFocusNode, controller: addressController, style: STextStyles.field(context), - decoration: standardInputDecoration( - "Paste address", - addressFocusNode, - context, - ).copyWith( - labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - if (ref.watch( - addressEntryDataProvider( - widget.id, - ).select((value) => value.address), - ) != - null) - TextFieldIconButton( - key: const Key("addAddressBookClearAddressButtonKey"), - onTap: () async { - addressController.text = ""; - ref - .read(addressEntryDataProvider(widget.id)) - .address = null; - }, - child: const XIcon(), - ), - if (ref.watch( - addressEntryDataProvider( - widget.id, - ).select((value) => value.address), - ) == - null) - TextFieldIconButton( - key: const Key("addAddressPasteAddressButtonKey"), - onTap: () async { - final ClipboardData? data = await widget.clipboard - .getData(Clipboard.kTextPlain); - - if (data?.text != null && data!.text!.isNotEmpty) { - String content = data.text!.trim(); - if (content.contains("\n")) { - content = content.substring( - 0, - content.indexOf("\n"), - ); - } - addressController.text = content; - ref - .read(addressEntryDataProvider(widget.id)) - .address = content.isEmpty ? null : content; - } - }, - child: const ClipboardIcon(), - ), - if (!Util.isDesktop && - ref.watch( + decoration: + standardInputDecoration( + "Paste address", + addressFocusNode, + context, + ).copyWith( + labelStyle: isDesktop + ? STextStyles.fieldLabel(context) + : null, + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + if (ref.watch( + addressEntryDataProvider( + widget.id, + ).select((value) => value.address), + ) != + null) + TextFieldIconButton( + key: const Key( + "addAddressBookClearAddressButtonKey", + ), + onTap: () async { + addressController.text = ""; + ref + .read(addressEntryDataProvider(widget.id)) + .address = + null; + }, + child: const XIcon(), + ), + if (ref.watch( addressEntryDataProvider( widget.id, ).select((value) => value.address), ) == null) - TextFieldIconButton( - key: const Key("addAddressBookEntryScanQrButtonKey"), - onTap: _onQrTapped, - child: const QrCodeIcon(), - ), - const SizedBox(width: 8), - ], + TextFieldIconButton( + key: const Key("addAddressPasteAddressButtonKey"), + onTap: () async { + final ClipboardData? data = await widget.clipboard + .getData(Clipboard.kTextPlain); + + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, + content.indexOf("\n"), + ); + } + addressController.text = content; + ref + .read(addressEntryDataProvider(widget.id)) + .address = content.isEmpty + ? null + : content; + } + }, + child: const ClipboardIcon(), + ), + if (!Util.isDesktop && + ref.watch( + addressEntryDataProvider( + widget.id, + ).select((value) => value.address), + ) == + null) + TextFieldIconButton( + key: const Key( + "addAddressBookEntryScanQrButtonKey", + ), + onTap: _onQrTapped, + child: const QrCodeIcon(), + ), + const SizedBox(width: 8), + ], + ), + ), ), - ), - ), key: const Key("addAddressBookEntryViewAddressField"), readOnly: false, autocorrect: false, @@ -517,8 +528,9 @@ class _NewContactAddressEntryFormState "Invalid address", textAlign: TextAlign.left, style: STextStyles.label(context).copyWith( - color: - Theme.of(context).extension()!.textError, + color: Theme.of( + context, + ).extension()!.textError, ), ), ], diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index 8f8a0c9b82..47911044de 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -163,14 +163,13 @@ class _BuyFormState extends ConsumerState { unawaited( showDialog( context: context, - builder: - (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Loading currency data", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), + builder: (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading currency data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), ), ); await _loadSimplexCryptos(); @@ -204,62 +203,60 @@ class _BuyFormState extends ConsumerState { _fiatFocusNode.unfocus(); _cryptoFocusNode.unfocus(); - final result = - isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Choose a crypto to buy", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, + final result = isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Choose a crypto to buy", + style: STextStyles.desktopH3(context), ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: - Theme.of( - context, - ).extension()!.background, - child: CryptoSelectionView(coins: coins), - ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of( + context, + ).extension()!.background, + child: CryptoSelectionView(coins: coins), ), - ], - ), + ), + ], ), ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => CryptoSelectionView(coins: coins), - ), - ); + ), + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => CryptoSelectionView(coins: coins), + ), + ); if (mounted && result is Crypto) { onSelected(result); @@ -272,14 +269,13 @@ class _BuyFormState extends ConsumerState { unawaited( showDialog( context: context, - builder: - (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Loading currency data", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), + builder: (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading currency data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), ), ); await _loadSimplexFiats(); @@ -333,62 +329,60 @@ class _BuyFormState extends ConsumerState { _fiatFocusNode.unfocus(); _cryptoFocusNode.unfocus(); - final result = - isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Choose a fiat with which to pay", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, + final result = isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Choose a fiat with which to pay", + style: STextStyles.desktopH3(context), ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: - Theme.of( - context, - ).extension()!.background, - child: FiatSelectionView(fiats: fiats), - ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of( + context, + ).extension()!.background, + child: FiatSelectionView(fiats: fiats), ), - ], - ), + ), + ], ), ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => FiatSelectionView(fiats: fiats), - ), - ); + ), + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => FiatSelectionView(fiats: fiats), + ), + ); if (mounted && result is Fiat) { onSelected(result); @@ -406,28 +400,25 @@ class _BuyFormState extends ConsumerState { unawaited( showDialog( context: context, - builder: - (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Loading quote data", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), + builder: (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading quote data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), ), ); quote = SimplexQuote( crypto: selectedCrypto!, fiat: selectedFiat!, - youPayFiatPrice: - buyWithFiat - ? Decimal.parse(_buyAmountController.text) - : Decimal.parse("100"), // dummy value - youReceiveCryptoAmount: - buyWithFiat - ? Decimal.parse("0.000420282") // dummy value - : Decimal.parse(_buyAmountController.text), // Ternary for this + youPayFiatPrice: buyWithFiat + ? Decimal.parse(_buyAmountController.text) + : Decimal.parse("100"), // dummy value + youReceiveCryptoAmount: buyWithFiat + ? Decimal.parse("0.000420282") // dummy value + : Decimal.parse(_buyAmountController.text), // Ternary for this id: "id", // anything; we get an ID back receivingAddress: _receiveAddressController.text, buyWithFiat: buyWithFiat, @@ -500,10 +491,9 @@ class _BuyFormState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -587,10 +577,9 @@ class _BuyFormState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -628,62 +617,60 @@ class _BuyFormState extends ConsumerState { _fiatFocusNode.unfocus(); _cryptoFocusNode.unfocus(); - final result = - isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Preview quote", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, + final result = isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Preview quote", + style: STextStyles.desktopH3(context), ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: - Theme.of( - context, - ).extension()!.background, - child: BuyQuotePreviewView(quote: quote), - ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of( + context, + ).extension()!.background, + child: BuyQuotePreviewView(quote: quote), ), - ], - ), + ), + ], ), ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => BuyQuotePreviewView(quote: quote), - ), - ); + ), + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BuyQuotePreviewView(quote: quote), + ), + ); if (mounted && result is SimplexQuote) { onSelected(result); @@ -698,11 +685,12 @@ class _BuyFormState extends ConsumerState { } final qrResult = await ref.read(pBarcodeScanner).scan(context: context); + if (qrResult.rawContent == null) return; Logging.instance.d("qrResult content: ${qrResult.rawContent}"); final paymentData = AddressUtils.parsePaymentUri( - qrResult.rawContent, + qrResult.rawContent!, logging: Logging.instance, ); @@ -816,18 +804,14 @@ class _BuyFormState extends ConsumerState { builder: (child) => SizedBox(width: 458, child: child), child: ConditionalParent( condition: !isDesktop, - builder: - (child) => LayoutBuilder( - builder: - (context, constraints) => SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight(child: child), - ), - ), + builder: (child) => LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight(child: child), ), + ), + ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -852,15 +836,14 @@ class _BuyFormState extends ConsumerState { vertical: 6, horizontal: 2, ), - color: - _hovering1 - ? Theme.of(context) - .extension()! - .currencyListItemBG - .withOpacity(_hovering1 ? 0.3 : 0) - : Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: _hovering1 + ? Theme.of(context) + .extension()! + .currencyListItemBG + .withOpacity(_hovering1 ? 0.3 : 0) + : Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Padding( padding: const EdgeInsets.all(12), child: Row( @@ -878,10 +861,9 @@ class _BuyFormState extends ConsumerState { ), SvgPicture.asset( Assets.svg.chevronDown, - color: - Theme.of(context) - .extension()! - .buttonTextSecondaryDisabled, + color: Theme.of(context) + .extension()! + .buttonTextSecondaryDisabled, width: 10, height: 5, ), @@ -899,8 +881,9 @@ class _BuyFormState extends ConsumerState { Text( "I want to pay with", style: STextStyles.itemSubtitle(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, ), ), ], @@ -919,15 +902,14 @@ class _BuyFormState extends ConsumerState { vertical: 3, horizontal: 2, ), - color: - _hovering2 - ? Theme.of(context) - .extension()! - .currencyListItemBG - .withOpacity(_hovering2 ? 0.3 : 0) - : Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: _hovering2 + ? Theme.of(context) + .extension()! + .currencyListItemBG + .withOpacity(_hovering2 ? 0.3 : 0) + : Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Padding( padding: const EdgeInsets.only( left: 12.0, @@ -943,10 +925,9 @@ class _BuyFormState extends ConsumerState { horizontal: 6, ), decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.currencyListItemBG, + color: Theme.of( + context, + ).extension()!.currencyListItemBG, borderRadius: BorderRadius.circular(4), ), child: Text( @@ -955,10 +936,9 @@ class _BuyFormState extends ConsumerState { ), textAlign: TextAlign.center, style: STextStyles.smallMed12(context).copyWith( - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), @@ -976,10 +956,9 @@ class _BuyFormState extends ConsumerState { ), SvgPicture.asset( Assets.svg.chevronDown, - color: - Theme.of(context) - .extension()! - .buttonTextSecondaryDisabled, + color: Theme.of(context) + .extension()! + .buttonTextSecondaryDisabled, width: 10, height: 5, ), @@ -997,8 +976,9 @@ class _BuyFormState extends ConsumerState { Text( buyWithFiat ? "Enter amount" : "Enter crypto amount", style: STextStyles.itemSubtitle(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, ), ), CustomTextButton( @@ -1026,13 +1006,12 @@ class _BuyFormState extends ConsumerState { // ? _BuyFormState.minFiat.toStringAsFixed(2) ?? '50.00' // : _BuyFormState.minCrypto.toStringAsFixed(8), focusNode: _buyAmountFocusNode, - keyboardType: - Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.left, // inputFormatters: [NumericalRangeFormatter()], onChanged: (_) { @@ -1050,10 +1029,9 @@ class _BuyFormState extends ConsumerState { ), hintText: "0", hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textFieldDefaultText, + color: Theme.of( + context, + ).extension()!.textFieldDefaultText, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, @@ -1064,34 +1042,33 @@ class _BuyFormState extends ConsumerState { const SizedBox(width: 2), buyWithFiat ? Container( - padding: const EdgeInsets.symmetric( - vertical: 3, - horizontal: 6, - ), - decoration: BoxDecoration( - color: - Theme.of(context) - .extension()! - .currencyListItemBG, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - format.simpleCurrencySymbol( - selectedFiat?.ticker.toUpperCase() ?? "ERR", + padding: const EdgeInsets.symmetric( + vertical: 3, + horizontal: 6, ), - textAlign: TextAlign.center, - style: STextStyles.smallMed12(context).copyWith( - color: - Theme.of(context) - .extension()! - .accentColorDark, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .currencyListItemBG, + borderRadius: BorderRadius.circular(4), ), - ), - ) + child: Text( + format.simpleCurrencySymbol( + selectedFiat?.ticker.toUpperCase() ?? "ERR", + ), + textAlign: TextAlign.center, + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ) : CoinIconForTicker( - ticker: selectedCrypto?.ticker ?? "BTC", - size: 20, - ), + ticker: selectedCrypto?.ticker ?? "BTC", + size: 20, + ), SizedBox( width: buyWithFiat ? 8 : 10, ), // maybe make isDesktop-aware? @@ -1100,10 +1077,9 @@ class _BuyFormState extends ConsumerState { ? selectedFiat?.ticker ?? "ERR" : selectedCrypto?.ticker ?? "ERR", style: STextStyles.smallMed14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), ], @@ -1118,50 +1094,49 @@ class _BuyFormState extends ConsumerState { children: [ _buyAmountController.text.isNotEmpty ? TextFieldIconButton( - key: const Key( - "buyViewClearAmountFieldButtonKey", - ), - onTap: () { - // if (_BuyFormState.buyWithFiat) { - // _buyAmountController.text = _BuyFormState - // .minFiat - // .toStringAsFixed(2); - // } else { - // if (selectedCrypto?.ticker == - // _BuyFormState.boundedCryptoTicker) { - // _buyAmountController.text = _BuyFormState - // .minCrypto - // .toStringAsFixed(8); - // } - // } - _buyAmountController.text = ""; - validateAmount(); - }, - child: const XIcon(), - ) + key: const Key( + "buyViewClearAmountFieldButtonKey", + ), + onTap: () { + // if (_BuyFormState.buyWithFiat) { + // _buyAmountController.text = _BuyFormState + // .minFiat + // .toStringAsFixed(2); + // } else { + // if (selectedCrypto?.ticker == + // _BuyFormState.boundedCryptoTicker) { + // _buyAmountController.text = _BuyFormState + // .minCrypto + // .toStringAsFixed(8); + // } + // } + _buyAmountController.text = ""; + validateAmount(); + }, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "buyViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); + key: const Key( + "buyViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); - final amountString = Decimal.tryParse( - data?.text ?? "", - ); - if (amountString != null) { - _buyAmountController.text = - amountString.toString(); + final amountString = Decimal.tryParse( + data?.text ?? "", + ); + if (amountString != null) { + _buyAmountController.text = amountString + .toString(); - validateAmount(); - } - }, - child: - _buyAmountController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + validateAmount(); + } + }, + child: _buyAmountController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), ], ), ), @@ -1182,8 +1157,9 @@ class _BuyFormState extends ConsumerState { Text( "Enter receiving address", style: STextStyles.itemSubtitle(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, ), ), if (AppConfig.isStackCoin(selectedCrypto?.ticker)) @@ -1209,8 +1185,8 @@ class _BuyFormState extends ConsumerState { // model.recipientAddress = // await manager.currentReceivingAddress; - final address = - await wallet.getCurrentReceivingAddress(); + final address = await wallet + .getCurrentReceivingAddress(); if (address!.type == AddressType.p2tr && wallet is Bip39HDWallet) { @@ -1289,85 +1265,87 @@ class _BuyFormState extends ConsumerState { }, focusNode: _receiveAddressFocusNode, style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${selectedCrypto?.ticker} address", - _receiveAddressFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 13, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: - _receiveAddressController.text.isEmpty + decoration: + standardInputDecoration( + "Enter ${selectedCrypto?.ticker} address", + _receiveAddressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 13, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _receiveAddressController.text.isEmpty ? const EdgeInsets.only(right: 8) : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _addressToggleFlag - ? TextFieldIconButton( - key: const Key( - "buyViewClearAddressFieldButtonKey", - ), - onTap: () { - _receiveAddressController.text = ""; - _address = ""; - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "buyViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - String content = data.text!.trim(); - if (content.contains("\n")) { - content = content.substring( - 0, - content.indexOf("\n"), - ); - } - - _receiveAddressController.text = content; - _address = content; - - setState(() { - _addressToggleFlag = - _receiveAddressController - .text - .isNotEmpty; - }); - } - }, - child: - _receiveAddressController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_receiveAddressController.text.isEmpty && - AppConfig.isStackCoin(selectedCrypto?.ticker) && - isDesktop) - TextFieldIconButton( - key: const Key("buyViewAddressBookButtonKey"), - onTap: () async { - final entry = await showDialog< - ContactAddressEntry? - >( - context: context, - builder: - (context) => DesktopDialog( + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "buyViewClearAddressFieldButtonKey", + ), + onTap: () { + _receiveAddressController.text = ""; + _address = ""; + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "buyViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, + content.indexOf("\n"), + ); + } + + _receiveAddressController.text = + content; + _address = content; + + setState(() { + _addressToggleFlag = + _receiveAddressController + .text + .isNotEmpty; + }); + } + }, + child: + _receiveAddressController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_receiveAddressController.text.isEmpty && + AppConfig.isStackCoin( + selectedCrypto?.ticker, + ) && + isDesktop) + TextFieldIconButton( + key: const Key("buyViewAddressBookButtonKey"), + onTap: () async { + final entry = await showDialog( + context: context, + builder: (context) => DesktopDialog( maxWidth: 696, maxHeight: 600, child: Column( @@ -1410,45 +1388,47 @@ class _BuyFormState extends ConsumerState { ], ), ), - ); + ); - if (entry != null) { - _receiveAddressController.text = - entry.address; - _address = entry.address; + if (entry != null) { + _receiveAddressController.text = + entry.address; + _address = entry.address; - setState(() { - _addressToggleFlag = true; - }); - } - }, - child: const AddressBookIcon(), - ), - if (_receiveAddressController.text.isEmpty && - AppConfig.isStackCoin(selectedCrypto?.ticker) && - !isDesktop) - TextFieldIconButton( - key: const Key("buyViewAddressBookButtonKey"), - onTap: () { - Navigator.of( - context, - rootNavigator: isDesktop, - ).pushNamed(AddressBookView.routeName); - }, - child: const AddressBookIcon(), - ), - if (_receiveAddressController.text.isEmpty && - !isDesktop) - TextFieldIconButton( - key: const Key("buyViewScanQrButtonKey"), - onTap: _onQrTapped, - child: const QrCodeIcon(), - ), - ], + setState(() { + _addressToggleFlag = true; + }); + } + }, + child: const AddressBookIcon(), + ), + if (_receiveAddressController.text.isEmpty && + AppConfig.isStackCoin( + selectedCrypto?.ticker, + ) && + !isDesktop) + TextFieldIconButton( + key: const Key("buyViewAddressBookButtonKey"), + onTap: () { + Navigator.of( + context, + rootNavigator: isDesktop, + ).pushNamed(AddressBookView.routeName); + }, + child: const AddressBookIcon(), + ), + if (_receiveAddressController.text.isEmpty && + !isDesktop) + TextFieldIconButton( + key: const Key("buyViewScanQrButtonKey"), + onTap: _onQrTapped, + child: const QrCodeIcon(), + ), + ], + ), + ), ), ), - ), - ), ), ), SizedBox(height: isDesktop ? 10 : 4), diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index a731bf9dfc..3bead4106e 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -70,9 +70,10 @@ class _Step2ViewState extends ConsumerState { void _onRefundQrTapped() async { try { final qrResult = await ref.read(pBarcodeScanner).scan(context: context); + if (qrResult.rawContent == null) return; final paymentData = AddressUtils.parsePaymentUri( - qrResult.rawContent, + qrResult.rawContent!, logging: Logging.instance, ); @@ -87,7 +88,7 @@ class _Step2ViewState extends ConsumerState { _refundController.text.isNotEmpty; }); } else { - _refundController.text = qrResult.rawContent; + _refundController.text = qrResult.rawContent!; model.refundAddress = _refundController.text; setState(() { @@ -123,9 +124,10 @@ class _Step2ViewState extends ConsumerState { void _onToQrTapped() async { try { final qrResult = await ref.read(pBarcodeScanner).scan(context: context); + if (qrResult.rawContent == null) return; final paymentData = AddressUtils.parsePaymentUri( - qrResult.rawContent, + qrResult.rawContent!, logging: Logging.instance, ); @@ -141,7 +143,7 @@ class _Step2ViewState extends ConsumerState { !ref.read(efExchangeProvider).supportsRefundAddress); }); } else { - _toController.text = qrResult.rawContent; + _toController.text = qrResult.rawContent!; model.recipientAddress = _toController.text; setState(() { @@ -373,156 +375,168 @@ class _Step2ViewState extends ConsumerState { !supportsRefund); }); }, - decoration: standardInputDecoration( - "Enter the ${model.receiveTicker.toUpperCase()} payout address", - _toFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: - _toController.text.isEmpty + decoration: + standardInputDecoration( + "Enter the ${model.receiveTicker.toUpperCase()} payout address", + _toFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _toController.text.isEmpty ? const EdgeInsets.only(right: 8) : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _toController.text.isNotEmpty - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - _toController.text = ""; - model.recipientAddress = - _toController.text; - - setState(() { - enableNext = - _toController - .text - .isNotEmpty && - (_refundController - .text - .isNotEmpty || - !supportsRefund); - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = - await clipboard.getData( - Clipboard.kTextPlain, - ); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = - data.text!.trim(); - - _toController.text = - content; - model.recipientAddress = - _toController.text; - - setState(() { - enableNext = - _toController - .text - .isNotEmpty && - (_refundController + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _toController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + _toController.text = ""; + model.recipientAddress = + _toController.text; + + setState(() { + enableNext = + _toController .text - .isNotEmpty || - !supportsRefund); - }); - } - }, - child: - _toController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_toController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey", - ), - onTap: () { - ref - .read( - exchangeFlowIsActiveStateProvider - .state, + .isNotEmpty && + (_refundController + .text + .isNotEmpty || + !supportsRefund); + }); + }, + child: const XIcon(), ) - .state = true; - Navigator.of( - context, - ).pushNamed(AddressBookView.routeName).then(( - _, - ) { - ref - .read( - exchangeFlowIsActiveStateProvider - .state, - ) - .state = false; + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? + data = await clipboard + .getData( + Clipboard + .kTextPlain, + ); + if (data?.text != + null && + data! + .text! + .isNotEmpty) { + final content = data + .text! + .trim(); + + _toController.text = + content; + model.recipientAddress = + _toController + .text; - final address = + setState(() { + enableNext = + _toController + .text + .isNotEmpty && + (_refundController + .text + .isNotEmpty || + !supportsRefund); + }); + } + }, + child: + _toController + .text + .isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_toController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey", + ), + onTap: () { + ref + .read( + exchangeFlowIsActiveStateProvider + .state, + ) + .state = + true; + Navigator.of( + context, + ).pushNamed(AddressBookView.routeName).then(( + _, + ) { ref + .read( + exchangeFlowIsActiveStateProvider + .state, + ) + .state = + false; + + final address = ref .read( exchangeFromAddressBookAddressStateProvider .state, ) .state; - if (address.isNotEmpty) { - _toController.text = - address; - model.recipientAddress = - _toController.text; - ref - .read( - exchangeFromAddressBookAddressStateProvider - .state, - ) - .state = ""; - } - setState(() { - enableNext = - _toController - .text - .isNotEmpty && - (_refundController + if (address.isNotEmpty) { + _toController.text = + address; + model.recipientAddress = + _toController.text; + ref + .read( + exchangeFromAddressBookAddressStateProvider + .state, + ) + .state = + ""; + } + setState(() { + enableNext = + _toController .text - .isNotEmpty || - !supportsRefund); - }); - }); - }, - child: const AddressBookIcon(), - ), - if (_toController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey", - ), - onTap: _onToQrTapped, - child: const QrCodeIcon(), - ), - ], + .isNotEmpty && + (_refundController + .text + .isNotEmpty || + !supportsRefund); + }); + }); + }, + child: + const AddressBookIcon(), + ), + if (_toController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey", + ), + onTap: _onToQrTapped, + child: const QrCodeIcon(), + ), + ], + ), + ), ), ), - ), - ), ), ), const SizedBox(height: 6), @@ -628,154 +642,172 @@ class _Step2ViewState extends ConsumerState { _refundController.text.isNotEmpty; }); }, - decoration: standardInputDecoration( - "Enter ${model.sendTicker.toUpperCase()} refund address", - _refundFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: - _refundController.text.isEmpty + decoration: + standardInputDecoration( + "Enter ${model.sendTicker.toUpperCase()} refund address", + _refundFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: + _refundController.text.isEmpty ? const EdgeInsets.only(right: 16) : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _refundController.text.isNotEmpty - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - _refundController.text = ""; - model.refundAddress = - _refundController.text; - - setState(() { - enableNext = - _toController - .text - .isNotEmpty && - _refundController - .text - .isNotEmpty; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = - await clipboard.getData( - Clipboard.kTextPlain, - ); - if (data?.text != null && - data! - .text! - .isNotEmpty) { - final content = - data.text!.trim(); - - _refundController.text = - content; - model.refundAddress = + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _refundController + .text + .isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { _refundController - .text; + .text = + ""; + model.refundAddress = + _refundController + .text; + + setState(() { + enableNext = + _toController + .text + .isNotEmpty && + _refundController + .text + .isNotEmpty; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? + data = await clipboard + .getData( + Clipboard + .kTextPlain, + ); + if (data?.text != + null && + data! + .text! + .isNotEmpty) { + final content = data + .text! + .trim(); - setState(() { - enableNext = - _toController - .text - .isNotEmpty && + _refundController + .text = + content; + model.refundAddress = + _refundController + .text; + + setState(() { + enableNext = + _toController + .text + .isNotEmpty && + _refundController + .text + .isNotEmpty; + }); + } + }, + child: _refundController .text - .isNotEmpty; - }); - } - }, - child: - _refundController - .text - .isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_refundController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey", - ), - onTap: () { - ref - .read( - exchangeFlowIsActiveStateProvider - .state, - ) - .state = true; - Navigator.of(context) - .pushNamed( - AddressBookView - .routeName, - ) - .then((_) { - ref - .read( - exchangeFlowIsActiveStateProvider - .state, - ) - .state = false; - final address = + .isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_refundController + .text + .isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey", + ), + onTap: () { + ref + .read( + exchangeFlowIsActiveStateProvider + .state, + ) + .state = + true; + Navigator.of(context) + .pushNamed( + AddressBookView + .routeName, + ) + .then((_) { ref + .read( + exchangeFlowIsActiveStateProvider + .state, + ) + .state = + false; + final address = ref .read( exchangeFromAddressBookAddressStateProvider .state, ) .state; - if (address - .isNotEmpty) { - _refundController - .text = address; - model.refundAddress = + if (address + .isNotEmpty) { _refundController - .text; - } - setState(() { - enableNext = - _toController - .text - .isNotEmpty && - _refundController - .text - .isNotEmpty; - }); - }); - }, - child: const AddressBookIcon(), - ), - if (_refundController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey", - ), - onTap: _onRefundQrTapped, - child: const QrCodeIcon(), - ), - ], + .text = + address; + model.refundAddress = + _refundController + .text; + } + setState(() { + enableNext = + _toController + .text + .isNotEmpty && + _refundController + .text + .isNotEmpty; + }); + }); + }, + child: + const AddressBookIcon(), + ), + if (_refundController + .text + .isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey", + ), + onTap: _onRefundQrTapped, + child: const QrCodeIcon(), + ), + ], + ), + ), ), ), - ), - ), ), ), if (supportsRefund) const SizedBox(height: 6), @@ -802,14 +834,12 @@ class _Step2ViewState extends ConsumerState { ), child: Text( "Back", - style: STextStyles.button( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.button(context) + .copyWith( + color: Theme.of(context) .extension()! .buttonTextSecondary, - ), + ), ), ), ), diff --git a/lib/pages/finalize_view/finalize_view.dart b/lib/pages/finalize_view/finalize_view.dart index e0a3415907..65fb50a1be 100644 --- a/lib/pages/finalize_view/finalize_view.dart +++ b/lib/pages/finalize_view/finalize_view.dart @@ -80,8 +80,8 @@ class _FinalizeViewState extends ConsumerState { if (mounted) { final qrResult = await ref.read(pBarcodeScanner).scan(context: context); - if (qrResult.rawContent.isNotEmpty && qrResult.rawContent != "null") { - _slateController.text = qrResult.rawContent; + if (qrResult.rawContent != null && qrResult.rawContent!.isNotEmpty) { + _slateController.text = qrResult.rawContent!; setState(() { _slateToggleFlag = _slateController.text.isNotEmpty; }); @@ -156,14 +156,12 @@ class _FinalizeViewState extends ConsumerState { if (ex != null) { await showDialog( context: context, - builder: - (context) => StackOkDialog( - desktopPopRootNavigator: Util.isDesktop, - title: "Slatepack finalize error", - message: - ex?.toString() ?? "Unexpected result without exception", - maxWidth: Util.isDesktop ? 400 : null, - ), + builder: (context) => StackOkDialog( + desktopPopRootNavigator: Util.isDesktop, + title: "Slatepack finalize error", + message: ex?.toString() ?? "Unexpected result without exception", + maxWidth: Util.isDesktop ? 400 : null, + ), ); } else { setState(() { @@ -201,45 +199,45 @@ class _FinalizeViewState extends ConsumerState { return ConditionalParent( condition: !Util.isDesktop, - builder: - (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Finalize slatepack", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: Constants.size.standardPadding, - ), - child: child, - ), + builder: (child) => Background( + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Finalize slatepack", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: Constants.size.standardPadding, ), + child: child, ), - ); - }, - ), - ), + ), + ), + ); + }, ), ), + ), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -268,61 +266,61 @@ class _FinalizeViewState extends ConsumerState { }, focusNode: _slateFocusNode, style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter Final Slatepack Message", - _slateFocusNode, - context, - desktopMed: true, - ).copyWith( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, // Adjust vertical padding for better alignment - ), - suffixIcon: Padding( - padding: - _slateController.text.isEmpty + decoration: + standardInputDecoration( + "Enter Final Slatepack Message", + _slateFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: + 12, // Adjust vertical padding for better alignment + ), + suffixIcon: Padding( + padding: _slateController.text.isEmpty ? const EdgeInsets.only(right: 8) : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _slateToggleFlag - ? TextFieldIconButton( - key: const Key( - "slateFinalizeClearFieldButtonKey", + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _slateToggleFlag + ? TextFieldIconButton( + key: const Key( + "slateFinalizeClearFieldButtonKey", + ), + onTap: () { + _slateController.text = ""; + setState(() { + _slateToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "slateFinalizePasteFieldButtonKey", + ), + onTap: _pasteSlatepack, + child: _slateController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_slateController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("sendViewScanQrButtonKey"), + onTap: _scanQr, + child: const QrCodeIcon(), ), - onTap: () { - _slateController.text = ""; - setState(() { - _slateToggleFlag = false; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "slateFinalizePasteFieldButtonKey", - ), - onTap: _pasteSlatepack, - child: - _slateController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_slateController.text.isEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("sendViewScanQrButtonKey"), - onTap: _scanQr, - child: const QrCodeIcon(), - ), - ], + ], + ), + ), ), ), - ), - ), ), ), Util.isDesktop ? const SizedBox(height: 24) : const Spacer(), diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart index 89be6129cb..5dbc649bcd 100644 --- a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -169,16 +169,15 @@ class _TransferOptionWidgetState extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: - (context) => SDialog( - child: SizedBox( - width: 580, - child: ConfirmNameTransactionView( - txData: txData, - walletId: widget.walletId, - ), - ), + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, ), + ), + ), ); } else { await Navigator.of(context).pushNamed( @@ -203,13 +202,12 @@ class _TransferOptionWidgetState extends ConsumerState { await showDialog( context: context, - builder: - (_) => StackOkDialog( - title: "Error", - message: err, - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: (_) => StackOkDialog( + title: "Error", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -238,12 +236,14 @@ class _TransferOptionWidgetState extends ConsumerState { } final qrResult = await ref.read(pBarcodeScanner).scan(context: context); + if (qrResult.rawContent == null) return; + final coin = ref.read(pWalletCoin(walletId)); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); final paymentData = AddressUtils.parsePaymentUri( - qrResult.rawContent, + qrResult.rawContent!, logging: Logging.instance, ); @@ -257,7 +257,7 @@ class _TransferOptionWidgetState extends ConsumerState { // now check for non standard encoded basic address } else { - _address = qrResult.rawContent.split("\n").first.trim(); + _address = qrResult.rawContent!.split("\n").first.trim(); _addressController.text = _address ?? ""; _setValidAddressProviders(_address); @@ -313,8 +313,9 @@ class _TransferOptionWidgetState extends ConsumerState { Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, + crossAxisAlignment: Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, children: [ ClipRRect( borderRadius: BorderRadius.circular( @@ -338,121 +339,120 @@ class _TransferOptionWidgetState extends ConsumerState { }, focusNode: _addressFocusNode, style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${ref.watch(pWalletCoin(walletId)).ticker} address", - _addressFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: - _addressController.text.isEmpty + decoration: + standardInputDecoration( + "Enter ${ref.watch(pWalletCoin(walletId)).ticker} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _addressController.text.isEmpty ? const EdgeInsets.only(right: 8) : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _addressController.text.isNotEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Address Field Input.", - key: const Key( - "nameTransferClearAddressFieldButtonKey", + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _addressController.text.isNotEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Address Field Input.", + key: const Key( + "nameTransferClearAddressFieldButtonKey", + ), + onTap: () { + _addressController.text = ""; + _address = ""; + _setValidAddressProviders(_address); + setState(() {}); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Address Field Input.", + key: const Key( + "nameTransferPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, + content.indexOf("\n"), + ); + } + + _addressController.text = content.trim(); + _address = content.trim(); + + _setValidAddressProviders(_address); + } + }, + child: _addressController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_addressController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Address Book Button. Opens Address Book For Address Field.", + key: const Key( + "nameTransferAddressBookButtonKey", + ), + onTap: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: ref.read(pWalletCoin(walletId)), + ); + }, + child: const AddressBookIcon(), ), - onTap: () { - _addressController.text = ""; - _address = ""; - _setValidAddressProviders(_address); - setState(() {}); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Address Field Input.", - key: const Key( - "nameTransferPasteAddressFieldButtonKey", + if (_addressController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("nameTransferScanQrButtonKey"), + onTap: _scanQr, + child: const QrCodeIcon(), ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - String content = data.text!.trim(); - if (content.contains("\n")) { - content = content.substring( - 0, - content.indexOf("\n"), - ); - } - - _addressController.text = content.trim(); - _address = content.trim(); - - _setValidAddressProviders(_address); - } - }, - child: - _addressController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_addressController.text.isEmpty) - TextFieldIconButton( - semanticsLabel: - "Address Book Button. Opens Address Book For Address Field.", - key: const Key("nameTransferAddressBookButtonKey"), - onTap: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: ref.read(pWalletCoin(walletId)), - ); - }, - child: const AddressBookIcon(), - ), - if (_addressController.text.isEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("nameTransferScanQrButtonKey"), - onTap: _scanQr, - child: const QrCodeIcon(), - ), - ], + ], + ), + ), ), ), - ), - ), ), ), SizedBox(height: Util.isDesktop ? 42 : 16), if (!Util.isDesktop) const Spacer(), ConditionalParent( condition: Util.isDesktop, - builder: - (child) => Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: ButtonHeight.l, - onPressed: - Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop, - ), - ), - const SizedBox(width: 16), - Expanded(child: child), - ], + builder: (child) => Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop, + ), ), + const SizedBox(width: 16), + Expanded(child: child), + ], + ), child: PrimaryButton( label: "Transfer", enabled: _enableButton, diff --git a/lib/pages/paynym/add_new_paynym_follow_view.dart b/lib/pages/paynym/add_new_paynym_follow_view.dart index 85e4c3ac73..37c5b1a8cf 100644 --- a/lib/pages/paynym/add_new_paynym_follow_view.dart +++ b/lib/pages/paynym/add_new_paynym_follow_view.dart @@ -122,8 +122,9 @@ class _AddNewPaynymFollowViewState } final qrResult = await ref.read(pBarcodeScanner).scan(context: context); + if (qrResult.rawContent == null) return; - final pCodeString = qrResult.rawContent; + final pCodeString = qrResult.rawContent!; _searchString = pCodeString; @@ -173,93 +174,82 @@ class _AddNewPaynymFollowViewState return ConditionalParent( condition: !isDesktop, - builder: - (child) => MasterScaffold( - isDesktop: isDesktop, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Text( - "New follow", - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: - (context, constraints) => SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ), + builder: (child) => MasterScaffold( + isDesktop: isDesktop, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "New follow", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), ), ), ), + ), + ), child: ConditionalParent( condition: isDesktop, - builder: - (child) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( + builder: (child) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "New follow", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, + padding: const EdgeInsets.only(left: 32), + child: Text( + "New follow", + style: STextStyles.desktopH3(context), ), - child: child, ), + const DesktopDialogCloseButton(), ], ), - ), + Padding( + padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), + child: child, + ), + ], + ), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), Text( "Featured PayNyms", - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.sectionLabelMedium12(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.sectionLabelMedium12(context), ), const SizedBox(height: 12), FeaturedPaynymsWidget(walletId: widget.walletId), const SizedBox(height: 24), Text( "Add new", - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.sectionLabelMedium12(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.sectionLabelMedium12(context), ), const SizedBox(height: 12), if (isDesktop) @@ -270,10 +260,9 @@ class _AddNewPaynymFollowViewState children: [ RoundedContainer( padding: const EdgeInsets.all(0), - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, height: 56, child: Center( child: TextField( @@ -286,15 +275,15 @@ class _AddNewPaynymFollowViewState _searchString = value; }); }, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of(context) + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) .extension()! .textFieldActiveText, - // height: 1.8, - ), + // height: 1.8, + ), decoration: InputDecoration( hintText: "Paste payment code", hoverColor: Colors.transparent, @@ -315,38 +304,32 @@ class _AddNewPaynymFollowViewState children: [ _searchController.text.isNotEmpty ? TextFieldIconButton( - onTap: _clear, - child: RoundedContainer( - padding: const EdgeInsets.all( - 8, + onTap: _clear, + child: RoundedContainer( + padding: const EdgeInsets.all( + 8, + ), + color: Theme.of(context) + .extension()! + .buttonBackSecondary, + child: const XIcon(), ), - color: - Theme.of(context) - .extension< - StackColors - >()! - .buttonBackSecondary, - child: const XIcon(), - ), - ) + ) : TextFieldIconButton( - key: const Key( - "paynymPasteAddressFieldButtonKey", - ), - onTap: _paste, - child: RoundedContainer( - padding: const EdgeInsets.all( - 8, + key: const Key( + "paynymPasteAddressFieldButtonKey", + ), + onTap: _paste, + child: RoundedContainer( + padding: const EdgeInsets.all( + 8, + ), + color: Theme.of(context) + .extension()! + .buttonBackSecondary, + child: const ClipboardIcon(), ), - color: - Theme.of(context) - .extension< - StackColors - >()! - .buttonBackSecondary, - child: const ClipboardIcon(), ), - ), TextFieldIconButton( key: const Key( "paynymScanQrButtonKey", @@ -354,10 +337,9 @@ class _AddNewPaynymFollowViewState onTap: _scanQr, child: RoundedContainer( padding: const EdgeInsets.all(8), - color: - Theme.of(context) - .extension()! - .buttonBackSecondary, + color: Theme.of(context) + .extension()! + .buttonBackSecondary, child: const QrCodeIcon(), ), ), @@ -392,39 +374,40 @@ class _AddNewPaynymFollowViewState }); }, style: STextStyles.field(context), - decoration: standardInputDecoration( - "Paste payment code", - searchFieldFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 8), - child: UnconstrainedBox( - child: Row( - children: [ - _searchController.text.isNotEmpty - ? TextFieldIconButton( - onTap: _clear, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "paynymPasteAddressFieldButtonKey", - ), - onTap: _paste, - child: const ClipboardIcon(), + decoration: + standardInputDecoration( + "Paste payment code", + searchFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + suffixIcon: Padding( + padding: const EdgeInsets.only(right: 8), + child: UnconstrainedBox( + child: Row( + children: [ + _searchController.text.isNotEmpty + ? TextFieldIconButton( + onTap: _clear, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "paynymPasteAddressFieldButtonKey", + ), + onTap: _paste, + child: const ClipboardIcon(), + ), + TextFieldIconButton( + key: const Key("paynymScanQrButtonKey"), + onTap: _scanQr, + child: const QrCodeIcon(), ), - TextFieldIconButton( - key: const Key("paynymScanQrButtonKey"), - onTap: _scanQr, - child: const QrCodeIcon(), + ], ), - ], + ), ), ), - ), - ), ), ), if (!isDesktop) const SizedBox(height: 12), @@ -433,21 +416,19 @@ class _AddNewPaynymFollowViewState if (_didSearch) const SizedBox(height: 20), if (_didSearch && _searchResult == null) RoundedWhiteContainer( - borderColor: - isDesktop - ? Theme.of( - context, - ).extension()!.backgroundAppBar - : null, + borderColor: isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Nothing found. Please check the payment code.", - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.label(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context), ), ], ), @@ -455,12 +436,11 @@ class _AddNewPaynymFollowViewState if (_didSearch && _searchResult != null) RoundedWhiteContainer( padding: const EdgeInsets.all(0), - borderColor: - isDesktop - ? Theme.of( - context, - ).extension()!.backgroundAppBar - : null, + borderColor: isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, child: PaynymCard( key: UniqueKey(), label: _searchResult!.nymName, diff --git a/lib/pages/receive_view/sub_widgets/slatepack_entry_dialog.dart b/lib/pages/receive_view/sub_widgets/slatepack_entry_dialog.dart index ec9aee0c36..941e7755f0 100644 --- a/lib/pages/receive_view/sub_widgets/slatepack_entry_dialog.dart +++ b/lib/pages/receive_view/sub_widgets/slatepack_entry_dialog.dart @@ -58,8 +58,8 @@ class _SlatepackEntryDialogState extends ConsumerState { if (mounted) { final qrResult = await ref.read(pBarcodeScanner).scan(context: context); - if (qrResult.rawContent.isNotEmpty && qrResult.rawContent != "null") { - _receiveSlateController.text = qrResult.rawContent; + if (qrResult.rawContent != null && qrResult.rawContent!.isNotEmpty) { + _receiveSlateController.text = qrResult.rawContent!; setState(() { _slateToggleFlag = _receiveSlateController.text.isNotEmpty; }); @@ -105,10 +105,9 @@ class _SlatepackEntryDialogState extends ConsumerState { Text( "Receive Slatepack", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textFieldActiveSearchIconRight, + color: Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), @@ -138,78 +137,75 @@ class _SlatepackEntryDialogState extends ConsumerState { }, focusNode: _slateFocusNode, style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textFieldActiveText, + color: Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), - decoration: standardInputDecoration( - "Enter Slatepack Message", - _slateFocusNode, - context, - desktopMed: true, - ).copyWith( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, // Adjust vertical padding for better alignment - ), - suffixIcon: Padding( - padding: - _receiveSlateController.text.isEmpty + decoration: + standardInputDecoration( + "Enter Slatepack Message", + _slateFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: + 12, // Adjust vertical padding for better alignment + ), + suffixIcon: Padding( + padding: _receiveSlateController.text.isEmpty ? const EdgeInsets.only(right: 8) : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _slateToggleFlag - ? TextFieldIconButton( - key: const Key( - "receiveViewClearSlatepackFieldButtonKey", - ), - onTap: () { - _receiveSlateController.text = ""; - setState(() { - _slateToggleFlag = false; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "receiveViewPasteSlatepackFieldButtonKey", + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _slateToggleFlag + ? TextFieldIconButton( + key: const Key( + "receiveViewClearSlatepackFieldButtonKey", + ), + onTap: () { + _receiveSlateController.text = ""; + setState(() { + _slateToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "receiveViewPasteSlatepackFieldButtonKey", + ), + onTap: _pasteSlatepack, + child: _receiveSlateController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_receiveSlateController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("sendViewScanQrButtonKey"), + onTap: _scanQr, + child: const QrCodeIcon(), ), - onTap: _pasteSlatepack, - child: - _receiveSlateController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_receiveSlateController.text.isEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("sendViewScanQrButtonKey"), - onTap: _scanQr, - child: const QrCodeIcon(), - ), - ], + ], + ), + ), ), ), - ), - ), ), ), const SizedBox(height: 16), PrimaryButton( label: "Import", enabled: _slateToggleFlag, - onPressed: - !_slateToggleFlag - ? null - : () => - Navigator.of(context).pop(_receiveSlateController.text), + onPressed: !_slateToggleFlag + ? null + : () => Navigator.of(context).pop(_receiveSlateController.text), ), const SizedBox(height: 16), SecondaryButton( diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart index 150eecb0b2..7e483726a2 100644 --- a/lib/pages/send_view/frost_ms/recipient.dart +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -125,8 +125,10 @@ class _RecipientState extends ConsumerState { Logging.instance.d("qrResult content: ${qrResult.rawContent}"); + if (qrResult.rawContent == null) return; + final paymentData = AddressUtils.parsePaymentUri( - qrResult.rawContent, + qrResult.rawContent!, logging: Logging.instance, ); @@ -148,7 +150,7 @@ class _RecipientState extends ConsumerState { .format(amount, withUnitName: false); } } else { - addressController.text = qrResult.rawContent.trim(); + addressController.text = qrResult.rawContent!.trim(); } setState(() { @@ -244,8 +246,9 @@ class _RecipientState extends ConsumerState { ), CustomTextButton( text: isSingle ? "Add another recipient" : "Remove", - onTap: - isSingle ? widget.addAnotherRecipientTapped : widget.remove, + onTap: isSingle + ? widget.addAnotherRecipientTapped + : widget.remove, ), ], ), @@ -268,93 +271,92 @@ class _RecipientState extends ConsumerState { _addressIsEmpty = addressController.text.isEmpty; }); }, - decoration: standardInputDecoration( - "Enter ${widget.coin.ticker} address", - addressFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: - _addressIsEmpty + decoration: + standardInputDecoration( + "Enter ${widget.coin.ticker} address", + addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _addressIsEmpty ? const EdgeInsets.only(right: 8) : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_addressIsEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Address Field Input.", - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - addressController.text = ""; - - setState(() { - _addressIsEmpty = true; - }); - - _updateRecipientData(); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Address Field Input.", - key: const Key( - "sendViewPasteAddressFieldButtonKey", + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_addressIsEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Address Field Input.", + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + addressController.text = ""; + + setState(() { + _addressIsEmpty = true; + }); + + _updateRecipientData(); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Address Field Input.", + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await ref + .read(pClipboard) + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, + content.indexOf("\n"), + ); + } + + addressController.text = content.trim(); + + setState(() { + _addressIsEmpty = + addressController.text.isEmpty; + }); + + _updateRecipientData(); + } + }, + child: _addressIsEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_addressIsEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. " + "Opens Camera For Scanning QR Code.", + key: const Key("sendViewScanQrButtonKey"), + onTap: _onQrTapped, + child: const QrCodeIcon(), ), - onTap: () async { - final ClipboardData? data = await ref - .read(pClipboard) - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - String content = data.text!.trim(); - if (content.contains("\n")) { - content = content.substring( - 0, - content.indexOf("\n"), - ); - } - - addressController.text = content.trim(); - - setState(() { - _addressIsEmpty = - addressController.text.isEmpty; - }); - - _updateRecipientData(); - } - }, - child: - _addressIsEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_addressIsEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: const Key("sendViewScanQrButtonKey"), - onTap: _onQrTapped, - child: const QrCodeIcon(), - ), - ], + ], + ), + ), ), ), - ), - ), ), ), SizedBox(height: isSingle ? 12 : 8), @@ -391,13 +393,12 @@ class _RecipientState extends ConsumerState { onChanged: (_) { _updateRecipientData(); }, - keyboardType: - Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( @@ -419,10 +420,9 @@ class _RecipientState extends ConsumerState { .watch(pAmountUnit(widget.coin)) .unitForCoin(widget.coin), style: STextStyles.smallMed14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 54cdf51eb8..884cb5dc2d 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -290,9 +290,10 @@ class _SendViewState extends ConsumerState { // ); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); + if (qrResult.rawContent == null) return; final paymentData = AddressUtils.parsePaymentUri( - qrResult.rawContent, + qrResult.rawContent!, logging: Logging.instance, ); @@ -300,7 +301,7 @@ class _SendViewState extends ConsumerState { paymentData.coin?.uriScheme == coin.uriScheme) { _applyUri(paymentData); } else { - _address = qrResult.rawContent.split("\n").first.trim(); + _address = qrResult.rawContent!.split("\n").first.trim(); sendToController.text = _address ?? ""; _setValidAddressProviders(_address); diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index fdf78136e0..8294209090 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -163,9 +163,10 @@ class _TokenSendViewState extends ConsumerState { // ); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); + if (qrResult.rawContent == null) return; final paymentData = AddressUtils.parsePaymentUri( - qrResult.rawContent, + qrResult.rawContent!, logging: Logging.instance, ); @@ -206,7 +207,7 @@ class _TokenSendViewState extends ConsumerState { // now check for non standard encoded basic address } else { - _address = qrResult.rawContent.split("\n").first.trim(); + _address = qrResult.rawContent!.split("\n").first.trim(); sendToController.text = _address ?? ""; _updatePreviewButtonState(_address, _amountToSend); @@ -249,24 +250,22 @@ class _TokenSendViewState extends ConsumerState { locale: ref.read(localeServiceChangeNotifierProvider).locale, ); if (baseAmount != null) { - final _price = - ref - .read(priceAnd24hChangeNotifierProvider) - .getTokenPrice(tokenContract.address) - ?.value; + final _price = ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice(tokenContract.address) + ?.value; if (_price == null || _price == Decimal.zero) { _amountToSend = Amount.zero; } else { - _amountToSend = - baseAmount <= Amount.zero - ? Amount.zero - : Amount.fromDecimal( - (baseAmount.decimal / _price).toDecimal( - scaleOnInfinitePrecision: tokenContract.decimals, - ), - fractionDigits: tokenContract.decimals, - ); + _amountToSend = baseAmount <= Amount.zero + ? Amount.zero + : Amount.fromDecimal( + (baseAmount.decimal / _price).toDecimal( + scaleOnInfinitePrecision: tokenContract.decimals, + ), + fractionDigits: tokenContract.decimals, + ); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -305,11 +304,10 @@ class _TokenSendViewState extends ConsumerState { } _cachedAmountToSend = _amountToSend; - final price = - ref - .read(priceAnd24hChangeNotifierProvider) - .getTokenPrice(tokenContract.address) - ?.value; + final price = ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice(tokenContract.address) + ?.value; if (price != null && price > Decimal.zero) { baseAmountController.text = (_amountToSend!.decimal * price) @@ -496,8 +494,9 @@ class _TokenSendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, - addressType: - tokenWallet.cryptoCurrency.getAddressType(_address!)!, + addressType: tokenWallet.cryptoCurrency.getAddressType( + _address!, + )!, ), ], feeRateType: ref.read(feeRateTypeMobileStateProvider), @@ -518,14 +517,13 @@ class _TokenSendViewState extends ConsumerState { Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: - (_) => ConfirmTransactionView( - txData: txData, - walletId: walletId, - isTokenTx: true, - onSuccess: clearSendForm, - routeOnSuccessName: TokenView.routeName, - ), + builder: (_) => ConfirmTransactionView( + txData: txData, + walletId: walletId, + isTokenTx: true, + onSuccess: clearSendForm, + routeOnSuccessName: TokenView.routeName, + ), settings: const RouteSettings( name: ConfirmTransactionView.routeName, ), @@ -555,10 +553,9 @@ class _TokenSendViewState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -699,10 +696,9 @@ class _TokenSendViewState extends ConsumerState { children: [ Container( decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.popupBG, + color: Theme.of( + context, + ).extension()!.popupBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -835,85 +831,90 @@ class _TokenSendViewState extends ConsumerState { }, focusNode: _addressFocusNode, style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${tokenContract.symbol} address", - _addressFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: - sendToController.text.isEmpty + decoration: + standardInputDecoration( + "Enter ${tokenContract.symbol} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: sendToController.text.isEmpty ? const EdgeInsets.only(right: 8) : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _addressToggleFlag - ? TextFieldIconButton( - key: const Key( - "tokenSendViewClearAddressFieldButtonKey", + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "tokenSendViewClearAddressFieldButtonKey", + ), + onTap: () { + sendToController.text = + ""; + _address = ""; + _updatePreviewButtonState( + _address, + _amountToSend, + ); + setState(() { + _addressToggleFlag = + false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "tokenSendViewPasteAddressFieldButtonKey", + ), + onTap: + _onTokenSendViewPasteAddressFieldButtonPressed, + child: + sendToController + .text + .isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey", + ), + onTap: () { + Navigator.of( + context, + ).pushNamed( + AddressBookView.routeName, + arguments: widget.coin, + ); + }, + child: + const AddressBookIcon(), ), - onTap: () { - sendToController.text = ""; - _address = ""; - _updatePreviewButtonState( - _address, - _amountToSend, - ); - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "tokenSendViewPasteAddressFieldButtonKey", + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey", + ), + onTap: + _onTokenSendViewScanQrButtonPressed, + child: const QrCodeIcon(), ), - onTap: - _onTokenSendViewPasteAddressFieldButtonPressed, - child: - sendToController - .text - .isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey", - ), - onTap: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: widget.coin, - ); - }, - child: const AddressBookIcon(), - ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey", - ), - onTap: - _onTokenSendViewScanQrButtonPressed, - child: const QrCodeIcon(), - ), - ], + ], + ), + ), ), ), - ), - ), ), ), Builder( @@ -935,14 +936,12 @@ class _TokenSendViewState extends ConsumerState { child: Text( error, textAlign: TextAlign.left, - style: STextStyles.label( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.label(context) + .copyWith( + color: Theme.of(context) .extension()! .textError, - ), + ), ), ), ); @@ -977,23 +976,21 @@ class _TokenSendViewState extends ConsumerState { autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, style: STextStyles.smallMed14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textDark, + color: Theme.of( + context, + ).extension()!.textDark, ), key: const Key( "amountInputFieldCryptoTextFieldKey", ), controller: cryptoAmountController, focusNode: _cryptoFocus, - keyboardType: - Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( @@ -1026,14 +1023,12 @@ class _TokenSendViewState extends ConsumerState { ref .watch(pAmountUnit(coin)) .unitForContract(tokenContract), - style: STextStyles.smallMed14( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) .extension()! .accentColorDark, - ), + ), ), ), ), @@ -1044,26 +1039,25 @@ class _TokenSendViewState extends ConsumerState { if (Prefs.instance.externalCalls) TextField( autocorrect: Util.isDesktop ? false : true, - enableSuggestions: - Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop + ? false + : true, style: STextStyles.smallMed14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textDark, + color: Theme.of( + context, + ).extension()!.textDark, ), key: const Key( "amountInputFieldFiatTextFieldKey", ), controller: baseAmountController, focusNode: _baseFocus, - keyboardType: - Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( @@ -1098,14 +1092,12 @@ class _TokenSendViewState extends ConsumerState { (value) => value.currency, ), ), - style: STextStyles.smallMed14( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) .extension()! .accentColorDark, - ), + ), ), ), ), @@ -1124,41 +1116,42 @@ class _TokenSendViewState extends ConsumerState { ), child: TextField( autocorrect: Util.isDesktop ? false : true, - enableSuggestions: - Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop + ? false + : true, controller: noteController, focusNode: _noteFocusNode, style: STextStyles.field(context), onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Type something...", - _noteFocusNode, - context, - ).copyWith( - suffixIcon: - noteController.text.isNotEmpty + decoration: + standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: noteController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only( - right: 0, - ), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - noteController.text = - ""; - }); - }, - ), - ], + padding: const EdgeInsets.only( + right: 0, ), - ), - ) + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = + ""; + }); + }, + ), + ], + ), + ), + ) : null, - ), + ), ), ), const SizedBox(height: 12), @@ -1172,8 +1165,9 @@ class _TokenSendViewState extends ConsumerState { children: [ TextField( autocorrect: Util.isDesktop ? false : true, - enableSuggestions: - Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop + ? false + : true, controller: feeController, readOnly: true, textInputAction: TextInputAction.none, @@ -1183,10 +1177,9 @@ class _TokenSendViewState extends ConsumerState { horizontal: 12, ), child: RawMaterialButton( - splashColor: - Theme.of( - context, - ).extension()!.highlight, + splashColor: Theme.of( + context, + ).extension()!.highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1201,19 +1194,21 @@ class _TokenSendViewState extends ConsumerState { top: Radius.circular(20), ), ), - builder: - (_) => TransactionFeeSelectionSheet( + builder: (_) => + TransactionFeeSelectionSheet( walletId: walletId, isToken: true, - amount: (Decimal.tryParse( - cryptoAmountController - .text, - ) ?? - Decimal.zero) - .toAmount( - fractionDigits: - tokenContract.decimals, - ), + amount: + (Decimal.tryParse( + cryptoAmountController + .text, + ) ?? + Decimal.zero) + .toAmount( + fractionDigits: + tokenContract + .decimals, + ), updateChosen: (String fee) { if (fee == "custom") { if (!isCustomFee.value) { @@ -1317,28 +1312,24 @@ class _TokenSendViewState extends ConsumerState { TextButton( onPressed: ref - .watch( - previewTokenTxButtonStateProvider - .state, - ) - .state - ? _previewTransaction - : null, + .watch( + previewTokenTxButtonStateProvider.state, + ) + .state + ? _previewTransaction + : null, style: ref - .watch( - previewTokenTxButtonStateProvider - .state, - ) - .state - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle( - context, - ), + .watch( + previewTokenTxButtonStateProvider.state, + ) + .state + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), child: Text( "Preview", style: STextStyles.button(context), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 738ccb8a98..75411e9cbc 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -375,7 +375,7 @@ class _AddEditNodeViewState extends ConsumerState { } else { try { final result = await ref.read(pBarcodeScanner).scan(context: context); - await _processQrData(result.rawContent); + await _processQrData(result.rawContent ?? ""); } on PlatformException catch (e, s) { if (mounted) { try { diff --git a/lib/utilities/barcode_scanner_interface.dart b/lib/utilities/barcode_scanner_interface.dart index f8256f2e55..5ab338fe01 100644 --- a/lib/utilities/barcode_scanner_interface.dart +++ b/lib/utilities/barcode_scanner_interface.dart @@ -20,7 +20,7 @@ import '../widgets/stack_dialog.dart'; import 'logger.dart'; class ScanResult { - final String rawContent; + final String? rawContent; ScanResult({required this.rawContent}); } @@ -35,12 +35,12 @@ class BarcodeScannerWrapper implements BarcodeScannerInterface { @override Future scan({required BuildContext context}) async { try { - final data = await showDialog( + final data = await showDialog( context: context, builder: (context) => const QrScanner(), ); - return ScanResult(rawContent: data.toString()); + return ScanResult(rawContent: data); } catch (e) { rethrow; } @@ -61,19 +61,18 @@ Future checkCamPermDeniedMobileAndOpenAppSettings( if ((iosShow || androidShow) && context.mounted) { final trySettings = await showDialog( context: context, - builder: - (context) => StackDialog( - title: "Camera permissions required", - message: "Open settings?", - leftButton: SecondaryButton( - label: "Cancel", - onPressed: Navigator.of(context).pop, - ), - rightButton: PrimaryButton( - label: "Continue", - onPressed: () => Navigator.of(context).pop(true), - ), - ), + builder: (context) => StackDialog( + title: "Camera permissions required", + message: "Open settings?", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + rightButton: PrimaryButton( + label: "Continue", + onPressed: () => Navigator.of(context).pop(true), + ), + ), ); if (trySettings == true) { @@ -83,15 +82,14 @@ Future checkCamPermDeniedMobileAndOpenAppSettings( if (context.mounted) { await showDialog( context: context, - builder: - (context) => StackDialog( - title: "Could not open app settings", - message: "You will need manually go find your app settings", - rightButton: PrimaryButton( - label: "Ok", - onPressed: Navigator.of(context).pop, - ), - ), + builder: (context) => StackDialog( + title: "Could not open app settings", + message: "You will need manually go find your app settings", + rightButton: PrimaryButton( + label: "Ok", + onPressed: Navigator.of(context).pop, + ), + ), ); } } diff --git a/lib/widgets/qr_scanner.dart b/lib/widgets/qr_scanner.dart index 66941ac9d3..7f3c428c62 100644 --- a/lib/widgets/qr_scanner.dart +++ b/lib/widgets/qr_scanner.dart @@ -1,45 +1,72 @@ +import 'dart:async'; +import 'dart:io'; + import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; import '../themes/stack_colors.dart'; -import '../utilities/logger.dart'; import '../utilities/text_styles.dart'; import 'background.dart'; import 'custom_buttons/app_bar_icon_button.dart'; -class QrScanner extends ConsumerWidget { +class QrScanner extends StatefulWidget { const QrScanner({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + State createState() => _QrScannerState(); +} + +class _QrScannerState extends State { + final GlobalKey qrKey = GlobalKey(debugLabel: "QR Scan Key"); + + QRViewController? controller; + + StreamSubscription? sub; + + void _onScanned(String? data) { + if (data != null && mounted) { + Navigator.of(context).pop(data); + } + } + + // In order to get hot reload to work we need to pause the camera if the platform + // is android, or resume the camera if the platform is iOS. + @override + void reassemble() { + super.reassemble(); + if (Platform.isAndroid) { + controller!.pauseCamera(); + } else if (Platform.isIOS) { + controller!.resumeCamera(); + } + } + + @override + void dispose() { + sub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.backgroundAppBar, + backgroundColor: Theme.of( + context, + ).extension()!.backgroundAppBar, leading: const AppBarBackButton(), title: Text("Scan QR code", style: STextStyles.navBarTitle(context)), ), - body: MobileScanner( - onDetect: (capture) { - final data = - ((capture.raw as Map?)?["data"] as List?)?.firstOrNull as Map?; - - final value = - data?["rawValue"] as String? ?? - data?["displayValue"] as String?; - - Navigator.of(context).pop(value); - }, - onDetectError: (error, stackTrace) { - Logging.instance.w( - "Mobile scanner", - error: error, - stackTrace: stackTrace, - ); - Navigator.of(context).pop(); + body: QRView( + key: qrKey, + onQRViewCreated: (QRViewController p1) { + sub?.cancel(); + controller = p1; + sub = controller!.scannedDataStream.listen((data) { + _onScanned(data.code); + }); }, ), ), diff --git a/lib/widgets/textfields/frost_step_field.dart b/lib/widgets/textfields/frost_step_field.dart index f94fac2b41..111ba88e5d 100644 --- a/lib/widgets/textfields/frost_step_field.dart +++ b/lib/widgets/textfields/frost_step_field.dart @@ -80,8 +80,9 @@ class _FrostStepFieldState extends ConsumerState { } final qrResult = await ref.read(pBarcodeScanner).scan(context: context); + if (qrResult.rawContent == null) return; - widget.controller.text = qrResult.rawContent; + widget.controller.text = qrResult.rawContent!; _changed(widget.controller.text); } else { @@ -128,15 +129,14 @@ class _FrostStepFieldState extends ConsumerState { Widget build(BuildContext context) { return ConditionalParent( condition: widget.label != null, - builder: - (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text(widget.label!, style: STextStyles.w500_14(context)), - const SizedBox(height: 4), - child, - ], - ), + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(widget.label!, style: STextStyles.w500_14(context)), + const SizedBox(height: 4), + child, + ], + ), child: TextField( controller: widget.controller, focusNode: widget.focusNode, @@ -147,60 +147,55 @@ class _FrostStepFieldState extends ConsumerState { onChanged: _changed, decoration: InputDecoration( hintText: widget.hint, - fillColor: - widget.focusNode.hasFocus - ? Theme.of( - context, - ).extension()!.textFieldActiveBG - : Theme.of( - context, - ).extension()!.textFieldDefaultBG, - hintStyle: - Util.isDesktop - ? STextStyles.desktopTextFieldLabel(context) - : STextStyles.fieldLabel(context), + fillColor: widget.focusNode.hasFocus + ? Theme.of(context).extension()!.textFieldActiveBG + : Theme.of(context).extension()!.textFieldDefaultBG, + hintStyle: Util.isDesktop + ? STextStyles.desktopTextFieldLabel(context) + : STextStyles.fieldLabel(context), enabledBorder: _inputBorder, focusedBorder: _inputBorder, errorBorder: _inputBorder, disabledBorder: _inputBorder, focusedErrorBorder: _inputBorder, suffixIcon: Padding( - padding: - _isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: _isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ !_isEmpty ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Frost Step Field Input.", - key: _xKey, - onTap: () { - widget.controller.text = ""; - - _changed(widget.controller.text); - }, - child: const XIcon(), - ) + semanticsLabel: + "Clear Button. Clears The Frost Step Field Input.", + key: _xKey, + onTap: () { + widget.controller.text = ""; + + _changed(widget.controller.text); + }, + child: const XIcon(), + ) : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Frost Step Field Input.", - key: _pasteKey, - onTap: () async { - final ClipboardData? data = await Clipboard.getData( - Clipboard.kTextPlain, - ); - if (data?.text != null && data!.text!.isNotEmpty) { - widget.controller.text = data.text!.trim(); - } - - _changed(widget.controller.text); - }, - child: _isEmpty ? const ClipboardIcon() : const XIcon(), - ), + semanticsLabel: + "Paste Button. Pastes From Clipboard To Frost Step Field Input.", + key: _pasteKey, + onTap: () async { + final ClipboardData? data = await Clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && data!.text!.isNotEmpty) { + widget.controller.text = data.text!.trim(); + } + + _changed(widget.controller.text); + }, + child: _isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_isEmpty && widget.showQrScanOption) TextFieldIconButton( semanticsLabel: diff --git a/pubspec.lock b/pubspec.lock index d30893855b..7ed626d613 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1611,14 +1611,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" - mobile_scanner: - dependency: "direct main" - description: - name: mobile_scanner - sha256: "5e7e09d904dc01de071b79b3f3789b302b0ed3c9c963109cd3f83ad90de62ecf" - url: "https://pub.dev" - source: hosted - version: "7.1.2" mockingjay: dependency: "direct dev" description: @@ -1940,6 +1932,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + qr_code_scanner_plus: + dependency: "direct main" + description: + name: qr_code_scanner_plus + sha256: b764e5004251c58d9dee0c295e6006e05bd8d249e78ac3383abdb5afe0a996cd + url: "https://pub.dev" + source: hosted + version: "2.0.14" qr_flutter: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index b6277798b2..80a1a18a10 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -157,7 +157,6 @@ dependencies: event_bus: ^2.0.0 uuid: ^3.0.5 crypto: ^3.0.2 - mobile_scanner: ^7.0.1 image: ^4.3.0 wakelock_plus: ^1.2.8 intl: ^0.17.0 @@ -251,6 +250,7 @@ dependencies: saf_util: ^0.11.0 saf_stream: ^0.12.3 unorm_dart: ^0.2.0 + qr_code_scanner_plus: ^2.0.14 dev_dependencies: flutter_test: From dc1cca5c347ae7c2295920df7f6feb4b79eef7af Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 5 Nov 2025 13:26:27 -0600 Subject: [PATCH 16/50] try to ensure only one pop occurs --- .../signing/sub_widgets/sign_message_tab.dart | 4 ++-- .../signing/sub_widgets/verify_message_tab.dart | 2 +- lib/utilities/if_not_already.dart | 16 +++++++++++----- lib/widgets/qr_scanner.dart | 17 +++++++++++++---- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/pages/signing/sub_widgets/sign_message_tab.dart b/lib/pages/signing/sub_widgets/sign_message_tab.dart index a4b0d9241f..e7970486e9 100644 --- a/lib/pages/signing/sub_widgets/sign_message_tab.dart +++ b/lib/pages/signing/sub_widgets/sign_message_tab.dart @@ -90,7 +90,7 @@ class _SignMessageFormState extends ConsumerState { messageController.text = ref.read(_pSignState).message; - _chooseAddress = IfNotAlreadyAsync(() async { + _chooseAddress = IfNotAlreadyAsync(() async { final Address? address; if (Util.isDesktop) { @@ -166,7 +166,7 @@ class _SignMessageFormState extends ConsumerState { } }).execute; - _sign = IfNotAlreadyAsync(() async { + _sign = IfNotAlreadyAsync(() async { Exception? ex; final state = ref.read(_pSignState); diff --git a/lib/pages/signing/sub_widgets/verify_message_tab.dart b/lib/pages/signing/sub_widgets/verify_message_tab.dart index 08b4e59158..0a33183174 100644 --- a/lib/pages/signing/sub_widgets/verify_message_tab.dart +++ b/lib/pages/signing/sub_widgets/verify_message_tab.dart @@ -82,7 +82,7 @@ class _VerifyMessageFormState extends ConsumerState { messageController.text = ref.read(_pVerifyState).message; signatureController.text = ref.read(_pVerifyState).signature; - _verify = IfNotAlreadyAsync(() async { + _verify = IfNotAlreadyAsync(() async { Exception? ex; final verified = await showLoading( diff --git a/lib/utilities/if_not_already.dart b/lib/utilities/if_not_already.dart index 664fc9aad6..41da52cf7b 100644 --- a/lib/utilities/if_not_already.dart +++ b/lib/utilities/if_not_already.dart @@ -18,18 +18,24 @@ class IfNotAlready { } } -class IfNotAlreadyAsync { - final Future Function() _function; +class IfNotAlreadyAsync { + final Future Function()? _function; + final Future Function(T? args)? _functionWithArgs; bool _locked = false; - IfNotAlreadyAsync(this._function); + IfNotAlreadyAsync(this._function) : _functionWithArgs = null; + IfNotAlreadyAsync.withArgs(this._functionWithArgs) : _function = null; - Future execute() async { + Future execute([T? args]) async { if (!_locked) { _locked = true; try { - await _function(); + if (_function == null) { + await _function!(); + } else { + await _functionWithArgs!(args); + } } finally { _locked = false; } diff --git a/lib/widgets/qr_scanner.dart b/lib/widgets/qr_scanner.dart index 7f3c428c62..cd19c839f5 100644 --- a/lib/widgets/qr_scanner.dart +++ b/lib/widgets/qr_scanner.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; import '../themes/stack_colors.dart'; +import '../utilities/if_not_already.dart'; import '../utilities/text_styles.dart'; import 'background.dart'; import 'custom_buttons/app_bar_icon_button.dart'; @@ -23,10 +24,18 @@ class _QrScannerState extends State { StreamSubscription? sub; - void _onScanned(String? data) { - if (data != null && mounted) { - Navigator.of(context).pop(data); - } + late final Future Function(String?) _onScanned; + + @override + void initState() { + super.initState(); + + _onScanned = IfNotAlreadyAsync.withArgs((data) async { + await sub?.cancel(); + if (mounted) { + Navigator.of(context).pop(data); + } + }).execute; } // In order to get hot reload to work we need to pause the camera if the platform From 2bc27922ca51f756b02a1ad8f55c01350ce490c8 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 5 Nov 2025 14:58:53 -0600 Subject: [PATCH 17/50] fix android SAF SWB --- .../stack_backup_views/auto_backup_view.dart | 25 ++++++++++--------- .../create_auto_backup_view.dart | 2 +- .../create_backup_view.dart | 2 +- .../edit_auto_backup_view.dart | 2 +- .../create_auto_backup.dart | 2 +- lib/services/auto_swb_service.dart | 14 ++++++++++- 6 files changed, 30 insertions(+), 17 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart index 57785f5f43..849f10be09 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart @@ -101,8 +101,9 @@ class _AutoBackupViewState extends ConsumerState { child: Text( "Back", style: STextStyles.button(context).copyWith( - color: - Theme.of(context).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -155,8 +156,9 @@ class _AutoBackupViewState extends ConsumerState { child: Text( "Back", style: STextStyles.button(context).copyWith( - color: - Theme.of(context).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -313,14 +315,13 @@ class _AutoBackupViewState extends ConsumerState { TextSpan( text: "stackwallet.com.", style: STextStyles.richLink(context), - recognizer: - TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse("https://stackwallet.com"), - mode: LaunchMode.externalApplication, - ); - }, + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse("https://stackwallet.com"), + mode: LaunchMode.externalApplication, + ); + }, ), ], ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index c1bd1b2edb..9e32ad392c 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -261,7 +261,7 @@ class _EnableAutoBackupViewState extends ConsumerState { await stackFileSystem.prepareStorage(); if (mounted) { final filePath = await stackFileSystem - .openFile(); + .pickDir(); if (mounted) { setState(() { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 0df1b215ca..ebb6b94cf2 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -310,7 +310,7 @@ class _RestoreFromFileViewState extends ConsumerState { await stackFileSystem.prepareStorage(); if (mounted) { final filePath = await stackFileSystem - .openFile(); + .pickDir(); if (mounted) { setState(() { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 81d7b9d0dd..077ff21c51 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -302,7 +302,7 @@ class _EditAutoBackupViewState extends ConsumerState { try { await stackFileSystem.prepareStorage(); if (mounted) { - final filePath = await stackFileSystem.openFile(); + final filePath = await stackFileSystem.pickDir(); if (mounted) { setState(() { diff --git a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart index 929c3c214f..1a0fc67533 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart @@ -321,7 +321,7 @@ class _CreateAutoBackup extends ConsumerState { await stackFileSystem.prepareStorage(); if (mounted) { final filePath = await stackFileSystem - .openFile(); + .pickDir(); if (mounted) { setState(() { diff --git a/lib/services/auto_swb_service.dart b/lib/services/auto_swb_service.dart index 39b080a336..419e7a0804 100644 --- a/lib/services/auto_swb_service.dart +++ b/lib/services/auto_swb_service.dart @@ -17,6 +17,7 @@ import 'package:tuple/tuple.dart'; import '../pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import '../utilities/flutter_secure_storage_interface.dart'; +import '../utilities/fs.dart'; import '../utilities/logger.dart'; import '../utilities/prefs.dart'; @@ -91,7 +92,11 @@ class AutoSWBService extends ChangeNotifier { adkVersion, ); - await File(fileToSave).writeAsString(content, flush: true); + await FS.writeStringToFile( + content, + autoBackupDirectoryPath, + fileToSave.split("/").last, + ); Prefs.instance.lastAutoBackup = now; @@ -121,6 +126,13 @@ class AutoSWBService extends ChangeNotifier { /// Trim the number of auto backup files based on age void trimBackups(String dirPath, int numberToKeep) { + if (Platform.isAndroid && dirPath.startsWith("content://")) { + Logging.instance.w( + "Android SAF lib doesn't provide a deletion API. Cannot trim/rotate out old backups", + ); + return; + } + final dir = Directory(dirPath); final List> files = []; From 2cdf67045188981a3b0bc5cac78f98e1db6ac687 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 5 Nov 2025 15:08:12 -0600 Subject: [PATCH 18/50] fix logical issue --- lib/utilities/if_not_already.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utilities/if_not_already.dart b/lib/utilities/if_not_already.dart index 41da52cf7b..8a2a0406e2 100644 --- a/lib/utilities/if_not_already.dart +++ b/lib/utilities/if_not_already.dart @@ -31,8 +31,8 @@ class IfNotAlreadyAsync { if (!_locked) { _locked = true; try { - if (_function == null) { - await _function!(); + if (_function != null) { + await _function(); } else { await _functionWithArgs!(args); } From 27b16751d222a87338884aae5d65e45d40584cee Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Nov 2025 07:51:51 -0600 Subject: [PATCH 19/50] "fix" windows not fully exiting on close --- lib/main.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 7b4d40fde8..1daf66c2ee 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -610,7 +610,8 @@ class _MaterialAppWithThemeState extends ConsumerState @override Future didRequestAppExit() async { debugPrint("didRequestAppExit called"); - if (Platform.isMacOS) { + if (Platform.isMacOS || Platform.isWindows) { + // Monero will cause issues if in the middle of syncing. // On macOS, mwebd fails to shut down, hanging the app on close. // // Exiting is a hack fix for this issue. From aafef3956a688a8824706045f255fd1325bd8593 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Nov 2025 07:52:33 -0600 Subject: [PATCH 20/50] windows mwebd.exe verification fix --- ..._mwebd_server_interface_impl.template.dart | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tool/wl_templates/MWEBD_mwebd_server_interface_impl.template.dart b/tool/wl_templates/MWEBD_mwebd_server_interface_impl.template.dart index 4098f8adcf..f7bec47186 100644 --- a/tool/wl_templates/MWEBD_mwebd_server_interface_impl.template.dart +++ b/tool/wl_templates/MWEBD_mwebd_server_interface_impl.template.dart @@ -29,27 +29,37 @@ class _MwebdServerInterfaceImpl extends MwebdServerInterface { static const _kExe = "mwebd.exe"; + static String? _cachedWinExePath; + Future _prepareWindowsExeDirPath() async { - final dir = (await StackFileSystem.applicationMwebdDirectory( - "dummy", - )).parent.path; - final exe = File(join(dir, _kExe)); + if (_cachedWinExePath == null) { + final dir = (await StackFileSystem.applicationMwebdDirectory( + "dummy", + )).parent.path; + + final exe = File(join(dir, _kExe)); + + if (await exe.exists()) { + await exe.delete(); + } - if (!(await exe.exists())) { final bytes = await rootBundle.load("assets/windows/mwebd.exe"); await exe.writeAsBytes( bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes), flush: true, ); + _cachedWinExePath = exe.parent.path; } - final hash = await sha256.bind(exe.openRead()).first; + final hash = await sha256 + .bind(File(join(_cachedWinExePath!, _kExe)).openRead()) + .first; final hexHash = Uint8List.fromList(hash.bytes).toHex; if (AppConfig.windowsMwebdExeHash != hexHash) { throw Exception("Windows mwebd.exe sha256 has mismatch!!!"); } - return exe.parent.path; + return _cachedWinExePath!; } @override From eb200eee56f80876e07b1ce0f1844eab9153c102 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Nov 2025 07:53:02 -0600 Subject: [PATCH 21/50] const HTTP constructor --- lib/networking/http.dart | 44 ++++++++++------------------------------ 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/lib/networking/http.dart b/lib/networking/http.dart index 80ea57c370..48b5f1c661 100644 --- a/lib/networking/http.dart +++ b/lib/networking/http.dart @@ -20,27 +20,21 @@ class Response { } class HTTP { + const HTTP(); + Future get({ required Uri url, Map? headers, - required ({ - InternetAddress host, - int port, - })? proxyInfo, + required ({InternetAddress host, int port})? proxyInfo, }) async { final httpClient = HttpClient(); try { if (proxyInfo != null) { SocksTCPClient.assignToHttpClient(httpClient, [ - ProxySettings( - proxyInfo.host, - proxyInfo.port, - ), + ProxySettings(proxyInfo.host, proxyInfo.port), ]); } - final HttpClientRequest request = await httpClient.getUrl( - url, - ); + final HttpClientRequest request = await httpClient.getUrl(url); if (headers != null) { headers.forEach((key, value) => request.headers.add(key, value)); @@ -48,10 +42,7 @@ class HTTP { final response = await request.close(); - return Response( - await _bodyBytes(response), - response.statusCode, - ); + return Response(await _bodyBytes(response), response.statusCode); } catch (e, s) { Logging.instance.w("HTTP.get() rethrew: ", error: e, stackTrace: s); rethrow; @@ -65,24 +56,16 @@ class HTTP { Map? headers, Object? body, Encoding? encoding, - required ({ - InternetAddress host, - int port, - })? proxyInfo, + required ({InternetAddress host, int port})? proxyInfo, }) async { final httpClient = HttpClient(); try { if (proxyInfo != null) { SocksTCPClient.assignToHttpClient(httpClient, [ - ProxySettings( - proxyInfo.host, - proxyInfo.port, - ), + ProxySettings(proxyInfo.host, proxyInfo.port), ]); } - final HttpClientRequest request = await httpClient.postUrl( - url, - ); + final HttpClientRequest request = await httpClient.postUrl(url); if (headers != null) { headers.forEach((key, value) => request.headers.add(key, value)); @@ -91,10 +74,7 @@ class HTTP { request.write(body); final response = await request.close(); - return Response( - await _bodyBytes(response), - response.statusCode, - ); + return Response(await _bodyBytes(response), response.statusCode); } catch (e, s) { Logging.instance.w("HTTP.post() rethrew: ", error: e, stackTrace: s); rethrow; @@ -110,9 +90,7 @@ class HTTP { (data) { bytes.addAll(data); }, - onDone: () => completer.complete( - Uint8List.fromList(bytes), - ), + onDone: () => completer.complete(Uint8List.fromList(bytes)), onError: (Object err, StackTrace s) => Logging.instance.e( "Http wrapper layer listen", error: err, From 384e140b2eda1c5a9332b01cbd04bd807c64a1cc Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Nov 2025 08:23:36 -0600 Subject: [PATCH 22/50] temp fix text overflow on desktop --- .../wallet_info_row/wallet_info_row.dart | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/widgets/wallet_info_row/wallet_info_row.dart b/lib/widgets/wallet_info_row/wallet_info_row.dart index 381d0e0d56..69aa9060e2 100644 --- a/lib/widgets/wallet_info_row/wallet_info_row.dart +++ b/lib/widgets/wallet_info_row/wallet_info_row.dart @@ -67,37 +67,38 @@ class WalletInfoRow extends ConsumerWidget { const SizedBox(width: 12), contract != null ? Row( - children: [ - Text( - contract.name, - style: STextStyles.desktopTextExtraSmall( - context, - ).copyWith( - color: - Theme.of( + children: [ + Text( + contract.name, + style: + STextStyles.desktopTextExtraSmall( context, - ).extension()!.textDark, + ).copyWith( + color: Theme.of( + context, + ).extension()!.textDark, + ), ), - ), - const SizedBox(width: 4), - CoinTickerTag( - ticker: ref.watch( - pWalletCoin(walletId).select((s) => s.ticker), + const SizedBox(width: 4), + CoinTickerTag( + ticker: ref.watch( + pWalletCoin(walletId).select((s) => s.ticker), + ), ), + ], + ) + : Expanded( + child: Text( + wallet.info.name, + overflow: TextOverflow.ellipsis, + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of( + context, + ).extension()!.textDark, + ), ), - ], - ) - : Text( - wallet.info.name, - style: STextStyles.desktopTextExtraSmall( - context, - ).copyWith( - color: - Theme.of( - context, - ).extension()!.textDark, ), - ), ], ), ), From b60fd0f39feb3897c67ea9c36c67d124e5b7508c Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Nov 2025 10:16:46 -0600 Subject: [PATCH 23/50] update native linux entry point --- .../templates/linux/my_application.cc | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/scripts/app_config/templates/linux/my_application.cc b/scripts/app_config/templates/linux/my_application.cc index a6eec39569..ba9d0a4fbc 100644 --- a/scripts/app_config/templates/linux/my_application.cc +++ b/scripts/app_config/templates/linux/my_application.cc @@ -14,6 +14,12 @@ struct _MyApplication { G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView *view) +{ + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); @@ -54,9 +60,18 @@ static void my_application_activate(GApplication* application) { fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self); + gtk_widget_realize(GTK_WIDGET(view)); + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); @@ -81,6 +96,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch return TRUE; } +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + // Implements GObject::dispose. static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); @@ -91,12 +124,20 @@ static void my_application_dispose(GObject* object) { static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, From 6e12222f71347fa20ed121e236dc81bcd9f432e4 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Nov 2025 10:35:00 -0600 Subject: [PATCH 24/50] https://github.com/cypherstack/stack_wallet/issues/1211 --- scripts/app_config/templates/linux/my_application.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/app_config/templates/linux/my_application.cc b/scripts/app_config/templates/linux/my_application.cc index ba9d0a4fbc..58584f452f 100644 --- a/scripts/app_config/templates/linux/my_application.cc +++ b/scripts/app_config/templates/linux/my_application.cc @@ -43,7 +43,9 @@ static void my_application_activate(GApplication* application) { } } #endif - if (use_header_bar) { + const char* gtk_csd_env_var = getenv("GTK_CSD"); + gboolean use_gtk_csd = !gtk_csd_env_var || strcmp(gtk_csd_env_var, "0") != 0; + if (use_header_bar && use_gtk_csd) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); gtk_header_bar_set_title(header_bar, "PlaceHolderName"); From c25c0ef2740209777b42cb45eabfc5fbfa467c6b Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Nov 2025 10:46:48 -0600 Subject: [PATCH 25/50] only stream newly added mwebd logs --- lib/services/mwebd_service.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/services/mwebd_service.dart b/lib/services/mwebd_service.dart index a3c219470a..10ad475318 100644 --- a/lib/services/mwebd_service.dart +++ b/lib/services/mwebd_service.dart @@ -203,11 +203,15 @@ final class MwebdService { "${Platform.pathSeparator}logs" "${Platform.pathSeparator}debug.log"; + final file = File(path); + + if (await file.exists()) { + offset = await file.length(); + } + Future poll() async { if (!controller.isClosed) { - final file = File(path); - - if (!file.existsSync()) { + if (!(await file.exists())) { return; } From 0a8b7a4469a081ce9b99f8bbb2510ef5ae2904fb Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Nov 2025 16:10:16 -0600 Subject: [PATCH 26/50] use mobile_app_privacy --- lib/main.dart | 30 +- .../security_views/security_view.dart | 350 +++++++++++------- lib/utilities/prefs.dart | 50 +++ pubspec.lock | 11 +- .../templates/pubspec.template.yaml | 4 + 5 files changed, 312 insertions(+), 133 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 1daf66c2ee..b0d58afbc7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,6 +23,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; import 'package:logger/logger.dart'; +import 'package:mobile_app_privacy/mobile_app_privacy.dart'; import 'package:path_provider/path_provider.dart'; import 'package:window_size/window_size.dart'; @@ -328,6 +329,10 @@ class _MaterialAppWithThemeState extends ConsumerState with WidgetsBindingObserver { static const platform = MethodChannel("STACK_WALLET_RESTORE"); + final _mobileAppPrivacy = Platform.isAndroid || Platform.isIOS + ? MobileAppPrivacy() + : null; + // late final Wallets _wallets; // late final Prefs _prefs; late final NotificationsService _notificationsService; @@ -459,6 +464,11 @@ class _MaterialAppWithThemeState extends ConsumerState }); } + if (Platform.isAndroid && + ref.read(prefsChangeNotifierProvider).disableScreenShots) { + unawaited(_mobileAppPrivacy?.setFlagSecure(true)); + } + String themeId; if (ref.read(prefsChangeNotifierProvider).enableSystemBrightness) { final brightness = WidgetsBinding.instance.window.platformBrightness; @@ -554,7 +564,18 @@ class _MaterialAppWithThemeState extends ConsumerState @override void didChangeAppLifecycleState(AppLifecycleState state) async { debugPrint("didChangeAppLifecycleState: ${state.name}"); - if (state == AppLifecycleState.resumed) {} + + if (state == AppLifecycleState.resumed) { + await _mobileAppPrivacy?.disableOverlay(); + } else { + if (ref.read(prefsChangeNotifierProvider).privacyScreen) { + await _mobileAppPrivacy?.enableOverlay( + color: ref.read(themeProvider).popupBG, // only android, ios uses blur + blurInsteadOfColor: true, // ignored on android + ); + } + } + switch (state) { case AppLifecycleState.inactive: break; @@ -692,6 +713,13 @@ class _MaterialAppWithThemeState extends ConsumerState // addToDebugMessagesDB: false); // }); + if (Platform.isAndroid) { + ref.listen( + prefsChangeNotifierProvider.select((s) => s.disableScreenShots), + (_, next) => _mobileAppPrivacy?.setFlagSecure(next), + ); + } + final colorScheme = ref.watch(colorProvider.state).state; return MaterialApp( diff --git a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart index 76331b0bb4..c3608fb969 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart @@ -8,6 +8,8 @@ * */ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -61,54 +63,50 @@ class _SecurityViewState extends ConsumerState { Future _createDuressPin() async { final result = await showDialog( context: context, - builder: - (context) => StackDialogBase( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + builder: (context) => StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Enable duress PIN", style: STextStyles.pageTitleH2(context)), + const SizedBox(height: 8), + Row( children: [ - Text( - "Enable duress PIN", - style: STextStyles.pageTitleH2(context), + Flexible( + child: Text( + "When unlocking the app with a duress PIN, only wallets" + " marked as visible in duress mode will be loaded and" + " shown. Be aware that providing a duress PIN instead" + " of your real PIN to law enforcement, border agents," + " or other authorities may be considered deception and" + " could carry legal consequences depending on your" + " jurisdiction. Use with care and according to your" + " threat model.", + style: STextStyles.smallMed14(context), + ), ), - const SizedBox(height: 8), - Row( - children: [ - Flexible( - child: Text( - "When unlocking the app with a duress PIN, only wallets" - " marked as visible in duress mode will be loaded and" - " shown. Be aware that providing a duress PIN instead" - " of your real PIN to law enforcement, border agents," - " or other authorities may be considered deception and" - " could carry legal consequences depending on your" - " jurisdiction. Use with care and according to your" - " threat model.", - style: STextStyles.smallMed14(context), - ), - ), - ], + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () => Navigator.of(context).pop(false), + ), ), - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - onPressed: () => Navigator.of(context).pop(false), - ), - ), - const SizedBox(width: 8), - Expanded( - child: PrimaryButton( - label: "Ok", - onPressed: () => Navigator.of(context).pop(true), - ), - ), - ], + const SizedBox(width: 8), + Expanded( + child: PrimaryButton( + label: "Ok", + onPressed: () => Navigator.of(context).pop(true), + ), ), ], ), - ), + ], + ), + ), ); if (result == true && mounted) { @@ -116,14 +114,13 @@ class _SecurityViewState extends ConsumerState { context, RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: - (_) => const LockscreenView( - showBackButton: true, - routeOnSuccess: CreateDuressPinView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: "Authenticate to create duress PIN", - biometricsAuthenticationTitle: "Create duress PIN", - ), + builder: (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: CreateDuressPinView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: "Authenticate to create duress PIN", + biometricsAuthenticationTitle: "Create duress PIN", + ), settings: const RouteSettings(name: "/createDuressPinLockscreen"), ), ); @@ -133,66 +130,62 @@ class _SecurityViewState extends ConsumerState { Future _deleteDuressPin() async { await showDialog( context: context, - builder: - (context) => StackDialogBase( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + builder: (context) => StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Disable duress PIN", style: STextStyles.pageTitleH2(context)), + const SizedBox(height: 8), + Row( children: [ - Text( - "Disable duress PIN", - style: STextStyles.pageTitleH2(context), - ), - const SizedBox(height: 8), - Row( - children: [ - Flexible( - child: Text( - "Your duress pin will be deleted. " - "You will be asked to create a PIN when you enable this again. " - "Are you sure you want to continue?", + Flexible( + child: Text( + "Your duress pin will be deleted. " + "You will be asked to create a PIN when you enable this again. " + "Are you sure you want to continue?", - style: STextStyles.smallMed14(context), - ), - ), - ], + style: STextStyles.smallMed14(context), + ), ), - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - onPressed: Navigator.of(context).pop, - ), - ), - const SizedBox(width: 8), - Expanded( - child: PrimaryButton( - label: "Ok", - onPressed: () async { - try { - await ref - .read(secureStoreProvider) - .delete(key: kDuressPinKey); - } catch (e, s) { - Logging.instance.f( - "dpin delete failed!!", - error: e, - stackTrace: s, - ); - } + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox(width: 8), + Expanded( + child: PrimaryButton( + label: "Ok", + onPressed: () async { + try { + await ref + .read(secureStoreProvider) + .delete(key: kDuressPinKey); + } catch (e, s) { + Logging.instance.f( + "dpin delete failed!!", + error: e, + stackTrace: s, + ); + } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - ), - ], + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), ), ], ), - ), + ], + ), + ), ); ref.read(prefsChangeNotifierProvider).hasDuressPin = false; @@ -235,15 +228,14 @@ class _SecurityViewState extends ConsumerState { RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: - (_) => const LockscreenView( - showBackButton: true, - routeOnSuccess: ChangePinView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to change PIN", - biometricsAuthenticationTitle: "Change PIN", - ), + builder: (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: ChangePinView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to change PIN", + biometricsAuthenticationTitle: "Change PIN", + ), settings: const RouteSettings( name: "/changepinlockscreen", ), @@ -312,8 +304,9 @@ class _SecurityViewState extends ConsumerState { ), onValueChanged: (newValue) { ref - .read(prefsChangeNotifierProvider) - .useBiometrics = newValue; + .read(prefsChangeNotifierProvider) + .useBiometrics = + newValue; }, ), ), @@ -358,8 +351,9 @@ class _SecurityViewState extends ConsumerState { ), onValueChanged: (newValue) { ref - .read(prefsChangeNotifierProvider) - .randomizePIN = newValue; + .read(prefsChangeNotifierProvider) + .randomizePIN = + newValue; }, ), ), @@ -405,8 +399,9 @@ class _SecurityViewState extends ConsumerState { ), onValueChanged: (newValue) { ref - .read(prefsChangeNotifierProvider) - .autoPin = newValue; + .read(prefsChangeNotifierProvider) + .autoPin = + newValue; }, ), ), @@ -417,6 +412,100 @@ class _SecurityViewState extends ConsumerState { }, ), ), + const SizedBox(height: 8), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Cover in background", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.privacyScreen, + ), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .privacyScreen = + newValue; + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), + if (Platform.isAndroid) const SizedBox(height: 8), + if (Platform.isAndroid) + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Disable screenshots", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.disableScreenShots, + ), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .disableScreenShots = + newValue; + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), if (!ref.watch(pDuress)) const SizedBox(height: 8), if (!ref.watch(pDuress)) RoundedWhiteContainer( @@ -508,8 +597,9 @@ class _SecurityViewState extends ConsumerState { ), onChanged: (newValue) { ref - .read(prefsChangeNotifierProvider) - .biometricsDuress = newValue; + .read(prefsChangeNotifierProvider) + .biometricsDuress = + newValue; }, ), ), @@ -536,17 +626,15 @@ class _SecurityViewState extends ConsumerState { RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: - (_) => const LockscreenView( - showBackButton: true, - routeOnSuccess: - AutoLockTimeoutSettingsView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to change auto lock settings", - biometricsAuthenticationTitle: - "Auto lock settings", - ), + builder: (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: + AutoLockTimeoutSettingsView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to change auto lock settings", + biometricsAuthenticationTitle: "Auto lock settings", + ), settings: const RouteSettings( name: "/autoLockTimeoutSettingsLockScreen", ), diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 09b2bbd97b..2f057de12c 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -81,6 +81,8 @@ class Prefs extends ChangeNotifier { _logsPath = await _getLogsPath(); _logLevel = await _getLogLevel(); _autoLockInfo = await _getAutoLockInfo(); + _privacyScreen = await _getPrivacyScreen(); + _disableScreenShots = await _getDisableScreenShots(); _initialized = true; } @@ -1383,4 +1385,52 @@ class Prefs extends ChangeNotifier { return (enabled: map["enabled"] as bool, minutes: map["minutes"] as int); } + + // mobile screen privacy + bool _privacyScreen = false; + bool get privacyScreen => _privacyScreen; + set privacyScreen(bool privacyScreen) { + if (_privacyScreen != privacyScreen) { + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "privacyScreen", + value: privacyScreen, + ); + _privacyScreen = privacyScreen; + notifyListeners(); + } + } + + Future _getPrivacyScreen() async { + return await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "privacyScreen", + ) + as bool? ?? + false; + } + + // android screen shot protection + bool _disableScreenShots = false; + bool get disableScreenShots => _disableScreenShots; + set disableScreenShots(bool disableScreenShots) { + if (_disableScreenShots != disableScreenShots) { + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "disableScreenShots", + value: disableScreenShots, + ); + _disableScreenShots = disableScreenShots; + notifyListeners(); + } + } + + Future _getDisableScreenShots() async { + return await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "disableScreenShots", + ) + as bool? ?? + false; + } } diff --git a/pubspec.lock b/pubspec.lock index 7ed626d613..9fd1e98b91 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1611,6 +1611,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + mobile_app_privacy: + dependency: "direct main" + description: + path: "." + ref: "v0.0.3" + resolved-ref: a949b6e79aa2c97af9d339690067800a5c5eb89e + url: "https://github.com/cypherstack/mobile_app_privacy" + source: git + version: "0.0.3" mockingjay: dependency: "direct dev" description: @@ -2673,5 +2682,5 @@ packages: source: hosted version: "0.2.4" sdks: - dart: ">=3.9.0 <4.0.0" + dart: ">=3.9.2 <4.0.0" flutter: ">=3.29.0 <4.0.0" diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index 80a1a18a10..2878e1ff8c 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -251,6 +251,10 @@ dependencies: saf_stream: ^0.12.3 unorm_dart: ^0.2.0 qr_code_scanner_plus: ^2.0.14 + mobile_app_privacy: + git: + url: https://github.com/cypherstack/mobile_app_privacy + ref: v0.0.3 dev_dependencies: flutter_test: From e1cb7bf3f2d130e7a7854d2c11a51ab92c02a094 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 12 Nov 2025 10:23:54 -0600 Subject: [PATCH 27/50] update cs_monero --- lib/main.dart | 6 +- lib/services/churning_service.dart | 8 +- .../intermediate/cryptonote_wallet.dart | 8 +- .../intermediate/lib_monero_wallet.dart | 159 ++++++++++-------- .../intermediate/lib_salvium_wallet.dart | 12 +- .../intermediate/lib_wownero_wallet.dart | 12 +- lib/widgets/tx_key_widget.dart | 2 +- .../interfaces/cs_monero_interface.dart | 35 ++-- pubspec.lock | 40 ++--- .../templates/pubspec.template.yaml | 4 +- ...XMR_cs_monero_interface_impl.template.dart | 69 +++++--- 11 files changed, 204 insertions(+), 151 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index b0d58afbc7..dd35ae7b51 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -631,8 +631,10 @@ class _MaterialAppWithThemeState extends ConsumerState @override Future didRequestAppExit() async { debugPrint("didRequestAppExit called"); - if (Platform.isMacOS || Platform.isWindows) { - // Monero will cause issues if in the middle of syncing. + if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { + // Monero will cause app to stop responding if in the middle of doing + // things like a scan on the c++ side of things. + // On macOS, mwebd fails to shut down, hanging the app on close. // // Exiting is a hack fix for this issue. diff --git a/lib/services/churning_service.dart b/lib/services/churning_service.dart index a7f08197c9..d0699871f5 100644 --- a/lib/services/churning_service.dart +++ b/lib/services/churning_service.dart @@ -32,9 +32,9 @@ class ChurningService extends ChangeNotifier { bool done = false; Object? lastSeenError; - bool _canChurn() { + Future _canChurn() async { if (wallet.wallet != null && - wallet.internalGetUnlockedBalance(accountIndex: kAccount)! > + await wallet.internalGetUnlockedBalance(accountIndex: kAccount) > BigInt.zero) { return true; } else { @@ -121,7 +121,7 @@ class ChurningService extends ChangeNotifier { bool complete() => !continuous && roundsCompleted >= roundsToDo; while (!complete() && _running) { - if (_canChurn()) { + if (await _canChurn()) { waitingForUnlockedBalance = ChurnStatus.success; makingChurnTransaction = ChurnStatus.running; notifyListeners(); @@ -186,7 +186,7 @@ class ChurningService extends ChangeNotifier { } Future _churnTxSimple() async { - final address = wallet.internalGetAddress( + final address = await wallet.internalGetAddress( accountIndex: kAccount, addressIndex: 0, ); diff --git a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart index 6023434270..62a08ce3a1 100644 --- a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart +++ b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart @@ -26,21 +26,21 @@ abstract class CryptonoteWallet Future getKeys(); - String getTxKeyFor({required String txid}); + Future getTxKeyFor({required String txid}); Future<(String, String)> hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing(); void setRefreshFromBlockHeight(int newHeight); - int getRefreshFromBlockHeight(); + Future getRefreshFromBlockHeight(); - String internalGetAddress({ + Future internalGetAddress({ required int accountIndex, required int addressIndex, }); - BigInt? internalGetUnlockedBalance({int accountIndex = 0}); + Future internalGetUnlockedBalance({int accountIndex = 0}); Future> internalGetOutputs({ bool refresh = false, bool includeSpent = false, diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index b1edd82be8..6c0c49884e 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -168,7 +168,7 @@ abstract class LibMoneroWallet bool walletExists(String path); @override - String getTxKeyFor({required String txid}) { + Future getTxKeyFor({required String txid}) { if (wallet == null) { throw Exception("Cannot get tx key in uninitialized libMoneroWallet"); } @@ -218,7 +218,7 @@ abstract class LibMoneroWallet Address? currentAddress = await getCurrentReceivingAddress(); if (currentAddress == null) { - currentAddress = addressFor(index: 0); + currentAddress = await addressFor(index: 0); await mainDB.updateOrPutAddresses([currentAddress]); } if (info.cachedReceivingAddress != currentAddress.value) { @@ -231,14 +231,14 @@ abstract class LibMoneroWallet if (wasNull) { try { _setSyncStatus(lib_monero_compat.ConnectingSyncStatus()); - csMonero.startSyncing(wallet!); + await csMonero.startSyncing(wallet!); } catch (_) { _setSyncStatus(lib_monero_compat.FailedSyncStatus()); // TODO log } } _setListener(); - csMonero.startListeners(wallet!); + await csMonero.startListeners(wallet!); csMonero.startAutoSaving(wallet!); unawaited(refresh()); @@ -267,8 +267,8 @@ abstract class LibMoneroWallet await csMonero.save(wallet!); } - Address addressFor({required int index, int account = 0}) { - final address = csMonero.getAddress( + Future
addressFor({required int index, int account = 0}) async { + final address = await csMonero.getAddress( wallet!, accountIndex: account, addressIndex: index, @@ -300,10 +300,10 @@ abstract class LibMoneroWallet try { return CWKeyData( walletId: walletId, - publicViewKey: csMonero.getPublicViewKey(wallet!), - privateViewKey: csMonero.getPrivateViewKey(wallet!), - publicSpendKey: csMonero.getPublicSpendKey(wallet!), - privateSpendKey: csMonero.getPrivateSpendKey(wallet!), + publicViewKey: await csMonero.getPublicViewKey(wallet!), + privateViewKey: await csMonero.getPrivateViewKey(wallet!), + publicSpendKey: await csMonero.getPublicSpendKey(wallet!), + privateSpendKey: await csMonero.getPrivateSpendKey(wallet!), ); } catch (e, s) { Logging.instance.f("getKeys failed: ", error: e, stackTrace: s); @@ -330,7 +330,10 @@ abstract class LibMoneroWallet throw Exception("Password not found $e, $s"); } wallet = await loadWallet(path: path, password: password); - return (csMonero.getAddress(wallet!), csMonero.getPrivateViewKey(wallet!)); + return ( + await csMonero.getAddress(wallet!), + await csMonero.getPrivateViewKey(wallet!), + ); } @override @@ -355,7 +358,7 @@ abstract class LibMoneroWallet ); await info.updateRestoreHeight( - newRestoreHeight: csMonero.getRefreshFromBlockHeight(wallet), + newRestoreHeight: await csMonero.getRefreshFromBlockHeight(wallet), isar: mainDB.isar, ); @@ -363,7 +366,7 @@ abstract class LibMoneroWallet // before wallet.init() is called await secureStorageInterface.write( key: Wallet.mnemonicKey(walletId: walletId), - value: csMonero.getSeed(wallet), + value: await csMonero.getSeed(wallet), ); await secureStorageInterface.write( key: Wallet.mnemonicPassphraseKey(walletId: walletId), @@ -390,9 +393,9 @@ abstract class LibMoneroWallet await mainDB.deleteWalletBlockchainData(walletId); highestPercentCached = 0; - unawaited(csMonero.rescanBlockchain(wallet!)); - csMonero.startSyncing(wallet!); - // unawaited(save()); + await csMonero.rescanBlockchain(wallet!); + await csMonero.startSyncing(wallet!); + unawaited(save()); }); unawaited(refresh()); return; @@ -451,7 +454,7 @@ abstract class LibMoneroWallet walletId: walletId, derivationIndex: 0, derivationPath: null, - value: csMonero.getAddress(this.wallet!), + value: await csMonero.getAddress(this.wallet!), publicKey: [], type: AddressType.cryptonote, subType: AddressSubType.receiving, @@ -470,11 +473,11 @@ abstract class LibMoneroWallet _setListener(); // libMoneroWallet?.setRecoveringFromSeed(isRecovery: true); - unawaited(csMonero.rescanBlockchain(wallet!)); - csMonero.startSyncing(wallet!); + await csMonero.rescanBlockchain(wallet!); + await csMonero.startSyncing(wallet!); // await save(); - csMonero.startListeners(wallet!); + await csMonero.startListeners(wallet!); csMonero.startAutoSaving(wallet!); } catch (e, s) { Logging.instance.e( @@ -503,7 +506,7 @@ abstract class LibMoneroWallet Future updateNode() async { final node = getCurrentNode(); - if (_torNodeMismatchGuard(node)) { + if (await _torNodeMismatchGuard(node)) { throw Exception("TOR – clearnet mismatch"); } @@ -548,8 +551,8 @@ abstract class LibMoneroWallet : "${proxy.host.address}:${proxy.port}", ); } - csMonero.startSyncing(wallet!); - csMonero.startListeners(wallet!); + await csMonero.startSyncing(wallet!); + await csMonero.startListeners(wallet!); csMonero.startAutoSaving(wallet!); _setSyncStatus(lib_monero_compat.ConnectedSyncStatus()); @@ -705,7 +708,7 @@ abstract class LibMoneroWallet Future get availableBalance async { try { return Amount( - rawValue: csMonero.getUnlockedBalance(wallet!)!, + rawValue: await csMonero.getUnlockedBalance(wallet!), fractionDigits: cryptoCurrency.fractionDigits, ); } catch (_) { @@ -715,28 +718,12 @@ abstract class LibMoneroWallet Future get totalBalance async { try { - final full = csMonero.getBalance(wallet!); - if (full != null) { - return Amount( - rawValue: full, - fractionDigits: cryptoCurrency.fractionDigits, - ); - } else { - final transactions = await csMonero.getAllTxs(wallet!, refresh: true); - BigInt transactionBalance = BigInt.zero; - for (final tx in transactions) { - if (!tx.isSpend) { - transactionBalance += tx.amount; - } else { - transactionBalance += -tx.amount - tx.fee; - } - } + final full = await csMonero.getBalance(wallet!); - return Amount( - rawValue: transactionBalance, - fractionDigits: cryptoCurrency.fractionDigits, - ); - } + return Amount( + rawValue: full, + fractionDigits: cryptoCurrency.fractionDigits, + ); } catch (_) { return info.cachedBalance.total; } @@ -744,13 +731,14 @@ abstract class LibMoneroWallet @override Future exit() async { - Logging.instance.i("exit called on $wallet!"); + Logging.instance.i("exit called on monero $walletId!"); if (wallet != null) { csMonero.stopAutoSaving(wallet!); - csMonero.stopListeners(wallet!); - csMonero.stopSyncing(wallet!); + await csMonero.stopListeners(wallet!); + await csMonero.stopSyncing(wallet!); await csMonero.save(wallet!); } + Logging.instance.i("exit call completed monero $walletId!"); } Future pathForWalletDir({ @@ -1011,7 +999,7 @@ abstract class LibMoneroWallet } } - bool _torNodeMismatchGuard(NodeModel node) { + Future _torNodeMismatchGuard(NodeModel node) async { _canPing = true; // Reset. final bool mismatch = @@ -1022,8 +1010,8 @@ abstract class LibMoneroWallet _canPing = false; if (wallet != null) { csMonero.stopAutoSaving(wallet!); - csMonero.stopListeners(wallet!); - csMonero.stopSyncing(wallet!); + await csMonero.stopListeners(wallet!); + await csMonero.stopSyncing(wallet!); } _setSyncStatus(lib_monero_compat.FailedSyncStatus()); } @@ -1120,36 +1108,75 @@ abstract class LibMoneroWallet // Awaiting this lock could be dangerous. // Since refresh is periodic (generally) if (refreshMutex.isLocked) { + Logging.instance.t( + "$runtimeType refresh() refreshMutex.isLocked=true, returning...", + ); return; } + // this acquire should be almost instant due to above check. + // Slight possibility of race but should be irrelevant + Logging.instance.t( + "$runtimeType refresh() refreshMutex.acquire() waiting...", + ); + await refreshMutex.acquire(); + Logging.instance.t( + "$runtimeType refresh() refreshMutex.acquire() acquired!", + ); + + Logging.instance.t("$runtimeType refresh() final node = getCurrentNode();"); final node = getCurrentNode(); - if (_torNodeMismatchGuard(node)) { + Logging.instance.i( + "$runtimeType refresh() await _torNodeMismatchGuard(node)", + ); + if (await _torNodeMismatchGuard(node)) { throw Exception("TOR – clearnet mismatch"); } - // this acquire should be almost instant due to above check. - // Slight possibility of race but should be irrelevant - await refreshMutex.acquire(); - - csMonero.startSyncing(wallet!); + Logging.instance.t( + "$runtimeType refresh() it csMonero.startSyncing(wallet!);", + ); + await csMonero.startSyncing(wallet!); + Logging.instance.t( + "$runtimeType refresh() _setSyncStatus(lib_monero_compat.StartingSyncStatus());", + ); _setSyncStatus(lib_monero_compat.StartingSyncStatus()); + Logging.instance.t("$runtimeType refresh() await updateTransactions();"); await updateTransactions(); + Logging.instance.t("$runtimeType refresh() await updateBalance();"); await updateBalance(); + Logging.instance.t( + "$runtimeType refresh() await checkReceivingAddressForTransactions();", + ); if (info.otherData[WalletInfoKeys.reuseAddress] != true) { await checkReceivingAddressForTransactions(); } + Logging.instance.t( + "$runtimeType refresh() refreshMutex.isLocked=${refreshMutex.isLocked} pre release.", + ); if (refreshMutex.isLocked) { refreshMutex.release(); + Logging.instance.t( + "$runtimeType refresh() refreshMutex.isLocked manually released.", + ); } + Logging.instance.t( + "$runtimeType refresh() wallet != null && await csMonero.isSynced(wallet!)", + ); final synced = wallet != null && await csMonero.isSynced(wallet!); + Logging.instance.t( + "$runtimeType refresh() wallet != null && await csMonero.isSynced(wallet!) == $synced", + ); if (synced) { + Logging.instance.t( + "$runtimeType refresh() _setSyncStatus(lib_monero_compat.SyncedSyncStatus());", + ); _setSyncStatus(lib_monero_compat.SyncedSyncStatus()); } } @@ -1163,7 +1190,7 @@ abstract class LibMoneroWallet ? 0 : currentReceiving.derivationIndex + 1; - final newReceivingAddress = addressFor(index: newReceivingIndex); + final newReceivingAddress = await addressFor(index: newReceivingIndex); // Add that new receiving address await mainDB.putAddress(newReceivingAddress); @@ -1217,7 +1244,7 @@ abstract class LibMoneroWallet final newReceivingIndex = curIndex + 1; // Use new index to derive a new receiving address - final newReceivingAddress = addressFor(index: newReceivingIndex); + final newReceivingAddress = await addressFor(index: newReceivingIndex); final existing = await mainDB .getAddresses(walletId) @@ -1421,7 +1448,7 @@ abstract class LibMoneroWallet } @override - int getRefreshFromBlockHeight() => wallet == null + Future getRefreshFromBlockHeight() => wallet == null ? throw Exception( "Cannot getRefreshFromBlockHeight when wallet is not open", ) @@ -1471,7 +1498,7 @@ abstract class LibMoneroWallet } @override - String internalGetAddress({ + Future internalGetAddress({ required int accountIndex, required int addressIndex, }) { @@ -1501,7 +1528,7 @@ abstract class LibMoneroWallet } @override - BigInt? internalGetUnlockedBalance({int accountIndex = 0}) { + Future internalGetUnlockedBalance({int accountIndex = 0}) { if (wallet == null) { throw Exception("Cannot internalCommitTx when wallet is not open"); } @@ -1563,7 +1590,7 @@ abstract class LibMoneroWallet walletId: walletId, derivationIndex: 0, derivationPath: null, - value: csMonero.getAddress(this.wallet!), + value: await csMonero.getAddress(this.wallet!), publicKey: [], type: AddressType.cryptonote, subType: AddressSubType.receiving, @@ -1578,11 +1605,11 @@ abstract class LibMoneroWallet await updateNode(); _setListener(); - unawaited(csMonero.rescanBlockchain(this.wallet!)); - csMonero.startSyncing(this.wallet!); + await csMonero.rescanBlockchain(this.wallet!); + await csMonero.startSyncing(this.wallet!); // await save(); - csMonero.startListeners(this.wallet!); + await csMonero.startListeners(this.wallet!); csMonero.startAutoSaving(this.wallet!); } catch (e, s) { Logging.instance.e( diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index cd81916e7f..4517f14985 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -164,7 +164,7 @@ abstract class LibSalviumWallet bool walletExists(String path); @override - String getTxKeyFor({required String txid}) { + Future getTxKeyFor({required String txid}) async { if (wallet == null) { throw Exception("Cannot get tx key in uninitialized libSalviumWallet"); } @@ -1414,7 +1414,7 @@ abstract class LibSalviumWallet } @override - int getRefreshFromBlockHeight() => wallet == null + Future getRefreshFromBlockHeight() async => wallet == null ? throw Exception( "Cannot getRefreshFromBlockHeight when wallet is not open", ) @@ -1464,10 +1464,10 @@ abstract class LibSalviumWallet } @override - String internalGetAddress({ + Future internalGetAddress({ required int accountIndex, required int addressIndex, - }) { + }) async { if (wallet == null) { throw Exception("Cannot internalCommitTx when wallet is not open"); } @@ -1494,11 +1494,11 @@ abstract class LibSalviumWallet } @override - BigInt? internalGetUnlockedBalance({int accountIndex = 0}) { + Future internalGetUnlockedBalance({int accountIndex = 0}) async { if (wallet == null) { throw Exception("Cannot internalCommitTx when wallet is not open"); } - return csSalvium.getUnlockedBalance(wallet!, accountIndex: accountIndex); + return csSalvium.getUnlockedBalance(wallet!, accountIndex: accountIndex)!; } @override diff --git a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart index 0dcdaecaac..5ebd2191a3 100644 --- a/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_wownero_wallet.dart @@ -170,7 +170,7 @@ abstract class LibWowneroWallet bool walletExists(String path); @override - String getTxKeyFor({required String txid}) { + Future getTxKeyFor({required String txid}) async { if (wallet == null) { throw Exception("Cannot get tx key in uninitialized LibWowneroWallet"); } @@ -1426,7 +1426,7 @@ abstract class LibWowneroWallet } @override - int getRefreshFromBlockHeight() => wallet == null + Future getRefreshFromBlockHeight() async => wallet == null ? throw Exception( "Cannot getRefreshFromBlockHeight when wallet is not open", ) @@ -1476,10 +1476,10 @@ abstract class LibWowneroWallet } @override - String internalGetAddress({ + Future internalGetAddress({ required int accountIndex, required int addressIndex, - }) { + }) async { if (wallet == null) { throw Exception("Cannot internalCommitTx when wallet is not open"); } @@ -1506,11 +1506,11 @@ abstract class LibWowneroWallet } @override - BigInt? internalGetUnlockedBalance({int accountIndex = 0}) { + Future internalGetUnlockedBalance({int accountIndex = 0}) async { if (wallet == null) { throw Exception("Cannot internalCommitTx when wallet is not open"); } - return csWownero.getUnlockedBalance(wallet!, accountIndex: accountIndex); + return csWownero.getUnlockedBalance(wallet!, accountIndex: accountIndex)!; } @override diff --git a/lib/widgets/tx_key_widget.dart b/lib/widgets/tx_key_widget.dart index 8222fdbabd..6872bbd43a 100644 --- a/lib/widgets/tx_key_widget.dart +++ b/lib/widgets/tx_key_widget.dart @@ -53,7 +53,7 @@ class _TxKeyWidgetState extends ConsumerState { final wallet = ref.read(pWallets).getWallet(widget.walletId) as CryptonoteWallet; - _private = wallet.getTxKeyFor(txid: widget.txid); + _private = await wallet.getTxKeyFor(txid: widget.txid); if (_private!.isEmpty) { _private = "Unavailable"; } diff --git a/lib/wl_gen/interfaces/cs_monero_interface.dart b/lib/wl_gen/interfaces/cs_monero_interface.dart index 4541c958f6..f9f30d5c83 100644 --- a/lib/wl_gen/interfaces/cs_monero_interface.dart +++ b/lib/wl_gen/interfaces/cs_monero_interface.dart @@ -27,7 +27,7 @@ abstract class CsMoneroInterface { required String password, }); - String getAddress( + Future getAddress( WrappedWallet wallet, { int accountIndex = 0, int addressIndex = 0, @@ -58,32 +58,32 @@ abstract class CsMoneroInterface { int height = 0, }); - String getTxKey(WrappedWallet wallet, String txid); + Future getTxKey(WrappedWallet wallet, String txid); Future save(WrappedWallet wallet); - String getPublicViewKey(WrappedWallet wallet); - String getPrivateViewKey(WrappedWallet wallet); - String getPublicSpendKey(WrappedWallet wallet); - String getPrivateSpendKey(WrappedWallet wallet); + Future getPublicViewKey(WrappedWallet wallet); + Future getPrivateViewKey(WrappedWallet wallet); + Future getPublicSpendKey(WrappedWallet wallet); + Future getPrivateSpendKey(WrappedWallet wallet); Future isSynced(WrappedWallet wallet); - void startSyncing(WrappedWallet wallet); - void stopSyncing(WrappedWallet wallet); + Future startSyncing(WrappedWallet wallet); + Future stopSyncing(WrappedWallet wallet); void startAutoSaving(WrappedWallet wallet); void stopAutoSaving(WrappedWallet wallet); bool hasListeners(WrappedWallet wallet); void addListener(WrappedWallet wallet, CsWalletListener listener); - void startListeners(WrappedWallet wallet); - void stopListeners(WrappedWallet wallet); + Future startListeners(WrappedWallet wallet); + Future stopListeners(WrappedWallet wallet); - Future rescanBlockchain(WrappedWallet wallet); + Future rescanBlockchain(WrappedWallet wallet); Future isConnectedToDaemon(WrappedWallet wallet); - int getRefreshFromBlockHeight(WrappedWallet wallet); - void setRefreshFromBlockHeight(WrappedWallet wallet, int height); + Future getRefreshFromBlockHeight(WrappedWallet wallet); + Future setRefreshFromBlockHeight(WrappedWallet wallet, int height); Future connect( WrappedWallet wallet, { @@ -101,8 +101,11 @@ abstract class CsMoneroInterface { bool refresh = false, }); - BigInt? getBalance(WrappedWallet wallet, {int accountIndex = 0}); - BigInt? getUnlockedBalance(WrappedWallet wallet, {int accountIndex = 0}); + Future getBalance(WrappedWallet wallet, {int accountIndex = 0}); + Future getUnlockedBalance( + WrappedWallet wallet, { + int accountIndex = 0, + }); Future> getAllTxs( WrappedWallet wallet, { @@ -154,7 +157,7 @@ abstract class CsMoneroInterface { bool validateAddress(String address, int network); - String getSeed(WrappedWallet wallet); + Future getSeed(WrappedWallet wallet); Future close(WrappedWallet wallet, {bool save = false}); } diff --git a/pubspec.lock b/pubspec.lock index 9fd1e98b91..95970311f0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -439,74 +439,74 @@ packages: dependency: "direct main" description: name: cs_monero - sha256: "2bc89f862b4a4bc5312999a35d266db035d2e8760736662e148f07d2ab36e43d" + sha256: "6370649167f46ead5cffac46d164dd749cb4b07989e9f0e08fe081c74c4e6b61" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.1.0" cs_monero_flutter_libs: dependency: "direct main" description: name: cs_monero_flutter_libs - sha256: "759272ed87908572c0b7bb47edae2c49744bb981a632fc6156526a33a4d17f74" + sha256: "459542acbfc01ee6f30446c656cba670c7f1b90e52b7921a4aa0dcbc275b9eca" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" cs_monero_flutter_libs_android: dependency: transitive description: name: cs_monero_flutter_libs_android - sha256: eafe9b72370f92135e94ba8f25acf8776300d0442e0fdedc31c0ce715aa80daa + sha256: f0785f34bcf9872347823303f09409b1238b2ed7e535b9722633b0022d6188f5 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" cs_monero_flutter_libs_android_arm64_v8a: dependency: transitive description: name: cs_monero_flutter_libs_android_arm64_v8a - sha256: d754cc1effdefdf8d1bf016fe69288f5a9cdde83269b5c153d4ac191dd18fa30 + sha256: "0b836dff1ead29229535a3228c7c57517127bea8b19c4c2d9bdae2770526f8ca" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" cs_monero_flutter_libs_android_armeabi_v7a: dependency: transitive description: name: cs_monero_flutter_libs_android_armeabi_v7a - sha256: "21c00dcbd506a737750dd228a36395b3f6a9b142ecc9032ee60a746fca80d731" + sha256: "7955bbf91e1c3ec66e352a33e36edbab509808db6db6debfbea06f1ad2396205" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" cs_monero_flutter_libs_android_x86_64: dependency: transitive description: name: cs_monero_flutter_libs_android_x86_64 - sha256: d13fb62be52f44d0fa3aedeaabc33809284c4452728f4dfb9b4147038cae6fa2 + sha256: f51f95aa4a09be497befe020621b0d62d749d900f4dfd585fe60b7c9692010a8 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" cs_monero_flutter_libs_ios: dependency: transitive description: name: cs_monero_flutter_libs_ios - sha256: "04673ca9a46f77ad0493f9dcd1cfe8e93ceacb78a2c46a0e733d0c6925231552" + sha256: dbc149c0787a7702a3842b4974b9bc30bad654daaa57886f874823c29c390ba7 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" cs_monero_flutter_libs_linux: dependency: transitive description: name: cs_monero_flutter_libs_linux - sha256: "4390b77d529cae362ed14ba4b5906c4e0f9bb4da5f3c8a0963f27c3e9c3197ff" + sha256: "5b8bbc68a7d2bb39efdea4834097ada1aa99fd7e0b1641943c4e06c89f96616e" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" cs_monero_flutter_libs_macos: dependency: transitive description: name: cs_monero_flutter_libs_macos - sha256: "1cf88d9d04f327b577d245b579b15e75ad4c43ab7f1996bd369ddc0fa84aec53" + sha256: ee02b78184b4168bc2bdb49c7ef71cc5019ffbed54c0feabcebdbc4cae5819ee url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" cs_monero_flutter_libs_platform_interface: dependency: transitive description: @@ -519,10 +519,10 @@ packages: dependency: transitive description: name: cs_monero_flutter_libs_windows - sha256: "3be0fe15bdcb6d1619b70d8dde52eaa02fdd028bd3cc522a4c472742f72a09a7" + sha256: "9db54230f83ec07e2dce39b6b90711616ba4ab1144c7f68e4b1c13b161a18cd3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" cs_salvium: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index 2878e1ff8c..48ca00e40c 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -61,8 +61,8 @@ dependencies: # %%END_ENABLE_TOR%% # %%ENABLE_XMR%% -# cs_monero: 2.0.0 -# cs_monero_flutter_libs: 2.0.0 +# cs_monero: 3.1.0 +# cs_monero_flutter_libs: 2.0.1 # %%END_ENABLE_XMR%% # %%ENABLE_WOW%% diff --git a/tool/wl_templates/XMR_cs_monero_interface_impl.template.dart b/tool/wl_templates/XMR_cs_monero_interface_impl.template.dart index 0d4d728410..b957ad36dd 100644 --- a/tool/wl_templates/XMR_cs_monero_interface_impl.template.dart +++ b/tool/wl_templates/XMR_cs_monero_interface_impl.template.dart @@ -66,9 +66,16 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { String walletId, { required String path, required String password, + int network = 0, // default to mainnet }) async { return WrappedWallet( - await lib_monero.MoneroWallet.loadWallet(path: path, password: password), + await lib_monero.MoneroWallet.loadWallet( + path: path, + password: password, + networkType: lib_monero.Network.values.firstWhere( + (e) => e.value == network, + ), + ), ); } @@ -82,14 +89,14 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { int getTxPriorityNormal() => lib_monero.TransactionPriority.normal.value; @override - String getAddress( + Future getAddress( WrappedWallet wallet, { int accountIndex = 0, int addressIndex = 0, - }) => wallet - .get() - .getAddress(accountIndex: accountIndex, addressIndex: addressIndex) - .value; + }) async => (await wallet.get().getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + )).value; @override Future getCreatedWallet({ @@ -97,6 +104,7 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { required String password, required int wordCount, required String seedOffset, + int network = 0, // default to mainnet }) async { final type = switch (wordCount) { 16 => lib_monero.MoneroSeedType.sixteen, @@ -109,6 +117,9 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { password: password, seedType: type, seedOffset: seedOffset, + networkType: lib_monero.Network.values.firstWhere( + (e) => e.value == network, + ), ); return WrappedWallet(wallet); @@ -121,6 +132,7 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { required String password, required String mnemonic, required String seedOffset, + int network = 0, // default to mainnet int height = 0, }) async { return WrappedWallet( @@ -130,6 +142,9 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { seed: mnemonic, restoreHeight: height, seedOffset: seedOffset, + networkType: lib_monero.Network.values.firstWhere( + (e) => e.value == network, + ), ), ); } @@ -141,6 +156,7 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { required String password, required String address, required String privateViewKey, + int network = 0, // default to mainnet int height = 0, }) async { return WrappedWallet( @@ -150,12 +166,15 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { address: address, viewKey: privateViewKey, restoreHeight: height, + networkType: lib_monero.Network.values.firstWhere( + (e) => e.value == network, + ), ), ); } @override - String getTxKey(WrappedWallet wallet, String txid) => + Future getTxKey(WrappedWallet wallet, String txid) => wallet.get().getTxKey(txid); @override @@ -163,19 +182,19 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { wallet.get().save(); @override - String getPublicViewKey(WrappedWallet wallet) => + Future getPublicViewKey(WrappedWallet wallet) => wallet.get().getPublicViewKey(); @override - String getPrivateViewKey(WrappedWallet wallet) => + Future getPrivateViewKey(WrappedWallet wallet) => wallet.get().getPrivateViewKey(); @override - String getPublicSpendKey(WrappedWallet wallet) => + Future getPublicSpendKey(WrappedWallet wallet) => wallet.get().getPublicSpendKey(); @override - String getPrivateSpendKey(WrappedWallet wallet) => + Future getPrivateSpendKey(WrappedWallet wallet) => wallet.get().getPrivateSpendKey(); @override @@ -183,11 +202,11 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { wallet.get().isSynced(); @override - void startSyncing(WrappedWallet wallet) => + Future startSyncing(WrappedWallet wallet) => wallet.get().startSyncing(); @override - void stopSyncing(WrappedWallet wallet) => + Future stopSyncing(WrappedWallet wallet) => wallet.get().stopSyncing(); @override @@ -214,23 +233,23 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { ); @override - void startListeners(WrappedWallet wallet) => + Future startListeners(WrappedWallet wallet) => wallet.get().startListeners(); @override - void stopListeners(WrappedWallet wallet) => + Future stopListeners(WrappedWallet wallet) => wallet.get().stopListeners(); @override - int getRefreshFromBlockHeight(WrappedWallet wallet) => + Future getRefreshFromBlockHeight(WrappedWallet wallet) => wallet.get().getRefreshFromBlockHeight(); @override - void setRefreshFromBlockHeight(WrappedWallet wallet, int height) => + Future setRefreshFromBlockHeight(WrappedWallet wallet, int height) => wallet.get().setRefreshFromBlockHeight(height); @override - Future rescanBlockchain(WrappedWallet wallet) => + Future rescanBlockchain(WrappedWallet wallet) => wallet.get().rescanBlockchain(); @override @@ -266,14 +285,16 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { }) => wallet.get().getAllTxids(refresh: refresh); @override - BigInt? getBalance(WrappedWallet wallet, {int accountIndex = 0}) => + Future getBalance(WrappedWallet wallet, {int accountIndex = 0}) => wallet.get().getBalance(accountIndex: accountIndex); @override - BigInt? getUnlockedBalance(WrappedWallet wallet, {int accountIndex = 0}) => - wallet.get().getUnlockedBalance( - accountIndex: accountIndex, - ); + Future getUnlockedBalance( + WrappedWallet wallet, { + int accountIndex = 0, + }) => wallet.get().getUnlockedBalance( + accountIndex: accountIndex, + ); @override Future> getAllTxs( @@ -496,7 +517,7 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { xmr_wallet_ffi.validateAddress(address, network); @override - String getSeed(WrappedWallet wallet) => + Future getSeed(WrappedWallet wallet) => wallet.get().getSeed(); @override From 75865133ffe877c85427526012d622836a24d32f Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 12 Nov 2025 19:36:27 -0600 Subject: [PATCH 28/50] fix spark names not showing as confirmed in names view --- .../wallet_mixin_interfaces/spark_interface.dart | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index d9059a5bb1..96dcb39a3b 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1236,9 +1236,6 @@ mixin SparkInterface Logging.instance.i("Refreshing spark names for $walletId ${info.name}"); final db = Drift.get(walletId); - final myNameStrings = await db.managers.sparkNames - .map((e) => e.name) - .get(); final names = await electrumXClient.getSparkNames(); // start update shared cache of all names @@ -1299,11 +1296,8 @@ mixin SparkInterface diversifier++; } - names.retainWhere( - (e) => - myAddresses.contains(e.address) && !myNameStrings.contains(e.name), - ); - Logging.instance.d("Found $names new spark names"); + names.retainWhere((e) => myAddresses.contains(e.address)); + Logging.instance.d("Found $names spark names"); if (names.isNotEmpty) { final List< From 2dcd2b35cdc3cf82562faf68607cc2465d1db9d7 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Nov 2025 09:33:50 -0600 Subject: [PATCH 29/50] SWB tweaks/fixes --- .../helpers/restore_create_backup.dart | 45 ++-- .../sub_widgets/restoring_wallet_card.dart | 251 ++++++++---------- lib/utilities/fs.dart | 24 +- 3 files changed, 155 insertions(+), 165 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 4a9cfd3d7d..5f93a9dea7 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -53,8 +53,6 @@ import '../../../../../wallets/isar/models/wallet_info.dart'; import '../../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart'; import '../../../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; -import '../../../../../wallets/wallet/impl/monero_wallet.dart'; -import '../../../../../wallets/wallet/impl/wownero_wallet.dart'; import '../../../../../wallets/wallet/impl/xelis_wallet.dart'; import '../../../../../wallets/wallet/intermediate/cryptonote_wallet.dart'; import '../../../../../wallets/wallet/wallet.dart'; @@ -414,6 +412,7 @@ abstract class SWB { mnemonicPassphrase: mnemonicPassphrase, ); Wallet? wallet; + bool didExit = false; try { String? serializedKeys; String? multisigConfig; @@ -458,25 +457,21 @@ abstract class SWB { viewOnlyData: viewOnlyData, ); - switch (wallet.runtimeType) { - case const (EpiccashWallet): - await (wallet as EpiccashWallet).init(isRestore: true); + switch (wallet) { + case EpiccashWallet(): + await wallet.init(isRestore: true); break; - case const (MimblewimblecoinWallet): - await (wallet as MimblewimblecoinWallet).init(isRestore: true); + case MimblewimblecoinWallet(): + await wallet.init(isRestore: true); break; - case const (MoneroWallet): - await (wallet as MoneroWallet).init(isRestore: true); + case CryptonoteWallet(): + await wallet.init(isRestore: true); break; - case const (WowneroWallet): - await (wallet as WowneroWallet).init(isRestore: true); - break; - - case const (XelisWallet): - await (wallet as XelisWallet).init(isRestore: true); + case XelisWallet(): + await wallet.init(isRestore: true); break; default: @@ -556,11 +551,14 @@ abstract class SWB { await restoringFuture; + final currentAddress = await wallet.getCurrentReceivingAddress(); + + await wallet.exit(); + didExit = true; + Logging.instance.i( "SWB restored: ${info.walletId} ${info.name} ${info.coin.prettyName}", ); - - final currentAddress = await wallet.getCurrentReceivingAddress(); uiState?.update( walletId: info.walletId, restoringStatus: StackRestoringStatus.success, @@ -571,7 +569,11 @@ abstract class SWB { mnemonicPassphrase: mnemonicPassphrase, ); } catch (e, s) { - Logging.instance.i("", error: e, stackTrace: s); + Logging.instance.e( + "${wallet?.runtimeType} _asyncRestore failed", + error: e, + stackTrace: s, + ); uiState?.update( walletId: info.walletId, restoringStatus: StackRestoringStatus.failed, @@ -580,7 +582,9 @@ abstract class SWB { ); return false; } finally { - await wallet?.exit(); + if (!didExit) { + await wallet?.exit(); + } } return true; } @@ -1231,7 +1235,8 @@ abstract class SWB { TradeWalletLookup lookup = TradeWalletLookup.fromJson(json); // update walletIds final List walletIds = lookup.walletIds - .map((e) => oldToNewWalletIdMap[e]!) + // fallback to e as that wallet may have been deleted in the past + .map((e) => oldToNewWalletIdMap[e] ?? e) .toList(); lookup = lookup.copyWith(walletIds: walletIds); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart index f38104ccd7..9a792c48de 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart @@ -22,18 +22,20 @@ import '../../../../../themes/stack_colors.dart'; import '../../../../../themes/theme_providers.dart'; import '../../../../../utilities/assets.dart'; import '../../../../../utilities/enums/stack_restoring_status.dart'; +import '../../../../../utilities/logger.dart'; import '../../../../../utilities/text_styles.dart'; import '../../../../../utilities/util.dart'; +import '../../../../../wallets/wallet/impl/epiccash_wallet.dart'; +import '../../../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; +import '../../../../../wallets/wallet/impl/xelis_wallet.dart'; +import '../../../../../wallets/wallet/intermediate/cryptonote_wallet.dart'; import '../../../../../widgets/loading_indicator.dart'; import '../../../../../widgets/rounded_container.dart'; import '../sub_views/recovery_phrase_view.dart'; import 'restoring_item_card.dart'; class RestoringWalletCard extends ConsumerStatefulWidget { - const RestoringWalletCard({ - super.key, - required this.provider, - }); + const RestoringWalletCard({super.key, required this.provider}); final ChangeNotifierProvider provider; @@ -45,13 +47,78 @@ class RestoringWalletCard extends ConsumerStatefulWidget { class _RestoringWalletCardState extends ConsumerState { late final ChangeNotifierProvider provider; + Future _retry() async { + final wallet = ref.read(provider).wallet!; + try { + ref + .read(stackRestoringUIStateProvider) + .update( + walletId: wallet.walletId, + restoringStatus: StackRestoringStatus.restoring, + ); + + switch (wallet) { + case EpiccashWallet(): + await wallet.init(isRestore: true); + break; + + case MimblewimblecoinWallet(): + await wallet.init(isRestore: true); + break; + + case CryptonoteWallet(): + await wallet.init(isRestore: true); + await wallet.open(); + break; + + case XelisWallet(): + await wallet.init(isRestore: true); + break; + + default: + await wallet.init(); + } + + await wallet.recover(isRescan: true); + + final address = await wallet.getCurrentReceivingAddress(); + + await wallet.exit(); + + if (mounted) { + ref + .read(stackRestoringUIStateProvider) + .update( + walletId: wallet.walletId, + restoringStatus: StackRestoringStatus.success, + address: address?.value, + ); + } + } catch (e, s) { + Logging.instance.e( + "retry SWB single wallet tapped", + error: e, + stackTrace: s, + ); + if (mounted) { + ref + .read(stackRestoringUIStateProvider) + .update( + walletId: wallet.walletId, + restoringStatus: StackRestoringStatus.failed, + ); + } + } + } + Widget _getIconForState(StackRestoringStatus state) { switch (state) { case StackRestoringStatus.waiting: return SvgPicture.asset( Assets.svg.loader, - color: - Theme.of(context).extension()!.buttonBackSecondary, + color: Theme.of( + context, + ).extension()!.buttonBackSecondary, ); case StackRestoringStatus.restoring: return const LoadingIndicator(); @@ -81,8 +148,9 @@ class _RestoringWalletCardState extends ConsumerState { @override Widget build(BuildContext context) { final coin = ref.watch(provider.select((value) => value.coin)); - final restoringStatus = - ref.watch(provider.select((value) => value.restoringState)); + final restoringStatus = ref.watch( + provider.select((value) => value.restoringState), + ); return !Util.isDesktop ? RestoringItemCard( left: SizedBox( @@ -93,9 +161,7 @@ class _RestoringWalletCardState extends ConsumerState { color: ref.watch(pCoinColor(coin)), child: Center( child: SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), height: 20, width: 20, ), @@ -103,36 +169,7 @@ class _RestoringWalletCardState extends ConsumerState { ), ), onRightTapped: restoringStatus == StackRestoringStatus.failed - ? () async { - final wallet = ref.read(provider).wallet!; - - ref.read(stackRestoringUIStateProvider).update( - walletId: wallet.walletId, - restoringStatus: StackRestoringStatus.restoring, - ); - - try { - await wallet.recover(isRescan: true); - - if (mounted) { - final address = - await wallet.getCurrentReceivingAddress(); - - ref.read(stackRestoringUIStateProvider).update( - walletId: wallet.walletId, - restoringStatus: StackRestoringStatus.success, - address: address!.value, - ); - } - } catch (_) { - if (mounted) { - ref.read(stackRestoringUIStateProvider).update( - walletId: wallet.walletId, - restoringStatus: StackRestoringStatus.failed, - ); - } - } - } + ? _retry : null, right: SizedBox( width: 20, @@ -149,30 +186,27 @@ class _RestoringWalletCardState extends ConsumerState { style: STextStyles.errorSmall(context), ) : ref.watch(provider.select((value) => value.address)) != null - ? Text( - ref.watch(provider.select((value) => value.address))!, - style: STextStyles.infoSmall(context), - ) - : null, + ? Text( + ref.watch(provider.select((value) => value.address))!, + style: STextStyles.infoSmall(context), + ) + : null, button: restoringStatus == StackRestoringStatus.failed ? Container( height: 20, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - borderRadius: BorderRadius.circular( - 1000, - ), + color: Theme.of( + context, + ).extension()!.buttonBackSecondary, + borderRadius: BorderRadius.circular(1000), ), child: RawMaterialButton( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - splashColor: - Theme.of(context).extension()!.highlight, + splashColor: Theme.of( + context, + ).extension()!.highlight, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), + borderRadius: BorderRadius.circular(1000), ), onPressed: () async { final mnemonic = ref.read(provider).mnemonic; @@ -193,9 +227,9 @@ class _RestoringWalletCardState extends ConsumerState { child: Text( "Show recovery phrase", style: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), @@ -216,11 +250,7 @@ class _RestoringWalletCardState extends ConsumerState { color: ref.watch(pCoinColor(coin)), child: Center( child: SvgPicture.file( - File( - ref.watch( - coinIconProvider(coin), - ), - ), + File(ref.watch(coinIconProvider(coin))), height: 20, width: 20, ), @@ -228,60 +258,7 @@ class _RestoringWalletCardState extends ConsumerState { ), ), onRightTapped: restoringStatus == StackRestoringStatus.failed - ? () async { - final wallet = ref.read(provider).wallet!; - - ref.read(stackRestoringUIStateProvider).update( - walletId: wallet.walletId, - restoringStatus: StackRestoringStatus.restoring, - ); - - try { - // final mnemonicList = await manager.mnemonic; - // int maxUnusedAddressGap = 20; - // if (coin is Firo) { - // maxUnusedAddressGap = 50; - // } - // const maxNumberOfIndexesToCheck = 1000; - // - // if (mnemonicList.isEmpty) { - // await manager.recoverFromMnemonic( - // mnemonic: ref.read(provider).mnemonic!, - // mnemonicPassphrase: - // ref.read(provider).mnemonicPassphrase!, - // maxUnusedAddressGap: maxUnusedAddressGap, - // maxNumberOfIndexesToCheck: - // maxNumberOfIndexesToCheck, - // height: ref.read(provider).height ?? 0, - // ); - // } else { - // await manager.fullRescan( - // maxUnusedAddressGap, - // maxNumberOfIndexesToCheck, - // ); - // } - - await wallet.recover(isRescan: true); - - if (mounted) { - final address = - await wallet.getCurrentReceivingAddress(); - - ref.read(stackRestoringUIStateProvider).update( - walletId: wallet.walletId, - restoringStatus: StackRestoringStatus.success, - address: address!.value, - ); - } - } catch (_) { - if (mounted) { - ref.read(stackRestoringUIStateProvider).update( - walletId: wallet.walletId, - restoringStatus: StackRestoringStatus.failed, - ); - } - } - } + ? _retry : null, right: SizedBox( width: 20, @@ -298,31 +275,27 @@ class _RestoringWalletCardState extends ConsumerState { style: STextStyles.errorSmall(context), ) : ref.watch(provider.select((value) => value.address)) != null - ? Text( - ref.watch(provider.select((value) => value.address))!, - style: STextStyles.infoSmall(context), - ) - : null, + ? Text( + ref.watch(provider.select((value) => value.address))!, + style: STextStyles.infoSmall(context), + ) + : null, button: restoringStatus == StackRestoringStatus.failed ? Container( height: 20, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - borderRadius: BorderRadius.circular( - 1000, - ), + color: Theme.of( + context, + ).extension()!.buttonBackSecondary, + borderRadius: BorderRadius.circular(1000), ), child: RawMaterialButton( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - splashColor: Theme.of(context) - .extension()! - .highlight, + splashColor: Theme.of( + context, + ).extension()!.highlight, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), + borderRadius: BorderRadius.circular(1000), ), onPressed: () async { final mnemonic = ref.read(provider).mnemonic; @@ -343,9 +316,9 @@ class _RestoringWalletCardState extends ConsumerState { child: Text( "Show recovery phrase", style: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), diff --git a/lib/utilities/fs.dart b/lib/utilities/fs.dart index b1c1b84a35..fc6f087aa3 100644 --- a/lib/utilities/fs.dart +++ b/lib/utilities/fs.dart @@ -2,6 +2,8 @@ import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:path/path.dart'; import 'package:saf_stream/saf_stream.dart'; import 'package:saf_util/saf_util.dart'; @@ -33,14 +35,24 @@ abstract final class FS { String fileName, ) { if (Platform.isAndroid && dirPath.startsWith("content://")) { - return SafStream().writeFileBytes( - dirPath, - fileName, - "txt", - utf8.encode(content), - ); + final token = ServicesBinding.rootIsolateToken!; + return compute(_androidSafWriteComputeWrapper, ( + dirPath: dirPath, + fileName: fileName, + content: content, + isoToken: token, + )); } else { return File(join(dirPath, fileName)).writeAsString(content, flush: true); } } } + +Future _androidSafWriteComputeWrapper( + ({String dirPath, String fileName, String content, RootIsolateToken isoToken}) + args, +) async { + BackgroundIsolateBinaryMessenger.ensureInitialized(args.isoToken); + final bytes = utf8.encode(args.content); + await SafStream().writeFileBytes(args.dirPath, args.fileName, "txt", bytes); +} From 3ca1bfcc231256cf5606bdf7d10d8065420ed1b8 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Nov 2025 09:43:22 -0600 Subject: [PATCH 30/50] fix switch on inherited type --- .../restore_view_only_wallet_view.dart | 122 ++++++++---------- .../restore_wallet_view.dart | 31 ++--- .../verify_recovery_phrase_view.dart | 21 ++- 3 files changed, 74 insertions(+), 100 deletions(-) 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 e0d3871c42..845ed7f58c 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 @@ -26,9 +26,8 @@ import '../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart'; import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; -import '../../../wallets/wallet/impl/monero_wallet.dart'; -import '../../../wallets/wallet/impl/wownero_wallet.dart'; import '../../../wallets/wallet/impl/xelis_wallet.dart'; +import '../../../wallets/wallet/intermediate/cryptonote_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -109,10 +108,9 @@ class _RestoreViewOnlyWalletViewState final ViewOnlyWalletType viewOnlyWalletType; if (widget.coin is Bip39HDCurrency) { - viewOnlyWalletType = - _addressOnly - ? ViewOnlyWalletType.addressOnly - : ViewOnlyWalletType.xPub; + viewOnlyWalletType = _addressOnly + ? ViewOnlyWalletType.addressOnly + : ViewOnlyWalletType.xPub; } else if (widget.coin is CryptonoteCurrency) { viewOnlyWalletType = ViewOnlyWalletType.cryptonote; } else { @@ -216,25 +214,21 @@ class _RestoreViewOnlyWalletViewState ); // TODO: extract interface with isRestore param - switch (wallet.runtimeType) { - case const (EpiccashWallet): - await (wallet as EpiccashWallet).init(isRestore: true); - break; - - case const (MimblewimblecoinWallet): - await (wallet as MimblewimblecoinWallet).init(isRestore: true); + switch (wallet) { + case EpiccashWallet(): + await wallet.init(isRestore: true); break; - case const (MoneroWallet): - await (wallet as MoneroWallet).init(isRestore: true); + case MimblewimblecoinWallet(): + await wallet.init(isRestore: true); break; - case const (WowneroWallet): - await (wallet as WowneroWallet).init(isRestore: true); + case CryptonoteWallet(): + await wallet.init(isRestore: true); break; - case const (XelisWallet): - await (wallet as XelisWallet).init(isRestore: true); + case XelisWallet(): + await wallet.init(isRestore: true); break; default: @@ -316,10 +310,9 @@ class _RestoreViewOnlyWalletViewState viewKeyController = TextEditingController(); if (widget.coin is Bip39HDCurrency) { - _currentDropDownValue = - (widget.coin as Bip39HDCurrency) - .supportedHardenedDerivationPaths - .last; + _currentDropDownValue = (widget.coin as Bip39HDCurrency) + .supportedHardenedDerivationPaths + .last; } } @@ -338,28 +331,27 @@ class _RestoreViewOnlyWalletViewState return MasterScaffold( isDesktop: isDesktop, - appBar: - isDesktop - ? const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), - ) - : AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 50), - ); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), + appBar: isDesktop + ? const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 50), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, ), + ), body: Container( color: Theme.of(context).extension()!.background, child: LayoutBuilder( @@ -384,10 +376,9 @@ class _RestoreViewOnlyWalletViewState SizedBox(height: isDesktop ? 0 : 4), Text( "Enter view only details", - style: - isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), if (isElectrumX) SizedBox(height: isDesktop ? 24 : 16), if (isElectrumX) @@ -398,14 +389,12 @@ class _RestoreViewOnlyWalletViewState key: UniqueKey(), onText: "Extended pub key", offText: "Single address", - onColor: - Theme.of( - context, - ).extension()!.popupBG, - offColor: - Theme.of(context) - .extension()! - .textFieldDefaultBG, + onColor: Theme.of( + context, + ).extension()!.popupBG, + offColor: Theme.of( + context, + ).extension()!.textFieldDefaultBG, isOn: _addressOnly, onValueChanged: (value) { setState(() { @@ -469,10 +458,9 @@ class _RestoreViewOnlyWalletViewState isExpanded: true, buttonStyleData: ButtonStyleData( decoration: BoxDecoration( - color: - Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -485,10 +473,9 @@ class _RestoreViewOnlyWalletViewState Assets.svg.chevronDown, width: 12, height: 6, - color: - Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ), ), @@ -496,10 +483,9 @@ class _RestoreViewOnlyWalletViewState offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: - Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), 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 1627c3cc93..5d101de146 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 @@ -43,10 +43,8 @@ import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; -import '../../../wallets/wallet/impl/monero_wallet.dart'; -import '../../../wallets/wallet/impl/salvium_wallet.dart'; -import '../../../wallets/wallet/impl/wownero_wallet.dart'; import '../../../wallets/wallet/impl/xelis_wallet.dart'; +import '../../../wallets/wallet/intermediate/cryptonote_wallet.dart'; import '../../../wallets/wallet/intermediate/external_wallet.dart'; import '../../../wallets/wallet/supporting/epiccash_wallet_info_extension.dart'; import '../../../wallets/wallet/supporting/mimblewimblecoin_wallet_info_extension.dart'; @@ -343,35 +341,26 @@ class _RestoreWalletViewState extends ConsumerState { ); // TODO: extract interface with isRestore param - switch (wallet.runtimeType) { - case const (EpiccashWallet): - await (wallet as EpiccashWallet).init(isRestore: true); + switch (wallet) { + case EpiccashWallet(): + await wallet.init(isRestore: true); break; - case const (MimblewimblecoinWallet): - await (wallet as MimblewimblecoinWallet).init(isRestore: true); + case MimblewimblecoinWallet(): + await wallet.init(isRestore: true); break; - case const (MoneroWallet): - await (wallet as MoneroWallet).init(isRestore: true); + case CryptonoteWallet(): + await wallet.init(isRestore: true); break; - case const (WowneroWallet): - await (wallet as WowneroWallet).init(isRestore: true); - break; - - case const (SalviumWallet): - await (wallet as SalviumWallet).init(isRestore: true); - break; - - case const (XelisWallet): - await (wallet as XelisWallet).init(isRestore: true); + case XelisWallet(): + await wallet.init(isRestore: true); break; default: await wallet.init(); } - await wallet.recover(isRescan: false); if (wallet is ExternalWallet) { diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index e19eca1415..e57507c011 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -34,8 +34,7 @@ import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; -import '../../../wallets/wallet/impl/monero_wallet.dart'; -import '../../../wallets/wallet/impl/wownero_wallet.dart'; +import '../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../../wallets/wallet/impl/xelis_wallet.dart'; import '../../../wallets/wallet/intermediate/cryptonote_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; @@ -204,21 +203,21 @@ class _VerifyRecoveryPhraseViewState try { // TODO: extract interface with isRestore param - switch (voWallet.runtimeType) { - case const (EpiccashWallet): - await (voWallet as EpiccashWallet).init(isRestore: true); + switch (voWallet) { + case EpiccashWallet(): + await voWallet.init(isRestore: true); break; - case const (MoneroWallet): - await (voWallet as MoneroWallet).init(isRestore: true); + case MimblewimblecoinWallet(): + await voWallet.init(isRestore: true); break; - case const (WowneroWallet): - await (voWallet as WowneroWallet).init(isRestore: true); + case CryptonoteWallet(): + await voWallet.init(isRestore: true); break; - case const (XelisWallet): - await (voWallet as XelisWallet).init(isRestore: true); + case XelisWallet(): + await voWallet.init(isRestore: true); break; default: From 8f7822dbc514ad294349ec55509afe7a5ed81312 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Nov 2025 15:16:57 -0600 Subject: [PATCH 31/50] update min flutter version --- pubspec.lock | 24 +++++++++---------- .../templates/pubspec.template.yaml | 6 ++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 95970311f0..1c8598eded 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1225,10 +1225,10 @@ packages: dependency: "direct main" description: name: google_fonts - sha256: "2776c66b3e97c6cdd58d1bd3281548b074b64f1fd5c8f82391f7456e38849567" + sha256: "517b20870220c48752eafa0ba1a797a092fb22df0d89535fd9991e86ee2cdd9c" url: "https://pub.dev" source: hosted - version: "4.0.5" + version: "6.3.2" google_identity_services_web: dependency: transitive description: @@ -1599,10 +1599,10 @@ packages: dependency: "direct main" description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -2248,26 +2248,26 @@ packages: dependency: transitive description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.11" + version: "0.6.12" tezart: dependency: "direct main" description: @@ -2682,5 +2682,5 @@ packages: source: hosted version: "0.2.4" sdks: - dart: ">=3.9.2 <4.0.0" - flutter: ">=3.29.0 <4.0.0" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.1 <4.0.0" diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index 48ca00e40c..2869ba4cd2 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -14,8 +14,8 @@ description: PLACEHOLDER version: PLACEHOLDER_V+PLACEHOLDER_B environment: - sdk: ">=3.9.0 <4.0.0" - flutter: ^3.29.0 + sdk: ">=3.10.0 <4.0.0" + flutter: ^3.38.1 dependencies: flutter: @@ -149,7 +149,7 @@ dependencies: # UI/Component plugins flutter_native_splash: ^2.2.4 - google_fonts: ^4.0.4 + google_fonts: ^6.3.2 url_launcher: ^6.0.5 flutter_svg: ^2.0.7 flutter_feather_icons: ^2.0.0+1 From f2b046353308039ffe358140fb58bb12ef2a6cc8 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 14 Nov 2025 10:39:58 -0600 Subject: [PATCH 32/50] update android build --- android/app/proguard-rules.pro | 7 ++++++- android/gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 2 +- scripts/app_config/templates/android/app/build.gradle | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 6a7964eae8..e1c48c38c0 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -32,4 +32,9 @@ -keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken # required for flutter file_picker --keep class androidx.lifecycle.DefaultLifecycleObserver \ No newline at end of file +-keep class androidx.lifecycle.DefaultLifecycleObserver + +# required for flutter_secure_storage +-dontwarn com.google.errorprone.annotations.** +-dontwarn javax.annotation.Nullable +-dontwarn javax.annotation.concurrent.GuardedBy diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index afa1e8eb0a..e4ef43fb98 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 04c37e5f2b..ebf08564f2 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.7.0' apply false + id "com.android.application" version '8.11.1' apply false id "org.jetbrains.kotlin.android" version "2.2.20" apply false } diff --git a/scripts/app_config/templates/android/app/build.gradle b/scripts/app_config/templates/android/app/build.gradle index dc1cb233a9..6a98be40fc 100644 --- a/scripts/app_config/templates/android/app/build.gradle +++ b/scripts/app_config/templates/android/app/build.gradle @@ -15,7 +15,7 @@ android { namespace "com.place.holder" compileSdk flutter.compileSdkVersion // ndkVersion flutter.ndkVersion - ndkVersion = "28.0.13004108" + ndkVersion = "28.2.13676358" packagingOptions { pickFirst 'lib/x86/libc++_shared.so' From e593452ff76fdf5491dc8bb1f46525eef7d66daa Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 14 Nov 2025 13:11:30 -0600 Subject: [PATCH 33/50] remove unused dependency --- pubspec.lock | 8 -------- scripts/app_config/templates/pubspec.template.yaml | 1 - 2 files changed, 9 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 1c8598eded..d85537b29f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -991,14 +991,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_feather_icons: - dependency: "direct main" - description: - name: flutter_feather_icons - sha256: b33b9c276fc8108254632da6644cf01f71af6c17fbfb26e136a86945f5ff9b67 - url: "https://pub.dev" - source: hosted - version: "2.0.0+1" flutter_hooks: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index 2869ba4cd2..b4aeb5a0b6 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -152,7 +152,6 @@ dependencies: google_fonts: ^6.3.2 url_launcher: ^6.0.5 flutter_svg: ^2.0.7 - flutter_feather_icons: ^2.0.0+1 decimal: ^2.1.0 event_bus: ^2.0.0 uuid: ^3.0.5 From b052871578e42f448569e5581b218ed74c842ad2 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 17 Nov 2025 10:07:53 -0600 Subject: [PATCH 34/50] dependency update spree --- .gitignore | 1 + .../app/src/main/res/drawable-hdpi/splash.png | Bin 6520 -> 0 bytes .../app/src/main/res/drawable-mdpi/splash.png | Bin 2284 -> 0 bytes .../src/main/res/drawable-v21/background.png | Bin 70 -> 69 bytes .../res/drawable-v21/launch_background.xml | 2 +- .../src/main/res/drawable-xhdpi/splash.png | Bin 4895 -> 0 bytes .../src/main/res/drawable-xxhdpi/splash.png | Bin 16819 -> 0 bytes .../src/main/res/drawable-xxxhdpi/splash.png | Bin 12561 -> 0 bytes .../app/src/main/res/drawable/background.png | Bin 70 -> 69 bytes .../main/res/drawable/launch_background.xml | 2 +- android/app/src/main/res/raw/keep.xml | 3 - .../src/main/res/values-night-v31/styles.xml | 20 ++ .../app/src/main/res/values-night/styles.xml | 22 ++ .../app/src/main/res/values-v31/styles.xml | 5 +- android/app/src/main/res/values/colors.xml | 4 - android/app/src/main/res/values/styles.xml | 18 +- .../LaunchBackground.imageset/background.png | Bin 70 -> 69 bytes ios/Runner/Base.lproj/LaunchScreen.storyboard | 2 +- lib/wallets/wallet/impl/ethereum_wallet.dart | 32 +-- lib/wallets/wallet/impl/stellar_wallet.dart | 72 +++---- .../impl/sub_wallets/eth_token_wallet.dart | 17 +- pubspec.lock | 204 +++++++++--------- scripts/app_config/shared/asset_generators.sh | 6 +- .../templates/pubspec.template.yaml | 97 +++++---- 24 files changed, 285 insertions(+), 222 deletions(-) delete mode 100644 android/app/src/main/res/drawable-hdpi/splash.png delete mode 100644 android/app/src/main/res/drawable-mdpi/splash.png delete mode 100644 android/app/src/main/res/drawable-xhdpi/splash.png delete mode 100644 android/app/src/main/res/drawable-xxhdpi/splash.png delete mode 100644 android/app/src/main/res/drawable-xxxhdpi/splash.png delete mode 100644 android/app/src/main/res/raw/keep.xml create mode 100644 android/app/src/main/res/values-night-v31/styles.xml create mode 100644 android/app/src/main/res/values-night/styles.xml delete mode 100644 android/app/src/main/res/values/colors.xml diff --git a/.gitignore b/.gitignore index 04721dd096..f373c61fe1 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ pubspec.yaml /android/app/src/main/profile/AndroidManifest.xml /android/app/src/main/kotlin/com/cypherstack/stackwallet/MainActivity.kt /android/app/src/main/res/**/ic_launcher.png +/android/app/src/main/res/**/splash.png /ios/Runner/Info.plist /ios/Runner.xcodeproj/project.pbxproj diff --git a/android/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png deleted file mode 100644 index b57b77cf008eaf9d69a5bf0681765676bceec28e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6520 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 WazoN}{NNuI;Gw#nTAhkr)c*j*&5*tT diff --git a/android/app/src/main/res/drawable-mdpi/splash.png b/android/app/src/main/res/drawable-mdpi/splash.png deleted file mode 100644 index 47903b909266079f6fdb53644642c5c25dc07ce9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2284 zcmb7``#;l*AICqN(O9#&<<^)wxpNq~C1y=g!|F)6#N-mDj-=Am>>`DNV`-B}qozsjsvR(EpkfxHJ3fCkB zwUmW-M}!qIlC>yfn>~LST$k_Mx5o(RZibre{XYQ8dwF@y{j}UF=i@Ee7W!wL5 zMMZ$Brplp4psbQthsfyi={X@5@-9tx$g&{jyVKR|o7m=ki4gpkjVmd8dLE^pzf)d5 z)O<5buTO4<``0mt*x>tXirjfDj1LouK6cxUts%k>y--nx`aQr|SS+J5Mro9rz|5~K zs0T`PQKQ^LbnOL~FYE|u=ovogVv`Xk-Q&LhQO`d$C9cBVFE|0E9ZO5-Hp016$ zKZj~n2I4#B)d)Jbo#346poI)CD46@wQy8DTH8)hcnYMmJoklLQ)eO@CF0UTA%->%5 z`LwD^N9$u5VUAJR3KSI;neN44-Z;NhC?>X_#bP(-@ebAQ~s zLXUX7NOE)IwkgLE*QFYy@rT}Ti@&GOeLmayUfab0IJZdkN!*ZXfQ??-_tG*^EM))_ ziNEvpDmPT^Cnw*%i`CWBOR{uE7D@2+KtEAF)`4mD;@ZzXy2$`c0EW`f*gJBxPZ469lo@T+#(szc78q*CiM>J$Rs8~XwE-WiN_H`3b!46P)o zn>)eWIgFTnkos9cOWHo0=&bZr zIZ9yQ$G1P%gq%wsSUFTo@#$SMGHNtEl&`NZ`$#bZ&4INyCF@j-OjIo=`|q9@S5Tti z1&Y=8(d}+Z6^fnx{cx>TmrsH$wBY@-Vw`;(UZ>14#lPl59EO-&H@npD7aB!Szf!4igvw^5%VIU;Z;h6+lk6L6LmRlu{5gwy?QUdoSYz%dOLbb^ z*D}>nO_w;OlMI4$XTlbTEycGSi;iu^JHe#ROT&rGCVy! zefZlH^H9K5kJ{TcVp#&o%#IXMynzm(@+p!1LvtLit7V8t z@wQ12l*KHKo9t-$d4X3)m@Mpqt@0-x2Be~!0$vFGSXA#=8&CFugv~8@uvdI?a_I?O zG6A4@R5(bZFaw0={s|6L@#xA58Lonc@_f0wpkRI{PR(?IinsO*h!;HRHcV55SH(t0 z<963f1}U7tf;kNqIY=(UTu`VblOJ ze_I-E%%t;Ws}qFNAB%BnWb3cb6n_LY`VJip1u)Lw=<2Qx-ZiMj0Q{1b0^eYj#Jg3d_`Ez^(%f-2c2lnavuw_Zh1=w67$n*h46hpV1?7}LLXqKMYk6ku?+>N_{w{6VaV9rQ|<0USu=`sWxi1B5-#TLYg zsqNE?2aQg_K?;X($le`(;sB7guw(1o%>Ae2fN<}4|I{zG?2n@JT6Wt9M`UVL0Ek20 zjK6J3aR!mcw9NMhVCS|VBeB{UI4E`=M=uN6DAFB}1x_aUsL!73D<7}sWAzPyvgLA*TB8L2W@m|fD3;dU7p`X2 z)Vq~5ZvMS5Z0FW3K~jlw!!jl4}M^kgdS*l(@W{@<+=6}Gp)aejR VIle*mkDV_DaCLSkHW4Uk{{rr7A65VW diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png index 8a4950a508d93bdb70f231934330330a3e52a8d4..60661e9a300421b82f2e7ff5fb37da3b345d912f 100644 GIT binary patch delta 49 zcmZ>Bogk^h#K6EXp*;8=kmB)laSW-Lll=4FUuy=|drYr>ET4QFD9+&N>gTe~DWM4f DhDZ@9 delta 50 zcmZ>Dn;@yn#=yWJFM6aCNb!2QIEGX(Cjb2R_jf%5=W*tj3;Vu20!lD=y85}Sb4q9e E0DXcH6#xJL diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml index 3fe6b2e882..3cc4948a14 100644 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/android/app/src/main/res/drawable-xhdpi/splash.png b/android/app/src/main/res/drawable-xhdpi/splash.png deleted file mode 100644 index 863332e3c40d0f4aea359bb4caa61dacb65ffcd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4895 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 diff --git a/android/app/src/main/res/drawable-xxhdpi/splash.png b/android/app/src/main/res/drawable-xxhdpi/splash.png deleted file mode 100644 index 17c2de611a9872781dcb0d6ef2a2de27bcae0eb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16819 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 Zflxc)=;UjS1@NknI$BG)RM9f*{{Zo-trq|Q diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png deleted file mode 100644 index 9b86db9d416daaf011519cf0177ef7604de401da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12561 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 diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png index 8a4950a508d93bdb70f231934330330a3e52a8d4..60661e9a300421b82f2e7ff5fb37da3b345d912f 100644 GIT binary patch delta 49 zcmZ>Bogk^h#K6EXp*;8=kmB)laSW-Lll=4FUuy=|drYr>ET4QFD9+&N>gTe~DWM4f DhDZ@9 delta 50 zcmZ>Dn;@yn#=yWJFM6aCNb!2QIEGX(Cjb2R_jf%5=W*tj3;Vu20!lD=y85}Sb4q9e E0DXcH6#xJL diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 3fe6b2e882..3cc4948a14 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/android/app/src/main/res/raw/keep.xml b/android/app/src/main/res/raw/keep.xml deleted file mode 100644 index 1d6c664db0..0000000000 --- a/android/app/src/main/res/raw/keep.xml +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 0000000000..31d2e50218 --- /dev/null +++ b/android/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000..dbc9ea9f1b --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml index e02ab7e5e7..8ba5d9a0bc 100644 --- a/android/app/src/main/res/values-v31/styles.xml +++ b/android/app/src/main/res/values-v31/styles.xml @@ -2,10 +2,11 @@ + + + - \ No newline at end of file + diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png index 8a4950a508d93bdb70f231934330330a3e52a8d4..60661e9a300421b82f2e7ff5fb37da3b345d912f 100644 GIT binary patch delta 49 zcmZ>Bogk^h#K6EXp*;8=kmB)laSW-Lll=4FUuy=|drYr>ET4QFD9+&N>gTe~DWM4f DhDZ@9 delta 50 zcmZ>Dn;@yn#=yWJFM6aCNb!2QIEGX(Cjb2R_jf%5=W*tj3;Vu20!lD=y85}Sb4q9e E0DXcH6#xJL diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard index 0430c335af..7aa6dfbc25 100644 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -41,4 +41,4 @@ - \ No newline at end of file + diff --git a/lib/wallets/wallet/impl/ethereum_wallet.dart b/lib/wallets/wallet/impl/ethereum_wallet.dart index 743d380e16..829110651b 100644 --- a/lib/wallets/wallet/impl/ethereum_wallet.dart +++ b/lib/wallets/wallet/impl/ethereum_wallet.dart @@ -7,6 +7,7 @@ import 'package:http/http.dart'; import 'package:isar_community/isar.dart'; import 'package:web3dart/json_rpc.dart' show RPCError; import 'package:web3dart/web3dart.dart' as web3; +import 'package:wallet/wallet.dart' as eth_wallet; import '../../../dto/ethereum/eth_tx_dto.dart'; import '../../../models/balance.dart'; @@ -133,10 +134,9 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), version: -1, - type: - addressTo == myAddress - ? TransactionType.sentToSelf - : TransactionType.outgoing, + type: addressTo == myAddress + ? TransactionType.sentToSelf + : TransactionType.outgoing, subType: TransactionSubType.none, otherData: jsonEncode(otherData), ); @@ -175,7 +175,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { final address = Address( walletId: walletId, - value: _credentials!.address.hexEip55, + value: _credentials!.address.eip55With0x, publicKey: [], // maybe store address bytes here? seems a waste of space though derivationIndex: 0, @@ -217,8 +217,8 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { final client = getEthClient(); final addressHex = (await getCurrentReceivingAddress())!.value; - final address = web3.EthereumAddress.fromHex(addressHex); - final web3.EtherAmount ethBalance = await client.getBalance(address); + final address = eth_wallet.EthereumAddress.fromHex(addressHex); + final eth_wallet.EtherAmount ethBalance = await client.getBalance(address); final balance = Balance( total: Amount( rawValue: ethBalance.getInWei, @@ -429,9 +429,9 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { return false; } - Future getMyWeb3Address() async { + Future getMyWeb3Address() async { final myAddress = (await getCurrentReceivingAddress())!.value; - final myWeb3Address = web3.EthereumAddress.fromHex(myAddress); + final myWeb3Address = eth_wallet.EthereumAddress.fromHex(myAddress); return myWeb3Address; } @@ -446,7 +446,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { > internalSharedPrepareSend({ required TxData txData, - required web3.EthereumAddress myWeb3Address, + required eth_wallet.EthereumAddress myWeb3Address, }) async { if (txData.feeRateType == null) throw Exception("Missing fee rate type."); if (txData.feeRateType == FeeRateType.custom && @@ -527,16 +527,16 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { } final tx = web3.Transaction( - to: web3.EthereumAddress.fromHex(address), + to: eth_wallet.EthereumAddress.fromHex(address), maxGas: txData.ethEIP1559Fee?.gasLimit ?? kEthereumMinGasLimit, - value: web3.EtherAmount.inWei(amount.raw), + value: eth_wallet.EtherAmount.inWei(amount.raw), nonce: prep.nonce, - maxFeePerGas: web3.EtherAmount.fromBigInt( - web3.EtherUnit.wei, + maxFeePerGas: eth_wallet.EtherAmount.fromBigInt( + eth_wallet.EtherUnit.wei, prep.maxBaseFee, ), - maxPriorityFeePerGas: web3.EtherAmount.fromBigInt( - web3.EtherUnit.wei, + maxPriorityFeePerGas: eth_wallet.EtherAmount.fromBigInt( + eth_wallet.EtherUnit.wei, prep.priorityFee, ), ); diff --git a/lib/wallets/wallet/impl/stellar_wallet.dart b/lib/wallets/wallet/impl/stellar_wallet.dart index 86cc1aa026..ad41549a52 100644 --- a/lib/wallets/wallet/impl/stellar_wallet.dart +++ b/lib/wallets/wallet/impl/stellar_wallet.dart @@ -140,8 +140,9 @@ class StellarWallet extends Bip39Wallet { HttpClient? _httpClient; if (AppConfig.hasFeature(AppFeature.tor) && prefs.useTor) { - final ({InternetAddress host, int port}) proxyInfo = - TorService.sharedInstance.getProxyInfo(); + final ({InternetAddress host, int port}) proxyInfo = TorService + .sharedInstance + .getProxyInfo(); _httpClient = HttpClient(); SocksTCPClient.assignToHttpClient(_httpClient, [ @@ -443,7 +444,7 @@ class StellarWallet extends Bip39Wallet { .order(stellar.RequestBuilderOrder.DESC) .limit(1) .execute() - .then((value) => value.records!.first.sequence); + .then((value) => value.records.first.sequence); await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); } catch (e, s) { Logging.instance.e( @@ -470,11 +471,10 @@ class StellarWallet extends Bip39Wallet { final List transactionList = []; stellar.Page payments; try { - payments = - await (await stellarSdk).payments - .forAccount(myAddress.value) - .order(stellar.RequestBuilderOrder.DESC) - .execute(); + payments = await (await stellarSdk).payments + .forAccount(myAddress.value) + .order(stellar.RequestBuilderOrder.DESC) + .execute(); } catch (e) { if (e is stellar.ErrorResponse && e.body.contains( @@ -492,13 +492,13 @@ class StellarWallet extends Bip39Wallet { rethrow; } } - for (final stellar.OperationResponse response in payments.records!) { + for (final stellar.OperationResponse response in payments.records) { // PaymentOperationResponse por; if (response is stellar.PaymentOperationResponse) { final por = response; - final addressTo = por.to!.accountId; - final addressFrom = por.from!.accountId; + final addressTo = por.to; + final addressFrom = por.from; final TransactionType type; if (addressFrom == myAddress.value) { @@ -513,7 +513,7 @@ class StellarWallet extends Bip39Wallet { final amount = Amount( rawValue: BigInt.parse( float - .parse(por.amount!) + .parse(por.amount) .toStringAsFixed(cryptoCurrency.fractionDigits) .replaceAll(".", ""), ), @@ -553,28 +553,27 @@ class StellarWallet extends Bip39Wallet { // por.transaction returns a null sometimes final stellar.TransactionResponse tx = await (await stellarSdk) .transactions - .transaction(por.transactionHash!); + .transaction(por.transactionHash); if (tx.hash.isNotEmpty) { - fee = tx.feeCharged!; + fee = tx.feeCharged; height = tx.ledger; } final otherData = { - "overrideFee": - Amount( - rawValue: BigInt.from(fee), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + "overrideFee": Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), }; final theTransaction = TransactionV2( walletId: walletId, blockHash: "", - hash: por.transactionHash!, - txid: por.transactionHash!, + hash: por.transactionHash, + txid: por.transactionHash, timestamp: - DateTime.parse(por.createdAt!).millisecondsSinceEpoch ~/ 1000, + DateTime.parse(por.createdAt).millisecondsSinceEpoch ~/ 1000, height: height, inputs: inputs, outputs: outputs, @@ -596,7 +595,7 @@ class StellarWallet extends Bip39Wallet { final amount = Amount( rawValue: BigInt.parse( float - .parse(caor.startingBalance!) + .parse(caor.startingBalance) .toStringAsFixed(cryptoCurrency.fractionDigits) .replaceAll(".", ""), ), @@ -613,9 +612,9 @@ class StellarWallet extends Bip39Wallet { valueStringSats: amount.raw.toString(), addresses: [ // this is what the previous code was doing and I don't think its correct - caor.sourceAccount!, + caor.sourceAccount, ], - walletOwns: caor.sourceAccount! == myAddress.value, + walletOwns: caor.sourceAccount == myAddress.value, ); final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, @@ -624,13 +623,13 @@ class StellarWallet extends Bip39Wallet { outpoint: null, addresses: [ // this is what the previous code was doing and I don't think its correct - caor.sourceAccount!, + caor.sourceAccount, ], valueStringSats: amount.raw.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, - walletOwns: caor.sourceAccount! == myAddress.value, + walletOwns: caor.sourceAccount == myAddress.value, ); outputs.add(output); @@ -639,28 +638,27 @@ class StellarWallet extends Bip39Wallet { int fee = 0; int height = 0; final tx = await (await stellarSdk).transactions.transaction( - caor.transactionHash!, + caor.transactionHash, ); if (tx.hash.isNotEmpty) { - fee = tx.feeCharged!; + fee = tx.feeCharged; height = tx.ledger; } final otherData = { - "overrideFee": - Amount( - rawValue: BigInt.from(fee), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + "overrideFee": Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), }; final theTransaction = TransactionV2( walletId: walletId, blockHash: "", - hash: caor.transactionHash!, - txid: caor.transactionHash!, + hash: caor.transactionHash, + txid: caor.transactionHash, timestamp: - DateTime.parse(caor.createdAt!).millisecondsSinceEpoch ~/ 1000, + DateTime.parse(caor.createdAt).millisecondsSinceEpoch ~/ 1000, height: height, inputs: inputs, outputs: outputs, diff --git a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart index d101552300..6aca5a0082 100644 --- a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart +++ b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:isar_community/isar.dart'; +import 'package:wallet/wallet.dart' as eth_wallet; import 'package:web3dart/web3dart.dart' as web3dart; import '../../../../dto/ethereum/eth_token_tx_dto.dart'; @@ -147,7 +148,7 @@ class EthTokenWallet extends Wallet { try { await super.init(); - final contractAddress = web3dart.EthereumAddress.fromHex( + final contractAddress = eth_wallet.EthereumAddress.fromHex( tokenContract.address, ); @@ -155,7 +156,7 @@ class EthTokenWallet extends Wallet { try { _tokenContract = await _updateTokenABI( forContract: tokenContract, - usingContractAddress: contractAddress.hex, + usingContractAddress: contractAddress.eip55With0x, ); } catch (e, s) { Logging.instance.w( @@ -184,7 +185,7 @@ class EthTokenWallet extends Wallet { // Some failure, try for proxy contract final contractAddressResponse = await EthereumAPI.getProxyTokenImplementationAddress( - contractAddress.hex, + contractAddress.eip55With0x, ); if (contractAddressResponse.value != null) { @@ -242,15 +243,15 @@ class EthTokenWallet extends Wallet { final tx = web3dart.Transaction.callContract( contract: _deployedContract, function: _sendFunction, - parameters: [web3dart.EthereumAddress.fromHex(address), amount.raw], + parameters: [eth_wallet.EthereumAddress.fromHex(address), amount.raw], maxGas: txData.ethEIP1559Fee?.gasLimit ?? kEthereumTokenMinGasLimit, nonce: prep.nonce, - maxFeePerGas: web3dart.EtherAmount.fromBigInt( - web3dart.EtherUnit.wei, + maxFeePerGas: eth_wallet.EtherAmount.fromBigInt( + eth_wallet.EtherUnit.wei, prep.maxBaseFee, ), - maxPriorityFeePerGas: web3dart.EtherAmount.fromBigInt( - web3dart.EtherUnit.wei, + maxPriorityFeePerGas: eth_wallet.EtherAmount.fromBigInt( + eth_wallet.EtherUnit.wei, prep.priorityFee, ), ); diff --git a/pubspec.lock b/pubspec.lock index d85537b29f..412a0a82c0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct main" description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "4.0.7" args: dependency: transitive description: @@ -61,71 +61,64 @@ packages: dependency: "direct main" description: name: basic_utils - sha256: "2064b21d3c41ed7654bc82cc476fd65542e04d60059b74d5eed490a4da08fc6c" + sha256: "548047bef0b3b697be19fa62f46de54d99c9019a69fb7db92c69e19d87f633c7" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.8.2" bech32: dependency: "direct main" description: path: "." - ref: b6d2a5b4cd17311d917787c0f9505f04932659b1 - resolved-ref: b6d2a5b4cd17311d917787c0f9505f04932659b1 + ref: "6a3388ff8f62c1fa5e624bb7f36c8e71fe53428d" + resolved-ref: "6a3388ff8f62c1fa5e624bb7f36c8e71fe53428d" url: "https://github.com/cypherstack/bech32.git" source: git version: "0.2.1" bip32: dependency: "direct main" description: - name: bip32 - sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" - url: "https://pub.dev" - source: hosted + path: "." + ref: "9a7e9b9bad9872c69dd1383d6b2e6090f85148fc" + resolved-ref: "9a7e9b9bad9872c69dd1383d6b2e6090f85148fc" + url: "https://github.com/cypherstack/bip32-dart" + source: git version: "2.0.0" - bip340: - dependency: "direct main" - description: - name: bip340 - sha256: "2a92f6ed68959f75d67c9a304c17928b9c9449587d4f75ee68f34152f7f69e87" - url: "https://pub.dev" - source: hosted - version: "0.2.0" bip39: dependency: "direct main" description: path: "." - ref: "0cd6d54e2860bea68fc50c801cb9db2a760192fb" - resolved-ref: "0cd6d54e2860bea68fc50c801cb9db2a760192fb" + ref: "20bc8ca0bf0a30c6965977a26c41475a9e862020" + resolved-ref: "20bc8ca0bf0a30c6965977a26c41475a9e862020" url: "https://github.com/cypherstack/stack-bip39.git" source: git - version: "1.0.6" + version: "1.0.7" bip47: dependency: "direct main" description: path: "." - ref: a6e7941b98a43a613708b1a12564bc17e712cfc7 - resolved-ref: a6e7941b98a43a613708b1a12564bc17e712cfc7 + ref: "3ef6b94375d7b4d972b0bc0bd9597532381a88ec" + resolved-ref: "3ef6b94375d7b4d972b0bc0bd9597532381a88ec" url: "https://github.com/cypherstack/bip47.git" source: git - version: "2.0.0" + version: "2.1.0" bitbox: dependency: "direct main" description: path: "." - ref: "50bf29957514a5712466ba37590a851212a244bf" - resolved-ref: "50bf29957514a5712466ba37590a851212a244bf" - url: "https://github.com/PiRK/bitbox-flutter.git" + ref: "4c3c1aadae089dd1ace705aedf012e1c89fe53ad" + resolved-ref: "4c3c1aadae089dd1ace705aedf012e1c89fe53ad" + url: "https://github.com/cypherstack/bitbox-flutter.git" source: git - version: "1.0.1" + version: "1.0.2" bitcoindart: dependency: "direct main" description: path: "." - ref: af6d6c27edfe2e7cc35772ed2684eb4cc826f0e4 - resolved-ref: af6d6c27edfe2e7cc35772ed2684eb4cc826f0e4 + ref: "7145be16bb88cffbd53326f7fa4570e414be09e4" + resolved-ref: "7145be16bb88cffbd53326f7fa4570e414be09e4" url: "https://github.com/cypherstack/bitcoindart.git" source: git - version: "3.0.1" + version: "3.0.2" blockchain_signer: dependency: transitive description: @@ -348,8 +341,8 @@ packages: dependency: "direct overridden" description: path: coinlib - ref: d212a8f974bf30be82ce486bf60d7135d80eb6a2 - resolved-ref: d212a8f974bf30be82ce486bf60d7135d80eb6a2 + ref: f90600053a4f149a6153f30057ac7f75c21ab962 + resolved-ref: f90600053a4f149a6153f30057ac7f75c21ab962 url: "https://www.github.com/julian-CStack/coinlib" source: git version: "4.1.0" @@ -357,8 +350,8 @@ packages: dependency: "direct main" description: path: coinlib_flutter - ref: d212a8f974bf30be82ce486bf60d7135d80eb6a2 - resolved-ref: d212a8f974bf30be82ce486bf60d7135d80eb6a2 + ref: f90600053a4f149a6153f30057ac7f75c21ab962 + resolved-ref: f90600053a4f149a6153f30057ac7f75c21ab962 url: "https://www.github.com/julian-CStack/coinlib" source: git version: "4.0.0" @@ -423,10 +416,10 @@ packages: dependency: "direct main" description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.7" cryptography: dependency: transitive description: @@ -439,10 +432,10 @@ packages: dependency: "direct main" description: name: cs_monero - sha256: "6370649167f46ead5cffac46d164dd749cb4b07989e9f0e08fe081c74c4e6b61" + sha256: b174f40e1887eb589e1e9aa99de8e9d0bc97b543f2330d5e5e7b01a6d313a9c2 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" cs_monero_flutter_libs: dependency: "direct main" description: @@ -726,10 +719,11 @@ packages: dart_bs58check: dependency: "direct main" description: - name: dart_bs58check - sha256: "4284e606795a18c1df5a955928bdc4e1b6f908da7ab0e87f49db51b3774e9e6c" - url: "https://pub.dev" - source: hosted + path: "." + ref: bed60e43e4e509ea45bb097e6caee9f8293ddf98 + resolved-ref: bed60e43e4e509ea45bb097e6caee9f8293ddf98 + url: "https://github.com/cypherstack/dart-bs58check" + source: git version: "3.0.2" dart_numerics: dependency: "direct main" @@ -767,10 +761,10 @@ packages: dependency: "direct main" description: name: decimal - sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + sha256: fc706a5618b81e5b367b01dd62621def37abc096f2b46a9bd9068b64c1fa36d0 url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "3.2.4" dependency_validator: dependency: "direct dev" description: @@ -888,16 +882,16 @@ packages: dependency: transitive description: name: eip55 - sha256: "213a9b86add87a5216328e8494b0ab836e401210c4d55eb5e521bd39e39169e1" + sha256: a81d6afe386ec965e584541fe8f19719bed8a7ae23a5f5061112e96c50e6521b url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" electrum_adapter: dependency: "direct main" description: path: "." - ref: "794ab2d7b88b34d64a89518f9b9f41dcc235aca1" - resolved-ref: "794ab2d7b88b34d64a89518f9b9f41dcc235aca1" + ref: b6fa44d015d3bfa06934b73219928c29ca48a290 + resolved-ref: b6fa44d015d3bfa06934b73219928c29ca48a290 url: "https://github.com/cypherstack/electrum_adapter.git" source: git version: "3.0.2" @@ -920,11 +914,12 @@ packages: ethereum_addresses: dependency: "direct main" description: - name: ethereum_addresses - sha256: e6ba01d44ecb9c5634367b017d6e94598fc937be8b28fc406d0e51ed6e9513dd - url: "https://pub.dev" - source: hosted - version: "1.0.2" + path: "." + ref: "6a5d3d69e54c175ae44b44040fb2743c9b6405a6" + resolved-ref: "6a5d3d69e54c175ae44b44040fb2743c9b6405a6" + url: "https://github.com/cypherstack/dart-ethereum_address" + source: git + version: "1.0.3" event_bus: dependency: "direct main" description: @@ -1200,8 +1195,8 @@ packages: dependency: "direct main" description: path: "." - ref: "540d0bc7dc27a97d45d63f412f26818a7f3b8b51" - resolved-ref: "540d0bc7dc27a97d45d63f412f26818a7f3b8b51" + ref: "14427bcbbe1e754bce4a1b93cdb0a31ce56d792b" + resolved-ref: "14427bcbbe1e754bce4a1b93cdb0a31ce56d792b" url: "https://github.com/cypherstack/fusiondart.git" source: git version: "1.0.0" @@ -1313,10 +1308,10 @@ packages: dependency: "direct main" description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.6.0" http2: dependency: transitive description: @@ -1353,10 +1348,10 @@ packages: dependency: "direct main" description: name: image - sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.5.4" import_sorter: dependency: "direct dev" description: @@ -1374,10 +1369,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.19.0" io: dependency: transitive description: @@ -1435,13 +1430,13 @@ packages: source: hosted version: "4.9.0" json_rpc_2: - dependency: transitive + dependency: "direct overridden" description: name: json_rpc_2 - sha256: "246b321532f0e8e2ba474b4d757eaa558ae4fdd0688fdbc1e1ca9705f9b8ca0e" + sha256: "3c46c2633aec07810c3d6a2eb08d575b5b4072980db08f1344e66aeb53d6e4a7" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "4.0.0" json_serializable: dependency: transitive description: @@ -1559,10 +1554,10 @@ packages: dependency: "direct main" description: name: lottie - sha256: a93542cc2d60a7057255405f62252533f8e8956e7e06754955669fd32fb4b216 + sha256: "8ae0be46dbd9e19641791dc12ee480d34e1fd3f84c749adc05f3ad9342b71b95" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "3.3.2" matcher: dependency: transitive description: @@ -1664,19 +1659,20 @@ packages: dependency: "direct main" description: path: "." - ref: "819b21164ef93cc0889049d4a8a1be2d0cc36a1b" - resolved-ref: "819b21164ef93cc0889049d4a8a1be2d0cc36a1b" - url: "https://github.com/Cyrix126/namecoin_dart" + ref: "73a29731ba493595fed331d92c7a4b5604fd6e23" + resolved-ref: "73a29731ba493595fed331d92c7a4b5604fd6e23" + url: "https://github.com/cypherstack/namecoin_dart" source: git - version: "2.0.0" + version: "2.0.1" nanodart: dependency: "direct main" description: - name: nanodart - sha256: "4b2f42d60307b54e8cf384d6193a567d07f8efd773858c0d5948246153c13282" - url: "https://pub.dev" - source: hosted - version: "2.0.0" + path: "." + ref: "1d3f30c8abd36d352a8b3147426308b77c77484e" + resolved-ref: "1d3f30c8abd36d352a8b3147426308b77c77484e" + url: "https://github.com/cypherstack/nanodart" + source: git + version: "2.0.1" nm: dependency: transitive description: @@ -1841,12 +1837,12 @@ packages: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.1.0" pinenacl: - dependency: "direct overridden" + dependency: transitive description: name: pinenacl sha256: "57e907beaacbc3c024a098910b6240758e899674de07d6949a67b52fd984cbdf" @@ -1873,10 +1869,10 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" url: "https://pub.dev" source: hosted - version: "3.9.1" + version: "4.0.0" pool: dependency: transitive description: @@ -1885,6 +1881,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" pretty_dio_logger: dependency: transitive description: @@ -2009,10 +2013,10 @@ packages: dependency: transitive description: name: sec - sha256: "8bbd56df884502192a441b5f5d667265498f2f8728a282beccd9db79e215f379" + sha256: "52a93800943642e0b5225408d0973a1837e2452b9aa8a501fdfbc8e76b6ac135" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" share_plus: dependency: "direct main" description: @@ -2184,10 +2188,10 @@ packages: dependency: "direct main" description: name: stellar_flutter_sdk - sha256: "7d505963fe11d0f90b3f798964c485ed9fa64731c38f14c9b2fb76d5d5bd6cd8" + sha256: eb07752e11c6365ee59a666f7a95964f761ec05250b0cecaf14698ebc66b09b0 url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "2.1.8" stream_channel: dependency: "direct main" description: @@ -2264,8 +2268,8 @@ packages: dependency: "direct main" description: path: "." - ref: d000cc245e51d3ff50e6467960fb3d9159d5b2a9 - resolved-ref: d000cc245e51d3ff50e6467960fb3d9159d5b2a9 + ref: "210fe8bbb93a9e0bcbc8e99894c261f53097d5e2" + resolved-ref: "210fe8bbb93a9e0bcbc8e99894c261f53097d5e2" url: "https://github.com/cypherstack/tezart.git" source: git version: "2.0.5" @@ -2305,10 +2309,10 @@ packages: dependency: transitive description: name: toml - sha256: "69756bc12eccf279b72217a87310d217efc4b3752f722e890f672801f19ac485" + sha256: d968d149c8bd06dc14e09ea3a140f90a3f2ba71949e7a91df4a46f3107400e71 url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.16.0" tor_ffi_plugin: dependency: "direct main" description: @@ -2346,10 +2350,10 @@ packages: dependency: "direct main" description: name: unorm_dart - sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b" + sha256: "0c69186b03ca6addab0774bcc0f4f17b88d4ce78d9d4d8f0619e30a99ead58e7" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.2" url_launcher: dependency: "direct main" description: @@ -2418,10 +2422,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.5.2" vector_graphics: dependency: transitive description: @@ -2504,13 +2508,13 @@ packages: source: git version: "0.2.2" wallet: - dependency: transitive + dependency: "direct main" description: name: wallet - sha256: "687fd89a16557649b26189e597792962f405797fc64113e8758eabc2c2605c32" + sha256: "20b6d8440039726841bd23b2bac64f888ec1ce1509edcc3ed2ad1753f613521e" url: "https://pub.dev" source: hosted - version: "0.0.13" + version: "0.0.18" wasm_interop: dependency: transitive description: @@ -2539,10 +2543,10 @@ packages: dependency: "direct main" description: name: web3dart - sha256: "0b96223a6b284e3146e65dc842ded139eca68a85c4ab79c5ba1a73284927d3cd" + sha256: bde2c92aac6f086988b6a1935c9d884f42a6acb772c93e1e2810f64af0db5600 url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "3.0.1" web_socket_channel: dependency: "direct main" description: @@ -2629,10 +2633,10 @@ packages: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" xxh3: dependency: transitive description: diff --git a/scripts/app_config/shared/asset_generators.sh b/scripts/app_config/shared/asset_generators.sh index 50d035a657..7ce9cea93d 100755 --- a/scripts/app_config/shared/asset_generators.sh +++ b/scripts/app_config/shared/asset_generators.sh @@ -16,12 +16,10 @@ if [[ "${APP_BUILD_PLATFORM}" = 'windows' ]]; then cmd.exe /c flutter pub get WIN_PATH_VERSION=$(wslpath -w ${YAML_FILE}) cmd.exe /c dart run flutter_launcher_icons -f "${WIN_PATH_VERSION}" - #native splash screen not used - #cmd.exe /c dart run flutter_native_splash:create + cmd.exe /c dart run flutter_native_splash:create else flutter pub get dart run flutter_launcher_icons -f "${YAML_FILE}" - #native splash screen not used - #dart run flutter_native_splash:create + dart run flutter_native_splash:create fi popd \ No newline at end of file diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index b4aeb5a0b6..dbb2a47e8f 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -61,7 +61,7 @@ dependencies: # %%END_ENABLE_TOR%% # %%ENABLE_XMR%% -# cs_monero: 3.1.0 +# cs_monero: 3.2.0 # cs_monero_flutter_libs: 2.0.1 # %%END_ENABLE_XMR%% @@ -91,7 +91,7 @@ dependencies: bitcoindart: git: url: https://github.com/cypherstack/bitcoindart.git - ref: af6d6c27edfe2e7cc35772ed2684eb4cc826f0e4 + ref: 7145be16bb88cffbd53326f7fa4570e414be09e4 stack_wallet_backup: git: @@ -106,10 +106,10 @@ dependencies: fusiondart: git: url: https://github.com/cypherstack/fusiondart.git - ref: 540d0bc7dc27a97d45d63f412f26818a7f3b8b51 + ref: 14427bcbbe1e754bce4a1b93cdb0a31ce56d792b # Utility plugins - http: ^0.13.0 + http: ^1.5.0 local_auth: ^2.3.0 permission_handler: ^12.0.0+1 flutter_local_notifications: ^17.2.2 @@ -125,21 +125,27 @@ dependencies: bip39: git: url: https://github.com/cypherstack/stack-bip39.git - ref: 0cd6d54e2860bea68fc50c801cb9db2a760192fb + ref: 20bc8ca0bf0a30c6965977a26c41475a9e862020 bitbox: git: - url: https://github.com/PiRK/bitbox-flutter.git - ref: 50bf29957514a5712466ba37590a851212a244bf - bip32: ^2.0.0 + url: https://github.com/cypherstack/bitbox-flutter.git + ref: 4c3c1aadae089dd1ace705aedf012e1c89fe53ad + bip32: + git: + url: https://github.com/cypherstack/bip32-dart + ref: 9a7e9b9bad9872c69dd1383d6b2e6090f85148fc bech32: git: url: https://github.com/cypherstack/bech32.git - ref: b6d2a5b4cd17311d917787c0f9505f04932659b1 + ref: 6a3388ff8f62c1fa5e624bb7f36c8e71fe53428d bs58check: ^1.0.2 # Eth Plugins - web3dart: 2.6.1 - ethereum_addresses: 1.0.2 + web3dart: 3.0.1 + ethereum_addresses: + git: + url: https://github.com/cypherstack/dart-ethereum_address + ref: 6a5d3d69e54c175ae44b44040fb2743c9b6405a6 # Storage plugins flutter_secure_storage: ^8.0.0 @@ -152,13 +158,13 @@ dependencies: google_fonts: ^6.3.2 url_launcher: ^6.0.5 flutter_svg: ^2.0.7 - decimal: ^2.1.0 + decimal: ^3.2.4 event_bus: ^2.0.0 - uuid: ^3.0.5 + uuid: ^4.5.2 crypto: ^3.0.2 image: ^4.3.0 wakelock_plus: ^1.2.8 - intl: ^0.17.0 + intl: ^0.19.0 devicelocale: git: url: https://github.com/cypherstack/flutter-devicelocale @@ -171,9 +177,9 @@ dependencies: qr_flutter: ^4.0.0 share_plus: ^7.0.2 emojis: ^0.9.9 - pointycastle: ^3.6.0 + pointycastle: ^4.0.0 package_info_plus: ^8.0.2 - lottie: ^2.3.2 + lottie: ^3.3.2 file_picker: ^10.3.3 connectivity_plus: ^4.0.1 isar_community: 3.3.0-dev.2 @@ -183,19 +189,25 @@ dependencies: equatable: ^2.0.5 async: ^2.10.0 dart_bs58: ^1.0.1 - dart_bs58check: ^3.0.2 + dart_bs58check: + git: + url: https://github.com/cypherstack/dart-bs58check + ref: bed60e43e4e509ea45bb097e6caee9f8293ddf98 hex: ^0.2.0 - archive: ^3.6.1 + archive: ^4.0.2 desktop_drop: ^0.4.4 - nanodart: ^2.0.0 + nanodart: + git: + url: https://github.com/cypherstack/nanodart + ref: 1d3f30c8abd36d352a8b3147426308b77c77484e basic_utils: ^5.5.4 - stellar_flutter_sdk: ^1.7.8 - bip340: ^0.2.0 + stellar_flutter_sdk: ^2.1.7 +# bip340: ^0.2.0 # tezart: ^2.0.5 tezart: git: url: https://github.com/cypherstack/tezart.git - ref: d000cc245e51d3ff50e6467960fb3d9159d5b2a9 + ref: 210fe8bbb93a9e0bcbc8e99894c261f53097d5e2 socks5_proxy: 1.0.3+dev.3 convert: ^3.1.1 flutter_hooks: ^0.20.3 @@ -204,11 +216,11 @@ dependencies: git: url: https://www.github.com/julian-CStack/coinlib path: coinlib_flutter - ref: d212a8f974bf30be82ce486bf60d7135d80eb6a2 + ref: f90600053a4f149a6153f30057ac7f75c21ab962 electrum_adapter: git: url: https://github.com/cypherstack/electrum_adapter.git - ref: 794ab2d7b88b34d64a89518f9b9f41dcc235aca1 + ref: b6fa44d015d3bfa06934b73219928c29ca48a290 stream_channel: ^2.1.0 solana: git: # TODO [prio=low]: Revert to official package once Tor support is merged upstream. @@ -239,8 +251,8 @@ dependencies: ref: 3c0cba27868ebb5c7d65ebc30a8e6e5342186692 namecoin: git: - url: https://github.com/Cyrix126/namecoin_dart - ref: 819b21164ef93cc0889049d4a8a1be2d0cc36a1b + url: https://github.com/cypherstack/namecoin_dart + ref: 73a29731ba493595fed331d92c7a4b5604fd6e23 drift: ^2.28.2 drift_flutter: ^0.2.7 path: ^1.9.1 @@ -248,13 +260,16 @@ dependencies: fixnum: ^1.1.1 saf_util: ^0.11.0 saf_stream: ^0.12.3 - unorm_dart: ^0.2.0 + unorm_dart: ^0.3.2 qr_code_scanner_plus: ^2.0.14 mobile_app_privacy: git: url: https://github.com/cypherstack/mobile_app_privacy ref: v0.0.3 + # required for web3dart to use EthereumAddress class... + wallet: 0.0.18 + dev_dependencies: flutter_test: sdk: flutter @@ -276,7 +291,8 @@ dev_dependencies: flutter_native_splash: image: assets/icon/splash.png color: "F7F7F7" - android_disable_fullscreen: true + android_12: + color: "F7F7F7" dependency_overrides: logger: @@ -290,23 +306,17 @@ dependency_overrides: # needed for dart 3.5+ (at least for now) win32: ^5.5.4 - # namecoin names lib needs to be updated + # coinlib_flutter requires this coinlib: git: url: https://www.github.com/julian-CStack/coinlib path: coinlib - ref: d212a8f974bf30be82ce486bf60d7135d80eb6a2 - - coinlib_flutter: - git: - url: https://www.github.com/julian-CStack/coinlib - path: coinlib_flutter - ref: d212a8f974bf30be82ce486bf60d7135d80eb6a2 + ref: f90600053a4f149a6153f30057ac7f75c21ab962 bip47: git: url: https://github.com/cypherstack/bip47.git - ref: a6e7941b98a43a613708b1a12564bc17e712cfc7 + ref: 3ef6b94375d7b4d972b0bc0bd9597532381a88ec # required for dart 3, at least until a fix is merged upstream wakelock_windows: @@ -315,16 +325,19 @@ dependency_overrides: ref: 2a9bca63a540771f241d688562351482b2cf234c path: wakelock_windows - # required override for nanodart + # required override for solana, etc bip39: git: url: https://github.com/cypherstack/stack-bip39.git - ref: 0cd6d54e2860bea68fc50c801cb9db2a760192fb + ref: 20bc8ca0bf0a30c6965977a26c41475a9e862020 + + # required to override solana's lower version + decimal: ^3.2.4 - crypto: 3.0.2 analyzer: ^8.2.0 - pinenacl: ^0.6.0 - http: ^0.13.0 + + # xelis override + json_rpc_2: ^4.0.0 # %%ENABLE_ISAR%% # isar_community: From 55bb2cb7b563c6348f878230cc9359f4043b2d10 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 17 Nov 2025 12:18:33 -0600 Subject: [PATCH 35/50] update epic node --- lib/wallets/crypto_currency/coins/epiccash.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallets/crypto_currency/coins/epiccash.dart b/lib/wallets/crypto_currency/coins/epiccash.dart index e60f90f6d2..84fb1b465f 100644 --- a/lib/wallets/crypto_currency/coins/epiccash.dart +++ b/lib/wallets/crypto_currency/coins/epiccash.dart @@ -70,11 +70,11 @@ class Epiccash extends Bip39Currency { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( - host: "http://epiccash.stackwallet.com", + host: "https://epic.stackwallet.com", port: 3413, name: DefaultNodes.defaultName, id: DefaultNodes.buildId(this), - useSSL: false, + useSSL: true, enabled: true, coinName: identifier, isFailover: true, From 952f1f6ad5445d39ad9c1ea865e05b76393671b4 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 17 Nov 2025 11:08:58 -0600 Subject: [PATCH 36/50] fix mobile splash colors --- scripts/app_config/templates/pubspec.template.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index dbb2a47e8f..3fc5657c62 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -291,8 +291,11 @@ dev_dependencies: flutter_native_splash: image: assets/icon/splash.png color: "F7F7F7" + color_dark_ios: "2A2D34" + color_dark_android: "2A2D34" android_12: color: "F7F7F7" + color_dark: "2A2D34" dependency_overrides: logger: From fe697728a9739154b1badc446d93aea1aba4c07d Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 17 Nov 2025 12:24:21 -0600 Subject: [PATCH 37/50] mobile splash generated assets cleanup --- .../main/res/drawable-night-v21/background.png | Bin 0 -> 69 bytes .../drawable-night-v21/launch_background.xml | 9 +++++++++ .../src/main/res/drawable-night/background.png | Bin 0 -> 69 bytes .../res/drawable-night/launch_background.xml | 9 +++++++++ .../src/main/res/values-night-v31/styles.xml | 2 +- .../LaunchBackground.imageset/Contents.json | 17 +++++++++-------- .../darkbackground.png | Bin 0 -> 69 bytes 7 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 android/app/src/main/res/drawable-night-v21/background.png create mode 100644 android/app/src/main/res/drawable-night-v21/launch_background.xml create mode 100644 android/app/src/main/res/drawable-night/background.png create mode 100644 android/app/src/main/res/drawable-night/launch_background.xml create mode 100644 ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png diff --git a/android/app/src/main/res/drawable-night-v21/background.png b/android/app/src/main/res/drawable-night-v21/background.png new file mode 100644 index 0000000000000000000000000000000000000000..5596c666ea505c2ab469892ccad9472eebba2bd2 GIT binary patch literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Ar*6yFJ3ZbWMEWan9x3p RDIX}#;OXk;vd$@?2>`Li4%Gkv literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-night-v21/launch_background.xml b/android/app/src/main/res/drawable-night-v21/launch_background.xml new file mode 100644 index 0000000000..3cc4948a14 --- /dev/null +++ b/android/app/src/main/res/drawable-night-v21/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-night/background.png b/android/app/src/main/res/drawable-night/background.png new file mode 100644 index 0000000000000000000000000000000000000000..5596c666ea505c2ab469892ccad9472eebba2bd2 GIT binary patch literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Ar*6yFJ3ZbWMEWan9x3p RDIX}#;OXk;vd$@?2>`Li4%Gkv literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-night/launch_background.xml b/android/app/src/main/res/drawable-night/launch_background.xml new file mode 100644 index 0000000000..3cc4948a14 --- /dev/null +++ b/android/app/src/main/res/drawable-night/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml index 31d2e50218..640c7ab463 100644 --- a/android/app/src/main/res/values-night-v31/styles.xml +++ b/android/app/src/main/res/values-night-v31/styles.xml @@ -6,7 +6,7 @@ false false shortEdges - #F7F7F7 + #2A2D34