diff --git a/.github/workflows/analyze-test.yml b/.github/workflows/analyze-test.yml index 6c92a115..022750fd 100644 --- a/.github/workflows/analyze-test.yml +++ b/.github/workflows/analyze-test.yml @@ -15,9 +15,9 @@ jobs: - uses: actions/setup-java@v1 with: java-version: '12.x' - - uses: subosito/flutter-action@v1.5.3 + - uses: subosito/flutter-action@v2 with: - channel: 'stable' + flutter-version: '2.10.0' - run: flutter pub get # run static analys code - run: flutter analyze @@ -31,9 +31,9 @@ jobs: - uses: actions/setup-java@v1 with: java-version: '12.x' - - uses: subosito/flutter-action@v1.5.3 + - uses: subosito/flutter-action@v2 with: - channel: 'stable' + flutter-version: '2.10.0' - name: run tests uses: ReactiveCircus/android-emulator-runner@v2.21.0 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 11acf3ef..ed78f7b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +### **0.9.1** (2022-05-12) +* Improved wallet performance: +From now on the wallet will only watch addresses that it knows to have coins and the unusued address (the one displayed in the "Receive" tab). +You can manually enable watching other addresses in the address book (slide left). +Background notifications only work for watched addresses. +Rescans are not affected. +* Price ticker: show latest price update +* Minor localization improvements + ### **0.9.0** (2022-04-26) * Bug fix: Edge case for importing paper wallets diff --git a/assets/translations/en.json b/assets/translations/en.json index 30c111e6..2c49150d 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -22,6 +22,9 @@ "addressbook_bottom_bar_sending_addresses": "Sending Addresses", "addressbook_dialog_remove_title": "Do you really want to remove this address?", "addressbook_dialog_remove_snack": "Address successfully removed", + "addressbook_dialog_addr_watched": "$address\nis now watched", + "addressbook_dialog_addr_unwatched": "$address\nis no longer watched", + "addressbook_dialog_addr_unwatch_unable": "$address\ncan't be unwatched\n(has balance or is the next change address)", "addressbook_edit_dialog_title": "Edit label", "addressbook_edit_dialog_input": "New label", "addressbook_export_dialog_title": "Export Private Key", @@ -31,6 +34,7 @@ "addressbook_hide_change": "Hide change addresses", "addressbook_hide_empty": "Hide empty", "addressbook_hide_used": "Hide used", + "addressbook_hide_unwatched": "Hide unwatched", "addressbook_show_balance": "Show balance", "addressbook_show_label": "Show label", "addressbook_no_label": "no label", @@ -41,6 +45,8 @@ "addressbook_swipe_export": "Export", "addressbook_swipe_share": "Share", "addressbook_swipe_send": "Send to", + "addressbook_swipe_watch": "Watch", + "addressbook_swipe_unwatch": "Unwatch", "addressbook_title": "$coin Addresses", "app_navigation": "Navigation", "app_settings": "Settings", @@ -235,6 +241,7 @@ "setup_price_feed_allow": "Allow price feed API", "setup_price_feed_description": "This will allow for your wallet balance to be displayed at real time exchange value.", "setup_price_feed_title": "External APIs", + "setup_price_feed_last_update": "Last update: $timestamp", "setup_bg_sync_description": "This will enable background notifications for your wallets.", "setup_bg_sync_allow": "Allow background sync API", "setup_securebox_fail": "We are very sorry.\nYour device does not support a secure enough way of storing keys.", @@ -271,6 +278,7 @@ "wallet_offline": "offline", "wallet_pop_menu_paperwallet": "Import Paper Wallet", "wallet_pop_menu_wif": "Import Private Key", + "wallet_pop_menu_performance": "Performance", "wallet_pop_menu_rescan": "Rescan", "wallet_pop_menu_servers": "Adjust Servers", "wallet_receive": "Receive", diff --git a/lib/models/wallet_address.dart b/lib/models/wallet_address.dart index 82d5f58b..e0fbcea1 100644 --- a/lib/models/wallet_address.dart +++ b/lib/models/wallet_address.dart @@ -19,6 +19,8 @@ class WalletAddress extends HiveObject { bool? _isChangeAddr = false; @HiveField(7, defaultValue: 0) int notificationBackendCount = 0; + @HiveField(8, defaultValue: false) + bool isWatched = false; WalletAddress({ required this.address, diff --git a/lib/models/wallet_address.g.dart b/lib/models/wallet_address.g.dart index 8b902ab4..388ce7cb 100644 --- a/lib/models/wallet_address.g.dart +++ b/lib/models/wallet_address.g.dart @@ -25,13 +25,14 @@ class WalletAddressAdapter extends TypeAdapter { wif: fields[5] as String?, ) .._isChangeAddr = fields[6] as bool? - ..notificationBackendCount = fields[7] == null ? 0 : fields[7] as int; + ..notificationBackendCount = fields[7] == null ? 0 : fields[7] as int + ..isWatched = fields[8] == null ? false : fields[8] as bool; } @override void write(BinaryWriter writer, WalletAddress obj) { writer - ..writeByte(8) + ..writeByte(9) ..writeByte(0) ..write(obj.address) ..writeByte(1) @@ -47,7 +48,9 @@ class WalletAddressAdapter extends TypeAdapter { ..writeByte(6) ..write(obj._isChangeAddr) ..writeByte(7) - ..write(obj.notificationBackendCount); + ..write(obj.notificationBackendCount) + ..writeByte(8) + ..write(obj.isWatched); } @override diff --git a/lib/providers/active_wallets.dart b/lib/providers/active_wallets.dart index 934133a0..ab0c406b 100644 --- a/lib/providers/active_wallets.dart +++ b/lib/providers/active_wallets.dart @@ -647,14 +647,14 @@ class ActiveWallets with ChangeNotifier { //change is too small! no change output _destroyedChange = changeAmount; if (_txAmount == 0) { - tx.addOutput(address, _txAmount); + tx.addOutput(address, BigInt.from(_txAmount)); } else { - tx.addOutput(address, _txAmount - fee); + tx.addOutput(address, BigInt.from(_txAmount - fee)); _destroyedChange = _destroyedChange + fee; } } else { - tx.addOutput(address, _txAmount); - tx.addOutput(_unusedAddress, changeAmount); + tx.addOutput(address, BigInt.from(_txAmount)); + tx.addOutput(_unusedAddress, BigInt.from(changeAmount)); } } else { LoggerWrapper.logInfo( @@ -662,7 +662,7 @@ class ActiveWallets with ChangeNotifier { 'buildTransaction', 'no change needed, tx amount $_txAmount, fee $fee, output added for $address ${_txAmount - fee}', ); - tx.addOutput(address, _txAmount - fee); + tx.addOutput(address, BigInt.from(_txAmount - fee)); } //add OP_RETURN if exists @@ -760,13 +760,22 @@ class ActiveWallets with ChangeNotifier { var answerMap = {}; if (address == null) { //get all + var utxos = await getWalletUtxos(identifier); addresses = await getWalletAddresses(identifier); - addresses.forEach((addr) { - if (addr.isOurs == true || addr.isOurs == null) { - // == null for backwards compatability - answerMap[addr.address] = getScriptHash(identifier, addr.address); - } - }); + addresses.forEach( + (addr) { + if (addr.isOurs == true || addr.isOurs == null) { + // == null for backwards compatability + //does addr have a balance? + var utxoRes = utxos + .firstWhereOrNull((element) => element.address == addr.address); + + if (addr.isWatched || utxoRes != null && utxoRes.value > 0) { + answerMap[addr.address] = getScriptHash(identifier, addr.address); + } + } + }, + ); } else { //get just one answerMap[address] = getScriptHash(identifier, address); @@ -852,6 +861,19 @@ class ActiveWallets with ChangeNotifier { await openWallet.save(); } + Future updateAddressWatched( + String identifier, String address, bool newValue) async { + var openWallet = getSpecificCoinWallet(identifier); + var addr = openWallet.addresses.firstWhereOrNull( + (element) => element.address == address, + ); + if (addr != null) { + addr.isWatched = newValue; + } + await openWallet.save(); + notifyListeners(); + } + void removeAddress(String identifier, WalletAddress addr) { var openWallet = getSpecificCoinWallet(identifier); openWallet.removeAddress(addr); diff --git a/lib/providers/electrum_connection.dart b/lib/providers/electrum_connection.dart index 2821a933..f03afc78 100644 --- a/lib/providers/electrum_connection.dart +++ b/lib/providers/electrum_connection.dart @@ -412,6 +412,7 @@ class ElectrumConnection with ChangeNotifier { addresses.entries.forEach((hash) { _addresses[hash.key] = hash.value; sendMessage('blockchain.scripthash.subscribe', hash.key, [hash.value]); + notifyListeners(); }); } diff --git a/lib/screens/app_settings_notifications.dart b/lib/screens/app_settings_notifications.dart index 2e850bad..673997c2 100644 --- a/lib/screens/app_settings_notifications.dart +++ b/lib/screens/app_settings_notifications.dart @@ -234,5 +234,4 @@ class _AppSettingsNotificationsScreenState ), ); } - //TODO allow to not listen to change addresses? .. would save data } diff --git a/lib/screens/setup/setup_create_wallet.dart b/lib/screens/setup/setup_create_wallet.dart index 881911a6..a81a1486 100644 --- a/lib/screens/setup/setup_create_wallet.dart +++ b/lib/screens/setup/setup_create_wallet.dart @@ -160,12 +160,6 @@ class _SetupCreateWalletScreenState extends State { @override Widget build(BuildContext context) { - if (_isLoading) { - return Center( - child: LoadingIndicator(), - ); - } - return Scaffold( appBar: AppBar( toolbarHeight: 0, @@ -175,212 +169,226 @@ class _SetupCreateWalletScreenState extends State { child: Container( height: SetupScreen.calcContainerHeight(context), color: Theme.of(context).primaryColor, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - PeerProgress(2), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - PeerButtonSetupBack(), - AutoSizeText( - AppLocalizations.instance - .translate('setup_save_title'), - maxFontSize: 28, - minFontSize: 25, - style: TextStyle( - color: Colors.white, - ), - ), - SizedBox( - width: 40, - ), - ], - ), - Column( - children: [ - Container( - padding: const EdgeInsets.all(4), - width: MediaQuery.of(context).size.width > 1200 - ? MediaQuery.of(context).size.width / 2 - : MediaQuery.of(context).size.width, - decoration: BoxDecoration( - borderRadius: - BorderRadius.all(Radius.circular(20)), - color: Theme.of(context).shadowColor, + child: _isLoading + ? Center( + child: LoadingIndicator(), + ) + : Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + PeerProgress(2), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + PeerButtonSetupBack(), + AutoSizeText( + AppLocalizations.instance + .translate('setup_save_title'), + maxFontSize: 28, + minFontSize: 25, + style: TextStyle( + color: Colors.white, + ), + ), + SizedBox( + width: 40, + ), + ], ), - child: Column( + Column( children: [ - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, + Container( + padding: const EdgeInsets.all(4), + width: MediaQuery.of(context).size.width > + 1200 + ? MediaQuery.of(context).size.width / 2 + : MediaQuery.of(context).size.width, + decoration: BoxDecoration( + borderRadius: + BorderRadius.all(Radius.circular(20)), + color: Theme.of(context).shadowColor, + ), + child: Column( children: [ - Icon( - Icons.vpn_key_rounded, - color: Theme.of(context).primaryColor, - size: 40, - ), - SizedBox( - width: 24, + Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + Icon( + Icons.vpn_key_rounded, + color: Theme.of(context) + .primaryColor, + size: 40, + ), + SizedBox( + width: 24, + ), + Container( + width: MediaQuery.of(context) + .size + .width > + 1200 + ? MediaQuery.of(context) + .size + .width / + 2.5 + : MediaQuery.of(context) + .size + .width / + 1.9, + child: Text( + AppLocalizations.instance + .translate( + 'setup_save_text1'), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .primaryContainer, + fontSize: 15, + ), + textAlign: TextAlign.left, + maxLines: 5, + ), + ), + ], + ), ), - Container( - width: MediaQuery.of(context) - .size - .width > - 1200 - ? MediaQuery.of(context) - .size - .width / - 2.5 - : MediaQuery.of(context) - .size - .width / - 1.9, - child: Text( - AppLocalizations.instance - .translate('setup_save_text1'), - style: TextStyle( + GestureDetector( + onDoubleTap: () { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar( + content: Text( + AppLocalizations.instance + .translate('snack_copied'), + textAlign: TextAlign.center, + ), + duration: Duration(seconds: 1), + )); + Clipboard.setData( + ClipboardData(text: _seed), + ); + setState(() { + _sharedYet = true; + }); + }, + child: Container( + height: 250, + padding: EdgeInsets.fromLTRB( + 16, 32, 16, 24), + decoration: BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(20)), color: Theme.of(context) - .colorScheme - .primaryContainer, - fontSize: 15, + .backgroundColor, + border: Border.all( + width: 2, + color: _sharedYet + ? Theme.of(context) + .shadowColor + : Theme.of(context) + .primaryColor, + ), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Container( + width: MediaQuery.of(context) + .size + .width > + 1200 + ? MediaQuery.of(context) + .size + .width / + 3 + : MediaQuery.of(context) + .size + .width * + 0.7, + child: Text( + _seed, + style: TextStyle( + fontSize: 16, + wordSpacing: 16, + color: Theme.of(context) + .colorScheme + .primaryContainer, + ), + ), + ) + ], ), - textAlign: TextAlign.left, - maxLines: 5, ), ), ], ), ), - GestureDetector( - onDoubleTap: () { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: Text( - AppLocalizations.instance - .translate('snack_copied'), - textAlign: TextAlign.center, - ), - duration: Duration(seconds: 1), - )); - Clipboard.setData( - ClipboardData(text: _seed), - ); - setState(() { - _sharedYet = true; - }); - }, - child: Container( - height: 250, - padding: - EdgeInsets.fromLTRB(16, 32, 16, 24), - decoration: BoxDecoration( - borderRadius: - BorderRadius.all(Radius.circular(20)), - color: Theme.of(context).backgroundColor, - border: Border.all( - width: 2, - color: _sharedYet - ? Theme.of(context).shadowColor - : Theme.of(context).primaryColor, - ), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Container( - width: MediaQuery.of(context) - .size - .width > - 1200 - ? MediaQuery.of(context) - .size - .width / - 3 - : MediaQuery.of(context) - .size - .width * - 0.7, - child: Text( - _seed, - style: TextStyle( - fontSize: 16, - wordSpacing: 16, - color: Theme.of(context) - .colorScheme - .primaryContainer, - ), - ), - ) - ], - ), + ], + ), + Column( + children: [ + Container( + width: MediaQuery.of(context).size.width > + 1200 + ? MediaQuery.of(context).size.width / 2 + : MediaQuery.of(context).size.width, + child: Slider( + activeColor: Colors.white, + inactiveColor: + Theme.of(context).shadowColor, + value: _currentSliderValue, + min: 12, + max: 24, + divisions: 4, + label: + _currentSliderValue.round().toString(), + onChanged: (value) { + setState(() { + _currentSliderValue = value; + }); + if (value % 3 == 0) { + recreatePhrase(value); + } + }, ), ), + Text( + AppLocalizations.instance + .translate('setup_seed_slider_label'), + style: TextStyle( + color: Colors.white, fontSize: 14), + textAlign: TextAlign.center, + ), ], ), - ), - ], + ], + ), ), - Column( - children: [ - Container( - width: MediaQuery.of(context).size.width > 1200 - ? MediaQuery.of(context).size.width / 2 - : MediaQuery.of(context).size.width, - child: Slider( - activeColor: Colors.white, - inactiveColor: Theme.of(context).shadowColor, - value: _currentSliderValue, - min: 12, - max: 24, - divisions: 4, - label: _currentSliderValue.round().toString(), - onChanged: (value) { - setState(() { - _currentSliderValue = value; - }); - if (value % 3 == 0) { - recreatePhrase(value); - } - }, - ), - ), - Text( - AppLocalizations.instance - .translate('setup_seed_slider_label'), - style: TextStyle(color: Colors.white, fontSize: 14), - textAlign: TextAlign.center, - ), - ], + ), + if (_sharedYet) + PeerButtonSetup( + action: () async => await handleContinue(), + text: AppLocalizations.instance.translate('continue'), + ) + else + PeerButtonSetup( + action: () async => await shareSeed(_seed), + text: AppLocalizations.instance.translate('export_now'), ), - ], - ), + SizedBox( + height: 32, + ), + ], ), - ), - if (_sharedYet) - PeerButtonSetup( - action: () async => await handleContinue(), - text: AppLocalizations.instance.translate('continue'), - ) - else - PeerButtonSetup( - action: () async => await shareSeed(_seed), - text: AppLocalizations.instance.translate('export_now'), - ), - SizedBox( - height: 32, - ), - ], - ), ), ), ); diff --git a/lib/screens/setup/setup_import_seed.dart b/lib/screens/setup/setup_import_seed.dart index 70893ee1..e0e6f4f4 100644 --- a/lib/screens/setup/setup_import_seed.dart +++ b/lib/screens/setup/setup_import_seed.dart @@ -201,10 +201,11 @@ class _SetupImportSeedState extends State { hintText: 'e.g. mushrooms pepper courgette onion asparagus garlic sweetcorn nut pumpkin potato bean spinach', hintStyle: TextStyle( - color: Theme.of(context) - .colorScheme - .secondary, - fontSize: 16), + color: Theme.of(context) + .colorScheme + .secondary, + fontSize: 16, + ), filled: true, fillColor: Theme.of(context).backgroundColor, diff --git a/lib/screens/wallet/wallet_home.dart b/lib/screens/wallet/wallet_home.dart index e280cf57..cb9f2259 100644 --- a/lib/screens/wallet/wallet_home.dart +++ b/lib/screens/wallet/wallet_home.dart @@ -241,50 +241,60 @@ class _WalletHomeState extends State } void selectPopUpMenuItem(String value) { - if (value == 'import_wallet') { - Navigator.of(context) - .pushNamed(Routes.ImportPaperWallet, arguments: _wallet.name); - } else if (value == 'import_wif') { - Navigator.of(context) - .pushNamed(Routes.ImportWif, arguments: _wallet.name); - } else if (value == 'server_settings') { - Navigator.of(context) - .pushNamed(Routes.ServerSettings, arguments: _wallet.name); - } else if (value == 'rescan') { - showDialog( - context: context, - builder: (_) => AlertDialog( - title: - Text(AppLocalizations.instance.translate('wallet_rescan_title')), - content: Text( - AppLocalizations.instance.translate('wallet_rescan_content')), - actions: [ - TextButton.icon( - label: Text(AppLocalizations.instance - .translate('server_settings_alert_cancel')), - icon: Icon(Icons.cancel), - onPressed: () { - Navigator.of(context).pop(); - }), - TextButton.icon( - label: Text( - AppLocalizations.instance.translate('jail_dialog_button')), - icon: Icon(Icons.check), - onPressed: () async { - //close connection - await _connectionProvider!.closeConnection(); - _rescanInProgress = true; - //init rescan - await Navigator.of(context).pushNamedAndRemoveUntil( - Routes.WalletImportScan, - (_) => false, - arguments: _wallet.name, - ); - }, - ), - ], - ), - ); + switch (value) { + case 'import_wallet': + Navigator.of(context) + .pushNamed(Routes.ImportPaperWallet, arguments: _wallet.name); + break; + case 'import_wif': + Navigator.of(context) + .pushNamed(Routes.ImportWif, arguments: _wallet.name); + break; + case 'server_settings': + Navigator.of(context) + .pushNamed(Routes.ServerSettings, arguments: _wallet.name); + break; + case 'performance': + Navigator.of(context) + .pushNamed(Routes.WalletPerformance, arguments: _wallet.name); + break; + case 'rescan': + showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text( + AppLocalizations.instance.translate('wallet_rescan_title')), + content: Text( + AppLocalizations.instance.translate('wallet_rescan_content')), + actions: [ + TextButton.icon( + label: Text(AppLocalizations.instance + .translate('server_settings_alert_cancel')), + icon: Icon(Icons.cancel), + onPressed: () { + Navigator.of(context).pop(); + }), + TextButton.icon( + label: Text( + AppLocalizations.instance.translate('jail_dialog_button')), + icon: Icon(Icons.check), + onPressed: () async { + //close connection + await _connectionProvider!.closeConnection(); + _rescanInProgress = true; + //init rescan + await Navigator.of(context).pushNamedAndRemoveUntil( + Routes.WalletImportScan, + (_) => false, + arguments: _wallet.name, + ); + }, + ), + ], + ), + ); + break; + default: } } @@ -319,7 +329,6 @@ class _WalletHomeState extends State child: SendTab(changeIndex, _address, _label, _connectionState), ); break; - default: body = Container(); break; @@ -426,6 +435,19 @@ class _WalletHomeState extends State ), ), ), + // PopupMenuItem( + // value: 'performance', + // child: ListTile( + // leading: Icon( + // Icons.bolt, + // color: Theme.of(context).colorScheme.secondary, + // ), + // title: Text( + // AppLocalizations.instance + // .translate('wallet_pop_menu_performance'), + // ), + // ), + // ), ]; }, ) diff --git a/lib/screens/wallet/wallet_performance.dart b/lib/screens/wallet/wallet_performance.dart new file mode 100644 index 00000000..e3acf958 --- /dev/null +++ b/lib/screens/wallet/wallet_performance.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +// import 'package:provider/provider.dart'; + +// import '../../providers/active_wallets.dart'; +import '../../tools/app_localizations.dart'; + +class WalletPerformanceScreen extends StatefulWidget { + const WalletPerformanceScreen({Key? key}) : super(key: key); + + @override + State createState() => + _WalletPerformanceScreenState(); +} + +class _WalletPerformanceScreenState extends State { + // late String _walletName; + bool _initial = true; + // late ActiveWallets _activeWallets; + + @override + void didChangeDependencies() { + if (_initial == true) { + setState(() { + // _walletName = ModalRoute.of(context)!.settings.arguments as String; + // _activeWallets = Provider.of(context); + _initial = false; + }); + } + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + AppLocalizations.instance.translate('wallet_pop_menu_performance'), + ), + ), + ); + } +} + +//TODO Hint: rescan not affected +//TODO Hint: disabling generation of change addresses will result in lack of privacy +//TODO Hint: Addresses can be watched manually by enabling them in the address book +//TODO Receive tab: allow manual generation of unused address if change address generation is disabled diff --git a/lib/tools/app_routes.dart b/lib/tools/app_routes.dart index b27fe024..f35640e4 100644 --- a/lib/tools/app_routes.dart +++ b/lib/tools/app_routes.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.dart'; +import 'package:peercoin/screens/wallet/wallet_performance.dart'; import '../screens/app_settings_notifications.dart'; import '../screens/app_settings_screen.dart'; @@ -33,6 +34,7 @@ class Routes { static const String SetupDataFeeds = '/setup-feeds'; static const String Transaction = '/tx-detail'; static const String WalletHome = '/wallet-home'; + static const String WalletPerformance = '/wallet-performance'; static const String WalletImportScan = '/wallet-import-scan'; static const String ImportPaperWallet = '/import-paperwallet'; static const String ImportWif = '/import-wif'; @@ -83,6 +85,10 @@ class Routes { widget: WalletImportScanScreen(), routeType: RouteTypes.requiresArguments, ), + Routes.WalletPerformance: (context) => RouterMaster( + widget: WalletPerformanceScreen(), + routeType: RouteTypes.requiresArguments, + ), Routes.ImportPaperWallet: (context) => RouterMaster( widget: ImportPaperWalletScreen(), routeType: RouteTypes.requiresArguments, diff --git a/lib/tools/background_sync.dart b/lib/tools/background_sync.dart index 5632b1a3..227b3898 100644 --- a/lib/tools/background_sync.dart +++ b/lib/tools/background_sync.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; import 'package:background_fetch/background_fetch.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; @@ -134,22 +135,34 @@ class BackgroundSync { if (_appOptions.notificationActiveWallets.contains(wallet.letterCode)) { //if activated, parse all addresses to a list that will be POSTed to backend later on var adressesToQuery = {}; + var utxos = wallet.utxos; + wallet.addresses.forEach( (walletAddress) async { + var utxoRes = utxos.firstWhereOrNull( + (element) => element.address == walletAddress.address); + if (walletAddress.isOurs == true) { - //check if that address already has a pending notification - var res = wallet.pendingTransactionNotifications - .where( - (element) => element.address == walletAddress.address, - ) - .toList(); - if (res.isNotEmpty) { - //addr does have a pending notification - adressesToQuery[walletAddress.address] = res[0].tx; - } else { - //addr does not have a pending notification - adressesToQuery[walletAddress.address] = - walletAddress.notificationBackendCount; + if (walletAddress.isWatched == true || + utxoRes != null && utxoRes.value > 0 || + wallet.addresses.indexOf(walletAddress) == + wallet.addresses.length - 1) + //assumes that last wallet in list is always the unused/change address + { + //check if that address already has a pending notification + var res = wallet.pendingTransactionNotifications + .where( + (element) => element.address == walletAddress.address, + ) + .toList(); + if (res.isNotEmpty) { + //addr does have a pending notification + adressesToQuery[walletAddress.address] = res[0].tx; + } else { + //addr does not have a pending notification + adressesToQuery[walletAddress.address] = + walletAddress.notificationBackendCount; + } } } }, @@ -188,8 +201,12 @@ class BackgroundSync { var addresses = bodyDecoded['addresses']; addresses.forEach((element) { //write tx result from API into coinwallet - wallet.putPendingTransactionNotification(PendingNotification( - address: element['address'], tx: element['tx'])); + wallet.putPendingTransactionNotification( + PendingNotification( + address: element['address'], + tx: element['tx'], + ), + ); }); if (fromScan == true) { diff --git a/lib/widgets/settings/settings_price_ticker.dart b/lib/widgets/settings/settings_price_ticker.dart index 5ff58486..0c0cc0c6 100644 --- a/lib/widgets/settings/settings_price_ticker.dart +++ b/lib/widgets/settings/settings_price_ticker.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import '../../providers/app_settings.dart'; import '../../tools/app_localizations.dart'; @@ -18,6 +19,7 @@ class SettingsPriceTicker extends StatefulWidget { class _SettingsPriceTickerState extends State { late bool _listExpanded; + var formattedTime; @override void initState() { @@ -34,6 +36,13 @@ class _SettingsPriceTickerState extends State { super.initState(); } + @override + void didChangeDependencies() { + formattedTime = DateFormat('yyyy-MM-dd HH:mm:ss') + .format(widget._settings.latestTickerUpdate); + super.didChangeDependencies(); + } + void enableFeed(BuildContext ctx) async { await showDialog( context: ctx, @@ -138,6 +147,18 @@ class _SettingsPriceTickerState extends State { Widget build(BuildContext context) { return Column( children: [ + widget._settings.selectedCurrency.isNotEmpty + ? Text( + AppLocalizations.instance.translate( + 'setup_price_feed_last_update', + {'timestamp': formattedTime}, + ), + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.secondary, + ), + ) + : Container(), ExpandedSection( expand: _listExpanded, child: Column( diff --git a/lib/widgets/wallet/addresses_tab.dart b/lib/widgets/wallet/addresses_tab.dart index d0490f86..d864cebe 100644 --- a/lib/widgets/wallet/addresses_tab.dart +++ b/lib/widgets/wallet/addresses_tab.dart @@ -3,6 +3,7 @@ import 'package:coinslib/coinslib.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:peercoin/providers/electrum_connection.dart'; import 'package:peercoin/widgets/wallet/wallet_home_qr.dart'; import 'package:provider/provider.dart'; @@ -14,6 +15,7 @@ import '../../providers/app_settings.dart'; import '../../screens/wallet/wallet_home.dart'; import '../../tools/app_localizations.dart'; import '../../tools/auth.dart'; +import '../../tools/logger_wrapper.dart'; import '../double_tab_to_clipboard.dart'; class AddressTab extends StatefulWidget { @@ -40,13 +42,22 @@ class _AddressTabState extends State { bool _showLabel = true; bool _showUsed = true; bool _showEmpty = true; + bool _showUnwatched = true; final Map _addressBalanceMap = {}; + String _currentChangeAddress = ''; + late ActiveWallets _activeWallets; + late ElectrumConnection _connection; + var _listenedAddresses; @override void didChangeDependencies() async { if (_initial) { applyFilter(); _availableCoin = AvailableCoins().getSpecificCoin(widget.name); + _activeWallets = Provider.of(context); + _connection = Provider.of(context); + _currentChangeAddress = _activeWallets.getUnusedAddress; + _listenedAddresses = _connection.listenedAddresses.keys; await fillAddressBalanceMap(); setState(() { _initial = false; @@ -56,8 +67,7 @@ class _AddressTabState extends State { } Future fillAddressBalanceMap() async { - final utxos = - await Provider.of(context).getWalletUtxos(widget.name); + final utxos = await _activeWallets.getWalletUtxos(widget.name); for (var tx in utxos) { if (tx.value > 0) { _addressBalanceMap[tx.address] = @@ -70,13 +80,20 @@ class _AddressTabState extends State { var _filteredListReceive = []; var _filteredListSend = []; - widget._walletAddresses!.forEach((e) { - if (e.isOurs == true || e.isOurs == null) { - _filteredListReceive.add(e); - } else { - _filteredListSend.add(e); - } - }); + widget._walletAddresses!.forEach( + (e) { + if (e.isOurs == true || e.isOurs == null) { + _filteredListReceive.add(e); + //fake watch change address and addresses with balance + if (_addressBalanceMap[e.address] != null || + e.address == _currentChangeAddress) { + e.isWatched = true; + } + } else { + _filteredListSend.add(e); + } + }, + ); //apply filters to receive list var _toRemove = []; @@ -91,6 +108,11 @@ class _AddressTabState extends State { _toRemove.add(address); } } + if (_showUnwatched == false) { + if (address.isWatched == false) { + _toRemove.add(address); + } + } if (_showEmpty == false) { if (_addressBalanceMap[address.address] == null) { _toRemove.add(address); @@ -230,6 +252,52 @@ class _AddressTabState extends State { ); } + Future _toggleWatched(WalletAddress addr) async { + var snackText; + //addresses with balance or currentChangeAddress can not be unwatched + if (_addressBalanceMap[addr.address] != null || + addr.address == _currentChangeAddress) { + snackText = 'addressbook_dialog_addr_unwatch_unable'; + } else { + snackText = addr.isWatched + ? 'addressbook_dialog_addr_unwatched' + : 'addressbook_dialog_addr_watched'; + + await _activeWallets.updateAddressWatched( + widget.name, + addr.address, + !addr.isWatched, + ); + + applyFilter(); + if (_connection.connectionState == ElectrumConnectionState.connected) { + if (!_listenedAddresses.contains(addr.address) && + addr.isWatched == true) { + //subscribe + LoggerWrapper.logInfo('AddressTab', '_toggleWatched', + 'watched and subscribed ${addr.address}'); + _connection.subscribeToScriptHashes( + await _activeWallets.getWalletScriptHashes( + widget.name, addr.address), + ); + } + } + } + //fire snack + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.instance.translate( + snackText, + {'address': addr.address}, + ), + textAlign: TextAlign.center, + ), + duration: Duration(seconds: 5), + ), + ); + } + Future _addressAddDialog(BuildContext context) async { var _labelController = TextEditingController(); var _addressController = TextEditingController(); @@ -399,15 +467,16 @@ class _AddressTabState extends State { .read() .removeAddress(widget.name, addr); applyFilter(); - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: Text( - AppLocalizations.instance.translate( - 'addressbook_dialog_remove_snack'), - textAlign: TextAlign.center, + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.instance.translate( + 'addressbook_dialog_remove_snack'), + textAlign: TextAlign.center, + ), + duration: Duration(seconds: 5), ), - duration: Duration(seconds: 5), - )); + ); Navigator.of(context).pop(); }, ), @@ -479,13 +548,27 @@ class _AddressTabState extends State { addr.address, ), ), + IconSlideAction( + caption: AppLocalizations.instance.translate( + addr.isWatched + ? 'addressbook_swipe_unwatch' + : 'addressbook_swipe_watch', + ), + color: Theme.of(context).colorScheme.secondary, + iconWidget: Icon( + addr.isWatched + ? Icons.visibility_off + : Icons.visibility, + color: Theme.of(context).backgroundColor, + ), + onTap: () => _toggleWatched(addr)), IconSlideAction( caption: AppLocalizations.instance .translate('addressbook_swipe_export'), - color: Theme.of(context).backgroundColor, + color: Theme.of(context).errorColor, iconWidget: Icon( Icons.vpn_key, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).backgroundColor, ), onTap: () => Auth.requireAuth( context: context, @@ -578,6 +661,29 @@ class _AddressTabState extends State { applyFilter(); }, ), + ChoiceChip( + backgroundColor: Theme.of(context).backgroundColor, + selectedColor: Theme.of(context).shadowColor, + visualDensity: + VisualDensity(horizontal: 0.0, vertical: -4), + label: Container( + child: AutoSizeText( + AppLocalizations.instance + .translate('addressbook_hide_unwatched'), + minFontSize: 10, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + selected: _showUnwatched, + onSelected: (_) { + setState(() { + _showUnwatched = _; + }); + applyFilter(); + }, + ), Padding( padding: const EdgeInsets.all(kIsWeb ? 8.0 : 0), child: ChoiceChip( diff --git a/lib/widgets/wallet/send_tab.dart b/lib/widgets/wallet/send_tab.dart index e1f83b71..eb4a740f 100644 --- a/lib/widgets/wallet/send_tab.dart +++ b/lib/widgets/wallet/send_tab.dart @@ -461,6 +461,10 @@ class _SendTabState extends State { _availableCoin.minimumTxValue) { return AppLocalizations.instance.translate( 'send_amount_below_minimum_unable', + { + 'amount': + '${_availableCoin.minimumTxValue / 1000000}' + }, ); } diff --git a/pubspec.lock b/pubspec.lock index 0fa43474..a1659d60 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -196,7 +196,7 @@ packages: name: coinslib url: "https://pub.dartlang.org" source: hosted - version: "3.1.2" + version: "3.1.3" collection: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index ad2e08b8..5f5c6416 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,11 @@ name: peercoin description: A new Peercoin wallet. -version: 0.9.0+87 +version: 0.9.1+88 environment: sdk: '>=2.12.0 <3.0.0' + flutter: '<= 2.10.0' publish_to: none @@ -16,7 +17,7 @@ dependencies: web_socket_channel: ^2.1.0 crypto: ^3.0.1 bip39: ^1.0.6 - coinslib: ^3.1.2 + coinslib: ^3.1.3 hive: ^2.1.0 hive_flutter: ^1.1.0 hive_generator: ^1.1.0 @@ -44,7 +45,7 @@ dependencies: background_fetch: ^1.0.1 flutter_logs: ^2.1.5 auto_size_text: ^3.0.0 - camera: ^0.9.4+19 + camera: ^0.9.4 dev_dependencies: integration_test: