fix(firo): clear stale op return send state
What changed, and why it matters
This commit fixes a UI state bug in the Stack Wallet app where leftover 'OP_RETURN' data (extra information sometimes attached to a cryptocurrency transaction) could stick around when a user changed the recipient address or scanned a new QR code. The leftover data could then incorrectly block or alter sending for coins that don't support it. The patch clears that stale state in more places and disables the preview-transaction button when OP_RETURN data is present for any coin other than Firo.
Treat as a bug-fix commit with possible UX/security side effects. Review whether any other provider state (amount, fee, memo) is similarly stale across address changes. Verify that disabling the preview button for non-Firo coins with OP_RETURN data matches intended product behavior and does not introduce a denial-of-service for legitimate multi-coin OP_RETURN use. No immediate incident response required unless user reports of stuck funds or wrong transaction data surface.
Security signals we found
UI state not cleared on user input change
Transaction preview button logic depends on stale provider state
OP_RETURN data intended only for Firo could persist across coin/address changes
No explicit cryptographic or network-layer vulnerability in diff
Evidence from the diff
The change adds _setOpReturnData(null) calls at multiple points in send_view.dart and desktop_send.dart where the recipient address is updated via QR scan, manual edit, contact selection, or URI parsing. It also updates pPreviewTxButtonEnabled in preview_tx_button_state_provider.dart to disable the preview button if opReturnData is non-null and the selected coin is not Firo. The prior code only checked opReturnData == null inside the Firo branch, meaning other coins could silently carry stale OP_RETURN state and have the preview button enabled, or Firo could retain stale OP_RETURN data from a previous send/URI and behave incorrectly.
Changed components
lib/pages/send_view/send_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dartlib/providers/ui/preview_tx_button_state_provider.dartInspect captured patch +16 / −1
diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart
index 87e5bd0..e20bbbf 100644
--- a/lib/pages/send_view/send_view.dart
+++ b/lib/pages/send_view/send_view.dart
@@ -257,6 +257,7 @@ class _SendViewState extends ConsumerState<SendView> {
paymentData.coin?.uriScheme == coin.uriScheme) {
_applyUri(paymentData);
} else {
+ _setOpReturnData(null);
if (coin is Epiccash) {
content = AddressUtils().formatEpicCashAddress(content);
}
@@ -270,6 +271,7 @@ class _SendViewState extends ConsumerState<SendView> {
});
}
} catch (e) {
+ _setOpReturnData(null);
// strip http:// and https:// if content contains @
if (coin is Epiccash) {
content = AddressUtils().formatEpicCashAddress(content);
@@ -323,6 +325,7 @@ class _SendViewState extends ConsumerState<SendView> {
paymentData.coin?.uriScheme == coin.uriScheme) {
_applyUri(paymentData);
} else {
+ _setOpReturnData(null);
_address = qrResult.rawContent!.split("\n").first.trim();
sendToController.text = _address ?? "";
@@ -1281,6 +1284,7 @@ class _SendViewState extends ConsumerState<SendView> {
if (parsed != null) {
_applyUri(parsed);
} else {
+ _setOpReturnData(null);
sendToController.text = content;
_address = content;
@@ -1812,6 +1816,7 @@ class _SendViewState extends ConsumerState<SendView> {
);
}
} else {
+ _setOpReturnData(null);
await _checkSparkNameAndOrSetAddress(
newValue,
setController: false,
@@ -1861,6 +1866,9 @@ class _SendViewState extends ConsumerState<SendView> {
.text =
"";
_address = "";
+ _setOpReturnData(
+ null,
+ );
_setValidAddressProviders(
_address,
);
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
index d5d93ad..9388807 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
@@ -1783,6 +1783,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
await _checkSparkNameAndOrSetAddress(newValue);
}
} else {
+ _setOpReturnData(null);
await _checkSparkNameAndOrSetAddress(
newValue,
setController: false,
@@ -1896,6 +1897,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
);
if (entry != null) {
+ _setOpReturnData(null);
sendToController.text =
entry.other ?? entry.label;
diff --git a/lib/providers/ui/preview_tx_button_state_provider.dart b/lib/providers/ui/preview_tx_button_state_provider.dart
index b079504..285d4b8 100644
--- a/lib/providers/ui/preview_tx_button_state_provider.dart
+++ b/lib/providers/ui/preview_tx_button_state_provider.dart
@@ -52,6 +52,11 @@ final pIsSlatepack = Provider.family<bool, String>((ref, walletId) {
final pPreviewTxButtonEnabled = Provider.autoDispose.family<bool, CryptoCurrency>(
(ref, coin) {
final amount = ref.watch(pSendAmount) ?? Amount.zero;
+ final opReturnData = ref.watch(pOpReturnData);
+
+ if (coin is! Firo && opReturnData != null) {
+ return false;
+ }
// For MWC slatepack transactions, address validation is not required.
if (coin is Mimblewimblecoin) {
@@ -76,7 +81,7 @@ final pPreviewTxButtonEnabled = Provider.autoDispose.family<bool, CryptoCurrency
return (ref.watch(pValidSendToAddress) ||
ref.watch(pValidSparkSendToAddress)) &&
!ref.watch(pIsExchangeAddress) &&
- ref.watch(pOpReturnData) == null &&
+ opReturnData == null &&
amount > Amount.zero;
case BalanceType.public:
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.