dev
dart 238 lines 7.66 KB
Raw
1 import 'dart:async';
2 import 'package:cw_core/node.dart';
3 import 'package:cw_core/wallet_type.dart';
4 import 'package:cake_wallet/store/app_store.dart';
5 import 'package:cake_wallet/store/settings_store.dart';
6 import 'package:cake_wallet/utils/feature_flag.dart';
7 import 'package:cw_core/utils/print_verbose.dart';
8 import 'package:hive/hive.dart';
9 import 'package:connectivity_plus/connectivity_plus.dart';
10 import 'package:cake_wallet/evm/evm.dart';
11 import 'package:cake_wallet/reactions/wallet_connect.dart';
12
13 class NodeSwitchingService {
14 NodeSwitchingService({
15 required this.appStore,
16 required this.settingsStore,
17 });
18
19 static const int _healthCheckIntervalSeconds = 30;
20
21 // Maximum number of node switching attempts per session
22 static const int _maxNodeSwitchingAttempts = 5;
23
24 // Cooldown period between node switching attempts (in seconds)
25 static const int _nodeSwitchingCooldownSeconds = 15;
26
27 int _switchingAttempts = 0;
28 DateTime? _lastSwitchingAttempt;
29 bool _hasExhaustedAllNodes = false;
30
31 String walletName = '';
32
33 Timer? _healthCheckTimer;
34
35 bool _isSwitching = false;
36 bool get isSwitching => _isSwitching;
37
38 final AppStore appStore;
39 final SettingsStore settingsStore;
40
41 final Map<WalletType, List<dynamic>> _usedNodeKeys = {};
42
43 void startHealthCheckTimer() {
44 _healthCheckTimer?.cancel();
45 _healthCheckTimer = Timer.periodic(
46 Duration(seconds: _healthCheckIntervalSeconds),
47 (_) => performHealthCheck(),
48 );
49
50 performHealthCheck();
51 }
52
53 void stopHealthCheckTimer() {
54 _healthCheckTimer?.cancel();
55 _healthCheckTimer = null;
56 }
57
58 Future<void> performHealthCheck() async {
59 if (appStore.wallet == null) return;
60
61 if (!FeatureFlag.isAutomaticNodeSwitchingEnabled ||
62 !settingsStore.enableAutomaticNodeSwitching) {
63 return;
64 }
65
66 if (_isSwitching) return;
67
68 // Reset counters when wallet changes
69 if (walletName.isNotEmpty && walletName != appStore.wallet!.name) {
70 _resetSwitchingState();
71 }
72 walletName = appStore.wallet!.name;
73
74 // Check if we've exhausted all switching attempts
75 if (_hasExhaustedAllNodes) {
76 printV('Node switching exhausted for wallet: $walletName. Skipping health check.');
77 return;
78 }
79
80 // Check cooldown period
81 if (_lastSwitchingAttempt != null) {
82 final timeSinceLastAttempt = DateTime.now().difference(_lastSwitchingAttempt!);
83 if (timeSinceLastAttempt.inSeconds < _nodeSwitchingCooldownSeconds) {
84 printV('Node switching in cooldown period. Skipping health check.');
85 return;
86 }
87 }
88
89 try {
90 final connectivityResult = await Connectivity().checkConnectivity();
91 if (connectivityResult == ConnectivityResult.none) {
92 printV('No network connectivity detected. Skipping node health check.');
93 return;
94 }
95
96 final isHealthy = await appStore.wallet!.checkNodeHealth();
97
98 if (!isHealthy) {
99 printV('Node health check failed. Attempting to switch to next trusted node.');
100 await _switchToNextTrustedNode();
101 } else {
102 // Reset switching attempts on successful health check
103 _switchingAttempts = 0;
104 _hasExhaustedAllNodes = false;
105 printV('Node health check passed. Current node is healthy.');
106 }
107 } catch (e) {
108 printV('Error during health check: $e');
109 }
110 }
111
112 /// Find an active node from the provided list, checking only unused nodes
113 /// Marks inactive nodes as used to avoid retrying them
114 Future<Node?> _findActiveNode(
115 List<Node> nodes,
116 WalletType walletType,
117 ) async {
118 for (final node in nodes) {
119 if (!_usedNodeKeys[walletType]!.contains(node.id)) {
120 final isActive = await node.requestNode();
121 if (isActive) {
122 return node;
123 } else {
124 printV('Node ${node.uriRaw} is not active. Marking as used.');
125 _usedNodeKeys[walletType]!.add(node.id);
126 }
127 }
128 }
129 return null;
130 }
131
132 /// Switch to the next available trusted node
133 Future<void> _switchToNextTrustedNode() async {
134 _isSwitching = true;
135
136 try {
137 // Check if we've exceeded maximum switching attempts
138 if (_switchingAttempts >= _maxNodeSwitchingAttempts) {
139 printV('Maximum node switching attempts ($_maxNodeSwitchingAttempts) reached. '
140 'Disabling automatic switching.');
141 _hasExhaustedAllNodes = true;
142 return;
143 }
144
145 final wallet = appStore.wallet!;
146 final walletType = wallet.type;
147
148 WalletType nodeWalletType = walletType;
149
150 int? chainId;
151 if (isEVMCompatibleChain(walletType)) {
152 chainId = evm!.getSelectedChainId(appStore.wallet!);
153 }
154
155 final currentNode = settingsStore.getCurrentNode(nodeWalletType, chainId: chainId);
156
157 // Get all trusted nodes for this wallet type
158 final trustedNodes = (await Node.getAll())
159 .where((node) => node.type == nodeWalletType && node.isEnabledForAutoSwitching)
160 .toList();
161
162 if (trustedNodes.isEmpty) {
163 printV('No trusted nodes available for switching');
164 _hasExhaustedAllNodes = true;
165 return;
166 }
167
168 // Initialize used nodes list for this wallet type if it does not exist
169 _usedNodeKeys.putIfAbsent(nodeWalletType, () => []);
170
171 // Add current node to used list if not already there
172 if (!_usedNodeKeys[nodeWalletType]!.contains(currentNode.id)) {
173 _usedNodeKeys[nodeWalletType]!.add(currentNode.id);
174 }
175
176 // Try to find an active unused node
177 Node? nextNode = await _findActiveNode(trustedNodes, nodeWalletType);
178
179 // If all trusted nodes have been used, check if we should reset
180 if (nextNode == null) {
181 printV('All trusted nodes have been tried for wallet type: $nodeWalletType');
182
183 // If we've tried all nodes and still haven't reached max attempts, reset and try again
184 if (_switchingAttempts < _maxNodeSwitchingAttempts) {
185 printV('Resetting used nodes list and trying again');
186 _usedNodeKeys[nodeWalletType]!.clear();
187 // Try again with cleared used list
188 nextNode = await _findActiveNode(trustedNodes, nodeWalletType);
189 }
190
191 // If still no active node found, we give up
192 if (nextNode == null) {
193 printV('No active nodes available for switching after checking all nodes.');
194 _hasExhaustedAllNodes = true;
195 return;
196 }
197 }
198
199 // Ensure the selected node is marked as used
200 if (!_usedNodeKeys[nodeWalletType]!.contains(nextNode.id)) {
201 _usedNodeKeys[nodeWalletType]!.add(nextNode.id);
202 }
203
204 printV(
205 'Switching from ${currentNode.uriRaw} to ${nextNode.uriRaw} (attempt $_switchingAttempts/$_maxNodeSwitchingAttempts)');
206 printV('Used nodes for ${nodeWalletType}: ${_usedNodeKeys[nodeWalletType]}');
207
208 // Update the current node in settings
209 settingsStore.nodes[nodeWalletType] = nextNode;
210
211 // Connect the wallet to the new node
212 await appStore.wallet!.connectToNode(node: nextNode);
213
214 await appStore.wallet!.startSync();
215
216 printV('Successfully switched to node: ${nextNode.uriRaw}');
217 } catch (e) {
218 printV('Error switching to next trusted node: $e');
219 } finally {
220 // Increment switching attempts counter on every attempt
221 _switchingAttempts++;
222 _lastSwitchingAttempt = DateTime.now();
223 _isSwitching = false;
224 }
225 }
226
227 /// Reset switching state when wallet changes
228 void _resetSwitchingState() {
229 _switchingAttempts = 0;
230 _lastSwitchingAttempt = null;
231 _hasExhaustedAllNodes = false;
232 _usedNodeKeys.clear();
233 printV('Node switching state reset for new wallet');
234 }
235
236 /// Check if automatic node switching is currently disabled due to exhaustion
237 bool get isAutomaticSwitchingDisabled => _hasExhaustedAllNodes;
238 }