diff --git a/.gitignore b/.gitignore index 0a8db4dcc3..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 @@ -122,3 +123,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/analysis_options.yaml b/analysis_options.yaml index 1252b9a85b..f63ed52ef1 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -94,6 +94,7 @@ linter: constant_identifier_names: false prefer_final_locals: true prefer_final_in_for_each: true + lines_longer_than_80_chars: true # require_trailing_commas: true // causes issues with dart 3.7 # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 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/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png deleted file mode 100644 index b57b77cf00..0000000000 Binary files a/android/app/src/main/res/drawable-hdpi/splash.png and /dev/null differ 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 47903b9092..0000000000 Binary files a/android/app/src/main/res/drawable-mdpi/splash.png and /dev/null differ 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 0000000000..5596c666ea Binary files /dev/null and b/android/app/src/main/res/drawable-night-v21/background.png differ 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 0000000000..5596c666ea Binary files /dev/null and b/android/app/src/main/res/drawable-night/background.png differ 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/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png index 8a4950a508..60661e9a30 100644 Binary files a/android/app/src/main/res/drawable-v21/background.png and b/android/app/src/main/res/drawable-v21/background.png differ 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 863332e3c4..0000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/splash.png and /dev/null differ 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 17c2de611a..0000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/splash.png and /dev/null differ 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 9b86db9d41..0000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png index 8a4950a508..60661e9a30 100644 Binary files a/android/app/src/main/res/drawable/background.png and b/android/app/src/main/res/drawable/background.png differ 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..640c7ab463 --- /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/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/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/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json index 9f447e1b38..8bb185b107 100644 --- a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json +++ b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json @@ -2,16 +2,17 @@ "images" : [ { "filename" : "background.png", - "idiom" : "universal", - "scale" : "1x" + "idiom" : "universal" }, { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "darkbackground.png", + "idiom" : "universal" } ], "info" : { diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png index 8a4950a508..60661e9a30 100644 Binary files a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png and b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png new file mode 100644 index 0000000000..5596c666ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png differ 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/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/main.dart b/lib/main.dart index 7b4d40fde8..dd35ae7b51 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; @@ -610,7 +631,10 @@ class _MaterialAppWithThemeState extends ConsumerState @override Future didRequestAppExit() async { debugPrint("didRequestAppExit called"); - if (Platform.isMacOS) { + 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. @@ -691,6 +715,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/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, 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/new_wallet_options/new_wallet_options_view.dart b/lib/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart index bfe41c3feb..1e7e9b99cd 100644 --- a/lib/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart @@ -22,24 +22,23 @@ import '../../../widgets/desktop/desktop_scaffold.dart'; import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/stack_text_field.dart'; +import '../../../widgets/toggle.dart'; import '../create_or_restore_wallet_view/sub_widgets/coin_image.dart'; import '../new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart'; import '../restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart'; import '../restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart'; -final pNewWalletOptions = StateProvider< - ({ - String mnemonicPassphrase, - int mnemonicWordsCount, - bool convertToViewOnly, - })?>( - (ref) => null, -); +final pNewWalletOptions = + StateProvider< + ({ + String mnemonicPassphrase, + int mnemonicWordsCount, + bool convertToViewOnly, + bool convertToViewOnlySpark, + })? + >((ref) => null); -enum NewWalletOptions { - Default, - Advanced; -} +enum NewWalletOptions { Default, Advanced } class NewWalletOptionsView extends ConsumerStatefulWidget { const NewWalletOptionsView({ @@ -66,6 +65,7 @@ class _NewWalletOptionsViewState extends ConsumerState { NewWalletOptions _selectedOptions = NewWalletOptions.Default; bool _convertToViewOnly = false; + bool _firoFlag = true; @override void initState() { @@ -94,17 +94,15 @@ class _NewWalletOptionsViewState extends ConsumerState { leading: AppBarBackButton(), trailing: ExitToMyStackButton(), ), - body: SizedBox( - width: 480, - child: child, - ), + body: SizedBox(width: 480, child: child), ), child: ConditionalParent( condition: !Util.isDesktop, builder: (child) => Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( leading: const AppBarBackButton(), title: Text( @@ -135,20 +133,10 @@ class _NewWalletOptionsViewState extends ConsumerState { ), child: Column( children: [ - if (Util.isDesktop) - const Spacer( - flex: 10, - ), + if (Util.isDesktop) const Spacer(flex: 10), + if (!Util.isDesktop) const SizedBox(height: 16), if (!Util.isDesktop) - const SizedBox( - height: 16, - ), - if (!Util.isDesktop) - CoinImage( - coin: widget.coin, - height: 100, - width: 100, - ), + CoinImage(coin: widget.coin, height: 100, width: 100), if (Util.isDesktop) Text( "Wallet options", @@ -157,9 +145,7 @@ class _NewWalletOptionsViewState extends ConsumerState { ? STextStyles.desktopH2(context) : STextStyles.pageTitleH1(context), ), - SizedBox( - height: Util.isDesktop ? 32 : 16, - ), + SizedBox(height: Util.isDesktop ? 32 : 16), DropdownButtonHideUnderline( child: DropdownButton2( value: _selectedOptions, @@ -187,34 +173,29 @@ class _NewWalletOptionsViewState extends ConsumerState { Assets.svg.chevronDown, width: 12, height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), ), 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, ), ), ), menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), if (_selectedOptions == NewWalletOptions.Advanced) Column( children: [ @@ -238,8 +219,9 @@ class _NewWalletOptionsViewState extends ConsumerState { onChanged: (value) { if (value is int) { ref - .read(mnemonicWordCountStateProvider.state) - .state = value; + .read(mnemonicWordCountStateProvider.state) + .state = + value; } }, isExpanded: true, @@ -257,9 +239,9 @@ class _NewWalletOptionsViewState extends ConsumerState { 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, ), @@ -293,30 +275,28 @@ class _NewWalletOptionsViewState extends ConsumerState { }, ), if (widget.coin.hasMnemonicPassphraseSupport) - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), if (widget.coin.hasMnemonicPassphraseSupport) RoundedWhiteContainer( child: Center( child: Text( "You may add a BIP39 passphrase. This is optional. " - "You will need BOTH your seed and your passphrase to recover the wallet.", + "You will need BOTH your seed and your passphrase to " + "recover the wallet.", style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: Theme.of( + context, + ).extension()!.textSubtitle1, ) : STextStyles.itemSubtitle(context), ), ), ), if (widget.coin.hasMnemonicPassphraseSupport) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (widget.coin.hasMnemonicPassphraseSupport) ClipRRect( borderRadius: BorderRadius.circular( @@ -327,89 +307,121 @@ class _NewWalletOptionsViewState extends ConsumerState { focusNode: passwordFocusNode, controller: passwordController, style: Util.isDesktop - ? STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ) + ? STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2) : STextStyles.field(context), obscureText: hidePassword, enableSuggestions: false, autocorrect: false, - decoration: standardInputDecoration( - "BIP39 passphrase", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => SizedBox( - height: 70, - child: child, - ), - child: Row( - children: [ - SizedBox( - width: Util.isDesktop ? 24 : 16, - ), - GestureDetector( - key: const Key( - "mnemonicPassphraseFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, - width: Util.isDesktop ? 24 : 16, - height: Util.isDesktop ? 24 : 16, - ), + decoration: + standardInputDecoration( + "BIP39 passphrase", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => + SizedBox(height: 70, child: child), + child: Row( + children: [ + SizedBox(width: Util.isDesktop ? 24 : 16), + GestureDetector( + key: const Key( + "mnemonicPassphraseFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of( + context, + ).extension()!.textDark3, + width: Util.isDesktop ? 24 : 16, + height: Util.isDesktop ? 24 : 16, + ), + ), + const SizedBox(width: 12), + ], ), - const SizedBox( - width: 12, - ), - ], + ), ), ), - ), - ), ), ), if (widget.coin is ViewOnlyOptionCurrencyInterface) - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), if (widget.coin is ViewOnlyOptionCurrencyInterface) CheckboxTextButton( - label: "Convert to view only wallet. " + label: + "Convert to view only wallet. " "You will only be shown the seed phrase once. " "Save it somewhere. " - "If you lose it you will lose access to any funds in this wallet.", + "If you lose it you will lose access to any funds in" + " this wallet.", onChanged: (value) { _convertToViewOnly = value; + if (mounted && widget.coin is Firo) { + setState(() {}); + } }, ), + if (_convertToViewOnly && + widget.coin is ViewOnlyOptionCurrencyInterface && + widget.coin is Firo) + const SizedBox(height: 24), + if (_convertToViewOnly && + widget.coin is ViewOnlyOptionCurrencyInterface && + widget.coin is Firo) + SizedBox( + height: 48, + child: Toggle( + key: UniqueKey(), + onColor: Theme.of( + context, + ).extension()!.popupBG, + offColor: Theme.of( + context, + ).extension()!.textFieldDefaultBG, + onText: "Spark", + offText: "XPub", + isOn: !_firoFlag, + onValueChanged: (value) { + FocusManager.instance.primaryFocus?.unfocus(); + setState(() { + _firoFlag = !value; + }); + }, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), ], ), if (!Util.isDesktop) const Spacer(), - SizedBox( - height: Util.isDesktop ? 32 : 16, - ), + SizedBox(height: Util.isDesktop ? 32 : 16), PrimaryButton( label: "Continue", onPressed: () { if (_selectedOptions == NewWalletOptions.Advanced) { ref.read(pNewWalletOptions.notifier).state = ( - mnemonicWordsCount: - ref.read(mnemonicWordCountStateProvider.state).state, + mnemonicWordsCount: ref + .read(mnemonicWordCountStateProvider.state) + .state, mnemonicPassphrase: passwordController.text, convertToViewOnly: _convertToViewOnly, + convertToViewOnlySpark: _convertToViewOnly && _firoFlag, ); } else { ref.read(pNewWalletOptions.notifier).state = null; @@ -417,21 +429,12 @@ class _NewWalletOptionsViewState extends ConsumerState { Navigator.of(context).pushNamed( NewWalletRecoveryPhraseWarningView.routeName, - arguments: Tuple2( - widget.walletName, - widget.coin, - ), + arguments: Tuple2(widget.walletName, widget.coin), ); }, ), - if (!Util.isDesktop) - const SizedBox( - height: 16, - ), - if (Util.isDesktop) - const Spacer( - flex: 15, - ), + if (!Util.isDesktop) const SizedBox(height: 16), + if (Util.isDesktop) const Spacer(flex: 15), ], ), ), diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index 031a8b41de..52a78a975f 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -199,7 +199,9 @@ class _NewWalletRecoveryPhraseWarningViewState .mnemonicPassphrase; } else { // this may not be epiccash and sol specific? - if (coin is Epiccash || coin is Solana) { + if (coin is Epiccash || + coin is Mimblewimblecoin || + coin is Solana) { mnemonicPassphrase = ""; } } 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 e4709aada9..4dd084f8c2 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,23 +26,21 @@ 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'; import '../../../widgets/desktop/desktop_app_bar.dart'; import '../../../widgets/desktop/desktop_scaffold.dart'; import '../../../widgets/desktop/primary_button.dart'; -import '../../../widgets/stack_text_field.dart'; import '../../../widgets/options.dart'; +import '../../../widgets/stack_text_field.dart'; import '../../home_view/home_view.dart'; import 'confirm_recovery_dialog.dart'; import 'sub_widgets/restore_failed_dialog.dart'; import 'sub_widgets/restore_succeeded_dialog.dart'; import 'sub_widgets/restoring_dialog.dart'; -import '../../../wallets/wallet/impl/firo_wallet.dart'; class RestoreViewOnlyWalletView extends ConsumerStatefulWidget { const RestoreViewOnlyWalletView({ @@ -110,6 +108,7 @@ class _RestoreViewOnlyWalletViewState ViewOnlyWalletType viewOnlyWalletType = _walletType; if (widget.coin is Bip39HDCurrency) { + // already set above } else if (widget.coin is CryptonoteCurrency) { viewOnlyWalletType = ViewOnlyWalletType.cryptonote; } else { @@ -127,6 +126,7 @@ class _RestoreViewOnlyWalletViewState name: widget.walletName, restoreHeight: widget.restoreBlockHeight, otherDataJsonString: jsonEncode(otherDataJson), + overrideAddressType: viewOnlyWalletType == .spark ? .spark : null, ); bool isRestoring = true; @@ -222,29 +222,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); - break; - - case const (MoneroWallet): - await (wallet as MoneroWallet).init(isRestore: true); + switch (wallet) { + case EpiccashWallet(): + await wallet.init(isRestore: true); break; - case const (WowneroWallet): - await (wallet as WowneroWallet).init(isRestore: true); + case MimblewimblecoinWallet(): + await wallet.init(isRestore: true); break; - case const (XelisWallet): - await (wallet as XelisWallet).init(isRestore: true); + case CryptonoteWallet(): + await wallet.init(isRestore: true); break; - case const (FiroWallet): - await (wallet as FiroWallet).init(); + case XelisWallet(): + await wallet.init(isRestore: true); break; default: @@ -352,28 +344,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( @@ -398,10 +389,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) @@ -414,18 +404,19 @@ class _RestoreViewOnlyWalletViewState "Single address", "Extended pub key", if (widget.coin is Firo) - isDesktop ? "Spark View Key" : "View Key" + isDesktop ? "Spark View Key" : "View Key", ], - onColor: Theme.of(context) - .extension()! - .popupBG, - offColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, - selectedIndex: _walletType.index-1, + onColor: Theme.of( + context, + ).extension()!.popupBG, + offColor: Theme.of( + context, + ).extension()!.textFieldDefaultBG, + selectedIndex: _walletType.index - 1, onValueChanged: (value) { setState(() { - _walletType = ViewOnlyWalletType.values[value+1]; + _walletType = + ViewOnlyWalletType.values[value + 1]; }); }, decoration: BoxDecoration( @@ -436,10 +427,9 @@ class _RestoreViewOnlyWalletViewState ), ), ), - SizedBox( - height: isDesktop ? 24 : 16, - ), - if (!isElectrumX || _walletType == ViewOnlyWalletType.addressOnly) + SizedBox(height: isDesktop ? 24 : 16), + if (!isElectrumX || + _walletType == ViewOnlyWalletType.addressOnly) FullTextField( key: const Key("viewOnlyAddressRestoreFieldKey"), label: "Address", @@ -459,11 +449,9 @@ class _RestoreViewOnlyWalletViewState } }, ), - if (!isElectrumX) - SizedBox( - height: isDesktop ? 16 : 12, - ), - if (isElectrumX && _walletType == ViewOnlyWalletType.xPub) + if (!isElectrumX) SizedBox(height: isDesktop ? 16 : 12), + if (isElectrumX && + _walletType == ViewOnlyWalletType.xPub) DropdownButtonHideUnderline( child: DropdownButton2( value: _currentDropDownValue, @@ -490,10 +478,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, ), @@ -506,10 +493,9 @@ class _RestoreViewOnlyWalletViewState Assets.svg.chevronDown, width: 12, height: 6, - color: - Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ), ), @@ -517,10 +503,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, ), @@ -534,11 +519,11 @@ class _RestoreViewOnlyWalletViewState ), ), ), - if (isElectrumX && _walletType == ViewOnlyWalletType.xPub) - SizedBox( - height: isDesktop ? 16 : 12, - ), - if (!isElectrumX || _walletType == ViewOnlyWalletType.xPub) + if (isElectrumX && + _walletType == ViewOnlyWalletType.xPub) + SizedBox(height: isDesktop ? 16 : 12), + if (!isElectrumX || + _walletType == ViewOnlyWalletType.xPub) FullTextField( key: const Key("viewOnlyKeyRestoreFieldKey"), label: @@ -560,18 +545,18 @@ class _RestoreViewOnlyWalletViewState }, ), if (_walletType == ViewOnlyWalletType.spark) - SizedBox( - height: isDesktop ? 16 : 12, - ), + SizedBox(height: isDesktop ? 16 : 12), if (_walletType == ViewOnlyWalletType.spark) FullTextField( - key: const Key("viewOnlySparkViewKeyRestoreFieldKey"), + key: const Key( + "viewOnlySparkViewKeyRestoreFieldKey", + ), label: "Spark View Key", controller: sparkViewKeyController, onChanged: (value) { - setState(() { - _enableRestoreButton = value.isNotEmpty; - }); + setState(() { + _enableRestoreButton = value.isNotEmpty; + }); }, ), if (!isDesktop) const Spacer(), 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..55016b713a 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) { @@ -622,7 +611,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) @@ -872,6 +861,8 @@ class _RestoreWalletViewState extends ConsumerState { child: Column( children: [ TextFormField( + enableIMEPersonalizedLearning: + false, obscureText: _hideSeedWords, autocorrect: !isDesktop, enableSuggestions: !isDesktop, @@ -1018,6 +1009,8 @@ class _RestoreWalletViewState extends ConsumerState { child: Column( children: [ TextFormField( + enableIMEPersonalizedLearning: + false, obscureText: _hideSeedWords, autocorrect: !isDesktop, enableSuggestions: !isDesktop, @@ -1159,6 +1152,7 @@ class _RestoreWalletViewState extends ConsumerState { vertical: 4, ), child: TextFormField( + enableIMEPersonalizedLearning: false, obscureText: _hideSeedWords, autocorrect: !isDesktop, enableSuggestions: !isDesktop, 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..c00dab02e3 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,12 +34,12 @@ 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'; import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_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_buttons/app_bar_icon_button.dart'; import '../../../widgets/desktop/desktop_app_bar.dart'; @@ -102,14 +102,16 @@ class _VerifyRecoveryPhraseViewState return result == "verified"; } - Future _convertToViewOnly() async { + Future _convertToViewOnly(bool firoSpark) async { int height = 0; final Map otherDataJson = { WalletInfoKeys.isViewOnlyKey: true, }; final ViewOnlyWalletType viewOnlyWalletType; - if (widget.wallet is ExtendedKeysInterface) { + if (firoSpark) { + viewOnlyWalletType = .spark; + } else if (widget.wallet is ExtendedKeysInterface) { viewOnlyWalletType = ViewOnlyWalletType.xPub; } else if (widget.wallet is CryptonoteWallet) { if (widget.wallet.cryptoCurrency is Monero) { @@ -144,10 +146,18 @@ class _VerifyRecoveryPhraseViewState name: widget.wallet.info.name, restoreHeight: height, otherDataJsonString: jsonEncode(otherDataJson), + overrideAddressType: viewOnlyWalletType == .spark ? .spark : null, ); final ViewOnlyWalletData viewOnlyData; - if (widget.wallet is ExtendedKeysInterface) { + if (viewOnlyWalletType == .spark) { + final sparkViewKey = (widget.wallet as SparkInterface).sparkViewKey; + + viewOnlyData = SparkViewOnlyWalletData( + walletId: voInfo.walletId, + viewKey: sparkViewKey!, + ); + } else if (widget.wallet is ExtendedKeysInterface) { final extendedKeyInfo = await (widget.wallet as ExtendedKeysInterface) .getXPubs(); final testPath = (_coin as Bip39HDCurrency).constructDerivePath( @@ -204,21 +214,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: @@ -300,7 +310,9 @@ class _VerifyRecoveryPhraseViewState try { Exception? ex; await showLoading( - whileFuture: _convertToViewOnly(), + whileFuture: _convertToViewOnly( + ref.read(pNewWalletOptions)?.convertToViewOnlySpark == true, + ), context: context, message: "Converting to view only wallet", rootNavigator: Util.isDesktop, diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index 64af275075..4a873cba66 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -64,8 +64,9 @@ class _AddressBookViewState extends ConsumerState { final coins = [...AppConfig.coins]; coins.removeWhere((e) => e is Firo && e.network.isTestNet); - final bool showTestNet = - ref.read(prefsChangeNotifierProvider).showTestNetCoins; + final bool showTestNet = ref + .read(prefsChangeNotifierProvider) + .showTestNetCoins; if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); @@ -88,10 +89,7 @@ class _AddressBookViewState extends ConsumerState { final String addressString; if (wallet is SparkInterface) { Address? address = await wallet.getCurrentReceivingSparkAddress(); - if (address == null) { - address = await wallet.generateNextSparkAddress(); - await ref.read(mainDBProvider).updateOrPutAddresses([address]); - } + address ??= await wallet.generateNextSparkAddress(saveToDB: true); addressString = address.value; } else { final address = await wallet.getCurrentReceivingAddress(); @@ -137,8 +135,9 @@ class _AddressBookViewState 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: () { @@ -162,16 +161,14 @@ class _AddressBookViewState extends ConsumerState { key: const Key("addressBookFilterViewButton"), size: 36, shadows: const [], - color: - Theme.of( - context, - ).extension()!.background, + color: Theme.of( + context, + ).extension()!.background, icon: SvgPicture.asset( Assets.svg.filter, - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -195,16 +192,14 @@ class _AddressBookViewState extends ConsumerState { key: const Key("addressBookAddNewContactViewButton"), size: 36, shadows: const [], - color: - Theme.of( - context, - ).extension()!.background, + color: Theme.of( + context, + ).extension()!.background, icon: SvgPicture.asset( Assets.svg.plus, - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -260,38 +255,37 @@ class _AddressBookViewState extends ConsumerState { borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), - child: - !isDesktop - ? TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, + child: !isDesktop + ? TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: + standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), ), - ), - suffixIcon: - _searchController.text.isNotEmpty - ? Padding( + suffixIcon: _searchController.text.isNotEmpty + ? Padding( padding: const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( @@ -309,10 +303,10 @@ class _AddressBookViewState extends ConsumerState { ), ), ) - : null, - ), - ) - : null, + : null, + ), + ) + : null, ), if (!isDesktop) const SizedBox(height: 16), Text("Favorites", style: STextStyles.smallMed12(context)), @@ -324,16 +318,15 @@ class _AddressBookViewState extends ConsumerState { children: [ ...contacts .where( - (element) => - element.addressesSorted - .where( - (e) => ref.watch( - addressBookFilterProvider.select( - (value) => value.coins.contains(e.coin), - ), - ), - ) - .isNotEmpty, + (element) => element.addressesSorted + .where( + (e) => ref.watch( + addressBookFilterProvider.select( + (value) => value.coins.contains(e.coin), + ), + ), + ) + .isNotEmpty, ) .where( (e) => @@ -375,17 +368,15 @@ class _AddressBookViewState extends ConsumerState { children: [ ...contacts .where( - (element) => - element.addressesSorted - .where( - (e) => ref.watch( - addressBookFilterProvider.select( - (value) => - value.coins.contains(e.coin), - ), - ), - ) - .isNotEmpty, + (element) => element.addressesSorted + .where( + (e) => ref.watch( + addressBookFilterProvider.select( + (value) => value.coins.contains(e.coin), + ), + ), + ) + .isNotEmpty, ) .where( (e) => ref 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/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/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/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/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/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/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 66b8c88a7c..7ef4f263b5 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, + ), ); } } @@ -182,17 +178,19 @@ class _ReceiveViewState extends ConsumerState { final Address? address; if (wallet is Bip39HDWallet && wallet is! BCashInterface) { DerivePathType? type; - if (wallet.isViewOnly && wallet is ExtendedKeysInterface) { - final voData = await wallet.getViewOnlyWalletData(); + if (wallet.isViewOnly && + wallet is ExtendedKeysInterface && + wallet.viewOnlyType != .spark) { + final voData = + await wallet.getViewOnlyWalletData() + as ExtendedKeysViewOnlyWalletData; for (final t in wallet.cryptoCurrency.supportedDerivationPathTypes) { final testPath = wallet.cryptoCurrency.constructDerivePath( derivePathType: t, chain: 0, index: 0, ); - if (voData is SparkViewOnlyWalletData) { - type = t; - } else if (testPath.startsWith((voData as ExtendedKeysViewOnlyWalletData).xPubs.first.path)) { + if (testPath.startsWith(voData.xPubs.first.path)) { type = t; break; } @@ -259,10 +257,7 @@ class _ReceiveViewState extends ConsumerState { ), ); - final address = await wallet.generateNextSparkAddress(); - await ref.read(mainDBProvider).isar.writeTxn(() async { - await ref.read(mainDBProvider).isar.addresses.put(address); - }); + final address = await wallet.generateNextSparkAddress(saveToDB: true); shouldPop = true; @@ -300,6 +295,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; @@ -314,6 +335,9 @@ class _ReceiveViewState extends ConsumerState { if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { _showMultiType = false; + if (wallet.viewOnlyType == .spark) { + _walletAddressTypes.add(.spark); + } } else { _showMultiType = _supportsSpark || @@ -323,7 +347,9 @@ class _ReceiveViewState extends ConsumerState { wallet.supportedAddressTypes.length > 1); } - _walletAddressTypes.add(wallet.info.mainAddressType); + if (_walletAddressTypes.isEmpty) { + _walletAddressTypes.add(wallet.info.mainAddressType); + } if (_showMultiType) { if (_supportsSpark) { @@ -341,7 +367,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 +379,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 +404,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 +497,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 +516,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 +581,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 +696,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 +747,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 +788,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/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/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/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 5616ccd9d2..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 @@ -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'; @@ -27,7 +27,9 @@ 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'; import '../../../../utilities/util.dart'; import '../../../../widgets/background.dart'; @@ -77,6 +79,103 @@ 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, + ); + + await FS.writeStringToFile( + encryptedDataString, + pathToSave, + fileToSavePath.split("/").last, + ); + + 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 +187,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 +250,34 @@ 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, - ); - } + onTap: Platform.isIOS + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); + if (mounted) { + final filePath = await stackFileSystem + .pickDir(); if (mounted) { setState(() { fileLocationController.text = - stackFileSystem.dirPath ?? ""; + filePath ?? ""; }); } - } catch (e, s) { - Logging.instance.e( - "$e\n$s", - error: e, - stackTrace: s, - ); } - }, + } catch (e, s) { + Logging.instance.e( + "$e\n$s", + error: e, + stackTrace: s, + ); + } + }, controller: fileLocationController, style: STextStyles.field(context), decoration: InputDecoration( @@ -194,10 +289,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 +312,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 +325,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 +410,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 +431,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 +463,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 +514,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 +539,8 @@ class _EnableAutoBackupViewState extends ConsumerState { top: Radius.circular(20), ), ), - builder: - (_) => - const BackupFrequencyTypeSelectSheet(), + builder: (_) => + const BackupFrequencyTypeSelectSheet(), ); }, child: Padding( @@ -466,10 +554,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 +571,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 +588,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 a04f0de247..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 @@ -18,12 +18,13 @@ 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/fs.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 +39,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 +73,116 @@ 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)); + + await FS.writeStringToFile( + encryptedDataString, + pathToSave, + fileToSavePath.split("/").last, + ); + + 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 +193,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) { @@ -117,8 +228,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 { @@ -128,7 +240,7 @@ class _RestoreFromFileViewState extends State { const Duration(milliseconds: 75), ); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -168,12 +280,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, @@ -183,7 +295,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( @@ -191,31 +303,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.isIOS + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); + if (mounted) { + final filePath = await stackFileSystem + .pickDir(); if (mounted) { setState(() { fileLocationController.text = - stackFileSystem.dirPath ?? ""; + filePath ?? ""; }); } - } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); } - }, + } catch (e, s) { + Logging.instance.e("", error: e, stackTrace: s); + } + }, controller: fileLocationController, style: STextStyles.field(context), decoration: InputDecoration( @@ -227,10 +334,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, ), @@ -256,19 +362,18 @@ class _RestoreFromFileViewState extends State { ); }, ), - 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), 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 +389,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 +473,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 +489,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 +519,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? @@ -458,399 +566,44 @@ 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), - ), - ); - }, - ) - : Row( - 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 = - 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", - ), - ); - } - } - }, - ); - }, + ? 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), ), - const SizedBox(width: 16), - SecondaryButton( - width: 183, - buttonHeight: ButtonHeight.m, - label: "Cancel", - onPressed: () {}, - ), - ], - ), + ) + : Row( + children: [ + Consumer( + builder: (context, ref, __) { + return PrimaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Create backup", + enabled: shouldEnableCreate, + onPressed: !shouldEnableCreate + ? null + : _createBackup, + ); + }, + ), + const SizedBox(width: 16), + SecondaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Cancel", + onPressed: () {}, + ), + ], + ), ], ), ), 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..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 @@ -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'; @@ -30,7 +28,9 @@ 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'; import '../../../../utilities/util.dart'; import '../../../../widgets/background.dart'; @@ -95,158 +95,96 @@ 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 DateTime now = DateTime.now(); - final String fileToSave = createAutoBackupFilename(pathToSave, now); + final fileToSavePath = createAutoBackupFilename(pathToSave, now); - final backup = await SWB.createStackWalletJSON( - secureStorage: ref.read(secureStoreProvider), - ); + final backup = await SWB.createStackWalletJSON( + secureStorage: secureStore, + ); - final bool result = await SWB.encryptStackWalletWithADK( - fileToSave, - adkString, - jsonEncode(backup), - adkVersion, - ); + final encryptedDataString = await SWB.encryptStackWalletWithADK( + adkString, + jsonEncode(backup), + adkVersion, + ); - // this future should already be complete unless there was an error encrypting - await Future.wait([fut]); + await FS.writeStringToFile( + encryptedDataString, + pathToSave, + fileToSavePath.split("/").last, + ); - 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 = ""; + return fileToSavePath; + }(), + context: context, + message: "Updating Auto 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: (_) => 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 +200,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 +241,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 +292,28 @@ 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); - } + onTap: Platform.isIOS + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); + if (mounted) { + final filePath = await stackFileSystem.pickDir(); if (mounted) { setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; + fileLocationController.text = filePath ?? ""; }); } - } catch (e, s) { - Logging.instance.e("$e\n$s", error: e, stackTrace: s); } - }, + } catch (e, s) { + Logging.instance.e("$e\n$s", error: e, stackTrace: s); + } + }, controller: fileLocationController, style: STextStyles.field(context), decoration: InputDecoration( @@ -388,10 +325,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 +355,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 +368,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 +452,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 +466,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 +498,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 +545,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 +590,9 @@ class _EditAutoBackupViewState extends ConsumerState { .backupFrequencyType != value) { ref - .read(prefsChangeNotifierProvider) - .backupFrequencyType = value; + .read(prefsChangeNotifierProvider) + .backupFrequencyType = + value; } setState(() { _currentDropDownValue = value; @@ -666,18 +604,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 +637,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 +676,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 +715,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 0bf51bcfa6..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 @@ -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'; @@ -51,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'; @@ -91,6 +91,36 @@ 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"], + if (!pathToSave.startsWith("content://")) + [!(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; @@ -130,88 +160,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, ); @@ -428,6 +412,7 @@ abstract class SWB { mnemonicPassphrase: mnemonicPassphrase, ); Wallet? wallet; + bool didExit = false; try { String? serializedKeys; String? multisigConfig; @@ -472,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: @@ -570,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, @@ -585,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, @@ -594,7 +582,9 @@ abstract class SWB { ); return false; } finally { - await wallet?.exit(); + if (!didExit) { + await wallet?.exit(); + } } return true; } @@ -1245,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/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_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..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 @@ -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, ), ), @@ -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); @@ -187,10 +186,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 +213,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 +234,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 +281,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 +320,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 +386,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 +436,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 +577,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/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/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index bae20daa68..57808de9fd 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -30,6 +30,7 @@ import '../../../services/event_bus/events/global/wallet_sync_status_changed_eve import '../../../services/event_bus/global_event_bus.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; +import '../../../utilities/if_not_already.dart'; import '../../../utilities/show_loading.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; @@ -42,6 +43,7 @@ import '../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../../wallets/wallet/intermediate/cryptonote_wallet.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_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'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -57,6 +59,7 @@ import 'frost_ms/frost_ms_options_view.dart'; import 'wallet_backup_views/wallet_backup_view.dart'; import 'wallet_network_settings_view/wallet_network_settings_view.dart'; import 'wallet_settings_wallet_settings/change_representative_view.dart'; +import 'wallet_settings_wallet_settings/spark_view_key_view.dart'; import 'wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'wallet_settings_wallet_settings/xpub_view.dart'; @@ -88,6 +91,7 @@ class _WalletSettingsViewState extends ConsumerState { late final CryptoCurrency coin; late String xpub; late final bool xPubEnabled; + late final bool sparkViewKeyEnabled; late final EventBus eventBus; @@ -97,6 +101,162 @@ class _WalletSettingsViewState extends ConsumerState { late StreamSubscription _syncStatusSubscription; // late StreamSubscription _nodeStatusSubscription; + late final VoidCallback _walletBackupPressed; + late final VoidCallback _walletXPubPressed; + late final VoidCallback _walletSparkViewKeyPressed; + + Future __walletSparkViewKeyPressedHelper() async { + final wallet = ref.read(pWallets).getWallet(walletId) as SparkInterface; + final sparkViewKeyHex = wallet.sparkViewKey!; + + if (mounted) { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: (walletId, sparkViewKeyHex), + showBackButton: true, + routeOnSuccess: SparkViewKeyView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: "Authenticate to view spark view key", + biometricsAuthenticationTitle: "View spark view key", + ), + settings: const RouteSettings( + name: "/viewSparkViewKeyDataLockscreen", + ), + ), + ); + } + } + + Future _walletXPubHelper() async { + final xpubData = await showLoading( + delay: const Duration(milliseconds: 800), + whileFuture: + (ref.read(pWallets).getWallet(walletId) as ExtendedKeysInterface) + .getXPubs(), + context: context, + message: "Loading xpubs", + rootNavigator: Util.isDesktop, + ); + + if (mounted) { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: (walletId, xpubData!), + showBackButton: true, + routeOnSuccess: XPubView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: "Authenticate to view xpub data", + biometricsAuthenticationTitle: "View xpub data", + ), + settings: const RouteSettings(name: "/viewXPubDataLockscreen"), + ), + ); + } + } + + Future _walletBackupPressedHelper() async { + // TODO: [prio=med] take wallets that don't have a mnemonic into account + + final wallet = ref.read(pWallets).getWallet(widget.walletId); + + List? mnemonic; + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? + frostWalletData; + if (wallet is BitcoinFrostWallet) { + final futures = [ + wallet.getSerializedKeys(), + wallet.getMultisigConfig(), + wallet.getSerializedKeysPrevGen(), + wallet.getMultisigConfigPrevGen(), + ]; + + final results = await Future.wait(futures); + + if (results.length == 4) { + frostWalletData = ( + myName: wallet.frostInfo.myName, + config: results[1]!, + keys: results[0]!, + prevGen: results[2] == null || results[3] == null + ? null + : (config: results[3]!, keys: results[2]!), + ); + } + } else { + if (wallet is MnemonicInterface) { + if (wallet is ViewOnlyOptionInterface && + (wallet as ViewOnlyOptionInterface).isViewOnly) { + // TODO: is something needed here? + } else { + mnemonic = await wallet.getMnemonicAsWords(); + } + } + } + + KeyDataInterface? keyData; + if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { + keyData = await wallet.getViewOnlyWalletData(); + } else if (wallet is ExtendedKeysInterface) { + keyData = await wallet.getXPrivs(); + } else if (wallet is CryptonoteWallet) { + keyData = await wallet.getKeys(); + } + + if (mounted) { + if (keyData != null && + wallet is ViewOnlyOptionInterface && + wallet.isViewOnly) { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: (walletId: walletId, keyData: keyData), + showBackButton: true, + routeOnSuccess: MobileKeyDataView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: "Authenticate to view recovery data", + biometricsAuthenticationTitle: "View recovery data", + ), + settings: const RouteSettings(name: "/viewRecoveryDataLockscreen"), + ), + ); + } else { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: ( + walletId: walletId, + mnemonic: mnemonic ?? [], + frostWalletData: frostWalletData, + keyData: keyData, + ), + showBackButton: true, + routeOnSuccess: WalletBackupView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: "Authenticate to view recovery phrase", + biometricsAuthenticationTitle: "View recovery phrase", + ), + settings: const RouteSettings(name: "/viewRecoverPhraseLockscreen"), + ), + ); + } + } + } + @override void initState() { walletId = widget.walletId; @@ -105,8 +265,10 @@ class _WalletSettingsViewState extends ConsumerState { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { xPubEnabled = false; + sparkViewKeyEnabled = false; } else { xPubEnabled = wallet is ExtendedKeysInterface; + sparkViewKeyEnabled = wallet is SparkInterface; } xpub = ""; @@ -139,6 +301,14 @@ class _WalletSettingsViewState extends ConsumerState { } }); + _walletBackupPressed = IfNotAlreadyAsync( + _walletBackupPressedHelper, + ).execute; + _walletXPubPressed = IfNotAlreadyAsync(_walletXPubHelper).execute; + _walletSparkViewKeyPressed = IfNotAlreadyAsync( + __walletSparkViewKeyPressedHelper, + ).execute; + // _nodeStatusSubscription = // eventBus.on().listen( // (event) async { @@ -262,146 +432,7 @@ class _WalletSettingsViewState extends ConsumerState { iconAssetName: Assets.svg.lock, iconSize: 16, title: "Wallet backup", - onPressed: () async { - // TODO: [prio=med] take wallets that don't have a mnemonic into account - - List? mnemonic; - ({ - String myName, - String config, - String keys, - ({String config, String keys})? - prevGen, - })? - frostWalletData; - if (wallet is BitcoinFrostWallet) { - final futures = [ - wallet.getSerializedKeys(), - wallet.getMultisigConfig(), - wallet - .getSerializedKeysPrevGen(), - wallet - .getMultisigConfigPrevGen(), - ]; - - final results = await Future.wait( - futures, - ); - - if (results.length == 4) { - frostWalletData = ( - myName: - wallet.frostInfo.myName, - config: results[1]!, - keys: results[0]!, - prevGen: - results[2] == null || - results[3] == null - ? null - : ( - config: results[3]!, - keys: results[2]!, - ), - ); - } - } else { - if (wallet is MnemonicInterface) { - if (wallet - is ViewOnlyOptionInterface && - (wallet as ViewOnlyOptionInterface) - .isViewOnly) { - // TODO: is something needed here? - } else { - mnemonic = await wallet - .getMnemonicAsWords(); - } - } - } - - KeyDataInterface? keyData; - if (wallet - is ViewOnlyOptionInterface && - wallet.isViewOnly) { - keyData = await wallet - .getViewOnlyWalletData(); - } else if (wallet - is ExtendedKeysInterface) { - keyData = await wallet - .getXPrivs(); - } else if (wallet - is CryptonoteWallet) { - keyData = await wallet.getKeys(); - } - - if (context.mounted) { - if (keyData != null && - wallet - is ViewOnlyOptionInterface && - wallet.isViewOnly) { - await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: ( - walletId: walletId, - keyData: keyData, - ), - showBackButton: true, - routeOnSuccess: - MobileKeyDataView - .routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to view recovery data", - biometricsAuthenticationTitle: - "View recovery data", - ), - settings: const RouteSettings( - name: - "/viewRecoveryDataLockscreen", - ), - ), - ); - } else { - await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: ( - walletId: walletId, - mnemonic: - mnemonic ?? [], - frostWalletData: - frostWalletData, - keyData: keyData, - ), - showBackButton: true, - routeOnSuccess: - WalletBackupView - .routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to view recovery phrase", - biometricsAuthenticationTitle: - "View recovery phrase", - ), - settings: const RouteSettings( - name: - "/viewRecoverPhraseLockscreen", - ), - ), - ); - } - } - }, + onPressed: _walletBackupPressed, ); }, ), @@ -435,35 +466,19 @@ class _WalletSettingsViewState extends ConsumerState { return SettingsListButton( iconAssetName: Assets.svg.eye, title: "Wallet xPub", - onPressed: () async { - final xpubData = await showLoading( - delay: const Duration( - milliseconds: 800, - ), - whileFuture: - (ref - .read(pWallets) - .getWallet( - walletId, - ) - as ExtendedKeysInterface) - .getXPubs(), - context: context, - message: "Loading xpubs", - rootNavigator: Util.isDesktop, - ); - if (context.mounted) { - await Navigator.of( - context, - ).pushNamed( - XPubView.routeName, - arguments: ( - widget.walletId, - xpubData, - ), - ); - } - }, + onPressed: _walletXPubPressed, + ); + }, + ), + if (sparkViewKeyEnabled) + const SizedBox(height: 8), + if (sparkViewKeyEnabled) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.eye, + title: "Spark view key", + onPressed: _walletSparkViewKeyPressed, ); }, ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_view_key_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_view_key_view.dart index 13c24d57ae..8364d83ea2 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_view_key_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_view_key_view.dart @@ -13,12 +13,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; +import '../../../../notifications/show_flush_bar.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/clipboard_interface.dart'; -import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../widgets/background.dart'; @@ -27,10 +26,8 @@ import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../widgets/desktop/primary_button.dart'; -import '../../../../widgets/detail_item.dart'; import '../../../../widgets/qr.dart'; import '../../../../widgets/rounded_white_container.dart'; -import '../../../../notifications/show_flush_bar.dart'; class SparkViewKeyView extends ConsumerStatefulWidget { const SparkViewKeyView({ @@ -38,11 +35,13 @@ class SparkViewKeyView extends ConsumerStatefulWidget { required this.walletId, required this.sparkViewKeyHex, this.clipboardInterface = const ClipboardWrapper(), + this.showDesktopDialogTitle = true, }); final String walletId; final String sparkViewKeyHex; final ClipboardInterface clipboardInterface; + final bool showDesktopDialogTitle; static const String routeName = "/spark_view_key"; @@ -75,7 +74,9 @@ class _SparkViewKeyViewState extends ConsumerState { condition: !isDesktop, builder: (child) => Background( child: Scaffold( - backgroundColor: Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -112,7 +113,7 @@ class _SparkViewKeyViewState extends ConsumerState { ), ), child: ConditionalParent( - condition: isDesktop, + condition: isDesktop && widget.showDesktopDialogTitle, builder: (child) => DesktopDialog( maxWidth: 600, maxHeight: double.infinity, @@ -130,7 +131,10 @@ class _SparkViewKeyViewState extends ConsumerState { ), ), DesktopDialogCloseButton( - onPressedOverride: Navigator.of(context, rootNavigator: true).pop, + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, ), ], ), @@ -150,12 +154,16 @@ class _SparkViewKeyViewState extends ConsumerState { SizedBox(height: Util.isDesktop ? 12 : 16), QR( data: widget.sparkViewKeyHex, - size: Util.isDesktop ? 256 : MediaQuery.of(context).size.width / 1.5, + size: Util.isDesktop + ? 256 + : MediaQuery.of(context).size.width / 1.5, ), SizedBox(height: Util.isDesktop ? 12 : 16), RoundedWhiteContainer( borderColor: Util.isDesktop - ? Theme.of(context).extension()!.textFieldDefaultBG + ? Theme.of( + context, + ).extension()!.textFieldDefaultBG : null, child: SelectableText( widget.sparkViewKeyHex, @@ -168,7 +176,9 @@ class _SparkViewKeyViewState extends ConsumerState { children: [ if (Util.isDesktop) const Spacer(), if (Util.isDesktop) const SizedBox(width: 16), - Expanded(child: PrimaryButton(label: "Copy", onPressed: _copy)), + Expanded( + child: PrimaryButton(label: "Copy", onPressed: _copy), + ), ], ), ], @@ -176,4 +186,4 @@ class _SparkViewKeyViewState extends ConsumerState { ), ); } -} \ No newline at end of file +} 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/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..e7970486e9 --- /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..0a33183174 --- /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/spark_names/buy_spark_name_view.dart b/lib/pages/spark_names/buy_spark_name_view.dart index 478893dc84..9eeef92566 100644 --- a/lib/pages/spark_names/buy_spark_name_view.dart +++ b/lib/pages/spark_names/buy_spark_name_view.dart @@ -80,8 +80,7 @@ class _BuySparkNameViewState extends ConsumerState { Logging.instance.t( "Found address that already has a spark name. Generating next address...", ); - myAddress = await wallet.generateNextSparkAddress(); - await ref.read(mainDBProvider).updateOrPutAddresses([myAddress]); + myAddress = await wallet.generateNextSparkAddress(saveToDB: true); } addressController.text = myAddress!.value; diff --git a/lib/pages/spark_names/confirm_spark_name_transaction_view.dart b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart index 3282b1f1fb..d1f4e68e3d 100644 --- a/lib/pages/spark_names/confirm_spark_name_transaction_view.dart +++ b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart @@ -16,7 +16,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import '../../models/isar/models/blockchain_data/address.dart'; import '../../models/isar/models/transaction_note.dart'; import '../../notifications/show_flush_bar.dart'; import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart'; @@ -132,10 +131,7 @@ class _ConfirmSparkNameTransactionViewState final address = txData.sparkNameInfo?.sparkAddress; final currentReceiving = await wallet.getCurrentReceivingSparkAddress(); if (currentReceiving?.value == address?.value) { - final address = await wallet.generateNextSparkAddress(); - await ref.read(mainDBProvider).isar.writeTxn(() async { - await ref.read(mainDBProvider).isar.addresses.put(address); - }); + await wallet.generateNextSparkAddress(saveToDB: true); } final db = ref.read(pDrift(walletId)); @@ -219,10 +215,9 @@ class _ConfirmSparkNameTransactionViewState child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: - Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -267,81 +262,76 @@ class _ConfirmSparkNameTransactionViewState return ConditionalParent( condition: !isDesktop, - builder: - (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Confirm transaction", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + builder: (child) => Background( + child: Scaffold( + backgroundColor: Theme.of( + context, + ).extension()!.background, + appBar: AppBar( + backgroundColor: Theme.of( + context, + ).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, - ), - ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, ), ), - ); - }, - ), - ), + ), + ), + ); + }, ), ), + ), + ), child: ConditionalParent( condition: isDesktop, - builder: - (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Row( children: [ - Row( - children: [ - AppBarBackButton( - size: 40, - iconSize: 24, - onPressed: - () => - Navigator.of(context, rootNavigator: true).pop(), - ), - Text( - "Confirm transaction", - style: STextStyles.desktopH3(context), - ), - ], + AppBarBackButton( + size: 40, + iconSize: 24, + onPressed: () => + Navigator.of(context, rootNavigator: true).pop(), + ), + Text( + "Confirm transaction", + style: STextStyles.desktopH3(context), ), - Flexible(child: SingleChildScrollView(child: child)), ], ), + Flexible(child: SingleChildScrollView(child: child)), + ], + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, @@ -487,18 +477,18 @@ class _ConfirmSparkNameTransactionViewState ), child: RoundedWhiteContainer( padding: const EdgeInsets.all(0), - borderColor: - Theme.of(context).extension()!.background, + borderColor: Theme.of( + context, + ).extension()!.background, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.background, + color: Theme.of( + context, + ).extension()!.background, borderRadius: BorderRadius.only( topLeft: Radius.circular( Constants.size.circularBorderRadius, @@ -550,24 +540,23 @@ class _ConfirmSparkNameTransactionViewState const SizedBox(height: 2), SelectableText( widget.txData.sparkNameInfo!.name, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of( + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( context, ).extension()!.textDark, - ), + ), ), ], ), ), Container( height: 1, - color: - Theme.of( - context, - ).extension()!.background, + color: Theme.of( + context, + ).extension()!.background, ), Padding( padding: const EdgeInsets.all(12), @@ -584,14 +573,14 @@ class _ConfirmSparkNameTransactionViewState const SizedBox(height: 2), SelectableText( widget.txData.sparkNameInfo!.additionalInfo, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of( + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( context, ).extension()!.textDark, - ), + ), ), ], ), @@ -609,14 +598,12 @@ class _ConfirmSparkNameTransactionViewState children: [ SelectableText( "Note (optional)", - style: STextStyles.desktopTextExtraSmall( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) .extension()! .textFieldActiveSearchIconRight, - ), + ), textAlign: TextAlign.left, ), const SizedBox(height: 10), @@ -631,49 +618,48 @@ class _ConfirmSparkNameTransactionViewState enableSuggestions: isDesktop ? false : true, controller: noteController, focusNode: _noteFocusNode, - style: STextStyles.desktopTextExtraSmall( - context, - ).copyWith( - color: - Theme.of( + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of( context, ).extension()!.textFieldActiveText, - height: 1.8, - ), + height: 1.8, + ), onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Type something...", - _noteFocusNode, - context, - desktopMed: true, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 11, - bottom: 12, - right: 5, - ), - suffixIcon: - noteController.text.isNotEmpty + decoration: + standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + 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: 20), @@ -697,10 +683,9 @@ class _ConfirmSparkNameTransactionViewState horizontal: 16, vertical: 18, ), - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Builder( builder: (context) { final externalCalls = ref.watch( @@ -711,21 +696,17 @@ class _ConfirmSparkNameTransactionViewState String fiatAmount = "N/A"; if (externalCalls) { - final price = - ref - .read(priceAnd24hChangeNotifierProvider) - .getPrice(coin) - ?.value; + final price = ref + .read(priceAnd24hChangeNotifierProvider) + .getPrice(coin) + ?.value; if (price != null && price > Decimal.zero) { fiatAmount = (amountWithoutChange.decimal * price) .toAmount(fractionDigits: 2) .fiatString( - locale: - ref - .read( - localeServiceChangeNotifierProvider, - ) - .locale, + locale: ref + .read(localeServiceChangeNotifierProvider) + .locale, ); } } @@ -770,10 +751,9 @@ class _ConfirmSparkNameTransactionViewState horizontal: 16, vertical: 18, ), - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( widget.txData.recipients!.first.address, style: STextStyles.itemSubtitle(context), @@ -797,10 +777,9 @@ class _ConfirmSparkNameTransactionViewState horizontal: 16, vertical: 18, ), - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( ref.watch(pAmountFormatter(coin)).format(fee!), style: STextStyles.itemSubtitle(context), @@ -827,10 +806,9 @@ class _ConfirmSparkNameTransactionViewState horizontal: 16, vertical: 18, ), - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle(context), @@ -840,64 +818,52 @@ class _ConfirmSparkNameTransactionViewState if (!isDesktop) const Spacer(), SizedBox(height: isDesktop ? 23 : 12), Padding( - padding: - isDesktop - ? const EdgeInsets.symmetric(horizontal: 32) - : const EdgeInsets.all(0), + padding: isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: RoundedContainer( - padding: - isDesktop - ? const EdgeInsets.symmetric( - horizontal: 16, - vertical: 18, - ) - : const EdgeInsets.all(12), - color: - Theme.of( - context, - ).extension()!.snackBarBackSuccess, + padding: isDesktop + ? const EdgeInsets.symmetric(horizontal: 16, vertical: 18) + : const EdgeInsets.all(12), + color: Theme.of( + context, + ).extension()!.snackBarBackSuccess, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( isDesktop ? "Total amount to send" : "Total amount", - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.titleBold12(context).copyWith( - color: - Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), ), SelectableText( ref .watch(pAmountFormatter(coin)) .format(amountWithoutChange + fee!), - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.itemSubtitle12(context).copyWith( - color: - Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), textAlign: TextAlign.right, ), ], @@ -906,10 +872,9 @@ class _ConfirmSparkNameTransactionViewState ), SizedBox(height: isDesktop ? 28 : 16), Padding( - padding: - isDesktop - ? const EdgeInsets.symmetric(horizontal: 32) - : const EdgeInsets.all(0), + padding: isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: PrimaryButton( label: "Send", buttonHeight: isDesktop ? ButtonHeight.l : null, @@ -919,28 +884,27 @@ class _ConfirmSparkNameTransactionViewState if (isDesktop) { unlocked = await showDialog( context: context, - builder: - (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [DesktopDialogCloseButton()], - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend(coin: coin), - ), - ], + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [DesktopDialogCloseButton()], ), - ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(coin: coin), + ), + ], + ), + ), ); } else { unlocked = await Navigator.push( @@ -948,18 +912,16 @@ class _ConfirmSparkNameTransactionViewState RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: - (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: - "Confirm Transaction", - ), + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), settings: const RouteSettings( name: "/confirmsendlockscreen", ), @@ -975,10 +937,9 @@ class _ConfirmSparkNameTransactionViewState unawaited( showFloatingFlushBar( type: FlushBarType.warning, - message: - Util.isDesktop - ? "Invalid passphrase" - : "Invalid PIN", + message: Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", context: context, ), ); diff --git a/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart b/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart index 61c35fe937..7a2d17974a 100644 --- a/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart +++ b/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart @@ -295,6 +295,9 @@ class _NameCard extends ConsumerWidget { ? STextStyles.w500_16(context) : STextStyles.w500_12(context)); + final _isViewOnlyWallet = + (ref.read(pWallets).getWallet(walletId) as SparkInterface).isViewOnly; + return RoundedWhiteContainer( padding: EdgeInsets.all(Util.isDesktop ? 24 : 16), child: IntrinsicHeight( @@ -318,59 +321,63 @@ class _NameCard extends ConsumerWidget { children: [ PrimaryButton( label: "Buy name", - enabled: isAvailable, + enabled: !_isViewOnlyWallet && isAvailable, buttonHeight: Util.isDesktop ? ButtonHeight.m : ButtonHeight.l, width: Util.isDesktop ? 140 : 120, - onPressed: () async { - if (context.mounted) { - if (Util.isDesktop) { - await showDialog( - context: context, - builder: (context) => SDialog( - child: SizedBox( - width: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, + onPressed: _isViewOnlyWallet + ? null + : () async { + if (context.mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Buy name", + style: STextStyles.desktopH3( + context, + ), + ), + ), + const DesktopDialogCloseButton(), + ], ), - child: Text( - "Buy name", - style: STextStyles.desktopH3(context), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: BuySparkNameView( + walletId: walletId, + name: name, + ), ), - ), - const DesktopDialogCloseButton(), - ], - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: BuySparkNameView( - walletId: walletId, - name: name, + ], ), ), - ], - ), - ), - ), - ); - } else { - await Navigator.of(context).pushNamed( - BuySparkNameView.routeName, - arguments: (walletId: walletId, name: name), - ); - } - } - }, + ), + ); + } else { + await Navigator.of(context).pushNamed( + BuySparkNameView.routeName, + arguments: (walletId: walletId, name: name), + ); + } + } + }, ), ], ), diff --git a/lib/pages/spark_names/sub_widgets/spark_name_details.dart b/lib/pages/spark_names/sub_widgets/spark_name_details.dart index 372f7484e6..6a5c75a753 100644 --- a/lib/pages/spark_names/sub_widgets/spark_name_details.dart +++ b/lib/pages/spark_names/sub_widgets/spark_name_details.dart @@ -5,10 +5,12 @@ import '../../../db/drift/database.dart'; import '../../../models/isar/models/isar_models.dart'; import '../../../providers/db/drift_provider.dart'; import '../../../providers/db/main_db_provider.dart'; +import '../../../providers/global/wallets_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../widgets/background.dart'; import '../../../widgets/conditional_parent.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -45,6 +47,8 @@ class _SparkNameDetailsViewState extends ConsumerState { late Stream _nameStream; late SparkName name; + late final bool _isViewOnlyWallet; + Stream? _labelStream; AddressLabel? label; @@ -81,37 +85,36 @@ class _SparkNameDetailsViewState extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: - (context) => SDialog( - child: SizedBox( - width: 580, - child: Column( + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Renew name", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: BuySparkNameView( - walletId: widget.walletId, - name: name.name, - nameToRenew: name, + padding: const EdgeInsets.only(left: 32), + child: Text( + "Renew name", + style: STextStyles.desktopH3(context), ), ), + const DesktopDialogCloseButton(), ], ), - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: BuySparkNameView( + walletId: widget.walletId, + name: name.name, + nameToRenew: name, + ), + ), + ], ), + ), + ), ); } else { await Navigator.of(context).pushNamed( @@ -133,6 +136,10 @@ class _SparkNameDetailsViewState extends ConsumerState { super.initState(); name = widget.name; + _isViewOnlyWallet = + (ref.read(pWallets).getWallet(widget.walletId) as SparkInterface) + .isViewOnly; + label = ref .read(mainDBProvider) .getAddressLabelSync(widget.walletId, name.address); @@ -143,9 +150,9 @@ class _SparkNameDetailsViewState extends ConsumerState { final db = ref.read(pDrift(widget.walletId)); - _nameStream = - (db.select(db.sparkNames) - ..where((e) => e.name.equals(name.name))).watchSingleOrNull(); + _nameStream = (db.select( + db.sparkNames, + )..where((e) => e.name.equals(name.name))).watchSingleOrNull(); } @override @@ -159,38 +166,37 @@ class _SparkNameDetailsViewState extends ConsumerState { return ConditionalParent( condition: !Util.isDesktop, - builder: - (child) => Background( - child: Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - backgroundColor: Colors.transparent, - // Theme.of(context).extension()!.background, - leading: const AppBarBackButton(), - title: Text( - "Spark name details", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight(child: child), - ), + builder: (child) => Background( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: Colors.transparent, + // Theme.of(context).extension()!.background, + leading: const AppBarBackButton(), + title: Text( + "Spark name details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, ), - ); - }, - ), - ), + child: IntrinsicHeight(child: child), + ), + ), + ); + }, ), ), + ), + ), child: ConditionalParent( condition: Util.isDesktop, builder: (child) { @@ -221,10 +227,9 @@ class _SparkNameDetailsViewState extends ConsumerState { child: RoundedContainer( padding: EdgeInsets.zero, color: Colors.transparent, - borderColor: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + borderColor: Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: child, ), ), @@ -244,19 +249,17 @@ class _SparkNameDetailsViewState extends ConsumerState { children: [ RoundedContainer( padding: const EdgeInsets.all(12), - color: - Util.isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.popupBG, + color: Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SelectableText( name.name, - style: - Util.isDesktop - ? STextStyles.pageTitleH2(context) - : STextStyles.w500_14(context), + style: Util.isDesktop + ? STextStyles.pageTitleH2(context) + : STextStyles.w500_14(context), ), ], ), @@ -264,14 +267,12 @@ class _SparkNameDetailsViewState extends ConsumerState { const _Div(), RoundedContainer( - padding: - Util.isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: - Util.isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.popupBG, + padding: Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -282,10 +283,9 @@ class _SparkNameDetailsViewState extends ConsumerState { Text( "Address", style: STextStyles.w500_14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of( + context, + ).extension()!.textSubtitle1, ), ), Util.isDesktop @@ -309,74 +309,69 @@ class _SparkNameDetailsViewState extends ConsumerState { return (label != null && label!.value.isNotEmpty) ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const _Div(), + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _Div(), - RoundedContainer( - padding: - Util.isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: - Util.isDesktop - ? Colors.transparent - : Theme.of( + RoundedContainer( + padding: Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: Util.isDesktop + ? Colors.transparent + : Theme.of( context, ).extension()!.popupBG, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Address label", - style: STextStyles.w500_14( - context, - ).copyWith( - color: - Theme.of(context) - .extension()! - .textSubtitle1, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address label", + style: STextStyles.w500_14(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), ), - ), - Util.isDesktop - ? tvd.IconCopyButton( - data: label!.value, - ) - : SimpleCopyButton( - data: label!.value, - ), - ], - ), - const SizedBox(height: 4), - SelectableText( - label!.value, - style: STextStyles.w500_14(context), - ), - ], + Util.isDesktop + ? tvd.IconCopyButton( + data: label!.value, + ) + : SimpleCopyButton( + data: label!.value, + ), + ], + ), + const SizedBox(height: 4), + SelectableText( + label!.value, + style: STextStyles.w500_14(context), + ), + ], + ), ), - ), - ], - ) + ], + ) : const SizedBox(width: 0, height: 0); }, ), const _Div(), RoundedContainer( - padding: - Util.isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: - Util.isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.popupBG, + padding: Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -387,10 +382,9 @@ class _SparkNameDetailsViewState extends ConsumerState { Text( "Expiry", style: STextStyles.w500_14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of( + context, + ).extension()!.textSubtitle1, ), ), const SizedBox(height: 4), @@ -402,11 +396,12 @@ class _SparkNameDetailsViewState extends ConsumerState { ), ], ), - if (remaining < _remainingMagic) + if (remaining < _remainingMagic && !_isViewOnlyWallet) PrimaryButton( label: "Renew", - buttonHeight: - Util.isDesktop ? ButtonHeight.xs : ButtonHeight.l, + buttonHeight: Util.isDesktop + ? ButtonHeight.xs + : ButtonHeight.l, onPressed: _renew, ), ], @@ -414,14 +409,12 @@ class _SparkNameDetailsViewState extends ConsumerState { ), const _Div(), RoundedContainer( - padding: - Util.isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: - Util.isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.popupBG, + padding: Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -429,10 +422,9 @@ class _SparkNameDetailsViewState extends ConsumerState { Text( "Additional info", style: STextStyles.w500_14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of( + context, + ).extension()!.textSubtitle1, ), ), const SizedBox(height: 4), diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index d2ce19bb6c..b3a3338791 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,7 +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 '../settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_view_key_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'; @@ -421,27 +422,6 @@ class _WalletViewState extends ConsumerState { } } - Future _onShowSparkViewKeyPressed(BuildContext context) async { - unawaited( - showDialog( - context: context, - builder: (_) => const LoadingIndicator(width: 100), - ), - ); - - final wallet = ref.read(pWallets).getWallet(walletId) as SparkInterface; - await wallet.init(); - final sparkViewKeyHex = wallet.viewKeyHex; - - if (context.mounted) { - Navigator.of(context).pop(); // Close loading dialog - await Navigator.of(context).pushNamed( - SparkViewKeyView.routeName, - arguments: (walletId, sparkViewKeyHex), - ); - } - } - Future attemptAnonymize() async { bool shouldPop = false; unawaited( @@ -643,7 +623,7 @@ class _WalletViewState extends ConsumerState { context, ).extension()!.background, icon: _buildNetworkIcon(_currentSyncStatus), - onPressed: () { + onPressed: () { Navigator.of(context).pushNamed( WalletNetworkSettingsView.routeName, arguments: Tuple3( @@ -1018,7 +998,7 @@ class _WalletViewState extends ConsumerState { ), SafeArea( child: WalletNavigationBar( - items: [ + items: [ WalletNavigationBarItemData( label: "Receive", icon: const ReceiveNavIcon(), @@ -1102,9 +1082,8 @@ class _WalletViewState extends ConsumerState { icon: const BuyNavIcon(), onTap: () => _onBuyPressed(context), ), - ], - moreItems: [ - if (wallet is SparkInterface) + if (wallet is SparkInterface || + (viewOnly && wallet.viewOnlyType == .spark)) WalletNavigationBarItemData( label: "Names", icon: const PaynymNavIcon(), @@ -1115,22 +1094,8 @@ class _WalletViewState extends ConsumerState { ); }, ), - if (wallet is SparkInterface) - WalletNavigationBarItemData( - label: "Show Spark View Key", - icon: SvgPicture.asset( - Assets.svg.eye, - height: 20, - width: 20, - colorFilter: ColorFilter.mode( - Theme.of( - context, - ).extension()!.bottomNavIconIcon, - BlendMode.srcIn, - ), - ), - onTap: () => _onShowSparkViewKeyPressed(context), - ), + ], + moreItems: [ if (ref.watch( pWallets.select( (value) => value @@ -1167,6 +1132,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/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart index f27147cb3f..87a04a3635 100644 --- a/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart @@ -17,7 +17,6 @@ import '../../models/isar/models/blockchain_data/address.dart'; import '../../models/isar/models/contact_entry.dart'; import '../../pages/address_book_views/subviews/add_address_book_entry_view.dart'; import '../../pages/address_book_views/subviews/address_book_filter_view.dart'; -import '../../providers/db/main_db_provider.dart'; import '../../providers/global/address_book_service_provider.dart'; import '../../providers/providers.dart'; import '../../providers/ui/address_book_providers/address_book_filter_provider.dart'; @@ -99,17 +98,18 @@ class _DesktopAddressBook extends ConsumerState { // if (widget.coin == null) { final coins = AppConfig.coins.toList(); - coins.removeWhere( - (e) => e is Firo && e.network.isTestNet, - ); + coins.removeWhere((e) => e is Firo && e.network.isTestNet); - final bool showTestNet = - ref.read(prefsChangeNotifierProvider).showTestNetCoins; + final bool showTestNet = ref + .read(prefsChangeNotifierProvider) + .showTestNetCoins; if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); } else { - ref.read(addressBookFilterProvider).addAll( + ref + .read(addressBookFilterProvider) + .addAll( coins.where((e) => e.network != CryptoCurrencyNetwork.test), false, ); @@ -125,10 +125,7 @@ class _DesktopAddressBook extends ConsumerState { final String addressString; if (wallet is SparkInterface) { Address? address = await wallet.getCurrentReceivingSparkAddress(); - if (address == null) { - address = await wallet.generateNextSparkAddress(); - await ref.read(mainDBProvider).updateOrPutAddresses([address]); - } + address ??= await wallet.generateNextSparkAddress(saveToDB: true); addressString = address.value; } else { final address = await wallet.getCurrentReceivingAddress(); @@ -166,8 +163,9 @@ class _DesktopAddressBook extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final contacts = - ref.watch(addressBookServiceProvider.select((value) => value.contacts)); + final contacts = ref.watch( + addressBookServiceProvider.select((value) => value.contacts), + ); final allContacts = contacts .where( @@ -176,8 +174,9 @@ class _DesktopAddressBook extends ConsumerState { element.addresses .where( (e) => ref.watch( - addressBookFilterProvider - .select((value) => value.coins.contains(e.coin)), + addressBookFilterProvider.select( + (value) => value.coins.contains(e.coin), + ), ), ) .isNotEmpty, @@ -194,8 +193,9 @@ class _DesktopAddressBook extends ConsumerState { element.addresses .where( (e) => ref.watch( - addressBookFilterProvider - .select((value) => value.coins.contains(e.coin)), + addressBookFilterProvider.select( + (value) => value.coins.contains(e.coin), + ), ), ) .isNotEmpty, @@ -213,22 +213,13 @@ class _DesktopAddressBook extends ConsumerState { isCompactHeight: true, leading: Row( children: [ - const SizedBox( - width: 24, - ), - Text( - "Address Book", - style: STextStyles.desktopH3(context), - ), + const SizedBox(width: 24), + Text("Address Book", style: STextStyles.desktopH3(context)), ], ), ), body: Padding( - padding: const EdgeInsets.only( - left: 24, - right: 24, - bottom: 24, - ), + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24), child: DesktopAddressBookScaffold( controlsLeft: ClipRRect( borderRadius: BorderRadius.circular( @@ -245,43 +236,44 @@ class _DesktopAddressBook extends ConsumerState { }); }, style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 20, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchTerm = ""; - }); - }, + decoration: + standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 20, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], ), - ], - ), - ), - ) - : null, - ), + ), + ) + : null, + ), ), ), controlsRight: Row( @@ -293,24 +285,22 @@ class _DesktopAddressBook extends ConsumerState { buttonHeight: ButtonHeight.l, icon: SvgPicture.asset( Assets.svg.filter, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: Theme.of( + context, + ).extension()!.buttonTextSecondary, ), onPressed: selectCryptocurrency, ), - const SizedBox( - width: 20, - ), + const SizedBox(width: 20), PrimaryButton( width: 184, label: "Add new", buttonHeight: ButtonHeight.l, icon: SvgPicture.asset( Assets.svg.circlePlus, - color: Theme.of(context) - .extension()! - .buttonTextPrimary, + color: Theme.of( + context, + ).extension()!.buttonTextPrimary, ), onPressed: newContact, ), @@ -326,10 +316,7 @@ class _DesktopAddressBook extends ConsumerState { lowerLabel: favorites.isEmpty ? null : Padding( - padding: const EdgeInsets.only( - top: 20, - bottom: 12, - ), + padding: const EdgeInsets.only(top: 20, bottom: 12), child: Text( "All contacts", style: STextStyles.smallMed12(context), @@ -337,15 +324,15 @@ class _DesktopAddressBook extends ConsumerState { ), favorites: favorites.isEmpty ? contacts.isNotEmpty - ? null - : RoundedWhiteContainer( - child: Center( - child: Text( - "Your favorite contacts will appear here", - style: STextStyles.itemSubtitle(context), + ? null + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), ), - ), - ) + ) : RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: Column( @@ -355,9 +342,9 @@ class _DesktopAddressBook extends ConsumerState { children: [ if (i > 0) Container( - color: Theme.of(context) - .extension()! - .background, + color: Theme.of( + context, + ).extension()!.background, height: 1, ), Padding( @@ -406,15 +393,15 @@ class _DesktopAddressBook extends ConsumerState { ), all: allContacts.isEmpty ? contacts.isNotEmpty - ? null - : RoundedWhiteContainer( - child: Center( - child: Text( - "Your contacts will appear here", - style: STextStyles.itemSubtitle(context), + ? null + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), ), - ), - ) + ) : Column( children: [ RoundedWhiteContainer( @@ -426,9 +413,9 @@ class _DesktopAddressBook extends ConsumerState { children: [ if (i > 0) Container( - color: Theme.of(context) - .extension()! - .background, + color: Theme.of( + context, + ).extension()!.background, height: 1, ), Padding( @@ -481,9 +468,7 @@ class _DesktopAddressBook extends ConsumerState { ), details: currentContactId == null ? Container() - : DesktopContactDetails( - contactId: currentContactId!, - ), + : DesktopContactDetails(contactId: currentContactId!), ), ), ); 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 713521b14c..ce8940c80d 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 @@ -196,17 +196,19 @@ class _DesktopReceiveState extends ConsumerState { final Address? address; if (wallet is Bip39HDWallet && wallet is! BCashInterface) { DerivePathType? type; - if (wallet.isViewOnly && wallet is ExtendedKeysInterface) { - final voData = await wallet.getViewOnlyWalletData(); + if (wallet.isViewOnly && + wallet is ExtendedKeysInterface && + wallet.viewOnlyType != .spark) { + final voData = + await wallet.getViewOnlyWalletData() + as ExtendedKeysViewOnlyWalletData; for (final t in wallet.cryptoCurrency.supportedDerivationPathTypes) { final testPath = wallet.cryptoCurrency.constructDerivePath( derivePathType: t, chain: 0, index: 0, ); - if (voData is SparkViewOnlyWalletData) { - type = t; - } else if (testPath.startsWith((voData as ExtendedKeysViewOnlyWalletData).xPubs.first.path)) { + if (testPath.startsWith(voData.xPubs.first.path)) { type = t; break; } @@ -270,10 +272,7 @@ class _DesktopReceiveState extends ConsumerState { ), ); - final address = await wallet.generateNextSparkAddress(); - await ref.read(mainDBProvider).isar.writeTxn(() async { - await ref.read(mainDBProvider).isar.addresses.put(address); - }); + final address = await wallet.generateNextSparkAddress(saveToDB: true); shouldPop = true; @@ -312,6 +311,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(); @@ -327,10 +352,8 @@ class _DesktopReceiveState extends ConsumerState { isMimblewimblecoin = wallet is MimblewimblecoinWallet; - if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly && wallet.viewOnlyType == ViewOnlyWalletType.spark) { + if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { showMultiType = false; - } else if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { - showMultiType = supportsSpark; } else { showMultiType = supportsSpark || @@ -340,11 +363,7 @@ class _DesktopReceiveState extends ConsumerState { wallet.supportedAddressTypes.length > 1); } - if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly && wallet.viewOnlyType == ViewOnlyWalletType.spark) { - _walletAddressTypes.add(AddressType.spark); - } else { - _walletAddressTypes.add(wallet.info.mainAddressType); - } + _walletAddressTypes.add(wallet.info.mainAddressType); if (showMultiType) { if (supportsSpark) { @@ -362,7 +381,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); } @@ -372,30 +393,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); } } @@ -419,42 +417,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 052569af24..63366f22fa 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'; @@ -39,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'; @@ -51,6 +53,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,12 +91,14 @@ enum WalletFeature { namecoinName("Domains", "Namecoin DNS"), sparkNames("Names", "Spark names"), salviumStaking("Staking", "Staking"), + sign("Sign/Verify", "Sign / Verify messages"), // special cases clearSparkCache("", ""), rbf("", ""), reuseAddress("", ""), - enableMweb("", ""); + enableMweb("", ""), + enableLegacyAddresses("", ""); final String label; final String description; @@ -417,6 +422,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, @@ -425,6 +462,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { ) { final coin = wallet.info.coin; final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly; + final isSparkViewOnly = isViewOnly && wallet.viewOnlyType == .spark; return [ if (!isViewOnly && @@ -436,7 +474,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { _onAnonymizeAllPressed, ), - if (wallet is SparkInterface) + if (wallet is SparkInterface && !isViewOnly || isSparkViewOnly) (WalletFeature.sparkNames, Assets.svg.robotHead, _onSparkNamesPressed), if (!isViewOnly && @@ -455,6 +493,9 @@ class _DesktopWalletFeaturesState extends ConsumerState { _onSalviumStakePressed, ), + if (wallet is SignVerifyInterface && !isViewOnly) + (WalletFeature.sign, Assets.svg.pencil, _onSignPressed), + if (showCoinControl) ( WalletFeature.coinControl, @@ -544,6 +585,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/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart index a3c281f3c3..fc348003a8 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart @@ -14,11 +14,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import '../../../../pages/pinpad_views/pinpad_dialog.dart'; import '../../../../pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart'; import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart'; -import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_view_key_view.dart'; +import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; import '../../../../providers/global/wallets_provider.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; @@ -35,6 +36,7 @@ import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interfa import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../addresses/desktop_wallet_addresses_view.dart'; +import '../../../password/request_desktop_auth_dialog.dart'; import 'desktop_delete_wallet_dialog.dart'; enum _WalletOptions { @@ -71,6 +73,28 @@ class WalletOptionsButton extends ConsumerWidget { final String walletId; + Future _auth( + BuildContext context, + String message, + VoidCallback onAuth, + ) async { + final verified = await showDialog( + context: context, + builder: (context) => Util.isDesktop + ? RequestDesktopAuthDialog(title: message) + : PinpadDialog( + biometricsAuthenticationTitle: message, + biometricsLocalizedReason: "Authenticate to show view key", + biometricsCancelButtonString: "CANCEL", + ), + barrierDismissible: !Util.isDesktop, + ); + + if (verified == "verified success" && context.mounted) { + onAuth(); + } + } + @override Widget build(BuildContext context, WidgetRef ref) { return RawMaterialButton( @@ -142,75 +166,80 @@ class WalletOptionsButton extends ConsumerWidget { } break; case _WalletOptions.showSparkKey: - final wallet = ref.read(pWallets).getWallet(walletId) as SparkInterface; - await wallet.init(); - final sparkViewKeyHex = wallet.viewKeyHex; + await _auth(context, "Show Spark view key", () async { + final wallet = + ref.read(pWallets).getWallet(walletId) as SparkInterface; + final sparkViewKeyHex = wallet.sparkViewKey!; - if (context.mounted) { - final result = await showDialog( - context: context, - barrierDismissible: false, - builder: (context) => Navigator( - initialRoute: SparkViewKeyView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - RouteGenerator.generateRoute( - RouteSettings( - name: SparkViewKeyView.routeName, - arguments: (walletId, sparkViewKeyHex), + if (context.mounted) { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: SparkViewKeyView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: SparkViewKeyView.routeName, + arguments: (walletId, sparkViewKeyHex), + ), ), - ), - ]; - }, - ), - ); + ]; + }, + ), + ); - if (result == true) { - if (context.mounted) { - Navigator.of(context).pop(); + if (result == true) { + if (context.mounted) { + Navigator.of(context).pop(); + } } } - } + }); + break; case _WalletOptions.showXpub: - final xpubData = await showLoading( - delay: const Duration(milliseconds: 800), - whileFuture: - (ref.read(pWallets).getWallet(walletId) - as ExtendedKeysInterface) - .getXPubs(), - context: context, - message: "Loading xpubs", - rootNavigator: Util.isDesktop, - ); - - if (context.mounted) { - final result = await showDialog( + await _auth(context, "Show xpub(s)", () async { + final xpubData = await showLoading( + delay: const Duration(milliseconds: 800), + whileFuture: + (ref.read(pWallets).getWallet(walletId) + as ExtendedKeysInterface) + .getXPubs(), context: context, - barrierDismissible: false, - builder: (context) => Navigator( - initialRoute: XPubView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - RouteGenerator.generateRoute( - RouteSettings( - name: XPubView.routeName, - arguments: (walletId, xpubData), - ), - ), - ]; - }, - ), + message: "Loading xpubs", + rootNavigator: Util.isDesktop, ); - if (result == true) { - if (context.mounted) { - Navigator.of(context).pop(); + if (context.mounted) { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: XPubView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: XPubView.routeName, + arguments: (walletId, xpubData), + ), + ), + ]; + }, + ), + ); + + if (result == true) { + if (context.mounted) { + Navigator.of(context).pop(); + } } } - } + }); break; case _WalletOptions.changeRepresentative: final result = await showDialog( @@ -315,16 +344,16 @@ class WalletOptionsPopupMenu extends ConsumerWidget { final wallet = ref.watch(pWallets).getWallet(walletId); bool xpubEnabled = wallet is ExtendedKeysInterface; - if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { - xpubEnabled = false; - } - final bool canChangeRep = coin is NanoCurrency; final bool isFrost = coin is FrostCurrency; final bool isCN = wallet is CryptonoteWallet; - final bool isSpark = wallet is SparkInterface; + bool isSpark = wallet is SparkInterface; + if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { + xpubEnabled = false; + isSpark = false; + } return Stack( children: [ Positioned( @@ -519,10 +548,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget { ), ), ), - if (isSpark) - const SizedBox( - height: 8, - ), + if (isSpark) const SizedBox(height: 8), if (isSpark) TransparentButton( onPressed: () { @@ -545,13 +571,14 @@ class WalletOptionsPopupMenu extends ConsumerWidget { Expanded( child: Text( _WalletOptions.showSparkKey.prettyName, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( + context, + ).extension()!.textDark, + ), ), ), ], 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 161459e439..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 @@ -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,34 +236,29 @@ 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 { 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); @@ -290,20 +280,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 +307,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 +323,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 +387,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..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 @@ -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'; @@ -32,7 +31,9 @@ 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'; import '../../../../utilities/util.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; @@ -44,9 +45,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 +88,141 @@ 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, + ); + + await FS.writeStringToFile( + encryptedDataString, + pathToSave, + fileToSavePath.split("/").last, + ); + + 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 +235,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 +288,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 +300,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,21 +314,21 @@ class _CreateAutoBackup extends ConsumerState { child: TextField( autocorrect: false, enableSuggestions: false, - onTap: Platform.isAndroid || Platform.isIOS + onTap: Platform.isIOS ? null : () async { try { await stackFileSystem.prepareStorage(); - if (mounted) { - await stackFileSystem.pickDir(context); - } - - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); + final filePath = await stackFileSystem + .pickDir(); + + if (mounted) { + setState(() { + fileLocationController.text = + filePath ?? ""; + }); + } } } catch (e, s) { Logging.instance.e( @@ -216,20 +346,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 +375,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 +402,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 +507,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 +538,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 +583,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 +595,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 +636,9 @@ class _CreateAutoBackup extends ConsumerState { .backupFrequencyType != value) { ref - .read(prefsChangeNotifierProvider) - .backupFrequencyType = value; + .read(prefsChangeNotifierProvider) + .backupFrequencyType = + value; } setState(() { _currentDropDownValue = value; @@ -542,18 +650,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 +689,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/route_generator.dart b/lib/route_generator.dart index 693940b58f..1fe6313e38 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'; @@ -428,6 +430,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( @@ -2527,7 +2549,7 @@ class RouteGenerator { } } - static Route getRoute({ + static Route getRoute({ bool shouldUseMaterialRoute = useMaterialPageRoute, required Widget Function(BuildContext) builder, String? title, @@ -2536,14 +2558,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, @@ -2553,7 +2575,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) { @@ -2571,7 +2593,7 @@ class RouteGenerator { ); } - static Route _routeError(String message) { + static Route _routeError(String message) { // Replace with robust ErrorView page final Widget errorView = Scaffold( appBar: AppBar( @@ -2585,7 +2607,7 @@ class RouteGenerator { ), ); - return getRoute( + return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => errorView, ); diff --git a/lib/services/auto_swb_service.dart b/lib/services/auto_swb_service.dart index 24f58d0f78..419e7a0804 100644 --- a/lib/services/auto_swb_service.dart +++ b/lib/services/auto_swb_service.dart @@ -17,14 +17,11 @@ 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'; -enum AutoSWBStatus { - idle, - backingUp, - error, -} +enum AutoSWBStatus { idle, backingUp, error } class AutoSWBService extends ChangeNotifier { Timer? _timer; @@ -74,27 +71,32 @@ 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 FS.writeStringToFile( + content, + autoBackupDirectoryPath, + fileToSave.split("/").last, + ); Prefs.instance.lastAutoBackup = now; @@ -124,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 = []; 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/services/mwebd_service.dart b/lib/services/mwebd_service.dart index 2462257505..10ad475318 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()); }); } @@ -205,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; } 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()); 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/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/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/lib/utilities/fs.dart b/lib/utilities/fs.dart new file mode 100644 index 0000000000..fc6f087aa3 --- /dev/null +++ b/lib/utilities/fs.dart @@ -0,0 +1,58 @@ +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'; + +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://")) { + 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); +} diff --git a/lib/utilities/if_not_already.dart b/lib/utilities/if_not_already.dart new file mode 100644 index 0000000000..8a2a0406e2 --- /dev/null +++ b/lib/utilities/if_not_already.dart @@ -0,0 +1,44 @@ +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; + final Future Function(T? args)? _functionWithArgs; + + bool _locked = false; + + IfNotAlreadyAsync(this._function) : _functionWithArgs = null; + IfNotAlreadyAsync.withArgs(this._functionWithArgs) : _function = null; + + Future execute([T? args]) async { + if (!_locked) { + _locked = true; + try { + if (_function != null) { + await _function(); + } else { + await _functionWithArgs!(args); + } + } finally { + _locked = false; + } + } + } +} 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/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/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/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, diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index 75bd15c07c..ac3ca1cacb 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/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index 5b2d6569c8..d65ceeeb4e 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( @@ -464,12 +468,13 @@ class WalletInfo implements IsarId { int restoreHeight = 0, String? walletIdOverride, String? otherDataJsonString, + AddressType? overrideAddressType, // added hack for spark view only wallets }) { return WalletInfo( coinName: coin.identifier, walletId: walletIdOverride ?? const Uuid().v1(), name: name, - mainAddressType: coin.defaultAddressType, + mainAddressType: overrideAddressType ?? coin.defaultAddressType, restoreHeight: restoreHeight, otherDataJsonString: otherDataJsonString, ); @@ -524,4 +529,5 @@ abstract class WalletInfoKeys { static const String mwebScanHeight = "mwebScanHeightKey"; static const String firoSparkUsedTagsCacheResetVersion = "firoSparkUsedTagsCacheResetVersionKey"; + static const String enableLegacyAddresses = "enableLegacyAddressesKey"; } 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/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 5cfc895831..8c69f79705 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'; @@ -10,6 +9,7 @@ import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; +import '../../../models/keys/view_only_wallet_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/extensions/extensions.dart'; import '../../../utilities/logger.dart'; @@ -24,7 +24,6 @@ import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; import '../wallet_mixin_interfaces/extended_keys_interface.dart'; import '../wallet_mixin_interfaces/spark_interface.dart'; -import '../../../models/keys/view_only_wallet_data.dart'; const sparkStartBlock = 819300; // (approx 18 Jan 2024) @@ -51,17 +50,6 @@ class FiroWallet extends Bip39HDWallet final Set _unconfirmedTxids = {}; - @override - Set get supportedAddressTypes { - if (isViewOnly && viewOnlyType == ViewOnlyWalletType.spark) { - return {AddressType.spark}; - } else { - final supportedAddressTypes = super.supportedAddressTypes; - supportedAddressTypes.add(AddressType.spark); - return supportedAddressTypes; - } - } - // =========================================================================== @override @@ -93,17 +81,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}; @@ -111,23 +97,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, @@ -166,13 +150,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, @@ -226,8 +209,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) => @@ -310,19 +294,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 @@ -357,7 +339,7 @@ class FiroWallet extends Bip39HDWallet output = output.copyWith(walletOwns: true); } else if (isSparkMint && isMySpark) { wasReceivedInThisWallet = true; - if (output.addresses.contains(sparkChangeAddress.value)) { + if (output.addresses.contains(sparkChangeAddress)) { changeAmountReceivedInThisWallet += output.value; } else { amountReceivedInThisWallet += output.value; @@ -415,10 +397,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) { @@ -502,11 +483,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; @@ -517,11 +497,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; @@ -683,18 +662,18 @@ class FiroWallet extends Bip39HDWallet @override Future> fetchAddressesForElectrumXScan() async { return await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.spark) - .or() - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.spark) + .or() + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); } @override @@ -787,53 +766,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/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 e45babf9f9..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'; @@ -110,10 +111,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,12 +131,24 @@ 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 { await super.init(); - final contractAddress = web3dart.EthereumAddress.fromHex( + final contractAddress = eth_wallet.EthereumAddress.fromHex( tokenContract.address, ); @@ -144,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( @@ -173,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) { @@ -217,11 +229,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); @@ -232,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, ), ); @@ -302,11 +313,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, 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/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index c325584ba4..c5dcd279b8 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; @@ -152,9 +154,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 +174,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 +297,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 +316,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 +343,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 +354,9 @@ mixin ElectrumXInterface satsPerVByte != null ? (satsPerVByte * vSizeForTwoOutPuts) : estimateTxFee( - vSize: vSizeForTwoOutPuts, - feeRatePerKB: selectedTxFeeRate, - ), + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ), ); Logging.instance.d("feeForTwoOutputs: $feeForTwoOutputs"); @@ -513,28 +512,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 +605,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 +665,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 +907,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 +1022,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 { @@ -1074,7 +1072,7 @@ mixin ElectrumXInterface ) async { final List
addressArray = []; int gapCounter = 0; - int highestIndexWithHistory = 0; + int highestIndexWithHistory = -1; for ( int index = 0; @@ -1118,10 +1116,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); @@ -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( @@ -1199,8 +1200,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, ); @@ -1211,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 @@ -1224,7 +1227,7 @@ mixin ElectrumXInterface } } - return (addresses: addressArray, index: index); + return (addresses: addressArray, index: highestIndexWithHistory); } Future>> fetchHistory( @@ -1391,21 +1394,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"); @@ -1646,57 +1646,16 @@ 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); 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], @@ -1828,19 +1787,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, @@ -1874,8 +1832,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; @@ -2042,6 +2000,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 ============================================ @@ -2063,6 +2072,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( @@ -2117,6 +2142,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 @@ -2177,11 +2220,11 @@ mixin ElectrumXInterface receiveFutures.add( canBatch ? checkGapsBatched( - txCountBatchSize, - root, - type, - receiveChain, - ) + txCountBatchSize, + root, + type, + receiveChain, + ) : checkGapsLinearly(root, type, receiveChain), ); } @@ -2201,11 +2244,11 @@ mixin ElectrumXInterface changeFutures.add( canBatch ? checkGapsBatched( - txCountBatchSize, - root, - type, - changeChain, - ) + txCountBatchSize, + root, + type, + changeChain, + ) : checkGapsLinearly(root, type, changeChain), ); } @@ -2217,48 +2260,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..35a00595a0 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,21 +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(); - - Logging.instance.f(pending); + final pending = await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .heightIsNull() + .and() + .blockHashIsNull() + .and() + .subTypeEqualTo(TransactionSubType.mweb) + .and() + .typeEqualTo(TransactionType.outgoing) + .findAll(); final client = await _client; for (final tx in pending) { @@ -391,11 +385,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 +497,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 +522,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 +551,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 +564,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 +586,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 +703,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 +861,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 +926,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, 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/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 212ecb4c3d..5e1560f0aa 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -17,6 +17,7 @@ import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; +import '../../../models/keys/view_only_wallet_data.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; import '../../../services/event_bus/global_event_bus.dart'; import '../../../services/spark_names_service.dart'; @@ -108,10 +109,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { late Address _currentSparkAddress; - late String viewKeyHex; + String? _viewKeyHex; + String? get sparkViewKey => _viewKeyHex!; - // Really we should just send change back to the same address. - late Address sparkChangeAddress; + Address? _sparkChangeAddress; + + String? get sparkChangeAddress => _sparkChangeAddress?.value; bool get isTestNet { return cryptoCurrency.network.isTestNet; @@ -132,13 +135,20 @@ mixin SparkInterface // private key data). int get sparkIndex => kDefaultSparkIndex; - Future
generateSparkAddress(int diversifier) async { - final sparkAddress = await libSpark.getAddressFromFullViewKey( - fullViewKeyHex: viewKeyHex, - index: sparkIndex, - diversifier: diversifier, - isTestNet: isTestNet, - ); + Future
_generateSparkAddress(int diversifier) async { + if (isViewOnly && viewOnlyType != .spark) { + throw Exception( + "Cannot generate a spark address for a non spark view only firo wallet", + ); + } + + final sparkAddress = + await computeWithLibSparkLogging(_getAddressFromFullViewKey, ( + fullViewKeyHex: _viewKeyHex!, + index: sparkIndex, + diversifier: diversifier, + isTestNet: isTestNet, + )); return Address( walletId: walletId, @@ -164,7 +174,7 @@ mixin SparkInterface }) async { return await computeWithLibSparkLogging(identifyCoinsStatic, ( walletId_: walletId, - viewKeyHex_: viewKeyHex, + viewKeyHex_: _viewKeyHex!, isTestNet_: isTestNet, anonymitySetCoins: anonymitySetCoins, groupId: groupId, @@ -266,6 +276,10 @@ mixin SparkInterface @override Future init() async { + if (isViewOnly && viewOnlyType != .spark) { + return super.init(); + } + try { final sparkUsedTagsResetVersion = info.otherData[WalletInfoKeys.firoSparkUsedTagsCacheResetVersion] @@ -284,13 +298,16 @@ mixin SparkInterface } if (isViewOnly) { - final walletData = - await getViewOnlyWalletData() as SparkViewOnlyWalletData; - viewKeyHex = walletData.viewKey; + final walletData = await getViewOnlyWalletData(); + if (walletData is SparkViewOnlyWalletData) { + _viewKeyHex = walletData.viewKey; + } else { + // TODO anything needed here? + } } else { final root = await getRootHDNode(); final privateKey = root.derivePath(sparkDerivationPath).privateKey.data; - viewKeyHex = libSpark.getFullViewKeyHexFromPrivateKeyData( + _viewKeyHex = libSpark.getFullViewKeyHexFromPrivateKeyData( privateKeyHex: privateKey.toHex, index: sparkIndex, ); @@ -298,8 +315,16 @@ mixin SparkInterface Address? address = await getCurrentReceivingSparkAddress(); if (address == null) { - address = await generateSparkAddress(1); + address = await _generateSparkAddress(1); await mainDB.putAddress(address); + if (isViewOnly && + viewOnlyType == .spark && + info.mainAddressType == .spark) { + await info.updateReceivingAddress( + newAddress: address.value, + isar: mainDB.isar, + ); + } } if (address.derivationIndex == -1) { @@ -307,7 +332,7 @@ mixin SparkInterface } _currentSparkAddress = address; - sparkChangeAddress = await generateSparkAddress(libSpark.sparkChange); + _sparkChangeAddress = await _generateSparkAddress(libSpark.sparkChange); } catch (e, s) { // do nothing, still allow user into wallet Logging.instance.e("$runtimeType init() failed", error: e, stackTrace: s); @@ -348,13 +373,25 @@ mixin SparkInterface } } - Future
generateNextSparkAddress() async { + Future
generateNextSparkAddress({required bool saveToDB}) async { int diversifier = _currentSparkAddress.derivationIndex + 1; if (diversifier == libSpark.sparkChange) { diversifier++; // ensure only receiving addresses are shown } - final newAddress = await generateSparkAddress(diversifier); + final newAddress = await _generateSparkAddress(diversifier); _currentSparkAddress = newAddress; + if (saveToDB) { + await mainDB.updateOrPutAddresses([newAddress]); + if (isViewOnly && + viewOnlyType == .spark && + info.mainAddressType == .spark) { + await info.updateReceivingAddress( + newAddress: newAddress.value, + isar: mainDB.isar, + ); + } + } + return newAddress; } @@ -618,7 +655,7 @@ mixin SparkInterface ), memo: txData.sparkRecipients![i].memo, isChange: - sparkChangeAddress.value == txData.sparkRecipients![i].address, + _sparkChangeAddress!.value == txData.sparkRecipients![i].address, )); } @@ -1299,18 +1336,11 @@ mixin SparkInterface } } - Future recoverViewOnlyWallet() async { - await recoverSparkWallet(latestSparkCoinId: 0); - } - Future refreshSparkNames() async { try { 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 @@ -1344,17 +1374,14 @@ mixin SparkInterface if (diversifier == libSpark.sparkChange) { diversifier++; } - final addressString = await generateSparkAddress(diversifier); + final addressString = await _generateSparkAddress(diversifier); myAddresses.add(addressString.value); 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< @@ -2244,11 +2271,11 @@ mixin SparkInterface final String destinationAddress; switch (cryptoCurrency.network) { case CryptoCurrencyNetwork.main: - destinationAddress = libSpark.stage3DevelopmentFundAddressMainNet; + destinationAddress = libSpark.stage3CommunityFundAddressMainNet; break; case CryptoCurrencyNetwork.test: - destinationAddress = libSpark.stage3DevelopmentFundAddressTestNet; + destinationAddress = libSpark.stage3CommunityFundAddressTestNet; break; default: @@ -2455,3 +2482,12 @@ int _estSparkFeeComputeFunc( return est; } + +Future _getAddressFromFullViewKey( + ({String fullViewKeyHex, int index, int diversifier, bool isTestNet}) args, +) => libSpark.getAddressFromFullViewKey( + fullViewKeyHex: args.fullViewKeyHex, + index: args.index, + diversifier: args.diversifier, + isTestNet: args.isTestNet, +); 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/options.dart b/lib/widgets/options.dart index 36bccd0c66..119696c010 100644 --- a/lib/widgets/options.dart +++ b/lib/widgets/options.dart @@ -22,7 +22,6 @@ class Options extends StatefulWidget { this.texts, this.onValueChanged, required this.selectedIndex, - this.controller, required this.onColor, required this.offColor, this.decoration, @@ -32,7 +31,6 @@ class Options extends StatefulWidget { final List? texts; final void Function(int)? onValueChanged; final int selectedIndex; - final DSBController? controller; final Color onColor; final Color offColor; final BoxDecoration? decoration; @@ -45,7 +43,6 @@ class OptionsState extends State { late final BoxDecoration? decoration; late final Color onColor; late final Color offColor; - late final DSBController? controller; final bool isDesktop = Util.isDesktop; @@ -62,14 +59,9 @@ class OptionsState extends State { onColor = widget.onColor; offColor = widget.offColor; decoration = widget.decoration; - controller = widget.controller; _selectedIndex = widget.selectedIndex; valueListener = ValueNotifier(_selectedIndex.toDouble()); - widget.controller?.activate = () { - _selectedIndex = (_selectedIndex + 1) % (widget.texts?.length ?? 1); - valueListener.value = _selectedIndex.toDouble(); - }; super.initState(); } @@ -90,7 +82,10 @@ class OptionsState extends State { final localPosition = box.globalToLocal(details.globalPosition); final optionsCount = widget.texts?.length ?? 1; final optionWidth = box.size.width / optionsCount; - final tappedIndex = (localPosition.dx / optionWidth).floor().clamp(0, optionsCount - 1); + final tappedIndex = (localPosition.dx / optionWidth).floor().clamp( + 0, + optionsCount - 1, + ); if (_selectedIndex != tappedIndex) { _selectedIndex = tappedIndex; widget.onValueChanged?.call(_selectedIndex); @@ -109,9 +104,7 @@ class OptionsState extends State { duration: tapAnimationDuration, height: constraint.maxHeight, width: constraint.maxWidth, - decoration: decoration?.copyWith( - color: offColor, - ), + decoration: decoration?.copyWith(color: offColor), ); }, ), @@ -121,9 +114,11 @@ class OptionsState extends State { key: const Key("draggableSwitchButtonSwitch"), onHorizontalDragStart: (_) => _isDragging = true, onHorizontalDragUpdate: (details) { - valueListener.value = (valueListener.value + - details.delta.dx / (constraint.maxWidth / optionsCount)) - .clamp(0.0, optionsCount - 1.0); + valueListener.value = + (valueListener.value + + details.delta.dx / + (constraint.maxWidth / optionsCount)) + .clamp(0.0, optionsCount - 1.0); }, onHorizontalDragEnd: (details) { final int oldValue = _selectedIndex; @@ -141,9 +136,7 @@ class OptionsState extends State { duration: tapAnimationDuration, height: constraint.maxHeight, width: constraint.maxWidth / optionsCount, - decoration: decoration?.copyWith( - color: onColor, - ), + decoration: decoration?.copyWith(color: onColor), ); }, ), @@ -152,7 +145,9 @@ class OptionsState extends State { animation: valueListener, builder: (context, child) { return AnimatedAlign( - duration: _isDragging ? Duration.zero : tapAnimationDuration, + duration: _isDragging + ? Duration.zero + : tapAnimationDuration, alignment: Alignment( (valueListener.value * 2 / (optionsCount - 1)) - 1, 0.5, @@ -174,31 +169,31 @@ class OptionsState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (widget.icons != null && widget.icons!.length > index) + if (widget.icons != null && + widget.icons!.length > index) SvgPicture.asset( widget.icons![index], width: 12, height: 14, color: isDesktop ? _selectedIndex != index - ? Theme.of(context) - .extension()! - .accentColorBlue - : Theme.of(context) - .extension()! - .buttonTextSecondary + ? Theme.of(context) + .extension()! + .accentColorBlue + : Theme.of(context) + .extension()! + .buttonTextSecondary : _selectedIndex != index - ? Theme.of(context) - .extension()! - .textDark - : Theme.of(context) - .extension()! - .textSubtitle1, - ), - if (widget.icons != null && widget.icons!.length > index) - const SizedBox( - width: 5, + ? Theme.of( + context, + ).extension()!.textDark + : Theme.of( + context, + ).extension()!.textSubtitle1, ), + if (widget.icons != null && + widget.icons!.length > index) + const SizedBox(width: 5), Flexible( child: Text( widget.texts?[index] ?? "", @@ -211,20 +206,20 @@ class OptionsState extends State { ).copyWith( color: _selectedIndex != index ? Theme.of(context) - .extension()! - .accentColorBlue + .extension()! + .accentColorBlue : Theme.of(context) - .extension()! - .buttonTextSecondary, + .extension()! + .buttonTextSecondary, ) : STextStyles.smallMed12(context).copyWith( color: _selectedIndex != index ? Theme.of(context) - .extension()! - .textDark + .extension()! + .textDark : Theme.of(context) - .extension()! - .textSubtitle1, + .extension()! + .textSubtitle1, ), ), ), @@ -242,8 +237,3 @@ class OptionsState extends State { ); } } - -class DSBController { - VoidCallback? activate; - bool? isOn; -} diff --git a/lib/widgets/qr_scanner.dart b/lib/widgets/qr_scanner.dart index 66941ac9d3..cd19c839f5 100644 --- a/lib/widgets/qr_scanner.dart +++ b/lib/widgets/qr_scanner.dart @@ -1,45 +1,81 @@ +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/if_not_already.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; + + 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 + // 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/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, + ), + ), + ); + } +} 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/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/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, ), - ), ], ), ), 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/lib/wl_gen/interfaces/lib_spark_interface.dart b/lib/wl_gen/interfaces/lib_spark_interface.dart index 6f0df295bc..92404f57f8 100644 --- a/lib/wl_gen/interfaces/lib_spark_interface.dart +++ b/lib/wl_gen/interfaces/lib_spark_interface.dart @@ -14,8 +14,8 @@ abstract class LibSparkInterface { int get maxNameLength; int get maxAdditionalInfoLengthBytes; String get nameRegexString; - String get stage3DevelopmentFundAddressMainNet; - String get stage3DevelopmentFundAddressTestNet; + String get stage3CommunityFundAddressMainNet; + String get stage3CommunityFundAddressTestNet; List get standardSparkNamesFee; void initSparkLogging(Level level); 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 efbd5cec2b..b98546188b 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,74 +432,74 @@ packages: dependency: "direct main" description: name: cs_monero - sha256: "2bc89f862b4a4bc5312999a35d266db035d2e8760736662e148f07d2ab36e43d" + sha256: b174f40e1887eb589e1e9aa99de8e9d0bc97b543f2330d5e5e7b01a6d313a9c2 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.2.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 +512,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: @@ -623,10 +616,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 +656,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: @@ -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: @@ -960,12 +955,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: @@ -992,14 +986,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: @@ -1034,8 +1020,8 @@ packages: dependency: "direct main" description: path: "." - ref: "84a139a25ab1691762002fafcae351e3d444a5c7" - resolved-ref: "84a139a25ab1691762002fafcae351e3d444a5c7" + ref: "4bd84c88e1b2a817a2604ec53030634cc3304bc7" + resolved-ref: "4bd84c88e1b2a817a2604ec53030634cc3304bc7" url: "https://github.com/cypherstack/flutter_libsparkmobile.git" source: git version: "0.1.0" @@ -1209,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" @@ -1226,10 +1212,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: @@ -1322,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: @@ -1362,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: @@ -1383,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: @@ -1444,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: @@ -1568,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: @@ -1600,10 +1586,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: @@ -1612,14 +1598,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" - mobile_scanner: + mobile_app_privacy: dependency: "direct main" description: - name: mobile_scanner - sha256: "5e7e09d904dc01de071b79b3f3789b302b0ed3c9c963109cd3f83ad90de62ecf" - url: "https://pub.dev" - source: hosted - version: "7.1.2" + 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: @@ -1672,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: @@ -1849,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" @@ -1881,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: @@ -1893,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: @@ -1941,6 +1937,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: @@ -1989,14 +1993,30 @@ 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: 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: @@ -2168,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: @@ -2224,32 +2244,32 @@ 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: path: "." - ref: d000cc245e51d3ff50e6467960fb3d9159d5b2a9 - resolved-ref: d000cc245e51d3ff50e6467960fb3d9159d5b2a9 + ref: "210fe8bbb93a9e0bcbc8e99894c261f53097d5e2" + resolved-ref: "210fe8bbb93a9e0bcbc8e99894c261f53097d5e2" url: "https://github.com/cypherstack/tezart.git" source: git version: "2.0.5" @@ -2289,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: @@ -2327,13 +2347,13 @@ packages: source: hosted version: "2.2.2" unorm_dart: - dependency: transitive + 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: @@ -2402,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: @@ -2488,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: @@ -2523,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: @@ -2613,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: @@ -2658,5 +2678,5 @@ packages: source: hosted version: "0.2.4" sdks: - dart: ">=3.9.0 <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/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 6a4ad28419..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, @@ -93,11 +105,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), 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/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' 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/linux/my_application.cc b/scripts/app_config/templates/linux/my_application.cc index a6eec39569..58584f452f 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); @@ -37,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"); @@ -54,9 +62,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 +98,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 +126,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, diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index aa097a4580..0723e2d5b1 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: @@ -40,7 +40,7 @@ dependencies: # flutter_libsparkmobile: # git: # url: https://github.com/cypherstack/flutter_libsparkmobile.git -# ref: 83928dbeb5f150be57e03b7e9f6d720ef7f7cf7b +# ref: 4bd84c88e1b2a817a2604ec53030634cc3304bc7 # %%END_ENABLE_FIRO%% # %%ENABLE_EPIC%% @@ -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.2.0 +# cs_monero_flutter_libs: 2.0.1 # %%END_ENABLE_XMR%% # %%ENABLE_WOW%% @@ -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 @@ -149,18 +155,16 @@ 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 - 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 - mobile_scanner: ^7.0.1 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 @@ -173,13 +177,10 @@ 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 - file_picker: - git: - url: https://github.com/cypherstack/flutter_file_picker.git - ref: b2849e63e1d418ad8d943c886cd3f4ed20d0ff23 + lottie: ^3.3.2 + 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 @@ -188,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 @@ -209,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. @@ -244,13 +251,24 @@ 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 mweb_client: ^0.2.0 fixnum: ^1.1.1 + saf_util: ^0.11.0 + saf_stream: ^0.12.3 + 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: @@ -273,7 +291,11 @@ dev_dependencies: flutter_native_splash: image: assets/icon/splash.png color: "F7F7F7" - android_disable_fullscreen: true + color_dark_ios: "2A2D34" + color_dark_android: "2A2D34" + android_12: + color: "F7F7F7" + color_dark: "2A2D34" dependency_overrides: logger: @@ -287,23 +309,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: @@ -312,16 +328,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: @@ -388,6 +407,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/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. 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/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", + ); + }); + }); +} 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/FIRO_lib_spark_interface_impl.template.dart b/tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart index f4120a8f6c..f2998be060 100644 --- a/tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart +++ b/tool/wl_templates/FIRO_lib_spark_interface_impl.template.dart @@ -48,12 +48,12 @@ class _LibSparkInterfaceImpl extends LibSparkInterface { String get nameRegexString => kNameRegexString; @override - String get stage3DevelopmentFundAddressMainNet => - kStage3DevelopmentFundAddressMainNet; + String get stage3CommunityFundAddressMainNet => + kStage3CommunityFundAddressMainNet; @override - String get stage3DevelopmentFundAddressTestNet => - kStage3DevelopmentFundAddressTestNet; + String get stage3CommunityFundAddressTestNet => + kStage3DCommunityFundAddressTestNet; @override List get standardSparkNamesFee => 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..f7bec47186 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,46 @@ 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"; + + static String? _cachedWinExePath; + + Future _prepareWindowsExeDirPath() async { + if (_cachedWinExePath == null) { + final dir = (await StackFileSystem.applicationMwebdDirectory( + "dummy", + )).parent.path; + + final exe = File(join(dir, _kExe)); + + if (await exe.exists()) { + await exe.delete(); + } + + 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(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 _cachedWinExePath!; + } + @override - Future<({OpaqueMwebdServer server, int port})> createAndStartServer( + Future<({DynamicObject server, int port})> createAndStartServer( CryptoCurrencyNetwork net, { required String chain, required String dataDir, @@ -37,36 +78,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; + } } } 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