Fix Firo masternode registration to use external collateral instead of redundantly re-sending 1000 FIRO
What changed, and why it matters
This commit fixes a bug in Stack Wallet's Firo masternode registration feature. Previously, the wallet would redundantly send another 1000 FIRO inside the registration transaction itself, using that new output as collateral. The updated code instead uses an existing 1000 FIRO unspent output (UTXO) in the wallet as collateral, which is the correct behavior for Firo masternodes. The change also adds checks that the collateral exists, is confirmed, is exactly 1000 FIRO, and is signed with the correct private key. This prevents users from accidentally spending an extra 1000 FIRO and makes the registration compatible with standard Firo masternode requirements.
Review and merge after confirming the ProRegTx serialization, signature format, and collateral outpoint byte order match Firo consensus. Test on Firo testnet or regtest with both P2PKH and P2SH payout addresses, and verify the wallet no longer creates a second 1000 FIRO output. Consider adding unit tests for the payload hash and signature construction.
Security signals we found
Fixes incorrect transaction construction that caused an extra 1000 FIRO to be sent
Adds validation that collateral UTXO is confirmed, unblocked, unused, and exactly 1000 FIRO
Adds signature validation using collateral private key for external collateral
Restricts voting and payout addresses to transparent P2PKH/P2SH, rejecting Spark addresses
Prevents voting address from matching collateral or payout address
Adds user-facing warnings when fee is subtracted from collateral amount
Changes masternode discovery to use actual ProRegTx transactions rather than inferring from UTXOs
Evidence from the diff
The patch rewrites Firo masternode registration to use an external collateral outpoint rather than embedding a new 1000 FIRO output in the provider registration transaction. Key changes: (1) CreateMasternodeView and RegisterMasternodeForm now require collateralTxid, collateralVout, and collateralAddress. (2) MasternodesHomeView searches the wallet for a confirmed, unblocked, exact 1000 FIRO transparent UTXO and prompts the user to register it. (3) ConfirmTransactionView detects a self-send of exactly 1000 FIRO and auto-navigates to masternode registration, resolving the collateral vout from the broadcast transaction. (4) FiroWallet.registerMasternode now validates the collateral UTXO, derives the collateral private key via its derivation path, builds a proper Firo ProRegTx payload referencing the external collateral, computes the payload hash, creates the Firo ‘MakeSignString’, signs it with the collateral key, and appends the 65-byte compact signature. It also selects fee inputs excluding the collateral UTXO and supports P2SH payout addresses. (5) getMyMasternodeProTxHashes now looks up actual ProRegTx transactions in the local DB instead of assuming collateral txid equals protx txid.
Changed components
lib/pages/masternodes/create_masternode_view.dartlib/pages/masternodes/masternodes_home_view.dartlib/pages/masternodes/sub_widgets/register_masternode_form.dartlib/pages/send_view/confirm_transaction_view.dartlib/route_generator.dartlib/wallets/wallet/impl/firo_wallet.dartInspect captured patch +686 / −571
diff --git a/lib/pages/masternodes/create_masternode_view.dart b/lib/pages/masternodes/create_masternode_view.dart
index 9d2940e..d3f8cba 100644
--- a/lib/pages/masternodes/create_masternode_view.dart
+++ b/lib/pages/masternodes/create_masternode_view.dart
@@ -14,12 +14,18 @@ class CreateMasternodeView extends ConsumerStatefulWidget {
const CreateMasternodeView({
super.key,
required this.firoWalletId,
+ required this.collateralTxid,
+ required this.collateralVout,
+ required this.collateralAddress,
this.popTxidOnSuccess = true,
});
static const routeName = "/createMasternodeView";
final String firoWalletId;
+ final String collateralTxid;
+ final int collateralVout;
+ final String collateralAddress;
final bool popTxidOnSuccess;
@override
@@ -32,32 +38,40 @@ class _CreateMasternodeDialogState extends ConsumerState<CreateMasternodeView> {
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
- builder: (child) => SizedBox(
- width: 660,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- mainAxisAlignment: .spaceBetween,
- children: [
- Padding(
- padding: const EdgeInsets.only(left: 32),
- child: Text(
- "Create masternode",
- style: STextStyles.desktopH3(context),
+ builder: (child) => Material(
+ color: Theme.of(context).extension<StackColors>()!.popupBG,
+ borderRadius: BorderRadius.circular(20),
+ child: SizedBox(
+ width: 660,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "Create masternode",
+ style: STextStyles.desktopH3(context),
+ ),
+ ),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Flexible(
+ child: Padding(
+ padding: const EdgeInsets.only(
+ left: 32,
+ bottom: 32,
+ right: 32,
),
+ child: child,
),
- const DesktopDialogCloseButton(),
- ],
- ),
- Flexible(
- child: Padding(
- padding: const EdgeInsets.only(left: 32, bottom: 32, right: 32),
- child: child,
),
- ),
- ],
+ ],
+ ),
),
),
child: ConditionalParent(
@@ -107,6 +121,9 @@ class _CreateMasternodeDialogState extends ConsumerState<CreateMasternodeView> {
),
child: RegisterMasternodeForm(
firoWalletId: widget.firoWalletId,
+ collateralTxid: widget.collateralTxid,
+ collateralVout: widget.collateralVout,
+ collateralAddress: widget.collateralAddress,
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 6933a5c..a9b4271 100644
--- a/lib/pages/masternodes/masternodes_home_view.dart
+++ b/lib/pages/masternodes/masternodes_home_view.dart
@@ -1,9 +1,12 @@
+import 'dart:async';
+
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 '../../providers/global/wallets_provider.dart';
import '../../themes/stack_colors.dart';
+import '../../utilities/amount/amount.dart';
import '../../utilities/assets.dart';
import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
@@ -32,17 +35,175 @@ class MasternodesHomeView extends ConsumerStatefulWidget {
_MasternodesHomeViewState();
}
-class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
+class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView>
+ with WidgetsBindingObserver {
late Future<List<MasternodeInfo>> _masternodesFuture;
+ bool _hasPromptedForCollateral = false;
+ bool _isCheckingForCollateral = false;
- Future<void> _showDesktopCreateMasternodeDialog() async {
- final txid = await showDialog<Object>(
- context: context,
- barrierDismissible: true,
- builder: (context) =>
- SDialog(child: CreateMasternodeView(firoWalletId: widget.walletId)),
- );
- _handleSuccessTxid(txid);
+ Future<({String txid, int vout, String address})?> _findCollateralUtxo()
+ async {
+ final wallet =
+ ref.read(pWallets).getWallet(widget.walletId) as FiroWallet;
+ final utxos = await wallet.mainDB.getUTXOs(widget.walletId).findAll();
+ final currentChainHeight = await wallet.chainHeight;
+ final masternodeRaw = Amount.fromDecimal(
+ kMasterNodeValue,
+ fractionDigits: wallet.cryptoCurrency.fractionDigits,
+ ).raw.toInt();
+
+ for (final utxo in utxos) {
+ if (utxo.value == masternodeRaw &&
+ !utxo.isBlocked &&
+ utxo.used != true &&
+ utxo.isConfirmed(
+ currentChainHeight,
+ wallet.cryptoCurrency.minConfirms,
+ wallet.cryptoCurrency.minCoinbaseConfirms,
+ ) &&
+ utxo.address != null) {
+ return (txid: utxo.txid, vout: utxo.vout, address: utxo.address!);
+ }
+ }
+ return null;
+ }
+
+ Future<void> _createMasternode() async {
+ 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,
+ ),
+ );
+ 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);
+ }
+ }
+
+ Future<void> _maybePromptForExistingCollateral() async {
+ if (_hasPromptedForCollateral || _isCheckingForCollateral || !mounted) {
+ return;
+ }
+ _isCheckingForCollateral = true;
+
+ try {
+ final collateral = await _findCollateralUtxo();
+ if (collateral == null || !mounted) {
+ return;
+ }
+ _hasPromptedForCollateral = true;
+
+ final wantsMN = await showDialog<bool>(
+ context: context,
+ barrierDismissible: true,
+ builder: (ctx) => StackDialog(
+ title: "Register Masternode?",
+ message:
+ "A 1000 FIRO collateral UTXO was found in your wallet. "
+ "Would you like to register a masternode now?",
+ leftButton: TextButton(
+ style: Theme.of(ctx)
+ .extension<StackColors>()!
+ .getSecondaryEnabledButtonStyle(ctx),
+ child: Text(
+ "Later",
+ style: STextStyles.button(
+ ctx,
+ ).copyWith(
+ color: Theme.of(ctx).extension<StackColors>()!.accentColorDark,
+ ),
+ ),
+ onPressed: () => Navigator.of(ctx).pop(false),
+ ),
+ rightButton: TextButton(
+ style: Theme.of(ctx)
+ .extension<StackColors>()!
+ .getPrimaryEnabledButtonStyle(ctx),
+ child: Text(
+ "Register",
+ style: STextStyles.button(ctx).copyWith(
+ color:
+ Theme.of(ctx).extension<StackColors>()!.buttonTextPrimary,
+ ),
+ ),
+ onPressed: () => Navigator.of(ctx).pop(true),
+ ),
+ ),
+ );
+
+ if (wantsMN != true || !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);
+ }
+ } finally {
+ _isCheckingForCollateral = false;
+ }
}
void _handleSuccessTxid(Object? txid) {
@@ -74,11 +235,29 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
@override
void initState() {
super.initState();
+ WidgetsBinding.instance.addObserver(this);
// TODO polling and update on successful registration
_masternodesFuture =
(ref.read(pWallets).getWallet(widget.walletId) as FiroWallet)
.getMyMasternodes();
+
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ unawaited(_maybePromptForExistingCollateral());
+ });
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ super.dispose();
+ }
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if (state == AppLifecycleState.resumed) {
+ unawaited(_maybePromptForExistingCollateral());
+ }
}
@override
@@ -143,7 +322,7 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
.srcIn,
),
),
- onPressed: _showDesktopCreateMasternodeDialog,
+ onPressed: _createMasternode,
),
),
)
@@ -184,13 +363,7 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
width: 20,
height: 20,
),
- onPressed: () async {
- final txid = await Navigator.of(context).pushNamed(
- CreateMasternodeView.routeName,
- arguments: widget.walletId,
- );
- _handleSuccessTxid(txid);
- },
+ onPressed: _createMasternode,
),
),
),
@@ -229,17 +402,7 @@ class _MasternodesHomeViewState extends ConsumerState<MasternodesHomeView> {
label: "Create Your First Masternode",
horizontalContentPadding: 16,
buttonHeight: Util.isDesktop ? .l : null,
- onPressed: () async {
- if (Util.isDesktop) {
- await _showDesktopCreateMasternodeDialog();
- } else {
- final txid = await Navigator.of(context).pushNamed(
- CreateMasternodeView.routeName,
- arguments: widget.walletId,
- );
- _handleSuccessTxid(txid);
- }
- },
+ onPressed: _createMasternode,
),
],
),
diff --git a/lib/pages/masternodes/sub_widgets/register_masternode_form.dart b/lib/pages/masternodes/sub_widgets/register_masternode_form.dart
index 84977d3..05c4d28 100644
--- a/lib/pages/masternodes/sub_widgets/register_masternode_form.dart
+++ b/lib/pages/masternodes/sub_widgets/register_masternode_form.dart
@@ -3,13 +3,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../providers/global/wallets_provider.dart';
import '../../../themes/stack_colors.dart';
-import '../../../utilities/amount/amount.dart';
import '../../../utilities/if_not_already.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/show_loading.dart';
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';
@@ -22,10 +20,16 @@ class RegisterMasternodeForm extends ConsumerStatefulWidget {
const RegisterMasternodeForm({
super.key,
required this.firoWalletId,
+ required this.collateralTxid,
+ required this.collateralVout,
+ required this.collateralAddress,
required this.onRegistrationSuccess,
});
final String firoWalletId;
+ final String collateralTxid;
+ final int collateralVout;
+ final String collateralAddress;
final void Function(String) onRegistrationSuccess;
@@ -36,8 +40,6 @@ class RegisterMasternodeForm extends ConsumerStatefulWidget {
class _RegisterMasternodeFormState
extends ConsumerState<RegisterMasternodeForm> {
- late final Amount _masternodeThreshold;
-
final _ipAndPortController = TextEditingController();
final _operatorPubKeyController = TextEditingController();
final _votingAddressController = TextEditingController();
@@ -104,6 +106,9 @@ class _RegisterMasternodeFormState
votingAddress,
operatorReward,
payoutAddress,
+ collateralTxid: widget.collateralTxid,
+ collateralVout: widget.collateralVout,
+ collateralAddress: widget.collateralAddress,
);
Logging.instance.i('Masternode registration submitted: $txId');
@@ -114,11 +119,6 @@ class _RegisterMasternodeFormState
@override
void initState() {
super.initState();
- final coin = ref.read(pWalletCoin(widget.firoWalletId));
- _masternodeThreshold = Amount.fromDecimal(
- kMasterNodeValue,
- fractionDigits: coin.fractionDigits,
- );
_register = IfNotAlreadyAsync<void>(() async {
Exception? ex;
@@ -168,24 +168,6 @@ class _RegisterMasternodeFormState
@override
Widget build(BuildContext context) {
final stack = Theme.of(context).extension<StackColors>()!;
- final spendableFiro = ref.watch(
- pWalletBalance(widget.firoWalletId).select((s) => s.spendable),
- );
- final canRegister = spendableFiro >= _masternodeThreshold;
- final availableCount = (spendableFiro.raw ~/ _masternodeThreshold.raw)
- .toInt();
-
- final infoColor = canRegister
- ? stack.snackBarTextSuccess
- : stack.snackBarTextError;
- final infoColorBG = canRegister
- ? stack.snackBarBackSuccess
- : stack.snackBarBackError;
-
- final infoMessage = canRegister
- ? "You can register $availableCount masternode(s)."
- : "Insufficient funds to register a masternode. "
- "You need at least 1000 public FIRO.";
return Column(
mainAxisSize: MainAxisSize.min,
@@ -195,14 +177,16 @@ class _RegisterMasternodeFormState
children: [
Expanded(
child: RoundedContainer(
- color: infoColorBG,
+ color: stack.snackBarBackSuccess,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
- infoMessage,
+ "Collateral: ${widget.collateralTxid.length >= 8 ? '${widget.collateralTxid.substring(0, 8)}...' : widget.collateralTxid}"
+ ":${widget.collateralVout} "
+ "(${widget.collateralAddress.length >= 10 ? '${widget.collateralAddress.substring(0, 10)}...' : widget.collateralAddress})",
style: STextStyles.w600_14(
context,
- ).copyWith(color: infoColor),
+ ).copyWith(color: stack.snackBarTextSuccess),
),
),
),
diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart
index dfd6c98..c79f1ae 100644
--- a/lib/pages/send_view/confirm_transaction_view.dart
+++ b/lib/pages/send_view/confirm_transaction_view.dart
@@ -16,8 +16,8 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
-
-import '../../models/isar/models/transaction_note.dart';
+import 'package:isar_community/isar.dart';
+import '../../models/isar/models/isar_models.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/sub_widgets/desktop_auth_send.dart';
@@ -45,6 +45,7 @@ import '../../wallets/wallet/impl/firo_wallet.dart';
import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart';
import '../../wallets/wallet/impl/solana_wallet.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
+import '../masternodes/create_masternode_view.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
@@ -268,6 +269,79 @@ 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;
+ }
+
Future<void> _attemptSend(BuildContext context) async {
final wallet = ref.read(pWallets).getWallet(walletId);
final coin = wallet.info.coin;
@@ -385,15 +459,15 @@ class _ConfirmTransactionViewState
}
final results = await Future.wait([txDataFuture, time]);
+ final confirmedTx = results.first as TxData;
sendProgressController.triggerSuccess?.call();
await Future<void>.delayed(const Duration(seconds: 5));
- if (wallet is FiroWallet &&
- (results.first as TxData).sparkMints != null) {
- txids.addAll((results.first as TxData).sparkMints!.map((e) => e.txid!));
+ if (wallet is FiroWallet && confirmedTx.sparkMints != null) {
+ txids.addAll(confirmedTx.sparkMints!.map((e) => e.txid!));
} else {
- txids.add((results.first as TxData).txid!);
+ txids.add(confirmedTx.txid!);
}
if (coin is! Ethereum) {
ref.refresh(desktopUseUTXOs);
@@ -415,13 +489,147 @@ class _ConfirmTransactionViewState
unawaited(ref.read(pCurrentTokenWallet)!.refresh());
}
} else {
- unawaited(wallet.refresh());
+ if (wallet is FiroWallet) {
+ try {
+ await wallet.refresh();
+ } catch (e, s) {
+ Logging.instance.w(
+ "Post-send wallet refresh failed: $e",
+ error: e,
+ stackTrace: s,
+ );
+ }
+ } else {
+ unawaited(wallet.refresh());
+ }
}
widget.onSuccess.call();
- // pop back to wallet
- if (context.mounted) {
+ // 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!
+ .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) {
+ final collateralVout = await _resolveFiroCollateralVout(
+ wallet: wallet,
+ txid: confirmedTx.txid!,
+ recipientAddress: mnRecipient.address,
+ amount: masternodeAmount,
+ );
+ 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 navigator = Navigator.of(context);
+ navigator.popUntil(
+ ModalRoute.withName(routeOnSuccessName),
+ );
+ unawaited(
+ navigator.pushNamed(
+ CreateMasternodeView.routeName,
+ arguments: {
+ 'walletId': walletId,
+ 'collateralTxid': confirmedTx.txid!,
+ 'collateralVout': collateralVout,
+ 'collateralAddress': mnRecipient.address,
+ },
+ ),
+ );
+ }
+ }
+ } 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 (widget.onSuccessInsteadOfRouteOnSuccess == null) {
Navigator.of(
context,
diff --git a/lib/route_generator.dart b/lib/route_generator.dart
index cad05cb..dc774fa 100644
--- a/lib/route_generator.dart
+++ b/lib/route_generator.dart
@@ -912,10 +912,15 @@ class RouteGenerator {
return _routeError("${settings.name} invalid args: ${args.toString()}");
case CreateMasternodeView.routeName:
- if (args is String) {
+ if (args is Map<String, dynamic>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
- builder: (_) => CreateMasternodeView(firoWalletId: args),
+ builder: (_) => CreateMasternodeView(
+ firoWalletId: args['walletId'] as String,
+ collateralTxid: args['collateralTxid'] as String,
+ collateralVout: args['collateralVout'] as int,
+ collateralAddress: args['collateralAddress'] as String,
+ ),
settings: RouteSettings(name: settings.name),
);
}
diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart
index c3b861f..593f618 100644
--- a/lib/wallets/wallet/impl/firo_wallet.dart
+++ b/lib/wallets/wallet/impl/firo_wallet.dart
@@ -2,7 +2,8 @@ import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
-import 'package:coinlib_flutter/coinlib_flutter.dart' show base58Decode, P2PKH;
+import 'package:coinlib_flutter/coinlib_flutter.dart'
+ show MessageSignature, base58Decode, P2PKH;
import 'package:crypto/crypto.dart' as crypto;
import 'package:decimal/decimal.dart';
import 'package:isar_community/isar.dart';
@@ -939,34 +940,70 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
String operatorPubKey,
String votingAddress,
int operatorReward,
- String payoutAddress,
- ) async {
- if (info.cachedBalance.spendable <
- Amount.fromDecimal(
- kMasterNodeValue,
- fractionDigits: cryptoCurrency.fractionDigits,
+ String payoutAddress, {
+ required String collateralTxid,
+ required int collateralVout,
+ required String collateralAddress,
+ }) async {
+ final collateralAddr =
+ await mainDB
+ .getAddresses(walletId)
+ .filter()
+ .valueEqualTo(collateralAddress)
+ .findFirst();
+ if (collateralAddr == null || collateralAddr.derivationPath == null) {
+ throw Exception(
+ 'Collateral address $collateralAddress not found in wallet '
+ 'or has no derivation path.',
+ );
+ }
+ final collateralUtxo =
+ await mainDB
+ .getUTXOs(walletId)
+ .filter()
+ .txidEqualTo(collateralTxid)
+ .and()
+ .voutEqualTo(collateralVout)
+ .findFirst();
+ final currentChainHeight = await chainHeight;
+ if (collateralUtxo == null ||
+ collateralUtxo.address != collateralAddress ||
+ collateralUtxo.isBlocked ||
+ collateralUtxo.used == true ||
+ !collateralUtxo.isConfirmed(
+ currentChainHeight,
+ cryptoCurrency.minConfirms,
+ cryptoCurrency.minCoinbaseConfirms,
)) {
throw Exception(
- 'Not enough funds to register a masternode. '
- 'You must have at least 1000 FIRO in your public balance.',
+ "Collateral outpoint is not yet confirmed/spendable. "
+ "Wait for confirmations and try again.",
);
}
-
- Address? collateralAddress = await getCurrentReceivingAddress();
- if (collateralAddress == null) {
- await generateNewReceivingAddress();
- collateralAddress = await getCurrentReceivingAddress();
+ final expectedCollateralRaw = Amount.fromDecimal(
+ kMasterNodeValue,
+ fractionDigits: cryptoCurrency.fractionDigits,
+ ).raw.toInt();
+ if (collateralUtxo.value != expectedCollateralRaw) {
+ throw Exception(
+ "Collateral outpoint must be exactly ${kMasterNodeValue.toString()} FIRO.",
+ );
}
- await generateNewReceivingAddress();
Address? ownerAddress = await getCurrentReceivingAddress();
- if (ownerAddress == null) {
+ if (ownerAddress == null || ownerAddress.value == collateralAddress) {
await generateNewReceivingAddress();
ownerAddress = await getCurrentReceivingAddress();
}
+ if (ownerAddress == null || ownerAddress.value == collateralAddress) {
+ await generateNewReceivingAddress();
+ ownerAddress = await getCurrentReceivingAddress();
+ }
+ if (ownerAddress == null) {
+ throw Exception("Could not derive owner address for masternode.");
+ }
await generateNewReceivingAddress();
- // Create the registration transaction.
final registrationTx = BytesBuilder();
// nVersion (16 bit)
@@ -974,7 +1011,7 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
(ByteData(2)..setInt16(0, 1, Endian.little)).buffer.asUint8List(),
);
- // nType (16 bit) (this is separate from the tx nType)
+ // nType (16 bit)
registrationTx.add(
(ByteData(2)..setInt16(0, 0, Endian.little)).buffer.asUint8List(),
);
@@ -984,22 +1021,23 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
(ByteData(2)..setInt16(0, 0, Endian.little)).buffer.asUint8List(),
);
- // collateralOutpoint.hash (256 bit)
- // This is null, referring to our own transaction.
- registrationTx.add(ByteData(32).buffer.asUint8List());
+ // collateralOutpoint.hash (256 bit) — real txid, byte-reversed
+ final collateralTxidBytes =
+ collateralTxid.toUint8ListFromHex.reversed.toList();
+ if (collateralTxidBytes.length != 32) {
+ throw Exception("Invalid collateral txid: $collateralTxid");
+ }
+ registrationTx.add(collateralTxidBytes);
- // collateralOutpoint.index (2 bytes)
- // This is going to be 0.
- // (The only other output will be change at position 1.)
+ // collateralOutpoint.index (uint32)
registrationTx.add(
- (ByteData(4)..setInt16(0, 0, Endian.little)).buffer.asUint8List(),
+ (ByteData(4)..setUint32(0, collateralVout, Endian.little))
+ .buffer
+ .asUint8List(),
);
- // addr.ip (4 bytes)
- final ipParts = ip
- .split('.')
- .map((e) => int.parse(e))
- .toList();
+ // addr — IPv4-mapped IPv6 (16 bytes) + port (2 bytes big-endian)
+ final ipParts = ip.split('.').map((e) => int.parse(e)).toList();
if (ipParts.length != 4) {
throw Exception("Invalid IP address: $ip");
}
@@ -1008,120 +1046,141 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
throw Exception("Invalid IP part: $part");
}
}
- // This is serialized as an IPv6 address (which it cannot be),
- // so there will be 12 bytes of padding.
registrationTx.add(ByteData(10).buffer.asUint8List());
registrationTx.add([0xff, 0xff]);
registrationTx.add(ipParts);
-
- // addr.port (2 bytes)
if (port < 1 || port > 65535) {
throw Exception("Invalid port: $port");
}
registrationTx.add(
- // network byte order
- (ByteData(2)..setInt16(0, port, Endian.big)).buffer.asUint8List(),
+ (ByteData(2)..setUint16(0, port, Endian.big)).buffer.asUint8List(),
);
// keyIDOwner (20 bytes)
- assert(ownerAddress!.value != collateralAddress!.value);
- if (!cryptoCurrency.validateAddress(ownerAddress!.value)) {
+ if (ownerAddress.value == collateralAddress) {
+ throw Exception("Owner address must differ from collateral address.");
+ }
+ if (!cryptoCurrency.validateAddress(ownerAddress.value)) {
throw Exception("Invalid owner address: ${ownerAddress.value}");
}
final ownerAddressBytes = base58Decode(ownerAddress.value);
- assert(ownerAddressBytes.length == 21); // should be infallible
- registrationTx.add(ownerAddressBytes.sublist(1)); // remove version byte
+ assert(ownerAddressBytes.length == 21);
+ registrationTx.add(ownerAddressBytes.sublist(1));
// pubKeyOperator (48 bytes)
final operatorPubKeyBytes = operatorPubKey.toUint8ListFromHex;
if (operatorPubKeyBytes.length != 48) {
- // These actually have a required format, but we're not going to check it.
- // The transaction will fail if it's not
- // valid.
throw Exception("Invalid operator public key: $operatorPubKey");
}
registrationTx.add(operatorPubKeyBytes);
- // keyIDVoting (40 bytes)
+ // keyIDVoting (20 bytes)
+ final String effectiveVotingAddress;
if (votingAddress == payoutAddress) {
throw Exception("Voting address and payout address cannot be the same.");
- } else if (votingAddress == collateralAddress!.value) {
+ } else if (votingAddress == collateralAddress) {
throw Exception(
"Voting address cannot be the same as the collateral address.",
);
} else if (votingAddress.isNotEmpty) {
- if (!cryptoCurrency.validateAddress(votingAddress)) {
- throw Exception("Invalid voting address: $votingAddress");
+ final votingType = cryptoCurrency.getAddressType(votingAddress);
+ if (votingType != AddressType.p2pkh) {
+ throw Exception(
+ "Voting address must be a transparent P2PKH address, "
+ "not a Spark or other address type.",
+ );
}
-
final votingAddressBytes = base58Decode(votingAddress);
- assert(votingAddressBytes.length == 21); // should be infallible
- registrationTx.add(votingAddressBytes.sublist(1)); // remove version byte
+ assert(votingAddressBytes.length == 21);
+ registrationTx.add(votingAddressBytes.sublist(1));
+ effectiveVotingAddress = votingAddress;
} else {
- registrationTx.add(ownerAddressBytes.sublist(1)); // remove version byte
+ registrationTx.add(ownerAddressBytes.sublist(1));
+ effectiveVotingAddress = ownerAddress.value;
}
- // nOperatorReward (16 bit); the operator gets nOperatorReward/10,000 of the reward.
+ // nOperatorReward (16 bit)
if (operatorReward < 0 || operatorReward > 10000) {
throw Exception("Invalid operator reward: $operatorReward");
}
registrationTx.add(
- (ByteData(
- 2,
- )..setInt16(0, operatorReward, Endian.little)).buffer.asUint8List(),
+ (ByteData(2)..setInt16(0, operatorReward, Endian.little))
+ .buffer
+ .asUint8List(),
);
- // scriptPayout (variable)
- if (!cryptoCurrency.validateAddress(payoutAddress)) {
- throw Exception("Invalid payout address: $payoutAddress");
+ // scriptPayout (variable) — must be P2PKH or P2SH per Firo consensus
+ final payoutType = cryptoCurrency.getAddressType(payoutAddress);
+ final Uint8List payoutScriptBytes;
+ if (payoutType == AddressType.p2pkh) {
+ final payoutHash = base58Decode(payoutAddress).sublist(1);
+ payoutScriptBytes = P2PKH.fromHash(payoutHash).script.compiled;
+ } else if (payoutType == AddressType.p2sh) {
+ final payoutHash = base58Decode(payoutAddress).sublist(1);
+ payoutScriptBytes = Uint8List.fromList([
+ 0xa9, // OP_HASH160
+ 0x14, // push 20 bytes
+ ...payoutHash,
+ 0x87, // OP_EQUAL
+ ]);
+ } else {
+ throw Exception(
+ "Payout address must be a transparent P2PKH or P2SH address, "
+ "not a Spark or other address type.",
+ );
}
- final payoutAddressScript = P2PKH.fromHash(
- base58Decode(payoutAddress).sublist(1),
- );
- final payoutAddressScriptLength =
- payoutAddressScript.script.compiled.length;
- assert(payoutAddressScriptLength < 253);
- registrationTx.addByte(payoutAddressScriptLength);
- registrationTx.add(payoutAddressScript.script.compiled);
+ assert(payoutScriptBytes.length < 253);
+ registrationTx.addByte(payoutScriptBytes.length);
+ registrationTx.add(payoutScriptBytes);
+
+ // --- coin selection for fee inputs only (exclude collateral UTXO) ---
+ final allUtxos = await mainDB.getUTXOs(walletId).findAll();
+ final feeUtxos =
+ allUtxos
+ .where(
+ (u) =>
+ !(u.txid == collateralTxid && u.vout == collateralVout) &&
+ !u.isBlocked &&
+ u.used != true &&
+ u.isConfirmed(
+ currentChainHeight,
+ cryptoCurrency.minConfirms,
+ cryptoCurrency.minCoinbaseConfirms,
+ ),
+ )
+ .map((e) => StandardInput(e) as BaseInput)
+ .toList();
final partialTxData = TxData(
- // nVersion: 3, nType: 1 (TRANSACTION_PROVIDER_REGISTER)
overrideVersion: 3 + (1 << 16),
- // coinSelection fee calculation uses a heuristic that doesn't know about
- // vExtraData, so we'll just use a really big fee to make sure the
- // transaction confirms.
feeRateAmount: cryptoCurrency.defaultFeeRate * BigInt.from(10),
recipients: [
TxRecipient(
- address: collateralAddress.value,
+ address: ownerAddress.value,
addressType: AddressType.p2pkh,
- amount: Amount.fromDecimal(
- kMasterNodeValue,
- fractionDigits: cryptoCurrency.fractionDigits,
- ),
- isChange: false,
+ amount: cryptoCurrency.dustLimit,
+ isChange: true,
),
],
);
final partialTx = await coinSelection(
txData: partialTxData,
+ // Use non-coin-control mode so unavailable UTXOs are filtered out
+ // instead of causing a hard failure when any candidate is blocked
+ // or not yet spendable.
coinControl: false,
isSendAll: false,
isSendAllCoinControlUtxos: false,
+ utxos: feeUtxos,
);
- // Calculate inputsHash (32 bytes).
+ // inputsHash (SHA256d of serialized inputs)
final inputsHashInput = BytesBuilder();
for (final input in partialTx.usedUTXOs!) {
final standardInput = input as StandardInput;
- // we reverse the txid bytes because fuck it, why not.
- final reversedTxidBytes = standardInput
- .utxo
- .txid
- .toUint8ListFromHex
- .reversed
- .toList();
+ final reversedTxidBytes =
+ standardInput.utxo.txid.toUint8ListFromHex.reversed.toList();
inputsHashInput.add(reversedTxidBytes);
inputsHashInput.add(
(ByteData(4)..setInt32(0, standardInput.utxo.vout, Endian.little))
@@ -1133,10 +1192,49 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
final inputsHashHash = crypto.sha256.convert(inputsHash).bytes;
registrationTx.add(inputsHashHash);
- // vchSig is a variable length field that we need iff the collateral is
- // NOT in the same transaction, but for us it is.
- registrationTx.addByte(0);
+ // --- payload hash & signature for external collateral ---
+ // SerializeHash(proRegTx) with SER_GETHASH excludes vchSig.
+ // The bytes built so far ARE the payload without vchSig.
+ final payloadForHash = registrationTx.toBytes();
+ final payloadHash =
+ crypto.sha256.convert(
+ crypto.sha256.convert(payloadForHash).bytes,
+ ).bytes;
+ // uint256::ToString() outputs bytes in reversed order
+ final payloadHashHex =
+ payloadHash.reversed
+ .map((b) => b.toRadixString(16).padLeft(2, '0'))
+ .join();
+
+ // MakeSignString format from Firo's providertx.cpp
+ final signString =
+ '$payoutAddress|$operatorReward|${ownerAddress.value}'
+ '|$effectiveVotingAddress|$payloadHashHex';
+
+ // Sign with the collateral private key
+ final root = await getRootHDNode();
+ final collateralKeyPair = root.derivePath(
+ collateralAddr.derivationPath!.value,
+ );
+ final messagePrefixBytes =
+ cryptoCurrency.networkParams.messagePrefix.codeUnits;
+ final cleanPrefix =
+ messagePrefixBytes.first == messagePrefixBytes.length - 1
+ ? String.fromCharCodes(messagePrefixBytes.sublist(1))
+ : cryptoCurrency.networkParams.messagePrefix;
+ final signed = MessageSignature.sign(
+ key: collateralKeyPair.privateKey,
+ message: signString,
+ prefix: cleanPrefix,
+ );
+
+ // vchSig — compact-size length + 65-byte compact signature
+ final vchSig = signed.signature.compact;
+ assert(vchSig.length == 65);
+ registrationTx.addByte(vchSig.length);
+ registrationTx.add(vchSig);
+ // --- build, sign, and broadcast ---
final finalTxData = partialTx.copyWith(
vExtraData: registrationTx.toBytes(),
);
@@ -1146,7 +1244,6 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
);
final finalTransactionHex = finalTx.raw!;
- assert(finalTransactionHex.contains(registrationTx.toBytes().toHex));
final broadcastedTxHash = await electrumXClient.broadcastTransaction(
rawTx: finalTransactionHex,
@@ -1213,43 +1310,34 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
}
Future<List<String>> getMyMasternodeProTxHashes() async {
- // - This registers only masternodes which have collateral in the same
- // transaction.
- // - If this seed is shared with firod or such and a masternode is created
- // there, it will probably not appear here
- // because that doesn't put collateral in the protx tx.
- // - An exactly 1000 FIRO vout will show up here even if it's not a
- // masternode collateral. This will just log an
- // info in getMyMasternodes.
- // - If this wallet created a masternode not owned by this wallet it will
- // erroneously be emitted here and actually
- // shown to the user as our own masternode, but this is contrived and
- // nothing actually produces transactions like
- // that.
-
- // utxos are UNSPENT txos, so broken masternodes will not show up here by
- // design.
- final utxos = await mainDB.getUTXOs(walletId).sortByBlockHeight().findAll();
-
final List<String> r = [];
+ // Look for ProRegTx transactions (nVersion=3, nType=1 → version field
+ // = 3 + (1 << 16) = 65539) that this wallet has broadcast.
+ final allTxs =
+ await mainDB.isar.transactionV2s
+ .where()
+ .walletIdEqualTo(walletId)
+ .findAll();
+ for (final tx in allTxs) {
+ if (tx.version == 3 + (1 << 16) && !r.contains(tx.txid)) {
+ r.add(tx.txid);
+ }
+ }
+
+ // Fallback: also check 1000 FIRO UTXOs (works for legacy internal
+ // collateral where the protx txid == collateral txid). Will harmlessly
+ // produce non-protx txids that getMyMasternodes filters out.
+ final utxos = await mainDB.getUTXOs(walletId).sortByBlockHeight().findAll();
final rawMasterNodeAmount = Amount.fromDecimal(
kMasterNodeValue,
fractionDigits: cryptoCurrency.fractionDigits,
).raw.toInt();
for (final utxo in utxos) {
- if (utxo.value != rawMasterNodeAmount) {
- continue;
+ if (utxo.value == rawMasterNodeAmount && !r.contains(utxo.txid)) {
+ r.add(utxo.txid);
}
-
- // A duplicate could occur if a protx transaction has a non-collateral
- // 1000 FIRO vout.
- if (r.contains(utxo.txid)) {
- continue;
- }
-
- r.add(utxo.txid);
}
return r;
diff --git a/pubspec.lock b/pubspec.lock
index 0aedf78..ee482a1 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -167,14 +167,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0"
- build_cli_annotations:
- dependency: transitive
- description:
- name: build_cli_annotations
- sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95
- url: "https://pub.dev"
- source: hosted
- version: "2.1.1"
build_config:
dependency: transitive
description:
@@ -285,10 +277,10 @@ packages:
dependency: transitive
description:
name: characters
- sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
- version: "1.4.0"
+ version: "1.4.1"
charcode:
dependency: transitive
description:
@@ -428,270 +420,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.9.0"
- cs_monero:
- dependency: "direct main"
- description:
- name: cs_monero
- sha256: b174f40e1887eb589e1e9aa99de8e9d0bc97b543f2330d5e5e7b01a6d313a9c2
- url: "https://pub.dev"
- source: hosted
- version: "3.2.0"
- cs_monero_flutter_libs:
- dependency: "direct main"
- description:
- name: cs_monero_flutter_libs
- sha256: "459542acbfc01ee6f30446c656cba670c7f1b90e52b7921a4aa0dcbc275b9eca"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.1"
- cs_monero_flutter_libs_android:
- dependency: transitive
- description:
- name: cs_monero_flutter_libs_android
- sha256: f0785f34bcf9872347823303f09409b1238b2ed7e535b9722633b0022d6188f5
- url: "https://pub.dev"
- source: hosted
- version: "1.1.2"
- cs_monero_flutter_libs_android_arm64_v8a:
- dependency: transitive
- description:
- name: cs_monero_flutter_libs_android_arm64_v8a
- sha256: "0b836dff1ead29229535a3228c7c57517127bea8b19c4c2d9bdae2770526f8ca"
- url: "https://pub.dev"
- source: hosted
- version: "1.3.0"
- cs_monero_flutter_libs_android_armeabi_v7a:
- dependency: transitive
- description:
- name: cs_monero_flutter_libs_android_armeabi_v7a
- sha256: "7955bbf91e1c3ec66e352a33e36edbab509808db6db6debfbea06f1ad2396205"
- url: "https://pub.dev"
- source: hosted
- version: "1.3.0"
- cs_monero_flutter_libs_android_x86_64:
- dependency: transitive
- description:
- name: cs_monero_flutter_libs_android_x86_64
- sha256: f51f95aa4a09be497befe020621b0d62d749d900f4dfd585fe60b7c9692010a8
- url: "https://pub.dev"
- source: hosted
- version: "1.3.0"
- cs_monero_flutter_libs_ios:
- dependency: transitive
- description:
- name: cs_monero_flutter_libs_ios
- sha256: dbc149c0787a7702a3842b4974b9bc30bad654daaa57886f874823c29c390ba7
- url: "https://pub.dev"
- source: hosted
- version: "1.3.0"
- cs_monero_flutter_libs_linux:
- dependency: transitive
- description:
- name: cs_monero_flutter_libs_linux
- sha256: "5b8bbc68a7d2bb39efdea4834097ada1aa99fd7e0b1641943c4e06c89f96616e"
- url: "https://pub.dev"
- source: hosted
- version: "1.3.0"
- cs_monero_flutter_libs_macos:
- dependency: transitive
- description:
- name: cs_monero_flutter_libs_macos
- sha256: ee02b78184b4168bc2bdb49c7ef71cc5019ffbed54c0feabcebdbc4cae5819ee
- url: "https://pub.dev"
- source: hosted
- version: "1.3.0"
- cs_monero_flutter_libs_platform_interface:
- dependency: transitive
- description:
- name: cs_monero_flutter_libs_platform_interface
- sha256: "7c832ed033257b82e2c30f1fc764f68fa4e4a780d4836a4f94384aaf9cd44ee7"
- url: "https://pub.dev"
- source: hosted
- version: "1.0.0"
- cs_monero_flutter_libs_windows:
- dependency: transitive
- description:
- name: cs_monero_flutter_libs_windows
- sha256: "9db54230f83ec07e2dce39b6b90711616ba4ab1144c7f68e4b1c13b161a18cd3"
- url: "https://pub.dev"
- source: hosted
- version: "1.3.0"
- cs_salvium:
- dependency: "direct main"
- description:
- name: cs_salvium
- sha256: e040a407bb485b177130a86dd6cd817b8cea933bbfae149a73c57a681deaa4a5
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
- cs_salvium_flutter_libs:
- dependency: "direct main"
- description:
- name: cs_salvium_flutter_libs
- sha256: "05a9f9e3f8cb539a310419d49270492e84d0f89bccb4c31512c854b1fe1f1c5f"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.1"
- cs_salvium_flutter_libs_android:
- dependency: transitive
- description:
- name: cs_salvium_flutter_libs_android
- sha256: ad9537942f7c1416fbb3432cb154d641262bd18c56471c4f62dd1d2e7e23f125
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
- cs_salvium_flutter_libs_android_arm64_v8a:
- dependency: transitive
- description:
- name: cs_salvium_flutter_libs_android_arm64_v8a
- sha256: "4c307cd3276c7aa2a461ebcfc726adf9b4d9427dbdbad120dbe50f54d3690b4e"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
- cs_salvium_flutter_libs_android_armeabi_v7a:
- dependency: transitive
- description:
- name: cs_salvium_flutter_libs_android_armeabi_v7a
- sha256: "9491e0cdd4452c9c907e137acd2d08f76d33efc7a9d4b86fbfab69224bc9f473"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
- cs_salvium_flutter_libs_android_x86_64:
- dependency: transitive
- description:
- name: cs_salvium_flutter_libs_android_x86_64
- sha256: "0b87ccd86bd9b0eeb659dade948d076cddf908d535fe803b769030da8ff406dc"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
- cs_salvium_flutter_libs_ios:
- dependency: transitive
- description:
- name: cs_salvium_flutter_libs_ios
- sha256: aa474e7da65ba36e23afc4936ffbe39328808619fbdac44dacad9aa3aafb1b08
- url: "https://pub.dev"
- source: hosted
- version: "2.0.1"
- cs_salvium_flutter_libs_linux:
- dependency: transitive
- description:
- name: cs_salvium_flutter_libs_linux
- sha256: "8adc16e9d0fb8dc439475ddb2eaa4fcde8433fa2cb6e14ce814b1a40965eda5c"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
- cs_salvium_flutter_libs_macos:
- dependency: transitive
- description:
- name: cs_salvium_flutter_libs_macos
- sha256: "988077e7affc6443a1b665bac6df3b39269cc1352375cb805bd6d26aac82b46f"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.1"
- cs_salvium_flutter_libs_platform_interface:
- dependency: transitive
- description:
- name: cs_salvium_flutter_libs_platform_interface
- sha256: "36ef1edd1481b92a95500fbdf397a371c1d624b58401a56638dc315f3c607dc0"
- url: "https://pub.dev"
- source: hosted
- version: "1.0.0"
- cs_salvium_flutter_libs_windows:
- dependency: transitive
- description:
- name: cs_salvium_flutter_libs_windows
- sha256: "934a1eeb95619df9e23eff13a6a6a356322297abfa6ab871283cdf665cc32c7f"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
- cs_wownero:
- dependency: "direct main"
- description:
- name: cs_wownero
- sha256: "9ff7a6be0f4524c6b9e5ca1d223df98e9455c7fe3b06f0b519280a175795e925"
- url: "https://pub.dev"
- source: hosted
- version: "2.0.0"
- cs_wownero_flutter_libs:
- dependency: "direct main"
- description:
- name: cs_wownero_flutter_libs
- sha256: ba1156d015a9f75c841f927ff2ce6565cd7cd37f15aaedd9aaf36703453a9884
- url: "https://pub.dev"
- source: hosted
- version: "2.0.3"
- cs_wownero_flutter_libs_android:
- dependency: transitive
- description:
- name: cs_wownero_flutter_libs_android
- sha256: "14fe0666999d078bcd91ca499a9e9395dd270211eedb2250c533cbd036cb328b"
- url: "https://pub.dev"
- source: hosted
- version: "1.2.0"
- cs_wownero_flutter_libs_android_arm64_v8a:
- dependency: transitive
- description:
- name: cs_wownero_flutter_libs_android_arm64_v8a
- sha256: "19f7e17ce7adf4615685f92b106c7f588dee80bb4768931c2505d2761a9fa06c"
- url: "https://pub.dev"
- source: hosted
- version: "1.2.0"
- cs_wownero_flutter_libs_android_armeabi_v7a:
- dependency: transitive
- description:
- name: cs_wownero_flutter_libs_android_armeabi_v7a
- sha256: "1b7dc845674c938259dcbce6b9d6e6c305c98c2ff9b83803b00ea0f1268dfb28"
- url: "https://pub.dev"
- source: hosted
- version: "1.2.0"
- cs_wownero_flutter_libs_android_x86_64:
- dependency: transitive
- description:
- name: cs_wownero_flutter_libs_android_x86_64
- sha256: c318ce80ef418d53aeef3698c89c0497394269311f8c5b75f160e0f81610f9d9
- url: "https://pub.dev"
- source: hosted
- version: "1.2.0"
- cs_wownero_flutter_libs_ios:
- dependency: transitive
- description:
- name: cs_wownero_flutter_libs_ios
- sha256: "9ffd158469a0a45668d89ce56b90e846dd823ffd44ee6997b50b76129e6f613c"
- url: "https://pub.dev"
- source: hosted
- version: "1.3.0"
- cs_wownero_flutter_libs_linux:
- dependency: transitive
- description:
- name: cs_wownero_flutter_libs_linux
- sha256: "441c9a7b28e28434942709915e6a54ea2392b3261f90c116e03b27b02fce7492"
- url: "https://pub.dev"
- source: hosted
- version: "1.4.0"
- cs_wownero_flutter_libs_macos:
- dependency: transitive
- description:
- name: cs_wownero_flutter_libs_macos
- sha256: e703975e6a6f698b01e07b238953547391faa4e930f09733d01f1346ee788fc7
- url: "https://pub.dev"
- source: hosted
- version: "1.2.0"
- cs_wownero_flutter_libs_platform_interface:
- dependency: transitive
- description:
- name: cs_wownero_flutter_libs_platform_interface
- sha256: "6a3bda9bcf5a904b36cbd0817e7ae8b7a64693e6f532f1783513e93c64436e6f"
- url: "https://pub.dev"
- source: hosted
- version: "1.1.0"
- cs_wownero_flutter_libs_windows:
- dependency: transitive
- description:
- name: cs_wownero_flutter_libs_windows
- sha256: fe7485863a6e83e31581cef36c62d3507a9ea36c73d56842b4fee059f349cb49
- url: "https://pub.dev"
- source: hosted
- version: "1.2.0"
csslib:
dependency: transitive
description:
@@ -1002,20 +730,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.13.1"
- flutter_libepiccash:
- dependency: "direct main"
- description:
- path: "crypto_plugins/flutter_libepiccash"
- relative: true
- source: path
- version: "0.0.1"
- flutter_libmwc:
- dependency: "direct main"
- description:
- path: "crypto_plugins/flutter_libmwc"
- relative: true
- source: path
- version: "0.0.1"
flutter_libsparkmobile:
dependency: "direct main"
description:
@@ -1057,14 +771,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.2.0"
- flutter_mwebd:
- dependency: "direct main"
- description:
- name: flutter_mwebd
- sha256: "14f2a331b2621b78ddf62081ca8a466f6a2b4352a66950fffd68615c14e63edf"
- url: "https://pub.dev"
- source: hosted
- version: "0.0.1-pre.11"
flutter_native_splash:
dependency: "direct main"
description:
@@ -1089,14 +795,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
- flutter_rust_bridge:
- dependency: transitive
- description:
- name: flutter_rust_bridge
- sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e"
- url: "https://pub.dev"
- source: hosted
- version: "2.11.1"
flutter_secure_storage:
dependency: "direct main"
description:
@@ -1187,13 +885,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
- frostdart:
- dependency: "direct main"
- description:
- path: "crypto_plugins/frostdart"
- relative: true
- source: path
- version: "0.0.1"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
@@ -1453,14 +1144,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.11.2"
- jsontool:
- dependency: transitive
- description:
- name: jsontool
- sha256: e49bf419e82d90f009426cd7fdec8d54ba8382975b3454ed16a3af3ee1d1b697
- url: "https://pub.dev"
- source: hosted
- version: "2.1.0"
keyboard_dismisser:
dependency: "direct main"
description:
@@ -1570,18 +1253,18 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
- version: "0.12.17"
+ version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
- version: "0.11.1"
+ version: "0.13.0"
memoize:
dependency: transitive
description:
@@ -2252,26 +1935,26 @@ packages:
dependency: transitive
description:
name: test
- sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
+ sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
url: "https://pub.dev"
source: hosted
- version: "1.26.3"
+ version: "1.29.0"
test_api:
dependency: transitive
description:
name: test_api
- sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
+ sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
- version: "0.7.7"
+ version: "0.7.9"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
+ sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
url: "https://pub.dev"
source: hosted
- version: "0.6.12"
+ version: "0.6.15"
tezart:
dependency: "direct main"
description:
@@ -2466,14 +2149,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
- very_good_analysis:
- dependency: transitive
- description:
- name: very_good_analysis
- sha256: "96245839dbcc45dfab1af5fa551603b5c7a282028a64746c19c547d21a7f1e3a"
- url: "https://pub.dev"
- source: hosted
- version: "10.0.0"
vm_service:
dependency: transitive
description:
@@ -2563,14 +2238,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.5"
- web_socket_client:
- dependency: transitive
- description:
- name: web_socket_client
- sha256: "394789177aa3bc1b7b071622a1dbf52a4631d7ce23c555c39bb2523e92316b07"
- url: "https://pub.dev"
- source: hosted
- version: "0.2.1"
webdriver:
dependency: transitive
description:
@@ -2620,23 +2287,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
- xelis_dart_sdk:
- dependency: "direct main"
- description:
- name: xelis_dart_sdk
- sha256: "2393fcd3dfe9175e34ed60e1a1f8821fb63d6a99d66894b9a24cdfc8cb4a6a4b"
- url: "https://pub.dev"
- source: hosted
- version: "0.30.9"
- xelis_flutter:
- dependency: "direct main"
- description:
- path: "."
- ref: "v0.2.1"
- resolved-ref: afcc21e0499e78236ca618c7d9f6bee8280dede1
- url: "https://github.com/xelis-project/xelis-flutter-ffi.git"
- source: git
- version: "0.2.1"
xml:
dependency: transitive
description:
Why this scored 70/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.