diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index 76fa097a4..a474fb7bb 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart @@ -19,6 +19,7 @@ import '../../../../../providers/global/wallets_provider.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../utilities/assets.dart'; import '../../../../../utilities/text_styles.dart'; +import '../../../../../utilities/util.dart'; import '../../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../../wallets/isar/models/wallet_info.dart'; import '../../../../../wallets/isar/providers/wallet_info_provider.dart'; @@ -32,7 +33,10 @@ import '../../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.da import '../../../../../widgets/custom_buttons/draggable_switch_button.dart'; import '../../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../../../widgets/desktop/primary_button.dart'; +import '../../../../../widgets/desktop/secondary_button.dart'; import '../../../../../widgets/rounded_container.dart'; +import '../../../../../widgets/stack_dialog.dart'; class MoreFeaturesDialog extends ConsumerStatefulWidget { const MoreFeaturesDialog({ @@ -102,6 +106,147 @@ class _MoreFeaturesDialogState extends ConsumerState { } } + bool _switchReuseAddressToggledLock = false; // Mutex. + Future _switchReuseAddressToggled(bool newValue) async { + if (newValue) { + await showDialog( + context: context, + builder: (context) { + final isDesktop = Util.isDesktop; + return isDesktop + ? DesktopDialog( + maxWidth: 576, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Warning!", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 8, + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox( + height: 43, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(false); + }, + label: "Cancel", + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(true); + }, + label: "Continue", + ), + ), + ], + ), + ], + ), + ), + ], + ), + ) + : StackDialog( + title: "Warning!", + message: + "Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?", + leftButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + "Continue", + style: STextStyles.button(context), + ), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ); + }, + ).then((confirmed) async { + if (_switchReuseAddressToggledLock) { + return; + } + _switchReuseAddressToggledLock = true; // Lock mutex. + + try { + if (confirmed == true) { + await ref.read(pWalletInfo(widget.walletId)).updateOtherData( + newEntries: { + WalletInfoKeys.reuseAddress: true, + }, + isar: ref.read(mainDBProvider).isar, + ); + } else { + await ref.read(pWalletInfo(widget.walletId)).updateOtherData( + newEntries: { + WalletInfoKeys.reuseAddress: false, + }, + isar: ref.read(mainDBProvider).isar, + ); + } + } finally { + // ensure _switchReuseAddressToggledLock is set to false no matter what. + _switchReuseAddressToggledLock = false; + } + }); + } else { + await ref.read(pWalletInfo(widget.walletId)).updateOtherData( + newEntries: { + WalletInfoKeys.reuseAddress: false, + }, + isar: ref.read(mainDBProvider).isar, + ); + } + } + @override Widget build(BuildContext context) { final wallet = ref.watch( @@ -253,6 +398,38 @@ class _MoreFeaturesDialogState extends ConsumerState { ], ), ), + // reuseAddress preference. + _MoreFeaturesItemBase( + child: Row( + children: [ + const SizedBox(width: 3), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + pWalletInfo(widget.walletId) + .select((value) => value.otherData), + )[WalletInfoKeys.reuseAddress] as bool? ?? + false, + onValueChanged: _switchReuseAddressToggled, + ), + ), + const SizedBox( + width: 16, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Reuse receiving address by default", + style: STextStyles.w600_20(context), + ), + ], + ), + ], + ), + ), const SizedBox( height: 28, ), diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index 6f6e12a42..6a7a9b54c 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -511,4 +511,5 @@ abstract class WalletInfoKeys { static const String firoSparkCacheSetTimestampCache = "firoSparkCacheSetTimestampCacheKey"; static const String enableOptInRbf = "enableOptInRbfKey"; + static const String reuseAddress = "reuseAddressKey"; } diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 33ae3fb24..f9fcb11b5 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -787,7 +787,9 @@ class FiroWallet extends Bip39HDWallet for (final tuple in receiveResults) { if (tuple.addresses.isEmpty) { - await checkReceivingAddressForTransactions(); + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await checkReceivingAddressForTransactions(); + } } else { highestReceivingIndexWithHistory = max( tuple.index, diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 1bca3c858..46540eef7 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -522,8 +522,10 @@ abstract class Wallet { // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. if (this is MultiAddressInterface) { - await (this as MultiAddressInterface) - .checkReceivingAddressForTransactions(); + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await (this as MultiAddressInterface) + .checkReceivingAddressForTransactions(); + } } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart index de1f7a699..661b10bd3 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart @@ -25,6 +25,7 @@ import '../../../utilities/amount/amount.dart'; import '../../../utilities/logger.dart'; import '../../../utilities/stack_file_system.dart'; import '../../crypto_currency/intermediate/cryptonote_currency.dart'; +import '../../isar/models/wallet_info.dart'; import '../intermediate/cryptonote_wallet.dart'; import 'multi_address_interface.dart'; @@ -286,7 +287,9 @@ mixin CwBasedInterface on CryptonoteWallet await updateTransactions(); await updateBalance(); - await checkReceivingAddressForTransactions(); + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await checkReceivingAddressForTransactions(); + } if (cwWalletBase?.syncStatus is SyncedSyncStatus) { refreshMutex.release(); @@ -342,6 +345,17 @@ mixin CwBasedInterface on CryptonoteWallet @override Future checkReceivingAddressForTransactions() async { + if (info.otherData[WalletInfoKeys.reuseAddress] == true) { + try { + throw Exception(); + } catch (_, s) { + Logging.instance.log( + "checkReceivingAddressForTransactions called but reuse address flag set: $s", + level: LogLevel.Error, + ); + } + } + try { int highestIndex = -1; final entries = cwWalletBase?.transactionHistory?.transactions?.entries; @@ -380,8 +394,10 @@ mixin CwBasedInterface on CryptonoteWallet // we need to update the address await mainDB.updateAddress(existing, newReceivingAddress); } - // keep checking until address with no tx history is set as current - await checkReceivingAddressForTransactions(); + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + // keep checking until address with no tx history is set as current + await checkReceivingAddressForTransactions(); + } } } on SocketException catch (se, s) { Logging.instance.log( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 035ae649f..c31f7cd4a 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -23,6 +23,7 @@ import '../../../utilities/logger.dart'; import '../../../utilities/paynym_is_api.dart'; import '../../crypto_currency/coins/firo.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../../isar/models/wallet_info.dart'; import '../../models/tx_data.dart'; import '../impl/bitcoin_wallet.dart'; import '../impl/firo_wallet.dart'; @@ -1315,6 +1316,17 @@ mixin ElectrumXInterface @override Future checkReceivingAddressForTransactions() async { + if (info.otherData[WalletInfoKeys.reuseAddress] == true) { + try { + throw Exception(); + } catch (_, s) { + Logging.instance.log( + "checkReceivingAddressForTransactions called but reuse address flag set: $s", + level: LogLevel.Error, + ); + } + } + try { final currentReceiving = await getCurrentReceivingAddress(); @@ -1336,9 +1348,12 @@ mixin ElectrumXInterface if (needsGenerate) { await generateNewReceivingAddress(); - // TODO: get rid of this? Could cause problems (long loading/infinite loop or something) - // keep checking until address with no tx history is set as current - await checkReceivingAddressForTransactions(); + // TODO: [prio=low] Make sure we scan all addresses but only show one. + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + // TODO: get rid of this? Could cause problems (long loading/infinite loop or something) + // keep checking until address with no tx history is set as current + await checkReceivingAddressForTransactions(); + } } } catch (e, s) { Logging.instance.log(