diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 97d8ef20b..3096b80eb 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -406,6 +406,8 @@ abstract class ElectrumWalletBase nodeSupportsSilentPayments = false; } + print("sp sync: ${message.syncStatus.progress() * 100}%"); + syncStatus = message.syncStatus; await walletInfo.updateRestoreHeight(message.height); } @@ -476,7 +478,12 @@ abstract class ElectrumWalletBase @override Future stopSync() async { syncStatus = StoppedSyncingSyncStatus(); + try { + await _receiveStream?.cancel(); + await electrumClient.close(); + } catch (_) {} _updateFeeRateTimer?.cancel(); + _autoSaveTimer?.cancel(); } @action diff --git a/lib/di.dart b/lib/di.dart index 9c3c5bcb2..c7bec6e52 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -290,6 +290,10 @@ Future setup({ getIt.registerFactory(() => BackgroundTasks()); } + // getIt.registerSingleton(BackgroundTasks( + // settingsStore: getIt.get(), + // )); + final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) && (secrets.wyreApiKey.isNotEmpty) && (secrets.wyreAccountId.isNotEmpty); @@ -369,62 +373,52 @@ Future setup({ type: type)); getIt.registerFactoryParam((args, closable) { - return WalletUnlockPage( - getIt.get(param1: args), - args.callback, - args.authPasswordHandler, - closable: closable); + return WalletUnlockPage(getIt.get(param1: args), args.callback, + args.authPasswordHandler, + closable: closable); }, instanceName: 'wallet_unlock_loadable'); getIt.registerFactory( - () => getIt.get( - param1: WalletUnlockArguments( - callback: (bool successful, _) { - if (successful) { - final authStore = getIt.get(); - authStore.allowed(); - }}), - param2: false, - instanceName: 'wallet_unlock_loadable'), - instanceName: 'wallet_password_login'); + () => getIt.get( + param1: WalletUnlockArguments(callback: (bool successful, _) { + if (successful) { + final authStore = getIt.get(); + authStore.allowed(); + } + }), + param2: false, + instanceName: 'wallet_unlock_loadable'), + instanceName: 'wallet_password_login'); getIt.registerFactoryParam((args, closable) { - return WalletUnlockPage( - getIt.get(param1: args), - args.callback, - args.authPasswordHandler, - closable: closable); + return WalletUnlockPage(getIt.get(param1: args), args.callback, + args.authPasswordHandler, + closable: closable); }, instanceName: 'wallet_unlock_verifiable'); getIt.registerFactoryParam((args, _) { - final currentWalletName = getIt - .get() - .getString(PreferencesKey.currentWalletName) ?? ''; + final currentWalletName = + getIt.get().getString(PreferencesKey.currentWalletName) ?? ''; final currentWalletTypeRaw = - getIt.get() - .getInt(PreferencesKey.currentWalletType) ?? 0; + getIt.get().getInt(PreferencesKey.currentWalletType) ?? 0; final currentWalletType = deserializeFromInt(currentWalletTypeRaw); - return WalletUnlockLoadableViewModel( - getIt.get(), - getIt.get(), - walletName: args.walletName ?? currentWalletName, - walletType: args.walletType ?? currentWalletType); + return WalletUnlockLoadableViewModel(getIt.get(), getIt.get(), + walletName: args.walletName ?? currentWalletName, + walletType: args.walletType ?? currentWalletType); }); - getIt.registerFactoryParam((args, _) { - final currentWalletName = getIt - .get() - .getString(PreferencesKey.currentWalletName) ?? ''; + getIt.registerFactoryParam( + (args, _) { + final currentWalletName = + getIt.get().getString(PreferencesKey.currentWalletName) ?? ''; final currentWalletTypeRaw = - getIt.get() - .getInt(PreferencesKey.currentWalletType) ?? 0; + getIt.get().getInt(PreferencesKey.currentWalletType) ?? 0; final currentWalletType = deserializeFromInt(currentWalletTypeRaw); - return WalletUnlockVerifiableViewModel( - getIt.get(), - walletName: args.walletName ?? currentWalletName, - walletType: args.walletType ?? currentWalletType); + return WalletUnlockVerifiableViewModel(getIt.get(), + walletName: args.walletName ?? currentWalletName, + walletType: args.walletType ?? currentWalletType); }); getIt.registerFactoryParam((WalletType type, _) => @@ -842,8 +836,9 @@ Future setup({ getIt.registerFactory(() => TrocadorProvidersViewModel(getIt.get())); getIt.registerFactory(() { - return OtherSettingsViewModel(getIt.get(), getIt.get().wallet!, - getIt.get());}); + return OtherSettingsViewModel( + getIt.get(), getIt.get().wallet!, getIt.get()); + }); getIt.registerFactory(() { return SecuritySettingsViewModel(getIt.get()); @@ -851,7 +846,8 @@ Future setup({ getIt.registerFactory(() => WalletSeedViewModel(getIt.get().wallet!)); - getIt.registerFactory(() => SeedSettingsViewModel(getIt.get(), getIt.get())); + getIt.registerFactory( + () => SeedSettingsViewModel(getIt.get(), getIt.get())); getIt.registerFactoryParam((bool isWalletCreated, _) => WalletSeedPage(getIt.get(), isNewWalletCreated: isWalletCreated)); @@ -1011,7 +1007,8 @@ Future setup({ _unspentCoinsInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.nano: case WalletType.banano: - return nano!.createNanoWalletService(_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); + return nano!.createNanoWalletService( + _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.polygon: return polygon!.createPolygonWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); @@ -1019,7 +1016,8 @@ Future setup({ return solana!.createSolanaWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.tron: - return tron!.createTronWalletService(_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); + return tron!.createTronWalletService( + _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.wownero: return wownero!.createWowneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.none: diff --git a/lib/entities/background_tasks.dart b/lib/entities/background_tasks.dart index 89ec1a5c4..631e16836 100644 --- a/lib/entities/background_tasks.dart +++ b/lib/entities/background_tasks.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_core/sync_status.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/widgets.dart'; @@ -25,7 +26,9 @@ const moneroSyncTaskKey = "com.fotolockr.cakewallet.monero_sync_task"; const mwebSyncTaskKey = "com.fotolockr.cakewallet.mweb_sync_task"; const initialNotificationTitle = 'Cake Background Sync'; -const initialNotificationContent = 'On standby - app is in the foreground'; +const standbyMessage = 'On standby - app is in the foreground'; +const readyMessage = 'Ready to sync - waiting until the app has been in the background for a while'; + const notificationId = 888; const notificationChannelId = 'cake_service'; const notificationChannelName = 'CAKE BACKGROUND SERVICE'; @@ -37,7 +40,24 @@ void setNotificationStandby(FlutterLocalNotificationsPlugin flutterLocalNotifica flutterLocalNotificationsPlugin.show( notificationId, initialNotificationTitle, - initialNotificationContent, + standbyMessage, + const NotificationDetails( + android: AndroidNotificationDetails( + notificationChannelId, + notificationChannelName, + icon: 'ic_bg_service_small', + ongoing: true, + ), + ), + ); +} + +void setNotificationReady(FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin) async { + flutterLocalNotificationsPlugin.cancelAll(); + flutterLocalNotificationsPlugin.show( + notificationId, + initialNotificationTitle, + readyMessage, const NotificationDetails( android: AndroidNotificationDetails( notificationChannelId, @@ -77,6 +97,10 @@ Future onStart(ServiceInstance service) async { setNotificationStandby(flutterLocalNotificationsPlugin); }); + service.on('setReady').listen((event) async { + setNotificationReady(flutterLocalNotificationsPlugin); + }); + // we have entered the background, start the sync: service.on('setBackground').listen((event) async { if (bgSyncStarted) { @@ -93,7 +117,7 @@ Future onStart(ServiceInstance service) async { // these errors still show up in logs which doesn't really make sense to me } - print("initialized app configs"); + print("INITIALIZED APP CONFIGS"); final currentWallet = getIt.get().wallet; // don't start syncing immediately: @@ -101,6 +125,7 @@ Future onStart(ServiceInstance service) async { final walletLoadingService = getIt.get(); final settingsStore = getIt.get(); + final walletListViewModel = getIt.get(); final typeRaw = getIt.get().getInt(PreferencesKey.currentWalletType); bool syncAll = true; @@ -108,101 +133,95 @@ Future onStart(ServiceInstance service) async { if (syncAll) { /// get all Monero wallets of the user and sync them - final List moneroWallets = getIt - .get() - .wallets + final List moneroWallets = walletListViewModel.wallets .where((element) => [WalletType.monero, WalletType.wownero].contains(element.type)) .toList(); for (int i = 0; i < moneroWallets.length; i++) { final wallet = await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name); - final node = getIt.get().getCurrentNode(moneroWallets[i].type); + final node = settingsStore.getCurrentNode(moneroWallets[i].type); await wallet.connectToNode(node: node); await wallet.startSync(); syncingWallets.add(wallet); } // get all litecoin wallets and sync them: - final List litecoinWallets = getIt - .get() - .wallets + final List litecoinWallets = walletListViewModel.wallets .where((element) => element.type == WalletType.litecoin) .toList(); // we only need to sync the first litecoin wallet since they share the same collection of blocks if (litecoinWallets.isNotEmpty) { - final firstWallet = litecoinWallets.first; - final wallet = await walletLoadingService.load(firstWallet.type, firstWallet.name); - final node = getIt.get().getCurrentNode(firstWallet.type); - await wallet.connectToNode(node: node); - await wallet.startSync(); - syncingWallets.add(wallet); + try { + final firstWallet = litecoinWallets.first; + final wallet = await walletLoadingService.load(firstWallet.type, firstWallet.name); + final node = settingsStore.getCurrentNode(firstWallet.type); + await wallet.connectToNode(node: node); + await wallet.startSync(); + syncingWallets.add(wallet); + } catch (e) { + // couldn't connect to mwebd (most likely) + print("error syncing litecoin wallet: $e"); + } } - final List bitcoinWallets = getIt - .get() - .wallets + final List bitcoinWallets = walletListViewModel.wallets .where((element) => element.type == WalletType.bitcoin) .toList(); for (int i = 0; i < bitcoinWallets.length; i++) { - final wallet = - await walletLoadingService.load(bitcoinWallets[i].type, bitcoinWallets[i].name); - final node = getIt.get().getCurrentNode(bitcoinWallets[i].type); - await wallet.connectToNode(node: node); - await wallet.startSync(); - // TODO: use proxy layer: - await (wallet as ElectrumWallet).setSilentPaymentsScanning(true); - syncingWallets.add(wallet); + try { + final wallet = + await walletLoadingService.load(bitcoinWallets[i].type, bitcoinWallets[i].name); + final node = settingsStore.getCurrentNode(bitcoinWallets[i].type); + await wallet.connectToNode(node: node); + await wallet.startSync(); + // TODO: use proxy layer: + await (wallet as ElectrumWallet).rescan(height: 1); + syncingWallets.add(wallet); + } catch (e) { + print("error syncing bitcoin wallet: $e"); + } } } else { - // /// if the user chose to sync only active wallet - // /// if the current wallet is monero; sync it only - // if (typeRaw == WalletType.monero.index || typeRaw == WalletType.wownero.index) { - // final name = getIt.get().getString(PreferencesKey.currentWalletName); - // wallet = await walletLoadingService.load(WalletType.values[typeRaw!], name!); - // final node = getIt.get().getCurrentNode(WalletType.values[typeRaw]); - // await wallet.connectToNode(node: node); - // await wallet.startSync(); - // syncingWallets.add(wallet); - // } + /// if the user chose to sync only active wallet + /// if the current wallet is monero; sync it only + if (typeRaw == WalletType.monero.index || typeRaw == WalletType.wownero.index) { + final name = getIt.get().getString(PreferencesKey.currentWalletName); + final wallet = await walletLoadingService.load(WalletType.values[typeRaw!], name!); + final node = getIt.get().getCurrentNode(WalletType.values[typeRaw]); + await wallet.connectToNode(node: node); + await wallet.startSync(); + syncingWallets.add(wallet); + } } print("STARTING SYNC TIMER"); _syncTimer?.cancel(); _syncTimer = Timer.periodic(const Duration(milliseconds: 2000), (timer) { - // final wallet = getIt.get().wallet; if (syncingWallets.isEmpty) { return; } - // final wallet = syncingWallets.first!; - // final syncProgress = ((wallet.syncStatus.progress() ?? 0) * 100).toStringAsPrecision(5); - // String title = "${wallet!.name} ${syncProgress}% Synced"; - // flutterLocalNotificationsPlugin.show( - // notificationId, - // title, - // 'Background sync - ${DateTime.now()}', - // const NotificationDetails( - // android: AndroidNotificationDetails( - // notificationChannelId, - // notificationChannelName, - // icon: 'ic_bg_service_small', - // ongoing: true, - // ), - // ), - // ); - for (int i = 0; i < syncingWallets.length; i++) { final wallet = syncingWallets[i]; final syncProgress = ((wallet!.syncStatus.progress()) * 100).toStringAsPrecision(5); - String title = "${wallet.name}-${syncProgress}% Synced"; + String title = "${wallet.name} - ${wallet.type}"; + late String content; + try { + final blocksLeft = (wallet.syncStatus as SyncingSyncStatus).blocksLeft; + content = "${blocksLeft} Blocks Left"; + } catch (e) { + print(e); + content = "${wallet.name} ${syncProgress}% Synced"; + } + content += " - ${DateTime.now()}"; flutterLocalNotificationsPlugin.show( notificationId + i, title, - 'Background sync - ${DateTime.now()}', + content, NotificationDetails( android: AndroidNotificationDetails( "${notificationChannelId}_$i", @@ -274,7 +293,7 @@ Future initializeService(FlutterBackgroundService bgService, bool useNotif isForegroundMode: true, notificationChannelId: notificationChannelId, initialNotificationTitle: initialNotificationTitle, - initialNotificationContent: initialNotificationContent, + initialNotificationContent: standbyMessage, foregroundServiceNotificationId: notificationId, foregroundServiceTypes: [AndroidForegroundType.dataSync], ), @@ -291,34 +310,40 @@ class BackgroundTasks { FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - void updateServiceState(bool foreground, bool useNotifications) async { - if (foreground) { - bgService.invoke('stopService'); - await Future.delayed(const Duration(seconds: 2)); - initializeService(bgService, useNotifications); - } else { - bgService.invoke("setBackground"); + void serviceBackground() { + bgService.invoke("setBackground"); + } + + Future serviceForeground() async { + final settingsStore = getIt.get(); + bool showNotifications = settingsStore.showSyncNotification; + bgService.invoke('stopService'); + await Future.delayed(const Duration(seconds: 2)); + initializeService(bgService, showNotifications); + } + + void serviceReady() { + final settingsStore = getIt.get(); + bool showNotifications = settingsStore.showSyncNotification; + if (showNotifications) { + bgService.invoke('setReady'); } } void registerBackgroundService() async { + print("REGISTER BACKGROUND SERVICE"); try { - bool hasMonero = getIt - .get() - .wallets - .any((element) => element.type == WalletType.monero); - - bool hasLitecoin = getIt - .get() - .wallets - .any((element) => element.type == WalletType.litecoin); - - bool hasBitcoin = getIt - .get() - .wallets - .any((element) => element.type == WalletType.bitcoin); - final settingsStore = getIt.get(); + final walletListViewModel = getIt.get(); + bool hasMonero = + walletListViewModel.wallets.any((element) => element.type == WalletType.monero); + + bool hasLitecoin = + walletListViewModel.wallets.any((element) => element.type == WalletType.litecoin); + + bool hasBitcoin = + walletListViewModel.wallets.any((element) => element.type == WalletType.bitcoin); + if (!settingsStore.silentPaymentsAlwaysScan) { hasBitcoin = false; } diff --git a/lib/entities/load_current_wallet.dart b/lib/entities/load_current_wallet.dart index ede06de4d..b6296621d 100644 --- a/lib/entities/load_current_wallet.dart +++ b/lib/entities/load_current_wallet.dart @@ -21,5 +21,6 @@ Future loadCurrentWallet({String? password}) async { final walletLoadingService = getIt.get(); final wallet = await walletLoadingService.load(type, name, password: password); await appStore.changeCurrentWallet(wallet); - getIt.get().registerBackgroundService(); + // TODO: re-enable (need to figure out how to prevent current wallet from being loaded in the background service!) + // getIt.get().registerBackgroundService(); } diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 56456e1e0..51d3c864c 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -163,11 +163,10 @@ class RootState extends State with WidgetsBindingObserver { } // background service handling: - bool showNotifications = getIt.get().showSyncNotification; switch (state) { case AppLifecycleState.resumed: // restart the background service if it was running before: - getIt.get().updateServiceState(true, showNotifications); + getIt.get().serviceForeground(); _stateTimer?.cancel(); if (!wasInBackground) { return; @@ -182,17 +181,19 @@ class RootState extends State with WidgetsBindingObserver { break; case AppLifecycleState.paused: // TODO: experimental: maybe should uncomment this: - // getIt.get().updateServiceState(false, showNotifications); + // getIt.get().serviceBackground(false, showNotifications); case AppLifecycleState.inactive: case AppLifecycleState.detached: default: + // anything other than resumed update the notification to say we're in the "ready" state: + getIt.get().serviceReady(); // if we enter any state other than resumed start a timer for 30 seconds // after which we'll consider the app to be in the background _stateTimer?.cancel(); // TODO: bump this to > 30 seconds when testing is done: _stateTimer = Timer(const Duration(seconds: 10), () async { wasInBackground = true; - getIt.get().updateServiceState(false, showNotifications); + getIt.get().serviceBackground(); }); break; }