fix: monero address display in tx history (#3249)
What changed, and why it matters
This commit fixes how Monero recipient addresses are shown in transaction history. Previously, saved descriptions could contain extra text (such as account names or labels) alongside the actual address, causing the app to display or copy a malformed address. The patch extracts only the valid Monero address portion when displaying it, reducing the chance a user accidentally copies or shares the wrong thing. It is a UI/data-sanitization fix rather than a cryptographic or network vulnerability.
Treat as a low-risk bugfix. Reviewers may optionally verify that the regex correctly handles all supported Monero address formats (standard, subaddress, integrated) and does not truncate valid addresses or accept invalid ones. No urgent security response is indicated.
Security signals we found
UI display sanitization of stored recipient address
Regex-based address extraction without checksum validation
Prevention of concatenated/duplicate address text in transaction descriptions
No cryptographic, network, or permission changes
Evidence from the diff
The change introduces _moneroRecipientAddressForDisplay(), a helper that strips whitespace and extracts a 95-character Monero standard/subaddress (4/8 prefix) or a 106-character integrated address via regex from a stored recipient-address string. It is applied in transaction_details_view_model.dart when resolving the recipient address for display and when building the AddressListItem. A related change in send_view_model.dart consolidates how parsed addresses are stored in transaction descriptions so that only one canonical address is saved instead of concatenating parsed and extracted addresses. The regex is broad (length/prefix based) and does not validate checksums or network bytes.
Changed components
lib/view_model/transaction_details_view_model.dartlib/view_model/send/send_view_model.dartMonero transaction history / recipient-address displayInspect captured patch +27 / −8
diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart
index 52e7c44f..35883f50 100644
--- a/lib/view_model/send/send_view_model.dart
+++ b/lib/view_model/send/send_view_model.dart
@@ -1106,10 +1106,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
Future<void> updateWalletBalance() async => await wallet.updateBalance();
Future<void> _addTransactionDescription() async {
- String address = outputs.fold('', (acc, value) {
- return value.isParsedAddress
- ? '$acc${value.address}\n${value.extractedAddress}\n\n'
- : '$acc${value.address}\n\n';
+ String address = outputs.fold('', (acc, value) {
+ final canonical = value.extractedAddress.trim().isNotEmpty
+ ? value.extractedAddress.trim()
+ : value.address.trim();
+ if (canonical.isEmpty) {
+ return acc;
+ }
+ return '$acc$canonical\n\n';
});
address = address.trim();
diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart
index 2c64d6e7..1032ac90 100644
--- a/lib/view_model/transaction_details_view_model.dart
+++ b/lib/view_model/transaction_details_view_model.dart
@@ -37,6 +37,19 @@ part 'transaction_details_view_model.g.dart';
bool _trueFunc(_) => true;
+/// We're adding a regex here so we can remove any already saved address that has the account in it.
+/// In the refactor, we will make another separate variable for accounts and the UI would handle it as needed.
+String _moneroRecipientAddressForDisplay(String raw, WalletType walletType) {
+ if (walletType != WalletType.monero || raw.isEmpty) return raw;
+
+ final compact = raw.replaceAll(RegExp(r'\s'), '');
+ final match = RegExp(
+ r'4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}',
+ caseSensitive: false,
+ ).firstMatch(compact);
+ return match?.group(0) ?? raw.trim();
+}
+
bool isLightning(TransactionInfo tx) {
printV(tx.additionalInfo);
return (tx.additionalInfo["isLightning"] as bool?) ?? false;
@@ -121,8 +134,9 @@ class TxDetailRowDefinition {
if(ret == null) {
ret = vm.transactionInfo.to ?? "";
}
- vm.isRecipientAddressShown = ret.isNotEmpty;
- return ret;
+ final resolvedAddress = _moneroRecipientAddressForDisplay(ret, vm.wallet.type);
+ vm.isRecipientAddressShown = resolvedAddress.isNotEmpty;
+ return resolvedAddress;
},
applicable: (vm) =>
vm.showRecipientAddress &&
@@ -252,11 +266,12 @@ abstract class TransactionDetailsViewModelBase with Store {
final recipientAddress = description.recipientAddress;
if (recipientAddress?.isNotEmpty ?? false) {
+ final recipientAddressForDisplay = _moneroRecipientAddressForDisplay(recipientAddress!, wallet.type);
items.add(
AddressListItem(
title: S.current.transaction_details_recipient_address,
- value: recipientAddress!,
- key: ValueKey('standard_list_item_${recipientAddress}_key'),
+ value: recipientAddressForDisplay,
+ key: ValueKey('standard_list_item_${recipientAddressForDisplay}_key'),
),
);
}
Why this scored 19/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.