import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:path_provider/path_provider.dart'; part 'wallet_sync_exporter.g.dart'; class WalletSyncExporter = WalletSyncExporterBase with _$WalletSyncExporter; abstract class WalletSyncExporterBase with Store { WalletSyncExporterBase(); static const String _exportPathKey = 'wallet_sync_export_path'; static const String _exportIntervalKey = 'wallet_sync_export_interval'; final walletLoadingService = getIt.get(); final walletListViewModel = getIt.get(); final settingsStore = getIt.get(); final keyService = getIt.get(); @observable Timer? syncTimer; @observable bool isTimerActive = false; @observable String exportPath = ''; @observable int exportIntervalMinutes = 30; @observable bool isSyncing = false; @observable String statusMessage = ''; @observable int progress = 0; @observable int totalWallets = 0; @observable int currentWalletIndex = 0; @observable String lastExportTime = ''; @observable Map exportData = {}; @observable String lastError = ''; Future initialize() async { final prefs = await SharedPreferences.getInstance(); exportPath = prefs.getString(_exportPathKey) ?? await _getDefaultExportPath(); exportIntervalMinutes = prefs.getInt(_exportIntervalKey) ?? 30; } Future _getDefaultExportPath() async { final directory = await getApplicationDocumentsDirectory(); return '${directory.path}/wallet_export.json'; } Future setExportPath(String path) async { exportPath = path; final prefs = await SharedPreferences.getInstance(); await prefs.setString(_exportPathKey, exportPath); } Future setExportInterval(int minutes) async { exportIntervalMinutes = minutes; final prefs = await SharedPreferences.getInstance(); await prefs.setInt(_exportIntervalKey, exportIntervalMinutes); if (isTimerActive) { stopPeriodicSync(); startPeriodicSync(); } } void startPeriodicSync() { stopPeriodicSync(); syncTimer = Timer.periodic( Duration(minutes: exportIntervalMinutes), (_) => syncAndExport(), ); isTimerActive = true; statusMessage = 'Periodic sync started (every $exportIntervalMinutes minutes)'; } void stopPeriodicSync() { syncTimer?.cancel(); syncTimer = null; isTimerActive = false; statusMessage = 'Periodic sync stopped'; } Future syncAndExport() async { if (isSyncing) { statusMessage = 'Sync already in progress'; return; } try { isSyncing = true; statusMessage = 'Starting sync and export process...'; final wallets = walletListViewModel.wallets; totalWallets = wallets.length; currentWalletIndex = 0; progress = 0; final Map newExportData = { 'timestamp': DateTime.now().toIso8601String(), 'wallets': >[], }; for (final walletItem in wallets) { currentWalletIndex++; progress = ((currentWalletIndex / totalWallets) * 100).round(); statusMessage = 'Processing wallet ${currentWalletIndex}/$totalWallets: ${walletItem.name}'; try { final wallet = await walletLoadingService.load(walletItem.type, walletItem.name); lastError = ''; await _syncWallet(wallet); final walletData = await _collectWalletData(wallet); (newExportData['wallets'] as List).add({ ...walletData, if (lastError.isNotEmpty) 'error': lastError, }); await wallet.close(shouldCleanup: true); } catch (e) { (newExportData['wallets'] as List).add({ 'name': walletItem.name, 'type': walletItem.type.toString(), 'error': e.toString(), }); statusMessage = 'Error processing ${walletItem.name}: $e'; } } exportData = newExportData; await _saveJsonToFile(exportData); lastExportTime = DateTime.now().toString(); statusMessage = 'Export completed successfully'; } catch (e) { statusMessage = 'Export failed: $e'; } finally { isSyncing = false; } } Future _syncWallet(WalletBase wallet) async { final node = settingsStore.getCurrentNode(wallet.type); await wallet.connectToNode(node: node); await wallet.startSync(); int stuckTicks = 0; bool isSynced = false; int tick = 0; while (!isSynced && stuckTicks < 30) { tick++; final syncStatus = wallet.syncStatus; if (syncStatus is AttemptingSyncStatus || syncStatus is NotConnectedSyncStatus || syncStatus is ConnectedSyncStatus) { statusMessage = 'Syncing ${wallet.name}: ${syncStatus.toString()} (stuckTicks: $stuckTicks/$tick)'; stuckTicks++; } else { stuckTicks = 0; } if (syncStatus is SyncedSyncStatus || syncStatus.progress() > 0.999) { isSynced = true; } statusMessage = 'Syncing ${wallet.name}: ${(syncStatus.progress() * 100).round()}% (tick: $tick)'; if (tick > 30) { lastError = 'Syncing ${wallet.name} takes $tick ticks, node: ${node.uriRaw}'; } if (tick > 150) { lastError = 'Syncing ${wallet.name} failed, timeout after 150 ticks, node: ${node.uriRaw}'; break; } await Future.delayed(const Duration(seconds: 1)); } } Future> _collectWalletData(WalletBase wallet) async { await wallet.walletAddresses.init(); await wallet.walletAddresses.updateAddressesInBox(); final Map walletData = { 'name': wallet.name, 'type': wallet.type.toString(), 'balance': Map.fromEntries( wallet.balance.entries.map((entry) { final currency = entry.key; final balance = entry.value; return MapEntry(currency.toString(), { 'formattedAvailableBalance': balance.formattedAvailableBalance, 'formattedAdditionalBalance': balance.formattedAdditionalBalance, 'formattedUnAvailableBalance': balance.formattedUnAvailableBalance, 'formattedSecondAvailableBalance': balance.formattedSecondAvailableBalance, 'formattedSecondAdditionalBalance': balance.formattedSecondAdditionalBalance, 'formattedFullAvailableBalance': balance.formattedFullAvailableBalance, }); }), ), 'transactions': >[], 'addresses': { 'primary': wallet.walletAddresses.address, 'all': wallet.walletAddresses.addressesMap, } }; final transactions = wallet.transactionHistory.transactions.values; for (final tx in transactions) { dynamic raw; try { raw = json.decode(json.encode(tx)); } catch (e) { raw = {}; } walletData['transactions'].add({ 'id': tx.id, 'amount': tx.amount.toString(), 'fee': tx.fee?.toString(), 'date': tx.date.toIso8601String(), 'direction': tx.direction.toString(), 'confirmations': tx.confirmations, 'isPending': tx.isPending, 'raw': raw, }); } return walletData; } Future _saveJsonToFile(Map data) async { try { final file = File(exportPath); final jsonStr = JsonEncoder.withIndent(' ').convert(data); await file.writeAsString(jsonStr); statusMessage = 'Saved to: $exportPath'; } catch (e) { statusMessage = 'Failed to save: $e'; throw e; } } }