diff --git a/assets/svg/coin_icons/Wownero.svg b/assets/svg/coin_icons/Wownero.svg new file mode 100644 index 000000000..f7a90e94c --- /dev/null +++ b/assets/svg/coin_icons/Wownero.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/svg/drd-icon.svg b/assets/svg/drd-icon.svg new file mode 100644 index 000000000..7f65c820d --- /dev/null +++ b/assets/svg/drd-icon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 4bffa40cb..8e3afd002 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 4bffa40cb60ad3d98cf0ea5b5d819f3f4895dcd6 +Subproject commit 8e3afd002968d21a3de788569356587a70818022 diff --git a/lib/main.dart b/lib/main.dart index be1d11d0c..0be26c39e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_libmonero/monero/monero.dart'; +import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -84,7 +85,6 @@ void main() async { appDirectory = (await getLibraryDirectory()); } // FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - await Hive.initFlutter(appDirectory.path); if (!(Logging.isArmLinux || Logging.isTestEnv)) { final isar = await Isar.open( [LogSchema], @@ -128,11 +128,14 @@ void main() async { Hive.registerAdapter(NodeAdapter()); - Hive.registerAdapter(WalletInfoAdapter()); + if (!Hive.isAdapterRegistered(WalletInfoAdapter().typeId)) { + Hive.registerAdapter(WalletInfoAdapter()); + } Hive.registerAdapter(WalletTypeAdapter()); Hive.registerAdapter(UnspentCoinsInfoAdapter()); + await Hive.initFlutter(appDirectory.path); await Hive.openBox(DB.boxNameDBInfo); int dbVersion = DB.instance.get( @@ -143,6 +146,7 @@ void main() async { } monero.onStartup(); + wownero.onStartup(); await Hive.openBox(DB.boxNameTheme); diff --git a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart index ffd32479d..b3be99e25 100644 --- a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart @@ -37,9 +37,11 @@ class CreateOrRestoreWalletView extends StatelessWidget { body: SizedBox( width: 480, child: Column( - mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ + const Spacer( + flex: 10, + ), CreateRestoreWalletTitle( coin: coin, isDesktop: isDesktop, @@ -67,6 +69,9 @@ class CreateOrRestoreWalletView extends StatelessWidget { coin: coin, isDesktop: isDesktop, ), + const Spacer( + flex: 15, + ), ], ), ), diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index cff9fd2cf..f7dbe3d33 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -151,6 +151,10 @@ class _NameYourWalletViewState extends ConsumerState { crossAxisAlignment: isDesktop ? CrossAxisAlignment.center : CrossAxisAlignment.stretch, children: [ + if (isDesktop) + const Spacer( + flex: 10, + ), if (!isDesktop) const Spacer( flex: 1, @@ -163,7 +167,7 @@ class _NameYourWalletViewState extends ConsumerState { height: 100, ), SizedBox( - height: isDesktop ? 24 : 16, + height: isDesktop ? 0 : 16, ), Text( "Name your ${coin.prettyName} wallet", @@ -358,6 +362,10 @@ class _NameYourWalletViewState extends ConsumerState { ), ), ), + if (isDesktop) + const Spacer( + flex: 15, + ), ], ); } diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart index 207451569..faab6d08c 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart @@ -90,224 +90,233 @@ class _NewWalletRecoveryPhraseViewState Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); return WillPopScope( - onWillPop: onWillPop, - child: MasterScaffold( - isDesktop: isDesktop, - appBar: isDesktop - ? DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await delete(); + onWillPop: onWillPop, + child: MasterScaffold( + isDesktop: isDesktop, + appBar: isDesktop + ? DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { + await delete(); - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName( - NewWalletRecoveryPhraseWarningView.routeName, - ), - ); - } - // Navigator.of(context).pop(); - }, - ), - trailing: ExitToMyStackButton( - onPressed: () async { - await delete(); - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(DesktopHomeView.routeName), - ); - } - }, - ), - ) - : AppBar( - leading: AppBarBackButton( - onPressed: () async { - await delete(); + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName( + NewWalletRecoveryPhraseWarningView.routeName, + ), + ); + } + // Navigator.of(context).pop(); + }, + ), + trailing: ExitToMyStackButton( + onPressed: () async { + await delete(); + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopHomeView.routeName), + ); + } + }, + ), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () async { + await delete(); - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName( - NewWalletRecoveryPhraseWarningView.routeName, - ), - ); - } - }, - ), - actions: [ - Padding( - padding: const EdgeInsets.all(10), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName( + NewWalletRecoveryPhraseWarningView.routeName, + ), + ); + } + }, + ), + actions: [ + Padding( + padding: const EdgeInsets.all(10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + color: Theme.of(context) + .extension()! + .background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.copy, + width: 24, + height: 24, color: Theme.of(context) .extension()! - .background, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.copy, - width: 24, - height: 24, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - ), - onPressed: () async { - await _copy(); - }, + .topNavIconPrimary, ), - ), - ), - ], - ), - body: Container( - color: Theme.of(context).extension()!.background, - width: isDesktop ? 600 : null, - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!isDesktop) - const SizedBox( - height: 4, - ), - if (!isDesktop) - Text( - _manager.walletName, - textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - SizedBox( - height: isDesktop ? 24 : 4, - ), - Text( - "Recovery Phrase", - textAlign: TextAlign.center, - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - color: isDesktop - ? Theme.of(context) - .extension()! - .background - : Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius), - ), - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(12), - child: Text( - "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", - textAlign: TextAlign.center, - style: isDesktop - ? STextStyles.desktopSubtitleH2(context) - : STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ), - SizedBox( - height: isDesktop ? 21 : 8, - ), - if (!isDesktop) - Expanded( - child: SingleChildScrollView( - child: MnemonicTable( - words: _mnemonic, - isDesktop: isDesktop, - ), - ), - ), - if (isDesktop) - MnemonicTable( - words: _mnemonic, - isDesktop: isDesktop, - ), - SizedBox( - height: isDesktop ? 24 : 16, - ), - if (isDesktop) - SizedBox( - height: 70, - child: TextButton( onPressed: () async { await _copy(); }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 20, - height: 20, - ), - const SizedBox( - width: 10, - ), - Text( - "Copy to clipboard", - style: STextStyles.desktopButtonSecondaryEnabled( - context), - ) - ], - ), - ), - ), - if (isDesktop) - const SizedBox( - height: 16, - ), - ConstrainedBox( - constraints: BoxConstraints( - minHeight: isDesktop ? 70 : 0, - ), - child: TextButton( - onPressed: () async { - final int next = Random().nextInt(_mnemonic.length); - ref - .read(verifyMnemonicWordIndexStateProvider.state) - .update((state) => next); - - ref - .read(verifyMnemonicCorrectWordStateProvider.state) - .update((state) => _mnemonic[next]); - - unawaited(Navigator.of(context).pushNamed( - VerifyRecoveryPhraseView.routeName, - arguments: Tuple2(_manager, _mnemonic), - )); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "I saved my recovery phrase", - style: isDesktop - ? STextStyles.desktopButtonEnabled(context) - : STextStyles.button(context), ), ), ), ], ), + body: Container( + color: Theme.of(context).extension()!.background, + width: isDesktop ? 600 : null, + child: Padding( + padding: + isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (isDesktop) + const Spacer( + flex: 10, + ), + if (!isDesktop) + const SizedBox( + height: 4, + ), + if (!isDesktop) + Text( + _manager.walletName, + textAlign: TextAlign.center, + style: STextStyles.label(context).copyWith( + fontSize: 12, + ), + ), + SizedBox( + height: isDesktop ? 24 : 4, + ), + Text( + "Recovery Phrase", + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + color: isDesktop + ? Theme.of(context).extension()!.background + : Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius), + ), + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), + child: Text( + "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + SizedBox( + height: isDesktop ? 21 : 8, + ), + if (!isDesktop) + Expanded( + child: SingleChildScrollView( + child: MnemonicTable( + words: _mnemonic, + isDesktop: isDesktop, + ), + ), + ), + if (isDesktop) + MnemonicTable( + words: _mnemonic, + isDesktop: isDesktop, + ), + SizedBox( + height: isDesktop ? 24 : 16, + ), + if (isDesktop) + SizedBox( + height: 70, + child: TextButton( + onPressed: () async { + await _copy(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + const SizedBox( + width: 10, + ), + Text( + "Copy to clipboard", + style: STextStyles.desktopButtonSecondaryEnabled( + context), + ) + ], + ), + ), + ), + if (isDesktop) + const SizedBox( + height: 16, + ), + ConstrainedBox( + constraints: BoxConstraints( + minHeight: isDesktop ? 70 : 0, + ), + child: TextButton( + onPressed: () async { + final int next = Random().nextInt(_mnemonic.length); + ref + .read(verifyMnemonicWordIndexStateProvider.state) + .update((state) => next); + + ref + .read(verifyMnemonicCorrectWordStateProvider.state) + .update((state) => _mnemonic[next]); + + unawaited(Navigator.of(context).pushNamed( + VerifyRecoveryPhraseView.routeName, + arguments: Tuple2(_manager, _mnemonic), + )); + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "I saved my recovery phrase", + style: isDesktop + ? STextStyles.desktopButtonEnabled(context) + : STextStyles.button(context), + ), + ), + ), + if (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 603e5cca1..83dc43933 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 @@ -59,7 +59,9 @@ class _NewWalletRecoveryPhraseWarningViewState final _numberOfPhraseWords = coin == Coin.monero ? Constants.seedPhraseWordCountMonero - : Constants.seedPhraseWordCountBip39; + : coin == Coin.wownero + ? 14 + : Constants.seedPhraseWordCountBip39; return MasterScaffold( isDesktop: isDesktop, @@ -79,9 +81,14 @@ class _NewWalletRecoveryPhraseWarningViewState ? CrossAxisAlignment.center : CrossAxisAlignment.stretch, children: [ - const SizedBox( - height: 4, - ), + if (isDesktop) + const Spacer( + flex: 10, + ), + if (!isDesktop) + const SizedBox( + height: 4, + ), if (!isDesktop) Text( walletName, @@ -90,9 +97,10 @@ class _NewWalletRecoveryPhraseWarningViewState fontSize: 12, ), ), - const SizedBox( - height: 4, - ), + if (!isDesktop) + const SizedBox( + height: 4, + ), Text( "Recovery Phrase", textAlign: TextAlign.center, @@ -130,6 +138,7 @@ class _NewWalletRecoveryPhraseWarningViewState builder: (_, ref, __) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, children: [ GestureDetector( onTap: () { @@ -291,6 +300,10 @@ class _NewWalletRecoveryPhraseWarningViewState }, ), ), + if (isDesktop) + const Spacer( + flex: 15, + ), ], ), ), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/confirm_recovery_dialog.dart b/lib/pages/add_wallet_views/restore_wallet_view/confirm_recovery_dialog.dart index 6ccc44d03..8c50b75b4 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/confirm_recovery_dialog.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/confirm_recovery_dialog.dart @@ -1,6 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class ConfirmRecoveryDialog extends StatelessWidget { @@ -11,40 +18,94 @@ class ConfirmRecoveryDialog extends StatelessWidget { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - return true; - }, - child: StackDialog( - title: "Are you ready?", - message: - "Restoring your wallet may take a while. Please do not exit this screen once the process is started.", - leftButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - Navigator.of(context).pop(); - }, + if (Util.isDesktop) { + return DesktopDialog( + child: Column( + children: [ + const DesktopDialogCloseButton(), + const SizedBox( + height: 5, + ), + SvgPicture.asset( + Assets.svg.drd, + width: 99, + height: 70, + ), + const Spacer(), + Text( + "Restore wallet", + style: STextStyles.desktopH2(context), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 16, + ), + Text( + "Restoring your wallet may take a while.\nPlease do not exit this screen once the process is started.", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + textAlign: TextAlign.center, + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Restore", + onPressed: () { + Navigator.of(context).pop(); + onConfirm.call(); + }, + ), + ) + ], + ), + ) + ], ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Restore", - style: STextStyles.button(context), + ); + } else { + return WillPopScope( + onWillPop: () async { + return true; + }, + child: StackDialog( + title: "Are you ready?", + message: + "Restoring your wallet may take a while. Please do not exit this screen once the process is started.", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + rightButton: PrimaryButton( + label: "Restore", + onPressed: () { + Navigator.of(context).pop(); + onConfirm.call(); + }, ), - onPressed: () { - Navigator.of(context).pop(); - onConfirm.call(); - }, ), - ), - ); + ); + } } } diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index ffa10afc4..0edc110be 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_o import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_options_platform_layout.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -79,59 +80,30 @@ class _RestoreOptionsViewState extends ConsumerState { colorArrowNext: Theme.of(context).extension()!.textSubtitle1, colorArrowPrevious: Theme.of(context).extension()!.textSubtitle1, - textStyleButtonNegative: GoogleFonts.inter( + textStyleButtonNegative: STextStyles.datePicker600(context).copyWith( color: baseColor, - letterSpacing: 0.5, - fontSize: 16, - fontWeight: FontWeight.w600, ), - textStyleButtonPositive: GoogleFonts.inter( + textStyleButtonPositive: STextStyles.datePicker600(context).copyWith( color: baseColor, - letterSpacing: 0.5, - fontSize: 16, - fontWeight: FontWeight.w600, ), - textStyleCurrentDayOnCalendar: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w400, - letterSpacing: 0.5, - color: Theme.of(context).extension()!.accentColorDark, - ), - textStyleDayHeader: GoogleFonts.inter( - letterSpacing: 0.5, - color: Theme.of(context).extension()!.accentColorDark, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - textStyleDayOnCalendar: GoogleFonts.inter( + textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context), + textStyleDayHeader: STextStyles.datePicker600(context), + textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith( color: baseColor, - fontSize: 12, - fontWeight: FontWeight.w400, - letterSpacing: 0.5, ), - textStyleDayOnCalendarDisabled: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w400, - letterSpacing: 0.5, + textStyleDayOnCalendarDisabled: + STextStyles.datePicker400(context).copyWith( color: Theme.of(context).extension()!.textSubtitle3, ), - textStyleDayOnCalendarSelected: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w400, - letterSpacing: 0.5, + textStyleDayOnCalendarSelected: + STextStyles.datePicker400(context).copyWith( color: Theme.of(context).extension()!.popupBG, ), - textStyleMonthYearHeader: GoogleFonts.inter( - letterSpacing: 0.5, + textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith( color: Theme.of(context).extension()!.textSubtitle1, - fontSize: 16, - fontWeight: FontWeight.w600, ), - textStyleYearButton: GoogleFonts.inter( - letterSpacing: 0.5, + textStyleYearButton: STextStyles.datePicker600(context).copyWith( color: Theme.of(context).extension()!.textWhite, - fontSize: 16, - fontWeight: FontWeight.w600, ), textStyleButtonAction: GoogleFonts.inter(), ); @@ -139,16 +111,10 @@ class _RestoreOptionsViewState extends ConsumerState { MaterialRoundedYearPickerStyle _buildYearPickerStyle() { return MaterialRoundedYearPickerStyle( - textStyleYear: GoogleFonts.inter( - letterSpacing: 0.5, + textStyleYear: STextStyles.datePicker600(context).copyWith( color: Theme.of(context).extension()!.textSubtitle2, - fontWeight: FontWeight.w600, - fontSize: 16, ), - textStyleYearSelected: GoogleFonts.inter( - letterSpacing: 0.5, - color: Theme.of(context).extension()!.accentColorDark, - fontWeight: FontWeight.w600, + textStyleYearSelected: STextStyles.datePicker600(context).copyWith( fontSize: 18, ), ); @@ -232,11 +198,13 @@ class _RestoreOptionsViewState extends ConsumerState { final lengths = Constants.possibleLengthsForCoin(coin).toList(); - return DesktopScaffold( + return MasterScaffold( + isDesktop: isDesktop, appBar: isDesktop ? const DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), ) : AppBar( leading: AppBarBackButton( @@ -260,10 +228,9 @@ class _RestoreOptionsViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (!isDesktop) - const Spacer( - flex: 1, - ), + Spacer( + flex: isDesktop ? 10 : 1, + ), if (!isDesktop) Image( image: AssetImage( @@ -272,7 +239,7 @@ class _RestoreOptionsViewState extends ConsumerState { height: 100, ), SizedBox( - height: isDesktop ? 24 : 16, + height: isDesktop ? 0 : 16, ), Text( "Restore options", @@ -417,6 +384,11 @@ class _RestoreOptionsViewState extends ConsumerState { isDesktop: isDesktop, onPressed: _nextEnabled ? nextPressed : null, ), + + if (isDesktop) + const Spacer( + flex: 15, + ), ], ), ), 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 a0ac25383..68f116a95 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widge import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/manager.dart'; @@ -36,8 +37,14 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; +import 'package:stackwallet/widgets/table_view/table_view.dart'; +import 'package:stackwallet/widgets/table_view/table_view_cell.dart'; +import 'package:stackwallet/widgets/table_view/table_view_row.dart'; import 'package:wakelock/wakelock.dart'; class RestoreWalletView extends ConsumerStatefulWidget { @@ -193,7 +200,7 @@ class _RestoreWalletViewState extends ConsumerState { // TODO: do actual check to make sure it is a valid mnemonic for monero if (bip39.validateMnemonic(mnemonic) == false && - !(widget.coin == Coin.monero)) { + !(widget.coin == Coin.monero || widget.coin == Coin.wownero)) { unawaited(showFloatingFlushBar( type: FlushBarType.warning, message: "Invalid seed phrase!", @@ -404,6 +411,7 @@ class _RestoreWalletViewState extends ConsumerState { prefix, style: STextStyles.fieldLabel(context).copyWith( color: prefixColor, + fontSize: Util.isDesktop ? 16 : 14, ), ), ), @@ -412,7 +420,7 @@ class _RestoreWalletViewState extends ConsumerState { minWidth: 16, minHeight: 16, maxWidth: 36, - maxHeight: 20, + maxHeight: 32, ), suffixIconConstraints: const BoxConstraints( minWidth: 16, @@ -529,193 +537,483 @@ class _RestoreWalletViewState extends ConsumerState { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 50)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("restoreWalletViewQrCodeButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension()!.background, - icon: QrCodeIcon( - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - onPressed: scanMnemonicQr, + final isDesktop = Util.isDesktop; + 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 (mounted) { + Navigator.of(context).pop(); + } + }, ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("restoreWalletPasteButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension()!.background, - icon: ClipboardIcon( - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("restoreWalletViewQrCodeButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension()! + .background, + icon: QrCodeIcon( + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + onPressed: scanMnemonicQr, + ), + ), ), - onPressed: pasteMnemonic, - ), + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("restoreWalletPasteButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension()! + .background, + icon: ClipboardIcon( + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + onPressed: pasteMnemonic, + ), + ), + ), + ], ), - ), - ], - ), body: Container( color: Theme.of(context).extension()!.background, child: Padding( padding: const EdgeInsets.all(12.0), child: Column( children: [ - Text( - widget.walletName, - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, + if (isDesktop) + const Spacer( + flex: 10, + ), + if (!isDesktop) + Text( + widget.walletName, + style: STextStyles.itemSubtitle(context), + ), + SizedBox( + height: isDesktop ? 0 : 4, ), Text( "Recovery phrase", - style: STextStyles.pageTitleH1(context), + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), - const SizedBox( - height: 8, + SizedBox( + height: isDesktop ? 16 : 8, ), Text( "Enter your $_seedWordCount-word recovery phrase.", - style: STextStyles.subtitle(context), + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), ), - const SizedBox( - height: 10, + SizedBox( + height: isDesktop ? 16 : 10, ), - Expanded( - child: SingleChildScrollView( - controller: controller, - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: pasteMnemonic, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.clipboard, + width: 22, + height: 22, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + const SizedBox( + width: 8, + ), + Text( + "Paste", + style: STextStyles + .desktopButtonSmallSecondaryEnabled(context), + ) + ], + ), + ), + ), + ], + ), + if (isDesktop) + const SizedBox( + height: 20, + ), + if (isDesktop) + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 1008, + ), + child: Builder( + builder: (BuildContext context) { + const cols = 4; + final int rows = _seedWordCount ~/ cols; + final int remainder = _seedWordCount % cols; + + return Column( children: [ - for (int i = 1; i <= _seedWordCount; i++) - Column( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 4), - child: TextFormField( - textCapitalization: TextCapitalization.none, - key: Key("restoreMnemonicFormField_$i"), - decoration: _getInputDecorationFor( - _inputStatuses[i - 1], "$i"), - autovalidateMode: - AutovalidateMode.onUserInteraction, - selectionControls: - i == 1 ? textSelectionControls : null, - onChanged: (value) { - if (value.isEmpty) { - setState(() { - _inputStatuses[i - 1] = - FormInputStatus.empty; - }); - } else if (_isValidMnemonicWord( - value.trim().toLowerCase())) { - setState(() { - _inputStatuses[i - 1] = - FormInputStatus.valid; - }); - } else { - setState(() { - _inputStatuses[i - 1] = - FormInputStatus.invalid; - }); - } - }, - controller: _controllers[i - 1], - style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension()! - .overlay, - ), - ), - ), - if (_inputStatuses[i - 1] == - FormInputStatus.invalid) - Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - bottom: 4.0, - ), - child: Text( - "Please check spelling", - textAlign: TextAlign.left, - style: - STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .textError, + Form( + key: _formKey, + child: TableView( + shrinkWrap: true, + rowSpacing: 20, + rows: [ + for (int i = 0; i < rows; i++) + TableViewRow( + crossAxisAlignment: + CrossAxisAlignment.start, + spacing: 16, + cells: [ + for (int j = 1; j <= cols; j++) + TableViewCell( + flex: 1, + child: Column( + children: [ + TextFormField( + textCapitalization: + TextCapitalization.none, + key: Key( + "restoreMnemonicFormField_$i"), + decoration: + _getInputDecorationFor( + _inputStatuses[ + i * 4 + j - 1], + "${i * 4 + j}"), + autovalidateMode: + AutovalidateMode + .onUserInteraction, + selectionControls: + i * 4 + j - 1 == 1 + ? textSelectionControls + : null, + onChanged: (value) { + if (value.isEmpty) { + setState(() { + _inputStatuses[ + i * 4 + j - 1] = + FormInputStatus.empty; + }); + } else if (_isValidMnemonicWord( + value + .trim() + .toLowerCase())) { + setState(() { + _inputStatuses[ + i * 4 + j - 1] = + FormInputStatus.valid; + }); + } else { + setState(() { + _inputStatuses[ + i * 4 + j - 1] = + FormInputStatus + .invalid; + }); + } + }, + controller: + _controllers[i * 4 + j - 1], + style: + STextStyles.field(context) + .copyWith( + color: Theme.of(context) + .extension()! + .overlay, + fontSize: isDesktop ? 16 : 14, + ), + ), + if (_inputStatuses[ + i * 4 + j - 1] == + FormInputStatus.invalid) + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: + const EdgeInsets.only( + left: 12.0, + bottom: 4.0, + ), + child: Text( + "Please check spelling", + textAlign: TextAlign.left, + style: STextStyles.label( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textError, + ), + ), + ), + ) + ], + ), ), - ), - ), - ) + ], + expandingChild: null, + ), + if (remainder > 0) + TableViewRow( + spacing: 16, + cells: [ + for (int i = rows * cols; + i < _seedWordCount; + i++) ...[ + TableViewCell( + flex: 1, + child: Column( + children: [ + TextFormField( + textCapitalization: + TextCapitalization.none, + key: Key( + "restoreMnemonicFormField_$i"), + decoration: + _getInputDecorationFor( + _inputStatuses[i], + "${i + 1}"), + autovalidateMode: + AutovalidateMode + .onUserInteraction, + selectionControls: i == 1 + ? textSelectionControls + : null, + onChanged: (value) { + if (value.isEmpty) { + setState(() { + _inputStatuses[i] = + FormInputStatus.empty; + }); + } else if (_isValidMnemonicWord( + value + .trim() + .toLowerCase())) { + setState(() { + _inputStatuses[i] = + FormInputStatus.valid; + }); + } else { + setState(() { + _inputStatuses[i] = + FormInputStatus + .invalid; + }); + } + }, + controller: _controllers[i], + style: + STextStyles.field(context) + .copyWith( + color: Theme.of(context) + .extension()! + .overlay, + fontSize: isDesktop ? 16 : 14, + ), + ), + if (_inputStatuses[i] == + FormInputStatus.invalid) + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: + const EdgeInsets.only( + left: 12.0, + bottom: 4.0, + ), + child: Text( + "Please check spelling", + textAlign: TextAlign.left, + style: STextStyles.label( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textError, + ), + ), + ), + ) + ], + ), + ), + ], + for (int i = remainder; + i < cols; + i++) ...[ + TableViewCell( + flex: 1, + child: Container(), + ), + ], + ], + expandingChild: null, + ), ], ), - Padding( - padding: const EdgeInsets.only( - top: 8.0, - ), - child: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonColor(context), - onPressed: requestRestore, - child: Text( - "Restore", - style: STextStyles.button(context), - ), - ), + ), + const SizedBox( + height: 32, + ), + PrimaryButton( + label: "Restore wallet", + width: 480, + onPressed: requestRestore, ), ], + ); + }, + ), + ), + if (isDesktop) + const Spacer( + flex: 15, + ), + if (!isDesktop) + Expanded( + child: SingleChildScrollView( + controller: controller, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (int i = 1; i <= _seedWordCount; i++) + Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 4), + child: TextFormField( + textCapitalization: + TextCapitalization.none, + key: Key("restoreMnemonicFormField_$i"), + decoration: _getInputDecorationFor( + _inputStatuses[i - 1], "$i"), + autovalidateMode: + AutovalidateMode.onUserInteraction, + selectionControls: + i == 1 ? textSelectionControls : null, + onChanged: (value) { + if (value.isEmpty) { + setState(() { + _inputStatuses[i - 1] = + FormInputStatus.empty; + }); + } else if (_isValidMnemonicWord( + value.trim().toLowerCase())) { + setState(() { + _inputStatuses[i - 1] = + FormInputStatus.valid; + }); + } else { + setState(() { + _inputStatuses[i - 1] = + FormInputStatus.invalid; + }); + } + }, + controller: _controllers[i - 1], + style: + STextStyles.field(context).copyWith( + color: Theme.of(context) + .extension()! + .overlay, + fontSize: isDesktop ? 16 : 14, + ), + ), + ), + if (_inputStatuses[i - 1] == + FormInputStatus.invalid) + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + bottom: 4.0, + ), + child: Text( + "Please check spelling", + textAlign: TextAlign.left, + style: STextStyles.label(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textError, + ), + ), + ), + ) + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 8.0, + ), + child: PrimaryButton( + onPressed: requestRestore, + label: "Restore", + ), + ), + ], + ), ), ), ), ), - ), ], ), ), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart index 2cd539c0c..51bb8f2d7 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart @@ -3,6 +3,10 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class RestoreSucceededDialog extends StatelessWidget { @@ -10,27 +14,82 @@ class RestoreSucceededDialog extends StatelessWidget { @override Widget build(BuildContext context) { - return StackDialog( - title: "Wallet restored", - message: "You can use your wallet now.", - icon: SvgPicture.asset( - Assets.svg.checkCircle, - width: 24, - height: 24, - color: Theme.of(context).extension()!.accentColorGreen, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Ok", - style: STextStyles.itemSubtitle12(context), + if (Util.isDesktop) { + return DesktopDialog( + child: Column( + children: [ + const DesktopDialogCloseButton(), + const Spacer( + flex: 1, + ), + SvgPicture.asset( + Assets.svg.checkCircle, + width: 40, + height: 40, + color: + Theme.of(context).extension()!.accentColorDark, + ), + const Spacer( + flex: 2, + ), + Text( + "Wallet restored", + style: STextStyles.desktopH2(context), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 16, + ), + Text( + "You can use your wallet now.", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + textAlign: TextAlign.center, + ), + const Spacer( + flex: 2, + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: PrimaryButton( + width: 272.5, + label: "OK", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); + ); + } else { + return StackDialog( + title: "Wallet restored", + message: "You can use your wallet now.", + icon: SvgPicture.asset( + Assets.svg.checkCircle, + width: 24, + height: 24, + color: Theme.of(context).extension()!.accentColorGreen, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Ok", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + } } } diff --git a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart index 80a688d03..001fff6c0 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart @@ -3,6 +3,10 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class RestoringDialog extends StatefulWidget { @@ -50,37 +54,105 @@ class _RestoringDialogState extends State @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - return false; - }, - child: StackDialog( - title: "Restoring wallet", - message: "This may take a while. Please do not exit this screen.", - icon: RotationTransition( - turns: _spinAnimation, - child: SvgPicture.asset(Assets.svg.arrowRotate3, - width: 24, - height: 24, - color: - Theme.of(context).extension()!.accentColorDark), + if (Util.isDesktop) { + return DesktopDialog( + child: Column( + children: [ + DesktopDialogCloseButton( + onPressedOverride: () async { + await onCancel.call(); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + const Spacer( + flex: 1, + ), + RotationTransition( + turns: _spinAnimation, + child: SvgPicture.asset(Assets.svg.arrowRotate3, + width: 40, + height: 40, + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + const Spacer( + flex: 2, + ), + Text( + "Restoring wallet...", + style: STextStyles.desktopH2(context), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 16, + ), + Text( + "Restoring your wallet may take a while.\nPlease do not exit this screen.", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + textAlign: TextAlign.center, + ), + const Spacer( + flex: 2, + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: SecondaryButton( + label: "Cancel", + width: 272.5, + onPressed: () async { + await onCancel.call(); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + ], ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), + ); + } else { + return WillPopScope( + onWillPop: () async { + return false; + }, + child: StackDialog( + title: "Restoring wallet", + message: "This may take a while. Please do not exit this screen.", + icon: RotationTransition( + turns: _spinAnimation, + child: SvgPicture.asset(Assets.svg.arrowRotate3, + width: 24, + height: 24, + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () async { + await onCancel.call(); + if (mounted) { + Navigator.of(context).pop(); + } + }, ), - onPressed: () async { - await onCancel.call(); - if (mounted) { - Navigator.of(context).pop(); - } - }, ), - ), - ); + ); + } } } diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/sub_widgets/word_table_item.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/sub_widgets/word_table_item.dart index 1cdc1a4f8..9f8f43b59 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/sub_widgets/word_table_item.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/sub_widgets/word_table_item.dart @@ -56,10 +56,23 @@ class WordTableItem extends ConsumerWidget { textAlign: TextAlign.center, style: isDesktop ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark, + color: selectedWord == word + ? Theme.of(context) + .extension()! + .textSelectedWordTableItem + : Theme.of(context) + .extension()! + .textDark, ) - : STextStyles.baseXS(context), + : STextStyles.baseXS(context).copyWith( + color: selectedWord == word + ? Theme.of(context) + .extension()! + .textSelectedWordTableItem + : Theme.of(context) + .extension()! + .textDark, + ), ), ], ), 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 5745c6b65..1e0b422cd 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 @@ -236,6 +236,10 @@ class _VerifyRecoveryPhraseViewState child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ + if (isDesktop) + const Spacer( + flex: 10, + ), SizedBox( height: isDesktop ? 24 : 4, ), @@ -349,6 +353,10 @@ class _VerifyRecoveryPhraseViewState ), ], ), + if (isDesktop) + const Spacer( + flex: 15, + ), ], ), ), diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index e677ab5c0..586ffc0da 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -85,7 +85,7 @@ class _SendFromViewState extends ConsumerState { height: 8, ), Text( - "You need to send ${amount.toStringAsFixed(coin == Coin.monero ? 12 : 8)} ${coin.ticker}", + "You need to send ${amount.toStringAsFixed(coin == Coin.monero ? Constants.satsPerCoinMonero : coin == Coin.wownero ? Constants.satsPerCoinWownero : Constants.satsPerCoin)} ${coin.ticker}", style: STextStyles.itemSubtitle(context), ), const SizedBox( @@ -307,7 +307,11 @@ class _SendFromCardState extends ConsumerState { "${Format.localizedStringAsFixed( value: snapshot.data!, locale: locale, - decimalPlaces: coin == Coin.monero ? 12 : 8, + decimalPlaces: coin == Coin.monero + ? Constants.satsPerCoinMonero + : coin == Coin.wownero + ? Constants.satsPerCoinWownero + : Constants.satsPerCoin, )} ${coin.ticker}", style: STextStyles.itemSubtitle(context), ); diff --git a/lib/pages/exchange_view/sub_widgets/step_indicator.dart b/lib/pages/exchange_view/sub_widgets/step_indicator.dart index fd0d3a859..96e1d4a4c 100644 --- a/lib/pages/exchange_view/sub_widgets/step_indicator.dart +++ b/lib/pages/exchange_view/sub_widgets/step_indicator.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; enum StepIndicatorStatus { current, completed, incomplete } @@ -39,9 +39,7 @@ class StepIndicator extends StatelessWidget { case StepIndicatorStatus.current: return Text( step.toString(), - style: GoogleFonts.roboto( - fontWeight: FontWeight.w600, - fontSize: 8, + style: STextStyles.stepIndicator(context).copyWith( color: Theme.of(context) .extension()! .stepIndicatorIconNumber, @@ -57,9 +55,7 @@ class StepIndicator extends StatelessWidget { case StepIndicatorStatus.incomplete: return Text( step.toString(), - style: GoogleFonts.roboto( - fontWeight: FontWeight.w600, - fontSize: 8, + style: STextStyles.stepIndicator(context).copyWith( color: Theme.of(context) .extension()! .stepIndicatorIconInactive, diff --git a/lib/pages/manage_favorites_view/manage_favorites_view.dart b/lib/pages/manage_favorites_view/manage_favorites_view.dart index 7d2b58781..7d15974a8 100644 --- a/lib/pages/manage_favorites_view/manage_favorites_view.dart +++ b/lib/pages/manage_favorites_view/manage_favorites_view.dart @@ -4,7 +4,10 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/managed_favorite.dart'; class ManageFavoritesView extends StatelessWidget { @@ -15,142 +18,306 @@ class ManageFavoritesView extends StatelessWidget { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - appBar: AppBar( - title: Text( - "Favorite wallets", - style: STextStyles.navBarTitle(context), - ), - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - body: Container( - color: Theme.of(context).extension()!.background, - child: Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 4, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Text( - "Drag to change wallet order.", - style: STextStyles.label(context), - ), - ), + + final isDesktop = Util.isDesktop; + + return MasterScaffold( + isDesktop: isDesktop, + appBar: isDesktop + ? DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + isCompactHeight: true, + leading: const AppBarBackButton( + isCompact: true, + ), + center: Expanded( + child: Text( + "Favorite wallets", + style: STextStyles.desktopH3(context), ), ), - const SizedBox( - height: 8, + ) + : AppBar( + title: Text( + "Favorite wallets", + style: STextStyles.navBarTitle(context), ), - Expanded( - child: Consumer( - builder: (_, ref, __) { - final favorites = ref.watch(favoritesProvider); - return ReorderableListView.builder( - key: key, - itemCount: favorites.length, - itemBuilder: (builderContext, index) { - final walletId = ref.read(favorites[index]).walletId; - return Padding( - key: Key( - "manageFavoriteWalletsItem_$walletId", - ), - padding: const EdgeInsets.all(4.0), - child: ManagedFavorite( - walletId: walletId, - ), - ); - }, - onReorder: (oldIndex, newIndex) { - ref - .read(walletsServiceChangeNotifierProvider) - .moveFavorite( - fromIndex: oldIndex, toIndex: newIndex); + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: isDesktop + ? Consumer( + builder: (_, ref, __) { + final favorites = ref.watch(favoritesProvider); + final nonFavorites = ref.watch(nonFavoritesProvider); - ref - .read(favoritesProvider) - .reorder(oldIndex, newIndex, true); - }, - proxyDecorator: (child, index, animation) { - return Material( - elevation: 15, - color: Colors.transparent, - // shadowColor: Colors.red, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular( - Constants.size.circularBorderRadius * 1.5, + return Column( + children: [ + const SizedBox( + height: 24, + ), + Expanded( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + "Drag to change wallet order.", + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), ), ), ), - child: child, - ); - }, - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 30, - bottom: 12, - left: 4, - right: 4, - ), - child: Text( - "Add to favorites", - style: STextStyles.itemSubtitle12(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ), - ), - ), - Expanded( - child: Consumer( - builder: (_, ref, __) { - final nonFavorites = ref.watch(nonFavoritesProvider); + const SizedBox( + height: 5, + ), + ReorderableListView.builder( + buildDefaultDragHandles: false, + shrinkWrap: true, + primary: false, + key: key, + itemCount: favorites.length, + itemBuilder: (builderContext, index) { + final walletId = + ref.read(favorites[index]).walletId; + return Padding( + key: Key( + "manageFavoriteWalletsItem_$walletId", + ), + padding: const EdgeInsets.symmetric( + vertical: 5, + horizontal: 24, + ), + child: ReorderableDelayedDragStartListener( + index: index, + child: ManagedFavorite( + walletId: walletId, + ), + ), + ); + }, + onReorder: (oldIndex, newIndex) { + ref + .read(walletsServiceChangeNotifierProvider) + .moveFavorite( + fromIndex: oldIndex, toIndex: newIndex); - return ListView.builder( - itemCount: nonFavorites.length, - itemBuilder: (buildContext, index) { - // final walletId = ref.watch( - // nonFavorites[index].select((value) => value.walletId)); - final walletId = ref.read(nonFavorites[index]).walletId; - return Padding( - key: Key( - "manageNonFavoriteWalletsItem_$walletId", + ref + .read(favoritesProvider) + .reorder(oldIndex, newIndex, true); + }, + proxyDecorator: (child, index, animation) { + return Material( + elevation: 15, + color: Colors.transparent, + // shadowColor: Colors.red, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular( + Constants.size.circularBorderRadius * 1.5, + ), + ), + ), + child: child, + ); + }, ), - padding: const EdgeInsets.all(4.0), - child: ManagedFavorite( - walletId: walletId, + Padding( + padding: const EdgeInsets.only( + top: 32, + bottom: 11, + left: 24, + right: 24, + ), + child: Text( + "Add to favorites", + style: + STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), ), - ); - }, - ); - }, + ListView.builder( + shrinkWrap: true, + primary: false, + itemCount: nonFavorites.length, + itemBuilder: (buildContext, index) { + // final walletId = ref.watch( + // nonFavorites[index].select((value) => value.walletId)); + final walletId = + ref.read(nonFavorites[index]).walletId; + return Padding( + key: Key( + "manageNonFavoriteWalletsItem_$walletId", + ), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 5, + ), + child: ManagedFavorite( + walletId: walletId, + ), + ); + }, + ) + ], + ), + ), + ], + ); + }, + ) + : Container( + color: Theme.of(context).extension()!.background, + child: Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 4, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + "Drag to change wallet order.", + style: STextStyles.label(context), + ), + ), + ), + ), + const SizedBox( + height: 8, + ), + Expanded( + child: Consumer( + builder: (_, ref, __) { + final favorites = ref.watch(favoritesProvider); + return ReorderableListView.builder( + key: key, + itemCount: favorites.length, + itemBuilder: (builderContext, index) { + final walletId = + ref.read(favorites[index]).walletId; + return Padding( + key: Key( + "manageFavoriteWalletsItem_$walletId", + ), + padding: const EdgeInsets.all(4.0), + child: ManagedFavorite( + walletId: walletId, + ), + ); + }, + onReorder: (oldIndex, newIndex) { + ref + .read(walletsServiceChangeNotifierProvider) + .moveFavorite( + fromIndex: oldIndex, toIndex: newIndex); + + ref + .read(favoritesProvider) + .reorder(oldIndex, newIndex, true); + }, + proxyDecorator: (child, index, animation) { + return Material( + elevation: 15, + color: Colors.transparent, + // shadowColor: Colors.red, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular( + Constants.size.circularBorderRadius * 1.5, + ), + ), + ), + child: child, + ); + }, + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 30, + bottom: 12, + left: 4, + right: 4, + ), + child: Text( + "Add to favorites", + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), + ), + Expanded( + child: Consumer( + builder: (_, ref, __) { + final nonFavorites = ref.watch(nonFavoritesProvider); + + return ListView.builder( + itemCount: nonFavorites.length, + itemBuilder: (buildContext, index) { + // final walletId = ref.watch( + // nonFavorites[index].select((value) => value.walletId)); + final walletId = + ref.read(nonFavorites[index]).walletId; + return Padding( + key: Key( + "manageNonFavoriteWalletsItem_$walletId", + ), + padding: const EdgeInsets.all(4.0), + child: ManagedFavorite( + walletId: walletId, + ), + ); + }, + ); + }, + ), + ), + ], ), ), - ], - ), - ), - ), + ), ); } } diff --git a/lib/pages/pinpad_views/create_pin_view.dart b/lib/pages/pinpad_views/create_pin_view.dart index c4e4ee592..f8b84cfb4 100644 --- a/lib/pages/pinpad_views/create_pin_view.dart +++ b/lib/pages/pinpad_views/create_pin_view.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; @@ -188,7 +187,10 @@ class _CreatePinViewState extends ConsumerState { fieldsCount: Constants.pinLength, eachFieldHeight: 12, eachFieldWidth: 12, - textStyle: GoogleFonts.workSans( + textStyle: STextStyles.infoSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, fontSize: 1, ), focusNode: _pinPutFocusNode2, 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 0cf3ebc4f..143b1e84d 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 @@ -90,6 +90,7 @@ class _AddEditNodeViewState extends ConsumerState { break; case Coin.monero: + case Coin.wownero: try { final uri = Uri.parse(formData.host!); if (uri.scheme.startsWith("http")) { @@ -113,12 +114,12 @@ class _AddEditNodeViewState extends ConsumerState { break; case Coin.bitcoin: - // case Coin.bitcoincash: + case Coin.bitcoincash: case Coin.dogecoin: case Coin.firo: case Coin.namecoin: case Coin.bitcoinTestNet: - // case Coin.bitcoincashTestnet: + case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: final client = ElectrumX( @@ -384,6 +385,7 @@ class _AddEditNodeViewState extends ConsumerState { // strip unused path String address = formData.host!; if (coin == Coin.monero || + coin == Coin.wownero || coin == Coin.epicCash) { if (address.startsWith("http")) { final uri = Uri.parse(address); @@ -530,15 +532,16 @@ class _NodeFormState extends ConsumerState { case Coin.dogecoin: case Coin.firo: case Coin.namecoin: - // case Coin.bitcoincash: + case Coin.bitcoincash: case Coin.bitcoinTestNet: - // case Coin.bitcoincashTestnet: + case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: return false; case Coin.epicCash: case Coin.monero: + case Coin.wownero: return true; } } @@ -699,7 +702,9 @@ class _NodeFormState extends ConsumerState { focusNode: _hostFocusNode, style: STextStyles.field(context), decoration: standardInputDecoration( - (widget.coin != Coin.monero && widget.coin != Coin.epicCash) + (widget.coin != Coin.monero && + widget.coin != Coin.wownero && + widget.coin != Coin.epicCash) ? "IP address" : "Url", _hostFocusNode, @@ -880,7 +885,9 @@ class _NodeFormState extends ConsumerState { const SizedBox( height: 8, ), - if (widget.coin != Coin.monero && widget.coin != Coin.epicCash) + if (widget.coin != Coin.monero && + widget.coin != Coin.wownero && + widget.coin != Coin.epicCash) Row( children: [ GestureDetector( @@ -931,11 +938,15 @@ class _NodeFormState extends ConsumerState { ), ], ), - if (widget.coin != Coin.monero && widget.coin != Coin.epicCash) + if (widget.coin != Coin.monero && + widget.coin != Coin.wownero && + widget.coin != Coin.epicCash) const SizedBox( height: 8, ), - if (widget.coin != Coin.monero && widget.coin != Coin.epicCash) + if (widget.coin != Coin.monero && + widget.coin != Coin.wownero && + widget.coin != Coin.epicCash) Row( children: [ GestureDetector( diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 377905630..8baddb700 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -81,6 +81,7 @@ class _NodeDetailsViewState extends ConsumerState { break; case Coin.monero: + case Coin.wownero: try { final uri = Uri.parse(node!.host); if (uri.scheme.startsWith("http")) { @@ -102,9 +103,9 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: - // case Coin.bitcoincash: + case Coin.bitcoincash: case Coin.namecoin: - // case Coin.bitcoincashTestnet: + case Coin.bitcoincashTestnet: final client = ElectrumX( host: node!.host, port: node.port, diff --git a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart index f083eb63a..39c95cad7 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/security_views/security_view.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -168,7 +167,10 @@ class _ChangePinViewState extends State { fieldsCount: Constants.pinLength, eachFieldHeight: 12, eachFieldWidth: 12, - textStyle: GoogleFonts.workSans( + textStyle: STextStyles.infoSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, fontSize: 1, ), focusNode: _pinPutFocusNode2, diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index 892cd13d8..c4045dc9b 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_net import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; +import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -205,7 +206,7 @@ class _WalletNetworkSettingsViewState .getManager(widget.walletId) .coin; - if (coin == Coin.monero || coin == Coin.epicCash) { + if (coin == Coin.monero || coin == Coin.wownero || coin == Coin.epicCash) { _blocksRemainingSubscription = eventBus.on().listen( (event) async { if (event.walletId == widget.walletId) { @@ -271,6 +272,15 @@ class _WalletNetworkSettingsViewState if (_percent < highestPercent) { _percent = highestPercent.clamp(0.0, 1.0); } + } else if (coin == Coin.wownero) { + double highestPercent = (ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as WowneroWallet) + .highestPercentCached; + if (_percent < highestPercent) { + _percent = highestPercent.clamp(0.0, 1.0); + } } else if (coin == Coin.epicCash) { double highestPercent = (ref .read(walletsChangeNotifierProvider) @@ -545,6 +555,7 @@ class _WalletNetworkSettingsViewState ), ), if (coin == Coin.monero || + coin == Coin.wownero || coin == Coin.epicCash) Text( " (Blocks to go: ${_blocksRemaining == -1 ? "?" : _blocksRemaining})", diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 391b5caec..fdb8bf1a9 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -241,7 +241,9 @@ class _TransactionDetailsViewState "$amountPrefix${Format.localizedStringAsFixed( value: coin == Coin.monero ? (amount / 10000.toDecimal()).toDecimal() - : amount, + : coin == Coin.wownero + ? (amount / 1000.toDecimal()).toDecimal() + : amount, locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), @@ -254,7 +256,7 @@ class _TransactionDetailsViewState height: 2, ), SelectableText( - "${Format.localizedStringAsFixed(value: (coin == Coin.monero ? (amount / 10000.toDecimal()).toDecimal() : amount) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1)), locale: ref.watch( + "${Format.localizedStringAsFixed(value: (coin == Coin.monero ? (amount / 10000.toDecimal()).toDecimal() : coin == Coin.wownero ? (amount / 1000.toDecimal()).toDecimal() : amount) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1)), locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), decimalPlaces: 2)} ${ref.watch( @@ -298,14 +300,14 @@ class _TransactionDetailsViewState ], ), ), - if (!(coin == Coin.monero && + if (!((coin == Coin.monero || coin == Coin.wownero) && _transaction.txType.toLowerCase() == "sent") && !((coin == Coin.firo || coin == Coin.firoTestNet) && _transaction.subType == "mint")) const SizedBox( height: 12, ), - if (!(coin == Coin.monero && + if (!((coin == Coin.monero || coin == Coin.wownero) && _transaction.txType.toLowerCase() == "sent") && !((coin == Coin.firo || coin == Coin.firoTestNet) && _transaction.subType == "mint")) @@ -464,7 +466,10 @@ class _TransactionDetailsViewState ? Format.localizedStringAsFixed( value: coin == Coin.monero ? (fee / 10000.toDecimal()).toDecimal() - : fee, + : coin == Coin.wownero + ? (fee / 1000.toDecimal()) + .toDecimal() + : fee, locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale)), @@ -473,7 +478,9 @@ class _TransactionDetailsViewState : Format.localizedStringAsFixed( value: coin == Coin.monero ? (fee / 10000.toDecimal()).toDecimal() - : fee, + : coin == Coin.wownero + ? (fee / 1000.toDecimal()).toDecimal() + : fee, locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale)), diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index 48fe89e65..8175597f6 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -4,7 +4,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:stackwallet/models/transaction_filter.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; @@ -122,77 +121,43 @@ class _TransactionSearchViewState colorArrowNext: Theme.of(context).extension()!.textSubtitle1, colorArrowPrevious: Theme.of(context).extension()!.textSubtitle1, - textStyleButtonNegative: GoogleFonts.inter( - letterSpacing: 0.5, + textStyleButtonNegative: STextStyles.datePicker600(context).copyWith( color: baseColor, - fontSize: 16, - fontWeight: FontWeight.w600, ), - textStyleButtonPositive: GoogleFonts.inter( - letterSpacing: 0.5, + textStyleButtonPositive: STextStyles.datePicker600(context).copyWith( color: baseColor, - fontSize: 16, - fontWeight: FontWeight.w600, ), - textStyleCurrentDayOnCalendar: GoogleFonts.inter( - letterSpacing: 0.5, - color: Theme.of(context).extension()!.accentColorDark, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - textStyleDayHeader: GoogleFonts.inter( - letterSpacing: 0.5, - color: Theme.of(context).extension()!.accentColorDark, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - textStyleDayOnCalendar: GoogleFonts.inter( - letterSpacing: 0.5, + textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context), + textStyleDayHeader: STextStyles.datePicker600(context), + textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith( color: baseColor, - fontSize: 12, - fontWeight: FontWeight.w400, ), - textStyleDayOnCalendarDisabled: GoogleFonts.inter( - letterSpacing: 0.5, + textStyleDayOnCalendarDisabled: + STextStyles.datePicker400(context).copyWith( color: Theme.of(context).extension()!.textSubtitle3, - fontWeight: FontWeight.w400, - fontSize: 12, ), - textStyleDayOnCalendarSelected: GoogleFonts.inter( - letterSpacing: 0.5, + textStyleDayOnCalendarSelected: + STextStyles.datePicker400(context).copyWith( color: Theme.of(context).extension()!.textWhite, - fontWeight: FontWeight.w400, - fontSize: 12, ), - textStyleMonthYearHeader: GoogleFonts.inter( - letterSpacing: 0.5, + textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith( color: Theme.of(context).extension()!.textSubtitle1, - fontSize: 16, - fontWeight: FontWeight.w600, ), - textStyleYearButton: GoogleFonts.inter( - letterSpacing: 0.5, + textStyleYearButton: STextStyles.datePicker600(context).copyWith( color: Theme.of(context).extension()!.textWhite, - fontSize: 16, - fontWeight: FontWeight.w600, ), - textStyleButtonAction: GoogleFonts.inter(), + // textStyleButtonAction: GoogleFonts.inter(), ); } MaterialRoundedYearPickerStyle _buildYearPickerStyle() { return MaterialRoundedYearPickerStyle( backgroundPicker: Theme.of(context).extension()!.popupBG, - textStyleYear: GoogleFonts.inter( - letterSpacing: 0.5, + textStyleYear: STextStyles.datePicker600(context).copyWith( color: Theme.of(context).extension()!.textSubtitle2, - fontWeight: FontWeight.w600, fontSize: 16, ), - textStyleYearSelected: GoogleFonts.inter( - letterSpacing: 0.5, - color: Theme.of(context).extension()!.accentColorDark, - fontWeight: FontWeight.w600, + textStyleYearSelected: STextStyles.datePicker600(context).copyWith( fontSize: 18, ), ); @@ -790,6 +755,11 @@ class _TransactionSearchViewState .floor() .toBigInt() .toInt(); + } else if (widget.coin == Coin.wownero) { + amount = (amountDecimal * Decimal.fromInt(Constants.satsPerCoinWownero)) + .floor() + .toBigInt() + .toInt(); } else { amount = (amountDecimal * Decimal.fromInt(Constants.satsPerCoin)) .floor() diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index bd4996ec1..adfcdfb6a 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; class DesktopHomeView extends ConsumerStatefulWidget { @@ -16,12 +17,9 @@ class DesktopHomeView extends ConsumerStatefulWidget { class _DesktopHomeViewState extends ConsumerState { int currentViewIndex = 0; final List contentViews = [ - // const Navigator( - // onGenerateRoute: RouteGenerator.generateRoute, - // initialRoute: MyStackView.routeName, - // ), - const MyStackView( - key: Key("myStackViewKey"), + const Navigator( + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: MyStackView.routeName, ), Container( color: Colors.green, diff --git a/lib/pages_desktop_specific/home/my_stack_view/desktop_favorite_wallets.dart b/lib/pages_desktop_specific/home/my_stack_view/desktop_favorite_wallets.dart new file mode 100644 index 000000000..91130fe19 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/desktop_favorite_wallets.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; +import 'package:stackwallet/pages/wallets_view/sub_widgets/favorite_card.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; + +class DesktopFavoriteWallets extends ConsumerWidget { + const DesktopFavoriteWallets({Key? key}) : super(key: key); + + static const cardWidth = 220.0; + static const cardHeight = 125.0; + static const standardPadding = 16.0; + + @override + Widget build(BuildContext context, WidgetRef ref) { + debugPrint("BUILD: $runtimeType"); + + final favorites = ref.watch(favoritesProvider); + bool hasFavorites = favorites.length > 0; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Favorite wallets", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + ), + BlueTextButton( + text: "Edit", + onTap: () { + Navigator.of(context).pushNamed(ManageFavoritesView.routeName); + }, + ), + ], + ), + const SizedBox( + height: 20, + ), + ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: (cardHeight * 2) + standardPadding, + minHeight: cardHeight, + ), + child: hasFavorites + ? SingleChildScrollView( + primary: false, + child: Wrap( + spacing: 16, + runSpacing: 16, + children: [ + ...favorites.map((p0) { + final walletId = ref.read(p0).walletId; + final managerProvider = ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(walletId); + + return FavoriteCard( + walletId: walletId, + width: cardWidth, + height: cardHeight, + managerProvider: managerProvider, + ); + }) + ], + ), + ) + : Container( + height: cardHeight, + width: cardWidth, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: MaterialButton( + splashColor: + Theme.of(context).extension()!.highlight, + key: const Key("favoriteWalletsAddFavoriteButtonKey"), + padding: const EdgeInsets.all(12), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius), + ), + onPressed: () { + Navigator.of(context) + .pushNamed(ManageFavoritesView.routeName); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.plus, + width: 14, + height: 14, + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + const SizedBox( + width: 4, + ), + Text( + "Add a favorite", + style: STextStyles.itemSubtitle(context).copyWith( + fontSize: 18, + ), + ), + ], + ), + ), + ), + ), + const SizedBox( + height: 40, + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart b/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart index b7860542a..6b60902c4 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages/wallets_view/sub_widgets/empty_wallets.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_wallets.dart'; -import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -24,9 +23,6 @@ class _MyStackViewState extends ConsumerState { debugPrint("BUILD: $runtimeType"); final hasWallets = ref.watch(walletsChangeNotifierProvider).hasWallets; - final showFavorites = ref.watch(prefsChangeNotifierProvider - .select((value) => value.showFavoriteWallets)); - return Column( children: [ DesktopAppBar( diff --git a/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart b/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart index e41c7643d..550db293e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart @@ -1,46 +1,32 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/desktop_favorite_wallets.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_summary_table.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; -class MyWallets extends StatefulWidget { +class MyWallets extends ConsumerStatefulWidget { const MyWallets({Key? key}) : super(key: key); @override - State createState() => _MyWalletsState(); + ConsumerState createState() => _MyWalletsState(); } -class _MyWalletsState extends State { +class _MyWalletsState extends ConsumerState { @override Widget build(BuildContext context) { + final showFavorites = ref.watch(prefsChangeNotifierProvider + .select((value) => value.showFavoriteWallets)); + return Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Favorite wallets", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), - ), - const SizedBox( - height: 20, - ), - // TODO favorites grid - Container( - color: Colors.deepPurpleAccent, - height: 210, - ), - - const SizedBox( - height: 40, - ), - + if (showFavorites) const DesktopFavoriteWallets(), Row( children: [ Text( @@ -55,12 +41,14 @@ class _MyWalletsState extends State { BlueTextButton( text: "Add new wallet", onTap: () { - Navigator.of(context).pushNamed(AddWalletView.routeName); + Navigator.of( + context, + rootNavigator: true, + ).pushNamed(AddWalletView.routeName); }, ), ], ), - const SizedBox( height: 20, ), diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 81b8198e5..30f04e918 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -1,3111 +1,3408 @@ -// import 'dart:async'; -// import 'dart:convert'; -// import 'dart:io'; -// import 'dart:typed_data'; -// -// import 'package:bech32/bech32.dart'; -// import 'package:bip32/bip32.dart' as bip32; -// import 'package:bip39/bip39.dart' as bip39; -// import 'package:bitcoindart/bitcoindart.dart'; -// import 'package:bs58check/bs58check.dart' as bs58check; -// import 'package:crypto/crypto.dart'; -// import 'package:decimal/decimal.dart'; -// import 'package:flutter/foundation.dart'; -// import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -// import 'package:http/http.dart'; -// import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; -// import 'package:stackwallet/electrumx_rpc/electrumx.dart'; -// import 'package:stackwallet/hive/db.dart'; -// import 'package:stackwallet/models/models.dart' as models; -// import 'package:stackwallet/models/paymint/fee_object_model.dart'; -// import 'package:stackwallet/models/paymint/transactions_model.dart'; -// import 'package:stackwallet/models/paymint/utxo_model.dart'; -// import 'package:stackwallet/services/coins/coin_service.dart'; -// import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; -// import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; -// import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; -// import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -// import 'package:stackwallet/services/event_bus/global_event_bus.dart'; -// import 'package:stackwallet/services/node_service.dart'; -// import 'package:stackwallet/services/notifications_api.dart'; -// import 'package:stackwallet/services/price.dart'; -// import 'package:stackwallet/services/transaction_notification_tracker.dart'; -// import 'package:stackwallet/utilities/assets.dart'; -// import 'package:stackwallet/utilities/constants.dart'; -// import 'package:stackwallet/utilities/default_nodes.dart'; -// import 'package:stackwallet/utilities/enums/coin_enum.dart'; -// import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -// import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -// import 'package:stackwallet/utilities/format.dart'; -// import 'package:stackwallet/utilities/logger.dart'; -// import 'package:stackwallet/utilities/prefs.dart'; -// import 'package:tuple/tuple.dart'; -// import 'package:uuid/uuid.dart'; -// import 'package:bitbox/bitbox.dart' as Bitbox; -// -// const int MINIMUM_CONFIRMATIONS = 3; -// const int DUST_LIMIT = 546; -// -// const String GENESIS_HASH_MAINNET = -// "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; -// const String GENESIS_HASH_TESTNET = -// "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"; -// -// enum DerivePathType { bip44 } -// -// bip32.BIP32 getBip32Node(int chain, int index, String mnemonic, -// NetworkType network, DerivePathType derivePathType) { -// final root = getBip32Root(mnemonic, network); -// -// final node = getBip32NodeFromRoot(chain, index, root, derivePathType); -// return node; -// } -// -// /// wrapper for compute() -// bip32.BIP32 getBip32NodeWrapper( -// Tuple5 args, -// ) { -// return getBip32Node( -// args.item1, -// args.item2, -// args.item3, -// args.item4, -// args.item5, -// ); -// } -// -// bip32.BIP32 getBip32NodeFromRoot( -// int chain, int index, bip32.BIP32 root, DerivePathType derivePathType) { -// String coinType; -// switch (root.network.wif) { -// case 0x80: // bch mainnet wif -// coinType = "145"; // bch mainnet -// break; -// case 0xef: // bch testnet wif -// coinType = "1"; // bch testnet -// break; -// default: -// throw Exception("Invalid Bitcoincash network type used!"); -// } -// switch (derivePathType) { -// case DerivePathType.bip44: -// return root.derivePath("m/44'/$coinType'/0'/$chain/$index"); -// default: -// throw Exception("DerivePathType must not be null."); -// } -// } -// -// /// wrapper for compute() -// bip32.BIP32 getBip32NodeFromRootWrapper( -// Tuple4 args, -// ) { -// return getBip32NodeFromRoot( -// args.item1, -// args.item2, -// args.item3, -// args.item4, -// ); -// } -// -// bip32.BIP32 getBip32Root(String mnemonic, NetworkType network) { -// final seed = bip39.mnemonicToSeed(mnemonic); -// final networkType = bip32.NetworkType( -// wif: network.wif, -// bip32: bip32.Bip32Type( -// public: network.bip32.public, -// private: network.bip32.private, -// ), -// ); -// -// final root = bip32.BIP32.fromSeed(seed, networkType); -// return root; -// } -// -// /// wrapper for compute() -// bip32.BIP32 getBip32RootWrapper(Tuple2 args) { -// return getBip32Root(args.item1, args.item2); -// } -// -// class BitcoinCashWallet extends CoinServiceAPI { -// static const integrationTestFlag = -// bool.fromEnvironment("IS_INTEGRATION_TEST"); -// final _prefs = Prefs.instance; -// -// Timer? timer; -// late Coin _coin; -// -// late final TransactionNotificationTracker txTracker; -// -// NetworkType get _network { -// switch (coin) { -// case Coin.bitcoincash: -// return bitcoincash; -// case Coin.bitcoincashTestnet: -// return bitcoincashtestnet; -// default: -// throw Exception("Bitcoincash network type not set!"); -// } -// } -// -// List outputsList = []; -// -// @override -// Coin get coin => _coin; -// -// @override -// Future> get allOwnAddresses => -// _allOwnAddresses ??= _fetchAllOwnAddresses(); -// Future>? _allOwnAddresses; -// -// Future? _utxoData; -// Future get utxoData => _utxoData ??= _fetchUtxoData(); -// -// @override -// Future> get unspentOutputs async => -// (await utxoData).unspentOutputArray; -// -// @override -// Future get availableBalance async { -// final data = await utxoData; -// return Format.satoshisToAmount( -// data.satoshiBalance - data.satoshiBalanceUnconfirmed); -// } -// -// @override -// Future get pendingBalance async { -// final data = await utxoData; -// return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); -// } -// -// @override -// Future get balanceMinusMaxFee async => -// (await availableBalance) - -// (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) -// .toDecimal(); -// -// @override -// Future get totalBalance async { -// if (!isActive) { -// final totalBalance = DB.instance -// .get(boxName: walletId, key: 'totalBalance') as int?; -// if (totalBalance == null) { -// final data = await utxoData; -// return Format.satoshisToAmount(data.satoshiBalance); -// } else { -// return Format.satoshisToAmount(totalBalance); -// } -// } -// final data = await utxoData; -// return Format.satoshisToAmount(data.satoshiBalance); -// } -// -// @override -// Future get currentReceivingAddress => -// _currentReceivingAddressP2PKH ??= -// _getCurrentAddressForChain(0, DerivePathType.bip44); -// -// Future? _currentReceivingAddressP2PKH; -// -// @override -// Future exit() async { -// _hasCalledExit = true; -// timer?.cancel(); -// timer = null; -// stopNetworkAlivePinging(); -// } -// -// bool _hasCalledExit = false; -// -// @override -// bool get hasCalledExit => _hasCalledExit; -// -// @override -// Future get fees => _feeObject ??= _getFees(); -// Future? _feeObject; -// -// @override -// Future get maxFee async { -// final fee = (await fees).fast; -// final satsFee = -// Format.satoshisToAmount(fee) * Decimal.fromInt(Constants.satsPerCoin); -// return satsFee.floor().toBigInt().toInt(); -// } -// -// @override -// Future> get mnemonic => _getMnemonicList(); -// -// Future get chainHeight async { -// try { -// final result = await _electrumXClient.getBlockHeadTip(); -// return result["height"] as int; -// } catch (e, s) { -// Logging.instance.log("Exception caught in chainHeight: $e\n$s", -// level: LogLevel.Error); -// return -1; -// } -// } -// -// Future get storedChainHeight async { -// final storedHeight = DB.instance -// .get(boxName: walletId, key: "storedChainHeight") as int?; -// return storedHeight ?? 0; -// } -// -// Future updateStoredChainHeight({required int newHeight}) async { -// DB.instance.put( -// boxName: walletId, key: "storedChainHeight", value: newHeight); -// } -// -// DerivePathType addressType({required String address}) { -// Uint8List? decodeBase58; -// Segwit? decodeBech32; -// try { -// decodeBase58 = bs58check.decode(address); -// } catch (err) { -// // Base58check decode fail -// } -// if (decodeBase58 != null) { -// if (decodeBase58[0] == _network.pubKeyHash) { -// // P2PKH -// return DerivePathType.bip44; -// } -// throw ArgumentError('Invalid version or Network mismatch'); -// } else { -// try { -// decodeBech32 = segwit.decode(address); -// } catch (err) { -// // Bech32 decode fail -// } -// if (_network.bech32 != decodeBech32!.hrp) { -// throw ArgumentError('Invalid prefix or Network mismatch'); -// } -// if (decodeBech32.version != 0) { -// throw ArgumentError('Invalid address version'); -// } -// } -// throw ArgumentError('$address has no matching Script'); -// } -// -// bool longMutex = false; -// -// @override -// Future recoverFromMnemonic({ -// required String mnemonic, -// required int maxUnusedAddressGap, -// required int maxNumberOfIndexesToCheck, -// required int height, -// }) async { -// longMutex = true; -// final start = DateTime.now(); -// try { -// Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag", -// level: LogLevel.Info); -// if (!integrationTestFlag) { -// final features = await electrumXClient.getServerFeatures(); -// Logging.instance.log("features: $features", level: LogLevel.Info); -// switch (coin) { -// case Coin.bitcoincash: -// if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { -// throw Exception("genesis hash does not match main net!"); -// } -// break; -// case Coin.bitcoincashTestnet: -// if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { -// throw Exception("genesis hash does not match test net!"); -// } -// break; -// default: -// throw Exception( -// "Attempted to generate a BitcoinCashWallet using a non bch coin type: ${coin.name}"); -// } -// } -// // check to make sure we aren't overwriting a mnemonic -// // this should never fail -// if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { -// longMutex = false; -// throw Exception("Attempted to overwrite mnemonic on restore!"); -// } -// await _secureStore.write( -// key: '${_walletId}_mnemonic', value: mnemonic.trim()); -// await _recoverWalletFromBIP32SeedPhrase( -// mnemonic: mnemonic.trim(), -// maxUnusedAddressGap: maxUnusedAddressGap, -// maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, -// ); -// } catch (e, s) { -// Logging.instance.log( -// "Exception rethrown from recoverFromMnemonic(): $e\n$s", -// level: LogLevel.Error); -// longMutex = false; -// rethrow; -// } -// longMutex = false; -// -// final end = DateTime.now(); -// Logging.instance.log( -// "$walletName recovery time: ${end.difference(start).inMilliseconds} millis", -// level: LogLevel.Info); -// } -// -// Future _recoverWalletFromBIP32SeedPhrase({ -// required String mnemonic, -// int maxUnusedAddressGap = 20, -// int maxNumberOfIndexesToCheck = 1000, -// }) async { -// longMutex = true; -// -// Map> p2pkhReceiveDerivations = {}; -// Map> p2pkhChangeDerivations = {}; -// -// final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); -// -// List p2pkhReceiveAddressArray = []; -// int p2pkhReceiveIndex = -1; -// -// List p2pkhChangeAddressArray = []; -// int p2pkhChangeIndex = -1; -// -// // The gap limit will be capped at [maxUnusedAddressGap] -// int receivingGapCounter = 0; -// int changeGapCounter = 0; -// -// // actual size is 12 due to p2pkh so 12x1 -// const txCountBatchSize = 12; -// -// try { -// // receiving addresses -// Logging.instance -// .log("checking receiving addresses...", level: LogLevel.Info); -// for (int index = 0; -// index < maxNumberOfIndexesToCheck && -// receivingGapCounter < maxUnusedAddressGap; -// index += txCountBatchSize) { -// Logging.instance.log( -// "index: $index, \t receivingGapCounter: $receivingGapCounter", -// level: LogLevel.Info); -// -// final receivingP2pkhID = "k_$index"; -// Map txCountCallArgs = {}; -// final Map receivingNodes = {}; -// -// for (int j = 0; j < txCountBatchSize; j++) { -// // bip44 / P2PKH -// final node44 = await compute( -// getBip32NodeFromRootWrapper, -// Tuple4( -// 0, -// index + j, -// root, -// DerivePathType.bip44, -// ), -// ); -// final p2pkhReceiveAddress = P2PKH( -// data: PaymentData(pubkey: node44.publicKey), -// network: _network) -// .data -// .address!; -// receivingNodes.addAll({ -// "${receivingP2pkhID}_$j": { -// "node": node44, -// "address": p2pkhReceiveAddress, -// } -// }); -// txCountCallArgs.addAll({ -// "${receivingP2pkhID}_$j": p2pkhReceiveAddress, -// }); -// } -// -// // get address tx counts -// final counts = await _getBatchTxCount(addresses: txCountCallArgs); -// -// // check and add appropriate addresses -// for (int k = 0; k < txCountBatchSize; k++) { -// int p2pkhTxCount = counts["${receivingP2pkhID}_$k"]!; -// if (p2pkhTxCount > 0) { -// final node = receivingNodes["${receivingP2pkhID}_$k"]; -// // add address to array -// p2pkhReceiveAddressArray.add(node["address"] as String); -// // set current index -// p2pkhReceiveIndex = index + k; -// // reset counter -// receivingGapCounter = 0; -// // add info to derivations -// p2pkhReceiveDerivations[node["address"] as String] = { -// "pubKey": Format.uint8listToString( -// (node["node"] as bip32.BIP32).publicKey), -// "wif": (node["node"] as bip32.BIP32).toWIF(), -// }; -// } -// -// // increase counter when no tx history found -// if (p2pkhTxCount == 0) { -// receivingGapCounter++; -// } -// } -// } -// -// Logging.instance -// .log("checking change addresses...", level: LogLevel.Info); -// // change addresses -// for (int index = 0; -// index < maxNumberOfIndexesToCheck && -// changeGapCounter < maxUnusedAddressGap; -// index += txCountBatchSize) { -// Logging.instance.log( -// "index: $index, \t changeGapCounter: $changeGapCounter", -// level: LogLevel.Info); -// final changeP2pkhID = "k_$index"; -// Map args = {}; -// final Map changeNodes = {}; -// -// for (int j = 0; j < txCountBatchSize; j++) { -// // bip44 / P2PKH -// final node44 = await compute( -// getBip32NodeFromRootWrapper, -// Tuple4( -// 1, -// index + j, -// root, -// DerivePathType.bip44, -// ), -// ); -// final p2pkhChangeAddress = P2PKH( -// data: PaymentData(pubkey: node44.publicKey), -// network: _network) -// .data -// .address!; -// changeNodes.addAll({ -// "${changeP2pkhID}_$j": { -// "node": node44, -// "address": p2pkhChangeAddress, -// } -// }); -// args.addAll({ -// "${changeP2pkhID}_$j": p2pkhChangeAddress, -// }); -// } -// -// // get address tx counts -// final counts = await _getBatchTxCount(addresses: args); -// -// // check and add appropriate addresses -// for (int k = 0; k < txCountBatchSize; k++) { -// int p2pkhTxCount = counts["${changeP2pkhID}_$k"]!; -// if (p2pkhTxCount > 0) { -// final node = changeNodes["${changeP2pkhID}_$k"]; -// // add address to array -// p2pkhChangeAddressArray.add(node["address"] as String); -// // set current index -// p2pkhChangeIndex = index + k; -// // reset counter -// changeGapCounter = 0; -// // add info to derivations -// p2pkhChangeDerivations[node["address"] as String] = { -// "pubKey": Format.uint8listToString( -// (node["node"] as bip32.BIP32).publicKey), -// "wif": (node["node"] as bip32.BIP32).toWIF(), -// }; -// } -// -// // increase counter when no tx history found -// if (p2pkhTxCount == 0) { -// changeGapCounter++; -// } -// } -// } -// -// // save the derivations (if any) -// if (p2pkhReceiveDerivations.isNotEmpty) { -// await addDerivations( -// chain: 0, -// derivePathType: DerivePathType.bip44, -// derivationsToAdd: p2pkhReceiveDerivations); -// } -// if (p2pkhChangeDerivations.isNotEmpty) { -// await addDerivations( -// chain: 1, -// derivePathType: DerivePathType.bip44, -// derivationsToAdd: p2pkhChangeDerivations); -// } -// -// // If restoring a wallet that never received any funds, then set receivingArray manually -// // If we didn't do this, it'd store an empty array -// if (p2pkhReceiveIndex == -1) { -// final address = -// await _generateAddressForChain(0, 0, DerivePathType.bip44); -// p2pkhReceiveAddressArray.add(address); -// p2pkhReceiveIndex = 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. -// if (p2pkhChangeIndex == -1) { -// final address = -// await _generateAddressForChain(1, 0, DerivePathType.bip44); -// p2pkhChangeAddressArray.add(address); -// p2pkhChangeIndex = 0; -// } -// -// await DB.instance.put( -// boxName: walletId, -// key: 'receivingAddressesP2PKH', -// value: p2pkhReceiveAddressArray); -// await DB.instance.put( -// boxName: walletId, -// key: 'changeAddressesP2PKH', -// value: p2pkhChangeAddressArray); -// await DB.instance.put( -// boxName: walletId, -// key: 'receivingIndexP2PKH', -// value: p2pkhReceiveIndex); -// await DB.instance.put( -// boxName: walletId, key: 'changeIndexP2PKH', value: p2pkhChangeIndex); -// await DB.instance -// .put(boxName: walletId, key: "id", value: _walletId); -// await DB.instance -// .put(boxName: walletId, key: "isFavorite", value: false); -// -// longMutex = false; -// } catch (e, s) { -// Logging.instance.log( -// "Exception rethrown from _recoverWalletFromBIP32SeedPhrase(): $e\n$s", -// level: LogLevel.Info); -// -// longMutex = false; -// rethrow; -// } -// } -// -// Future refreshIfThereIsNewData() async { -// if (longMutex) return false; -// if (_hasCalledExit) return false; -// Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info); -// -// try { -// bool needsRefresh = false; -// Logging.instance.log( -// "notified unconfirmed transactions: ${txTracker.pendings}", -// level: LogLevel.Info); -// Set txnsToCheck = {}; -// -// for (final String txid in txTracker.pendings) { -// if (!txTracker.wasNotifiedConfirmed(txid)) { -// txnsToCheck.add(txid); -// } -// } -// -// for (String txid in txnsToCheck) { -// final txn = await electrumXClient.getTransaction(txHash: txid); -// var confirmations = txn["confirmations"]; -// if (confirmations is! int) continue; -// bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; -// if (!isUnconfirmed) { -// // unconfirmedTxs = {}; -// needsRefresh = true; -// break; -// } -// } -// if (!needsRefresh) { -// var allOwnAddresses = await _fetchAllOwnAddresses(); -// List> allTxs = -// await _fetchHistory(allOwnAddresses); -// final txData = await transactionData; -// for (Map transaction in allTxs) { -// if (txData.findTransaction(transaction['tx_hash'] as String) == -// null) { -// Logging.instance.log( -// " txid not found in address history already ${transaction['tx_hash']}", -// level: LogLevel.Info); -// needsRefresh = true; -// break; -// } -// } -// } -// return needsRefresh; -// } catch (e, s) { -// Logging.instance.log( -// "Exception caught in refreshIfThereIsNewData: $e\n$s", -// level: LogLevel.Info); -// rethrow; -// } -// } -// -// Future getAllTxsToWatch( -// TransactionData txData, -// ) async { -// if (_hasCalledExit) return; -// List unconfirmedTxnsToNotifyPending = []; -// List unconfirmedTxnsToNotifyConfirmed = []; -// -// // Get all unconfirmed incoming transactions -// for (final chunk in txData.txChunks) { -// for (final tx in chunk.transactions) { -// if (tx.confirmedStatus) { -// if (txTracker.wasNotifiedPending(tx.txid) && -// !txTracker.wasNotifiedConfirmed(tx.txid)) { -// unconfirmedTxnsToNotifyConfirmed.add(tx); -// } -// } else { -// if (!txTracker.wasNotifiedPending(tx.txid)) { -// unconfirmedTxnsToNotifyPending.add(tx); -// } -// } -// } -// } -// -// // notify on new incoming transaction -// for (final tx in unconfirmedTxnsToNotifyPending) { -// if (tx.txType == "Received") { -// NotificationApi.showNotification( -// title: "Incoming transaction", -// body: walletName, -// walletId: walletId, -// iconAssetName: Assets.svg.iconFor(coin: coin), -// date: DateTime.now(), -// shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, -// coinName: coin.name, -// txid: tx.txid, -// confirmations: tx.confirmations, -// requiredConfirmations: MINIMUM_CONFIRMATIONS, -// ); -// await txTracker.addNotifiedPending(tx.txid); -// } else if (tx.txType == "Sent") { -// NotificationApi.showNotification( -// title: "Sending transaction", -// body: walletName, -// walletId: walletId, -// iconAssetName: Assets.svg.iconFor(coin: coin), -// date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), -// shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, -// coinName: coin.name, -// txid: tx.txid, -// confirmations: tx.confirmations, -// requiredConfirmations: MINIMUM_CONFIRMATIONS, -// ); -// await txTracker.addNotifiedPending(tx.txid); -// } -// } -// -// // notify on confirmed -// for (final tx in unconfirmedTxnsToNotifyConfirmed) { -// if (tx.txType == "Received") { -// NotificationApi.showNotification( -// title: "Incoming transaction confirmed", -// body: walletName, -// walletId: walletId, -// iconAssetName: Assets.svg.iconFor(coin: coin), -// date: DateTime.now(), -// shouldWatchForUpdates: false, -// coinName: coin.name, -// ); -// -// await txTracker.addNotifiedConfirmed(tx.txid); -// } else if (tx.txType == "Sent") { -// NotificationApi.showNotification( -// title: "Outgoing transaction confirmed", -// body: walletName, -// walletId: walletId, -// iconAssetName: Assets.svg.iconFor(coin: coin), -// date: DateTime.now(), -// shouldWatchForUpdates: false, -// coinName: coin.name, -// ); -// await txTracker.addNotifiedConfirmed(tx.txid); -// } -// } -// } -// -// bool refreshMutex = false; -// -// bool _shouldAutoSync = false; -// -// @override -// bool get shouldAutoSync => _shouldAutoSync; -// -// @override -// set shouldAutoSync(bool shouldAutoSync) { -// if (_shouldAutoSync != shouldAutoSync) { -// _shouldAutoSync = shouldAutoSync; -// if (!shouldAutoSync) { -// timer?.cancel(); -// timer = null; -// stopNetworkAlivePinging(); -// } else { -// startNetworkAlivePinging(); -// refresh(); -// } -// } -// } -// -// //TODO Show percentages properly/more consistently -// /// Refreshes display data for the wallet -// @override -// Future refresh() async { -// final bchaddr = Bitbox.Address.toCashAddress(await currentReceivingAddress); -// print("bchaddr: $bchaddr ${await currentReceivingAddress}"); -// -// if (refreshMutex) { -// Logging.instance.log("$walletId $walletName refreshMutex denied", -// level: LogLevel.Info); -// return; -// } else { -// refreshMutex = true; -// } -// -// try { -// GlobalEventBus.instance.fire( -// WalletSyncStatusChangedEvent( -// WalletSyncStatus.syncing, -// walletId, -// coin, -// ), -// ); -// -// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); -// -// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); -// -// final currentHeight = await chainHeight; -// const storedHeight = 1; //await storedChainHeight; -// -// Logging.instance -// .log("chain height: $currentHeight", level: LogLevel.Info); -// Logging.instance -// .log("cached height: $storedHeight", level: LogLevel.Info); -// -// if (currentHeight != storedHeight) { -// if (currentHeight != -1) { -// // -1 failed to fetch current height -// updateStoredChainHeight(newHeight: currentHeight); -// } -// -// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); -// await _checkChangeAddressForTransactions(DerivePathType.bip44); -// -// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); -// await _checkCurrentReceivingAddressesForTransactions(); -// -// final newTxData = _fetchTransactionData(); -// GlobalEventBus.instance -// .fire(RefreshPercentChangedEvent(0.50, walletId)); -// -// final newUtxoData = _fetchUtxoData(); -// final feeObj = _getFees(); -// GlobalEventBus.instance -// .fire(RefreshPercentChangedEvent(0.60, walletId)); -// -// _transactionData = Future(() => newTxData); -// -// GlobalEventBus.instance -// .fire(RefreshPercentChangedEvent(0.70, walletId)); -// _feeObject = Future(() => feeObj); -// _utxoData = Future(() => newUtxoData); -// GlobalEventBus.instance -// .fire(RefreshPercentChangedEvent(0.80, walletId)); -// -// await getAllTxsToWatch(await newTxData); -// GlobalEventBus.instance -// .fire(RefreshPercentChangedEvent(0.90, walletId)); -// } -// -// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); -// GlobalEventBus.instance.fire( -// WalletSyncStatusChangedEvent( -// WalletSyncStatus.synced, -// walletId, -// coin, -// ), -// ); -// refreshMutex = false; -// -// if (shouldAutoSync) { -// timer ??= Timer.periodic(const Duration(seconds: 150), (timer) async { -// // chain height check currently broken -// // if ((await chainHeight) != (await storedChainHeight)) { -// if (await refreshIfThereIsNewData()) { -// await refresh(); -// GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( -// "New data found in $walletId $walletName in background!", -// walletId)); -// } -// // } -// }); -// } -// } catch (error, strace) { -// refreshMutex = false; -// GlobalEventBus.instance.fire( -// NodeConnectionStatusChangedEvent( -// NodeConnectionStatus.disconnected, -// walletId, -// coin, -// ), -// ); -// GlobalEventBus.instance.fire( -// WalletSyncStatusChangedEvent( -// WalletSyncStatus.unableToSync, -// walletId, -// coin, -// ), -// ); -// Logging.instance.log( -// "Caught exception in refreshWalletData(): $error\n$strace", -// level: LogLevel.Error); -// } -// } -// -// @override -// Future> prepareSend({ -// required String address, -// required int satoshiAmount, -// Map? args, -// }) async { -// try { -// final feeRateType = args?["feeRate"]; -// final feeRateAmount = args?["feeRateAmount"]; -// if (feeRateType is FeeRateType || feeRateAmount is int) { -// late final int rate; -// if (feeRateType is FeeRateType) { -// int fee = 0; -// final feeObject = await fees; -// switch (feeRateType) { -// case FeeRateType.fast: -// fee = feeObject.fast; -// break; -// case FeeRateType.average: -// fee = feeObject.medium; -// break; -// case FeeRateType.slow: -// fee = feeObject.slow; -// break; -// } -// rate = fee; -// } else { -// rate = feeRateAmount as int; -// } -// // check for send all -// bool isSendAll = false; -// final balance = Format.decimalAmountToSatoshis(await availableBalance); -// if (satoshiAmount == balance) { -// isSendAll = true; -// } -// -// final result = -// await coinSelection(satoshiAmount, rate, address, isSendAll); -// Logging.instance.log("SEND RESULT: $result", level: LogLevel.Info); -// if (result is int) { -// switch (result) { -// case 1: -// throw Exception("Insufficient balance!"); -// case 2: -// throw Exception("Insufficient funds to pay for transaction fee!"); -// default: -// throw Exception("Transaction failed with error code $result"); -// } -// } else { -// final hex = result["hex"]; -// if (hex is String) { -// final fee = result["fee"] as int; -// final vSize = result["vSize"] as int; -// -// Logging.instance.log("txHex: $hex", level: LogLevel.Info); -// Logging.instance.log("fee: $fee", level: LogLevel.Info); -// Logging.instance.log("vsize: $vSize", level: LogLevel.Info); -// // fee should never be less than vSize sanity check -// if (fee < vSize) { -// throw Exception( -// "Error in fee calculation: Transaction fee cannot be less than vSize"); -// } -// return result as Map; -// } else { -// throw Exception("sent hex is not a String!!!"); -// } -// } -// } else { -// throw ArgumentError("Invalid fee rate argument provided!"); -// } -// } catch (e, s) { -// Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", -// level: LogLevel.Error); -// rethrow; -// } -// } -// -// @override -// Future confirmSend({dynamic txData}) async { -// try { -// Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); -// final txHash = await _electrumXClient.broadcastTransaction( -// rawTx: txData["hex"] as String); -// Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); -// return txHash; -// } catch (e, s) { -// Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", -// level: LogLevel.Error); -// rethrow; -// } -// } -// -// @override -// Future send({ -// required String toAddress, -// required int amount, -// Map args = const {}, -// }) async { -// try { -// final txData = await prepareSend( -// address: toAddress, satoshiAmount: amount, args: args); -// final txHash = await confirmSend(txData: txData); -// return txHash; -// } catch (e, s) { -// Logging.instance -// .log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error); -// rethrow; -// } -// } -// -// @override -// Future testNetworkConnection() async { -// try { -// final result = await _electrumXClient.ping(); -// return result; -// } catch (_) { -// return false; -// } -// } -// -// Timer? _networkAliveTimer; -// -// void startNetworkAlivePinging() { -// // call once on start right away -// _periodicPingCheck(); -// -// // then periodically check -// _networkAliveTimer = Timer.periodic( -// Constants.networkAliveTimerDuration, -// (_) async { -// _periodicPingCheck(); -// }, -// ); -// } -// -// void _periodicPingCheck() async { -// bool hasNetwork = await testNetworkConnection(); -// _isConnected = hasNetwork; -// if (_isConnected != hasNetwork) { -// NodeConnectionStatus status = hasNetwork -// ? NodeConnectionStatus.connected -// : NodeConnectionStatus.disconnected; -// GlobalEventBus.instance -// .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); -// } -// } -// -// void stopNetworkAlivePinging() { -// _networkAliveTimer?.cancel(); -// _networkAliveTimer = null; -// } -// -// bool _isConnected = false; -// -// @override -// bool get isConnected => _isConnected; -// -// @override -// Future initializeNew() async { -// Logging.instance -// .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); -// -// if ((DB.instance.get(boxName: walletId, key: "id")) != null) { -// throw Exception( -// "Attempted to initialize a new wallet using an existing wallet ID!"); -// } -// await _prefs.init(); -// try { -// await _generateNewWallet(); -// } catch (e, s) { -// Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", -// level: LogLevel.Fatal); -// rethrow; -// } -// await Future.wait([ -// DB.instance.put(boxName: walletId, key: "id", value: _walletId), -// DB.instance -// .put(boxName: walletId, key: "isFavorite", value: false), -// ]); -// } -// -// @override -// Future initializeExisting() async { -// Logging.instance.log("Opening existing ${coin.prettyName} wallet.", -// level: LogLevel.Info); -// -// if ((DB.instance.get(boxName: walletId, key: "id")) == null) { -// throw Exception( -// "Attempted to initialize an existing wallet using an unknown wallet ID!"); -// } -// await _prefs.init(); -// final data = -// DB.instance.get(boxName: walletId, key: "latest_tx_model") -// as TransactionData?; -// if (data != null) { -// _transactionData = Future(() => data); -// } -// } -// -// @override -// Future get transactionData => -// _transactionData ??= _fetchTransactionData(); -// Future? _transactionData; -// -// @override -// bool validateAddress(String address) { -// try { -// // 0 for bitcoincash: address scheme, 1 for legacy address -// final format = Bitbox.Address.detectFormat(address); -// print("format $format"); -// return true; -// } catch (e, s) { -// return false; -// } -// } -// -// @override -// String get walletId => _walletId; -// late String _walletId; -// -// @override -// String get walletName => _walletName; -// late String _walletName; -// -// // setter for updating on rename -// @override -// set walletName(String newName) => _walletName = newName; -// -// late ElectrumX _electrumXClient; -// -// ElectrumX get electrumXClient => _electrumXClient; -// -// late CachedElectrumX _cachedElectrumXClient; -// -// CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; -// -// late FlutterSecureStorageInterface _secureStore; -// -// late PriceAPI _priceAPI; -// -// BitcoinCashWallet({ -// required String walletId, -// required String walletName, -// required Coin coin, -// required ElectrumX client, -// required CachedElectrumX cachedClient, -// required TransactionNotificationTracker tracker, -// PriceAPI? priceAPI, -// FlutterSecureStorageInterface? secureStore, -// }) { -// txTracker = tracker; -// _walletId = walletId; -// _walletName = walletName; -// _coin = coin; -// _electrumXClient = client; -// _cachedElectrumXClient = cachedClient; -// -// _priceAPI = priceAPI ?? PriceAPI(Client()); -// _secureStore = -// secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); -// } -// -// @override -// Future updateNode(bool shouldRefresh) async { -// final failovers = NodeService() -// .failoverNodesFor(coin: coin) -// .map((e) => ElectrumXNode( -// address: e.host, -// port: e.port, -// name: e.name, -// id: e.id, -// useSSL: e.useSSL, -// )) -// .toList(); -// final newNode = await getCurrentNode(); -// _cachedElectrumXClient = CachedElectrumX.from( -// node: newNode, -// prefs: _prefs, -// failovers: failovers, -// ); -// _electrumXClient = ElectrumX.from( -// node: newNode, -// prefs: _prefs, -// failovers: failovers, -// ); -// -// if (shouldRefresh) { -// refresh(); -// } -// } -// -// Future> _getMnemonicList() async { -// final mnemonicString = -// await _secureStore.read(key: '${_walletId}_mnemonic'); -// if (mnemonicString == null) { -// return []; -// } -// final List data = mnemonicString.split(' '); -// return data; -// } -// -// Future getCurrentNode() async { -// final node = NodeService().getPrimaryNodeFor(coin: coin) ?? -// DefaultNodes.getNodeFor(coin); -// -// return ElectrumXNode( -// address: node.host, -// port: node.port, -// name: node.name, -// useSSL: node.useSSL, -// id: node.id, -// ); -// } -// -// Future> _fetchAllOwnAddresses() async { -// final List allAddresses = []; -// -// final receivingAddressesP2PKH = DB.instance.get( -// boxName: walletId, key: 'receivingAddressesP2PKH') as List; -// final changeAddressesP2PKH = -// DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') -// as List; -// -// // for (var i = 0; i < receivingAddresses.length; i++) { -// // if (!allAddresses.contains(receivingAddresses[i])) { -// // allAddresses.add(receivingAddresses[i]); -// // } -// // } -// // for (var i = 0; i < changeAddresses.length; i++) { -// // if (!allAddresses.contains(changeAddresses[i])) { -// // allAddresses.add(changeAddresses[i]); -// // } -// // } -// for (var i = 0; i < receivingAddressesP2PKH.length; i++) { -// if (!allAddresses.contains(receivingAddressesP2PKH[i])) { -// allAddresses.add(receivingAddressesP2PKH[i] as String); -// } -// } -// for (var i = 0; i < changeAddressesP2PKH.length; i++) { -// if (!allAddresses.contains(changeAddressesP2PKH[i])) { -// allAddresses.add(changeAddressesP2PKH[i] as String); -// } -// } -// return allAddresses; -// } -// -// Future _getFees() async { -// try { -// //TODO adjust numbers for different speeds? -// const int f = 1, m = 5, s = 20; -// -// final fast = await electrumXClient.estimateFee(blocks: f); -// final medium = await electrumXClient.estimateFee(blocks: m); -// final slow = await electrumXClient.estimateFee(blocks: s); -// -// final feeObject = FeeObject( -// numberOfBlocksFast: f, -// numberOfBlocksAverage: m, -// numberOfBlocksSlow: s, -// fast: Format.decimalAmountToSatoshis(fast), -// medium: Format.decimalAmountToSatoshis(medium), -// slow: Format.decimalAmountToSatoshis(slow), -// ); -// -// Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); -// return feeObject; -// } catch (e) { -// Logging.instance -// .log("Exception rethrown from _getFees(): $e", level: LogLevel.Error); -// rethrow; -// } -// } -// -// Future _generateNewWallet() async { -// Logging.instance -// .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); -// if (!integrationTestFlag) { -// final features = await electrumXClient.getServerFeatures(); -// Logging.instance.log("features: $features", level: LogLevel.Info); -// switch (coin) { -// case Coin.bitcoincash: -// if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { -// throw Exception("genesis hash does not match main net!"); -// } -// break; -// case Coin.bitcoincashTestnet: -// if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { -// throw Exception("genesis hash does not match test net!"); -// } -// break; -// default: -// throw Exception( -// "Attempted to generate a BitcoinWallet using a non bitcoin coin type: ${coin.name}"); -// } -// } -// -// // this should never fail -// if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { -// throw Exception( -// "Attempted to overwrite mnemonic on generate new wallet!"); -// } -// await _secureStore.write( -// key: '${_walletId}_mnemonic', -// value: bip39.generateMnemonic(strength: 256)); -// -// // Set relevant indexes -// await DB.instance -// .put(boxName: walletId, key: "receivingIndexP2PKH", value: 0); -// await DB.instance -// .put(boxName: walletId, key: "changeIndexP2PKH", value: 0); -// await DB.instance.put( -// boxName: walletId, -// key: 'blocked_tx_hashes', -// value: ["0xdefault"], -// ); // A list of transaction hashes to represent frozen utxos in wallet -// // initialize address book entries -// await DB.instance.put( -// boxName: walletId, -// key: 'addressBookEntries', -// value: {}); -// -// // Generate and add addresses to relevant arrays -// // final initialReceivingAddress = -// // await _generateAddressForChain(0, 0, DerivePathType.bip44); -// // final initialChangeAddress = -// // await _generateAddressForChain(1, 0, DerivePathType.bip44); -// final initialReceivingAddressP2PKH = -// await _generateAddressForChain(0, 0, DerivePathType.bip44); -// final initialChangeAddressP2PKH = -// await _generateAddressForChain(1, 0, DerivePathType.bip44); -// -// // await _addToAddressesArrayForChain( -// // initialReceivingAddress, 0, DerivePathType.bip44); -// // await _addToAddressesArrayForChain( -// // initialChangeAddress, 1, DerivePathType.bip44); -// await _addToAddressesArrayForChain( -// initialReceivingAddressP2PKH, 0, DerivePathType.bip44); -// await _addToAddressesArrayForChain( -// initialChangeAddressP2PKH, 1, DerivePathType.bip44); -// -// // this._currentReceivingAddress = Future(() => initialReceivingAddress); -// _currentReceivingAddressP2PKH = Future(() => initialReceivingAddressP2PKH); -// -// Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); -// } -// -// /// Generates a new internal or external chain address for the wallet using a BIP44 derivation path. -// /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! -// /// [index] - This can be any integer >= 0 -// Future _generateAddressForChain( -// int chain, -// int index, -// DerivePathType derivePathType, -// ) async { -// final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); -// final node = await compute( -// getBip32NodeWrapper, -// Tuple5( -// chain, -// index, -// mnemonic!, -// _network, -// derivePathType, -// ), -// ); -// final data = PaymentData(pubkey: node.publicKey); -// String address; -// -// switch (derivePathType) { -// case DerivePathType.bip44: -// address = P2PKH(data: data, network: _network).data.address!; -// break; -// // default: -// // // should never hit this due to all enum cases handled -// // return null; -// } -// -// // add generated address & info to derivations -// await addDerivation( -// chain: chain, -// address: address, -// pubKey: Format.uint8listToString(node.publicKey), -// wif: node.toWIF(), -// derivePathType: derivePathType, -// ); -// -// return address; -// } -// -// /// Increases the index for either the internal or external chain, depending on [chain]. -// /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! -// Future _incrementAddressIndexForChain( -// int chain, DerivePathType derivePathType) async { -// // Here we assume chain == 1 if it isn't 0 -// String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; -// switch (derivePathType) { -// case DerivePathType.bip44: -// indexKey += "P2PKH"; -// break; -// } -// -// final newIndex = -// (DB.instance.get(boxName: walletId, key: indexKey)) + 1; -// await DB.instance -// .put(boxName: walletId, key: indexKey, value: newIndex); -// } -// -// /// Adds [address] to the relevant chain's address array, which is determined by [chain]. -// /// [address] - Expects a standard native segwit address -// /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! -// Future _addToAddressesArrayForChain( -// String address, int chain, DerivePathType derivePathType) async { -// String chainArray = ''; -// if (chain == 0) { -// chainArray = 'receivingAddresses'; -// } else { -// chainArray = 'changeAddresses'; -// } -// switch (derivePathType) { -// case DerivePathType.bip44: -// chainArray += "P2PKH"; -// break; -// } -// -// final addressArray = -// DB.instance.get(boxName: walletId, key: chainArray); -// if (addressArray == null) { -// Logging.instance.log( -// 'Attempting to add the following to $chainArray array for chain $chain:${[ -// address -// ]}', -// level: LogLevel.Info); -// await DB.instance -// .put(boxName: walletId, key: chainArray, value: [address]); -// } else { -// // Make a deep copy of the existing list -// final List newArray = []; -// addressArray -// .forEach((dynamic _address) => newArray.add(_address as String)); -// newArray.add(address); // Add the address passed into the method -// await DB.instance -// .put(boxName: walletId, key: chainArray, value: newArray); -// } -// } -// -// /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] -// /// and -// /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! -// Future _getCurrentAddressForChain( -// int chain, DerivePathType derivePathType) async { -// // Here, we assume that chain == 1 if it isn't 0 -// String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; -// switch (derivePathType) { -// case DerivePathType.bip44: -// arrayKey += "P2PKH"; -// break; -// } -// final internalChainArray = -// DB.instance.get(boxName: walletId, key: arrayKey); -// return internalChainArray.last as String; -// } -// -// String _buildDerivationStorageKey( -// {required int chain, required DerivePathType derivePathType}) { -// String key; -// String chainId = chain == 0 ? "receive" : "change"; -// switch (derivePathType) { -// case DerivePathType.bip44: -// key = "${walletId}_${chainId}DerivationsP2PKH"; -// break; -// } -// return key; -// } -// -// Future> _fetchDerivations( -// {required int chain, required DerivePathType derivePathType}) async { -// // build lookup key -// final key = _buildDerivationStorageKey( -// chain: chain, derivePathType: derivePathType); -// -// // fetch current derivations -// final derivationsString = await _secureStore.read(key: key); -// return Map.from( -// jsonDecode(derivationsString ?? "{}") as Map); -// } -// -// /// Add a single derivation to the local secure storage for [chain] and -// /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. -// /// This will overwrite a previous entry where the address of the new derivation -// /// matches a derivation currently stored. -// Future addDerivation({ -// required int chain, -// required String address, -// required String pubKey, -// required String wif, -// required DerivePathType derivePathType, -// }) async { -// // build lookup key -// final key = _buildDerivationStorageKey( -// chain: chain, derivePathType: derivePathType); -// -// // fetch current derivations -// final derivationsString = await _secureStore.read(key: key); -// final derivations = -// Map.from(jsonDecode(derivationsString ?? "{}") as Map); -// -// // add derivation -// derivations[address] = { -// "pubKey": pubKey, -// "wif": wif, -// }; -// -// // save derivations -// final newReceiveDerivationsString = jsonEncode(derivations); -// await _secureStore.write(key: key, value: newReceiveDerivationsString); -// } -// -// /// Add multiple derivations to the local secure storage for [chain] and -// /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. -// /// This will overwrite any previous entries where the address of the new derivation -// /// matches a derivation currently stored. -// /// The [derivationsToAdd] must be in the format of: -// /// { -// /// addressA : { -// /// "pubKey": , -// /// "wif": , -// /// }, -// /// addressB : { -// /// "pubKey": , -// /// "wif": , -// /// }, -// /// } -// Future addDerivations({ -// required int chain, -// required DerivePathType derivePathType, -// required Map derivationsToAdd, -// }) async { -// // build lookup key -// final key = _buildDerivationStorageKey( -// chain: chain, derivePathType: derivePathType); -// -// // fetch current derivations -// final derivationsString = await _secureStore.read(key: key); -// final derivations = -// Map.from(jsonDecode(derivationsString ?? "{}") as Map); -// -// // add derivation -// derivations.addAll(derivationsToAdd); -// -// // save derivations -// final newReceiveDerivationsString = jsonEncode(derivations); -// await _secureStore.write(key: key, value: newReceiveDerivationsString); -// } -// -// Future _fetchUtxoData() async { -// final List allAddresses = await _fetchAllOwnAddresses(); -// -// try { -// final fetchedUtxoList = >>[]; -// -// final Map>> batches = {}; -// const batchSizeMax = 10; -// int batchNumber = 0; -// for (int i = 0; i < allAddresses.length; i++) { -// if (batches[batchNumber] == null) { -// batches[batchNumber] = {}; -// } -// final scripthash = _convertToScriptHash(allAddresses[i], _network); -// print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash"); -// batches[batchNumber]!.addAll({ -// scripthash: [scripthash] -// }); -// if (i % batchSizeMax == batchSizeMax - 1) { -// batchNumber++; -// } -// } -// -// for (int i = 0; i < batches.length; i++) { -// final response = -// await _electrumXClient.getBatchUTXOs(args: batches[i]!); -// for (final entry in response.entries) { -// if (entry.value.isNotEmpty) { -// fetchedUtxoList.add(entry.value); -// } -// } -// } -// -// final priceData = -// await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); -// Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; -// final List> outputArray = []; -// int satoshiBalance = 0; -// int satoshiBalancePending = 0; -// -// for (int i = 0; i < fetchedUtxoList.length; i++) { -// for (int j = 0; j < fetchedUtxoList[i].length; j++) { -// int value = fetchedUtxoList[i][j]["value"] as int; -// satoshiBalance += value; -// -// final txn = await cachedElectrumXClient.getTransaction( -// txHash: fetchedUtxoList[i][j]["tx_hash"] as String, -// verbose: true, -// coin: coin, -// ); -// -// final Map utxo = {}; -// final int confirmations = txn["confirmations"] as int? ?? 0; -// final bool confirmed = txn["confirmations"] == null -// ? false -// : txn["confirmations"] as int >= MINIMUM_CONFIRMATIONS; -// if (!confirmed) { -// satoshiBalancePending += value; -// } -// -// utxo["txid"] = txn["txid"]; -// utxo["vout"] = fetchedUtxoList[i][j]["tx_pos"]; -// utxo["value"] = value; -// -// utxo["status"] = {}; -// utxo["status"]["confirmed"] = confirmed; -// utxo["status"]["confirmations"] = confirmations; -// utxo["status"]["block_height"] = fetchedUtxoList[i][j]["height"]; -// utxo["status"]["block_hash"] = txn["blockhash"]; -// utxo["status"]["block_time"] = txn["blocktime"]; -// -// final fiatValue = ((Decimal.fromInt(value) * currentPrice) / -// Decimal.fromInt(Constants.satsPerCoin)) -// .toDecimal(scaleOnInfinitePrecision: 2); -// utxo["rawWorth"] = fiatValue; -// utxo["fiatWorth"] = fiatValue.toString(); -// outputArray.add(utxo); -// } -// } -// -// Decimal currencyBalanceRaw = -// ((Decimal.fromInt(satoshiBalance) * currentPrice) / -// Decimal.fromInt(Constants.satsPerCoin)) -// .toDecimal(scaleOnInfinitePrecision: 2); -// -// final Map result = { -// "total_user_currency": currencyBalanceRaw.toString(), -// "total_sats": satoshiBalance, -// "total_btc": (Decimal.fromInt(satoshiBalance) / -// Decimal.fromInt(Constants.satsPerCoin)) -// .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) -// .toString(), -// "outputArray": outputArray, -// "unconfirmed": satoshiBalancePending, -// }; -// -// final dataModel = UtxoData.fromJson(result); -// -// final List allOutputs = dataModel.unspentOutputArray; -// Logging.instance -// .log('Outputs fetched: $allOutputs', level: LogLevel.Info); -// await _sortOutputs(allOutputs); -// await DB.instance.put( -// boxName: walletId, key: 'latest_utxo_model', value: dataModel); -// await DB.instance.put( -// boxName: walletId, -// key: 'totalBalance', -// value: dataModel.satoshiBalance); -// return dataModel; -// } catch (e, s) { -// Logging.instance -// .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); -// final latestTxModel = -// DB.instance.get(boxName: walletId, key: 'latest_utxo_model'); -// -// if (latestTxModel == null) { -// final emptyModel = { -// "total_user_currency": "0.00", -// "total_sats": 0, -// "total_btc": "0", -// "outputArray": [] -// }; -// return UtxoData.fromJson(emptyModel); -// } else { -// Logging.instance -// .log("Old output model located", level: LogLevel.Warning); -// return latestTxModel as models.UtxoData; -// } -// } -// } -// -// /// Takes in a list of UtxoObjects and adds a name (dependent on object index within list) -// /// and checks for the txid associated with the utxo being blocked and marks it accordingly. -// /// Now also checks for output labeling. -// Future _sortOutputs(List utxos) async { -// final blockedHashArray = -// DB.instance.get(boxName: walletId, key: 'blocked_tx_hashes') -// as List?; -// final List lst = []; -// if (blockedHashArray != null) { -// for (var hash in blockedHashArray) { -// lst.add(hash as String); -// } -// } -// final labels = -// DB.instance.get(boxName: walletId, key: 'labels') as Map? ?? -// {}; -// -// outputsList = []; -// -// for (var i = 0; i < utxos.length; i++) { -// if (labels[utxos[i].txid] != null) { -// utxos[i].txName = labels[utxos[i].txid] as String? ?? ""; -// } else { -// utxos[i].txName = 'Output #$i'; -// } -// -// if (utxos[i].status.confirmed == false) { -// outputsList.add(utxos[i]); -// } else { -// if (lst.contains(utxos[i].txid)) { -// utxos[i].blocked = true; -// outputsList.add(utxos[i]); -// } else if (!lst.contains(utxos[i].txid)) { -// outputsList.add(utxos[i]); -// } -// } -// } -// } -// -// Future getTxCount({required String address}) async { -// String? scripthash; -// try { -// scripthash = _convertToScriptHash(address, _network); -// final transactions = -// await electrumXClient.getHistory(scripthash: scripthash); -// return transactions.length; -// } catch (e) { -// Logging.instance.log( -// "Exception rethrown in _getTxCount(address: $address, scripthash: $scripthash): $e", -// level: LogLevel.Error); -// rethrow; -// } -// } -// -// Future> _getBatchTxCount({ -// required Map addresses, -// }) async { -// try { -// final Map> args = {}; -// print("Address $addresses"); -// for (final entry in addresses.entries) { -// args[entry.key] = [_convertToScriptHash(entry.value, _network)]; -// } -// -// print("Args ${jsonEncode(args)}"); -// -// final response = await electrumXClient.getBatchHistory(args: args); -// print("Response ${jsonEncode(response)}"); -// final Map result = {}; -// for (final entry in response.entries) { -// result[entry.key] = entry.value.length; -// } -// print("result ${jsonEncode(result)}"); -// return result; -// } catch (e, s) { -// Logging.instance.log( -// "Exception rethrown in _getBatchTxCount(address: $addresses: $e\n$s", -// level: LogLevel.Error); -// rethrow; -// } -// } -// -// Future _checkReceivingAddressForTransactions( -// DerivePathType derivePathType) async { -// try { -// final String currentExternalAddr = -// await _getCurrentAddressForChain(0, derivePathType); -// final int txCount = await getTxCount(address: currentExternalAddr); -// Logging.instance.log( -// 'Number of txs for current receiving address $currentExternalAddr: $txCount', -// level: LogLevel.Info); -// -// if (txCount >= 1) { -// // First increment the receiving index -// await _incrementAddressIndexForChain(0, derivePathType); -// -// // Check the new receiving index -// String indexKey = "receivingIndex"; -// switch (derivePathType) { -// case DerivePathType.bip44: -// indexKey += "P2PKH"; -// break; -// } -// final newReceivingIndex = -// DB.instance.get(boxName: walletId, key: indexKey) as int; -// -// // Use new index to derive a new receiving address -// final newReceivingAddress = await _generateAddressForChain( -// 0, newReceivingIndex, derivePathType); -// -// // Add that new receiving address to the array of receiving addresses -// await _addToAddressesArrayForChain( -// newReceivingAddress, 0, derivePathType); -// -// // Set the new receiving address that the service -// -// switch (derivePathType) { -// case DerivePathType.bip44: -// _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); -// break; -// } -// } -// } on SocketException catch (se, s) { -// Logging.instance.log( -// "SocketException caught in _checkReceivingAddressForTransactions($derivePathType): $se\n$s", -// level: LogLevel.Error); -// return; -// } catch (e, s) { -// Logging.instance.log( -// "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s", -// level: LogLevel.Error); -// rethrow; -// } -// } -// -// Future _checkChangeAddressForTransactions( -// DerivePathType derivePathType) async { -// try { -// final String currentExternalAddr = -// await _getCurrentAddressForChain(1, derivePathType); -// final int txCount = await getTxCount(address: currentExternalAddr); -// Logging.instance.log( -// 'Number of txs for current change address $currentExternalAddr: $txCount', -// level: LogLevel.Info); -// -// if (txCount >= 1) { -// // First increment the change index -// await _incrementAddressIndexForChain(1, derivePathType); -// -// // Check the new change index -// String indexKey = "changeIndex"; -// switch (derivePathType) { -// case DerivePathType.bip44: -// indexKey += "P2PKH"; -// break; -// } -// final newChangeIndex = -// DB.instance.get(boxName: walletId, key: indexKey) as int; -// -// // Use new index to derive a new change address -// final newChangeAddress = -// await _generateAddressForChain(1, newChangeIndex, derivePathType); -// -// // Add that new receiving address to the array of change addresses -// await _addToAddressesArrayForChain(newChangeAddress, 1, derivePathType); -// } -// } catch (e, s) { -// Logging.instance.log( -// "Exception rethrown from _checkChangeAddressForTransactions($derivePathType): $e\n$s", -// level: LogLevel.Error); -// rethrow; -// } -// } -// -// Future _checkCurrentReceivingAddressesForTransactions() async { -// try { -// for (final type in DerivePathType.values) { -// await _checkReceivingAddressForTransactions(type); -// } -// } catch (e, s) { -// Logging.instance.log( -// "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", -// level: LogLevel.Info); -// rethrow; -// } -// } -// -// /// public wrapper because dart can't test private... -// Future checkCurrentReceivingAddressesForTransactions() async { -// if (Platform.environment["FLUTTER_TEST"] == "true") { -// try { -// return _checkCurrentReceivingAddressesForTransactions(); -// } catch (_) { -// rethrow; -// } -// } -// } -// -// Future _checkCurrentChangeAddressesForTransactions() async { -// try { -// for (final type in DerivePathType.values) { -// await _checkChangeAddressForTransactions(type); -// } -// } catch (e, s) { -// Logging.instance.log( -// "Exception rethrown from _checkCurrentChangeAddressesForTransactions(): $e\n$s", -// level: LogLevel.Error); -// rethrow; -// } -// } -// -// /// public wrapper because dart can't test private... -// Future checkCurrentChangeAddressesForTransactions() async { -// if (Platform.environment["FLUTTER_TEST"] == "true") { -// try { -// return _checkCurrentChangeAddressesForTransactions(); -// } catch (_) { -// rethrow; -// } -// } -// } -// -// /// attempts to convert a string to a valid scripthash -// /// -// /// Returns the scripthash or throws an exception on invalid bch address -// String _convertToScriptHash(String bchAddress, NetworkType network) { -// try { -// final output = Address.addressToOutputScript(bchAddress, network); -// final hash = sha256.convert(output.toList(growable: false)).toString(); -// -// final chars = hash.split(""); -// final reversedPairs = []; -// var i = chars.length - 1; -// while (i > 0) { -// reversedPairs.add(chars[i - 1]); -// reversedPairs.add(chars[i]); -// i -= 2; -// } -// return reversedPairs.join(""); -// } catch (e) { -// rethrow; -// } -// } -// -// Future>> _fetchHistory( -// List allAddresses) async { -// try { -// List> allTxHashes = []; -// -// final Map>> batches = {}; -// final Map requestIdToAddressMap = {}; -// const batchSizeMax = 10; -// int batchNumber = 0; -// for (int i = 0; i < allAddresses.length; i++) { -// if (batches[batchNumber] == null) { -// batches[batchNumber] = {}; -// } -// final scripthash = _convertToScriptHash(allAddresses[i], _network); -// final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); -// requestIdToAddressMap[id] = allAddresses[i]; -// batches[batchNumber]!.addAll({ -// id: [scripthash] -// }); -// if (i % batchSizeMax == batchSizeMax - 1) { -// batchNumber++; -// } -// } -// -// for (int i = 0; i < batches.length; i++) { -// final response = -// await _electrumXClient.getBatchHistory(args: batches[i]!); -// for (final entry in response.entries) { -// for (int j = 0; j < entry.value.length; j++) { -// entry.value[j]["address"] = requestIdToAddressMap[entry.key]; -// if (!allTxHashes.contains(entry.value[j])) { -// allTxHashes.add(entry.value[j]); -// } -// } -// } -// } -// -// return allTxHashes; -// } catch (e, s) { -// Logging.instance.log("_fetchHistory: $e\n$s", level: LogLevel.Error); -// rethrow; -// } -// } -// -// bool _duplicateTxCheck( -// List> allTransactions, String txid) { -// for (int i = 0; i < allTransactions.length; i++) { -// if (allTransactions[i]["txid"] == txid) { -// return true; -// } -// } -// return false; -// } -// -// Future _fetchTransactionData() async { -// final List allAddresses = await _fetchAllOwnAddresses(); -// -// final changeAddressesP2PKH = -// DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') -// as List; -// -// final List> allTxHashes = -// await _fetchHistory(allAddresses); -// -// final cachedTransactions = -// DB.instance.get(boxName: walletId, key: 'latest_tx_model') -// as TransactionData?; -// int latestTxnBlockHeight = -// DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") -// as int? ?? -// 0; -// -// final unconfirmedCachedTransactions = -// cachedTransactions?.getAllTransactions() ?? {}; -// unconfirmedCachedTransactions -// .removeWhere((key, value) => value.confirmedStatus); -// -// print("CACHED_TRANSACTIONS_IS $cachedTransactions"); -// if (cachedTransactions != null) { -// for (final tx in allTxHashes.toList(growable: false)) { -// final txHeight = tx["height"] as int; -// if (txHeight > 0 && -// txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { -// if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { -// allTxHashes.remove(tx); -// } -// } -// } -// } -// -// List> allTransactions = []; -// -// for (final txHash in allTxHashes) { -// Logging.instance.log("bch: $txHash", level: LogLevel.Info); -// final tx = await cachedElectrumXClient.getTransaction( -// txHash: txHash["tx_hash"] as String, -// verbose: true, -// coin: coin, -// ); -// -// // Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}"); -// // TODO fix this for sent to self transactions? -// if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { -// tx["address"] = txHash["address"]; -// tx["height"] = txHash["height"]; -// allTransactions.add(tx); -// } -// } -// -// Logging.instance.log("addAddresses: $allAddresses", level: LogLevel.Info); -// Logging.instance.log("allTxHashes: $allTxHashes", level: LogLevel.Info); -// -// Logging.instance.log("allTransactions length: ${allTransactions.length}", -// level: LogLevel.Info); -// -// final priceData = -// await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); -// Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; -// final List> midSortedArray = []; -// -// for (final txObject in allTransactions) { -// List sendersArray = []; -// List recipientsArray = []; -// -// // Usually only has value when txType = 'Send' -// int inputAmtSentFromWallet = 0; -// // Usually has value regardless of txType due to change addresses -// int outputAmtAddressedToWallet = 0; -// int fee = 0; -// -// Map midSortedTx = {}; -// -// for (int i = 0; i < (txObject["vin"] as List).length; i++) { -// final input = txObject["vin"][i] as Map; -// final prevTxid = input["txid"] as String; -// final prevOut = input["vout"] as int; -// -// final tx = await _cachedElectrumXClient.getTransaction( -// txHash: prevTxid, coin: coin); -// -// for (final out in tx["vout"] as List) { -// if (prevOut == out["n"]) { -// final address = out["scriptPubKey"]["addresses"][0] as String?; -// if (address != null) { -// sendersArray.add(address); -// } -// } -// } -// } -// -// Logging.instance.log("sendersArray: $sendersArray", level: LogLevel.Info); -// -// for (final output in txObject["vout"] as List) { -// final address = output["scriptPubKey"]["addresses"][0] as String?; -// if (address != null) { -// recipientsArray.add(address); -// } -// } -// -// Logging.instance -// .log("recipientsArray: $recipientsArray", level: LogLevel.Info); -// -// final foundInSenders = -// allAddresses.any((element) => sendersArray.contains(element)); -// Logging.instance -// .log("foundInSenders: $foundInSenders", level: LogLevel.Info); -// -// // If txType = Sent, then calculate inputAmtSentFromWallet -// if (foundInSenders) { -// int totalInput = 0; -// for (int i = 0; i < (txObject["vin"] as List).length; i++) { -// final input = txObject["vin"][i] as Map; -// final prevTxid = input["txid"] as String; -// final prevOut = input["vout"] as int; -// final tx = await _cachedElectrumXClient.getTransaction( -// txHash: prevTxid, -// coin: coin, -// ); -// -// for (final out in tx["vout"] as List) { -// if (prevOut == out["n"]) { -// inputAmtSentFromWallet += -// (Decimal.parse(out["value"].toString()) * -// Decimal.fromInt(Constants.satsPerCoin)) -// .toBigInt() -// .toInt(); -// } -// } -// } -// totalInput = inputAmtSentFromWallet; -// int totalOutput = 0; -// -// for (final output in txObject["vout"] as List) { -// final address = output["scriptPubKey"]["addresses"][0]; -// final value = output["value"]; -// final _value = (Decimal.parse(value.toString()) * -// Decimal.fromInt(Constants.satsPerCoin)) -// .toBigInt() -// .toInt(); -// totalOutput += _value; -// if (changeAddressesP2PKH.contains(address)) { -// inputAmtSentFromWallet -= _value; -// } else { -// // change address from 'sent from' to the 'sent to' address -// txObject["address"] = address; -// } -// } -// // calculate transaction fee -// fee = totalInput - totalOutput; -// // subtract fee from sent to calculate correct value of sent tx -// inputAmtSentFromWallet -= fee; -// } else { -// // counters for fee calculation -// int totalOut = 0; -// int totalIn = 0; -// -// // add up received tx value -// for (final output in txObject["vout"] as List) { -// final address = output["scriptPubKey"]["addresses"][0]; -// if (address != null) { -// final value = (Decimal.parse(output["value"].toString()) * -// Decimal.fromInt(Constants.satsPerCoin)) -// .toBigInt() -// .toInt(); -// totalOut += value; -// if (allAddresses.contains(address)) { -// outputAmtAddressedToWallet += value; -// } -// } -// } -// -// // calculate fee for received tx -// for (int i = 0; i < (txObject["vin"] as List).length; i++) { -// final input = txObject["vin"][i] as Map; -// final prevTxid = input["txid"] as String; -// final prevOut = input["vout"] as int; -// final tx = await _cachedElectrumXClient.getTransaction( -// txHash: prevTxid, -// coin: coin, -// ); -// -// for (final out in tx["vout"] as List) { -// if (prevOut == out["n"]) { -// totalIn += (Decimal.parse(out["value"].toString()) * -// Decimal.fromInt(Constants.satsPerCoin)) -// .toBigInt() -// .toInt(); -// } -// } -// } -// fee = totalIn - totalOut; -// } -// -// // create final tx map -// midSortedTx["txid"] = txObject["txid"]; -// midSortedTx["confirmed_status"] = (txObject["confirmations"] != null) && -// (txObject["confirmations"] as int >= MINIMUM_CONFIRMATIONS); -// midSortedTx["confirmations"] = txObject["confirmations"] ?? 0; -// midSortedTx["timestamp"] = txObject["blocktime"] ?? -// (DateTime.now().millisecondsSinceEpoch ~/ 1000); -// -// if (foundInSenders) { -// midSortedTx["txType"] = "Sent"; -// midSortedTx["amount"] = inputAmtSentFromWallet; -// final String worthNow = -// ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / -// Decimal.fromInt(Constants.satsPerCoin)) -// .toDecimal(scaleOnInfinitePrecision: 2) -// .toStringAsFixed(2); -// midSortedTx["worthNow"] = worthNow; -// midSortedTx["worthAtBlockTimestamp"] = worthNow; -// } else { -// midSortedTx["txType"] = "Received"; -// midSortedTx["amount"] = outputAmtAddressedToWallet; -// final worthNow = -// ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / -// Decimal.fromInt(Constants.satsPerCoin)) -// .toDecimal(scaleOnInfinitePrecision: 2) -// .toStringAsFixed(2); -// midSortedTx["worthNow"] = worthNow; -// } -// midSortedTx["aliens"] = []; -// midSortedTx["fees"] = fee; -// midSortedTx["address"] = txObject["address"]; -// midSortedTx["inputSize"] = txObject["vin"].length; -// midSortedTx["outputSize"] = txObject["vout"].length; -// midSortedTx["inputs"] = txObject["vin"]; -// midSortedTx["outputs"] = txObject["vout"]; -// -// final int height = txObject["height"] as int; -// midSortedTx["height"] = height; -// -// if (height >= latestTxnBlockHeight) { -// latestTxnBlockHeight = height; -// } -// -// midSortedArray.add(midSortedTx); -// } -// -// // sort by date ---- //TODO not sure if needed -// // shouldn't be any issues with a null timestamp but I got one at some point? -// midSortedArray -// .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); -// // { -// // final aT = a["timestamp"]; -// // final bT = b["timestamp"]; -// // -// // if (aT == null && bT == null) { -// // return 0; -// // } else if (aT == null) { -// // return -1; -// // } else if (bT == null) { -// // return 1; -// // } else { -// // return bT - aT; -// // } -// // }); -// -// // buildDateTimeChunks -// final Map result = {"dateTimeChunks": []}; -// final dateArray = []; -// -// for (int i = 0; i < midSortedArray.length; i++) { -// final txObject = midSortedArray[i]; -// final date = extractDateFromTimestamp(txObject["timestamp"] as int); -// final txTimeArray = [txObject["timestamp"], date]; -// -// if (dateArray.contains(txTimeArray[1])) { -// result["dateTimeChunks"].forEach((dynamic chunk) { -// if (extractDateFromTimestamp(chunk["timestamp"] as int) == -// txTimeArray[1]) { -// if (chunk["transactions"] == null) { -// chunk["transactions"] = >[]; -// } -// chunk["transactions"].add(txObject); -// } -// }); -// } else { -// dateArray.add(txTimeArray[1]); -// final chunk = { -// "timestamp": txTimeArray[0], -// "transactions": [txObject], -// }; -// result["dateTimeChunks"].add(chunk); -// } -// } -// -// final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; -// transactionsMap -// .addAll(TransactionData.fromJson(result).getAllTransactions()); -// -// final txModel = TransactionData.fromMap(transactionsMap); -// -// await DB.instance.put( -// boxName: walletId, -// key: 'storedTxnDataHeight', -// value: latestTxnBlockHeight); -// await DB.instance.put( -// boxName: walletId, key: 'latest_tx_model', value: txModel); -// -// return txModel; -// } -// -// int estimateTxFee({required int vSize, required int feeRatePerKB}) { -// return vSize * (feeRatePerKB / 1000).ceil(); -// } -// -// /// The coinselection algorithm decides whether or not the user is eligible to make the transaction -// /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return -// /// a map containing the tx hex along with other important information. If not, then it will return -// /// an integer (1 or 2) -// dynamic coinSelection(int satoshiAmountToSend, int selectedTxFeeRate, -// String _recipientAddress, bool isSendAll, -// {int additionalOutputs = 0, List? utxos}) async { -// Logging.instance -// .log("Starting coinSelection ----------", level: LogLevel.Info); -// final List availableOutputs = utxos ?? outputsList; -// final List spendableOutputs = []; -// int spendableSatoshiValue = 0; -// -// // Build list of spendable outputs and totaling their satoshi amount -// for (var i = 0; i < availableOutputs.length; i++) { -// if (availableOutputs[i].blocked == false && -// availableOutputs[i].status.confirmed == true) { -// spendableOutputs.add(availableOutputs[i]); -// spendableSatoshiValue += availableOutputs[i].value; -// } -// } -// -// // sort spendable by age (oldest first) -// spendableOutputs.sort( -// (a, b) => b.status.confirmations.compareTo(a.status.confirmations)); -// -// Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", -// level: LogLevel.Info); -// Logging.instance -// .log("spendableOutputs: $spendableOutputs", level: LogLevel.Info); -// Logging.instance.log("spendableSatoshiValue: $spendableSatoshiValue", -// level: LogLevel.Info); -// Logging.instance -// .log("satoshiAmountToSend: $satoshiAmountToSend", level: LogLevel.Info); -// // If the amount the user is trying to send is smaller than the amount that they have spendable, -// // then return 1, which indicates that they have an insufficient balance. -// if (spendableSatoshiValue < satoshiAmountToSend) { -// return 1; -// // If the amount the user wants to send is exactly equal to the amount they can spend, then return -// // 2, which indicates that they are not leaving enough over to pay the transaction fee -// } else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) { -// return 2; -// } -// // If neither of these statements pass, we assume that the user has a spendable balance greater -// // than the amount they're attempting to send. Note that this value still does not account for -// // the added transaction fee, which may require an extra input and will need to be checked for -// // later on. -// -// // Possible situation right here -// int satoshisBeingUsed = 0; -// int inputsBeingConsumed = 0; -// List utxoObjectsToUse = []; -// -// for (var i = 0; -// satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; -// i++) { -// utxoObjectsToUse.add(spendableOutputs[i]); -// satoshisBeingUsed += spendableOutputs[i].value; -// inputsBeingConsumed += 1; -// } -// for (int i = 0; -// i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; -// i++) { -// utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); -// satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; -// inputsBeingConsumed += 1; -// } -// -// Logging.instance -// .log("satoshisBeingUsed: $satoshisBeingUsed", level: LogLevel.Info); -// Logging.instance -// .log("inputsBeingConsumed: $inputsBeingConsumed", level: LogLevel.Info); -// Logging.instance -// .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); -// Logging.instance -// .log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info); -// -// // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray -// List recipientsArray = [_recipientAddress]; -// List recipientsAmtArray = [satoshiAmountToSend]; -// -// // gather required signing data -// final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); -// -// if (isSendAll) { -// Logging.instance -// .log("Attempting to send all $coin", level: LogLevel.Info); -// -// final int vSizeForOneOutput = (await buildTransaction( -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// recipients: [_recipientAddress], -// satoshiAmounts: [satoshisBeingUsed - 1], -// ))["vSize"] as int; -// int feeForOneOutput = estimateTxFee( -// vSize: vSizeForOneOutput, -// feeRatePerKB: selectedTxFeeRate, -// ); -// if (feeForOneOutput < (vSizeForOneOutput + 1)) { -// feeForOneOutput = (vSizeForOneOutput + 1); -// } -// -// final int amount = satoshiAmountToSend - feeForOneOutput; -// dynamic txn = await buildTransaction( -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// recipients: recipientsArray, -// satoshiAmounts: [amount], -// ); -// Map transactionObject = { -// "hex": txn["hex"], -// "recipient": recipientsArray[0], -// "recipientAmt": amount, -// "fee": feeForOneOutput, -// "vSize": txn["vSize"], -// }; -// return transactionObject; -// } -// -// final int vSizeForOneOutput = (await buildTransaction( -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// recipients: [_recipientAddress], -// satoshiAmounts: [satoshisBeingUsed - 1], -// ))["vSize"] as int; -// final int vSizeForTwoOutPuts = (await buildTransaction( -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// recipients: [ -// _recipientAddress, -// await _getCurrentAddressForChain(1, DerivePathType.bip44), -// ], -// satoshiAmounts: [ -// satoshiAmountToSend, -// satoshisBeingUsed - satoshiAmountToSend - 1, -// ], // dust limit is the minimum amount a change output should be -// ))["vSize"] as int; -// debugPrint("vSizeForOneOutput $vSizeForOneOutput"); -// debugPrint("vSizeForTwoOutPuts $vSizeForTwoOutPuts"); -// -// // Assume 1 output, only for recipient and no change -// var feeForOneOutput = estimateTxFee( -// vSize: vSizeForOneOutput, -// feeRatePerKB: selectedTxFeeRate, -// ); -// // Assume 2 outputs, one for recipient and one for change -// var feeForTwoOutputs = estimateTxFee( -// vSize: vSizeForTwoOutPuts, -// feeRatePerKB: selectedTxFeeRate, -// ); -// -// Logging.instance -// .log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info); -// Logging.instance -// .log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info); -// if (feeForOneOutput < (vSizeForOneOutput + 1)) { -// feeForOneOutput = (vSizeForOneOutput + 1); -// } -// if (feeForTwoOutputs < ((vSizeForTwoOutPuts + 1))) { -// feeForTwoOutputs = ((vSizeForTwoOutPuts + 1)); -// } -// -// Logging.instance -// .log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info); -// Logging.instance -// .log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info); -// -// if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { -// if (satoshisBeingUsed - satoshiAmountToSend > -// feeForOneOutput + DUST_LIMIT) { -// // Here, we know that theoretically, we may be able to include another output(change) but we first need to -// // factor in the value of this output in satoshis. -// int changeOutputSize = -// satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs; -// // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and -// // the second output's size > 546 satoshis, we perform the mechanics required to properly generate and use a new -// // change address. -// if (changeOutputSize > DUST_LIMIT && -// satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == -// feeForTwoOutputs) { -// // generate new change address if current change address has been used -// await _checkChangeAddressForTransactions(DerivePathType.bip44); -// final String newChangeAddress = -// await _getCurrentAddressForChain(1, DerivePathType.bip44); -// -// int feeBeingPaid = -// satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; -// -// recipientsArray.add(newChangeAddress); -// recipientsAmtArray.add(changeOutputSize); -// // At this point, we have the outputs we're going to use, the amounts to send along with which addresses -// // we intend to send these amounts to. We have enough to send instructions to build the transaction. -// Logging.instance.log('2 outputs in tx', level: LogLevel.Info); -// Logging.instance -// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); -// Logging.instance.log('Recipient output size: $satoshiAmountToSend', -// level: LogLevel.Info); -// Logging.instance.log('Change Output Size: $changeOutputSize', -// level: LogLevel.Info); -// Logging.instance.log( -// 'Difference (fee being paid): $feeBeingPaid sats', -// level: LogLevel.Info); -// Logging.instance -// .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); -// dynamic txn = await buildTransaction( -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// recipients: recipientsArray, -// satoshiAmounts: recipientsAmtArray, -// ); -// -// // make sure minimum fee is accurate if that is being used -// if (txn["vSize"] - feeBeingPaid == 1) { -// int changeOutputSize = -// satoshisBeingUsed - satoshiAmountToSend - (txn["vSize"] as int); -// feeBeingPaid = -// satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; -// recipientsAmtArray.removeLast(); -// recipientsAmtArray.add(changeOutputSize); -// Logging.instance.log('Adjusted Input size: $satoshisBeingUsed', -// level: LogLevel.Info); -// Logging.instance.log( -// 'Adjusted Recipient output size: $satoshiAmountToSend', -// level: LogLevel.Info); -// Logging.instance.log( -// 'Adjusted Change Output Size: $changeOutputSize', -// level: LogLevel.Info); -// Logging.instance.log( -// 'Adjusted Difference (fee being paid): $feeBeingPaid sats', -// level: LogLevel.Info); -// Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', -// level: LogLevel.Info); -// txn = await buildTransaction( -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// recipients: recipientsArray, -// satoshiAmounts: recipientsAmtArray, -// ); -// } -// -// Map transactionObject = { -// "hex": txn["hex"], -// "recipient": recipientsArray[0], -// "recipientAmt": recipientsAmtArray[0], -// "fee": feeBeingPaid, -// "vSize": txn["vSize"], -// }; -// return transactionObject; -// } else { -// // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize -// // is smaller than or equal to [DUST_LIMIT]. Revert to single output transaction. -// Logging.instance.log('1 output in tx', level: LogLevel.Info); -// Logging.instance -// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); -// Logging.instance.log('Recipient output size: $satoshiAmountToSend', -// level: LogLevel.Info); -// Logging.instance.log( -// 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', -// level: LogLevel.Info); -// Logging.instance -// .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); -// dynamic txn = await buildTransaction( -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// recipients: recipientsArray, -// satoshiAmounts: recipientsAmtArray, -// ); -// Map transactionObject = { -// "hex": txn["hex"], -// "recipient": recipientsArray[0], -// "recipientAmt": recipientsAmtArray[0], -// "fee": satoshisBeingUsed - satoshiAmountToSend, -// "vSize": txn["vSize"], -// }; -// return transactionObject; -// } -// } else { -// // No additional outputs needed since adding one would mean that it'd be smaller than 546 sats -// // which makes it uneconomical to add to the transaction. Here, we pass data directly to instruct -// // the wallet to begin crafting the transaction that the user requested. -// Logging.instance.log('1 output in tx', level: LogLevel.Info); -// Logging.instance -// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); -// Logging.instance.log('Recipient output size: $satoshiAmountToSend', -// level: LogLevel.Info); -// Logging.instance.log( -// 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', -// level: LogLevel.Info); -// Logging.instance -// .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); -// dynamic txn = await buildTransaction( -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// recipients: recipientsArray, -// satoshiAmounts: recipientsAmtArray, -// ); -// Map transactionObject = { -// "hex": txn["hex"], -// "recipient": recipientsArray[0], -// "recipientAmt": recipientsAmtArray[0], -// "fee": satoshisBeingUsed - satoshiAmountToSend, -// "vSize": txn["vSize"], -// }; -// return transactionObject; -// } -// } else if (satoshisBeingUsed - satoshiAmountToSend == feeForOneOutput) { -// // In this scenario, no additional change output is needed since inputs - outputs equal exactly -// // what we need to pay for fees. Here, we pass data directly to instruct the wallet to begin -// // crafting the transaction that the user requested. -// Logging.instance.log('1 output in tx', level: LogLevel.Info); -// Logging.instance -// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); -// Logging.instance.log('Recipient output size: $satoshiAmountToSend', -// level: LogLevel.Info); -// Logging.instance.log( -// 'Fee being paid: ${satoshisBeingUsed - satoshiAmountToSend} sats', -// level: LogLevel.Info); -// Logging.instance -// .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); -// dynamic txn = await buildTransaction( -// utxosToUse: utxoObjectsToUse, -// utxoSigningData: utxoSigningData, -// recipients: recipientsArray, -// satoshiAmounts: recipientsAmtArray, -// ); -// Map transactionObject = { -// "hex": txn["hex"], -// "recipient": recipientsArray[0], -// "recipientAmt": recipientsAmtArray[0], -// "fee": feeForOneOutput, -// "vSize": txn["vSize"], -// }; -// return transactionObject; -// } else { -// // Remember that returning 2 indicates that the user does not have a sufficient balance to -// // pay for the transaction fee. Ideally, at this stage, we should check if the user has any -// // additional outputs they're able to spend and then recalculate fees. -// Logging.instance.log( -// 'Cannot pay tx fee - checking for more outputs and trying again', -// level: LogLevel.Warning); -// // try adding more outputs -// if (spendableOutputs.length > inputsBeingConsumed) { -// return coinSelection(satoshiAmountToSend, selectedTxFeeRate, -// _recipientAddress, isSendAll, -// additionalOutputs: additionalOutputs + 1, utxos: utxos); -// } -// return 2; -// } -// } -// -// Future> fetchBuildTxData( -// List utxosToUse, -// ) async { -// // return data -// Map results = {}; -// Map> addressTxid = {}; -// -// // addresses to check -// List addressesP2PKH = []; -// -// try { -// // Populating the addresses to check -// for (var i = 0; i < utxosToUse.length; i++) { -// final txid = utxosToUse[i].txid; -// final tx = await _cachedElectrumXClient.getTransaction( -// txHash: txid, -// coin: coin, -// ); -// -// for (final output in tx["vout"] as List) { -// final n = output["n"]; -// if (n != null && n == utxosToUse[i].vout) { -// final address = output["scriptPubKey"]["addresses"][0] as String; -// if (!addressTxid.containsKey(address)) { -// addressTxid[address] = []; -// } -// (addressTxid[address] as List).add(txid); -// switch (addressType(address: address)) { -// case DerivePathType.bip44: -// addressesP2PKH.add(address); -// break; -// } -// } -// } -// } -// -// // p2pkh / bip44 -// final p2pkhLength = addressesP2PKH.length; -// if (p2pkhLength > 0) { -// final receiveDerivations = await _fetchDerivations( -// chain: 0, -// derivePathType: DerivePathType.bip44, -// ); -// final changeDerivations = await _fetchDerivations( -// chain: 1, -// derivePathType: DerivePathType.bip44, -// ); -// for (int i = 0; i < p2pkhLength; i++) { -// // receives -// final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; -// // if a match exists it will not be null -// if (receiveDerivation != null) { -// final data = P2PKH( -// data: PaymentData( -// pubkey: Format.stringToUint8List( -// receiveDerivation["pubKey"] as String)), -// network: _network, -// ).data; -// -// for (String tx in addressTxid[addressesP2PKH[i]]!) { -// results[tx] = { -// "output": data.output, -// "keyPair": ECPair.fromWIF( -// receiveDerivation["wif"] as String, -// network: _network, -// ), -// }; -// } -// } else { -// // if its not a receive, check change -// final changeDerivation = changeDerivations[addressesP2PKH[i]]; -// // if a match exists it will not be null -// if (changeDerivation != null) { -// final data = P2PKH( -// data: PaymentData( -// pubkey: Format.stringToUint8List( -// changeDerivation["pubKey"] as String)), -// network: _network, -// ).data; -// -// for (String tx in addressTxid[addressesP2PKH[i]]!) { -// results[tx] = { -// "output": data.output, -// "keyPair": ECPair.fromWIF( -// changeDerivation["wif"] as String, -// network: _network, -// ), -// }; -// } -// } -// } -// } -// } -// -// return results; -// } catch (e, s) { -// Logging.instance -// .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); -// rethrow; -// } -// } -// -// /// Builds and signs a transaction -// Future> buildTransaction({ -// required List utxosToUse, -// required Map utxoSigningData, -// required List recipients, -// required List satoshiAmounts, -// }) async { -// final builder = Bitbox.Bitbox.transactionBuilder(); -// -// // retrieve address' utxos from the rest api -// List _utxos = -// []; // await Bitbox.Address.utxo(address) as List; -// utxosToUse.forEach((element) { -// _utxos.add(Bitbox.Utxo( -// element.txid, -// element.vout, -// Bitbox.BitcoinCash.fromSatoshi(element.value), -// element.value, -// 0, -// MINIMUM_CONFIRMATIONS + 1)); -// }); -// Logger.print("bch utxos: ${_utxos}"); -// -// // placeholder for input signatures -// final signatures = []; -// -// // placeholder for total input balance -// int totalBalance = 0; -// -// // iterate through the list of address _utxos and use them as inputs for the -// // withdrawal transaction -// _utxos.forEach((Bitbox.Utxo utxo) { -// // add the utxo as an input for the transaction -// builder.addInput(utxo.txid, utxo.vout); -// final ec = utxoSigningData[utxo.txid]["keyPair"] as ECPair; -// -// final bitboxEC = Bitbox.ECPair.fromWIF(ec.toWIF()); -// -// // add a signature to the list to be used later -// signatures.add({ -// "vin": signatures.length, -// "key_pair": bitboxEC, -// "original_amount": utxo.satoshis -// }); -// -// totalBalance += utxo.satoshis; -// }); -// -// // calculate the fee based on number of inputs and one expected output -// final fee = -// Bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length); -// -// // calculate how much balance will be left over to spend after the fee -// final sendAmount = totalBalance - fee; -// -// // add the output based on the address provided in the testing data -// for (int i = 0; i < recipients.length; i++) { -// String recipient = recipients[i]; -// int satoshiAmount = satoshiAmounts[i]; -// builder.addOutput(recipient, satoshiAmount); -// } -// -// // sign all inputs -// signatures.forEach((signature) { -// builder.sign( -// signature["vin"] as int, -// signature["key_pair"] as Bitbox.ECPair, -// signature["original_amount"] as int); -// }); -// -// // build the transaction -// final tx = builder.build(); -// final txHex = tx.toHex(); -// final vSize = tx.virtualSize(); -// Logger.print("bch raw hex: $txHex"); -// -// return {"hex": txHex, "vSize": vSize}; -// } -// -// @override -// Future fullRescan( -// int maxUnusedAddressGap, -// int maxNumberOfIndexesToCheck, -// ) async { -// Logging.instance.log("Starting full rescan!", level: LogLevel.Info); -// longMutex = true; -// GlobalEventBus.instance.fire( -// WalletSyncStatusChangedEvent( -// WalletSyncStatus.syncing, -// walletId, -// coin, -// ), -// ); -// -// // clear cache -// _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); -// -// // back up data -// await _rescanBackup(); -// -// try { -// final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); -// await _recoverWalletFromBIP32SeedPhrase( -// mnemonic: mnemonic!, -// maxUnusedAddressGap: maxUnusedAddressGap, -// maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, -// ); -// -// longMutex = false; -// Logging.instance.log("Full rescan complete!", level: LogLevel.Info); -// GlobalEventBus.instance.fire( -// WalletSyncStatusChangedEvent( -// WalletSyncStatus.synced, -// walletId, -// coin, -// ), -// ); -// } catch (e, s) { -// GlobalEventBus.instance.fire( -// WalletSyncStatusChangedEvent( -// WalletSyncStatus.unableToSync, -// walletId, -// coin, -// ), -// ); -// -// // restore from backup -// await _rescanRestore(); -// -// longMutex = false; -// Logging.instance.log("Exception rethrown from fullRescan(): $e\n$s", -// level: LogLevel.Error); -// rethrow; -// } -// } -// -// Future _rescanRestore() async { -// Logging.instance.log("starting rescan restore", level: LogLevel.Info); -// -// // restore from backup -// // p2pkh -// final tempReceivingAddressesP2PKH = DB.instance -// .get(boxName: walletId, key: 'receivingAddressesP2PKH_BACKUP'); -// final tempChangeAddressesP2PKH = DB.instance -// .get(boxName: walletId, key: 'changeAddressesP2PKH_BACKUP'); -// final tempReceivingIndexP2PKH = DB.instance -// .get(boxName: walletId, key: 'receivingIndexP2PKH_BACKUP'); -// final tempChangeIndexP2PKH = DB.instance -// .get(boxName: walletId, key: 'changeIndexP2PKH_BACKUP'); -// await DB.instance.put( -// boxName: walletId, -// key: 'receivingAddressesP2PKH', -// value: tempReceivingAddressesP2PKH); -// await DB.instance.put( -// boxName: walletId, -// key: 'changeAddressesP2PKH', -// value: tempChangeAddressesP2PKH); -// await DB.instance.put( -// boxName: walletId, -// key: 'receivingIndexP2PKH', -// value: tempReceivingIndexP2PKH); -// await DB.instance.put( -// boxName: walletId, -// key: 'changeIndexP2PKH', -// value: tempChangeIndexP2PKH); -// await DB.instance.delete( -// key: 'receivingAddressesP2PKH_BACKUP', boxName: walletId); -// await DB.instance -// .delete(key: 'changeAddressesP2PKH_BACKUP', boxName: walletId); -// await DB.instance -// .delete(key: 'receivingIndexP2PKH_BACKUP', boxName: walletId); -// await DB.instance -// .delete(key: 'changeIndexP2PKH_BACKUP', boxName: walletId); -// -// // P2PKH derivations -// final p2pkhReceiveDerivationsString = await _secureStore.read( -// key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); -// final p2pkhChangeDerivationsString = await _secureStore.read( -// key: "${walletId}_changeDerivationsP2PKH_BACKUP"); -// -// await _secureStore.write( -// key: "${walletId}_receiveDerivationsP2PKH", -// value: p2pkhReceiveDerivationsString); -// await _secureStore.write( -// key: "${walletId}_changeDerivationsP2PKH", -// value: p2pkhChangeDerivationsString); -// -// await _secureStore.delete( -// key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); -// await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH_BACKUP"); -// -// // UTXOs -// final utxoData = DB.instance -// .get(boxName: walletId, key: 'latest_utxo_model_BACKUP'); -// await DB.instance.put( -// boxName: walletId, key: 'latest_utxo_model', value: utxoData); -// await DB.instance -// .delete(key: 'latest_utxo_model_BACKUP', boxName: walletId); -// -// Logging.instance.log("rescan restore complete", level: LogLevel.Info); -// } -// -// Future _rescanBackup() async { -// Logging.instance.log("starting rescan backup", level: LogLevel.Info); -// -// // backup current and clear data -// // p2pkh -// final tempReceivingAddressesP2PKH = DB.instance -// .get(boxName: walletId, key: 'receivingAddressesP2PKH'); -// await DB.instance.put( -// boxName: walletId, -// key: 'receivingAddressesP2PKH_BACKUP', -// value: tempReceivingAddressesP2PKH); -// await DB.instance -// .delete(key: 'receivingAddressesP2PKH', boxName: walletId); -// -// final tempChangeAddressesP2PKH = DB.instance -// .get(boxName: walletId, key: 'changeAddressesP2PKH'); -// await DB.instance.put( -// boxName: walletId, -// key: 'changeAddressesP2PKH_BACKUP', -// value: tempChangeAddressesP2PKH); -// await DB.instance -// .delete(key: 'changeAddressesP2PKH', boxName: walletId); -// -// final tempReceivingIndexP2PKH = -// DB.instance.get(boxName: walletId, key: 'receivingIndexP2PKH'); -// await DB.instance.put( -// boxName: walletId, -// key: 'receivingIndexP2PKH_BACKUP', -// value: tempReceivingIndexP2PKH); -// await DB.instance -// .delete(key: 'receivingIndexP2PKH', boxName: walletId); -// -// final tempChangeIndexP2PKH = -// DB.instance.get(boxName: walletId, key: 'changeIndexP2PKH'); -// await DB.instance.put( -// boxName: walletId, -// key: 'changeIndexP2PKH_BACKUP', -// value: tempChangeIndexP2PKH); -// await DB.instance -// .delete(key: 'changeIndexP2PKH', boxName: walletId); -// -// // P2PKH derivations -// final p2pkhReceiveDerivationsString = -// await _secureStore.read(key: "${walletId}_receiveDerivationsP2PKH"); -// final p2pkhChangeDerivationsString = -// await _secureStore.read(key: "${walletId}_changeDerivationsP2PKH"); -// -// await _secureStore.write( -// key: "${walletId}_receiveDerivationsP2PKH_BACKUP", -// value: p2pkhReceiveDerivationsString); -// await _secureStore.write( -// key: "${walletId}_changeDerivationsP2PKH_BACKUP", -// value: p2pkhChangeDerivationsString); -// -// await _secureStore.delete(key: "${walletId}_receiveDerivationsP2PKH"); -// await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH"); -// -// // UTXOs -// final utxoData = -// DB.instance.get(boxName: walletId, key: 'latest_utxo_model'); -// await DB.instance.put( -// boxName: walletId, key: 'latest_utxo_model_BACKUP', value: utxoData); -// await DB.instance -// .delete(key: 'latest_utxo_model', boxName: walletId); -// -// Logging.instance.log("rescan backup complete", level: LogLevel.Info); -// } -// -// @override -// set isFavorite(bool markFavorite) { -// DB.instance.put( -// boxName: walletId, key: "isFavorite", value: markFavorite); -// } -// -// @override -// bool get isFavorite { -// try { -// return DB.instance.get(boxName: walletId, key: "isFavorite") -// as bool; -// } catch (e, s) { -// Logging.instance -// .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); -// rethrow; -// } -// } -// -// @override -// bool get isRefreshing => refreshMutex; -// -// bool isActive = false; -// -// @override -// void Function(bool)? get onIsActiveWalletChanged => -// (isActive) => this.isActive = isActive; -// -// @override -// Future estimateFeeFor(int satoshiAmount, int feeRate) async { -// final available = Format.decimalAmountToSatoshis(await availableBalance); -// -// if (available == satoshiAmount) { -// return satoshiAmount - sweepAllEstimate(feeRate); -// } else if (satoshiAmount <= 0 || satoshiAmount > available) { -// return roughFeeEstimate(1, 2, feeRate); -// } -// -// int runningBalance = 0; -// int inputCount = 0; -// for (final output in outputsList) { -// runningBalance += output.value; -// inputCount++; -// if (runningBalance > satoshiAmount) { -// break; -// } -// } -// -// final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); -// final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); -// -// if (runningBalance - satoshiAmount > oneOutPutFee) { -// if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { -// final change = runningBalance - satoshiAmount - twoOutPutFee; -// if (change > DUST_LIMIT && -// runningBalance - satoshiAmount - change == twoOutPutFee) { -// return runningBalance - satoshiAmount - change; -// } else { -// return runningBalance - satoshiAmount; -// } -// } else { -// return runningBalance - satoshiAmount; -// } -// } else if (runningBalance - satoshiAmount == oneOutPutFee) { -// return oneOutPutFee; -// } else { -// return twoOutPutFee; -// } -// } -// -// // TODO: correct formula for bch? -// int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { -// return ((181 * inputCount) + (34 * outputCount) + 10) * -// (feeRatePerKB / 1000).ceil(); -// } -// -// int sweepAllEstimate(int feeRate) { -// int available = 0; -// int inputCount = 0; -// for (final output in outputsList) { -// if (output.status.confirmed) { -// available += output.value; -// inputCount++; -// } -// } -// -// // transaction will only have 1 output minus the fee -// final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); -// -// return available - estimatedFee; -// } -// -// @override -// Future generateNewAddress() async { -// try { -// await _incrementAddressIndexForChain( -// 0, DerivePathType.bip44); // First increment the receiving index -// final newReceivingIndex = DB.instance.get( -// boxName: walletId, -// key: 'receivingIndexP2PKH') as int; // Check the new receiving index -// final newReceivingAddress = await _generateAddressForChain( -// 0, -// newReceivingIndex, -// DerivePathType -// .bip44); // Use new index to derive a new receiving address -// await _addToAddressesArrayForChain( -// newReceivingAddress, -// 0, -// DerivePathType -// .bip44); // Add that new receiving address to the array of receiving addresses -// _currentReceivingAddressP2PKH = Future(() => -// newReceivingAddress); // Set the new receiving address that the service -// -// return true; -// } catch (e, s) { -// Logging.instance.log( -// "Exception rethrown from generateNewAddress(): $e\n$s", -// level: LogLevel.Error); -// return false; -// } -// } -// } -// -// // Bitcoincash Network -// final bitcoincash = NetworkType( -// messagePrefix: '\x18Bitcoin Signed Message:\n', -// bech32: 'bc', -// bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), -// pubKeyHash: 0x00, -// scriptHash: 0x05, -// wif: 0x80); -// -// final bitcoincashtestnet = NetworkType( -// messagePrefix: '\x18Bitcoin Signed Message:\n', -// bech32: 'tb', -// bip32: Bip32Type(public: 0x043587cf, private: 0x04358394), -// pubKeyHash: 0x6f, -// scriptHash: 0xc4, -// wif: 0xef); +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:bech32/bech32.dart'; +import 'package:bip32/bip32.dart' as bip32; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bitbox/bitbox.dart' as Bitbox; +import 'package:bitcoindart/bitcoindart.dart'; +import 'package:bs58check/bs58check.dart' as bs58check; +import 'package:crypto/crypto.dart'; +import 'package:decimal/decimal.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:http/http.dart'; +import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx.dart'; +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/models/models.dart' as models; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/paymint/transactions_model.dart'; +import 'package:stackwallet/models/paymint/utxo_model.dart'; +import 'package:stackwallet/services/coins/coin_service.dart'; +import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/notifications_api.dart'; +import 'package:stackwallet/services/price.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:tuple/tuple.dart'; +import 'package:uuid/uuid.dart'; + +const int MINIMUM_CONFIRMATIONS = 3; +const int DUST_LIMIT = 546; + +const String GENESIS_HASH_MAINNET = + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; +const String GENESIS_HASH_TESTNET = + "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"; + +enum DerivePathType { bip44, bip49 } + +bip32.BIP32 getBip32Node(int chain, int index, String mnemonic, + NetworkType network, DerivePathType derivePathType) { + final root = getBip32Root(mnemonic, network); + + final node = getBip32NodeFromRoot(chain, index, root, derivePathType); + return node; +} + +/// wrapper for compute() +bip32.BIP32 getBip32NodeWrapper( + Tuple5 args, +) { + return getBip32Node( + args.item1, + args.item2, + args.item3, + args.item4, + args.item5, + ); +} + +bip32.BIP32 getBip32NodeFromRoot( + int chain, int index, bip32.BIP32 root, DerivePathType derivePathType) { + String coinType; + switch (root.network.wif) { + case 0x80: // bch mainnet wif + coinType = "145"; // bch mainnet + break; + case 0xef: // bch testnet wif + coinType = "1"; // bch testnet + break; + default: + throw Exception("Invalid Bitcoincash network type used!"); + } + switch (derivePathType) { + case DerivePathType.bip44: + return root.derivePath("m/44'/$coinType'/0'/$chain/$index"); + case DerivePathType.bip49: + return root.derivePath("m/49'/$coinType'/0'/$chain/$index"); + default: + throw Exception("DerivePathType must not be null."); + } +} + +/// wrapper for compute() +bip32.BIP32 getBip32NodeFromRootWrapper( + Tuple4 args, +) { + return getBip32NodeFromRoot( + args.item1, + args.item2, + args.item3, + args.item4, + ); +} + +bip32.BIP32 getBip32Root(String mnemonic, NetworkType network) { + final seed = bip39.mnemonicToSeed(mnemonic); + final networkType = bip32.NetworkType( + wif: network.wif, + bip32: bip32.Bip32Type( + public: network.bip32.public, + private: network.bip32.private, + ), + ); + + final root = bip32.BIP32.fromSeed(seed, networkType); + return root; +} + +/// wrapper for compute() +bip32.BIP32 getBip32RootWrapper(Tuple2 args) { + return getBip32Root(args.item1, args.item2); +} + +class BitcoinCashWallet extends CoinServiceAPI { + static const integrationTestFlag = + bool.fromEnvironment("IS_INTEGRATION_TEST"); + final _prefs = Prefs.instance; + + Timer? timer; + late Coin _coin; + + late final TransactionNotificationTracker txTracker; + + NetworkType get _network { + switch (coin) { + case Coin.bitcoincash: + return bitcoincash; + case Coin.bitcoincashTestnet: + return bitcoincashtestnet; + default: + throw Exception("Bitcoincash network type not set!"); + } + } + + List outputsList = []; + + @override + Coin get coin => _coin; + + @override + Future> get allOwnAddresses => + _allOwnAddresses ??= _fetchAllOwnAddresses(); + Future>? _allOwnAddresses; + + Future? _utxoData; + Future get utxoData => _utxoData ??= _fetchUtxoData(); + + @override + Future> get unspentOutputs async => + (await utxoData).unspentOutputArray; + + @override + Future get availableBalance async { + final data = await utxoData; + return Format.satoshisToAmount( + data.satoshiBalance - data.satoshiBalanceUnconfirmed); + } + + @override + Future get pendingBalance async { + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + } + + @override + Future get balanceMinusMaxFee async => + (await availableBalance) - + (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(); + + @override + Future get totalBalance async { + if (!isActive) { + final totalBalance = DB.instance + .get(boxName: walletId, key: 'totalBalance') as int?; + if (totalBalance == null) { + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalance); + } else { + return Format.satoshisToAmount(totalBalance); + } + } + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalance); + } + + @override + Future get currentReceivingAddress => + _currentReceivingAddressP2PKH ??= + _getCurrentAddressForChain(0, DerivePathType.bip44); + Future? _currentReceivingAddressP2PKH; + + Future get currentReceivingAddressP2SH => + _currentReceivingAddressP2SH ??= + _getCurrentAddressForChain(0, DerivePathType.bip49); + Future? _currentReceivingAddressP2SH; + + @override + Future exit() async { + _hasCalledExit = true; + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } + + bool _hasCalledExit = false; + + @override + bool get hasCalledExit => _hasCalledExit; + + @override + Future get fees => _feeObject ??= _getFees(); + Future? _feeObject; + + @override + Future get maxFee async { + final fee = (await fees).fast; + final satsFee = + Format.satoshisToAmount(fee) * Decimal.fromInt(Constants.satsPerCoin); + return satsFee.floor().toBigInt().toInt(); + } + + @override + Future> get mnemonic => _getMnemonicList(); + + Future get chainHeight async { + try { + final result = await _electrumXClient.getBlockHeadTip(); + return result["height"] as int; + } catch (e, s) { + Logging.instance.log("Exception caught in chainHeight: $e\n$s", + level: LogLevel.Error); + return -1; + } + } + + Future get storedChainHeight async { + final storedHeight = DB.instance + .get(boxName: walletId, key: "storedChainHeight") as int?; + return storedHeight ?? 0; + } + + Future updateStoredChainHeight({required int newHeight}) async { + DB.instance.put( + boxName: walletId, key: "storedChainHeight", value: newHeight); + } + + DerivePathType addressType({required String address}) { + Uint8List? decodeBase58; + Segwit? decodeBech32; + try { + decodeBase58 = bs58check.decode(address); + } catch (err) { + // Base58check decode fail + } + if (decodeBase58 != null) { + if (decodeBase58[0] == _network.pubKeyHash) { + // P2PKH + return DerivePathType.bip44; + } + + if (decodeBase58[0] == _network.scriptHash) { + // P2SH + return DerivePathType.bip49; + } + throw ArgumentError('Invalid version or Network mismatch'); + } else { + try { + decodeBech32 = segwit.decode(address); + } catch (err) { + // Bech32 decode fail + } + if (_network.bech32 != decodeBech32!.hrp) { + throw ArgumentError('Invalid prefix or Network mismatch'); + } + if (decodeBech32.version != 0) { + throw ArgumentError('Invalid address version'); + } + } + throw ArgumentError('$address has no matching Script'); + } + + bool longMutex = false; + + @override + Future recoverFromMnemonic({ + required String mnemonic, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height, + }) async { + longMutex = true; + final start = DateTime.now(); + try { + Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag", + level: LogLevel.Info); + if (!integrationTestFlag) { + final features = await electrumXClient.getServerFeatures(); + Logging.instance.log("features: $features", level: LogLevel.Info); + switch (coin) { + case Coin.bitcoincash: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main net!"); + } + break; + case Coin.bitcoincashTestnet: + if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + throw Exception("genesis hash does not match test net!"); + } + break; + default: + throw Exception( + "Attempted to generate a BitcoinCashWallet using a non bch coin type: ${coin.name}"); + } + } + // check to make sure we aren't overwriting a mnemonic + // this should never fail + if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { + longMutex = false; + throw Exception("Attempted to overwrite mnemonic on restore!"); + } + await _secureStore.write( + key: '${_walletId}_mnemonic', value: mnemonic.trim()); + await _recoverWalletFromBIP32SeedPhrase( + mnemonic: mnemonic.trim(), + maxUnusedAddressGap: maxUnusedAddressGap, + maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, + ); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverFromMnemonic(): $e\n$s", + level: LogLevel.Error); + longMutex = false; + rethrow; + } + longMutex = false; + + final end = DateTime.now(); + Logging.instance.log( + "$walletName recovery time: ${end.difference(start).inMilliseconds} millis", + level: LogLevel.Info); + } + + Future _recoverWalletFromBIP32SeedPhrase({ + required String mnemonic, + int maxUnusedAddressGap = 20, + int maxNumberOfIndexesToCheck = 1000, + }) async { + longMutex = true; + + Map> p2pkhReceiveDerivations = {}; + Map> p2shReceiveDerivations = {}; + Map> p2pkhChangeDerivations = {}; + Map> p2shChangeDerivations = {}; + + final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); + + List p2pkhReceiveAddressArray = []; + List p2shReceiveAddressArray = []; + int p2pkhReceiveIndex = -1; + int p2shReceiveIndex = -1; + + List p2pkhChangeAddressArray = []; + List p2shChangeAddressArray = []; + int p2pkhChangeIndex = -1; + int p2shChangeIndex = -1; + + // The gap limit will be capped at [maxUnusedAddressGap] + // int receivingGapCounter = 0; + // int changeGapCounter = 0; + + // actual size is 24 due to p2pkh and p2sh so 12x2 + const txCountBatchSize = 12; + + try { + // receiving addresses + Logging.instance + .log("checking receiving addresses...", level: LogLevel.Info); + final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0); + + final resultReceive49 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 0); + + Logging.instance + .log("checking change addresses...", level: LogLevel.Info); + // change addresses + final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1); + + final resultChange49 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 1); + + await Future.wait( + [resultReceive44, resultReceive49, resultChange44, resultChange49]); + + p2pkhReceiveAddressArray = + (await resultReceive44)['addressArray'] as List; + p2pkhReceiveIndex = (await resultReceive44)['index'] as int; + p2pkhReceiveDerivations = (await resultReceive44)['derivations'] + as Map>; + + p2shReceiveAddressArray = + (await resultReceive49)['addressArray'] as List; + p2shReceiveIndex = (await resultReceive49)['index'] as int; + p2shReceiveDerivations = (await resultReceive49)['derivations'] + as Map>; + + p2pkhChangeAddressArray = + (await resultChange44)['addressArray'] as List; + p2pkhChangeIndex = (await resultChange44)['index'] as int; + p2pkhChangeDerivations = (await resultChange44)['derivations'] + as Map>; + + p2shChangeAddressArray = + (await resultChange49)['addressArray'] as List; + p2shChangeIndex = (await resultChange49)['index'] as int; + p2shChangeDerivations = (await resultChange49)['derivations'] + as Map>; + + // save the derivations (if any) + if (p2pkhReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhReceiveDerivations); + } + if (p2shReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip49, + derivationsToAdd: p2shReceiveDerivations); + } + if (p2pkhChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhChangeDerivations); + } + if (p2shChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip49, + derivationsToAdd: p2shChangeDerivations); + } + + // If restoring a wallet that never received any funds, then set receivingArray manually + // If we didn't do this, it'd store an empty array + if (p2pkhReceiveIndex == -1) { + final address = + await _generateAddressForChain(0, 0, DerivePathType.bip44); + p2pkhReceiveAddressArray.add(address); + p2pkhReceiveIndex = 0; + } + if (p2shReceiveIndex == -1) { + final address = + await _generateAddressForChain(0, 0, DerivePathType.bip49); + p2shReceiveAddressArray.add(address); + p2shReceiveIndex = 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. + if (p2pkhChangeIndex == -1) { + final address = + await _generateAddressForChain(1, 0, DerivePathType.bip44); + p2pkhChangeAddressArray.add(address); + p2pkhChangeIndex = 0; + } + if (p2shChangeIndex == -1) { + final address = + await _generateAddressForChain(1, 0, DerivePathType.bip49); + p2shChangeAddressArray.add(address); + p2shChangeIndex = 0; + } + + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: p2pkhReceiveAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: p2pkhChangeAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2SH', + value: p2shReceiveAddressArray); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2SH', + value: p2shChangeAddressArray); + await DB.instance.put( + boxName: walletId, key: 'changeIndexP2PKH', value: p2pkhChangeIndex); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: p2pkhReceiveIndex); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2SH', + value: p2shReceiveIndex); + await DB.instance.put( + boxName: walletId, key: 'changeIndexP2SH', value: p2shChangeIndex); + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + await DB.instance + .put(boxName: walletId, key: "isFavorite", value: false); + + longMutex = false; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _recoverWalletFromBIP32SeedPhrase(): $e\n$s", + level: LogLevel.Info); + + longMutex = false; + rethrow; + } + } + + Future> _checkGaps( + int maxNumberOfIndexesToCheck, + int maxUnusedAddressGap, + int txCountBatchSize, + bip32.BIP32 root, + DerivePathType type, + int account) async { + List addressArray = []; + int returningIndex = -1; + Map> derivations = {}; + int gapCounter = 0; + for (int index = 0; + index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap; + index += txCountBatchSize) { + List iterationsAddressArray = []; + Logging.instance.log( + "index: $index, \t GapCounter $account ${type.name}: $gapCounter", + level: LogLevel.Info); + + final _id = "k_$index"; + Map txCountCallArgs = {}; + final Map receivingNodes = {}; + + for (int j = 0; j < txCountBatchSize; j++) { + final node = await compute( + getBip32NodeFromRootWrapper, + Tuple4( + account, + index + j, + root, + type, + ), + ); + String? address; + switch (type) { + case DerivePathType.bip44: + address = P2PKH( + data: PaymentData(pubkey: node.publicKey), + network: _network) + .data + .address!; + break; + case DerivePathType.bip49: + address = P2SH( + data: PaymentData( + redeem: P2WPKH( + data: PaymentData(pubkey: node.publicKey), + network: _network) + .data), + network: _network) + .data + .address!; + break; + default: + throw Exception("No Path type $type exists"); + } + receivingNodes.addAll({ + "${_id}_$j": { + "node": node, + "address": address, + } + }); + txCountCallArgs.addAll({ + "${_id}_$j": address, + }); + } + + // get address tx counts + final counts = await _getBatchTxCount(addresses: txCountCallArgs); + print("Counts $counts"); + // check and add appropriate addresses + for (int k = 0; k < txCountBatchSize; k++) { + int count = counts["${_id}_$k"]!; + if (count > 0) { + final node = receivingNodes["${_id}_$k"]; + // add address to array + addressArray.add(node["address"] as String); + iterationsAddressArray.add(node["address"] as String); + // set current index + returningIndex = index + k; + // reset counter + gapCounter = 0; + // add info to derivations + derivations[node["address"] as String] = { + "pubKey": Format.uint8listToString( + (node["node"] as bip32.BIP32).publicKey), + "wif": (node["node"] as bip32.BIP32).toWIF(), + }; + } + + // increase counter when no tx history found + if (count == 0) { + gapCounter++; + } + } + // cache all the transactions while waiting for the current function to finish. + unawaited(getTransactionCacheEarly(iterationsAddressArray)); + } + return { + "addressArray": addressArray, + "index": returningIndex, + "derivations": derivations + }; + } + + Future getTransactionCacheEarly(List allAddresses) async { + try { + final List> allTxHashes = + await _fetchHistory(allAddresses); + for (final txHash in allTxHashes) { + try { + unawaited(cachedElectrumXClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: coin, + )); + } catch (e) { + continue; + } + } + } catch (e) { + // + } + } + + Future refreshIfThereIsNewData() async { + if (longMutex) return false; + if (_hasCalledExit) return false; + Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info); + + try { + bool needsRefresh = false; + Logging.instance.log( + "notified unconfirmed transactions: ${txTracker.pendings}", + level: LogLevel.Info); + Set txnsToCheck = {}; + + for (final String txid in txTracker.pendings) { + if (!txTracker.wasNotifiedConfirmed(txid)) { + txnsToCheck.add(txid); + } + } + + for (String txid in txnsToCheck) { + final txn = await electrumXClient.getTransaction(txHash: txid); + var confirmations = txn["confirmations"]; + if (confirmations is! int) continue; + bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; + if (!isUnconfirmed) { + // unconfirmedTxs = {}; + needsRefresh = true; + break; + } + } + if (!needsRefresh) { + var allOwnAddresses = await _fetchAllOwnAddresses(); + List> allTxs = + await _fetchHistory(allOwnAddresses); + final txData = await transactionData; + for (Map transaction in allTxs) { + if (txData.findTransaction(transaction['tx_hash'] as String) == + null) { + Logging.instance.log( + " txid not found in address history already ${transaction['tx_hash']}", + level: LogLevel.Info); + needsRefresh = true; + break; + } + } + } + return needsRefresh; + } catch (e, s) { + Logging.instance.log( + "Exception caught in refreshIfThereIsNewData: $e\n$s", + level: LogLevel.Info); + rethrow; + } + } + + Future getAllTxsToWatch( + TransactionData txData, + ) async { + if (_hasCalledExit) return; + List unconfirmedTxnsToNotifyPending = []; + List unconfirmedTxnsToNotifyConfirmed = []; + + // Get all unconfirmed incoming transactions + for (final chunk in txData.txChunks) { + for (final tx in chunk.transactions) { + if (tx.confirmedStatus) { + if (txTracker.wasNotifiedPending(tx.txid) && + !txTracker.wasNotifiedConfirmed(tx.txid)) { + unconfirmedTxnsToNotifyConfirmed.add(tx); + } + } else { + if (!txTracker.wasNotifiedPending(tx.txid)) { + unconfirmedTxnsToNotifyPending.add(tx); + } + } + } + } + + // notify on new incoming transaction + for (final tx in unconfirmedTxnsToNotifyPending) { + if (tx.txType == "Received") { + NotificationApi.showNotification( + title: "Incoming transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + ); + await txTracker.addNotifiedPending(tx.txid); + } else if (tx.txType == "Sent") { + NotificationApi.showNotification( + title: "Sending transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + ); + await txTracker.addNotifiedPending(tx.txid); + } + } + + // notify on confirmed + for (final tx in unconfirmedTxnsToNotifyConfirmed) { + if (tx.txType == "Received") { + NotificationApi.showNotification( + title: "Incoming transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: false, + coinName: coin.name, + ); + + await txTracker.addNotifiedConfirmed(tx.txid); + } else if (tx.txType == "Sent") { + NotificationApi.showNotification( + title: "Outgoing transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: false, + coinName: coin.name, + ); + await txTracker.addNotifiedConfirmed(tx.txid); + } + } + } + + bool refreshMutex = false; + + bool _shouldAutoSync = false; + + @override + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) { + if (_shouldAutoSync != shouldAutoSync) { + _shouldAutoSync = shouldAutoSync; + if (!shouldAutoSync) { + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } else { + startNetworkAlivePinging(); + refresh(); + } + } + } + + //TODO Show percentages properly/more consistently + /// Refreshes display data for the wallet + @override + Future refresh() async { + final bchaddr = Bitbox.Address.toCashAddress(await currentReceivingAddress); + print("bchaddr: $bchaddr ${await currentReceivingAddress}"); + + if (refreshMutex) { + Logging.instance.log("$walletId $walletName refreshMutex denied", + level: LogLevel.Info); + return; + } else { + refreshMutex = true; + } + + try { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); + + final currentHeight = await chainHeight; + const storedHeight = 1; //await storedChainHeight; + + Logging.instance + .log("chain height: $currentHeight", level: LogLevel.Info); + Logging.instance + .log("cached height: $storedHeight", level: LogLevel.Info); + + if (currentHeight != storedHeight) { + if (currentHeight != -1) { + // -1 failed to fetch current height + updateStoredChainHeight(newHeight: currentHeight); + } + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); + await _checkChangeAddressForTransactions(DerivePathType.bip44); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); + await _checkCurrentReceivingAddressesForTransactions(); + + final newTxData = _fetchTransactionData(); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.50, walletId)); + + final newUtxoData = _fetchUtxoData(); + final feeObj = _getFees(); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.60, walletId)); + + _transactionData = Future(() => newTxData); + + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.70, walletId)); + _feeObject = Future(() => feeObj); + _utxoData = Future(() => newUtxoData); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.80, walletId)); + + await getAllTxsToWatch(await newTxData); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.90, walletId)); + } + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + refreshMutex = false; + + if (shouldAutoSync) { + timer ??= Timer.periodic(const Duration(seconds: 150), (timer) async { + // chain height check currently broken + // if ((await chainHeight) != (await storedChainHeight)) { + if (await refreshIfThereIsNewData()) { + await refresh(); + GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId)); + } + // } + }); + } + } catch (error, strace) { + refreshMutex = false; + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + coin, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + Logging.instance.log( + "Caught exception in refreshWalletData(): $error\n$strace", + level: LogLevel.Error); + } + } + + @override + Future> prepareSend({ + required String address, + required int satoshiAmount, + Map? args, + }) async { + try { + final feeRateType = args?["feeRate"]; + final feeRateAmount = args?["feeRateAmount"]; + if (feeRateType is FeeRateType || feeRateAmount is int) { + late final int rate; + if (feeRateType is FeeRateType) { + int fee = 0; + final feeObject = await fees; + switch (feeRateType) { + case FeeRateType.fast: + fee = feeObject.fast; + break; + case FeeRateType.average: + fee = feeObject.medium; + break; + case FeeRateType.slow: + fee = feeObject.slow; + break; + } + rate = fee; + } else { + rate = feeRateAmount as int; + } + // check for send all + bool isSendAll = false; + final balance = Format.decimalAmountToSatoshis(await availableBalance); + if (satoshiAmount == balance) { + isSendAll = true; + } + + final result = + await coinSelection(satoshiAmount, rate, address, isSendAll); + Logging.instance.log("SEND RESULT: $result", level: LogLevel.Info); + if (result is int) { + switch (result) { + case 1: + throw Exception("Insufficient balance!"); + case 2: + throw Exception("Insufficient funds to pay for transaction fee!"); + default: + throw Exception("Transaction failed with error code $result"); + } + } else { + final hex = result["hex"]; + if (hex is String) { + final fee = result["fee"] as int; + final vSize = result["vSize"] as int; + + Logging.instance.log("txHex: $hex", level: LogLevel.Info); + Logging.instance.log("fee: $fee", level: LogLevel.Info); + Logging.instance.log("vsize: $vSize", level: LogLevel.Info); + // fee should never be less than vSize sanity check + if (fee < vSize) { + throw Exception( + "Error in fee calculation: Transaction fee cannot be less than vSize"); + } + return result as Map; + } else { + throw Exception("sent hex is not a String!!!"); + } + } + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + @override + Future confirmSend({dynamic txData}) async { + try { + Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); + final txHash = await _electrumXClient.broadcastTransaction( + rawTx: txData["hex"] as String); + Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + return txHash; + } catch (e, s) { + Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + @override + Future send({ + required String toAddress, + required int amount, + Map args = const {}, + }) async { + try { + final txData = await prepareSend( + address: toAddress, satoshiAmount: amount, args: args); + final txHash = await confirmSend(txData: txData); + return txHash; + } catch (e, s) { + Logging.instance + .log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + @override + Future testNetworkConnection() async { + try { + final result = await _electrumXClient.ping(); + return result; + } catch (_) { + return false; + } + } + + Timer? _networkAliveTimer; + + void startNetworkAlivePinging() { + // call once on start right away + _periodicPingCheck(); + + // then periodically check + _networkAliveTimer = Timer.periodic( + Constants.networkAliveTimerDuration, + (_) async { + _periodicPingCheck(); + }, + ); + } + + void _periodicPingCheck() async { + bool hasNetwork = await testNetworkConnection(); + _isConnected = hasNetwork; + if (_isConnected != hasNetwork) { + NodeConnectionStatus status = hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; + GlobalEventBus.instance + .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); + } + } + + void stopNetworkAlivePinging() { + _networkAliveTimer?.cancel(); + _networkAliveTimer = null; + } + + bool _isConnected = false; + + @override + bool get isConnected => _isConnected; + + @override + Future initializeNew() async { + Logging.instance + .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); + + if ((DB.instance.get(boxName: walletId, key: "id")) != null) { + throw Exception( + "Attempted to initialize a new wallet using an existing wallet ID!"); + } + await _prefs.init(); + try { + await _generateNewWallet(); + } catch (e, s) { + Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", + level: LogLevel.Fatal); + rethrow; + } + await Future.wait([ + DB.instance.put(boxName: walletId, key: "id", value: _walletId), + DB.instance + .put(boxName: walletId, key: "isFavorite", value: false), + ]); + } + + @override + Future initializeExisting() async { + Logging.instance.log("Opening existing ${coin.prettyName} wallet.", + level: LogLevel.Info); + + if ((DB.instance.get(boxName: walletId, key: "id")) == null) { + throw Exception( + "Attempted to initialize an existing wallet using an unknown wallet ID!"); + } + await _prefs.init(); + final data = + DB.instance.get(boxName: walletId, key: "latest_tx_model") + as TransactionData?; + if (data != null) { + _transactionData = Future(() => data); + } + } + + @override + Future get transactionData => + _transactionData ??= _fetchTransactionData(); + Future? _transactionData; + + @override + bool validateAddress(String address) { + try { + // 0 for bitcoincash: address scheme, 1 for legacy address + final format = Bitbox.Address.detectFormat(address); + print("format $format"); + return true; + } catch (e, s) { + return false; + } + } + + @override + String get walletId => _walletId; + late String _walletId; + + @override + String get walletName => _walletName; + late String _walletName; + + // setter for updating on rename + @override + set walletName(String newName) => _walletName = newName; + + late ElectrumX _electrumXClient; + + ElectrumX get electrumXClient => _electrumXClient; + + late CachedElectrumX _cachedElectrumXClient; + + CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; + + late FlutterSecureStorageInterface _secureStore; + + late PriceAPI _priceAPI; + + BitcoinCashWallet({ + required String walletId, + required String walletName, + required Coin coin, + required ElectrumX client, + required CachedElectrumX cachedClient, + required TransactionNotificationTracker tracker, + PriceAPI? priceAPI, + FlutterSecureStorageInterface? secureStore, + }) { + txTracker = tracker; + _walletId = walletId; + _walletName = walletName; + _coin = coin; + _electrumXClient = client; + _cachedElectrumXClient = cachedClient; + + _priceAPI = priceAPI ?? PriceAPI(Client()); + _secureStore = + secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + } + + @override + Future updateNode(bool shouldRefresh) async { + final failovers = NodeService() + .failoverNodesFor(coin: coin) + .map((e) => ElectrumXNode( + address: e.host, + port: e.port, + name: e.name, + id: e.id, + useSSL: e.useSSL, + )) + .toList(); + final newNode = await getCurrentNode(); + _cachedElectrumXClient = CachedElectrumX.from( + node: newNode, + prefs: _prefs, + failovers: failovers, + ); + _electrumXClient = ElectrumX.from( + node: newNode, + prefs: _prefs, + failovers: failovers, + ); + + if (shouldRefresh) { + refresh(); + } + } + + Future> _getMnemonicList() async { + final mnemonicString = + await _secureStore.read(key: '${_walletId}_mnemonic'); + if (mnemonicString == null) { + return []; + } + final List data = mnemonicString.split(' '); + return data; + } + + Future getCurrentNode() async { + final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + + return ElectrumXNode( + address: node.host, + port: node.port, + name: node.name, + useSSL: node.useSSL, + id: node.id, + ); + } + + Future> _fetchAllOwnAddresses() async { + final List allAddresses = []; + + final receivingAddressesP2PKH = DB.instance.get( + boxName: walletId, key: 'receivingAddressesP2PKH') as List; + final changeAddressesP2PKH = + DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') + as List; + + // for (var i = 0; i < receivingAddresses.length; i++) { + // if (!allAddresses.contains(receivingAddresses[i])) { + // allAddresses.add(receivingAddresses[i]); + // } + // } + // for (var i = 0; i < changeAddresses.length; i++) { + // if (!allAddresses.contains(changeAddresses[i])) { + // allAddresses.add(changeAddresses[i]); + // } + // } + for (var i = 0; i < receivingAddressesP2PKH.length; i++) { + if (!allAddresses.contains(receivingAddressesP2PKH[i])) { + allAddresses.add(receivingAddressesP2PKH[i] as String); + } + } + for (var i = 0; i < changeAddressesP2PKH.length; i++) { + if (!allAddresses.contains(changeAddressesP2PKH[i])) { + allAddresses.add(changeAddressesP2PKH[i] as String); + } + } + return allAddresses; + } + + Future _getFees() async { + try { + //TODO adjust numbers for different speeds? + const int f = 1, m = 5, s = 20; + + final fast = await electrumXClient.estimateFee(blocks: f); + final medium = await electrumXClient.estimateFee(blocks: m); + final slow = await electrumXClient.estimateFee(blocks: s); + + final feeObject = FeeObject( + numberOfBlocksFast: f, + numberOfBlocksAverage: m, + numberOfBlocksSlow: s, + fast: Format.decimalAmountToSatoshis(fast), + medium: Format.decimalAmountToSatoshis(medium), + slow: Format.decimalAmountToSatoshis(slow), + ); + + Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); + return feeObject; + } catch (e) { + Logging.instance + .log("Exception rethrown from _getFees(): $e", level: LogLevel.Error); + rethrow; + } + } + + Future _generateNewWallet() async { + Logging.instance + .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); + if (!integrationTestFlag) { + final features = await electrumXClient.getServerFeatures(); + Logging.instance.log("features: $features", level: LogLevel.Info); + switch (coin) { + case Coin.bitcoincash: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main net!"); + } + break; + case Coin.bitcoincashTestnet: + if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + throw Exception("genesis hash does not match test net!"); + } + break; + default: + throw Exception( + "Attempted to generate a BitcoinWallet using a non bitcoin coin type: ${coin.name}"); + } + } + + // this should never fail + if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { + throw Exception( + "Attempted to overwrite mnemonic on generate new wallet!"); + } + await _secureStore.write( + key: '${_walletId}_mnemonic', + value: bip39.generateMnemonic(strength: 256)); + + // Set relevant indexes + await DB.instance + .put(boxName: walletId, key: "receivingIndexP2PKH", value: 0); + await DB.instance + .put(boxName: walletId, key: "changeIndexP2PKH", value: 0); + await DB.instance + .put(boxName: walletId, key: "receivingIndexP2SH", value: 0); + await DB.instance + .put(boxName: walletId, key: "changeIndexP2SH", value: 0); + await DB.instance.put( + boxName: walletId, + key: 'blocked_tx_hashes', + value: ["0xdefault"], + ); // A list of transaction hashes to represent frozen utxos in wallet + // initialize address book entries + await DB.instance.put( + boxName: walletId, + key: 'addressBookEntries', + value: {}); + + // Generate and add addresses to relevant arrays + final initialReceivingAddressP2PKH = + await _generateAddressForChain(0, 0, DerivePathType.bip44); + final initialChangeAddressP2PKH = + await _generateAddressForChain(1, 0, DerivePathType.bip44); + + final initialReceivingAddressP2SH = + await _generateAddressForChain(0, 0, DerivePathType.bip49); + final initialChangeAddressP2SH = + await _generateAddressForChain(1, 0, DerivePathType.bip49); + + await _addToAddressesArrayForChain( + initialReceivingAddressP2PKH, 0, DerivePathType.bip44); + await _addToAddressesArrayForChain( + initialChangeAddressP2PKH, 1, DerivePathType.bip44); + + await _addToAddressesArrayForChain( + initialReceivingAddressP2SH, 0, DerivePathType.bip49); + await _addToAddressesArrayForChain( + initialChangeAddressP2SH, 1, DerivePathType.bip49); + + // this._currentReceivingAddress = Future(() => initialReceivingAddress); + _currentReceivingAddressP2PKH = Future(() => initialReceivingAddressP2PKH); + _currentReceivingAddressP2SH = Future(() => initialReceivingAddressP2SH); + + Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); + } + + /// Generates a new internal or external chain address for the wallet using a BIP44 or BIP49 derivation path. + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + /// [index] - This can be any integer >= 0 + Future _generateAddressForChain( + int chain, + int index, + DerivePathType derivePathType, + ) async { + final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); + final node = await compute( + getBip32NodeWrapper, + Tuple5( + chain, + index, + mnemonic!, + _network, + derivePathType, + ), + ); + final data = PaymentData(pubkey: node.publicKey); + final p2shData = PaymentData( + redeem: + P2WPKH(data: PaymentData(pubkey: node.publicKey), network: _network) + .data); + String address; + + switch (derivePathType) { + case DerivePathType.bip44: + address = P2PKH(data: data, network: _network).data.address!; + break; + case DerivePathType.bip49: + address = P2SH(data: p2shData, network: _network).data.address!; + break; + // default: + // // should never hit this due to all enum cases handled + // return null; + } + + // add generated address & info to derivations + await addDerivation( + chain: chain, + address: address, + pubKey: Format.uint8listToString(node.publicKey), + wif: node.toWIF(), + derivePathType: derivePathType, + ); + + return address; + } + + /// Increases the index for either the internal or external chain, depending on [chain]. + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _incrementAddressIndexForChain( + int chain, DerivePathType derivePathType) async { + // Here we assume chain == 1 if it isn't 0 + String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + } + + final newIndex = + (DB.instance.get(boxName: walletId, key: indexKey)) + 1; + await DB.instance + .put(boxName: walletId, key: indexKey, value: newIndex); + } + + /// Adds [address] to the relevant chain's address array, which is determined by [chain]. + /// [address] - Expects a standard native segwit address + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _addToAddressesArrayForChain( + String address, int chain, DerivePathType derivePathType) async { + String chainArray = ''; + if (chain == 0) { + chainArray = 'receivingAddresses'; + } else { + chainArray = 'changeAddresses'; + } + switch (derivePathType) { + case DerivePathType.bip44: + chainArray += "P2PKH"; + break; + case DerivePathType.bip49: + chainArray += "P2SH"; + break; + } + + final addressArray = + DB.instance.get(boxName: walletId, key: chainArray); + if (addressArray == null) { + Logging.instance.log( + 'Attempting to add the following to $chainArray array for chain $chain:${[ + address + ]}', + level: LogLevel.Info); + await DB.instance + .put(boxName: walletId, key: chainArray, value: [address]); + } else { + // Make a deep copy of the existing list + final List newArray = []; + addressArray + .forEach((dynamic _address) => newArray.add(_address as String)); + newArray.add(address); // Add the address passed into the method + await DB.instance + .put(boxName: walletId, key: chainArray, value: newArray); + } + } + + /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] + /// and + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _getCurrentAddressForChain( + int chain, DerivePathType derivePathType) async { + // Here, we assume that chain == 1 if it isn't 0 + String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; + switch (derivePathType) { + case DerivePathType.bip44: + arrayKey += "P2PKH"; + break; + case DerivePathType.bip49: + arrayKey += "P2SH"; + break; + } + + print("Array key is ${jsonEncode(arrayKey)}"); + final internalChainArray = + DB.instance.get(boxName: walletId, key: arrayKey); + return internalChainArray.last as String; + } + + String _buildDerivationStorageKey( + {required int chain, required DerivePathType derivePathType}) { + String key; + String chainId = chain == 0 ? "receive" : "change"; + switch (derivePathType) { + case DerivePathType.bip44: + key = "${walletId}_${chainId}DerivationsP2PKH"; + break; + case DerivePathType.bip49: + key = "${walletId}_${chainId}DerivationsP2SH"; + break; + } + return key; + } + + Future> _fetchDerivations( + {required int chain, required DerivePathType derivePathType}) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + return Map.from( + jsonDecode(derivationsString ?? "{}") as Map); + } + + /// Add a single derivation to the local secure storage for [chain] and + /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. + /// This will overwrite a previous entry where the address of the new derivation + /// matches a derivation currently stored. + Future addDerivation({ + required int chain, + required String address, + required String pubKey, + required String wif, + required DerivePathType derivePathType, + }) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + final derivations = + Map.from(jsonDecode(derivationsString ?? "{}") as Map); + + // add derivation + derivations[address] = { + "pubKey": pubKey, + "wif": wif, + }; + + // save derivations + final newReceiveDerivationsString = jsonEncode(derivations); + await _secureStore.write(key: key, value: newReceiveDerivationsString); + } + + /// Add multiple derivations to the local secure storage for [chain] and + /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. + /// This will overwrite any previous entries where the address of the new derivation + /// matches a derivation currently stored. + /// The [derivationsToAdd] must be in the format of: + /// { + /// addressA : { + /// "pubKey": , + /// "wif": , + /// }, + /// addressB : { + /// "pubKey": , + /// "wif": , + /// }, + /// } + Future addDerivations({ + required int chain, + required DerivePathType derivePathType, + required Map derivationsToAdd, + }) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + final derivations = + Map.from(jsonDecode(derivationsString ?? "{}") as Map); + + // add derivation + derivations.addAll(derivationsToAdd); + + // save derivations + final newReceiveDerivationsString = jsonEncode(derivations); + await _secureStore.write(key: key, value: newReceiveDerivationsString); + } + + Future _fetchUtxoData() async { + final List allAddresses = await _fetchAllOwnAddresses(); + + try { + final fetchedUtxoList = >>[]; + + final Map>> batches = {}; + const batchSizeMax = 10; + int batchNumber = 0; + for (int i = 0; i < allAddresses.length; i++) { + if (batches[batchNumber] == null) { + batches[batchNumber] = {}; + } + final scripthash = _convertToScriptHash(allAddresses[i], _network); + print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash"); + batches[batchNumber]!.addAll({ + scripthash: [scripthash] + }); + if (i % batchSizeMax == batchSizeMax - 1) { + batchNumber++; + } + } + + for (int i = 0; i < batches.length; i++) { + final response = + await _electrumXClient.getBatchUTXOs(args: batches[i]!); + for (final entry in response.entries) { + if (entry.value.isNotEmpty) { + fetchedUtxoList.add(entry.value); + } + } + } + + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final List> outputArray = []; + int satoshiBalance = 0; + int satoshiBalancePending = 0; + + for (int i = 0; i < fetchedUtxoList.length; i++) { + for (int j = 0; j < fetchedUtxoList[i].length; j++) { + int value = fetchedUtxoList[i][j]["value"] as int; + satoshiBalance += value; + + final txn = await cachedElectrumXClient.getTransaction( + txHash: fetchedUtxoList[i][j]["tx_hash"] as String, + verbose: true, + coin: coin, + ); + + final Map utxo = {}; + final int confirmations = txn["confirmations"] as int? ?? 0; + final bool confirmed = txn["confirmations"] == null + ? false + : txn["confirmations"] as int >= MINIMUM_CONFIRMATIONS; + if (!confirmed) { + satoshiBalancePending += value; + } + + utxo["txid"] = txn["txid"]; + utxo["vout"] = fetchedUtxoList[i][j]["tx_pos"]; + utxo["value"] = value; + + utxo["status"] = {}; + utxo["status"]["confirmed"] = confirmed; + utxo["status"]["confirmations"] = confirmations; + utxo["status"]["block_height"] = fetchedUtxoList[i][j]["height"]; + utxo["status"]["block_hash"] = txn["blockhash"]; + utxo["status"]["block_time"] = txn["blocktime"]; + + final fiatValue = ((Decimal.fromInt(value) * currentPrice) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2); + utxo["rawWorth"] = fiatValue; + utxo["fiatWorth"] = fiatValue.toString(); + outputArray.add(utxo); + } + } + + Decimal currencyBalanceRaw = + ((Decimal.fromInt(satoshiBalance) * currentPrice) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2); + + final Map result = { + "total_user_currency": currencyBalanceRaw.toString(), + "total_sats": satoshiBalance, + "total_btc": (Decimal.fromInt(satoshiBalance) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + .toString(), + "outputArray": outputArray, + "unconfirmed": satoshiBalancePending, + }; + + final dataModel = UtxoData.fromJson(result); + + final List allOutputs = dataModel.unspentOutputArray; + Logging.instance + .log('Outputs fetched: $allOutputs', level: LogLevel.Info); + await _sortOutputs(allOutputs); + await DB.instance.put( + boxName: walletId, key: 'latest_utxo_model', value: dataModel); + await DB.instance.put( + boxName: walletId, + key: 'totalBalance', + value: dataModel.satoshiBalance); + return dataModel; + } catch (e, s) { + Logging.instance + .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); + final latestTxModel = + DB.instance.get(boxName: walletId, key: 'latest_utxo_model'); + + if (latestTxModel == null) { + final emptyModel = { + "total_user_currency": "0.00", + "total_sats": 0, + "total_btc": "0", + "outputArray": [] + }; + return UtxoData.fromJson(emptyModel); + } else { + Logging.instance + .log("Old output model located", level: LogLevel.Warning); + return latestTxModel as models.UtxoData; + } + } + } + + /// Takes in a list of UtxoObjects and adds a name (dependent on object index within list) + /// and checks for the txid associated with the utxo being blocked and marks it accordingly. + /// Now also checks for output labeling. + Future _sortOutputs(List utxos) async { + final blockedHashArray = + DB.instance.get(boxName: walletId, key: 'blocked_tx_hashes') + as List?; + final List lst = []; + if (blockedHashArray != null) { + for (var hash in blockedHashArray) { + lst.add(hash as String); + } + } + final labels = + DB.instance.get(boxName: walletId, key: 'labels') as Map? ?? + {}; + + outputsList = []; + + for (var i = 0; i < utxos.length; i++) { + if (labels[utxos[i].txid] != null) { + utxos[i].txName = labels[utxos[i].txid] as String? ?? ""; + } else { + utxos[i].txName = 'Output #$i'; + } + + if (utxos[i].status.confirmed == false) { + outputsList.add(utxos[i]); + } else { + if (lst.contains(utxos[i].txid)) { + utxos[i].blocked = true; + outputsList.add(utxos[i]); + } else if (!lst.contains(utxos[i].txid)) { + outputsList.add(utxos[i]); + } + } + } + } + + Future getTxCount({required String address}) async { + String? scripthash; + try { + scripthash = _convertToScriptHash(address, _network); + final transactions = + await electrumXClient.getHistory(scripthash: scripthash); + return transactions.length; + } catch (e) { + Logging.instance.log( + "Exception rethrown in _getTxCount(address: $address, scripthash: $scripthash): $e", + level: LogLevel.Error); + rethrow; + } + } + + Future> _getBatchTxCount({ + required Map addresses, + }) async { + try { + final Map> args = {}; + print("Address $addresses"); + for (final entry in addresses.entries) { + args[entry.key] = [_convertToScriptHash(entry.value, _network)]; + } + + print("Args ${jsonEncode(args)}"); + + final response = await electrumXClient.getBatchHistory(args: args); + print("Response ${jsonEncode(response)}"); + final Map result = {}; + for (final entry in response.entries) { + result[entry.key] = entry.value.length; + } + print("result ${jsonEncode(result)}"); + return result; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown in _getBatchTxCount(address: $addresses: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future _checkReceivingAddressForTransactions( + DerivePathType derivePathType) async { + try { + final String currentExternalAddr = + await _getCurrentAddressForChain(0, derivePathType); + final int txCount = await getTxCount(address: currentExternalAddr); + Logging.instance.log( + 'Number of txs for current receiving address $currentExternalAddr: $txCount', + level: LogLevel.Info); + + if (txCount >= 1) { + // First increment the receiving index + await _incrementAddressIndexForChain(0, derivePathType); + + // Check the new receiving index + String indexKey = "receivingIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + } + final newReceivingIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new receiving address + final newReceivingAddress = await _generateAddressForChain( + 0, newReceivingIndex, derivePathType); + + // Add that new receiving address to the array of receiving addresses + await _addToAddressesArrayForChain( + newReceivingAddress, 0, derivePathType); + + // Set the new receiving address that the service + + switch (derivePathType) { + case DerivePathType.bip44: + _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); + break; + case DerivePathType.bip49: + _currentReceivingAddressP2SH = Future(() => newReceivingAddress); + break; + } + } + } on SocketException catch (se, s) { + Logging.instance.log( + "SocketException caught in _checkReceivingAddressForTransactions($derivePathType): $se\n$s", + level: LogLevel.Error); + return; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future _checkChangeAddressForTransactions( + DerivePathType derivePathType) async { + try { + final String currentExternalAddr = + await _getCurrentAddressForChain(1, derivePathType); + final int txCount = await getTxCount(address: currentExternalAddr); + Logging.instance.log( + 'Number of txs for current change address $currentExternalAddr: $txCount', + level: LogLevel.Info); + + if (txCount >= 1) { + // First increment the change index + await _incrementAddressIndexForChain(1, derivePathType); + + // Check the new change index + String indexKey = "changeIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + } + final newChangeIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new change address + final newChangeAddress = + await _generateAddressForChain(1, newChangeIndex, derivePathType); + + // Add that new receiving address to the array of change addresses + await _addToAddressesArrayForChain(newChangeAddress, 1, derivePathType); + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkChangeAddressForTransactions($derivePathType): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future _checkCurrentReceivingAddressesForTransactions() async { + try { + for (final type in DerivePathType.values) { + await _checkReceivingAddressForTransactions(type); + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", + level: LogLevel.Info); + rethrow; + } + } + + /// public wrapper because dart can't test private... + Future checkCurrentReceivingAddressesForTransactions() async { + if (Platform.environment["FLUTTER_TEST"] == "true") { + try { + return _checkCurrentReceivingAddressesForTransactions(); + } catch (_) { + rethrow; + } + } + } + + Future _checkCurrentChangeAddressesForTransactions() async { + try { + for (final type in DerivePathType.values) { + await _checkChangeAddressForTransactions(type); + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkCurrentChangeAddressesForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + /// public wrapper because dart can't test private... + Future checkCurrentChangeAddressesForTransactions() async { + if (Platform.environment["FLUTTER_TEST"] == "true") { + try { + return _checkCurrentChangeAddressesForTransactions(); + } catch (_) { + rethrow; + } + } + } + + /// attempts to convert a string to a valid scripthash + /// + /// Returns the scripthash or throws an exception on invalid bch address + String _convertToScriptHash(String bchAddress, NetworkType network) { + try { + final output = Address.addressToOutputScript(bchAddress, network); + final hash = sha256.convert(output.toList(growable: false)).toString(); + + final chars = hash.split(""); + final reversedPairs = []; + var i = chars.length - 1; + while (i > 0) { + reversedPairs.add(chars[i - 1]); + reversedPairs.add(chars[i]); + i -= 2; + } + return reversedPairs.join(""); + } catch (e) { + rethrow; + } + } + + Future>> _fetchHistory( + List allAddresses) async { + try { + List> allTxHashes = []; + + final Map>> batches = {}; + final Map requestIdToAddressMap = {}; + const batchSizeMax = 10; + int batchNumber = 0; + for (int i = 0; i < allAddresses.length; i++) { + if (batches[batchNumber] == null) { + batches[batchNumber] = {}; + } + final scripthash = _convertToScriptHash(allAddresses[i], _network); + final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); + requestIdToAddressMap[id] = allAddresses[i]; + batches[batchNumber]!.addAll({ + id: [scripthash] + }); + if (i % batchSizeMax == batchSizeMax - 1) { + batchNumber++; + } + } + + for (int i = 0; i < batches.length; i++) { + final response = + await _electrumXClient.getBatchHistory(args: batches[i]!); + for (final entry in response.entries) { + for (int j = 0; j < entry.value.length; j++) { + entry.value[j]["address"] = requestIdToAddressMap[entry.key]; + if (!allTxHashes.contains(entry.value[j])) { + allTxHashes.add(entry.value[j]); + } + } + } + } + + return allTxHashes; + } catch (e, s) { + Logging.instance.log("_fetchHistory: $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + bool _duplicateTxCheck( + List> allTransactions, String txid) { + for (int i = 0; i < allTransactions.length; i++) { + if (allTransactions[i]["txid"] == txid) { + return true; + } + } + return false; + } + + Future _fetchTransactionData() async { + final List allAddresses = await _fetchAllOwnAddresses(); + + final changeAddressesP2PKH = + DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') + as List; + + final List> allTxHashes = + await _fetchHistory(allAddresses); + + final cachedTransactions = + DB.instance.get(boxName: walletId, key: 'latest_tx_model') + as TransactionData?; + int latestTxnBlockHeight = + DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") + as int? ?? + 0; + + final unconfirmedCachedTransactions = + cachedTransactions?.getAllTransactions() ?? {}; + unconfirmedCachedTransactions + .removeWhere((key, value) => value.confirmedStatus); + + print("CACHED_TRANSACTIONS_IS $cachedTransactions"); + if (cachedTransactions != null) { + for (final tx in allTxHashes.toList(growable: false)) { + final txHeight = tx["height"] as int; + if (txHeight > 0 && + txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { + if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { + allTxHashes.remove(tx); + } + } + } + } + + List> allTransactions = []; + + for (final txHash in allTxHashes) { + Logging.instance.log("bch: $txHash", level: LogLevel.Info); + final tx = await cachedElectrumXClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: coin, + ); + + // Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}"); + // TODO fix this for sent to self transactions? + if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { + tx["address"] = txHash["address"]; + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + + Logging.instance.log("addAddresses: $allAddresses", level: LogLevel.Info); + Logging.instance.log("allTxHashes: $allTxHashes", level: LogLevel.Info); + + Logging.instance.log("allTransactions length: ${allTransactions.length}", + level: LogLevel.Info); + + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final List> midSortedArray = []; + + for (final txObject in allTransactions) { + List sendersArray = []; + List recipientsArray = []; + + // Usually only has value when txType = 'Send' + int inputAmtSentFromWallet = 0; + // Usually has value regardless of txType due to change addresses + int outputAmtAddressedToWallet = 0; + int fee = 0; + + Map midSortedTx = {}; + + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"][i] as Map; + final prevTxid = input["txid"] as String; + final prevOut = input["vout"] as int; + + final tx = await _cachedElectrumXClient.getTransaction( + txHash: prevTxid, coin: coin); + + for (final out in tx["vout"] as List) { + if (prevOut == out["n"]) { + final address = out["scriptPubKey"]["addresses"][0] as String?; + if (address != null) { + sendersArray.add(address); + } + } + } + } + + Logging.instance.log("sendersArray: $sendersArray", level: LogLevel.Info); + + for (final output in txObject["vout"] as List) { + final address = output["scriptPubKey"]["addresses"][0] as String?; + if (address != null) { + recipientsArray.add(address); + } + } + + Logging.instance + .log("recipientsArray: $recipientsArray", level: LogLevel.Info); + + final foundInSenders = + allAddresses.any((element) => sendersArray.contains(element)); + Logging.instance + .log("foundInSenders: $foundInSenders", level: LogLevel.Info); + + // If txType = Sent, then calculate inputAmtSentFromWallet + if (foundInSenders) { + int totalInput = 0; + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"][i] as Map; + final prevTxid = input["txid"] as String; + final prevOut = input["vout"] as int; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: prevTxid, + coin: coin, + ); + + for (final out in tx["vout"] as List) { + if (prevOut == out["n"]) { + inputAmtSentFromWallet += + (Decimal.parse(out["value"].toString()) * + Decimal.fromInt(Constants.satsPerCoin)) + .toBigInt() + .toInt(); + } + } + } + totalInput = inputAmtSentFromWallet; + int totalOutput = 0; + + for (final output in txObject["vout"] as List) { + final address = output["scriptPubKey"]["addresses"][0]; + final value = output["value"]; + final _value = (Decimal.parse(value.toString()) * + Decimal.fromInt(Constants.satsPerCoin)) + .toBigInt() + .toInt(); + totalOutput += _value; + if (changeAddressesP2PKH.contains(address)) { + inputAmtSentFromWallet -= _value; + } else { + // change address from 'sent from' to the 'sent to' address + txObject["address"] = address; + } + } + // calculate transaction fee + fee = totalInput - totalOutput; + // subtract fee from sent to calculate correct value of sent tx + inputAmtSentFromWallet -= fee; + } else { + // counters for fee calculation + int totalOut = 0; + int totalIn = 0; + + // add up received tx value + for (final output in txObject["vout"] as List) { + final address = output["scriptPubKey"]["addresses"][0]; + if (address != null) { + final value = (Decimal.parse(output["value"].toString()) * + Decimal.fromInt(Constants.satsPerCoin)) + .toBigInt() + .toInt(); + totalOut += value; + if (allAddresses.contains(address)) { + outputAmtAddressedToWallet += value; + } + } + } + + // calculate fee for received tx + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"][i] as Map; + final prevTxid = input["txid"] as String; + final prevOut = input["vout"] as int; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: prevTxid, + coin: coin, + ); + + for (final out in tx["vout"] as List) { + if (prevOut == out["n"]) { + totalIn += (Decimal.parse(out["value"].toString()) * + Decimal.fromInt(Constants.satsPerCoin)) + .toBigInt() + .toInt(); + } + } + } + fee = totalIn - totalOut; + } + + // create final tx map + midSortedTx["txid"] = txObject["txid"]; + midSortedTx["confirmed_status"] = (txObject["confirmations"] != null) && + (txObject["confirmations"] as int >= MINIMUM_CONFIRMATIONS); + midSortedTx["confirmations"] = txObject["confirmations"] ?? 0; + midSortedTx["timestamp"] = txObject["blocktime"] ?? + (DateTime.now().millisecondsSinceEpoch ~/ 1000); + + if (foundInSenders) { + midSortedTx["txType"] = "Sent"; + midSortedTx["amount"] = inputAmtSentFromWallet; + final String worthNow = + ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2) + .toStringAsFixed(2); + midSortedTx["worthNow"] = worthNow; + midSortedTx["worthAtBlockTimestamp"] = worthNow; + } else { + midSortedTx["txType"] = "Received"; + midSortedTx["amount"] = outputAmtAddressedToWallet; + final worthNow = + ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2) + .toStringAsFixed(2); + midSortedTx["worthNow"] = worthNow; + } + midSortedTx["aliens"] = []; + midSortedTx["fees"] = fee; + midSortedTx["address"] = txObject["address"]; + midSortedTx["inputSize"] = txObject["vin"].length; + midSortedTx["outputSize"] = txObject["vout"].length; + midSortedTx["inputs"] = txObject["vin"]; + midSortedTx["outputs"] = txObject["vout"]; + + final int height = txObject["height"] as int; + midSortedTx["height"] = height; + + if (height >= latestTxnBlockHeight) { + latestTxnBlockHeight = height; + } + + midSortedArray.add(midSortedTx); + } + + // sort by date ---- //TODO not sure if needed + // shouldn't be any issues with a null timestamp but I got one at some point? + midSortedArray + .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); + // { + // final aT = a["timestamp"]; + // final bT = b["timestamp"]; + // + // if (aT == null && bT == null) { + // return 0; + // } else if (aT == null) { + // return -1; + // } else if (bT == null) { + // return 1; + // } else { + // return bT - aT; + // } + // }); + + // buildDateTimeChunks + final Map result = {"dateTimeChunks": []}; + final dateArray = []; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = extractDateFromTimestamp(txObject["timestamp"] as int); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp(chunk["timestamp"] as int) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = >[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; + transactionsMap + .addAll(TransactionData.fromJson(result).getAllTransactions()); + + final txModel = TransactionData.fromMap(transactionsMap); + + await DB.instance.put( + boxName: walletId, + key: 'storedTxnDataHeight', + value: latestTxnBlockHeight); + await DB.instance.put( + boxName: walletId, key: 'latest_tx_model', value: txModel); + + return txModel; + } + + int estimateTxFee({required int vSize, required int feeRatePerKB}) { + return vSize * (feeRatePerKB / 1000).ceil(); + } + + /// The coinselection algorithm decides whether or not the user is eligible to make the transaction + /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return + /// a map containing the tx hex along with other important information. If not, then it will return + /// an integer (1 or 2) + dynamic coinSelection(int satoshiAmountToSend, int selectedTxFeeRate, + String _recipientAddress, bool isSendAll, + {int additionalOutputs = 0, List? utxos}) async { + Logging.instance + .log("Starting coinSelection ----------", level: LogLevel.Info); + final List availableOutputs = utxos ?? outputsList; + final List spendableOutputs = []; + int spendableSatoshiValue = 0; + + // Build list of spendable outputs and totaling their satoshi amount + for (var i = 0; i < availableOutputs.length; i++) { + if (availableOutputs[i].blocked == false && + availableOutputs[i].status.confirmed == true) { + spendableOutputs.add(availableOutputs[i]); + spendableSatoshiValue += availableOutputs[i].value; + } + } + + // sort spendable by age (oldest first) + spendableOutputs.sort( + (a, b) => b.status.confirmations.compareTo(a.status.confirmations)); + + Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", + level: LogLevel.Info); + Logging.instance + .log("spendableOutputs: $spendableOutputs", level: LogLevel.Info); + Logging.instance.log("spendableSatoshiValue: $spendableSatoshiValue", + level: LogLevel.Info); + Logging.instance + .log("satoshiAmountToSend: $satoshiAmountToSend", level: LogLevel.Info); + // If the amount the user is trying to send is smaller than the amount that they have spendable, + // then return 1, which indicates that they have an insufficient balance. + if (spendableSatoshiValue < satoshiAmountToSend) { + return 1; + // If the amount the user wants to send is exactly equal to the amount they can spend, then return + // 2, which indicates that they are not leaving enough over to pay the transaction fee + } else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) { + return 2; + } + // If neither of these statements pass, we assume that the user has a spendable balance greater + // than the amount they're attempting to send. Note that this value still does not account for + // the added transaction fee, which may require an extra input and will need to be checked for + // later on. + + // Possible situation right here + int satoshisBeingUsed = 0; + int inputsBeingConsumed = 0; + List utxoObjectsToUse = []; + + for (var i = 0; + satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += spendableOutputs[i].value; + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; + inputsBeingConsumed += 1; + } + + Logging.instance + .log("satoshisBeingUsed: $satoshisBeingUsed", level: LogLevel.Info); + Logging.instance + .log("inputsBeingConsumed: $inputsBeingConsumed", level: LogLevel.Info); + Logging.instance + .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); + Logging.instance + .log('satoshiAmountToSend $satoshiAmountToSend', level: LogLevel.Info); + + // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray + List recipientsArray = [_recipientAddress]; + List recipientsAmtArray = [satoshiAmountToSend]; + + // gather required signing data + final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + + if (isSendAll) { + Logging.instance + .log("Attempting to send all $coin", level: LogLevel.Info); + + final int vSizeForOneOutput = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [_recipientAddress], + satoshiAmounts: [satoshisBeingUsed - 1], + ))["vSize"] as int; + int feeForOneOutput = estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ); + if (feeForOneOutput < (vSizeForOneOutput + 1)) { + feeForOneOutput = (vSizeForOneOutput + 1); + } + + final int amount = satoshiAmountToSend - feeForOneOutput; + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: [amount], + ); + Map transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": amount, + "fee": feeForOneOutput, + "vSize": txn["vSize"], + }; + return transactionObject; + } + + final int vSizeForOneOutput = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [_recipientAddress], + satoshiAmounts: [satoshisBeingUsed - 1], + ))["vSize"] as int; + final int vSizeForTwoOutPuts = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [ + _recipientAddress, + await _getCurrentAddressForChain(1, DerivePathType.bip44), + ], + satoshiAmounts: [ + satoshiAmountToSend, + satoshisBeingUsed - satoshiAmountToSend - 1, + ], // dust limit is the minimum amount a change output should be + ))["vSize"] as int; + debugPrint("vSizeForOneOutput $vSizeForOneOutput"); + debugPrint("vSizeForTwoOutPuts $vSizeForTwoOutPuts"); + + // Assume 1 output, only for recipient and no change + var feeForOneOutput = estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ); + // Assume 2 outputs, one for recipient and one for change + var feeForTwoOutputs = estimateTxFee( + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ); + + Logging.instance + .log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info); + Logging.instance + .log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info); + if (feeForOneOutput < (vSizeForOneOutput + 1)) { + feeForOneOutput = (vSizeForOneOutput + 1); + } + if (feeForTwoOutputs < ((vSizeForTwoOutPuts + 1))) { + feeForTwoOutputs = ((vSizeForTwoOutPuts + 1)); + } + + Logging.instance + .log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info); + Logging.instance + .log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info); + + if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { + if (satoshisBeingUsed - satoshiAmountToSend > + feeForOneOutput + DUST_LIMIT) { + // Here, we know that theoretically, we may be able to include another output(change) but we first need to + // factor in the value of this output in satoshis. + int changeOutputSize = + satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs; + // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and + // the second output's size > 546 satoshis, we perform the mechanics required to properly generate and use a new + // change address. + if (changeOutputSize > DUST_LIMIT && + satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == + feeForTwoOutputs) { + // generate new change address if current change address has been used + await _checkChangeAddressForTransactions(DerivePathType.bip44); + final String newChangeAddress = + await _getCurrentAddressForChain(1, DerivePathType.bip44); + + int feeBeingPaid = + satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; + + recipientsArray.add(newChangeAddress); + recipientsAmtArray.add(changeOutputSize); + // At this point, we have the outputs we're going to use, the amounts to send along with which addresses + // we intend to send these amounts to. We have enough to send instructions to build the transaction. + Logging.instance.log('2 outputs in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log('Change Output Size: $changeOutputSize', + level: LogLevel.Info); + Logging.instance.log( + 'Difference (fee being paid): $feeBeingPaid sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + + // make sure minimum fee is accurate if that is being used + if (txn["vSize"] - feeBeingPaid == 1) { + int changeOutputSize = + satoshisBeingUsed - satoshiAmountToSend - (txn["vSize"] as int); + feeBeingPaid = + satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; + recipientsAmtArray.removeLast(); + recipientsAmtArray.add(changeOutputSize); + Logging.instance.log('Adjusted Input size: $satoshisBeingUsed', + level: LogLevel.Info); + Logging.instance.log( + 'Adjusted Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Adjusted Change Output Size: $changeOutputSize', + level: LogLevel.Info); + Logging.instance.log( + 'Adjusted Difference (fee being paid): $feeBeingPaid sats', + level: LogLevel.Info); + Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', + level: LogLevel.Info); + txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + } + + Map transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": feeBeingPaid, + "vSize": txn["vSize"], + }; + return transactionObject; + } else { + // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize + // is smaller than or equal to [DUST_LIMIT]. Revert to single output transaction. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": satoshisBeingUsed - satoshiAmountToSend, + "vSize": txn["vSize"], + }; + return transactionObject; + } + } else { + // No additional outputs needed since adding one would mean that it'd be smaller than 546 sats + // which makes it uneconomical to add to the transaction. Here, we pass data directly to instruct + // the wallet to begin crafting the transaction that the user requested. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": satoshisBeingUsed - satoshiAmountToSend, + "vSize": txn["vSize"], + }; + return transactionObject; + } + } else if (satoshisBeingUsed - satoshiAmountToSend == feeForOneOutput) { + // In this scenario, no additional change output is needed since inputs - outputs equal exactly + // what we need to pay for fees. Here, we pass data directly to instruct the wallet to begin + // crafting the transaction that the user requested. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Fee being paid: ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": feeForOneOutput, + "vSize": txn["vSize"], + }; + return transactionObject; + } else { + // Remember that returning 2 indicates that the user does not have a sufficient balance to + // pay for the transaction fee. Ideally, at this stage, we should check if the user has any + // additional outputs they're able to spend and then recalculate fees. + Logging.instance.log( + 'Cannot pay tx fee - checking for more outputs and trying again', + level: LogLevel.Warning); + // try adding more outputs + if (spendableOutputs.length > inputsBeingConsumed) { + return coinSelection(satoshiAmountToSend, selectedTxFeeRate, + _recipientAddress, isSendAll, + additionalOutputs: additionalOutputs + 1, utxos: utxos); + } + return 2; + } + } + + Future> fetchBuildTxData( + List utxosToUse, + ) async { + // return data + Map results = {}; + Map> addressTxid = {}; + + // addresses to check + List addressesP2PKH = []; + List addressesP2SH = []; + + try { + // Populating the addresses to check + for (var i = 0; i < utxosToUse.length; i++) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + final address = output["scriptPubKey"]["addresses"][0] as String; + if (!addressTxid.containsKey(address)) { + addressTxid[address] = []; + } + (addressTxid[address] as List).add(txid); + switch (addressType(address: address)) { + case DerivePathType.bip44: + addressesP2PKH.add(address); + break; + case DerivePathType.bip49: + addressesP2SH.add(address); + break; + } + } + } + } + + // p2pkh / bip44 + final p2pkhLength = addressesP2PKH.length; + if (p2pkhLength > 0) { + final receiveDerivations = await _fetchDerivations( + chain: 0, + derivePathType: DerivePathType.bip44, + ); + final changeDerivations = await _fetchDerivations( + chain: 1, + derivePathType: DerivePathType.bip44, + ); + for (int i = 0; i < p2pkhLength; i++) { + // receives + final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; + // if a match exists it will not be null + if (receiveDerivation != null) { + final data = P2PKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + receiveDerivation["pubKey"] as String)), + network: _network, + ).data; + + for (String tx in addressTxid[addressesP2PKH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + receiveDerivation["wif"] as String, + network: _network, + ), + }; + } + } else { + // if its not a receive, check change + final changeDerivation = changeDerivations[addressesP2PKH[i]]; + // if a match exists it will not be null + if (changeDerivation != null) { + final data = P2PKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + changeDerivation["pubKey"] as String)), + network: _network, + ).data; + + for (String tx in addressTxid[addressesP2PKH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + changeDerivation["wif"] as String, + network: _network, + ), + }; + } + } + } + } + } + + // p2sh / bip49 + final p2shLength = addressesP2SH.length; + if (p2shLength > 0) { + final receiveDerivations = await _fetchDerivations( + chain: 0, + derivePathType: DerivePathType.bip49, + ); + final changeDerivations = await _fetchDerivations( + chain: 1, + derivePathType: DerivePathType.bip49, + ); + for (int i = 0; i < p2shLength; i++) { + // receives + final receiveDerivation = receiveDerivations[addressesP2SH[i]]; + // if a match exists it will not be null + if (receiveDerivation != null) { + final p2wpkh = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + receiveDerivation["pubKey"] as String)), + network: _network) + .data; + + final redeemScript = p2wpkh.output; + + final data = + P2SH(data: PaymentData(redeem: p2wpkh), network: _network).data; + + for (String tx in addressTxid[addressesP2SH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + receiveDerivation["wif"] as String, + network: _network, + ), + "redeemScript": redeemScript, + }; + } + } else { + // if its not a receive, check change + final changeDerivation = changeDerivations[addressesP2SH[i]]; + // if a match exists it will not be null + if (changeDerivation != null) { + final p2wpkh = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + changeDerivation["pubKey"] as String)), + network: _network) + .data; + + final redeemScript = p2wpkh.output; + + final data = + P2SH(data: PaymentData(redeem: p2wpkh), network: _network) + .data; + + for (String tx in addressTxid[addressesP2SH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + changeDerivation["wif"] as String, + network: _network, + ), + "redeemScript": redeemScript, + }; + } + } + } + } + } + + return results; + } catch (e, s) { + Logging.instance + .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); + rethrow; + } + } + + /// Builds and signs a transaction + Future> buildTransaction({ + required List utxosToUse, + required Map utxoSigningData, + required List recipients, + required List satoshiAmounts, + }) async { + final builder = Bitbox.Bitbox.transactionBuilder(); + + // retrieve address' utxos from the rest api + List _utxos = + []; // await Bitbox.Address.utxo(address) as List; + utxosToUse.forEach((element) { + _utxos.add(Bitbox.Utxo( + element.txid, + element.vout, + Bitbox.BitcoinCash.fromSatoshi(element.value), + element.value, + 0, + MINIMUM_CONFIRMATIONS + 1)); + }); + Logger.print("bch utxos: ${_utxos}"); + + // placeholder for input signatures + final signatures = []; + + // placeholder for total input balance + int totalBalance = 0; + + // iterate through the list of address _utxos and use them as inputs for the + // withdrawal transaction + _utxos.forEach((Bitbox.Utxo utxo) { + // add the utxo as an input for the transaction + builder.addInput(utxo.txid, utxo.vout); + final ec = utxoSigningData[utxo.txid]["keyPair"] as ECPair; + + final bitboxEC = Bitbox.ECPair.fromWIF(ec.toWIF()); + + // add a signature to the list to be used later + signatures.add({ + "vin": signatures.length, + "key_pair": bitboxEC, + "original_amount": utxo.satoshis + }); + + totalBalance += utxo.satoshis; + }); + + // calculate the fee based on number of inputs and one expected output + final fee = + Bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length); + + // calculate how much balance will be left over to spend after the fee + final sendAmount = totalBalance - fee; + + // add the output based on the address provided in the testing data + for (int i = 0; i < recipients.length; i++) { + String recipient = recipients[i]; + int satoshiAmount = satoshiAmounts[i]; + builder.addOutput(recipient, satoshiAmount); + } + + // sign all inputs + signatures.forEach((signature) { + builder.sign( + signature["vin"] as int, + signature["key_pair"] as Bitbox.ECPair, + signature["original_amount"] as int); + }); + + // build the transaction + final tx = builder.build(); + final txHex = tx.toHex(); + final vSize = tx.virtualSize(); + Logger.print("bch raw hex: $txHex"); + + return {"hex": txHex, "vSize": vSize}; + } + + @override + Future fullRescan( + int maxUnusedAddressGap, + int maxNumberOfIndexesToCheck, + ) async { + Logging.instance.log("Starting full rescan!", level: LogLevel.Info); + longMutex = true; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + // clear cache + _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); + + // back up data + await _rescanBackup(); + + try { + final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); + await _recoverWalletFromBIP32SeedPhrase( + mnemonic: mnemonic!, + maxUnusedAddressGap: maxUnusedAddressGap, + maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, + ); + + longMutex = false; + Logging.instance.log("Full rescan complete!", level: LogLevel.Info); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + } catch (e, s) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + + // restore from backup + await _rescanRestore(); + + longMutex = false; + Logging.instance.log("Exception rethrown from fullRescan(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future _rescanRestore() async { + Logging.instance.log("starting rescan restore", level: LogLevel.Info); + + // restore from backup + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'receivingAddressesP2PKH_BACKUP'); + final tempChangeAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'changeAddressesP2PKH_BACKUP'); + final tempReceivingIndexP2PKH = DB.instance + .get(boxName: walletId, key: 'receivingIndexP2PKH_BACKUP'); + final tempChangeIndexP2PKH = DB.instance + .get(boxName: walletId, key: 'changeIndexP2PKH_BACKUP'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: tempReceivingAddressesP2PKH); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: tempChangeAddressesP2PKH); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: tempReceivingIndexP2PKH); + await DB.instance.put( + boxName: walletId, + key: 'changeIndexP2PKH', + value: tempChangeIndexP2PKH); + await DB.instance.delete( + key: 'receivingAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'changeAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'receivingIndexP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'changeIndexP2PKH_BACKUP', boxName: walletId); + + // p2Sh + final tempReceivingAddressesP2SH = DB.instance + .get(boxName: walletId, key: 'receivingAddressesP2SH_BACKUP'); + final tempChangeAddressesP2SH = DB.instance + .get(boxName: walletId, key: 'changeAddressesP2SH_BACKUP'); + final tempReceivingIndexP2SH = DB.instance + .get(boxName: walletId, key: 'receivingIndexP2SH_BACKUP'); + final tempChangeIndexP2SH = DB.instance + .get(boxName: walletId, key: 'changeIndexP2SH_BACKUP'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2SH', + value: tempReceivingAddressesP2SH); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2SH', + value: tempChangeAddressesP2SH); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2SH', + value: tempReceivingIndexP2SH); + await DB.instance.put( + boxName: walletId, key: 'changeIndexP2SH', value: tempChangeIndexP2SH); + await DB.instance.delete( + key: 'receivingAddressesP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'changeAddressesP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'receivingIndexP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete(key: 'changeIndexP2SH_BACKUP', boxName: walletId); + + // P2PKH derivations + final p2pkhReceiveDerivationsString = await _secureStore.read( + key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); + final p2pkhChangeDerivationsString = await _secureStore.read( + key: "${walletId}_changeDerivationsP2PKH_BACKUP"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2PKH", + value: p2pkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2PKH", + value: p2pkhChangeDerivationsString); + + await _secureStore.delete( + key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH_BACKUP"); + + // P2SH derivations + final p2shReceiveDerivationsString = await _secureStore.read( + key: "${walletId}_receiveDerivationsP2SH_BACKUP"); + final p2shChangeDerivationsString = await _secureStore.read( + key: "${walletId}_changeDerivationsP2SH_BACKUP"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2SH", + value: p2shReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2SH", + value: p2shChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH_BACKUP"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH_BACKUP"); + + // UTXOs + final utxoData = DB.instance + .get(boxName: walletId, key: 'latest_utxo_model_BACKUP'); + await DB.instance.put( + boxName: walletId, key: 'latest_utxo_model', value: utxoData); + await DB.instance + .delete(key: 'latest_utxo_model_BACKUP', boxName: walletId); + + Logging.instance.log("rescan restore complete", level: LogLevel.Info); + } + + Future _rescanBackup() async { + Logging.instance.log("starting rescan backup", level: LogLevel.Info); + + // backup current and clear data + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'receivingAddressesP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2PKH_BACKUP', + value: tempReceivingAddressesP2PKH); + await DB.instance + .delete(key: 'receivingAddressesP2PKH', boxName: walletId); + + final tempChangeAddressesP2PKH = DB.instance + .get(boxName: walletId, key: 'changeAddressesP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2PKH_BACKUP', + value: tempChangeAddressesP2PKH); + await DB.instance + .delete(key: 'changeAddressesP2PKH', boxName: walletId); + + final tempReceivingIndexP2PKH = + DB.instance.get(boxName: walletId, key: 'receivingIndexP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2PKH_BACKUP', + value: tempReceivingIndexP2PKH); + await DB.instance + .delete(key: 'receivingIndexP2PKH', boxName: walletId); + + final tempChangeIndexP2PKH = + DB.instance.get(boxName: walletId, key: 'changeIndexP2PKH'); + await DB.instance.put( + boxName: walletId, + key: 'changeIndexP2PKH_BACKUP', + value: tempChangeIndexP2PKH); + await DB.instance + .delete(key: 'changeIndexP2PKH', boxName: walletId); + + // p2sh + final tempReceivingAddressesP2SH = DB.instance + .get(boxName: walletId, key: 'receivingAddressesP2SH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingAddressesP2SH_BACKUP', + value: tempReceivingAddressesP2SH); + await DB.instance + .delete(key: 'receivingAddressesP2SH', boxName: walletId); + + final tempChangeAddressesP2SH = + DB.instance.get(boxName: walletId, key: 'changeAddressesP2SH'); + await DB.instance.put( + boxName: walletId, + key: 'changeAddressesP2SH_BACKUP', + value: tempChangeAddressesP2SH); + await DB.instance + .delete(key: 'changeAddressesP2SH', boxName: walletId); + + final tempReceivingIndexP2SH = + DB.instance.get(boxName: walletId, key: 'receivingIndexP2SH'); + await DB.instance.put( + boxName: walletId, + key: 'receivingIndexP2SH_BACKUP', + value: tempReceivingIndexP2SH); + await DB.instance + .delete(key: 'receivingIndexP2SH', boxName: walletId); + + final tempChangeIndexP2SH = + DB.instance.get(boxName: walletId, key: 'changeIndexP2SH'); + await DB.instance.put( + boxName: walletId, + key: 'changeIndexP2SH_BACKUP', + value: tempChangeIndexP2SH); + await DB.instance + .delete(key: 'changeIndexP2SH', boxName: walletId); + + // P2PKH derivations + final p2pkhReceiveDerivationsString = + await _secureStore.read(key: "${walletId}_receiveDerivationsP2PKH"); + final p2pkhChangeDerivationsString = + await _secureStore.read(key: "${walletId}_changeDerivationsP2PKH"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2PKH_BACKUP", + value: p2pkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2PKH_BACKUP", + value: p2pkhChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2PKH"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH"); + + // P2SH derivations + final p2shReceiveDerivationsString = + await _secureStore.read(key: "${walletId}_receiveDerivationsP2SH"); + final p2shChangeDerivationsString = + await _secureStore.read(key: "${walletId}_changeDerivationsP2SH"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2SH_BACKUP", + value: p2shReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2SH_BACKUP", + value: p2shChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH"); + + // UTXOs + final utxoData = + DB.instance.get(boxName: walletId, key: 'latest_utxo_model'); + await DB.instance.put( + boxName: walletId, key: 'latest_utxo_model_BACKUP', value: utxoData); + await DB.instance + .delete(key: 'latest_utxo_model', boxName: walletId); + + Logging.instance.log("rescan backup complete", level: LogLevel.Info); + } + + @override + set isFavorite(bool markFavorite) { + DB.instance.put( + boxName: walletId, key: "isFavorite", value: markFavorite); + } + + @override + bool get isFavorite { + try { + return DB.instance.get(boxName: walletId, key: "isFavorite") + as bool; + } catch (e, s) { + Logging.instance + .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + @override + bool get isRefreshing => refreshMutex; + + bool isActive = false; + + @override + void Function(bool)? get onIsActiveWalletChanged => + (isActive) => this.isActive = isActive; + + @override + Future estimateFeeFor(int satoshiAmount, int feeRate) async { + final available = Format.decimalAmountToSatoshis(await availableBalance); + + if (available == satoshiAmount) { + return satoshiAmount - sweepAllEstimate(feeRate); + } else if (satoshiAmount <= 0 || satoshiAmount > available) { + return roughFeeEstimate(1, 2, feeRate); + } + + int runningBalance = 0; + int inputCount = 0; + for (final output in outputsList) { + runningBalance += output.value; + inputCount++; + if (runningBalance > satoshiAmount) { + break; + } + } + + final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); + final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); + + if (runningBalance - satoshiAmount > oneOutPutFee) { + if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - satoshiAmount - twoOutPutFee; + if (change > DUST_LIMIT && + runningBalance - satoshiAmount - change == twoOutPutFee) { + return runningBalance - satoshiAmount - change; + } else { + return runningBalance - satoshiAmount; + } + } else { + return runningBalance - satoshiAmount; + } + } else if (runningBalance - satoshiAmount == oneOutPutFee) { + return oneOutPutFee; + } else { + return twoOutPutFee; + } + } + + // TODO: correct formula for bch? + int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return ((181 * inputCount) + (34 * outputCount) + 10) * + (feeRatePerKB / 1000).ceil(); + } + + int sweepAllEstimate(int feeRate) { + int available = 0; + int inputCount = 0; + for (final output in outputsList) { + if (output.status.confirmed) { + available += output.value; + inputCount++; + } + } + + // transaction will only have 1 output minus the fee + final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); + + return available - estimatedFee; + } + + @override + Future generateNewAddress() async { + try { + await _incrementAddressIndexForChain( + 0, DerivePathType.bip44); // First increment the receiving index + final newReceivingIndex = DB.instance.get( + boxName: walletId, + key: 'receivingIndexP2PKH') as int; // Check the new receiving index + final newReceivingAddress = await _generateAddressForChain( + 0, + newReceivingIndex, + DerivePathType + .bip44); // Use new index to derive a new receiving address + await _addToAddressesArrayForChain( + newReceivingAddress, + 0, + DerivePathType + .bip44); // Add that new receiving address to the array of receiving addresses + _currentReceivingAddressP2PKH = Future(() => + newReceivingAddress); // Set the new receiving address that the service + + return true; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from generateNewAddress(): $e\n$s", + level: LogLevel.Error); + return false; + } + } +} + +// Bitcoincash Network +final bitcoincash = NetworkType( + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'bc', + bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), + pubKeyHash: 0x00, + scriptHash: 0x05, + wif: 0x80); + +final bitcoincashtestnet = NetworkType( + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'tb', + bip32: Bip32Type(public: 0x043587cf, private: 0x04358394), + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef); diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 69acd843f..089b20a33 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -4,10 +4,12 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/models.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart'; +import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; +import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -98,25 +100,25 @@ abstract class CoinServiceAPI { tracker: tracker, ); - // case Coin.bitcoincash: - // return BitcoinCashWallet( - // walletId: walletId, - // walletName: walletName, - // coin: coin, - // client: client, - // cachedClient: cachedClient, - // tracker: tracker, - // ); - // - // case Coin.bitcoincashTestnet: - // return BitcoinCashWallet( - // walletId: walletId, - // walletName: walletName, - // coin: coin, - // client: client, - // cachedClient: cachedClient, - // tracker: tracker, - // ); + case Coin.bitcoincash: + return BitcoinCashWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + client: client, + cachedClient: cachedClient, + tracker: tracker, + ); + + case Coin.bitcoincashTestnet: + return BitcoinCashWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + client: client, + cachedClient: cachedClient, + tracker: tracker, + ); case Coin.dogecoin: return DogecoinWallet( @@ -144,6 +146,14 @@ abstract class CoinServiceAPI { // tracker: tracker, ); + case Coin.wownero: + return WowneroWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + // tracker: tracker, + ); + case Coin.namecoin: return NamecoinWallet( walletId: walletId, diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 9734a9b51..bedd50327 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -2443,7 +2443,11 @@ class NamecoinWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { - final address = out["scriptPubKey"]["addresses"][0] as String?; + String? address = out["scriptPubKey"]["address"] as String?; + if (address == null && out["scriptPubKey"]["addresses"] != null) { + address = out["scriptPubKey"]["addresses"][0] as String?; + } + if (address != null) { sendersArray.add(address); } @@ -2454,7 +2458,10 @@ class NamecoinWallet extends CoinServiceAPI { Logging.instance.log("sendersArray: $sendersArray", level: LogLevel.Info); for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["addresses"][0] as String?; + String? address = output["scriptPubKey"]["address"] as String?; + if (address == null && output["scriptPubKey"]["addresses"] != null) { + address = output["scriptPubKey"]["addresses"][0] as String?; + } if (address != null) { recipientsArray.add(address); } @@ -2519,7 +2526,10 @@ class NamecoinWallet extends CoinServiceAPI { // add up received tx value for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["addresses"][0]; + String? address = output["scriptPubKey"]["address"] as String?; + if (address == null && output["scriptPubKey"]["addresses"] != null) { + address = output["scriptPubKey"]["addresses"][0] as String?; + } if (address != null) { final value = (Decimal.parse(output["value"].toString()) * Decimal.fromInt(Constants.satsPerCoin)) diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart new file mode 100644 index 000000000..7114269ec --- /dev/null +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -0,0 +1,1560 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart'; +import 'package:cw_wownero/api/wallet.dart'; +import 'package:cw_wownero/pending_wownero_transaction.dart'; +import 'package:cw_wownero/wownero_amount_format.dart'; +import 'package:cw_wownero/wownero_wallet.dart'; +import 'package:dart_numerics/dart_numerics.dart'; +import 'package:decimal/decimal.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_libmonero/core/key_service.dart'; +import 'package:flutter_libmonero/core/wallet_creation_service.dart'; +import 'package:flutter_libmonero/view_model/send/output.dart' + as wownero_output; +import 'package:flutter_libmonero/wownero/wownero.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:http/http.dart'; +import 'package:mutex/mutex.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/paymint/transactions_model.dart'; +import 'package:stackwallet/models/paymint/utxo_model.dart'; +import 'package:stackwallet/services/coins/coin_service.dart'; +import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/price.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; + +const int MINIMUM_CONFIRMATIONS = 10; + +//https://github.com/wownero-project/wownero/blob/8361d60aef6e17908658128284899e3a11d808d4/src/cryptonote_config.h#L162 +const String GENESIS_HASH_MAINNET = + "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; +const String GENESIS_HASH_TESTNET = + "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; + +class WowneroWallet extends CoinServiceAPI { + static const integrationTestFlag = + bool.fromEnvironment("IS_INTEGRATION_TEST"); + final _prefs = Prefs.instance; + + Timer? timer; + Timer? wowneroAutosaveTimer; + late Coin _coin; + + late FlutterSecureStorageInterface _secureStore; + + late PriceAPI _priceAPI; + + Future getCurrentNode() async { + return NodeService().getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + } + + WowneroWallet( + {required String walletId, + required String walletName, + required Coin coin, + PriceAPI? priceAPI, + FlutterSecureStorageInterface? secureStore}) { + _walletId = walletId; + _walletName = walletName; + _coin = coin; + + _priceAPI = priceAPI ?? PriceAPI(Client()); + _secureStore = + secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + } + + bool _shouldAutoSync = false; + + @override + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) { + if (_shouldAutoSync != shouldAutoSync) { + _shouldAutoSync = shouldAutoSync; + if (!shouldAutoSync) { + timer?.cancel(); + wowneroAutosaveTimer?.cancel(); + timer = null; + wowneroAutosaveTimer = null; + stopNetworkAlivePinging(); + } else { + startNetworkAlivePinging(); + // Walletbase needs to be open for this to work + refresh(); + } + } + } + + @override + Future updateNode(bool shouldRefresh) async { + final node = await getCurrentNode(); + + final host = Uri.parse(node.host).host; + await walletBase?.connectToNode( + node: Node(uri: "$host:${node.port}", type: WalletType.wownero)); + + // TODO: is this sync call needed? Do we need to notify ui here? + await walletBase?.startSync(); + + if (shouldRefresh) { + await refresh(); + } + } + + Future> _getMnemonicList() async { + final mnemonicString = + await _secureStore.read(key: '${_walletId}_mnemonic'); + if (mnemonicString == null) { + return []; + } + final List data = mnemonicString.split(' '); + return data; + } + + @override + Future> get mnemonic => _getMnemonicList(); + + Future get currentNodeHeight async { + try { + if (walletBase!.syncStatus! is SyncedSyncStatus && + walletBase!.syncStatus!.progress() == 1.0) { + return await walletBase!.getNodeHeight(); + } + } catch (e, s) {} + int _height = -1; + try { + _height = (walletBase!.syncStatus as SyncingSyncStatus).height; + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Warning); + } + + int blocksRemaining = -1; + + try { + blocksRemaining = + (walletBase!.syncStatus as SyncingSyncStatus).blocksLeft; + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Warning); + } + int currentHeight = _height + blocksRemaining; + if (_height == -1 || blocksRemaining == -1) { + currentHeight = int64MaxValue; + } + final cachedHeight = DB.instance + .get(boxName: walletId, key: "storedNodeHeight") as int? ?? + 0; + + if (currentHeight > cachedHeight && currentHeight != int64MaxValue) { + await DB.instance.put( + boxName: walletId, key: "storedNodeHeight", value: currentHeight); + return currentHeight; + } else { + return cachedHeight; + } + } + + Future get currentSyncingHeight async { + //TODO return the tip of the wownero blockchain + try { + if (walletBase!.syncStatus! is SyncedSyncStatus && + walletBase!.syncStatus!.progress() == 1.0) { + Logging.instance + .log("currentSyncingHeight lol", level: LogLevel.Warning); + return getSyncingHeight(); + } + } catch (e, s) {} + int syncingHeight = -1; + try { + syncingHeight = (walletBase!.syncStatus as SyncingSyncStatus).height; + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Warning); + } + final cachedHeight = + DB.instance.get(boxName: walletId, key: "storedSyncingHeight") + as int? ?? + 0; + + if (syncingHeight > cachedHeight) { + await DB.instance.put( + boxName: walletId, key: "storedSyncingHeight", value: syncingHeight); + return syncingHeight; + } else { + return cachedHeight; + } + } + + Future updateStoredChainHeight({required int newHeight}) async { + await DB.instance.put( + boxName: walletId, key: "storedChainHeight", value: newHeight); + } + + int get storedChainHeight { + return DB.instance.get(boxName: walletId, key: "storedChainHeight") + as int? ?? + 0; + } + + /// Increases the index for either the internal or external chain, depending on [chain]. + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _incrementAddressIndexForChain(int chain) async { + // Here we assume chain == 1 if it isn't 0 + String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; + + final newIndex = + (DB.instance.get(boxName: walletId, key: indexKey)) + 1; + await DB.instance + .put(boxName: walletId, key: indexKey, value: newIndex); + } + + Future _checkCurrentReceivingAddressesForTransactions() async { + try { + await _checkReceivingAddressForTransactions(); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future _checkReceivingAddressForTransactions() async { + try { + int highestIndex = -1; + for (var element + in walletBase!.transactionHistory!.transactions!.entries) { + if (element.value.direction == TransactionDirection.incoming) { + int curAddressIndex = + element.value.additionalInfo!['addressIndex'] as int; + if (curAddressIndex > highestIndex) { + highestIndex = curAddressIndex; + } + } + } + + // Check the new receiving index + String indexKey = "receivingIndex"; + final curIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + if (highestIndex >= curIndex) { + // First increment the receiving index + await _incrementAddressIndexForChain(0); + final newReceivingIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new receiving address + final newReceivingAddress = + await _generateAddressForChain(0, newReceivingIndex); + + // Add that new receiving address to the array of receiving addresses + await _addToAddressesArrayForChain(newReceivingAddress, 0); + + // Set the new receiving address that the service + + _currentReceivingAddress = Future(() => newReceivingAddress); + } + } on SocketException catch (se, s) { + Logging.instance.log( + "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", + level: LogLevel.Error); + return; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + @override + bool get isRefreshing => refreshMutex; + + bool refreshMutex = false; + + Timer? syncPercentTimer; + + Mutex syncHeightMutex = Mutex(); + Future stopSyncPercentTimer() async { + syncPercentTimer?.cancel(); + syncPercentTimer = null; + } + + Future startSyncPercentTimer() async { + if (syncPercentTimer != null) { + return; + } + syncPercentTimer?.cancel(); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(highestPercentCached, walletId)); + syncPercentTimer = Timer.periodic(const Duration(seconds: 30), (_) async { + if (syncHeightMutex.isLocked) { + return; + } + await syncHeightMutex.protect(() async { + // int restoreheight = walletBase!.walletInfo.restoreHeight ?? 0; + int _height = await currentSyncingHeight; + int _currentHeight = await currentNodeHeight; + double progress = 0; + try { + progress = walletBase!.syncStatus!.progress(); + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Warning); + } + + final int blocksRemaining = _currentHeight - _height; + + GlobalEventBus.instance + .fire(BlocksRemainingEvent(blocksRemaining, walletId)); + + if (progress == 1 && _currentHeight > 0 && _height > 0) { + await stopSyncPercentTimer(); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + return; + } + + // for some reason this can be 0 which screws up the percent calculation + // int64MaxValue is NOT the best value to use here + if (_currentHeight < 1) { + _currentHeight = int64MaxValue; + } + + if (_height < 1) { + _height = 1; + } + + double restorePercent = progress; + double highestPercent = highestPercentCached; + + Logging.instance.log( + "currentSyncingHeight: $_height, nodeHeight: $_currentHeight, restorePercent: $restorePercent, highestPercentCached: $highestPercentCached", + level: LogLevel.Info); + + if (restorePercent > 0 && restorePercent <= 1) { + // if (restorePercent > highestPercent) { + highestPercent = restorePercent; + highestPercentCached = restorePercent; + // } + } + + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(highestPercent, walletId)); + }); + }); + } + + double get highestPercentCached => + DB.instance.get(boxName: walletId, key: "highestPercentCached") + as double? ?? + 0; + set highestPercentCached(double value) => DB.instance.put( + boxName: walletId, + key: "highestPercentCached", + value: value, + ); + + /// Refreshes display data for the wallet + @override + Future refresh() async { + if (refreshMutex) { + Logging.instance.log("$walletId $walletName refreshMutex denied", + level: LogLevel.Info); + return; + } else { + refreshMutex = true; + } + + if (walletBase == null) { + throw Exception("Tried to call refresh() in wownero without walletBase!"); + } + + try { + await startSyncPercentTimer(); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + final int _currentSyncingHeight = await currentSyncingHeight; + final int storedHeight = storedChainHeight; + int _currentNodeHeight = await currentNodeHeight; + + double progress = 0; + try { + progress = (walletBase!.syncStatus!).progress(); + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Warning); + } + await _fetchTransactionData(); + + bool stillSyncing = false; + Logging.instance.log( + "storedHeight: $storedHeight, _currentSyncingHeight: $_currentSyncingHeight, _currentNodeHeight: $_currentNodeHeight, progress: $progress, issynced: ${await walletBase!.isConnected()}", + level: LogLevel.Info); + + if (progress < 1.0) { + stillSyncing = true; + } + + if (_currentSyncingHeight > storedHeight) { + // 0 is returned from wownero as I assume an error????? + if (_currentSyncingHeight > 0) { + // 0 failed to fetch current height??? + await updateStoredChainHeight(newHeight: _currentSyncingHeight); + } + } + + await _checkCurrentReceivingAddressesForTransactions(); + String indexKey = "receivingIndex"; + final curIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + // Use new index to derive a new receiving address + try { + final newReceivingAddress = await _generateAddressForChain(0, curIndex); + _currentReceivingAddress = Future(() => newReceivingAddress); + } catch (e, s) { + Logging.instance.log( + "Failed to call _generateAddressForChain(0, $curIndex): $e\n$s", + level: LogLevel.Error); + } + final newTxData = await _fetchTransactionData(); + _transactionData = Future(() => newTxData); + + if (isActive || shouldAutoSync) { + timer ??= Timer.periodic(const Duration(seconds: 60), (timer) async { + debugPrint("run timer"); + //TODO: check for new data and refresh if needed. if wownero even needs this + // chain height check currently broken + // if ((await chainHeight) != (await storedChainHeight)) { + // if (await refreshIfThereIsNewData()) { + await refresh(); + GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId)); + // } + // } + }); + wowneroAutosaveTimer ??= + Timer.periodic(const Duration(seconds: 93), (timer) async { + debugPrint("run wownero timer"); + if (isActive) { + await walletBase?.save(); + GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId)); + } + }); + } + + if (stillSyncing) { + debugPrint("still syncing"); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + refreshMutex = false; + return; + } + await stopSyncPercentTimer(); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + refreshMutex = false; + } catch (error, strace) { + refreshMutex = false; + await stopSyncPercentTimer(); + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + coin, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + Logging.instance.log( + "Caught exception in refreshWalletData(): $error\n$strace", + level: LogLevel.Error); + } + } + + @override + // TODO: implement allOwnAddresses + Future> get allOwnAddresses { + return Future(() => []); + } + + @override + Future get balanceMinusMaxFee async => + (await availableBalance) - + (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(); + + @override + Future get currentReceivingAddress => + _currentReceivingAddress ??= _getCurrentAddressForChain(0); + + @override + Future exit() async { + await stopSyncPercentTimer(); + _hasCalledExit = true; + isActive = false; + await walletBase?.save(prioritySave: true); + walletBase?.close(); + wowneroAutosaveTimer?.cancel(); + wowneroAutosaveTimer = null; + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } + + bool _hasCalledExit = false; + + @override + bool get hasCalledExit => _hasCalledExit; + + Future? _currentReceivingAddress; + + Future _getFees() async { + return FeeObject( + numberOfBlocksFast: 10, + numberOfBlocksAverage: 10, + numberOfBlocksSlow: 10, + fast: 4, + medium: 2, + slow: 0); + } + + @override + Future get fees => _feeObject ??= _getFees(); + Future? _feeObject; + + @override + // TODO: implement fullRescan + Future fullRescan( + int maxUnusedAddressGap, + int maxNumberOfIndexesToCheck, + ) async { + var restoreHeight = walletBase?.walletInfo.restoreHeight; + await walletBase?.rescan(height: restoreHeight); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + return; + } + + Future _generateAddressForChain(int chain, int index) async { + // + String address = walletBase!.getTransactionAddress(chain, index); + + return address; + } + + /// Adds [address] to the relevant chain's address array, which is determined by [chain]. + /// [address] - Expects a standard native segwit address + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _addToAddressesArrayForChain(String address, int chain) async { + String chainArray = ''; + if (chain == 0) { + chainArray = 'receivingAddresses'; + } else { + chainArray = 'changeAddresses'; + } + + final addressArray = + DB.instance.get(boxName: walletId, key: chainArray); + if (addressArray == null) { + Logging.instance.log( + 'Attempting to add the following to $chainArray array for chain $chain:${[ + address + ]}', + level: LogLevel.Info); + await DB.instance + .put(boxName: walletId, key: chainArray, value: [address]); + } else { + // Make a deep copy of the existing list + final List newArray = []; + addressArray + .forEach((dynamic _address) => newArray.add(_address as String)); + newArray.add(address); // Add the address passed into the method + await DB.instance + .put(boxName: walletId, key: chainArray, value: newArray); + } + } + + /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] + /// and + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _getCurrentAddressForChain(int chain) async { + // Here, we assume that chain == 1 if it isn't 0 + String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; + final internalChainArray = (DB.instance + .get(boxName: walletId, key: arrayKey)) as List; + return internalChainArray.last as String; + } + + //TODO: take in the default language when creating wallet. + Future _generateNewWallet() async { + Logging.instance + .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); + // TODO: ping wownero server and make sure the genesis hash matches + // if (!integrationTestFlag) { + // final features = await electrumXClient.getServerFeatures(); + // Logging.instance.log("features: $features"); + // if (_networkType == BasicNetworkType.main) { + // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + // throw Exception("genesis hash does not match main net!"); + // } + // } else if (_networkType == BasicNetworkType.test) { + // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + // throw Exception("genesis hash does not match test net!"); + // } + // } + // } + + // this should never fail + if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { + throw Exception( + "Attempted to overwrite mnemonic on generate new wallet!"); + } + + storage = const FlutterSecureStorage(); + // TODO: Wallet Service may need to be switched to Wownero + walletService = + wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); + prefs = await SharedPreferences.getInstance(); + keysStorage = KeyService(storage!); + WalletInfo walletInfo; + WalletCredentials credentials; + try { + String name = _walletId; + final dirPath = + await pathForWalletDir(name: name, type: WalletType.wownero); + final path = await pathForWallet(name: name, type: WalletType.wownero); + credentials = wownero.createWowneroNewWalletCredentials( + name: name, + language: "English", + ); + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, WalletType.wownero), + name: name, + type: WalletType.wownero, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + dirPath: dirPath, + // TODO: find out what to put for address + address: ''); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + sharedPreferences: prefs, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService?.changeWalletType(); + // To restore from a seed + final wallet = await _walletCreationService?.create(credentials); + + // subtract a couple days to ensure we have a buffer for SWB + final bufferedCreateHeight = + getSeedHeightSync(wallet?.seed.trim() as String); + + await DB.instance.put( + boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); + walletInfo.restoreHeight = bufferedCreateHeight; + + await _secureStore.write( + key: '${_walletId}_mnemonic', value: wallet?.seed.trim()); + walletInfo.address = wallet?.walletAddresses.address; + await DB.instance + .add(boxName: WalletInfo.boxName, value: walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + } catch (e, s) { + debugPrint(e.toString()); + debugPrint(s.toString()); + } + final node = await getCurrentNode(); + final host = Uri.parse(node.host).host; + await walletBase?.connectToNode( + node: Node(uri: "$host:${node.port}", type: WalletType.wownero)); + await walletBase?.startSync(); + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + + // Set relevant indexes + await DB.instance + .put(boxName: walletId, key: "receivingIndex", value: 0); + await DB.instance + .put(boxName: walletId, key: "changeIndex", value: 0); + await DB.instance.put( + boxName: walletId, + key: 'blocked_tx_hashes', + value: ["0xdefault"], + ); // A list of transaction hashes to represent frozen utxos in wallet + // initialize address book entries + await DB.instance.put( + boxName: walletId, + key: 'addressBookEntries', + value: {}); + await DB.instance + .put(boxName: walletId, key: "isFavorite", value: false); + + // Generate and add addresses to relevant arrays + final initialReceivingAddress = await _generateAddressForChain(0, 0); + // final initialChangeAddress = await _generateAddressForChain(1, 0); + + await _addToAddressesArrayForChain(initialReceivingAddress, 0); + // await _addToAddressesArrayForChain(initialChangeAddress, 1); + + await DB.instance.put( + boxName: walletId, + key: 'receivingAddresses', + value: [initialReceivingAddress]); + await DB.instance + .put(boxName: walletId, key: "receivingIndex", value: 0); + + _currentReceivingAddress = Future(() => initialReceivingAddress); + + Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); + } + + @override + // TODO: implement initializeWallet + Future initializeNew() async { + await _prefs.init(); + // TODO: ping actual wownero network + // try { + // final hasNetwork = await _electrumXClient.ping(); + // if (!hasNetwork) { + // return false; + // } + // } catch (e, s) { + // Logging.instance.log("Caught in initializeWallet(): $e\n$s"); + // return false; + // } + storage = const FlutterSecureStorage(); + walletService = + wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); + prefs = await SharedPreferences.getInstance(); + keysStorage = KeyService(storage!); + + await _generateNewWallet(); + // var password; + // try { + // password = + // await keysStorage?.getWalletPassword(walletName: this._walletId); + // } catch (e, s) { + // Logging.instance.log("$e $s"); + // Logging.instance.log("Generating new ${coin.ticker} wallet."); + // // Triggers for new users automatically. Generates new wallet + // await _generateNewWallet(wallet); + // await wallet.put("id", this._walletId); + // return true; + // } + // walletBase = (await walletService?.openWallet(this._walletId, password)) + // as WowneroWalletBase; + // Logging.instance.log("Opening existing ${coin.ticker} wallet."); + // // Wallet already exists, triggers for a returning user + // final currentAddress = awaicurrentHeightt _getCurrentAddressForChain(0); + // this._currentReceivingAddress = Future(() => currentAddress); + // + // await walletBase?.connectToNode( + // node: Node( + // uri: "xmr-node.cakewallet.com:18081", type: WalletType.wownero)); + // walletBase?.startSync(); + + return true; + } + + @override + Future initializeExisting() async { + Logging.instance.log( + "Opening existing ${coin.prettyName} wallet $walletName...", + level: LogLevel.Info); + + if ((DB.instance.get(boxName: walletId, key: "id")) == null) { + debugPrint("Exception was thrown"); + throw Exception( + "Attempted to initialize an existing wallet using an unknown wallet ID!"); + } + + storage = const FlutterSecureStorage(); + walletService = + wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); + prefs = await SharedPreferences.getInstance(); + keysStorage = KeyService(storage!); + + await _prefs.init(); + final data = + DB.instance.get(boxName: walletId, key: "latest_tx_model") + as TransactionData?; + if (data != null) { + _transactionData = Future(() => data); + } + + String? password; + try { + password = await keysStorage?.getWalletPassword(walletName: _walletId); + } catch (e, s) { + debugPrint("Exception was thrown $e $s"); + throw Exception("Password not found $e, $s"); + } + walletBase = (await walletService?.openWallet(_walletId, password!)) + as WowneroWalletBase; + debugPrint("walletBase $walletBase"); + Logging.instance.log( + "Opened existing ${coin.prettyName} wallet $walletName", + level: LogLevel.Info); + // Wallet already exists, triggers for a returning user + + String indexKey = "receivingIndex"; + final curIndex = + await DB.instance.get(boxName: walletId, key: indexKey) as int; + // Use new index to derive a new receiving address + final newReceivingAddress = await _generateAddressForChain(0, curIndex); + Logging.instance.log("xmr address in init existing: $newReceivingAddress", + level: LogLevel.Info); + _currentReceivingAddress = Future(() => newReceivingAddress); + } + + @override + Future get maxFee async { + var bal = await availableBalance; + var fee = walletBase!.calculateEstimatedFee( + wownero.getDefaultTransactionPriority(), bal.toBigInt().toInt()) ~/ + 10000; + + return fee; + } + + @override + // TODO: implement pendingBalance + Future get pendingBalance => throw UnimplementedError(); + + bool longMutex = false; + + // TODO: are these needed? + FlutterSecureStorage? storage; + WalletService? walletService; + SharedPreferences? prefs; + KeyService? keysStorage; + WowneroWalletBase? walletBase; + WalletCreationService? _walletCreationService; + + String toStringForinfo(WalletInfo info) { + return "id: ${info.id} name: ${info.name} type: ${info.type} recovery: ${info.isRecovery}" + " restoreheight: ${info.restoreHeight} timestamp: ${info.timestamp} dirPath: ${info.dirPath} " + "path: ${info.path} address: ${info.address} addresses: ${info.addresses}"; + } + + Future pathForWalletDir({ + required String name, + required WalletType type, + }) async { + Directory root = (await getApplicationDocumentsDirectory()); + if (Platform.isIOS) { + root = (await getLibraryDirectory()); + } + final prefix = walletTypeToString(type).toLowerCase(); + final walletsDir = Directory('${root.path}/wallets'); + final walletDire = Directory('${walletsDir.path}/$prefix/$name'); + + if (!walletDire.existsSync()) { + walletDire.createSync(recursive: true); + } + + return walletDire.path; + } + + Future pathForWallet({ + required String name, + required WalletType type, + }) async => + await pathForWalletDir(name: name, type: type) + .then((path) => '$path/$name'); + + // TODO: take in a dynamic height + @override + Future recoverFromMnemonic({ + required String mnemonic, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height, + }) async { + await _prefs.init(); + longMutex = true; + final start = DateTime.now(); + try { + // Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag"); + // if (!integrationTestFlag) { + // final features = await electrumXClient.getServerFeatures(); + // Logging.instance.log("features: $features"); + // if (_networkType == BasicNetworkType.main) { + // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + // throw Exception("genesis hash does not match main net!"); + // } + // } else if (_networkType == BasicNetworkType.test) { + // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + // throw Exception("genesis hash does not match test net!"); + // } + // } + // } + // check to make sure we aren't overwriting a mnemonic + // this should never fail + if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { + longMutex = false; + throw Exception("Attempted to overwrite mnemonic on restore!"); + } + await _secureStore.write( + key: '${_walletId}_mnemonic', value: mnemonic.trim()); + + height = getSeedHeightSync(mnemonic.trim()); + + await DB.instance + .put(boxName: walletId, key: "restoreHeight", value: height); + + storage = const FlutterSecureStorage(); + walletService = + wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); + prefs = await SharedPreferences.getInstance(); + keysStorage = KeyService(storage!); + WalletInfo walletInfo; + WalletCredentials credentials; + String name = _walletId; + final dirPath = + await pathForWalletDir(name: name, type: WalletType.wownero); + final path = await pathForWallet(name: name, type: WalletType.wownero); + credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( + name: name, + height: height, + mnemonic: mnemonic.trim(), + ); + try { + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, WalletType.wownero), + name: name, + type: WalletType.wownero, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + dirPath: dirPath, + // TODO: find out what to put for address + address: ''); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + sharedPreferences: prefs, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService!.changeWalletType(); + // To restore from a seed + final wallet = + await _walletCreationService!.restoreFromSeed(credentials); + walletInfo.address = wallet.walletAddresses.address; + await DB.instance + .add(boxName: WalletInfo.boxName, value: walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + await DB.instance.put( + boxName: walletId, + key: 'receivingAddresses', + value: [walletInfo.address!]); + await DB.instance + .put(boxName: walletId, key: "receivingIndex", value: 0); + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + await DB.instance + .put(boxName: walletId, key: "changeIndex", value: 0); + await DB.instance.put( + boxName: walletId, + key: 'blocked_tx_hashes', + value: ["0xdefault"], + ); // A list of transaction hashes to represent frozen utxos in wallet + // initialize address book entries + await DB.instance.put( + boxName: walletId, + key: 'addressBookEntries', + value: {}); + await DB.instance + .put(boxName: walletId, key: "isFavorite", value: false); + } catch (e, s) { + debugPrint(e.toString()); + debugPrint(s.toString()); + } + final node = await getCurrentNode(); + final host = Uri.parse(node.host).host; + await walletBase?.connectToNode( + node: Node(uri: "$host:${node.port}", type: WalletType.wownero)); + await walletBase?.rescan(height: credentials.height); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverFromMnemonic(): $e\n$s", + level: LogLevel.Error); + longMutex = false; + rethrow; + } + longMutex = false; + + final end = DateTime.now(); + Logging.instance.log( + "$walletName Recovery time: ${end.difference(start).inMilliseconds} millis", + level: LogLevel.Info); + } + + @override + Future send({ + required String toAddress, + required int amount, + Map args = const {}, + }) async { + try { + final txData = await prepareSend( + address: toAddress, satoshiAmount: amount, args: args); + final txHash = await confirmSend(txData: txData); + return txHash; + } catch (e, s) { + Logging.instance + .log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + @override + Future testNetworkConnection() async { + return await walletBase?.isConnected() ?? false; + } + + Timer? _networkAliveTimer; + + void startNetworkAlivePinging() { + // call once on start right away + _periodicPingCheck(); + + // then periodically check + _networkAliveTimer = Timer.periodic( + Constants.networkAliveTimerDuration, + (_) async { + _periodicPingCheck(); + }, + ); + } + + void _periodicPingCheck() async { + bool hasNetwork = await testNetworkConnection(); + _isConnected = hasNetwork; + if (_isConnected != hasNetwork) { + NodeConnectionStatus status = hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; + GlobalEventBus.instance + .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); + } + } + + void stopNetworkAlivePinging() { + _networkAliveTimer?.cancel(); + _networkAliveTimer = null; + } + + bool _isConnected = false; + + @override + bool get isConnected => _isConnected; + + @override + Future get totalBalance async { + var transactions = walletBase?.transactionHistory!.transactions; + int transactionBalance = 0; + for (var tx in transactions!.entries) { + if (tx.value.direction == TransactionDirection.incoming) { + transactionBalance += tx.value.amount!; + } else { + transactionBalance += -tx.value.amount! - tx.value.fee!; + } + } + + // TODO: grab total balance + var bal = 0; + for (var element in walletBase!.balance!.entries) { + bal = bal + element.value.fullBalance; + } + debugPrint("balances: $transactionBalance $bal"); + if (isActive) { + String am = wowneroAmountToString(amount: bal); + + return Decimal.parse(am); + } else { + String am = wowneroAmountToString(amount: transactionBalance); + + return Decimal.parse(am); + } + } + + @override + // TODO: implement onIsActiveWalletChanged + void Function(bool)? get onIsActiveWalletChanged => (isActive) async { + await walletBase?.save(); + walletBase?.close(); + wowneroAutosaveTimer?.cancel(); + wowneroAutosaveTimer = null; + timer?.cancel(); + timer = null; + await stopSyncPercentTimer(); + if (isActive) { + String? password; + try { + password = + await keysStorage?.getWalletPassword(walletName: _walletId); + } catch (e, s) { + debugPrint("Exception was thrown $e $s"); + throw Exception("Password not found $e, $s"); + } + walletBase = (await walletService?.openWallet(_walletId, password!)) + as WowneroWalletBase?; + if (!(await walletBase!.isConnected())) { + final node = await getCurrentNode(); + final host = Uri.parse(node.host).host; + await walletBase?.connectToNode( + node: + Node(uri: "$host:${node.port}", type: WalletType.wownero)); + await walletBase?.startSync(); + } + await refresh(); + } + this.isActive = isActive; + }; + + bool isActive = false; + + @override + Future get transactionData => + _transactionData ??= _fetchTransactionData(); + Future? _transactionData; + + Future _fetchTransactionData() async { + final transactions = walletBase?.transactionHistory!.transactions; + + final cachedTransactions = + DB.instance.get(boxName: walletId, key: 'latest_tx_model') + as TransactionData?; + int latestTxnBlockHeight = + DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") + as int? ?? + 0; + + final txidsList = DB.instance + .get(boxName: walletId, key: "cachedTxids") as List? ?? + []; + + final Set cachedTxids = Set.from(txidsList); + + // TODO: filter to skip cached + confirmed txn processing in next step + // final unconfirmedCachedTransactions = + // cachedTransactions?.getAllTransactions() ?? {}; + // unconfirmedCachedTransactions + // .removeWhere((key, value) => value.confirmedStatus); + // + // if (cachedTransactions != null) { + // for (final tx in allTxHashes.toList(growable: false)) { + // final txHeight = tx["height"] as int; + // if (txHeight > 0 && + // txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { + // if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { + // allTxHashes.remove(tx); + // } + // } + // } + // } + + // sort thing stuff + // change to get Wownero price + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final List> midSortedArray = []; + + if (transactions != null) { + for (var tx in transactions.entries) { + cachedTxids.add(tx.value.id); + Logging.instance.log( + "${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} " + "${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} " + "${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}" + " ${tx.value.keyIndex}", + level: LogLevel.Info); + String am = wowneroAmountToString(amount: tx.value.amount!); + final worthNow = (currentPrice * Decimal.parse(am)).toStringAsFixed(2); + Map midSortedTx = {}; + // // create final tx map + midSortedTx["txid"] = tx.value.id; + midSortedTx["confirmed_status"] = !tx.value.isPending && + tx.value.confirmations != null && + tx.value.confirmations! >= MINIMUM_CONFIRMATIONS; + midSortedTx["confirmations"] = tx.value.confirmations ?? 0; + midSortedTx["timestamp"] = + (tx.value.date.millisecondsSinceEpoch ~/ 1000); + midSortedTx["txType"] = + tx.value.direction == TransactionDirection.incoming + ? "Received" + : "Sent"; + midSortedTx["amount"] = tx.value.amount; + midSortedTx["worthNow"] = worthNow; + midSortedTx["worthAtBlockTimestamp"] = worthNow; + midSortedTx["fees"] = tx.value.fee; + // TODO: shouldn't wownero have an address I can grab + if (tx.value.direction == TransactionDirection.incoming) { + final addressInfo = tx.value.additionalInfo; + + midSortedTx["address"] = walletBase?.getTransactionAddress( + addressInfo!['accountIndex'] as int, + addressInfo['addressIndex'] as int, + ); + } else { + midSortedTx["address"] = ""; + } + + final int txHeight = tx.value.height ?? 0; + midSortedTx["height"] = txHeight; + if (txHeight >= latestTxnBlockHeight) { + latestTxnBlockHeight = txHeight; + } + + midSortedTx["aliens"] = []; + midSortedTx["inputSize"] = 0; + midSortedTx["outputSize"] = 0; + midSortedTx["inputs"] = []; + midSortedTx["outputs"] = []; + midSortedArray.add(midSortedTx); + } + } + + // sort by date ---- + midSortedArray + .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); + Logging.instance.log(midSortedArray, level: LogLevel.Info); + + // buildDateTimeChunks + final Map result = {"dateTimeChunks": []}; + final dateArray = []; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = extractDateFromTimestamp(txObject["timestamp"] as int); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp(chunk["timestamp"] as int) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = >[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; + transactionsMap + .addAll(TransactionData.fromJson(result).getAllTransactions()); + + final txModel = TransactionData.fromMap(transactionsMap); + + await DB.instance.put( + boxName: walletId, + key: 'storedTxnDataHeight', + value: latestTxnBlockHeight); + await DB.instance.put( + boxName: walletId, key: 'latest_tx_model', value: txModel); + await DB.instance.put( + boxName: walletId, + key: 'cachedTxids', + value: cachedTxids.toList(growable: false)); + + return txModel; + } + + @override + // TODO: implement unspentOutputs + Future> get unspentOutputs => throw UnimplementedError(); + + @override + // TODO: implement validateAddress + bool validateAddress(String address) { + bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || + RegExp("[a-zA-Z0-9]{106}").hasMatch(address); + return valid; + } + + @override + String get walletId => _walletId; + late String _walletId; + + @override + String get walletName => _walletName; + late String _walletName; + + // setter for updating on rename + @override + set walletName(String newName) => _walletName = newName; + + @override + set isFavorite(bool markFavorite) { + DB.instance.put( + boxName: walletId, key: "isFavorite", value: markFavorite); + } + + @override + bool get isFavorite { + try { + return DB.instance.get(boxName: walletId, key: "isFavorite") + as bool; + } catch (e, s) { + Logging.instance + .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + @override + // TODO: implement availableBalance + Future get availableBalance async { + var bal = 0; + for (var element in walletBase!.balance!.entries) { + bal = bal + element.value.unlockedBalance; + } + String am = wowneroAmountToString(amount: bal); + + return Decimal.parse(am); + } + + @override + Coin get coin => _coin; + + @override + Future confirmSend({required Map txData}) async { + try { + Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); + final pendingWowneroTransaction = + txData['pendingWowneroTransaction'] as PendingWowneroTransaction; + try { + await pendingWowneroTransaction.commit(); + Logging.instance.log( + "transaction ${pendingWowneroTransaction.id} has been sent", + level: LogLevel.Info); + return pendingWowneroTransaction.id; + } catch (e, s) { + Logging.instance.log("$walletName wownero confirmSend: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } catch (e, s) { + Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Info); + rethrow; + } + } + + // TODO: fix the double free memory crash error. + @override + Future> prepareSend( + {required String address, + required int satoshiAmount, + Map? args}) async { + int amount = satoshiAmount; + String toAddress = address; + try { + final feeRate = args?["feeRate"]; + if (feeRate is FeeRateType) { + MoneroTransactionPriority feePriority = MoneroTransactionPriority.slow; + switch (feeRate) { + case FeeRateType.fast: + feePriority = MoneroTransactionPriority.fastest; + break; + case FeeRateType.average: + feePriority = MoneroTransactionPriority.medium; + break; + case FeeRateType.slow: + feePriority = MoneroTransactionPriority.slow; + break; + } + + Future? awaitPendingTransaction; + try { + Logging.instance + .log("$toAddress $amount $args", level: LogLevel.Info); + String amountToSend = wowneroAmountToString(amount: amount * 1000); + Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); + + wownero_output.Output output = wownero_output.Output(walletBase!); + output.address = toAddress; + output.setCryptoAmount(amountToSend); + + List outputs = [output]; + Object tmp = wownero.createWowneroTransactionCreationCredentials( + outputs: outputs, priority: feePriority); + + awaitPendingTransaction = walletBase!.createTransaction(tmp); + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", + level: LogLevel.Warning); + } + + PendingWowneroTransaction pendingWowneroTransaction = + await (awaitPendingTransaction!) as PendingWowneroTransaction; + int realfee = (Decimal.parse(pendingWowneroTransaction.feeFormatted) * + 100000000.toDecimal()) + .toBigInt() + .toInt(); + debugPrint("fee? $realfee"); + Map txData = { + "pendingWowneroTransaction": pendingWowneroTransaction, + "fee": realfee, + "addresss": toAddress, + "recipientAmt": satoshiAmount, + }; + + Logging.instance.log("prepare send: $txData", level: LogLevel.Info); + return txData; + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepare send(): $e\n$s", + level: LogLevel.Info); + + if (e.toString().contains("Incorrect unlocked balance")) { + throw Exception("Insufficient balance!"); + } else if (e is CreationTransactionException) { + throw Exception("Insufficient funds to pay for transaction fee!"); + } else { + throw Exception("Transaction failed with error code $e"); + } + } + } + + @override + Future estimateFeeFor(int satoshiAmount, int feeRate) async { + MoneroTransactionPriority? priority; + switch (feeRate) { + case 1: + priority = MoneroTransactionPriority.regular; + break; + case 2: + priority = MoneroTransactionPriority.medium; + break; + case 3: + priority = MoneroTransactionPriority.fast; + break; + case 4: + priority = MoneroTransactionPriority.fastest; + break; + case 0: + default: + priority = MoneroTransactionPriority.slow; + break; + } + final fee = + (walletBase?.calculateEstimatedFee(priority, satoshiAmount) ?? 0) ~/ + 10000; + return fee; + } + + @override + Future generateNewAddress() async { + try { + const String indexKey = "receivingIndex"; + // First increment the receiving index + await _incrementAddressIndexForChain(0); + final newReceivingIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new receiving address + final newReceivingAddress = + await _generateAddressForChain(0, newReceivingIndex); + + // Add that new receiving address to the array of receiving addresses + await _addToAddressesArrayForChain(newReceivingAddress, 0); + + // Set the new receiving address that the service + + _currentReceivingAddress = Future(() => newReceivingAddress); + + return true; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from generateNewAddress(): $e\n$s", + level: LogLevel.Error); + return false; + } + } +} diff --git a/lib/services/price.dart b/lib/services/price.dart index c1211d0ca..d157580ad 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -79,8 +79,7 @@ class PriceAPI { Map> result = {}; try { final uri = Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"); - // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"); + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"); // final uri = Uri.parse( // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero%2Cbitcoin%2Cepic-cash%2Czcoin%2Cdogecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"); diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 034db9308..db3011d4e 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -223,7 +223,7 @@ class Wallets extends ChangeNotifier { final shouldSetAutoSync = shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(manager.walletId); - if (manager.coin == Coin.monero) { + if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { walletInitFutures.add(manager.initializeExisting().then((value) { @@ -312,7 +312,7 @@ class Wallets extends ChangeNotifier { final shouldSetAutoSync = shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(manager.walletId); - if (manager.coin == Coin.monero) { + if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { walletInitFutures.add(manager.initializeExisting().then((value) { diff --git a/lib/services/wallets_service.dart b/lib/services/wallets_service.dart index 0700950a4..b30f9e9e5 100644 --- a/lib/services/wallets_service.dart +++ b/lib/services/wallets_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_libmonero/monero/monero.dart'; +import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; @@ -367,8 +368,13 @@ class WalletsService extends ChangeNotifier { await DB.instance.delete( boxName: DB.boxNameAllWalletsData, key: "${walletId}_mnemonicHasBeenVerified"); - - if (coinFromPrettyName(shell['coin'] as String) == Coin.monero) { + if (coinFromPrettyName(shell['coin'] as String) == Coin.wownero) { + final wowService = + wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); + await wowService.remove(walletId); + Logging.instance + .log("monero wallet: $walletId deleted", level: LogLevel.Info); + } else if (coinFromPrettyName(shell['coin'] as String) == Coin.monero) { final xmrService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); await xmrService.remove(walletId); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 6acee305d..d73c5d3ce 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:bitcoindart/bitcoindart.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter_libepiccash/epic_cash.dart'; -// import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart'; +import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; @@ -42,8 +42,8 @@ class AddressUtils { switch (coin) { case Coin.bitcoin: return Address.validateAddress(address, bitcoin); - // case Coin.bitcoincash: - // return Address.validateAddress(address, bitcoincash); + case Coin.bitcoincash: + return Address.validateAddress(address, bitcoincash); case Coin.dogecoin: return Address.validateAddress(address, dogecoin); case Coin.epicCash: @@ -53,12 +53,15 @@ class AddressUtils { case Coin.monero: return RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || RegExp("[a-zA-Z0-9]{106}").hasMatch(address); + case Coin.wownero: + return RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || + RegExp("[a-zA-Z0-9]{106}").hasMatch(address); case Coin.namecoin: return Address.validateAddress(address, namecoin, namecoin.bech32!); case Coin.bitcoinTestNet: return Address.validateAddress(address, testnet); - // case Coin.bitcoincashTestnet: - // return Address.validateAddress(address, bitcoincashtestnet); + case Coin.bitcoincashTestnet: + return Address.validateAddress(address, bitcoincashtestnet); case Coin.firoTestNet: return Address.validateAddress(address, firoTestNetwork); case Coin.dogecoinTestNet: diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index b61f91312..94558e623 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -51,6 +51,7 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension()!.themeType.name}/tx-exchange-icon-failed.svg"; + String get drd => "assets/svg/drd-icon.svg"; String get plus => "assets/svg/plus.svg"; String get gear => "assets/svg/gear.svg"; String get bell => "assets/svg/bell.svg"; @@ -127,6 +128,7 @@ class _SVG { String get epicCash => "assets/svg/coin_icons/EpicCash.svg"; String get firo => "assets/svg/coin_icons/Firo.svg"; String get monero => "assets/svg/coin_icons/Monero.svg"; + String get wownero => "assets/svg/coin_icons/Wownero.svg"; String get namecoin => "assets/svg/coin_icons/Namecoin.svg"; String get chevronRight => "assets/svg/chevron-right.svg"; @@ -145,8 +147,8 @@ class _SVG { switch (coin) { case Coin.bitcoin: return bitcoin; - // case Coin.bitcoincash: - // return bitcoincash; + case Coin.bitcoincash: + return bitcoincash; case Coin.dogecoin: return dogecoin; case Coin.epicCash: @@ -155,12 +157,14 @@ class _SVG { return firo; case Coin.monero: return monero; + case Coin.wownero: + return wownero; case Coin.namecoin: return namecoin; case Coin.bitcoinTestNet: return bitcoinTestnet; - // case Coin.bitcoincashTestnet: - // return bitcoincashTestnet; + case Coin.bitcoincashTestnet: + return bitcoincashTestnet; case Coin.firoTestNet: return firoTestnet; case Coin.dogecoinTestNet: @@ -176,6 +180,7 @@ class _PNG { String get splash => "assets/images/splash.png"; String get monero => "assets/images/monero.png"; + String get wownero => "assets/images/wownero.png"; String get firo => "assets/images/firo.png"; String get dogecoin => "assets/images/doge.png"; String get bitcoin => "assets/images/bitcoin.png"; @@ -188,9 +193,9 @@ class _PNG { case Coin.bitcoin: case Coin.bitcoinTestNet: return bitcoin; - // case Coin.bitcoincash: - // case Coin.bitcoincashTestnet: - // return bitcoincash; + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + return bitcoincash; case Coin.dogecoin: case Coin.dogecoinTestNet: return dogecoin; @@ -202,6 +207,8 @@ class _PNG { return firo; case Coin.monero: return monero; + case Coin.wownero: + return wownero; case Coin.namecoin: return namecoin; } diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index e12fe84f8..f89f270be 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -18,15 +18,17 @@ Uri getBlockExplorerTransactionUrlFor({ throw UnimplementedError("missing block explorer for epic cash"); case Coin.monero: return Uri.parse("https://xmrchain.net/tx/$txid"); + case Coin.wownero: + return Uri.parse("https://explore.wownero.com/search?value=$txid"); case Coin.firo: return Uri.parse("https://explorer.firo.org/tx/$txid"); case Coin.firoTestNet: return Uri.parse("https://testexplorer.firo.org/tx/$txid"); - // case Coin.bitcoincash: - // return Uri.parse("https://blockchair.com/bitcoin-cash/transaction/$txid"); - // case Coin.bitcoincashTestnet: - // return Uri.parse( - // "https://blockexplorer.one/bitcoin-cash/testnet/tx/$txid"); + case Coin.bitcoincash: + return Uri.parse("https://blockchair.com/bitcoin-cash/transaction/$txid"); + case Coin.bitcoincashTestnet: + return Uri.parse( + "https://blockexplorer.one/bitcoin-cash/testnet/tx/$txid"); case Coin.namecoin: return Uri.parse("https://chainz.cryptoid.info/nmc/tx.dws?$txid.htm"); } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 62f31a770..f5139477c 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -18,6 +18,7 @@ abstract class Constants { //TODO: correct for monero? static const int satsPerCoinMonero = 1000000000000; + static const int satsPerCoinWownero = 100000000000; static const int satsPerCoin = 100000000; static const int decimalPlaces = 8; @@ -39,8 +40,8 @@ abstract class Constants { final List values = []; switch (coin) { case Coin.bitcoin: - // case Coin.bitcoincash: - // case Coin.bitcoincashTestnet: + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: case Coin.dogecoin: case Coin.firo: case Coin.bitcoinTestNet: @@ -54,6 +55,9 @@ abstract class Constants { case Coin.monero: values.addAll([25]); break; + case Coin.wownero: + values.addAll([14]); + break; } return values; } @@ -65,9 +69,9 @@ abstract class Constants { case Coin.bitcoinTestNet: return 600; - // case Coin.bitcoincash: - // case Coin.bitcoincashTestnet: - // return 600; + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + return 600; case Coin.dogecoin: case Coin.dogecoinTestNet: @@ -83,6 +87,9 @@ abstract class Constants { case Coin.monero: return 120; + case Coin.wownero: + return 120; + case Coin.namecoin: return 600; } diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 5fa82f9ac..2ffd6f307 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -13,10 +13,11 @@ abstract class DefaultNodes { firo, monero, epicCash, - // bitcoincash, + bitcoincash, namecoin, + wownero, bitcoinTestnet, - // bitcoincashTestnet, + bitcoincashTestnet, dogecoinTestnet, firoTestnet, ]; @@ -33,17 +34,17 @@ abstract class DefaultNodes { isDown: false, ); - // static NodeModel get bitcoincash => NodeModel( - // host: "bitcoincash.stackwallet.com", - // port: 50002, - // name: defaultName, - // id: _nodeId(Coin.bitcoincash), - // useSSL: true, - // enabled: true, - // coinName: Coin.bitcoincash.name, - // isFailover: true, - // isDown: false, - // ); + static NodeModel get bitcoincash => NodeModel( + host: "bitcoincash.stackwallet.com", + port: 50002, + name: defaultName, + id: _nodeId(Coin.bitcoincash), + useSSL: true, + enabled: true, + coinName: Coin.bitcoincash.name, + isFailover: true, + isDown: false, + ); static NodeModel get dogecoin => NodeModel( host: "dogecoin.stackwallet.com", @@ -83,6 +84,20 @@ abstract class DefaultNodes { isDown: false, ); + // TODO: eventually enable ssl and set scheme to https + // currently get certificate failure + static NodeModel get wownero => NodeModel( + host: "http://eu-west-2.wow.xmr.pm", + port: 34568, + name: defaultName, + id: _nodeId(Coin.wownero), + useSSL: false, + enabled: true, + coinName: Coin.wownero.name, + isFailover: true, + isDown: false, + ); + static NodeModel get epicCash => NodeModel( host: "http://epiccash.stackwallet.com", port: 3413, @@ -143,25 +158,25 @@ abstract class DefaultNodes { isDown: false, ); - // static NodeModel get bitcoincashTestnet => NodeModel( - // host: "testnet.hsmiths.com", - // port: 53012, - // name: defaultName, - // id: _nodeId(Coin.bitcoincash), - // useSSL: true, - // enabled: true, - // coinName: Coin.bitcoincash.name, - // isFailover: true, - // isDown: false, - // ); + static NodeModel get bitcoincashTestnet => NodeModel( + host: "testnet.hsmiths.com", + port: 53012, + name: defaultName, + id: _nodeId(Coin.bitcoincashTestnet), + useSSL: true, + enabled: true, + coinName: Coin.bitcoincashTestnet.name, + isFailover: true, + isDown: false, + ); static NodeModel getNodeFor(Coin coin) { switch (coin) { case Coin.bitcoin: return bitcoin; - // - // case Coin.bitcoincash: - // return bitcoincash; + + case Coin.bitcoincash: + return bitcoincash; case Coin.dogecoin: return dogecoin; @@ -175,14 +190,17 @@ abstract class DefaultNodes { case Coin.monero: return monero; + case Coin.wownero: + return wownero; + case Coin.namecoin: return namecoin; case Coin.bitcoinTestNet: return bitcoinTestnet; - // case Coin.bitcoincashTestnet: - // return bitcoincashTestnet; + case Coin.bitcoincashTestnet: + return bitcoincashTestnet; case Coin.firoTestNet: return firoTestnet; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 1c30d86ec..78a04e220 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -1,4 +1,6 @@ import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart' as btc; +import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart' + as bch; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart' as doge; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart' @@ -7,14 +9,16 @@ import 'package:stackwallet/services/coins/firo/firo_wallet.dart' as firo; import 'package:stackwallet/services/coins/monero/monero_wallet.dart' as xmr; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' as nmc; +import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; enum Coin { bitcoin, - // bitcoincash, + bitcoincash, dogecoin, epicCash, firo, monero, + wownero, namecoin, /// @@ -22,21 +26,21 @@ enum Coin { /// bitcoinTestNet, - // bitcoincashTestnet, + bitcoincashTestnet, dogecoinTestNet, firoTestNet, } // remove firotestnet for now -const int kTestNetCoinCount = 2; +const int kTestNetCoinCount = 3; extension CoinExt on Coin { String get prettyName { switch (this) { case Coin.bitcoin: return "Bitcoin"; - // case Coin.bitcoincash: - // return "Bitcoin Cash"; + case Coin.bitcoincash: + return "Bitcoin Cash"; case Coin.dogecoin: return "Dogecoin"; case Coin.epicCash: @@ -45,12 +49,14 @@ extension CoinExt on Coin { return "Firo"; case Coin.monero: return "Monero"; + case Coin.wownero: + return "Wownero"; case Coin.namecoin: return "Namecoin"; case Coin.bitcoinTestNet: return "tBitcoin"; - // case Coin.bitcoincashTestnet: - // return "tBitcoin Cash"; + case Coin.bitcoincashTestnet: + return "tBitcoin Cash"; case Coin.firoTestNet: return "tFiro"; case Coin.dogecoinTestNet: @@ -62,8 +68,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "BTC"; - // case Coin.bitcoincash: - // return "BCH"; + case Coin.bitcoincash: + return "BCH"; case Coin.dogecoin: return "DOGE"; case Coin.epicCash: @@ -72,12 +78,14 @@ extension CoinExt on Coin { return "FIRO"; case Coin.monero: return "XMR"; + case Coin.wownero: + return "WOW"; case Coin.namecoin: return "NMC"; case Coin.bitcoinTestNet: return "tBTC"; - // case Coin.bitcoincashTestnet: - // return "tBCH"; + case Coin.bitcoincashTestnet: + return "tBCH"; case Coin.firoTestNet: return "tFIRO"; case Coin.dogecoinTestNet: @@ -89,8 +97,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "bitcoin"; - // case Coin.bitcoincash: - // return "bitcoincash"; + case Coin.bitcoincash: + return "bitcoincash"; case Coin.dogecoin: return "dogecoin"; case Coin.epicCash: @@ -100,12 +108,14 @@ extension CoinExt on Coin { return "firo"; case Coin.monero: return "monero"; + case Coin.wownero: + return "wownero"; case Coin.namecoin: return "namecoin"; case Coin.bitcoinTestNet: return "bitcoin"; - // case Coin.bitcoincashTestnet: - // return "bitcoincash"; + case Coin.bitcoincashTestnet: + return "bitcoincash"; case Coin.firoTestNet: return "firo"; case Coin.dogecoinTestNet: @@ -116,18 +126,19 @@ extension CoinExt on Coin { bool get isElectrumXCoin { switch (this) { case Coin.bitcoin: - // case Coin.bitcoincash: + case Coin.bitcoincash: case Coin.dogecoin: case Coin.firo: case Coin.namecoin: case Coin.bitcoinTestNet: - // case Coin.bitcoincashTestnet: + case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: return true; case Coin.epicCash: case Coin.monero: + case Coin.wownero: return false; } } @@ -138,9 +149,9 @@ extension CoinExt on Coin { case Coin.bitcoinTestNet: return btc.MINIMUM_CONFIRMATIONS; - // case Coin.bitcoincash: - // case Coin.bitcoincashTestnet: - // return bch.MINIMUM_CONFIRMATIONS; + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + return bch.MINIMUM_CONFIRMATIONS; case Coin.firo: case Coin.firoTestNet: @@ -155,6 +166,10 @@ extension CoinExt on Coin { case Coin.monero: return xmr.MINIMUM_CONFIRMATIONS; + + case Coin.wownero: + return wow.MINIMUM_CONFIRMATIONS; + case Coin.namecoin: return nmc.MINIMUM_CONFIRMATIONS; } @@ -166,10 +181,10 @@ Coin coinFromPrettyName(String name) { case "Bitcoin": case "bitcoin": return Coin.bitcoin; - // case "Bitcoincash": - // case "bitcoincash": - // case "Bitcoin Cash": - // return Coin.bitcoincash; + case "Bitcoincash": + case "bitcoincash": + case "Bitcoin Cash": + return Coin.bitcoincash; case "Dogecoin": case "dogecoin": return Coin.dogecoin; @@ -190,10 +205,10 @@ Coin coinFromPrettyName(String name) { case "bitcoinTestNet": return Coin.bitcoinTestNet; - // case "Bitcoincash Testnet": - // case "tBitcoin Cash": - // case "Bitcoin Cash Testnet": - // return Coin.bitcoincashTestnet; + case "Bitcoincash Testnet": + case "tBitcoin Cash": + case "Bitcoin Cash Testnet": + return Coin.bitcoincashTestnet; case "Firo Testnet": case "tFiro": case "firoTestNet": @@ -202,6 +217,10 @@ Coin coinFromPrettyName(String name) { case "tDogecoin": case "dogecoinTestNet": return Coin.dogecoinTestNet; + case "Wownero": + case "tWownero": + case "wownero": + return Coin.wownero; default: throw ArgumentError.value( name, "name", "No Coin enum value with that prettyName"); @@ -212,8 +231,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { switch (ticker.toLowerCase()) { case "btc": return Coin.bitcoin; - // case "bch": - // return Coin.bitcoincash; + case "bch": + return Coin.bitcoincash; case "doge": return Coin.dogecoin; case "epic": @@ -226,12 +245,14 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.namecoin; case "tbtc": return Coin.bitcoinTestNet; - // case "tbch": - // return Coin.bitcoincashTestnet; + case "tbch": + return Coin.bitcoincashTestnet; case "tfiro": return Coin.firoTestNet; case "tdoge": return Coin.dogecoinTestNet; + case "wow": + return Coin.wownero; default: throw ArgumentError.value( ticker, "name", "No Coin enum value with that ticker"); diff --git a/lib/utilities/listenable_list.dart b/lib/utilities/listenable_list.dart index 5838c0f51..ef07478aa 100644 --- a/lib/utilities/listenable_list.dart +++ b/lib/utilities/listenable_list.dart @@ -52,4 +52,8 @@ class ListenableList extends ChangeNotifier { notifyListeners(); } } + + Iterable map(E Function(T) toElement) { + return _list.map(toElement); + } } diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 1f19da807..7c6aa2aaf 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -1,290 +1,848 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; class STextStyles { - static TextStyle pageTitleH1(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w600, - fontSize: 20, - ); + static StackColors _theme(BuildContext context) => + Theme.of(context).extension()!; - static TextStyle pageTitleH2(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w600, - fontSize: 18, - ); + static TextStyle pageTitleH1(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 20, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 20, + ); + } + } - static TextStyle navBarTitle(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); + static TextStyle pageTitleH2(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 18, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 18, + ); + } + } - static TextStyle titleBold12(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); + static TextStyle navBarTitle(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); + } + } - static TextStyle titleBold12_400(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); + static TextStyle titleBold12(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); + } + } - static TextStyle subtitle(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w400, - fontSize: 16, - ); + static TextStyle titleBold12_400(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); + } + } - static TextStyle subtitle500(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); + static TextStyle subtitle(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); + } + } - static TextStyle subtitle600(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w600, - fontSize: 16, - ); + static TextStyle subtitle500(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); + } + } - static TextStyle button(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 16, - ); + static TextStyle subtitle600(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); + } + } - static TextStyle largeMedium14(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - ); + static TextStyle button(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimary, + fontWeight: FontWeight.w500, + fontSize: 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimary, + fontWeight: FontWeight.w500, + fontSize: 16, + ); + } + } - static TextStyle smallMed14(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark3, - fontWeight: FontWeight.w500, - fontSize: 16, - ); + static TextStyle largeMedium14(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); + } + } - static TextStyle smallMed12(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark3, - fontWeight: FontWeight.w500, - fontSize: 14, - ); + static TextStyle smallMed14(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 16, + ); + } + } - static TextStyle label(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 12, - ); + static TextStyle smallMed12(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + } + } - static TextStyle label700(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textSubtitle1, - fontWeight: FontWeight.w700, - fontSize: 12, - ); + static TextStyle label(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 12, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 12, + ); + } + } - static TextStyle itemSubtitle(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.infoItemLabel, - fontWeight: FontWeight.w500, - fontSize: 14, - ); + static TextStyle label700(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w700, + fontSize: 12, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w700, + fontSize: 12, + ); + } + } - static TextStyle itemSubtitle12(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); + static TextStyle itemSubtitle(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).infoItemLabel, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).infoItemLabel, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + } + } - static TextStyle itemSubtitle12_600(BuildContext context) => - GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - ); + static TextStyle itemSubtitle12(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + } + } - static TextStyle fieldLabel(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); + static TextStyle itemSubtitle12_600(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + } + } - static TextStyle field(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w500, - fontSize: 14, - height: 1.5, - ); + static TextStyle fieldLabel(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textSubtitle2, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.5, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textSubtitle2, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.5, + ); + } + } - static TextStyle baseXS(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w400, - fontSize: 14, - ); + static TextStyle field(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.5, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.5, + ); + } + } - static TextStyle link(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.accentColorRed, - fontWeight: FontWeight.w500, - fontSize: 14, - ); + static TextStyle baseXS(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 14, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 14, + ); + } + } - static TextStyle link2(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.infoItemIcons, - fontWeight: FontWeight.w500, - fontSize: 14, - ); + static TextStyle link(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).accentColorRed, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).accentColorRed, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + } + } - static TextStyle richLink(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.accentColorBlue, - fontWeight: FontWeight.w500, - fontSize: 12, - ); + static TextStyle link2(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).infoItemIcons, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).infoItemIcons, + fontWeight: FontWeight.w500, + fontSize: 14, + ); + } + } - static TextStyle w600_10(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w600, - fontSize: 12, - ); + static TextStyle richLink(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).accentColorBlue, + fontWeight: FontWeight.w500, + fontSize: 12, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).accentColorBlue, + fontWeight: FontWeight.w500, + fontSize: 12, + ); + } + } - static TextStyle syncPercent(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); + static TextStyle w600_10(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 12, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 12, + ); + } + } - static TextStyle buttonSmall(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w500, - fontSize: 12, - ); + static TextStyle syncPercent(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 12, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 12, + ); + } + } - static TextStyle errorSmall(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textError, - fontWeight: FontWeight.w500, - fontSize: 10, - ); + static TextStyle buttonSmall(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 12, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 12, + ); + } + } - static TextStyle infoSmall(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textSubtitle1, - fontWeight: FontWeight.w500, - fontSize: 10, - ); + static TextStyle errorSmall(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textError, + fontWeight: FontWeight.w500, + fontSize: 10, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textError, + fontWeight: FontWeight.w500, + fontSize: 10, + ); + } + } + + static TextStyle infoSmall(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 10, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 10, + ); + } + } // Desktop - static TextStyle desktopH2(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w600, - fontSize: 32, - height: 32 / 32, - ); + static TextStyle desktopH2(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 32, + height: 32 / 32, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 32, + height: 32 / 32, + ); + } + } - static TextStyle desktopH3(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w600, - fontSize: 24, - height: 24 / 24, - ); + static TextStyle desktopH3(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 24, + height: 24 / 24, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 24, + height: 24 / 24, + ); + } + } - static TextStyle desktopTextMedium(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); + static TextStyle desktopTextMedium(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 30 / 20, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 30 / 20, + ); + } + } - static TextStyle desktopTextMediumRegular(BuildContext context) => - GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 30 / 20, - ); + static TextStyle desktopTextMediumRegular(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 20, + height: 30 / 20, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 20, + height: 30 / 20, + ); + } + } - static TextStyle desktopSubtitleH2(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w400, - fontSize: 20, - height: 28 / 20, - ); + static TextStyle desktopSubtitleH2(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 20, + height: 28 / 20, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 20, + height: 28 / 20, + ); + } + } - static TextStyle desktopSubtitleH1(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w400, - fontSize: 24, - height: 33 / 24, - ); + static TextStyle desktopSubtitleH1(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 24, + height: 33 / 24, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 24, + height: 33 / 24, + ); + } + } - static TextStyle desktopButtonEnabled(BuildContext context) => - GoogleFonts.inter( - color: Theme.of(context).extension()!.buttonTextPrimary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); + static TextStyle desktopButtonEnabled(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimary, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimary, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); + } + } - static TextStyle desktopButtonDisabled(BuildContext context) => - GoogleFonts.inter( - color: Theme.of(context) - .extension()! - .buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); + static TextStyle desktopButtonDisabled(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); + } + } - static TextStyle desktopButtonSecondaryEnabled(BuildContext context) => - GoogleFonts.inter( - color: Theme.of(context).extension()!.buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 26 / 20, - ); + static TextStyle desktopButtonSecondaryEnabled(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondary, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondary, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); + } + } - static TextStyle desktopTextExtraSmall(BuildContext context) => - GoogleFonts.inter( - color: Theme.of(context) - .extension()! - .buttonTextPrimaryDisabled, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); + static TextStyle desktopButtonSecondaryDisabled(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); + } + } - static TextStyle desktopButtonSmallSecondaryEnabled(BuildContext context) => - GoogleFonts.inter( - color: Theme.of(context).extension()!.buttonTextSecondary, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 24 / 16, - ); + static TextStyle desktopTextExtraSmall(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 24 / 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 24 / 16, + ); + } + } - static TextStyle desktopTextFieldLabel(BuildContext context) => - GoogleFonts.inter( - color: Theme.of(context).extension()!.textSubtitle2, - fontWeight: FontWeight.w500, - fontSize: 20, - height: 30 / 20, - ); + static TextStyle desktopButtonSmallSecondaryEnabled(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondary, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 24 / 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondary, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 24 / 16, + ); + } + } - static TextStyle desktopMenuItem(BuildContext context) => GoogleFonts.inter( - color: Theme.of(context) - .extension()! - .textDark - .withOpacity(0.8), - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); - static TextStyle desktopMenuItemSelected(BuildContext context) => - GoogleFonts.inter( - color: Theme.of(context).extension()!.textDark, - fontWeight: FontWeight.w500, - fontSize: 16, - height: 20.8 / 16, - ); + static TextStyle desktopTextFieldLabel(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textSubtitle2, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 30 / 20, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textSubtitle2, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 30 / 20, + ); + } + } + + static TextStyle desktopMenuItem(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark.withOpacity(0.8), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark.withOpacity(0.8), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); + } + } + + static TextStyle desktopMenuItemSelected(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); + } + } + + static TextStyle stepIndicator(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.roboto( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 8, + ); + case ThemeType.dark: + return GoogleFonts.roboto( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 8, + ); + } + } + + static TextStyle numberDefault(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.roboto( + color: _theme(context).numberTextDefault, + fontWeight: FontWeight.w400, + fontSize: 26, + ); + case ThemeType.dark: + return GoogleFonts.roboto( + color: _theme(context).numberTextDefault, + fontWeight: FontWeight.w400, + fontSize: 26, + ); + } + } + + static TextStyle datePicker400(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + letterSpacing: 0.5, + color: _theme(context).accentColorDark, + fontWeight: FontWeight.w400, + fontSize: 12, + ); + case ThemeType.dark: + return GoogleFonts.inter( + letterSpacing: 0.5, + color: _theme(context).accentColorDark, + fontWeight: FontWeight.w400, + fontSize: 12, + ); + } + } + + static TextStyle datePicker600(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + letterSpacing: 0.5, + color: _theme(context).accentColorDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); + case ThemeType.dark: + return GoogleFonts.inter( + letterSpacing: 0.5, + color: _theme(context).accentColorDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); + } + } } diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index d30499f81..361d922dc 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -174,6 +174,7 @@ abstract class StackColorTheme { Color get loadingOverlayTextColor; Color get myStackContactIconBG; Color get textConfirmTotalAmount; + Color get textSelectedWordTableItem; } class CoinThemeColor { @@ -193,9 +194,9 @@ class CoinThemeColor { case Coin.bitcoin: case Coin.bitcoinTestNet: return bitcoin; - // case Coin.bitcoincash: - // case Coin.bitcoincashTestnet: - // return bitcoincash; + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + return bitcoincash; case Coin.dogecoin: case Coin.dogecoinTestNet: return dogecoin; @@ -208,8 +209,8 @@ class CoinThemeColor { return monero; case Coin.namecoin: return namecoin; - // case Coin.wownero: - // return wownero; + case Coin.wownero: + return wownero; } } } diff --git a/lib/utilities/theme/dark_colors.dart b/lib/utilities/theme/dark_colors.dart index b28880da8..e7c4e51db 100644 --- a/lib/utilities/theme/dark_colors.dart +++ b/lib/utilities/theme/dark_colors.dart @@ -301,4 +301,6 @@ class DarkColors extends StackColorTheme { Color get myStackContactIconBG => const Color(0x88747778); @override Color get textConfirmTotalAmount => const Color(0xFF003921); + @override + Color get textSelectedWordTableItem => const Color(0xFF00297A); } diff --git a/lib/utilities/theme/light_colors.dart b/lib/utilities/theme/light_colors.dart index 4c7ac7ab4..ea3a7cb92 100644 --- a/lib/utilities/theme/light_colors.dart +++ b/lib/utilities/theme/light_colors.dart @@ -301,4 +301,6 @@ class LightColors extends StackColorTheme { Color get myStackContactIconBG => textFieldDefaultBG; @override Color get textConfirmTotalAmount => const Color(0xFF232323); + @override + Color get textSelectedWordTableItem => const Color(0xFF232323); } diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 16cc70c93..c6ee28892 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -169,6 +169,7 @@ class StackColors extends ThemeExtension { final Color loadingOverlayTextColor; final Color myStackContactIconBG; final Color textConfirmTotalAmount; + final Color textSelectedWordTableItem; StackColors({ required this.themeType, @@ -300,6 +301,7 @@ class StackColors extends ThemeExtension { required this.loadingOverlayTextColor, required this.myStackContactIconBG, required this.textConfirmTotalAmount, + required this.textSelectedWordTableItem, }); factory StackColors.fromStackColorTheme(StackColorTheme colorTheme) { @@ -435,6 +437,7 @@ class StackColors extends ThemeExtension { loadingOverlayTextColor: colorTheme.loadingOverlayTextColor, myStackContactIconBG: colorTheme.myStackContactIconBG, textConfirmTotalAmount: colorTheme.textConfirmTotalAmount, + textSelectedWordTableItem: colorTheme.textSelectedWordTableItem, ); } @@ -569,6 +572,7 @@ class StackColors extends ThemeExtension { Color? loadingOverlayTextColor, Color? myStackContactIconBG, Color? textConfirmTotalAmount, + Color? textSelectedWordTableItem, }) { return StackColors( themeType: themeType ?? this.themeType, @@ -736,6 +740,8 @@ class StackColors extends ThemeExtension { myStackContactIconBG: myStackContactIconBG ?? this.myStackContactIconBG, textConfirmTotalAmount: textConfirmTotalAmount ?? this.textConfirmTotalAmount, + textSelectedWordTableItem: + textSelectedWordTableItem ?? this.textSelectedWordTableItem, ); } @@ -1388,6 +1394,11 @@ class StackColors extends ThemeExtension { other.textConfirmTotalAmount, t, )!, + textSelectedWordTableItem: Color.lerp( + textSelectedWordTableItem, + other.textSelectedWordTableItem, + t, + )!, ); } @@ -1396,9 +1407,9 @@ class StackColors extends ThemeExtension { case Coin.bitcoin: case Coin.bitcoinTestNet: return _coin.bitcoin; - // case Coin.bitcoincash: - // case Coin.bitcoincashTestnet: - // return _coin.bitcoincash; + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + return _coin.bitcoincash; case Coin.dogecoin: case Coin.dogecoinTestNet: return _coin.dogecoin; @@ -1411,8 +1422,8 @@ class StackColors extends ThemeExtension { return _coin.monero; case Coin.namecoin: return _coin.namecoin; - // case Coin.wownero: - // return wownero; + case Coin.wownero: + return _coin.wownero; } } @@ -1463,6 +1474,13 @@ class StackColors extends ThemeExtension { ), ); + ButtonStyle? getSecondaryDisabledButtonColor(BuildContext context) => + Theme.of(context).textButtonTheme.style?.copyWith( + backgroundColor: MaterialStateProperty.all( + buttonBackSecondaryDisabled, + ), + ); + ButtonStyle? getSmallSecondaryEnabledButtonColor(BuildContext context) => Theme.of(context).textButtonTheme.style?.copyWith( backgroundColor: MaterialStateProperty.all( diff --git a/lib/widgets/custom_buttons/app_bar_icon_button.dart b/lib/widgets/custom_buttons/app_bar_icon_button.dart index c3dd77629..eb926112a 100644 --- a/lib/widgets/custom_buttons/app_bar_icon_button.dart +++ b/lib/widgets/custom_buttons/app_bar_icon_button.dart @@ -47,9 +47,14 @@ class AppBarIconButton extends StatelessWidget { } class AppBarBackButton extends StatelessWidget { - const AppBarBackButton({Key? key, this.onPressed}) : super(key: key); + const AppBarBackButton({ + Key? key, + this.onPressed, + this.isCompact = false, + }) : super(key: key); final VoidCallback? onPressed; + final bool isCompact; @override Widget build(BuildContext context) { @@ -62,15 +67,19 @@ class AppBarBackButton extends StatelessWidget { ) : const EdgeInsets.all(10), child: AppBarIconButton( - size: isDesktop ? 56 : 32, + size: isDesktop + ? isCompact + ? 42 + : 56 + : 32, color: isDesktop ? Theme.of(context).extension()!.textFieldDefaultBG : Theme.of(context).extension()!.background, shadows: const [], icon: SvgPicture.asset( Assets.svg.arrowLeft, - width: 24, - height: 24, + width: isCompact ? 18 : 24, + height: isCompact ? 18 : 24, color: Theme.of(context).extension()!.topNavIconPrimary, ), onPressed: onPressed ?? Navigator.of(context).pop, diff --git a/lib/widgets/custom_pin_put/pin_keyboard.dart b/lib/widgets/custom_pin_put/pin_keyboard.dart index 6c1f50d78..1dcac4d4c 100644 --- a/lib/widgets/custom_pin_put/pin_keyboard.dart +++ b/lib/widgets/custom_pin_put/pin_keyboard.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; class NumberKey extends StatefulWidget { @@ -72,12 +72,7 @@ class _NumberKeyState extends State { child: Center( child: Text( number, - style: GoogleFonts.roboto( - color: - Theme.of(context).extension()!.numberTextDefault, - fontWeight: FontWeight.w400, - fontSize: 26, - ), + style: STextStyles.numberDefault(context), ), ), ), diff --git a/lib/widgets/desktop/custom_text_button.dart b/lib/widgets/desktop/custom_text_button.dart new file mode 100644 index 000000000..b96a697b8 --- /dev/null +++ b/lib/widgets/desktop/custom_text_button.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/util.dart'; + +class CustomTextButtonBase extends StatelessWidget { + const CustomTextButtonBase({ + Key? key, + this.width, + this.height, + this.textButton, + }) : super(key: key); + + final double? width; + final double? height; + final TextButton? textButton; + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + return SizedBox( + height: isDesktop && height == null ? 70 : height, + width: width, + child: textButton, + ); + } +} diff --git a/lib/widgets/desktop/desktop_dialog.dart b/lib/widgets/desktop/desktop_dialog.dart new file mode 100644 index 000000000..5ada3a545 --- /dev/null +++ b/lib/widgets/desktop/desktop_dialog.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class DesktopDialog extends StatelessWidget { + const DesktopDialog({Key? key, this.child}) : super(key: key); + + final Widget? child; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 641, + maxHeight: 474, + ), + child: Material( + borderRadius: BorderRadius.circular( + 20, + ), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + ), + child: child, + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/desktop/desktop_dialog_close_button.dart b/lib/widgets/desktop/desktop_dialog_close_button.dart new file mode 100644 index 000000000..da992d5f6 --- /dev/null +++ b/lib/widgets/desktop/desktop_dialog_close_button.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; + +class DesktopDialogCloseButton extends StatelessWidget { + const DesktopDialogCloseButton({ + Key? key, + this.onPressedOverride, + }) : super(key: key); + + final VoidCallback? onPressedOverride; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AppBarIconButton( + color: + Theme.of(context).extension()!.textFieldDefaultBG, + size: 40, + icon: SvgPicture.asset( + Assets.svg.x, + color: Theme.of(context).extension()!.textDark, + width: 22, + height: 22, + ), + onPressed: () { + if (onPressedOverride == null) { + Navigator.of(context).pop(); + } else { + onPressedOverride!.call(); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/widgets/desktop/primary_button.dart b/lib/widgets/desktop/primary_button.dart new file mode 100644 index 000000000..6034cc08b --- /dev/null +++ b/lib/widgets/desktop/primary_button.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/custom_text_button.dart'; + +class PrimaryButton extends StatelessWidget { + const PrimaryButton({ + Key? key, + this.width, + this.height, + this.label, + this.icon, + this.onPressed, + this.enabled = true, + }) : super(key: key); + + final double? width; + final double? height; + final String? label; + final VoidCallback? onPressed; + final bool enabled; + final Widget? icon; + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + return CustomTextButtonBase( + height: height, + width: width, + textButton: TextButton( + onPressed: enabled ? onPressed : null, + style: enabled + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonColor(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) icon!, + if (icon != null && label != null) + const SizedBox( + width: 10, + ), + if (label != null) + Text( + label!, + style: isDesktop + ? enabled + ? STextStyles.desktopButtonEnabled(context) + : STextStyles.desktopButtonDisabled(context) + : STextStyles.button(context).copyWith( + color: enabled + ? Theme.of(context) + .extension()! + .buttonTextPrimary + : Theme.of(context) + .extension()! + .buttonTextPrimaryDisabled, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/desktop/secondary_button.dart b/lib/widgets/desktop/secondary_button.dart new file mode 100644 index 000000000..2a88e548d --- /dev/null +++ b/lib/widgets/desktop/secondary_button.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/custom_text_button.dart'; + +class SecondaryButton extends StatelessWidget { + const SecondaryButton({ + Key? key, + this.width, + this.height, + this.label, + this.icon, + this.onPressed, + this.enabled = true, + }) : super(key: key); + + final double? width; + final double? height; + final String? label; + final VoidCallback? onPressed; + final bool enabled; + final Widget? icon; + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + return CustomTextButtonBase( + height: height, + width: width, + textButton: TextButton( + onPressed: enabled ? onPressed : null, + style: enabled + ? Theme.of(context) + .extension()! + .getSecondaryEnabledButtonColor(context) + : Theme.of(context) + .extension()! + .getSecondaryDisabledButtonColor(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) icon!, + if (icon != null && label != null) + const SizedBox( + width: 10, + ), + if (label != null) + Text( + label!, + style: isDesktop + ? enabled + ? STextStyles.desktopButtonSecondaryEnabled(context) + : STextStyles.desktopButtonSecondaryDisabled(context) + : STextStyles.button(context).copyWith( + color: enabled + ? Theme.of(context) + .extension()! + .buttonTextSecondary + : Theme.of(context) + .extension()! + .buttonTextSecondaryDisabled, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/managed_favorite.dart b/lib/widgets/managed_favorite.dart index 3cd8a92d3..5ced849fd 100644 --- a/lib/widgets/managed_favorite.dart +++ b/lib/widgets/managed_favorite.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/favorite_toggle.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -30,8 +31,10 @@ class _ManagedFavoriteCardState extends ConsumerState { .select((value) => value.getManager(widget.walletId))); debugPrint("BUILD: $runtimeType with walletId ${widget.walletId}"); + final isDesktop = Util.isDesktop; + return RoundedWhiteContainer( - padding: const EdgeInsets.all(4.0), + padding: EdgeInsets.all(isDesktop ? 0 : 4.0), child: RawMaterialButton( onPressed: () { final provider = ref @@ -59,7 +62,12 @@ class _ManagedFavoriteCardState extends ConsumerState { ), ), child: Padding( - padding: const EdgeInsets.all(8), + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ) + : const EdgeInsets.all(8), child: Row( children: [ Container( @@ -73,7 +81,7 @@ class _ManagedFavoriteCardState extends ConsumerState { ), ), child: Padding( - padding: const EdgeInsets.all(4), + padding: EdgeInsets.all(isDesktop ? 6 : 4), child: SvgPicture.asset( Assets.svg.iconFor(coin: manager.coin), width: 20, @@ -84,37 +92,79 @@ class _ManagedFavoriteCardState extends ConsumerState { const SizedBox( width: 12, ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), - Text( - "${Format.localizedStringAsFixed( - value: manager.cachedTotalBalance, - locale: ref.watch(localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: 8, - )} ${manager.coin.ticker}", - style: STextStyles.itemSubtitle(context), - ), - ], + if (isDesktop) + Expanded( + child: Row( + children: [ + Expanded( + child: Text( + manager.walletName, + style: STextStyles.titleBold12(context), + ), + ), + Expanded( + child: Text( + "${Format.localizedStringAsFixed( + value: manager.cachedTotalBalance, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale)), + decimalPlaces: 8, + )} ${manager.coin.ticker}", + style: STextStyles.itemSubtitle(context), + ), + ), + Text( + manager.isFavorite + ? "Remove from favorites" + : "Add to favorites", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: manager.isFavorite + ? Theme.of(context) + .extension()! + .accentColorRed + : Theme.of(context) + .extension()! + .buttonTextBorderless, + ), + ) + ], + ), ), - ), - FavoriteToggle( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (!isDesktop) + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + manager.walletName, + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 2, + ), + Text( + "${Format.localizedStringAsFixed( + value: manager.cachedTotalBalance, + locale: ref.watch(localeServiceChangeNotifierProvider + .select((value) => value.locale)), + decimalPlaces: 8, + )} ${manager.coin.ticker}", + style: STextStyles.itemSubtitle(context), + ), + ], + ), + ), + if (!isDesktop) + FavoriteToggle( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + initialState: manager.isFavorite, + onChanged: null, ), - initialState: manager.isFavorite, - onChanged: null, - ), ], ), ), diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 1ad4941c5..58a5bf30a 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -85,6 +85,7 @@ class NodeOptionsSheet extends ConsumerWidget { break; case Coin.monero: + case Coin.wownero: try { final uri = Uri.parse(node.host); if (uri.scheme.startsWith("http")) { @@ -106,9 +107,9 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: - // case Coin.bitcoincash: + case Coin.bitcoincash: case Coin.namecoin: - // case Coin.bitcoincashTestnet: + case Coin.bitcoincashTestnet: final client = ElectrumX( host: node.host, port: node.port, diff --git a/lib/widgets/table_view/table_view.dart b/lib/widgets/table_view/table_view.dart index 74103fe04..7e8693f0d 100644 --- a/lib/widgets/table_view/table_view.dart +++ b/lib/widgets/table_view/table_view.dart @@ -6,10 +6,12 @@ class TableView extends StatefulWidget { Key? key, required this.rows, this.rowSpacing = 10.0, + this.shrinkWrap = false, }) : super(key: key); final List rows; final double rowSpacing; + final bool shrinkWrap; @override State createState() => _TableViewState(); @@ -19,6 +21,7 @@ class _TableViewState extends State { @override Widget build(BuildContext context) { return ListView( + shrinkWrap: widget.shrinkWrap, children: [ for (int i = 0; i < widget.rows.length; i++) Column( diff --git a/lib/widgets/table_view/table_view_row.dart b/lib/widgets/table_view/table_view_row.dart index e20a23e94..e95eb68bd 100644 --- a/lib/widgets/table_view/table_view_row.dart +++ b/lib/widgets/table_view/table_view_row.dart @@ -11,6 +11,8 @@ class TableViewRow extends StatelessWidget { this.decoration, this.onExpandChanged, this.padding = const EdgeInsets.all(0), + this.spacing = 0.0, + this.crossAxisAlignment = CrossAxisAlignment.center, }) : super(key: key); final List cells; @@ -18,6 +20,8 @@ class TableViewRow extends StatelessWidget { final Decoration? decoration; final void Function(ExpandableState)? onExpandChanged; final EdgeInsetsGeometry padding; + final double spacing; + final CrossAxisAlignment crossAxisAlignment; @override Widget build(BuildContext context) { @@ -27,13 +31,18 @@ class TableViewRow extends StatelessWidget { ? Padding( padding: padding, child: Row( + crossAxisAlignment: crossAxisAlignment, children: [ - ...cells.map( - (e) => Expanded( - flex: e.flex, - child: e, + for (int i = 0; i < cells.length; i++) ...[ + if (i != 0 && i != cells.length) + SizedBox( + width: spacing, + ), + Expanded( + flex: cells[i].flex, + child: cells[i], ), - ), + ], ], ), ) @@ -43,12 +52,16 @@ class TableViewRow extends StatelessWidget { padding: padding, child: Row( children: [ - ...cells.map( - (e) => Expanded( - flex: e.flex, - child: e, + for (int i = 0; i < cells.length; i++) ...[ + if (i != 0 && i != cells.length) + SizedBox( + width: spacing, + ), + Expanded( + flex: cells[i].flex, + child: cells[i], ), - ), + ], ], ), ), diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index 4a8da2357..b2feaca7a 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -172,7 +172,9 @@ class _TransactionCardState extends ConsumerState { builder: (_) { final amount = coin == Coin.monero ? (_transaction.amount ~/ 10000) - : _transaction.amount; + : coin == Coin.wownero + ? (_transaction.amount ~/ 1000) + : _transaction.amount; return Text( "${Format.satoshiAmountToPrettyString(amount, locale)} ${coin.ticker}", style: @@ -212,6 +214,8 @@ class _TransactionCardState extends ConsumerState { int value = _transaction.amount; if (coin == Coin.monero) { value = (value ~/ 10000); + } else if (coin == Coin.wownero) { + value = (value ~/ 1000); } return Text( diff --git a/pubspec.lock b/pubspec.lock index 71145aef4..2cc67f611 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -317,6 +317,13 @@ packages: relative: true source: path version: "0.0.1" + cw_wownero: + dependency: "direct main" + description: + path: "crypto_plugins/flutter_libmonero/cw_wownero" + relative: true + source: path + version: "0.0.1" dart_numerics: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 26adc1a73..dbe44c852 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.4.49+64 +version: 1.4.52+68 environment: sdk: ">=2.17.0 <3.0.0" @@ -31,6 +31,9 @@ dependencies: cw_monero: path: ./crypto_plugins/flutter_libmonero/cw_monero + cw_wownero: + path: ./crypto_plugins/flutter_libmonero/cw_wownero + cw_core: path: ./crypto_plugins/flutter_libmonero/cw_core @@ -189,6 +192,7 @@ flutter: - assets/svg/clipboard.svg - assets/images/stack.png - assets/images/monero.png + - assets/images/wownero.png - assets/images/firo.png - assets/images/doge.png - assets/images/bitcoin.png @@ -292,6 +296,7 @@ flutter: - assets/svg/coin_icons/EpicCash.svg - assets/svg/coin_icons/Firo.svg - assets/svg/coin_icons/Monero.svg + - assets/svg/coin_icons/Wownero.svg - assets/svg/coin_icons/Namecoin.svg # lottie animations - assets/lottie/test.json @@ -306,6 +311,7 @@ flutter: - assets/svg/wallet-fa.svg - assets/svg/exchange-3.svg - assets/svg/message-question-1.svg + - assets/svg/drd-icon.svg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. diff --git a/test/models/transactions_model_test.dart b/test/models/transactions_model_test.dart index 31df03a5e..1e19f769a 100644 --- a/test/models/transactions_model_test.dart +++ b/test/models/transactions_model_test.dart @@ -1,7 +1,152 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stackwallet/models/models.dart'; +import '../services/coins/firo/sample_data/transaction_data_samples.dart'; + void main() { + group("TransactionData", () { + test("TransactionData from Json", () { + final txChunk = TransactionChunk.fromJson({ + "timestamp": 993260735, + "transactions": [ + { + "txid": "txid", + "confirmed_status": true, + "timestamp": 1876352482, + "txType": "txType", + "amount": 10, + "worthNow": "1", + "worthAtBlockTimestamp": "1", + "fees": 1, + "inputSize": 1, + "outputSize": 1, + "inputs": [], + "outputs": [], + "address": "address", + "height": 1, + "confirmations": 1, + "aliens": [], + "subType": "mint", + "isCancelled": false, + "slateId": "slateId", + "otherData": "otherData", + } + ] + }); + final txdata = + TransactionData.fromJson({"dateTimeChunks": [], "txChunks": []}); + txdata.findTransaction("txid"); + txdata.getAllTransactions(); + }); + }); + + group("Timestamp", () { + test("Timestamp is now", () { + final date = extractDateFromTimestamp(0); + }); + + test("Timestamp is null", () { + final date = extractDateFromTimestamp(null); + }); + + test("Timestamp is a random date", () { + final date = extractDateFromTimestamp(1876352482); + }); + }); + + group("Transaction", () { + test("Transaction from Json", () { + final tx = Transaction.fromJson({ + "txid": "txid", + "confirmed_status": true, + "timestamp": 1876352482, + "txType": "txType", + "amount": 10, + "worthNow": "1", + "worthAtBlockTimestamp": "1", + "fees": 1, + "inputSize": 1, + "outputSize": 1, + "inputs": [], + "outputs": [], + "address": "address", + "height": 1, + "confirmations": 1, + "aliens": [], + "subType": "mint", + "isCancelled": false, + "slateId": "slateId", + "otherData": "otherData", + }); + }); + + test("Transaction from Lelantus Json", () { + final tx = Transaction.fromLelantusJson({ + "txid": "txid", + "confirmed_status": true, + "timestamp": 1876352482, + "txType": "txType", + "amount": 10, + "worthNow": "1", + "worthAtBlockTimestamp": "1", + "fees": 1, + "inputSize": 1, + "outputSize": 1, + "inputs": [], + "outputs": [], + "address": "address", + "height": 1, + "confirmations": 1, + "aliens": [], + "subType": "mint", + "isCancelled": false, + "slateId": "slateId", + "otherData": "otherData", + }); + }); + + test("TransactionChunk", () { + final transactionchunk = TransactionChunk.fromJson({ + "timestamp": 45920, + "transactions": [], + }); + expect( + transactionchunk.toString(), "timestamp: 45920 transactions: [\n]"); + }); + + test("TransactionChunk with a transaction", () { + final txChunk = TransactionChunk.fromJson({ + "timestamp": 993260735, + "transactions": [ + { + "txid": "txid", + "confirmed_status": true, + "timestamp": 1876352482, + "txType": "txType", + "amount": 10, + "worthNow": "1", + "worthAtBlockTimestamp": "1", + "fees": 1, + "inputSize": 1, + "outputSize": 1, + "inputs": [], + "outputs": [], + "address": "address", + "height": 1, + "confirmations": 1, + "aliens": [], + "subType": "mint", + "isCancelled": false, + "slateId": "slateId", + "otherData": "otherData", + } + ] + }); + expect(txChunk.toString(), + "timestamp: 993260735 transactions: [\n {txid: txid, type: txType, subType: mint, value: 10, fee: 1, height: 1, confirm: true, confirmations: 1, address: address, timestamp: 1876352482, worthNow: 1, inputs: [], slateid: slateId } \n]"); + }); + }); + group("Transaction isMinting", () { test("Transaction isMinting unconfirmed mint", () { final tx = Transaction( @@ -94,4 +239,57 @@ void main() { expect(tx1 == tx2, false); expect(tx2.toString(), tx1.toString()); }); + + group("Input", () { + test("Input.toString", () { + final input = Input( + txid: "txid", + vout: 1, + prevout: null, + scriptsig: "scriptsig", + scriptsigAsm: "scriptsigAsm", + witness: [], + isCoinbase: false, + sequence: 1, + innerRedeemscriptAsm: "innerRedeemscriptAsm", + ); //Input + + expect(input.toString(), "{txid: txid}"); + }); + + test("Input.toString", () { + final input = Input.fromJson({ + "txid": "txid", + "vout": 1, + "prevout": null, + "scriptSig": {"hex": "somehexString", "asm": "someasmthing"}, + "scriptsigAsm": "scriptsigAsm", + "witness": [], + "isCoinbase": false, + "sequence": 1, + "innerRedeemscriptAsm": "innerRedeemscriptAsm", + }); //Input + + expect(input.toString(), "{txid: txid}"); + }); + }); + + group("Output", () { + test("Output.toString", () { + final output = Output.fromJson({ + "scriptPubKey": { + "hex": "somehexSting", + "asm": "someasmthing", + "type": "sometype", + "addresses": "someaddresses", + }, + "scriptpubkeyAsm": "scriptpubkeyAsm", + "scriptpubkeyType": "scriptpubkeyType", + "scriptpubkeyAddress": "address", + "value": 2, + }); //Input + + expect(output.toString(), "Instance of \'Output\'"); + }); + }); } diff --git a/test/price_test.dart b/test/price_test.dart index b700d36be..b806bfd61 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -23,8 +23,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"), - // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -37,12 +36,10 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); - // '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"), - // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -53,8 +50,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"), - // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -72,14 +68,12 @@ void main() { await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(cachedPrice.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); - // '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); // verify only called once during filling of cache verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"), - // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -90,7 +84,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -103,8 +97,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); - // '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); test("no internet available", () async { @@ -112,8 +105,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"), - // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenThrow(const SocketException( @@ -124,10 +116,8 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); - expect( - price.toString(), - // '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); - '{Coin.bitcoin: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + expect(price.toString(), + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); tearDown(() async { diff --git a/test/services/coins/bitcoincash/bitcoincash_history_sample_data.dart b/test/services/coins/bitcoincash/bitcoincash_history_sample_data.dart index 42312585e..22f8ccf77 100644 --- a/test/services/coins/bitcoincash/bitcoincash_history_sample_data.dart +++ b/test/services/coins/bitcoincash/bitcoincash_history_sample_data.dart @@ -32,6 +32,40 @@ final Map> historyBatchArgs1 = { "k_0_11": ["9d0163f011c1259568c188c4770606b25c823f8b76bbd262c1c7f3095ed24620"] }; +final Map> historyBatchArgs2 = { + "k_0_0": ["e47feb2ab9db7424d044dc5b2f29404ec35682dc7ea2100acab4b85eb15b8a84"], + "k_0_1": ["ff7f0d2a4b8e2805706ece77f4e672550fe4c505a150c781639814338eda1734"], + "k_0_2": ["68c84a2f0e8b52769e22270f9cf26bc1e3ffed55fe368e94c5d761403b86a7c5"], + "k_0_3": ["7ec0c8c9b961e481a3fd05b997997929a36c174857ba4f06e8ba55b5a29670fd"], + "k_0_4": ["42adc3dee3f1c8bc25384ab5254a75af64fc94b6e29d3ae6c0ccfb50c7910c88"], + "k_0_5": ["d3e419ee2b5ef5969a76d3e5ae14a15fe2dfa26ac434de95b0616416e4dc8955"], + "k_0_6": ["0cf8f653ae181c1a33d94787f7a34c0a95bd0465003488c1a6e7f81b87063573"], + "k_0_7": ["8c8e647009329da2c508aa08b2b28b2f83932efca4ed5a6c1b29ccec37fda9c1"], + "k_0_8": ["30c063757244ac50049d2604c7f9d671e9cbac53f4e76a9c1147112934336057"], + "k_0_9": ["2440d59c1dc1fd6dde77f52d8cf4f8f799005f53e8027f10828ef1eee739694e"], + "k_0_10": [ + "1a2e9fd10dc64048e1bf86c58c37611920facf486e80a36a60e20eb3496c3aad" + ], + "k_0_11": ["052e9116071688691bf12f2db3154f72562d957cf058bce2b88a144d67968da0"] +}; + +final Map> historyBatchArgs3 = { + "k_0_0": ["4694828a4841338b5921e10f9d97a9c6d2a4ff50593be21572391fefbd2179be"], + "k_0_1": ["1c2336c32dc62f00862ee6a75643e01017c86edece10b5a9d7defbd5f66b0a80"], + "k_0_2": ["e96b7f9655acc58d58ffc322626c9742f3941fef790e3d2836d5db74d3708427"], + "k_0_3": ["abc8d1cf0dc8f66678eb0d322eb4f907cb2552cde74ace5201c4978787db91e4"], + "k_0_4": ["421b02221022dd34081669fd21e7a9a83f8761e068f48b5291e0accf9d1b5867"], + "k_0_5": ["1233abcc5848b1bcf6a561984f1f0596d270ef47320c281a5abf7a2098dd5902"], + "k_0_6": ["963f002eddfb35100830f416c97605947a9df7ea885554923d6427b79d519079"], + "k_0_7": ["8b511de883d14c0f40b8eed22c23432b3d9c97ce521c4ad94338189065ae3e94"], + "k_0_8": ["954d176b775a925fcbceb36b717114b0f01af6ac082d89971379e789a4bba613"], + "k_0_9": ["3c8c79404dba45b8228547a5b3da306e1cdc3438e02f9e6ec1c88ba2fb6ef6df"], + "k_0_10": [ + "a253190f30e26c482d3864d675da927a9df20c52172826ea630ca3f600da4642" + ], + "k_0_11": ["bd39e800a3822ebaa5e33938b29cff5fcf867def7c0ac6d65af350b737357f65"] +}; + final Map>> historyBatchResponse = { "k_0_0": [], "s_0_0": [{}, {}], @@ -117,4 +151,8 @@ final List activeScriptHashes = [ "26f92666caebb9a17b14f5b573b385348cdc80065472b8961091f3226d2f650f", "2f18558e5d3015cb6578aee1c3e4b645725fa4e1d26ce22cb31c9949f3b4957c", "bf5a6c56814e80eed11e1e459801515f8c2b83da812568aa9dc26e6356f6965b", + "f0c86f888f2aca0efaf1705247dbd1ebc02347c183e197310c9062ea2c9d2e34", + "ff7f0d2a4b8e2805706ece77f4e672550fe4c505a150c781639814338eda1734", + "04818da846fe5e03ac993d2e0c1ccc3848ff6073c3aba6a572df4efc5432ae8b", + "1c2336c32dc62f00862ee6a75643e01017c86edece10b5a9d7defbd5f66b0a80", ]; diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart index 703876280..c373b739a 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart @@ -1,3 +1,5 @@ +// import 'dart:convert'; +// // import 'package:bitcoindart/bitcoindart.dart'; // import 'package:decimal/decimal.dart'; // import 'package:flutter_test/flutter_test.dart'; @@ -43,8 +45,9 @@ void main() {} // }); // // test("bitcoincash DerivePathType enum", () { -// expect(DerivePathType.values.length, 1); -// expect(DerivePathType.values.toString(), "[DerivePathType.bip44]"); +// expect(DerivePathType.values.length, 2); +// expect(DerivePathType.values.toString(), +// "[DerivePathType.bip44, DerivePathType.bip49]"); // }); // // group("bip32 node/root", () { @@ -1270,13 +1273,13 @@ void main() {} // } // expect(didThrow, false); // -// verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); +// verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(2); // verify(client?.getServerFeatures()).called(1); // verifyNever(client?.ping()).called(0); // -// expect(secureStore?.interactions, 11); -// expect(secureStore?.reads, 7); -// expect(secureStore?.writes, 4); +// expect(secureStore?.interactions, 20); +// expect(secureStore?.reads, 13); +// expect(secureStore?.writes, 7); // expect(secureStore?.deletes, 0); // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); @@ -1316,9 +1319,9 @@ void main() {} // verify(client?.getServerFeatures()).called(1); // verifyNever(client?.ping()).called(0); // -// expect(secureStore?.interactions, 8); -// expect(secureStore?.reads, 5); -// expect(secureStore?.writes, 3); +// expect(secureStore?.interactions, 14); +// expect(secureStore?.reads, 9); +// expect(secureStore?.writes, 5); // expect(secureStore?.deletes, 0); // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); @@ -1365,13 +1368,13 @@ void main() {} // } // expect(didThrow, false); // -// verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); +// verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(2); // verify(client?.getServerFeatures()).called(1); // verifyNever(client?.ping()).called(0); // -// expect(secureStore?.interactions, 11); -// expect(secureStore?.reads, 7); -// expect(secureStore?.writes, 4); +// expect(secureStore?.interactions, 20); +// expect(secureStore?.reads, 13); +// expect(secureStore?.writes, 7); // expect(secureStore?.deletes, 0); // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); @@ -1412,9 +1415,9 @@ void main() {} // verify(client?.getServerFeatures()).called(1); // verifyNever(client?.ping()).called(0); // -// expect(secureStore?.interactions, 8); -// expect(secureStore?.reads, 5); -// expect(secureStore?.writes, 3); +// expect(secureStore?.interactions, 14); +// expect(secureStore?.reads, 9); +// expect(secureStore?.writes, 5); // expect(secureStore?.deletes, 0); // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); @@ -1829,6 +1832,10 @@ void main() {} // .thenAnswer((_) async => emptyHistoryBatchResponse); // when(client?.getBatchHistory(args: historyBatchArgs1)) // .thenAnswer((_) async => emptyHistoryBatchResponse); +// when(client?.getBatchHistory(args: historyBatchArgs2)) +// .thenAnswer((_) async => emptyHistoryBatchResponse); +// when(client?.getBatchHistory(args: historyBatchArgs3)) +// .thenAnswer((_) async => emptyHistoryBatchResponse); // // final wallet = await Hive.openBox(testWalletId); // @@ -1844,6 +1851,8 @@ void main() {} // verify(client?.getServerFeatures()).called(1); // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); +// verify(client?.getBatchHistory(args: historyBatchArgs2)).called(1); +// verify(client?.getBatchHistory(args: historyBatchArgs3)).called(1); // // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); @@ -1982,8 +1991,24 @@ void main() {} // .thenAnswer((_) async => historyBatchResponse); // when(client?.getBatchHistory(args: historyBatchArgs1)) // .thenAnswer((_) async => historyBatchResponse); +// when(client?.getBatchHistory(args: historyBatchArgs2)) +// .thenAnswer((_) async => historyBatchResponse); +// when(client?.getBatchHistory(args: historyBatchArgs3)) +// .thenAnswer((_) async => historyBatchResponse); // -// final wallet = await Hive.openBox(testWalletId); +// List dynamicArgValues = []; +// +// when(client?.getBatchHistory(args: anyNamed("args"))) +// .thenAnswer((realInvocation) async { +// if (realInvocation.namedArguments.values.first.length == 1) { +// dynamicArgValues.add(realInvocation.namedArguments.values.first); +// } +// +// return historyBatchResponse; +// }); +// +// // final wallet = await Hive.openBox(testWalletId); +// await Hive.openBox(testWalletId); // // bool hasThrown = false; // try { @@ -2000,10 +2025,20 @@ void main() {} // verify(client?.getServerFeatures()).called(1); // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); +// verify(client?.getBatchHistory(args: historyBatchArgs2)).called(1); +// verify(client?.getBatchHistory(args: historyBatchArgs3)).called(1); // -// expect(secureStore?.interactions, 6); -// expect(secureStore?.writes, 3); -// expect(secureStore?.reads, 3); +// for (final arg in dynamicArgValues) { +// final map = Map>.from(arg as Map); +// +// verify(client?.getBatchHistory(args: map)).called(1); +// expect(activeScriptHashes.contains(map.values.first.first as String), +// true); +// } +// +// expect(secureStore?.interactions, 10); +// expect(secureStore?.writes, 5); +// expect(secureStore?.reads, 5); // expect(secureStore?.deletes, 0); // // verifyNoMoreInteractions(client); @@ -2027,10 +2062,34 @@ void main() {} // .thenAnswer((_) async => historyBatchResponse); // when(client?.getBatchHistory(args: historyBatchArgs1)) // .thenAnswer((_) async => historyBatchResponse); -// +// when(client?.getBatchHistory(args: historyBatchArgs2)) +// .thenAnswer((_) async => historyBatchResponse); +// when(client?.getBatchHistory(args: historyBatchArgs3)) +// .thenAnswer((_) async => historyBatchResponse); // when(cachedClient?.clearSharedTransactionCache(coin: Coin.bitcoincash)) // .thenAnswer((realInvocation) async {}); // +// when(client?.getBatchHistory(args: { +// "0": [ +// "04818da846fe5e03ac993d2e0c1ccc3848ff6073c3aba6a572df4efc5432ae8b" +// ] +// })).thenAnswer((_) async => {"0": []}); +// when(client?.getBatchHistory(args: { +// "0": [ +// "f0c86f888f2aca0efaf1705247dbd1ebc02347c183e197310c9062ea2c9d2e34" +// ] +// })).thenAnswer((_) async => {"0": []}); +// when(client?.getBatchHistory(args: { +// "0": [ +// "ff7f0d2a4b8e2805706ece77f4e672550fe4c505a150c781639814338eda1734" +// ] +// })).thenAnswer((_) async => {"0": []}); +// when(client?.getBatchHistory(args: { +// "0": [ +// "1c2336c32dc62f00862ee6a75643e01017c86edece10b5a9d7defbd5f66b0a80" +// ] +// })).thenAnswer((_) async => {"0": []}); +// // final wallet = await Hive.openBox(testWalletId); // // // restore so we have something to rescan @@ -2046,25 +2105,51 @@ void main() {} // final preChangeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); // final preReceivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); // final preChangeIndexP2PKH = await wallet.get('changeIndexP2PKH'); +// +// final preReceivingAddressesP2SH = +// await wallet.get('receivingAddressesP2SH'); +// final preChangeAddressesP2SH = await wallet.get('changeAddressesP2SH'); +// final preReceivingIndexP2SH = await wallet.get('receivingIndexP2PKH'); +// final preChangeIndexP2SH = await wallet.get('changeIndexP2SH'); +// // final preUtxoData = await wallet.get('latest_utxo_model'); // final preReceiveDerivationsStringP2PKH = await secureStore?.read( // key: "${testWalletId}_receiveDerivationsP2PKH"); // final preChangeDerivationsStringP2PKH = await secureStore?.read( // key: "${testWalletId}_changeDerivationsP2PKH"); // +// final preReceiveDerivationsStringP2SH = await secureStore?.read( +// key: "${testWalletId}_receiveDerivationsP2SH"); +// final preChangeDerivationsStringP2SH = +// await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); +// // // destroy the data that the rescan will fix // await wallet.put( // 'receivingAddressesP2PKH', ["some address", "some other address"]); // await wallet // .put('changeAddressesP2PKH', ["some address", "some other address"]); // +// await wallet.put( +// 'receivingAddressesP2SH', ["some address", "some other address"]); +// await wallet +// .put('changeAddressesP2SH', ["some address", "some other address"]); +// // await wallet.put('receivingIndexP2PKH', 123); // await wallet.put('changeIndexP2PKH', 123); +// +// await wallet.put('receivingIndexP2SH', 123); +// await wallet.put('changeIndexP2SH', 123); +// // await secureStore?.write( // key: "${testWalletId}_receiveDerivationsP2PKH", value: "{}"); // await secureStore?.write( // key: "${testWalletId}_changeDerivationsP2PKH", value: "{}"); // +// await secureStore?.write( +// key: "${testWalletId}_receiveDerivationsP2SH", value: "{}"); +// await secureStore?.write( +// key: "${testWalletId}_changeDerivationsP2SH", value: "{}"); +// // bool hasThrown = false; // try { // await bch?.fullRescan(2, 1000); @@ -2079,29 +2164,73 @@ void main() {} // final changeAddressesP2PKH = await wallet.get('changeAddressesP2PKH'); // final receivingIndexP2PKH = await wallet.get('receivingIndexP2PKH'); // final changeIndexP2PKH = await wallet.get('changeIndexP2PKH'); +// +// final receivingAddressesP2SH = await wallet.get('receivingAddressesP2SH'); +// final changeAddressesP2SH = await wallet.get('changeAddressesP2SH'); +// final receivingIndexP2SH = await wallet.get('receivingIndexP2SH'); +// final changeIndexP2SH = await wallet.get('changeIndexP2SH'); +// // final utxoData = await wallet.get('latest_utxo_model'); // final receiveDerivationsStringP2PKH = await secureStore?.read( // key: "${testWalletId}_receiveDerivationsP2PKH"); // final changeDerivationsStringP2PKH = await secureStore?.read( // key: "${testWalletId}_changeDerivationsP2PKH"); // +// final receiveDerivationsStringP2SH = await secureStore?.read( +// key: "${testWalletId}_receiveDerivationsP2SH"); +// final changeDerivationsStringP2SH = +// await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); +// // expect(preReceivingAddressesP2PKH, receivingAddressesP2PKH); // expect(preChangeAddressesP2PKH, changeAddressesP2PKH); // expect(preReceivingIndexP2PKH, receivingIndexP2PKH); // expect(preChangeIndexP2PKH, changeIndexP2PKH); +// +// expect(preReceivingAddressesP2SH, receivingAddressesP2SH); +// expect(preChangeAddressesP2SH, changeAddressesP2SH); +// expect(preReceivingIndexP2SH, receivingIndexP2SH); +// expect(preChangeIndexP2SH, changeIndexP2SH); +// // expect(preUtxoData, utxoData); +// // expect(preReceiveDerivationsStringP2PKH, receiveDerivationsStringP2PKH); // expect(preChangeDerivationsStringP2PKH, changeDerivationsStringP2PKH); // +// expect(preReceiveDerivationsStringP2SH, receiveDerivationsStringP2SH); +// expect(preChangeDerivationsStringP2SH, changeDerivationsStringP2SH); +// // verify(client?.getServerFeatures()).called(1); // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(2); // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(2); +// verify(client?.getBatchHistory(args: historyBatchArgs2)).called(2); +// verify(client?.getBatchHistory(args: historyBatchArgs3)).called(2); // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.bitcoincash)) // .called(1); // -// expect(secureStore?.writes, 9); -// expect(secureStore?.reads, 12); -// expect(secureStore?.deletes, 2); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "04818da846fe5e03ac993d2e0c1ccc3848ff6073c3aba6a572df4efc5432ae8b" +// ] +// })).called(2); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "f0c86f888f2aca0efaf1705247dbd1ebc02347c183e197310c9062ea2c9d2e34" +// ] +// })).called(2); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "ff7f0d2a4b8e2805706ece77f4e672550fe4c505a150c781639814338eda1734" +// ] +// })).called(2); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "1c2336c32dc62f00862ee6a75643e01017c86edece10b5a9d7defbd5f66b0a80" +// ] +// })).called(2); +// +// expect(secureStore?.writes, 17); +// expect(secureStore?.reads, 22); +// expect(secureStore?.deletes, 4); // // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); @@ -2125,19 +2254,33 @@ void main() {} // .thenAnswer((_) async => historyBatchResponse); // when(client?.getBatchHistory(args: historyBatchArgs1)) // .thenAnswer((_) async => historyBatchResponse); +// when(client?.getBatchHistory(args: historyBatchArgs2)) +// .thenAnswer((_) async => historyBatchResponse); +// when(client?.getBatchHistory(args: historyBatchArgs3)) +// .thenAnswer((_) async => historyBatchResponse); +// when(cachedClient?.clearSharedTransactionCache(coin: Coin.bitcoincash)) +// .thenAnswer((realInvocation) async {}); +// // when(client?.getBatchHistory(args: { // "0": [ // "04818da846fe5e03ac993d2e0c1ccc3848ff6073c3aba6a572df4efc5432ae8b" // ] // })).thenAnswer((realInvocation) async => {"0": []}); -// // when(client?.getBatchHistory(args: { // "0": [ // "f0c86f888f2aca0efaf1705247dbd1ebc02347c183e197310c9062ea2c9d2e34" // ] -// })).thenAnswer((realInvocation) async => {"0": []}); -// when(cachedClient?.clearSharedTransactionCache(coin: Coin.dogecoin)) -// .thenAnswer((realInvocation) async {}); +// })).thenAnswer((_) async => {"0": []}); +// when(client?.getBatchHistory(args: { +// "0": [ +// "ff7f0d2a4b8e2805706ece77f4e672550fe4c505a150c781639814338eda1734" +// ] +// })).thenAnswer((_) async => {"0": []}); +// when(client?.getBatchHistory(args: { +// "0": [ +// "1c2336c32dc62f00862ee6a75643e01017c86edece10b5a9d7defbd5f66b0a80" +// ] +// })).thenAnswer((_) async => {"0": []}); // // final wallet = await Hive.openBox(testWalletId); // @@ -2195,13 +2338,36 @@ void main() {} // // verify(client?.getServerFeatures()).called(1); // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(2); -// verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); +// verify(client?.getBatchHistory(args: historyBatchArgs1)).called(2); +// verify(client?.getBatchHistory(args: historyBatchArgs2)).called(2); +// verify(client?.getBatchHistory(args: historyBatchArgs3)).called(2); // verify(cachedClient?.clearSharedTransactionCache(coin: Coin.bitcoincash)) // .called(1); // -// expect(secureStore?.writes, 7); -// expect(secureStore?.reads, 12); -// expect(secureStore?.deletes, 4); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "04818da846fe5e03ac993d2e0c1ccc3848ff6073c3aba6a572df4efc5432ae8b" +// ] +// })).called(1); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "f0c86f888f2aca0efaf1705247dbd1ebc02347c183e197310c9062ea2c9d2e34" +// ] +// })).called(2); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "ff7f0d2a4b8e2805706ece77f4e672550fe4c505a150c781639814338eda1734" +// ] +// })).called(2); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "1c2336c32dc62f00862ee6a75643e01017c86edece10b5a9d7defbd5f66b0a80" +// ] +// })).called(2); +// +// expect(secureStore?.writes, 13); +// expect(secureStore?.reads, 18); +// expect(secureStore?.deletes, 8); // }); // // // // test("fetchBuildTxData succeeds", () async { @@ -2686,17 +2852,30 @@ void main() {} // .thenAnswer((_) async => historyBatchResponse); // when(client?.getBatchHistory(args: historyBatchArgs1)) // .thenAnswer((_) async => historyBatchResponse); +// when(client?.getBatchHistory(args: historyBatchArgs2)) +// .thenAnswer((_) async => historyBatchResponse); +// when(client?.getBatchHistory(args: historyBatchArgs3)) +// .thenAnswer((_) async => historyBatchResponse); // when(client?.getBatchHistory(args: { // "0": [ // "f0c86f888f2aca0efaf1705247dbd1ebc02347c183e197310c9062ea2c9d2e34" // ] // })).thenAnswer((realInvocation) async => {"0": []}); -// // when(client?.getBatchHistory(args: { // "0": [ // "04818da846fe5e03ac993d2e0c1ccc3848ff6073c3aba6a572df4efc5432ae8b" // ] // })).thenAnswer((realInvocation) async => {"0": []}); +// when(client?.getBatchHistory(args: { +// "0": [ +// "ff7f0d2a4b8e2805706ece77f4e672550fe4c505a150c781639814338eda1734" +// ] +// })).thenAnswer((realInvocation) async => {"0": []}); +// when(client?.getBatchHistory(args: { +// "0": [ +// "1c2336c32dc62f00862ee6a75643e01017c86edece10b5a9d7defbd5f66b0a80" +// ] +// })).thenAnswer((realInvocation) async => {"0": []}); // // final wallet = await Hive.openBox(testWalletId); // // recover to fill data @@ -2713,10 +2892,33 @@ void main() {} // verify(client?.getServerFeatures()).called(1); // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); +// verify(client?.getBatchHistory(args: historyBatchArgs2)).called(1); +// verify(client?.getBatchHistory(args: historyBatchArgs3)).called(1); // -// expect(secureStore?.interactions, 6); -// expect(secureStore?.writes, 3); -// expect(secureStore?.reads, 3); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "f0c86f888f2aca0efaf1705247dbd1ebc02347c183e197310c9062ea2c9d2e34" +// ] +// })).called(1); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "04818da846fe5e03ac993d2e0c1ccc3848ff6073c3aba6a572df4efc5432ae8b" +// ] +// })).called(1); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "ff7f0d2a4b8e2805706ece77f4e672550fe4c505a150c781639814338eda1734" +// ] +// })).called(1); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "1c2336c32dc62f00862ee6a75643e01017c86edece10b5a9d7defbd5f66b0a80" +// ] +// })).called(1); +// +// expect(secureStore?.interactions, 10); +// expect(secureStore?.writes, 5); +// expect(secureStore?.reads, 5); // expect(secureStore?.deletes, 0); // // verifyNoMoreInteractions(client); @@ -2741,17 +2943,32 @@ void main() {} // .thenAnswer((_) async => historyBatchResponse); // when(client?.getBatchHistory(args: historyBatchArgs1)) // .thenAnswer((_) async => historyBatchResponse); -// when(client?.getBatchHistory(args: { -// "0": [ -// "f0c86f888f2aca0efaf1705247dbd1ebc02347c183e197310c9062ea2c9d2e34" -// ] -// })).thenAnswer((realInvocation) async => {"0": []}); +// when(client?.getBatchHistory(args: historyBatchArgs2)) +// .thenAnswer((_) async => historyBatchResponse); +// when(client?.getBatchHistory(args: historyBatchArgs3)) +// .thenAnswer((_) async => historyBatchResponse); // // when(client?.getBatchHistory(args: { // "0": [ // "04818da846fe5e03ac993d2e0c1ccc3848ff6073c3aba6a572df4efc5432ae8b" // ] // })).thenAnswer((realInvocation) async => {"0": []}); +// when(client?.getBatchHistory(args: { +// "0": [ +// "f0c86f888f2aca0efaf1705247dbd1ebc02347c183e197310c9062ea2c9d2e34" +// ] +// })).thenAnswer((realInvocation) async => {"0": []}); +// when(client?.getBatchHistory(args: { +// "0": [ +// "ff7f0d2a4b8e2805706ece77f4e672550fe4c505a150c781639814338eda1734" +// ] +// })).thenAnswer((realInvocation) async => {"0": []}); +// when(client?.getBatchHistory(args: { +// "0": [ +// "1c2336c32dc62f00862ee6a75643e01017c86edece10b5a9d7defbd5f66b0a80" +// ] +// })).thenAnswer((realInvocation) async => {"0": []}); +// // when(client?.getHistory(scripthash: anyNamed("scripthash"))) // .thenThrow(Exception("some exception")); // @@ -2769,12 +2986,36 @@ void main() {} // verify(client?.getServerFeatures()).called(1); // verify(client?.getBatchHistory(args: historyBatchArgs0)).called(1); // verify(client?.getBatchHistory(args: historyBatchArgs1)).called(1); +// verify(client?.getBatchHistory(args: historyBatchArgs2)).called(1); +// verify(client?.getBatchHistory(args: historyBatchArgs3)).called(1); +// +// verify(client?.getBatchHistory(args: { +// "0": [ +// "04818da846fe5e03ac993d2e0c1ccc3848ff6073c3aba6a572df4efc5432ae8b" +// ] +// })).called(1); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "f0c86f888f2aca0efaf1705247dbd1ebc02347c183e197310c9062ea2c9d2e34" +// ] +// })).called(1); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "ff7f0d2a4b8e2805706ece77f4e672550fe4c505a150c781639814338eda1734" +// ] +// })).called(1); +// verify(client?.getBatchHistory(args: { +// "0": [ +// "1c2336c32dc62f00862ee6a75643e01017c86edece10b5a9d7defbd5f66b0a80" +// ] +// })).called(1); +// // verify(client?.getBlockHeadTip()).called(1); // verify(client?.getHistory(scripthash: anyNamed("scripthash"))).called(1); // -// expect(secureStore?.interactions, 6); -// expect(secureStore?.writes, 3); -// expect(secureStore?.reads, 3); +// expect(secureStore?.interactions, 10); +// expect(secureStore?.writes, 5); +// expect(secureStore?.reads, 5); // expect(secureStore?.deletes, 0); // // verifyNoMoreInteractions(client);