Files
hash-wallet/lib/core/node_switching_service.dart
T
jwinterm 04203654f1 Rename Flutter project + binary: cake_wallet -> hash_wallet
The big one. Touches 760+ files. Locks in the user's decision to break
import-line compatibility with upstream cake-tech/cake_wallet in exchange
for a binary named hash_wallet on every platform.

Changes:
- pubspec_description.yaml: project name cake_wallet -> hash_wallet
- 762 Dart files: every 'package:cake_wallet/...' import rewritten to
  'package:hash_wallet/...' (lib/, cw_*/lib/, tool/, test/, integration_test/)
- linux/CMakeLists.txt: BINARY_NAME cake_wallet -> hash_wallet
- windows/CMakeLists.txt: project + BINARY_NAME (CakeWallet -> HashWallet)
- windows/runner/Runner.rc: company / file / product strings to Hash Wallet
  + Such Software LLC copyright
- macos/Runner.xcodeproj/project_base.pbxproj: cake_wallet_new.app refs
  -> hash_wallet.app (macOS output bundle name)
- com.suchsoftware.HashWallet.yml (Flatpak): all binary path refs
  cake_wallet -> hash_wallet (e.g. /app/hash_wallet/hash_wallet)
- linux/com.suchsoftware.HashWallet.desktop: Exec=hash_wallet
- scripts/android/copy_monero_deps.sh: CW_DIR target path
- configure_hash_wallet.sh: error message typo (old filename)

INTENTIONALLY UNCHANGED:
- cw_* subpackage names (cw_core, cw_bitcoin, cw_wownero, etc.) — they
  remain as the historical 'cake wallet' naming; renaming them would
  cascade through every sub-package's internal structure
- Method channel name 'com.cake_wallet/native_utils' in iOS/macOS/Android
  AppDelegates and MainActivity — it's a string identifier that has to
  match the Dart side exactly. Renaming requires both-sides change; it's
  internal-only and not user-visible.
- ghcr.io/cake-tech/cake_wallet:debian13-... docker image references in
  build scripts — that's Cake Labs' published image we use as the build
  environment, not ours.

User data directory now follows the new binary name:
  ~/.local/share/hash_wallet/  (was ~/.local/share/cake_wallet/)
  ~/.config/hash_wallet/       (was ~/.config/cake_wallet/)
Existing users on the old name will need migration if any exist.
2026-05-16 11:33:25 -04:00

241 lines
7.7 KiB
Dart

