Merge pull request #165 from MAGICGrants/fixes
What changed, and why it matters
This update fixes a display bug in a Monero wallet app where the receive screen could show a payment address and a subaddress number that did not match. The mismatch could mislead a user into thinking funds were sent to one numbered account when they were actually sent to another, creating confusion and possible accounting/payment errors. The patch reads the address and its index together as a single value so they stay consistent.
Treat as a low-severity UI consistency fix. Verify that all callers of getUnusedSubaddress()/unusedSubaddressIndex are migrated to the atomic unusedSubaddress getter, and add regression tests ensuring the displayed subaddress and its label always match.
Security signals we found
UI state desynchronization between address and index
Atomic getter introduced to prevent mismatched subaddress display
Potential for user confusion / incorrect payment attribution
No cryptographic or network-layer changes
Evidence from the diff
The receive screen previously called wallet.getUnusedSubaddress() for the displayed address and wallet.unusedSubaddressIndex separately for the label. Because these values are fetched independently, a state change between the two reads could produce a label (#N) that does not correspond to the displayed subaddress. The patch introduces a single getter unusedSubaddress that returns both the index and address atomically, and updates receive.dart to use that tuple. It also switches lws_keys.dart and lws_details.dart to use appWalletOf(context, listen: true) and removes a stale _primaryAddress state field, ensuring UI rebuilds when wallet state changes.
Changed components
lib/screens/receive.dartlib/models/app_wallet.dartlib/models/monero_wallet_adapter.dartlib/screens/lws_keys.dartlib/screens/lws_details.dartInspect captured patch +13 / −9
### lib/models/app_wallet.dart
@@ -108,6 +108,9 @@ abstract interface class AppWallet implements Listenable {
// Receive (serverSupportsSubaddresses is declared with the sync getters above)
String? getUnusedSubaddress();
+
+ /// The subaddress to hand out with the index it actually is, as one value.
+ ({int index, String address})? get unusedSubaddress;
int? get unusedSubaddressIndex;
bool? get unusedSubaddressIndexIsSupported;
### lib/models/monero_wallet_adapter.dart
@@ -84,6 +84,8 @@ class MoneroWalletAdapter extends ChangeNotifier implements AppWallet {
@override
String? getUnusedSubaddress() => _wallet.getUnusedSubaddress();
@override
+ ({int index, String address})? get unusedSubaddress => _wallet.unusedSubaddress;
+ @override
int? get unusedSubaddressIndex => _wallet.unusedSubaddressIndex;
@override
bool? get unusedSubaddressIndexIsSupported => _wallet.unusedSubaddressIndexIsSupported;
### lib/screens/lws_details.dart
@@ -38,7 +38,7 @@ class _LwsDetailsScreenState extends State<LwsDetailsScreen> with SecureScreenMi
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- final primaryAddress = appWalletOf(context).getPrimaryAddress();
+ final primaryAddress = appWalletOf(context, listen: true).getPrimaryAddress();
final restoreHeight = ModalRoute.of(context)!.settings.arguments as int;
return LwsKeysView(
### lib/screens/lws_keys.dart
@@ -15,7 +15,6 @@ class LwsKeysScreen extends StatefulWidget {
class _LwsKeysScreenState extends State<LwsKeysScreen> with SecureScreenMixin {
var _restoreHeight = 0;
- var _primaryAddress = '';
var _secretViewKey = '';
@override
@@ -31,7 +30,6 @@ class _LwsKeysScreenState extends State<LwsKeysScreen> with SecureScreenMixin {
if (!mounted) return;
setState(() {
_restoreHeight = restoreHeight;
- _primaryAddress = wallet.getPrimaryAddress();
_secretViewKey = secretViewKey;
});
}
@@ -46,6 +44,7 @@ class _LwsKeysScreenState extends State<LwsKeysScreen> with SecureScreenMixin {
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
+ final primaryAddress = appWalletOf(context, listen: true).getPrimaryAddress();
return LwsKeysView(
labels: LwsKeysLabels(
@@ -57,7 +56,7 @@ class _LwsKeysScreenState extends State<LwsKeysScreen> with SecureScreenMixin {
reveal: i18n.generateSeedReveal,
warning: i18n.lwsKeysWarning,
),
- primaryAddress: _primaryAddress,
+ primaryAddress: primaryAddress,
secretViewKey: _secretViewKey,
restoreHeight: _restoreHeight.toString(),
onCopy: _copy,
### lib/screens/receive.dart
@@ -73,8 +73,10 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
final i18n = AppLocalizations.of(context)!;
final wallet = appWalletOf(context, listen: true);
final primaryAddress = wallet.getPrimaryAddress();
- final subaddress = wallet.getUnusedSubaddress();
final isDemoMode = wallet.connectionAddress == 'demo';
+ // The address and the index that labels it, as one value: read separately
+ // they can name different subaddresses.
+ final sub = isDemoMode ? null : wallet.unusedSubaddress;
final subSupported = wallet.serverSupportsSubaddresses;
final canToggle = subSupported == true && !isDemoMode;
@@ -83,7 +85,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
address = primaryAddress;
}
if (subSupported == true) {
- address = _showSubaddress ? subaddress : primaryAddress;
+ address = _showSubaddress ? sub?.address : primaryAddress;
}
final ready = (subSupported != null || isDemoMode) && address != null;
@@ -104,9 +106,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
onSelectTab: (index) => setState(() => _showSubaddress = index == 0),
address: address ?? '',
qrHeading: canToggle && _showSubaddress
- ? (wallet.unusedSubaddressIndex != null
- ? '${i18n.receiveSubaddressTab} #${wallet.unusedSubaddressIndex}'
- : i18n.receiveSubaddressTab)
+ ? (sub != null ? '${i18n.receiveSubaddressTab} #${sub.index}' : i18n.receiveSubaddressTab)
: i18n.receiveAddressHeading('Monero'),
warning: warning,
onCopy: () => _copyAddressToClipboard(address!),Why this scored 49/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.