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
|
||||
|
||||
on: [pull_request]
|
||||
on: [push]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
|
|
|
@ -26,6 +26,9 @@ class SolanaTransactionInfo extends TransactionInfo {
|
|||
final double txFee;
|
||||
final TransactionDirection direction;
|
||||
|
||||
@override
|
||||
int get confirmations => 0;
|
||||
|
||||
String? _fiatAmount;
|
||||
|
||||
@override
|
||||
|
|
|
@ -28,6 +28,9 @@ class TronTransactionInfo extends TransactionInfo {
|
|||
final int? txFee;
|
||||
final TransactionDirection direction;
|
||||
|
||||
@override
|
||||
int get confirmations => 0;
|
||||
|
||||
factory TronTransactionInfo.fromJson(Map<String, dynamic> data) {
|
||||
return TronTransactionInfo(
|
||||
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/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/wallet_sync_exporter_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/wallet_sync_exporter.dart';
|
||||
import 'package:cake_wallet/view_model/link_view_model.dart';
|
||||
import 'package:cake_wallet/tron/tron.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(() => DevMoneroCallProfilerPage());
|
||||
getIt.registerFactory(() => WalletSyncExporterPage(getIt.get<WalletSyncExporter>()));
|
||||
getIt.registerFactory(() => WalletSyncExporter());
|
||||
|
||||
_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/dev/monero_background_sync.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/exchange/exchange_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>(),
|
||||
);
|
||||
|
||||
case Routes.devWalletSyncExporter:
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => getIt.get<WalletSyncExporterPage>(),
|
||||
);
|
||||
|
||||
default:
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => Scaffold(
|
||||
|
|
|
@ -113,6 +113,7 @@ class Routes {
|
|||
static const backgroundSync = '/background_sync';
|
||||
static const devMoneroBackgroundSync = '/dev/monero_background_sync';
|
||||
static const devMoneroCallProfiler = '/dev/monero_call_profiler';
|
||||
static const devWalletSyncExporter = '/dev/wallet_sync_exporter';
|
||||
static const signPage = '/sign_page';
|
||||
static const connectDevices = '/device/connect';
|
||||
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) =>
|
||||
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(),
|
||||
SettingsVersionCell(
|
||||
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