Skip to content

Commit 9d95fd0

Browse files
Check v4 offline nodes (#1053)
* WIP: implement checkeing for v4 nodes * fix bug in registrar client && make background service check on v4 nodes * fix v4 checking * change local notifications pkg to awesome package * fix remove notification from notifications stack in android * fix bug in Podfile * Update app/lib/services/notification_service.dart Co-authored-by: AhmedHanafy725 <41957921+AhmedHanafy725@users.noreply.github.com> --------- Co-authored-by: AhmedHanafy725 <41957921+AhmedHanafy725@users.noreply.github.com>
1 parent 182b5d5 commit 9d95fd0

10 files changed

Lines changed: 331 additions & 158 deletions

File tree

app/ios/Podfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ post_install do |installer|
3838
installer.pods_project.targets.each do |target|
3939
flutter_additional_ios_build_settings(target)
4040
end
41+
4142
installer.pods_project.targets.each do |target|
4243
if target.name == "lottie-ios"
4344
target.build_configurations.each do |config|

app/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,9 @@
390390
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
391391
buildSettings = {
392392
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
393+
APPLICATION_EXTENSION_API_ONLY = NO;
393394
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
395+
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
394396
CLANG_ENABLE_MODULES = YES;
395397
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
396398
CURRENT_PROJECT_VERSION = 191;
@@ -535,7 +537,9 @@
535537
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
536538
buildSettings = {
537539
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
540+
APPLICATION_EXTENSION_API_ONLY = NO;
538541
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
542+
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
539543
CLANG_ENABLE_MODULES = YES;
540544
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
541545
CURRENT_PROJECT_VERSION = 191;
@@ -570,7 +574,9 @@
570574
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
571575
buildSettings = {
572576
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
577+
APPLICATION_EXTENSION_API_ONLY = NO;
573578
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
579+
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
574580
CLANG_ENABLE_MODULES = YES;
575581
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
576582
CURRENT_PROJECT_VERSION = 191;

app/ios/Runner/AppDelegate.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import UIKit
22
import Flutter
3-
import flutter_local_notifications
43

54
@UIApplicationMain
65
@objc class AppDelegate: FlutterAppDelegate {
@@ -12,9 +11,6 @@ import flutter_local_notifications
1211
_ application: UIApplication,
1312
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
1413
) -> Bool {
15-
FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in
16-
GeneratedPluginRegistrant.register(with: registry)
17-
}
1814

1915
if #available(iOS 10.0, *) {
2016
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate

app/lib/screens/app_lifecycle_observer.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
33
import 'package:threebotlogin/helpers/logger.dart';
44
import 'package:threebotlogin/main.dart';
55
import 'package:threebotlogin/screens/home_screen.dart';
6+
import 'package:threebotlogin/services/notification_service.dart';
67

78
class AppLifecycleObserver extends ConsumerStatefulWidget {
89
final Widget child;
@@ -47,6 +48,11 @@ class _AppLifecycleObserverState extends ConsumerState<AppLifecycleObserver>
4748
ref.read(lastPausedProvider.notifier).state = now;
4849
logger.i('App Paused. Last paused time updated: $now');
4950
} else if (state == AppLifecycleState.resumed) {
51+
logger.i('App Resumed. Notifying notification service.');
52+
WidgetsBinding.instance.addPostFrameCallback((_) {
53+
NotificationService().onAppResumed();
54+
});
55+
5056
if (now - lastPaused >= pinCheckTimeout) {
5157
logger.i('Timeout expired. Requiring authentication.');
5258
// Navigate to Home Screen

app/lib/screens/farm_screen.dart

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -178,26 +178,34 @@ class _FarmScreenState extends ConsumerState<FarmScreen>
178178
await registrarClient!.accounts.getByPublicKey(publicKey);
179179
final farms = await registrarClient!.farms
180180
.list(registrarFarm.FarmFilter(twinID: account.twinID));
181-
final nodes = await registrarClient!.nodes
182-
.list(registrarNode.NodeFilter(twinID: account.twinID));
183181

184-
v4Farms.addAll(farms.map((f) => Farm(
185-
name: f.farmName,
186-
walletAddress: f.stellarAddress!,
187-
tfchainWalletSecret: w.tfchainSecret,
188-
walletName: w.name,
189-
twinId: f.twinID,
190-
farmId: f.farmID!,
191-
nodes: nodes
192-
.where((n) => n.farmID == f.farmID)
193-
.map((n) => Node(
194-
nodeId: n.nodeID,
195-
status: NodeStatus.Up,
196-
country: n.location.country,
197-
uptime:
198-
(n.uptime.isNotEmpty) ? n.uptime.last.duration : 0))
199-
.toList(),
200-
)));
182+
for (var f in farms) {
183+
final farmNodes = await registrarClient!.nodes
184+
.list(registrarNode.NodeFilter(farmID: f.farmID));
185+
final nodes = farmNodes.map((n) {
186+
// Convert ISO timestamp to Unix timestamp
187+
final lastSeenDate = DateTime.parse(n.lastSeen);
188+
final unixTimestamp = lastSeenDate.millisecondsSinceEpoch ~/ 1000;
189+
190+
return Node(
191+
nodeId: n.nodeID,
192+
status: n.online ? NodeStatus.Up : NodeStatus.Down,
193+
country: n.location.country,
194+
uptime: (n.uptime.isNotEmpty) ? n.uptime.last.duration : 0,
195+
updatedAt: unixTimestamp,
196+
);
197+
}).toList();
198+
199+
v4Farms.addAll(farms.map((f) => Farm(
200+
name: f.farmName,
201+
walletAddress: f.stellarAddress!,
202+
tfchainWalletSecret: w.tfchainSecret,
203+
walletName: w.name,
204+
twinId: f.twinID,
205+
farmId: f.farmID!,
206+
nodes: nodes,
207+
)));
208+
}
201209
} catch (e) {
202210
continue;
203211
}

app/lib/services/background_service.dart

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async {
1616
final bool notificationsEnabled = await isNodeStatusNotificationEnabled();
1717

1818
logger.i(
19-
'Background Fetch Headless Task: $taskId, Notifications Enabled: $notificationsEnabled');
19+
'[BackgroundFetch] Headless Task: $taskId, Notifications Enabled: $notificationsEnabled');
2020

2121
if (!notificationsEnabled) {
2222
logger.i(
@@ -29,9 +29,18 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async {
2929

3030
Future<void> checkNodeStatus(String taskId) async {
3131
try {
32-
final offlineNodes = await NodeCheckService.pingNodesInBackground();
32+
final v3OfflineNodes = await NodeCheckService.pingV3NodesInBackground();
33+
final v4OfflineNodes = await NodeCheckService.pingV4NodesInBackground();
34+
final offlineNodes = [...v3OfflineNodes, ...v4OfflineNodes];
3335

34-
if (offlineNodes.isEmpty) return;
36+
logger.i(
37+
'[BackgroundFetch] Total offline nodes found: ${offlineNodes.length} for task $taskId');
38+
39+
if (offlineNodes.isEmpty) {
40+
logger.i(
41+
'[BackgroundFetch] No offline nodes found, finishing task $taskId');
42+
return;
43+
}
3544

3645
final StringBuffer bodyBuffer = StringBuffer();
3746
final List<Node> nodesToNotify = [];
@@ -43,7 +52,11 @@ Future<void> checkNodeStatus(String taskId) async {
4352
for (final node in offlineNodes) {
4453
final nodeUpdatedAtMs = node.updatedAt! * 1000;
4554

46-
if (nodeUpdatedAtMs <= sevenDaysAgoTimestampMs) continue;
55+
if (nodeUpdatedAtMs <= sevenDaysAgoTimestampMs) {
56+
logger.i(
57+
'[BackgroundFetch] Skipping node ${node.nodeId} - offline for more than 7 days');
58+
continue;
59+
}
4760

4861
final downtime = Duration(milliseconds: nowInMs - nodeUpdatedAtMs);
4962

@@ -62,7 +75,11 @@ Future<void> checkNodeStatus(String taskId) async {
6275
}
6376
}
6477

65-
if (nodesToNotify.isEmpty) return;
78+
if (nodesToNotify.isEmpty) {
79+
logger.i(
80+
'[BackgroundFetch] No nodes to notify after interval check, finishing task $taskId');
81+
return;
82+
}
6683

6784
await NotificationService().showNotification(
6885
id: nodesToNotify.hashCode,
@@ -73,8 +90,9 @@ Future<void> checkNodeStatus(String taskId) async {
7390
groupKey: 'offline_nodes',
7491
);
7592
} catch (e) {
76-
logger.e('Error in checkNodeStatus for task $taskId: $e');
93+
logger.e('[BackgroundFetch] Error in checkNodeStatus for task $taskId: $e');
7794
} finally {
95+
logger.i('[BackgroundFetch] Finishing task $taskId');
7896
BackgroundFetch.finish(taskId);
7997
}
8098
}

app/lib/services/nodes_check_service.dart

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:threebotlogin/helpers/globals.dart';
23
import 'package:threebotlogin/helpers/logger.dart';
34
import 'package:threebotlogin/models/farm.dart';
45
import 'package:threebotlogin/models/wallet.dart';
56
import 'package:threebotlogin/providers/wallets_provider.dart';
7+
import 'package:threebotlogin/services/crypto_service.dart';
68
import 'package:threebotlogin/services/gridproxy_service.dart';
79
import 'package:threebotlogin/services/tfchain_service.dart';
10+
import 'package:registrar_client/models/farm.dart' as registrarFarm;
11+
import 'package:registrar_client/models/node.dart' as registrarNode;
12+
import 'package:registrar_client/registrar_client.dart' as registrar;
813

914
class NodeCheckService {
10-
static Future<List<Node>> pingNodesInBackground() async {
15+
static Future<List<Node>> pingV3NodesInBackground() async {
1116
final container = ProviderContainer();
1217
try {
1318
final walletsNotifierInstance = container.read(walletsNotifier.notifier);
@@ -41,16 +46,81 @@ class NodeCheckService {
4146
.toList();
4247
allNodes.addAll(nodes);
4348
}
44-
final offlineNodes =
49+
final offlineNodes =
4550
allNodes.where((n) => n.status != NodeStatus.Up).toList();
4651

4752
return offlineNodes;
48-
4953
} catch (e) {
5054
logger.e('[NodeCheckService] Error: $e');
5155
return [];
5256
} finally {
5357
container.dispose();
5458
}
5559
}
60+
61+
static Future<List<Node>> pingV4NodesInBackground() async {
62+
final container = ProviderContainer();
63+
final List<Node> allOfflineNodes = [];
64+
65+
try {
66+
final walletsNotifierInstance = container.read(walletsNotifier.notifier);
67+
68+
await walletsNotifierInstance.waitUntilListed();
69+
70+
final List<Wallet> wallets = container.read(walletsNotifier);
71+
72+
if (wallets.isEmpty) {
73+
logger.i('[NodeCheckService] No wallets found');
74+
return [];
75+
}
76+
77+
registrar.RegistrarClient? registrarClient = registrar.RegistrarClient(
78+
baseUrl: Globals().registrarURL,
79+
mnemonicOrSeed: wallets.first.tfchainSecret);
80+
for (var w in wallets) {
81+
try {
82+
final publicKey = await derivePublicKey(w.tfchainSecret);
83+
final account =
84+
await registrarClient.accounts.getByPublicKey(publicKey);
85+
final farms = await registrarClient.farms
86+
.list(registrarFarm.FarmFilter(twinID: account.twinID));
87+
88+
for (var f in farms) {
89+
final farmNodes = await registrarClient.nodes
90+
.list(registrarNode.NodeFilter(farmID: f.farmID));
91+
92+
final nodes = farmNodes.map((n) {
93+
// Convert ISO timestamp to Unix timestamp
94+
final lastSeenDate = DateTime.parse(n.lastSeen);
95+
final unixTimestamp = lastSeenDate.millisecondsSinceEpoch ~/ 1000;
96+
97+
return Node(
98+
nodeId: n.nodeID,
99+
status: n.online ? NodeStatus.Up : NodeStatus.Down,
100+
country: n.location.country,
101+
uptime: (n.uptime.isNotEmpty) ? n.uptime.last.duration : 0,
102+
updatedAt: unixTimestamp,
103+
);
104+
}).toList();
105+
106+
final offlineNodes =
107+
nodes.where((n) => n.status != NodeStatus.Up).toList();
108+
allOfflineNodes.addAll(offlineNodes);
109+
}
110+
} catch (e) {
111+
logger.e('[NodeCheckService] Error: $e');
112+
continue;
113+
}
114+
}
115+
116+
logger.i(
117+
'[NodeCheckService] Found ${allOfflineNodes.length} offline nodes');
118+
return allOfflineNodes;
119+
} catch (e) {
120+
logger.e('[NodeCheckService] Error in pingV4NodesInBackground: $e');
121+
return [];
122+
} finally {
123+
container.dispose();
124+
}
125+
}
56126
}

0 commit comments

Comments
 (0)