diff --git a/.gitmodules b/.gitmodules index d9a561cc1c..d0c1810aa1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "crypto_plugins/frostdart"] path = crypto_plugins/frostdart url = https://github.com/cypherstack/frostdart +[submodule "crypto_plugins/flutter_libmwc"] + path = crypto_plugins/flutter_libmwc + url = https://github.com/cypherstack/flutter_libmwc diff --git a/README.md b/README.md index 4c18c3181a..286bb6f544 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Highlights include: - [Bitcoin Cash](https://bch.info/en/) - [Dogecoin](https://dogecoin.com/) - [Epic Cash](https://linktr.ee/epiccash) + - [MimbleWimbleCoin](https://mwc.mw) - [Ethereum](https://ethereum.org/en/) - [Firo](https://firo.org/) - [Litecoin](https://litecoin.org/) diff --git a/crypto_plugins/flutter_libmwc b/crypto_plugins/flutter_libmwc new file mode 160000 index 0000000000..a692b7d685 --- /dev/null +++ b/crypto_plugins/flutter_libmwc @@ -0,0 +1 @@ +Subproject commit a692b7d6859445791f6ba4270e4cf7e897506d78 diff --git a/lib/app_config.dart b/lib/app_config.dart index 09eeae26f7..5f9b37ab77 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -1,3 +1,6 @@ +// ignore: unused_import +import 'dart:io'; + import 'wallets/crypto_currency/crypto_currency.dart'; import 'wallets/crypto_currency/intermediate/frost_currency.dart'; diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index 3a2026785d..06d9646638 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -95,6 +95,11 @@ class TransactionV2 { bool get isEpiccashTransaction => _getFromOtherData(key: TxV2OdKeys.isEpiccashTransaction) == true; + + @ignore + bool get isMimblewimblecoinTransaction => + _getFromOtherData(key: TxV2OdKeys.isMimblewimblecoinTransaction) == true; + int? get numberOfMessages => _getFromOtherData(key: TxV2OdKeys.numberOfMessages) as int?; String? get slateId => _getFromOtherData(key: TxV2OdKeys.slateId) as String?; @@ -292,6 +297,74 @@ class TransactionV2 { } } + if (isMimblewimblecoinTransaction) { + if (slateId == null) { + return "Restored Funds"; + } + + if (isCancelled) { + return "Cancelled"; + } else if (type == TransactionType.incoming) { + if (isConfirmed(currentChainHeight, minConfirms, minCoinbaseConfirms)) { + return "Received"; + } else { + if (numberOfMessages == 1) { + return "Receiving (waiting for sender)"; + } else if ((numberOfMessages ?? 0) > 1) { + return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no) + } else { + return "Receiving ${prettyConfirms()}"; + } + } + } else if (type == TransactionType.outgoing) { + if (isConfirmed(currentChainHeight, minConfirms, minCoinbaseConfirms)) { + return "Sent (confirmed)"; + } else { + if (numberOfMessages == 1) { + return "Sending (waiting for receiver)"; + } else if ((numberOfMessages ?? 0) > 1) { + return "Sending (waiting for confirmations)"; + } else { + return "Sending ${prettyConfirms()}"; + } + } + } + } + + if (isMimblewimblecoinTransaction) { + if (slateId == null) { + return "Restored Funds"; + } + + if (isCancelled) { + return "Cancelled"; + } else if (type == TransactionType.incoming) { + if (isConfirmed(currentChainHeight, minConfirms, minCoinbaseConfirms)) { + return "Received"; + } else { + if (numberOfMessages == 1) { + return "Receiving (waiting for sender)"; + } else if ((numberOfMessages ?? 0) > 1) { + return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no) + } else { + return "Receiving ${prettyConfirms()}"; + } + } + } else if (type == TransactionType.outgoing) { + if (isConfirmed(currentChainHeight, minConfirms, minCoinbaseConfirms)) { + return "Sent (confirmed)"; + } else { + if (numberOfMessages == 1) { + return "Sending (waiting for receiver)"; + } else if ((numberOfMessages ?? 0) > 1) { + return "Sending (waiting for confirmations)"; + } else { + return "Sending ${prettyConfirms()}"; + } + } + } + } + if (type == TransactionType.incoming) { // if (_transaction.isMinting) { // return "Minting"; @@ -356,6 +429,7 @@ abstract final class TxV2OdKeys { static const size = "size"; static const vSize = "vSize"; static const isEpiccashTransaction = "isEpiccashTransaction"; + static const isMimblewimblecoinTransaction = "isMimblewimblecoinTransaction"; static const numberOfMessages = "numberOfMessages"; static const slateId = "slateId"; static const onChainNote = "onChainNote"; diff --git a/lib/models/isar/stack_theme.dart b/lib/models/isar/stack_theme.dart index efa2f7d05e..7c2f0b7ab0 100644 --- a/lib/models/isar/stack_theme.dart +++ b/lib/models/isar/stack_theme.dart @@ -1943,6 +1943,7 @@ class ThemeAssets implements IThemeAssets { late final String wownero; late final String namecoin; late final String particl; + late final String mimblewimblecoin; late final String bitcoinImage; late final String bitcoincashImage; late final String dogecoinImage; diff --git a/lib/models/mwc_slatepack_models.dart b/lib/models/mwc_slatepack_models.dart new file mode 100644 index 0000000000..8b5c05b832 --- /dev/null +++ b/lib/models/mwc_slatepack_models.dart @@ -0,0 +1,116 @@ +class SlatepackResult { + final bool success; + final String? error; + final String? slatepack; + final String? slateJson; + final bool? wasEncrypted; + final String? recipientAddress; + + SlatepackResult({ + required this.success, + this.error, + this.slatepack, + this.slateJson, + this.wasEncrypted, + this.recipientAddress, + }); + + @override + String toString() { + return "SlatepackResult(" + "success: $success, " + "error: $error, " + "slatepack: $slatepack, " + "slateJson: $slateJson, " + "wasEncrypted: $wasEncrypted, " + "recipientAddress: $recipientAddress" + ")"; + } +} + +class SlatepackDecodeResult { + final bool success; + final String? error; + final String? slateJson; + final bool? wasEncrypted; + final String? senderAddress; + final String? recipientAddress; + + SlatepackDecodeResult({ + required this.success, + this.error, + this.slateJson, + this.wasEncrypted, + this.senderAddress, + this.recipientAddress, + }); + + @override + String toString() { + return "SlatepackDecodeResult(" + "success: $success, " + "error: $error, " + "slateJson: $slateJson, " + "wasEncrypted: $wasEncrypted, " + "senderAddress: $senderAddress, " + "recipientAddress: $recipientAddress" + ")"; + } +} + +class ReceiveResult { + final bool success; + final String? error; + final String? slateId; + final String? commitId; + final String? responseSlatepack; + final bool? wasEncrypted; + final String? recipientAddress; + + ReceiveResult({ + required this.success, + this.error, + this.slateId, + this.commitId, + this.responseSlatepack, + this.wasEncrypted, + this.recipientAddress, + }); + + @override + String toString() { + return "ReceiveResult(" + "success: $success, " + "error: $error, " + "slateId: $slateId, " + "commitId: $commitId, " + "responseSlatepack: $responseSlatepack, " + "wasEncrypted: $wasEncrypted, " + "recipientAddress: $recipientAddress" + ")"; + } +} + +class FinalizeResult { + final bool success; + final String? error; + final String? slateId; + final String? commitId; + + FinalizeResult({ + required this.success, + this.error, + this.slateId, + this.commitId, + }); + + @override + String toString() { + return "FinalizeResult(" + "success: $success, " + "error: $error, " + "slateId: $slateId, " + "commitId: $commitId" + ")"; + } +} diff --git a/lib/models/mwcmqs_config_model.dart b/lib/models/mwcmqs_config_model.dart new file mode 100644 index 0000000000..067d1a8ee5 --- /dev/null +++ b/lib/models/mwcmqs_config_model.dart @@ -0,0 +1,89 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'dart:convert'; + +import 'package:hive/hive.dart'; + +import 'mwcmqs_server_model.dart'; + +part 'type_adaptors/mwcmqs_config_model.g.dart'; + +@HiveType(typeId: 82) +class MwcMqsConfigModel { + @HiveField(1) + final String host; + @HiveField(2) + final int? port; + + MwcMqsConfigModel({ + required this.host, + this.port + }); + + MwcMqsConfigModel copyWith({ + int? port, + bool? protocolInsecure, + }) { + return MwcMqsConfigModel( + host: host, + port: this.port ?? 443, + ); + } + + Map toMap() { + final Map map = {}; + map['mwcmqs_domain'] = host; + map['mwcmqs_port'] = port; + return map; + } + + Map toJson() { + return { + 'mwcmqs_domain': host, + 'mwcmqs_port': port, + }; + } + + @override + String toString() { + return json.encode(toJson()); + } + + static MwcMqsConfigModel fromString(String MwcMqsConfigString) { + final dynamic _mwcmqs = json.decode(MwcMqsConfigString); + + final oldDomain = _mwcmqs["domain"] ?? "empty"; + if (oldDomain != "empty") { + _mwcmqs['mwcmqs_domain'] = _mwcmqs['domain']; + } + final oldPort = _mwcmqs["port"] ?? "empty"; + if (oldPort != "empty") { + _mwcmqs['mwcmqs_port'] = _mwcmqs['port']; + } + + + return MwcMqsConfigModel( + host: _mwcmqs['mwcmqs_domain'] as String, + port: _mwcmqs['mwcmqs_port'] as int + ); + } + + static MwcMqsConfigModel fromServer( + MwcMqsServerModel server, { + bool? protocolInsecure, + int? addressIndex, + }) { + return MwcMqsConfigModel( + host: server.host, + port: server.port ?? 443 + ); + } +} diff --git a/lib/models/mwcmqs_server_model.dart b/lib/models/mwcmqs_server_model.dart new file mode 100644 index 0000000000..c6ffd757f9 --- /dev/null +++ b/lib/models/mwcmqs_server_model.dart @@ -0,0 +1,93 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:hive/hive.dart'; + +part 'type_adaptors/mwcmqs_server_model.g.dart'; + +@HiveType(typeId: 81) +class MwcMqsServerModel { + @HiveField(0) + final String id; + @HiveField(1) + final String host; + @HiveField(2) + final int? port; + @HiveField(3) + final String name; + @HiveField(4) + final bool? useSSL; + @HiveField(5) + final bool? enabled; + @HiveField(6) + final bool? isFailover; + @HiveField(7) + final bool? isDown; + + MwcMqsServerModel({ + required this.id, + required this.host, + this.port, + required this.name, + this.useSSL, + this.enabled, + this.isFailover, + this.isDown, + }); + + MwcMqsServerModel copyWith({ + String? host, + int? port, + String? name, + bool? useSSL, + bool? enabled, + bool? isFailover, + bool? isDown, + }) { + return MwcMqsServerModel( + id: id, + host: host ?? this.host, + port: port ?? this.port, + name: name ?? this.name, + useSSL: useSSL ?? this.useSSL, + enabled: enabled ?? this.enabled, + isFailover: isFailover ?? this.isFailover, + isDown: isDown ?? this.isDown, + ); + } + + Map toMap() { + final Map map = {}; + map['id'] = id; + map['host'] = host; + map['port'] = port; + map['name'] = name; + map['useSSL'] = useSSL; + map['enabled'] = enabled; + map['isFailover'] = isFailover; + map['isDown'] = isDown; + return map; + } + + bool get isDefault => id.startsWith("default_"); + + Map toJson() { + return { + 'id': id, + 'host': host, + 'port': port, + 'name': name, + 'useSSL': useSSL, + 'enabled': enabled, + 'isFailover': isFailover, + 'isDown': isDown, + }; + } +} diff --git a/lib/models/type_adaptors/mwcmqs_config_model.g.dart b/lib/models/type_adaptors/mwcmqs_config_model.g.dart new file mode 100644 index 0000000000..f66fbdb56b --- /dev/null +++ b/lib/models/type_adaptors/mwcmqs_config_model.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../mwcmqs_config_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class MwcMqsConfigModelAdapter extends TypeAdapter { + @override + final int typeId = 82; + + @override + MwcMqsConfigModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return MwcMqsConfigModel( + host: fields[1] as String, + port: fields[2] as int?, + ); + } + + @override + void write(BinaryWriter writer, MwcMqsConfigModel obj) { + writer + ..writeByte(2) + ..writeByte(1) + ..write(obj.host) + ..writeByte(2) + ..write(obj.port); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MwcMqsConfigModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/type_adaptors/mwcmqs_server_model.g.dart b/lib/models/type_adaptors/mwcmqs_server_model.g.dart new file mode 100644 index 0000000000..dabdd9464d --- /dev/null +++ b/lib/models/type_adaptors/mwcmqs_server_model.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of '../mwcmqs_server_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class MwcMqsServerModelAdapter extends TypeAdapter { + @override + final int typeId = 81; + + @override + MwcMqsServerModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return MwcMqsServerModel( + id: fields[0] as String, + host: fields[1] as String, + port: fields[2] as int?, + name: fields[3] as String, + useSSL: fields[4] as bool?, + enabled: fields[5] as bool?, + isFailover: fields[6] as bool?, + isDown: fields[7] as bool?, + ); + } + + @override + void write(BinaryWriter writer, MwcMqsServerModel obj) { + writer + ..writeByte(8) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.host) + ..writeByte(2) + ..write(obj.port) + ..writeByte(3) + ..write(obj.name) + ..writeByte(4) + ..write(obj.useSSL) + ..writeByte(5) + ..write(obj.enabled) + ..writeByte(6) + ..write(obj.isFailover) + ..writeByte(7) + ..write(obj.isDown); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MwcMqsServerModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart index 9d835ea4ef..0d7c53e8b0 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart @@ -27,7 +27,8 @@ import '../../../../../widgets/detail_item.dart'; import '../../../../../widgets/loading_indicator.dart'; import '../../../../../widgets/rounded_container.dart'; import '../../../../home_view/home_view.dart'; -import '../../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import '../../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart' + as tvd; class FrostCreateStep5 extends ConsumerStatefulWidget { const FrostCreateStep5({super.key}); @@ -90,7 +91,7 @@ class _FrostCreateStep5State extends ConsumerState { detail: multisigConfig, button: Util.isDesktop - ? IconCopyButton(data: multisigConfig) + ? tvd.IconCopyButton(data: multisigConfig) : SimpleCopyButton(data: multisigConfig), ), const SizedBox(height: 12), @@ -99,7 +100,7 @@ class _FrostCreateStep5State extends ConsumerState { detail: serializedKeys, button: Util.isDesktop - ? IconCopyButton(data: serializedKeys) + ? tvd.IconCopyButton(data: serializedKeys) : SimpleCopyButton(data: serializedKeys), ), if (!Util.isDesktop) const Spacer(), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 5cc90beba5..c230578940 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -419,7 +419,9 @@ class _SeedRestoreOptionState extends ConsumerState { return Column( children: [ - if (isCnAnd25 || widget.coin is Epiccash) + if (isCnAnd25 || + widget.coin is Epiccash || + widget.coin is Mimblewimblecoin) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -446,9 +448,13 @@ class _SeedRestoreOptionState extends ConsumerState { ), ], ), - if (isCnAnd25 || widget.coin is Epiccash) + if (isCnAnd25 || + widget.coin is Epiccash || + widget.coin is Mimblewimblecoin) SizedBox(height: Util.isDesktop ? 16 : 8), - if (isCnAnd25 || widget.coin is Epiccash) + if (isCnAnd25 || + widget.coin is Epiccash || + widget.coin is Mimblewimblecoin) ref.watch(_pIsUsingDate) ? RestoreFromDatePicker( onTap: widget.dateChooserFunction, @@ -505,29 +511,35 @@ class _SeedRestoreOptionState extends ConsumerState { ), ), ), - if (isCnAnd25 || widget.coin is Epiccash) const SizedBox(height: 8), - if (isCnAnd25 || widget.coin is Epiccash) - RoundedWhiteContainer( - child: Center( - child: Text( - ref.watch(_pIsUsingDate) - ? "Choose the date you made the wallet (approximate is fine)" - : "Enter the initial block height of the wallet", - style: - Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textSubtitle1, - ) - : STextStyles.smallMed12( - context, - ).copyWith(fontSize: 10), - ), + if (isCnAnd25 || + widget.coin is Epiccash || + widget.coin is Mimblewimblecoin) + const SizedBox(height: 8), + if (isCnAnd25 || + widget.coin is Epiccash || + widget.coin is Mimblewimblecoin) + SizedBox(height: Util.isDesktop ? 16 : 8), + RoundedWhiteContainer( + child: Center( + child: Text( + ref.watch(_pIsUsingDate) + ? "Choose the date you made the wallet (approximate is fine)" + : "Enter the initial block height of the wallet", + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ) + : STextStyles.smallMed12(context).copyWith(fontSize: 10), ), ), - if (isCnAnd25 || widget.coin is Epiccash) + ), + if (isCnAnd25 || + widget.coin is Epiccash || + widget.coin is Mimblewimblecoin) SizedBox(height: Util.isDesktop ? 24 : 16), Text( "Choose recovery phrase length", diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart index a473d83ecc..e0d3871c42 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart @@ -25,6 +25,7 @@ import '../../../wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; import '../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart'; import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; +import '../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../wallets/wallet/impl/wownero_wallet.dart'; import '../../../wallets/wallet/impl/xelis_wallet.dart'; @@ -219,6 +220,10 @@ class _RestoreViewOnlyWalletViewState case const (EpiccashWallet): await (wallet as EpiccashWallet).init(isRestore: true); break; + + case const (MimblewimblecoinWallet): + await (wallet as MimblewimblecoinWallet).init(isRestore: true); + break; case const (MoneroWallet): await (wallet as MoneroWallet).init(isRestore: true); diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index aeb9ff845e..dceb76d80d 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -44,12 +44,14 @@ import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; +import '../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../wallets/wallet/impl/salvium_wallet.dart'; import '../../../wallets/wallet/impl/wownero_wallet.dart'; import '../../../wallets/wallet/impl/xelis_wallet.dart'; import '../../../wallets/wallet/intermediate/external_wallet.dart'; import '../../../wallets/wallet/supporting/epiccash_wallet_info_extension.dart'; +import '../../../wallets/wallet/supporting/mimblewimblecoin_wallet_info_extension.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/desktop/desktop_app_bar.dart'; @@ -224,7 +226,7 @@ class _RestoreWalletViewState extends ConsumerState { } mnemonic = mnemonic.trim(); - final int height = widget.restoreBlockHeight; + int height = widget.restoreBlockHeight; String? otherDataJsonString; // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index @@ -242,6 +244,34 @@ class _RestoreWalletViewState extends ConsumerState { ).toMap(), ), }); + } else if (widget.coin is Mimblewimblecoin) { + // final int secondsSinceEpoch = + // widget.restoreFromDate!.millisecondsSinceEpoch ~/ 1000; + final int secondsSinceEpoch = + DateTime.now().millisecondsSinceEpoch ~/ 1000; + const int mimblewimblecoinFirstBlock = 1573462801; + const double overestimateSecondsPerBlock = 61; + final int chosenSeconds = + secondsSinceEpoch - mimblewimblecoinFirstBlock; + final int approximateHeight = + chosenSeconds ~/ overestimateSecondsPerBlock; + height = approximateHeight; + if (height < 0) { + height = 0; + } + otherDataJsonString = jsonEncode({ + WalletInfoKeys.mimblewimblecoinData: jsonEncode( + ExtraMimblewimblecoinWalletInfo( + receivingIndex: 0, + changeIndex: 0, + slatesToAddresses: {}, + slatesToCommits: {}, + lastScannedBlock: height, + restoreHeight: height, + creationHeight: height, + ).toMap(), + ), + }); } // TODO: do actual check to make sure it is a valid mnemonic for monero + xelis @@ -325,6 +355,10 @@ class _RestoreWalletViewState extends ConsumerState { await (wallet as EpiccashWallet).init(isRestore: true); break; + case const (MimblewimblecoinWallet): + await (wallet as MimblewimblecoinWallet).init(isRestore: true); + break; + case const (MoneroWallet): await (wallet as MoneroWallet).init(isRestore: true); break; diff --git a/lib/pages/coin_control/utxo_details_view.dart b/lib/pages/coin_control/utxo_details_view.dart index 9959abdda6..3e4a93670a 100644 --- a/lib/pages/coin_control/utxo_details_view.dart +++ b/lib/pages/coin_control/utxo_details_view.dart @@ -35,7 +35,7 @@ import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/desktop/secondary_button.dart'; import '../../widgets/icon_widgets/utxo_status_icon.dart'; import '../../widgets/rounded_container.dart'; -import '../wallet_view/transaction_views/transaction_details_view.dart'; +import '../wallet_view/transaction_views/transaction_details_view.dart' as tdv; class UtxoDetailsView extends ConsumerStatefulWidget { const UtxoDetailsView({ @@ -83,10 +83,11 @@ class _UtxoDetailsViewState extends ConsumerState { @override void initState() { - utxo = MainDB.instance.isar.utxos - .where() - .idEqualTo(widget.utxoId) - .findFirstSync()!; + utxo = + MainDB.instance.isar.utxos + .where() + .idEqualTo(widget.utxoId) + .findFirstSync()!; streamUTXO = MainDB.instance.watchUTXO(id: widget.utxoId); @@ -112,53 +113,50 @@ class _UtxoDetailsViewState extends ConsumerState { final confirmed = _isConfirmed( utxo!, currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet(widget.walletId), - ), - ), + ref.watch(pWallets.select((s) => s.getWallet(widget.walletId))), ); return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(_popWithRefresh ? "refresh" : null); - }, - ), - title: Text( - "Output details", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: child, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () { + Navigator.of( + context, + ).pop(_popWithRefresh ? "refresh" : null); + }, + ), + title: Text( + "Output details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), + ), ), - ), - ), - ); - }, + ); + }, + ), + ), ), ), - ), - ), child: StreamBuilder( stream: streamUTXO, builder: (context, snapshot) { @@ -184,8 +182,9 @@ class _UtxoDetailsViewState extends ConsumerState { ), DesktopDialogCloseButton( onPressedOverride: () { - Navigator.of(context) - .pop(_popWithRefresh ? "refresh" : null); + Navigator.of( + context, + ).pop(_popWithRefresh ? "refresh" : null); }, ), ], @@ -204,15 +203,14 @@ class _UtxoDetailsViewState extends ConsumerState { child: RoundedContainer( padding: EdgeInsets.zero, color: Colors.transparent, - borderColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + borderColor: + Theme.of(context) + .extension()! + .textFieldDefaultBG, child: child, ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), SecondaryButton( buttonHeight: ButtonHeight.l, label: utxo!.isBlocked ? "Unfreeze" : "Freeze", @@ -229,15 +227,13 @@ class _UtxoDetailsViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (!isDesktop) - const SizedBox( - height: 10, - ), + if (!isDesktop) const SizedBox(height: 10), RoundedContainer( padding: const EdgeInsets.all(12), - color: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.popupBG, + color: + isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -246,22 +242,23 @@ class _UtxoDetailsViewState extends ConsumerState { if (isDesktop) UTXOStatusIcon( blocked: utxo!.isBlocked, - status: confirmed - ? UTXOStatusIconStatus.confirmed - : UTXOStatusIconStatus.unconfirmed, - background: Theme.of(context) - .extension()! - .popupBG, + status: + confirmed + ? UTXOStatusIconStatus.confirmed + : UTXOStatusIconStatus.unconfirmed, + background: + Theme.of( + context, + ).extension()!.popupBG, selected: false, width: 32, height: 32, ), - if (isDesktop) - const SizedBox( - width: 16, - ), + if (isDesktop) const SizedBox(width: 16), Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( utxo!.value.toAmountAsRaw( fractionDigits: coin.fractionDigits, ), @@ -274,18 +271,19 @@ class _UtxoDetailsViewState extends ConsumerState { utxo!.isBlocked ? "Frozen" : confirmed - ? "Available" - : "Unconfirmed", + ? "Available" + : "Unconfirmed", style: STextStyles.w500_14(context).copyWith( - color: utxo!.isBlocked - ? const Color(0xFF7FA2D4) // todo theme - : confirmed - ? Theme.of(context) - .extension()! - .accentColorGreen - : Theme.of(context) - .extension()! - .accentColorYellow, + color: + utxo!.isBlocked + ? const Color(0xFF7FA2D4) // todo theme + : confirmed + ? Theme.of( + context, + ).extension()!.accentColorGreen + : Theme.of( + context, + ).extension()!.accentColorYellow, ), ), ], @@ -293,12 +291,14 @@ class _UtxoDetailsViewState extends ConsumerState { ), const _Div(), RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.popupBG, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -309,9 +309,10 @@ class _UtxoDetailsViewState extends ConsumerState { Text( "Label", style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), SimpleEditButton( @@ -319,32 +320,27 @@ class _UtxoDetailsViewState extends ConsumerState { editLabel: "label", onValueChanged: (newName) { MainDB.instance.putUTXO( - utxo!.copyWith( - name: newName, - ), + utxo!.copyWith(name: newName), ); }, ), ], ), - const SizedBox( - height: 4, - ), - Text( - utxo!.name, - style: STextStyles.w500_14(context), - ), + const SizedBox(height: 4), + Text(utxo!.name, style: STextStyles.w500_14(context)), ], ), ), const _Div(), RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.popupBG, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -355,39 +351,35 @@ class _UtxoDetailsViewState extends ConsumerState { Text( "Address", style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), isDesktop - ? IconCopyButton( - data: utxo!.address!, - ) - : SimpleCopyButton( - data: utxo!.address!, - ), + ? tdv.IconCopyButton(data: utxo!.address!) + : SimpleCopyButton(data: utxo!.address!), ], ), - const SizedBox( - height: 4, - ), - Text( - utxo!.address!, - style: STextStyles.w500_14(context), - ), + const SizedBox(height: 4), + Text(utxo!.address!, style: STextStyles.w500_14(context)), ], ), ), if (label != null && label!.value.isNotEmpty) const _Div(), if (label != null && label!.value.isNotEmpty) RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.popupBG, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + isDesktop + ? Colors.transparent + : Theme.of( + context, + ).extension()!.popupBG, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -398,38 +390,32 @@ class _UtxoDetailsViewState extends ConsumerState { Text( "Address label", style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), isDesktop - ? IconCopyButton( - data: label!.value, - ) - : SimpleCopyButton( - data: label!.value, - ), + ? tdv.IconCopyButton(data: label!.value) + : SimpleCopyButton(data: label!.value), ], ), - const SizedBox( - height: 4, - ), - Text( - label!.value, - style: STextStyles.w500_14(context), - ), + const SizedBox(height: 4), + Text(label!.value, style: STextStyles.w500_14(context)), ], ), ), const _Div(), RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.popupBG, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -440,38 +426,32 @@ class _UtxoDetailsViewState extends ConsumerState { Text( "Transaction ID", style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), isDesktop - ? IconCopyButton( - data: utxo!.txid, - ) - : SimpleCopyButton( - data: utxo!.txid, - ), + ? tdv.IconCopyButton(data: utxo!.txid) + : SimpleCopyButton(data: utxo!.txid), ], ), - const SizedBox( - height: 4, - ), - Text( - utxo!.txid, - style: STextStyles.w500_14(context), - ), + const SizedBox(height: 4), + Text(utxo!.txid, style: STextStyles.w500_14(context)), ], ), ), const _Div(), RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.popupBG, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -479,14 +459,13 @@ class _UtxoDetailsViewState extends ConsumerState { Text( "Confirmations", style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( "${utxo!.getConfirmations(currentHeight)}", style: STextStyles.w500_14(context), @@ -501,14 +480,16 @@ class _UtxoDetailsViewState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: isDesktop - ? Colors.transparent - : Theme.of(context) - .extension()! - .popupBG, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + isDesktop + ? Colors.transparent + : Theme.of( + context, + ).extension()!.popupBG, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -519,9 +500,10 @@ class _UtxoDetailsViewState extends ConsumerState { Text( "Freeze reason", style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of(context) + .extension()! + .textSubtitle1, ), ), SimpleEditButton( @@ -529,17 +511,13 @@ class _UtxoDetailsViewState extends ConsumerState { editLabel: "freeze reason", onValueChanged: (newReason) { MainDB.instance.putUTXO( - utxo!.copyWith( - blockedReason: newReason, - ), + utxo!.copyWith(blockedReason: newReason), ); }, ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( utxo!.blockedReason ?? "", style: STextStyles.w500_14(context), @@ -556,10 +534,7 @@ class _UtxoDetailsViewState extends ConsumerState { label: utxo!.isBlocked ? "Unfreeze" : "Freeze", onPressed: _toggleFreeze, ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), ], ), ); @@ -581,9 +556,7 @@ class _Div extends StatelessWidget { color: Theme.of(context).extension()!.textFieldDefaultBG, ); } else { - return const SizedBox( - height: 12, - ); + return const SizedBox(height: 12); } } } diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 6e56e57553..39623c3d84 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -57,7 +57,7 @@ import '../../widgets/rounded_container.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_dialog.dart'; import '../wallet_view/transaction_views/edit_note_view.dart'; -import '../wallet_view/transaction_views/transaction_details_view.dart'; +import '../wallet_view/transaction_views/transaction_details_view.dart' as tdv; import 'edit_trade_note_view.dart'; import 'send_from_view.dart'; @@ -518,7 +518,7 @@ class _TradeDetailsViewState extends ConsumerState { ), ], ), - IconCopyButton(data: trade.payInAmount), + tdv.IconCopyButton(data: trade.payInAmount), ], ), const SizedBox(height: 6), @@ -604,20 +604,20 @@ class _TradeDetailsViewState extends ConsumerState { maxHeight: MediaQuery.of(context).size.height - 64, maxWidth: 580, - child: TransactionDetailsView( + child: tdv.TransactionDetailsView( coin: coin, transaction: transactionIfSentFromStack!, walletId: walletId!, ), ), const RouteSettings( - name: TransactionDetailsView.routeName, + name: tdv.TransactionDetailsView.routeName, ), ), ); } else { Navigator.of(context).pushNamed( - TransactionDetailsView.routeName, + tdv.TransactionDetailsView.routeName, arguments: Tuple3( transactionIfSentFromStack!, coin, @@ -664,7 +664,7 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - if (isDesktop) IconCopyButton(data: trade.payInAddress), + if (isDesktop) tdv.IconCopyButton(data: trade.payInAddress), ], ), ), @@ -687,7 +687,7 @@ class _TradeDetailsViewState extends ConsumerState { style: STextStyles.itemSubtitle(context), ), isDesktop - ? IconCopyButton(data: trade.payInAddress) + ? tdv.IconCopyButton(data: trade.payInAddress) : GestureDetector( onTap: () async { final address = trade.payInAddress; @@ -840,7 +840,7 @@ class _TradeDetailsViewState extends ConsumerState { children: [ Text("Memo", style: STextStyles.itemSubtitle(context)), isDesktop - ? IconCopyButton(data: trade.payInExtraId) + ? tdv.IconCopyButton(data: trade.payInExtraId) : GestureDetector( onTap: () async { final address = trade.payInExtraId; @@ -904,7 +904,7 @@ class _TradeDetailsViewState extends ConsumerState { style: STextStyles.itemSubtitle(context), ), isDesktop - ? IconPencilButton( + ? tdv.IconPencilButton( onPressed: () { showDialog( context: context, @@ -984,7 +984,7 @@ class _TradeDetailsViewState extends ConsumerState { style: STextStyles.itemSubtitle(context), ), isDesktop - ? IconPencilButton( + ? tdv.IconPencilButton( onPressed: () { showDialog( context: context, @@ -1088,7 +1088,7 @@ class _TradeDetailsViewState extends ConsumerState { style: STextStyles.itemSubtitle12(context), ), if (isDesktop) - IconCopyButton( + tdv.IconCopyButton( data: Format.extractDateFrom( trade.timestamp.millisecondsSinceEpoch ~/ 1000, ), @@ -1121,7 +1121,7 @@ class _TradeDetailsViewState extends ConsumerState { ), ], ), - if (isDesktop) IconCopyButton(data: trade.exchangeName), + if (isDesktop) tdv.IconCopyButton(data: trade.exchangeName), if (!isDesktop) SelectableText( trade.exchangeName, @@ -1155,7 +1155,7 @@ class _TradeDetailsViewState extends ConsumerState { ), ], ), - if (isDesktop) IconCopyButton(data: trade.tradeId), + if (isDesktop) tdv.IconCopyButton(data: trade.tradeId), if (!isDesktop) Row( children: [ diff --git a/lib/pages/finalize_view/finalize_view.dart b/lib/pages/finalize_view/finalize_view.dart new file mode 100644 index 0000000000..e0a3415907 --- /dev/null +++ b/lib/pages/finalize_view/finalize_view.dart @@ -0,0 +1,340 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../notifications/show_flush_bar.dart'; +import '../../providers/global/barcode_scanner_provider.dart'; +import '../../providers/global/wallets_provider.dart'; +import '../../themes/stack_colors.dart'; +import '../../utilities/barcode_scanner_interface.dart'; +import '../../utilities/clipboard_interface.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/logger.dart'; +import '../../utilities/show_loading.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/desktop/primary_button.dart'; +import '../../widgets/icon_widgets/clipboard_icon.dart'; +import '../../widgets/icon_widgets/qrcode_icon.dart'; +import '../../widgets/icon_widgets/x_icon.dart'; +import '../../widgets/stack_dialog.dart'; +import '../../widgets/stack_text_field.dart'; +import '../../widgets/textfield_icon_button.dart'; + +class FinalizeView extends ConsumerStatefulWidget { + const FinalizeView({ + super.key, + required this.walletId, + this.clipboard = const ClipboardWrapper(), + }); + + static const String routeName = "/finalizeView"; + + final String walletId; + final ClipboardInterface clipboard; + + @override + ConsumerState createState() => _FinalizeViewState(); +} + +class _FinalizeViewState extends ConsumerState { + late final TextEditingController _slateController; + late final FocusNode _slateFocusNode; + + bool _slateToggleFlag = false; + + Future _pasteSlatepack() async { + final ClipboardData? data = await widget.clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && data!.text!.isNotEmpty) { + _slateController.text = data.text!; + setState(() { + _slateToggleFlag = _slateController.text.isNotEmpty; + }); + } + } + + Future _scanQr() async { + try { + if (!Util.isDesktop && _slateFocusNode.hasFocus) { + _slateFocusNode.unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + + if (mounted) { + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); + if (qrResult.rawContent.isNotEmpty && qrResult.rawContent != "null") { + _slateController.text = qrResult.rawContent; + setState(() { + _slateToggleFlag = _slateController.text.isNotEmpty; + }); + } + } + } on PlatformException catch (e, s) { + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Failed to get camera permissions while trying to scan qr code in SendView: ", + error: e, + stackTrace: s, + ); + } + } + } + + Future _finalize() async { + // add delay for showloading exception catching hack fix + await Future.delayed(const Duration(seconds: 1)); + + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as MimblewimblecoinWallet; + + final decoded = await wallet.decodeSlatepack(_slateController.text); + if (!decoded.success) { + throw Exception(decoded.error ?? "Failed to decode slatepack"); + } + + final analysis = await wallet.analyzeSlatepack(_slateController.text); + if (analysis.status != "S2") { + throw Exception("Invalid slatepack type: ${analysis.status}"); + } + + final result = await wallet.finalizeSlatepack(_slateController.text); + + if (!result.success) { + throw Exception( + result.error ?? "Finalize failed without providing an error???", + ); + } + } + + Future _finalizePressed() async { + if (!Util.isDesktop && _slateFocusNode.hasFocus) { + _slateFocusNode.unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Exception? ex; + await showLoading( + whileFuture: _finalize(), + context: context, + message: "Finalizing slatepack...", + rootNavigator: Util.isDesktop, + onException: (e) => ex = e, + ); + + if (mounted) { + if (ex != null) { + await showDialog( + context: context, + builder: + (context) => StackOkDialog( + desktopPopRootNavigator: Util.isDesktop, + title: "Slatepack finalize error", + message: + ex?.toString() ?? "Unexpected result without exception", + maxWidth: Util.isDesktop ? 400 : null, + ), + ); + } else { + setState(() { + _slateController.text = ""; + }); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Transaction finalized and broadcast successfully!", + context: context, + ), + ); + } + } + } + } + + @override + void initState() { + super.initState(); + _slateController = TextEditingController(); + _slateFocusNode = FocusNode(); + } + + @override + void dispose() { + _slateController.dispose(); + _slateFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + return ConditionalParent( + condition: !Util.isDesktop, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Finalize slatepack", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: Constants.size.standardPadding, + ), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 5, + key: const Key("finalizeSlatepackFieldKey"), + controller: _slateController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + setState(() { + _slateToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _slateFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter Final Slatepack Message", + _slateFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, // Adjust vertical padding for better alignment + ), + suffixIcon: Padding( + padding: + _slateController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _slateToggleFlag + ? TextFieldIconButton( + key: const Key( + "slateFinalizeClearFieldButtonKey", + ), + onTap: () { + _slateController.text = ""; + setState(() { + _slateToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "slateFinalizePasteFieldButtonKey", + ), + onTap: _pasteSlatepack, + child: + _slateController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_slateController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("sendViewScanQrButtonKey"), + onTap: _scanQr, + child: const QrCodeIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + Util.isDesktop ? const SizedBox(height: 24) : const Spacer(), + PrimaryButton( + label: "Finalize Slatepack", + enabled: _slateToggleFlag, + onPressed: _slateToggleFlag ? _finalizePressed : null, + ), + + if (!Util.isDesktop) SizedBox(height: Constants.size.standardPadding), + ], + ), + ); + } +} diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart index 4beb066630..223c3d9c14 100644 --- a/lib/pages/home_view/home_view.dart +++ b/lib/pages/home_view/home_view.dart @@ -57,6 +57,9 @@ class HomeView extends ConsumerStatefulWidget { class _HomeViewState extends ConsumerState { final GlobalKey _key = GlobalKey(); + // keep reference to be able to remove listener in dispose without requiring the riverpod ref + late final Prefs _prefs; + late final PageController _pageController; late final RotateIconController _rotateIconController; @@ -209,6 +212,7 @@ class _HomeViewState extends ConsumerState { @override void initState() { + _prefs = ref.read(prefsChangeNotifierProvider); _autoLockInfo = ref.read(prefsChangeNotifierProvider).autoLockInfo; if (_autoLockInfo.enabled) { _idleMonitor = IdleMonitor( @@ -242,7 +246,7 @@ class _HomeViewState extends ConsumerState { @override dispose() { - ref.read(prefsChangeNotifierProvider).removeListener(_prefsTimeoutListener); + _prefs.removeListener(_prefsTimeoutListener); _idleMonitor?.detach(); _pageController.dispose(); _rotateIconController.forward = null; diff --git a/lib/pages/namecoin_names/sub_widgets/name_details.dart b/lib/pages/namecoin_names/sub_widgets/name_details.dart index 7fa77f807f..d16ca0b1ac 100644 --- a/lib/pages/namecoin_names/sub_widgets/name_details.dart +++ b/lib/pages/namecoin_names/sub_widgets/name_details.dart @@ -23,7 +23,8 @@ import '../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/dialogs/s_dialog.dart'; import '../../../widgets/rounded_container.dart'; -import '../../wallet_view/transaction_views/transaction_details_view.dart'; +import '../../wallet_view/transaction_views/transaction_details_view.dart' + as tdv; import '../manage_domain_view.dart'; import 'transfer_option_widget.dart'; import 'update_option_widget.dart'; @@ -60,8 +61,10 @@ class _ManageDomainsWidgetState extends ConsumerState { final data = jsonDecode(utxo.otherData!) as Map; final nameData = jsonDecode(data["nameOpData"] as String) as Map; - opNameData = - OpNameData(nameData.cast(), utxo.blockHeight ?? currentHeight); + opNameData = OpNameData( + nameData.cast(), + utxo.blockHeight ?? currentHeight, + ); _setName(); } @@ -76,32 +79,29 @@ class _ManageDomainsWidgetState extends ConsumerState { ref .read(secureStoreProvider) .read( - key: nameSaltKeyBuilder( - utxo!.txid, - widget.walletId, - utxo!.vout, - ), + key: nameSaltKeyBuilder(utxo!.txid, widget.walletId, utxo!.vout), ) .then((onValue) { - if (onValue != null) { - final data = (jsonDecode(onValue) as Map).cast(); - WidgetsBinding.instance.addPostFrameCallback((_) { - constructedName = data["name"]!; - value = data["value"]!; - if (mounted) { - setState(() {}); - } - }); - } else { - WidgetsBinding.instance.addPostFrameCallback((_) { - constructedName = "UNKNOWN"; - value = ""; - if (mounted) { - setState(() {}); + if (onValue != null) { + final data = + (jsonDecode(onValue) as Map).cast(); + WidgetsBinding.instance.addPostFrameCallback((_) { + constructedName = data["name"]!; + value = data["value"]!; + if (mounted) { + setState(() {}); + } + }); + } else { + WidgetsBinding.instance.addPostFrameCallback((_) { + constructedName = "UNKNOWN"; + value = ""; + if (mounted) { + setState(() {}); + } + }); } }); - } - }); } } } @@ -114,10 +114,7 @@ class _ManageDomainsWidgetState extends ConsumerState { message = "Expires in $blocksNameExpiration+ blocks"; color = theme.accentColorGreen; } else { - final remaining = opNameData?.expiredBlockLeft( - currentChainHeight, - false, - ); + final remaining = opNameData?.expiredBlockLeft(currentChainHeight, false); final semiRemaining = opNameData?.expiredBlockLeft( currentChainHeight, true, @@ -141,10 +138,7 @@ class _ManageDomainsWidgetState extends ConsumerState { bool _checkConfirmedUtxo(int currentHeight) { return (ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet) - .checkUtxoConfirmed( - utxo!, - currentHeight, - ); + .checkUtxoConfirmed(utxo!, currentHeight); } @override @@ -165,10 +159,9 @@ class _ManageDomainsWidgetState extends ConsumerState { _setName(); if (utxo?.address != null) { - label = ref.read(mainDBProvider).getAddressLabelSync( - widget.walletId, - utxo!.address!, - ); + label = ref + .read(mainDBProvider) + .getAddressLabelSync(widget.walletId, utxo!.address!); if (label != null) { streamLabel = ref.read(mainDBProvider).watchAddressLabel(id: label!.id); @@ -187,67 +180,73 @@ class _ManageDomainsWidgetState extends ConsumerState { Theme.of(context).extension()!, ); - final canManage = utxo != null && + final canManage = + utxo != null && _checkConfirmedUtxo(currentHeight) && (opNameData?.op == OpName.nameUpdate || opNameData?.op == OpName.nameFirstUpdate); return ConditionalParent( condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - backgroundColor: Colors.transparent, - // Theme.of(context).extension()!.background, - leading: const AppBarBackButton(), - title: Text( - "Domain details", - style: STextStyles.navBarTitle(context), - ), - actions: canManage - ? [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: CustomTextButton( - key: const Key("addAddressBookEntryFavoriteButtonKey"), - text: "Manage", - onTap: () { - Navigator.of(context).pushNamed( - ManageDomainView.routeName, - arguments: (walletId: widget.walletId, utxo: utxo!), - ); - }, - ), - ), - ] - : null, - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: child, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: Colors.transparent, + // Theme.of(context).extension()!.background, + leading: const AppBarBackButton(), + title: Text( + "Domain details", + style: STextStyles.navBarTitle(context), + ), + actions: + canManage + ? [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: CustomTextButton( + key: const Key( + "addAddressBookEntryFavoriteButtonKey", + ), + text: "Manage", + onTap: () { + Navigator.of(context).pushNamed( + ManageDomainView.routeName, + arguments: ( + walletId: widget.walletId, + utxo: utxo!, + ), + ); + }, + ), + ), + ] + : null, + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), + ), ), - ), - ), - ); - }, + ); + }, + ), + ), ), ), - ), - ), child: ConditionalParent( condition: Util.isDesktop, builder: (child) { @@ -278,9 +277,10 @@ class _ManageDomainsWidgetState extends ConsumerState { child: RoundedContainer( padding: EdgeInsets.zero, color: Colors.transparent, - borderColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + borderColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: child, ), ), @@ -341,9 +341,7 @@ class _ManageDomainsWidgetState extends ConsumerState { }, ), ), - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), Expanded( child: SecondaryButton( label: "Update", @@ -398,10 +396,7 @@ class _ManageDomainsWidgetState extends ConsumerState { ], ), ), - if (canManage) - const SizedBox( - height: 32, - ), + if (canManage) const SizedBox(height: 32), ], ), ); @@ -415,193 +410,148 @@ class _ManageDomainsWidgetState extends ConsumerState { return utxo == null ? Center( - child: Text( - "Missing output. Was it used recently?", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorRed, - ), + child: Text( + "Missing output. Was it used recently?", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.accentColorRed, ), - ) + ), + ) : Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // if (!isDesktop) - // const SizedBox( - // height: 10, - // ), - RoundedContainer( - padding: const EdgeInsets.all(12), - color: Util.isDesktop - ? Colors.transparent - : Theme.of(context) - .extension()! - .popupBG, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - constructedName ?? "", - style: STextStyles.pageTitleH2(context), - ), - if (Util.isDesktop) - SelectableText( - opNameData!.op.name, - style: STextStyles.w500_14(context), - ), - ], - ), - if (!Util.isDesktop) + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // if (!isDesktop) + // const SizedBox( + // height: 10, + // ), + RoundedContainer( + padding: const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of( + context, + ).extension()!.popupBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ SelectableText( - opNameData!.op.name, - style: STextStyles.w500_14(context), + constructedName ?? "", + style: STextStyles.pageTitleH2(context), ), - ], - ), - ), - const _Div(), - RoundedContainer( - padding: Util.isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: Util.isDesktop - ? Colors.transparent - : Theme.of(context) - .extension()! - .popupBG, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Value", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), + if (Util.isDesktop) + SelectableText( + opNameData!.op.name, + style: STextStyles.w500_14(context), ), - ], - ), - const SizedBox( - height: 4, - ), + ], + ), + if (!Util.isDesktop) SelectableText( - value ?? "", + opNameData!.op.name, style: STextStyles.w500_14(context), ), - ], - ), + ], ), - const _Div(), - RoundedContainer( - padding: Util.isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: Util.isDesktop - ? Colors.transparent - : Theme.of(context) - .extension()! - .popupBG, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Address", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), + ), + const _Div(), + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of( + context, + ).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Value", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), - Util.isDesktop - ? IconCopyButton( - data: utxo!.address!, - ) - : SimpleCopyButton( - data: utxo!.address!, - ), - ], - ), - const SizedBox( - height: 4, - ), - SelectableText( - utxo!.address!, - style: STextStyles.w500_14(context), - ), - ], - ), + ), + ], + ), + const SizedBox(height: 4), + SelectableText( + value ?? "", + style: STextStyles.w500_14(context), + ), + ], ), - if (label != null && label!.value.isNotEmpty) - const _Div(), - if (label != null && label!.value.isNotEmpty) - RoundedContainer( - padding: Util.isDesktop + ), + const _Div(), + RoundedContainer( + padding: + Util.isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), - color: Util.isDesktop + color: + Util.isDesktop ? Colors.transparent - : Theme.of(context) - .extension()! - .popupBG, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + : Theme.of( + context, + ).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Address label", - style: - STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - Util.isDesktop - ? IconCopyButton( - data: label!.value, - ) - : SimpleCopyButton( - data: label!.value, - ), - ], - ), - const SizedBox( - height: 4, - ), - SelectableText( - label!.value, - style: STextStyles.w500_14(context), + Text( + "Address", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), ), + Util.isDesktop + ? tdv.IconCopyButton(data: utxo!.address!) + : SimpleCopyButton(data: utxo!.address!), ], ), - ), - const _Div(), + const SizedBox(height: 4), + SelectableText( + utxo!.address!, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + if (label != null && label!.value.isNotEmpty) const _Div(), + if (label != null && label!.value.isNotEmpty) RoundedContainer( - padding: Util.isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: Util.isDesktop - ? Colors.transparent - : Theme.of(context) - .extension()! - .popupBG, + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of( + context, + ).extension()!.popupBG, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -610,100 +560,138 @@ class _ManageDomainsWidgetState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Transaction ID", + "Address label", style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of(context) + .extension()! + .textSubtitle1, ), ), Util.isDesktop - ? IconCopyButton( - data: utxo!.txid, - ) - : SimpleCopyButton( - data: utxo!.txid, - ), + ? tdv.IconCopyButton(data: label!.value) + : SimpleCopyButton(data: label!.value), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( - utxo!.txid, + label!.value, style: STextStyles.w500_14(context), ), ], ), ), - const _Div(), - RoundedContainer( - padding: Util.isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: Util.isDesktop - ? Colors.transparent - : Theme.of(context) - .extension()! - .popupBG, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Expiry", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - const SizedBox( - height: 4, - ), - SelectableText( - message, - style: STextStyles.w500_14(context).copyWith( - color: color, + const _Div(), + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of( + context, + ).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction ID", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), ), - ), - ], - ), + Util.isDesktop + ? tdv.IconCopyButton(data: utxo!.txid) + : SimpleCopyButton(data: utxo!.txid), + ], + ), + const SizedBox(height: 4), + SelectableText( + utxo!.txid, + style: STextStyles.w500_14(context), + ), + ], ), - const _Div(), - RoundedContainer( - padding: Util.isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: Util.isDesktop - ? Colors.transparent - : Theme.of(context) - .extension()! - .popupBG, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Confirmations", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - const SizedBox( - height: 4, + ), + const _Div(), + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of( + context, + ).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Expiry", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), - SelectableText( - "${utxo!.getConfirmations(currentHeight)}", - style: STextStyles.w500_14(context), + ), + const SizedBox(height: 4), + SelectableText( + message, + style: STextStyles.w500_14( + context, + ).copyWith(color: color), + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of( + context, + ).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Confirmations", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), - ], - ), + ), + const SizedBox(height: 4), + SelectableText( + "${utxo!.getConfirmations(currentHeight)}", + style: STextStyles.w500_14(context), + ), + ], ), - ], - ); + ), + ], + ); }, ), ), @@ -723,9 +711,7 @@ class _Div extends StatelessWidget { color: Theme.of(context).extension()!.textFieldDefaultBG, ); } else { - return const SizedBox( - height: 12, - ); + return const SizedBox(height: 12); } } } diff --git a/lib/pages/receive_view/addresses/address_details_view.dart b/lib/pages/receive_view/addresses/address_details_view.dart index 96c2e66a62..51dfd7e4ee 100644 --- a/lib/pages/receive_view/addresses/address_details_view.dart +++ b/lib/pages/receive_view/addresses/address_details_view.dart @@ -38,7 +38,8 @@ import '../../../widgets/qr.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/transaction_card.dart'; import '../../wallet_view/sub_widgets/no_transactions_found.dart'; -import '../../wallet_view/transaction_views/transaction_details_view.dart'; +import '../../wallet_view/transaction_views/transaction_details_view.dart' + as tdv; import '../../wallet_view/transaction_views/tx_v2/transaction_v2_card.dart'; import 'address_tag.dart'; @@ -306,7 +307,7 @@ class _AddressDetailsViewState extends ConsumerState { detail: address.value, button: isDesktop - ? IconCopyButton(data: address.value) + ? tdv.IconCopyButton(data: address.value) : SimpleCopyButton(data: address.value), ), const _Div(height: 12), diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index fe44acc9bf..3cd473b489 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -33,6 +33,7 @@ import '../../utilities/text_styles.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/impl/bitcoin_wallet.dart'; +import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; @@ -47,10 +48,14 @@ import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/custom_loading_overlay.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/desktop/secondary_button.dart'; +import '../../widgets/dialogs/s_dialog.dart'; import '../../widgets/qr.dart'; import '../../widgets/rounded_white_container.dart'; +import '../../widgets/stack_dialog.dart'; import 'addresses/wallet_addresses_view.dart'; import 'generate_receiving_uri_qr_code_view.dart'; +import 'sub_widgets/mwc_slatepack_import_dialog.dart'; +import 'sub_widgets/slatepack_entry_dialog.dart'; class ReceiveView extends ConsumerStatefulWidget { const ReceiveView({ @@ -84,6 +89,71 @@ class _ReceiveViewState extends ConsumerState { final Map _addressMap = {}; final Map> _addressSubMap = {}; + Future _importSlatepack() async { + final slatepackString = await showDialog( + context: context, + builder: (context) => const SlatepackEntryDialog(), + ); + + if (slatepackString == null) return; + if (mounted) { + final wallet = + ref.read(pWallets).getWallet(walletId) as MimblewimblecoinWallet; + + Exception? ex; + final result = await showLoading( + whileFuture: wallet.fullDecodeSlatepack(slatepackString), + context: context, + message: "Decoding slatepack...", + onException: (e) => ex = e, + ); + + if (result == null || ex != null) { + if (mounted) { + await showDialog( + context: context, + builder: + (context) => StackOkDialog( + title: "Slatepack receive error", + message: + ex?.toString() ?? "Unexpected result without exception", + ), + ); + } + return; + } + + if (mounted) { + final response = + await showDialog<({String responseSlatepack, bool wasEncrypted})>( + context: context, + builder: + (context) => SDialog( + child: MwcSlatepackImportDialog( + walletId: widget.walletId, + clipboard: widget.clipboard, + rawSlatepack: result.raw, + decoded: result.result, + slatepackType: result.type, + ), + ), + ); + + if (mounted && response != null) { + await showDialog( + context: context, + barrierDismissible: false, + builder: + (context) => SlatepackResponseDialog( + responseSlatepack: response.responseSlatepack, + wasEncrypted: response.wasEncrypted, + ), + ); + } + } + } + } + Future generateNewAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); @@ -692,6 +762,14 @@ class _ReceiveViewState extends ConsumerState { ? generateNewSparkAddress : generateNewAddress, ), + // MWC Slatepack import button. + if (coin is Mimblewimblecoin) ...[ + const SizedBox(height: 12), + SecondaryButton( + label: "Import Slatepack", + onPressed: _importSlatepack, + ), + ], const SizedBox(height: 30), RoundedWhiteContainer( child: Padding( diff --git a/lib/pages/receive_view/sub_widgets/mwc_slatepack_import_dialog.dart b/lib/pages/receive_view/sub_widgets/mwc_slatepack_import_dialog.dart new file mode 100644 index 0000000000..6b1bc060d8 --- /dev/null +++ b/lib/pages/receive_view/sub_widgets/mwc_slatepack_import_dialog.dart @@ -0,0 +1,361 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../models/mwc_slatepack_models.dart'; +import '../../../providers/global/wallets_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/amount/amount_formatter.dart'; +import '../../../utilities/clipboard_interface.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/mimblewimblecoin_wallet.dart'; +import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/custom_buttons/simple_copy_button.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/detail_item.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/rounded_white_container.dart'; +import '../../../widgets/stack_dialog.dart'; + +class MwcSlatepackImportDialog extends ConsumerStatefulWidget { + const MwcSlatepackImportDialog({ + super.key, + required this.walletId, + required this.rawSlatepack, + required this.decoded, + required this.slatepackType, + this.clipboard = const ClipboardWrapper(), + }); + + final String walletId; + final String rawSlatepack; + final SlatepackDecodeResult decoded; + final String slatepackType; + final ClipboardInterface clipboard; + + @override + ConsumerState createState() => + _MwcSlatepackImportDialogState(); +} + +class _MwcSlatepackImportDialogState + extends ConsumerState { + Future<({String responseSlatepack, bool wasEncrypted})> + _processSlatepack() async { + // add delay for showloading exception catching hack fix + await Future.delayed(const Duration(seconds: 1)); + + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as MimblewimblecoinWallet; + + // Determine action based on slatepack type. + if (widget.slatepackType.contains("S1")) { + // This is an initial slatepack - receive it and create response. + final result = await wallet.receiveSlatepack(widget.rawSlatepack); + + if (result.success && result.responseSlatepack != null) { + return ( + responseSlatepack: result.responseSlatepack!, + wasEncrypted: result.wasEncrypted ?? false, + ); + } else { + throw Exception(result.error ?? 'Failed to process slatepack'); + } + } else { + throw Exception('Unsupported slatepack type: ${widget.slatepackType}'); + } + } + + Future _processPressed() async { + Exception? ex; + final result = await showLoading( + whileFuture: _processSlatepack(), + context: context, + message: "Processing slatepack...", + onException: (e) => ex = e, + ); + + if (result == null || ex != null) { + if (mounted) { + await showDialog( + context: context, + useRootNavigator: true, + builder: + (context) => StackOkDialog( + desktopPopRootNavigator: true, + maxWidth: Util.isDesktop ? 400 : null, + title: "Slatepack receive error", + message: + ex?.toString() ?? "Unexpected result without exception", + ), + ); + } + return; + } + + if (mounted) { + Navigator.of(context).pop(result); + } + } + + late final Amount? _amount; + + // late final Amount? _fee; + + @override + void initState() { + final map = jsonDecode(widget.decoded.slateJson!) as Map; + + final rawAmount = BigInt.tryParse(map["amount"].toString()); + _amount = + rawAmount == null + ? null + : Amount( + rawValue: rawAmount, + fractionDigits: + ref.read(pWalletCoin(widget.walletId)).fractionDigits, + ); + + // final rawFee = BigInt.tryParse(map["fee"].toString()); + // _fee = + // rawFee == null + // ? null + // : Amount( + // rawValue: rawFee, + // fractionDigits: + // ref.read(pWalletCoin(widget.walletId)).fractionDigits, + // ); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isDesktop) + // Header with title and close button. + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Import Slatepack", + style: STextStyles.pageTitleH2(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: isDesktop ? 32 : 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ConditionalParent( + condition: isDesktop, + builder: + (child) => RoundedWhiteContainer( + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, + padding: const EdgeInsets.all(0), + child: child, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isDesktop) + Padding( + padding: const EdgeInsets.only(top: 24, bottom: 24), + child: Text( + "Import slatepack", + style: STextStyles.pageTitleH2(context), + ), + ), + + // DetailItem(title: "Type", detail: widget.slatepackType), + // const DetailDivider(), + // DetailItem( + // title: "Encrypted", + // detail: (widget.decoded.wasEncrypted ?? false).toString(), + // ), + // if (widget.decoded.senderAddress != null) + // const DetailDivider(), + // if (widget.decoded.senderAddress != null) + // DetailItem( + // title: "From", + // detail: widget.decoded.senderAddress!, + // ), + // if (widget.decoded.recipientAddress != null) + // const DetailDivider(), + // if (widget.decoded.recipientAddress != null) + // DetailItem( + // title: "To", + // detail: widget.decoded.recipientAddress!, + // ), + // if (_amount != null) const DetailDivider(), + if (_amount != null) + DetailItem( + title: "Amount", + detail: ref + .watch( + pAmountFormatter( + ref.watch(pWalletCoin(widget.walletId)), + ), + ) + .format(_amount), + ), + // if (_fee != null) const DetailDivider(), + // if (_fee != null) + // DetailItem( + // title: "Fee", + // detail: ref + // .watch( + // pAmountFormatter( + // ref.watch(pWalletCoin(widget.walletId)), + // ), + // ) + // .format(_fee), + // ), + ], + ), + ), + const SizedBox(height: 24), + ConditionalParent( + condition: isDesktop, + builder: + (child) => Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [child], + ), + child: PrimaryButton( + width: isDesktop ? 220 : null, + + buttonHeight: isDesktop ? ButtonHeight.l : null, + label: "Process", + onPressed: _processPressed, + ), + ), + if (!isDesktop) const SizedBox(height: 12), + if (!isDesktop) + SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + ], + ), + ), + isDesktop ? const SizedBox(height: 32) : const SizedBox(height: 24), + ], + ); + } +} + +class SlatepackResponseDialog extends StatelessWidget { + const SlatepackResponseDialog({ + super.key, + required this.responseSlatepack, + required this.wasEncrypted, + }); + + final String responseSlatepack; + final bool wasEncrypted; + + @override + Widget build(BuildContext context) { + return SDialog( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header with title and close button. + if (Util.isDesktop) + Padding( + padding: const EdgeInsets.only(left: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Response Slatepack", + style: STextStyles.pageTitleH2(context), + ), + const DesktopDialogCloseButton(), + ], + ), + ), + Padding( + padding: + Util.isDesktop + ? const EdgeInsets.only(left: 32, right: 32, bottom: 32) + : const EdgeInsets.only(left: 24, right: 24, bottom: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!Util.isDesktop) const SizedBox(height: 24), + Text( + "Return this slatepack to the sender to complete the transaction.", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Response slatepack", + style: STextStyles.itemSubtitle(context), + ), + SimpleCopyButton(data: responseSlatepack), + ], + ), + const SizedBox(height: 8), + ConditionalParent( + condition: !Util.isDesktop, + builder: + (child) => SizedBox( + height: 220, + child: SingleChildScrollView(child: child), + ), + child: SelectableText( + responseSlatepack, + style: STextStyles.w500_14(context), + ), + ), + const SizedBox(height: 24), + ConditionalParent( + condition: Util.isDesktop, + builder: + (child) => Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [child], + ), + child: PrimaryButton( + label: "Done", + width: Util.isDesktop ? 220 : null, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/receive_view/sub_widgets/slatepack_entry_dialog.dart b/lib/pages/receive_view/sub_widgets/slatepack_entry_dialog.dart new file mode 100644 index 0000000000..ec9aee0c36 --- /dev/null +++ b/lib/pages/receive_view/sub_widgets/slatepack_entry_dialog.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../providers/global/barcode_scanner_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/barcode_scanner_interface.dart'; +import '../../../utilities/clipboard_interface.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/icon_widgets/clipboard_icon.dart'; +import '../../../widgets/icon_widgets/qrcode_icon.dart'; +import '../../../widgets/icon_widgets/x_icon.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../../../widgets/stack_text_field.dart'; +import '../../../widgets/textfield_icon_button.dart'; + +class SlatepackEntryDialog extends ConsumerStatefulWidget { + const SlatepackEntryDialog({ + super.key, + this.clipboard = const ClipboardWrapper(), + }); + + final ClipboardInterface clipboard; + + @override + ConsumerState createState() => + _SlatepackEntryDialogState(); +} + +class _SlatepackEntryDialogState extends ConsumerState { + final _receiveSlateController = TextEditingController(); + final _slateFocusNode = FocusNode(); + + bool _slateToggleFlag = false; + + Future _pasteSlatepack() async { + final ClipboardData? data = await widget.clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && data!.text!.isNotEmpty) { + _receiveSlateController.text = data.text!; + setState(() { + _slateToggleFlag = _receiveSlateController.text.isNotEmpty; + }); + } + } + + Future _scanQr() async { + try { + if (_slateFocusNode.hasFocus) { + _slateFocusNode.unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + + if (mounted) { + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); + if (qrResult.rawContent.isNotEmpty && qrResult.rawContent != "null") { + _receiveSlateController.text = qrResult.rawContent; + setState(() { + _slateToggleFlag = _receiveSlateController.text.isNotEmpty; + }); + } + } + } on PlatformException catch (e, s) { + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Failed to get camera permissions while trying to scan qr code in SendView: ", + error: e, + stackTrace: s, + ); + } + } + } + + @override + void dispose() { + _receiveSlateController.dispose(); + _slateFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return StackDialogBase( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Receive Slatepack", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 5, + key: const Key("receiveViewSlatepackFieldKey"), + controller: _receiveSlateController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + setState(() { + _slateToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _slateFocusNode, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Enter Slatepack Message", + _slateFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, // Adjust vertical padding for better alignment + ), + suffixIcon: Padding( + padding: + _receiveSlateController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _slateToggleFlag + ? TextFieldIconButton( + key: const Key( + "receiveViewClearSlatepackFieldButtonKey", + ), + onTap: () { + _receiveSlateController.text = ""; + setState(() { + _slateToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "receiveViewPasteSlatepackFieldButtonKey", + ), + onTap: _pasteSlatepack, + child: + _receiveSlateController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_receiveSlateController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("sendViewScanQrButtonKey"), + onTap: _scanQr, + child: const QrCodeIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox(height: 16), + PrimaryButton( + label: "Import", + enabled: _slateToggleFlag, + onPressed: + !_slateToggleFlag + ? null + : () => + Navigator.of(context).pop(_receiveSlateController.text), + ), + const SizedBox(height: 16), + SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 220d0e04fd..93a6f052ad 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -9,6 +9,7 @@ */ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:decimal/decimal.dart'; @@ -34,11 +35,13 @@ import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/coins/epiccash.dart'; import '../../wallets/crypto_currency/coins/ethereum.dart'; +import '../../wallets/crypto_currency/coins/mimblewimblecoin.dart'; import '../../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/models/tx_data.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; +import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; @@ -54,6 +57,7 @@ import '../../widgets/stack_text_field.dart'; import '../../widgets/textfield_icon_button.dart'; import '../pinpad_views/lock_screen_view.dart'; import '../wallet_view/wallet_view.dart'; +import 'sub_widgets/mwc_slatepack_dialog.dart'; import 'sub_widgets/sending_transaction_dialog.dart'; class ConfirmTransactionView extends ConsumerStatefulWidget { @@ -99,6 +103,90 @@ class _ConfirmTransactionViewState late final FocusNode _onChainNoteFocusNode; late final TextEditingController onChainNoteController; + /// Handle MWC slatepack creation for manual exchange. + Future _handleMwcSlatepackCreation( + BuildContext context, + MimblewimblecoinWallet wallet, + ) async { + try { + // Close the progress dialog first. + Navigator.of(context).pop(); + + // Get recipient information from txData. + final recipient = widget.txData.recipients?.first; + if (recipient == null) { + throw Exception('No recipient found in transaction data'); + } + + // Create slatepack. + final slatepackResult = await wallet.createSlatepack( + amount: recipient.amount, + recipientAddress: + recipient.address.isNotEmpty ? recipient.address : null, + message: + onChainNoteController.text.isNotEmpty + ? onChainNoteController.text + : null, + encrypt: + recipient + .address + .isNotEmpty, // Encrypt if we have a recipient address. + ); + + if (!slatepackResult.success || slatepackResult.slatepack == null) { + throw Exception(slatepackResult.error ?? 'Failed to create slatepack'); + } + + // Show slatepack dialog. + if (context.mounted) { + await showDialog( + context: context, + barrierDismissible: false, + builder: + (context) => MwcSlatepackDialog(slatepackResult: slatepackResult), + ); + + // After slatepack dialog is closed, navigate back to wallet. + if (context.mounted) { + widget.onSuccess.call(); + if (widget.onSuccessInsteadOfRouteOnSuccess == null) { + Navigator.of( + context, + ).popUntil(ModalRoute.withName(routeOnSuccessName)); + } else { + widget.onSuccessInsteadOfRouteOnSuccess!.call(); + } + } + } + } catch (e, s) { + Logging.instance.e('Failed to create MWC slatepack: $e\n$s'); + + if (context.mounted) { + // Show user-friendly error message. + final errorMessage = e.toString().contains('insufficient funds') + ? 'Insufficient funds for this transaction' + : e.toString().contains('wallet not open') + ? 'Wallet not accessible. Please restart the app.' + : 'Failed to create slatepack: ${e.toString()}'; + + await showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text('Slatepack Creation Failed'), + content: Text('Failed to create slatepack: $e'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + } + } + Future _attemptSend(BuildContext context) async { final wallet = ref.read(pWallets).getWallet(walletId); final coin = wallet.info.coin; @@ -155,7 +243,31 @@ class _ConfirmTransactionViewState break; } } else { - if (coin is Epiccash) { + if (coin is Mimblewimblecoin) { + // Check if this is a slatepack transaction (manual exchange). + final otherDataMap = + widget.txData.otherData != null + ? jsonDecode(widget.txData.otherData!) + : null; + final transactionMethod = + otherDataMap?['transactionMethod'] as String?; + + if (transactionMethod == 'slatepack') { + // Handle slatepack creation instead of direct send. + await _handleMwcSlatepackCreation( + context, + wallet as MimblewimblecoinWallet, + ); + return; // Exit early, don't continue with normal transaction flow. + } else { + // Handle MWCMQS or HTTP transactions normally. + txDataFuture = wallet.confirmSend( + txData: widget.txData.copyWith( + noteOnChain: onChainNoteController.text, + ), + ); + } + } else if (coin is Epiccash) { txDataFuture = wallet.confirmSend( txData: widget.txData.copyWith( noteOnChain: onChainNoteController.text, @@ -562,9 +674,11 @@ class _ConfirmTransactionViewState ], ), ), - if (coin is Epiccash && widget.txData.noteOnChain!.isNotEmpty) + if ((coin is Epiccash || coin is Mimblewimblecoin) && + widget.txData.noteOnChain!.isNotEmpty) const SizedBox(height: 12), - if (coin is Epiccash && widget.txData.noteOnChain!.isNotEmpty) + if ((coin is Epiccash || coin is Mimblewimblecoin) && + widget.txData.noteOnChain!.isNotEmpty) RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -589,7 +703,9 @@ class _ConfirmTransactionViewState crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - (coin is Epiccash) ? "Local Note" : "Note", + (coin is Epiccash || coin is Mimblewimblecoin) + ? "Local Note" + : "Note", style: STextStyles.smallMed12(context), ), const SizedBox(height: 4), @@ -926,14 +1042,15 @@ class _ConfirmTransactionViewState mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (coin is Epiccash) + if (coin is Epiccash || coin is Mimblewimblecoin) Text( "On chain Note (optional)", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin is Epiccash) const SizedBox(height: 8), - if (coin is Epiccash) + if (coin is Epiccash || coin is Mimblewimblecoin) + const SizedBox(height: 8), + if (coin is Epiccash || coin is Mimblewimblecoin) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -975,9 +1092,10 @@ class _ConfirmTransactionViewState ), ), ), - if (coin is Epiccash) const SizedBox(height: 12), + if (coin is Epiccash || coin is Mimblewimblecoin) + const SizedBox(height: 12), SelectableText( - (coin is Epiccash) + (coin is Epiccash || coin is Mimblewimblecoin) ? "Local Note (optional)" : "Note (optional)", style: STextStyles.desktopTextExtraSmall( diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart index ee45a6e70a..927a09b3cf 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart @@ -21,7 +21,8 @@ import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/detail_item.dart'; import '../../../../widgets/expandable.dart'; import '../../../../widgets/stack_dialog.dart'; -import '../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import '../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart' + as tvd; import '../../../wallet_view/wallet_view.dart'; class FrostSendStep4 extends ConsumerStatefulWidget { @@ -43,9 +44,9 @@ class _FrostSendStep4State extends ConsumerState { @override void initState() { - final wallet = ref.read(pWallets).getWallet( - ref.read(pFrostScaffoldArgs)!.walletId!, - ) as BitcoinFrostWallet; + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; cryptoCurrency = wallet.cryptoCurrency; @@ -80,79 +81,70 @@ class _FrostSendStep4State extends ConsumerState { DetailItem( title: "Tx hex (debug mode only)", detail: ref.watch(pFrostTxData)!.raw!, - button: Util.isDesktop - ? IconCopyButton( - data: ref.watch(pFrostTxData)!.raw!, - ) - : SimpleCopyButton( - data: ref.watch(pFrostTxData)!.raw!, - ), - ), - if (kDebugMode) - const SizedBox( - height: 12, + button: + Util.isDesktop + ? tvd.IconCopyButton(data: ref.watch(pFrostTxData)!.raw!) + : SimpleCopyButton(data: ref.watch(pFrostTxData)!.raw!), ), + if (kDebugMode) const SizedBox(height: 12), Text( "Send ${cryptoCurrency.ticker}", style: STextStyles.w600_20(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), recipients.length == 1 ? _Recipient( - address: recipients[0].address, - amount: ref - .watch(pAmountFormatter(cryptoCurrency)) - .format(recipients[0].amount), - ) + address: recipients[0].address, + amount: ref + .watch(pAmountFormatter(cryptoCurrency)) + .format(recipients[0].amount), + ) : Column( - children: [ - for (int i = 0; i < recipients.length; i++) - Padding( - padding: const EdgeInsets.only(top: 10), - child: Expandable( - onExpandChanged: (state) { - setState(() { - _expandedStates[i] = - state == ExpandableState.expanded; - }); - }, - header: Padding( - padding: const EdgeInsets.only(top: 12, bottom: 6), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recipient ${i + 1}", - style: STextStyles.itemSubtitle(context), - ), - SvgPicture.asset( - _expandedStates[i] - ? Assets.svg.chevronUp - : Assets.svg.chevronDown, - width: 12, - height: 6, - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ], - ), - ), - body: _Recipient( - address: recipients[i].address, - amount: ref - .watch(pAmountFormatter(cryptoCurrency)) - .format(recipients[i].amount), + children: [ + for (int i = 0; i < recipients.length; i++) + Padding( + padding: const EdgeInsets.only(top: 10), + child: Expandable( + onExpandChanged: (state) { + setState(() { + _expandedStates[i] = + state == ExpandableState.expanded; + }); + }, + header: Padding( + padding: const EdgeInsets.only(top: 12, bottom: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipient ${i + 1}", + style: STextStyles.itemSubtitle(context), + ), + SvgPicture.asset( + _expandedStates[i] + ? Assets.svg.chevronUp + : Assets.svg.chevronDown, + width: 12, + height: 6, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), + ], ), ), + body: _Recipient( + address: recipients[i].address, + amount: ref + .watch(pAmountFormatter(cryptoCurrency)) + .format(recipients[i].amount), + ), ), - ], - ), - const SizedBox( - height: 12, - ), + ), + ], + ), + const SizedBox(height: 12), DetailItem( title: "Transaction fee", detail: ref @@ -160,38 +152,27 @@ class _FrostSendStep4State extends ConsumerState { .format(ref.watch(pFrostTxData)!.fee!), horizontal: true, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), DetailItem( title: "Total", - detail: ref.watch(pAmountFormatter(cryptoCurrency)).format( + detail: ref + .watch(pAmountFormatter(cryptoCurrency)) + .format( ref.watch(pFrostTxData)!.fee! + recipients.map((e) => e.amount).reduce((v, e) => v += e), ), horizontal: true, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), DetailItem( title: "Note", detail: ref.watch(pFrostTxData)!.note ?? "", ), - const SizedBox( - height: 12, - ), - DetailItem( - title: "Signers", - detail: signers, - ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), + DetailItem(title: "Signers", detail: signers), + const SizedBox(height: 12), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), PrimaryButton( label: "Approve transaction", onPressed: () async { @@ -205,12 +186,8 @@ class _FrostSendStep4State extends ConsumerState { final txData = await showLoading( whileFuture: ref .read(pWallets) - .getWallet( - ref.read(pFrostScaffoldArgs)!.walletId!, - ) - .confirmSend( - txData: ref.read(pFrostTxData)!, - ), + .getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + .confirmSend(txData: ref.read(pFrostTxData)!), context: context, message: "Broadcasting transaction to network", rootNavigator: true, // used to pop using root nav @@ -227,7 +204,10 @@ class _FrostSendStep4State extends ConsumerState { if (txData != null) { ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; ref.read(pFrostTxData.state).state = txData; - ref.read(pFrostScaffoldArgs)!.parentNav.popUntil( + ref + .read(pFrostScaffoldArgs)! + .parentNav + .popUntil( ModalRoute.withName( Util.isDesktop ? MyStackView.routeName @@ -237,17 +217,18 @@ class _FrostSendStep4State extends ConsumerState { } } } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); if (context.mounted) { return await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Broadcast error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - onOkPressed: - Navigator.of(context, rootNavigator: true).pop, - ), + builder: + (_) => StackOkDialog( + title: "Broadcast error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + onOkPressed: + Navigator.of(context, rootNavigator: true).pop, + ), ); } } finally { @@ -262,11 +243,7 @@ class _FrostSendStep4State extends ConsumerState { } class _Recipient extends StatelessWidget { - const _Recipient({ - super.key, - required this.address, - required this.amount, - }); + const _Recipient({super.key, required this.address, required this.amount}); final String address; final String amount; @@ -276,18 +253,9 @@ class _Recipient extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, children: [ - DetailItem( - title: "Address", - detail: address, - ), - const SizedBox( - height: 6, - ), - DetailItem( - title: "Amount", - detail: amount, - horizontal: true, - ), + DetailItem(title: "Address", detail: address), + const SizedBox(height: 6), + DetailItem(title: "Amount", detail: amount, horizontal: true), ], ); } diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index cfaa41f64b..1577e568e8 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -21,6 +21,7 @@ import 'package:tuple/tuple.dart'; import '../../models/input.dart'; import '../../models/isar/models/isar_models.dart'; +import '../../models/mwc_slatepack_models.dart'; import '../../models/paynym/paynym_account_lite.dart'; import '../../models/send_view_auto_fill_data.dart'; import '../../providers/providers.dart'; @@ -41,10 +42,12 @@ import '../../utilities/barcode_scanner_interface.dart'; import '../../utilities/clipboard_interface.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; +import '../../utilities/enums/mwc_transaction_method.dart'; import '../../utilities/eth_commons.dart'; import '../../utilities/extensions/extensions.dart'; import '../../utilities/logger.dart'; import '../../utilities/prefs.dart'; +import '../../utilities/show_loading.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; @@ -52,6 +55,7 @@ import '../../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/models/tx_data.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; +import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; @@ -67,6 +71,7 @@ import '../../widgets/icon_widgets/addressbook_icon.dart'; import '../../widgets/icon_widgets/clipboard_icon.dart'; import '../../widgets/icon_widgets/qrcode_icon.dart'; import '../../widgets/icon_widgets/x_icon.dart'; +import '../../widgets/mwc_txs_method_toggle.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_dialog.dart'; import '../../widgets/stack_text_field.dart'; @@ -76,6 +81,7 @@ import '../coin_control/coin_control_view.dart'; import 'confirm_transaction_view.dart'; import 'sub_widgets/building_transaction_dialog.dart'; import 'sub_widgets/dual_balance_selection_sheet.dart'; +import 'sub_widgets/mwc_slatepack_dialog.dart'; import 'sub_widgets/transaction_fee_selection_sheet.dart'; class SendView extends ConsumerStatefulWidget { @@ -242,9 +248,11 @@ class _SendViewState extends ConsumerState { }); } } catch (e) { + // strip http:// and https:// if content contains @ if (coin is Epiccash) { - // strip http:// and https:// if content contains @ content = AddressUtils().formatEpicCashAddress(content); + } else if (coin is Mimblewimblecoin) { + content = AddressUtils().formatAddressMwc(content); } await _checkSparkNameAndOrSetAddress(content); @@ -408,7 +416,9 @@ class _SendViewState extends ConsumerState { _cryptoAmountChangedFeeUpdateTimer?.cancel(); _cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { - if (coin is! Epiccash && !_baseFocus.hasFocus) { + if (coin is! Epiccash && + coin is! Mimblewimblecoin && + !_baseFocus.hasFocus) { setState(() { _calculateFeesFuture = calculateFees( amount ?? 0.toAmountAsRaw(fractionDigits: coin.fractionDigits), @@ -427,7 +437,9 @@ class _SendViewState extends ConsumerState { void _baseAmountChanged() { _baseAmountChangedFeeUpdateTimer?.cancel(); _baseAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { - if (coin is! Epiccash && !_cryptoFocus.hasFocus) { + if (coin is! Epiccash && + coin is! Mimblewimblecoin && + !_cryptoFocus.hasFocus) { setState(() { _calculateFeesFuture = calculateFees( ref.read(pSendAmount) == null @@ -599,6 +611,97 @@ class _SendViewState extends ConsumerState { } } + Future _createSlatepack() async { + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 100)); + + try { + if (mounted) { + final wallet = + ref.read(pWallets).getWallet(walletId) as MimblewimblecoinWallet; + + final amount = ref.read(pSendAmount)!; + + Future wrappedFutureWithDelay() async { + await Future.delayed(const Duration(seconds: 1)); + return wallet.createSlatepack( + amount: amount, + recipientAddress: null, + // No specific recipient for manual slatepack. + message: + onChainNoteController.text.isNotEmpty == true + ? onChainNoteController.text + : null, + encrypt: false, // No encryption without recipient address. + ); + } + + // Create slatepack. + Exception? ex; + final slatepackResult = await showLoading( + whileFuture: wrappedFutureWithDelay(), + context: context, + message: "Building slatepack...", + delay: const Duration(seconds: 2), + onException: (e) => ex = e, + ); + + if (slatepackResult == null || + !slatepackResult.success || + slatepackResult.slatepack == null || + ex != null) { + String error = + ex?.toString() ?? + slatepackResult?.error ?? + 'Failed to create slatepack'; + if (error.startsWith("Exception:")) { + error = error.replaceFirst("Exception:", "").trim(); + } + throw Exception(error); + } + + // refresh asap to show the pending slate tx in history + unawaited(() async { + await Future.delayed(Duration.zero); + await wallet.refresh(); + }()); + + // Show slatepack dialog. + if (mounted) { + await showDialog( + context: context, + barrierDismissible: false, + builder: + (context) => StackDialogBase( + child: MwcSlatepackDialog(slatepackResult: slatepackResult), + ), + ); + + // Clear form after slatepack dialog is closed. + clearSendForm(); + } + } + } catch (e, s) { + Logging.instance.e( + 'Failed to create MWC slatepack on mobile', + error: e, + stackTrace: s, + ); + + if (mounted) { + await showDialog( + context: context, + builder: + (context) => StackOkDialog( + title: "Slatepack Creation Failed", + message: e.toString(), + ), + ); + } + } + } + Future _previewTransaction() async { // wait for keyboard to disappear FocusScope.of(context).unfocus(); @@ -1286,6 +1389,11 @@ class _SendViewState extends ConsumerState { ); } + final isMwcSlatepack = + coin is Mimblewimblecoin && + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack; + return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -1461,34 +1569,46 @@ class _SendViewState extends ConsumerState { ), ), const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - isPaynymSend - ? "Send to PayNym address" - : "Send to", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - // if (coin is Monero) - // CustomTextButton( - // text: "Use OpenAlias", - // onTap: () async { - // await showModalBottomSheet( - // context: context, - // builder: (context) => - // OpenAliasBottomSheet( - // onSelected: (address) { - // sendToController.text = address; - // }, - // ), - // ); - // }, - // ), - ], - ), - const SizedBox(height: 8), + + // MWC Transaction Method Selector. + if (coin is Mimblewimblecoin) ...[ + const SizedBox( + height: 40, + child: MwcTxsMethodToggle(), + ), + const SizedBox(height: 16), + ], + + if (!isMwcSlatepack) + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + isPaynymSend + ? "Send to PayNym address" + : "Send to", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + // if (coin is Monero) + // CustomTextButton( + // text: "Use OpenAlias", + // onTap: () async { + // await showModalBottomSheet( + // context: context, + // builder: (context) => + // OpenAliasBottomSheet( + // onSelected: (address) { + // sendToController.text = address; + // }, + // ), + // ); + // }, + // ), + ], + ), + if (!isMwcSlatepack) const SizedBox(height: 8), if (isPaynymSend) TextField( key: const Key("sendViewPaynymAddressFieldKey"), @@ -1497,7 +1617,7 @@ class _SendViewState extends ConsumerState { readOnly: true, style: STextStyles.fieldLabel(context), ), - if (!isPaynymSend) + if (!isPaynymSend && !isMwcSlatepack) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1553,7 +1673,9 @@ class _SendViewState extends ConsumerState { focusNode: _addressFocusNode, style: STextStyles.field(context), decoration: standardInputDecoration( - "Enter ${coin.ticker} address", + isMwcSlatepack + ? "Enter ${coin.ticker} address (optional)" + : "Enter ${coin.ticker} address", _addressFocusNode, context, ).copyWith( @@ -2208,7 +2330,8 @@ class _SendViewState extends ConsumerState { ), ), ), - if (coin is Epiccash) const SizedBox(height: 12), + if (coin is Epiccash || coin is Mimblewimblecoin) + const SizedBox(height: 12), Text( (coin is Epiccash) ? "Local Note (optional)" @@ -2216,6 +2339,61 @@ class _SendViewState extends ConsumerState { style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), + if (coin is Epiccash || coin is Mimblewimblecoin) + const SizedBox(height: 8), + if (coin is Epiccash || coin is Mimblewimblecoin) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: + noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 0, + ), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = + ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + if (coin is Epiccash || coin is Mimblewimblecoin) + const SizedBox(height: 12), + Text( + (coin is Epiccash || coin is Mimblewimblecoin) + ? "Local Note (optional)" + : "Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( @@ -2261,18 +2439,24 @@ class _SendViewState extends ConsumerState { ), ), const SizedBox(height: 12), - if (hasFees) + if (coin is! Epiccash && + coin is! Mimblewimblecoin && + coin is! NanoCurrency && + coin is! Tezos) Text( - "Transaction fee ${isEth - ? isCustomFee.value - ? "" - : "(max)" - : "(estimated)"}", + "Transaction fee (estimated)", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (hasFees) const SizedBox(height: 8), - if (hasFees) + if (coin is! Epiccash && + coin is! Mimblewimblecoin && + coin is! NanoCurrency && + coin is! Tezos) + const SizedBox(height: 8), + if (coin is! Epiccash && + coin is! Mimblewimblecoin && + coin is! NanoCurrency && + coin is! Tezos) Stack( children: [ TextField( @@ -2287,147 +2471,155 @@ class _SendViewState extends ConsumerState { padding: const EdgeInsets.symmetric( horizontal: 12, ), - child: RawMaterialButton( - splashColor: - Theme.of( - context, - ).extension()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, ), - onPressed: - isFiro && - ref - .watch( - publicPrivateBalanceStateProvider - .state, - ) - .state != - BalanceType.public - ? null - : _onFeeSelectPressed, - child: - (isFiro && - ref - .watch( - publicPrivateBalanceStateProvider - .state, - ) - .state != - BalanceType.public) - ? Row( - children: [ - FutureBuilder( - future: - _calculateFeesFuture, - builder: ( - context, - snapshot, - ) { - if (snapshot.connectionState == - ConnectionState - .done && - snapshot.hasData) { - _setCurrentFee( - snapshot.data!, - false, - ); - return Text( - "~${snapshot.data!}", - style: - STextStyles.itemSubtitle( - context, - ), - ); - } else { - return AnimatedText( - stringsToLoopThrough: - stringsToLoopThrough, - style: - STextStyles.itemSubtitle( - context, - ), - ); - } - }, - ), - ], - ) - : Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Row( - children: [ - Text( - ref + child: RawMaterialButton( + splashColor: + Theme.of(context) + .extension()! + .highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: + isFiro && + ref .watch( - feeRateTypeMobileStateProvider + publicPrivateBalanceStateProvider .state, ) - .state - .prettyName, - style: - STextStyles.itemSubtitle12( - context, - ), - ), - const SizedBox(width: 10), - FutureBuilder( - future: - _calculateFeesFuture, - builder: ( - context, - snapshot, - ) { - if (snapshot.connectionState == - ConnectionState - .done && - snapshot - .hasData) { - _setCurrentFee( - snapshot.data!, - false, - ); - return Text( - isCustomFee.value - ? "" - : "~${snapshot.data!}", - style: - STextStyles.itemSubtitle( - context, - ), - ); - } else { - return AnimatedText( - stringsToLoopThrough: - stringsToLoopThrough, - style: - STextStyles.itemSubtitle( - context, - ), - ); - } - }, - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: - Theme.of(context) - .extension< - StackColors - >()! - .textSubtitle2, - ), - ], - ), + .state != + BalanceType.public + ? null + : _onFeeSelectPressed, + child: + (isFiro && + ref + .watch( + publicPrivateBalanceStateProvider + .state, + ) + .state != + BalanceType.public) + ? Row( + children: [ + FutureBuilder( + future: + _calculateFeesFuture, + builder: ( + context, + snapshot, + ) { + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + _setCurrentFee( + snapshot.data!, + false, + ); + return Text( + "~${snapshot.data!}", + style: + STextStyles.itemSubtitle( + context, + ), + ); + } else { + return AnimatedText( + stringsToLoopThrough: + stringsToLoopThrough, + style: + STextStyles.itemSubtitle( + context, + ), + ); + } + }, + ), + ], + ) + : Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Row( + children: [ + Text( + ref + .watch( + feeRateTypeMobileStateProvider + .state, + ) + .state + .prettyName, + style: + STextStyles.itemSubtitle12( + context, + ), + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: + _calculateFeesFuture, + builder: ( + context, + snapshot, + ) { + if (snapshot.connectionState == + ConnectionState + .done && + snapshot + .hasData) { + _setCurrentFee( + snapshot.data!, + false, + ); + return Text( + isCustomFee + .value + ? "" + : "~${snapshot.data!}", + style: + STextStyles.itemSubtitle( + context, + ), + ); + } else { + return AnimatedText( + stringsToLoopThrough: + stringsToLoopThrough, + style: + STextStyles.itemSubtitle( + context, + ), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textSubtitle2, + ), + ], + ), + ), ), ), ], @@ -2457,7 +2649,12 @@ class _SendViewState extends ConsumerState { TextButton( onPressed: ref.watch(pPreviewTxButtonEnabled(coin)) - ? _previewTransaction + ? ref.watch( + pSelectedMwcTransactionMethod, + ) == + MwcTransactionMethod.slatepack + ? _createSlatepack + : _previewTransaction : null, style: ref.watch(pPreviewTxButtonEnabled(coin)) @@ -2470,7 +2667,10 @@ class _SendViewState extends ConsumerState { context, ), child: Text( - "Preview", + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack + ? "Create slatepack" + : "Preview", style: STextStyles.button(context), ), ), diff --git a/lib/pages/send_view/sub_widgets/mwc_slatepack_dialog.dart b/lib/pages/send_view/sub_widgets/mwc_slatepack_dialog.dart new file mode 100644 index 0000000000..ec57cf0a53 --- /dev/null +++ b/lib/pages/send_view/sub_widgets/mwc_slatepack_dialog.dart @@ -0,0 +1,214 @@ +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 '../../../models/mwc_slatepack_models.dart'; +import '../../../notifications/show_flush_bar.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/assets.dart'; +import '../../../utilities/clipboard_interface.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/qr.dart'; +import '../../../widgets/rounded_container.dart'; +import '../../../widgets/rounded_white_container.dart'; + +class MwcSlatepackDialog extends ConsumerStatefulWidget { + const MwcSlatepackDialog({ + super.key, + required this.slatepackResult, + this.clipboard = const ClipboardWrapper(), + }); + + final SlatepackResult slatepackResult; + final ClipboardInterface clipboard; + + @override + ConsumerState createState() => _MwcSlatepackDialogState(); +} + +class _MwcSlatepackDialogState extends ConsumerState { + void _copySlatepack() { + widget.clipboard.setData( + ClipboardData(text: widget.slatepackResult.slatepack!), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Slatepack copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + } + + void _shareSlatepack() { + // TODO: Implement file sharing for desktop platforms. + showFloatingFlushBar( + type: FlushBarType.info, + message: "Share functionality coming soon", + context: context, + ); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: + (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header with title and close button. + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Send Slatepack", + style: STextStyles.pageTitleH2(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding(padding: const EdgeInsets.all(32), child: child), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Instructions. + RoundedContainer( + color: + Theme.of(context).extension()!.textFieldDefaultBG, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Next Steps:", + style: STextStyles.label( + context, + ).copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 8), + Text( + "1. Share this slatepack with the recipient\n" + "2. Wait for them to return the response slatepack\n" + "3. Import their response to finalize the transaction", + style: STextStyles.w400_14(context), + ), + ], + ), + ), + + // Encryption status. + // we don't encrypt so ignore for now + // if (widget.slatepackResult.wasEncrypted == true) + // Container( + // padding: const EdgeInsets.symmetric( + // horizontal: 12, + // vertical: 8, + // ), + // decoration: BoxDecoration( + // color: Theme.of( + // context, + // ).extension()!.infoItemIcons.withOpacity(0.1), + // borderRadius: BorderRadius.circular(8), + // ), + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Icon( + // Icons.lock, + // size: 16, + // color: + // Theme.of( + // context, + // ).extension()!.infoItemIcons, + // ), + // const SizedBox(width: 8), + // Text( + // "Encrypted for recipient", + // style: STextStyles.label(context).copyWith( + // color: + // Theme.of( + // context, + // ).extension()!.infoItemIcons, + // ), + // ), + // ], + // ), + // ), + const SizedBox(height: 12), + + // QR Code view. + Center( + child: QR( + data: widget.slatepackResult.slatepack!, + size: 220, + // errorCorrectionLevel: QrErrorCorrectLevel.M, + ), + ), + + const SizedBox(height: 12), + + // Slatepack text view. + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text("Slatepack", style: STextStyles.itemSubtitle(context)), + const Spacer(), + GestureDetector( + onTap: _copySlatepack, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 10, + height: 10, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, + ), + const SizedBox(width: 4), + Text("Copy", style: STextStyles.link2(context)), + ], + ), + ), + ], + ), + const SizedBox(height: 8), + Container( + constraints: const BoxConstraints( + maxHeight: 200, + minHeight: 100, + ), + child: SingleChildScrollView( + child: SelectableText( + widget.slatepackResult.slatepack!, + style: STextStyles.w400_14( + context, + ).copyWith(fontFamily: 'monospace'), + ), + ), + ), + ], + ), + ), + + if (!Util.isDesktop) + PrimaryButton(label: "Done", onPressed: Navigator.of(context).pop), + ], + ), + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/about_view.dart b/lib/pages/settings_views/global_settings_view/about_view.dart index 06c3774a4a..88ed1a06fe 100644 --- a/lib/pages/settings_views/global_settings_view/about_view.dart +++ b/lib/pages/settings_views/global_settings_view/about_view.dart @@ -218,6 +218,95 @@ class AboutView extends ConsumerWidget { }, ), const SizedBox(height: 12), + if (AppConfig.coins + .whereType() + .isNotEmpty) + const SizedBox(height: 12), + if (AppConfig.coins + .whereType() + .isNotEmpty) + FutureBuilder( + future: + GitStatus.getMimblewimblecoinCommitStatus(), + builder: ( + context, + AsyncSnapshot snapshot, + ) { + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + stateOfCommit = snapshot.data!; + } + + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Mimblewimblecoin Build Commit", + style: STextStyles.titleBold12(context), + ), + const SizedBox(height: 4), + SelectableText( + GitStatus.mimblewimblecoinCommit, + style: GitStatus.styleForStatus( + stateOfCommit, + context, + ), + ), + ], + ), + ); + }, + ), + if (AppConfig.coins.whereType().isNotEmpty) + const SizedBox(height: 12), + // if (AppConfig.coins.whereType().isNotEmpty) + // FutureBuilder( + // future: GitStatus.getMoneroCommitStatus(), + // builder: ( + // context, + // AsyncSnapshot snapshot, + // ) { + // CommitStatus stateOfCommit = + // CommitStatus.notLoaded; + // + // if (snapshot.connectionState == + // ConnectionState.done && + // snapshot.hasData) { + // stateOfCommit = snapshot.data!; + // } + // return RoundedWhiteContainer( + // child: Column( + // crossAxisAlignment: + // CrossAxisAlignment.stretch, + // children: [ + // Text( + // "Monero Build Commit", + // style: STextStyles.titleBold12(context), + // ), + // const SizedBox( + // height: 4, + // ), + // SelectableText( + // GitStatus.moneroCommit, + // style: GitStatus.styleForStatus( + // stateOfCommit, + // context, + // ), + // ), + // ], + // ), + // ); + // }, + // ), + // const SizedBox( + // height: 12, + // ), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -239,6 +328,30 @@ class AboutView extends ConsumerWidget { ], ), ), + if (AppConfig.coins.whereType().isNotEmpty) + const SizedBox(height: 12), + if (AppConfig.coins.whereType().isNotEmpty) + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Website", + style: STextStyles.titleBold12(context), + ), + const SizedBox(height: 4), + CustomTextButton( + text: "https://stackwallet.com", + onTap: () { + launchUrl( + Uri.parse("https://stackwallet.com"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), if (AppConfig.coins.whereType().isNotEmpty) const SizedBox(height: 12), if (AppConfig.coins.whereType().isNotEmpty) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index ca5b825adc..e0ebd4c532 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -89,6 +89,10 @@ class _AddEditNodeViewState extends ConsumerState { ref.read(nodeFormDataProvider).host = data.host; ref.read(nodeFormDataProvider).port = data.port; ref.read(nodeFormDataProvider).useSSL = data.useSSL; + } else if (coin is Mimblewimblecoin) { + ref.read(nodeFormDataProvider).host = data.host; + ref.read(nodeFormDataProvider).port = data.port; + ref.read(nodeFormDataProvider).useSSL = data.useSSL; } else if (coin is CryptonoteCurrency) { ref.read(nodeFormDataProvider).host = data.host; } @@ -928,6 +932,8 @@ class _NodeFormState extends ConsumerState { if (widget.coin is Epiccash) { enableSSLCheckbox = !node.host.startsWith("http"); + } else if (widget.coin is Mimblewimblecoin) { + enableSSLCheckbox = !node.host.startsWith("http"); } else { enableSSLCheckbox = true; } @@ -1101,6 +1107,17 @@ class _NodeFormState extends ConsumerState { _useSSL = true; } } + if (widget.coin is Mimblewimblecoin) { + if (newValue.startsWith("https://")) { + _useSSL = true; + enableSSLCheckbox = false; + } else if (newValue.startsWith("http://")) { + _useSSL = false; + enableSSLCheckbox = false; + } else { + enableSSLCheckbox = true; + } + } _updateState(); setState(() {}); }, @@ -1353,9 +1370,15 @@ class _NodeFormState extends ConsumerState { ), ], ), - if (widget.coin is! CryptonoteCurrency && widget.coin is! Epiccash) - const SizedBox(height: 8), - if (widget.coin is! CryptonoteCurrency && widget.coin is! Epiccash) + if (widget.coin is! CryptonoteCurrency && + widget.coin is! Epiccash && + widget.coin is! Mimblewimblecoin) + const SizedBox( + height: 8, + ), + if (widget.coin is! CryptonoteCurrency && + widget.coin is! Epiccash && + widget.coin is! Mimblewimblecoin) Row( children: [ GestureDetector( diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 1d8d9c90f1..a178dddab9 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -52,6 +52,7 @@ import '../../../../../wallets/isar/models/frost_wallet_info.dart'; import '../../../../../wallets/isar/models/wallet_info.dart'; import '../../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart'; import '../../../../../wallets/wallet/impl/epiccash_wallet.dart'; +import '../../../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../../../wallets/wallet/impl/wownero_wallet.dart'; import '../../../../../wallets/wallet/impl/xelis_wallet.dart'; @@ -476,6 +477,10 @@ abstract class SWB { await (wallet as EpiccashWallet).init(isRestore: true); break; + case const (MimblewimblecoinWallet): + await (wallet as MimblewimblecoinWallet).init(isRestore: true); + break; + case const (MoneroWallet): await (wallet as MoneroWallet).init(isRestore: true); break; @@ -496,7 +501,8 @@ abstract class SWB { if (restoreHeight <= 0) { if (wallet is EpiccashWallet || wallet is LibMoneroWallet || - wallet is LibSalviumWallet) { + wallet is LibSalviumWallet || + wallet is MimblewimblecoinWallet) { restoreHeight = 0; } else { restoreHeight = walletbackup['storedChainHeight'] as int? ?? 0; diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart index a0b502da24..71064a134b 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import '../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; + import '../../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import '../../../../providers/db/main_db_provider.dart'; import '../../../../themes/stack_colors.dart'; @@ -15,12 +15,11 @@ import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/desktop/desktop_app_bar.dart'; import '../../../../widgets/desktop/desktop_scaffold.dart'; import '../../../../widgets/rounded_white_container.dart'; +import '../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart' + as tdv; class FrostParticipantsView extends ConsumerWidget { - const FrostParticipantsView({ - super.key, - required this.walletId, - }); + const FrostParticipantsView({super.key, required this.walletId}); static const String routeName = "/frostParticipantsView"; @@ -29,74 +28,73 @@ class FrostParticipantsView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) - final frostInfo = ref - .read(mainDBProvider) - .isar - .frostWalletInfo - .getByWalletIdSync(walletId)!; + final frostInfo = + ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; return ConditionalParent( condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), + builder: + (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox(width: 480, child: child), + ), child: ConditionalParent( condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Participants", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Participants", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), ), - ), - ), - ); - }, + ); + }, + ), + ), ), ), - ), - ), child: Column( - crossAxisAlignment: Util.isDesktop - ? CrossAxisAlignment.start - : CrossAxisAlignment.stretch, + crossAxisAlignment: + Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ for (int i = 0; i < frostInfo.participants.length; i++) Padding( - padding: const EdgeInsets.symmetric( - vertical: 5, - ), + padding: const EdgeInsets.symmetric(vertical: 5), child: RoundedWhiteContainer( child: Row( children: [ @@ -104,12 +102,11 @@ class FrostParticipantsView extends ConsumerWidget { width: 26, height: 26, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldActiveBG, - borderRadius: BorderRadius.circular( - 200, - ), + color: + Theme.of( + context, + ).extension()!.textFieldActiveBG, + borderRadius: BorderRadius.circular(200), ), child: Center( child: SvgPicture.asset( @@ -119,9 +116,7 @@ class FrostParticipantsView extends ConsumerWidget { ), ), ), - const SizedBox( - width: 8, - ), + const SizedBox(width: 8), Expanded( child: Text( frostInfo.participants[i] == frostInfo.myName @@ -130,12 +125,8 @@ class FrostParticipantsView extends ConsumerWidget { style: STextStyles.w500_14(context), ), ), - const SizedBox( - width: 8, - ), - IconCopyButton( - data: frostInfo.participants[i], - ), + const SizedBox(width: 8), + tdv.IconCopyButton(data: frostInfo.participants[i]), ], ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart index d94cca0882..dded7630c0 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart @@ -39,7 +39,8 @@ import '../../../../widgets/qr.dart'; import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_dialog.dart'; import '../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; -import '../../../wallet_view/transaction_views/transaction_details_view.dart'; +import '../../../wallet_view/transaction_views/transaction_details_view.dart' + as tdv; import '../../sub_widgets/view_only_wallet_data_widget.dart'; import 'cn_wallet_keys.dart'; import 'wallet_xprivs.dart'; @@ -323,7 +324,7 @@ class _FrostKeys extends StatelessWidget { detail: frostWalletData!.config, button: Util.isDesktop - ? IconCopyButton(data: frostWalletData!.config) + ? tdv.IconCopyButton(data: frostWalletData!.config) : SimpleCopyButton(data: frostWalletData!.config), ), const SizedBox(height: 16), @@ -332,7 +333,7 @@ class _FrostKeys extends StatelessWidget { detail: frostWalletData!.keys, button: Util.isDesktop - ? IconCopyButton(data: frostWalletData!.keys) + ? tdv.IconCopyButton(data: frostWalletData!.keys) : SimpleCopyButton(data: frostWalletData!.keys), ), if (prevGen) const SizedBox(height: 24), @@ -350,7 +351,7 @@ class _FrostKeys extends StatelessWidget { detail: frostWalletData!.prevGen!.config, button: Util.isDesktop - ? IconCopyButton( + ? tdv.IconCopyButton( data: frostWalletData!.prevGen!.config, ) : SimpleCopyButton( @@ -364,7 +365,7 @@ class _FrostKeys extends StatelessWidget { detail: frostWalletData!.prevGen!.keys, button: Util.isDesktop - ? IconCopyButton( + ? tdv.IconCopyButton( data: frostWalletData!.prevGen!.keys, ) : SimpleCopyButton( diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index 27e7cc63c2..75b121ff36 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -34,11 +34,13 @@ import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/coins/epiccash.dart'; import '../../../../wallets/crypto_currency/coins/litecoin.dart'; +import '../../../../wallets/crypto_currency/coins/mimblewimblecoin.dart'; import '../../../../wallets/crypto_currency/coins/monero.dart'; import '../../../../wallets/crypto_currency/coins/salvium.dart'; import '../../../../wallets/crypto_currency/coins/wownero.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; +import '../../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../../../wallets/wallet/impl/salvium_wallet.dart'; import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; @@ -277,6 +279,7 @@ class _WalletNetworkSettingsViewState coin is Wownero || coin is Epiccash || coin is Salvium || + coin is Mimblewimblecoin || (coin is Litecoin && ref.read(pWalletInfo(widget.walletId)).isMwebEnabled)) { _blocksRemainingSubscription = eventBus.on().listen( @@ -366,6 +369,14 @@ class _WalletNetworkSettingsViewState if (_percent < highestPercent) { _percent = highestPercent.clamp(0.0, 1.0); } + } else if (coin is Mimblewimblecoin) { + final double highestPercent = + (ref.watch(pWallets).getWallet(widget.walletId) + as MimblewimblecoinWallet) + .highestPercent; + if (_percent < highestPercent) { + _percent = highestPercent.clamp(0.0, 1.0); + } } return ConditionalParent( @@ -383,7 +394,11 @@ class _WalletNetworkSettingsViewState ), title: Text("Network", style: STextStyles.navBarTitle(context)), actions: [ - if (ref.watch(pWalletCoin(widget.walletId)) is! Epiccash) + if (ref.watch(pWalletCoin(widget.walletId)) is! Epiccash && + ref.watch(pWalletCoin(widget.walletId)) + is! Mimblewimblecoin || + ref.watch(pWalletCoin(widget.walletId)) + is! Mimblewimblecoin) Padding( padding: const EdgeInsets.only( top: 10, @@ -655,6 +670,7 @@ class _WalletNetworkSettingsViewState coin is Wownero || coin is Epiccash || coin is Salvium || + coin is Mimblewimblecoin || (coin is Litecoin && ref.watch( pWalletInfo( @@ -990,9 +1006,13 @@ class _WalletNetworkSettingsViewState coin: ref.watch(pWalletCoin(widget.walletId)), popBackToRoute: WalletNetworkSettingsView.routeName, ), - if (isDesktop && ref.watch(pWalletCoin(widget.walletId)) is! Epiccash) + if (isDesktop && + ref.watch(pWalletCoin(widget.walletId)) is! Epiccash && + ref.watch(pWalletCoin(widget.walletId)) is! Mimblewimblecoin) const SizedBox(height: 32), - if (isDesktop && ref.watch(pWalletCoin(widget.walletId)) is! Epiccash) + if (isDesktop && + ref.watch(pWalletCoin(widget.walletId)) is! Epiccash && + ref.watch(pWalletCoin(widget.walletId)) is! Mimblewimblecoin) Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( @@ -1006,7 +1026,9 @@ class _WalletNetworkSettingsViewState ], ), ), - if (isDesktop && ref.watch(pWalletCoin(widget.walletId)) is! Epiccash) + if (isDesktop && + ref.watch(pWalletCoin(widget.walletId)) is! Epiccash && + ref.watch(pWalletCoin(widget.walletId)) is! Mimblewimblecoin) RoundedWhiteContainer( borderColor: isDesktop diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 3d899add75..b1b4854511 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -18,6 +18,7 @@ import 'package:tuple/tuple.dart'; import '../../../db/hive/db.dart'; import '../../../db/sqlite/firo_cache.dart'; import '../../../models/epicbox_config_model.dart'; +import '../../../models/mwcmqs_config_model.dart'; import '../../../models/keys/key_data_interface.dart'; import '../../../models/keys/view_only_wallet_data.dart'; import '../../../notifications/show_flush_bar.dart'; @@ -39,6 +40,7 @@ import '../../../wallets/wallet/impl/bitcoin_frost_wallet.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; +import '../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; @@ -704,3 +706,103 @@ class _EpiBoxInfoFormState extends ConsumerState { ); } } + +class MwcMqsInfoForm extends ConsumerStatefulWidget { + const MwcMqsInfoForm({ + super.key, + required this.walletId, + }); + + final String walletId; + + @override + ConsumerState createState() => _MwcmqsInfoFormState(); +} + +class _MwcmqsInfoFormState extends ConsumerState { + final hostController = TextEditingController(); + final portController = TextEditingController(); + + late MimblewimblecoinWallet wallet; + + @override + void initState() { + wallet = + ref.read(pWallets).getWallet(widget.walletId) as MimblewimblecoinWallet; + + wallet.getMwcMqsConfig().then((MwcMqsConfigModel mwcmqsConfig) { + hostController.text = mwcmqsConfig.host; + portController.text = "${mwcmqsConfig.port ?? 443}"; + }); + super.initState(); + } + + @override + void dispose() { + hostController.dispose(); + portController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: hostController, + decoration: const InputDecoration(hintText: "Host"), + ), + const SizedBox( + height: 8, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: portController, + decoration: const InputDecoration(hintText: "Port"), + keyboardType: + Util.isDesktop ? null : const TextInputType.numberWithOptions(), + ), + const SizedBox( + height: 8, + ), + TextButton( + onPressed: () async { + try { + await wallet.updateMwcmqsConfig( + hostController.text, + int.parse(portController.text), + ); + if (mounted) { + await showFloatingFlushBar( + context: context, + message: "Mwcmqs info saved!", + type: FlushBarType.success, + ); + } + unawaited(wallet.refresh()); + } catch (e) { + await showFloatingFlushBar( + context: context, + message: "Failed to save mwcmqs info: $e", + type: FlushBarType.warning, + ); + } + }, + child: Text( + "Save", + style: STextStyles.button(context).copyWith( + color: + Theme.of(context).extension()!.accentColorDark, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart index 03ba8cdf0e..2bcf90827f 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart @@ -35,7 +35,8 @@ import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_dialog.dart'; import '../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; import '../../../home_view/home_view.dart'; -import '../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import '../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart' + as tdv; class DeleteWalletRecoveryPhraseView extends ConsumerStatefulWidget { const DeleteWalletRecoveryPhraseView({ @@ -236,7 +237,7 @@ class _DeleteWalletRecoveryPhraseViewState detail: widget.frostWalletData!.config, button: Util.isDesktop - ? IconCopyButton( + ? tdv.IconCopyButton( data: widget .frostWalletData! @@ -255,7 +256,7 @@ class _DeleteWalletRecoveryPhraseViewState detail: widget.frostWalletData!.keys, button: Util.isDesktop - ? IconCopyButton( + ? tdv.IconCopyButton( data: widget.frostWalletData!.keys, ) @@ -283,7 +284,7 @@ class _DeleteWalletRecoveryPhraseViewState .config, button: Util.isDesktop - ? IconCopyButton( + ? tdv.IconCopyButton( data: widget .frostWalletData! @@ -306,7 +307,7 @@ class _DeleteWalletRecoveryPhraseViewState widget.frostWalletData!.prevGen!.keys, button: Util.isDesktop - ? IconCopyButton( + ? tdv.IconCopyButton( data: widget .frostWalletData! diff --git a/lib/pages/spark_names/sub_widgets/spark_name_details.dart b/lib/pages/spark_names/sub_widgets/spark_name_details.dart index 97983735ab..372f7484e6 100644 --- a/lib/pages/spark_names/sub_widgets/spark_name_details.dart +++ b/lib/pages/spark_names/sub_widgets/spark_name_details.dart @@ -17,7 +17,8 @@ import '../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/dialogs/s_dialog.dart'; import '../../../widgets/rounded_container.dart'; -import '../../wallet_view/transaction_views/transaction_details_view.dart'; +import '../../wallet_view/transaction_views/transaction_details_view.dart' + as tvd; import '../buy_spark_name_view.dart'; class SparkNameDetailsView extends ConsumerStatefulWidget { @@ -288,7 +289,7 @@ class _SparkNameDetailsViewState extends ConsumerState { ), ), Util.isDesktop - ? IconCopyButton(data: name.address) + ? tvd.IconCopyButton(data: name.address) : SimpleCopyButton(data: name.address), ], ), @@ -344,7 +345,9 @@ class _SparkNameDetailsViewState extends ConsumerState { ), ), Util.isDesktop - ? IconCopyButton(data: label!.value) + ? tvd.IconCopyButton( + data: label!.value, + ) : SimpleCopyButton( data: label!.value, ), diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 9fcbfc3f47..397fd7d798 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -22,7 +22,6 @@ import '../../../models/isar/models/contact_entry.dart'; import '../../../models/isar/models/transaction_note.dart'; import '../../../models/transaction_filter.dart'; import '../../../notifications/show_flush_bar.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/address_book_service_provider.dart'; import '../../../providers/providers.dart'; import '../../../providers/ui/transaction_filter_provider.dart'; @@ -49,7 +48,7 @@ import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; import '../../../widgets/transaction_card.dart'; import '../sub_widgets/tx_icon.dart'; -import 'transaction_details_view.dart'; +import 'transaction_details_view.dart' as tvd; import 'transaction_search_filter_view.dart'; typedef _GroupedTransactions = @@ -853,6 +852,10 @@ class _DesktopTransactionCardRowState return "Restored Funds"; } + if (coin is Mimblewimblecoin && _transaction.slateId == null) { + return "Restored Funds"; + } + if (_transaction.subType == TransactionSubType.mint) { if (_transaction.isConfirmed(height, minConfirms)) { return "Anonymized"; @@ -954,6 +957,19 @@ class _DesktopTransactionCardRowState ); return; } + + if (coin is Mimblewimblecoin && _transaction.slateId == null) { + unawaited( + showFloatingFlushBar( + context: context, + message: + "Restored Mimblewimblecoin funds from your Seed have no Data.", + type: FlushBarType.warning, + duration: const Duration(seconds: 5), + ), + ); + return; + } if (Util.isDesktop) { await showDialog( context: context, @@ -961,7 +977,7 @@ class _DesktopTransactionCardRowState (context) => DesktopDialog( maxHeight: MediaQuery.of(context).size.height - 64, maxWidth: 580, - child: TransactionDetailsView( + child: tvd.TransactionDetailsView( transaction: _transaction, coin: coin, walletId: walletId, @@ -971,7 +987,7 @@ class _DesktopTransactionCardRowState } else { unawaited( Navigator.of(context).pushNamed( - TransactionDetailsView.routeName, + tvd.TransactionDetailsView.routeName, arguments: Tuple3(_transaction, coin, walletId), ), ); diff --git a/lib/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart b/lib/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart index f1824d17e6..da9c45a68f 100644 --- a/lib/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart +++ b/lib/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart @@ -9,7 +9,10 @@ */ import 'package:flutter/material.dart'; + +import '../../../../utilities/util.dart'; import '../../../../widgets/animated_widgets/rotating_arrows.dart'; +import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/stack_dialog.dart'; class CancellingTransactionProgressDialog extends StatefulWidget { @@ -28,28 +31,33 @@ class _CancellingTransactionProgressDialogState onWillPop: () async { return false; }, - child: const StackDialog( - title: "Cancelling transaction", - message: "This may take a while. Please do not exit this screen.", - icon: RotatingArrows( - width: 24, - height: 24, + child: ConditionalParent( + condition: Util.isDesktop, + builder: + (child) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [SizedBox(width: 400, child: child)], + ), + child: const StackDialog( + title: "Cancelling transaction", + message: "This may take a while. Please do not exit this screen.", + icon: RotatingArrows(width: 24, height: 24), + // rightButton: TextButton( + // style: Theme.of(context).textButtonTheme.style?.copyWith( + // backgroundColor: MaterialStateProperty.all( + // CFColors.buttonGray, + // ), + // ), + // child: Text( + // "Cancel", + // style: STextStyles.itemSubtitle12(context), + // ), + // onPressed: () { + // Navigator.of(context).pop(); + // onCancel.call(); + // }, + // ), ), - // rightButton: TextButton( - // style: Theme.of(context).textButtonTheme.style?.copyWith( - // backgroundColor: MaterialStateProperty.all( - // CFColors.buttonGray, - // ), - // ), - // child: Text( - // "Cancel", - // style: STextStyles.itemSubtitle12(context), - // ), - // onPressed: () { - // Navigator.of(context).pop(); - // onCancel.call(); - // }, - // ), ), ); } diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 7dc810ac80..e1b2c89f0e 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -16,6 +16,7 @@ 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:stackwallet/wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import 'package:tuple/tuple.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -1586,6 +1587,10 @@ class _TransactionDetailsViewState "Could not open in block explorer", message: "Failed to open \"${uri.toString()}\"", + maxWidth: + Util.isDesktop + ? 400 + : null, ), ), ); @@ -1770,7 +1775,7 @@ class _TransactionDetailsViewState ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: - (coin is Epiccash && + ((coin is Epiccash || coin is Mimblewimblecoin) && _transaction.getConfirmations(currentHeight) < 1 && _transaction.isCancelled == false) ? ConditionalParent( @@ -1819,6 +1824,65 @@ class _TransactionDetailsViewState final result = await wallet .cancelPendingTransactionAndPost(id); + + if (context.mounted) { + // pop progress dialog + Navigator.of(context).pop(); + + if (result.isEmpty) { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Transaction cancelled", + maxWidth: Util.isDesktop ? 400 : null, + onOkPressed: (_) { + Navigator.of(context).popUntil( + ModalRoute.withName( + WalletView.routeName, + ), + ); + }, + ), + ); + } else { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Failed to cancel transaction", + message: result, + maxWidth: Util.isDesktop ? 400 : null, + ), + ); + } + } + } else if (wallet is MimblewimblecoinWallet) { + final String? id = _transaction.slateId; + if (id == null) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Could not find MWC transaction ID", + context: context, + ), + ); + return; + } + + unawaited( + showDialog( + barrierDismissible: false, + context: context, + builder: + (_) => + const CancellingTransactionProgressDialog(), + ), + ); + + final result = await wallet + .cancelPendingTransactionAndPost(id); + if (context.mounted) { // pop progress dialog Navigator.of(context).pop(); @@ -1829,8 +1893,8 @@ class _TransactionDetailsViewState builder: (_) => StackOkDialog( title: "Transaction cancelled", + maxWidth: Util.isDesktop ? 400 : null, onOkPressed: (_) { - wallet.refresh(); Navigator.of(context).popUntil( ModalRoute.withName( WalletView.routeName, @@ -1846,6 +1910,7 @@ class _TransactionDetailsViewState (_) => StackOkDialog( title: "Failed to cancel transaction", message: result, + maxWidth: Util.isDesktop ? 400 : null, ), ); } @@ -1854,7 +1919,8 @@ class _TransactionDetailsViewState unawaited( showFloatingFlushBar( type: FlushBarType.warning, - message: "ERROR: Wallet type is not Epic Cash", + message: + "ERROR: Wallet type is not Epic Cash or MimbleWimbleCoin", context: context, ), ); diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart index 3532646000..b4b95f1cb1 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart @@ -21,7 +21,6 @@ import '../../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../../models/isar/models/contact_entry.dart'; import '../../../../models/isar/models/isar_models.dart'; import '../../../../models/transaction_filter.dart'; -import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/global/address_book_service_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../providers/ui/transaction_filter_provider.dart'; @@ -51,7 +50,7 @@ import '../../../../widgets/textfield_icon_button.dart'; import '../../sub_widgets/tx_icon.dart'; import '../transaction_search_filter_view.dart'; import 'transaction_v2_card.dart'; -import 'transaction_v2_details_view.dart'; +import 'transaction_v2_details_view.dart' as tvd; typedef _GroupedTransactions = ({String label, DateTime startDate, List transactions}); @@ -992,7 +991,7 @@ class _DesktopTransactionCardRowState (context) => DesktopDialog( maxHeight: MediaQuery.of(context).size.height - 64, maxWidth: 580, - child: TransactionV2DetailsView( + child: tvd.TransactionV2DetailsView( transaction: _transaction, coin: coin, walletId: walletId, @@ -1002,7 +1001,7 @@ class _DesktopTransactionCardRowState } else { unawaited( Navigator.of(context).pushNamed( - TransactionV2DetailsView.routeName, + tvd.TransactionV2DetailsView.routeName, arguments: (tx: _transaction, coin: coin, walletId: walletId), ), ); diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart index 3fb0aaaab4..a8b30fd5ae 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart @@ -25,7 +25,7 @@ import '../../../../widgets/coin_ticker_tag.dart'; import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../sub_widgets/tx_icon.dart'; -import 'transaction_v2_details_view.dart'; +import 'transaction_v2_details_view.dart' as tvd; class TransactionCardV2 extends ConsumerStatefulWidget { const TransactionCardV2({super.key, required this.transaction}); @@ -200,7 +200,7 @@ class _TransactionCardStateV2 extends ConsumerState { (context) => DesktopDialog( maxHeight: MediaQuery.of(context).size.height - 64, maxWidth: 580, - child: TransactionV2DetailsView( + child: tvd.TransactionV2DetailsView( transaction: _transaction, coin: coin, walletId: walletId, @@ -210,7 +210,7 @@ class _TransactionCardStateV2 extends ConsumerState { } else { unawaited( Navigator.of(context).pushNamed( - TransactionV2DetailsView.routeName, + tvd.TransactionV2DetailsView.routeName, arguments: (tx: _transaction, coin: coin, walletId: walletId), ), ); diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart index c2a7e9adf4..2f2bbf83b4 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart @@ -42,6 +42,7 @@ import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../../../../wallets/isar/models/spark_coin.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; +import '../../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; @@ -273,6 +274,35 @@ class _TransactionV2DetailsViewState ), ) .toList(); + } else if (_transaction.isMimblewimblecoinTransaction) { + switch (_transaction.type) { + case TransactionType.outgoing: + case TransactionType.unknown: + amount = _transaction.getAmountSentFromThisWallet( + fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, + ); + break; + + case TransactionType.incoming: + case TransactionType.sentToSelf: + amount = _transaction.getAmountReceivedInThisWallet( + fractionDigits: fractionDigits, + ); + break; + } + data = + _transaction.outputs + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ), + ), + ) + .toList(); } else { switch (_transaction.type) { case TransactionType.outgoing: @@ -1118,11 +1148,11 @@ class _TransactionV2DetailsViewState ], ), ), - if (coin is Epiccash) + if (coin is Epiccash || coin is Mimblewimblecoin) isDesktop ? const _Divider() : const SizedBox(height: 12), - if (coin is Epiccash) + if (coin is Epiccash || coin is Mimblewimblecoin) RoundedWhiteContainer( padding: isDesktop @@ -1195,7 +1225,8 @@ class _TransactionV2DetailsViewState MainAxisAlignment.spaceBetween, children: [ Text( - (coin is Epiccash) + (coin is Epiccash || + coin is Mimblewimblecoin) ? "Local Note" : "Note ", style: @@ -1266,7 +1297,9 @@ class _TransactionV2DetailsViewState .watch( pTransactionNote(( txid: - (coin is Epiccash) + (coin is Epiccash || + coin + is Mimblewimblecoin) ? _transaction.slateId .toString() : _transaction.txid, @@ -2067,9 +2100,11 @@ class _TransactionV2DetailsViewState context, ), ), - if (coin is! Epiccash) + if (coin is! Epiccash && + coin is! Mimblewimblecoin) const SizedBox(height: 8), - if (coin is! Epiccash) + if (coin is! Epiccash && + coin is! Mimblewimblecoin) CustomTextButton( text: "Open in block explorer", @@ -2122,6 +2157,10 @@ class _TransactionV2DetailsViewState "Could not open in block explorer", message: "Failed to open \"${uri.toString()}\"", + maxWidth: + Util.isDesktop + ? 400 + : null, ), ), ); @@ -2229,11 +2268,11 @@ class _TransactionV2DetailsViewState // ], // ), // ), - if (coin is Epiccash) + if (coin is Epiccash || coin is Mimblewimblecoin) isDesktop ? const _Divider() : const SizedBox(height: 12), - if (coin is Epiccash) + if (coin is Epiccash || coin is Mimblewimblecoin) RoundedWhiteContainer( padding: isDesktop @@ -2313,13 +2352,17 @@ class _TransactionV2DetailsViewState ), ), ), + if ((coin is Epiccash || coin is Mimblewimblecoin) && + _transaction.getConfirmations(currentHeight) < 1 && + _transaction.isCancelled == false) + const SizedBox(height: 40), ], ), ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: - (coin is Epiccash && + ((coin is Epiccash || coin is Mimblewimblecoin) && _transaction.getConfirmations(currentHeight) < 1 && _transaction.isCancelled == false) ? ConditionalParent( @@ -2366,6 +2409,64 @@ class _TransactionV2DetailsViewState ), ); + final result = await wallet + .cancelPendingTransactionAndPost(id); + if (context.mounted) { + // Pop progress dialog. + Navigator.of(context).pop(); + + if (result.isEmpty) { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Transaction cancelled", + onOkPressed: (_) { + Navigator.of(context).popUntil( + ModalRoute.withName( + WalletView.routeName, + ), + ); + }, + maxWidth: Util.isDesktop ? 400 : null, + ), + ); + unawaited(wallet.refresh()); + } else { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Failed to cancel transaction", + message: result, + maxWidth: Util.isDesktop ? 400 : null, + ), + ); + } + } + } else if (wallet is MimblewimblecoinWallet) { + final String? id = _transaction.slateId; + if (id == null) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Could not find MWC transaction ID", + context: context, + ), + ); + return; + } + + unawaited( + showDialog( + barrierDismissible: false, + context: context, + builder: + (_) => + const CancellingTransactionProgressDialog(), + ), + ); + final result = await wallet .cancelPendingTransactionAndPost(id); if (context.mounted) { @@ -2379,15 +2480,16 @@ class _TransactionV2DetailsViewState (_) => StackOkDialog( title: "Transaction cancelled", onOkPressed: (_) { - wallet.refresh(); Navigator.of(context).popUntil( ModalRoute.withName( WalletView.routeName, ), ); }, + maxWidth: Util.isDesktop ? 400 : null, ), ); + unawaited(wallet.refresh()); } else { await showDialog( context: context, @@ -2395,6 +2497,7 @@ class _TransactionV2DetailsViewState (_) => StackOkDialog( title: "Failed to cancel transaction", message: result, + maxWidth: Util.isDesktop ? 400 : null, ), ); } @@ -2403,7 +2506,8 @@ class _TransactionV2DetailsViewState unawaited( showFloatingFlushBar( type: FlushBarType.warning, - message: "ERROR: Wallet type is not Epic Cash", + message: + "ERROR: Wallet type is not Epic Cash or MimbleWimbleCoin", context: context, ), ); diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index a479ae4eb7..c61340a4b5 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -51,6 +51,7 @@ import '../../wallets/crypto_currency/intermediate/frost_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/impl/bitcoin_frost_wallet.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; +import '../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; @@ -75,6 +76,7 @@ import '../../widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart'; import '../../widgets/wallet_navigation_bar/components/icons/churn_nav_icon.dart'; import '../../widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart'; import '../../widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart'; +import '../../widgets/wallet_navigation_bar/components/icons/finalize_nav_icon.dart'; import '../../widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart'; import '../../widgets/wallet_navigation_bar/components/icons/fusion_nav_icon.dart'; import '../../widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart'; @@ -88,6 +90,7 @@ import '../cashfusion/cashfusion_view.dart'; import '../churning/churning_view.dart'; import '../coin_control/coin_control_view.dart'; import '../exchange_view/wallet_initiated_exchange_view.dart'; +import '../finalize_view/finalize_view.dart'; import '../monkey/monkey_view.dart'; import '../namecoin_names/namecoin_names_home_view.dart'; import '../notification_views/notifications_view.dart'; @@ -1015,6 +1018,21 @@ class _WalletViewState extends ConsumerState { } }, ), + if (wallet is MimblewimblecoinWallet) + WalletNavigationBarItemData( + label: "Finalize", + icon: const FinalizeNavIcon(), + onTap: () { + if (mounted) { + unawaited( + Navigator.of(context).pushNamed( + FinalizeView.routeName, + arguments: walletId, + ), + ); + } + }, + ), if (ref.watch(pWalletCoin(walletId)) is FrostCurrency) WalletNavigationBarItemData( label: "Sign", diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 5cea18ff26..f926cce6e8 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -22,7 +22,9 @@ import '../../../../models/isar/models/isar_models.dart'; import '../../../../models/keys/view_only_wallet_data.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/receive_view/generate_receiving_uri_qr_code_view.dart'; +import '../../../../pages/receive_view/sub_widgets/mwc_slatepack_import_dialog.dart'; import '../../../../providers/providers.dart'; +import '../../../../providers/ui/preview_tx_button_state_provider.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/address_utils.dart'; @@ -30,6 +32,7 @@ import '../../../../utilities/assets.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../../utilities/enums/mwc_transaction_method.dart'; import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; @@ -37,6 +40,7 @@ import '../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/impl/bitcoin_wallet.dart'; +import '../../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; @@ -48,9 +52,17 @@ import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_loading_overlay.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; +import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart'; +import '../../../../widgets/dialogs/s_dialog.dart'; +import '../../../../widgets/icon_widgets/clipboard_icon.dart'; +import '../../../../widgets/icon_widgets/x_icon.dart'; +import '../../../../widgets/mwc_txs_method_toggle.dart'; import '../../../../widgets/qr.dart'; import '../../../../widgets/rounded_white_container.dart'; +import '../../../../widgets/stack_dialog.dart'; +import '../../../../widgets/stack_text_field.dart'; +import '../../../../widgets/textfield_icon_button.dart'; class DesktopReceive extends ConsumerStatefulWidget { const DesktopReceive({ @@ -75,6 +87,11 @@ class _DesktopReceiveState extends ConsumerState { late final bool supportsSpark; late bool supportsMweb; late final bool showMultiType; + late final bool isMimblewimblecoin; + late TextEditingController _receiveSlateController; + String? _slate; + bool _slateToggleFlag = false; + final _slateFocusNode = FocusNode(); int _currentIndex = 0; @@ -82,6 +99,81 @@ class _DesktopReceiveState extends ConsumerState { final Map _addressMap = {}; final Map> _addressSubMap = {}; + Future _pasteSlatepack() async { + final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); + if (data?.text != null && data!.text!.isNotEmpty) { + _slate = data.text; + _receiveSlateController.text = _slate!; + setState(() { + _slateToggleFlag = _receiveSlateController.text.isNotEmpty; + }); + } + } + + Future _onReceiveSlatePressed() async { + final wallet = + ref.read(pWallets).getWallet(walletId) as MimblewimblecoinWallet; + + Exception? ex; + final result = await showLoading( + whileFuture: wallet.fullDecodeSlatepack(_receiveSlateController.text), + context: context, + message: "Decoding slatepack...", + rootNavigator: Util.isDesktop, + onException: (e) => ex = e, + ); + + if (result == null || ex != null) { + if (mounted) { + await showDialog( + context: context, + useRootNavigator: true, + builder: + (context) => StackOkDialog( + desktopPopRootNavigator: true, + title: "Slatepack receive error", + message: + ex?.toString() ?? "Unexpected result without exception", + maxWidth: 400, + ), + ); + } + return; + } + + if (mounted) { + final response = + await showDialog<({String responseSlatepack, bool wasEncrypted})>( + context: context, + builder: + (context) => SDialog( + child: SizedBox( + width: 700, + child: MwcSlatepackImportDialog( + walletId: widget.walletId, + clipboard: widget.clipboard, + rawSlatepack: result.raw, + decoded: result.result, + slatepackType: result.type, + ), + ), + ), + ); + + if (mounted && response != null) { + await showDialog( + context: context, + barrierDismissible: false, + builder: + (context) => SlatepackResponseDialog( + responseSlatepack: response.responseSlatepack, + wasEncrypted: response.wasEncrypted, + ), + ); + } + } + } + Future generateNewAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is MultiAddressInterface) { @@ -227,6 +319,7 @@ class _DesktopReceiveState extends ConsumerState { @override void initState() { + _receiveSlateController = TextEditingController(); walletId = widget.walletId; coin = ref.read(pWalletInfo(walletId)).coin; clipboard = widget.clipboard; @@ -237,6 +330,8 @@ class _DesktopReceiveState extends ConsumerState { !wallet.info.isViewOnly && wallet.info.isMwebEnabled; + isMimblewimblecoin = wallet is MimblewimblecoinWallet; + if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { showMultiType = false; } else { @@ -308,6 +403,7 @@ class _DesktopReceiveState extends ConsumerState { @override void dispose() { + _receiveSlateController.dispose(); for (final subscription in _addressSubMap.values) { subscription.cancel(); } @@ -393,164 +489,199 @@ class _DesktopReceiveState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ConditionalParent( - condition: showMultiType, - builder: - (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - DropdownButtonHideUnderline( - child: DropdownButton2( - value: _currentIndex, - items: [ - for (int i = 0; i < _walletAddressTypes.length; i++) - DropdownMenuItem( - value: i, - child: Text( - supportsSpark && - _walletAddressTypes[i] == - AddressType.p2pkh - ? "Transparent address" - : "${_walletAddressTypes[i].readableName} address", - style: STextStyles.w500_14(context), + const SizedBox(height: 4), + if (isMimblewimblecoin) + Padding( + padding: const EdgeInsets.all(0), + child: Container( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()?.textFieldDefaultBG ?? + Colors.white, // Fallback color + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: + Theme.of( + context, + ).extension()?.backgroundAppBar ?? + Colors.grey, // Fallback color + width: 1, + ), + ), + child: const SizedBox( + height: + 60, // Provide an explicit height to avoid infinite constraints + child: MwcTxsMethodToggle(), + ), + ), + ), + if (!(isMimblewimblecoin && + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack)) + const SizedBox(height: 20), + if (!(isMimblewimblecoin && + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack)) + ConditionalParent( + condition: showMultiType, + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DropdownButtonHideUnderline( + child: DropdownButton2( + value: _currentIndex, + items: [ + for (int i = 0; i < _walletAddressTypes.length; i++) + DropdownMenuItem( + value: i, + child: Text( + supportsSpark && + _walletAddressTypes[i] == + AddressType.p2pkh + ? "Transparent address" + : "${_walletAddressTypes[i].readableName} address", + style: STextStyles.w500_14(context), + ), + ), + ], + onChanged: (value) { + if (value != null && value != _currentIndex) { + setState(() { + _currentIndex = value; + }); + } + }, + isExpanded: true, + iconStyleData: IconStyleData( + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ), - ], - onChanged: (value) { - if (value != null && value != _currentIndex) { - setState(() { - _currentIndex = value; - }); - } - }, - isExpanded: true, - iconStyleData: IconStyleData( - icon: Padding( - padding: const EdgeInsets.only(right: 10), - child: SvgPicture.asset( - Assets.svg.chevronDown, - width: 12, - height: 6, + ), + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( color: - Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), ), - ), - buttonStyleData: ButtonStyleData( - decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + dropdownStyleData: DropdownStyleData( + offset: const Offset(0, -10), + elevation: 0, + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), ), - ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, -10), - elevation: 0, - decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, ), ), ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - ), ), - ), - const SizedBox(height: 12), - child, - ], - ), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - clipboard.setData(ClipboardData(text: address)); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: - Theme.of( - context, - ).extension()!.backgroundAppBar, - width: 1, - ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + const SizedBox(height: 12), + child, + ], ), - child: RoundedWhiteContainer( - child: Column( - children: [ - Row( - children: [ - Text( - "Your ${widget.contractAddress == null ? coin.ticker : ref.watch(pCurrentTokenWallet.select((value) => value!.tokenContract.symbol))} address", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 15, - height: 15, - color: - Theme.of( - context, - ).extension()!.infoItemIcons, - ), - const SizedBox(width: 4), - Text("Copy", style: STextStyles.link2(context)), - ], - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: Text( - address, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of( - context, - ).extension()!.textDark, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + clipboard.setData(ClipboardData(text: address)); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, + width: 1, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: RoundedWhiteContainer( + child: Column( + children: [ + Row( + children: [ + Text( + "Your ${widget.contractAddress == null ? coin.ticker : ref.watch(pCurrentTokenWallet.select((value) => value!.tokenContract.symbol))} address", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 15, + height: 15, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, + ), + const SizedBox(width: 4), + Text("Copy", style: STextStyles.link2(context)), + ], + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Text( + address, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ), ), ), - ), if (canGen) const SizedBox(height: 20), @@ -567,90 +698,229 @@ class _DesktopReceiveState extends ConsumerState { : generateNewAddress, label: "Generate new address", ), - const SizedBox(height: 32), - Center( - child: QR( - data: AddressUtils.buildUriString(coin.uriScheme, address, {}), - size: 200, + const SizedBox(height: 20), + if (isMimblewimblecoin && + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Label Text + Text( + "Receive Slatepack", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 5, + key: const Key("receiveViewSlatepackFieldKey"), + controller: _receiveSlateController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + _slate = newValue; + setState(() { + _slateToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _slateFocusNode, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Enter Slatepack Message", + _slateFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: + 12, // Adjust vertical padding for better alignment + ), + suffixIcon: Padding( + padding: + _receiveSlateController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _slateToggleFlag + ? TextFieldIconButton( + key: const Key( + "receiveViewClearSlatepackFieldButtonKey", + ), + onTap: () { + _receiveSlateController.text = ""; + _slate = ""; + setState(() { + _slateToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "receiveViewPasteSlatepackFieldButtonKey", + ), + onTap: _pasteSlatepack, + child: + _receiveSlateController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + ], + ) + //Padding( + // padding: const EdgeInsets.symmetric(vertical: 8.0), + // child: TextField( + // maxLines: 8, // Set to a higher number to make the height larger + // minLines: 5, // Allow it to shrink if input is small + // decoration: InputDecoration( + // labelText: 'Enter Slatepack Message', + // alignLabelWithHint: true, + // border: OutlineInputBorder( + // borderRadius: BorderRadius.circular(8), + // ), + // filled: true, + // fillColor: Theme.of(context).extension()?.textFieldDefaultBG ?? Colors.white, + // contentPadding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 12.0), + // ), + // onChanged: (value) { + // // Handle text input changes (e.g., store in a state variable) + // debugPrint('Slatepack Message: $value'); + // }, + // ), + //) + else + Center( + child: QR( + data: AddressUtils.buildUriString(coin.uriScheme, address, {}), + size: 200, + ), ), - ), const SizedBox(height: 32), + // TODO: create transparent button class to account for hover - GestureDetector( - onTap: () async { - if (Util.isDesktop) { - await showDialog( - context: context, - builder: - (context) => DesktopDialog( - maxHeight: double.infinity, - maxWidth: 580, - child: Column( - children: [ - Row( - children: [ - const AppBarBackButton(size: 40, iconSize: 24), - Text( - "Generate QR code", - style: STextStyles.desktopH3(context), - ), - ], - ), - IntrinsicHeight( - child: Navigator( - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: - (_, __) => [ - RouteGenerator.generateRoute( - RouteSettings( - name: GenerateUriQrCodeView.routeName, - arguments: Tuple2(coin, address), + // Conditional logic for 'Submit' button or QR code + if (isMimblewimblecoin && + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Receive Slatepack", + enabled: _slateToggleFlag, + onPressed: _slateToggleFlag ? _onReceiveSlatePressed : null, + ), + ) + else + GestureDetector( + onTap: () async { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: + (context) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + children: [ + Row( + children: [ + const AppBarBackButton(size: 40, iconSize: 24), + Text( + "Generate QR code", + style: STextStyles.desktopH3(context), + ), + ], + ), + IntrinsicHeight( + child: Navigator( + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: + (_, __) => [ + RouteGenerator.generateRoute( + RouteSettings( + name: GenerateUriQrCodeView.routeName, + arguments: Tuple2(coin, address), + ), ), - ), - ], + ], + ), ), + ], + ), + ), + ); + } else { + unawaited( + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: + (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: address, ), - ], + settings: const RouteSettings( + name: GenerateUriQrCodeView.routeName, ), ), - ); - } else { - unawaited( - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: - (_) => GenerateUriQrCodeView( - coin: coin, - receivingAddress: address, - ), - settings: const RouteSettings( - name: GenerateUriQrCodeView.routeName, - ), ), - ), - ); - } - }, - child: Container( - color: Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.svg.qrcode, - width: 14, - height: 16, - color: - Theme.of( - context, - ).extension()!.accentColorBlue, - ), - const SizedBox(width: 8), - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Text( + ); + } + }, + child: Container( + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.qrcode, + width: 14, + height: 16, + color: + Theme.of( + context, + ).extension()!.accentColorBlue, + ), + const SizedBox(width: 8), + Text( "Create new QR code", style: STextStyles.desktopTextExtraSmall(context).copyWith( color: @@ -659,11 +929,10 @@ class _DesktopReceiveState extends ConsumerState { ).extension()!.accentColorBlue, ), ), - ), - ], + ], + ), ), ), - ), ], ); } 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 b4c183c43b..ac30df24ea 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 @@ -20,10 +20,12 @@ import 'package:flutter_svg/flutter_svg.dart'; import '../../../../models/isar/models/blockchain_data/address.dart'; import '../../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../../models/isar/models/contact_entry.dart'; +import '../../../../models/mwc_slatepack_models.dart'; import '../../../../models/paynym/paynym_account_lite.dart'; import '../../../../models/send_view_auto_fill_data.dart'; import '../../../../pages/send_view/confirm_transaction_view.dart'; import '../../../../pages/send_view/sub_widgets/building_transaction_dialog.dart'; +import '../../../../pages/send_view/sub_widgets/mwc_slatepack_dialog.dart'; import '../../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; import '../../../../providers/providers.dart'; import '../../../../providers/ui/fee_rate_type_state_provider.dart'; @@ -40,8 +42,10 @@ import '../../../../utilities/amount/amount_unit.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; +import '../../../../utilities/enums/mwc_transaction_method.dart'; import '../../../../utilities/logger.dart'; import '../../../../utilities/prefs.dart'; +import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; @@ -49,6 +53,7 @@ import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/models/tx_data.dart'; import '../../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; @@ -65,6 +70,7 @@ import '../../../../widgets/icon_widgets/addressbook_icon.dart'; import '../../../../widgets/icon_widgets/clipboard_icon.dart'; import '../../../../widgets/icon_widgets/qrcode_icon.dart'; import '../../../../widgets/icon_widgets/x_icon.dart'; +import '../../../../widgets/mwc_txs_method_toggle.dart'; import '../../../../widgets/rounded_container.dart'; import '../../../../widgets/stack_text_field.dart'; import '../../../../widgets/textfield_icon_button.dart'; @@ -112,6 +118,7 @@ class _DesktopSendState extends ConsumerState { final _nonceFocusNode = FocusNode(); late final bool isStellar; + late final bool isMimblewimblecoin; String? _note; String? _onChainNote; @@ -160,9 +167,145 @@ class _DesktopSendState extends ConsumerState { } } + /// Handle MWC slatepack creation for desktop. + Future _handleDesktopSlatepackCreation( + MimblewimblecoinWallet wallet, + ) async { + try { + final amount = ref.read(pSendAmount)!; + + Future wrappedFutureWithDelay() async { + await Future.delayed(const Duration(seconds: 1)); + return wallet.createSlatepack( + amount: amount, + recipientAddress: null, // No specific recipient for manual slatepack. + message: _onChainNote?.isNotEmpty == true ? _onChainNote : null, + encrypt: false, // No encryption without recipient address. + ); + } + + // Create slatepack. + Exception? ex; + final slatepackResult = await showLoading( + whileFuture: wrappedFutureWithDelay(), + context: context, + rootNavigator: true, + message: "Building slatepack...", + delay: const Duration(seconds: 2), + onException: (e) => ex = e, + ); + + if (slatepackResult == null || + !slatepackResult.success || + slatepackResult.slatepack == null || + ex != null) { + String error = + ex?.toString() ?? + slatepackResult?.error ?? + 'Failed to create slatepack'; + if (error.startsWith("Exception:")) { + error = error.replaceFirst("Exception:", "").trim(); + } + throw Exception(error); + } + + // refresh asap to show the pending slate tx in history + unawaited(() async { + await Future.delayed(Duration.zero); + await wallet.refresh(); + }()); + + // Show slatepack dialog. + if (mounted) { + await showDialog( + context: context, + barrierDismissible: false, + builder: + (context) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 700, + child: MwcSlatepackDialog(slatepackResult: slatepackResult), + ), + ); + + // Clear form after slatepack dialog is closed. + clearSendForm(); + } + } catch (e, s) { + Logging.instance.e( + 'Failed to create MWC slatepack on desktop', + error: e, + stackTrace: s, + ); + + if (mounted) { + await showDialog( + context: context, + builder: + (context) => DesktopDialog( + maxWidth: 450, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only(left: 32, bottom: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Slatepack Creation Failed', + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.only(right: 32), + child: Text( + 'Failed to create slatepack: $e', + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith(fontSize: 18), + ), + ), + const SizedBox(height: 40), + Padding( + padding: const EdgeInsets.only(right: 32), + child: Row( + children: [ + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: 'OK', + onPressed: () => Navigator.of(context).pop(), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + } + } + Future previewSend() async { final wallet = ref.read(pWallets).getWallet(walletId); + // Handle MWC slatepack transactions directly. + if (isMimblewimblecoin && + ref.read(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack) { + await _handleDesktopSlatepackCreation(wallet as MimblewimblecoinWallet); + return; + } + final Amount amount = ref.read(pSendAmount)!; final Amount availableBalance; if (coin is Firo || ref.read(pWalletInfo(walletId)).isMwebEnabled) { @@ -477,6 +620,9 @@ class _DesktopSendState extends ConsumerState { if (coin is Epiccash) { txData = txData.copyWith(noteOnChain: _onChainNote ?? ""); } + if (coin is Mimblewimblecoin) { + txData = txData.copyWith(noteOnChain: _onChainNote ?? ""); + } } // pop building dialog Navigator.of(context, rootNavigator: true).pop(); @@ -775,6 +921,9 @@ class _DesktopSendState extends ConsumerState { if (coin is Epiccash) { content = AddressUtils().formatEpicCashAddress(content); } + if (coin is Mimblewimblecoin) { + content = AddressUtils().formatAddressMwc(content); + } sendToController.text = content; _address = content; @@ -790,6 +939,10 @@ class _DesktopSendState extends ConsumerState { // strip http:// and https:// if content contains @ content = AddressUtils().formatEpicCashAddress(content); } + if (coin is Mimblewimblecoin) { + // strip http:// and https:// if content contains @ + content = AddressUtils().formatAddressMwc(content); + } await _checkSparkNameAndOrSetAddress(content); @@ -932,6 +1085,7 @@ class _DesktopSendState extends ConsumerState { clipboard = widget.clipboard; isStellar = coin is Stellar; + isMimblewimblecoin = coin is Mimblewimblecoin; sendToController = TextEditingController(); cryptoAmountController = TextEditingController(); @@ -1026,6 +1180,21 @@ class _DesktopSendState extends ConsumerState { final balType = ref.watch(publicPrivateBalanceStateProvider); + if (coin is Mimblewimblecoin) { + sendToController.addListener(() { + _address = sendToController.text; + + if (_address != null && _address!.isNotEmpty) { + _address = _address!.trim(); + if (_address!.contains("\n")) { + _address = _address!.substring(0, _address!.indexOf("\n")); + } + + sendToController.text = formatAddressMwc(_address!); + } + }); + } + final isMwebEnabled = ref.watch( pWalletInfo(walletId).select((s) => s.isMwebEnabled), ); @@ -1059,7 +1228,36 @@ class _DesktopSendState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), - if (showPrivateBalance) + if (showPrivateBalance) const SizedBox(height: 4), + if (isMimblewimblecoin) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Container( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()?.textFieldDefaultBG ?? + Colors.white, // Fallback color + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: + Theme.of( + context, + ).extension()?.backgroundAppBar ?? + Colors.grey, // Fallback color + width: 1, + ), + ), + child: const SizedBox( + height: + 60, // Provide an explicit height to avoid infinite constraints + child: MwcTxsMethodToggle(), + ), + ), + ), + + if (coin is Firo) Text( "Send from", style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -1358,7 +1556,10 @@ class _DesktopSendState extends ConsumerState { ), ), const SizedBox(height: 20), - if (!isPaynymSend) + if (!isPaynymSend && + !(isMimblewimblecoin && + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack)) Text( "Send to", style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -1369,8 +1570,15 @@ class _DesktopSendState extends ConsumerState { ), textAlign: TextAlign.left, ), - if (!isPaynymSend) const SizedBox(height: 10), - if (!isPaynymSend) + if (!isPaynymSend && + !(isMimblewimblecoin && + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack)) + const SizedBox(height: 10), + if (!isPaynymSend && + !(isMimblewimblecoin && + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack)) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1428,7 +1636,10 @@ class _DesktopSendState extends ConsumerState { height: 1.8, ), decoration: standardInputDecoration( - "Enter ${coin.ticker} address", + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack + ? "Enter ${coin.ticker} address (optional)" + : "Enter ${coin.ticker} address", _addressFocusNode, context, desktopMed: true, @@ -1546,7 +1757,10 @@ class _DesktopSendState extends ConsumerState { ), ), ), - if (!isPaynymSend) + if (!isPaynymSend && + !(isMimblewimblecoin && + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack)) Builder( builder: (_) { final String? error; @@ -1565,6 +1779,12 @@ class _DesktopSendState extends ConsumerState { } else { if (_data != null && _data.contactLabel == _address) { error = null; + } else if (coin is Mimblewimblecoin && + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack) { + // For MWC slatepack transactions, address validation is not required. + // TODO: When implementing encrypted slatepacks, address validation will be required. + error = null; } else if (!ref.watch(pValidSendToAddress)) { error = "Invalid address"; } else { @@ -1659,7 +1879,10 @@ class _DesktopSendState extends ConsumerState { ), ), if (!isPaynymSend) const SizedBox(height: 20), - if (coin is! NanoCurrency && coin is! Epiccash && coin is! Tezos) + if (coin is! NanoCurrency && + coin is! Epiccash && + coin is! Tezos && + coin is! Mimblewimblecoin) DesktopSendFeeForm( walletId: walletId, isToken: false, @@ -1724,7 +1947,11 @@ class _DesktopSendState extends ConsumerState { const SizedBox(height: 36), PrimaryButton( buttonHeight: ButtonHeight.l, - label: "Preview send", + label: + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.slatepack + ? "Create slatepack" + : "Preview send", enabled: ref.watch(pPreviewTxButtonEnabled(coin)), onPressed: ref.watch(pPreviewTxButtonEnabled(coin)) ? previewSend : null, @@ -1752,3 +1979,29 @@ String formatAddress(String epicAddress) { } return epicAddress; } + +String formatAddressMwc(String mimblewimblecoinAddress) { + // strip http:// or https:// prefixes if the address contains an @ symbol (and is thus an mwcmqs address) + if ((mimblewimblecoinAddress.startsWith("http://") || + mimblewimblecoinAddress.startsWith("https://")) && + mimblewimblecoinAddress.contains("@")) { + mimblewimblecoinAddress = mimblewimblecoinAddress.replaceAll("http://", ""); + mimblewimblecoinAddress = mimblewimblecoinAddress.replaceAll( + "https://", + "", + ); + } + // strip mailto: prefix + if (mimblewimblecoinAddress.startsWith("mailto:")) { + mimblewimblecoinAddress = mimblewimblecoinAddress.replaceAll("mailto:", ""); + } + // strip / suffix if the address contains an @ symbol (and is thus an mwcmqs address) + if (mimblewimblecoinAddress.endsWith("/") && + mimblewimblecoinAddress.contains("@")) { + mimblewimblecoinAddress = mimblewimblecoinAddress.substring( + 0, + mimblewimblecoinAddress.length - 1, + ); + } + return mimblewimblecoinAddress; +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index 622630c4a7..9a8d94da6f 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../frost_route_generator.dart'; +import '../../../../pages/finalize_view/finalize_view.dart'; import '../../../../pages/send_view/frost_ms/frost_send_view.dart'; import '../../../../pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart'; import '../../../../providers/global/wallets_provider.dart'; @@ -28,11 +29,7 @@ import 'desktop_send.dart'; import 'desktop_token_send.dart'; class MyWallet extends ConsumerStatefulWidget { - const MyWallet({ - super.key, - required this.walletId, - this.contractAddress, - }); + const MyWallet({super.key, required this.walletId, this.contractAddress}); final String walletId; final String? contractAddress; @@ -42,14 +39,12 @@ class MyWallet extends ConsumerStatefulWidget { } class _MyWalletState extends ConsumerState { - final titles = [ - "Send", - "Receive", - ]; + final titles = ["Send", "Receive"]; late final bool isEth; late final CryptoCurrency coin; late final bool isFrost; + late final bool isMimblewimblecoin; late final bool isViewOnly; @override @@ -58,6 +53,11 @@ class _MyWalletState extends ConsumerState { coin = wallet.info.coin; isFrost = wallet is BitcoinFrostWallet; isEth = coin is Ethereum; + isMimblewimblecoin = coin is Mimblewimblecoin; + + if (isMimblewimblecoin) { + titles.add("Finalize"); + } if (isEth && widget.contractAddress == null) { titles.add("Transactions"); @@ -102,64 +102,57 @@ class _MyWalletState extends ConsumerState { widget.contractAddress == null ? isFrost ? Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: - const EdgeInsets.fromLTRB(0, 20, 0, 0), - child: SecondaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Import sign config", - onPressed: () async { - final wallet = ref - .read(pWallets) - .getWallet(widget.walletId) - as BitcoinFrostWallet; - ref.read(pFrostScaffoldArgs.state).state = - ( - info: ( - walletName: wallet.info.name, - frostCurrency: wallet.cryptoCurrency, - ), - walletId: widget.walletId, - stepRoutes: FrostRouteGenerator - .signFrostTxStepRoutes, - parentNav: Navigator.of(context), - frostInterruptionDialogType: - FrostInterruptionDialogType - .transactionCreation, - callerRouteName: MyStackView.routeName, - ); - - await Navigator.of(context).pushNamed( - FrostStepScaffold.routeName, - ); - }, - ), + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0, 20, 0, 0), + child: SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Import sign config", + onPressed: () async { + final wallet = + ref + .read(pWallets) + .getWallet(widget.walletId) + as BitcoinFrostWallet; + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: widget.walletId, + stepRoutes: + FrostRouteGenerator + .signFrostTxStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType + .transactionCreation, + callerRouteName: MyStackView.routeName, + ); + + await Navigator.of( + context, + ).pushNamed(FrostStepScaffold.routeName); + }, ), - ], - ), - FrostSendView( - walletId: widget.walletId, - coin: coin, - ), - ], - ) - : Padding( - padding: const EdgeInsets.all(20), - child: DesktopSend( - walletId: widget.walletId, + ), + ], ), - ) + FrostSendView(walletId: widget.walletId, coin: coin), + ], + ) + : Padding( + padding: const EdgeInsets.all(20), + child: DesktopSend(walletId: widget.walletId), + ) : Padding( - padding: const EdgeInsets.all(20), - child: DesktopTokenSend( - walletId: widget.walletId, - ), - ), + padding: const EdgeInsets.all(20), + child: DesktopTokenSend(walletId: widget.walletId), + ), Padding( padding: const EdgeInsets.all(20), child: DesktopReceive( @@ -167,6 +160,12 @@ class _MyWalletState extends ConsumerState { contractAddress: widget.contractAddress, ), ), + if (isMimblewimblecoin) + Padding( + padding: const EdgeInsets.all(20), + child: FinalizeView(walletId: widget.walletId), + ), + if (isEth && widget.contractAddress == null) Padding( padding: const EdgeInsets.only(top: 8.0), @@ -174,9 +173,7 @@ class _MyWalletState extends ConsumerState { constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height - 362, ), - child: TransactionsV2List( - walletId: widget.walletId, - ), + child: TransactionsV2List(walletId: widget.walletId), ), ), ], diff --git a/lib/pages_desktop_specific/password/delete_password_warning_view.dart b/lib/pages_desktop_specific/password/delete_password_warning_view.dart index 1eb1331a7f..1699262fb7 100644 --- a/lib/pages_desktop_specific/password/delete_password_warning_view.dart +++ b/lib/pages_desktop_specific/password/delete_password_warning_view.dart @@ -63,6 +63,12 @@ class _ForgotPasswordDesktopViewState await epicDir.delete(recursive: true); } + final mimblewimblecoinDir = + Directory("${appRoot.path}/mimblewimblecoin"); + if (mimblewimblecoinDir.existsSync()) { + await mimblewimblecoinDir.delete(recursive: true); + } + await Isar.getInstance("desktopStore")?.close(deleteFromDisk: true); await (await StackFileSystem.applicationHiveDirectory()) @@ -79,6 +85,12 @@ class _ForgotPasswordDesktopViewState if (epicDir.existsSync()) { await epicDir.delete(recursive: true); } + final mimblewimblecoinDir = + Directory("${appRoot.path}/mimblewimblecoin"); + if (mimblewimblecoinDir.existsSync()) { + await mimblewimblecoinDir.delete(recursive: true); + } + await (await StackFileSystem.applicationHiveDirectory()) .delete(recursive: true); await (await StackFileSystem.applicationIsarDirectory()) diff --git a/lib/pages_desktop_specific/settings/settings_menu/desktop_about_view.dart b/lib/pages_desktop_specific/settings/settings_menu/desktop_about_view.dart index 179ce76501..22c00c51a7 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/desktop_about_view.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/desktop_about_view.dart @@ -365,63 +365,122 @@ class DesktopAboutView extends ConsumerWidget { ); }, ), - // if (AppConfig.coins - // .whereType() - // .isNotEmpty) - // FutureBuilder( - // future: GitStatus - // .getMoneroCommitStatus(), - // builder: ( - // context, - // AsyncSnapshot - // snapshot, - // ) { - // CommitStatus stateOfCommit = - // CommitStatus.notLoaded; + if (AppConfig.coins + .whereType() + .isNotEmpty) + FutureBuilder( + future: GitStatus + .getMimblewimblecoinCommitStatus(), + builder: ( + context, + AsyncSnapshot + snapshot, + ) { + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + stateOfCommit = + snapshot.data!; + } + + return Column( + mainAxisSize: + MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + "Mimblewimblecoin Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( + context, + ) + .extension< + StackColors>()! + .textDark, + ), + ), + const SizedBox( + height: 2, + ), + SelectableText( + GitStatus + .mimblewimblecoinCommit, + style: GitStatus + .styleForStatus( + stateOfCommit, + context, + ), + ), + ], + ); + }, + ), + //if (AppConfig.coins + // .whereType() + // .isNotEmpty) + // FutureBuilder( + // future: GitStatus + // .getMoneroCommitStatus(), + // builder: ( + // context, + // AsyncSnapshot + // snapshot, + // ) { + // CommitStatus stateOfCommit = + // CommitStatus.notLoaded; // - // if (snapshot.connectionState == - // ConnectionState - // .done && - // snapshot.hasData) { - // stateOfCommit = - // snapshot.data!; - // } - // return Column( - // mainAxisSize: - // MainAxisSize.min, - // crossAxisAlignment: - // CrossAxisAlignment - // .start, - // children: [ - // Text( - // "Monero Build Commit", - // style: STextStyles - // .desktopTextExtraExtraSmall( - // context, - // ).copyWith( - // color: Theme.of( - // context, - // ) - // .extension< - // StackColors>()! - // .textDark, - // ), - // ), - // const SizedBox( - // height: 2, - // ), - // SelectableText( - // GitStatus.moneroCommit, - // style: GitStatus - // .styleForStatus( - // stateOfCommit, - // context, - // ), - // ), - // ], - // ); - // }, - // ), + // if (snapshot.connectionState == + // ConnectionState + // .done && + // snapshot.hasData) { + // stateOfCommit = + // snapshot.data!; + // } + // return Column( + // mainAxisSize: + // MainAxisSize.min, + // crossAxisAlignment: + // CrossAxisAlignment + // .start, + // children: [ + // Text( + // "Monero Build Commit", + // style: STextStyles + // .desktopTextExtraExtraSmall( + // context, + // ).copyWith( + // color: Theme.of( + // context, + // ) + // .extension< + // StackColors>()! + // .textDark, + // ), + // ), + // const SizedBox( + // height: 2, + // ), + // SelectableText( + // GitStatus.moneroCommit, + // style: GitStatus + // .styleForStatus( + // stateOfCommit, + // context, + // ), + // ), + // ], + // ); + // }, + // ), ], ), const SizedBox(height: 35), diff --git a/lib/providers/ui/preview_tx_button_state_provider.dart b/lib/providers/ui/preview_tx_button_state_provider.dart index 9ff71aca7d..de20131e11 100644 --- a/lib/providers/ui/preview_tx_button_state_provider.dart +++ b/lib/providers/ui/preview_tx_button_state_provider.dart @@ -11,6 +11,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../utilities/amount/amount.dart'; +import '../../utilities/enums/mwc_transaction_method.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../wallet/public_private_balance_state_provider.dart'; @@ -20,10 +21,23 @@ final pValidSparkSendToAddress = StateProvider.autoDispose((_) => false); final pIsExchangeAddress = StateProvider((_) => false); +// MWC Transaction Method Provider. +final pSelectedMwcTransactionMethod = StateProvider( + (_) => MwcTransactionMethod.slatepack, +); + final pPreviewTxButtonEnabled = Provider.autoDispose .family((ref, coin) { final amount = ref.watch(pSendAmount) ?? Amount.zero; + // For MWC slatepack transactions, address validation is not required. + if (coin is Mimblewimblecoin) { + final selectedMethod = ref.watch(pSelectedMwcTransactionMethod); + if (selectedMethod == MwcTransactionMethod.slatepack) { + return amount > Amount.zero; + } + } + if (coin is Firo) { final firoType = ref.watch(publicPrivateBalanceStateProvider); switch (firoType) { diff --git a/lib/route_generator.dart b/lib/route_generator.dart index ee09bdba0a..064e54df9a 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -68,6 +68,7 @@ import 'pages/exchange_view/exchange_step_views/step_4_view.dart'; import 'pages/exchange_view/send_from_view.dart'; import 'pages/exchange_view/trade_details_view.dart'; import 'pages/exchange_view/wallet_initiated_exchange_view.dart'; +import 'pages/finalize_view/finalize_view.dart'; import 'pages/generic/single_field_edit_view.dart'; import 'pages/home_view/home_view.dart'; import 'pages/intro_view.dart'; @@ -165,7 +166,8 @@ import 'pages/wallet_view/transaction_views/transaction_search_filter_view.dart' import 'pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart'; import 'pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart'; import 'pages/wallet_view/transaction_views/tx_v2/fusion_group_details_view.dart'; -import 'pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart' + as tvd; import 'pages/wallet_view/wallet_view.dart'; import 'pages/wallets_view/wallets_overview.dart'; import 'pages/wallets_view/wallets_view.dart'; @@ -1614,13 +1616,13 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case TransactionV2DetailsView.routeName: + case tvd.TransactionV2DetailsView.routeName: if (args is ({TransactionV2 tx, CryptoCurrency coin, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: - (_) => TransactionV2DetailsView( + (_) => tvd.TransactionV2DetailsView( transaction: args.tx, coin: args.coin, walletId: args.walletId, @@ -1711,6 +1713,16 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case FinalizeView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FinalizeView(walletId: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case WalletAddressesView.routeName: if (args is String) { return getRoute( diff --git a/lib/services/price.dart b/lib/services/price.dart index 9641d0328a..7748271281 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -34,6 +34,7 @@ class PriceAPI { Dash: "dash", Dogecoin: "dogecoin", Epiccash: "epic-cash", + Mimblewimblecoin: "mimblewimblecoin", Ecash: "ecash", Ethereum: "ethereum", Fact0rn: "fact0rn", diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 7b5f9a376b..0f478d6ddc 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -26,6 +26,7 @@ import '../wallets/crypto_currency/crypto_currency.dart'; import '../wallets/crypto_currency/intermediate/cryptonote_currency.dart'; import '../wallets/isar/models/wallet_info.dart'; import '../wallets/wallet/impl/epiccash_wallet.dart'; +import '../wallets/wallet/impl/mimblewimblecoin_wallet.dart'; import '../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../wallets/wallet/wallet.dart'; @@ -132,6 +133,14 @@ class Wallets { Logging.instance.d( "epic wallet: $walletId deleted with result: $deleteResult", ); + } else if (info.coin is Mimblewimblecoin) { + final deleteResult = await deleteMimblewimblecoinWallet( + walletId: walletId, + secureStore: secureStorage, + ); + Logging.instance.i( + "Mimblewimblecoin wallet: $walletId deleted with result: $deleteResult", + ); } // delete wallet data in main db diff --git a/lib/themes/coin_icon_provider.dart b/lib/themes/coin_icon_provider.dart index 4a789ad76a..1deb22b4e1 100644 --- a/lib/themes/coin_icon_provider.dart +++ b/lib/themes/coin_icon_provider.dart @@ -28,6 +28,8 @@ final coinIconProvider = Provider.family((ref, coin) { return assets.dogecoin; case const (Epiccash): return assets.epicCash; + case const (Mimblewimblecoin): + return assets.mimblewimblecoin; case const (Firo): return assets.firo; case const (Monero): diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 9e63370e1e..ff0880cec7 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -261,6 +261,31 @@ class AddressUtils { } return epicAddress; } + + /// Formats an address string to remove any unnecessary prefixes or suffixes. + String formatAddressMwc(String mimblewimblecoinAddress) { + // strip http:// or https:// prefixes if the address contains an @ symbol (and is thus an mwcmqs address) + if ((mimblewimblecoinAddress.startsWith("http://") || + mimblewimblecoinAddress.startsWith("https://")) && + mimblewimblecoinAddress.contains("@")) { + mimblewimblecoinAddress = + mimblewimblecoinAddress.replaceAll("http://", ""); + mimblewimblecoinAddress = + mimblewimblecoinAddress.replaceAll("https://", ""); + } + // strip mailto: prefix + if (mimblewimblecoinAddress.startsWith("mailto:")) { + mimblewimblecoinAddress = + mimblewimblecoinAddress.replaceAll("mailto:", ""); + } + // strip / suffix if the address contains an @ symbol (and is thus an mwcmqs address) + if (mimblewimblecoinAddress.endsWith("/") && + mimblewimblecoinAddress.contains("@")) { + mimblewimblecoinAddress = mimblewimblecoinAddress.substring( + 0, mimblewimblecoinAddress.length - 1); + } + return mimblewimblecoinAddress; + } } class PaymentUriData { diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart index a8cbfa701e..79e45232b3 100644 --- a/lib/utilities/amount/amount_unit.dart +++ b/lib/utilities/amount/amount_unit.dart @@ -63,6 +63,7 @@ enum AmountUnit { // case Coin.dogecoin: // case Coin.eCash: // case Coin.epicCash: + // case Coin.mimblewimblecoin: // case Coin.stellar: // TODO: check if this is correct // case Coin.stellarTestnet: // case Coin.tezos: diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index d14cdc38ca..e75bf8eb4e 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -246,6 +246,7 @@ class _SVG { String get bitcoincash => "assets/svg/coin_icons/Bitcoincash.svg"; String get dogecoin => "assets/svg/coin_icons/Dogecoin.svg"; String get epicCash => "assets/svg/coin_icons/EpicCash.svg"; + String get mimblewimblecoin => "assets/svg/coin_icons/Mimblewimblecoin.svg"; String get ethereum => "assets/svg/coin_icons/Ethereum.svg"; String get firo => "assets/svg/coin_icons/Firo.svg"; String get monero => "assets/svg/coin_icons/Monero.svg"; diff --git a/lib/utilities/default_mwcmqs.dart b/lib/utilities/default_mwcmqs.dart new file mode 100644 index 0000000000..84e7356d84 --- /dev/null +++ b/lib/utilities/default_mwcmqs.dart @@ -0,0 +1,53 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import '../models/mwcmqs_server_model.dart'; + +abstract class DefaultMwcMqs { + static const String defaultName = "Default"; + + static List get all => [americas, asia, europe]; + static List get defaultIds => ['americas', 'asia', 'europe']; + + static MwcMqsServerModel get americas => MwcMqsServerModel( + host: 'mqs.mwc.mw', + port: 443, + name: 'Americas', + id: 'americas', + useSSL: true, + enabled: true, + isFailover: true, + isDown: false, + ); + + static MwcMqsServerModel get asia => MwcMqsServerModel( + host: 'mqs.mwc.mw', + port: 443, + name: 'Asia', + id: 'asia', + useSSL: true, + enabled: true, + isFailover: true, + isDown: false, + ); + + static MwcMqsServerModel get europe => MwcMqsServerModel( + host: 'mqs.mwc.mw', + port: 443, + name: 'Europe', + id: 'europe', + useSSL: true, + enabled: true, + isFailover: true, + isDown: false, + ); + + static final defaultMwcMqsServer = americas; +} diff --git a/lib/utilities/enums/mwc_transaction_method.dart b/lib/utilities/enums/mwc_transaction_method.dart new file mode 100644 index 0000000000..4b24ad37f7 --- /dev/null +++ b/lib/utilities/enums/mwc_transaction_method.dart @@ -0,0 +1,70 @@ +/// Enum to represent different MWC transaction methods. +enum MwcTransactionMethod { + /// Manual slatepack exchange (copy/paste, QR codes, files). + slatepack, + + /// Automatic transaction via MWCMQS. + mwcmqs; + + // /// Direct HTTP/HTTPS to recipient's wallet. + // http, + // + // /// Unknown or unsupported method. + // unknown; + + /// Human readable name for the transaction method. + String get displayName { + switch (this) { + case MwcTransactionMethod.slatepack: + return 'Slatepack'; + case MwcTransactionMethod.mwcmqs: + return 'MWCMQS'; + // case MwcTransactionMethod.http: + // return 'HTTP'; + // case MwcTransactionMethod.unknown: + // return 'Unknown'; + } + } + + /// Description of how the transaction method works. + String get description { + switch (this) { + case MwcTransactionMethod.slatepack: + return 'Manual exchange via text, QR codes, or files'; + case MwcTransactionMethod.mwcmqs: + return 'Automatic exchange via MWCMQS messaging'; + // case MwcTransactionMethod.http: + // return 'Direct connection to recipient wallet'; + // case MwcTransactionMethod.unknown: + // return 'Unsupported transaction method'; + } + } + + /// Whether this method requires manual intervention. + bool get isManual { + switch (this) { + case MwcTransactionMethod.slatepack: + return true; + case MwcTransactionMethod.mwcmqs: + return false; + // case MwcTransactionMethod.http: + // return false; + // case MwcTransactionMethod.unknown: + // return true; + } + } + + /// Whether this method works offline. + bool get worksOffline { + switch (this) { + case MwcTransactionMethod.slatepack: + return true; + case MwcTransactionMethod.mwcmqs: + return false; + // case MwcTransactionMethod.http: + // return false; + // case MwcTransactionMethod.unknown: + // return false; + } + } +} diff --git a/lib/utilities/git_status.dart b/lib/utilities/git_status.dart index c89cf24398..2b19e334e6 100644 --- a/lib/utilities/git_status.dart +++ b/lib/utilities/git_status.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_libepiccash/git_versions.dart' as epic_versions; +import 'package:flutter_libmwc/git_versions.dart' as mimblewimblecoin_versions; import 'package:http/http.dart'; import '../../../themes/stack_colors.dart'; @@ -18,6 +19,8 @@ enum CommitStatus { isHead, isOldCommit, notACommit, notLoaded } abstract class GitStatus { static String get epicCashCommit => epic_versions.getPluginVersion(); // static String get moneroCommit => monero_versions.getPluginVersion(); + static String get mimblewimblecoinCommit => + mimblewimblecoin_versions.getPluginVersion(); static String get appCommitHash => AppConfig.commitHash; @@ -51,6 +54,60 @@ abstract class GitStatus { return _cachedEpicStatus!; } + static CommitStatus? _cachedMimblewimblecoinStatus; + static Future getMimblewimblecoinCommitStatus() async { + if (_cachedMimblewimblecoinStatus != null) { + return _cachedMimblewimblecoinStatus!; + } + final List results = await Future.wait([ + _doesCommitExist("cypherstack", "flutter_libmwc", mimblewimblecoinCommit), + _isHeadCommit( + "cypherstack", + "flutter_libmwc", + "main", + mimblewimblecoinCommit, + ), + ]); + + final commitExists = results[0]; + final commitIsHead = results[1]; + + if (commitExists && commitIsHead) { + _cachedMimblewimblecoinStatus = CommitStatus.isHead; + } else if (commitExists) { + _cachedMimblewimblecoinStatus = CommitStatus.isOldCommit; + } else { + _cachedMimblewimblecoinStatus = CommitStatus.notACommit; + } + + return _cachedMimblewimblecoinStatus!; + } + + //static CommitStatus? _cachedMoneroStatus; + //static Future getMoneroCommitStatus() async { + // if (_cachedMoneroStatus != null) { + // return _cachedMoneroStatus!; + // } + // + // final List results = await Future.wait([ + // _doesCommitExist("cypherstack", "flutter_libmonero", moneroCommit), + // _isHeadCommit("cypherstack", "flutter_libmonero", "main", moneroCommit), + // ]); + // + // final commitExists = results[0]; + // final commitIsHead = results[1]; + // + // if (commitExists && commitIsHead) { + // _cachedMoneroStatus = CommitStatus.isHead; + // } else if (commitExists) { + // _cachedMoneroStatus = CommitStatus.isOldCommit; + // } else { + // _cachedMoneroStatus = CommitStatus.notACommit; + // } + // + // return _cachedMoneroStatus!; + //} + static TextStyle styleForStatus(CommitStatus status, BuildContext context) { final Color color; switch (status) { diff --git a/lib/utilities/test_mwcmqs_connection.dart b/lib/utilities/test_mwcmqs_connection.dart new file mode 100644 index 0000000000..c0615b25da --- /dev/null +++ b/lib/utilities/test_mwcmqs_connection.dart @@ -0,0 +1,90 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'dart:convert'; + +import '../networking/http.dart'; +import '../pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; +import '../services/tor_service.dart'; +import 'logger.dart'; +import 'prefs.dart'; + +Future _testMwcMqsNodeConnection(Uri uri) async { + final HTTP client = HTTP(); + try { + final headers = {'Content-Type': 'application/json'}; + + if (uri.toString() == 'https://mwc713.mwc.mw/v1/version') { + const username = 'mwcmain'; + const password = '11ne3EAUtOXVKwhxm84U'; + final credentials = base64Encode(utf8.encode('$username:$password')); + headers['Authorization'] = 'Basic $credentials'; + } + final response = await client + .get( + url: uri, + headers: headers, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ) + .timeout( + const Duration(milliseconds: 2000), + onTimeout: () async => Response(utf8.encode('Error'), 408), + ); + + final json = jsonDecode(response.body); + + if (response.code == 200 && json["node_version"] != null) { + return true; + } else { + return false; + } + } catch (e, s) { + Logging.instance.w("$e\n$s"); + return false; + } +} + +// returns node data with properly formatted host/url if successful, otherwise null +Future testMwcNodeConnection(NodeFormData data) async { + if (data.host == null || data.port == null || data.useSSL == null) { + return null; + } + const String path_postfix = "/v1/version"; + + if (data.host!.startsWith("https://")) { + data.useSSL = true; + } else if (data.host!.startsWith("http://")) { + data.useSSL = false; + } else { + if (data.useSSL!) { + data.host = "https://${data.host!}"; + } else { + data.host = "http://${data.host!}"; + } + } + + Uri uri = Uri.parse(data.host! + path_postfix); + + uri = uri.replace(port: data.port); + + try { + if (await _testMwcMqsNodeConnection(uri)) { + return data; + } else { + return null; + } + } catch (e, s) { + Logging.instance.w("$e\n$s"); + return null; + } +} diff --git a/lib/utilities/test_node_connection.dart b/lib/utilities/test_node_connection.dart index 7e8616ac27..8491ca3db5 100644 --- a/lib/utilities/test_node_connection.dart +++ b/lib/utilities/test_node_connection.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:on_chain/ada/ada.dart'; import 'package:socks5_proxy/socks.dart'; +import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk; import '../networking/http.dart'; import '../pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; @@ -23,19 +24,15 @@ import 'logger.dart'; import 'test_epic_box_connection.dart'; import 'test_eth_node_connection.dart'; import 'test_monero_node_connection.dart'; +import 'test_mwcmqs_connection.dart'; import 'test_stellar_node_connection.dart'; import 'tor_plain_net_option_enum.dart'; -import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk; - Future _xmrHelper( NodeFormData nodeFormData, BuildContext context, void Function(NodeFormData)? onSuccess, - ({ - InternetAddress host, - int port, - })? proxyInfo, + ({InternetAddress host, int port})? proxyInfo, ) async { final data = nodeFormData; final url = data.host!; @@ -128,19 +125,29 @@ Future testNodeConnection({ onSuccess?.call(data); } } catch (e, s) { - Logging.instance.w( - "$e\n$s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); + } + break; + + case Mimblewimblecoin(): + try { + final data = await testMwcNodeConnection(formData); + + if (data != null) { + testPassed = true; + onSuccess?.call(data); + } + } catch (e, s) { + Logging.instance.w("$e\n$s"); } break; case CryptonoteCurrency(): try { - final proxyInfo = ref.read(prefsChangeNotifierProvider).useTor - ? ref.read(pTorService).getProxyInfo() - : null; + final proxyInfo = + ref.read(prefsChangeNotifierProvider).useTor + ? ref.read(pTorService).getProxyInfo() + : null; final url = formData.host!; final uri = Uri.tryParse(url); @@ -179,11 +186,7 @@ Future testNodeConnection({ } } } catch (e, s) { - Logging.instance.w( - "$e\n$s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); } break; @@ -214,8 +217,10 @@ Future testNodeConnection({ case Stellar(): try { - testPassed = - await testStellarNodeConnection(formData.host!, formData.port!); + testPassed = await testStellarNodeConnection( + formData.host!, + formData.port!, + ); } catch (_) {} break; @@ -226,14 +231,11 @@ Future testNodeConnection({ final response = await HTTP().post( url: uri, headers: {"Content-Type": "application/json"}, - body: jsonEncode( - { - "action": "version", - }, - ), - proxyInfo: ref.read(prefsChangeNotifierProvider).useTor - ? ref.read(pTorService).getProxyInfo() - : null, + body: jsonEncode({"action": "version"}), + proxyInfo: + ref.read(prefsChangeNotifierProvider).useTor + ? ref.read(pTorService).getProxyInfo() + : null, ); testPassed = response.code == 200; @@ -259,9 +261,7 @@ Future testNodeConnection({ ); final health = await rpcClient.getHealth(); - Logging.instance.i( - "Solana testNodeConnection \"health=$health\"", - ); + Logging.instance.i("Solana testNodeConnection \"health=$health\""); return true; } catch (_) { testPassed = false; @@ -273,10 +273,7 @@ Future testNodeConnection({ final client = HttpClient(); if (ref.read(prefsChangeNotifierProvider).useTor) { final proxyInfo = TorService.sharedInstance.getProxyInfo(); - final proxySettings = ProxySettings( - proxyInfo.host, - proxyInfo.port, - ); + final proxySettings = ProxySettings(proxyInfo.host, proxyInfo.port); SocksTCPClient.assignToHttpClient(client, [proxySettings]); } final blockfrostProvider = BlockforestProvider( @@ -290,9 +287,7 @@ Future testNodeConnection({ BlockfrostRequestBackendHealthStatus(), ); - Logging.instance.i( - "Cardano testNodeConnection \"health=$health\"", - ); + Logging.instance.i("Cardano testNodeConnection \"health=$health\""); return health; } catch (_) { @@ -305,7 +300,7 @@ Future testNodeConnection({ final daemon = xelis_sdk.DaemonClient( endPoint: "${formData.host!}:${formData.port!}", secureWebSocket: formData.useSSL ?? false, - timeout: 5000 + timeout: 5000, ); daemon.connect(); diff --git a/lib/wallets/crypto_currency/coins/mimblewimblecoin.dart b/lib/wallets/crypto_currency/coins/mimblewimblecoin.dart new file mode 100644 index 0000000000..6d670d5ff6 --- /dev/null +++ b/lib/wallets/crypto_currency/coins/mimblewimblecoin.dart @@ -0,0 +1,156 @@ +import 'package:flutter_libmwc/lib.dart' as mimblewimblecoin; + +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/node_model.dart'; +import '../../../utilities/default_nodes.dart'; +import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../utilities/enums/mwc_transaction_method.dart'; +import '../crypto_currency.dart'; +import '../intermediate/bip39_currency.dart'; + +class Mimblewimblecoin extends Bip39Currency { + Mimblewimblecoin(super.network) { + _idMain = "mimblewimblecoin"; + _uriScheme = "mimblewimblecoin"; // ? + switch (network) { + case CryptoCurrencyNetwork.main: + _id = _idMain; + _name = "MimbleWimbleCoin"; + _ticker = "MWC"; + default: + throw Exception("Unsupported network: $network"); + } + } + + late final String _id; + @override + String get identifier => _id; + + late final String _idMain; + @override + String get mainNetId => _idMain; + + late final String _name; + @override + String get prettyName => _name; + + late final String _uriScheme; + @override + String get uriScheme => _uriScheme; + + late final String _ticker; + @override + String get ticker => _ticker; + + @override + String get genesisHash { + return "not used in mimblewimblecoin"; + } + + @override + // change this to change the number of confirms a tx needs in order to show as confirmed + int get minConfirms => 3; + + @override + bool validateAddress(String address) { + // Use libmwc for address validation. + return mimblewimblecoin.Libmwc.validateSendAddress(address: address); + } + + /// Check if data is a slatepack. + bool isSlatepack(String data) { + return data.trim().startsWith('BEGINSLATE') && + (data.trim().endsWith('ENDSLATEPACK') || + data.trim().endsWith('ENDSLATEPACK.') || + data.trim().endsWith('ENDSLATE_BIN') || + data.trim().endsWith('ENDSLATE_BIN.')); + } + + /// Check if address is MWCMQS format. + bool isMwcmqsAddress(String address) { + return address.startsWith('mwcmqs://'); + } + + /// Detect transaction type based on address/data format. + MwcTransactionMethod getTransactionMethod(String addressOrData) { + if (isSlatepack(addressOrData)) { + return MwcTransactionMethod.slatepack; + } else if (isMwcmqsAddress(addressOrData)) { + return MwcTransactionMethod.mwcmqs; + // } else if (isHttpAddress(addressOrData)) { + // return MwcTransactionMethod.http; + } else { + throw Exception("Unknown MwcTransactionMethod found!"); + // return MwcTransactionMethod.unknown; + } + } + + @override + NodeModel defaultNode({required bool isPrimary}) { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: "https://mwc713.mwc.mw", + port: 443, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(this), + useSSL: true, + enabled: true, + coinName: identifier, + isFailover: true, + isDown: false, + torEnabled: true, + clearnetEnabled: true, + isPrimary: true, + ); + + default: + throw UnimplementedError(); + } + } + + @override + int get defaultSeedPhraseLength => 12; + + @override + int get fractionDigits => 9; + + @override + bool get hasBuySupport => false; + + @override + bool get hasMnemonicPassphraseSupport => false; + + @override + List get possibleMnemonicLengths => [defaultSeedPhraseLength, 24]; + + @override + AddressType get defaultAddressType => AddressType.mimbleWimble; + + @override + BigInt get satsPerCoin => BigInt.from(1000000000); + + @override + int get targetBlockTimeSeconds => 60; + + @override + DerivePathType get defaultDerivePathType => + throw UnsupportedError( + "$runtimeType does not use bitcoin style derivation paths", + ); + + @override + Uri defaultBlockExplorer(String txid) { + switch (network) { + default: + throw Exception( + "Unsupported network for defaultBlockExplorer(): $network", + ); + } + } + + @override + AddressType? getAddressType(String address) { + return AddressType.mimbleWimble; + } +} diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 0c2f2cb838..8d02b130c0 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -11,6 +11,7 @@ export 'coins/dash.dart'; export 'coins/dogecoin.dart'; export 'coins/ecash.dart'; export 'coins/epiccash.dart'; +export 'coins/mimblewimblecoin.dart'; export 'coins/ethereum.dart'; export 'coins/fact0rn.dart'; export 'coins/firo.dart'; @@ -65,7 +66,7 @@ abstract class CryptoCurrency { int get minConfirms; int get minCoinbaseConfirms => minConfirms; - // TODO: [prio=low] could be handled differently as (at least) epiccash does not use this + // TODO: [prio=low] could be handled differently as (at least) epiccash/mimblewimblecoin does not use this String get genesisHash; bool validateAddress(String address); diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index 9d790a8308..6b1a54919a 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -508,6 +508,7 @@ class WalletInfo implements IsarId { abstract class WalletInfoKeys { static const String tokenContractAddresses = "tokenContractAddressesKey"; static const String epiccashData = "epiccashDataKey"; + static const String mimblewimblecoinData = "mimblewimblecoinDataKey"; static const String bananoMonkeyImageBytes = "monkeyImageBytesKey"; static const String tezosDerivationPath = "tezosDerivationPathKey"; static const String xelisDerivationPath = "xelisDerivationPathKey"; diff --git a/lib/wallets/wallet/impl/mimblewimblecoin_wallet.dart b/lib/wallets/wallet/impl/mimblewimblecoin_wallet.dart new file mode 100644 index 0000000000..567908275e --- /dev/null +++ b/lib/wallets/wallet/impl/mimblewimblecoin_wallet.dart @@ -0,0 +1,1617 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_libmwc/lib.dart' as mimblewimblecoin; +import 'package:flutter_libmwc/models/transaction.dart' + as mimblewimblecoin_models; +import 'package:isar/isar.dart'; +import 'package:mutex/mutex.dart'; +import 'package:stack_wallet_backup/generate_password.dart'; + +import '../../../models/balance.dart'; +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import '../../../models/mwc_slatepack_models.dart'; +import '../../../models/mwcmqs_config_model.dart'; +import '../../../models/node_model.dart'; +import '../../../models/paymint/fee_object_model.dart'; +import '../../../pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; +import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; +import '../../../services/event_bus/events/global/node_connection_status_changed_event.dart'; +import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; +import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import '../../../services/event_bus/global_event_bus.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/default_mwcmqs.dart'; +import '../../../utilities/flutter_secure_storage_interface.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/stack_file_system.dart'; +import '../../../utilities/test_mwcmqs_connection.dart'; +import '../../crypto_currency/crypto_currency.dart'; +import '../../models/tx_data.dart'; +import '../intermediate/bip39_wallet.dart'; +import '../supporting/mimblewimblecoin_wallet_info_extension.dart'; + +class MimblewimblecoinWallet extends Bip39Wallet { + MimblewimblecoinWallet(CryptoCurrencyNetwork network) + : super(Mimblewimblecoin(network)); + + final syncMutex = Mutex(); + NodeModel? _mimblewimblecoinNode; + Timer? timer; + + double highestPercent = 0; + Future get getSyncPercent async { + final int lastScannedBlock = + info.mimblewimblecoinData?.lastScannedBlock ?? 0; + final _chainHeight = await chainHeight; + final double restorePercent = lastScannedBlock / _chainHeight; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(highestPercent, walletId), + ); + if (restorePercent > highestPercent) { + highestPercent = restorePercent; + } + + final int blocksRemaining = _chainHeight - lastScannedBlock; + GlobalEventBus.instance.fire( + BlocksRemainingEvent(blocksRemaining, walletId), + ); + + return restorePercent < 0 ? 0.0 : restorePercent; + } + + Future updateMwcmqsConfig(String host, int port) async { + final String stringConfig = jsonEncode({ + "mwcmqs_domain": host, + "mwcmqs_port": port, + }); + await secureStorageInterface.write( + key: '${walletId}_mwcmqsConfig', + value: stringConfig, + ); + + // Restart MWCMQS listener with new configuration if wallet has a handle. + try { + final handle = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); + if (handle != null && handle.isNotEmpty) { + await stopSlatepackListener(); + await startSlatepackListener(); + Logging.instance.i( + 'Restarted MWCMQS listener with new config: $host:$port', + ); + } + } catch (e, s) { + Logging.instance.e( + 'Failed to restart MWCMQS listener after config update: $e\n$s', + ); + } + } + + Future _ensureWalletOpen() async { + final existing = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); + if (existing != null && existing.isNotEmpty) return existing; + + final config = await _getRealConfig(); + final password = await secureStorageInterface.read( + key: '${walletId}_password', + ); + if (password == null) { + throw Exception('Wallet password not found'); + } + final opened = await mimblewimblecoin.Libmwc.openWallet( + config: config, + password: password, + ); + await secureStorageInterface.write( + key: '${walletId}_wallet', + value: opened, + ); + return opened; + } + + /// Returns an empty String on success, error message on failure. + Future cancelPendingTransactionAndPost(String txSlateId) async { + try { + final String wallet = + (await secureStorageInterface.read(key: '${walletId}_wallet'))!; + + final result = await mimblewimblecoin.Libmwc.cancelTransaction( + wallet: wallet, + transactionId: txSlateId, + ); + Logging.instance.i("cancel $txSlateId result: $result"); + return result; + } catch (e, s) { + Logging.instance.e("$e, $s"); + return e.toString(); + } + } + + Future getMwcMqsConfig() async { + // Check if there's a custom MWCMQS config stored. + final customConfigJson = await secureStorageInterface.read( + key: '${walletId}_mwcmqsConfig', + ); + + if (customConfigJson != null) { + try { + final customConfig = + jsonDecode(customConfigJson) as Map; + final host = customConfig['mwcmqs_domain'] as String?; + final port = customConfig['mwcmqs_port'] as int?; + + if (host != null && port != null) { + return MwcMqsConfigModel(host: host, port: port); + } + } catch (e) { + Logging.instance.w('Failed to parse custom MWCMQS config: $e'); + } + } + + // Fall back to default server. + return MwcMqsConfigModel.fromServer(DefaultMwcMqs.defaultMwcMqsServer); + } + + // ================= Slatepack Operations =================================== + + /// Create a slatepack for sending MWC. + Future createSlatepack({ + required Amount amount, + String? recipientAddress, + String? message, + bool encrypt = false, + int? minimumConfirmations, + }) async { + try { + final handle = await _ensureWalletOpen(); + + // Generate S1 slate JSON. + final s1Json = await mimblewimblecoin.Libmwc.txInit( + wallet: handle, + amount: amount.raw.toInt(), + minimumConfirmations: + minimumConfirmations ?? cryptoCurrency.minConfirms, + selectionStrategyIsAll: false, + message: message ?? '', + ); + + // Encode to slatepack. + final encoded = await mimblewimblecoin.Libmwc.encodeSlatepack( + slateJson: s1Json, + recipientAddress: recipientAddress, + encrypt: encrypt, + wallet: handle, + ); + + return SlatepackResult( + success: true, + slatepack: encoded.slatepack, + slateJson: s1Json, + wasEncrypted: encoded.wasEncrypted, + recipientAddress: encoded.recipientAddress, + ); + } catch (e, s) { + Logging.instance.e('Failed to create slatepack: $e\n$s'); + return SlatepackResult(success: false, error: e.toString()); + } + } + + // this isn't nearly the best way to handle this but better than copy pasting + // this function in two places (mobile and desktop widgets)... + Future<({SlatepackDecodeResult result, String type, String raw})?> + fullDecodeSlatepack(String slatepack) async { + // add delay for showloading exception catching hack fix + await Future.delayed(const Duration(seconds: 1)); + + if (slatepack.isEmpty) { + return null; + } + + // Basic format validation. + final coin = cryptoCurrency as Mimblewimblecoin; + if (!coin.isSlatepack(slatepack)) { + throw Exception("Invalid slatepack format"); + } + + // Attempt to decode. + final decoded = await decodeSlatepack(slatepack); + + if (decoded.success) { + final analysis = await analyzeSlatepack(slatepack); + + String _determineSlatepackType(SlatepackDecodeResult decoded) { + // Fallback analysis based on sender/recipient addresses. + if (decoded.senderAddress != null && decoded.recipientAddress != null) { + return "S2 (Response)"; + } else if (decoded.senderAddress != null) { + return "S1 (Initial)"; + } else { + return "Unknown"; + } + } + + final String slatepackType = switch (analysis.status) { + 'S1' => "S1 (Initial Send)", + 'S2' => "S2 (Response)", + 'S3' => "S3 (Finalized)", + _ => _determineSlatepackType(decoded), // Fallback. + }; + + return (result: decoded, type: slatepackType, raw: slatepack); + } else { + throw Exception(decoded.error ?? "Failed to decode slatepack"); + } + } + + /// Decode a slatepack. + Future decodeSlatepack(String slatepack) async { + try { + final handle = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); + final result = + handle != null + ? await mimblewimblecoin.Libmwc.decodeSlatepackWithWallet( + wallet: handle, + slatepack: slatepack, + ) + : await mimblewimblecoin.Libmwc.decodeSlatepack( + slatepack: slatepack, + ); + + return SlatepackDecodeResult( + success: true, + slateJson: result.slateJson, + wasEncrypted: result.wasEncrypted, + senderAddress: result.senderAddress, + recipientAddress: result.recipientAddress, + ); + } catch (e, s) { + Logging.instance.e('Failed to decode slatepack: $e\n$s'); + return SlatepackDecodeResult(success: false, error: e.toString()); + } + } + + /// Receive a slatepack and return response slatepack. + Future receiveSlatepack(String slatepack) async { + try { + final handle = await _ensureWalletOpen(); + + // Decode to get slate JSON and sender address. + final decoded = await mimblewimblecoin.Libmwc.decodeSlatepackWithWallet( + wallet: handle, + slatepack: slatepack, + ); + + // Receive and get updated slate JSON. + final received = await mimblewimblecoin.Libmwc.txReceiveDetailed( + wallet: handle, + slateJson: decoded.slateJson, + ); + + // Encode response back to sender if address available. + final encoded = await mimblewimblecoin.Libmwc.encodeSlatepack( + slateJson: received.slateJson, + recipientAddress: decoded.senderAddress, + encrypt: decoded.senderAddress != null, + wallet: handle, + ); + + return ReceiveResult( + success: true, + slateId: received.slateId, + commitId: received.commitId, + responseSlatepack: encoded.slatepack, + wasEncrypted: encoded.wasEncrypted, + recipientAddress: decoded.senderAddress, + ); + } catch (e, s) { + Logging.instance.e('Failed to receive slatepack: $e\n$s'); + return ReceiveResult(success: false, error: e.toString()); + } + } + + /// Finalize a slatepack (sender step 3). + Future finalizeSlatepack(String slatepack) async { + try { + final handle = await _ensureWalletOpen(); + + // Decode to get slate JSON. + final decoded = await mimblewimblecoin.Libmwc.decodeSlatepackWithWallet( + wallet: handle, + slatepack: slatepack, + ); + + // Finalize transaction. + final finalized = await mimblewimblecoin.Libmwc.txFinalize( + wallet: handle, + slateJson: decoded.slateJson, + ); + + return FinalizeResult( + success: true, + slateId: finalized.slateId, + commitId: finalized.commitId, + ); + } catch (e, s) { + Logging.instance.e('Failed to finalize slatepack: $e\n$s'); + return FinalizeResult(success: false, error: e.toString()); + } + } + + /// Start MWCMQS listener for automatic transaction processing. + Future startSlatepackListener() async { + try { + await _ensureWalletOpen(); + final mwcmqsConfig = await getMwcMqsConfig(); + final wallet = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); + mimblewimblecoin.Libmwc.startMwcMqsListener( + wallet: wallet!, + mwcmqsConfig: mwcmqsConfig.toString(), + ); + } catch (e, s) { + Logging.instance.e('Failed to start slatepack listener: $e\n$s'); + rethrow; + } + } + + /// Stop MWCMQS listener. + Future stopSlatepackListener() async { + try { + mimblewimblecoin.Libmwc.stopMwcMqsListener(); + } catch (e, s) { + Logging.instance.e('Failed to stop slatepack listener: $e\n$s'); + } + } + + /// Validate MWC address. + bool validateMwcAddress(String address) { + return mimblewimblecoin.Libmwc.validateSendAddress(address: address); + } + + /// Detect if an address is a slatepack. + bool isSlatepack(String data) { + return data.trim().startsWith('BEGINSLATE') && + (data.trim().endsWith('ENDSLATEPACK') || + data.trim().endsWith('ENDSLATEPACK.') || + data.trim().endsWith('ENDSLATE_BIN') || + data.trim().endsWith('ENDSLATE_BIN.')); + } + + /// Detect if an address is MWCMQS format. + bool isMwcmqsAddress(String address) { + return address.startsWith('mwcmqs://'); + } + + /// Detect if an address is HTTP format. + bool isHttpAddress(String address) { + return address.startsWith('http://') || address.startsWith('https://'); + } + + /// Analyze a slatepack and determine transaction type and metadata. + /// Returns a record with transaction type and slate information. + Future< + ({ + String type, + String status, + String? amount, + bool wasEncrypted, + String? senderAddress, + String? recipientAddress, + String slateId, + }) + > + analyzeSlatepack(String slatepack) async { + try { + // Get wallet handle if available + final wallet = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); + + // Decode the slatepack + final decoded = + wallet != null + ? await mimblewimblecoin.Libmwc.decodeSlatepackWithWallet( + wallet: wallet, + slatepack: slatepack, + ) + : await mimblewimblecoin.Libmwc.decodeSlatepack( + slatepack: slatepack, + ); + + // Parse the slate JSON to extract metadata + final slateData = jsonDecode(decoded.slateJson); + final String slateId = "${slateData['id'] ?? ''}"; + final String? amountStr = slateData['amount']?.toString(); + + Logging.instance.d('Analyzed slatepack with ID: $slateId'); + + // Determine slate status from the slate structure + String status = 'Unknown'; + String type = 'Unknown'; + + // Check participant data to determine slate status + final List? participants = + slateData['participant_data'] as List?; + if (participants != null && participants.isNotEmpty) { + // Count how many participants have signatures + int signedParticipants = 0; + for (final participant in participants) { + if (participant['part_sig'] != null) { + signedParticipants++; + } + } + + // Determine status based on signatures and participant count + if (signedParticipants == 0) { + status = 'S1'; + type = 'Outgoing'; // Initial send slate - this is outgoing + } else if (signedParticipants == 1) { + status = 'S2'; + type = 'Incoming'; // Response slate - this means we're receiving + } else if (signedParticipants >= participants.length) { + status = 'S3'; + type = 'Outgoing'; // Finalized slate - completed outgoing transaction + } + } + + // Fallback: check for explicit 'sta' field (some slates may have this) + if (status == 'Unknown' && slateData['sta'] != null) { + status = "${slateData['sta']}"; + if (status == 'S1') { + type = 'Outgoing'; + } else if (status == 'S2') { + type = 'Incoming'; + } else if (status == 'S3') { + type = 'Outgoing'; + } + } + + return ( + type: type, + status: status, + amount: amountStr, + wasEncrypted: decoded.wasEncrypted, + senderAddress: decoded.senderAddress, + recipientAddress: decoded.recipientAddress, + slateId: slateId, + ); + } catch (e) { + // If we can't decode it, return unknown + return ( + type: 'Unknown', + status: 'Unknown', + amount: null, + wasEncrypted: false, + senderAddress: null, + recipientAddress: null, + slateId: '', + ); + } + } + + /// Improved transaction type detection for slatepacks. + /// This replaces "Unknown" types with better determined types based on slate analysis. + Future getSlatepackTransactionType(String address) async { + try { + // Check if the address is actually a slatepack + if (!isSlatepack(address)) { + return 'Unknown'; + } + + // Analyze the slatepack to determine the actual transaction type + final analysis = await analyzeSlatepack(address); + + // Map slate status to meaningful transaction types + switch (analysis.status) { + case 'S1': + return 'Outgoing'; // Initial send slate - this is outgoing + case 'S2': + return 'Incoming'; // Response slate - this means we're receiving + case 'S3': + return 'Outgoing'; // Finalized slate - completed outgoing transaction + default: + return analysis.type; // Fall back to our basic analysis + } + } catch (e) { + // If analysis fails, return Unknown + return 'Unknown'; + } + } + + /// Enhanced transaction type detection that can analyze slatepack transactions. + /// Use this method to improve "Unknown" transaction types after they're loaded. + Future getEnhancedTransactionType( + TransactionV2 transaction, + ) async { + try { + // If transaction is already properly typed, return as-is + if (transaction.type != TransactionType.unknown) { + return transaction.type; + } + + // Check if this is a MWC transaction with slatepack data + if (transaction.isMimblewimblecoinTransaction) { + // Try to analyze any slatepack addresses in the transaction + for (final output in transaction.outputs) { + for (final address in output.addresses) { + if (isSlatepack(address)) { + final slatepackType = await getSlatepackTransactionType(address); + switch (slatepackType) { + case 'Outgoing': + return TransactionType.outgoing; + case 'Incoming': + return TransactionType.incoming; + default: + continue; + } + } + } + } + } + + // If we can't determine a better type, return unknown + return TransactionType.unknown; + } catch (e) { + Logging.instance.w("Failed to enhance transaction type: $e"); + return transaction.type; + } + } + + // ================= Private ================================================= + + Future _getConfig() async { + if (_mimblewimblecoinNode == null) { + await updateNode(); + } + final NodeModel node = _mimblewimblecoinNode!; + final String nodeAddress = node.host; + final int port = node.port; + + final uri = Uri.parse(nodeAddress).replace(port: port); + + final String nodeApiAddress = uri.toString(); + final walletDir = await _currentWalletDirPath(); + + final Map config = {}; + config["wallet_dir"] = walletDir; + config["check_node_api_http_addr"] = nodeApiAddress; + config["chain"] = "mainnet"; + config["account"] = "default"; + final String stringConfig = jsonEncode(config); + return stringConfig; + } + + Future _currentWalletDirPath() async { + final Directory appDir = await StackFileSystem.applicationRootDirectory(); + + final path = "${appDir.path}/mimblewimblecoin"; + final String name = walletId.trim(); + return '$path/$name'; + } + + Future _nativeFee( + int satoshiAmount, { + bool ifErrorEstimateFee = false, + }) async { + final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); + try { + final available = info.cachedBalance.spendable.raw.toInt(); + final transactionFees = await mimblewimblecoin.Libmwc.getTransactionFees( + wallet: wallet!, + amount: satoshiAmount, + minimumConfirmations: cryptoCurrency.minConfirms, + available: available, + ); + + int realFee = 0; + try { + realFee = + (Decimal.parse(transactionFees.fee.toString())).toBigInt().toInt(); + } catch (e, s) { + //todo: come back to this + debugPrint("$e $s"); + } + return realFee; + } catch (e, s) { + Logging.instance.e("Error getting fees $e - $s"); + rethrow; + } + } + + Future _startSync() async { + Logging.instance.i("request start sync"); + final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); + const int refreshFromNode = 1; + if (!syncMutex.isLocked) { + await syncMutex.protect(() async { + // How does getWalletBalances start syncing???? + await mimblewimblecoin.Libmwc.getWalletBalances( + wallet: wallet!, + refreshFromNode: refreshFromNode, + minimumConfirmations: 10, + ); + }); + } else { + Logging.instance.i("request start sync denied"); + } + } + + Future< + ({ + double awaitingFinalization, + double pending, + double spendable, + double total, + }) + > + _allWalletBalances() async { + final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); + const refreshFromNode = 0; + return await mimblewimblecoin.Libmwc.getWalletBalances( + wallet: wallet!, + refreshFromNode: refreshFromNode, + minimumConfirmations: cryptoCurrency.minConfirms, + ); + } + + Future _putSendToAddresses( + ({String slateId, String commitId}) slateData, + Map txAddressInfo, + ) async { + try { + final slatesToCommits = info.mimblewimblecoinData?.slatesToCommits ?? {}; + final from = txAddressInfo['from']; + final to = txAddressInfo['to']; + slatesToCommits[slateData.slateId] = { + "commitId": slateData.commitId, + "from": from, + "to": to, + }; + await info.updateExtraMimblewimblecoinWalletInfo( + mimblewimblecoinData: info.mimblewimblecoinData!.copyWith( + slatesToCommits: slatesToCommits, + ), + isar: mainDB.isar, + ); + return true; + } catch (e, s) { + Logging.instance.e("ERROR STORING ADDRESS $e $s"); + return false; + } + } + + Future _getCurrentIndex() async { + try { + final int receivingIndex = info.mimblewimblecoinData!.receivingIndex; + // TODO: go through pendingarray and processed array and choose the index + // of the last one that has not been processed, or the index after the one most recently processed; + return receivingIndex; + } catch (e, s) { + Logging.instance.e("$e $s"); + return 0; + } + } + + Future
_generateAndStoreReceivingAddressForIndex(int index) async { + Address? address = await getCurrentReceivingAddress(); + + if (address == null) { + final mwcmqsConfig = await getMwcMqsConfig(); + address = await thisWalletAddress(index, mwcmqsConfig); + } + + if (info.cachedReceivingAddress != address.value) { + await info.updateReceivingAddress( + newAddress: address.value, + isar: mainDB.isar, + ); + } + return address; + } + + Future
thisWalletAddress( + int index, + MwcMqsConfigModel mwcmqsConfig, + ) async { + final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); + + final walletAddress = await mimblewimblecoin.Libmwc.getAddressInfo( + wallet: wallet!, + index: index, + ); + + Logging.instance.i("WALLET_ADDRESS_IS $walletAddress"); + + final address = Address( + walletId: walletId, + value: walletAddress, + derivationIndex: index, + derivationPath: null, + type: AddressType.mimbleWimble, + subType: AddressSubType.receiving, + publicKey: [], // ?? + ); + await mainDB.updateOrPutAddresses([address]); + return address; + } + + Future _startScans() async { + try { + //First stop the current listener + mimblewimblecoin.Libmwc.stopMwcMqsListener(); + final wallet = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); + + // max number of blocks to scan per loop iteration + const scanChunkSize = 10000; + + // force firing of scan progress event + await getSyncPercent; + + // fetch current chain height and last scanned block (should be the + // restore height if full rescan or a wallet restore) + int chainHeight = await this.chainHeight; + int lastScannedBlock = info.mimblewimblecoinData!.lastScannedBlock; + + // loop while scanning in chain in chunks (of blocks?) + while (lastScannedBlock < chainHeight) { + Logging.instance.i( + "chainHeight: $chainHeight, lastScannedBlock: $lastScannedBlock", + ); + + final int nextScannedBlock = await mimblewimblecoin.Libmwc.scanOutputs( + wallet: wallet!, + startHeight: lastScannedBlock, + numberOfBlocks: scanChunkSize, + ); + + // update local cache + await info.updateExtraMimblewimblecoinWalletInfo( + mimblewimblecoinData: info.mimblewimblecoinData!.copyWith( + lastScannedBlock: nextScannedBlock, + ), + isar: mainDB.isar, + ); + + // force firing of scan progress event + await getSyncPercent; + + // update while loop condition variables + chainHeight = await this.chainHeight; + lastScannedBlock = nextScannedBlock; + } + + Logging.instance.i("_startScans successfully at the tip"); + //Once scanner completes restart listener + await _listenToMwcmqs(); + } catch (e, s) { + Logging.instance.e("_startScans failed: $e\n$s"); + rethrow; + } + } + + Future _listenToMwcmqs() async { + Logging.instance.i("STARTING WALLET LISTENER ...."); + final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); + final MwcMqsConfigModel mwcmqsConfig = await getMwcMqsConfig(); + mimblewimblecoin.Libmwc.startMwcMqsListener( + wallet: wallet!, + mwcmqsConfig: mwcmqsConfig.toString(), + ); + } + + // As opposed to fake config? + Future _getRealConfig() async { + String? config = await secureStorageInterface.read( + key: '${walletId}_config', + ); + if (Platform.isIOS) { + final walletDir = await _currentWalletDirPath(); + final editConfig = jsonDecode(config as String); + + editConfig["wallet_dir"] = walletDir; + config = jsonEncode(editConfig); + } + return config!; + } + + int _calculateRestoreHeightFrom({required DateTime date}) { + final int secondsSinceEpoch = date.millisecondsSinceEpoch ~/ 1000; + const int mimblewimblecoinFirstBlock = 1565370278; + const double overestimateSecondsPerBlock = 61; + final int chosenSeconds = secondsSinceEpoch - mimblewimblecoinFirstBlock; + final int approximateHeight = chosenSeconds ~/ overestimateSecondsPerBlock; + int height = approximateHeight; + if (height < 0) { + height = 0; + } + return height; + } + + // ============== Overrides ================================================== + + @override + int get isarTransactionVersion => 2; + + @override + FilterOperation? get changeAddressFilterOperation => + FilterGroup.and(standardChangeAddressFilters); + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + @override + Future checkSaveInitialReceivingAddress() async { + // epiccash seems ok with nothing here? + } + + @override + Future init({bool? isRestore}) async { + if (isRestore != true) { + String? encodedWallet = await secureStorageInterface.read( + key: "${walletId}_wallet", + ); + + // check if should create a new wallet + if (encodedWallet == null) { + await updateNode(); + final mnemonicString = await getMnemonic(); + + final String password = generatePassword(); + final String stringConfig = await _getConfig(); + final MwcMqsConfigModel mwcmqsConfig = await getMwcMqsConfig(); + //if (!_logsInitialized) { + // await mimblewimblecoin.Libmwc.initLogs(config: stringConfig); + // _logsInitialized = true; // Set flag to true after initializing + // } + await secureStorageInterface.write( + key: '${walletId}_config', + value: stringConfig, + ); + await secureStorageInterface.write( + key: '${walletId}_password', + value: password, + ); + await secureStorageInterface.write( + key: '${walletId}_mwcmqsConfig', + value: mwcmqsConfig.toString(), + ); + + final String name = walletId; + + await mimblewimblecoin.Libmwc.initializeNewWallet( + config: stringConfig, + mnemonic: mnemonicString, + password: password, + name: name, + ); + + //Open wallet + encodedWallet = await mimblewimblecoin.Libmwc.openWallet( + config: stringConfig, + password: password, + ); + await secureStorageInterface.write( + key: '${walletId}_wallet', + value: encodedWallet, + ); + //Store MwcMqs address info + await _generateAndStoreReceivingAddressForIndex(0); + + // subtract a couple days to ensure we have a buffer for SWB + final bufferedCreateHeight = _calculateRestoreHeightFrom( + date: DateTime.now().subtract(const Duration(days: 2)), + ); + + final mimblewimblecoinData = ExtraMimblewimblecoinWalletInfo( + receivingIndex: 0, + changeIndex: 0, + slatesToAddresses: {}, + slatesToCommits: {}, + lastScannedBlock: bufferedCreateHeight, + restoreHeight: bufferedCreateHeight, + creationHeight: bufferedCreateHeight, + ); + + await info.updateExtraMimblewimblecoinWalletInfo( + mimblewimblecoinData: mimblewimblecoinData, + isar: mainDB.isar, + ); + } else { + try { + final config = await _getRealConfig(); + //if (!_logsInitialized) { + // await mimblewimblecoin.Libmwc.initLogs(config: config); + // _logsInitialized = true; // Set flag to true after initializing + //} + final password = await secureStorageInterface.read( + key: '${walletId}_password', + ); + + final walletOpen = await mimblewimblecoin.Libmwc.openWallet( + config: config, + password: password!, + ); + await secureStorageInterface.write( + key: '${walletId}_wallet', + value: walletOpen, + ); + + await updateNode(); + } catch (e, s) { + // do nothing, still allow user into wallet + Logging.instance.e("$runtimeType init() failed: $e\n$s"); + } + } + } + + return await super.init(); + } + + @override + Future confirmSend({required TxData txData}) async { + try { + final wallet = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); + final MwcMqsConfigModel mwcmqsConfig = await getMwcMqsConfig(); + + // TODO determine whether it is worth sending change to a change address. + + final String receiverAddress = txData.recipients!.first.address; + + //if (!receiverAddress.startsWith("http://") || + // !receiverAddress.startsWith("https://")) { + // final bool isMwcmqsConnected = await _testMwcmqsServer( + // mwcmqsConfig, + // ); + // if (!isMwcmqsConnected) { + // throw Exception( + // "Failed to send TX : Unable to reach mimblewimblecoin server"); + // } + //} + + ({String commitId, String slateId}) transaction; + + if (receiverAddress.startsWith("http://") || + receiverAddress.startsWith("https://")) { + transaction = await mimblewimblecoin.Libmwc.txHttpSend( + wallet: wallet!, + selectionStrategyIsAll: 0, + minimumConfirmations: cryptoCurrency.minConfirms, + message: txData.noteOnChain ?? "", + amount: txData.recipients!.first.amount.raw.toInt(), + address: txData.recipients!.first.address, + ); + } else if (receiverAddress.startsWith("mwcmqs://")) { + transaction = await mimblewimblecoin.Libmwc.createTransaction( + wallet: wallet!, + amount: txData.recipients!.first.amount.raw.toInt(), + address: txData.recipients!.first.address, + secretKeyIndex: 0, + mwcmqsConfig: mwcmqsConfig.toString(), + minimumConfirmations: cryptoCurrency.minConfirms, + note: txData.noteOnChain!, + ); + } else { + throw Exception( + "Unsupported address format: $receiverAddress. Please use a valid address.", + ); + } + + final Map txAddressInfo = {}; + txAddressInfo['from'] = (await getCurrentReceivingAddress())!.value; + txAddressInfo['to'] = txData.recipients!.first.address; + await _putSendToAddresses(transaction, txAddressInfo); + + return txData.copyWith(txid: transaction.slateId); + } catch (e, s) { + Logging.instance.e("Mimblewimblecoin confirmSend: $e\n$s"); + rethrow; + } + } + + @override + Future prepareSend({required TxData txData}) async { + try { + if (txData.recipients?.length != 1) { + throw Exception( + "Mimblewimblecoin prepare send requires a single recipient!", + ); + } + + TxRecipient recipient = txData.recipients!.first; + final String receiverAddress = recipient.address; + + // Check if this is a slatepack being provided instead of an address. + if (isSlatepack(receiverAddress)) { + // For slatepack input, we need different handling + // This would be used for receiving/finalizing slatepacks. + return txData.copyWith( + fee: Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits), + otherData: jsonEncode({'isSlatepackInput': true}), + ); + } + + // For regular address-based sends, calculate fee + final int realFee = await _nativeFee(recipient.amount.raw.toInt()); + final feeAmount = Amount( + rawValue: BigInt.from(realFee), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + if (feeAmount > info.cachedBalance.spendable) { + throw Exception( + "Mimblewimblecoin prepare send fee is greater than available balance!", + ); + } + + if (info.cachedBalance.spendable == recipient.amount) { + recipient = TxRecipient( + address: recipient.address, + amount: recipient.amount - feeAmount, + isChange: recipient.isChange, + addressType: AddressType.mimbleWimble, + ); + } + + // Determine transaction method based on address format. + String txMethod = 'unknown'; + if (isMwcmqsAddress(receiverAddress)) { + txMethod = 'mwcmqs'; + } else if (isHttpAddress(receiverAddress)) { + txMethod = 'http'; + } else if (validateMwcAddress(receiverAddress)) { + txMethod = 'slatepack'; // Manual slatepack exchange. + } + + return txData.copyWith( + recipients: [recipient], + fee: feeAmount, + otherData: jsonEncode({'transactionMethod': txMethod}), + ); + } catch (e, s) { + Logging.instance.e("Mimblewimblecoin prepareSend: $e\n$s"); + rethrow; + } + } + + @override + Future recover({required bool isRescan}) async { + try { + await refreshMutex.protect(() async { + if (isRescan) { + // clear blockchain info + await mainDB.deleteWalletBlockchainData(walletId); + + await info.updateExtraMimblewimblecoinWalletInfo( + mimblewimblecoinData: info.mimblewimblecoinData!.copyWith( + lastScannedBlock: info.mimblewimblecoinData!.restoreHeight, + ), + isar: mainDB.isar, + ); + + unawaited(_startScans()); + } else { + await updateNode(); + final String password = generatePassword(); + + final String stringConfig = await _getConfig(); + final MwcMqsConfigModel mwcmqsConfig = await getMwcMqsConfig(); + + await secureStorageInterface.write( + key: '${walletId}_config', + value: stringConfig, + ); + await secureStorageInterface.write( + key: '${walletId}_password', + value: password, + ); + + await secureStorageInterface.write( + key: '${walletId}_mwcmqsConfig', + value: mwcmqsConfig.toString(), + ); + + await mimblewimblecoin.Libmwc.recoverWallet( + config: stringConfig, + password: password, + mnemonic: await getMnemonic(), + name: info.walletId, + ); + + final mimblewimblecoinData = ExtraMimblewimblecoinWalletInfo( + receivingIndex: 0, + changeIndex: 0, + slatesToAddresses: {}, + slatesToCommits: {}, + lastScannedBlock: info.restoreHeight, + restoreHeight: info.restoreHeight, + creationHeight: + info.mimblewimblecoinData?.creationHeight ?? info.restoreHeight, + ); + + await info.updateExtraMimblewimblecoinWalletInfo( + mimblewimblecoinData: mimblewimblecoinData, + isar: mainDB.isar, + ); + + //Open Wallet + final walletOpen = await mimblewimblecoin.Libmwc.openWallet( + config: stringConfig, + password: password, + ); + await secureStorageInterface.write( + key: '${walletId}_wallet', + value: walletOpen, + ); + + await _generateAndStoreReceivingAddressForIndex( + mimblewimblecoinData.receivingIndex, + ); + } + }); + + unawaited(refresh()); + } catch (e, s) { + Logging.instance.i( + "Exception rethrown from electrumx_mixin recover(): $e\n$s", + ); + + rethrow; + } + } + + @override + Future refresh() async { + // Awaiting this lock could be dangerous. + // Since refresh is periodic (generally) + if (refreshMutex.isLocked) { + return; + } + + try { + // this acquire should be almost instant due to above check. + // Slight possibility of race but should be irrelevant + await refreshMutex.acquire(); + + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + cryptoCurrency, + ), + ); + + // if (info.epicData?.creationHeight == null) { + // await info.updateExtraEpiccashWalletInfo(epicData: inf, isar: isar) + // await epicUpdateCreationHeight(await chainHeight); + // } + + // this will always be zero???? + final int curAdd = await _getCurrentIndex(); + await _generateAndStoreReceivingAddressForIndex(curAdd); + + await _startScans(); + + unawaited(_startSync()); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); + await updateChainHeight(); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); + + // if (this is MultiAddressInterface) { + // await (this as MultiAddressInterface) + // .checkReceivingAddressForTransactions(); + // } + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); + + // // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. + // if (this is MultiAddressInterface) { + // await (this as MultiAddressInterface) + // .checkChangeAddressForTransactions(); + // } + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.50, walletId)); + final fetchFuture = updateTransactions(); + // if (currentHeight != storedHeight) { + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.60, walletId)); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.70, walletId)); + + await fetchFuture; + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.80, walletId)); + + // await getAllTxsToWatch(); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.90, walletId)); + + await updateBalance(); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + cryptoCurrency, + ), + ); + + if (shouldAutoSync) { + timer ??= Timer.periodic(const Duration(seconds: 150), (timer) async { + // chain height check currently broken + // if ((await chainHeight) != (await storedChainHeight)) { + + // TODO: [prio=med] some kind of quick check if wallet needs to refresh to replace the old refreshIfThereIsNewData call + // if (await refreshIfThereIsNewData()) { + unawaited(refresh()); + + // } + // } + }); + } + } catch (error, strace) { + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + cryptoCurrency, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + cryptoCurrency, + ), + ); + Logging.instance.e( + "Caught exception in refreshWalletData(): $error\n$strace", + ); + } finally { + refreshMutex.release(); + } + } + + @override + Future updateBalance() async { + try { + final balances = await _allWalletBalances(); + final balance = Balance( + total: Amount.fromDecimal( + Decimal.parse(balances.total.toString()) + + Decimal.parse(balances.awaitingFinalization.toString()), + fractionDigits: cryptoCurrency.fractionDigits, + ), + spendable: Amount.fromDecimal( + Decimal.parse(balances.spendable.toString()), + fractionDigits: cryptoCurrency.fractionDigits, + ), + blockedTotal: Amount.zeroWith( + fractionDigits: cryptoCurrency.fractionDigits, + ), + pendingSpendable: Amount.fromDecimal( + Decimal.parse(balances.pending.toString()), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + + await info.updateBalance(newBalance: balance, isar: mainDB.isar); + } catch (e, s) { + Logging.instance.e( + "Mimblewimblecoin wallet failed to update balance: $e\n$s", + ); + } + } + + @override + Future updateTransactions() async { + try { + final wallet = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); + const refreshFromNode = 1; + + final myAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .typeEqualTo(AddressType.mimbleWimble) + .and() + .subTypeEqualTo(AddressSubType.receiving) + .and() + .valueIsNotEmpty() + .valueProperty() + .findAll(); + final myAddressesSet = myAddresses.toSet(); + + final transactions = await mimblewimblecoin.Libmwc.getTransactions( + wallet: wallet!, + refreshFromNode: refreshFromNode, + ); + + final List txns = []; + + final slatesToCommits = info.mimblewimblecoinData?.slatesToCommits ?? {}; + + for (final tx in transactions) { + Logging.instance.i("tx: $tx"); + + final isIncoming = + tx.txType == mimblewimblecoin_models.TransactionType.TxReceived || + tx.txType == + mimblewimblecoin_models.TransactionType.TxReceivedCancelled; + final slateId = tx.txSlateId; + final commitId = slatesToCommits[slateId]?['commitId'] as String?; + final numberOfMessages = tx.messages?.messages.length; + final onChainNote = tx.messages?.messages[0].message; + final addressFrom = slatesToCommits[slateId]?["from"] as String?; + final addressTo = slatesToCommits[slateId]?["to"] as String?; + + final credit = int.parse(tx.amountCredited); + final debit = int.parse(tx.amountDebited); + final fee = int.tryParse(tx.fee ?? "0") ?? 0; + + // hack Mimblewimblecoin tx data into inputs and outputs + final List outputs = []; + final List inputs = []; + final addressFromIsMine = myAddressesSet.contains(addressFrom); + final addressToIsMine = myAddressesSet.contains(addressTo); + + OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "00", + valueStringSats: credit.toString(), + addresses: [if (addressFrom != null) addressFrom], + walletOwns: true, + ); + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: null, + scriptSigAsm: null, + sequence: null, + outpoint: null, + addresses: [if (addressTo != null) addressTo], + valueStringSats: debit.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ); + + final TransactionType txType; + if (isIncoming) { + if (addressToIsMine && addressFromIsMine) { + txType = TransactionType.sentToSelf; + } else { + txType = TransactionType.incoming; + } + output = output.copyWith( + addresses: [ + myAddressesSet + .first, // Must be changed if we ever do more than a single wallet address!!! + ], + walletOwns: true, + ); + } else { + // For outgoing transactions, check if we have a slatepack address to analyze + TransactionType determinedType = TransactionType.outgoing; + + // Try to get better type determination for slatepack transactions + if (addressTo != null) { + try { + final slatepackType = await getSlatepackTransactionType( + addressTo, + ); + if (slatepackType == 'Incoming') { + determinedType = TransactionType.incoming; + } else if (slatepackType == 'Outgoing') { + determinedType = TransactionType.outgoing; + } + // If slatepackType is 'Unknown', we keep the original outgoing type + } catch (e) { + // If analysis fails, keep original type determination + Logging.instance.w( + "Failed to analyze slatepack for better type detection: $e", + ); + } + } + + txType = determinedType; + } + + outputs.add(output); + inputs.add(input); + + final otherData = { + "isMimblewimblecoinTransaction": true, + "numberOfMessages": numberOfMessages, + "slateId": slateId, + "onChainNote": onChainNote, + "isCancelled": + tx.txType == + mimblewimblecoin_models.TransactionType.TxSentCancelled || + tx.txType == + mimblewimblecoin_models.TransactionType.TxReceivedCancelled, + "overrideFee": + Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + }; + + final txn = TransactionV2( + walletId: walletId, + blockHash: null, + hash: commitId ?? tx.id.toString(), + txid: commitId ?? tx.id.toString(), + timestamp: + DateTime.parse(tx.creationTs).millisecondsSinceEpoch ~/ 1000, + height: tx.confirmed ? tx.kernelLookupMinHeight ?? 1 : null, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + version: 0, + type: txType, + subType: TransactionSubType.none, + otherData: jsonEncode(otherData), + ); + + txns.add(txn); + } + + await mainDB.isar.writeTxn(() async { + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .deleteAll(); + await mainDB.isar.transactionV2s.putAll(txns); + }); + } catch (e, s) { + Logging.instance.w( + "${cryptoCurrency.runtimeType} ${cryptoCurrency.network} net wallet" + " \"${info.name}\"_${info.walletId} updateTransactions() failed: $e\n$s", + ); + } + } + + @override + Future updateUTXOs() async { + // not used for mimblewimblecoin + return false; + } + + @override + Future updateNode() async { + _mimblewimblecoinNode = getCurrentNode(); + + // TODO: [prio=low] move this out of secure storage if secure storage not needed + final String stringConfig = await _getConfig(); + await secureStorageInterface.write( + key: '${walletId}_config', + value: stringConfig, + ); + + // unawaited(refresh()); + } + + @override + Future pingCheck() async { + try { + final node = nodeService.getPrimaryNodeFor(currency: cryptoCurrency); + return await testMwcNodeConnection( + NodeFormData() + ..host = node!.host + ..useSSL = node.useSSL + ..port = node.port, + ) != + null; + } catch (e, s) { + Logging.instance.i("$e\n$s"); + return false; + } + } + + @override + Future updateChainHeight() async { + final config = await _getRealConfig(); + final latestHeight = await mimblewimblecoin.Libmwc.getChainHeight( + config: config, + ); + await info.updateCachedChainHeight( + newHeight: latestHeight, + isar: mainDB.isar, + ); + } + + @override + Future estimateFeeFor(Amount amount, BigInt feeRate) async { + // setting ifErrorEstimateFee doesn't do anything as its not used in the nativeFee function????? + final int currentFee = await _nativeFee( + amount.raw.toInt(), + ifErrorEstimateFee: true, + ); + return Amount( + rawValue: BigInt.from(currentFee), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + Future get fees async { + // this wasn't done before the refactor either so... + // TODO: implement _getFees + return FeeObject( + numberOfBlocksFast: 10, + numberOfBlocksAverage: 10, + numberOfBlocksSlow: 10, + fast: BigInt.one, + medium: BigInt.one, + slow: BigInt.one, + ); + } + + @override + Future updateSentCachedTxData({required TxData txData}) async { + // TODO: [prio=low] Was not used before refactor so maybe not required(?) + return txData; + } + + @override + Future exit() async { + timer?.cancel(); + timer = null; + await super.exit(); + Logging.instance.i("Mimblewimblecoin_wallet exit finished"); + } +} + +Future deleteMimblewimblecoinWallet({ + required String walletId, + required SecureStorageInterface secureStore, +}) async { + final wallet = await secureStore.read(key: '${walletId}_wallet'); + String? config = await secureStore.read(key: '${walletId}_config'); + if (Platform.isIOS) { + final Directory appDir = await StackFileSystem.applicationRootDirectory(); + + final path = "${appDir.path}/mimblewimblecoin"; + final String name = walletId.trim(); + final walletDir = '$path/$name'; + + final editConfig = jsonDecode(config as String); + + editConfig["wallet_dir"] = walletDir; + config = jsonEncode(editConfig); + } + + if (wallet == null) { + return "Tried to delete non existent mimblewimblecoin wallet file with walletId=$walletId"; + } else { + try { + return mimblewimblecoin.Libmwc.deleteWallet( + wallet: wallet, + config: config!, + ); + } catch (e, s) { + Logging.instance.e("$e\n$s"); + return "deleteMimblewimblecoinWallet($walletId) failed..."; + } + } +} diff --git a/lib/wallets/wallet/supporting/mimblewimblecoin_wallet_info_extension.dart b/lib/wallets/wallet/supporting/mimblewimblecoin_wallet_info_extension.dart new file mode 100644 index 0000000000..f9de38fca5 --- /dev/null +++ b/lib/wallets/wallet/supporting/mimblewimblecoin_wallet_info_extension.dart @@ -0,0 +1,111 @@ +import 'dart:convert'; + +import 'package:isar/isar.dart'; + +import '../../../utilities/logger.dart'; +import '../../isar/models/wallet_info.dart'; + +extension MimblewimblecoinWalletInfoExtension on WalletInfo { + ExtraMimblewimblecoinWalletInfo? get mimblewimblecoinData { + final String? data = + otherData[WalletInfoKeys.mimblewimblecoinData] as String?; + if (data == null) { + return null; + } + try { + return ExtraMimblewimblecoinWalletInfo.fromMap( + Map.from(jsonDecode(data) as Map), + ); + } catch (e, s) { + Logging.instance.e( + "ExtraMimblewimblecoinWalletInfo.fromMap failed: $e\n$s", + ); + return null; + } + } + + Future updateExtraMimblewimblecoinWalletInfo({ + required ExtraMimblewimblecoinWalletInfo mimblewimblecoinData, + required Isar isar, + }) async { + await updateOtherData( + newEntries: { + WalletInfoKeys.mimblewimblecoinData: jsonEncode( + mimblewimblecoinData.toMap(), + ), + }, + isar: isar, + ); + } +} + +/// Holds data previously stored in hive +class ExtraMimblewimblecoinWalletInfo { + final int receivingIndex; + final int changeIndex; + + // TODO [prio=low] strongly type these maps at some point + final Map slatesToAddresses; + final Map slatesToCommits; + + final int lastScannedBlock; + final int restoreHeight; + final int creationHeight; + + ExtraMimblewimblecoinWalletInfo({ + required this.receivingIndex, + required this.changeIndex, + required this.slatesToAddresses, + required this.slatesToCommits, + required this.lastScannedBlock, + required this.restoreHeight, + required this.creationHeight, + }); + + // Convert the object to JSON + Map toMap() { + return { + 'receivingIndex': receivingIndex, + 'changeIndex': changeIndex, + 'slatesToAddresses': slatesToAddresses, + 'slatesToCommits': slatesToCommits, + 'lastScannedBlock': lastScannedBlock, + 'restoreHeight': restoreHeight, + 'creationHeight': creationHeight, + }; + } + + ExtraMimblewimblecoinWalletInfo.fromMap(Map json) + : receivingIndex = json['receivingIndex'] as int, + changeIndex = json['changeIndex'] as int, + slatesToAddresses = json['slatesToAddresses'] as Map, + slatesToCommits = json['slatesToCommits'] as Map, + lastScannedBlock = json['lastScannedBlock'] as int, + restoreHeight = json['restoreHeight'] as int, + creationHeight = json['creationHeight'] as int; + + ExtraMimblewimblecoinWalletInfo copyWith({ + int? receivingIndex, + int? changeIndex, + Map? slatesToAddresses, + Map? slatesToCommits, + int? lastScannedBlock, + int? restoreHeight, + int? creationHeight, + }) { + return ExtraMimblewimblecoinWalletInfo( + receivingIndex: receivingIndex ?? this.receivingIndex, + changeIndex: changeIndex ?? this.changeIndex, + slatesToAddresses: slatesToAddresses ?? this.slatesToAddresses, + slatesToCommits: slatesToCommits ?? this.slatesToCommits, + lastScannedBlock: lastScannedBlock ?? this.lastScannedBlock, + restoreHeight: restoreHeight ?? this.restoreHeight, + creationHeight: creationHeight ?? this.creationHeight, + ); + } + + @override + String toString() { + return toMap().toString(); + } +} diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 55319d12e8..1d3a49b894 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -34,6 +34,7 @@ import 'impl/dash_wallet.dart'; import 'impl/dogecoin_wallet.dart'; import 'impl/ecash_wallet.dart'; import 'impl/epiccash_wallet.dart'; +import 'impl/mimblewimblecoin_wallet.dart'; import 'impl/ethereum_wallet.dart'; import 'impl/fact0rn_wallet.dart'; import 'impl/firo_wallet.dart'; @@ -362,6 +363,9 @@ abstract class Wallet { case const (Epiccash): return EpiccashWallet(net); + case const (Mimblewimblecoin): + return MimblewimblecoinWallet(net); + case const (Ethereum): return EthereumWallet(net); diff --git a/lib/widgets/custom_buttons/simple_paste_button.dart b/lib/widgets/custom_buttons/simple_paste_button.dart new file mode 100644 index 0000000000..4dfdf3c7b4 --- /dev/null +++ b/lib/widgets/custom_buttons/simple_paste_button.dart @@ -0,0 +1,60 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2025 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2025-09-16 + * + */ + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../themes/stack_colors.dart'; +import '../../utilities/assets.dart'; +import '../../utilities/clipboard_interface.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../conditional_parent.dart'; + +class SimplePasteButton extends StatelessWidget { + const SimplePasteButton({ + super.key, + required this.onPaste, + this.clipboard = const ClipboardWrapper(), + }); + + final void Function(String?) onPaste; + final ClipboardInterface clipboard; + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: + (child) => + MouseRegion(cursor: SystemMouseCursors.click, child: child), + child: GestureDetector( + onTap: () async { + // TODO handle async better here!!!!!!!!!!!!!!!!!!!!!!!! + final data = await clipboard.getData(Clipboard.kTextPlain); + onPaste(data?.text); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.clipboard, + width: 10, + height: 10, + color: Theme.of(context).extension()!.infoItemIcons, + ), + const SizedBox(width: 4), + Text("Paste", style: STextStyles.link2(context)), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/detail_item.dart b/lib/widgets/detail_item.dart index 13e8975d31..91a662538d 100644 --- a/lib/widgets/detail_item.dart +++ b/lib/widgets/detail_item.dart @@ -35,9 +35,9 @@ class DetailItem extends StatelessWidget { TextStyle detailStyle = STextStyles.w500_14(context); String _detail = detail; if (overrideDetailTextColor != null) { - detailStyle = STextStyles.w500_14(context).copyWith( - color: overrideDetailTextColor, - ); + detailStyle = STextStyles.w500_14( + context, + ).copyWith(color: overrideDetailTextColor); } if (detail.isEmpty && showEmptyDetail) { @@ -51,24 +51,14 @@ class DetailItem extends StatelessWidget { horizontal: horizontal, borderColor: borderColor, expandDetail: expandDetail, - title: disableSelectableText - ? Text( - title, - style: STextStyles.itemSubtitle(context), - ) - : SelectableText( - title, - style: STextStyles.itemSubtitle(context), - ), - detail: disableSelectableText - ? Text( - _detail, - style: detailStyle, - ) - : SelectableText( - _detail, - style: detailStyle, - ), + title: + disableSelectableText + ? Text(title, style: STextStyles.itemSubtitle(context)) + : SelectableText(title, style: STextStyles.itemSubtitle(context)), + detail: + disableSelectableText + ? Text(_detail, style: detailStyle) + : SelectableText(_detail, style: detailStyle), ); } } @@ -95,56 +85,65 @@ class DetailItemBase extends StatelessWidget { Widget build(BuildContext context) { return ConditionalParent( condition: !Util.isDesktop || borderColor != null, - builder: (child) => RoundedWhiteContainer( - padding: Util.isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - borderColor: borderColor, - child: child, - ), + builder: + (child) => RoundedWhiteContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + borderColor: borderColor, + child: child, + ), child: ConditionalParent( condition: Util.isDesktop && borderColor == null, - builder: (child) => Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - child: horizontal - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - title, - if (expandDetail) - const SizedBox( - width: 16, + builder: + (child) => Padding(padding: const EdgeInsets.all(16), child: child), + child: + horizontal + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + title, + if (expandDetail) const SizedBox(width: 16), + ConditionalParent( + condition: expandDetail, + builder: (child) => Expanded(child: child), + child: detail, + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [title, button ?? Container()], ), - ConditionalParent( - condition: expandDetail, - builder: (child) => Expanded(child: child), - child: detail, - ), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - title, - button ?? Container(), - ], - ), - const SizedBox( - height: 5, - ), - ConditionalParent( - condition: expandDetail, - builder: (child) => Expanded(child: child), - child: detail, - ), - ], - ), + const SizedBox(height: 5), + ConditionalParent( + condition: expandDetail, + builder: (child) => Expanded(child: child), + child: detail, + ), + ], + ), ), ); } } + +class DetailDivider extends StatelessWidget { + const DetailDivider({super.key}); + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return Container( + height: 1, + color: Theme.of(context).extension()!.backgroundAppBar, + ); + } else { + return const SizedBox(height: 12); + } + } +} diff --git a/lib/widgets/mwc_txs_method_toggle.dart b/lib/widgets/mwc_txs_method_toggle.dart new file mode 100644 index 0000000000..4381d6e236 --- /dev/null +++ b/lib/widgets/mwc_txs_method_toggle.dart @@ -0,0 +1,67 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../providers/ui/preview_tx_button_state_provider.dart'; +import '../themes/stack_colors.dart'; +import '../utilities/assets.dart'; +import '../utilities/constants.dart'; +import '../utilities/enums/mwc_transaction_method.dart'; +import '../utilities/util.dart'; +import 'toggle.dart'; + +class MwcTxsMethodToggle extends ConsumerWidget { + const MwcTxsMethodToggle({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; + + return Toggle( + onValueChanged: (value) { + ref.read(pSelectedMwcTransactionMethod.notifier).state = + value + ? MwcTransactionMethod.mwcmqs + : MwcTransactionMethod.slatepack; + }, + isOn: + ref.watch(pSelectedMwcTransactionMethod) == + MwcTransactionMethod.mwcmqs, + onColor: + isDesktop + ? Theme.of( + context, + ).extension()!.rateTypeToggleDesktopColorOn + : Theme.of( + context, + ).extension()!.rateTypeToggleColorOn, + offColor: + isDesktop + ? Theme.of( + context, + ).extension()!.rateTypeToggleDesktopColorOff + : Theme.of( + context, + ).extension()!.rateTypeToggleColorOff, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onIcon: Assets.svg.gear, + onText: "Slatepack", + offIcon: Assets.svg.radioSyncing, + offText: "Automatic", + ); + } +} diff --git a/lib/widgets/onetime_popups/tor_has_been_add_dialog.dart b/lib/widgets/onetime_popups/tor_has_been_add_dialog.dart index 9709c46b3d..05dd4311da 100644 --- a/lib/widgets/onetime_popups/tor_has_been_add_dialog.dart +++ b/lib/widgets/onetime_popups/tor_has_been_add_dialog.dart @@ -153,7 +153,7 @@ class _TorHasBeenAddedDialogState extends State<_TorHasBeenAddedDialog> { height: Util.isDesktop ? 24 : 16, ), Text( - "Note: Tor does NOT yet work for Monero or Epic Cash wallets. " + "Note: Tor does NOT yet work for Monero, Mimblewimblecoin or Epic Cash wallets. " "Opening one of these will leak your IP address.", style: Util.isDesktop ? STextStyles.desktopTextMedium(context) diff --git a/lib/widgets/stack_dialog.dart b/lib/widgets/stack_dialog.dart index f601330b6f..2c56aa7c03 100644 --- a/lib/widgets/stack_dialog.dart +++ b/lib/widgets/stack_dialog.dart @@ -180,7 +180,7 @@ class StackOkDialog extends StatelessWidget { child: Row( children: [ Flexible( - child: Text( + child: SelectableText( message!, style: STextStyles.smallMed14(context), ), diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index f4227005e9..211776fe80 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -27,6 +27,7 @@ import '../utilities/constants.dart'; import '../utilities/format.dart'; import '../utilities/text_styles.dart'; import '../utilities/util.dart'; +import '../wallets/crypto_currency/coins/mimblewimblecoin.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; import 'desktop/desktop_dialog.dart'; @@ -63,6 +64,10 @@ class _TransactionCardState extends ConsumerState { return "Restored Funds"; } + if (coin is Mimblewimblecoin && _transaction.slateId == null) { + return "Restored Funds"; + } + final confirmedStatus = _transaction.isConfirmed( currentHeight, minConfirms, @@ -193,6 +198,20 @@ class _TransactionCardState extends ConsumerState { ); return; } + + if (coin is Mimblewimblecoin && _transaction.slateId == null) { + unawaited( + showFloatingFlushBar( + context: context, + message: + "Restored Mimblewimblecoin funds from your Seed have no Data.", + type: FlushBarType.warning, + duration: const Duration(seconds: 5), + ), + ); + return; + } + if (Util.isDesktop) { await showDialog( context: context, diff --git a/lib/widgets/tx_key_widget.dart b/lib/widgets/tx_key_widget.dart index ba8e9b33c4..f9e1fb4bc5 100644 --- a/lib/widgets/tx_key_widget.dart +++ b/lib/widgets/tx_key_widget.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../pages/pinpad_views/pinpad_dialog.dart'; -import '../pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import '../pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart' + as tvd; import '../pages_desktop_specific/password/request_desktop_auth_dialog.dart'; import '../providers/global/wallets_provider.dart'; import '../utilities/text_styles.dart'; @@ -14,11 +15,7 @@ import 'detail_item.dart'; class TxKeyWidget extends ConsumerStatefulWidget { /// The [walletId] MUST be the id of a [LibMoneroWallet]! - const TxKeyWidget({ - super.key, - required this.walletId, - required this.txid, - }); + const TxKeyWidget({super.key, required this.walletId, required this.txid}); final String walletId; final String txid; @@ -41,14 +38,18 @@ class _TxKeyWidgetState extends ConsumerState { try { final verified = await showDialog( context: context, - builder: (context) => Util.isDesktop - ? const RequestDesktopAuthDialog(title: "Show private view key") - : const PinpadDialog( - biometricsAuthenticationTitle: "Show private view key", - biometricsLocalizedReason: - "Authenticate to show private view key", - biometricsCancelButtonString: "CANCEL", - ), + builder: + (context) => + Util.isDesktop + ? const RequestDesktopAuthDialog( + title: "Show private view key", + ) + : const PinpadDialog( + biometricsAuthenticationTitle: "Show private view key", + biometricsLocalizedReason: + "Authenticate to show private view key", + biometricsCancelButtonString: "CANCEL", + ), barrierDismissible: !Util.isDesktop, ); @@ -75,23 +76,17 @@ class _TxKeyWidgetState extends ConsumerState { @override Widget build(BuildContext context) { return DetailItemBase( - button: _private == null - ? CustomTextButton( - text: "Show", - onTap: _loadTxKey, - enabled: _private == null, - ) - : Util.isDesktop - ? IconCopyButton( - data: _private!, - ) - : SimpleCopyButton( - data: _private!, - ), - title: Text( - "Private view key", - style: STextStyles.itemSubtitle(context), - ), + button: + _private == null + ? CustomTextButton( + text: "Show", + onTap: _loadTxKey, + enabled: _private == null, + ) + : Util.isDesktop + ? tvd.IconCopyButton(data: _private!) + : SimpleCopyButton(data: _private!), + title: Text("Private view key", style: STextStyles.itemSubtitle(context)), detail: SelectableText( // TODO _private ?? "*" * 52, // 52 is approx length diff --git a/lib/widgets/wallet_navigation_bar/components/icons/finalize_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/finalize_nav_icon.dart new file mode 100644 index 0000000000..04b19d2135 --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/finalize_nav_icon.dart @@ -0,0 +1,40 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2025 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2025-09-18 + * + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/assets.dart'; + +class FinalizeNavIcon extends StatelessWidget { + const FinalizeNavIcon({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of( + context, + ).extension()!.bottomNavIconIcon.withOpacity(0.4), + borderRadius: BorderRadius.circular(24), + ), + child: Padding( + padding: const EdgeInsets.all(6.0), + child: SvgPicture.asset( + Assets.svg.circleLock, + width: 12, + height: 12, + color: Theme.of(context).extension()!.bottomNavIconIcon, + ), + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 02b019f4d7..2fe5bba128 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_libepiccash_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLibepiccashPlugin"); flutter_libepiccash_plugin_register_with_registrar(flutter_libepiccash_registrar); + g_autoptr(FlPluginRegistrar) flutter_libmwc_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLibmwcPlugin"); + flutter_libmwc_plugin_register_with_registrar(flutter_libmwc_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 239136a988..0ce0471161 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_drop devicelocale flutter_libepiccash + flutter_libmwc flutter_secure_storage_linux isar_flutter_libs sqlite3_flutter_libs diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b30b6beadf..b916c95c86 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -14,6 +14,7 @@ import device_info_plus import devicelocale import file_picker import flutter_libepiccash +import flutter_libmwc import flutter_local_notifications import flutter_secure_storage_macos import isar_flutter_libs @@ -38,6 +39,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FlutterLibepiccashPlugin.register(with: registry.registrar(forPlugin: "FlutterLibepiccashPlugin")) + FlutterLibmwcPlugin.register(with: registry.registrar(forPlugin: "FlutterLibmwcPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 4ef194c2bb..866a1e3310 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -813,11 +813,11 @@ packages: dependency: "direct main" description: path: "." - ref: f0b1300140d45c13e7722f8f8d20308efeba8449 - resolved-ref: f0b1300140d45c13e7722f8f8d20308efeba8449 + ref: "794ab2d7b88b34d64a89518f9b9f41dcc235aca1" + resolved-ref: "794ab2d7b88b34d64a89518f9b9f41dcc235aca1" url: "https://github.com/cypherstack/electrum_adapter.git" source: git - version: "3.0.0" + version: "3.0.2" emojis: dependency: "direct main" description: @@ -940,6 +940,13 @@ packages: 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: @@ -1119,8 +1126,8 @@ packages: dependency: "direct main" description: path: "." - ref: afaad488f5215a9c2c211e5e2f8460237eef60f1 - resolved-ref: afaad488f5215a9c2c211e5e2f8460237eef60f1 + ref: "540d0bc7dc27a97d45d63f412f26818a7f3b8b51" + resolved-ref: "540d0bc7dc27a97d45d63f412f26818a7f3b8b51" url: "https://github.com/cypherstack/fusiondart.git" source: git version: "1.0.0" @@ -1963,11 +1970,10 @@ packages: socks_socket: dependency: transitive description: - path: "." - ref: master - resolved-ref: e6232c53c1595469931ababa878759a067c02e94 - url: "https://github.com/cypherstack/socks_socket.git" - source: git + name: socks_socket + sha256: "53bc7eae40a3aa16ea810b0e9de3bb23ba7beb0b40d09357b89190f2f44374cc" + url: "https://pub.dev" + source: hosted version: "1.1.1" solana: dependency: "direct main" @@ -2200,8 +2206,8 @@ packages: dependency: "direct main" description: path: "." - ref: "752f054b65c500adb9cad578bf183a978e012502" - resolved-ref: "752f054b65c500adb9cad578bf183a978e012502" + ref: "16c9e709e984ec89e8715ce378b038c93ad7add3" + resolved-ref: "16c9e709e984ec89e8715ce378b038c93ad7add3" url: "https://github.com/cypherstack/tor.git" source: git version: "0.0.1" diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index d791b933be..dc904e95dd 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -11,6 +11,7 @@ PLUGINS_DIR=../../crypto_plugins source ../rust_version.sh set_rust_version_for_libepiccash (cd "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android && ./build_all.sh ) +(cd "${PLUGINS_DIR}"/flutter_libmwc/scripts/android && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/android/build_all_campfire.sh b/scripts/android/build_all_campfire.sh index d791b933be..dc904e95dd 100755 --- a/scripts/android/build_all_campfire.sh +++ b/scripts/android/build_all_campfire.sh @@ -11,6 +11,7 @@ PLUGINS_DIR=../../crypto_plugins source ../rust_version.sh set_rust_version_for_libepiccash (cd "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android && ./build_all.sh ) +(cd "${PLUGINS_DIR}"/flutter_libmwc/scripts/android && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/android/build_all_duo.sh b/scripts/android/build_all_duo.sh index 39579d2381..1a0d6058f9 100755 --- a/scripts/android/build_all_duo.sh +++ b/scripts/android/build_all_duo.sh @@ -13,6 +13,7 @@ PLUGINS_DIR=../../crypto_plugins source ../rust_version.sh set_rust_version_for_libepiccash (cd "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android && ./build_all.sh ) +(cd "${PLUGINS_DIR}"/flutter_libmwc/scripts/android && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/app_config/configure_stack_wallet.sh b/scripts/app_config/configure_stack_wallet.sh index 5aa170a980..b407b7d473 100755 --- a/scripts/app_config/configure_stack_wallet.sh +++ b/scripts/app_config/configure_stack_wallet.sh @@ -62,6 +62,7 @@ final List _supportedCoins = List.unmodifiable([ Dogecoin(CryptoCurrencyNetwork.main), Ecash(CryptoCurrencyNetwork.main), Epiccash(CryptoCurrencyNetwork.main), + if (!Platform.isMacOS) Mimblewimblecoin(CryptoCurrencyNetwork.main), Ethereum(CryptoCurrencyNetwork.main), Fact0rn(CryptoCurrencyNetwork.main), Firo(CryptoCurrencyNetwork.main), diff --git a/scripts/app_config/templates/linux/CMakeLists.txt b/scripts/app_config/templates/linux/CMakeLists.txt index 94d6e7f5ee..4c140a4b95 100644 --- a/scripts/app_config/templates/linux/CMakeLists.txt +++ b/scripts/app_config/templates/linux/CMakeLists.txt @@ -137,6 +137,9 @@ install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libepiccash/scripts/linux/build/rust/target/x86_64-unknown-linux-gnu/release/libepic_cash_wallet.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmwc/scripts/linux/build/rust/target/x86_64-unknown-linux-gnu/release/libmwc_wallet.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/linux/build/jsoncpp/build/src/lib_json/libjsoncpp.so.1.7.4" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/linux/build/jsoncpp/build/src/lib_json/libjsoncpp.so.1" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 84826bc289..e73ecadf87 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -47,6 +47,9 @@ dependencies: flutter_libepiccash: path: ./crypto_plugins/flutter_libepiccash + flutter_libmwc: + path: ./crypto_plugins/flutter_libmwc + bitcoindart: git: url: https://github.com/cypherstack/bitcoindart.git diff --git a/scripts/app_config/templates/windows/CMakeLists.txt b/scripts/app_config/templates/windows/CMakeLists.txt index 650a25d76b..7226cf1378 100644 --- a/scripts/app_config/templates/windows/CMakeLists.txt +++ b/scripts/app_config/templates/windows/CMakeLists.txt @@ -83,6 +83,9 @@ install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libepiccash/scripts/windows/build/libepic_cash_wallet.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmwc/scripts/windows/build/libmwc_cash_wallet.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" diff --git a/scripts/ios/build_all.sh b/scripts/ios/build_all.sh index b6f93e35f6..83177db5c2 100755 --- a/scripts/ios/build_all.sh +++ b/scripts/ios/build_all.sh @@ -14,6 +14,7 @@ rustup target add x86_64-apple-ios source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/ios && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/ios/ && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/ios/build_all_campfire.sh b/scripts/ios/build_all_campfire.sh index b6f93e35f6..83177db5c2 100755 --- a/scripts/ios/build_all_campfire.sh +++ b/scripts/ios/build_all_campfire.sh @@ -14,6 +14,7 @@ rustup target add x86_64-apple-ios source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/ios && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/ios/ && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/ios/build_all_duo.sh b/scripts/ios/build_all_duo.sh index ea00ed5990..0b560202b6 100755 --- a/scripts/ios/build_all_duo.sh +++ b/scripts/ios/build_all_duo.sh @@ -16,6 +16,7 @@ rustup target add x86_64-apple-ios source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/ios && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/ios/ && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/linux/build_all.sh b/scripts/linux/build_all.sh index a2e6c3d859..50490b1979 100755 --- a/scripts/linux/build_all.sh +++ b/scripts/linux/build_all.sh @@ -13,6 +13,7 @@ mkdir -p build source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/linux && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/linux && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/linux/build_all_campfire.sh b/scripts/linux/build_all_campfire.sh index a2e6c3d859..50490b1979 100755 --- a/scripts/linux/build_all_campfire.sh +++ b/scripts/linux/build_all_campfire.sh @@ -13,6 +13,7 @@ mkdir -p build source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/linux && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/linux && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/linux/build_all_duo.sh b/scripts/linux/build_all_duo.sh index b9c18f6b23..a29947f4f6 100755 --- a/scripts/linux/build_all_duo.sh +++ b/scripts/linux/build_all_duo.sh @@ -16,6 +16,7 @@ mkdir -p build source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/linux && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/linux && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/linux/build_secp256k1.sh b/scripts/linux/build_secp256k1.sh index 17bcd32c96..e139cc9377 100755 --- a/scripts/linux/build_secp256k1.sh +++ b/scripts/linux/build_secp256k1.sh @@ -10,5 +10,5 @@ mkdir -p build && cd build cmake .. cmake --build . mkdir -p ../../../../../build -cp lib/libsecp256k1.so.2.2.2 "../../../../../build/libsecp256k1.so" +cp lib/libsecp256k1.so.2.*.* "../../../../../build/libsecp256k1.so" cd ../../../ \ No newline at end of file diff --git a/scripts/macos/build_all.sh b/scripts/macos/build_all.sh index 5cfed585f3..de9b79efaa 100755 --- a/scripts/macos/build_all.sh +++ b/scripts/macos/build_all.sh @@ -7,6 +7,7 @@ set -x -e source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/macos && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/macos && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/macos/build_all_campfire.sh b/scripts/macos/build_all_campfire.sh index e13ad106d7..2bc4aaf4bb 100755 --- a/scripts/macos/build_all_campfire.sh +++ b/scripts/macos/build_all_campfire.sh @@ -7,6 +7,7 @@ set -x -e source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/macos && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/macos && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/macos/build_all_duo.sh b/scripts/macos/build_all_duo.sh index a520cafc27..29a0d82420 100755 --- a/scripts/macos/build_all_duo.sh +++ b/scripts/macos/build_all_duo.sh @@ -9,6 +9,7 @@ set -x -e source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/macos && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/macos && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/rust_version.sh b/scripts/rust_version.sh index b894b7a5d9..65bf911f49 100755 --- a/scripts/rust_version.sh +++ b/scripts/rust_version.sh @@ -18,4 +18,3 @@ set_rust_version_for_libepiccash() { exit 1 fi } - diff --git a/scripts/windows/build_all.sh b/scripts/windows/build_all.sh index 3383944106..6d7395bbf3 100755 --- a/scripts/windows/build_all.sh +++ b/scripts/windows/build_all.sh @@ -8,6 +8,7 @@ mkdir -p build source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/windows && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/windows && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/windows/build_all_campfire.sh b/scripts/windows/build_all_campfire.sh index 3383944106..6d7395bbf3 100755 --- a/scripts/windows/build_all_campfire.sh +++ b/scripts/windows/build_all_campfire.sh @@ -8,6 +8,7 @@ mkdir -p build source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/windows && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/windows && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/windows/build_all_duo.sh b/scripts/windows/build_all_duo.sh index e70eb145d2..6a19b94f52 100755 --- a/scripts/windows/build_all_duo.sh +++ b/scripts/windows/build_all_duo.sh @@ -10,6 +10,7 @@ mkdir -p build source ../rust_version.sh set_rust_version_for_libepiccash (cd ../../crypto_plugins/flutter_libepiccash/scripts/windows && ./build_all.sh ) +(cd ../../crypto_plugins/flutter_libmwc/scripts/windows && ./build_all.sh ) # set rust (back) to a more recent stable release after building epiccash set_rust_to_everything_else diff --git a/scripts/windows/deps.sh b/scripts/windows/deps.sh index f68011a69b..6786700705 100644 --- a/scripts/windows/deps.sh +++ b/scripts/windows/deps.sh @@ -1,6 +1,7 @@ #!/bin/bash cd ../../crypto_plugins/flutter_libepiccash/scripts/windows && ./deps.sh +(cd ../../crypto_plugins/flutter_libmwc/scripts/windows && ./deps.sh) sudo apt install libgtk2.0-dev wait diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 22ab2a712c..498fb7da55 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -1223,6 +1223,7 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { ); @override +<<<<<<< bool get advancedFiroFeatures => (super.noSuchMethod( Invocation.getter(#advancedFiroFeatures), returnValue: false, @@ -1262,6 +1263,21 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { ); @override +======= + bool get enableExchange => (super.noSuchMethod( + Invocation.getter(#enableExchange), + returnValue: false, + ) as bool); + @override + set enableExchange(bool? showExchange) => super.noSuchMethod( + Invocation.setter( + #enableExchange, + showExchange, + ), + returnValueForMissingStub: null, + ); + @override +>>>>>>> bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 7ea2055fce..35c4f4ba06 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -1143,6 +1143,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override +<<<<<<< bool get advancedFiroFeatures => (super.noSuchMethod( Invocation.getter(#advancedFiroFeatures), returnValue: false, @@ -1182,6 +1183,21 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override +======= + bool get enableExchange => (super.noSuchMethod( + Invocation.getter(#enableExchange), + returnValue: false, + ) as bool); + @override + set enableExchange(bool? showExchange) => super.noSuchMethod( + Invocation.setter( + #enableExchange, + showExchange, + ), + returnValueForMissingStub: null, + ); + @override +>>>>>>> bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/price_test.dart b/test/price_test.dart index 2a5b9c0383..e44e6b3a63 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -1,3 +1,5 @@ +// TODO MWC + import 'dart:convert'; import 'dart:io'; diff --git a/test/sample_data/theme_json.dart b/test/sample_data/theme_json.dart index 6f7601b42b..660e680e68 100644 --- a/test/sample_data/theme_json.dart +++ b/test/sample_data/theme_json.dart @@ -10,6 +10,7 @@ const Map lightThemeJsonMap = { "firo": "0xFFFF897A", "dogecoin": "0xFFFFE079", "epicCash": "0xFFC5C7CB", + "mimblewimblecoin": "0xFFC5C7CB", "ethereum": "0xFFA7ADE9", "monero": "0xFFFF9E6B", "namecoin": "0xFF91B1E1", @@ -186,6 +187,7 @@ const Map lightThemeJsonMap = { "bitcoincash": "dummy.svg", "dogecoin": "dummy.svg", "epicCash": "dummy.svg", + "mimblewimblecoin": "dummy.svg", "ethereum": "dummy.svg", "firo": "dummy.svg", "monero": "dummy.svg", @@ -197,6 +199,7 @@ const Map lightThemeJsonMap = { "bitcoincash_image": "dummy.svg", "dogecoin_image": "dummy.svg", "epicCash_image": "dummy.svg.svg", + "mimblewimblecoin_image": "dummy.svg", "ethereum_image": "dummy.svg", "firo_image": "dummy.svg", "monero_image": "dummy.svg", @@ -208,6 +211,7 @@ const Map lightThemeJsonMap = { "bitcoincash_image_secondary": "dummy.svg", "dogecoin_image_secondary": "dummy.svg", "epicCash_image_secondary": "dummy.svg.svg", + "mimblewimblecoin_image_secondary": "dummy.svg", "ethereum_image_secondary": "dummy.svg", "firo_image_secondary": "dummy.svg", "monero_image_secondary": "dummy.svg", diff --git a/test/sample_data/theme_json_v2.dart b/test/sample_data/theme_json_v2.dart index 3f9fbfb7f3..1327789c02 100644 --- a/test/sample_data/theme_json_v2.dart +++ b/test/sample_data/theme_json_v2.dart @@ -13,6 +13,7 @@ const Map lightThemeJsonMap = { "dogecoin": "0xFFFFE079", "eCash": "0xFFC5C7CB", "epicCash": "0xFFC5C7CB", + "mimblewimblecoin": "0xFFC5C7CB", "ethereum": "0xFFA7ADE9", "monero": "0xFFFF9E6B", "namecoin": "0xFF91B1E1", @@ -194,6 +195,7 @@ const Map lightThemeJsonMap = { "dogecoin": "dummy.svg", "eCash": "dummy.svg", "epicCash": "dummy.svg", + "mimblewimblecoin": "dummy.svg", "ethereum": "dummy.svg", "firo": "dummy.svg", "monero": "dummy.svg", @@ -208,6 +210,7 @@ const Map lightThemeJsonMap = { "dogecoin": "dummy.svg", "eCash": "dummy.svg", "epicCash": "dummy.svg", + "mimblewimblecoin": "dummy.svg", "ethereum": "dummy.svg", "firo": "dummy.svg", "monero": "dummy.svg", @@ -222,6 +225,7 @@ const Map lightThemeJsonMap = { "dogecoin": "dummy.svg", "eCash": "dummy.svg", "epicCash": "dummy.svg", + "mimblewimblecoin": "dummy.svg", "ethereum": "dummy.svg", "firo": "dummy.svg", "monero": "dummy.svg", diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 643d6cc6b9..4e9c72fe13 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -584,6 +584,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ); @override +<<<<<<< bool get advancedFiroFeatures => (super.noSuchMethod( Invocation.getter(#advancedFiroFeatures), returnValue: false, @@ -623,6 +624,21 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ); @override +======= + bool get enableExchange => (super.noSuchMethod( + Invocation.getter(#enableExchange), + returnValue: false, + ) as bool); + @override + set enableExchange(bool? showExchange) => super.noSuchMethod( + Invocation.setter( + #enableExchange, + showExchange, + ), + returnValueForMissingStub: null, + ); + @override +>>>>>>> bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/services/node_service_test.dart b/test/services/node_service_test.dart index cb0acd6de6..9a80477bf0 100644 --- a/test/services/node_service_test.dart +++ b/test/services/node_service_test.dart @@ -1,3 +1,5 @@ +// TODO MWC + import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; @@ -164,6 +166,28 @@ void main() { clearnetEnabled: true, isPrimary: true, ); + final nodeD = NodeModel( + host: "host3", + port: 423, + name: "btcnode", + id: "pnodeID3", + useSSL: true, + enabled: true, + coinName: "mimblewimblecoin", + isFailover: true, + isDown: false, + ); + final nodeD = NodeModel( + host: "host3", + port: 423, + name: "btcnode", + id: "pnodeID3", + useSSL: true, + enabled: true, + coinName: "mimblewimblecoin", + isFailover: true, + isDown: false, + ); setUp(() async { await NodeService( diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 410094cdf6..d19fa2405f 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -867,6 +867,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override +<<<<<<< bool get advancedFiroFeatures => (super.noSuchMethod( Invocation.getter(#advancedFiroFeatures), returnValue: false, @@ -906,6 +907,21 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override +======= + bool get enableExchange => (super.noSuchMethod( + Invocation.getter(#enableExchange), + returnValue: false, + ) as bool); + @override + set enableExchange(bool? showExchange) => super.noSuchMethod( + Invocation.setter( + #enableExchange, + showExchange, + ), + returnValueForMissingStub: null, + ); + @override +>>>>>>> bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index 9b7d998285..5d31195e2b 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -742,6 +742,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override +<<<<<<< bool get advancedFiroFeatures => (super.noSuchMethod( Invocation.getter(#advancedFiroFeatures), returnValue: false, @@ -781,6 +782,21 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override +======= + bool get enableExchange => (super.noSuchMethod( + Invocation.getter(#enableExchange), + returnValue: false, + ) as bool); + @override + set enableExchange(bool? showExchange) => super.noSuchMethod( + Invocation.setter( + #enableExchange, + showExchange, + ), + returnValueForMissingStub: null, + ); + @override +>>>>>>> bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index cd8edb2ab0..cd442044ea 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -840,6 +840,7 @@ class MockPrefs extends _i1.Mock implements _i13.Prefs { ); @override +<<<<<<< bool get advancedFiroFeatures => (super.noSuchMethod( Invocation.getter(#advancedFiroFeatures), returnValue: false, @@ -879,6 +880,21 @@ class MockPrefs extends _i1.Mock implements _i13.Prefs { ); @override +======= + bool get enableExchange => (super.noSuchMethod( + Invocation.getter(#enableExchange), + returnValue: false, + ) as bool); + @override + set enableExchange(bool? showExchange) => super.noSuchMethod( + Invocation.setter( + #enableExchange, + showExchange, + ), + returnValueForMissingStub: null, + ); + @override +>>>>>>> bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 63c954ce12..91b5f9fd30 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DesktopDropPlugin")); FlutterLibepiccashPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterLibepiccashPluginCApi")); + FlutterLibmwcPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterLibmwcPluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); IsarFlutterLibsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ac0ec291de..3a7e8ad5ca 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST cs_salvium_flutter_libs_windows desktop_drop flutter_libepiccash + flutter_libmwc flutter_secure_storage_windows isar_flutter_libs local_auth_windows