mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
add balance watcher [skip slack]
This commit is contained in:
parent
87207c61ba
commit
f39b58e35d
9 changed files with 574 additions and 1 deletions
2
.github/workflows/pr_test_build_android.yml
vendored
2
.github/workflows/pr_test_build_android.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
name: Cake Wallet Android
|
name: Cake Wallet Android
|
||||||
|
|
||||||
on: [pull_request]
|
on: [push]
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
|
|
@ -26,6 +26,9 @@ class SolanaTransactionInfo extends TransactionInfo {
|
||||||
final double txFee;
|
final double txFee;
|
||||||
final TransactionDirection direction;
|
final TransactionDirection direction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get confirmations => 0;
|
||||||
|
|
||||||
String? _fiatAmount;
|
String? _fiatAmount;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -28,6 +28,9 @@ class TronTransactionInfo extends TransactionInfo {
|
||||||
final int? txFee;
|
final int? txFee;
|
||||||
final TransactionDirection direction;
|
final TransactionDirection direction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get confirmations => 0;
|
||||||
|
|
||||||
factory TronTransactionInfo.fromJson(Map<String, dynamic> data) {
|
factory TronTransactionInfo.fromJson(Map<String, dynamic> data) {
|
||||||
return TronTransactionInfo(
|
return TronTransactionInfo(
|
||||||
id: data['id'] as String,
|
id: data['id'] as String,
|
||||||
|
|
|
@ -36,8 +36,10 @@ import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||||
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
|
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
|
||||||
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
|
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
|
||||||
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
|
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/dev/wallet_sync_exporter_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/settings/background_sync_page.dart';
|
import 'package:cake_wallet/src/screens/settings/background_sync_page.dart';
|
||||||
import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
|
import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
|
||||||
|
import 'package:cake_wallet/view_model/dev/wallet_sync_exporter.dart';
|
||||||
import 'package:cake_wallet/view_model/link_view_model.dart';
|
import 'package:cake_wallet/view_model/link_view_model.dart';
|
||||||
import 'package:cake_wallet/tron/tron.dart';
|
import 'package:cake_wallet/tron/tron.dart';
|
||||||
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
|
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
|
||||||
|
@ -1452,5 +1454,8 @@ Future<void> setup({
|
||||||
|
|
||||||
getIt.registerFactory(() => DevMoneroBackgroundSyncPage(getIt.get<DevMoneroBackgroundSync>()));
|
getIt.registerFactory(() => DevMoneroBackgroundSyncPage(getIt.get<DevMoneroBackgroundSync>()));
|
||||||
getIt.registerFactory(() => DevMoneroCallProfilerPage());
|
getIt.registerFactory(() => DevMoneroCallProfilerPage());
|
||||||
|
getIt.registerFactory(() => WalletSyncExporterPage(getIt.get<WalletSyncExporter>()));
|
||||||
|
getIt.registerFactory(() => WalletSyncExporter());
|
||||||
|
|
||||||
_isSetupFinished = true;
|
_isSetupFinished = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
|
import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
|
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
|
||||||
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
|
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/dev/wallet_sync_exporter_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
|
import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
|
import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart';
|
import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart';
|
||||||
|
@ -847,6 +848,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||||
builder: (_) => getIt.get<DevMoneroCallProfilerPage>(),
|
builder: (_) => getIt.get<DevMoneroCallProfilerPage>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case Routes.devWalletSyncExporter:
|
||||||
|
return MaterialPageRoute<void>(
|
||||||
|
builder: (_) => getIt.get<WalletSyncExporterPage>(),
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return MaterialPageRoute<void>(
|
return MaterialPageRoute<void>(
|
||||||
builder: (_) => Scaffold(
|
builder: (_) => Scaffold(
|
||||||
|
|
|
@ -113,6 +113,7 @@ class Routes {
|
||||||
static const backgroundSync = '/background_sync';
|
static const backgroundSync = '/background_sync';
|
||||||
static const devMoneroBackgroundSync = '/dev/monero_background_sync';
|
static const devMoneroBackgroundSync = '/dev/monero_background_sync';
|
||||||
static const devMoneroCallProfiler = '/dev/monero_call_profiler';
|
static const devMoneroCallProfiler = '/dev/monero_call_profiler';
|
||||||
|
static const devWalletSyncExporter = '/dev/wallet_sync_exporter';
|
||||||
static const signPage = '/sign_page';
|
static const signPage = '/sign_page';
|
||||||
static const connectDevices = '/device/connect';
|
static const connectDevices = '/device/connect';
|
||||||
static const urqrAnimatedPage = '/urqr/animated_page';
|
static const urqrAnimatedPage = '/urqr/animated_page';
|
||||||
|
|
280
lib/src/screens/dev/wallet_sync_exporter_page.dart
Normal file
280
lib/src/screens/dev/wallet_sync_exporter_page.dart
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||||
|
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||||
|
import 'package:cake_wallet/view_model/dev/wallet_sync_exporter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||||
|
|
||||||
|
class WalletSyncExporterPage extends BasePage {
|
||||||
|
final WalletSyncExporter viewModel;
|
||||||
|
|
||||||
|
WalletSyncExporterPage(this.viewModel) {
|
||||||
|
viewModel.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get title => "[dev] wallet sync exporter";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget body(BuildContext context) {
|
||||||
|
return Observer(
|
||||||
|
builder: (_) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildStatusSection(context),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
_buildSettingsSection(context),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
_buildControlSection(context),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
_buildExportInfoSection(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusSection(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Status', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildStatusRow(context, 'Sync in progress:', viewModel.isSyncing ? 'Yes' : 'No'),
|
||||||
|
if (viewModel.totalWallets > 0) ...[
|
||||||
|
_buildStatusRow(context, 'Progress:',
|
||||||
|
'${viewModel.currentWalletIndex}/${viewModel.totalWallets} (${viewModel.progress}%)'),
|
||||||
|
],
|
||||||
|
_buildStatusRow(context, 'Status:', viewModel.statusMessage),
|
||||||
|
_buildStatusRow(context, 'Timer active:',
|
||||||
|
viewModel.syncTimer != null ? 'Yes (${viewModel.exportIntervalMinutes}m)' : 'No'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSettingsSection(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Settings', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildTextFieldRow(
|
||||||
|
context,
|
||||||
|
'Export path:',
|
||||||
|
viewModel.exportPath,
|
||||||
|
(value) => viewModel.setExportPath(value),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
_buildTextFieldRow(
|
||||||
|
context,
|
||||||
|
'Interval (minutes):',
|
||||||
|
viewModel.exportIntervalMinutes.toString(),
|
||||||
|
(value) {
|
||||||
|
final interval = int.tryParse(value);
|
||||||
|
if (interval != null && interval > 0) {
|
||||||
|
viewModel.setExportInterval(interval);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildControlSection(BuildContext context) {
|
||||||
|
final isButtonsEnabled = !viewModel.isSyncing;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Controls', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
|
children: [
|
||||||
|
PrimaryButton(
|
||||||
|
text: "Sync & Export Now",
|
||||||
|
color: Colors.purple,
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: isButtonsEnabled ? () => viewModel.syncAndExport() : null,
|
||||||
|
isDisabled: !isButtonsEnabled,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
text: viewModel.syncTimer == null ? "Start Periodic Sync" : "Stop Periodic Sync",
|
||||||
|
color: Colors.purple,
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: isButtonsEnabled
|
||||||
|
? () {
|
||||||
|
if (viewModel.syncTimer == null) {
|
||||||
|
viewModel.startPeriodicSync();
|
||||||
|
} else {
|
||||||
|
viewModel.stopPeriodicSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
isDisabled: !isButtonsEnabled,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildExportInfoSection(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Last Export', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildStatusRow(
|
||||||
|
context,
|
||||||
|
'Last export time:',
|
||||||
|
viewModel.lastExportTime.isEmpty ? 'Never' : viewModel.lastExportTime,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (viewModel.exportData.isNotEmpty) ...[
|
||||||
|
SizedBox(height: 8),
|
||||||
|
PrimaryButton(
|
||||||
|
text: "View Export Data",
|
||||||
|
color: Colors.purple,
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Export Data Summary'),
|
||||||
|
content: Text(_buildDataSummary()),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('Close'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusRow(BuildContext context, String title, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTextFieldRow(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
String value,
|
||||||
|
Function(String) onChanged,
|
||||||
|
{TextInputType keyboardType = TextInputType.text}
|
||||||
|
) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: TextEditingController(text: value),
|
||||||
|
onChanged: onChanged,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildDataSummary() {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
final data = viewModel.exportData;
|
||||||
|
|
||||||
|
buffer.writeln('Timestamp: ${data['timestamp'] ?? 'Unknown'}');
|
||||||
|
buffer.writeln('Wallets: ${(data['wallets'] as List?)?.length ?? 0}');
|
||||||
|
|
||||||
|
int totalTransactions = 0;
|
||||||
|
for (final wallet in (data['wallets'] as List?) ?? []) {
|
||||||
|
if (wallet is Map && wallet['transactions'] is List) {
|
||||||
|
totalTransactions += (wallet['transactions'] as List).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.writeln('Total transactions: $totalTransactions');
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,6 +77,12 @@ class OtherSettingsPage extends BasePage {
|
||||||
handler: (BuildContext context) =>
|
handler: (BuildContext context) =>
|
||||||
Navigator.of(context).pushNamed(Routes.devMoneroCallProfiler),
|
Navigator.of(context).pushNamed(Routes.devMoneroCallProfiler),
|
||||||
),
|
),
|
||||||
|
if (FeatureFlag.hasDevOptions)
|
||||||
|
SettingsCellWithArrow(
|
||||||
|
title: '[dev] wallet sync exporter',
|
||||||
|
handler: (BuildContext context) =>
|
||||||
|
Navigator.of(context).pushNamed(Routes.devWalletSyncExporter),
|
||||||
|
),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
SettingsVersionCell(
|
SettingsVersionCell(
|
||||||
title: S.of(context).version(_otherSettingsViewModel.currentVersion)),
|
title: S.of(context).version(_otherSettingsViewModel.currentVersion)),
|
||||||
|
|
269
lib/view_model/dev/wallet_sync_exporter.dart
Normal file
269
lib/view_model/dev/wallet_sync_exporter.dart
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue