What changed, and why it matters
This commit is a routine user-interface update for the mobile masternode list in Stack Wallet. It adds a new details screen, replaces an inline list with a reusable card widget, swaps a generic loading spinner for the app's own loading indicator, and registers the new screen in the navigation router. There is no indication of any security fix or vulnerability being addressed.
No security action required; treat as normal feature/UI work.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff is a UI refactor for the masternodes feature. It introduces MasternodeDetailsView (a new Flutter page), updates MasternodesHomeView to use LoadingIndicator and removes a private _wallet getter in favor of an inline cast, rewrites MasternodesList to use _MasternodeCard with RoundedWhiteContainer and navigation to the details view, and adds the route to RouteGenerator with a MasternodeInfo argument check. No cryptographic, network, or permission changes are present.
Changed components
lib/pages/masternodes/masternode_details_view.dartlib/pages/masternodes/masternodes_home_view.dartlib/pages/masternodes/sub_widgets/masternodes_list.dartlib/route_generator.dartInspect captured patch +121 / −103
diff --git a/lib/pages/masternodes/masternode_details_view.dart b/lib/pages/masternodes/masternode_details_view.dart
new file mode 100644
index 0000000..ebc3ea2
--- /dev/null
+++ b/lib/pages/masternodes/masternode_details_view.dart
@@ -0,0 +1,57 @@
+import 'package:flutter/material.dart';
+
+import '../../themes/stack_colors.dart';
+import '../../utilities/text_styles.dart';
+import '../../wallets/wallet/impl/firo_wallet.dart';
+import '../../widgets/background.dart';
+import '../../widgets/custom_buttons/app_bar_icon_button.dart';
+import 'sub_widgets/masternode_info_widget.dart';
+
+class MasternodeDetailsView extends StatelessWidget {
+ const MasternodeDetailsView({super.key, required this.node});
+
+ static const String routeName = "/masternodeDetailsView";
+
+ final MasternodeInfo node;
+
+ @override
+ Widget build(BuildContext context) {
+ return Background(
+ child: Scaffold(
+ backgroundColor: Theme.of(context).extension<StackColors>()!.background,
+ appBar: AppBar(
+ leading: const AppBarBackButton(),
+ title: Text(
+ "Masternode details",
+ style: STextStyles.navBarTitle(context),
+ ),
+ ),
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight,
+ ),
+ child: IntrinsicHeight(
+ child: Column(
+ mainAxisSize: .min,
+ children: [
+ MasternodeInfoWidget(info: node),
+ const SizedBox(height: 16),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/masternodes/masternodes_home_view.dart b/lib/pages/masternodes/masternodes_home_view.dart
index c5b631f..37ab2fd 100644
--- a/lib/pages/masternodes/masternodes_home_view.dart
+++ b/lib/pages/masternodes/masternodes_home_view.dart
@@ -13,6 +13,7 @@ import '../../widgets/desktop/desktop_app_bar.dart';
import '../../widgets/desktop/desktop_scaffold.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/dialogs/s_dialog.dart';
+import '../../widgets/loading_indicator.dart';
import 'create_masternode_view.dart';
import 'sub_widgets/masternodes_list.dart';
import 'sub_widgets/masternodes_table_desktop.dart';
@@ -32,9 +33,6 @@ class MasternodesHomeView extends ConsumerStatefulWidget {
class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
late Future<List<MasternodeInfo>> _masternodesFuture;
- FiroWallet get _wallet =>
- ref.read(pWallets).getWallet(widget.walletId) as FiroWallet;
-
void _showDesktopCreateMasternodeDialog() {
showDialog<void>(
context: context,
@@ -47,7 +45,11 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
@override
void initState() {
super.initState();
- _masternodesFuture = _wallet.getMyMasternodes();
+
+ // TODO polling and update on successful registration
+ _masternodesFuture =
+ (ref.read(pWallets).getWallet(widget.walletId) as FiroWallet)
+ .getMyMasternodes();
}
@override
@@ -168,7 +170,7 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
future: _masternodesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
- return const Center(child: CircularProgressIndicator());
+ return const Center(child: LoadingIndicator(height: 50, width: 50));
}
if (snapshot.hasError) {
return Center(
diff --git a/lib/pages/masternodes/sub_widgets/masternodes_list.dart b/lib/pages/masternodes/sub_widgets/masternodes_list.dart
index 7f30c23..8df056f 100644
--- a/lib/pages/masternodes/sub_widgets/masternodes_list.dart
+++ b/lib/pages/masternodes/sub_widgets/masternodes_list.dart
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/text_styles.dart';
import '../../../wallets/wallet/impl/firo_wallet.dart';
+import '../../../widgets/rounded_white_container.dart';
+import '../masternode_details_view.dart';
class MasternodesList extends StatelessWidget {
const MasternodesList({super.key, required this.nodes});
@@ -11,113 +13,58 @@ class MasternodesList extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final stack = Theme.of(context).extension<StackColors>()!;
- return Container(
- color: stack.textFieldDefaultBG,
- child: ListView.separated(
- padding: EdgeInsets.zero,
- itemCount: nodes.length,
- separatorBuilder: (_, __) => const SizedBox(height: 1),
- itemBuilder: (context, index) {
- final node = nodes[index];
- final status = node.revocationReason == 0 ? 'Active' : 'Revoked';
-
- return Container(
- width: double.infinity,
- color: stack.textFieldDefaultBG,
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Expanded(
- child: Align(
- alignment: Alignment.centerLeft,
- child: Text(
- 'IP: ${node.serviceAddr}',
- style: STextStyles.w600_14(context),
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ),
- 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),
- ),
- ),
- ],
- ),
- const SizedBox(height: 8),
- _buildMobileRow(
- 'Last Paid Height',
- node.lastPaidHeight.toString(),
- ),
- const SizedBox(height: 12),
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- OutlinedButton.icon(
- onPressed: () => _showMasternodeInfoDialog(node),
- icon: const Icon(Icons.info_outline),
- label: const Text('Details'),
- style: OutlinedButton.styleFrom(
- backgroundColor: stack.textFieldDefaultBG,
- foregroundColor: stack.buttonTextSecondary,
- side: BorderSide(
- color: stack.buttonBackBorderSecondary,
- ),
- ),
- ),
- ],
- ),
- ],
- ),
- );
- },
+ return ListView.separated(
+ padding: EdgeInsets.zero,
+ itemCount: nodes.length,
+ separatorBuilder: (_, __) => const SizedBox(height: 8),
+ itemBuilder: (context, index) => Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: _MasternodeCard(node: nodes[index]),
),
);
}
+}
- Widget _buildMobileRow(String label, String value) {
- return Padding(
- padding: const EdgeInsets.only(bottom: 4),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
+// TODO better styling
+class _MasternodeCard extends StatelessWidget {
+ const _MasternodeCard({super.key, required this.node});
+
+ final MasternodeInfo node;
+
+ @override
+ Widget build(BuildContext context) {
+ final stack = Theme.of(context).extension<StackColors>()!;
+ return RoundedWhiteContainer(
+ onPressed: () => Navigator.of(
+ context,
+ ).pushNamed(MasternodeDetailsView.routeName, arguments: node),
+ child: Column(
+ mainAxisSize: .min,
children: [
- SizedBox(
- width: 120,
- child: Align(
- alignment: Alignment.centerLeft,
- child: Text(
- '$label:',
- style: STextStyles.w500_12(context).copyWith(
- color: Theme.of(
+ 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),
+ ),
+ child: Text(
+ node.revocationReason == 0 ? "ACTIVE" : "REVOKED",
+ style: STextStyles.w600_12(
context,
- ).extension<StackColors>()!.textSubtitle1,
+ ).copyWith(color: stack.textWhite),
),
),
- ),
+ ],
),
- Expanded(
- child: Align(
- alignment: Alignment.centerLeft,
- child: Text(value, style: STextStyles.w500_12(context)),
- ),
+ Row(
+ mainAxisAlignment: .spaceBetween,
+ children: [Text("Last Paid Height: ${node.lastPaidHeight}")],
),
],
),
diff --git a/lib/route_generator.dart b/lib/route_generator.dart
index 483d829..9cc7a52 100644
--- a/lib/route_generator.dart
+++ b/lib/route_generator.dart
@@ -78,6 +78,7 @@ import 'pages/home_view/home_view.dart';
import 'pages/intro_view.dart';
import 'pages/manage_favorites_view/manage_favorites_view.dart';
import 'pages/masternodes/create_masternode_view.dart';
+import 'pages/masternodes/masternode_details_view.dart';
import 'pages/masternodes/masternodes_home_view.dart';
import 'pages/monkey/monkey_view.dart';
import 'pages/namecoin_names/buy_domain_view.dart';
@@ -233,6 +234,7 @@ import 'utilities/enums/add_wallet_type_enum.dart';
import 'wallets/crypto_currency/crypto_currency.dart';
import 'wallets/crypto_currency/intermediate/frost_currency.dart';
import 'wallets/models/tx_data.dart';
+import 'wallets/wallet/impl/firo_wallet.dart';
import 'wallets/wallet/wallet.dart';
import 'wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import 'widgets/choose_coin_view.dart';
@@ -919,6 +921,16 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
+ case MasternodeDetailsView.routeName:
+ if (args is MasternodeInfo) {
+ return getRoute(
+ shouldUseMaterialRoute: useMaterialPageRoute,
+ builder: (_) => MasternodeDetailsView(node: args),
+ settings: RouteSettings(name: settings.name),
+ );
+ }
+ return _routeError("${settings.name} invalid args: ${args.toString()}");
+
case BuySparkNameView.routeName:
if (args is ({String walletId, String name})) {
return getRoute(
Why this scored 15/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.