diff --git a/lib/pages/masternodes/masternode_constants.dart b/lib/pages/masternodes/masternode_constants.dart deleted file mode 100644 index 9ee8c692b7..0000000000 --- a/lib/pages/masternodes/masternode_constants.dart +++ /dev/null @@ -1,12 +0,0 @@ -abstract final class MasternodeCollateralNotes { - MasternodeCollateralNotes._(); - - static const unshield = - "Masternode collateral unshield (1000 FIRO to transparent)."; - static const prep = "Masternode collateral prep (1000 FIRO self-send)."; - - static bool isUnshield(String? note) => - note != null && note.contains(unshield); - - static bool isPrep(String? note) => note != null && note.contains(prep); -} diff --git a/lib/pages/masternodes/masternodes_home_view.dart b/lib/pages/masternodes/masternodes_home_view.dart index 22698f11a1..f6dd1caeeb 100644 --- a/lib/pages/masternodes/masternodes_home_view.dart +++ b/lib/pages/masternodes/masternodes_home_view.dart @@ -9,7 +9,9 @@ import 'package:tuple/tuple.dart'; import '../../models/isar/models/blockchain_data/utxo.dart'; import '../../models/send_view_auto_fill_data.dart'; +import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart'; import '../../providers/global/wallets_provider.dart'; +import '../../providers/wallet/public_private_balance_state_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/assets.dart'; @@ -21,6 +23,8 @@ import '../../wallets/isar/models/wallet_info.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/desktop_app_bar.dart'; +import '../../widgets/desktop/desktop_dialog.dart'; +import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/desktop/desktop_scaffold.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/desktop/secondary_button.dart'; @@ -29,9 +33,7 @@ import '../../widgets/loading_indicator.dart'; import '../../widgets/stack_dialog.dart'; import '../send_view/send_view.dart'; import 'create_masternode_view.dart'; -import 'masternode_constants.dart'; import 'sub_widgets/masternodes_list.dart'; -import 'sub_widgets/masternodes_table_desktop.dart'; class MasternodesHomeView extends ConsumerStatefulWidget { const MasternodesHomeView({super.key, required this.walletId}); @@ -79,6 +81,16 @@ class _MasternodesHomeViewState extends ConsumerState { ); } + Future> _registeredCollateral() async { + try { + return (await _masternodesFuture) + .map((e) => "${e.collateralHash}:${e.collateralIndex}") + .toSet(); + } catch (_) { + return {}; + } + } + Future<({String txid, int vout, String address})?> _findCollateralUtxo() async { final wallet = ref.read(pWallets).getWallet(widget.walletId) as FiroWallet; @@ -86,6 +98,7 @@ class _MasternodesHomeViewState extends ConsumerState { .getUTXOs(widget.walletId) .findAll(); final currentChainHeight = await wallet.chainHeight; + final registered = await _registeredCollateral(); final masternodeRaw = Amount.fromDecimal( kMasterNodeValue, fractionDigits: wallet.cryptoCurrency.fractionDigits, @@ -95,6 +108,7 @@ class _MasternodesHomeViewState extends ConsumerState { if (utxo.value == masternodeRaw && !utxo.isBlocked && utxo.used != true && + !registered.contains("${utxo.txid}:${utxo.vout}") && utxo.isConfirmed( currentChainHeight, wallet.cryptoCurrency.minConfirms, @@ -379,37 +393,43 @@ class _MasternodesHomeViewState extends ConsumerState { 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); - } + await _openCreateMasternode(collateral); } finally { _createMasternodeLock = false; } } + Future _openCreateMasternode( + ({String txid, int vout, String address}) collateral, + ) async { + final Object? txid; + if (Util.isDesktop) { + txid = await showDialog( + context: context, + barrierDismissible: true, + builder: (context) => SDialog( + child: CreateMasternodeView( + firoWalletId: widget.walletId, + collateralTxid: collateral.txid, + collateralVout: collateral.vout, + collateralAddress: collateral.address, + ), + ), + ); + } else { + txid = await Navigator.of(context).pushNamed( + CreateMasternodeView.routeName, + arguments: { + 'walletId': widget.walletId, + 'collateralTxid': collateral.txid, + 'collateralVout': collateral.vout, + 'collateralAddress': collateral.address, + }, + ); + } + _handleSuccessTxid(txid); + } + Future _openCreateCollateralSendFlow( FiroWallet wallet, { bool fromPrivate = false, @@ -424,23 +444,57 @@ class _MasternodesHomeViewState extends ConsumerState { return; } - await Navigator.of(context).pushNamed( - SendView.routeName, - arguments: Tuple3( - widget.walletId, - wallet.cryptoCurrency, - SendViewAutoFillData( - address: selfAddress.value, - contactLabel: "My FIRO address", - amount: fromPrivate - ? (unshieldAmount ?? kMasterNodeValue) - : kMasterNodeValue, - note: fromPrivate - ? MasternodeCollateralNotes.unshield - : MasternodeCollateralNotes.prep, - ), - ), + ref.read(publicPrivateBalanceStateProvider.state).state = fromPrivate + ? BalanceType.private + : BalanceType.public; + + final ticker = wallet.cryptoCurrency.ticker; + final autoFillData = SendViewAutoFillData( + address: selfAddress.value, + contactLabel: selfAddress.value, + amount: fromPrivate + ? (unshieldAmount ?? kMasterNodeValue) + : kMasterNodeValue, ); + + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: .spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Send $ticker", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), + child: DesktopSend( + walletId: widget.walletId, + autoFillData: autoFillData, + ), + ), + ], + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + SendView.routeName, + arguments: Tuple3(widget.walletId, wallet.cryptoCurrency, autoFillData), + ); + } } Future _maybePromptForExistingCollateral() async { @@ -473,6 +527,8 @@ class _MasternodesHomeViewState extends ConsumerState { message: "A 1000 FIRO collateral UTXO was found in your wallet. " "Would you like to register a masternode now?", + width: Util.isDesktop ? 580 : null, + padding: .all(Util.isDesktop ? 32 : 24), leftButton: TextButton( style: Theme.of( ctx, @@ -495,70 +551,50 @@ class _MasternodesHomeViewState extends ConsumerState { ), ); - if (wantsMN == false || wantsMN == null) { + if (wantsMN != true) { await _persistDismissedCollateral( wallet, collateral.txid, collateral.vout, ); + return; } - if (wantsMN != true || !mounted) { + if (!mounted) { return; } - if (Util.isDesktop) { - final txid = await showDialog( - context: context, - barrierDismissible: true, - builder: (context) => SDialog( - child: CreateMasternodeView( - firoWalletId: widget.walletId, - collateralTxid: collateral.txid, - collateralVout: collateral.vout, - collateralAddress: collateral.address, - ), - ), - ); - _handleSuccessTxid(txid); - } else { - final txid = await Navigator.of(context).pushNamed( - CreateMasternodeView.routeName, - arguments: { - 'walletId': widget.walletId, - 'collateralTxid': collateral.txid, - 'collateralVout': collateral.vout, - 'collateralAddress': collateral.address, - }, - ); - _handleSuccessTxid(txid); - } + await _openCreateMasternode(collateral); } finally { _isCheckingForCollateral = false; } } + Future> _fetchMasternodes() => + (ref.read(pWallets).getWallet(widget.walletId) as FiroWallet) + .getMyMasternodes(); + void _handleSuccessTxid(Object? txid) { Logging.instance.i( "$runtimeType _handleSuccessTxid($txid) called where mounted=$mounted", ); if (mounted && txid is String) { setState(() { - _masternodesFuture = - (ref.read(pWallets).getWallet(widget.walletId) as FiroWallet) - .getMyMasternodes(); + _masternodesFuture = _fetchMasternodes(); }); - showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Masternode Registration Submitted", - message: - "Masternode registration submitted, your masternode will " - "appear in the list after the tx is confirmed.\n\nTransaction" - " ID: $txid", - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 400 : null, + unawaited( + showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Masternode Registration Submitted", + message: + "Masternode registration submitted, your masternode will " + "appear in the list after the tx is confirmed.\n\nTransaction" + " ID: $txid", + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 400 : null, + ), ), ); } @@ -568,9 +604,7 @@ class _MasternodesHomeViewState extends ConsumerState { void initState() { super.initState(); - _masternodesFuture = - (ref.read(pWallets).getWallet(widget.walletId) as FiroWallet) - .getMyMasternodes(); + _masternodesFuture = _fetchMasternodes(); WidgetsBinding.instance.addPostFrameCallback((_) { unawaited(_maybePromptForExistingCollateral()); @@ -693,59 +727,68 @@ class _MasternodesHomeViewState extends ConsumerState { return const Center(child: LoadingIndicator(height: 50, width: 50)); } if (snapshot.hasError) { - return Center( - child: Text( - "Failed to load masternodes", - style: STextStyles.w600_14(context), - ), + return _CenteredMessage( + message: "Failed to load masternodes", + buttonLabel: "Retry", + onPressed: () => + setState(() => _masternodesFuture = _fetchMasternodes()), ); } final nodes = snapshot.data ?? const []; if (nodes.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "No masternodes found", - style: STextStyles.w600_14(context), - ), - const SizedBox(height: 24), - Row( - mainAxisSize: .min, - mainAxisAlignment: .center, - children: [ - PrimaryButton( - label: "Create Your First Masternode", - horizontalContentPadding: 16, - buttonHeight: Util.isDesktop ? .l : null, - onPressed: _createMasternode, - ), - ], - ), - ], - ), + return _CenteredMessage( + message: "No masternodes found", + buttonLabel: "Create Your First Masternode", + onPressed: _createMasternode, ); } - if (Util.isDesktop) { - return MasternodesTableDesktop(nodes: nodes); - } else { - return MasternodesList(nodes: nodes); - } + return MasternodesList(nodes: nodes); }, ), ); } } -class _OpenSendDialog extends StatelessWidget { - const _OpenSendDialog({ - super.key, - required this.title, +class _CenteredMessage extends StatelessWidget { + const _CenteredMessage({ required this.message, + required this.buttonLabel, + required this.onPressed, }); + final String message, buttonLabel; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: .center, + children: [ + Text(message, style: STextStyles.w600_14(context)), + const SizedBox(height: 24), + Row( + mainAxisSize: .min, + mainAxisAlignment: .center, + children: [ + PrimaryButton( + label: buttonLabel, + horizontalContentPadding: 16, + buttonHeight: Util.isDesktop ? .l : null, + onPressed: onPressed, + ), + ], + ), + ], + ), + ); + } +} + +class _OpenSendDialog extends StatelessWidget { + const _OpenSendDialog({required this.title, required this.message}); + final String title, message; @override diff --git a/lib/pages/masternodes/sub_widgets/masternode_info_widget.dart b/lib/pages/masternodes/sub_widgets/masternode_info_widget.dart index c25b696f12..1838476c89 100644 --- a/lib/pages/masternodes/sub_widgets/masternode_info_widget.dart +++ b/lib/pages/masternodes/sub_widgets/masternode_info_widget.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; import '../../../themes/stack_colors.dart'; -import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../widgets/conditional_parent.dart'; -import '../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../widgets/detail_item.dart'; import '../../../widgets/rounded_white_container.dart'; @@ -21,40 +19,15 @@ class MasternodeInfoWidget extends StatelessWidget { return ConditionalParent( condition: Util.isDesktop, - builder: (child) => Column( - crossAxisAlignment: .stretch, - mainAxisSize: .min, - children: [ - // not really the place for this in terms of structure but running - // out of time... - Row( - mainAxisAlignment: .spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Masternode details", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Flexible( - child: Padding( - padding: const EdgeInsets.only(left: 32, bottom: 32, right: 32), - child: RoundedWhiteContainer( - padding: .zero, - - // using listview kind of breaks - borderColor: Theme.of( - context, - ).extension()!.backgroundAppBar, - child: child, - ), - ), - ), - ], + builder: (child) => Padding( + padding: const EdgeInsets.only(left: 32, bottom: 32, right: 32), + child: RoundedWhiteContainer( + padding: .zero, + borderColor: Theme.of( + context, + ).extension()!.backgroundAppBar, + child: child, + ), ), child: Column( mainAxisSize: .min, diff --git a/lib/pages/masternodes/sub_widgets/masternodes_list.dart b/lib/pages/masternodes/sub_widgets/masternodes_list.dart index 8df056fba1..45446b8e15 100644 --- a/lib/pages/masternodes/sub_widgets/masternodes_list.dart +++ b/lib/pages/masternodes/sub_widgets/masternodes_list.dart @@ -2,9 +2,13 @@ import 'package:flutter/material.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; import '../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; import '../../../widgets/rounded_white_container.dart'; import '../masternode_details_view.dart'; +import 'masternode_info_widget.dart'; class MasternodesList extends StatelessWidget { const MasternodesList({super.key, required this.nodes}); @@ -14,57 +18,105 @@ class MasternodesList extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.separated( - padding: EdgeInsets.zero, + padding: EdgeInsets.all(Util.isDesktop ? 24 : 16), itemCount: nodes.length, separatorBuilder: (_, __) => const SizedBox(height: 8), - itemBuilder: (context, index) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: _MasternodeCard(node: nodes[index]), - ), + itemBuilder: (_, index) => _MasternodeCard(node: nodes[index]), ); } } -// TODO better styling class _MasternodeCard extends StatelessWidget { - const _MasternodeCard({super.key, required this.node}); + const _MasternodeCard({required this.node}); final MasternodeInfo node; + Future _showDetails(BuildContext context) async { + if (Util.isDesktop) { + await showDialog( + context: context, + barrierDismissible: true, + builder: (context) => SDialog( + contentCanScroll: false, + child: SizedBox( + width: 600, + child: Column( + crossAxisAlignment: .stretch, + mainAxisSize: .min, + children: [ + Row( + mainAxisAlignment: .spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Masternode details", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Flexible( + child: SingleChildScrollView( + child: MasternodeInfoWidget(info: node), + ), + ), + ], + ), + ), + ), + ); + } else { + await Navigator.of( + context, + ).pushNamed(MasternodeDetailsView.routeName, arguments: node); + } + } + @override Widget build(BuildContext context) { final stack = Theme.of(context).extension()!; + final isActive = node.revocationReason == 0; + return RoundedWhiteContainer( - onPressed: () => Navigator.of( - context, - ).pushNamed(MasternodeDetailsView.routeName, arguments: node), - child: Column( - mainAxisSize: .min, + padding: const EdgeInsets.all(16), + onPressed: () => _showDetails(context), + child: Row( children: [ - Row( - mainAxisAlignment: .spaceBetween, - children: [ - Text("IP: ${node.serviceAddr}"), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: node.revocationReason == 0 - ? stack.accentColorGreen - : stack.accentColorRed, - borderRadius: BorderRadius.circular(8), + Expanded( + child: Column( + crossAxisAlignment: .start, + mainAxisSize: .min, + children: [ + Text( + "${node.serviceAddr}:${node.servicePort}", + style: STextStyles.titleBold12(context), + overflow: TextOverflow.ellipsis, ), - child: Text( - node.revocationReason == 0 ? "ACTIVE" : "REVOKED", - style: STextStyles.w600_12( + const SizedBox(height: 2), + Text( + "Last paid height: ${node.lastPaidHeight}", + style: STextStyles.baseXS( context, - ).copyWith(color: stack.textWhite), + ).copyWith(color: stack.textSubtitle1), ), - ), - ], + ], + ), ), - Row( - mainAxisAlignment: .spaceBetween, - children: [Text("Last Paid Height: ${node.lastPaidHeight}")], + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isActive ? stack.accentColorGreen : stack.accentColorRed, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + isActive ? "ACTIVE" : "REVOKED", + style: STextStyles.w600_12( + context, + ).copyWith(color: stack.textWhite), + ), ), ], ), diff --git a/lib/pages/masternodes/sub_widgets/masternodes_table_desktop.dart b/lib/pages/masternodes/sub_widgets/masternodes_table_desktop.dart deleted file mode 100644 index 3b3727d892..0000000000 --- a/lib/pages/masternodes/sub_widgets/masternodes_table_desktop.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../themes/stack_colors.dart'; -import '../../../utilities/text_styles.dart'; -import '../../../wallets/wallet/impl/firo_wallet.dart'; -import '../../../widgets/dialogs/s_dialog.dart'; -import 'masternode_info_widget.dart'; - -class MasternodesTableDesktop extends StatelessWidget { - const MasternodesTableDesktop({super.key, required this.nodes}); - - final List nodes; - - @override - Widget build(BuildContext context) { - final stack = Theme.of(context).extension()!; - return Container( - color: stack.textFieldDefaultBG, - child: Column( - children: [ - // Fixed header - Container( - height: 56, - color: stack.textFieldDefaultBG, - child: Row( - children: [ - const Expanded( - flex: 2, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Text('IP'), - ), - ), - ), - const Expanded( - flex: 2, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Text('Last Paid Height'), - ), - ), - ), - const Expanded( - flex: 2, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Text('Status'), - ), - ), - ), - Expanded(flex: 3, child: Container()), - ], - ), - ), - // Scrollable content - Expanded( - child: Container( - width: double.infinity, - color: stack.textFieldDefaultBG, - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - children: nodes.map((node) { - final status = node.revocationReason == 0 - ? 'Active' - : 'Revoked'; - return SizedBox( - height: 48, - child: Row( - children: [ - Expanded( - flex: 2, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Text( - node.serviceAddr, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - Expanded( - flex: 2, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Text( - node.lastPaidHeight.toString(), - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - Expanded( - flex: 2, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: status.toLowerCase() == 'active' - ? stack.accentColorGreen - : stack.accentColorRed, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - status.toUpperCase(), - style: STextStyles.w600_12( - context, - ).copyWith(color: stack.textWhite), - ), - ), - ), - ), - ), - Expanded( - flex: 3, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () { - showDialog( - context: context, - barrierDismissible: true, - builder: (context) => SDialog( - child: SizedBox( - width: 600, - child: MasternodeInfoWidget( - info: node, - ), - ), - ), - ); - }, - icon: const Icon(Icons.info_outline), - tooltip: 'View Details', - ), - ], - ), - ), - ), - ), - ], - ), - ); - }).toList(), - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/masternodes/sub_widgets/register_masternode_form.dart b/lib/pages/masternodes/sub_widgets/register_masternode_form.dart index 5d8dd945f1..6bd79d17a2 100644 --- a/lib/pages/masternodes/sub_widgets/register_masternode_form.dart +++ b/lib/pages/masternodes/sub_widgets/register_masternode_form.dart @@ -126,6 +126,7 @@ class _RegisterMasternodeFormState final txId = await showLoading( whileFutureAlt: _registerMasternode, context: context, + rootNavigator: Util.isDesktop, message: "Creating and submitting masternode registration...", delay: const Duration(seconds: 1), onException: (e) => ex = e, diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 732008dfb7..8da704edf4 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -218,7 +218,7 @@ class _ConfirmTransactionViewState context: context, builder: (context) => AlertDialog( title: const Text('Slatepack Creation Failed'), - content: Text('Failed to create slatepack: $e'), + content: Text(errorMessage), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), @@ -297,7 +297,7 @@ class _ConfirmTransactionViewState context: context, builder: (context) => AlertDialog( title: const Text('Slate Creation Failed'), - content: Text('Failed to create slate: $e'), + content: Text(errorMessage), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), @@ -479,118 +479,7 @@ class _ConfirmTransactionViewState widget.onSuccess.call(); - // Check for 1000 FIRO transparent self-send → prompt MN registration - bool navigatedToMN = false; - if (wallet is FiroWallet && - confirmedTx.recipients != null && - confirmedTx.sparkMints == null && - txids.isNotEmpty && - context.mounted) { - try { - final masternodeAmount = Amount.fromDecimal( - kMasterNodeValue, - fractionDigits: wallet.cryptoCurrency.fractionDigits, - ); - final txFeeRaw = confirmedTx.fee?.raw ?? BigInt.zero; - - final mnRecipient = confirmedTx.recipients! - // Exact 1000 FIRO: multiple such outputs uses the first match only. - .where((r) => !r.isChange && r.amount == masternodeAmount) - .firstOrNull; - - if (mnRecipient != null && confirmedTx.txid != null) { - final ownAddress = await ref - .read(mainDBProvider) - .getAddresses(walletId) - .filter() - .valueEqualTo(mnRecipient.address) - .findFirst(); - - if (ownAddress != null && context.mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Collateral transaction sent", - message: - "Your 1000 FIRO collateral transaction was sent " - "successfully. Once it confirms, open Masternodes and " - "click Create Masternode to continue.", - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 420 : null, - ), - ); - - if (context.mounted) { - // Pop confirm + send; returns to the screen that opened send - // (e.g. Masternodes or desktop wallet) without relying on - // popUntil matching a route name in the nested navigator. - final navigator = Navigator.of(context); - for (var i = 0; i < 2 && navigator.canPop(); i++) { - navigator.pop(); - } - navigatedToMN = true; - } - } - } else if (mnRecipient != null && - confirmedTx.txid == null && - context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: - "Could not determine transaction id for collateral " - "auto-detection. Register from the Masternodes screen " - "once the transaction appears.", - context: context, - ), - ); - } else { - // If fee was subtracted from the recipient, users can enter 1000 but - // end up with ~999.99... output which is not valid MN collateral. - final nearMnRecipient = - confirmedTx.recipients! - .where( - (r) => !r.isChange && r.amount.raw < masternodeAmount.raw, - ) - .where( - (r) => (masternodeAmount.raw - r.amount.raw) <= txFeeRaw, - ) - .toList() - ..sort((a, b) => b.amount.raw.compareTo(a.amount.raw)); - - if (nearMnRecipient.isNotEmpty) { - final maybeOwnAddress = await ref - .read(mainDBProvider) - .getAddresses(walletId) - .filter() - .valueEqualTo(nearMnRecipient.first.address) - .findFirst(); - - if (maybeOwnAddress != null && context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: - "Masternode collateral requires one exact 1000 FIRO " - "transparent output. Fee appears to have been " - "subtracted from the recipient amount. Send 1000 " - "to yourself again with fee paid on top.", - context: context, - ), - ); - } - } - } - } catch (e, s) { - Logging.instance.w( - "Skipping masternode collateral auto-detection: $e", - error: e, - stackTrace: s, - ); - } - } - - if (!navigatedToMN && context.mounted) { + if (context.mounted) { if (widget.onSuccessInsteadOfRouteOnSuccess == null) { Navigator.of( context, @@ -1675,9 +1564,9 @@ class _ConfirmTransactionViewState } } } else { - final unlocked = await Navigator.push( + final unlocked = await Navigator.push( context, - RouteGenerator.getRoute( + RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, builder: (_) => const LockscreenView( diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 7eb453d6a5..18b8d5be2e 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -16,7 +16,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:isar_community/isar.dart'; import 'package:tuple/tuple.dart'; import '../../models/epic_slatepack_models.dart'; @@ -69,8 +68,6 @@ import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/dialogs/firo_exchange_address_dialog.dart'; -import '../../widgets/dialogs/s_dialog.dart'; -import '../../widgets/desktop/secondary_button.dart'; import '../../widgets/epic_txs_method_toggle.dart'; import '../../widgets/eth_fee_form.dart'; import '../../widgets/fee_slider.dart'; @@ -85,7 +82,6 @@ import '../../widgets/stack_text_field.dart'; import '../../widgets/textfield_icon_button.dart'; import '../address_book_views/address_book_view.dart'; import '../coin_control/coin_control_view.dart'; -import '../masternodes/masternode_constants.dart'; import 'confirm_transaction_view.dart'; import 'sub_widgets/building_transaction_dialog.dart'; import 'sub_widgets/dual_balance_selection_sheet.dart'; @@ -147,8 +143,6 @@ class _SendViewState extends ConsumerState { late final bool hasOptionalMemo; late final bool isFiro; late final bool isEth; - late final bool _isMasternodeCollateralSelfSend; - late final bool _isMasternodeCollateralUnshield; Amount? _cachedAmountToSend; String? _address; @@ -296,104 +290,6 @@ class _SendViewState extends ConsumerState { } } - Future _pickMyAddressForMasternodeCollateral() async { - final wallet = ref.read(pWallets).getWallet(walletId); - if (wallet is! FiroWallet) { - return; - } - - var currentAddress = await wallet.getCurrentReceivingAddress(); - if (currentAddress == null) { - await wallet.generateNewReceivingAddress(); - currentAddress = await wallet.getCurrentReceivingAddress(); - } - - final allWalletAddresses = await wallet.mainDB.isar.addresses - .where() - .walletIdEqualTo(walletId) - .findAll(); - - final transparentAddresses = allWalletAddresses - .where((e) => e.type != AddressType.spark) - .map((e) => e.value) - .where((String e) => e.isNotEmpty) - .toSet(); - - if (currentAddress != null && - wallet.cryptoCurrency.getAddressType(currentAddress.value) != - AddressType.spark) { - transparentAddresses.add(currentAddress.value); - } - - final addresses = {...transparentAddresses}.toList()..sort(); - - if (!mounted || addresses.isEmpty) { - return; - } - - final selectedAddress = await showDialog( - context: context, - builder: (ctx) => SDialog( - contentCanScroll: false, - padding: EdgeInsets.all(Util.isDesktop ? 32 : 16), - child: SizedBox( - width: Util.isDesktop ? 520 : null, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Choose your address", - style: Util.isDesktop - ? STextStyles.desktopH3(ctx) - : STextStyles.pageTitleH2(ctx), - ), - const SizedBox(height: 16), - ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(ctx).size.height * 0.5, - ), - child: ListView.builder( - shrinkWrap: true, - itemCount: addresses.length, - itemBuilder: (_, index) => ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - addresses[index], - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Util.isDesktop - ? STextStyles.w500_16(ctx) - : STextStyles.w500_14(ctx), - ), - onTap: () => Navigator.of(ctx).pop(addresses[index]), - ), - ), - ), - const SizedBox(height: 16), - SecondaryButton( - buttonHeight: ButtonHeight.l, - label: "Cancel", - onPressed: () => Navigator.of(ctx).pop(), - ), - ], - ), - ), - ), - ); - - if (selectedAddress == null) { - return; - } - - _address = selectedAddress; - sendToController.text = selectedAddress; - _setValidAddressProviders(_address); - setState(() { - _addressToggleFlag = true; - }); - } - Future _scanQr() async { try { // ref @@ -1241,7 +1137,7 @@ class _SendViewState extends ConsumerState { } // pop building dialog - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: true).pop(); unawaited( Navigator.of(context).push( @@ -1268,7 +1164,7 @@ class _SendViewState extends ConsumerState { Logging.instance.e("$e\n$s", error: e, stackTrace: s); if (mounted) { // pop building dialog - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: true).pop(); unawaited( showDialog( @@ -1371,40 +1267,6 @@ class _SendViewState extends ConsumerState { late final bool hasFees; - void _onSendToAddressPasteButtonPressed() async { - final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); - if (data?.text != null && data!.text!.isNotEmpty) { - String content = data.text!.trim(); - if (content.contains("\n")) { - content = content.substring(0, content.indexOf("\n")); - } - - if (coin is Epiccash) { - // strip http:// and https:// if content contains @ - content = AddressUtils().formatEpicCashAddress(content); - } - - final trimmed = content.trim(); - final parsed = AddressUtils.parsePaymentUri( - trimmed, - logging: Logging.instance, - ); - if (parsed != null) { - _applyUri(parsed); - } else { - _setOpReturnData(null); - sendToController.text = content; - _address = content; - - _setValidAddressProviders(_address); - - setState(() { - _addressToggleFlag = sendToController.text.isNotEmpty; - }); - } - } - } - void _onFeeSelectPressed() { showModalBottomSheet( backgroundColor: Colors.transparent, @@ -1451,21 +1313,8 @@ class _SendViewState extends ConsumerState { _data = widget.autoFillData; walletId = widget.walletId; clipboard = widget.clipboard; - _isMasternodeCollateralUnshield = - MasternodeCollateralNotes.isUnshield(_data?.note) && isFiro; - _isMasternodeCollateralSelfSend = - (MasternodeCollateralNotes.isPrep(_data?.note) || - _isMasternodeCollateralUnshield) && - isFiro; WidgetsBinding.instance.addPostFrameCallback((_) { - if (_isMasternodeCollateralUnshield) { - ref.read(publicPrivateBalanceStateProvider.state).state = - BalanceType.private; - } else if (_isMasternodeCollateralSelfSend) { - ref.read(publicPrivateBalanceStateProvider.state).state = - BalanceType.public; - } ref.refresh(feeSheetSessionCacheProvider); ref.refresh(pIsExchangeAddress); }); @@ -1495,21 +1344,27 @@ class _SendViewState extends ConsumerState { baseAmountController.addListener(_baseAmountChanged); if (_data != null) { - if (_data.amount != null) { + final hasAmount = _data.amount != null; + if (hasAmount) { final amount = Amount.fromDecimal( _data.amount!, fractionDigits: coin.fractionDigits, ); + _cryptoAmountChangeLock = true; cryptoAmountController.text = ref .read(pAmountFormatter(coin)) .format(amount, withUnitName: false); + _cryptoAmountChangeLock = false; } sendToController.text = _data.contactLabel; _address = _data.address.trim(); _addressToggleFlag = true; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (hasAmount) { + _cryptoAmountChanged(); + } _setValidAddressProviders(_address); }); } @@ -1675,13 +1530,7 @@ class _SendViewState extends ConsumerState { backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( leading: AppBarBackButton( - onPressed: () { - if (_isMasternodeCollateralSelfSend) { - Navigator.of(context).pop(); - } else { - Navigator.of(context).pop(); - } - }, + onPressed: () => Navigator.of(context).pop(), ), title: Text( "Send ${coin.ticker}", @@ -2033,18 +1882,6 @@ class _SendViewState extends ConsumerState { child: const AddressBookIcon(), ), - if (_isMasternodeCollateralSelfSend) - TextFieldIconButton( - semanticsLabel: - "My addresses button. Opens your wallet addresses for collateral self-send.", - key: const Key( - "sendViewMyAddressesButtonKey", - ), - onTap: - _pickMyAddressForMasternodeCollateral, - child: - const AddressBookIcon(), - ), if (sendToController .text .isEmpty) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index b6619f9f8d..c14d35f440 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -96,6 +96,7 @@ import '../coin_control/coin_control_view.dart'; import '../epic_finalize_view/epic_finalize_view.dart'; import '../exchange_view/wallet_initiated_exchange_view.dart'; import '../finalize_view/finalize_view.dart'; +import '../masternodes/masternodes_home_view.dart'; import '../monkey/monkey_view.dart'; import '../more_view/gift_cards_view.dart'; import '../more_view/services_view.dart'; @@ -1207,27 +1208,27 @@ class _WalletViewState extends ConsumerState { ); }, ), - // if (!viewOnly && wallet is FiroWallet) - // WalletNavigationBarItemData( - // label: "Masternodes", - // icon: SvgPicture.asset( - // Assets.svg.recycle, - // height: 20, - // width: 20, - // colorFilter: ColorFilter.mode( - // Theme.of( - // context, - // ).extension()!.bottomNavIconIcon, - // BlendMode.srcIn, - // ), - // ), - // onTap: () { - // Navigator.of(context).pushNamed( - // MasternodesHomeView.routeName, - // arguments: widget.walletId, - // ); - // }, - // ), + if (!viewOnly && wallet is FiroWallet) + WalletNavigationBarItemData( + label: "Masternodes", + icon: SvgPicture.asset( + Assets.svg.recycle, + height: 20, + width: 20, + colorFilter: ColorFilter.mode( + Theme.of( + context, + ).extension()!.bottomNavIconIcon, + BlendMode.srcIn, + ), + ), + onTap: () { + Navigator.of(context).pushNamed( + MasternodesHomeView.routeName, + arguments: widget.walletId, + ); + }, + ), if (wallet is NamecoinWallet) WalletNavigationBarItemData( label: "Domains", diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 9388807794..5060d2bdb0 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -1246,12 +1246,27 @@ class _DesktopSendState extends ConsumerState { cryptoAmountController.addListener(onCryptoAmountChanged); if (_data != null) { - if (_data.amount != null) { - cryptoAmountController.text = _data.amount!.toString(); + final hasAmount = _data.amount != null; + if (hasAmount) { + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format( + _data.amount!.toAmount(fractionDigits: coin.fractionDigits), + withUnitName: false, + ); + _cryptoAmountChangeLock = false; } sendToController.text = _data.contactLabel; _address = _data.address; _addressToggleFlag = true; + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (hasAmount) { + _cryptoAmountChanged(); + } + _setValidAddressProviders(_address); + }); } if (isPaynymSend) { diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index f924ad43c2..4793f0ada3 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -505,8 +505,8 @@ class _DesktopWalletFeaturesState extends ConsumerState { if (wallet is SignVerifyInterface && !isViewOnly) (WalletFeature.sign, Assets.svg.pencil, _onSignPressed), - // if (!isViewOnly && wallet is FiroWallet) - // (WalletFeature.masternodes, Assets.svg.recycle, _onMasternodesPressed), + if (!isViewOnly && wallet is FiroWallet) + (WalletFeature.masternodes, Assets.svg.recycle, _onMasternodesPressed), if (showCoinControl) ( WalletFeature.coinControl,