diff --git a/lib/models/isar/models/blockchain_data/utxo.dart b/lib/models/isar/models/blockchain_data/utxo.dart index 77de5ae3a..57f9ede64 100644 --- a/lib/models/isar/models/blockchain_data/utxo.dart +++ b/lib/models/isar/models/blockchain_data/utxo.dart @@ -85,9 +85,14 @@ class UTXO { bool isConfirmed( int currentChainHeight, int minimumConfirms, - int minimumCoinbaseConfirms, - ) { + int minimumCoinbaseConfirms, { + int? overrideMinConfirms, // added to handle namecoin name op outputs + }) { final confirmations = getConfirmations(currentChainHeight); + + if (overrideMinConfirms != null) { + return confirmations >= overrideMinConfirms; + } return confirmations >= (isCoinbase ? minimumCoinbaseConfirms : minimumConfirms); } @@ -180,4 +185,5 @@ class UTXO { abstract final class UTXOOtherDataKeys { static const keyImage = "keyImage"; static const spent = "spent"; + static const nameOpData = "nameOpData"; } diff --git a/lib/models/namecoin_dns/dns_a_record_address_type.dart b/lib/models/namecoin_dns/dns_a_record_address_type.dart new file mode 100644 index 000000000..8709baa49 --- /dev/null +++ b/lib/models/namecoin_dns/dns_a_record_address_type.dart @@ -0,0 +1,25 @@ +enum DNSAddressType { + IPv4, + IPv6, + Tor, + Freenet, + I2P, + ZeroNet; + + String get key { + switch (this) { + case DNSAddressType.IPv4: + return "ip"; + case DNSAddressType.IPv6: + return "ip6"; + case DNSAddressType.Tor: + return "_tor"; + case DNSAddressType.Freenet: + return "freenet"; + case DNSAddressType.I2P: + return "i2p"; + case DNSAddressType.ZeroNet: + return "zeronet"; + } + } +} diff --git a/lib/models/namecoin_dns/dns_record.dart b/lib/models/namecoin_dns/dns_record.dart new file mode 100644 index 000000000..9c54bf98b --- /dev/null +++ b/lib/models/namecoin_dns/dns_record.dart @@ -0,0 +1,129 @@ +import 'dart:convert'; + +import 'package:meta/meta.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../utilities/extensions/extensions.dart'; +import 'dns_record_type.dart'; + +@Immutable() +abstract class DNSRecordBase { + final String name; + + DNSRecordBase({required this.name}); + + String getValueString(); +} + +@Immutable() +final class RawDNSRecord extends DNSRecordBase { + final String value; + + RawDNSRecord({required super.name, required this.value}); + + @override + String getValueString() => value; + + @override + String toString() { + return "RawDNSRecord(name: $name, value: $value)"; + } +} + +@Immutable() +final class DNSRecord extends DNSRecordBase { + final DNSRecordType type; + final Map data; + + DNSRecord({ + required super.name, + required this.type, + required this.data, + }); + + @override + String getValueString() { + // TODO error handling + dynamic value = data; + while (value is Map) { + value = value[value.keys.first]; + } + + return value.toString(); + } + + DNSRecord copyWith({ + DNSRecordType? type, + Map? data, + }) { + return DNSRecord( + type: type ?? this.type, + data: data ?? this.data, + name: name, + ); + } + + @override + String toString() { + return "DNSRecord(name: $name, type: $type, data: $data)"; + } + + static String merge(List records) { + final Map result = {}; + + for (final record in records) { + switch (record.type) { + case DNSRecordType.CNAME: + if (result[record.data.keys.first] != null) { + throw Exception("CNAME record already exists"); + } + _deepMerge(result, record.data); + break; + + case DNSRecordType.TLS: + case DNSRecordType.NS: + case DNSRecordType.DS: + case DNSRecordType.SRV: + case DNSRecordType.SSH: + case DNSRecordType.TXT: + case DNSRecordType.IMPORT: + case DNSRecordType.A: + _deepMerge(result, record.data); + break; + } + } + + final string = jsonEncode(result); + if (string.toUint8ListFromUtf8.length > valueMaxLength) { + throw Exception( + "Value length (${string.toUint8ListFromUtf8.length}) exceeds maximum" + " allowed ($valueMaxLength)", + ); + } + + return string; + } +} + +void _deepMerge(Map base, Map updates) { + updates.forEach((key, value) { + if (value is Map && base[key] is Map) { + _deepMerge(base[key] as Map, value); + } else if (value is List && base[key] is List) { + (base[key] as List).addAll(value); + } else { + if (base[key] != null) { + throw Exception( + "Attempted to overwrite value: ${base[key]} where key=$key", + ); + } + if (value is Map) { + base[key] = Map.from(value); + } else if (value is List) { + base[key] = List.from(value); + } else { + base[key] = value; + } + } + }); +} diff --git a/lib/models/namecoin_dns/dns_record_type.dart b/lib/models/namecoin_dns/dns_record_type.dart new file mode 100644 index 000000000..6e25cc038 --- /dev/null +++ b/lib/models/namecoin_dns/dns_record_type.dart @@ -0,0 +1,68 @@ +enum DNSRecordType { + A, + CNAME, + NS, + DS, + TLS, + SRV, + TXT, + IMPORT, + SSH; + + String get info { + switch (this) { + case DNSRecordType.A: + return "An A record maps your domain to an address (IPv4, IPv6, Tor," + " Freenet, I2P, or ZeroNet)."; + case DNSRecordType.CNAME: + return "A CNAME record redirects your domain to another domain," + " essentially acting as an alias."; + case DNSRecordType.NS: + return "An NS record specifies the nameservers that are authoritative" + " for your domain."; + case DNSRecordType.DS: + return "A DS record holds information about DNSSEC (DNS Security " + "Extensions) for your domain, helping with verification and " + "integrity."; + case DNSRecordType.TLS: + return "A TLS record is used for specifying details about how to " + "establish secure connections (like TLS certificates) for your" + " domain."; + case DNSRecordType.SRV: + return "An SRV record specifies the location of servers for specific" + " services, such as SIP, XMPP, or Minecraft servers."; + case DNSRecordType.TXT: + return "A TXT record allows you to add arbitrary text to your domain's" + " DNS record, often used for verification (e.g., SPF, DKIM)."; + case DNSRecordType.IMPORT: + return "An IMPORT record is used to bring in DNS records from an" + " external source into your domain's configuration."; + case DNSRecordType.SSH: + return "An SSH record provides information related to SSH public keys" + " for securely connecting to your domain's services."; + } + } + + String? get key { + switch (this) { + case DNSRecordType.A: + return null; + case DNSRecordType.CNAME: + return "alias"; + case DNSRecordType.NS: + return "ns"; + case DNSRecordType.DS: + return "ds"; + case DNSRecordType.TLS: + return "tls"; + case DNSRecordType.SRV: + return "srv"; + case DNSRecordType.TXT: + return "txt"; + case DNSRecordType.IMPORT: + return "import"; + case DNSRecordType.SSH: + return "sshfp"; + } + } +} diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 03d289237..748f5074e 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -153,7 +153,12 @@ class _AddWalletViewState extends ConsumerState { } WidgetsBinding.instance.addPostFrameCallback((_) { - ref.refresh(addWalletSelectedEntityStateProvider); + if (mounted) { + ref.refresh(addWalletSelectedEntityStateProvider); + if (isDesktop) { + _searchFocusNode.requestFocus(); + } + } }); super.initState(); diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index 07703a149..cb288f845 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -26,6 +26,8 @@ import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../wallets/wallet/wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; import '../../widgets/animated_widgets/rotate_icon.dart'; import '../../widgets/app_bar_field.dart'; @@ -88,6 +90,18 @@ class _CoinControlViewState extends ConsumerState { await coinControlInterface.updateBalance(); } + bool _isConfirmed(UTXO utxo, int currentChainHeight, Wallet wallet) { + if (wallet is NamecoinWallet) { + return wallet.checkUtxoConfirmed(utxo, currentChainHeight); + } else { + return utxo.isConfirmed( + currentChainHeight, + wallet.cryptoCurrency.minConfirms, + wallet.cryptoCurrency.minCoinbaseConfirms, + ); + } + } + @override void initState() { if (widget.selectedUTXOs != null) { @@ -347,10 +361,15 @@ class _CoinControlViewState extends ConsumerState { CoinControlViewType.manage || (widget.type == CoinControlViewType.use && !utxo.isBlocked && - utxo.isConfirmed( + _isConfirmed( + utxo, currentHeight, - minConfirms, - coin.minCoinbaseConfirms, + ref.watch( + pWallets.select( + (s) => s + .getWallet(widget.walletId), + ), + ), )), initialSelectedState: isSelected, onSelectedChanged: (value) { @@ -412,10 +431,16 @@ class _CoinControlViewState extends ConsumerState { (widget.type == CoinControlViewType.use && !_showBlocked && - utxo.isConfirmed( + _isConfirmed( + utxo, currentHeight, - minConfirms, - coin.minCoinbaseConfirms, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, + ), + ), + ), )), initialSelectedState: isSelected, onSelectedChanged: (value) { @@ -557,10 +582,16 @@ class _CoinControlViewState extends ConsumerState { CoinControlViewType .use && !utxo.isBlocked && - utxo.isConfirmed( + _isConfirmed( + utxo, currentHeight, - minConfirms, - coin.minCoinbaseConfirms, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, + ), + ), + ), )), initialSelectedState: isSelected, onSelectedChanged: (value) { diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart index 0881c7eb6..624b41eee 100644 --- a/lib/pages/coin_control/utxo_card.dart +++ b/lib/pages/coin_control/utxo_card.dart @@ -20,6 +20,8 @@ import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/constants.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../wallets/wallet/wallet.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/icon_widgets/utxo_status_icon.dart'; import '../../widgets/rounded_container.dart'; @@ -52,6 +54,18 @@ class _UtxoCardState extends ConsumerState { late bool _selected; + bool _isConfirmed(UTXO utxo, int currentChainHeight, Wallet wallet) { + if (wallet is NamecoinWallet) { + return wallet.checkUtxoConfirmed(utxo, currentChainHeight); + } else { + return utxo.isConfirmed( + currentChainHeight, + wallet.cryptoCurrency.minConfirms, + wallet.cryptoCurrency.minCoinbaseConfirms, + ); + } + } + @override void initState() { _selected = widget.initialSelectedState; @@ -110,18 +124,16 @@ class _UtxoCardState extends ConsumerState { ), child: UTXOStatusIcon( blocked: utxo.isBlocked, - status: utxo.isConfirmed( + status: _isConfirmed( + utxo, currentHeight, - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms, - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minCoinbaseConfirms, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, + ), + ), + ), ) ? UTXOStatusIconStatus.confirmed : UTXOStatusIconStatus.unconfirmed, diff --git a/lib/pages/coin_control/utxo_details_view.dart b/lib/pages/coin_control/utxo_details_view.dart index ba71f7a01..9959abdda 100644 --- a/lib/pages/coin_control/utxo_details_view.dart +++ b/lib/pages/coin_control/utxo_details_view.dart @@ -23,6 +23,8 @@ import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../wallets/wallet/wallet.dart'; import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -67,6 +69,18 @@ class _UtxoDetailsViewState extends ConsumerState { await MainDB.instance.putUTXO(utxo!.copyWith(isBlocked: !utxo!.isBlocked)); } + bool _isConfirmed(UTXO utxo, int currentChainHeight, Wallet wallet) { + if (wallet is NamecoinWallet) { + return wallet.checkUtxoConfirmed(utxo, currentChainHeight); + } else { + return utxo.isConfirmed( + currentChainHeight, + wallet.cryptoCurrency.minConfirms, + wallet.cryptoCurrency.minCoinbaseConfirms, + ); + } + } + @override void initState() { utxo = MainDB.instance.isar.utxos @@ -95,14 +109,14 @@ class _UtxoDetailsViewState extends ConsumerState { final coin = ref.watch(pWalletCoin(widget.walletId)); final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); - final confirmed = utxo!.isConfirmed( + final confirmed = _isConfirmed( + utxo!, currentHeight, - ref.watch(pWallets).getWallet(widget.walletId).cryptoCurrency.minConfirms, - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minCoinbaseConfirms, + ref.watch( + pWallets.select( + (s) => s.getWallet(widget.walletId), + ), + ), ); return ConditionalParent( diff --git a/lib/pages/namecoin_names/add_dns_record/add_dns_step_1.dart b/lib/pages/namecoin_names/add_dns_record/add_dns_step_1.dart new file mode 100644 index 000000000..dd9119cac --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/add_dns_step_1.dart @@ -0,0 +1,257 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../route_generator.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/assets.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../widgets/desktop/desktop_dialog.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/stack_dialog.dart'; +import 'add_dns_step_2.dart'; + +class AddDnsStep1 extends StatefulWidget { + const AddDnsStep1({super.key, required this.name}); + + final String name; + + @override + State createState() => _AddDnsStep1State(); +} + +class _AddDnsStep1State extends State { + DNSRecordType? _recordType; + + bool _nextLock = false; + void _next() { + if (_nextLock) return; + _nextLock = true; + try { + if (mounted) { + Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (context) { + return Util.isDesktop + ? DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Add DNS record", + style: STextStyles.desktopH3( + context, + ), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: AddDnsStep2( + recordType: _recordType!, + name: widget.name, + ), + ), + ], + ), + ) + : StackDialogBase( + keyboardPaddingAmount: + MediaQuery.of(context).viewInsets.bottom, + child: AddDnsStep2( + recordType: _recordType!, + name: widget.name, + ), + ); + }, + ), + ); + } + } finally { + _nextLock = false; + } + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!Util.isDesktop) + Text( + "Add DNS record", + style: STextStyles.pageTitleH2(context), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + Text( + "Choose a record type", + style: Util.isDesktop + ? STextStyles.w500_12(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ) + : STextStyles.w500_14(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + SizedBox( + height: Util.isDesktop ? 12 : 8, + ), + DropdownButtonHideUnderline( + child: DropdownButton2( + hint: Text( + "Choose a record type", + style: STextStyles.fieldLabel(context), + ), + 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, + maxHeight: Util.isDesktop ? null : 200, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + ), + isExpanded: true, + value: _recordType, + onChanged: (value) { + if (value is DNSRecordType && _recordType != value) { + setState(() { + _recordType = value; + }); + } + }, + iconStyleData: IconStyleData( + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension()!.textDark3, + ), + ), + ), + items: [ + ...DNSRecordType.values.map( + (e) => DropdownMenuItem( + value: e, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + e.name, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ), + ), + ], + ), + ), + if (_recordType != null) + SizedBox( + height: Util.isDesktop ? 10 : 6, + ), + if (_recordType != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Expanded( + child: Text( + _recordType!.info, + style: Util.isDesktop + ? STextStyles.w500_10(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ) + : STextStyles.w500_8(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ), + ), + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Next", + enabled: _recordType != null, + onPressed: _next, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + ), + ), + ], + ), + if (Util.isDesktop) + const SizedBox( + height: 32, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/add_dns_step_2.dart b/lib/pages/namecoin_names/add_dns_record/add_dns_step_2.dart new file mode 100644 index 000000000..f6bb22f67 --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/add_dns_step_2.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; + +import '../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/stack_dialog.dart'; +import 'name_form_interface.dart'; +import 'sub_widgets/a_form.dart'; +import 'sub_widgets/cname_form.dart'; +import 'sub_widgets/ds_form.dart'; +import 'sub_widgets/import_form.dart'; +import 'sub_widgets/ns_form.dart'; +import 'sub_widgets/srv_form.dart'; +import 'sub_widgets/ssh_form.dart'; +import 'sub_widgets/tls_form.dart'; +import 'sub_widgets/txt_form.dart'; + +class AddDnsStep2 extends StatefulWidget { + const AddDnsStep2({ + super.key, + required this.recordType, + required this.name, + }); + + final String name; + final DNSRecordType recordType; + + @override + State createState() => _AddDnsStep2State(); +} + +class _AddDnsStep2State extends State { + final GlobalKey _formStateKey = GlobalKey(); + + bool _nextLock = false; + void _nextPressed() { + if (_nextLock) return; + _nextLock = true; + try { + final record = _formStateKey.currentState!.buildRecord(); + Navigator.of(context, rootNavigator: true).pop( + record, + ); + } catch (e, s) { + Logging.instance.e( + runtimeType, + error: e, + stackTrace: s, + ); + + final String err; + switch (e.runtimeType) { + case const (ArgumentError): + err = e.toString().replaceFirst( + "Invalid Arguments(s): ", + "", + ); + + case const (Exception): + err = e.toString().replaceFirst( + "Exception: ", + "", + ); + + default: + err = e.toString(); + } + + showDialog( + context: context, + useRootNavigator: true, + builder: (context) { + return StackOkDialog( + desktopPopRootNavigator: true, // mobile as well due to sub nav flow + title: "Error", + maxWidth: 500, + message: err, + ); + }, + ); + } finally { + _nextLock = false; + } + } + + NameFormStatefulWidget? _form; + NameFormStatefulWidget get form => _form ??= _buildForm(); + + NameFormStatefulWidget _buildForm() { + switch (widget.recordType) { + case DNSRecordType.A: + return AForm(key: _formStateKey, name: widget.name); + case DNSRecordType.CNAME: + return CNAMEForm(key: _formStateKey, name: widget.name); + case DNSRecordType.NS: + return NSForm(key: _formStateKey, name: widget.name); + case DNSRecordType.DS: + return DSForm(key: _formStateKey, name: widget.name); + case DNSRecordType.TLS: + return TLSForm(key: _formStateKey, name: widget.name); + case DNSRecordType.SRV: + return SRVForm(key: _formStateKey, name: widget.name); + case DNSRecordType.TXT: + return TXTForm(key: _formStateKey, name: widget.name); + case DNSRecordType.IMPORT: + return IMPORTForm(key: _formStateKey, name: widget.name); + case DNSRecordType.SSH: + return SSHForm(key: _formStateKey, name: widget.name); + } + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!Util.isDesktop) + Text( + "Add DNS record", + style: STextStyles.pageTitleH2(context), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + form, + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Next", + onPressed: _nextPressed, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + ), + ), + ], + ), + if (Util.isDesktop) + const SizedBox( + height: 32, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/name_form_interface.dart b/lib/pages/namecoin_names/add_dns_record/name_form_interface.dart new file mode 100644 index 000000000..e5fc996bd --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/name_form_interface.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +import '../../../models/namecoin_dns/dns_record.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; + +abstract class NameFormStatefulWidget extends StatefulWidget { + const NameFormStatefulWidget({super.key, required this.name}); + + final String name; +} + +abstract class NameFormState + extends State { + DNSRecord buildRecord(); +} + +class DNSFieldText extends StatelessWidget { + const DNSFieldText(this.text, {super.key}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + text, + style: Util.isDesktop + ? STextStyles.w500_12(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ) + : STextStyles.w500_14(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ); + } +} + +class DNSFormField extends StatelessWidget { + const DNSFormField({super.key, required this.controller, this.keyboardType}); + + final TextEditingController controller; + final TextInputType? keyboardType; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + controller: controller, + textAlignVertical: TextAlignVertical.center, + keyboardType: keyboardType, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.all(16), + hintStyle: STextStyles.fieldLabel(context), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + ), + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/a_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/a_form.dart new file mode 100644 index 000000000..ec6728f9a --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/a_form.dart @@ -0,0 +1,239 @@ +import 'dart:io'; + +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../../models/namecoin_dns/dns_a_record_address_type.dart'; +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/assets.dart'; +import '../../../../utilities/constants.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class AForm extends NameFormStatefulWidget { + const AForm({super.key, required super.name}); + + @override + NameFormState createState() => _AFormState(); +} + +class _AFormState extends NameFormState { + final _addressDataController = TextEditingController(); + final _addressDataFieldFocus = FocusNode(); + + DNSAddressType _addressType = DNSAddressType.IPv4; + + @override + DNSRecord buildRecord() { + final parts = _addressDataController.text.split(",").map((e) => e.trim()); + + final List addresses = []; + + for (final part in parts) { + switch (_addressType) { + case DNSAddressType.IPv4: + final address = + InternetAddress(part.trim(), type: InternetAddressType.IPv4); + addresses.add(address.address); + break; + + case DNSAddressType.IPv6: + final address = InternetAddress(part, type: InternetAddressType.IPv6); + addresses.add(address.address); + break; + + case DNSAddressType.Tor: + final regex = RegExp(r'^[a-z2-7]{56}\.onion$'); + if (regex.hasMatch(part)) { + addresses.add(part); + } else { + throw Exception("Invalid tor address: $part"); + } + + case DNSAddressType.Freenet: + // TODO: verify + final regex = RegExp(r'(CHK|SSK|USK)@[a-zA-Z0-9~-]{43,}/?'); + final kskRegex = RegExp(r'KSK@[\w\-.~]+'); + if (regex.hasMatch(part) || kskRegex.hasMatch(part)) { + addresses.add(part); + } else { + throw Exception("Invalid freenet address: $part"); + } + + case DNSAddressType.I2P: + // TODO: verify + final b32Regex = RegExp(r'^[a-z2-7]{52}\.b32\.i2p$'); + final b64Regex = RegExp(r'^[A-Za-z0-9+/=]{516,}$'); + if (b32Regex.hasMatch(part) || b64Regex.hasMatch(part)) { + addresses.add(part); + } else { + throw Exception("Invalid i2p address: $part"); + } + + case DNSAddressType.ZeroNet: + // TODO: verify + final regex = RegExp(r'^[13][a-km-zA-HJ-NP-Z1-9]{32,33}$'); + if (regex.hasMatch(part)) { + addresses.add(part); + } else { + throw Exception("Invalid zeronet address: $part"); + } + } + } + + final Map map; + + if (_addressType == DNSAddressType.Tor) { + map = { + "map": { + "_tor": { + "txt": addresses, + }, + }, + }; + } else { + map = { + _addressType!.key: addresses, + }; + } + + return DNSRecord( + name: widget.name, + type: DNSRecordType.A, + data: map, + ); + } + + @override + void dispose() { + _addressDataController.dispose(); + _addressDataFieldFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DNSFieldText( + "Address type", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DropdownButtonHideUnderline( + child: DropdownButton2( + hint: Text( + "Choose address type", + style: STextStyles.fieldLabel(context), + ), + dropdownStyleData: DropdownStyleData( + offset: const Offset(0, -10), + elevation: 0, + maxHeight: Util.isDesktop ? null : 200, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + ), + isExpanded: true, + value: _addressType, + onChanged: (value) { + if (value is DNSAddressType && _addressType != value) { + setState(() { + _addressType = value; + }); + } + }, + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + iconStyleData: IconStyleData( + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension()!.textDark3, + ), + ), + ), + items: [ + ...DNSAddressType.values.map( + (e) => DropdownMenuItem( + value: e, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + e.name, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ), + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Value", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + focusNode: _addressDataFieldFocus, + controller: _addressDataController, + textAlignVertical: TextAlignVertical.center, + maxLines: 3, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.all(16), + hintText: "e.g. 255.255.255.255, " + "76f4a520a262c269dcba66bc1f560452e30a44e14ce6b37ce20b8.onion", + hintStyle: STextStyles.fieldLabel(context), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/cname_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/cname_form.dart new file mode 100644 index 000000000..34cc7a600 --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/cname_form.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class CNAMEForm extends NameFormStatefulWidget { + const CNAMEForm({super.key, required super.name}); + + @override + NameFormState createState() => _CNAMEFormState(); +} + +class _CNAMEFormState extends NameFormState { + final _aliasController = TextEditingController(); + + @override + DNSRecord buildRecord() { + final address = _aliasController.text.trim(); + + return DNSRecord( + name: widget.name, + type: DNSRecordType.CNAME, + data: {"alias": address}, + ); + } + + @override + void dispose() { + _aliasController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Alias of", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _aliasController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ds_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ds_form.dart new file mode 100644 index 000000000..29cdbaf6f --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ds_form.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class DSForm extends NameFormStatefulWidget { + const DSForm({super.key, required super.name}); + + @override + NameFormState createState() => _DSFormState(); +} + +class _DSFormState extends NameFormState { + final _keytagController = TextEditingController(); + final _algoController = TextEditingController(); + final _typeController = TextEditingController(); + final _hashController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + name: widget.name, + type: DNSRecordType.DS, + data: { + "ds": [ + [ + int.parse(_keytagController.text.trim()), + int.parse(_algoController.text.trim()), + int.parse(_typeController.text.trim()), + _hashController.text.trim(), + ], + ], + }, + ); + } + + @override + void dispose() { + _keytagController.dispose(); + _algoController.dispose(); + _typeController.dispose(); + _hashController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Keytag", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _keytagController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Algorithm", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _algoController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Hash type", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _typeController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Hash (base64)", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _hashController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/import_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/import_form.dart new file mode 100644 index 000000000..cfcaa0d23 --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/import_form.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class IMPORTForm extends NameFormStatefulWidget { + const IMPORTForm({super.key, required super.name}); + + @override + NameFormState createState() => _IMPORTFormState(); +} + +class _IMPORTFormState extends NameFormState { + final _nameController = TextEditingController(); + final _subdomainController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + name: widget.name, + type: DNSRecordType.IMPORT, + data: { + "import": [ + [ + _nameController.text.trim(), + if (_subdomainController.text.trim().isNotEmpty) + _subdomainController.text.trim(), + ], + ], + }, + ); + } + + @override + void dispose() { + _nameController.dispose(); + _subdomainController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Namecoin name", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _nameController, + ), + const DNSFieldText( + "Subdomain (optional)", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _subdomainController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ns_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ns_form.dart new file mode 100644 index 000000000..6d6eb914c --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ns_form.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class NSForm extends NameFormStatefulWidget { + const NSForm({super.key, required super.name}); + + @override + NameFormState createState() => _NSFormState(); +} + +class _NSFormState extends NameFormState { + final _serverController = TextEditingController(); + + @override + DNSRecord buildRecord() { + final address = _serverController.text.trim(); + + return DNSRecord( + name: widget.name, + type: DNSRecordType.NS, + data: { + "ns": [address], + }, + ); + } + + @override + void dispose() { + _serverController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Nameserver", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _serverController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/srv_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/srv_form.dart new file mode 100644 index 000000000..db3c01a0c --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/srv_form.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class SRVForm extends NameFormStatefulWidget { + const SRVForm({super.key, required super.name}); + + @override + NameFormState createState() => _SRVFormState(); +} + +class _SRVFormState extends NameFormState { + final _priorityController = TextEditingController(); + final _weightController = TextEditingController(); + final _portController = TextEditingController(); + final _hostController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + name: widget.name, + type: DNSRecordType.SRV, + data: { + "srv": [ + [ + int.parse(_priorityController.text.trim()), + int.parse(_weightController.text.trim()), + int.parse(_portController.text.trim()), + _hostController.text.trim(), + ], + ], + }, + ); + } + + @override + void dispose() { + _priorityController.dispose(); + _weightController.dispose(); + _portController.dispose(); + _hostController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Priority", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _priorityController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Weight", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _weightController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Port", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _portController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Host", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _hostController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ssh_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ssh_form.dart new file mode 100644 index 000000000..9503544be --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ssh_form.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class SSHForm extends NameFormStatefulWidget { + const SSHForm({super.key, required super.name}); + + @override + NameFormState createState() => _SSHFormState(); +} + +class _SSHFormState extends NameFormState { + final _algoController = TextEditingController(); + final _fingerprintTypeController = TextEditingController(); + final _fingerprintController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + name: widget.name, + type: DNSRecordType.SSH, + data: { + "sshfp": [ + [ + int.parse(_algoController.text.trim()), + int.parse(_fingerprintTypeController.text.trim()), + _fingerprintController.text.trim(), + ], + ], + }, + ); + } + + @override + void dispose() { + _algoController.dispose(); + _fingerprintTypeController.dispose(); + _fingerprintController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Algorithm", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _algoController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Fingerprint type", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _fingerprintTypeController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Fingerprint (base64)", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _fingerprintController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/tls_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/tls_form.dart new file mode 100644 index 000000000..509e0b201 --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/tls_form.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class TLSForm extends NameFormStatefulWidget { + const TLSForm({super.key, required super.name}); + + @override + NameFormState createState() => _TLSFormState(); +} + +class _TLSFormState extends NameFormState { + final _pubkeyController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + name: widget.name, + type: DNSRecordType.TLS, + data: { + "map": { + "*": { + "tls": [ + [ + 2, + 1, + 0, + _pubkeyController.text.trim(), + ], + ], + }, + }, + }, + ); + } + + @override + void dispose() { + _pubkeyController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "DANE-TA public key (base64)", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _pubkeyController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/txt_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/txt_form.dart new file mode 100644 index 000000000..bd331cd21 --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/txt_form.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class TXTForm extends NameFormStatefulWidget { + const TXTForm({super.key, required super.name}); + + @override + NameFormState createState() => _TXTFormState(); +} + +class _TXTFormState extends NameFormState { + final _valueController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + name: widget.name, + type: DNSRecordType.TXT, + data: { + "txt": [_valueController.text.trim()], + }, + ); + } + + @override + void dispose() { + _valueController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Value", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _valueController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart new file mode 100644 index 000000000..d0dddda3e --- /dev/null +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -0,0 +1,541 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../providers/providers.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/models/name_op_state.dart'; +import '../../../wallets/models/tx_data.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../../models/namecoin_dns/dns_a_record_address_type.dart'; +import '../../models/namecoin_dns/dns_record.dart'; +import '../../models/namecoin_dns/dns_record_type.dart'; +import '../../route_generator.dart'; +import '../../themes/stack_colors.dart'; +import '../../utilities/amount/amount_formatter.dart'; +import '../../utilities/show_loading.dart'; +import '../../utilities/text_styles.dart'; +import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/custom_buttons/blue_text_button.dart'; +import '../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../widgets/dialogs/s_dialog.dart'; +import '../../widgets/rounded_white_container.dart'; +import 'add_dns_record/add_dns_step_1.dart'; +import 'confirm_name_transaction_view.dart'; + +class BuyDomainView extends ConsumerStatefulWidget { + const BuyDomainView({ + super.key, + required this.walletId, + required this.domainName, + }); + + final String walletId; + final String domainName; + + static const routeName = "/buyDomainView"; + + @override + ConsumerState createState() => _BuyDomainWidgetState(); +} + +class _BuyDomainWidgetState extends ConsumerState { + bool _settingsHidden = true; + final List _dnsRecords = []; + + String _getFormattedDNSRecords() { + if (_dnsRecords.isEmpty) return ""; + + return DNSRecord.merge(_dnsRecords); + } + + String _getNameFormattedForInternal() { + String formattedName = widget.domainName; + if (!formattedName.startsWith("d/")) { + formattedName = "d/$formattedName"; + } + if (formattedName.endsWith(".bit")) { + formattedName.split(".bit").first; + } + return formattedName; + } + + Future _preRegFuture() async { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; + final myAddress = await wallet.getCurrentReceivingAddress(); + if (myAddress == null) { + throw Exception("No receiving address found"); + } + + final value = _getFormattedDNSRecords(); + + Logging.instance.t("Formatted namecoin name value: $value"); + + // get address private key for deterministic salt + final pk = await wallet.getPrivateKey(myAddress); + + final formattedName = _getNameFormattedForInternal(); + + final data = await compute(_computeScriptNameNew, (formattedName, pk.data)); + + TxData txData = TxData( + opNameState: NameOpState( + name: formattedName, + saltHex: data.$2, + commitment: data.$3, + value: value, + nameScriptHex: data.$1, + type: OpName.nameNew, + outputPosition: -1, //currently unknown, updated later + ), + note: "Reserve ${widget.domainName.substring(2)}.bit", + feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? + recipients: [ + ( + address: myAddress.value, + isChange: false, + amount: Amount( + rawValue: BigInt.from(kNameNewAmountSats), + fractionDigits: wallet.cryptoCurrency.fractionDigits, + ), + ), + ], + ); + + txData = await wallet.prepareNameSend(txData: txData); + return txData; + } + + bool _preRegLock = false; + Future _preRegister() async { + if (_preRegLock) return; + _preRegLock = true; + try { + final txData = (await showLoading( + whileFuture: _preRegFuture(), + context: context, + message: "Preparing transaction...", + onException: (e) { + throw e; + }, + ))!; + + if (mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + ConfirmNameTransactionView.routeName, + arguments: (txData, widget.walletId), + ); + } + } + } catch (e, s) { + Logging.instance.e("_preRegister failed", error: e, stackTrace: s); + + if (mounted) { + String err = e.toString(); + if (err.startsWith("Exception: ")) { + err = err.replaceFirst("Exception: ", ""); + } + + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _preRegLock = false; + } + } + + bool _addLock = false; + Future _addRecord() async { + if (_addLock) return; + _addLock = true; + try { + final value = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return Navigator( + onGenerateRoute: (settings) { + return RouteGenerator.getRoute( + builder: (context) { + return Util.isDesktop + ? SDialog( + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Add DNS record", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: AddDnsStep1( + name: _getNameFormattedForInternal(), + ), + ), + ], + ), + ), + ) + : StackDialogBase( + child: AddDnsStep1( + name: _getNameFormattedForInternal(), + ), + ); + }, + ); + }, + ); + }, + ); + + if (mounted && value != null) { + setState(() { + _dnsRecords.add(value); + }); + } + } catch (e, s) { + Logging.instance.e("Add DNS record failed", error: e, stackTrace: s); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Add DNS record failed", + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _addLock = false; + } + } + + @override + Widget build(BuildContext context) { + final coin = ref.watch(pWalletCoin(widget.walletId)); + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + leading: const AppBarBackButton(), + titleSpacing: 0, + title: Text( + "Buy domain", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (ctx, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ); + }, + child: Column( + crossAxisAlignment: Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, + children: [ + if (!Util.isDesktop) + Text( + "Buy domain", + style: Util.isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH2(context), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + Row( + mainAxisAlignment: Util.isDesktop + ? MainAxisAlignment.center + : MainAxisAlignment.start, + children: [ + Text( + "Name registration will take approximately 2 to 4 hours.", + style: Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ) + : STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), + ], + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Domain name", + style: Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ), + ), + Text( + "${widget.domainName.substring(2)}.bit", + style: Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 16 : 8, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ), + ), + Text( + ref.watch(pAmountFormatter(coin)).format( + Amount( + rawValue: BigInt.from(kNameNewAmountSats), + fractionDigits: coin.fractionDigits, + ), + ), + style: Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Row( + children: [child], + ), + child: CustomTextButton( + text: _settingsHidden ? "More settings" : "Hide settings", + onTap: () { + setState(() { + _settingsHidden = !_settingsHidden; + }); + }, + ), + ), + if (!_settingsHidden) + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + if (!_settingsHidden) + if (_dnsRecords.isEmpty) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Add DNS records to your domain name", + style: STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ], + ), + ), + if (!_settingsHidden) + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Expanded(child: child), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ..._dnsRecords.map( + (e) => DNSRecordCard( + key: ValueKey(e), + record: e, + onRemoveTapped: () => setState(() { + _dnsRecords.remove(e); + }), + ), + ), + SizedBox( + height: Util.isDesktop ? 16 : 8, + ), + SecondaryButton( + label: _dnsRecords.isEmpty + ? "Add DNS record" + : "Add another DNS record", + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _addRecord, + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + if (!Util.isDesktop && _settingsHidden) const Spacer(), + PrimaryButton( + label: "Buy", + // width: Util.isDesktop ? 160 : double.infinity, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _preRegister, + ), + SizedBox( + height: Util.isDesktop ? 32 : 16, + ), + ], + ), + ); + } +} + +(String, String, String) _computeScriptNameNew((String, Uint8List) args) { + return scriptNameNew(args.$1, args.$2); +} + +class DNSRecordCard extends StatelessWidget { + const DNSRecordCard({ + super.key, + required this.record, + required this.onRemoveTapped, + }); + + final DNSRecord record; + final VoidCallback onRemoveTapped; + + String get _extraInfo { + if (record.type == DNSRecordType.A) { + // TODO error handling + return " - ${DNSAddressType.values.firstWhere((e) => e.key == record.data.keys.first).name}"; + } + + return ""; + } + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${record.type.name}$_extraInfo", + ), + CustomTextButton( + text: "Remove", + onTap: onRemoveTapped, + ), + ], + ), + Text(record.getValueString()), + ], + ), + ); + } +} diff --git a/lib/pages/namecoin_names/confirm_name_transaction_view.dart b/lib/pages/namecoin_names/confirm_name_transaction_view.dart new file mode 100644 index 000000000..ec0fc926a --- /dev/null +++ b/lib/pages/namecoin_names/confirm_name_transaction_view.dart @@ -0,0 +1,1081 @@ +/* + * 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 'dart:io'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../models/isar/models/transaction_note.dart'; +import '../../notifications/show_flush_bar.dart'; +import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart'; +import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; +import '../../providers/db/main_db_provider.dart'; +import '../../providers/global/secure_store_provider.dart'; +import '../../providers/providers.dart'; +import '../../route_generator.dart'; +import '../../themes/stack_colors.dart'; +import '../../themes/theme_providers.dart'; +import '../../utilities/amount/amount.dart'; +import '../../utilities/amount/amount_formatter.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/logger.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/models/tx_data.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/desktop/desktop_dialog.dart'; +import '../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../widgets/desktop/primary_button.dart'; +import '../../widgets/icon_widgets/x_icon.dart'; +import '../../widgets/rounded_container.dart'; +import '../../widgets/rounded_white_container.dart'; +import '../../widgets/stack_dialog.dart'; +import '../../widgets/stack_text_field.dart'; +import '../../widgets/textfield_icon_button.dart'; +import '../pinpad_views/lock_screen_view.dart'; +import '../send_view/sub_widgets/sending_transaction_dialog.dart'; + +class ConfirmNameTransactionView extends ConsumerStatefulWidget { + const ConfirmNameTransactionView({ + super.key, + required this.txData, + required this.walletId, + }); + + static const String routeName = "/confirmNameTransactionView"; + + final TxData txData; + final String walletId; + + @override + ConsumerState createState() => + _ConfirmNameTransactionViewState(); +} + +class _ConfirmNameTransactionViewState + extends ConsumerState { + late final String walletId; + late final bool isDesktop; + + late final FocusNode _noteFocusNode; + late final TextEditingController noteController; + + Future _attemptSend() async { + final wallet = ref.read(pWallets).getWallet(walletId); + final coin = wallet.info.coin; + + final sendProgressController = ProgressAndSuccessController(); + + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return SendingTransactionDialog( + coin: coin, + controller: sendProgressController, + ); + }, + ), + ); + + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + final List txids = []; + Future txDataFuture; + + final note = noteController.text; + + try { + txDataFuture = wallet.confirmSend(txData: widget.txData); + + // await futures in parallel + final futureResults = await Future.wait([ + txDataFuture, + time, + ]); + + final txData = (futureResults.first as TxData); + + sendProgressController.triggerSuccess?.call(); + + // await futures in parallel + await Future.wait([ + // wait for animation + Future.delayed(const Duration(seconds: 5)), + + // associated name data for reg tx + ref.read(secureStoreProvider).write( + key: nameSaltKeyBuilder( + txData.txid!, + walletId, + txData.opNameState!.outputPosition, + ), + value: encodeNameSaltData( + txData.opNameState!.name, + txData.opNameState!.saltHex, + txData.opNameState!.value, + ), + ), + ]); + + txids.add(txData.txid!); + ref.refresh(desktopUseUTXOs); + + // save note + for (final txid in txids) { + await ref.read(mainDBProvider).putTransactionNote( + TransactionNote( + walletId: walletId, + txid: txid, + value: note, + ), + ); + } + + unawaited(wallet.refresh()); + + if (mounted) { + // pop sending dialog + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + // pop confirm send view + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + // pop buy popup + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + + // pop name details view + if (txData.opNameState!.type == OpName.nameUpdate) { + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + } + } + } catch (e, s) { + const niceError = "Broadcast name transaction failed"; + + Logging.instance.e(niceError, error: e, stackTrace: s); + + if (mounted) { + // pop sending dialog + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + if (isDesktop) { + return DesktopDialog( + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + niceError, + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 24, + ), + Flexible( + child: SingleChildScrollView( + child: SelectableText( + e.toString(), + style: STextStyles.smallMed14(context), + ), + ), + ), + const SizedBox( + height: 56, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ), + ); + } else { + return StackDialog( + title: niceError, + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + } + }, + ); + } + } + } + + @override + void initState() { + isDesktop = Util.isDesktop; + walletId = widget.walletId; + _noteFocusNode = FocusNode(); + noteController = TextEditingController(); + noteController.text = widget.txData.note ?? ""; + + super.initState(); + } + + @override + void dispose() { + noteController.dispose(); + + _noteFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final coin = ref.watch(pWalletCoin(walletId)); + + final unit = coin.ticker; + + final fee = widget.txData.fee; + final amountWithoutChange = widget.txData.amountWithoutChange!; + + 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: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, + ), + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + AppBarBackButton( + size: 40, + iconSize: 24, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(), + ), + Text( + "Confirm transaction", + style: STextStyles.desktopH3(context), + ), + ], + ), + Flexible( + child: SingleChildScrollView( + child: child, + ), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Confirm Name transaction", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Name", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + widget.txData.opNameState!.name, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Value", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + widget.txData.opNameState!.value, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Recipient", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + widget.txData.recipients!.first.address, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + ), + SelectableText( + ref.watch(pAmountFormatter(coin)).format( + amountWithoutChange, + ), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction fee", + style: STextStyles.smallMed12(context), + ), + SelectableText( + ref.watch(pAmountFormatter(coin)).format(fee!), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + if (widget.txData.fee != null && widget.txData.vSize != null) + const SizedBox( + height: 12, + ), + if (widget.txData.fee != null && widget.txData.vSize != null) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "sats/vByte", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + "~${fee.raw.toInt() ~/ widget.txData.vSize!}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + if (widget.txData.note != null && + widget.txData.note!.isNotEmpty) + const SizedBox( + height: 12, + ), + if (widget.txData.note != null && + widget.txData.note!.isNotEmpty) + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Note", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + widget.txData.note!, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + ], + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 50, + ), + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: + Theme.of(context).extension()!.background, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .background, + borderRadius: BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 22, + ), + child: Row( + children: [ + SvgPicture.file( + File( + ref.watch( + themeProvider.select( + (value) => value.assets.send, + ), + ), + ), + width: 32, + height: 32, + ), + const SizedBox( + width: 16, + ), + Text( + "Send $unit Name transaction", + style: STextStyles.desktopTextMedium(context), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Name", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox( + height: 2, + ), + SelectableText( + widget.txData.opNameState!.name, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ], + ), + ), + Container( + height: 1, + color: Theme.of(context) + .extension()! + .background, + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Value", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox( + height: 2, + ), + SelectableText( + widget.txData.opNameState!.value, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ], + ), + ), + ], + ), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + "Note (optional)", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 5, + autocorrect: isDesktop ? false : true, + enableSuggestions: isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + 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, + ), + ), + ), + const SizedBox( + height: 20, + ), + ], + ), + ), + + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + ), + child: Text( + "Amount", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 10, + left: 32, + right: 32, + ), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: Builder( + builder: (context) { + final externalCalls = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls, + ), + ); + String fiatAmount = "N/A"; + + if (externalCalls) { + final price = ref + .read( + priceAnd24hChangeNotifierProvider, + ) + .getPrice(coin) + .item1; + if (price > Decimal.zero) { + fiatAmount = (amountWithoutChange.decimal * price) + .toAmount(fractionDigits: 2) + .fiatString( + locale: ref + .read( + localeServiceChangeNotifierProvider, + ) + .locale, + ); + } + } + + return Row( + children: [ + SelectableText( + ref.watch(pAmountFormatter(coin)).format( + amountWithoutChange, + ), + style: STextStyles.itemSubtitle( + context, + ), + ), + if (externalCalls) + Text( + " | ", + style: STextStyles.itemSubtitle( + context, + ), + ), + if (externalCalls) + SelectableText( + "~$fiatAmount ${ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), + )}", + style: STextStyles.itemSubtitle( + context, + ), + ), + ], + ); + }, + ), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + ), + child: Text( + "Recipient", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 10, + left: 32, + right: 32, + ), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: SelectableText( + widget.txData.recipients!.first.address, + style: STextStyles.itemSubtitle(context), + ), + ), + ), + // todo amoutn here + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + ), + child: Text( + "Transaction fee", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 10, + left: 32, + right: 32, + ), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: SelectableText( + ref.watch(pAmountFormatter(coin)).format(fee!), + style: STextStyles.itemSubtitle(context), + ), + ), + ), + if (isDesktop && + widget.txData.fee != null && + widget.txData.vSize != null) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + ), + child: Text( + "sats/vByte", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop && + widget.txData.fee != null && + widget.txData.vSize != null) + Padding( + padding: const EdgeInsets.only( + top: 10, + left: 32, + right: 32, + ), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: SelectableText( + "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + if (!isDesktop) const Spacer(), + SizedBox( + height: isDesktop ? 23 : 12, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 32, + ) + : const EdgeInsets.all(0), + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ) + : const EdgeInsets.all(12), + color: Theme.of(context) + .extension()! + .snackBarBackSuccess, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isDesktop ? "Total amount to send" : "Total amount", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + ), + SelectableText( + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange + fee!), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ), + ], + ), + ), + ), + SizedBox( + height: isDesktop ? 28 : 16, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 32, + ) + : const EdgeInsets.all(0), + child: PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + final dynamic unlocked; + + if (isDesktop) { + unlocked = await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend( + coin: coin, + ), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: + const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + } + + if (mounted) { + if (unlocked == true) { + unawaited(_attemptSend()); + } else { + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", + context: context, + ), + ); + } + } + } + }, + ), + ), + if (isDesktop) + const SizedBox( + height: 32, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/namecoin_names/manage_domain_view.dart b/lib/pages/namecoin_names/manage_domain_view.dart new file mode 100644 index 000000000..6d2679d60 --- /dev/null +++ b/lib/pages/namecoin_names/manage_domain_view.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +import '../../models/isar/models/blockchain_data/utxo.dart'; +import '../../themes/stack_colors.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/text_styles.dart'; +import '../../widgets/background.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/toggle.dart'; +import 'sub_widgets/transfer_option_widget.dart'; +import 'sub_widgets/update_option_widget.dart'; + +class ManageDomainView extends StatefulWidget { + const ManageDomainView({ + super.key, + required this.walletId, + required this.utxo, + }); + + final String walletId; + final UTXO utxo; + + static const routeName = "/manageDomainView"; + + @override + State createState() => _ManageDomainViewState(); +} + +class _ManageDomainViewState extends State { + bool _onTransfer = true; + + @override + Widget build(BuildContext context) { + return Background( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + leading: const AppBarBackButton(), + titleSpacing: 0, + title: Text( + "Manage domain", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + SizedBox( + height: 48, + child: Toggle( + key: UniqueKey(), + onColor: + Theme.of(context).extension()!.popupBG, + offColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + onText: "Transfer", + offText: "Update", + isOn: !_onTransfer, + onValueChanged: (value) { + FocusManager.instance.primaryFocus?.unfocus(); + setState(() { + _onTransfer = !value; + }); + }, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: IndexedStack( + index: _onTransfer ? 0 : 1, + children: [ + TransferOptionWidget( + walletId: widget.walletId, + utxo: widget.utxo, + ), + UpdateOptionWidget( + walletId: widget.walletId, + utxo: widget.utxo, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/namecoin_names/namecoin_names_home_view.dart b/lib/pages/namecoin_names/namecoin_names_home_view.dart new file mode 100644 index 000000000..8970de7c6 --- /dev/null +++ b/lib/pages/namecoin_names/namecoin_names_home_view.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../themes/stack_colors.dart'; +import '../../utilities/assets.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/desktop/desktop_app_bar.dart'; +import '../../widgets/desktop/desktop_scaffold.dart'; +import '../../widgets/toggle.dart'; +import 'sub_widgets/buy_domain_option_widget.dart'; +import 'sub_widgets/manage_domains_option_widget.dart'; + +class NamecoinNamesHomeView extends ConsumerStatefulWidget { + const NamecoinNamesHomeView({ + super.key, + required this.walletId, + }); + + final String walletId; + + static const String routeName = "/namecoinNamesHomeView"; + + @override + ConsumerState createState() => + _NamecoinNamesHomeViewState(); +} + +class _NamecoinNamesHomeViewState extends ConsumerState { + bool _onManage = true; + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; + + return MasterScaffold( + isDesktop: isDesktop, + appBar: isDesktop + ? DesktopAppBar( + isCompactHeight: true, + background: Theme.of(context).extension()!.popupBG, + leading: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 24, + right: 20, + ), + child: AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + ), + SvgPicture.asset( + Assets.svg.robotHead, + width: 32, + height: 32, + color: Theme.of(context).extension()!.textDark, + ), + const SizedBox( + width: 10, + ), + Text( + "Domains", + style: STextStyles.desktopH3(context), + ), + ], + ), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Domains", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: ConditionalParent( + condition: !isDesktop, + builder: (child) => SafeArea( + child: Padding( + padding: const EdgeInsets.only( + top: 16, + left: 16, + right: 16, + ), + child: child, + ), + ), + child: Util.isDesktop + ? Padding( + padding: const EdgeInsets.only( + top: 24, + left: 24, + right: 24, + ), + child: Row( + children: [ + SizedBox( + width: 460, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + "Buy domain", + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ], + ), + const SizedBox( + height: 14, + ), + Flexible( + child: BuyDomainOptionWidget( + walletId: widget.walletId, + ), + ), + ], + ), + ), + const SizedBox( + width: 24, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + "Manage domains", + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ], + ), + const SizedBox( + height: 14, + ), + Flexible( + child: SingleChildScrollView( + child: ManageDomainsOptionWidget( + walletId: widget.walletId, + ), + ), + ), + ], + ), + ), + ], + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 48, + child: Toggle( + key: UniqueKey(), + onColor: + Theme.of(context).extension()!.popupBG, + offColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + onText: "Buy domain", + offText: "Manage domains", + isOn: !_onManage, + onValueChanged: (value) { + FocusManager.instance.primaryFocus?.unfocus(); + setState(() { + _onManage = !value; + }); + }, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: IndexedStack( + index: _onManage ? 0 : 1, + children: [ + BuyDomainOptionWidget( + walletId: widget.walletId, + ), + LayoutBuilder( + builder: (context, constraints) { + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: SingleChildScrollView( + child: IntrinsicHeight( + child: ManageDomainsOptionWidget( + walletId: widget.walletId, + ), + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart new file mode 100644 index 000000000..1040dda4f --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart @@ -0,0 +1,396 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../providers/providers.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/assets.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/extensions/impl/string.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/show_loading.dart'; +import '../../../utilities/text_formatters.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/rounded_white_container.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../buy_domain_view.dart'; + +class BuyDomainOptionWidget extends ConsumerStatefulWidget { + const BuyDomainOptionWidget({super.key, required this.walletId}); + + final String walletId; + + @override + ConsumerState createState() => _BuyDomainWidgetState(); +} + +class _BuyDomainWidgetState extends ConsumerState { + static const kMaxByteLength = nameMaxLength - 2; // subtract length of "d/" + + final _nameController = TextEditingController(); + final _nameFieldFocus = FocusNode(); + + String? get formattedNameInField { + if (_nameController.text.isNotEmpty) { + if (_nameController.text.startsWith("d/")) { + return _nameController.text; + } else { + return "d/${_nameController.text}"; + } + } + return null; + } + + bool _isAvailable = false; + String? _lastLookedUpName; + + bool _lookupLock = false; + Future _lookup() async { + if (_lookupLock) return; + _lookupLock = true; + try { + _isAvailable = false; + + _lastLookedUpName = formattedNameInField; + final result = await showLoading( + whileFuture: + (ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet) + .lookupName(_lastLookedUpName!), + context: context, + message: "Searching...", + onException: (e) => throw e, + rootNavigator: Util.isDesktop, + delay: const Duration(seconds: 2), + ); + + _isAvailable = result?.nameState == NameState.available; + + if (mounted) { + setState(() {}); + } + + Logging.instance.i("LOOKUP RESULT: $result"); + } catch (e, s) { + Logging.instance.e("_lookup failed", error: e, stackTrace: s); + + String? err; + if (e.toString().contains("Contains invalid characters")) { + err = "Contains invalid characters"; + } + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Name lookup failed", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _lookupLock = false; + } + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _nameFieldFocus.requestFocus(); + } + }); + } + + @override + void dispose() { + _nameController.dispose(); + _nameFieldFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final double dotBitBoxLength = Util.isDesktop ? 100 : 74; + return Column( + crossAxisAlignment: + Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, + children: [ + SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: Container( + height: 48, + width: 100, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), // Adjust radius as needed + bottomLeft: + Radius.circular(Constants.size.circularBorderRadius), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextField( + inputFormatters: [ + Utf8ByteLengthLimitingTextInputFormatter( + kMaxByteLength, + ), + ], + textInputAction: TextInputAction.search, + focusNode: _nameFieldFocus, + controller: _nameController, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + prefixIcon: Padding( + padding: const EdgeInsets.all(14), + child: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .textFieldDefaultSearchIconLeft, + ), + ), + fillColor: Colors.transparent, + hintText: "Find a domain name", + hintStyle: STextStyles.fieldLabel(context), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + onSubmitted: (_) { + if (_nameController.text.isNotEmpty) { + _lookup(); + } + }, + onChanged: (value) { + // trigger look up button enabled/disabled state change + setState(() {}); + }, + ), + ), + ], + ), + ), + ), + Container( + height: 48, + width: dotBitBoxLength, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .buttonBackPrimary, + borderRadius: BorderRadius.only( + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), // Adjust radius as needed + bottomRight: + Radius.circular(Constants.size.circularBorderRadius), + ), + ), + child: Center( + child: Text( + ".bit", + style: STextStyles.w600_14(context).copyWith( + color: Theme.of(context) + .extension()! + .buttonTextPrimary, + ), + ), + ), + ), + ], + ), + ), + const SizedBox( + height: 4, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: EdgeInsets.only(right: dotBitBoxLength), + child: Builder( + builder: (context) { + final length = + _nameController.text.toUint8ListFromUtf8.lengthInBytes; + return Text( + "$length/$kMaxByteLength", + style: STextStyles.w500_10(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle2, + ), + ); + }, + ), + ), + ], + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + SecondaryButton( + label: "Lookup", + enabled: _nameController.text.isNotEmpty, + // width: Util.isDesktop ? 160 : double.infinity, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _lookup, + ), + const SizedBox( + height: 32, + ), + if (_lastLookedUpName != null) + _NameCard( + walletId: widget.walletId, + isAvailable: _isAvailable, + formattedName: _lastLookedUpName!, + ), + ], + ); + } +} + +class _NameCard extends ConsumerWidget { + const _NameCard({ + super.key, + required this.walletId, + required this.isAvailable, + required this.formattedName, + }); + + final String walletId; + final bool isAvailable; + final String formattedName; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final availability = isAvailable ? "Available" : "Unavailable"; + final color = isAvailable + ? Theme.of(context).extension()!.accentColorGreen + : Theme.of(context).extension()!.accentColorRed; + + final style = (Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_12(context)); + + return RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 24 : 16), + child: IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${formattedName.substring(2)}.bit", + style: style, + ), + const SizedBox( + height: 4, + ), + Text( + availability, + style: style.copyWith( + color: color, + ), + ), + ], + ), + ), + Column( + children: [ + PrimaryButton( + label: "Buy domain", + enabled: isAvailable, + buttonHeight: + Util.isDesktop ? ButtonHeight.m : ButtonHeight.l, + width: Util.isDesktop ? 140 : 120, + onPressed: () async { + if (context.mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Buy domain", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: BuyDomainView( + walletId: walletId, + domainName: formattedName, + ), + ), + ], + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + BuyDomainView.routeName, + arguments: ( + walletId: walletId, + domainName: formattedName + ), + ); + } + } + }, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart new file mode 100644 index 000000000..f4a7df875 --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart @@ -0,0 +1,104 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../models/isar/models/blockchain_data/utxo.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import 'owned_name_card.dart'; + +class ManageDomainsOptionWidget extends ConsumerStatefulWidget { + const ManageDomainsOptionWidget({ + super.key, + required this.walletId, + }); + + final String walletId; + + @override + ConsumerState createState() => + _ManageDomainsWidgetState(); +} + +class _ManageDomainsWidgetState + extends ConsumerState { + double _tempWidth = 0; + double? _width; + int _count = 0; + + void _sillyHack(double value, int length) { + if (value > _tempWidth) _tempWidth = value; + _count++; + if (_count == length) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _width = _tempWidth; + _tempWidth = 0; + }); + } + }); + } + } + + @override + Widget build(BuildContext context) { + final height = ref.watch(pWalletChainHeight(widget.walletId)); + return StreamBuilder( + stream: ref.watch( + mainDBProvider.select( + (s) => s.isar.utxos + .where() + .walletIdEqualTo(widget.walletId) + .filter() + .otherDataIsNotNull() + .watch(fireImmediately: true), + ), + ), + builder: (context, snapshot) { + List<(UTXO, OpNameData)> list = []; + if (snapshot.hasData) { + list = snapshot.data!.map((utxo) { + final data = jsonDecode(utxo.otherData!) as Map; + + final nameData = jsonDecode(data["nameOpData"] as String) as Map; + + return ( + utxo, + OpNameData(nameData.cast(), utxo.blockHeight ?? height) + ); + }).toList(growable: false); + } + + return Column( + children: [ + ...list.map( + (e) => Padding( + padding: const EdgeInsets.only( + bottom: 10, + ), + child: OwnedNameCard( + key: ValueKey(e), + utxo: e.$1, + opNameData: e.$2, + firstColWidth: _width, + calculatedFirstColWidth: (value) => _sillyHack( + value, + list.length, + ), + ), + ), + ), + SizedBox( + height: Util.isDesktop ? 14 : 6, + ), + ], + ); + }, + ); + } +} diff --git a/lib/pages/namecoin_names/sub_widgets/name_details.dart b/lib/pages/namecoin_names/sub_widgets/name_details.dart new file mode 100644 index 000000000..7fa77f807 --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/name_details.dart @@ -0,0 +1,731 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../models/isar/models/isar_models.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../providers/global/secure_store_provider.dart'; +import '../../../providers/global/wallets_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/background.dart'; +import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../widgets/custom_buttons/blue_text_button.dart'; +import '../../../widgets/custom_buttons/simple_copy_button.dart'; +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 '../manage_domain_view.dart'; +import 'transfer_option_widget.dart'; +import 'update_option_widget.dart'; + +class NameDetailsView extends ConsumerStatefulWidget { + const NameDetailsView({ + super.key, + required this.utxoId, + required this.walletId, + }); + + static const routeName = "/namecoinNameDetails"; + + final Id utxoId; + final String walletId; + + @override + ConsumerState createState() => _ManageDomainsWidgetState(); +} + +class _ManageDomainsWidgetState extends ConsumerState { + late Stream streamUTXO; + UTXO? utxo; + OpNameData? opNameData; + + String? constructedName, value; + + Stream? streamLabel; + AddressLabel? label; + + void setUtxo(UTXO? utxo, int currentHeight) { + if (utxo != null) { + this.utxo = utxo; + final data = jsonDecode(utxo.otherData!) as Map; + + final nameData = jsonDecode(data["nameOpData"] as String) as Map; + opNameData = + OpNameData(nameData.cast(), utxo.blockHeight ?? currentHeight); + + _setName(); + } + } + + void _setName() { + try { + constructedName = opNameData!.constructedName; + value = opNameData!.value; + } catch (_) { + if (opNameData?.op == OpName.nameNew) { + ref + .read(secureStoreProvider) + .read( + 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(() {}); + } + }); + } + }); + } + } + } + + (String, Color) _getExpiry(int currentChainHeight, StackColors theme) { + final String message; + final Color color; + + if (utxo?.blockHash == null) { + message = "Expires in $blocksNameExpiration+ blocks"; + color = theme.accentColorGreen; + } else { + final remaining = opNameData?.expiredBlockLeft( + currentChainHeight, + false, + ); + final semiRemaining = opNameData?.expiredBlockLeft( + currentChainHeight, + true, + ); + + if (remaining == null) { + color = theme.accentColorRed; + message = "Expired"; + } else { + message = "Expires in $remaining blocks"; + if (semiRemaining == null) { + color = theme.accentColorYellow; + } else { + color = theme.accentColorGreen; + } + } + } + + return (message, color); + } + + bool _checkConfirmedUtxo(int currentHeight) { + return (ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet) + .checkUtxoConfirmed( + utxo!, + currentHeight, + ); + } + + @override + void initState() { + super.initState(); + + setUtxo( + ref + .read(mainDBProvider) + .isar + .utxos + .where() + .idEqualTo(widget.utxoId) + .findFirstSync(), + ref.read(pWalletChainHeight(widget.walletId)), + ); + + _setName(); + + if (utxo?.address != null) { + label = ref.read(mainDBProvider).getAddressLabelSync( + widget.walletId, + utxo!.address!, + ); + + if (label != null) { + streamLabel = ref.read(mainDBProvider).watchAddressLabel(id: label!.id); + } + } + + streamUTXO = ref.read(mainDBProvider).watchUTXO(id: widget.utxoId); + } + + @override + Widget build(BuildContext context) { + final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); + + final (message, color) = _getExpiry( + currentHeight, + Theme.of(context).extension()!, + ); + + 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, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) { + return SizedBox( + width: 641, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Domain details", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + top: 10, + ), + child: RoundedContainer( + padding: EdgeInsets.zero, + color: Colors.transparent, + borderColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: child, + ), + ), + if (canManage) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Transfer", + buttonHeight: ButtonHeight.l, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return SDialog( + child: SizedBox( + width: 641, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Transfer domain", + style: STextStyles.desktopH3( + context, + ), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + top: 16, + ), + child: TransferOptionWidget( + walletId: widget.walletId, + utxo: utxo!, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + const SizedBox( + width: 32, + ), + Expanded( + child: SecondaryButton( + label: "Update", + buttonHeight: ButtonHeight.l, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return SDialog( + child: SizedBox( + width: 641, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Update domain", + style: STextStyles.desktopH3( + context, + ), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: UpdateOptionWidget( + walletId: widget.walletId, + utxo: utxo!, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + if (canManage) + const SizedBox( + height: 32, + ), + ], + ), + ); + }, + child: StreamBuilder( + stream: streamUTXO, + builder: (context, snapshot) { + if (snapshot.hasData) { + setUtxo(snapshot.data!, currentHeight); + } + + return utxo == null + ? Center( + 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) + SelectableText( + 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( + "Value", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + 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: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address", + 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), + ), + ], + ), + ), + 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, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + 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), + ), + ], + ), + ), + 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 + ? 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( + "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: [ + 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), + ), + ], + ), + ), + ], + ); + }, + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return Container( + width: double.infinity, + height: 1.0, + color: Theme.of(context).extension()!.textFieldDefaultBG, + ); + } else { + return const SizedBox( + height: 12, + ); + } + } +} diff --git a/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart b/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart new file mode 100644 index 000000000..b345966c6 --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart @@ -0,0 +1,227 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../models/isar/models/isar_models.dart'; +import '../../../providers/global/secure_store_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/rounded_white_container.dart'; +import 'name_details.dart'; + +class OwnedNameCard extends ConsumerStatefulWidget { + const OwnedNameCard({ + super.key, + required this.opNameData, + required this.utxo, + this.firstColWidth, + this.calculatedFirstColWidth, + }); + + final OpNameData opNameData; + final UTXO utxo; + + final double? firstColWidth; + final void Function(double)? calculatedFirstColWidth; + + @override + ConsumerState createState() => _OwnedNameCardState(); +} + +class _OwnedNameCardState extends ConsumerState { + String? constructedName, value; + + (String, Color) _getExpiry(int currentChainHeight, StackColors theme) { + final String message; + final Color color; + + if (widget.utxo.blockHash == null) { + message = "Expires in $blocksNameExpiration+ blocks"; + color = theme.accentColorGreen; + } else { + final remaining = widget.opNameData.expiredBlockLeft( + currentChainHeight, + false, + ); + final semiRemaining = widget.opNameData.expiredBlockLeft( + currentChainHeight, + true, + ); + + if (remaining == null) { + color = theme.accentColorRed; + message = "Expired"; + } else { + message = "Expires in $remaining blocks"; + if (semiRemaining == null) { + color = theme.accentColorYellow; + } else { + color = theme.accentColorGreen; + } + } + } + + return (message, color); + } + + bool _lock = false; + + Future _showDetails() async { + if (_lock) return; + _lock = true; + try { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => SDialog( + child: NameDetailsView( + utxoId: widget.utxo.id, + walletId: widget.utxo.walletId, + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + NameDetailsView.routeName, + arguments: ( + widget.utxo.id, + widget.utxo.walletId, + ), + ); + } + } finally { + _lock = false; + } + } + + void _setName() { + try { + constructedName = widget.opNameData.constructedName; + value = widget.opNameData.value; + } catch (_) { + if (widget.opNameData.op == OpName.nameNew) { + ref + .read(secureStoreProvider) + .read( + key: nameSaltKeyBuilder( + widget.utxo.txid, + widget.utxo.walletId, + widget.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(() {}); + } + }); + } + }); + } + } + } + + @override + void initState() { + super.initState(); + _setName(); + } + + double _callbackWidth = 0; + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final (message, color) = _getExpiry( + ref.watch(pWalletChainHeight(widget.utxo.walletId)), + Theme.of(context).extension()!, + ); + + return RoundedWhiteContainer( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ConditionalParent( + condition: widget.firstColWidth != null && Util.isDesktop, + builder: (child) => ConstrainedBox( + constraints: BoxConstraints(maxWidth: widget.firstColWidth!), + child: child, + ), + child: ConditionalParent( + condition: widget.firstColWidth == null && Util.isDesktop, + builder: (child) => LayoutBuilder( + builder: (context, constraints) { + if (widget.firstColWidth == null && + _callbackWidth != constraints.maxWidth) { + _callbackWidth = constraints.maxWidth; + widget.calculatedFirstColWidth?.call(_callbackWidth); + } + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: constraints.maxWidth), + child: child, + ); + }, + ), + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(constructedName ?? ""), + const SizedBox( + height: 8, + ), + SelectableText( + message, + style: STextStyles.w500_12(context).copyWith( + color: color, + ), + ), + ], + ), + ), + ), + ), + if (Util.isDesktop) + Expanded( + child: SelectableText( + value ?? "", + style: STextStyles.w500_12(context), + ), + ), + if (Util.isDesktop) + const SizedBox( + width: 12, + ), + PrimaryButton( + label: "Details", + buttonHeight: Util.isDesktop ? ButtonHeight.xs : ButtonHeight.l, + onPressed: _showDetails, + ), + ], + ), + ); + } +} diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart new file mode 100644 index 000000000..dcd8e1282 --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -0,0 +1,480 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../models/isar/models/blockchain_data/utxo.dart'; +import '../../../providers/providers.dart'; +import '../../../utilities/address_utils.dart'; +import '../../../utilities/amount/amount.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 '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../wallets/models/name_op_state.dart'; +import '../../../wallets/models/tx_data.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/conditional_parent.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/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/stack_dialog.dart'; +import '../../../widgets/stack_text_field.dart'; +import '../../../widgets/textfield_icon_button.dart'; +import '../../address_book_views/address_book_view.dart'; +import '../../send_view/sub_widgets/building_transaction_dialog.dart'; +import '../confirm_name_transaction_view.dart'; + +class TransferOptionWidget extends ConsumerStatefulWidget { + const TransferOptionWidget({ + super.key, + required this.walletId, + required this.utxo, + this.clipboard = const ClipboardWrapper(), + this.barcodeScanner = const BarcodeScannerWrapper(), + }); + + final String walletId; + final UTXO utxo; + + final ClipboardInterface clipboard; + final BarcodeScannerInterface barcodeScanner; + + @override + ConsumerState createState() => + _TransferOptionWidgetState(); +} + +class _TransferOptionWidgetState extends ConsumerState { + late final String walletId; + late final ClipboardInterface clipboard; + late final BarcodeScannerInterface scanner; + late final TextEditingController _addressController; + late final FocusNode _addressFocusNode; + + String? _address; + + bool _previewLock = false; + Future _preview() async { + if (_previewLock) return; + _previewLock = true; + + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + + try { + final wallet = ref.read(pWallets).getWallet(walletId) as NamecoinWallet; + + bool wasCancelled = false; + + if (mounted) { + if (Util.isDesktop) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: BuildingTransactionDialog( + coin: wallet.info.coin, + isSpark: false, + onCancel: () { + wasCancelled = true; + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ), + ); + }, + ), + ); + } else { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + coin: wallet.info.coin, + isSpark: false, + onCancel: () { + wasCancelled = true; + Navigator.of(context).pop(); + }, + ); + }, + ), + ); + } + } + + final opName = wallet.getOpNameDataFrom(widget.utxo)!; + + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + final nameScriptHex = scriptNameUpdate(opName.fullname, opName.value); + + final txDataFuture = wallet.prepareNameSend( + txData: TxData( + feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? + recipients: [ + ( + address: _address!, + isChange: false, + amount: Amount( + rawValue: BigInt.from(kNameAmountSats), + fractionDigits: wallet.cryptoCurrency.fractionDigits, + ), + ), + ], + note: "Transfer ${opName.constructedName}", + opNameState: NameOpState( + name: opName.fullname, + saltHex: "", + commitment: "", + value: opName.value, + nameScriptHex: nameScriptHex, + type: OpName.nameUpdate, + output: widget.utxo, + outputPosition: -1, //currently unknown, updated later + ), + ), + ); + + final results = await Future.wait([ + txDataFuture, + time, + ]); + + final txData = results.first as TxData; + + if (!wasCancelled && mounted) { + // pop building dialog + Navigator.of(context).pop(); + + if (mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + ConfirmNameTransactionView.routeName, + arguments: (txData, widget.walletId), + ); + } + } + } + } catch (e, s) { + Logging.instance.e( + "_preview transfer name failed", + error: e, + stackTrace: s, + ); + + if (mounted) { + String err = e.toString(); + if (err.startsWith("Exception: ")) { + err = err.replaceFirst("Exception: ", ""); + } + + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _previewLock = false; + } + } + + bool _enableButton = false; + + void _setValidAddressProviders(String? address) { + _enableButton = ref + .read(pWallets) + .getWallet(walletId) + .cryptoCurrency + .validateAddress(address ?? ""); + if (mounted) { + setState(() {}); + } + } + + Future _scanQr() async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + final coin = ref.read(pWalletCoin(walletId)); + + Logging.instance.d("qrResult content: ${qrResult.rawContent}"); + + final paymentData = AddressUtils.parsePaymentUri( + qrResult.rawContent, + logging: Logging.instance, + ); + + if (paymentData != null && + paymentData.coin?.uriScheme == coin.uriScheme) { + // auto fill address + _address = paymentData.address.trim(); + _addressController.text = _address!; + + _setValidAddressProviders(_address); + + // now check for non standard encoded basic address + } else { + _address = qrResult.rawContent.split("\n").first.trim(); + _addressController.text = _address ?? ""; + + _setValidAddressProviders(_address); + } + } on PlatformException catch (e, s) { + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.e( + "Failed to get camera permissions while trying to scan qr code in" + " $runtimeType", + error: e, + stackTrace: s, + ); + } + } + + @override + void initState() { + super.initState(); + walletId = widget.walletId; + clipboard = widget.clipboard; + scanner = widget.barcodeScanner; + _addressController = TextEditingController(); + _addressFocusNode = FocusNode(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _addressFocusNode.requestFocus(); + } + }); + } + + @override + void dispose() { + _addressController.dispose(); + _addressFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("nameTransferViewAddressFieldKey"), + controller: _addressController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + _address = newValue.trim(); + _setValidAddressProviders(_address); + }, + focusNode: _addressFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${ref.watch(pWalletCoin(walletId)).ticker} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _addressController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _addressController.text.isNotEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Address Field Input.", + key: const Key( + "nameTransferClearAddressFieldButtonKey", + ), + onTap: () { + _addressController.text = ""; + _address = ""; + _setValidAddressProviders( + _address, + ); + setState(() {}); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Address Field Input.", + key: const Key( + "nameTransferPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, + content.indexOf( + "\n", + ), + ); + } + + _addressController.text = content.trim(); + _address = content.trim(); + + _setValidAddressProviders( + _address, + ); + } + }, + child: _addressController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_addressController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Address Book Button. Opens Address Book For Address Field.", + key: const Key( + "nameTransferAddressBookButtonKey", + ), + onTap: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: ref.read(pWalletCoin(walletId)), + ); + }, + child: const AddressBookIcon(), + ), + if (_addressController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key( + "nameTransferScanQrButtonKey", + ), + onTap: _scanQr, + child: const QrCodeIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + SizedBox( + height: Util.isDesktop ? 42 : 16, + ), + if (!Util.isDesktop) const Spacer(), + ConditionalParent( + condition: Util.isDesktop, + builder: (child) => Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: child, + ), + ], + ), + child: PrimaryButton( + label: "Transfer", + enabled: _enableButton, + // width: Util.isDesktop ? 160 : double.infinity, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _preview, + ), + ), + if (!Util.isDesktop) + const SizedBox( + height: 16, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart new file mode 100644 index 000000000..80c016245 --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart @@ -0,0 +1,356 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../models/isar/models/blockchain_data/utxo.dart'; +import '../../../providers/global/wallets_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/barcode_scanner_interface.dart'; +import '../../../utilities/clipboard_interface.dart'; +import '../../../utilities/extensions/extensions.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/text_formatters.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/models/name_op_state.dart'; +import '../../../wallets/models/tx_data.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.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/stack_dialog.dart'; +import '../../send_view/sub_widgets/building_transaction_dialog.dart'; +import '../confirm_name_transaction_view.dart'; + +class UpdateOptionWidget extends ConsumerStatefulWidget { + const UpdateOptionWidget({ + super.key, + required this.walletId, + required this.utxo, + this.clipboard = const ClipboardWrapper(), + this.barcodeScanner = const BarcodeScannerWrapper(), + }); + + final String walletId; + final UTXO utxo; + + final ClipboardInterface clipboard; + final BarcodeScannerInterface barcodeScanner; + + @override + ConsumerState createState() => _BuyDomainWidgetState(); +} + +class _BuyDomainWidgetState extends ConsumerState { + final _controller = TextEditingController(); + + late final bool wasJson; + late final String _currentValue; + + String _getNewValue() { + final value = _controller.text; + try { + final json = jsonDecode(value); + final minified = jsonEncode(json); + return minified; + } catch (_) {} + return value; + } + + int _countLength() { + try { + final json = jsonDecode(_controller.text); + final minified = jsonEncode(json); + return minified.toUint8ListFromUtf8.lengthInBytes; + } catch (_) {} + + return _controller.text.toUint8ListFromUtf8.lengthInBytes; + } + + bool _previewLock = false; + Future _previewUpdate() async { + if (_previewLock) return; + _previewLock = true; + try { + final newValue = _getNewValue(); + if (newValue == _currentValue) { + throw Exception("Value was not changed!"); + } + + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; + + bool wasCancelled = false; + + if (mounted) { + if (Util.isDesktop) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: BuildingTransactionDialog( + coin: wallet.info.coin, + isSpark: false, + onCancel: () { + wasCancelled = true; + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ), + ); + }, + ), + ); + } else { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + coin: wallet.info.coin, + isSpark: false, + onCancel: () { + wasCancelled = true; + Navigator.of(context).pop(); + }, + ); + }, + ), + ); + } + } + + final _address = await wallet.getCurrentReceivingAddress(); + + final opName = wallet.getOpNameDataFrom(widget.utxo)!; + + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + final nameScriptHex = scriptNameUpdate(opName.fullname, newValue); + + final txDataFuture = wallet.prepareNameSend( + txData: TxData( + feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? + recipients: [ + ( + address: _address!.value, + isChange: false, + amount: Amount( + rawValue: BigInt.from(kNameAmountSats), + fractionDigits: wallet.cryptoCurrency.fractionDigits, + ), + ), + ], + note: "Update ${opName.constructedName} (${opName.fullname})", + opNameState: NameOpState( + name: opName.fullname, + saltHex: "", + commitment: "", + value: newValue, + nameScriptHex: nameScriptHex, + type: OpName.nameUpdate, + output: widget.utxo, + outputPosition: -1, //currently unknown, updated later + ), + ), + ); + + final results = await Future.wait([ + txDataFuture, + time, + ]); + + final txData = results.first as TxData; + + if (!wasCancelled && mounted) { + // pop building dialog + Navigator.of(context).pop(); + + if (mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + ConfirmNameTransactionView.routeName, + arguments: (txData, widget.walletId), + ); + } + } + } + } catch (e, s) { + Logging.instance.e( + "_preview update name failed", + error: e, + stackTrace: s, + ); + + String? err; + if (e.toString().contains("Contains invalid characters")) { + err = "Contains invalid characters"; + } + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Update failed", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _previewLock = false; + } + } + + @override + void initState() { + super.initState(); + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; + + _currentValue = wallet.getOpNameDataFrom(widget.utxo)!.value; + + // see if json, if so format nicely + try { + final json = jsonDecode(_currentValue); + _controller.text = const JsonEncoder.withIndent(" ").convert(json); + wasJson = true; + } catch (_) { + _controller.text = _currentValue; + wasJson = false; + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, + children: [ + Text( + "Edit value", + style: STextStyles.label(context), + ), + const SizedBox( + height: 6, + ), + TextField( + controller: _controller, + maxLines: null, + autocorrect: false, + enableSuggestions: false, + style: const TextStyle(fontFamily: "monospace"), + onChanged: (_) { + setState(() {}); + }, + inputFormatters: [ + Utf8ByteLengthLimitingTextInputFormatter( + valueMaxLength, + tryMinifyJson: true, + ), + ], + ), + const SizedBox( + height: 4, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Builder( + builder: (context) { + final length = _countLength(); + return Text( + "$length/$valueMaxLength", + style: STextStyles.w500_10(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle2, + ), + ); + }, + ), + ], + ), + SizedBox( + height: Util.isDesktop ? 32 : 16, + ), + if (!Util.isDesktop) const Spacer(), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Update", + enabled: _controller.text.isNotEmpty, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _previewUpdate, + ), + ), + ], + ), + if (!Util.isDesktop) + const SizedBox( + height: 16, + ), + ], + ); + } +} diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index d94e25d7a..41422c7e0 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -52,6 +52,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/namecoin_wallet.dart'; import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; @@ -87,6 +88,7 @@ import '../churning/churning_view.dart'; import '../coin_control/coin_control_view.dart'; import '../exchange_view/wallet_initiated_exchange_view.dart'; import '../monkey/monkey_view.dart'; +import '../namecoin_names/namecoin_names_home_view.dart'; import '../notification_views/notifications_view.dart'; import '../ordinals/ordinals_view.dart'; import '../paynym/paynym_claim_view.dart'; @@ -1172,6 +1174,17 @@ class _WalletViewState extends ConsumerState { ); }, ), + if (wallet is NamecoinWallet) + WalletNavigationBarItemData( + label: "Domains", + icon: const PaynymNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + NamecoinNamesHomeView.routeName, + arguments: widget.walletId, + ); + }, + ), if (!viewOnly && wallet is PaynymInterface) WalletNavigationBarItemData( label: "PayNym", diff --git a/lib/pages_desktop_specific/coin_control/utxo_row.dart b/lib/pages_desktop_specific/coin_control/utxo_row.dart index 26204375c..548e2c05e 100644 --- a/lib/pages_desktop_specific/coin_control/utxo_row.dart +++ b/lib/pages_desktop_specific/coin_control/utxo_row.dart @@ -20,7 +20,9 @@ import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/text_styles.dart'; +import '../../wallets/crypto_currency/coins/namecoin.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/desktop/secondary_button.dart'; @@ -135,19 +137,18 @@ class _UtxoRowState extends ConsumerState { ), UTXOStatusIcon( blocked: utxo.isBlocked, - status: utxo.isConfirmed( - ref.watch(pWalletChainHeight(widget.walletId)), - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms, - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minCoinbaseConfirms, - ) + status: (coin is Namecoin + ? (ref.watch(pWallets).getWallet(widget.walletId) + as NamecoinWallet) + .checkUtxoConfirmed( + utxo, + ref.watch(pWalletChainHeight(widget.walletId)), + ) + : utxo.isConfirmed( + ref.watch(pWalletChainHeight(widget.walletId)), + coin.minConfirms, + coin.minCoinbaseConfirms, + )) ? UTXOStatusIconStatus.confirmed : UTXOStatusIconStatus.unconfirmed, background: Theme.of(context).extension()!.popupBG, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index 7993ee0a6..74512e256 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -19,6 +19,7 @@ import 'package:flutter_svg/svg.dart'; import '../../../../app_config.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/monkey/monkey_view.dart'; +import '../../../../pages/namecoin_names/namecoin_names_home_view.dart'; import '../../../../pages/paynym/paynym_claim_view.dart'; import '../../../../pages/paynym/paynym_home_view.dart'; import '../../../../providers/desktop/current_desktop_menu_item.dart'; @@ -99,6 +100,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { onMonkeyPressed: _onMonkeyPressed, onFusionPressed: _onFusionPressed, onChurnPressed: _onChurnPressed, + onNamesPressed: _onNamesPressed, ), ); } @@ -380,6 +382,15 @@ class _DesktopWalletFeaturesState extends ConsumerState { ); } + void _onNamesPressed() { + Navigator.of(context, rootNavigator: true).pop(); + + Navigator.of(context).pushNamed( + NamecoinNamesHomeView.routeName, + arguments: widget.walletId, + ); + } + @override Widget build(BuildContext context) { final wallet = ref.watch(pWallets).getWallet(widget.walletId); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index eb2746558..ef452c09b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart @@ -31,6 +31,7 @@ import '../../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../../wallets/isar/models/wallet_info.dart'; import '../../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; @@ -62,6 +63,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget { required this.onMonkeyPressed, required this.onFusionPressed, required this.onChurnPressed, + required this.onNamesPressed, }); final String walletId; @@ -75,6 +77,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget { final VoidCallback? onMonkeyPressed; final VoidCallback? onFusionPressed; final VoidCallback? onChurnPressed; + final VoidCallback? onNamesPressed; @override ConsumerState createState() => _MoreFeaturesDialogState(); @@ -474,6 +477,13 @@ class _MoreFeaturesDialogState extends ConsumerState { iconAsset: Assets.svg.churn, onPressed: () async => widget.onChurnPressed?.call(), ), + if (wallet is NamecoinWallet) + _MoreFeaturesItem( + label: "Domains", + detail: "Namecoin DNS", + iconAsset: Assets.svg.robotHead, + onPressed: () async => widget.onNamesPressed?.call(), + ), if (wallet is SparkInterface && !isViewOnly) _MoreFeaturesClearSparkCacheItem( cryptoCurrency: wallet.cryptoCurrency, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 71a3b890e..6695a53b8 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -72,6 +72,11 @@ import 'pages/home_view/home_view.dart'; import 'pages/intro_view.dart'; import 'pages/manage_favorites_view/manage_favorites_view.dart'; import 'pages/monkey/monkey_view.dart'; +import 'pages/namecoin_names/buy_domain_view.dart'; +import 'pages/namecoin_names/confirm_name_transaction_view.dart'; +import 'pages/namecoin_names/manage_domain_view.dart'; +import 'pages/namecoin_names/namecoin_names_home_view.dart'; +import 'pages/namecoin_names/sub_widgets/name_details.dart'; import 'pages/notification_views/notifications_view.dart'; import 'pages/ordinals/ordinal_details_view.dart'; import 'pages/ordinals/ordinals_filter_view.dart'; @@ -716,6 +721,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case NameDetailsView.routeName: + if (args is (Id, String)) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => NameDetailsView( + walletId: args.$2, + utxoId: args.$1, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case PaynymClaimView.routeName: if (args is String) { return getRoute( @@ -772,6 +792,35 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case NamecoinNamesHomeView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => NamecoinNamesHomeView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case ManageDomainView.routeName: + if (args is ({String walletId, UTXO utxo})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ManageDomainView( + walletId: args.walletId, + utxo: args.utxo, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case FusionProgressView.routeName: if (args is String) { return getRoute( @@ -1843,6 +1892,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case ConfirmNameTransactionView.routeName: + if (args is (TxData, String)) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ConfirmNameTransactionView( + txData: args.$1, + walletId: args.$2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case WalletInitiatedExchangeView.routeName: if (args is Tuple2) { return getRoute( @@ -2155,6 +2219,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case BuyDomainView.routeName: + if (args is ({String walletId, String domainName})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => BuyDomainView( + walletId: args.walletId, + domainName: args.domainName, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // == Desktop specific routes ============================================ case CreatePasswordView.routeName: if (args is bool) { diff --git a/lib/utilities/text_formatters.dart b/lib/utilities/text_formatters.dart new file mode 100644 index 000000000..a7cf76d4f --- /dev/null +++ b/lib/utilities/text_formatters.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:flutter/services.dart'; + +class Utf8ByteLengthLimitingTextInputFormatter extends TextInputFormatter { + Utf8ByteLengthLimitingTextInputFormatter( + this.maxBytes, { + this.tryMinifyJson = false, + }) : assert(maxBytes == -1 || maxBytes > 0); + + final int maxBytes; + final bool tryMinifyJson; + + static String _maybeTryMinify(String text, bool tryMinifyJson) { + if (tryMinifyJson) { + try { + final json = jsonDecode(text); + final minified = jsonEncode(json); + return minified; + } catch (_) {} + } + + return text; + } + + static TextEditingValue truncate( + TextEditingValue value, + int maxBytes, + bool tryMinifyJson, + ) { + final String text = _maybeTryMinify(value.text, tryMinifyJson); + final encoded = utf8.encode(text); + + if (encoded.length <= maxBytes) { + return value; + } + + int validLength = maxBytes; + while (validLength > 0 && (encoded[validLength] & 0xC0) == 0x80) { + validLength--; + } + + final truncated = utf8.decode(encoded.sublist(0, validLength)); + + return TextEditingValue( + text: truncated, + selection: value.selection.copyWith( + baseOffset: math.min(value.selection.start, truncated.length), + extentOffset: math.min(value.selection.end, truncated.length), + ), + composing: !value.composing.isCollapsed && + truncated.length > value.composing.start + ? TextRange( + start: value.composing.start, + end: math.min(value.composing.end, truncated.length), + ) + : TextRange.empty, + ); + } + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + if (maxBytes == -1 || + utf8 + .encode(_maybeTryMinify(newValue.text, tryMinifyJson)) + .lengthInBytes <= + maxBytes) { + return newValue; + } + + assert(maxBytes > 0); + + if (utf8 + .encode(_maybeTryMinify(oldValue.text, tryMinifyJson)) + .lengthInBytes == + maxBytes && + oldValue.selection.isCollapsed) { + return oldValue; + } + + return truncate(newValue, maxBytes, tryMinifyJson); + } +} diff --git a/lib/wallets/crypto_currency/coins/particl.dart b/lib/wallets/crypto_currency/coins/particl.dart index 067aae72d..8788b6114 100644 --- a/lib/wallets/crypto_currency/coins/particl.dart +++ b/lib/wallets/crypto_currency/coins/particl.dart @@ -219,7 +219,7 @@ class Particl extends Bip39HDCurrency with ElectrumXCurrencyInterface { int get targetBlockTimeSeconds => 600; @override - DerivePathType get defaultDerivePathType => DerivePathType.bip84; + DerivePathType get defaultDerivePathType => DerivePathType.bip44; @override Uri defaultBlockExplorer(String txid) { diff --git a/lib/wallets/models/name_op_state.dart b/lib/wallets/models/name_op_state.dart new file mode 100644 index 000000000..5390db018 --- /dev/null +++ b/lib/wallets/models/name_op_state.dart @@ -0,0 +1,59 @@ +import 'package:namecoin/namecoin.dart'; + +import '../../models/isar/models/blockchain_data/utxo.dart'; + +class NameOpState { + final String name; + final OpName type; + final String saltHex; + final String commitment; + final String value; + final String nameScriptHex; + final int outputPosition; + final UTXO? output; + + NameOpState({ + required this.name, + required this.type, + required this.saltHex, + required this.commitment, + required this.value, + required this.nameScriptHex, + required this.outputPosition, + this.output, + }); + + NameOpState copyWith({ + String? name, + OpName? type, + String? saltHex, + String? commitment, + String? value, + String? nameScriptHex, + int? outputPosition, + }) { + return NameOpState( + name: name ?? this.name, + type: type ?? this.type, + saltHex: saltHex ?? this.saltHex, + commitment: commitment ?? this.commitment, + value: value ?? this.value, + nameScriptHex: nameScriptHex ?? this.nameScriptHex, + outputPosition: outputPosition ?? this.outputPosition, + output: output, + ); + } + + @override + String toString() { + return "NameOpState(" + "name: $name, " + "type: ${type.name}, " + "saltHex: $saltHex, " + "commitment: $commitment, " + "value: $value, " + "nameScriptHex: $nameScriptHex, " + "outputPosition: $outputPosition, " + "output: $output)"; + } +} diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 652a5605f..21fa206c9 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -8,6 +8,7 @@ import '../../models/paynym/paynym_account_lite.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; import '../isar/models/spark_coin.dart'; +import 'name_op_state.dart'; typedef TxRecipient = ({String address, Amount amount, bool isChange}); @@ -77,6 +78,9 @@ class TxData { final bool ignoreCachedBalanceChecks; + // Namecoin Name related + final NameOpState? opNameState; + TxData({ this.feeRateType, this.feeRateAmount, @@ -113,6 +117,7 @@ class TxData { this.usedSparkCoins, this.tempTx, this.ignoreCachedBalanceChecks = false, + this.opNameState, }); Amount? get amount { @@ -239,6 +244,7 @@ class TxData { List? usedSparkCoins, TransactionV2? tempTx, bool? ignoreCachedBalanceChecks, + NameOpState? opNameState, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -277,6 +283,7 @@ class TxData { tempTx: tempTx ?? this.tempTx, ignoreCachedBalanceChecks: ignoreCachedBalanceChecks ?? this.ignoreCachedBalanceChecks, + opNameState: opNameState ?? this.opNameState, ); } @@ -316,5 +323,6 @@ class TxData { 'usedSparkCoins: $usedSparkCoins, ' 'tempTx: $tempTx, ' 'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, ' + 'opNameState: $opNameState, ' '}'; } diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 9aa1357f0..aed72a33a 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -738,7 +738,7 @@ class FiroWallet extends Bip39HDWallet ); // receiving addresses - Logging.instance.d("checking receiving addresses..."); + Logging.instance.i("checking receiving addresses..."); final canBatch = await serverCanBatch; diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 5eb54b33b..64dc23801 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -1,17 +1,60 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:isar/isar.dart'; +import 'package:namecoin/namecoin.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/isar/models/isar_models.dart'; +import '../../../models/signing_data.dart'; import '../../../utilities/amount/amount.dart'; +import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../utilities/enums/fee_rate_type_enum.dart'; +import '../../../utilities/extensions/extensions.dart'; import '../../../utilities/logger.dart'; import '../../crypto_currency/crypto_currency.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../../models/name_op_state.dart'; +import '../../models/tx_data.dart'; import '../intermediate/bip39_hd_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; +import '../wallet_mixin_interfaces/cpfp_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/rbf_interface.dart'; + +const kNameWaitBlocks = blocksMinToRenewName; +const kNameTxVersion = 0x7100; +const kNameTxDefaultFeeRate = FeeRateType.slow; + +const kNameNewAmountSats = 150_0000; +const kNameAmountSats = 100_0000; + +String nameSaltKeyBuilder(String txid, String walletId, int txPos) { + if (txPos.isNegative) { + throw Exception("Invalid vout index"); + } + + return "${walletId}_${txid}_${txPos}nameSaltData"; +} + +String encodeNameSaltData(String name, String salt, String value) => + jsonEncode({ + "name": name, + "salt": salt, + "value": value, + }); + +({String salt, String name, String value}) decodeNameSaltData(String value) { + try { + final map = (jsonDecode(value) as Map).cast(); + return (salt: map["salt"]!, name: map["name"]!, value: map["value"]!); + } catch (_) { + throw Exception("Bad name salt data"); + } +} class NamecoinWallet extends Bip39HDWallet @@ -50,19 +93,118 @@ class NamecoinWallet // =========================================================================== @override - Future< - ({ - bool blocked, - String? blockedReason, - String? utxoLabel, - })> checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, String? utxoOwnerAddress, - ) async { - // Namecoin doesn't have special outputs like tokens, ordinals, etc. - return (blocked: false, blockedReason: null, utxoLabel: null); + ) { + throw UnsupportedError( + "Namecoin does not used the checkBlockUTXO() function. " + "Due to tight integration with names, output freezing is handled directly" + " in the overridden parseUTXO() function.", + ); + } + + @override + Future parseUTXO({ + required Map jsonUTXO, + }) async { + final txn = await electrumXCachedClient.getTransaction( + txHash: jsonUTXO["tx_hash"] as String, + verbose: true, + cryptoCurrency: cryptoCurrency, + ); + + final inputs = txn["vin"] as List? ?? []; + final isCoinbase = inputs.any((e) => (e as Map?)?["coinbase"] != null); + + final vout = jsonUTXO["tx_pos"] as int; + + final outputs = txn["vout"] as List; + + String? utxoOwnerAddress; + + bool shouldBlock = false; + String? blockReason; + String? label; + String? otherDataString; + + for (final output in outputs) { + // find matching output + if (output["n"] == vout) { + utxoOwnerAddress = + output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]?["address"] as String?; + + // check for nameOp + if (output["scriptPubKey"]?["nameOp"] != null) { + // block/freeze regardless of whether parsing the raw data succeeds + shouldBlock = true; + blockReason = "Contains name"; + + try { + final rawNameOP = (output["scriptPubKey"]["nameOp"] as Map) + .cast(); + + otherDataString = jsonEncode({ + UTXOOtherDataKeys.nameOpData: jsonEncode(rawNameOP), + }); + final nameOp = OpNameData( + rawNameOP, + jsonUTXO["height"] as int, + ); + Logging.instance.i( + "nameOp:\n$nameOp", + ); + + switch (nameOp.op) { + case OpName.nameNew: + label = "Name New"; + break; + case OpName.nameFirstUpdate: + label = "Name First Update: ${nameOp.fullname}"; + break; + case OpName.nameUpdate: + label = "Name Update: ${nameOp.fullname}"; + break; + } + } catch (e, s) { + Logging.instance.w( + "Namecoin OpNameData failed to parse" + " \"${output["scriptPubKey"]?["nameOp"]}\"", + error: e, + stackTrace: s, + ); + label = "Failed to parse raw nameOp data"; + } + } + + break; + } + } + + final utxo = UTXO( + walletId: walletId, + txid: txn["txid"] as String, + vout: vout, + value: jsonUTXO["value"] as int, + name: label ?? "", + isBlocked: shouldBlock, + blockedReason: blockReason, + isCoinbase: txn["is_coinbase"] as bool? ?? + txn["is-coinbase"] as bool? ?? + txn["iscoinbase"] as bool? ?? + isCoinbase, + blockHash: txn["blockhash"] as String?, + blockHeight: jsonUTXO["height"] as int?, + blockTime: txn["blocktime"] as int?, + address: utxoOwnerAddress, + otherData: otherDataString, + ); + + return utxo; } @override @@ -242,7 +384,7 @@ class NamecoinWallet .fold(BigInt.zero, (value, element) => value + element); TransactionType type; - final TransactionSubType subType = TransactionSubType.none; + const TransactionSubType subType = TransactionSubType.none; // At least one input was owned by this wallet. if (wasSentFromThisWallet) { @@ -290,4 +432,940 @@ class NamecoinWallet await mainDB.updateOrPutTransactionV2s(txns); } + + // namecoin names ============================================================ + + Future<({OpNameData? data, NameState nameState})> lookupName( + String name, + ) async { + // first check own utxos. Should only need to check NAME NEW here. + // NAME UPDATE and NAME FIRST UPDATE will appear readable from electrumx + final utxos = + await mainDB.getUTXOs(walletId).filter().otherDataIsNotNull().findAll(); + for (final utxo in utxos) { + final nameOp = getOpNameDataFrom(utxo); + if (nameOp?.op == OpName.nameNew) { + final sKey = nameSaltKeyBuilder(utxo.txid, walletId, utxo.vout); + + final encoded = await secureStorageInterface.read(key: sKey); + if (encoded == null) { + // seems this NAME NEW was created elsewhere + continue; + } + + final data = decodeNameSaltData(encoded); + + if (data.name == name) { + return ( + data: null, + nameState: NameState.unavailable, + ); + } + } + } + + bool available = false; + + final nameScriptHash = nameIdentifierToScriptHash(name); + + final historyWithName = await electrumXClient.getHistory( + scripthash: nameScriptHash, + ); + OpNameData? opNameData; + if (historyWithName.isNotEmpty) { + final txHeight = historyWithName.last["height"] as int; + final txHash = historyWithName.last["tx_hash"] as String; + + final txMap = await electrumXCachedClient.getTransaction( + txHash: txHash, + cryptoCurrency: cryptoCurrency, + ); + + try { + opNameData = OpNameData.fromTx(txMap, txHeight); + final isExpired = opNameData.expired(await chainHeight); + + Logging.instance.i( + "Name $opNameData \nis expired = $isExpired", + ); + available = isExpired; + } catch (_) { + available = false; // probably + } + } else { + Logging.instance.i("Name \"$name\" not found."); + available = true; + } + + return ( + data: opNameData, + nameState: available ? NameState.available : NameState.unavailable, + ); + } + + // TODO: handle this differently? + final Set<(int, String)> _unknownNameNewOutputs = {}; + + /// Must be called in refresh() AFTER the wallet's UTXOs have been updated! + Future checkAutoRegisterNameNewOutputs() async { + Logging.instance.t( + "$walletId checkAutoRegisterNameNewOutputs()", + ); + try { + final currentHeight = await chainHeight; + // not ideal filtering + final utxos = await mainDB + .getUTXOs(walletId) + .filter() + .otherDataIsNotNull() + .and() + .blockHeightIsNotNull() + .and() + .blockHeightGreaterThan(0) + .and() + .blockHeightLessThan(currentHeight - kNameWaitBlocks) + .findAll(); + + Logging.instance.t( + "_unknownNameNewOutputs(count=${_unknownNameNewOutputs.length})" + ":\n$_unknownNameNewOutputs", + ); + + // check cache and remove known auto unspendable name new outputs + utxos.removeWhere( + (e) => _unknownNameNewOutputs.contains((e.vout, e.txid)), + ); + + for (final utxo in utxos) { + final nameOp = getOpNameDataFrom(utxo); + if (nameOp != null) { + Logging.instance.t( + "Found OpName: $nameOp\n\nIN UTXO: $utxo", + ); + + if (nameOp.op == OpName.nameNew) { + // at this point we should have an unspent UTXO that is at least + // 12 blocks old which we can now do nameFirstUpdate on + + //TODO: Should check if name was registered by someone else here + + final sKey = nameSaltKeyBuilder(utxo.txid, walletId, utxo.vout); + + final encoded = await secureStorageInterface.read(key: sKey); + if (encoded == null) { + Logging.instance.d( + "Found OpName NAME NEW utxo without local matching data." + "\nUTXO: $utxo" + "\nUnable to auto register.", + ); + _unknownNameNewOutputs.add((utxo.vout, utxo.txid)); + continue; + } + + final data = decodeNameSaltData(encoded); + + // verify cached matches + final myAddress = await mainDB.getAddress(walletId, utxo.address!); + final pk = await getPrivateKey(myAddress!); + final generatedSalt = scriptNameNew(data.name, pk.data).$2; + + // TODO replace assert with proper error + assert(generatedSalt == data.salt); + + final nameScriptHex = scriptNameFirstUpdate( + data.name, + data.value, + data.salt, + ); + + String noteName = + data.name.startsWith("d/") ? data.name.substring(2) : data.name; + if (!noteName.endsWith(".bit")) { + noteName += ".bit"; + } + + TxData txData = TxData( + utxos: {utxo}, + opNameState: NameOpState( + name: data.name, + saltHex: data.salt, + commitment: "n/a", + value: data.value, + nameScriptHex: nameScriptHex, + type: OpName.nameFirstUpdate, + output: utxo, + outputPosition: -1, //currently unknown, updated later + ), + note: "Purchase $noteName", + feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? + recipients: [ + ( + address: (await getCurrentReceivingAddress())!.value, + isChange: false, + amount: Amount( + rawValue: BigInt.from(kNameAmountSats), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ), + ], + ); + + // generate tx + txData = await prepareNameSend(txData: txData); + + // broadcast tx + txData = await confirmSend(txData: txData); + + // clear out value from local secure storage on successful registration + await secureStorageInterface.delete(key: sKey); + } + } + } + } catch (e, s) { + Logging.instance.e( + "checkAutoRegisterNameNewOutputs() failed", + error: e, + stackTrace: s, + ); + } + } + + /// Builds and signs a transaction + Future _createNameTx({ + required TxData txData, + required List utxoSigningData, + required bool isForFeeCalcPurposesOnly, + }) async { + Logging.instance.d("Starting _createNameTx ----------"); + + assert(txData.recipients!.where((e) => !e.isChange).length == 1); + + if (!isForFeeCalcPurposesOnly) { + final nameAmount = + txData.recipients!.where((e) => !e.isChange).first.amount; + + switch (txData.opNameState!.type) { + case OpName.nameNew: + assert( + nameAmount.raw == BigInt.from(kNameNewAmountSats), + ); + break; + case OpName.nameFirstUpdate || OpName.nameUpdate: + assert( + nameAmount.raw == BigInt.from(kNameAmountSats), + ); + break; + } + } + + // temp tx data to show in gui while waiting for real data from server + final List tempInputs = []; + final List tempOutputs = []; + + final List prevOuts = []; + + coinlib.Transaction clTx = coinlib.Transaction( + version: kNameTxVersion, + inputs: [], + outputs: [], + ); + + // TODO: [prio=high]: check this opt in rbf + final sequence = this is RbfInterface && (this as RbfInterface).flagOptInRBF + ? 0xffffffff - 10 + : 0xffffffff - 1; + + // Add transaction inputs + for (int i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + + final hash = Uint8List.fromList( + txid.toUint8ListFromHex.reversed.toList(), + ); + + final prevOutpoint = coinlib.OutPoint( + hash, + utxoSigningData[i].utxo.vout, + ); + + final prevOutput = coinlib.Output.fromAddress( + BigInt.from(utxoSigningData[i].utxo.value), + coinlib.Address.fromString( + utxoSigningData[i].utxo.address!, + cryptoCurrency.networkParams, + ), + ); + + prevOuts.add(prevOutput); + + final coinlib.Input input; + + switch (utxoSigningData[i].derivePathType) { + case DerivePathType.bip44: + input = coinlib.P2PKHInput( + prevOut: prevOutpoint, + publicKey: utxoSigningData[i].keyPair!.publicKey, + sequence: sequence, + ); + + // TODO: fix this as it is (probably) wrong! + case DerivePathType.bip49: + throw Exception("TODO p2sh"); + // input = coinlib.P2SHMultisigInput( + // prevOut: prevOutpoint, + // program: coinlib.MultisigProgram.decompile( + // utxoSigningData[i].redeemScript!, + // ), + // sequence: sequence, + // ); + + case DerivePathType.bip84: + input = coinlib.P2WPKHInput( + prevOut: prevOutpoint, + publicKey: utxoSigningData[i].keyPair!.publicKey, + sequence: sequence, + ); + + case DerivePathType.bip86: + input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); + + default: + throw UnsupportedError( + "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + ); + } + + clTx = clTx.addInput(input); + + tempInputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: input.scriptSig.toHex, + scriptSigAsm: null, + sequence: sequence, + outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: utxoSigningData[i].utxo.txid, + vout: utxoSigningData[i].utxo.vout, + ), + addresses: utxoSigningData[i].utxo.address == null + ? [] + : [utxoSigningData[i].utxo.address!], + valueStringSats: utxoSigningData[i].utxo.value.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ), + ); + } + + int? nameOpVoutIndex; + + int nonChangeCount = 0; // sanity check counter. Should only hit 1. + // Add transaction outputs + for (int i = 0; i < txData.recipients!.length; i++) { + final address = coinlib.Address.fromString( + normalizeAddress(txData.recipients![i].address), + cryptoCurrency.networkParams, + ); + + final coinlib.Output output; + + // there should only be 1 name output + if (!txData.recipients![i].isChange) { + nonChangeCount++; + if (nonChangeCount > 1) { + Logging.instance.d("Oddly formatted Name txData: $txData"); + throw Exception("Oddly formatted Name tx"); + } + final scriptPubKey = address.program.script.compiled; + output = coinlib.Output.fromScriptBytes( + txData.recipients![i].amount.raw, // should be 0.015 or 0.01 + Uint8List.fromList( + txData.opNameState!.nameScriptHex.toUint8ListFromHex + scriptPubKey, + ), + ); + // redundant sanity check + if (nameOpVoutIndex != null) { + throw Exception("More than one NAME OP output detected!"); + } + nameOpVoutIndex = i; + } else { + // change output + output = coinlib.Output.fromAddress( + txData.recipients![i].amount.raw, + address, + ); + } + + clTx = clTx.addOutput(output); + + tempOutputs.add( + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "000000", + valueStringSats: txData.recipients![i].amount.raw.toString(), + addresses: [ + txData.recipients![i].address.toString(), + ], + walletOwns: (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .valueEqualTo(txData.recipients![i].address) + .valueProperty() + .findFirst()) != + null, + ), + ); + } + + try { + // Sign the transaction accordingly + for (int i = 0; i < utxoSigningData.length; i++) { + final value = BigInt.from(utxoSigningData[i].utxo.value); + coinlib.ECPrivateKey key = utxoSigningData[i].keyPair!.privateKey; + + if (clTx.inputs[i] is coinlib.TaprootKeyInput) { + final taproot = coinlib.Taproot( + internalKey: utxoSigningData[i].keyPair!.publicKey, + ); + + key = taproot.tweakPrivateKey(key); + } + + clTx = clTx.sign( + inputN: i, + value: value, + key: key, + prevOuts: prevOuts, + ); + } + } catch (e, s) { + Logging.instance.e( + "Caught exception while signing transaction: ", + error: e, + stackTrace: s, + ); + rethrow; + } + + if (nameOpVoutIndex == null) { + throw Exception("No NAME OP output detected!"); + } + + return txData.copyWith( + raw: clTx.toHex(), + vSize: clTx.vSize(), + opNameState: txData.opNameState!.copyWith( + outputPosition: nameOpVoutIndex, + ), + tempTx: TransactionV2( + walletId: walletId, + blockHash: null, + hash: clTx.hashHex, + txid: clTx.txid, + height: null, + timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(tempInputs), + outputs: List.unmodifiable(tempOutputs), + version: clTx.version, + type: + tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) && + txData.paynymAccountLite == null + ? TransactionType.sentToSelf + : TransactionType.outgoing, + subType: TransactionSubType.none, + otherData: null, + ), + ); + } + + Future prepareNameSend({ + required TxData txData, + }) async { + try { + if (txData.amount == null) { + throw Exception("No recipients in attempted transaction!"); + } + + Logging.instance.t( + "prepareNameSend called with TxData:\n\n$txData", + ); + + final feeRateType = txData.feeRateType; + final customSatsPerVByte = txData.satsPerVByte; + final feeRateAmount = txData.feeRateAmount; + final utxos = txData.utxos; + + if (txData.note == null) { + txData = txData.copyWith( + note: "Name transaction ${txData.opNameState!.type.name}", + ); + } + + final bool coinControl = utxos != null; + + if (customSatsPerVByte != null) { + final result = await coinSelectionName( + txData: txData.copyWith(feeRateAmount: -1), + utxos: utxos?.toList(), + coinControl: coinControl, + ); + + Logging.instance.d("PREPARE NAME SEND RESULT: $result"); + + if (result.fee!.raw.toInt() < result.vSize!) { + throw Exception( + "Error in fee calculation: Transaction fee cannot be less than vSize", + ); + } + + return result; + } else if (feeRateType is FeeRateType || feeRateAmount is int) { + late final int rate; + if (feeRateType is FeeRateType) { + int fee = 0; + final feeObject = await fees; + switch (feeRateType) { + case FeeRateType.fast: + fee = feeObject.fast; + break; + case FeeRateType.average: + fee = feeObject.medium; + break; + case FeeRateType.slow: + fee = feeObject.slow; + break; + default: + throw ArgumentError("Invalid use of custom fee"); + } + rate = fee; + } else { + rate = feeRateAmount as int; + } + + final result = await coinSelectionName( + txData: txData.copyWith( + feeRateAmount: rate, + ), + utxos: utxos?.toList(), + coinControl: coinControl, + ); + + Logging.instance.d( + "prepare send: $result", + ); + if (result.fee!.raw.toInt() < result.vSize!) { + throw Exception( + "Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot " + "be less than vSize (${result.vSize})"); + } + + return result; + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from prepareNameSend(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + Future coinSelectionName({ + required TxData txData, + required bool coinControl, + int additionalOutputs = 0, + List? utxos, + }) async { + Logging.instance.d("Starting coinSelectionName ----------"); + + assert(txData.recipients!.length == 1); + + if (coinControl && utxos == null) { + throw Exception("Coin control used where utxos is null!"); + } + + if ((txData.opNameState!.type == OpName.nameFirstUpdate || + txData.opNameState!.type == OpName.nameUpdate) && + txData.opNameState!.output == null) { + throw Exception("Missing name output to update"); + } + + final recipientAddress = txData.recipients!.first.address; + final satoshiAmountToSend = txData.amount!.raw; + final int? satsPerVByte = txData.satsPerVByte; + final selectedTxFeeRate = txData.feeRateAmount!; + + final int expectedSatsValue; + switch (txData.opNameState!.type) { + case OpName.nameNew: + expectedSatsValue = kNameNewAmountSats; + break; + case OpName.nameFirstUpdate || OpName.nameUpdate: + expectedSatsValue = kNameAmountSats; + break; + } + + if (satoshiAmountToSend != BigInt.from(expectedSatsValue)) { + throw Exception( + "Invalid Name amount for ${txData.opNameState!.type}: ${txData.amount}", + ); + } + + final List availableOutputs = + utxos ?? await mainDB.getUTXOs(walletId).findAll(); + + if (txData.opNameState!.type == OpName.nameUpdate || + txData.opNameState!.type == OpName.nameFirstUpdate) { + // name output is added later + availableOutputs.removeWhere((e) => e == txData.opNameState!.output!); + } + + final currentChainHeight = await chainHeight; + + final canCPFP = this is CpfpInterface && coinControl; + + final spendableOutputs = availableOutputs + .where( + (e) => + !e.isBlocked && + (e.used != true) && + (canCPFP || + e.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + )), + ) + .toList(); + + if (coinControl) { + if (spendableOutputs.length < availableOutputs.length) { + throw ArgumentError("Attempted to use an unavailable utxo"); + } + // don't care about sorting if using all utxos + } else { + // sort spendable by age (oldest first) + spendableOutputs.sort( + (a, b) => (b.blockTime ?? currentChainHeight) + .compareTo((a.blockTime ?? currentChainHeight)), + ); + } + + // add name output to modify + if (txData.opNameState!.type == OpName.nameUpdate || + txData.opNameState!.type == OpName.nameFirstUpdate) { + spendableOutputs.insert(0, txData.opNameState!.output!); + } + + final spendableSatoshiValue = + spendableOutputs.fold(BigInt.zero, (p, e) => p + BigInt.from(e.value)); + + if (spendableSatoshiValue < satoshiAmountToSend) { + throw Exception("Insufficient balance"); + } else if (spendableSatoshiValue == satoshiAmountToSend) { + throw Exception("Insufficient balance to pay transaction fee"); + } + Logging.instance.d( + "spendableOutputs.length: ${spendableOutputs.length}" + "\navailableOutputs.length: ${availableOutputs.length}" + "\nspendableOutputs: $spendableOutputs" + "\nspendableSatoshiValue: $spendableSatoshiValue" + "\nsatoshiAmountToSend: $satoshiAmountToSend", + ); + + BigInt satoshisBeingUsed = BigInt.zero; + int inputsBeingConsumed = 0; + final List utxoObjectsToUse = []; + + if (!coinControl) { + for (int i = 0; + satoshisBeingUsed < satoshiAmountToSend && + i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += BigInt.from(spendableOutputs[i].value); + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && + inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += + BigInt.from(spendableOutputs[inputsBeingConsumed].value); + inputsBeingConsumed += 1; + } + } else { + satoshisBeingUsed = spendableSatoshiValue; + utxoObjectsToUse.addAll(spendableOutputs); + inputsBeingConsumed = spendableOutputs.length; + } + + Logging.instance.d( + "satoshisBeingUsed: $satoshisBeingUsed" + "\ninputsBeingConsumed: $inputsBeingConsumed" + "\nutxoObjectsToUse: $utxoObjectsToUse", + ); + + // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray + final List recipientsArray = [recipientAddress]; + final List recipientsAmtArray = [satoshiAmountToSend]; + + // gather required signing data + final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + + final int vSizeForOneOutput; + try { + vSizeForOneOutput = (await _createNameTx( + utxoSigningData: utxoSigningData, + isForFeeCalcPurposesOnly: true, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshisBeingUsed], + ), + ), + )) + .vSize!; + } catch (e, s) { + Logging.instance.e("vSizeForOneOutput: $e", error: e, stackTrace: s); + rethrow; + } + + final int vSizeForTwoOutPuts; + + BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b; + + try { + vSizeForTwoOutPuts = (await _createNameTx( + utxoSigningData: utxoSigningData, + isForFeeCalcPurposesOnly: true, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress, (await getCurrentChangeAddress())!.value], + [ + satoshiAmountToSend, + maxBI( + BigInt.zero, + satoshisBeingUsed - satoshiAmountToSend, + ), + ], + ), + ), + )) + .vSize!; + } catch (e, s) { + Logging.instance.e("vSizeForTwoOutPuts: $e", error: e, stackTrace: s); + rethrow; + } + + // Assume 1 output, only for recipient and no change + final feeForOneOutput = BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForOneOutput) + : estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ), + ); + // Assume 2 outputs, one for recipient and one for change + final feeForTwoOutputs = BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForTwoOutPuts) + : estimateTxFee( + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ), + ); + + Logging.instance.d( + "feeForTwoOutputs: $feeForTwoOutputs" + "\nfeeForOneOutput: $feeForOneOutput", + ); + + final difference = satoshisBeingUsed - satoshiAmountToSend; + + Future _singleOutputTxn() async { + Logging.instance.d( + 'Input size: $satoshisBeingUsed' + '\nRecipient output size: $satoshiAmountToSend' + '\nFee being paid: $difference sats' + '\nEstimated fee: $feeForOneOutput', + ); + final txnData = await _createNameTx( + isForFeeCalcPurposesOnly: false, + utxoSigningData: utxoSigningData, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + recipientsArray, + recipientsAmtArray, + ), + ), + ); + return txnData.copyWith( + fee: Amount( + rawValue: feeForOneOutput, + fractionDigits: cryptoCurrency.fractionDigits, + ), + usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + ); + } + + // no change output required + if (difference == feeForOneOutput) { + Logging.instance.d('1 output in tx'); + return await _singleOutputTxn(); + } else if (difference < feeForOneOutput) { + Logging.instance.w( + 'Cannot pay tx fee - checking for more outputs and trying again', + ); + // try adding more outputs + if (spendableOutputs.length > inputsBeingConsumed) { + return coinSelectionName( + txData: txData, + additionalOutputs: additionalOutputs + 1, + utxos: utxos, + coinControl: coinControl, + ); + } + throw Exception("Insufficient balance to pay transaction fee"); + } else { + if (difference > (feeForOneOutput + cryptoCurrency.dustLimit.raw)) { + final changeOutputSize = difference - feeForTwoOutputs; + // check if possible to add the change output + if (changeOutputSize > cryptoCurrency.dustLimit.raw && + difference - changeOutputSize == feeForTwoOutputs) { + // generate new change address if current change address has been used + await checkChangeAddressForTransactions(); + final String newChangeAddress = + (await getCurrentChangeAddress())!.value; + + BigInt feeBeingPaid = difference - changeOutputSize; + + // add change output + recipientsArray.add(newChangeAddress); + recipientsAmtArray.add(changeOutputSize); + + Logging.instance.d('2 outputs in tx' + '\nInput size: $satoshisBeingUsed' + '\nRecipient output size: $satoshiAmountToSend' + '\nChange Output Size: $changeOutputSize' + '\nDifference (fee being paid): $feeBeingPaid sats' + '\nEstimated fee: $feeForTwoOutputs'); + + TxData txnData = await _createNameTx( + utxoSigningData: utxoSigningData, + isForFeeCalcPurposesOnly: false, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + recipientsArray, + recipientsAmtArray, + ), + ), + ); + + // make sure minimum fee is accurate if that is being used + if (BigInt.from(txnData.vSize!) - feeBeingPaid == BigInt.one) { + final changeOutputSize = difference - BigInt.from(txnData.vSize!); + feeBeingPaid = difference - changeOutputSize; + recipientsAmtArray.removeLast(); + recipientsAmtArray.add(changeOutputSize); + + Logging.instance.d( + '\nAdjusted Input size: $satoshisBeingUsed' + '\nAdjusted Recipient output size: $satoshiAmountToSend' + '\nAdjusted Change Output Size: $changeOutputSize' + '\nAdjusted Difference (fee being paid): $feeBeingPaid sats' + '\nAdjusted Estimated fee: $feeForTwoOutputs', + ); + + txnData = await _createNameTx( + utxoSigningData: utxoSigningData, + isForFeeCalcPurposesOnly: false, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + recipientsArray, + recipientsAmtArray, + ), + ), + ); + } + + return txnData.copyWith( + fee: Amount( + rawValue: feeBeingPaid, + fractionDigits: cryptoCurrency.fractionDigits, + ), + usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + ); + } else { + // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize + // is smaller than or equal to cryptoCurrency.dustLimit. Revert to single output transaction. + Logging.instance.d( + 'Reverting to 1 output in tx', + ); + + return await _singleOutputTxn(); + } + } + } + + return txData; + } + + @override + bool ignoreUtxoInBalance(UTXO utxo) { + if (getOpNameDataFrom(utxo) != null) { + // ignore name outputs in balance calculation + return true; + } + return false; + } + + /// return null if utxo does not contain name op + OpNameData? getOpNameDataFrom(UTXO utxo) { + if (utxo.otherData == null) { + return null; + } + final otherData = jsonDecode(utxo.otherData!) as Map; + if (otherData[UTXOOtherDataKeys.nameOpData] != null) { + try { + final nameOp = OpNameData( + (jsonDecode(otherData[UTXOOtherDataKeys.nameOpData] as String) as Map) + .cast(), + utxo.blockHeight!, + ); + return nameOp; + } catch (e, s) { + Logging.instance.d( + "getOpNameDataFrom($utxo) failed", + error: e, + stackTrace: s, + ); + return null; + } + } + return null; + } + + bool checkUtxoConfirmed(UTXO utxo, int currentChainHeight) { + final isNameOpOutput = getOpNameDataFrom(utxo) != null; + + final confirmedStatus = utxo.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + overrideMinConfirms: isNameOpOutput ? kNameWaitBlocks : null, + ); + return confirmedStatus; + } +} + +enum NameState { + available, + unavailable; } diff --git a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart index 832ce3e13..777dda064 100644 --- a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart +++ b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart @@ -6,6 +6,7 @@ import 'package:isar/isar.dart'; import '../../../models/balance.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../models/keys/view_only_wallet_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; @@ -33,14 +34,19 @@ abstract class Bip39HDWallet extends Bip39Wallet return coinlib.HDPrivateKey.fromSeed(seed); } + Future getPrivateKey(Address address) async { + return (await getRootHDNode()) + .derivePath(address.derivationPath!.value) + .privateKey; + } + Future getPrivateKeyWIF(Address address) async { - final keys = - (await getRootHDNode()).derivePath(address.derivationPath!.value); + final privateKey = await getPrivateKey(address); final List data = [ cryptoCurrency.networkParams.wifPrefix, - ...keys.privateKey.data, - if (keys.privateKey.compressed) 1, + ...privateKey.data, + if (privateKey.compressed) 1, ]; final checksum = coinlib.sha256DoubleHash(Uint8List.fromList(data)).sublist(0, 4); @@ -189,6 +195,9 @@ abstract class Bip39HDWallet extends Bip39Wallet return address; } + /// If this function returns true, the UTXO will be ignored in displayed balance + bool ignoreUtxoInBalance(UTXO utxo) => false; + // ========== Private ======================================================== Future _viewOnlyPathHelper() async { @@ -324,6 +333,8 @@ abstract class Bip39HDWallet extends Bip39Wallet ); for (final utxo in utxos) { + if (ignoreUtxoInBalance(utxo)) continue; + final utxoAmount = Amount( rawValue: BigInt.from(utxo.value), fractionDigits: cryptoCurrency.fractionDigits, diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 36cec7b85..161c68f5f 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -663,17 +663,21 @@ abstract class Wallet { await (this as SparkInterface).refreshSparkData((0.3, 0.6)); } - final fetchFuture = updateTransactions(); - - _fireRefreshPercentChange(0.6); - final utxosRefreshFuture = updateUTXOs(); - // if (currentHeight != storedHeight) { - _fireRefreshPercentChange(0.65); - - await utxosRefreshFuture; - _fireRefreshPercentChange(0.70); - - await fetchFuture; + if (this is NamecoinWallet) { + await updateUTXOs(); + _fireRefreshPercentChange(0.6); + await (this as NamecoinWallet).checkAutoRegisterNameNewOutputs(); + _fireRefreshPercentChange(0.70); + await updateTransactions(); + } else { + final fetchFuture = updateTransactions(); + _fireRefreshPercentChange(0.6); + final utxosRefreshFuture = updateUTXOs(); + _fireRefreshPercentChange(0.65); + await utxosRefreshFuture; + _fireRefreshPercentChange(0.70); + await fetchFuture; + } // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. if (!viewOnly && this is PaynymInterface && codesToCheck.isNotEmpty) { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index d4b387b8d..2660d60ff 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -74,7 +74,7 @@ mixin ElectrumXInterface } Future> - _helperRecipientsConvert( + helperRecipientsConvert( List addrs, List satValues, ) async { @@ -248,7 +248,7 @@ mixin ElectrumXInterface vSizeForOneOutput = (await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( [recipientAddress], [satoshisBeingUsed - BigInt.one], ), @@ -268,7 +268,7 @@ mixin ElectrumXInterface vSizeForTwoOutPuts = (await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( [recipientAddress, (await getCurrentChangeAddress())!.value], [ satoshiAmountToSend, @@ -330,7 +330,7 @@ mixin ElectrumXInterface final txnData = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), @@ -392,7 +392,7 @@ mixin ElectrumXInterface TxData txnData = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), @@ -425,7 +425,7 @@ mixin ElectrumXInterface txnData = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), @@ -474,7 +474,7 @@ mixin ElectrumXInterface final int vSizeForOneOutput = (await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( [recipientAddress], [satoshisBeingUsed - BigInt.one], ), @@ -511,7 +511,7 @@ mixin ElectrumXInterface final data = await buildTransaction( txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( [recipientAddress], [amount], ), @@ -1155,8 +1155,6 @@ mixin ElectrumXInterface } } - /// The optional (nullable) param [checkBlock] is a callback that can be used - /// to check if a utxo should be marked as blocked Future parseUTXO({ required Map jsonUTXO, }) async { @@ -1474,7 +1472,7 @@ mixin ElectrumXInterface } // receiving addresses - Logging.instance.e( + Logging.instance.i( "checking receiving addresses...", ); @@ -1685,8 +1683,11 @@ mixin ElectrumXInterface return await mainDB.updateUTXOs(walletId, outputArray); } catch (e, s) { - Logging.instance - .e("Output fetch unsuccessful: ", error: e, stackTrace: s); + Logging.instance.e( + "Output fetch unsuccessful: ", + error: e, + stackTrace: s, + ); return false; } } @@ -2002,7 +2003,7 @@ mixin ElectrumXInterface if (root != null) { // receiving addresses - Logging.instance.d( + Logging.instance.i( "checking receiving addresses...", ); diff --git a/lib/widgets/desktop/desktop_fee_dialog.dart b/lib/widgets/desktop/desktop_fee_dialog.dart index 322fbb876..713aaa109 100644 --- a/lib/widgets/desktop/desktop_fee_dialog.dart +++ b/lib/widgets/desktop/desktop_fee_dialog.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:cs_monero/cs_monero.dart' as lib_monero; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/models.dart'; diff --git a/lib/widgets/desktop/desktop_scaffold.dart b/lib/widgets/desktop/desktop_scaffold.dart index 25b18963b..920c66306 100644 --- a/lib/widgets/desktop/desktop_scaffold.dart +++ b/lib/widgets/desktop/desktop_scaffold.dart @@ -9,6 +9,7 @@ */ import 'package:flutter/material.dart'; + import '../../themes/stack_colors.dart'; import '../background.dart'; @@ -70,8 +71,7 @@ class MasterScaffold extends StatelessWidget { } else { return Background( child: Scaffold( - backgroundColor: background ?? - Theme.of(context).extension()!.background, + backgroundColor: background ?? Colors.transparent, appBar: appBar as PreferredSizeWidget?, body: body, ), diff --git a/lib/widgets/dialogs/s_dialog.dart b/lib/widgets/dialogs/s_dialog.dart new file mode 100644 index 000000000..a6b32148c --- /dev/null +++ b/lib/widgets/dialogs/s_dialog.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import '../../themes/stack_colors.dart'; +import '../../utilities/util.dart'; +import '../conditional_parent.dart'; + +class SDialog extends StatelessWidget { + const SDialog({ + super.key, + required this.child, + this.padding = EdgeInsets.zero, + this.contentCanScroll = true, + this.margin, + this.background, + this.mainAxisAlignment, + this.crossAxisAlignment, + }); + + final Widget child; + final bool contentCanScroll; + final Color? background; + final EdgeInsets? margin; + final EdgeInsets padding; + final MainAxisAlignment? mainAxisAlignment; + final CrossAxisAlignment? crossAxisAlignment; + + @override + Widget build(BuildContext context) { + return Padding( + padding: margin ?? EdgeInsets.all(Util.isDesktop ? 32 : 16), + child: Column( + mainAxisAlignment: mainAxisAlignment ?? + (Util.isDesktop ? MainAxisAlignment.center : MainAxisAlignment.end), + crossAxisAlignment: crossAxisAlignment ?? CrossAxisAlignment.center, + children: [ + Flexible( + child: Material( + borderRadius: BorderRadius.circular(20), + child: Container( + decoration: BoxDecoration( + color: background ?? + Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + ), + child: ConditionalParent( + condition: contentCanScroll, + builder: (child) => SingleChildScrollView( + child: child, + ), + child: Padding( + padding: padding, + child: child, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 590466016..233e824f8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1385,6 +1385,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + namecoin: + dependency: "direct main" + description: + path: "." + ref: "819b21164ef93cc0889049d4a8a1be2d0cc36a1b" + resolved-ref: "819b21164ef93cc0889049d4a8a1be2d0cc36a1b" + url: "https://github.com/Cyrix126/namecoin_dart" + source: git + version: "2.0.0" nanodart: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 5a2b8e6cd..145e191ed 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -214,6 +214,10 @@ dependencies: git: url: https://github.com/cypherstack/logger ref: 3c0cba27868ebb5c7d65ebc30a8e6e5342186692 + namecoin: + git: + url: https://github.com/Cyrix126/namecoin_dart + ref: 819b21164ef93cc0889049d4a8a1be2d0cc36a1b dev_dependencies: flutter_test: diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index a5c7c4c54..4ac0b9849 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -4,14 +4,15 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i9; -import 'dart:ui' as _i14; +import 'dart:ui' as _i15; import 'package:decimal/decimal.dart' as _i4; +import 'package:logger/logger.dart' as _i13; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i8; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i6; import 'package:stackwallet/models/electrumx_response/spark_models.dart' as _i3; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i13; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i14; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i12; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i11; import 'package:stackwallet/utilities/prefs.dart' as _i10; @@ -1130,6 +1131,45 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i13.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i13.Level.all, + ) as _i13.Level); + + @override + set logLevel(_i13.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -1186,18 +1226,18 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { ) as _i9.Future); @override - _i13.AmountUnit amountUnit(_i2.CryptoCurrency? coin) => (super.noSuchMethod( + _i14.AmountUnit amountUnit(_i2.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i13.AmountUnit.normal, - ) as _i13.AmountUnit); + returnValue: _i14.AmountUnit.normal, + ) as _i14.AmountUnit); @override void updateAmountUnit({ required _i2.CryptoCurrency? coin, - required _i13.AmountUnit? amountUnit, + required _i14.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -1270,7 +1310,7 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { ); @override - void addListener(_i14.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i15.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1279,7 +1319,7 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { ); @override - void removeListener(_i14.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i15.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index fee6a0d7f..476ab3883 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -7,6 +7,7 @@ import 'dart:async' as _i10; import 'dart:typed_data' as _i19; import 'dart:ui' as _i14; +import 'package:logger/logger.dart' as _i22; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i16; import 'package:stackwallet/db/isar/main_db.dart' as _i3; @@ -17,7 +18,7 @@ import 'package:stackwallet/services/locale_service.dart' as _i15; import 'package:stackwallet/services/node_service.dart' as _i2; import 'package:stackwallet/services/wallets.dart' as _i9; import 'package:stackwallet/themes/theme_service.dart' as _i17; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i22; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i23; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i21; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i20; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -1128,6 +1129,45 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i22.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i22.Level.all, + ) as _i22.Level); + + @override + set logLevel(_i22.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -1184,18 +1224,18 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ) as _i10.Future); @override - _i22.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + _i23.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i22.AmountUnit.normal, - ) as _i22.AmountUnit); + returnValue: _i23.AmountUnit.normal, + ) as _i23.AmountUnit); @override void updateAmountUnit({ required _i4.CryptoCurrency? coin, - required _i22.AmountUnit? amountUnit, + required _i23.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index b7a8d49d5..565f05f82 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -3,40 +3,41 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i9; -import 'dart:ui' as _i12; +import 'dart:async' as _i10; +import 'dart:ui' as _i13; -import 'package:decimal/decimal.dart' as _i18; +import 'package:decimal/decimal.dart' as _i19; +import 'package:logger/logger.dart' as _i9; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i7; import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart' - as _i21; + as _i22; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart' - as _i23; -import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart' as _i24; +import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart' + as _i25; import 'package:stackwallet/models/exchange/response_objects/estimate.dart' - as _i20; + as _i21; import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart' - as _i22; + as _i23; import 'package:stackwallet/models/exchange/response_objects/range.dart' - as _i19; + as _i20; import 'package:stackwallet/models/exchange/response_objects/trade.dart' - as _i14; -import 'package:stackwallet/models/isar/exchange_cache/currency.dart' as _i17; -import 'package:stackwallet/models/isar/exchange_cache/pair.dart' as _i25; + as _i15; +import 'package:stackwallet/models/isar/exchange_cache/currency.dart' as _i18; +import 'package:stackwallet/models/isar/exchange_cache/pair.dart' as _i26; import 'package:stackwallet/networking/http.dart' as _i3; import 'package:stackwallet/services/exchange/change_now/change_now_api.dart' - as _i16; + as _i17; import 'package:stackwallet/services/exchange/exchange_response.dart' as _i4; -import 'package:stackwallet/services/trade_notes_service.dart' as _i15; -import 'package:stackwallet/services/trade_service.dart' as _i13; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i10; +import 'package:stackwallet/services/trade_notes_service.dart' as _i16; +import 'package:stackwallet/services/trade_service.dart' as _i14; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i11; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i8; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i6; import 'package:stackwallet/utilities/prefs.dart' as _i5; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' - as _i11; + as _i12; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart' as _i2; @@ -557,6 +558,45 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i9.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i9.Level.all, + ) as _i9.Level); + + @override + set logLevel(_i9.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -564,67 +604,67 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ) as bool); @override - _i9.Future init() => (super.noSuchMethod( + _i10.Future init() => (super.noSuchMethod( Invocation.method( #init, [], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( + _i10.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( Invocation.method( #incrementCurrentNotificationIndex, [], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future isExternalCallsSet() => (super.noSuchMethod( + _i10.Future isExternalCallsSet() => (super.noSuchMethod( Invocation.method( #isExternalCallsSet, [], ), - returnValue: _i9.Future.value(false), - ) as _i9.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i9.Future saveUserID(String? userId) => (super.noSuchMethod( + _i10.Future saveUserID(String? userId) => (super.noSuchMethod( Invocation.method( #saveUserID, [userId], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( + _i10.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( Invocation.method( #saveSignupEpoch, [signupEpoch], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.AmountUnit amountUnit(_i11.CryptoCurrency? coin) => (super.noSuchMethod( + _i11.AmountUnit amountUnit(_i12.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i10.AmountUnit.normal, - ) as _i10.AmountUnit); + returnValue: _i11.AmountUnit.normal, + ) as _i11.AmountUnit); @override void updateAmountUnit({ - required _i11.CryptoCurrency? coin, - required _i10.AmountUnit? amountUnit, + required _i12.CryptoCurrency? coin, + required _i11.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -639,7 +679,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ); @override - int maxDecimals(_i11.CryptoCurrency? coin) => (super.noSuchMethod( + int maxDecimals(_i12.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #maxDecimals, [coin], @@ -649,7 +689,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { @override void updateMaxDecimals({ - required _i11.CryptoCurrency? coin, + required _i12.CryptoCurrency? coin, required int? maxDecimals, }) => super.noSuchMethod( @@ -665,7 +705,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ); @override - _i2.FusionInfo getFusionServerInfo(_i11.CryptoCurrency? coin) => + _i2.FusionInfo getFusionServerInfo(_i12.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #getFusionServerInfo, @@ -682,7 +722,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { @override void setFusionServerInfo( - _i11.CryptoCurrency? coin, + _i12.CryptoCurrency? coin, _i2.FusionInfo? fusionServerInfo, ) => super.noSuchMethod( @@ -697,7 +737,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ); @override - void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -706,7 +746,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ); @override - void removeListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -736,16 +776,16 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { /// A class which mocks [TradesService]. /// /// See the documentation for Mockito's code generation for more information. -class MockTradesService extends _i1.Mock implements _i13.TradesService { +class MockTradesService extends _i1.Mock implements _i14.TradesService { MockTradesService() { _i1.throwOnMissingStub(this); } @override - List<_i14.Trade> get trades => (super.noSuchMethod( + List<_i15.Trade> get trades => (super.noSuchMethod( Invocation.getter(#trades), - returnValue: <_i14.Trade>[], - ) as List<_i14.Trade>); + returnValue: <_i15.Trade>[], + ) as List<_i15.Trade>); @override bool get hasListeners => (super.noSuchMethod( @@ -754,14 +794,14 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { ) as bool); @override - _i14.Trade? get(String? tradeId) => (super.noSuchMethod(Invocation.method( + _i15.Trade? get(String? tradeId) => (super.noSuchMethod(Invocation.method( #get, [tradeId], - )) as _i14.Trade?); + )) as _i15.Trade?); @override - _i9.Future add({ - required _i14.Trade? trade, + _i10.Future add({ + required _i15.Trade? trade, required bool? shouldNotifyListeners, }) => (super.noSuchMethod( @@ -773,13 +813,13 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { #shouldNotifyListeners: shouldNotifyListeners, }, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future edit({ - required _i14.Trade? trade, + _i10.Future edit({ + required _i15.Trade? trade, required bool? shouldNotifyListeners, }) => (super.noSuchMethod( @@ -791,13 +831,13 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { #shouldNotifyListeners: shouldNotifyListeners, }, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future delete({ - required _i14.Trade? trade, + _i10.Future delete({ + required _i15.Trade? trade, required bool? shouldNotifyListeners, }) => (super.noSuchMethod( @@ -809,12 +849,12 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { #shouldNotifyListeners: shouldNotifyListeners, }, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future deleteByUuid({ + _i10.Future deleteByUuid({ required String? uuid, required bool? shouldNotifyListeners, }) => @@ -827,12 +867,12 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { #shouldNotifyListeners: shouldNotifyListeners, }, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -841,7 +881,7 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { ); @override - void removeListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -871,7 +911,7 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { /// A class which mocks [TradeNotesService]. /// /// See the documentation for Mockito's code generation for more information. -class MockTradeNotesService extends _i1.Mock implements _i15.TradeNotesService { +class MockTradeNotesService extends _i1.Mock implements _i16.TradeNotesService { MockTradeNotesService() { _i1.throwOnMissingStub(this); } @@ -906,7 +946,7 @@ class MockTradeNotesService extends _i1.Mock implements _i15.TradeNotesService { ) as String); @override - _i9.Future set({ + _i10.Future set({ required String? tradeId, required String? note, }) => @@ -919,23 +959,23 @@ class MockTradeNotesService extends _i1.Mock implements _i15.TradeNotesService { #note: note, }, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future delete({required String? tradeId}) => (super.noSuchMethod( + _i10.Future delete({required String? tradeId}) => (super.noSuchMethod( Invocation.method( #delete, [], {#tradeId: tradeId}, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -944,7 +984,7 @@ class MockTradeNotesService extends _i1.Mock implements _i15.TradeNotesService { ); @override - void removeListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -974,7 +1014,7 @@ class MockTradeNotesService extends _i1.Mock implements _i15.TradeNotesService { /// A class which mocks [ChangeNowAPI]. /// /// See the documentation for Mockito's code generation for more information. -class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { +class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { MockChangeNowAPI() { _i1.throwOnMissingStub(this); } @@ -989,54 +1029,55 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { ) as _i3.HTTP); @override - _i9.Future<_i4.ExchangeResponse>> getAvailableCurrencies({ + _i10.Future<_i4.ExchangeResponse>> + getAvailableCurrencies({ bool? fixedRate, bool? active, }) => - (super.noSuchMethod( - Invocation.method( - #getAvailableCurrencies, - [], - { - #fixedRate: fixedRate, - #active: active, - }, - ), - returnValue: - _i9.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( - this, - Invocation.method( - #getAvailableCurrencies, - [], - { - #fixedRate: fixedRate, - #active: active, - }, - ), - )), - ) as _i9.Future<_i4.ExchangeResponse>>); + (super.noSuchMethod( + Invocation.method( + #getAvailableCurrencies, + [], + { + #fixedRate: fixedRate, + #active: active, + }, + ), + returnValue: + _i10.Future<_i4.ExchangeResponse>>.value( + _FakeExchangeResponse_2>( + this, + Invocation.method( + #getAvailableCurrencies, + [], + { + #fixedRate: fixedRate, + #active: active, + }, + ), + )), + ) as _i10.Future<_i4.ExchangeResponse>>); @override - _i9.Future<_i4.ExchangeResponse>> getCurrenciesV2() => + _i10.Future<_i4.ExchangeResponse>> getCurrenciesV2() => (super.noSuchMethod( Invocation.method( #getCurrenciesV2, [], ), returnValue: - _i9.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( + _i10.Future<_i4.ExchangeResponse>>.value( + _FakeExchangeResponse_2>( this, Invocation.method( #getCurrenciesV2, [], ), )), - ) as _i9.Future<_i4.ExchangeResponse>>); + ) as _i10.Future<_i4.ExchangeResponse>>); @override - _i9.Future<_i4.ExchangeResponse>> getPairedCurrencies({ + _i10.Future<_i4.ExchangeResponse>> getPairedCurrencies({ required String? ticker, bool? fixedRate, }) => @@ -1050,8 +1091,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), returnValue: - _i9.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( + _i10.Future<_i4.ExchangeResponse>>.value( + _FakeExchangeResponse_2>( this, Invocation.method( #getPairedCurrencies, @@ -1062,10 +1103,10 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse>>); + ) as _i10.Future<_i4.ExchangeResponse>>); @override - _i9.Future<_i4.ExchangeResponse<_i18.Decimal>> getMinimalExchangeAmount({ + _i10.Future<_i4.ExchangeResponse<_i19.Decimal>> getMinimalExchangeAmount({ required String? fromTicker, required String? toTicker, String? apiKey, @@ -1080,8 +1121,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9.Future<_i4.ExchangeResponse<_i18.Decimal>>.value( - _FakeExchangeResponse_2<_i18.Decimal>( + returnValue: _i10.Future<_i4.ExchangeResponse<_i19.Decimal>>.value( + _FakeExchangeResponse_2<_i19.Decimal>( this, Invocation.method( #getMinimalExchangeAmount, @@ -1093,10 +1134,10 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i18.Decimal>>); + ) as _i10.Future<_i4.ExchangeResponse<_i19.Decimal>>); @override - _i9.Future<_i4.ExchangeResponse<_i19.Range>> getRange({ + _i10.Future<_i4.ExchangeResponse<_i20.Range>> getRange({ required String? fromTicker, required String? toTicker, required bool? isFixedRate, @@ -1113,8 +1154,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9.Future<_i4.ExchangeResponse<_i19.Range>>.value( - _FakeExchangeResponse_2<_i19.Range>( + returnValue: _i10.Future<_i4.ExchangeResponse<_i20.Range>>.value( + _FakeExchangeResponse_2<_i20.Range>( this, Invocation.method( #getRange, @@ -1127,13 +1168,13 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i19.Range>>); + ) as _i10.Future<_i4.ExchangeResponse<_i20.Range>>); @override - _i9.Future<_i4.ExchangeResponse<_i20.Estimate>> getEstimatedExchangeAmount({ + _i10.Future<_i4.ExchangeResponse<_i21.Estimate>> getEstimatedExchangeAmount({ required String? fromTicker, required String? toTicker, - required _i18.Decimal? fromAmount, + required _i19.Decimal? fromAmount, String? apiKey, }) => (super.noSuchMethod( @@ -1147,8 +1188,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9.Future<_i4.ExchangeResponse<_i20.Estimate>>.value( - _FakeExchangeResponse_2<_i20.Estimate>( + returnValue: _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>.value( + _FakeExchangeResponse_2<_i21.Estimate>( this, Invocation.method( #getEstimatedExchangeAmount, @@ -1161,14 +1202,14 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i20.Estimate>>); + ) as _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>); @override - _i9.Future<_i4.ExchangeResponse<_i20.Estimate>> + _i10.Future<_i4.ExchangeResponse<_i21.Estimate>> getEstimatedExchangeAmountFixedRate({ required String? fromTicker, required String? toTicker, - required _i18.Decimal? fromAmount, + required _i19.Decimal? fromAmount, required bool? reversed, bool? useRateId = true, String? apiKey, @@ -1186,8 +1227,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9.Future<_i4.ExchangeResponse<_i20.Estimate>>.value( - _FakeExchangeResponse_2<_i20.Estimate>( + returnValue: _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>.value( + _FakeExchangeResponse_2<_i21.Estimate>( this, Invocation.method( #getEstimatedExchangeAmountFixedRate, @@ -1202,18 +1243,18 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i20.Estimate>>); + ) as _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>); @override - _i9.Future<_i4.ExchangeResponse<_i21.CNExchangeEstimate>> + _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>> getEstimatedExchangeAmountV2({ required String? fromTicker, required String? toTicker, - required _i21.CNEstimateType? fromOrTo, - required _i18.Decimal? amount, + required _i22.CNEstimateType? fromOrTo, + required _i19.Decimal? amount, String? fromNetwork, String? toNetwork, - _i21.CNFlowType? flow = _i21.CNFlowType.standard, + _i22.CNFlowType? flow = _i22.CNFlowType.standard, String? apiKey, }) => (super.noSuchMethod( @@ -1231,9 +1272,9 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: - _i9.Future<_i4.ExchangeResponse<_i21.CNExchangeEstimate>>.value( - _FakeExchangeResponse_2<_i21.CNExchangeEstimate>( + returnValue: _i10 + .Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>>.value( + _FakeExchangeResponse_2<_i22.CNExchangeEstimate>( this, Invocation.method( #getEstimatedExchangeAmountV2, @@ -1250,19 +1291,19 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i21.CNExchangeEstimate>>); + ) as _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>>); @override - _i9.Future<_i4.ExchangeResponse>> + _i10.Future<_i4.ExchangeResponse>> getAvailableFixedRateMarkets({String? apiKey}) => (super.noSuchMethod( Invocation.method( #getAvailableFixedRateMarkets, [], {#apiKey: apiKey}, ), - returnValue: _i9 - .Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( + returnValue: _i10 + .Future<_i4.ExchangeResponse>>.value( + _FakeExchangeResponse_2>( this, Invocation.method( #getAvailableFixedRateMarkets, @@ -1270,15 +1311,15 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { {#apiKey: apiKey}, ), )), - ) as _i9.Future<_i4.ExchangeResponse>>); + ) as _i10.Future<_i4.ExchangeResponse>>); @override - _i9.Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>> + _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>> createStandardExchangeTransaction({ required String? fromTicker, required String? toTicker, required String? receivingAddress, - required _i18.Decimal? amount, + required _i19.Decimal? amount, String? extraId = r'', String? userId = r'', String? contactEmail = r'', @@ -1303,9 +1344,9 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9 - .Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>>.value( - _FakeExchangeResponse_2<_i23.ExchangeTransaction>( + returnValue: _i10 + .Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>.value( + _FakeExchangeResponse_2<_i24.ExchangeTransaction>( this, Invocation.method( #createStandardExchangeTransaction, @@ -1324,15 +1365,15 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>>); + ) as _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>); @override - _i9.Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>> + _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>> createFixedRateExchangeTransaction({ required String? fromTicker, required String? toTicker, required String? receivingAddress, - required _i18.Decimal? amount, + required _i19.Decimal? amount, required String? rateId, required bool? reversed, String? extraId = r'', @@ -1361,9 +1402,9 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9 - .Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>>.value( - _FakeExchangeResponse_2<_i23.ExchangeTransaction>( + returnValue: _i10 + .Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>.value( + _FakeExchangeResponse_2<_i24.ExchangeTransaction>( this, Invocation.method( #createFixedRateExchangeTransaction, @@ -1384,12 +1425,12 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>>); + ) as _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>); @override - _i9.Future< + _i10.Future< _i4 - .ExchangeResponse<_i24.ExchangeTransactionStatus>> getTransactionStatus({ + .ExchangeResponse<_i25.ExchangeTransactionStatus>> getTransactionStatus({ required String? id, String? apiKey, }) => @@ -1402,9 +1443,9 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9 - .Future<_i4.ExchangeResponse<_i24.ExchangeTransactionStatus>>.value( - _FakeExchangeResponse_2<_i24.ExchangeTransactionStatus>( + returnValue: _i10 + .Future<_i4.ExchangeResponse<_i25.ExchangeTransactionStatus>>.value( + _FakeExchangeResponse_2<_i25.ExchangeTransactionStatus>( this, Invocation.method( #getTransactionStatus, @@ -1415,10 +1456,10 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i24.ExchangeTransactionStatus>>); + ) as _i10.Future<_i4.ExchangeResponse<_i25.ExchangeTransactionStatus>>); @override - _i9.Future<_i4.ExchangeResponse>> + _i10.Future<_i4.ExchangeResponse>> getAvailableFloatingRatePairs({bool? includePartners = false}) => (super.noSuchMethod( Invocation.method( @@ -1427,8 +1468,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { {#includePartners: includePartners}, ), returnValue: - _i9.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( + _i10.Future<_i4.ExchangeResponse>>.value( + _FakeExchangeResponse_2>( this, Invocation.method( #getAvailableFloatingRatePairs, @@ -1436,5 +1477,5 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { {#includePartners: includePartners}, ), )), - ) as _i9.Future<_i4.ExchangeResponse>>); + ) as _i10.Future<_i4.ExchangeResponse>>); } diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index c2bc00d2c..218b9c0c5 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -5,19 +5,20 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i10; import 'dart:typed_data' as _i15; -import 'dart:ui' as _i20; +import 'dart:ui' as _i21; +import 'package:logger/logger.dart' as _i19; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i17; import 'package:stackwallet/db/isar/main_db.dart' as _i3; import 'package:stackwallet/models/isar/stack_theme.dart' as _i14; -import 'package:stackwallet/models/node_model.dart' as _i22; +import 'package:stackwallet/models/node_model.dart' as _i23; import 'package:stackwallet/networking/http.dart' as _i6; -import 'package:stackwallet/services/locale_service.dart' as _i21; +import 'package:stackwallet/services/locale_service.dart' as _i22; import 'package:stackwallet/services/node_service.dart' as _i2; import 'package:stackwallet/services/wallets.dart' as _i9; import 'package:stackwallet/themes/theme_service.dart' as _i13; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i19; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i20; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i18; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i16; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -833,6 +834,45 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i19.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i19.Level.all, + ) as _i19.Level); + + @override + set logLevel(_i19.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -889,18 +929,18 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ) as _i10.Future); @override - _i19.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + _i20.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i19.AmountUnit.normal, - ) as _i19.AmountUnit); + returnValue: _i20.AmountUnit.normal, + ) as _i20.AmountUnit); @override void updateAmountUnit({ required _i4.CryptoCurrency? coin, - required _i19.AmountUnit? amountUnit, + required _i20.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -973,7 +1013,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - void addListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -982,7 +1022,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - void removeListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1012,7 +1052,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { /// A class which mocks [LocaleService]. /// /// See the documentation for Mockito's code generation for more information. -class MockLocaleService extends _i1.Mock implements _i21.LocaleService { +class MockLocaleService extends _i1.Mock implements _i22.LocaleService { MockLocaleService() { _i1.throwOnMissingStub(this); } @@ -1044,7 +1084,7 @@ class MockLocaleService extends _i1.Mock implements _i21.LocaleService { ) as _i10.Future); @override - void addListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1053,7 +1093,7 @@ class MockLocaleService extends _i1.Mock implements _i21.LocaleService { ); @override - void removeListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1098,16 +1138,16 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i8.SecureStorageInterface); @override - List<_i22.NodeModel> get primaryNodes => (super.noSuchMethod( + List<_i23.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), - returnValue: <_i22.NodeModel>[], - ) as List<_i22.NodeModel>); + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override - List<_i22.NodeModel> get nodes => (super.noSuchMethod( + List<_i23.NodeModel> get nodes => (super.noSuchMethod( Invocation.getter(#nodes), - returnValue: <_i22.NodeModel>[], - ) as List<_i22.NodeModel>); + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override bool get hasListeners => (super.noSuchMethod( @@ -1128,7 +1168,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override _i10.Future setPrimaryNodeFor({ required _i4.CryptoCurrency? coin, - required _i22.NodeModel? node, + required _i23.NodeModel? node, bool? shouldNotifyListeners = false, }) => (super.noSuchMethod( @@ -1146,33 +1186,33 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i10.Future); @override - _i22.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => + _i23.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => (super.noSuchMethod(Invocation.method( #getPrimaryNodeFor, [], {#currency: currency}, - )) as _i22.NodeModel?); + )) as _i23.NodeModel?); @override - List<_i22.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => + List<_i23.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #getNodesFor, [coin], ), - returnValue: <_i22.NodeModel>[], - ) as List<_i22.NodeModel>); + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override - _i22.NodeModel? getNodeById({required String? id}) => + _i23.NodeModel? getNodeById({required String? id}) => (super.noSuchMethod(Invocation.method( #getNodeById, [], {#id: id}, - )) as _i22.NodeModel?); + )) as _i23.NodeModel?); @override - List<_i22.NodeModel> failoverNodesFor( + List<_i23.NodeModel> failoverNodesFor( {required _i4.CryptoCurrency? currency}) => (super.noSuchMethod( Invocation.method( @@ -1180,12 +1220,12 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { [], {#currency: currency}, ), - returnValue: <_i22.NodeModel>[], - ) as List<_i22.NodeModel>); + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override _i10.Future add( - _i22.NodeModel? node, + _i23.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => @@ -1240,7 +1280,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override _i10.Future edit( - _i22.NodeModel? editedNode, + _i23.NodeModel? editedNode, String? password, bool? shouldNotifyListeners, ) => @@ -1268,7 +1308,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i10.Future); @override - void addListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1277,7 +1317,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ); @override - void removeListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index 5e878015f..cf7098de6 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -5,18 +5,19 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i10; import 'dart:io' as _i8; -import 'dart:ui' as _i17; +import 'dart:ui' as _i18; +import 'package:logger/logger.dart' as _i16; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i14; import 'package:stackwallet/db/isar/main_db.dart' as _i3; -import 'package:stackwallet/models/node_model.dart' as _i18; +import 'package:stackwallet/models/node_model.dart' as _i19; import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart' - as _i20; + as _i21; import 'package:stackwallet/services/node_service.dart' as _i2; -import 'package:stackwallet/services/tor_service.dart' as _i19; +import 'package:stackwallet/services/tor_service.dart' as _i20; import 'package:stackwallet/services/wallets.dart' as _i9; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i16; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i17; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i15; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i13; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -28,7 +29,7 @@ import 'package:stackwallet/wallets/isar/models/wallet_info.dart' as _i11; import 'package:stackwallet/wallets/wallet/wallet.dart' as _i5; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart' as _i6; -import 'package:tor_ffi_plugin/tor_ffi_plugin.dart' as _i21; +import 'package:tor_ffi_plugin/tor_ffi_plugin.dart' as _i22; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -708,6 +709,45 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i16.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i16.Level.all, + ) as _i16.Level); + + @override + set logLevel(_i16.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -764,18 +804,18 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ) as _i10.Future); @override - _i16.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + _i17.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i16.AmountUnit.normal, - ) as _i16.AmountUnit); + returnValue: _i17.AmountUnit.normal, + ) as _i17.AmountUnit); @override void updateAmountUnit({ required _i4.CryptoCurrency? coin, - required _i16.AmountUnit? amountUnit, + required _i17.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -848,7 +888,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - void addListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -857,7 +897,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - void removeListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -902,16 +942,16 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i7.SecureStorageInterface); @override - List<_i18.NodeModel> get primaryNodes => (super.noSuchMethod( + List<_i19.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), - returnValue: <_i18.NodeModel>[], - ) as List<_i18.NodeModel>); + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override - List<_i18.NodeModel> get nodes => (super.noSuchMethod( + List<_i19.NodeModel> get nodes => (super.noSuchMethod( Invocation.getter(#nodes), - returnValue: <_i18.NodeModel>[], - ) as List<_i18.NodeModel>); + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override bool get hasListeners => (super.noSuchMethod( @@ -932,7 +972,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override _i10.Future setPrimaryNodeFor({ required _i4.CryptoCurrency? coin, - required _i18.NodeModel? node, + required _i19.NodeModel? node, bool? shouldNotifyListeners = false, }) => (super.noSuchMethod( @@ -950,33 +990,33 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i10.Future); @override - _i18.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => + _i19.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => (super.noSuchMethod(Invocation.method( #getPrimaryNodeFor, [], {#currency: currency}, - )) as _i18.NodeModel?); + )) as _i19.NodeModel?); @override - List<_i18.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => + List<_i19.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #getNodesFor, [coin], ), - returnValue: <_i18.NodeModel>[], - ) as List<_i18.NodeModel>); + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override - _i18.NodeModel? getNodeById({required String? id}) => + _i19.NodeModel? getNodeById({required String? id}) => (super.noSuchMethod(Invocation.method( #getNodeById, [], {#id: id}, - )) as _i18.NodeModel?); + )) as _i19.NodeModel?); @override - List<_i18.NodeModel> failoverNodesFor( + List<_i19.NodeModel> failoverNodesFor( {required _i4.CryptoCurrency? currency}) => (super.noSuchMethod( Invocation.method( @@ -984,12 +1024,12 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { [], {#currency: currency}, ), - returnValue: <_i18.NodeModel>[], - ) as List<_i18.NodeModel>); + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override _i10.Future add( - _i18.NodeModel? node, + _i19.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => @@ -1044,7 +1084,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override _i10.Future edit( - _i18.NodeModel? editedNode, + _i19.NodeModel? editedNode, String? password, bool? shouldNotifyListeners, ) => @@ -1072,7 +1112,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i10.Future); @override - void addListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1081,7 +1121,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ); @override - void removeListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1111,16 +1151,16 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { /// A class which mocks [TorService]. /// /// See the documentation for Mockito's code generation for more information. -class MockTorService extends _i1.Mock implements _i19.TorService { +class MockTorService extends _i1.Mock implements _i20.TorService { MockTorService() { _i1.throwOnMissingStub(this); } @override - _i20.TorConnectionStatus get status => (super.noSuchMethod( + _i21.TorConnectionStatus get status => (super.noSuchMethod( Invocation.getter(#status), - returnValue: _i20.TorConnectionStatus.disconnected, - ) as _i20.TorConnectionStatus); + returnValue: _i21.TorConnectionStatus.disconnected, + ) as _i21.TorConnectionStatus); @override ({_i8.InternetAddress host, int port}) getProxyInfo() => (super.noSuchMethod( @@ -1143,7 +1183,7 @@ class MockTorService extends _i1.Mock implements _i19.TorService { @override void init({ required String? torDataDirPath, - _i21.Tor? mockableOverride, + _i22.Tor? mockableOverride, }) => super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index a5816bdcb..a146a9abf 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -4,27 +4,28 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i11; -import 'dart:typed_data' as _i25; +import 'dart:typed_data' as _i26; import 'dart:ui' as _i17; -import 'package:decimal/decimal.dart' as _i22; +import 'package:decimal/decimal.dart' as _i23; import 'package:isar/isar.dart' as _i9; +import 'package:logger/logger.dart' as _i20; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i16; import 'package:stackwallet/db/isar/main_db.dart' as _i3; -import 'package:stackwallet/models/isar/models/block_explorer.dart' as _i27; +import 'package:stackwallet/models/isar/models/block_explorer.dart' as _i28; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart' - as _i29; -import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i26; -import 'package:stackwallet/models/isar/models/isar_models.dart' as _i28; -import 'package:stackwallet/models/isar/stack_theme.dart' as _i24; + as _i30; +import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i27; +import 'package:stackwallet/models/isar/models/isar_models.dart' as _i29; +import 'package:stackwallet/models/isar/stack_theme.dart' as _i25; import 'package:stackwallet/networking/http.dart' as _i8; import 'package:stackwallet/services/locale_service.dart' as _i15; import 'package:stackwallet/services/node_service.dart' as _i2; -import 'package:stackwallet/services/price_service.dart' as _i21; +import 'package:stackwallet/services/price_service.dart' as _i22; import 'package:stackwallet/services/wallets.dart' as _i10; -import 'package:stackwallet/themes/theme_service.dart' as _i23; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i20; +import 'package:stackwallet/themes/theme_service.dart' as _i24; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i21; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i19; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i18; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -817,6 +818,45 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i20.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i20.Level.all, + ) as _i20.Level); + + @override + set logLevel(_i20.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -873,18 +913,18 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ) as _i11.Future); @override - _i20.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + _i21.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i20.AmountUnit.normal, - ) as _i20.AmountUnit); + returnValue: _i21.AmountUnit.normal, + ) as _i21.AmountUnit); @override void updateAmountUnit({ required _i4.CryptoCurrency? coin, - required _i20.AmountUnit? amountUnit, + required _i21.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -996,7 +1036,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { /// A class which mocks [PriceService]. /// /// See the documentation for Mockito's code generation for more information. -class MockPriceService extends _i1.Mock implements _i21.PriceService { +class MockPriceService extends _i1.Mock implements _i22.PriceService { MockPriceService() { _i1.throwOnMissingStub(this); } @@ -1042,36 +1082,36 @@ class MockPriceService extends _i1.Mock implements _i21.PriceService { ) as bool); @override - _i7.Tuple2<_i22.Decimal, double> getPrice(_i4.CryptoCurrency? coin) => + _i7.Tuple2<_i23.Decimal, double> getPrice(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #getPrice, [coin], ), - returnValue: _FakeTuple2_5<_i22.Decimal, double>( + returnValue: _FakeTuple2_5<_i23.Decimal, double>( this, Invocation.method( #getPrice, [coin], ), ), - ) as _i7.Tuple2<_i22.Decimal, double>); + ) as _i7.Tuple2<_i23.Decimal, double>); @override - _i7.Tuple2<_i22.Decimal, double> getTokenPrice(String? contractAddress) => + _i7.Tuple2<_i23.Decimal, double> getTokenPrice(String? contractAddress) => (super.noSuchMethod( Invocation.method( #getTokenPrice, [contractAddress], ), - returnValue: _FakeTuple2_5<_i22.Decimal, double>( + returnValue: _FakeTuple2_5<_i23.Decimal, double>( this, Invocation.method( #getTokenPrice, [contractAddress], ), ), - ) as _i7.Tuple2<_i22.Decimal, double>); + ) as _i7.Tuple2<_i23.Decimal, double>); @override _i11.Future updatePrice() => (super.noSuchMethod( @@ -1141,7 +1181,7 @@ class MockPriceService extends _i1.Mock implements _i21.PriceService { /// A class which mocks [ThemeService]. /// /// See the documentation for Mockito's code generation for more information. -class MockThemeService extends _i1.Mock implements _i23.ThemeService { +class MockThemeService extends _i1.Mock implements _i24.ThemeService { MockThemeService() { _i1.throwOnMissingStub(this); } @@ -1174,10 +1214,10 @@ class MockThemeService extends _i1.Mock implements _i23.ThemeService { ) as _i3.MainDB); @override - List<_i24.StackTheme> get installedThemes => (super.noSuchMethod( + List<_i25.StackTheme> get installedThemes => (super.noSuchMethod( Invocation.getter(#installedThemes), - returnValue: <_i24.StackTheme>[], - ) as List<_i24.StackTheme>); + returnValue: <_i25.StackTheme>[], + ) as List<_i25.StackTheme>); @override void init(_i3.MainDB? db) => super.noSuchMethod( @@ -1189,7 +1229,7 @@ class MockThemeService extends _i1.Mock implements _i23.ThemeService { ); @override - _i11.Future install({required _i25.Uint8List? themeArchiveData}) => + _i11.Future install({required _i26.Uint8List? themeArchiveData}) => (super.noSuchMethod( Invocation.method( #install, @@ -1233,35 +1273,35 @@ class MockThemeService extends _i1.Mock implements _i23.ThemeService { ) as _i11.Future); @override - _i11.Future> fetchThemes() => + _i11.Future> fetchThemes() => (super.noSuchMethod( Invocation.method( #fetchThemes, [], ), - returnValue: _i11.Future>.value( - <_i23.StackThemeMetaData>[]), - ) as _i11.Future>); + returnValue: _i11.Future>.value( + <_i24.StackThemeMetaData>[]), + ) as _i11.Future>); @override - _i11.Future<_i25.Uint8List> fetchTheme( - {required _i23.StackThemeMetaData? themeMetaData}) => + _i11.Future<_i26.Uint8List> fetchTheme( + {required _i24.StackThemeMetaData? themeMetaData}) => (super.noSuchMethod( Invocation.method( #fetchTheme, [], {#themeMetaData: themeMetaData}, ), - returnValue: _i11.Future<_i25.Uint8List>.value(_i25.Uint8List(0)), - ) as _i11.Future<_i25.Uint8List>); + returnValue: _i11.Future<_i26.Uint8List>.value(_i26.Uint8List(0)), + ) as _i11.Future<_i26.Uint8List>); @override - _i24.StackTheme? getTheme({required String? themeId}) => + _i25.StackTheme? getTheme({required String? themeId}) => (super.noSuchMethod(Invocation.method( #getTheme, [], {#themeId: themeId}, - )) as _i24.StackTheme?); + )) as _i25.StackTheme?); } /// A class which mocks [MainDB]. @@ -1314,13 +1354,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - List<_i26.ContactEntry> getContactEntries() => (super.noSuchMethod( + List<_i27.ContactEntry> getContactEntries() => (super.noSuchMethod( Invocation.method( #getContactEntries, [], ), - returnValue: <_i26.ContactEntry>[], - ) as List<_i26.ContactEntry>); + returnValue: <_i27.ContactEntry>[], + ) as List<_i27.ContactEntry>); @override _i11.Future deleteContactEntry({required String? id}) => @@ -1345,16 +1385,16 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i26.ContactEntry? getContactEntry({required String? id}) => + _i27.ContactEntry? getContactEntry({required String? id}) => (super.noSuchMethod(Invocation.method( #getContactEntry, [], {#id: id}, - )) as _i26.ContactEntry?); + )) as _i27.ContactEntry?); @override _i11.Future putContactEntry( - {required _i26.ContactEntry? contactEntry}) => + {required _i27.ContactEntry? contactEntry}) => (super.noSuchMethod( Invocation.method( #putContactEntry, @@ -1365,17 +1405,17 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i27.TransactionBlockExplorer? getTransactionBlockExplorer( + _i28.TransactionBlockExplorer? getTransactionBlockExplorer( {required _i4.CryptoCurrency? cryptoCurrency}) => (super.noSuchMethod(Invocation.method( #getTransactionBlockExplorer, [], {#cryptoCurrency: cryptoCurrency}, - )) as _i27.TransactionBlockExplorer?); + )) as _i28.TransactionBlockExplorer?); @override _i11.Future putTransactionBlockExplorer( - _i27.TransactionBlockExplorer? explorer) => + _i28.TransactionBlockExplorer? explorer) => (super.noSuchMethod( Invocation.method( #putTransactionBlockExplorer, @@ -1385,13 +1425,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i9.QueryBuilder<_i28.Address, _i28.Address, _i9.QAfterWhereClause> + _i9.QueryBuilder<_i29.Address, _i29.Address, _i9.QAfterWhereClause> getAddresses(String? walletId) => (super.noSuchMethod( Invocation.method( #getAddresses, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i28.Address, _i28.Address, + returnValue: _FakeQueryBuilder_8<_i29.Address, _i29.Address, _i9.QAfterWhereClause>( this, Invocation.method( @@ -1400,10 +1440,10 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ), ), ) as _i9 - .QueryBuilder<_i28.Address, _i28.Address, _i9.QAfterWhereClause>); + .QueryBuilder<_i29.Address, _i29.Address, _i9.QAfterWhereClause>); @override - _i11.Future putAddress(_i28.Address? address) => (super.noSuchMethod( + _i11.Future putAddress(_i29.Address? address) => (super.noSuchMethod( Invocation.method( #putAddress, [address], @@ -1412,7 +1452,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Future> putAddresses(List<_i28.Address>? addresses) => + _i11.Future> putAddresses(List<_i29.Address>? addresses) => (super.noSuchMethod( Invocation.method( #putAddresses, @@ -1422,7 +1462,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future>); @override - _i11.Future> updateOrPutAddresses(List<_i28.Address>? addresses) => + _i11.Future> updateOrPutAddresses(List<_i29.Address>? addresses) => (super.noSuchMethod( Invocation.method( #updateOrPutAddresses, @@ -1432,7 +1472,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future>); @override - _i11.Future<_i28.Address?> getAddress( + _i11.Future<_i29.Address?> getAddress( String? walletId, String? address, ) => @@ -1444,13 +1484,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { address, ], ), - returnValue: _i11.Future<_i28.Address?>.value(), - ) as _i11.Future<_i28.Address?>); + returnValue: _i11.Future<_i29.Address?>.value(), + ) as _i11.Future<_i29.Address?>); @override _i11.Future updateAddress( - _i28.Address? oldAddress, - _i28.Address? newAddress, + _i29.Address? oldAddress, + _i29.Address? newAddress, ) => (super.noSuchMethod( Invocation.method( @@ -1464,13 +1504,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i9.QueryBuilder<_i28.Transaction, _i28.Transaction, _i9.QAfterWhereClause> + _i9.QueryBuilder<_i29.Transaction, _i29.Transaction, _i9.QAfterWhereClause> getTransactions(String? walletId) => (super.noSuchMethod( Invocation.method( #getTransactions, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i28.Transaction, _i28.Transaction, + returnValue: _FakeQueryBuilder_8<_i29.Transaction, _i29.Transaction, _i9.QAfterWhereClause>( this, Invocation.method( @@ -1478,11 +1518,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { [walletId], ), ), - ) as _i9.QueryBuilder<_i28.Transaction, _i28.Transaction, + ) as _i9.QueryBuilder<_i29.Transaction, _i29.Transaction, _i9.QAfterWhereClause>); @override - _i11.Future putTransaction(_i28.Transaction? transaction) => + _i11.Future putTransaction(_i29.Transaction? transaction) => (super.noSuchMethod( Invocation.method( #putTransaction, @@ -1493,7 +1533,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { @override _i11.Future> putTransactions( - List<_i28.Transaction>? transactions) => + List<_i29.Transaction>? transactions) => (super.noSuchMethod( Invocation.method( #putTransactions, @@ -1503,7 +1543,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future>); @override - _i11.Future<_i28.Transaction?> getTransaction( + _i11.Future<_i29.Transaction?> getTransaction( String? walletId, String? txid, ) => @@ -1515,11 +1555,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { txid, ], ), - returnValue: _i11.Future<_i28.Transaction?>.value(), - ) as _i11.Future<_i28.Transaction?>); + returnValue: _i11.Future<_i29.Transaction?>.value(), + ) as _i11.Future<_i29.Transaction?>); @override - _i11.Stream<_i28.Transaction?> watchTransaction({ + _i11.Stream<_i29.Transaction?> watchTransaction({ required int? id, bool? fireImmediately = false, }) => @@ -1532,11 +1572,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i28.Transaction?>.empty(), - ) as _i11.Stream<_i28.Transaction?>); + returnValue: _i11.Stream<_i29.Transaction?>.empty(), + ) as _i11.Stream<_i29.Transaction?>); @override - _i9.QueryBuilder<_i28.UTXO, _i28.UTXO, _i9.QAfterWhereClause> getUTXOs( + _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause> getUTXOs( String? walletId) => (super.noSuchMethod( Invocation.method( @@ -1544,17 +1584,17 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { [walletId], ), returnValue: - _FakeQueryBuilder_8<_i28.UTXO, _i28.UTXO, _i9.QAfterWhereClause>( + _FakeQueryBuilder_8<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause>( this, Invocation.method( #getUTXOs, [walletId], ), ), - ) as _i9.QueryBuilder<_i28.UTXO, _i28.UTXO, _i9.QAfterWhereClause>); + ) as _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause>); @override - _i9.QueryBuilder<_i28.UTXO, _i28.UTXO, _i9.QAfterFilterCondition> + _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterFilterCondition> getUTXOsByAddress( String? walletId, String? address, @@ -1567,7 +1607,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { address, ], ), - returnValue: _FakeQueryBuilder_8<_i28.UTXO, _i28.UTXO, + returnValue: _FakeQueryBuilder_8<_i29.UTXO, _i29.UTXO, _i9.QAfterFilterCondition>( this, Invocation.method( @@ -1579,10 +1619,10 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ), ), ) as _i9 - .QueryBuilder<_i28.UTXO, _i28.UTXO, _i9.QAfterFilterCondition>); + .QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterFilterCondition>); @override - _i11.Future putUTXO(_i28.UTXO? utxo) => (super.noSuchMethod( + _i11.Future putUTXO(_i29.UTXO? utxo) => (super.noSuchMethod( Invocation.method( #putUTXO, [utxo], @@ -1592,7 +1632,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Future putUTXOs(List<_i28.UTXO>? utxos) => (super.noSuchMethod( + _i11.Future putUTXOs(List<_i29.UTXO>? utxos) => (super.noSuchMethod( Invocation.method( #putUTXOs, [utxos], @@ -1604,7 +1644,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { @override _i11.Future updateUTXOs( String? walletId, - List<_i28.UTXO>? utxos, + List<_i29.UTXO>? utxos, ) => (super.noSuchMethod( Invocation.method( @@ -1618,7 +1658,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Stream<_i28.UTXO?> watchUTXO({ + _i11.Stream<_i29.UTXO?> watchUTXO({ required int? id, bool? fireImmediately = false, }) => @@ -1631,11 +1671,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i28.UTXO?>.empty(), - ) as _i11.Stream<_i28.UTXO?>); + returnValue: _i11.Stream<_i29.UTXO?>.empty(), + ) as _i11.Stream<_i29.UTXO?>); @override - _i9.QueryBuilder<_i28.TransactionNote, _i28.TransactionNote, + _i9.QueryBuilder<_i29.TransactionNote, _i29.TransactionNote, _i9.QAfterWhereClause> getTransactionNotes( String? walletId) => (super.noSuchMethod( @@ -1643,19 +1683,19 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #getTransactionNotes, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i28.TransactionNote, - _i28.TransactionNote, _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_8<_i29.TransactionNote, + _i29.TransactionNote, _i9.QAfterWhereClause>( this, Invocation.method( #getTransactionNotes, [walletId], ), ), - ) as _i9.QueryBuilder<_i28.TransactionNote, _i28.TransactionNote, + ) as _i9.QueryBuilder<_i29.TransactionNote, _i29.TransactionNote, _i9.QAfterWhereClause>); @override - _i11.Future putTransactionNote(_i28.TransactionNote? transactionNote) => + _i11.Future putTransactionNote(_i29.TransactionNote? transactionNote) => (super.noSuchMethod( Invocation.method( #putTransactionNote, @@ -1667,7 +1707,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { @override _i11.Future putTransactionNotes( - List<_i28.TransactionNote>? transactionNotes) => + List<_i29.TransactionNote>? transactionNotes) => (super.noSuchMethod( Invocation.method( #putTransactionNotes, @@ -1678,7 +1718,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Future<_i28.TransactionNote?> getTransactionNote( + _i11.Future<_i29.TransactionNote?> getTransactionNote( String? walletId, String? txid, ) => @@ -1690,11 +1730,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { txid, ], ), - returnValue: _i11.Future<_i28.TransactionNote?>.value(), - ) as _i11.Future<_i28.TransactionNote?>); + returnValue: _i11.Future<_i29.TransactionNote?>.value(), + ) as _i11.Future<_i29.TransactionNote?>); @override - _i11.Stream<_i28.TransactionNote?> watchTransactionNote({ + _i11.Stream<_i29.TransactionNote?> watchTransactionNote({ required int? id, bool? fireImmediately = false, }) => @@ -1707,29 +1747,29 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i28.TransactionNote?>.empty(), - ) as _i11.Stream<_i28.TransactionNote?>); + returnValue: _i11.Stream<_i29.TransactionNote?>.empty(), + ) as _i11.Stream<_i29.TransactionNote?>); @override - _i9.QueryBuilder<_i28.AddressLabel, _i28.AddressLabel, _i9.QAfterWhereClause> + _i9.QueryBuilder<_i29.AddressLabel, _i29.AddressLabel, _i9.QAfterWhereClause> getAddressLabels(String? walletId) => (super.noSuchMethod( Invocation.method( #getAddressLabels, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i28.AddressLabel, - _i28.AddressLabel, _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_8<_i29.AddressLabel, + _i29.AddressLabel, _i9.QAfterWhereClause>( this, Invocation.method( #getAddressLabels, [walletId], ), ), - ) as _i9.QueryBuilder<_i28.AddressLabel, _i28.AddressLabel, + ) as _i9.QueryBuilder<_i29.AddressLabel, _i29.AddressLabel, _i9.QAfterWhereClause>); @override - _i11.Future putAddressLabel(_i28.AddressLabel? addressLabel) => + _i11.Future putAddressLabel(_i29.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #putAddressLabel, @@ -1739,7 +1779,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - int putAddressLabelSync(_i28.AddressLabel? addressLabel) => + int putAddressLabelSync(_i29.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #putAddressLabelSync, @@ -1749,7 +1789,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as int); @override - _i11.Future putAddressLabels(List<_i28.AddressLabel>? addressLabels) => + _i11.Future putAddressLabels(List<_i29.AddressLabel>? addressLabels) => (super.noSuchMethod( Invocation.method( #putAddressLabels, @@ -1760,7 +1800,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Future<_i28.AddressLabel?> getAddressLabel( + _i11.Future<_i29.AddressLabel?> getAddressLabel( String? walletId, String? addressString, ) => @@ -1772,11 +1812,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { addressString, ], ), - returnValue: _i11.Future<_i28.AddressLabel?>.value(), - ) as _i11.Future<_i28.AddressLabel?>); + returnValue: _i11.Future<_i29.AddressLabel?>.value(), + ) as _i11.Future<_i29.AddressLabel?>); @override - _i28.AddressLabel? getAddressLabelSync( + _i29.AddressLabel? getAddressLabelSync( String? walletId, String? addressString, ) => @@ -1786,10 +1826,10 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { walletId, addressString, ], - )) as _i28.AddressLabel?); + )) as _i29.AddressLabel?); @override - _i11.Stream<_i28.AddressLabel?> watchAddressLabel({ + _i11.Stream<_i29.AddressLabel?> watchAddressLabel({ required int? id, bool? fireImmediately = false, }) => @@ -1802,11 +1842,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i28.AddressLabel?>.empty(), - ) as _i11.Stream<_i28.AddressLabel?>); + returnValue: _i11.Stream<_i29.AddressLabel?>.empty(), + ) as _i11.Stream<_i29.AddressLabel?>); @override - _i11.Future updateAddressLabel(_i28.AddressLabel? addressLabel) => + _i11.Future updateAddressLabel(_i29.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #updateAddressLabel, @@ -1850,7 +1890,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { @override _i11.Future addNewTransactionData( - List<_i7.Tuple2<_i28.Transaction, _i28.Address?>>? transactionsData, + List<_i7.Tuple2<_i29.Transaction, _i29.Address?>>? transactionsData, String? walletId, ) => (super.noSuchMethod( @@ -1867,7 +1907,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { @override _i11.Future> updateOrPutTransactionV2s( - List<_i29.TransactionV2>? transactions) => + List<_i30.TransactionV2>? transactions) => (super.noSuchMethod( Invocation.method( #updateOrPutTransactionV2s, @@ -1877,13 +1917,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future>); @override - _i9.QueryBuilder<_i28.EthContract, _i28.EthContract, _i9.QWhere> + _i9.QueryBuilder<_i29.EthContract, _i29.EthContract, _i9.QWhere> getEthContracts() => (super.noSuchMethod( Invocation.method( #getEthContracts, [], ), - returnValue: _FakeQueryBuilder_8<_i28.EthContract, _i28.EthContract, + returnValue: _FakeQueryBuilder_8<_i29.EthContract, _i29.EthContract, _i9.QWhere>( this, Invocation.method( @@ -1892,27 +1932,27 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ), ), ) as _i9 - .QueryBuilder<_i28.EthContract, _i28.EthContract, _i9.QWhere>); + .QueryBuilder<_i29.EthContract, _i29.EthContract, _i9.QWhere>); @override - _i11.Future<_i28.EthContract?> getEthContract(String? contractAddress) => + _i11.Future<_i29.EthContract?> getEthContract(String? contractAddress) => (super.noSuchMethod( Invocation.method( #getEthContract, [contractAddress], ), - returnValue: _i11.Future<_i28.EthContract?>.value(), - ) as _i11.Future<_i28.EthContract?>); + returnValue: _i11.Future<_i29.EthContract?>.value(), + ) as _i11.Future<_i29.EthContract?>); @override - _i28.EthContract? getEthContractSync(String? contractAddress) => + _i29.EthContract? getEthContractSync(String? contractAddress) => (super.noSuchMethod(Invocation.method( #getEthContractSync, [contractAddress], - )) as _i28.EthContract?); + )) as _i29.EthContract?); @override - _i11.Future putEthContract(_i28.EthContract? contract) => + _i11.Future putEthContract(_i29.EthContract? contract) => (super.noSuchMethod( Invocation.method( #putEthContract, @@ -1922,7 +1962,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Future putEthContracts(List<_i28.EthContract>? contracts) => + _i11.Future putEthContracts(List<_i29.EthContract>? contracts) => (super.noSuchMethod( Invocation.method( #putEthContracts, @@ -1947,7 +1987,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { /// A class which mocks [IThemeAssets]. /// /// See the documentation for Mockito's code generation for more information. -class MockIThemeAssets extends _i1.Mock implements _i24.IThemeAssets { +class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { MockIThemeAssets() { _i1.throwOnMissingStub(this); }