What changed, and why it matters
This commit is a UI refactor for the masternode feature in Stack Wallet. It removes a hard-coded note-based detection system for masternode collateral transactions and instead lets the user explicitly start the masternode registration flow from the masternode screen. It also fixes a couple of small UI bugs, such as showing the wrong error message text in slatepack/slate failure dialogs and properly formatting pre-filled amounts in the desktop send form. There is no obvious security vulnerability introduced, but the change removes a large block of auto-detection logic that previously tried to identify 1000-FIRO self-sends as collateral.
Treat as a routine UI/UX refactor. Code reviewers should verify that removing the auto-detection logic does not leave users without a clear path to register masternodes after sending collateral, and that the new `_registeredCollateral()` check correctly matches the UTXO format used by the wallet. The two corrected error-message dialogs should be smoke-tested for Epic Cash sends.
Security signals we found
Removal of transaction-note-based collateral detection reduces reliance on user-editable note fields for security-critical state
Removal of post-send auto-prompt logic eliminates a class of UI confusion/UX bugs around collateral identification
Addition of registered-collateral filtering prevents re-selection of UTXOs already tied to a registered masternode
Two dialog content strings corrected from raw exception interpolation to a sanitized errorMessage variable
No new cryptographic, network, or permission changes observed
Evidence from the diff
The patch refactors Firo masternode UI flows. Key changes: deletes masternode_constants.dart and the MasternodeCollateralNotes class, removing note-based collateral detection; removes the auto-detection/prompt logic in confirm_transaction_view.dart that scanned outgoing recipients for 1000 FIRO transparent self-sends; moves collateral creation into MasternodesHomeView via explicit send dialogs (mobile and desktop); adds _registeredCollateral() filtering so the wallet does not suggest already-registered collateral UTXOs; fixes two dialog content strings in confirm_transaction_view.dart to use errorMessage instead of interpolating exception object e; enables the previously commented-out Masternodes navigation item in mobile wallet view and desktop wallet features; unifies desktop/mobile masternode list rendering into MasternodesList; and fixes amount formatting in DesktopSend/SendView when auto-fill data is present.
Changed components
lib/pages/masternodes/masternodes_home_view.dartlib/pages/masternodes/sub_widgets/masternodes_list.dartlib/pages/masternodes/sub_widgets/masternode_info_widget.dartlib/pages/masternodes/sub_widgets/register_masternode_form.dartlib/pages/send_view/confirm_transaction_view.dartlib/pages/send_view/send_view.dartlib/pages/wallet_view/wallet_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dartInspect captured patch +281 / −669
diff --git a/lib/pages/masternodes/masternode_constants.dart b/lib/pages/masternodes/masternode_constants.dart
deleted file mode 100644
index 9ee8c69..0000000
--- a/lib/pages/masternodes/masternode_constants.dart
+++ /dev/null
@@ -1,12 +0,0 @@
-abstract final class MasternodeCollateralNotes {
- MasternodeCollateralNotes._();
-
- static const unshield =
- "Masternode collateral unshield (1000 FIRO to transparent).";
- static const prep = "Masternode collateral prep (1000 FIRO self-send).";
-
- static bool isUnshield(String? note) =>
- note != null && note.contains(unshield);
-
- static bool isPrep(String? note) => note != null && note.contains(prep);
-}
diff --git a/lib/pages/masternodes/masternodes_home_view.dart b/lib/pages/masternodes/masternodes_home_view.dart
index 22698f1..f6dd1ca 100644
--- a/lib/pages/masternodes/masternodes_home_view.dart
+++ b/lib/pages/masternodes/masternodes_home_view.dart
@@ -9,7 +9,9 @@ import 'package:tuple/tuple.dart';
import '../../models/isar/models/blockchain_data/utxo.dart';
import '../../models/send_view_auto_fill_data.dart';
+import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart';
import '../../providers/global/wallets_provider.dart';
+import '../../providers/wallet/public_private_balance_state_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/amount/amount.dart';
import '../../utilities/assets.dart';
@@ -21,6 +23,8 @@ import '../../wallets/isar/models/wallet_info.dart';
import '../../wallets/wallet/impl/firo_wallet.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_app_bar.dart';
+import '../../widgets/desktop/desktop_dialog.dart';
+import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/desktop_scaffold.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
@@ -29,9 +33,7 @@ import '../../widgets/loading_indicator.dart';
import '../../widgets/stack_dialog.dart';
import '../send_view/send_view.dart';
import 'create_masternode_view.dart';
-import 'masternode_constants.dart';
import 'sub_widgets/masternodes_list.dart';
-import 'sub_widgets/masternodes_table_desktop.dart';
class MasternodesHomeView extends ConsumerStatefulWidget {
const MasternodesHomeView({super.key, required this.walletId});
@@ -79,6 +81,16 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
);
}
+ Future<Set<String>> _registeredCollateral() async {
+ try {
+ return (await _masternodesFuture)
+ .map((e) => "${e.collateralHash}:${e.collateralIndex}")
+ .toSet();
+ } catch (_) {
+ return {};
+ }
+ }
+
Future<({String txid, int vout, String address})?>
_findCollateralUtxo() async {
final wallet = ref.read(pWallets).getWallet(widget.walletId) as FiroWallet;
@@ -86,6 +98,7 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
.getUTXOs(widget.walletId)
.findAll();
final currentChainHeight = await wallet.chainHeight;
+ final registered = await _registeredCollateral();
final masternodeRaw = Amount.fromDecimal(
kMasterNodeValue,
fractionDigits: wallet.cryptoCurrency.fractionDigits,
@@ -95,6 +108,7 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
if (utxo.value == masternodeRaw &&
!utxo.isBlocked &&
utxo.used != true &&
+ !registered.contains("${utxo.txid}:${utxo.vout}") &&
utxo.isConfirmed(
currentChainHeight,
wallet.cryptoCurrency.minConfirms,
@@ -379,37 +393,43 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
return;
}
- if (Util.isDesktop) {
- final txid = await showDialog<Object>(
- context: context,
- barrierDismissible: true,
- builder: (context) => SDialog(
- child: CreateMasternodeView(
- firoWalletId: widget.walletId,
- collateralTxid: collateral.txid,
- collateralVout: collateral.vout,
- collateralAddress: collateral.address,
- ),
- ),
- );
- _handleSuccessTxid(txid);
- } else {
- final txid = await Navigator.of(context).pushNamed(
- CreateMasternodeView.routeName,
- arguments: {
- 'walletId': widget.walletId,
- 'collateralTxid': collateral.txid,
- 'collateralVout': collateral.vout,
- 'collateralAddress': collateral.address,
- },
- );
- _handleSuccessTxid(txid);
- }
+ await _openCreateMasternode(collateral);
} finally {
_createMasternodeLock = false;
}
}
+ Future<void> _openCreateMasternode(
+ ({String txid, int vout, String address}) collateral,
+ ) async {
+ final Object? txid;
+ if (Util.isDesktop) {
+ txid = await showDialog<Object>(
+ context: context,
+ barrierDismissible: true,
+ builder: (context) => SDialog(
+ child: CreateMasternodeView(
+ firoWalletId: widget.walletId,
+ collateralTxid: collateral.txid,
+ collateralVout: collateral.vout,
+ collateralAddress: collateral.address,
+ ),
+ ),
+ );
+ } else {
+ txid = await Navigator.of(context).pushNamed(
+ CreateMasternodeView.routeName,
+ arguments: {
+ 'walletId': widget.walletId,
+ 'collateralTxid': collateral.txid,
+ 'collateralVout': collateral.vout,
+ 'collateralAddress': collateral.address,
+ },
+ );
+ }
+ _handleSuccessTxid(txid);
+ }
+
Future<void> _openCreateCollateralSendFlow(
FiroWallet wallet, {
bool fromPrivate = false,
@@ -424,23 +444,57 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
return;
}
- await Navigator.of(context).pushNamed(
- SendView.routeName,
- arguments: Tuple3(
- widget.walletId,
- wallet.cryptoCurrency,
- SendViewAutoFillData(
- address: selfAddress.value,
- contactLabel: "My FIRO address",
- amount: fromPrivate
- ? (unshieldAmount ?? kMasterNodeValue)
- : kMasterNodeValue,
- note: fromPrivate
- ? MasternodeCollateralNotes.unshield
- : MasternodeCollateralNotes.prep,
- ),
- ),
+ ref.read(publicPrivateBalanceStateProvider.state).state = fromPrivate
+ ? BalanceType.private
+ : BalanceType.public;
+
+ final ticker = wallet.cryptoCurrency.ticker;
+ final autoFillData = SendViewAutoFillData(
+ address: selfAddress.value,
+ contactLabel: selfAddress.value,
+ amount: fromPrivate
+ ? (unshieldAmount ?? kMasterNodeValue)
+ : kMasterNodeValue,
);
+
+ if (Util.isDesktop) {
+ await showDialog<void>(
+ context: context,
+ builder: (context) => DesktopDialog(
+ maxWidth: 580,
+ maxHeight: double.infinity,
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: .spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "Send $ticker",
+ style: STextStyles.desktopH3(context),
+ ),
+ ),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32),
+ child: DesktopSend(
+ walletId: widget.walletId,
+ autoFillData: autoFillData,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ } else {
+ await Navigator.of(context).pushNamed(
+ SendView.routeName,
+ arguments: Tuple3(widget.walletId, wallet.cryptoCurrency, autoFillData),
+ );
+ }
}
Future<void> _maybePromptForExistingCollateral() async {
@@ -473,6 +527,8 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
message:
"A 1000 FIRO collateral UTXO was found in your wallet. "
"Would you like to register a masternode now?",
+ width: Util.isDesktop ? 580 : null,
+ padding: .all(Util.isDesktop ? 32 : 24),
leftButton: TextButton(
style: Theme.of(
ctx,
@@ -495,70 +551,50 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
),
);
- if (wantsMN == false || wantsMN == null) {
+ if (wantsMN != true) {
await _persistDismissedCollateral(
wallet,
collateral.txid,
collateral.vout,
);
+ return;
}
- if (wantsMN != true || !mounted) {
+ if (!mounted) {
return;
}
- if (Util.isDesktop) {
- final txid = await showDialog<Object>(
- context: context,
- barrierDismissible: true,
- builder: (context) => SDialog(
- child: CreateMasternodeView(
- firoWalletId: widget.walletId,
- collateralTxid: collateral.txid,
- collateralVout: collateral.vout,
- collateralAddress: collateral.address,
- ),
- ),
- );
- _handleSuccessTxid(txid);
- } else {
- final txid = await Navigator.of(context).pushNamed(
- CreateMasternodeView.routeName,
- arguments: {
- 'walletId': widget.walletId,
- 'collateralTxid': collateral.txid,
- 'collateralVout': collateral.vout,
- 'collateralAddress': collateral.address,
- },
- );
- _handleSuccessTxid(txid);
- }
+ await _openCreateMasternode(collateral);
} finally {
_isCheckingForCollateral = false;
}
}
+ Future<List<MasternodeInfo>> _fetchMasternodes() =>
+ (ref.read(pWallets).getWallet(widget.walletId) as FiroWallet)
+ .getMyMasternodes();
+
void _handleSuccessTxid(Object? txid) {
Logging.instance.i(
"$runtimeType _handleSuccessTxid($txid) called where mounted=$mounted",
);
if (mounted && txid is String) {
setState(() {
- _masternodesFuture =
- (ref.read(pWallets).getWallet(widget.walletId) as FiroWallet)
- .getMyMasternodes();
+ _masternodesFuture = _fetchMasternodes();
});
- showDialog<void>(
- context: context,
- 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,
+ unawaited(
+ showDialog<void>(
+ context: context,
+ 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,
+ ),
),
);
}
@@ -568,9 +604,7 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
void initState() {
super.initState();
- _masternodesFuture =
- (ref.read(pWallets).getWallet(widget.walletId) as FiroWallet)
- .getMyMasternodes();
+ _masternodesFuture = _fetchMasternodes();
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_maybePromptForExistingCollateral());
@@ -693,59 +727,68 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
return const Center(child: LoadingIndicator(height: 50, width: 50));
}
if (snapshot.hasError) {
- return Center(
- child: Text(
- "Failed to load masternodes",
- style: STextStyles.w600_14(context),
- ),
+ return _CenteredMessage(
+ message: "Failed to load masternodes",
+ buttonLabel: "Retry",
+ onPressed: () =>
+ setState(() => _masternodesFuture = _fetchMasternodes()),
);
}
final nodes = snapshot.data ?? const <MasternodeInfo>[];
if (nodes.isEmpty) {
- return Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(
- "No masternodes found",
- style: STextStyles.w600_14(context),
- ),
- const SizedBox(height: 24),
- Row(
- mainAxisSize: .min,
- mainAxisAlignment: .center,
- children: [
- PrimaryButton(
- label: "Create Your First Masternode",
- horizontalContentPadding: 16,
- buttonHeight: Util.isDesktop ? .l : null,
- onPressed: _createMasternode,
- ),
- ],
- ),
- ],
- ),
+ return _CenteredMessage(
+ message: "No masternodes found",
+ buttonLabel: "Create Your First Masternode",
+ onPressed: _createMasternode,
);
}
- if (Util.isDesktop) {
- return MasternodesTableDesktop(nodes: nodes);
- } else {
- return MasternodesList(nodes: nodes);
- }
+ return MasternodesList(nodes: nodes);
},
),
);
}
}
-class _OpenSendDialog extends StatelessWidget {
- const _OpenSendDialog({
- super.key,
- required this.title,
+class _CenteredMessage extends StatelessWidget {
+ const _CenteredMessage({
required this.message,
+ required this.buttonLabel,
+ required this.onPressed,
});
+ final String message, buttonLabel;
+ final VoidCallback onPressed;
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Column(
+ mainAxisAlignment: .center,
+ children: [
+ Text(message, style: STextStyles.w600_14(context)),
+ const SizedBox(height: 24),
+ Row(
+ mainAxisSize: .min,
+ mainAxisAlignment: .center,
+ children: [
+ PrimaryButton(
+ label: buttonLabel,
+ horizontalContentPadding: 16,
+ buttonHeight: Util.isDesktop ? .l : null,
+ onPressed: onPressed,
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _OpenSendDialog extends StatelessWidget {
+ const _OpenSendDialog({required this.title, required this.message});
+
final String title, message;
@override
diff --git a/lib/pages/masternodes/sub_widgets/masternode_info_widget.dart b/lib/pages/masternodes/sub_widgets/masternode_info_widget.dart
index c25b696..20e61d2 100644
--- a/lib/pages/masternodes/sub_widgets/masternode_info_widget.dart
+++ b/lib/pages/masternodes/sub_widgets/masternode_info_widget.dart
@@ -25,8 +25,6 @@ class MasternodeInfoWidget extends StatelessWidget {
crossAxisAlignment: .stretch,
mainAxisSize: .min,
children: [
- // not really the place for this in terms of structure but running
- // out of time...
Row(
mainAxisAlignment: .spaceBetween,
children: [
@@ -45,8 +43,6 @@ class MasternodeInfoWidget extends StatelessWidget {
padding: const EdgeInsets.only(left: 32, bottom: 32, right: 32),
child: RoundedWhiteContainer(
padding: .zero,
-
- // using listview kind of breaks
borderColor: Theme.of(
context,
).extension<StackColors>()!.backgroundAppBar,
diff --git a/lib/pages/masternodes/sub_widgets/masternodes_list.dart b/lib/pages/masternodes/sub_widgets/masternodes_list.dart
index 8df056f..4534362 100644
--- a/lib/pages/masternodes/sub_widgets/masternodes_list.dart
+++ b/lib/pages/masternodes/sub_widgets/masternodes_list.dart
@@ -2,9 +2,12 @@ import 'package:flutter/material.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/text_styles.dart';
+import '../../../utilities/util.dart';
import '../../../wallets/wallet/impl/firo_wallet.dart';
+import '../../../widgets/dialogs/s_dialog.dart';
import '../../../widgets/rounded_white_container.dart';
import '../masternode_details_view.dart';
+import 'masternode_info_widget.dart';
class MasternodesList extends StatelessWidget {
const MasternodesList({super.key, required this.nodes});
@@ -14,57 +17,78 @@ class MasternodesList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.separated(
- padding: EdgeInsets.zero,
+ padding: EdgeInsets.all(Util.isDesktop ? 24 : 16),
itemCount: nodes.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
- itemBuilder: (context, index) => Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: _MasternodeCard(node: nodes[index]),
- ),
+ itemBuilder: (_, index) => _MasternodeCard(node: nodes[index]),
);
}
}
-// TODO better styling
class _MasternodeCard extends StatelessWidget {
- const _MasternodeCard({super.key, required this.node});
+ const _MasternodeCard({required this.node});
final MasternodeInfo node;
+ Future<void> _showDetails(BuildContext context) async {
+ if (Util.isDesktop) {
+ await showDialog<void>(
+ context: context,
+ barrierDismissible: true,
+ builder: (_) => SDialog(
+ child: SizedBox(width: 600, child: MasternodeInfoWidget(info: node)),
+ ),
+ );
+ } else {
+ await Navigator.of(
+ context,
+ ).pushNamed(MasternodeDetailsView.routeName, arguments: node);
+ }
+ }
+
@override
Widget build(BuildContext context) {
final stack = Theme.of(context).extension<StackColors>()!;
+ final isActive = node.revocationReason == 0;
+
return RoundedWhiteContainer(
- onPressed: () => Navigator.of(
- context,
- ).pushNamed(MasternodeDetailsView.routeName, arguments: node),
- child: Column(
- mainAxisSize: .min,
+ padding: const EdgeInsets.all(16),
+ onPressed: () => _showDetails(context),
+ child: Row(
children: [
- Row(
- mainAxisAlignment: .spaceBetween,
- children: [
- Text("IP: ${node.serviceAddr}"),
- Container(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
- decoration: BoxDecoration(
- color: node.revocationReason == 0
- ? stack.accentColorGreen
- : stack.accentColorRed,
- borderRadius: BorderRadius.circular(8),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: .start,
+ mainAxisSize: .min,
+ children: [
+ Text(
+ "${node.serviceAddr}:${node.servicePort}",
+ style: STextStyles.titleBold12(context),
+ overflow: TextOverflow.ellipsis,
),
- child: Text(
- node.revocationReason == 0 ? "ACTIVE" : "REVOKED",
- style: STextStyles.w600_12(
+ const SizedBox(height: 2),
+ Text(
+ "Last paid height: ${node.lastPaidHeight}",
+ style: STextStyles.baseXS(
context,
- ).copyWith(color: stack.textWhite),
+ ).copyWith(color: stack.textSubtitle1),
),
- ),
- ],
+ ],
+ ),
),
- Row(
- mainAxisAlignment: .spaceBetween,
- children: [Text("Last Paid Height: ${node.lastPaidHeight}")],
+ const SizedBox(width: 12),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ decoration: BoxDecoration(
+ color: isActive ? stack.accentColorGreen : stack.accentColorRed,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ isActive ? "ACTIVE" : "REVOKED",
+ style: STextStyles.w600_12(
+ context,
+ ).copyWith(color: stack.textWhite),
+ ),
),
],
),
diff --git a/lib/pages/masternodes/sub_widgets/masternodes_table_desktop.dart b/lib/pages/masternodes/sub_widgets/masternodes_table_desktop.dart
deleted file mode 100644
index 3b3727d..0000000
--- a/lib/pages/masternodes/sub_widgets/masternodes_table_desktop.dart
+++ /dev/null
@@ -1,182 +0,0 @@
-import 'package:flutter/material.dart';
-
-import '../../../themes/stack_colors.dart';
-import '../../../utilities/text_styles.dart';
-import '../../../wallets/wallet/impl/firo_wallet.dart';
-import '../../../widgets/dialogs/s_dialog.dart';
-import 'masternode_info_widget.dart';
-
-class MasternodesTableDesktop extends StatelessWidget {
- const MasternodesTableDesktop({super.key, required this.nodes});
-
- final List<MasternodeInfo> nodes;
-
- @override
- Widget build(BuildContext context) {
- final stack = Theme.of(context).extension<StackColors>()!;
- return Container(
- color: stack.textFieldDefaultBG,
- child: Column(
- children: [
- // Fixed header
- Container(
- height: 56,
- color: stack.textFieldDefaultBG,
- child: Row(
- children: [
- const Expanded(
- flex: 2,
- child: Align(
- alignment: Alignment.centerRight,
- child: Padding(
- padding: EdgeInsets.symmetric(horizontal: 16),
- child: Text('IP'),
- ),
- ),
- ),
- const Expanded(
- flex: 2,
- child: Align(
- alignment: Alignment.centerRight,
- child: Padding(
- padding: EdgeInsets.symmetric(horizontal: 16),
- child: Text('Last Paid Height'),
- ),
- ),
- ),
- const Expanded(
- flex: 2,
- child: Align(
- alignment: Alignment.centerRight,
- child: Padding(
- padding: EdgeInsets.symmetric(horizontal: 16),
- child: Text('Status'),
- ),
- ),
- ),
- Expanded(flex: 3, child: Container()),
- ],
- ),
- ),
- // Scrollable content
- Expanded(
- child: Container(
- width: double.infinity,
- color: stack.textFieldDefaultBG,
- child: SingleChildScrollView(
- scrollDirection: Axis.vertical,
- child: Column(
- children: nodes.map((node) {
- final status = node.revocationReason == 0
- ? 'Active'
- : 'Revoked';
- return SizedBox(
- height: 48,
- child: Row(
- children: [
- Expanded(
- flex: 2,
- child: Align(
- alignment: Alignment.centerRight,
- child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- ),
- child: Text(
- node.serviceAddr,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ),
- ),
- Expanded(
- flex: 2,
- child: Align(
- alignment: Alignment.centerRight,
- child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- ),
- child: Text(
- node.lastPaidHeight.toString(),
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ),
- ),
- Expanded(
- flex: 2,
- child: Align(
- alignment: Alignment.centerRight,
- child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- ),
- child: Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 8,
- vertical: 4,
- ),
- decoration: BoxDecoration(
- color: status.toLowerCase() == 'active'
- ? stack.accentColorGreen
- : stack.accentColorRed,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Text(
- status.toUpperCase(),
- style: STextStyles.w600_12(
- context,
- ).copyWith(color: stack.textWhite),
- ),
- ),
- ),
- ),
- ),
- Expanded(
- flex: 3,
- child: Align(
- alignment: Alignment.centerRight,
- child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- IconButton(
- onPressed: () {
- showDialog<void>(
- context: context,
- barrierDismissible: true,
- builder: (context) => SDialog(
- child: SizedBox(
- width: 600,
- child: MasternodeInfoWidget(
- info: node,
- ),
- ),
- ),
- );
- },
- icon: const Icon(Icons.info_outline),
- tooltip: 'View Details',
- ),
- ],
- ),
- ),
- ),
- ),
- ],
- ),
- );
- }).toList(),
- ),
- ),
- ),
- ),
- ],
- ),
- );
- }
-}
diff --git a/lib/pages/masternodes/sub_widgets/register_masternode_form.dart b/lib/pages/masternodes/sub_widgets/register_masternode_form.dart
index 5d8dd94..6bd79d1 100644
--- a/lib/pages/masternodes/sub_widgets/register_masternode_form.dart
+++ b/lib/pages/masternodes/sub_widgets/register_masternode_form.dart
@@ -126,6 +126,7 @@ class _RegisterMasternodeFormState
final txId = await showLoading(
whileFutureAlt: _registerMasternode,
context: context,
+ rootNavigator: Util.isDesktop,
message: "Creating and submitting masternode registration...",
delay: const Duration(seconds: 1),
onException: (e) => ex = e,
diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart
index 732008d..8da704e 100644
--- a/lib/pages/send_view/confirm_transaction_view.dart
+++ b/lib/pages/send_view/confirm_transaction_view.dart
@@ -218,7 +218,7 @@ class _ConfirmTransactionViewState
context: context,
builder: (context) => AlertDialog(
title: const Text('Slatepack Creation Failed'),
- content: Text('Failed to create slatepack: $e'),
+ content: Text(errorMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
@@ -297,7 +297,7 @@ class _ConfirmTransactionViewState
context: context,
builder: (context) => AlertDialog(
title: const Text('Slate Creation Failed'),
- content: Text('Failed to create slate: $e'),
+ content: Text(errorMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
@@ -479,118 +479,7 @@ class _ConfirmTransactionViewState
widget.onSuccess.call();
- // Check for 1000 FIRO transparent self-send → prompt MN registration
- bool navigatedToMN = false;
- if (wallet is FiroWallet &&
- confirmedTx.recipients != null &&
- confirmedTx.sparkMints == null &&
- txids.isNotEmpty &&
- context.mounted) {
- try {
- final masternodeAmount = Amount.fromDecimal(
- kMasterNodeValue,
- fractionDigits: wallet.cryptoCurrency.fractionDigits,
- );
- final txFeeRaw = confirmedTx.fee?.raw ?? BigInt.zero;
-
- final mnRecipient = confirmedTx.recipients!
- // Exact 1000 FIRO: multiple such outputs uses the first match only.
- .where((r) => !r.isChange && r.amount == masternodeAmount)
- .firstOrNull;
-
- if (mnRecipient != null && confirmedTx.txid != null) {
- final ownAddress = await ref
- .read(mainDBProvider)
- .getAddresses(walletId)
- .filter()
- .valueEqualTo(mnRecipient.address)
- .findFirst();
-
- if (ownAddress != null && context.mounted) {
- 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) {
- // 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 &&
- confirmedTx.txid == null &&
- context.mounted) {
- unawaited(
- showFloatingFlushBar(
- type: FlushBarType.warning,
- message:
- "Could not determine transaction id for collateral "
- "auto-detection. Register from the Masternodes screen "
- "once the transaction appears.",
- context: context,
- ),
- );
- } else {
- // If fee was subtracted from the recipient, users can enter 1000 but
- // end up with ~999.99... output which is not valid MN collateral.
- final nearMnRecipient =
- confirmedTx.recipients!
- .where(
- (r) => !r.isChange && r.amount.raw < masternodeAmount.raw,
- )
- .where(
- (r) => (masternodeAmount.raw - r.amount.raw) <= txFeeRaw,
- )
- .toList()
- ..sort((a, b) => b.amount.raw.compareTo(a.amount.raw));
-
- if (nearMnRecipient.isNotEmpty) {
- final maybeOwnAddress = await ref
- .read(mainDBProvider)
- .getAddresses(walletId)
- .filter()
- .valueEqualTo(nearMnRecipient.first.address)
- .findFirst();
-
- if (maybeOwnAddress != null && context.mounted) {
- unawaited(
- showFloatingFlushBar(
- type: FlushBarType.warning,
- message:
- "Masternode collateral requires one exact 1000 FIRO "
- "transparent output. Fee appears to have been "
- "subtracted from the recipient amount. Send 1000 "
- "to yourself again with fee paid on top.",
- context: context,
- ),
- );
- }
- }
- }
- } catch (e, s) {
- Logging.instance.w(
- "Skipping masternode collateral auto-detection: $e",
- error: e,
- stackTrace: s,
- );
- }
- }
-
- if (!navigatedToMN && context.mounted) {
+ if (context.mounted) {
if (widget.onSuccessInsteadOfRouteOnSuccess == null) {
Navigator.of(
context,
@@ -1675,9 +1564,9 @@ class _ConfirmTransactionViewState
}
}
} else {
- final unlocked = await Navigator.push(
+ final unlocked = await Navigator.push<bool>(
context,
- RouteGenerator.getRoute(
+ RouteGenerator.getRoute<bool>(
shouldUseMaterialRoute:
RouteGenerator.useMaterialPageRoute,
builder: (_) => const LockscreenView(
diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart
index 7eb453d..18b8d5b 100644
--- a/lib/pages/send_view/send_view.dart
+++ b/lib/pages/send_view/send_view.dart
@@ -16,7 +16,6 @@ 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';
@@ -69,8 +68,6 @@ import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/custom_buttons/blue_text_button.dart';
import '../../widgets/dialogs/firo_exchange_address_dialog.dart';
-import '../../widgets/dialogs/s_dialog.dart';
-import '../../widgets/desktop/secondary_button.dart';
import '../../widgets/epic_txs_method_toggle.dart';
import '../../widgets/eth_fee_form.dart';
import '../../widgets/fee_slider.dart';
@@ -85,7 +82,6 @@ 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/masternode_constants.dart';
import 'confirm_transaction_view.dart';
import 'sub_widgets/building_transaction_dialog.dart';
import 'sub_widgets/dual_balance_selection_sheet.dart';
@@ -147,8 +143,6 @@ class _SendViewState extends ConsumerState<SendView> {
late final bool hasOptionalMemo;
late final bool isFiro;
late final bool isEth;
- late final bool _isMasternodeCollateralSelfSend;
- late final bool _isMasternodeCollateralUnshield;
Amount? _cachedAmountToSend;
String? _address;
@@ -296,104 +290,6 @@ 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) => SDialog(
- contentCanScroll: false,
- padding: EdgeInsets.all(Util.isDesktop ? 32 : 16),
- child: SizedBox(
- width: Util.isDesktop ? 520 : null,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Text(
- "Choose your address",
- style: Util.isDesktop
- ? STextStyles.desktopH3(ctx)
- : STextStyles.pageTitleH2(ctx),
- ),
- const SizedBox(height: 16),
- ConstrainedBox(
- constraints: BoxConstraints(
- maxHeight: MediaQuery.of(ctx).size.height * 0.5,
- ),
- child: ListView.builder(
- shrinkWrap: true,
- itemCount: addresses.length,
- itemBuilder: (_, index) => ListTile(
- contentPadding: EdgeInsets.zero,
- title: Text(
- addresses[index],
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: Util.isDesktop
- ? STextStyles.w500_16(ctx)
- : STextStyles.w500_14(ctx),
- ),
- onTap: () => Navigator.of(ctx).pop(addresses[index]),
- ),
- ),
- ),
- const SizedBox(height: 16),
- SecondaryButton(
- buttonHeight: ButtonHeight.l,
- label: "Cancel",
- onPressed: () => Navigator.of(ctx).pop(),
- ),
- ],
- ),
- ),
- ),
- );
-
- if (selectedAddress == null) {
- return;
- }
-
- _address = selectedAddress;
- sendToController.text = selectedAddress;
- _setValidAddressProviders(_address);
- setState(() {
- _addressToggleFlag = true;
- });
- }
-
Future<void> _scanQr() async {
try {
// ref
@@ -1241,7 +1137,7 @@ class _SendViewState extends ConsumerState<SendView> {
}
// pop building dialog
- Navigator.of(context).pop();
+ Navigator.of(context, rootNavigator: true).pop();
unawaited(
Navigator.of(context).push(
@@ -1268,7 +1164,7 @@ class _SendViewState extends ConsumerState<SendView> {
Logging.instance.e("$e\n$s", error: e, stackTrace: s);
if (mounted) {
// pop building dialog
- Navigator.of(context).pop();
+ Navigator.of(context, rootNavigator: true).pop();
unawaited(
showDialog<dynamic>(
@@ -1371,40 +1267,6 @@ class _SendViewState extends ConsumerState<SendView> {
late final bool hasFees;
- void _onSendToAddressPasteButtonPressed() async {
- final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain);
- if (data?.text != null && data!.text!.isNotEmpty) {
- String content = data.text!.trim();
- if (content.contains("\n")) {
- content = content.substring(0, content.indexOf("\n"));
- }
-
- if (coin is Epiccash) {
- // strip http:// and https:// if content contains @
- content = AddressUtils().formatEpicCashAddress(content);
- }
-
- final trimmed = content.trim();
- final parsed = AddressUtils.parsePaymentUri(
- trimmed,
- logging: Logging.instance,
- );
- if (parsed != null) {
- _applyUri(parsed);
- } else {
- _setOpReturnData(null);
- sendToController.text = content;
- _address = content;
-
- _setValidAddressProviders(_address);
-
- setState(() {
- _addressToggleFlag = sendToController.text.isNotEmpty;
- });
- }
- }
- }
-
void _onFeeSelectPressed() {
showModalBottomSheet<dynamic>(
backgroundColor: Colors.transparent,
@@ -1451,21 +1313,8 @@ class _SendViewState extends ConsumerState<SendView> {
_data = widget.autoFillData;
walletId = widget.walletId;
clipboard = widget.clipboard;
- _isMasternodeCollateralUnshield =
- MasternodeCollateralNotes.isUnshield(_data?.note) && isFiro;
- _isMasternodeCollateralSelfSend =
- (MasternodeCollateralNotes.isPrep(_data?.note) ||
- _isMasternodeCollateralUnshield) &&
- isFiro;
WidgetsBinding.instance.addPostFrameCallback((_) {
- if (_isMasternodeCollateralUnshield) {
- ref.read(publicPrivateBalanceStateProvider.state).state =
- BalanceType.private;
- } else if (_isMasternodeCollateralSelfSend) {
- ref.read(publicPrivateBalanceStateProvider.state).state =
- BalanceType.public;
- }
ref.refresh(feeSheetSessionCacheProvider);
ref.refresh(pIsExchangeAddress);
});
@@ -1495,21 +1344,27 @@ class _SendViewState extends ConsumerState<SendView> {
baseAmountController.addListener(_baseAmountChanged);
if (_data != null) {
- if (_data.amount != null) {
+ final hasAmount = _data.amount != null;
+ if (hasAmount) {
final amount = Amount.fromDecimal(
_data.amount!,
fractionDigits: coin.fractionDigits,
);
+ _cryptoAmountChangeLock = true;
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(amount, withUnitName: false);
+ _cryptoAmountChangeLock = false;
}
sendToController.text = _data.contactLabel;
_address = _data.address.trim();
_addressToggleFlag = true;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+ if (hasAmount) {
+ _cryptoAmountChanged();
+ }
_setValidAddressProviders(_address);
});
}
@@ -1675,13 +1530,7 @@ class _SendViewState extends ConsumerState<SendView> {
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
- onPressed: () {
- if (_isMasternodeCollateralSelfSend) {
- Navigator.of(context).pop();
- } else {
- Navigator.of(context).pop();
- }
- },
+ onPressed: () => Navigator.of(context).pop(),
),
title: Text(
"Send ${coin.ticker}",
@@ -2033,18 +1882,6 @@ 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/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart
index b6619f9..c14d35f 100644
--- a/lib/pages/wallet_view/wallet_view.dart
+++ b/lib/pages/wallet_view/wallet_view.dart
@@ -96,6 +96,7 @@ import '../coin_control/coin_control_view.dart';
import '../epic_finalize_view/epic_finalize_view.dart';
import '../exchange_view/wallet_initiated_exchange_view.dart';
import '../finalize_view/finalize_view.dart';
+import '../masternodes/masternodes_home_view.dart';
import '../monkey/monkey_view.dart';
import '../more_view/gift_cards_view.dart';
import '../more_view/services_view.dart';
@@ -1207,27 +1208,27 @@ class _WalletViewState extends ConsumerState<WalletView> {
);
},
),
- // if (!viewOnly && wallet is FiroWallet)
- // WalletNavigationBarItemData(
- // label: "Masternodes",
- // icon: SvgPicture.asset(
- // Assets.svg.recycle,
- // height: 20,
- // width: 20,
- // colorFilter: ColorFilter.mode(
- // Theme.of(
- // context,
- // ).extension<StackColors>()!.bottomNavIconIcon,
- // BlendMode.srcIn,
- // ),
- // ),
- // onTap: () {
- // Navigator.of(context).pushNamed(
- // MasternodesHomeView.routeName,
- // arguments: widget.walletId,
- // );
- // },
- // ),
+ if (!viewOnly && wallet is FiroWallet)
+ WalletNavigationBarItemData(
+ label: "Masternodes",
+ icon: SvgPicture.asset(
+ Assets.svg.recycle,
+ height: 20,
+ width: 20,
+ colorFilter: ColorFilter.mode(
+ Theme.of(
+ context,
+ ).extension<StackColors>()!.bottomNavIconIcon,
+ BlendMode.srcIn,
+ ),
+ ),
+ onTap: () {
+ Navigator.of(context).pushNamed(
+ MasternodesHomeView.routeName,
+ arguments: widget.walletId,
+ );
+ },
+ ),
if (wallet is NamecoinWallet)
WalletNavigationBarItemData(
label: "Domains",
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 9388807..5060d2b 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
@@ -1246,12 +1246,27 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
cryptoAmountController.addListener(onCryptoAmountChanged);
if (_data != null) {
- if (_data.amount != null) {
- cryptoAmountController.text = _data.amount!.toString();
+ final hasAmount = _data.amount != null;
+ if (hasAmount) {
+ _cryptoAmountChangeLock = true;
+ cryptoAmountController.text = ref
+ .read(pAmountFormatter(coin))
+ .format(
+ _data.amount!.toAmount(fractionDigits: coin.fractionDigits),
+ withUnitName: false,
+ );
+ _cryptoAmountChangeLock = false;
}
sendToController.text = _data.contactLabel;
_address = _data.address;
_addressToggleFlag = true;
+
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (hasAmount) {
+ _cryptoAmountChanged();
+ }
+ _setValidAddressProviders(_address);
+ });
}
if (isPaynymSend) {
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart
index f924ad4..4793f0a 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart
@@ -505,8 +505,8 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
if (wallet is SignVerifyInterface && !isViewOnly)
(WalletFeature.sign, Assets.svg.pencil, _onSignPressed),
- // if (!isViewOnly && wallet is FiroWallet)
- // (WalletFeature.masternodes, Assets.svg.recycle, _onMasternodesPressed),
+ if (!isViewOnly && wallet is FiroWallet)
+ (WalletFeature.masternodes, Assets.svg.recycle, _onMasternodesPressed),
if (showCoinControl)
(
WalletFeature.coinControl,
Why this scored 20/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.