What changed, and why it matters
This commit updates the Firo masternode user interface in Stack Wallet. It changes how the app guides users to create a 1000 FIRO collateral transaction, switches some dialog navigation to use the root navigator, and removes the automatic masternode registration flow that previously followed a successful collateral send. There is no clear security vulnerability introduced, but the changes touch navigation and state handling in ways that could affect stability or user experience.
Treat as a routine UI/UX refactor. Review the Particl wallet change for any unintended behavioral change in transaction signing, and verify that the new dialog dismissal guards prevent all crash paths without leaving dialogs stuck open. No immediate security response is indicated.
Security signals we found
Dialog navigation changed to use root navigator and guard against popping when not mounted or not open
Automatic masternode registration flow after collateral send was removed; user is now instructed to continue manually
Particl transaction input construction no longer passes `isParticl: true` to the builder
New pending-collateral UTXO search added, with confirmation checks
Send flow now pre-fills a self-address and amount for masternode collateral
Evidence from the diff
The patch refactors masternode collateral creation and sending flows. Key changes include: adding a helper to find pending collateral UTXOs, prompting the user to self-send 1000 FIRO when no collateral exists, pre-filling the Send view for this purpose, removing automatic vout resolution and post-send masternode registration from ConfirmTransactionView, and making dialog dismissal more defensive with useRootNavigator and isSendingDialogOpen guards. A Particl wallet change removes an isParticl flag from a transaction input builder. The commit is described as a UI update, not a security fix.
Changed components
lib/pages/masternodes/create_masternode_view.dartlib/pages/masternodes/masternodes_home_view.dartlib/pages/send_view/confirm_transaction_view.dartlib/pages/send_view/send_view.dartlib/pages/send_view/sub_widgets/building_transaction_dialog.dartlib/pages/exchange_view/confirm_change_now_send.dartlib/wallets/wallet/impl/particl_wallet.dartlib/widgets/stack_dialog.dartInspect captured patch +395 / −297
diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart
index b98e4e3..3a5c759 100644
--- a/lib/pages/exchange_view/confirm_change_now_send.dart
+++ b/lib/pages/exchange_view/confirm_change_now_send.dart
@@ -83,10 +83,24 @@ class _ConfirmChangeNowSendViewState
final coin = wallet.info.coin;
final sendProgressController = ProgressAndSuccessController();
+ var isSendingDialogOpen = false;
+ void closeSendingDialog() {
+ if (!context.mounted || !isSendingDialogOpen) {
+ return;
+ }
+ final rootNavigator = Navigator.of(context, rootNavigator: true);
+ if (rootNavigator.canPop()) {
+ rootNavigator.pop();
+ }
+ isSendingDialogOpen = false;
+ }
+
+ isSendingDialogOpen = true;
unawaited(
showDialog<void>(
context: context,
+ useRootNavigator: true,
useSafeArea: false,
barrierDismissible: false,
builder: (context) {
@@ -95,7 +109,7 @@ class _ConfirmChangeNowSendViewState
controller: sendProgressController,
);
},
- ),
+ ).whenComplete(() => isSendingDialogOpen = false),
);
final time = Future<dynamic>.delayed(const Duration(milliseconds: 2500));
@@ -141,10 +155,8 @@ class _ConfirmChangeNowSendViewState
// pop back to wallet
if (context.mounted) {
+ closeSendingDialog();
if (Util.isDesktop) {
- // pop sending dialog
- Navigator.of(context, rootNavigator: true).pop();
-
// one day we'll do routing right
Navigator.of(context, rootNavigator: true).pop();
if (widget.fromDesktopStep4) {
@@ -162,7 +174,7 @@ class _ConfirmChangeNowSendViewState
);
// pop sending dialog
- Navigator.of(context).pop();
+ closeSendingDialog();
await showDialog<dynamic>(
context: context,
diff --git a/lib/pages/masternodes/create_masternode_view.dart b/lib/pages/masternodes/create_masternode_view.dart
index 3692724..9916c04 100644
--- a/lib/pages/masternodes/create_masternode_view.dart
+++ b/lib/pages/masternodes/create_masternode_view.dart
@@ -54,7 +54,11 @@ class _CreateMasternodeDialogState extends ConsumerState<CreateMasternodeView> {
style: STextStyles.desktopH3(context),
),
),
- const DesktopDialogCloseButton(),
+ DesktopDialogCloseButton(
+ onPressedOverride: () {
+ Navigator.of(context, rootNavigator: true).pop();
+ },
+ ),
],
),
Flexible(
diff --git a/lib/pages/masternodes/masternodes_home_view.dart b/lib/pages/masternodes/masternodes_home_view.dart
index 0405056..e50fd10 100644
--- a/lib/pages/masternodes/masternodes_home_view.dart
+++ b/lib/pages/masternodes/masternodes_home_view.dart
@@ -4,7 +4,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:isar_community/isar.dart';
+import 'package:tuple/tuple.dart';
+import '../../models/send_view_auto_fill_data.dart';
import '../../models/isar/models/blockchain_data/utxo.dart';
import '../../providers/global/wallets_provider.dart';
import '../../themes/stack_colors.dart';
@@ -22,6 +24,7 @@ import '../../widgets/desktop/primary_button.dart';
import '../../widgets/dialogs/s_dialog.dart';
import '../../widgets/loading_indicator.dart';
import '../../widgets/stack_dialog.dart';
+import '../send_view/send_view.dart';
import 'create_masternode_view.dart';
import 'sub_widgets/masternodes_list.dart';
import 'sub_widgets/masternodes_table_desktop.dart';
@@ -39,6 +42,11 @@ class MasternodesHomeView extends ConsumerStatefulWidget {
}
class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
+ static final BigInt _masternodeCollateralRaw = Amount.fromDecimal(
+ kMasterNodeValue,
+ fractionDigits: 8,
+ ).raw;
+
late Future<List<MasternodeInfo>> _masternodesFuture;
bool _hasPromptedForCollateral = false;
bool _isCheckingForCollateral = false;
@@ -95,27 +103,153 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
return null;
}
+ Future<
+ ({String txid, int vout, String address, int confirmations, int required})?
+ >
+ _findPendingCollateralUtxo() async {
+ final wallet = ref.read(pWallets).getWallet(widget.walletId) as FiroWallet;
+ final List<UTXO> utxos = await wallet.mainDB
+ .getUTXOs(widget.walletId)
+ .findAll();
+ final currentChainHeight = await wallet.chainHeight;
+ final requiredConfirms = wallet.cryptoCurrency.minConfirms;
+ final masternodeRaw = Amount.fromDecimal(
+ kMasterNodeValue,
+ fractionDigits: wallet.cryptoCurrency.fractionDigits,
+ ).raw.toInt();
+
+ ({String txid, int vout, String address, int confirmations, int required})?
+ bestPending;
+
+ for (final utxo in utxos) {
+ if (utxo.value != masternodeRaw ||
+ utxo.isBlocked ||
+ utxo.used == true ||
+ utxo.address == null) {
+ continue;
+ }
+
+ final confirmations = utxo.getConfirmations(currentChainHeight);
+ final isConfirmed = utxo.isConfirmed(
+ currentChainHeight,
+ wallet.cryptoCurrency.minConfirms,
+ wallet.cryptoCurrency.minCoinbaseConfirms,
+ );
+
+ if (isConfirmed) {
+ continue;
+ }
+
+ final candidate = (
+ txid: utxo.txid,
+ vout: utxo.vout,
+ address: utxo.address!,
+ confirmations: confirmations,
+ required: requiredConfirms,
+ );
+
+ if (bestPending == null ||
+ candidate.confirmations > bestPending.confirmations) {
+ bestPending = candidate;
+ }
+ }
+
+ return bestPending;
+ }
+
Future<void> _createMasternode() async {
+ final wallet = ref.read(pWallets).getWallet(widget.walletId) as FiroWallet;
final collateral = await _findCollateralUtxo();
if (!mounted) {
return;
}
if (collateral == null) {
- await showDialog<void>(
- context: context,
- builder: (_) => StackOkDialog(
- title: "No collateral found",
- message:
- "A masternode needs one confirmed, unblocked transparent "
- "UTXO of exactly 1000 FIRO.\n\n"
- "Total balance above 1000 FIRO is not enough if no single "
- "1000 output exists. Also ensure fee is not subtracted from "
- "the recipient amount when sending to yourself.",
- desktopPopRootNavigator: Util.isDesktop,
- maxWidth: Util.isDesktop ? 400 : null,
- ),
- );
+ final pendingCollateral = await _findPendingCollateralUtxo();
+ if (!mounted) {
+ return;
+ }
+ if (pendingCollateral != null) {
+ final message =
+ "Your 1000 FIRO collateral is on its way.\n\n"
+ "Waiting for confirmations...\n"
+ "Once confirmed, click Create Masternode again to continue.";
+ await showDialog<void>(
+ context: context,
+ builder: (ctx) => StackOkDialog(
+ title: "Waiting for collateral confirmation",
+ message: message,
+ desktopPopRootNavigator: Util.isDesktop,
+ maxWidth: Util.isDesktop ? 420 : null,
+ ),
+ );
+ return;
+ }
+
+ final spendableBalance = wallet.info.cachedBalance.spendable.raw;
+ if (spendableBalance < _masternodeCollateralRaw) {
+ await showDialog<void>(
+ context: context,
+ builder: (ctx) => StackOkDialog(
+ title: "Not enough FIRO to create the collateral",
+ message:
+ "A masternode collateral is exactly 1000 FIRO on your transparent balance, plus a "
+ "small network fee to send it. Your spendable transparent balance is "
+ "below this amount.\n\n"
+ "Add more FIRO to your wallet, then click Create "
+ "Masternode again to continue.",
+ desktopPopRootNavigator: Util.isDesktop,
+ maxWidth: Util.isDesktop ? 420 : null,
+ ),
+ );
+ return;
+ }
+
+ if (Util.isDesktop) {
+ final shouldOpenSend = await showDialog<bool>(
+ context: context,
+ builder: (ctx) => StackDialog(
+ title: "Set up your 1000 FIRO masternode collateral?",
+ message:
+ "Registering a masternode requires a 1000 FIRO collateral: "
+ "a single confirmed amount sitting in your wallet. We didn't "
+ "find one, but you have enough FIRO to create it.\n\n"
+ "We can help by opening the Send window with a new address "
+ "you own pre-filled, ready for you to send 1000 FIRO to it. "
+ "This consolidates your smaller amounts into the single 1000 "
+ "FIRO collateral you need. The network fee is paid from your "
+ "remaining balance.\n\n"
+ "Once you have sent it, wait for the transaction to confirm, "
+ "then click Create Masternode again to continue.",
+ leftButton: TextButton(
+ style: Theme.of(
+ ctx,
+ ).extension<StackColors>()!.getSecondaryEnabledButtonStyle(ctx),
+ onPressed: () => Navigator.of(ctx).pop(),
+ child: Text(
+ "Cancel",
+ style: STextStyles.button(ctx).copyWith(
+ color: Theme.of(
+ ctx,
+ ).extension<StackColors>()!.accentColorDark,
+ ),
+ ),
+ ),
+ rightButton: TextButton(
+ style: Theme.of(
+ ctx,
+ ).extension<StackColors>()!.getPrimaryEnabledButtonStyle(ctx),
+ onPressed: () => Navigator.of(ctx).pop(true),
+ child: Text("Open Send", style: STextStyles.button(ctx)),
+ ),
+ ),
+ );
+ if (shouldOpenSend == true && mounted) {
+ await _openCreateCollateralSendFlow(wallet);
+ }
+ } else {
+ await _openCreateCollateralSendFlow(wallet);
+ }
return;
}
@@ -147,6 +281,31 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
}
}
+ Future<void> _openCreateCollateralSendFlow(FiroWallet wallet) async {
+ var selfAddress = await wallet.getCurrentReceivingAddress();
+ if (selfAddress == null) {
+ await wallet.generateNewReceivingAddress();
+ selfAddress = await wallet.getCurrentReceivingAddress();
+ }
+ if (!mounted || selfAddress == null) {
+ return;
+ }
+
+ await Navigator.of(context).pushNamed(
+ SendView.routeName,
+ arguments: Tuple3(
+ widget.walletId,
+ wallet.cryptoCurrency,
+ SendViewAutoFillData(
+ address: selfAddress.value,
+ contactLabel: "My FIRO address",
+ amount: kMasterNodeValue,
+ note: "Masternode collateral prep (1000 FIRO self-send).",
+ ),
+ ),
+ );
+ }
+
Future<void> _maybePromptForExistingCollateral() async {
if (_hasPromptedForCollateral || _isCheckingForCollateral || !mounted) {
return;
diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart
index 8f7eab5..732008d 100644
--- a/lib/pages/send_view/confirm_transaction_view.dart
+++ b/lib/pages/send_view/confirm_transaction_view.dart
@@ -24,8 +24,8 @@ import '../../models/isar/models/transaction_note.dart';
import '../../models/isar/ordinal.dart';
import '../../notifications/show_flush_bar.dart';
import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart';
+import '../../pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart';
-import '../../providers/global/global_nav_key_provider.dart';
import '../../providers/providers.dart';
import '../../providers/wallet/public_private_balance_state_provider.dart';
import '../../route_generator.dart';
@@ -57,7 +57,6 @@ import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
-import '../../widgets/dialogs/s_dialog.dart';
import '../../widgets/icon_widgets/x_icon.dart';
import '../../widgets/rounded_container.dart';
import '../../widgets/rounded_white_container.dart';
@@ -65,7 +64,6 @@ import '../../widgets/stack_dialog.dart';
import '../../widgets/stack_text_field.dart';
import '../../widgets/textfield_icon_button.dart';
import '../../wl_gen/interfaces/libepiccash_interface.dart';
-import '../masternodes/create_masternode_view.dart';
import '../pinpad_views/lock_screen_view.dart';
import '../wallet_view/wallet_view.dart';
import 'sub_widgets/epic_slatepack_dialog.dart';
@@ -78,7 +76,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget {
required this.txData,
required this.walletId,
required this.onSuccess,
- this.routeOnSuccessName = WalletView.routeName,
+ this.routeOnSuccessName,
this.isTradeTransaction = false,
this.isPaynymTransaction = false,
this.isPaynymNotificationTransaction = false,
@@ -90,7 +88,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget {
final TxData txData;
final String walletId;
- final String routeOnSuccessName;
+ final String? routeOnSuccessName;
final bool isTradeTransaction;
final bool isPaynymTransaction;
final bool isPaynymNotificationTransaction;
@@ -312,106 +310,28 @@ class _ConfirmTransactionViewState
}
}
- Future<int?> _resolveFiroCollateralVout({
- required FiroWallet wallet,
- required String txid,
- required String recipientAddress,
- required Amount amount,
- }) async {
- try {
- final tx = await wallet.electrumXClient.getTransaction(txHash: txid);
- final outputs = tx['vout'];
- if (outputs is! List) {
- return null;
- }
-
- for (final output in outputs) {
- if (output is! Map) {
- continue;
- }
- final outputMap = Map<String, dynamic>.from(output);
- final n = outputMap['n'];
- final outputIndex = switch (n) {
- int value => value,
- String value => int.tryParse(value),
- _ => null,
- };
- if (outputIndex == null) {
- continue;
- }
-
- final valueDecimal = Decimal.tryParse(outputMap['value'].toString());
- if (valueDecimal == null) {
- continue;
- }
- final outputAmount = Amount.fromDecimal(
- valueDecimal,
- fractionDigits: wallet.cryptoCurrency.fractionDigits,
- );
- if (outputAmount != amount) {
- continue;
- }
-
- final scriptPubKey = outputMap['scriptPubKey'];
- if (scriptPubKey is! Map) {
- continue;
- }
-
- final recipientAddresses = <String>{};
- final addresses = scriptPubKey['addresses'];
- if (addresses is List) {
- recipientAddresses.addAll(addresses.whereType<String>());
- }
-
- final address = scriptPubKey['address'];
- if (address is String) {
- recipientAddresses.add(address);
- }
-
- if (recipientAddresses.contains(recipientAddress)) {
- return outputIndex;
- }
- }
- } catch (e, s) {
- Logging.instance.w(
- "Failed to resolve collateral vout for txid=$txid: $e",
- error: e,
- stackTrace: s,
- );
- }
-
- return null;
- }
-
- void _showMasternodeSubmittedDialog(BuildContext? rootContext, String txid) {
- if (rootContext == null) {
- return;
- }
- unawaited(
- showDialog<void>(
- context: rootContext,
- builder: (_) => StackOkDialog(
- title: "Masternode Registration Submitted",
- message:
- "Masternode registration submitted, your masternode will "
- "appear in the list after the tx is confirmed.\n\nTransaction "
- "ID: $txid",
- desktopPopRootNavigator: Util.isDesktop,
- maxWidth: Util.isDesktop ? 400 : null,
- ),
- ),
- );
- }
-
Future<void> _attemptSend(BuildContext context) async {
final wallet = ref.read(pWallets).getWallet(walletId);
final coin = wallet.info.coin;
final sendProgressController = ProgressAndSuccessController();
+ var isSendingDialogOpen = true;
+
+ void closeSendingDialog() {
+ if (!context.mounted || !isSendingDialogOpen) {
+ return;
+ }
+ final navigator = Navigator.of(context, rootNavigator: true);
+ if (navigator.canPop()) {
+ navigator.pop();
+ }
+ isSendingDialogOpen = false;
+ }
unawaited(
showDialog<dynamic>(
context: context,
+ useRootNavigator: true,
useSafeArea: false,
barrierDismissible: false,
builder: (context) {
@@ -420,7 +340,7 @@ class _ConfirmTransactionViewState
controller: sendProgressController,
);
},
- ),
+ ).whenComplete(() => isSendingDialogOpen = false),
);
final time = Future<dynamic>.delayed(const Duration(milliseconds: 2500));
@@ -481,6 +401,7 @@ class _ConfirmTransactionViewState
context,
wallet as MimblewimblecoinWallet,
);
+ closeSendingDialog();
return; // Exit early, don't continue with normal transaction flow.
} else {
// Handle MWCMQS or HTTP transactions normally.
@@ -504,6 +425,7 @@ class _ConfirmTransactionViewState
context,
wallet as EpiccashWallet,
);
+ closeSendingDialog();
return; // Exit early, don't continue with normal transaction flow.
} else {
// Handle Epicbox transactions normally.
@@ -553,6 +475,8 @@ class _ConfirmTransactionViewState
unawaited(wallet.refresh());
}
+ closeSendingDialog();
+
widget.onSuccess.call();
// Check for 1000 FIRO transparent self-send → prompt MN registration
@@ -583,97 +507,28 @@ class _ConfirmTransactionViewState
.findFirst();
if (ownAddress != null && context.mounted) {
- final collateralVout = await _resolveFiroCollateralVout(
- wallet: wallet,
- txid: confirmedTx.txid!,
- recipientAddress: mnRecipient.address,
- amount: masternodeAmount,
+ await showDialog<void>(
+ context: context,
+ builder: (_) => StackOkDialog(
+ title: "Collateral transaction sent",
+ message:
+ "Your 1000 FIRO collateral transaction was sent "
+ "successfully. Once it confirms, open Masternodes and "
+ "click Create Masternode to continue.",
+ desktopPopRootNavigator: Util.isDesktop,
+ maxWidth: Util.isDesktop ? 420 : null,
+ ),
);
- if (!context.mounted) {
- return;
- }
- if (collateralVout == null) {
- unawaited(
- showFloatingFlushBar(
- type: FlushBarType.warning,
- message:
- "Unable to determine collateral output index "
- "automatically. Open Masternodes and select your "
- "1000 FIRO UTXO manually.",
- context: context,
- ),
- );
- } else {
- navigatedToMN = true;
- final rootContext = ref.read(pNavKey).currentContext;
-
- void completeMnParentNavigation() {
- if (widget.onSuccessInsteadOfRouteOnSuccess == null) {
- if (isDesktop) {
- Navigator.of(
- context,
- ).popUntil(ModalRoute.withName(routeOnSuccessName));
- } else {
- final navigator = Navigator.of(context);
- navigator.popUntil(
- ModalRoute.withName(routeOnSuccessName),
- );
- }
- } else {
- widget.onSuccessInsteadOfRouteOnSuccess!.call();
- }
- }
-
- completeMnParentNavigation();
-
- if (isDesktop) {
- if (rootContext != null && rootContext.mounted) {
- unawaited(
- showDialog<Object>(
- context: rootContext,
- barrierDismissible: true,
- builder: (_) => SDialog(
- child: CreateMasternodeView(
- firoWalletId: walletId,
- collateralTxid: confirmedTx.txid!,
- collateralVout: collateralVout,
- collateralAddress: mnRecipient.address,
- ),
- ),
- ).then((result) {
- if (result is String) {
- _showMasternodeSubmittedDialog(rootContext, result);
- }
- }),
- );
- }
- } else {
- final navContext =
- (rootContext != null && rootContext.mounted)
- ? rootContext
- : context;
- if (!navContext.mounted) {
- return;
- }
- unawaited(
- Navigator.of(navContext)
- .pushNamed(
- CreateMasternodeView.routeName,
- arguments: {
- 'walletId': walletId,
- 'collateralTxid': confirmedTx.txid!,
- 'collateralVout': collateralVout,
- 'collateralAddress': mnRecipient.address,
- },
- )
- .then((result) {
- if (result is String) {
- _showMasternodeSubmittedDialog(rootContext, result);
- }
- }),
- );
+ if (context.mounted) {
+ // Pop confirm + send; returns to the screen that opened send
+ // (e.g. Masternodes or desktop wallet) without relying on
+ // popUntil matching a route name in the nested navigator.
+ final navigator = Navigator.of(context);
+ for (var i = 0; i < 2 && navigator.canPop(); i++) {
+ navigator.pop();
}
+ navigatedToMN = true;
}
}
} else if (mnRecipient != null &&
@@ -747,7 +602,7 @@ class _ConfirmTransactionViewState
} on BadHttpAddressException catch (_) {
if (context.mounted) {
// pop building dialog
- Navigator.of(context).pop();
+ closeSendingDialog();
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
@@ -763,7 +618,7 @@ class _ConfirmTransactionViewState
Logging.instance.e(message, error: e, stackTrace: s);
// pop sending dialog
if (context.mounted) {
- Navigator.of(context).pop();
+ closeSendingDialog();
await showDialog<void>(
context: context,
@@ -840,7 +695,9 @@ class _ConfirmTransactionViewState
isDesktop = Util.isDesktop;
walletId = widget.walletId;
- routeOnSuccessName = widget.routeOnSuccessName;
+ routeOnSuccessName =
+ widget.routeOnSuccessName ??
+ (Util.isDesktop ? DesktopWalletView.routeName : WalletView.routeName);
_noteFocusNode = FocusNode();
noteController = TextEditingController();
noteController.text = widget.txData.note ?? "";
@@ -981,8 +838,7 @@ class _ConfirmTransactionViewState
AppBarBackButton(
size: 40,
iconSize: 24,
- onPressed: () =>
- Navigator.of(context, rootNavigator: true).pop(),
+ onPressed: () => Navigator.of(context).pop(),
),
Text(
"Confirm $unit transaction",
diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart
index 94b5663..db5b292 100644
--- a/lib/pages/send_view/send_view.dart
+++ b/lib/pages/send_view/send_view.dart
@@ -16,6 +16,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
+import 'package:isar_community/isar.dart';
import 'package:tuple/tuple.dart';
import '../../models/epic_slatepack_models.dart';
@@ -81,6 +82,7 @@ import '../../widgets/stack_text_field.dart';
import '../../widgets/textfield_icon_button.dart';
import '../address_book_views/address_book_view.dart';
import '../coin_control/coin_control_view.dart';
+import '../masternodes/masternodes_home_view.dart';
import 'confirm_transaction_view.dart';
import 'sub_widgets/building_transaction_dialog.dart';
import 'sub_widgets/dual_balance_selection_sheet.dart';
@@ -142,6 +144,7 @@ class _SendViewState extends ConsumerState<SendView> {
late final bool hasOptionalMemo;
late final bool isFiro;
late final bool isEth;
+ late final bool _isMasternodeCollateralSelfSend;
Amount? _cachedAmountToSend;
String? _address;
@@ -270,6 +273,82 @@ class _SendViewState extends ConsumerState<SendView> {
}
}
+ Future<void> _pickMyAddressForMasternodeCollateral() async {
+ final wallet = ref.read(pWallets).getWallet(walletId);
+ if (wallet is! FiroWallet) {
+ return;
+ }
+
+ var currentAddress = await wallet.getCurrentReceivingAddress();
+ if (currentAddress == null) {
+ await wallet.generateNewReceivingAddress();
+ currentAddress = await wallet.getCurrentReceivingAddress();
+ }
+
+ final allWalletAddresses = await wallet.mainDB.isar.addresses
+ .where()
+ .walletIdEqualTo(walletId)
+ .findAll();
+
+ final transparentAddresses = allWalletAddresses
+ .where((e) => e.type != AddressType.spark)
+ .map((e) => e.value)
+ .where((String e) => e.isNotEmpty)
+ .toSet();
+
+ if (currentAddress != null &&
+ wallet.cryptoCurrency.getAddressType(currentAddress.value) !=
+ AddressType.spark) {
+ transparentAddresses.add(currentAddress.value);
+ }
+
+ final addresses = <String>{...transparentAddresses}.toList()..sort();
+
+ if (!mounted || addresses.isEmpty) {
+ return;
+ }
+
+ final selectedAddress = await showDialog<String>(
+ context: context,
+ builder: (ctx) => AlertDialog(
+ title: const Text("Choose your address"),
+ content: SizedBox(
+ width: 520,
+ child: ListView.builder(
+ shrinkWrap: true,
+ itemCount: addresses.length,
+ itemBuilder: (_, index) => ListTile(
+ contentPadding: EdgeInsets.zero,
+ title: Text(
+ addresses[index],
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ onTap: () => Navigator.of(ctx).pop(addresses[index]),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(ctx).pop(),
+ child: const Text("Cancel"),
+ ),
+ ],
+ ),
+ );
+
+ if (selectedAddress == null) {
+ return;
+ }
+
+ _address = selectedAddress;
+ sendToController.text = selectedAddress;
+ _setValidAddressProviders(_address);
+ setState(() {
+ _addressToggleFlag = true;
+ });
+ }
+
Future<void> _scanQr() async {
try {
// ref
@@ -867,13 +946,15 @@ class _SendViewState extends ConsumerState<SendView> {
}
}
+ final shouldShowBuildingDialog = mounted && !Util.isDesktop;
try {
bool wasCancelled = false;
- if (mounted) {
+ if (shouldShowBuildingDialog) {
unawaited(
showDialog<void>(
context: context,
+ useRootNavigator: false,
useSafeArea: false,
barrierDismissible: false,
builder: (context) {
@@ -885,8 +966,6 @@ class _SendViewState extends ConsumerState<SendView> {
BalanceType.private,
onCancel: () {
wasCancelled = true;
-
- Navigator.of(context).pop();
},
);
},
@@ -1062,8 +1141,10 @@ class _SendViewState extends ConsumerState<SendView> {
txData = txData.copyWith(noteOnChain: onChainNoteController.text);
}
- // pop building dialog
- Navigator.of(context).pop();
+ if (shouldShowBuildingDialog) {
+ // pop building dialog
+ Navigator.of(context, rootNavigator: false).pop();
+ }
unawaited(
Navigator.of(context).push(
@@ -1073,7 +1154,11 @@ class _SendViewState extends ConsumerState<SendView> {
txData: txData,
walletId: walletId,
isPaynymTransaction: isPaynymSend,
- onSuccess: clearSendForm,
+ onSuccess: () {
+ if (mounted) {
+ clearSendForm();
+ }
+ },
),
settings: const RouteSettings(
name: ConfirmTransactionView.routeName,
@@ -1085,8 +1170,10 @@ class _SendViewState extends ConsumerState<SendView> {
} catch (e, s) {
Logging.instance.e("$e\n$s", error: e, stackTrace: s);
if (mounted) {
- // pop building dialog
- Navigator.of(context).pop();
+ if (shouldShowBuildingDialog) {
+ // pop building dialog
+ Navigator.of(context, rootNavigator: false).pop();
+ }
unawaited(
showDialog<dynamic>(
@@ -1266,8 +1353,14 @@ class _SendViewState extends ConsumerState<SendView> {
_data = widget.autoFillData;
walletId = widget.walletId;
clipboard = widget.clipboard;
+ _isMasternodeCollateralSelfSend =
+ (_data?.note.contains("Masternode collateral prep") ?? false) && isFiro;
WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (_isMasternodeCollateralSelfSend) {
+ ref.read(publicPrivateBalanceStateProvider.state).state =
+ BalanceType.public;
+ }
ref.refresh(feeSheetSessionCacheProvider);
ref.refresh(pIsExchangeAddress);
});
@@ -1476,12 +1569,10 @@ class _SendViewState extends ConsumerState<SendView> {
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
- onPressed: () async {
- if (FocusScope.of(context).hasFocus) {
- FocusScope.of(context).unfocus();
- await Future<void>.delayed(const Duration(milliseconds: 50));
- }
- if (context.mounted) {
+ onPressed: () {
+ if (_isMasternodeCollateralSelfSend) {
+ Navigator.of(context).pop();
+ } else {
Navigator.of(context).pop();
}
},
@@ -1830,6 +1921,18 @@ class _SendViewState extends ConsumerState<SendView> {
child:
const AddressBookIcon(),
),
+ if (_isMasternodeCollateralSelfSend)
+ TextFieldIconButton(
+ semanticsLabel:
+ "My addresses button. Opens your wallet addresses for collateral self-send.",
+ key: const Key(
+ "sendViewMyAddressesButtonKey",
+ ),
+ onTap:
+ _pickMyAddressForMasternodeCollateral,
+ child:
+ const AddressBookIcon(),
+ ),
if (sendToController
.text
.isEmpty)
diff --git a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart
index f7dc832..095bd9f 100644
--- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart
+++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart
@@ -50,49 +50,29 @@ class _RestoringDialogState extends ConsumerState<BuildingTransactionDialog> {
@override
Widget build(BuildContext context) {
- final assetPath = ref.watch(
- coinImageSecondaryProvider(
- widget.coin,
- ),
- );
+ final assetPath = ref.watch(coinImageSecondaryProvider(widget.coin));
if (Util.isDesktop) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
- Text(
- "Generating transaction",
- style: STextStyles.desktopH3(context),
- ),
- if (widget.isSpark)
- const SizedBox(
- height: 16,
- ),
+ Text("Generating transaction", style: STextStyles.desktopH3(context)),
+ if (widget.isSpark) const SizedBox(height: 16),
if (widget.isSpark)
Text(
"This may take a few minutes...",
style: STextStyles.desktopSubtitleH2(context),
),
- const SizedBox(
- height: 40,
- ),
+ const SizedBox(height: 40),
assetPath.endsWith(".gif")
- ? Image.file(
- File(
- assetPath,
- ),
- )
- : const RotatingArrows(
- width: 40,
- height: 40,
- ),
- const SizedBox(
- height: 40,
- ),
+ ? Image.file(File(assetPath))
+ : const RotatingArrows(width: 40, height: 40),
+ const SizedBox(height: 40),
SecondaryButton(
buttonHeight: ButtonHeight.l,
label: "Cancel",
onPressed: () {
+ Navigator.of(context).pop();
onCancel.call();
},
),
@@ -109,29 +89,20 @@ class _RestoringDialogState extends ConsumerState<BuildingTransactionDialog> {
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
- Image.file(
- File(
- assetPath,
- ),
- ),
+ Image.file(File(assetPath)),
Text(
"Generating transaction",
textAlign: TextAlign.center,
style: STextStyles.pageTitleH2(context),
),
- if (widget.isSpark)
- const SizedBox(
- height: 12,
- ),
+ if (widget.isSpark) const SizedBox(height: 12),
if (widget.isSpark)
Text(
"This may take a few minutes...",
textAlign: TextAlign.center,
style: STextStyles.w500_16(context),
),
- const SizedBox(
- height: 32,
- ),
+ const SizedBox(height: 32),
Row(
children: [
const Spacer(),
@@ -157,12 +128,10 @@ class _RestoringDialogState extends ConsumerState<BuildingTransactionDialog> {
)
: StackDialog(
title: "Generating transaction",
- message:
- widget.isSpark ? "This may take a few minutes..." : null,
- icon: const RotatingArrows(
- width: 24,
- height: 24,
- ),
+ message: widget.isSpark
+ ? "This may take a few minutes..."
+ : null,
+ icon: const RotatingArrows(width: 24, height: 24),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart
index 65bc9c4..7f4b84c 100644
--- a/lib/wallets/wallet/impl/particl_wallet.dart
+++ b/lib/wallets/wallet/impl/particl_wallet.dart
@@ -512,7 +512,6 @@ class ParticlWallet<T extends ElectrumXCurrencyInterface>
),
witnessValue: insAndKeys[i].utxo.value,
redeemScript: extraData[i].redeem,
- isParticl: true,
overridePrefix: cryptoCurrency.networkParams.bech32Hrp,
);
}
diff --git a/lib/widgets/stack_dialog.dart b/lib/widgets/stack_dialog.dart
index 2c56aa7..3af1a00 100644
--- a/lib/widgets/stack_dialog.dart
+++ b/lib/widgets/stack_dialog.dart
@@ -37,10 +37,9 @@ class StackDialogBase extends StatelessWidget {
bottom: 16 + keyboardPaddingAmount,
),
child: Column(
- mainAxisAlignment:
- !Util.isDesktop
- ? MainAxisAlignment.end
- : MainAxisAlignment.center,
+ mainAxisAlignment: !Util.isDesktop
+ ? MainAxisAlignment.end
+ : MainAxisAlignment.center,
children: [
Flexible(
child: SingleChildScrollView(
@@ -48,8 +47,9 @@ class StackDialogBase extends StatelessWidget {
borderRadius: BorderRadius.circular(20),
child: Container(
decoration: BoxDecoration(
- color:
- Theme.of(context).extension<StackColors>()!.popupBG,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(20),
),
child: Padding(padding: padding, child: child),
@@ -199,30 +199,26 @@ class StackOkDialog extends StatelessWidget {
const SizedBox(width: 8),
Expanded(
child: TextButton(
- onPressed:
- !Util.isDesktop
- ? () {
- Navigator.of(context).pop();
- onOkPressed?.call("OK");
+ onPressed: !Util.isDesktop
+ ? () {
+ Navigator.of(context).pop();
+ onOkPressed?.call("OK");
+ }
+ : () {
+ if (desktopPopRootNavigator) {
+ Navigator.of(context, rootNavigator: true).pop();
+ } else {
+ int count = 0;
+ Navigator.of(
+ context,
+ ).popUntil((_) => count++ >= 2);
+ // onOkPressed?.call("OK");
}
- : () {
- if (desktopPopRootNavigator) {
- Navigator.of(
- context,
- rootNavigator: true,
- ).pop();
- } else {
- int count = 0;
- Navigator.of(
- context,
- ).popUntil((_) => count++ >= 2);
- // onOkPressed?.call("OK");
- }
- },
+ },
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
- child: Text("Ok", style: STextStyles.button(context)),
+ child: Text("OK", style: STextStyles.button(context)),
),
),
],
Why this scored 23/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.