Remove unconfirmed outgoing transactions workaround
What changed, and why it matters
This commit removes a temporary workaround that stored unconfirmed outgoing transactions in the app's local settings. The app now relies on the underlying Monero wallet library to report pending transactions directly. The change also updates the transaction list UI so that transactions with no block height (height == -1) are shown as unconfirmed. There is no direct evidence in the commit that this fixes a security vulnerability; it appears to be a cleanup of technical debt that may reduce the risk of stale or misleading transaction data.
Treat as a routine maintenance change. Review whether the Monero wallet library reliably returns unconfirmed outgoing transactions with height == -1 so users are not shown stale or missing pending transactions. No urgent security action is indicated by the diff alone.
Security signals we found
Removal of local SharedPreferences storage for pending outgoing transaction metadata
Elimination of manual pending/confirmed transaction merge logic
UI change to display transactions with height == -1 as unconfirmed
No explicit security fix language in commit message or diff
Evidence from the diff
The patch deletes the SharedPreferences-based pending-outgoing-transaction cache in lib/models/wallet_model.dart. Methods addPendingOutgoingTx, _removePendingOutgoingTx, _persistPendingOutgoingTxs, _getPendingOutgoingTxs, getPendingOutgoingTxsAmountSum, and _getFullTxHistory are removed. _getConfirmedTxHistory is renamed to _getTxHistory and now returns only the transaction history reported by the wallet2 API. Callers loadTxHistory and loadUnusedSubaddressIndex are updated to use the synchronous _getTxHistory. In lib/screens/wallet_home.dart, the pending-transaction UI logic is broadened to treat height == -1 as unconfirmed. The commit message frames this only as removing a workaround, with no security claims.
Changed components
lib/models/wallet_model.dartlib/screens/wallet_home.dartSharedPreferences pending outgoing transaction cacheInspect captured patch +11 / −92
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index 32cdd45..168f4f6 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -7,7 +7,6 @@ import 'dart:isolate';
import 'dart:math';
import 'package:dart_date/dart_date.dart';
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';
@@ -244,11 +243,8 @@ class WalletModel with ChangeNotifier {
Future<void> loadTxHistory({bool persistCount = true}) async {
final txCount = _w2TxHistory!.count();
var hasPendingTx = false;
- final pendingOutgoingTxs = await _getPendingOutgoingTxs();
- if (pendingOutgoingTxs.isNotEmpty) {
- hasPendingTx = true;
- } else if (_txHistory.isNotEmpty) {
+ if (_txHistory.isNotEmpty) {
final lastTx = txHistory[0];
if (lastTx.confirmations < 10) {
@@ -259,7 +255,7 @@ class WalletModel with ChangeNotifier {
if (txCount > _txHistory.length || hasPendingTx) {
final txCountDiff = txCount - _txHistory.length;
- _txHistory = await _getFullTxHistory();
+ _txHistory = _getTxHistory();
// Notify new transactions on desktop
if ((Platform.isLinux || Platform.isWindows || Platform.isMacOS) &&
@@ -447,7 +443,7 @@ class WalletModel with ChangeNotifier {
}
Future<void> loadUnusedSubaddressIndex() async {
- final txHistory = await _getFullTxHistory();
+ final txHistory = _getTxHistory();
Set<int> usedIndexes = {};
@@ -1058,24 +1054,6 @@ class WalletModel with ChangeNotifier {
throw FormatException(errorMsg);
}
- final recipient = TxRecipient(destinationAddress, doubleAmountFromInt(tx.amount()));
-
- final TxDetails txDetails = TxDetails(
- index: null,
- direction: consts.txDirectionOutgoing,
- hash: tx.txid(''),
- amount: doubleAmountFromInt(tx.amount()),
- fee: doubleAmountFromInt(tx.fee()),
- recipients: [recipient],
- accountIndex: 0,
- subaddrIndexList: [],
- timestamp: (DateTime.now().millisecondsSinceEpoch / 1000).round(),
- height: 0,
- confirmations: 0,
- key: _w2Wallet!.getTxKey(txid: tx.txid('')),
- );
-
- await addPendingOutgoingTx(txDetails);
await refresh();
await loadTxHistory();
}
@@ -1103,79 +1081,20 @@ class WalletModel with ChangeNotifier {
return resolvedAddress;
}
- Future<void> addPendingOutgoingTx(TxDetails tx) async {
- final pendingOutgoingTxs = await _getPendingOutgoingTxs();
- pendingOutgoingTxs.add(tx);
- await _persistPendingOutgoingTxs(pendingOutgoingTxs);
- }
-
- Future<void> _removePendingOutgoingTx(String hash) async {
- final pendingOutgoingTxs = await _getPendingOutgoingTxs();
- pendingOutgoingTxs.removeWhere((tx) => tx.hash == hash);
- _persistPendingOutgoingTxs(pendingOutgoingTxs);
- }
-
- Future<void> _persistPendingOutgoingTxs(List<TxDetails> txs) async {
- final txsJson = txs.map((tx) => json.encode(tx)).toList();
- final prefs = await SharedPreferences.getInstance();
- await prefs.setStringList(SharedPreferencesKeys.pendingOutgoingTxs, txsJson);
- }
-
- Future<List<TxDetails>> _getPendingOutgoingTxs() async {
- final prefs = await SharedPreferences.getInstance();
- final txsJson = prefs.getStringList(SharedPreferencesKeys.pendingOutgoingTxs) ?? [];
-
- final txs = txsJson
- .map((jsonString) => TxDetails.fromJson(json.decode(jsonString) as Map<String, dynamic>))
- .toList();
-
- 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> _getTxHistory() {
final txCount = _w2TxHistory!.count();
- final List<TxDetails> confirmedTxs = [];
+ final List<TxDetails> txs = [];
for (int i = 0; i < txCount; i++) {
final tx = getTxDetails(i);
- confirmedTxs.add(tx);
+ txs.add(tx);
}
- confirmedTxs.sort((a, b) {
+ txs.sort((a, b) {
return a.timestamp < b.timestamp ? 1 : -1;
});
- return confirmedTxs;
- }
-
- Future<List<TxDetails>> _getFullTxHistory() async {
- 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 pendingOutgoingTx in pendingOutgoingTxs) {
- if (confirmedTxMap.containsKey(pendingOutgoingTx.hash)) {
- _removePendingOutgoingTx(pendingOutgoingTx.hash);
- } else {
- fullTxHistory.add(pendingOutgoingTx);
- }
- }
-
- fullTxHistory.sort((a, b) => b.timestamp.compareTo(a.timestamp));
-
- return fullTxHistory;
+ return txs;
}
TxDetails getTxDetails(int txIndex) {
diff --git a/lib/screens/wallet_home.dart b/lib/screens/wallet_home.dart
index 386f061..13a49d3 100644
--- a/lib/screens/wallet_home.dart
+++ b/lib/screens/wallet_home.dart
@@ -103,17 +103,17 @@ class _TransactionListItemState extends State<_TransactionListItem> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
- if (widget.tx.confirmations < 10)
+ if (widget.tx.confirmations < 10 || widget.tx.height == -1)
Row(
children: [
Text(
- '${widget.tx.confirmations}/10',
+ '${widget.tx.height == -1 ? '0' : widget.tx.confirmations}/10',
style: TextStyle(color: Colors.amber.shade700),
),
Icon(Icons.hourglass_top_rounded, color: Colors.amber.shade700, size: 20),
],
),
- if (widget.tx.confirmations >= 10) Text(''),
+ if (widget.tx.confirmations >= 10 && widget.tx.height != -1) Text(''),
Text(
timeago.format(
DateTime.fromMillisecondsSinceEpoch(widget.tx.timestamp * 1000),
Why this scored 22/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.