mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
269 lines
8.1 KiB
Dart
269 lines
8.1 KiB
Dart
|
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<WalletLoadingService>();
|
||
|
final walletListViewModel = getIt.get<WalletListViewModel>();
|
||
|
final settingsStore = getIt.get<SettingsStore>();
|
||
|
final keyService = getIt.get<KeyService>();
|
||
|
|
||
|
@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<String, dynamic> exportData = {};
|
||
|
|
||
|
@observable
|
||
|
String lastError = '';
|
||
|
|
||
|
Future<void> initialize() async {
|
||
|
final prefs = await SharedPreferences.getInstance();
|
||
|
exportPath = prefs.getString(_exportPathKey) ?? await _getDefaultExportPath();
|
||
|
exportIntervalMinutes = prefs.getInt(_exportIntervalKey) ?? 30;
|
||
|
}
|
||
|
|
||
|
Future<String> _getDefaultExportPath() async {
|
||
|
final directory = await getApplicationDocumentsDirectory();
|
||
|
return '${directory.path}/wallet_export.json';
|
||
|
}
|
||
|
|
||
|
Future<void> setExportPath(String path) async {
|
||
|
exportPath = path;
|
||
|
final prefs = await SharedPreferences.getInstance();
|
||
|
await prefs.setString(_exportPathKey, exportPath);
|
||
|
}
|
||
|
|
||
|
Future<void> 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<void> 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<String, dynamic> newExportData = {
|
||
|
'timestamp': DateTime.now().toIso8601String(),
|
||
|
'wallets': <Map<String, dynamic>>[],
|
||
|
};
|
||
|
|
||
|
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<void> _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<Map<String, dynamic>> _collectWalletData(WalletBase wallet) async {
|
||
|
await wallet.walletAddresses.init();
|
||
|
await wallet.walletAddresses.updateAddressesInBox();
|
||
|
final Map<String, dynamic> 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': <Map<String, dynamic>>[],
|
||
|
'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<void> _saveJsonToFile(Map<String, dynamic> 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;
|
||
|
}
|
||
|
}
|
||
|
}
|