What changed, and why it matters
This commit is a general bug-fix and reliability update for a Monero wallet app. It tightens balance checks when sending money, improves how pending and confirmed transactions are tracked, replaces an old notification system with a background task, and fixes some UI status icons. There is no clear security vulnerability being patched, but the changes reduce the chance of users accidentally creating invalid transactions or seeing misleading balances.
Review the `// REMOVE THIS` hardcoded `+ 1` in `periodic_tasks.dart` before release because it will trigger a spurious incoming-transaction notification. Reconsider removing the 30-second timeout in `getCurrentBlockchainHeight` to avoid indefinite hangs. Otherwise, treat this as a routine reliability patch; no urgent security response is indicated.
Security signals we found
Send validation switched from totalBalance to unlockedBalance, reducing risk of constructing transactions that fail due to locked/unconfirmed funds
createTx now surfaces pending transaction error strings as exceptions instead of returning a possibly-invalid pending transaction object
Pending outgoing transactions are now tracked separately from confirmed history and cleaned up once confirmed, reducing stale-pending-tx state
Background task now starts Tor when the wallet is configured to use Tor, improving privacy consistency for background checks
HTTP timeout removed in getCurrentBlockchainHeight, increasing risk of hung requests
Evidence from the diff
The patch refactors transaction history handling: _txHistory is made non-nullable, pending outgoing transactions are persisted under a new SharedPreferences key, and a helper sums pending outgoing amounts. The send flow now checks unlockedBalance instead of totalBalance, auto-enables sweep-all when the entered amount equals the unlocked balance, and surfaces a new ‘insufficient balance to cover fee’ message. createTx now throws if the Monero pending-transaction error string is non-empty. The background notification logic is moved from main.dart/wallet_home.dart into periodic_tasks.dart, adds Tor startup in the background task, and changes polling intervals. A hardcoded + 1 in the new-transaction count is marked with a // REMOVE THIS TODO, which could cause one false notification. UI status icons in wallet_home.dart are reworked. The getCurrentBlockchainHeight HTTP call loses its 30-second timeout.
Changed components
lib/models/wallet_model.dartlib/screens/send.dartlib/screens/confirm_send.dartlib/periodic_tasks.dartlib/main.dartlib/screens/wallet_home.dartlib/util/height.dartInspect captured patch +281 / −172
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 2c57dfe..ab8ff35 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -67,6 +67,7 @@
"sendOpenAliasResolveError": "Invalid OpenAlias.",
"sendInvalidAddressError": "Invalid address.",
"sendInsufficientBalanceError": "Insufficient balance.",
+ "sendInsufficientBalanceToCoverFeeError": "Insufficient balance to cover the network fee.",
"settingsTitle": "Settings",
"settingsNotifyNewTxs": "Notify New Transactions",
"settingsLanguageLabel": "Language",
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index 05429a4..cb86a62 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -500,6 +500,12 @@ abstract class AppLocalizations {
/// **'Insufficient balance.'**
String get sendInsufficientBalanceError;
+ /// No description provided for @sendInsufficientBalanceToCoverFeeError.
+ ///
+ /// In en, this message translates to:
+ /// **'Insufficient balance to cover the network fee.'**
+ String get sendInsufficientBalanceToCoverFeeError;
+
/// No description provided for @settingsTitle.
///
/// In en, this message translates to:
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 0be702a..96ff5e3 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -220,6 +220,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sendInsufficientBalanceError => 'Insufficient balance.';
+ @override
+ String get sendInsufficientBalanceToCoverFeeError =>
+ 'Insufficient balance to cover the network fee.';
+
@override
String get settingsTitle => 'Settings';
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index 349f25b..66e7fa4 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -220,6 +220,10 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get sendInsufficientBalanceError => 'Saldo insuficiente.';
+ @override
+ String get sendInsufficientBalanceToCoverFeeError =>
+ 'Saldo insuficiente para cobrir a taxa da rede.';
+
@override
String get settingsTitle => 'Configurações';
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 7e1d6b7..e4ca588 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -67,6 +67,7 @@
"sendOpenAliasResolveError": "OpenAlias inválido.",
"sendInvalidAddressError": "Endereço inválido.",
"sendInsufficientBalanceError": "Saldo insuficiente.",
+ "sendInsufficientBalanceToCoverFeeError": "Saldo insuficiente para cobrir a taxa da rede.",
"settingsTitle": "Configurações",
"settingsNotifyNewTxs": "Notificar Novas Transações",
"settingsLanguageLabel": "Idioma",
diff --git a/lib/main.dart b/lib/main.dart
index 8d00931..e7a8426 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:monero_light_wallet/periodic_tasks.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:timeago/timeago.dart' as timeago;
@@ -12,7 +13,6 @@ import 'package:monero_light_wallet/screens/secret_keys.dart';
import 'package:monero_light_wallet/services/tor_service.dart';
import 'package:monero_light_wallet/models/language_model.dart';
import 'package:monero_light_wallet/l10n/app_localizations.dart';
-import 'package:monero_light_wallet/services/notifications_service.dart';
import 'package:monero_light_wallet/screens/settings.dart';
import 'package:monero_light_wallet/models/wallet_model.dart';
import 'package:monero_light_wallet/screens/connection_setup.dart';
@@ -29,7 +29,7 @@ import 'package:monero_light_wallet/util/logging.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
- await NotificationService().init();
+ registerPeriodicTasks();
timeago.setLocaleMessages('pt', timeago.PtBrMessages());
runApp(MyApp());
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index 8432c98..e02b672 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -10,6 +10,9 @@ import 'dart:isolate';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
+import 'package:monero/monero.dart' as monero;
+import 'package:monero/src/monero.dart';
+import 'package:monero/src/wallet2.dart';
import 'package:monero_light_wallet/consts.dart';
import 'package:monero_light_wallet/services/shared_preferences_service.dart';
@@ -18,9 +21,6 @@ import 'package:monero_light_wallet/util/formatting.dart';
import 'package:monero_light_wallet/util/height.dart';
import 'package:monero_light_wallet/util/logging.dart';
import 'package:monero_light_wallet/util/wallet.dart';
-import 'package:monero/monero.dart' as monero;
-import 'package:monero/src/monero.dart';
-import 'package:monero/src/wallet2.dart';
String generateHexString(int length) {
final Random random = Random.secure();
@@ -143,7 +143,7 @@ class WalletModel with ChangeNotifier {
int? _syncedHeight;
double? _unlockedBalance;
double? _totalBalance;
- List<TxDetails>? _txHistory = [];
+ List<TxDetails> _txHistory = [];
Wallet2Wallet? get w2Wallet => _w2Wallet;
bool get hasAttemptedConnection => _hasAttemptedConnection;
@@ -152,7 +152,7 @@ class WalletModel with ChangeNotifier {
int? get syncedHeight => _syncedHeight;
double? get unlockedBalance => _unlockedBalance;
double? get totalBalance => _totalBalance;
- List<TxDetails>? get txHistory => _txHistory;
+ List<TxDetails> get txHistory => _txHistory;
bool get usingTor => _connectionUseTor;
WalletModel() {
@@ -164,8 +164,6 @@ class WalletModel with ChangeNotifier {
}
void _startTimers() {
- log(LogLevel.info, "Starting timers");
-
Timer.periodic(Duration(seconds: 1), (timer) {
_runCheckConnectionTimerTask();
});
@@ -209,17 +207,33 @@ class WalletModel with ChangeNotifier {
loadSyncedHeight(),
loadUnlockedBalance(),
loadTotalBalance(),
- _loadTxHistory(),
+ loadTxHistory(),
]);
notifyListeners();
}
- Future<void> _loadTxHistory() async {
+ Future<void> loadTxHistory({bool persistCount = true}) async {
final txCount = _w2TxHistory!.count();
+ var hasPendingTx = false;
+ final pendingOutgoingTxs = await _getPendingOutgoingTxs();
- if (_txHistory == null || txCount > _txHistory!.length) {
+ if (pendingOutgoingTxs.isNotEmpty) {
+ hasPendingTx = true;
+ } else if (_txHistory.isNotEmpty) {
+ final lastTx = txHistory[0];
+
+ if (lastTx.confirmations < 10) {
+ hasPendingTx = true;
+ }
+ }
+
+ if (txCount > _txHistory.length || hasPendingTx) {
_txHistory = await _getFullTxHistory();
+
+ if (persistCount) {
+ await persistTxHistoryCount();
+ }
}
}
@@ -278,15 +292,13 @@ class WalletModel with ChangeNotifier {
}
Future<void> persistTxHistoryCount() async {
- if (_txHistory == null) {
- throw Exception(
- "Should not attempt to persist tx history count when tx history is null",
- );
+ if (_txHistory.isEmpty) {
+ return;
}
await SharedPreferencesService.set<int>(
SharedPreferencesKeys.txHistoryCount,
- _txHistory!.length,
+ _txHistory.length,
);
}
@@ -536,6 +548,13 @@ class WalletModel with ChangeNotifier {
Future delete() async {
_w2WalletManager.closeWallet(_w2Wallet!, false);
_w2Wallet = null;
+ _hasAttemptedConnection = false;
+ _isConnected = false;
+ _isSynced = false;
+ _syncedHeight = null;
+ _unlockedBalance = null;
+ _totalBalance = null;
+ _txHistory = [];
final path = await getWalletPath();
await File(path).delete();
@@ -661,7 +680,13 @@ class WalletModel with ChangeNotifier {
}),
);
- return MoneroPendingTransaction(txPointer);
+ final pendingTx = MoneroPendingTransaction(txPointer);
+
+ if (pendingTx.errorString() != '') {
+ throw Exception(pendingTx.errorString());
+ }
+
+ return pendingTx;
}
Future<void> commitTx(
@@ -707,10 +732,9 @@ class WalletModel with ChangeNotifier {
key: _w2Wallet!.getTxKey(txid: tx.txid('')),
);
- await addPendingTx(txDetails);
-
- store();
+ await addPendingOutgoingTx(txDetails);
await refresh();
+ await loadTxHistory();
}
String resolveOpenAlias(String address) {
@@ -720,29 +744,33 @@ class WalletModel with ChangeNotifier {
);
}
- Future<void> addPendingTx(TxDetails tx) async {
- final pendingTxs = await _getPendingTxs();
- pendingTxs.add(tx);
- _persistPendingTxs(pendingTxs);
+ Future<void> addPendingOutgoingTx(TxDetails tx) async {
+ final pendingOutgoingTxs = await _getPendingOutgoingTxs();
+ pendingOutgoingTxs.add(tx);
+ await _persistPendingOutgoingTxs(pendingOutgoingTxs);
}
- Future<void> _removePendingTx(String hash) async {
- final pendingTxs = await _getPendingTxs();
- pendingTxs.removeWhere((tx) => tx.hash == hash);
- _persistPendingTxs(pendingTxs);
+ Future<void> _removePendingOutgoingTx(String hash) async {
+ final pendingOutgoingTxs = await _getPendingOutgoingTxs();
+ pendingOutgoingTxs.removeWhere((tx) => tx.hash == hash);
+ _persistPendingOutgoingTxs(pendingOutgoingTxs);
}
- Future<void> _persistPendingTxs(List<TxDetails> pendingTxs) async {
+ Future<void> _persistPendingOutgoingTxs(List<TxDetails> txs) async {
+ final txsJson = txs.map((tx) => json.encode(tx)).toList();
final prefs = await SharedPreferences.getInstance();
- final txsJson = pendingTxs.map((tx) => json.encode(tx)).toList();
- await prefs.setStringList('pendingTxs', txsJson);
+ await prefs.setStringList(
+ SharedPreferencesKeys.pendingOutgoingTxs,
+ txsJson,
+ );
}
- Future<List<TxDetails>> _getPendingTxs() async {
+ Future<List<TxDetails>> _getPendingOutgoingTxs() async {
final prefs = await SharedPreferences.getInstance();
- final txsJson = prefs.getStringList('pendingTxs') ?? [];
+ final txsJson =
+ prefs.getStringList(SharedPreferencesKeys.pendingOutgoingTxs) ?? [];
- final pendingTxs = txsJson
+ final txs = txsJson
.map(
(jsonString) => TxDetails.fromJson(
json.decode(jsonString) as Map<String, dynamic>,
@@ -750,10 +778,20 @@ class WalletModel with ChangeNotifier {
)
.toList();
- return pendingTxs;
+ return txs;
+ }
+
+ Future<double> getPendingOutgoingTxsAmountSum() async {
+ final txs = await _getPendingOutgoingTxs();
+
+ final amountSum = txs
+ .map((tx) => tx.amount + tx.fee)
+ .reduce((value, el) => value + el);
+
+ return amountSum;
}
- List<TxDetails> getConfirmedTxHistory() {
+ List<TxDetails> _getConfirmedTxHistory() {
final txCount = _w2TxHistory!.count();
final List<TxDetails> confirmedTxs = [];
@@ -770,19 +808,19 @@ class WalletModel with ChangeNotifier {
}
Future<List<TxDetails>> _getFullTxHistory() async {
- final pendingTxs = await _getPendingTxs();
- final confirmedTxHistory = getConfirmedTxHistory();
+ final pendingOutgoingTxs = await _getPendingOutgoingTxs();
+ final confirmedTxHistory = _getConfirmedTxHistory();
final confirmedTxMap = {for (var tx in confirmedTxHistory) tx.hash: tx};
final fullTxHistory = <TxDetails>[];
fullTxHistory.addAll(confirmedTxHistory);
- for (final pendingTx in pendingTxs) {
- if (confirmedTxMap.containsKey(pendingTx.hash)) {
- _removePendingTx(pendingTx.hash);
+ for (final pendingOutgoingTx in pendingOutgoingTxs) {
+ if (confirmedTxMap.containsKey(pendingOutgoingTx.hash)) {
+ _removePendingOutgoingTx(pendingOutgoingTx.hash);
} else {
- fullTxHistory.add(pendingTx);
+ fullTxHistory.add(pendingOutgoingTx);
}
}
diff --git a/lib/periodic_tasks.dart b/lib/periodic_tasks.dart
index f421605..f876ac0 100644
--- a/lib/periodic_tasks.dart
+++ b/lib/periodic_tasks.dart
@@ -1,86 +1,132 @@
import 'package:monero_light_wallet/models/wallet_model.dart';
import 'package:monero_light_wallet/services/notifications_service.dart';
+import 'package:monero_light_wallet/services/shared_preferences_service.dart';
+import 'package:monero_light_wallet/services/tor_service.dart';
+import 'package:monero_light_wallet/util/logging.dart';
import 'package:workmanager/workmanager.dart';
import 'package:monero_light_wallet/consts.dart' as consts;
class PeriodicTasks {
- static const newTransactionsCheck = 'newTransactionsCheck';
+ static const txNotifier = 'txNotifier';
}
-@pragma('vm:entry-point')
-void callbackDispatcher() {
- Workmanager().executeTask((task, inputData) async {
- final wallet = WalletModel();
+Future<bool> runTxNotifier() async {
+ final wallet = WalletModel();
- switch (task) {
- case PeriodicTasks.newTransactionsCheck:
- if (!await wallet.hasExistingWallet()) {
- return true;
- }
-
- await wallet.openExisting();
- await wallet.loadPersistedConnection();
- await wallet.refresh();
- await wallet.connectToDaemon();
+ if (!await wallet.hasExistingWallet()) {
+ return true;
+ }
- if (wallet.isConnected) {
- return false;
- }
+ await wallet.openExisting();
+ await wallet.loadPersistedConnection();
- int itersBeforeSynced = 0;
+ if (wallet.usingTor) {
+ await TorService.sharedInstance.start();
+ await TorService.sharedInstance.waitUntilConnected().timeout(
+ Duration(minutes: 2),
+ onTimeout: () =>
+ log(LogLevel.warn, '[TX Notifier] Tor connection timed out'),
+ );
+ }
- while (true) {
- if (wallet.isSynced) {
- break;
- }
+ await wallet.refresh();
+ await wallet.connectToDaemon();
+ await wallet.loadTxHistory(persistCount: false);
- await Future.delayed(Duration(seconds: 10));
+ int itersBeforeSynced = 0;
- itersBeforeSynced++;
+ while (true) {
+ if (wallet.isConnected && wallet.isSynced) {
+ break;
+ }
- if (itersBeforeSynced == 20) {
- return false;
- }
- }
+ await Future.delayed(Duration(seconds: 2));
- if (wallet.txHistory == null) {
- return true;
- }
+ itersBeforeSynced++;
- final persistedTxCount = await wallet.getPersistedTxHistoryCount();
- final currentTxCount = wallet.txHistory!.length;
- final newTxCount = currentTxCount - persistedTxCount;
+ if (itersBeforeSynced == 20) {
+ log(LogLevel.warn, '[TX Notifier] Wallet connection timed out');
+ return false;
+ }
+ }
+
+ final persistedTxCount = await wallet.getPersistedTxHistoryCount();
+ final currentTxCount = wallet.txHistory.length;
+ final countOfNewTxs = currentTxCount - persistedTxCount + 1; // REMOVE THIS
+
+ if (countOfNewTxs > 0 && currentTxCount != 0) {
+ log(LogLevel.info, '[TX Notifier] Found new transactions');
+
+ for (int i = 0; i < countOfNewTxs; i++) {
+ final tx = wallet.txHistory[i];
+ if (tx.direction == consts.txDirectionIncoming) {
+ log(LogLevel.info, '[TX Notifier] Notifying transaction $i');
+ NotificationService().showIncomingTxNotification(tx.amount);
+ } else {
+ log(LogLevel.info, '[TX Notifier] Not notifying outgoing transaction');
+ }
+ }
- if (newTxCount > 0 && currentTxCount != 0) {
- for (int i = 0; i < newTxCount; i++) {
- final tx = wallet.getTxDetails(i);
- if (tx.direction == consts.txDirectionIncoming) {
- NotificationService().showIncomingTxNotification(tx.amount);
- }
- }
+ await wallet.persistTxHistoryCount();
+ } else {
+ log(LogLevel.info, '[TX Notifier] No new transactions found');
+ }
- await wallet.persistTxHistoryCount();
- }
+ return true;
+}
- break;
+@pragma('vm:entry-point')
+void _callbackDispatcher() {
+ Workmanager().executeTask((task, inputData) async {
+ switch (PeriodicTasks.txNotifier) {
+ case PeriodicTasks.txNotifier:
+ return runTxNotifier();
}
return true;
});
}
-Future<void> startNewTransactionsCheckTask() async {
- await Workmanager().registerPeriodicTask(
- PeriodicTasks.newTransactionsCheck,
- "New transactions check",
- frequency: Duration(minutes: 15),
- constraints: Constraints(
- networkType: NetworkType.connected,
- requiresBatteryNotLow: true,
- ),
- );
+Future<void> registerTxNotifierTaskIfEnabled() async {
+ final notificationsIsAllowed = await NotificationService().promptPermission();
+
+ if (!notificationsIsAllowed) {
+ await SharedPreferencesService.set<bool>(
+ SharedPreferencesKeys.notificationsEnabled,
+ false,
+ );
+ return;
+ }
+
+ final notificationsEnabled =
+ await SharedPreferencesService.get<bool>(
+ SharedPreferencesKeys.notificationsEnabled,
+ ) ??
+ false;
+
+ if (notificationsEnabled) {
+ // This will replace an existing task, so we can prevent code from eg an old
+ // release from remaining forever.
+ await Workmanager().cancelByUniqueName(PeriodicTasks.txNotifier);
+ await Workmanager().registerPeriodicTask(
+ "New transactions check",
+ PeriodicTasks.txNotifier,
+ frequency: Duration(minutes: 15),
+ constraints: Constraints(
+ networkType: NetworkType.connected,
+ requiresBatteryNotLow: true,
+ ),
+ );
+ }
}
-Future<void> cancelNewTransactionsCheckTask() async {
- await Workmanager().cancelByUniqueName(PeriodicTasks.newTransactionsCheck);
+Future<void> unregisterTxNotifierTask() async {
+ await Workmanager().cancelByUniqueName(PeriodicTasks.txNotifier);
+}
+
+Future<void> registerPeriodicTasks() async {
+ await NotificationService().init();
+ Workmanager().initialize(_callbackDispatcher, isInDebugMode: true);
+
+ await registerTxNotifierTaskIfEnabled();
}
diff --git a/lib/screens/confirm_send.dart b/lib/screens/confirm_send.dart
index a3bf6f9..06d2c37 100644
--- a/lib/screens/confirm_send.dart
+++ b/lib/screens/confirm_send.dart
@@ -5,6 +5,7 @@ import 'package:monero_light_wallet/l10n/app_localizations.dart';
import 'package:monero_light_wallet/models/fiat_rate_model.dart';
import 'package:monero_light_wallet/models/wallet_model.dart';
import 'package:monero_light_wallet/util/formatting.dart';
+import 'package:monero_light_wallet/util/logging.dart';
import 'package:provider/provider.dart';
class ConfirmSendScreenArgs {
@@ -105,16 +106,20 @@ class _ConfirmSendScreenState extends State<ConfirmSendScreen> {
);
}
} on FormatException catch (error) {
+ var errorMsg = error.toString().replaceFirst('FormatException: ', '');
+
+ if (error.toString().contains('HTTP error code 500')) {
+ errorMsg =
+ 'Failed to send transaction. You might have insufficient unlocked balance.';
+ }
+
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(
- error.toString().replaceFirst('FormatException: ', ''),
- ),
- ),
- );
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text(errorMsg)));
}
} catch (error) {
+ log(LogLevel.error, error.toString());
if (mounted) {
ScaffoldMessenger.of(
context,
diff --git a/lib/screens/send.dart b/lib/screens/send.dart
index 65b385a..134c46b 100644
--- a/lib/screens/send.dart
+++ b/lib/screens/send.dart
@@ -101,7 +101,7 @@ class _SendScreenState extends State<SendScreen> {
return;
}
- if (amount > (wallet.totalBalance ?? 0)) {
+ if (amount > (wallet.unlockedBalance ?? 0)) {
setState(() {
_amountError = i18n.sendInsufficientBalanceError;
_isLoading = false;
@@ -128,10 +128,22 @@ class _SendScreenState extends State<SendScreen> {
);
}
} catch (error) {
- if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
+ if (error.toString().contains('Unlocked funds too low')) {
+ if (wallet.unlockedBalance! > amount) {
+ setState(() {
+ _amountError = i18n.sendInsufficientBalanceToCoverFeeError;
+ });
+ } else {
+ setState(() {
+ _amountError = i18n.sendInsufficientBalanceError;
+ });
+ }
+ } else {
+ if (mounted) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
+ }
}
}
@@ -149,8 +161,16 @@ class _SendScreenState extends State<SendScreen> {
});
}
- void _onSendAmountChanged() {
- if (_isSweepAll) {
+ void _onSendAmountChanged(double amount) {
+ final wallet = Provider.of<WalletModel>(context, listen: false);
+
+ if (amount == wallet.unlockedBalance! && !_isSweepAll) {
+ setState(() {
+ _isSweepAll = true;
+ });
+ }
+
+ if (amount != wallet.unlockedBalance! && _isSweepAll) {
setState(() {
_isSweepAll = false;
});
@@ -194,7 +214,8 @@ class _SendScreenState extends State<SendScreen> {
children: [
TextField(
controller: _amountController,
- onChanged: (value) => _onSendAmountChanged(),
+ onChanged: (value) =>
+ _onSendAmountChanged(double.parse(value)),
keyboardType: TextInputType.numberWithOptions(
decimal: true,
),
diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart
index f9479c0..b06beab 100644
--- a/lib/screens/settings.dart
+++ b/lib/screens/settings.dart
@@ -55,18 +55,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
final isAllowed = await NotificationService().promptPermission();
if (isAllowed) {
- startNewTransactionsCheckTask();
await SharedPreferencesService.set<bool>(
SharedPreferencesKeys.notificationsEnabled,
true,
);
+ await registerTxNotifierTaskIfEnabled();
}
} else {
- cancelNewTransactionsCheckTask();
await SharedPreferencesService.set<bool>(
SharedPreferencesKeys.notificationsEnabled,
false,
);
+ await unregisterTxNotifierTask();
}
}
diff --git a/lib/screens/wallet_home.dart b/lib/screens/wallet_home.dart
index c5ef9fe..681f0d8 100644
--- a/lib/screens/wallet_home.dart
+++ b/lib/screens/wallet_home.dart
@@ -1,4 +1,3 @@
-import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -9,11 +8,8 @@ import 'package:monero_light_wallet/widgets/monero_amount.dart';
import 'package:monero_light_wallet/widgets/status_icon.dart';
import 'package:provider/provider.dart';
import 'package:skeletonizer/skeletonizer.dart';
-import 'package:workmanager/workmanager.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:monero_light_wallet/l10n/app_localizations.dart';
-import 'package:monero_light_wallet/services/shared_preferences_service.dart';
-import 'package:monero_light_wallet/periodic_tasks.dart';
import 'package:monero_light_wallet/models/wallet_model.dart';
import 'package:monero_light_wallet/consts.dart' as consts;
import 'package:monero_light_wallet/widgets/wallet_navigation_bar.dart';
@@ -32,9 +28,6 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
void initState() {
super.initState();
- Workmanager().initialize(callbackDispatcher, isInDebugMode: true);
- _initNewTxsCheckIfNeeded();
-
WidgetsBinding.instance.addPostFrameCallback((_) {
final Map<String, dynamic>? args =
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
@@ -59,22 +52,6 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
Navigator.pushNamed(context, '/tx_details', arguments: txDetails);
}
- Future<void> _initNewTxsCheckIfNeeded() async {
- final notificationsEnabled =
- await SharedPreferencesService.get<bool>(
- SharedPreferencesKeys.notificationsEnabled,
- ) ??
- false;
-
- final taskIsRunning = await Workmanager().isScheduledByUniqueName(
- PeriodicTasks.newTransactionsCheck,
- );
-
- if (notificationsEnabled && !taskIsRunning) {
- await startNewTransactionsCheckTask();
- }
- }
-
void _showTxSuccessToast() {
final i18n = AppLocalizations.of(context)!;
@@ -96,12 +73,13 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
final lockedBalance =
(wallet.totalBalance ?? 0) - (wallet.unlockedBalance ?? 0);
final fiatSymbol = fiatRate.fiatCode == 'EUR' ? '€' : '\$';
- var connectionStatus = LwsConnectionStatus.disconnected;
+ var lwsConnectionIconStatus = StatusIconStatus.fail;
+ var fiatApiIconStatus = StatusIconStatus.loading;
if (wallet.isConnected &&
wallet.isSynced &&
(wallet.syncedHeight ?? 0) > 0) {
- connectionStatus = LwsConnectionStatus.connected;
+ lwsConnectionIconStatus = StatusIconStatus.complete;
} else if (wallet.usingTor &&
TorService.sharedInstance.status ==
TorConnectionStatus.connecting ||
@@ -110,7 +88,15 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
wallet.isConnected &&
wallet.isSynced &&
(wallet.syncedHeight ?? 0) == 0) {
- connectionStatus = LwsConnectionStatus.connecting;
+ lwsConnectionIconStatus = StatusIconStatus.loading;
+ }
+
+ if (fiatRate.rate is double &&
+ !fiatRate.hasFailed &&
+ TorService.sharedInstance.status == TorConnectionStatus.connected) {
+ fiatApiIconStatus = StatusIconStatus.complete;
+ } else if (fiatRate.hasFailed) {
+ fiatApiIconStatus = StatusIconStatus.fail;
}
return Scaffold(
@@ -136,8 +122,10 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
child: Column(
spacing: 10,
children: [
- if (connectionStatus ==
- LwsConnectionStatus.connected)
+ // TODO: this can be made better
+ if (!wallet.usingTor &&
+ lwsConnectionIconStatus ==
+ StatusIconStatus.complete)
SizedBox(
width: 26,
height: 26,
@@ -147,9 +135,9 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
color: Colors.teal,
),
),
-
- if (connectionStatus ==
- LwsConnectionStatus.connecting)
+ if (!wallet.usingTor &&
+ lwsConnectionIconStatus ==
+ StatusIconStatus.loading)
SizedBox(
width: 22,
height: 22,
@@ -161,25 +149,25 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
strokeWidth: 2,
),
),
-
- if (connectionStatus ==
- LwsConnectionStatus.disconnected)
+ if (!wallet.usingTor &&
+ lwsConnectionIconStatus ==
+ StatusIconStatus.fail)
SizedBox(
width: 26,
height: 26,
child: Icon(Icons.cancel, color: Colors.red),
),
-
+ if (wallet.usingTor)
+ StatusIcon(
+ status: lwsConnectionIconStatus,
+ child: SvgPicture.asset(
+ 'assets/icons/tor.svg',
+ width: 22,
+ height: 22,
+ ),
+ ),
StatusIcon(
- status:
- fiatRate.rate is double &&
- !fiatRate.hasFailed &&
- TorService.sharedInstance.status ==
- TorConnectionStatus.connected
- ? StatusIconStatus.complete
- : fiatRate.hasFailed
- ? StatusIconStatus.fail
- : StatusIconStatus.loading,
+ status: fiatApiIconStatus,
child: SvgPicture.asset(
'assets/icons/tor.svg',
width: 22,
@@ -269,14 +257,14 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
),
),
),
- if (wallet.txHistory != null && wallet.txHistory!.isNotEmpty)
+ if (wallet.txHistory.isNotEmpty)
Expanded(
child: SizedBox(
child: ListView.separated(
separatorBuilder: (context, index) => Divider(),
- itemCount: wallet.txHistory!.length,
+ itemCount: wallet.txHistory.length,
itemBuilder: (BuildContext context, int index) {
- final tx = wallet.txHistory![index];
+ final tx = wallet.txHistory[index];
final amountFiat = fiatRate.rate is double
? tx.amount * fiatRate.rate!
: null;
@@ -379,9 +367,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
),
),
),
- if (wallet.txHistory != null && wallet.txHistory!.isEmpty)
- Text(i18n.homeNoTransactions),
- if (wallet.txHistory == null) CircularProgressIndicator(),
+ if (wallet.txHistory.isEmpty) Text(i18n.homeNoTransactions),
],
),
),
diff --git a/lib/services/shared_preferences_service.dart b/lib/services/shared_preferences_service.dart
index 0f89040..3ee0dd5 100644
--- a/lib/services/shared_preferences_service.dart
+++ b/lib/services/shared_preferences_service.dart
@@ -12,6 +12,7 @@ class SharedPreferencesKeys {
static const String showReceiveSubaddress = 'showReceiveSubaddress';
static const String walletRestoreHeight = 'walletRestoreHeight';
static const String txHistoryCount = 'txHistoryCount';
+ static const String pendingOutgoingTxs = 'pendingOutgoingTxs';
}
class SharedPreferencesService {
diff --git a/lib/util/height.dart b/lib/util/height.dart
index 3348367..366440f 100644
--- a/lib/util/height.dart
+++ b/lib/util/height.dart
@@ -20,11 +20,7 @@ Future<int> getCurrentBlockchainHeight() async {
for (String url in urls) {
try {
- final response = await makeSocksHttpRequest(
- 'GET',
- url,
- proxyInfo,
- ).timeout(Duration(seconds: 30));
+ final response = await makeSocksHttpRequest('GET', url, proxyInfo);
if (response.statusCode == 200) {
final jsonResponse = jsonDecode(response.body);
Why this scored 32/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.