diff --git a/lib/pages/masternodes/create_masternode_view.dart b/lib/pages/masternodes/create_masternode_view.dart index 9d2940ef7..d3f8cbad6 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 { 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()!.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 { ), 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 6933a5c1e..a9b427145 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 { +class _MasternodesHomeViewState extends ConsumerState + with WidgetsBindingObserver { late Future> _masternodesFuture; + bool _hasPromptedForCollateral = false; + bool _isCheckingForCollateral = false; - Future _showDesktopCreateMasternodeDialog() async { - final txid = await showDialog( - 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 _createMasternode() async { + final collateral = await _findCollateralUtxo(); + if (!mounted) { + return; + } + + if (collateral == null) { + await showDialog( + 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( + 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 _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( + 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()! + .getSecondaryEnabledButtonStyle(ctx), + child: Text( + "Later", + style: STextStyles.button( + ctx, + ).copyWith( + color: Theme.of(ctx).extension()!.accentColorDark, + ), + ), + onPressed: () => Navigator.of(ctx).pop(false), + ), + rightButton: TextButton( + style: Theme.of(ctx) + .extension()! + .getPrimaryEnabledButtonStyle(ctx), + child: Text( + "Register", + style: STextStyles.button(ctx).copyWith( + color: + Theme.of(ctx).extension()!.buttonTextPrimary, + ), + ), + onPressed: () => Navigator.of(ctx).pop(true), + ), + ), + ); + + if (wantsMN != true || !mounted) { + return; + } + + if (Util.isDesktop) { + final txid = await showDialog( + 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 { @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 { .srcIn, ), ), - onPressed: _showDesktopCreateMasternodeDialog, + onPressed: _createMasternode, ), ), ) @@ -184,13 +363,7 @@ class _MasternodesHomeViewState extends ConsumerState { 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 { 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 84977d3d4..05c4d2895 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 { - 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(() async { Exception? ex; @@ -168,24 +168,6 @@ class _RegisterMasternodeFormState @override Widget build(BuildContext context) { final stack = Theme.of(context).extension()!; - 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 dfd6c98bd..c79f1aec6 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 _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.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 = {}; + final addresses = scriptPubKey['addresses']; + if (addresses is List) { + recipientAddresses.addAll( + addresses.whereType(), + ); + } + + 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 _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.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 cad05cbcd..dc774fa9f 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) { 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 c3b861ff7..593f6185f 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 extends Bip39HDWallet 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 extends Bip39HDWallet (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 extends Bip39HDWallet (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 extends Bip39HDWallet 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 extends Bip39HDWallet 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 extends Bip39HDWallet ); final finalTransactionHex = finalTx.raw!; - assert(finalTransactionHex.contains(registrationTx.toBytes().toHex)); final broadcastedTxHash = await electrumXClient.broadcastTransaction( rawTx: finalTransactionHex, @@ -1213,43 +1310,34 @@ class FiroWallet extends Bip39HDWallet } Future> 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 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 0aedf7867..ee482a1c9 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: