various masternode related fixes and improvements
What changed, and why it matters
This commit fixes several bugs in the masternode registration flow of a cryptocurrency wallet app. It improves input validation (requiring a properly formatted IP:port, and an operator reward between 0% and 100%), fixes a unit-conversion bug when calculating the operator reward, and improves UI feedback after registration. There is no clear evidence of a security vulnerability being patched, but the changes reduce the chance of user mistakes and malformed transactions.
Treat as a routine bug-fix/improvement commit. Reviewers may want to verify the new validation rules cover all malformed inputs and that the 10000-basis conversion matches the Firo masternode registration protocol exactly. No urgent security response is indicated based on the available materials.
Security signals we found
Input validation added for IP:port format and operator reward percentage range
Operator reward unit conversion corrected to protocol-specified 10000 basis
Success/failure dialog flow refactored to prevent stale context usage
No explicit security disclosure, CVE, or researcher attribution in commit or references
Evidence from the diff
The patch modifies four files related to Firo masternode registration. Key changes: (1) CreateMasternodeView now accepts a popTxidOnSuccess flag and passes a success callback to RegisterMasternodeForm. (2) MasternodesHomeView now awaits the registration result and refreshes the masternode list, showing a confirmation dialog. (3) RegisterMasternodeForm validates that the IP/port field contains exactly two colon-separated non-empty parts, that the operator reward is a non-negative number ≤100, and fixes the operator reward conversion from percent to the 10000-basis unit used by the Firo protocol ((10000 * (operatorPercent / 100)).round().clamp(0, 10000)). It also moves success dialog handling to the parent view. (4) FiroWallet changes MasternodeInfo.operatorReward from int to double and parses it accordingly. No explicit security fix or CVE is mentioned.
Changed components
lib/pages/masternodes/create_masternode_view.dartlib/pages/masternodes/masternodes_home_view.dartlib/pages/masternodes/sub_widgets/register_masternode_form.dartlib/wallets/wallet/impl/firo_wallet.dartInspect captured patch +129 / −73
diff --git a/lib/pages/masternodes/create_masternode_view.dart b/lib/pages/masternodes/create_masternode_view.dart
index 0f54e54..9d2940e 100644
--- a/lib/pages/masternodes/create_masternode_view.dart
+++ b/lib/pages/masternodes/create_masternode_view.dart
@@ -11,11 +11,16 @@ import '../../widgets/desktop/desktop_dialog_close_button.dart';
import 'sub_widgets/register_masternode_form.dart';
class CreateMasternodeView extends ConsumerStatefulWidget {
- const CreateMasternodeView({super.key, required this.firoWalletId});
+ const CreateMasternodeView({
+ super.key,
+ required this.firoWalletId,
+ this.popTxidOnSuccess = true,
+ });
static const routeName = "/createMasternodeView";
final String firoWalletId;
+ final bool popTxidOnSuccess;
@override
ConsumerState<CreateMasternodeView> createState() =>
@@ -100,7 +105,14 @@ class _CreateMasternodeDialogState extends ConsumerState<CreateMasternodeView> {
),
),
),
- child: RegisterMasternodeForm(firoWalletId: widget.firoWalletId),
+ child: RegisterMasternodeForm(
+ firoWalletId: widget.firoWalletId,
+ onRegistrationSuccess: (txid) {
+ if (widget.popTxidOnSuccess && mounted) {
+ Navigator.of(context, rootNavigator: Util.isDesktop).pop(txid);
+ }
+ },
+ ),
),
);
}
diff --git a/lib/pages/masternodes/masternodes_home_view.dart b/lib/pages/masternodes/masternodes_home_view.dart
index 37ab2fd..6933a5c 100644
--- a/lib/pages/masternodes/masternodes_home_view.dart
+++ b/lib/pages/masternodes/masternodes_home_view.dart
@@ -5,6 +5,7 @@ import 'package:flutter_svg/svg.dart';
import '../../providers/global/wallets_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
+import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/wallet/impl/firo_wallet.dart';
@@ -14,6 +15,7 @@ import '../../widgets/desktop/desktop_scaffold.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/dialogs/s_dialog.dart';
import '../../widgets/loading_indicator.dart';
+import '../../widgets/stack_dialog.dart';
import 'create_masternode_view.dart';
import 'sub_widgets/masternodes_list.dart';
import 'sub_widgets/masternodes_table_desktop.dart';
@@ -33,13 +35,40 @@ class MasternodesHomeView extends ConsumerStatefulWidget {
class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
late Future<List<MasternodeInfo>> _masternodesFuture;
- void _showDesktopCreateMasternodeDialog() {
- showDialog<void>(
+ Future<void> _showDesktopCreateMasternodeDialog() async {
+ final txid = await showDialog<Object>(
context: context,
barrierDismissible: true,
builder: (context) =>
SDialog(child: CreateMasternodeView(firoWalletId: widget.walletId)),
);
+ _handleSuccessTxid(txid);
+ }
+
+ 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();
+ });
+
+ 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,
+ ),
+ );
+ }
}
@override
@@ -155,11 +184,12 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
width: 20,
height: 20,
),
- onPressed: () {
- Navigator.of(context).pushNamed(
+ onPressed: () async {
+ final txid = await Navigator.of(context).pushNamed(
CreateMasternodeView.routeName,
arguments: widget.walletId,
);
+ _handleSuccessTxid(txid);
},
),
),
@@ -199,14 +229,15 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
label: "Create Your First Masternode",
horizontalContentPadding: 16,
buttonHeight: Util.isDesktop ? .l : null,
- onPressed: () {
+ onPressed: () async {
if (Util.isDesktop) {
- _showDesktopCreateMasternodeDialog();
+ await _showDesktopCreateMasternodeDialog();
} else {
- Navigator.of(context).pushNamed(
+ final txid = await Navigator.of(context).pushNamed(
CreateMasternodeView.routeName,
arguments: widget.walletId,
);
+ _handleSuccessTxid(txid);
}
},
),
diff --git a/lib/pages/masternodes/sub_widgets/register_masternode_form.dart b/lib/pages/masternodes/sub_widgets/register_masternode_form.dart
index 22f9638..84977d3 100644
--- a/lib/pages/masternodes/sub_widgets/register_masternode_form.dart
+++ b/lib/pages/masternodes/sub_widgets/register_masternode_form.dart
@@ -11,6 +11,7 @@ import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../wallets/wallet/impl/firo_wallet.dart';
+import '../../../widgets/conditional_parent.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/desktop/secondary_button.dart';
import '../../../widgets/rounded_container.dart';
@@ -18,10 +19,16 @@ import '../../../widgets/stack_dialog.dart';
import '../../../widgets/textfields/adaptive_text_field.dart';
class RegisterMasternodeForm extends ConsumerStatefulWidget {
- const RegisterMasternodeForm({super.key, required this.firoWalletId});
+ const RegisterMasternodeForm({
+ super.key,
+ required this.firoWalletId,
+ required this.onRegistrationSuccess,
+ });
final String firoWalletId;
+ final void Function(String) onRegistrationSuccess;
+
@override
ConsumerState<RegisterMasternodeForm> createState() =>
_RegisterMasternodeFormState();
@@ -53,11 +60,18 @@ class _RegisterMasternodeFormState
void _validate() {
if (mounted) {
+ final percent = double.tryParse(_operatorRewardController.text);
setState(() {
_enableCreateButton = [
- _ipAndPortController.text.trim().isNotEmpty,
+ _ipAndPortController.text
+ .trim()
+ .split(":")
+ .where((e) => e.isNotEmpty)
+ .length ==
+ 2,
_operatorPubKeyController.text.trim().isNotEmpty,
- _operatorRewardController.text.trim().isNotEmpty,
+ percent != null && !percent.isNegative,
+ percent != null && percent <= 100.0,
_payoutAddressController.text.trim().isNotEmpty,
].every((e) => e);
});
@@ -70,11 +84,16 @@ class _RegisterMasternodeFormState
final port = int.parse(parts[1]);
final operatorPubKey = _operatorPubKeyController.text.trim();
final votingAddress = _votingAddressController.text.trim();
- final operatorReward = _operatorRewardController.text.trim().isNotEmpty
- ? (double.parse(_operatorRewardController.text.trim()) * 100).floor()
- : 0;
final payoutAddress = _payoutAddressController.text.trim();
+ // according to https://github.com/cypherstack/stack_wallet/blob/c898a70f808ed5490b8dd23571f5f162d9e38158/lib/wallets/wallet/impl/firo_wallet.dart#L1064
+ // this should be a percent of 10000
+ final operatorPercent = double.parse(_operatorRewardController.text);
+ final operatorReward = (10000 * (operatorPercent / 100)).round().clamp(
+ 0,
+ 10000,
+ );
+
final wallet =
ref.read(pWallets).getWallet(widget.firoWalletId) as FiroWallet;
@@ -105,7 +124,7 @@ class _RegisterMasternodeFormState
Exception? ex;
final txId = await showLoading(
- whileFuture: _registerMasternode(),
+ whileFutureAlt: _registerMasternode,
context: context,
message: "Creating and submitting masternode registration...",
delay: const Duration(seconds: 1),
@@ -113,33 +132,25 @@ class _RegisterMasternodeFormState
);
if (mounted) {
- final String title;
- String message;
if (ex != null || txId == null) {
- message = ex?.toString().trim() ?? "Unknown error: txId=$txId";
+ String message = ex?.toString().trim() ?? "Unknown error: txId=$txId";
const exceptionPrefix = "Exception:";
while (message.startsWith(exceptionPrefix) &&
message.length > exceptionPrefix.length) {
message = message.substring(exceptionPrefix.length).trim();
}
- title = "Registration failed";
+ await showDialog<void>(
+ context: context,
+ builder: (_) => StackOkDialog(
+ title: "Registration failed",
+ message: message,
+ desktopPopRootNavigator: Util.isDesktop,
+ maxWidth: Util.isDesktop ? 400 : null,
+ ),
+ );
} else {
- title = "Masternode Registration Submitted";
- message =
- "Masternode registration submitted, your masternode will "
- "appear in the list after the tx is confirmed.\n\nTransaction"
- " ID: $txId";
+ widget.onRegistrationSuccess.call(txId);
}
-
- await showDialog<void>(
- context: context,
- builder: (_) => StackOkDialog(
- title: title,
- message: message,
- desktopPopRootNavigator: Util.isDesktop,
- maxWidth: Util.isDesktop ? 400 : null,
- ),
- );
}
}).execute;
}
@@ -180,26 +191,23 @@ class _RegisterMasternodeFormState
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Flexible(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Flexible(
- child: RoundedContainer(
- color: infoColorBG,
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(
- infoMessage,
- style: STextStyles.w600_14(
- context,
- ).copyWith(color: infoColor),
- ),
+ Row(
+ children: [
+ Expanded(
+ child: RoundedContainer(
+ color: infoColorBG,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(
+ infoMessage,
+ style: STextStyles.w600_14(
+ context,
+ ).copyWith(color: infoColor),
),
),
),
- ],
- ),
+ ),
+ ],
),
SizedBox(height: Util.isDesktop ? 24 : 16),
@@ -254,27 +262,32 @@ class _RegisterMasternodeFormState
onChangedComprehensive: (_) => _validate(),
),
- Util.isDesktop ? const SizedBox(height: 32) : const Spacer(),
+ Util.isDesktop
+ ? const SizedBox(height: 32)
+ : const SizedBox(height: 16),
+ if (!Util.isDesktop) const Spacer(),
- Row(
- children: [
- Expanded(
- child: SecondaryButton(
- label: "Cancel",
- onPressed: Navigator.of(context).pop,
- buttonHeight: Util.isDesktop ? .l : null,
- ),
- ),
- SizedBox(width: Util.isDesktop ? 24 : 16),
- Expanded(
- child: PrimaryButton(
- label: "Create",
- enabled: _enableCreateButton,
- onPressed: _enableCreateButton ? _register : null,
- buttonHeight: Util.isDesktop ? .l : null,
+ ConditionalParent(
+ condition: Util.isDesktop,
+ builder: (child) => Row(
+ children: [
+ Expanded(
+ child: SecondaryButton(
+ label: "Cancel",
+ onPressed: Navigator.of(context).pop,
+ buttonHeight: .l,
+ ),
),
- ),
- ],
+ const SizedBox(width: 24),
+ Expanded(child: child),
+ ],
+ ),
+ child: PrimaryButton(
+ label: "Create",
+ enabled: _enableCreateButton,
+ onPressed: _enableCreateButton ? _register : null,
+ buttonHeight: Util.isDesktop ? .l : null,
+ ),
),
],
);
diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart
index 64e7966..bbb9f10 100644
--- a/lib/wallets/wallet/impl/firo_wallet.dart
+++ b/lib/wallets/wallet/impl/firo_wallet.dart
@@ -34,7 +34,7 @@ class MasternodeInfo {
final String collateralHash;
final int collateralIndex;
final String collateralAddress;
- final int operatorReward;
+ final double operatorReward;
final String serviceAddr;
final int servicePort;
final int registeredHeight;
@@ -1181,7 +1181,7 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
collateralHash: info["collateralHash"] as String,
collateralIndex: info["collateralIndex"] as int,
collateralAddress: info["collateralAddress"] as String,
- operatorReward: info["operatorReward"] as int,
+ operatorReward: double.parse(info["operatorReward"].toString()),
serviceAddr: (info["state"]["service"] as String).substring(
0,
(info["state"]["service"] as String).lastIndexOf(":"),
Why this scored 31/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.