import 'dart:async';
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/utils/feature_flag.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:hive/hive.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
class NodeSwitchingService {
NodeSwitchingService({
required this.appStore,
required this.settingsStore,
required this.nodeSource,
});
static const int _healthCheckIntervalSeconds = 30;
// Maximum number of node switching attempts per session
static const int _maxNodeSwitchingAttempts = 5;
// Cooldown period between node switching attempts (in seconds)
static const int _nodeSwitchingCooldownSeconds = 15;
int _switchingAttempts = 0;
DateTime? _lastSwitchingAttempt;
bool _hasExhaustedAllNodes = false;
String walletName = '';
Timer? _healthCheckTimer;
bool _isSwitching = false;
bool get isSwitching => _isSwitching;
final AppStore appStore;
final SettingsStore settingsStore;
final Box<Node> nodeSource;
final Map<WalletType, List<dynamic>> _usedNodeKeys = {};
void startHealthCheckTimer() {
_healthCheckTimer?.cancel();
_healthCheckTimer = Timer.periodic(
Duration(seconds: _healthCheckIntervalSeconds),
(_) => performHealthCheck(),
);
performHealthCheck();
}
void stopHealthCheckTimer() {
_healthCheckTimer?.cancel();
_healthCheckTimer = null;
}
Future<void> performHealthCheck() async {
if (appStore.wallet == null) return;
if (!FeatureFlag.isAutomaticNodeSwitchingEnabled ||
!settingsStore.enableAutomaticNodeSwitching) {
return;
}
if (_isSwitching) return;
// Reset counters when wallet changes
if (walletName.isNotEmpty && walletName != appStore.wallet!.name) {
_resetSwitchingState();
}
walletName = appStore.wallet!.name;
// Check if we've exhausted all switching attempts
if (_hasExhaustedAllNodes) {
printV('Node switching exhausted for wallet: $walletName. Skipping health check.');
return;
}
// Check cooldown period
if (_lastSwitchingAttempt != null) {
final timeSinceLastAttempt = DateTime.now().difference(_lastSwitchingAttempt!);
if (timeSinceLastAttempt.inSeconds < _nodeSwitchingCooldownSeconds) {
printV('Node switching in cooldown period. Skipping health check.');
return;
}
}
try {
final connectivityResult = await Connectivity().checkConnectivity();
if (connectivityResult == ConnectivityResult.none) {
printV('No network connectivity detected. Skipping node health check.');
return;
}
final isHealthy = await appStore.wallet!.checkNodeHealth();
if (!isHealthy) {
printV('Node health check failed. Attempting to switch to next trusted node.');
await _switchToNextTrustedNode();
} else {
// Reset switching attempts on successful health check
_switchingAttempts = 0;
_hasExhaustedAllNodes = false;
printV('Node health check passed. Current node is healthy.');
}
} catch (e) {
printV('Error during health check: $e');
}
}
/// Find an active node from the provided list, checking only unused nodes
/// Marks inactive nodes as used to avoid retrying them
Future<Node?> _findActiveNode(
List<Node> nodes,
WalletType walletType,
) async {
for (final node in nodes) {
if (!_usedNodeKeys[walletType]!.contains(node.key)) {
final isActive = await node.requestNode();
if (isActive) {
return node;
} else {
printV('Node ${node.uriRaw} is not active. Marking as used.');
_usedNodeKeys[walletType]!.add(node.key);
}
}
}
return null;
}
/// Switch to the next available trusted node
Future<void> _switchToNextTrustedNode() async {
_isSwitching = true;
try {
// Check if we've exceeded maximum switching attempts
if (_switchingAttempts >= _maxNodeSwitchingAttempts) {
printV('Maximum node switching attempts ($_maxNodeSwitchingAttempts) reached. '
'Disabling automatic switching.');
_hasExhaustedAllNodes = true;
return;
}
final wallet = appStore.wallet!;
final walletType = wallet.type;
WalletType nodeWalletType = walletType;
int? chainId;
if (isEVMCompatibleChain(walletType)) {
chainId = evm!.getSelectedChainId(appStore.wallet!);
}
final currentNode = settingsStore.getCurrentNode(nodeWalletType, chainId: chainId);
// Get all trusted nodes for this wallet type
final trustedNodes = nodeSource.values
.where((node) => node.type == nodeWalletType && node.isEnabledForAutoSwitching)
.toList();
if (trustedNodes.isEmpty) {
printV('No trusted nodes available for switching');
_hasExhaustedAllNodes = true;
return;
}
// Initialize used nodes list for this wallet type if it does not exist
_usedNodeKeys.putIfAbsent(nodeWalletType, () => []);
// Add current node to used list if not already there
if (!_usedNodeKeys[nodeWalletType]!.contains(currentNode.key)) {
_usedNodeKeys[nodeWalletType]!.add(currentNode.key);
}
// Try to find an active unused node
Node? nextNode = await _findActiveNode(trustedNodes, nodeWalletType);
// If all trusted nodes have been used, check if we should reset
if (nextNode == null) {
printV('All trusted nodes have been tried for wallet type: $nodeWalletType');
// If we've tried all nodes and still haven't reached max attempts, reset and try again
if (_switchingAttempts < _maxNodeSwitchingAttempts) {
printV('Resetting used nodes list and trying again');
_usedNodeKeys[nodeWalletType]!.clear();
// Try again with cleared used list
nextNode = await _findActiveNode(trustedNodes, nodeWalletType);
}
// If still no active node found, we give up
if (nextNode == null) {
printV('No active nodes available for switching after checking all nodes.');
_hasExhaustedAllNodes = true;
return;
}
}
// Ensure the selected node is marked as used
if (!_usedNodeKeys[nodeWalletType]!.contains(nextNode.key)) {
_usedNodeKeys[nodeWalletType]!.add(nextNode.key);
}
printV(
'Switching from ${currentNode.uriRaw} to ${nextNode.uriRaw} (attempt $_switchingAttempts/$_maxNodeSwitchingAttempts)');
printV('Used nodes for ${nodeWalletType}: ${_usedNodeKeys[nodeWalletType]}');
// Update the current node in settings
settingsStore.nodes[nodeWalletType] = nextNode;
// Connect the wallet to the new node
await appStore.wallet!.connectToNode(node: nextNode);
await appStore.wallet!.startSync();
printV('Successfully switched to node: ${nextNode.uriRaw}');
} catch (e) {
printV('Error switching to next trusted node: $e');
} finally {
// Increment switching attempts counter on every attempt
_switchingAttempts++;
_lastSwitchingAttempt = DateTime.now();
_isSwitching = false;
}
}
/// Reset switching state when wallet changes
void _resetSwitchingState() {
_switchingAttempts = 0;
_lastSwitchingAttempt = null;
_hasExhaustedAllNodes = false;
_usedNodeKeys.clear();
printV('Node switching state reset for new wallet');
}
/// Check if automatic node switching is currently disabled due to exhaustion
bool get isAutomaticSwitchingDisabled => _hasExhaustedAllNodes;
}