From 09c9104269d72620e542e2132fae519cf8471689 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 9 Jun 2025 15:11:39 -0600 Subject: [PATCH 01/42] WIP setup mweb --- .../isar/models/blockchain_data/address.dart | 5 ++++- lib/wallets/wallet/impl/litecoin_wallet.dart | 4 +++- .../mweb_interface.dart | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index cb239bc5e..c1dfb3a24 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -175,7 +175,8 @@ enum AddressType { solana, cardanoShelley, xelis, - fact0rn; + fact0rn, + mweb; String get readableName { switch (this) { @@ -217,6 +218,8 @@ enum AddressType { return "Xelis"; case AddressType.fact0rn: return "FACT0RN"; + case AddressType.mweb: + return "MWEB"; } } } diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart index e72b8e633..7f0c4fd8b 100644 --- a/lib/wallets/wallet/impl/litecoin_wallet.dart +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -16,6 +16,7 @@ import '../intermediate/bip39_hd_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; import '../wallet_mixin_interfaces/extended_keys_interface.dart'; +import '../wallet_mixin_interfaces/mweb_interface.dart'; import '../wallet_mixin_interfaces/ordinals_interface.dart'; import '../wallet_mixin_interfaces/rbf_interface.dart'; @@ -26,7 +27,8 @@ class LitecoinWallet ExtendedKeysInterface, CoinControlInterface, RbfInterface, - OrdinalsInterface { + OrdinalsInterface, + MwebInterface { @override int get isarTransactionVersion => 2; diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart new file mode 100644 index 000000000..1de1577cc --- /dev/null +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -0,0 +1,20 @@ +import 'package:isar/isar.dart'; + +import '../../../models/isar/models/isar_models.dart'; +import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import 'electrumx_interface.dart'; + +mixin MwebInterface + on ElectrumXInterface { + // TODO + + Future getCurrentReceivingMwebAddress() async { + return await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .sortByDerivationIndexDesc() + .findFirst(); + } +} From 06d513a1434a49a98977959ad9b5edf89bc2ce62 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 10 Jun 2025 13:51:09 -0600 Subject: [PATCH 02/42] replace barcode_scan2 with mobile_scanner --- .../restore/restore_frost_ms_wallet_view.dart | 2 +- .../restore_wallet_view.dart | 2 +- .../new_contact_address_entry_form.dart | 2 +- lib/pages/buy_view/buy_form.dart | 2 +- .../exchange_step_views/step_2_view.dart | 4 +- .../sub_widgets/transfer_option_widget.dart | 2 +- .../paynym/add_new_paynym_follow_view.dart | 2 +- lib/pages/send_view/frost_ms/recipient.dart | 2 +- lib/pages/send_view/send_view.dart | 2 +- lib/pages/send_view/token_send_view.dart | 2 +- .../add_edit_node_view.dart | 2 +- .../sub_widgets/desktop_token_send.dart | 17 +++++-- lib/utilities/barcode_scanner_interface.dart | 20 ++++++-- lib/widgets/qr_scanner.dart | 48 +++++++++++++++++++ lib/widgets/textfields/frost_step_field.dart | 2 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 18 +++---- scripts/app_config/templates/pubspec.template | 3 +- 18 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 lib/widgets/qr_scanner.dart diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index 9b388a5a3..facaeed44 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -212,7 +212,7 @@ class _RestoreFrostMsWalletViewState await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); configFieldController.text = qrResult.rawContent; diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index 535fbe07a..aeb9ff845 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -596,7 +596,7 @@ class _RestoreWalletViewState extends ConsumerState { Future scanMnemonicQr() async { try { - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final results = AddressUtils.decodeQRSeedData(qrResult.rawContent); diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index 818393aec..b6dcd7bde 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -70,7 +70,7 @@ class _NewContactAddressEntryFormState // .read(shouldShowLockscreenOnResumeStateProvider // .state) // .state = false; - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); // Future.delayed( // const Duration(seconds: 2), diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index d62a2bdc4..c194ec9ee 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -693,7 +693,7 @@ class _BuyFormState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index 621b981ec..a731bf9df 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -69,7 +69,7 @@ class _Step2ViewState extends ConsumerState { void _onRefundQrTapped() async { try { - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final paymentData = AddressUtils.parsePaymentUri( qrResult.rawContent, @@ -122,7 +122,7 @@ class _Step2ViewState extends ConsumerState { void _onToQrTapped() async { try { - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final paymentData = AddressUtils.parsePaymentUri( qrResult.rawContent, diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart index 1061eb128..6aaad59c0 100644 --- a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -236,7 +236,7 @@ class _TransferOptionWidgetState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final coin = ref.read(pWalletCoin(walletId)); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); diff --git a/lib/pages/paynym/add_new_paynym_follow_view.dart b/lib/pages/paynym/add_new_paynym_follow_view.dart index d74a17429..85e4c3ac7 100644 --- a/lib/pages/paynym/add_new_paynym_follow_view.dart +++ b/lib/pages/paynym/add_new_paynym_follow_view.dart @@ -121,7 +121,7 @@ class _AddNewPaynymFollowViewState await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final pCodeString = qrResult.rawContent; diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart index 36122962c..150eecb0b 100644 --- a/lib/pages/send_view/frost_ms/recipient.dart +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -121,7 +121,7 @@ class _RecipientState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 54a2f2f8c..3fca255c7 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -268,7 +268,7 @@ class _SendViewState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); // Future.delayed( // const Duration(seconds: 2), diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 635df7e89..fe7f4bb46 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -151,7 +151,7 @@ class _TokenSendViewState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); // Future.delayed( // const Duration(seconds: 2), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 27b50796d..ca5b825ad 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -377,7 +377,7 @@ class _AddEditNodeViewState extends ConsumerState { } } else { try { - final result = await ref.read(pBarcodeScanner).scan(); + final result = await ref.read(pBarcodeScanner).scan(context: context); await _processQrData(result.rawContent); } on PlatformException catch (e, s) { if (mounted) { diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index 34071ea8e..aac8ec01d 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -43,6 +43,7 @@ import '../../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/qr_code_scanner_dialog.dart'; import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/eth_fee_form.dart'; import '../../../../widgets/icon_widgets/addressbook_icon.dart'; @@ -420,12 +421,20 @@ class _DesktopTokenSendState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await showDialog( + context: context, + builder: (context) => const QrCodeScannerDialog(), + ); + + if (qrResult == null) { + Logging.instance.w("Qr scanning cancelled"); + return; + } - Logging.instance.d("qrResult content: ${qrResult.rawContent}"); + Logging.instance.d("qrResult content: $qrResult"); final paymentData = AddressUtils.parsePaymentUri( - qrResult.rawContent, + qrResult, logging: Logging.instance, ); @@ -464,7 +473,7 @@ class _DesktopTokenSendState extends ConsumerState { // now check for non standard encoded basic address } else { - _address = qrResult.rawContent.split("\n").first.trim(); + _address = qrResult.split("\n").first.trim(); sendToController.text = _address ?? ""; _updatePreviewButtonState(_address, _amountToSend); diff --git a/lib/utilities/barcode_scanner_interface.dart b/lib/utilities/barcode_scanner_interface.dart index 87b079d03..f8256f2e5 100644 --- a/lib/utilities/barcode_scanner_interface.dart +++ b/lib/utilities/barcode_scanner_interface.dart @@ -10,27 +10,37 @@ import 'dart:io'; -import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import '../widgets/desktop/primary_button.dart'; import '../widgets/desktop/secondary_button.dart'; +import '../widgets/qr_scanner.dart'; import '../widgets/stack_dialog.dart'; import 'logger.dart'; +class ScanResult { + final String rawContent; + + ScanResult({required this.rawContent}); +} + abstract class BarcodeScannerInterface { - Future scan({ScanOptions options = const ScanOptions()}); + Future scan({required BuildContext context}); } class BarcodeScannerWrapper implements BarcodeScannerInterface { const BarcodeScannerWrapper(); @override - Future scan({ScanOptions options = const ScanOptions()}) async { + Future scan({required BuildContext context}) async { try { - final result = await BarcodeScanner.scan(options: options); - return result; + final data = await showDialog( + context: context, + builder: (context) => const QrScanner(), + ); + + return ScanResult(rawContent: data.toString()); } catch (e) { rethrow; } diff --git a/lib/widgets/qr_scanner.dart b/lib/widgets/qr_scanner.dart new file mode 100644 index 000000000..66941ac9d --- /dev/null +++ b/lib/widgets/qr_scanner.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import '../themes/stack_colors.dart'; +import '../utilities/logger.dart'; +import '../utilities/text_styles.dart'; +import 'background.dart'; +import 'custom_buttons/app_bar_icon_button.dart'; + +class QrScanner extends ConsumerWidget { + const QrScanner({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.backgroundAppBar, + leading: const AppBarBackButton(), + title: Text("Scan QR code", style: STextStyles.navBarTitle(context)), + ), + body: MobileScanner( + onDetect: (capture) { + final data = + ((capture.raw as Map?)?["data"] as List?)?.firstOrNull as Map?; + + final value = + data?["rawValue"] as String? ?? + data?["displayValue"] as String?; + + Navigator.of(context).pop(value); + }, + onDetectError: (error, stackTrace) { + Logging.instance.w( + "Mobile scanner", + error: error, + stackTrace: stackTrace, + ); + Navigator.of(context).pop(); + }, + ), + ), + ); + } +} diff --git a/lib/widgets/textfields/frost_step_field.dart b/lib/widgets/textfields/frost_step_field.dart index 31364555b..f94fac2b4 100644 --- a/lib/widgets/textfields/frost_step_field.dart +++ b/lib/widgets/textfields/frost_step_field.dart @@ -79,7 +79,7 @@ class _FrostStepFieldState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); widget.controller.text = qrResult.rawContent; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d2bf8597f..b30b6bead 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -18,6 +18,7 @@ import flutter_local_notifications import flutter_secure_storage_macos import isar_flutter_libs import local_auth_darwin +import mobile_scanner import package_info_plus import path_provider_foundation import share_plus @@ -41,6 +42,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/pubspec.lock b/pubspec.lock index c5dda1160..c5630097a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -70,14 +70,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.12.0" - barcode_scan2: - dependency: "direct main" - description: - name: barcode_scan2 - sha256: efbe38629e6df2200e4d60ebe252e8e041cd5ae7b50f194a20f01779ade9d1c3 - url: "https://pub.dev" - source: hosted - version: "4.5.0" basic_utils: dependency: "direct main" description: @@ -1229,7 +1221,7 @@ packages: source: hosted version: "1.0.3" image: - dependency: transitive + dependency: "direct main" description: name: image sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d @@ -1482,6 +1474,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" + url: "https://pub.dev" + source: hosted + version: "7.0.1" mockingjay: dependency: "direct dev" description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index e70887c87..45cf268ff 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -122,7 +122,8 @@ dependencies: event_bus: ^2.0.0 uuid: ^3.0.5 crypto: ^3.0.2 - barcode_scan2: ^4.5.0 + mobile_scanner: ^7.0.1 + image: ^4.3.0 wakelock_plus: ^1.2.8 intl: ^0.17.0 devicelocale: From a28b629ef3726d00214fc4d0c5471c5e6976e3cc Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 10 Jun 2025 16:32:26 -0600 Subject: [PATCH 03/42] update fusiondart --- pubspec.lock | 4 ++-- scripts/app_config/templates/pubspec.template | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index c5630097a..274c5a07d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1111,8 +1111,8 @@ packages: dependency: "direct main" description: path: "." - ref: "9dc883f4432c8db4ec44cb8cc836963295d63952" - resolved-ref: "9dc883f4432c8db4ec44cb8cc836963295d63952" + ref: afaad488f5215a9c2c211e5e2f8460237eef60f1 + resolved-ref: afaad488f5215a9c2c211e5e2f8460237eef60f1 url: "https://github.com/cypherstack/fusiondart.git" source: git version: "1.0.0" diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 45cf268ff..02a8b55f5 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -70,7 +70,7 @@ dependencies: fusiondart: git: url: https://github.com/cypherstack/fusiondart.git - ref: 9dc883f4432c8db4ec44cb8cc836963295d63952 + ref: afaad488f5215a9c2c211e5e2f8460237eef60f1 # Utility plugins http: ^0.13.0 From 518888ae3ad43ae7b537d99e3a00e401e052c6ff Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 10 Jun 2025 16:34:36 -0600 Subject: [PATCH 04/42] add mweb dependencies --- linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 54 +++++++++++++++++-- scripts/app_config/templates/pubspec.template | 2 + windows/flutter/generated_plugins.cmake | 1 + 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 8bca8e723..239136a98 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -20,6 +20,7 @@ list(APPEND FLUTTER_FFI_PLUGIN_LIST camera_linux coinlib_flutter flutter_libsparkmobile + flutter_mwebd frostdart tor_ffi_plugin xelis_flutter diff --git a/pubspec.lock b/pubspec.lock index 274c5a07d..563118121 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -981,6 +981,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.2.0" + flutter_mwebd: + dependency: "direct main" + description: + name: flutter_mwebd + sha256: b390f5c99aaddec5d4c785c6c3d96b786b4eb333e5e1fd0051079cec1aacf40a + url: "https://pub.dev" + source: hosted + version: "0.0.1-pre.1" flutter_native_splash: dependency: "direct main" description: @@ -1132,6 +1140,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.5" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + url: "https://pub.dev" + source: hosted + version: "0.3.3+1" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: b81fe352cc4a330b3710d2b7ad258d9bcef6f909bb759b306bf42973a7d046db + url: "https://pub.dev" + source: hosted + version: "2.0.0" graphs: dependency: transitive description: @@ -1140,6 +1164,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + grpc: + dependency: transitive + description: + name: grpc + sha256: "30e1edae6846b163a64f6d8716e3443980fe1f7d2d1f086f011d24ea186f2582" + url: "https://pub.dev" + source: hosted + version: "4.0.4" hex: dependency: "direct main" description: @@ -1196,6 +1228,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.6" + http2: + dependency: transitive + description: + name: http2 + sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa" + url: "https://pub.dev" + source: hosted + version: "2.3.1" http_multi_server: dependency: transitive description: @@ -1522,6 +1562,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + mweb_client: + dependency: "direct main" + description: + name: mweb_client + sha256: "8c9498adaaa1166a49ee114b03498268aff176778eeb311ed049fd22657feacb" + url: "https://pub.dev" + source: hosted + version: "0.1.0" namecoin: dependency: "direct main" description: @@ -1767,10 +1815,10 @@ packages: dependency: transitive description: name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + sha256: "579fe5557eae58e3adca2e999e38f02441d8aa908703854a9e0a0f47fa857731" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.1.0" pub_semver: dependency: transitive description: @@ -2513,5 +2561,5 @@ packages: source: hosted version: "0.2.3" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.7.2 <4.0.0" flutter: ">=3.29.0" diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 02a8b55f5..3fa0f3077 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -221,6 +221,8 @@ dependencies: path: ^1.9.1 cs_salvium: ^1.2.0 cs_salvium_flutter_libs: ^1.0.2 + flutter_mwebd: ^0.0.1-pre.1 + mweb_client: ^0.1.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b7bea9e3f..ac0ec291d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -23,6 +23,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST coinlib_flutter flutter_libsparkmobile + flutter_mwebd frostdart tor_ffi_plugin xelis_flutter From a41a5fa666121616ebd362ca428e3438f6948deb Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 11 Jun 2025 15:33:25 -0600 Subject: [PATCH 05/42] WIP drift mweb outputs and build runner gen updates --- lib/db/drift/database.dart | 26 +- lib/db/drift/database.g.dart | 1120 ++++++++++++++--- .../models/blockchain_data/address.g.dart | 4 + lib/wallets/isar/models/wallet_info.g.dart | 4 + test/cached_electrumx_test.mocks.dart | 15 + .../pages/send_view/send_view_test.mocks.dart | 36 +- ...d_address_book_view_screen_test.mocks.dart | 45 +- .../exchange/exchange_view_test.mocks.dart | 15 + .../lockscreen_view_screen_test.mocks.dart | 21 +- .../create_pin_view_screen_test.mocks.dart | 21 +- ...restore_wallet_view_screen_test.mocks.dart | 94 +- ...dd_custom_node_view_screen_test.mocks.dart | 21 +- .../node_details_view_screen_test.mocks.dart | 21 +- ...twork_settings_view_screen_test.mocks.dart | 21 +- ...allet_settings_view_screen_test.mocks.dart | 107 +- .../send_view_screen_test.mocks.dart | 19 +- .../bitcoin/bitcoin_wallet_test.mocks.dart | 37 - .../bitcoincash_wallet_test.mocks.dart | 37 - .../dogecoin/dogecoin_wallet_test.mocks.dart | 37 - .../namecoin/namecoin_wallet_test.mocks.dart | 37 - .../particl/particl_wallet_test.mocks.dart | 37 - .../managed_favorite_test.mocks.dart | 36 +- test/widget_tests/node_card_test.mocks.dart | 21 +- .../node_options_sheet_test.mocks.dart | 36 +- .../transaction_card_test.mocks.dart | 26 +- ...et_info_row_balance_future_test.mocks.dart | 21 +- .../wallet_info_row_test.mocks.dart | 21 +- 27 files changed, 1197 insertions(+), 739 deletions(-) diff --git a/lib/db/drift/database.dart b/lib/db/drift/database.dart index 36cb67150..c2ed93a46 100644 --- a/lib/db/drift/database.dart +++ b/lib/db/drift/database.dart @@ -44,13 +44,35 @@ class SparkNames extends Table { Set get primaryKey => {name}; } -@DriftDatabase(tables: [SparkNames]) +class MwebUtxos extends Table { + TextColumn get outputId => text()(); + TextColumn get address => text()(); + IntColumn get value => integer()(); + IntColumn get height => integer()(); + IntColumn get blockTime => integer()(); + BoolColumn get blocked => boolean()(); + BoolColumn get used => boolean()(); + + @override + Set get primaryKey => {outputId}; +} + +@DriftDatabase(tables: [SparkNames, MwebUtxos]) final class WalletDatabase extends _$WalletDatabase { WalletDatabase._(String walletId, [QueryExecutor? executor]) : super(executor ?? _openConnection(walletId)); @override - int get schemaVersion => 1; + int get schemaVersion => 2; + + @override + MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (m, from, to) async { + if (from == 1 && to == 2) { + await m.createTable(mwebUtxos); + } + }, + ); static QueryExecutor _openConnection(String walletId) { return driftDatabase( diff --git a/lib/db/drift/database.g.dart b/lib/db/drift/database.g.dart index 42113b071..416fa4170 100644 --- a/lib/db/drift/database.g.dart +++ b/lib/db/drift/database.g.dart @@ -12,66 +12,97 @@ class $SparkNamesTable extends SparkNames static const VerificationMeta _nameMeta = const VerificationMeta('name'); @override late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'UNIQUE NOT NULL COLLATE NOCASE'); - static const VerificationMeta _addressMeta = - const VerificationMeta('address'); + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'UNIQUE NOT NULL COLLATE NOCASE', + ); + static const VerificationMeta _addressMeta = const VerificationMeta( + 'address', + ); @override late final GeneratedColumn address = GeneratedColumn( - 'address', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _validUntilMeta = - const VerificationMeta('validUntil'); + 'address', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _validUntilMeta = const VerificationMeta( + 'validUntil', + ); @override late final GeneratedColumn validUntil = GeneratedColumn( - 'valid_until', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - static const VerificationMeta _additionalInfoMeta = - const VerificationMeta('additionalInfo'); + 'valid_until', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _additionalInfoMeta = const VerificationMeta( + 'additionalInfo', + ); @override late final GeneratedColumn additionalInfo = GeneratedColumn( - 'additional_info', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + 'additional_info', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); @override - List get $columns => - [name, address, validUntil, additionalInfo]; + List get $columns => [ + name, + address, + validUntil, + additionalInfo, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'spark_names'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('name')) { context.handle( - _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); } else if (isInserting) { context.missing(_nameMeta); } if (data.containsKey('address')) { - context.handle(_addressMeta, - address.isAcceptableOrUnknown(data['address']!, _addressMeta)); + context.handle( + _addressMeta, + address.isAcceptableOrUnknown(data['address']!, _addressMeta), + ); } else if (isInserting) { context.missing(_addressMeta); } if (data.containsKey('valid_until')) { context.handle( - _validUntilMeta, - validUntil.isAcceptableOrUnknown( - data['valid_until']!, _validUntilMeta)); + _validUntilMeta, + validUntil.isAcceptableOrUnknown(data['valid_until']!, _validUntilMeta), + ); } else if (isInserting) { context.missing(_validUntilMeta); } if (data.containsKey('additional_info')) { context.handle( + _additionalInfoMeta, + additionalInfo.isAcceptableOrUnknown( + data['additional_info']!, _additionalInfoMeta, - additionalInfo.isAcceptableOrUnknown( - data['additional_info']!, _additionalInfoMeta)); + ), + ); } return context; } @@ -82,14 +113,25 @@ class $SparkNamesTable extends SparkNames SparkName map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return SparkName( - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - address: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}address'])!, - validUntil: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}valid_until'])!, - additionalInfo: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}additional_info']), + name: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + address: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}address'], + )!, + validUntil: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}valid_until'], + )!, + additionalInfo: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}additional_info'], + ), ); } @@ -104,11 +146,12 @@ class SparkName extends DataClass implements Insertable { final String address; final int validUntil; final String? additionalInfo; - const SparkName( - {required this.name, - required this.address, - required this.validUntil, - this.additionalInfo}); + const SparkName({ + required this.name, + required this.address, + required this.validUntil, + this.additionalInfo, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -126,14 +169,17 @@ class SparkName extends DataClass implements Insertable { name: Value(name), address: Value(address), validUntil: Value(validUntil), - additionalInfo: additionalInfo == null && nullToAbsent - ? const Value.absent() - : Value(additionalInfo), + additionalInfo: + additionalInfo == null && nullToAbsent + ? const Value.absent() + : Value(additionalInfo), ); } - factory SparkName.fromJson(Map json, - {ValueSerializer? serializer}) { + factory SparkName.fromJson( + Map json, { + ValueSerializer? serializer, + }) { serializer ??= driftRuntimeOptions.defaultSerializer; return SparkName( name: serializer.fromJson(json['name']), @@ -153,27 +199,28 @@ class SparkName extends DataClass implements Insertable { }; } - SparkName copyWith( - {String? name, - String? address, - int? validUntil, - Value additionalInfo = const Value.absent()}) => - SparkName( - name: name ?? this.name, - address: address ?? this.address, - validUntil: validUntil ?? this.validUntil, - additionalInfo: - additionalInfo.present ? additionalInfo.value : this.additionalInfo, - ); + SparkName copyWith({ + String? name, + String? address, + int? validUntil, + Value additionalInfo = const Value.absent(), + }) => SparkName( + name: name ?? this.name, + address: address ?? this.address, + validUntil: validUntil ?? this.validUntil, + additionalInfo: + additionalInfo.present ? additionalInfo.value : this.additionalInfo, + ); SparkName copyWithCompanion(SparkNamesCompanion data) { return SparkName( name: data.name.present ? data.name.value : this.name, address: data.address.present ? data.address.value : this.address, validUntil: data.validUntil.present ? data.validUntil.value : this.validUntil, - additionalInfo: data.additionalInfo.present - ? data.additionalInfo.value - : this.additionalInfo, + additionalInfo: + data.additionalInfo.present + ? data.additionalInfo.value + : this.additionalInfo, ); } @@ -219,9 +266,9 @@ class SparkNamesCompanion extends UpdateCompanion { required int validUntil, this.additionalInfo = const Value.absent(), this.rowid = const Value.absent(), - }) : name = Value(name), - address = Value(address), - validUntil = Value(validUntil); + }) : name = Value(name), + address = Value(address), + validUntil = Value(validUntil); static Insertable custom({ Expression? name, Expression? address, @@ -238,12 +285,13 @@ class SparkNamesCompanion extends UpdateCompanion { }); } - SparkNamesCompanion copyWith( - {Value? name, - Value? address, - Value? validUntil, - Value? additionalInfo, - Value? rowid}) { + SparkNamesCompanion copyWith({ + Value? name, + Value? address, + Value? validUntil, + Value? additionalInfo, + Value? rowid, + }) { return SparkNamesCompanion( name: name ?? this.name, address: address ?? this.address, @@ -287,31 +335,503 @@ class SparkNamesCompanion extends UpdateCompanion { } } +class $MwebUtxosTable extends MwebUtxos + with TableInfo<$MwebUtxosTable, MwebUtxo> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MwebUtxosTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _outputIdMeta = const VerificationMeta( + 'outputId', + ); + @override + late final GeneratedColumn outputId = GeneratedColumn( + 'output_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _addressMeta = const VerificationMeta( + 'address', + ); + @override + late final GeneratedColumn address = GeneratedColumn( + 'address', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _valueMeta = const VerificationMeta('value'); + @override + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _heightMeta = const VerificationMeta('height'); + @override + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _blockTimeMeta = const VerificationMeta( + 'blockTime', + ); + @override + late final GeneratedColumn blockTime = GeneratedColumn( + 'block_time', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _blockedMeta = const VerificationMeta( + 'blocked', + ); + @override + late final GeneratedColumn blocked = GeneratedColumn( + 'blocked', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("blocked" IN (0, 1))', + ), + ); + static const VerificationMeta _usedMeta = const VerificationMeta('used'); + @override + late final GeneratedColumn used = GeneratedColumn( + 'used', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("used" IN (0, 1))', + ), + ); + @override + List get $columns => [ + outputId, + address, + value, + height, + blockTime, + blocked, + used, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'mweb_utxos'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('output_id')) { + context.handle( + _outputIdMeta, + outputId.isAcceptableOrUnknown(data['output_id']!, _outputIdMeta), + ); + } else if (isInserting) { + context.missing(_outputIdMeta); + } + if (data.containsKey('address')) { + context.handle( + _addressMeta, + address.isAcceptableOrUnknown(data['address']!, _addressMeta), + ); + } else if (isInserting) { + context.missing(_addressMeta); + } + if (data.containsKey('value')) { + context.handle( + _valueMeta, + value.isAcceptableOrUnknown(data['value']!, _valueMeta), + ); + } else if (isInserting) { + context.missing(_valueMeta); + } + if (data.containsKey('height')) { + context.handle( + _heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta), + ); + } else if (isInserting) { + context.missing(_heightMeta); + } + if (data.containsKey('block_time')) { + context.handle( + _blockTimeMeta, + blockTime.isAcceptableOrUnknown(data['block_time']!, _blockTimeMeta), + ); + } else if (isInserting) { + context.missing(_blockTimeMeta); + } + if (data.containsKey('blocked')) { + context.handle( + _blockedMeta, + blocked.isAcceptableOrUnknown(data['blocked']!, _blockedMeta), + ); + } else if (isInserting) { + context.missing(_blockedMeta); + } + if (data.containsKey('used')) { + context.handle( + _usedMeta, + used.isAcceptableOrUnknown(data['used']!, _usedMeta), + ); + } else if (isInserting) { + context.missing(_usedMeta); + } + return context; + } + + @override + Set get $primaryKey => {outputId}; + @override + MwebUtxo map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MwebUtxo( + outputId: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}output_id'], + )!, + address: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}address'], + )!, + value: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}value'], + )!, + height: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + )!, + blockTime: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}block_time'], + )!, + blocked: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}blocked'], + )!, + used: + attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}used'], + )!, + ); + } + + @override + $MwebUtxosTable createAlias(String alias) { + return $MwebUtxosTable(attachedDatabase, alias); + } +} + +class MwebUtxo extends DataClass implements Insertable { + final String outputId; + final String address; + final int value; + final int height; + final int blockTime; + final bool blocked; + final bool used; + const MwebUtxo({ + required this.outputId, + required this.address, + required this.value, + required this.height, + required this.blockTime, + required this.blocked, + required this.used, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['output_id'] = Variable(outputId); + map['address'] = Variable(address); + map['value'] = Variable(value); + map['height'] = Variable(height); + map['block_time'] = Variable(blockTime); + map['blocked'] = Variable(blocked); + map['used'] = Variable(used); + return map; + } + + MwebUtxosCompanion toCompanion(bool nullToAbsent) { + return MwebUtxosCompanion( + outputId: Value(outputId), + address: Value(address), + value: Value(value), + height: Value(height), + blockTime: Value(blockTime), + blocked: Value(blocked), + used: Value(used), + ); + } + + factory MwebUtxo.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MwebUtxo( + outputId: serializer.fromJson(json['outputId']), + address: serializer.fromJson(json['address']), + value: serializer.fromJson(json['value']), + height: serializer.fromJson(json['height']), + blockTime: serializer.fromJson(json['blockTime']), + blocked: serializer.fromJson(json['blocked']), + used: serializer.fromJson(json['used']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'outputId': serializer.toJson(outputId), + 'address': serializer.toJson(address), + 'value': serializer.toJson(value), + 'height': serializer.toJson(height), + 'blockTime': serializer.toJson(blockTime), + 'blocked': serializer.toJson(blocked), + 'used': serializer.toJson(used), + }; + } + + MwebUtxo copyWith({ + String? outputId, + String? address, + int? value, + int? height, + int? blockTime, + bool? blocked, + bool? used, + }) => MwebUtxo( + outputId: outputId ?? this.outputId, + address: address ?? this.address, + value: value ?? this.value, + height: height ?? this.height, + blockTime: blockTime ?? this.blockTime, + blocked: blocked ?? this.blocked, + used: used ?? this.used, + ); + MwebUtxo copyWithCompanion(MwebUtxosCompanion data) { + return MwebUtxo( + outputId: data.outputId.present ? data.outputId.value : this.outputId, + address: data.address.present ? data.address.value : this.address, + value: data.value.present ? data.value.value : this.value, + height: data.height.present ? data.height.value : this.height, + blockTime: data.blockTime.present ? data.blockTime.value : this.blockTime, + blocked: data.blocked.present ? data.blocked.value : this.blocked, + used: data.used.present ? data.used.value : this.used, + ); + } + + @override + String toString() { + return (StringBuffer('MwebUtxo(') + ..write('outputId: $outputId, ') + ..write('address: $address, ') + ..write('value: $value, ') + ..write('height: $height, ') + ..write('blockTime: $blockTime, ') + ..write('blocked: $blocked, ') + ..write('used: $used') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(outputId, address, value, height, blockTime, blocked, used); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MwebUtxo && + other.outputId == this.outputId && + other.address == this.address && + other.value == this.value && + other.height == this.height && + other.blockTime == this.blockTime && + other.blocked == this.blocked && + other.used == this.used); +} + +class MwebUtxosCompanion extends UpdateCompanion { + final Value outputId; + final Value address; + final Value value; + final Value height; + final Value blockTime; + final Value blocked; + final Value used; + final Value rowid; + const MwebUtxosCompanion({ + this.outputId = const Value.absent(), + this.address = const Value.absent(), + this.value = const Value.absent(), + this.height = const Value.absent(), + this.blockTime = const Value.absent(), + this.blocked = const Value.absent(), + this.used = const Value.absent(), + this.rowid = const Value.absent(), + }); + MwebUtxosCompanion.insert({ + required String outputId, + required String address, + required int value, + required int height, + required int blockTime, + required bool blocked, + required bool used, + this.rowid = const Value.absent(), + }) : outputId = Value(outputId), + address = Value(address), + value = Value(value), + height = Value(height), + blockTime = Value(blockTime), + blocked = Value(blocked), + used = Value(used); + static Insertable custom({ + Expression? outputId, + Expression? address, + Expression? value, + Expression? height, + Expression? blockTime, + Expression? blocked, + Expression? used, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (outputId != null) 'output_id': outputId, + if (address != null) 'address': address, + if (value != null) 'value': value, + if (height != null) 'height': height, + if (blockTime != null) 'block_time': blockTime, + if (blocked != null) 'blocked': blocked, + if (used != null) 'used': used, + if (rowid != null) 'rowid': rowid, + }); + } + + MwebUtxosCompanion copyWith({ + Value? outputId, + Value? address, + Value? value, + Value? height, + Value? blockTime, + Value? blocked, + Value? used, + Value? rowid, + }) { + return MwebUtxosCompanion( + outputId: outputId ?? this.outputId, + address: address ?? this.address, + value: value ?? this.value, + height: height ?? this.height, + blockTime: blockTime ?? this.blockTime, + blocked: blocked ?? this.blocked, + used: used ?? this.used, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (outputId.present) { + map['output_id'] = Variable(outputId.value); + } + if (address.present) { + map['address'] = Variable(address.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (blockTime.present) { + map['block_time'] = Variable(blockTime.value); + } + if (blocked.present) { + map['blocked'] = Variable(blocked.value); + } + if (used.present) { + map['used'] = Variable(used.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MwebUtxosCompanion(') + ..write('outputId: $outputId, ') + ..write('address: $address, ') + ..write('value: $value, ') + ..write('height: $height, ') + ..write('blockTime: $blockTime, ') + ..write('blocked: $blocked, ') + ..write('used: $used, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + abstract class _$WalletDatabase extends GeneratedDatabase { _$WalletDatabase(QueryExecutor e) : super(e); $WalletDatabaseManager get managers => $WalletDatabaseManager(this); late final $SparkNamesTable sparkNames = $SparkNamesTable(this); + late final $MwebUtxosTable mwebUtxos = $MwebUtxosTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [sparkNames]; + List get allSchemaEntities => [sparkNames, mwebUtxos]; } -typedef $$SparkNamesTableCreateCompanionBuilder = SparkNamesCompanion Function({ - required String name, - required String address, - required int validUntil, - Value additionalInfo, - Value rowid, -}); -typedef $$SparkNamesTableUpdateCompanionBuilder = SparkNamesCompanion Function({ - Value name, - Value address, - Value validUntil, - Value additionalInfo, - Value rowid, -}); +typedef $$SparkNamesTableCreateCompanionBuilder = + SparkNamesCompanion Function({ + required String name, + required String address, + required int validUntil, + Value additionalInfo, + Value rowid, + }); +typedef $$SparkNamesTableUpdateCompanionBuilder = + SparkNamesCompanion Function({ + Value name, + Value address, + Value validUntil, + Value additionalInfo, + Value rowid, + }); class $$SparkNamesTableFilterComposer extends Composer<_$WalletDatabase, $SparkNamesTable> { @@ -323,17 +843,24 @@ class $$SparkNamesTableFilterComposer super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnFilters(column)); + column: $table.name, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get address => $composableBuilder( - column: $table.address, builder: (column) => ColumnFilters(column)); + column: $table.address, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get validUntil => $composableBuilder( - column: $table.validUntil, builder: (column) => ColumnFilters(column)); + column: $table.validUntil, + builder: (column) => ColumnFilters(column), + ); ColumnFilters get additionalInfo => $composableBuilder( - column: $table.additionalInfo, - builder: (column) => ColumnFilters(column)); + column: $table.additionalInfo, + builder: (column) => ColumnFilters(column), + ); } class $$SparkNamesTableOrderingComposer @@ -346,17 +873,24 @@ class $$SparkNamesTableOrderingComposer super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnOrderings(column)); + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get address => $composableBuilder( - column: $table.address, builder: (column) => ColumnOrderings(column)); + column: $table.address, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get validUntil => $composableBuilder( - column: $table.validUntil, builder: (column) => ColumnOrderings(column)); + column: $table.validUntil, + builder: (column) => ColumnOrderings(column), + ); ColumnOrderings get additionalInfo => $composableBuilder( - column: $table.additionalInfo, - builder: (column) => ColumnOrderings(column)); + column: $table.additionalInfo, + builder: (column) => ColumnOrderings(column), + ); } class $$SparkNamesTableAnnotationComposer @@ -375,85 +909,353 @@ class $$SparkNamesTableAnnotationComposer $composableBuilder(column: $table.address, builder: (column) => column); GeneratedColumn get validUntil => $composableBuilder( - column: $table.validUntil, builder: (column) => column); + column: $table.validUntil, + builder: (column) => column, + ); GeneratedColumn get additionalInfo => $composableBuilder( - column: $table.additionalInfo, builder: (column) => column); + column: $table.additionalInfo, + builder: (column) => column, + ); } -class $$SparkNamesTableTableManager extends RootTableManager< - _$WalletDatabase, - $SparkNamesTable, - SparkName, - $$SparkNamesTableFilterComposer, - $$SparkNamesTableOrderingComposer, - $$SparkNamesTableAnnotationComposer, - $$SparkNamesTableCreateCompanionBuilder, - $$SparkNamesTableUpdateCompanionBuilder, - (SparkName, BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>), - SparkName, - PrefetchHooks Function()> { +class $$SparkNamesTableTableManager + extends + RootTableManager< + _$WalletDatabase, + $SparkNamesTable, + SparkName, + $$SparkNamesTableFilterComposer, + $$SparkNamesTableOrderingComposer, + $$SparkNamesTableAnnotationComposer, + $$SparkNamesTableCreateCompanionBuilder, + $$SparkNamesTableUpdateCompanionBuilder, + ( + SparkName, + BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>, + ), + SparkName, + PrefetchHooks Function() + > { $$SparkNamesTableTableManager(_$WalletDatabase db, $SparkNamesTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$SparkNamesTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$SparkNamesTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$SparkNamesTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value name = const Value.absent(), - Value address = const Value.absent(), - Value validUntil = const Value.absent(), - Value additionalInfo = const Value.absent(), - Value rowid = const Value.absent(), - }) => - SparkNamesCompanion( - name: name, - address: address, - validUntil: validUntil, - additionalInfo: additionalInfo, - rowid: rowid, - ), - createCompanionCallback: ({ - required String name, - required String address, - required int validUntil, - Value additionalInfo = const Value.absent(), - Value rowid = const Value.absent(), - }) => - SparkNamesCompanion.insert( - name: name, - address: address, - validUntil: validUntil, - additionalInfo: additionalInfo, - rowid: rowid, + createFilteringComposer: + () => $$SparkNamesTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => $$SparkNamesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => $$SparkNamesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value name = const Value.absent(), + Value address = const Value.absent(), + Value validUntil = const Value.absent(), + Value additionalInfo = const Value.absent(), + Value rowid = const Value.absent(), + }) => SparkNamesCompanion( + name: name, + address: address, + validUntil: validUntil, + additionalInfo: additionalInfo, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String name, + required String address, + required int validUntil, + Value additionalInfo = const Value.absent(), + Value rowid = const Value.absent(), + }) => SparkNamesCompanion.insert( + name: name, + address: address, + validUntil: validUntil, + additionalInfo: additionalInfo, + rowid: rowid, + ), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$SparkNamesTableProcessedTableManager = + ProcessedTableManager< + _$WalletDatabase, + $SparkNamesTable, + SparkName, + $$SparkNamesTableFilterComposer, + $$SparkNamesTableOrderingComposer, + $$SparkNamesTableAnnotationComposer, + $$SparkNamesTableCreateCompanionBuilder, + $$SparkNamesTableUpdateCompanionBuilder, + ( + SparkName, + BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>, + ), + SparkName, + PrefetchHooks Function() + >; +typedef $$MwebUtxosTableCreateCompanionBuilder = + MwebUtxosCompanion Function({ + required String outputId, + required String address, + required int value, + required int height, + required int blockTime, + required bool blocked, + required bool used, + Value rowid, + }); +typedef $$MwebUtxosTableUpdateCompanionBuilder = + MwebUtxosCompanion Function({ + Value outputId, + Value address, + Value value, + Value height, + Value blockTime, + Value blocked, + Value used, + Value rowid, + }); + +class $$MwebUtxosTableFilterComposer + extends Composer<_$WalletDatabase, $MwebUtxosTable> { + $$MwebUtxosTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get outputId => $composableBuilder( + column: $table.outputId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get address => $composableBuilder( + column: $table.address, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get value => $composableBuilder( + column: $table.value, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get height => $composableBuilder( + column: $table.height, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get blockTime => $composableBuilder( + column: $table.blockTime, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get blocked => $composableBuilder( + column: $table.blocked, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get used => $composableBuilder( + column: $table.used, + builder: (column) => ColumnFilters(column), + ); +} + +class $$MwebUtxosTableOrderingComposer + extends Composer<_$WalletDatabase, $MwebUtxosTable> { + $$MwebUtxosTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get outputId => $composableBuilder( + column: $table.outputId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get address => $composableBuilder( + column: $table.address, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get value => $composableBuilder( + column: $table.value, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get height => $composableBuilder( + column: $table.height, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get blockTime => $composableBuilder( + column: $table.blockTime, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get blocked => $composableBuilder( + column: $table.blocked, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get used => $composableBuilder( + column: $table.used, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$MwebUtxosTableAnnotationComposer + extends Composer<_$WalletDatabase, $MwebUtxosTable> { + $$MwebUtxosTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get outputId => + $composableBuilder(column: $table.outputId, builder: (column) => column); + + GeneratedColumn get address => + $composableBuilder(column: $table.address, builder: (column) => column); + + GeneratedColumn get value => + $composableBuilder(column: $table.value, builder: (column) => column); + + GeneratedColumn get height => + $composableBuilder(column: $table.height, builder: (column) => column); + + GeneratedColumn get blockTime => + $composableBuilder(column: $table.blockTime, builder: (column) => column); + + GeneratedColumn get blocked => + $composableBuilder(column: $table.blocked, builder: (column) => column); + + GeneratedColumn get used => + $composableBuilder(column: $table.used, builder: (column) => column); +} + +class $$MwebUtxosTableTableManager + extends + RootTableManager< + _$WalletDatabase, + $MwebUtxosTable, + MwebUtxo, + $$MwebUtxosTableFilterComposer, + $$MwebUtxosTableOrderingComposer, + $$MwebUtxosTableAnnotationComposer, + $$MwebUtxosTableCreateCompanionBuilder, + $$MwebUtxosTableUpdateCompanionBuilder, + ( + MwebUtxo, + BaseReferences<_$WalletDatabase, $MwebUtxosTable, MwebUtxo>, ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + MwebUtxo, + PrefetchHooks Function() + > { + $$MwebUtxosTableTableManager(_$WalletDatabase db, $MwebUtxosTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: + () => $$MwebUtxosTableFilterComposer($db: db, $table: table), + createOrderingComposer: + () => $$MwebUtxosTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: + () => $$MwebUtxosTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value outputId = const Value.absent(), + Value address = const Value.absent(), + Value value = const Value.absent(), + Value height = const Value.absent(), + Value blockTime = const Value.absent(), + Value blocked = const Value.absent(), + Value used = const Value.absent(), + Value rowid = const Value.absent(), + }) => MwebUtxosCompanion( + outputId: outputId, + address: address, + value: value, + height: height, + blockTime: blockTime, + blocked: blocked, + used: used, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String outputId, + required String address, + required int value, + required int height, + required int blockTime, + required bool blocked, + required bool used, + Value rowid = const Value.absent(), + }) => MwebUtxosCompanion.insert( + outputId: outputId, + address: address, + value: value, + height: height, + blockTime: blockTime, + blocked: blocked, + used: used, + rowid: rowid, + ), + withReferenceMapper: + (p0) => + p0 + .map( + (e) => ( + e.readTable(table), + BaseReferences(db, table, e), + ), + ) + .toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$SparkNamesTableProcessedTableManager = ProcessedTableManager< - _$WalletDatabase, - $SparkNamesTable, - SparkName, - $$SparkNamesTableFilterComposer, - $$SparkNamesTableOrderingComposer, - $$SparkNamesTableAnnotationComposer, - $$SparkNamesTableCreateCompanionBuilder, - $$SparkNamesTableUpdateCompanionBuilder, - (SparkName, BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>), - SparkName, - PrefetchHooks Function()>; +typedef $$MwebUtxosTableProcessedTableManager = + ProcessedTableManager< + _$WalletDatabase, + $MwebUtxosTable, + MwebUtxo, + $$MwebUtxosTableFilterComposer, + $$MwebUtxosTableOrderingComposer, + $$MwebUtxosTableAnnotationComposer, + $$MwebUtxosTableCreateCompanionBuilder, + $$MwebUtxosTableUpdateCompanionBuilder, + (MwebUtxo, BaseReferences<_$WalletDatabase, $MwebUtxosTable, MwebUtxo>), + MwebUtxo, + PrefetchHooks Function() + >; class $WalletDatabaseManager { final _$WalletDatabase _db; $WalletDatabaseManager(this._db); $$SparkNamesTableTableManager get sparkNames => $$SparkNamesTableTableManager(_db, _db.sparkNames); + $$MwebUtxosTableTableManager get mwebUtxos => + $$MwebUtxosTableTableManager(_db, _db.mwebUtxos); } diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index a9289a481..e3df46a6b 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -280,6 +280,8 @@ const _AddresstypeEnumValueMap = { 'solana': 15, 'cardanoShelley': 16, 'xelis': 17, + 'fact0rn': 18, + 'mweb': 19, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -300,6 +302,8 @@ const _AddresstypeValueEnumMap = { 15: AddressType.solana, 16: AddressType.cardanoShelley, 17: AddressType.xelis, + 18: AddressType.fact0rn, + 19: AddressType.mweb, }; Id _addressGetId(Address object) { diff --git a/lib/wallets/isar/models/wallet_info.g.dart b/lib/wallets/isar/models/wallet_info.g.dart index 5e93564c0..3dda37c77 100644 --- a/lib/wallets/isar/models/wallet_info.g.dart +++ b/lib/wallets/isar/models/wallet_info.g.dart @@ -270,6 +270,8 @@ const _WalletInfomainAddressTypeEnumValueMap = { 'solana': 15, 'cardanoShelley': 16, 'xelis': 17, + 'fact0rn': 18, + 'mweb': 19, }; const _WalletInfomainAddressTypeValueEnumMap = { 0: AddressType.p2pkh, @@ -290,6 +292,8 @@ const _WalletInfomainAddressTypeValueEnumMap = { 15: AddressType.solana, 16: AddressType.cardanoShelley, 17: AddressType.xelis, + 18: AddressType.fact0rn, + 19: AddressType.mweb, }; Id _walletInfoGetId(WalletInfo object) { diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 22ab2a712..0d5b6a2f4 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -1261,6 +1261,21 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { returnValueForMissingStub: null, ); + @override + bool get useMweb => (super.noSuchMethod( + Invocation.getter(#useMweb), + returnValue: false, + ) as bool); + + @override + set useMweb(bool? useMweb) => super.noSuchMethod( + Invocation.setter( + #useMweb, + useMweb, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 436a918fd..84a91812a 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -348,7 +348,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -395,25 +395,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - @override - _i10.Future edit( - _i13.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) as _i10.Future); - @override _i10.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( @@ -1200,6 +1181,21 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get useMweb => (super.noSuchMethod( + Invocation.getter(#useMweb), + returnValue: false, + ) as bool); + + @override + set useMweb(bool? useMweb) => super.noSuchMethod( + Invocation.setter( + #useMweb, + useMweb, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), diff --git a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart index 25bfa08e4..192ce703a 100644 --- a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart @@ -3,14 +3,14 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; +import 'dart:async' as _i4; import 'dart:ui' as _i7; -import 'package:barcode_scan2/barcode_scan2.dart' as _i2; +import 'package:flutter/material.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i3; import 'package:stackwallet/services/address_book_service.dart' as _i6; -import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i4; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -49,29 +49,28 @@ class _FakeContactEntry_1 extends _i1.SmartFake implements _i3.ContactEntry { /// /// See the documentation for Mockito's code generation for more information. class MockBarcodeScannerWrapper extends _i1.Mock - implements _i4.BarcodeScannerWrapper { + implements _i2.BarcodeScannerWrapper { MockBarcodeScannerWrapper() { _i1.throwOnMissingStub(this); } @override - _i5.Future<_i2.ScanResult> scan( - {_i2.ScanOptions? options = const _i2.ScanOptions()}) => + _i4.Future<_i2.ScanResult> scan({required _i5.BuildContext? context}) => (super.noSuchMethod( Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), - returnValue: _i5.Future<_i2.ScanResult>.value(_FakeScanResult_0( + returnValue: _i4.Future<_i2.ScanResult>.value(_FakeScanResult_0( this, Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), )), - ) as _i5.Future<_i2.ScanResult>); + ) as _i4.Future<_i2.ScanResult>); } /// A class which mocks [AddressBookService]. @@ -111,15 +110,15 @@ class MockAddressBookService extends _i1.Mock ) as _i3.ContactEntry); @override - _i5.Future> search(String? text) => + _i4.Future> search(String? text) => (super.noSuchMethod( Invocation.method( #search, [text], ), returnValue: - _i5.Future>.value(<_i3.ContactEntry>[]), - ) as _i5.Future>); + _i4.Future>.value(<_i3.ContactEntry>[]), + ) as _i4.Future>); @override bool matches( @@ -138,33 +137,33 @@ class MockAddressBookService extends _i1.Mock ) as bool); @override - _i5.Future addContact(_i3.ContactEntry? contact) => (super.noSuchMethod( + _i4.Future addContact(_i3.ContactEntry? contact) => (super.noSuchMethod( Invocation.method( #addContact, [contact], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future editContact(_i3.ContactEntry? editedContact) => + _i4.Future editContact(_i3.ContactEntry? editedContact) => (super.noSuchMethod( Invocation.method( #editContact, [editedContact], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future removeContact(String? id) => (super.noSuchMethod( + _i4.Future removeContact(String? id) => (super.noSuchMethod( Invocation.method( #removeContact, [id], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override void addListener(_i7.VoidCallback? listener) => super.noSuchMethod( diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 643d6cc6b..1f78d9ce4 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -622,6 +622,21 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); + @override + bool get useMweb => (super.noSuchMethod( + Invocation.getter(#useMweb), + returnValue: false, + ) as bool); + + @override + set useMweb(bool? useMweb) => super.noSuchMethod( + Invocation.setter( + #useMweb, + useMweb, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index c68851019..804721d4c 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -209,7 +209,7 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -256,25 +256,6 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - @override - _i4.Future edit( - _i7.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override _i4.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index 08e96b5a3..9218bd83e 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -209,7 +209,7 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -256,25 +256,6 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - @override - _i4.Future edit( - _i7.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override _i4.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index 3c66bf1f3..68ac1be3e 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -3,15 +3,15 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; +import 'dart:async' as _i4; import 'dart:ui' as _i7; -import 'package:barcode_scan2/barcode_scan2.dart' as _i2; +import 'package:flutter/material.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/models/node_model.dart' as _i9; import 'package:stackwallet/services/node_service.dart' as _i8; import 'package:stackwallet/services/wallets_service.dart' as _i6; -import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i4; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i2; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' as _i3; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' @@ -55,29 +55,28 @@ class _FakeSecureStorageInterface_1 extends _i1.SmartFake /// /// See the documentation for Mockito's code generation for more information. class MockBarcodeScannerWrapper extends _i1.Mock - implements _i4.BarcodeScannerWrapper { + implements _i2.BarcodeScannerWrapper { MockBarcodeScannerWrapper() { _i1.throwOnMissingStub(this); } @override - _i5.Future<_i2.ScanResult> scan( - {_i2.ScanOptions? options = const _i2.ScanOptions()}) => + _i4.Future<_i2.ScanResult> scan({required _i5.BuildContext? context}) => (super.noSuchMethod( Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), - returnValue: _i5.Future<_i2.ScanResult>.value(_FakeScanResult_0( + returnValue: _i4.Future<_i2.ScanResult>.value(_FakeScanResult_0( this, Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), )), - ) as _i5.Future<_i2.ScanResult>); + ) as _i4.Future<_i2.ScanResult>); } /// A class which mocks [WalletsService]. @@ -89,12 +88,12 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { } @override - _i5.Future> get walletNames => + _i4.Future> get walletNames => (super.noSuchMethod( Invocation.getter(#walletNames), - returnValue: _i5.Future>.value( + returnValue: _i4.Future>.value( {}), - ) as _i5.Future>); + ) as _i4.Future>); @override bool get hasListeners => (super.noSuchMethod( @@ -175,17 +174,17 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { ) as bool); @override - _i5.Future updateDefaults() => (super.noSuchMethod( + _i4.Future updateDefaults() => (super.noSuchMethod( Invocation.method( #updateDefaults, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future setPrimaryNodeFor({ + _i4.Future setPrimaryNodeFor({ required _i10.CryptoCurrency? coin, required _i9.NodeModel? node, bool? shouldNotifyListeners = false, @@ -200,9 +199,9 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { #shouldNotifyListeners: shouldNotifyListeners, }, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override _i9.NodeModel? getPrimaryNodeFor({required _i10.CryptoCurrency? currency}) => @@ -243,26 +242,26 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { ) as List<_i9.NodeModel>); @override - _i5.Future save( + _i4.Future save( _i9.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, shouldNotifyListeners, ], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future delete( + _i4.Future delete( String? id, bool? shouldNotifyListeners, ) => @@ -274,12 +273,12 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { shouldNotifyListeners, ], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future setEnabledState( + _i4.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, @@ -293,38 +292,19 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { shouldNotifyListeners, ], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future edit( - _i9.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - - @override - _i5.Future updateCommunityNodes() => (super.noSuchMethod( + _i4.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( #updateCommunityNodes, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override void addListener(_i7.VoidCallback? listener) => super.noSuchMethod( diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index d6d788327..b4eb99213 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -149,7 +149,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index 6a4ad08ce..787f1f801 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -149,7 +149,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart index b8fee8040..6a2a923e0 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart @@ -149,7 +149,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index 40a295640..d3a16fd4f 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -3,7 +3,7 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; import 'dart:ui' as _i13; import 'package:local_auth/local_auth.dart' as _i7; @@ -11,13 +11,13 @@ import 'package:local_auth_android/local_auth_android.dart' as _i8; import 'package:local_auth_darwin/local_auth_darwin.dart' as _i9; import 'package:local_auth_windows/local_auth_windows.dart' as _i10; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i6; +import 'package:mockito/src/dummies.dart' as _i4; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i3; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i2; import 'package:stackwallet/services/wallets_service.dart' as _i12; import 'package:stackwallet/utilities/biometrics.dart' as _i11; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' - as _i5; + as _i6; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -61,33 +61,13 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i2.ElectrumXClient); - @override - _i4.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i5.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( #base64ToHex, [source], ), - returnValue: _i6.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.method( #base64ToHex, @@ -102,7 +82,7 @@ class MockCachedElectrumXClient extends _i1.Mock #base64ToReverseHex, [source], ), - returnValue: _i6.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.method( #base64ToReverseHex, @@ -112,9 +92,9 @@ class MockCachedElectrumXClient extends _i1.Mock ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i5.CryptoCurrency? cryptoCurrency, + required _i6.CryptoCurrency? cryptoCurrency, bool? verbose = true, }) => (super.noSuchMethod( @@ -128,38 +108,21 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); - - @override - _i4.Future> getUsedCoinSerials({ - required _i5.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache( - {required _i5.CryptoCurrency? cryptoCurrency}) => + _i5.Future clearSharedTransactionCache( + {required _i6.CryptoCurrency? cryptoCurrency}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#cryptoCurrency: cryptoCurrency}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [LocalAuthentication]. @@ -172,13 +135,13 @@ class MockLocalAuthentication extends _i1.Mock } @override - _i4.Future get canCheckBiometrics => (super.noSuchMethod( + _i5.Future get canCheckBiometrics => (super.noSuchMethod( Invocation.getter(#canCheckBiometrics), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future authenticate({ + _i5.Future authenticate({ required String? localizedReason, Iterable<_i8.AuthMessages>? authMessages = const [ _i9.IOSAuthMessages(), @@ -197,37 +160,37 @@ class MockLocalAuthentication extends _i1.Mock #options: options, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future stopAuthentication() => (super.noSuchMethod( + _i5.Future stopAuthentication() => (super.noSuchMethod( Invocation.method( #stopAuthentication, [], ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future isDeviceSupported() => (super.noSuchMethod( + _i5.Future isDeviceSupported() => (super.noSuchMethod( Invocation.method( #isDeviceSupported, [], ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getAvailableBiometrics() => + _i5.Future> getAvailableBiometrics() => (super.noSuchMethod( Invocation.method( #getAvailableBiometrics, [], ), returnValue: - _i4.Future>.value(<_i8.BiometricType>[]), - ) as _i4.Future>); + _i5.Future>.value(<_i8.BiometricType>[]), + ) as _i5.Future>); } /// A class which mocks [Biometrics]. @@ -239,7 +202,7 @@ class MockBiometrics extends _i1.Mock implements _i11.Biometrics { } @override - _i4.Future authenticate({ + _i5.Future authenticate({ required String? cancelButtonText, required String? localizedReason, required String? title, @@ -254,8 +217,8 @@ class MockBiometrics extends _i1.Mock implements _i11.Biometrics { #title: title, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); } /// A class which mocks [WalletsService]. @@ -267,12 +230,12 @@ class MockWalletsService extends _i1.Mock implements _i12.WalletsService { } @override - _i4.Future> get walletNames => + _i5.Future> get walletNames => (super.noSuchMethod( Invocation.getter(#walletNames), - returnValue: _i4.Future>.value( + returnValue: _i5.Future>.value( {}), - ) as _i4.Future>); + ) as _i5.Future>); @override bool get hasListeners => (super.noSuchMethod( diff --git a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart index a7a3ad695..cd4db9558 100644 --- a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart @@ -3,11 +3,11 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i3; -import 'package:barcode_scan2/barcode_scan2.dart' as _i2; +import 'package:flutter/material.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i3; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -36,27 +36,26 @@ class _FakeScanResult_0 extends _i1.SmartFake implements _i2.ScanResult { /// /// See the documentation for Mockito's code generation for more information. class MockBarcodeScannerWrapper extends _i1.Mock - implements _i3.BarcodeScannerWrapper { + implements _i2.BarcodeScannerWrapper { MockBarcodeScannerWrapper() { _i1.throwOnMissingStub(this); } @override - _i4.Future<_i2.ScanResult> scan( - {_i2.ScanOptions? options = const _i2.ScanOptions()}) => + _i3.Future<_i2.ScanResult> scan({required _i4.BuildContext? context}) => (super.noSuchMethod( Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), - returnValue: _i4.Future<_i2.ScanResult>.value(_FakeScanResult_0( + returnValue: _i3.Future<_i2.ScanResult>.value(_FakeScanResult_0( this, Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), )), - ) as _i4.Future<_i2.ScanResult>); + ) as _i3.Future<_i2.ScanResult>); } diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index c840f4b50..35fbc67a1 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -733,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -803,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index 50706b9cc..7c455f73d 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -733,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -803,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 93185c901..86090d418 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -733,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -803,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index 287146e12..a397b611a 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -733,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -803,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart index 8a1cce451..861fa4327 100644 --- a/test/services/coins/particl/particl_wallet_test.mocks.dart +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -733,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -803,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 4b1944af8..14bc27820 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -905,6 +905,21 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get useMweb => (super.noSuchMethod( + Invocation.getter(#useMweb), + returnValue: false, + ) as bool); + + @override + set useMweb(bool? useMweb) => super.noSuchMethod( + Invocation.setter( + #useMweb, + useMweb, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -1263,7 +1278,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -1310,25 +1325,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - @override - _i10.Future edit( - _i23.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) as _i10.Future); - @override _i10.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/node_card_test.mocks.dart b/test/widget_tests/node_card_test.mocks.dart index 808f10fab..19c8c5166 100644 --- a/test/widget_tests/node_card_test.mocks.dart +++ b/test/widget_tests/node_card_test.mocks.dart @@ -149,7 +149,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index d01578f3f..a346758aa 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -780,6 +780,21 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get useMweb => (super.noSuchMethod( + Invocation.getter(#useMweb), + returnValue: false, + ) as bool); + + @override + set useMweb(bool? useMweb) => super.noSuchMethod( + Invocation.setter( + #useMweb, + useMweb, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -1067,7 +1082,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -1114,25 +1129,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - @override - _i10.Future edit( - _i19.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) as _i10.Future); - @override _i10.Future updateCommunityNodes() => (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 a090e757e..939703910 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -878,6 +878,21 @@ class MockPrefs extends _i1.Mock implements _i13.Prefs { returnValueForMissingStub: null, ); + @override + bool get useMweb => (super.noSuchMethod( + Invocation.getter(#useMweb), + returnValue: false, + ) as bool); + + @override + set useMweb(bool? useMweb) => super.noSuchMethod( + Invocation.setter( + #useMweb, + useMweb, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -1976,17 +1991,6 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { returnValue: _i10.Future.value(), returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - - @override - _i10.Future getHighestUsedMintIndex({required String? walletId}) => - (super.noSuchMethod( - Invocation.method( - #getHighestUsedMintIndex, - [], - {#walletId: walletId}, - ), - returnValue: _i10.Future.value(), - ) as _i10.Future); } /// A class which mocks [IThemeAssets]. diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index fc2c10fbb..d406a0c35 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -316,7 +316,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -363,25 +363,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); - @override - _i8.Future edit( - _i11.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); - @override _i8.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 5f16a8a2e..6018c1f93 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -456,7 +456,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -503,25 +503,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); - @override - _i9.Future edit( - _i15.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - @override _i9.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( From 009a90d737cc0c12897718de8d3a379cc6849cc1 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 11 Jun 2025 15:34:08 -0600 Subject: [PATCH 06/42] mweb utils --- .../global/mweb_status_changed_event.dart | 24 +++++++++++++ lib/utilities/prefs.dart | 34 +++++++++++++++++++ lib/utilities/stack_file_system.dart | 9 +++++ 3 files changed, 67 insertions(+) create mode 100644 lib/services/event_bus/events/global/mweb_status_changed_event.dart diff --git a/lib/services/event_bus/events/global/mweb_status_changed_event.dart b/lib/services/event_bus/events/global/mweb_status_changed_event.dart new file mode 100644 index 000000000..70dd4f3d6 --- /dev/null +++ b/lib/services/event_bus/events/global/mweb_status_changed_event.dart @@ -0,0 +1,24 @@ +/* + * 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 '../../../../utilities/logger.dart'; + +enum MwebStatus { enabled, disabled } + +class MwebPreferenceChangedEvent { + String? message; + MwebStatus status; + + MwebPreferenceChangedEvent({required this.status, this.message}) { + Logging.instance.w( + "MwebPreferenceChangedEvent changed to \"$status\" with message: $message", + ); + } +} diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 087aaa631..fa3872a7c 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -16,6 +16,7 @@ import 'package:uuid/uuid.dart'; import '../app_config.dart'; import '../db/hive/db.dart'; +import '../services/event_bus/events/global/mweb_status_changed_event.dart'; import '../services/event_bus/events/global/tor_status_changed_event.dart'; import '../services/event_bus/global_event_bus.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; @@ -1347,4 +1348,37 @@ class Prefs extends ChangeNotifier { return Level.warning; } } + + // enabled mweb + + bool _useMweb = false; + + bool get useMweb => _useMweb; + + set useMweb(bool useMweb) { + if (_useMweb != useMweb) { + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "useMweb", + value: useMweb, + ); + _useMweb = useMweb; + notifyListeners(); + GlobalEventBus.instance.fire( + MwebPreferenceChangedEvent( + status: useMweb ? MwebStatus.enabled : MwebStatus.disabled, + message: "useMweb updated in prefs", + ), + ); + } + } + + Future _getUseMweb() async { + return await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "useMweb", + ) + as bool? ?? + false; + } } diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index 7a254dffe..c7b91eca1 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -139,6 +139,15 @@ abstract class StackFileSystem { } } + static Future applicationMwebdDirectory(String network) async { + final root = await applicationRootDirectory(); + final dir = Directory(path.join(root.path, "mwebd", network)); + if (!dir.existsSync()) { + await dir.create(); + } + return dir; + } + static Future applicationFiroCacheSQLiteDirectory() async { final root = await applicationRootDirectory(); if (_createSubDirs) { From f57e53c749549db39cb32a303bc374c5c416aa13 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 12 Jun 2025 09:56:32 -0600 Subject: [PATCH 07/42] formatting --- .../sub_widgets/desktop_receive.dart | 350 +++++++++--------- 1 file changed, 166 insertions(+), 184 deletions(-) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index b90cc2f6a..51422b09c 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -22,7 +22,6 @@ import '../../../../models/isar/models/isar_models.dart'; import '../../../../models/keys/view_only_wallet_data.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/receive_view/generate_receiving_uri_qr_code_view.dart'; -import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; @@ -91,10 +90,9 @@ class _DesktopReceiveState extends ConsumerState { return WillPopScope( onWillPop: () async => shouldPop, child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.5), + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -109,8 +107,9 @@ class _DesktopReceiveState extends ConsumerState { if (wallet is Bip39HDWallet && wallet is! BCashInterface) { DerivePathType? type; if (wallet.isViewOnly && wallet is ExtendedKeysInterface) { - final voData = await wallet.getViewOnlyWalletData() - as ExtendedKeysViewOnlyWalletData; + final voData = + await wallet.getViewOnlyWalletData() + as ExtendedKeysViewOnlyWalletData; for (final t in wallet.cryptoCurrency.supportedDerivationPathTypes) { final testPath = wallet.cryptoCurrency.constructDerivePath( derivePathType: t, @@ -168,10 +167,9 @@ class _DesktopReceiveState extends ConsumerState { return WillPopScope( onWillPop: () async => shouldPop, child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.5), + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -209,7 +207,8 @@ class _DesktopReceiveState extends ConsumerState { if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { showMultiType = false; } else { - showMultiType = supportsSpark || + showMultiType = + supportsSpark || (wallet is! BCashInterface && wallet is Bip39HDWallet && wallet.supportedAddressTypes.length > 1); @@ -222,9 +221,9 @@ class _DesktopReceiveState extends ConsumerState { _walletAddressTypes.insert(0, AddressType.spark); } else { _walletAddressTypes.addAll( - (wallet as Bip39HDWallet) - .supportedAddressTypes - .where((e) => e != wallet.info.mainAddressType), + (wallet as Bip39HDWallet).supportedAddressTypes.where( + (e) => e != wallet.info.mainAddressType, + ), ); } } @@ -233,8 +232,9 @@ class _DesktopReceiveState extends ConsumerState { _walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh); } - _addressMap[_walletAddressTypes[_currentIndex]] = - ref.read(pWalletReceivingAddress(walletId)); + _addressMap[_walletAddressTypes[_currentIndex]] = ref.read( + pWalletReceivingAddress(walletId), + ); if (showMultiType) { for (final type in _walletAddressTypes) { @@ -250,15 +250,15 @@ class _DesktopReceiveState extends ConsumerState { .findFirst() .asStream() .listen((event) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _addressMap[type] = - event?.value ?? _addressMap[type] ?? "[No address yet]"; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[type] = + event?.value ?? _addressMap[type] ?? "[No address yet]"; + }); + } }); - } - }); - }); + }); } } @@ -284,8 +284,9 @@ class _DesktopReceiveState extends ConsumerState { address = ref.watch(pWalletReceivingAddress(walletId)); } - final wallet = - ref.watch(pWallets.select((value) => value.getWallet(walletId))); + final wallet = ref.watch( + pWallets.select((value) => value.getWallet(walletId)), + ); final bool canGen; if (wallet is ViewOnlyOptionInterface && @@ -301,91 +302,90 @@ class _DesktopReceiveState extends ConsumerState { children: [ ConditionalParent( condition: showMultiType, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - DropdownButtonHideUnderline( - child: DropdownButton2( - value: _currentIndex, - items: [ - for (int i = 0; i < _walletAddressTypes.length; i++) - DropdownMenuItem( - value: i, - child: Text( - supportsSpark && - _walletAddressTypes[i] == AddressType.p2pkh - ? "Transparent address" - : "${_walletAddressTypes[i].readableName} address", - style: STextStyles.w500_14(context), + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DropdownButtonHideUnderline( + child: DropdownButton2( + value: _currentIndex, + items: [ + for (int i = 0; i < _walletAddressTypes.length; i++) + DropdownMenuItem( + value: i, + child: Text( + supportsSpark && + _walletAddressTypes[i] == + AddressType.p2pkh + ? "Transparent address" + : "${_walletAddressTypes[i].readableName} address", + style: STextStyles.w500_14(context), + ), + ), + ], + onChanged: (value) { + if (value != null && value != _currentIndex) { + setState(() { + _currentIndex = value; + }); + } + }, + isExpanded: true, + iconStyleData: IconStyleData( + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), ), ), - ], - onChanged: (value) { - if (value != null && value != _currentIndex) { - setState(() { - _currentIndex = value; - }); - } - }, - isExpanded: true, - iconStyleData: IconStyleData( - icon: Padding( - padding: const EdgeInsets.only(right: 10), - child: SvgPicture.asset( - Assets.svg.chevronDown, - width: 12, - height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), ), - ), - ), - buttonStyleData: ButtonStyleData( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + dropdownStyleData: DropdownStyleData( + offset: const Offset(0, -10), + elevation: 0, + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), ), - ), - ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, -10), - elevation: 0, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), ), ), ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - ), - ), - ), - const SizedBox( - height: 12, + const SizedBox(height: 12), + child, + ], ), - child, - ], - ), child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { - clipboard.setData( - ClipboardData( - text: address, - ), - ); + clipboard.setData(ClipboardData(text: address)); showFloatingFlushBar( type: FlushBarType.info, message: "Copied to clipboard", @@ -396,9 +396,10 @@ class _DesktopReceiveState extends ConsumerState { child: Container( decoration: BoxDecoration( border: Border.all( - color: Theme.of(context) - .extension()! - .backgroundAppBar, + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, width: 1, ), borderRadius: BorderRadius.circular( @@ -411,11 +412,7 @@ class _DesktopReceiveState extends ConsumerState { Row( children: [ Text( - "Your ${widget.contractAddress == null ? coin.ticker : ref.watch( - pCurrentTokenWallet.select( - (value) => value!.tokenContract.symbol, - ), - )} address", + "Your ${widget.contractAddress == null ? coin.ticker : ref.watch(pCurrentTokenWallet.select((value) => value!.tokenContract.symbol))} address", style: STextStyles.itemSubtitle(context), ), const Spacer(), @@ -425,24 +422,18 @@ class _DesktopReceiveState extends ConsumerState { Assets.svg.copy, width: 15, height: 15, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), + color: + Theme.of( + context, + ).extension()!.infoItemIcons, ), + const SizedBox(width: 4), + Text("Copy", style: STextStyles.link2(context)), ], ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Row( children: [ Expanded( @@ -451,9 +442,10 @@ class _DesktopReceiveState extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ), @@ -467,85 +459,75 @@ class _DesktopReceiveState extends ConsumerState { ), ), - if (canGen) - const SizedBox( - height: 20, - ), + if (canGen) const SizedBox(height: 20), if (canGen) SecondaryButton( buttonHeight: ButtonHeight.l, - onPressed: supportsSpark && - _walletAddressTypes[_currentIndex] == AddressType.spark - ? generateNewSparkAddress - : generateNewAddress, + onPressed: + supportsSpark && + _walletAddressTypes[_currentIndex] == AddressType.spark + ? generateNewSparkAddress + : generateNewAddress, label: "Generate new address", ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), Center( child: QR( - data: AddressUtils.buildUriString( - coin.uriScheme, - address, - {}, - ), + data: AddressUtils.buildUriString(coin.uriScheme, address, {}), size: 200, ), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), // TODO: create transparent button class to account for hover GestureDetector( onTap: () async { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: double.infinity, - maxWidth: 580, - child: Column( - children: [ - Row( + builder: + (context) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( children: [ - const AppBarBackButton( - size: 40, - iconSize: 24, + Row( + children: [ + const AppBarBackButton(size: 40, iconSize: 24), + Text( + "Generate QR code", + style: STextStyles.desktopH3(context), + ), + ], ), - Text( - "Generate QR code", - style: STextStyles.desktopH3(context), + IntrinsicHeight( + child: Navigator( + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: + (_, __) => [ + RouteGenerator.generateRoute( + RouteSettings( + name: GenerateUriQrCodeView.routeName, + arguments: Tuple2(coin, address), + ), + ), + ], + ), ), ], ), - IntrinsicHeight( - child: Navigator( - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) => [ - RouteGenerator.generateRoute( - RouteSettings( - name: GenerateUriQrCodeView.routeName, - arguments: Tuple2(coin, address), - ), - ), - ], - ), - ), - ], - ), - ), + ), ); } else { unawaited( Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: coin, - receivingAddress: address, - ), + builder: + (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: address, + ), settings: const RouteSettings( name: GenerateUriQrCodeView.routeName, ), @@ -564,21 +546,21 @@ class _DesktopReceiveState extends ConsumerState { Assets.svg.qrcode, width: 14, height: 16, - color: Theme.of(context) - .extension()! - .accentColorBlue, - ), - const SizedBox( - width: 8, + color: + Theme.of( + context, + ).extension()!.accentColorBlue, ), + const SizedBox(width: 8), Padding( padding: const EdgeInsets.only(bottom: 2), child: Text( "Create new QR code", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorBlue, + color: + Theme.of( + context, + ).extension()!.accentColorBlue, ), ), ), From 6a055951d476e3bcdc2b15d0a6a3bf01affb9dfc Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 12 Jun 2025 16:23:21 -0600 Subject: [PATCH 08/42] WIP: mweb integration - desktop focus --- lib/db/drift/database.dart | 22 + .../wallet_network_settings_view.dart | 39 +- .../mweb_utxos_view.dart | 281 +++++++++ .../sub_widgets/desktop_receive.dart | 96 ++- .../sub_widgets/desktop_wallet_features.dart | 23 +- .../more_features/more_features_dialog.dart | 159 ++++- .../sub_widgets/network_info_button.dart | 176 +++--- lib/route_generator.dart | 11 + lib/services/mwebd_service.dart | 228 ++++++++ lib/utilities/prefs.dart | 34 -- lib/utilities/stack_file_system.dart | 2 +- lib/wallets/isar/models/wallet_info.dart | 16 + lib/wallets/models/tx_data.dart | 18 + lib/wallets/wallet/impl/litecoin_wallet.dart | 2 + .../mweb_interface.dart | 552 ++++++++++++++++++ pubspec.lock | 6 +- scripts/app_config/templates/pubspec.template | 3 +- 17 files changed, 1514 insertions(+), 154 deletions(-) create mode 100644 lib/pages_desktop_specific/mweb_utxos_view.dart create mode 100644 lib/services/mwebd_service.dart diff --git a/lib/db/drift/database.dart b/lib/db/drift/database.dart index c2ed93a46..4bfd94341 100644 --- a/lib/db/drift/database.dart +++ b/lib/db/drift/database.dart @@ -9,6 +9,7 @@ */ import 'dart:async'; +import 'dart:math' as math; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; @@ -57,6 +58,27 @@ class MwebUtxos extends Table { Set get primaryKey => {outputId}; } +extension MwebUtxoExt on MwebUtxo { + int getConfirmations(int currentChainHeight) { + if (blockTime <= 0) return 0; + if (height <= 0) return 0; + return math.max(0, currentChainHeight - (height - 1)); + } + + bool isConfirmed( + int currentChainHeight, + int minimumConfirms, { + int? overrideMinConfirms, + }) { + final confirmations = getConfirmations(currentChainHeight); + + if (overrideMinConfirms != null) { + return confirmations >= overrideMinConfirms; + } + return confirmations >= minimumConfirms; + } +} + @DriftDatabase(tables: [SparkNames, MwebUtxos]) final class WalletDatabase extends _$WalletDatabase { WalletDatabase._(String walletId, [QueryExecutor? executor]) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index 506f1167c..27e7cc63c 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -33,13 +33,16 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/coins/epiccash.dart'; +import '../../../../wallets/crypto_currency/coins/litecoin.dart'; import '../../../../wallets/crypto_currency/coins/monero.dart'; +import '../../../../wallets/crypto_currency/coins/salvium.dart'; import '../../../../wallets/crypto_currency/coins/wownero.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; -import '../../../../wallets/wallet/impl/monero_wallet.dart'; -import '../../../../wallets/wallet/impl/wownero_wallet.dart'; +import '../../../../wallets/wallet/impl/salvium_wallet.dart'; +import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../widgets/animated_text.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/conditional_parent.dart'; @@ -269,7 +272,13 @@ class _WalletNetworkSettingsViewState final coin = ref.read(pWalletCoin(widget.walletId)); - if (coin is Monero || coin is Wownero || coin is Epiccash) { + // TODO: handle isMwebEnabled toggled + if (coin is Monero || + coin is Wownero || + coin is Epiccash || + coin is Salvium || + (coin is Litecoin && + ref.read(pWalletInfo(widget.walletId)).isMwebEnabled)) { _blocksRemainingSubscription = eventBus.on().listen( (event) async { if (event.walletId == widget.walletId) { @@ -329,16 +338,23 @@ class _WalletNetworkSettingsViewState final coin = ref.watch(pWalletCoin(widget.walletId)); - if (coin is Monero) { + if (coin is Salvium) { final double highestPercent = - (ref.read(pWallets).getWallet(widget.walletId) as MoneroWallet) + (ref.read(pWallets).getWallet(widget.walletId) as SalviumWallet) .highestPercentCached; if (_percent < highestPercent) { _percent = highestPercent.clamp(0.0, 1.0); } - } else if (coin is Wownero) { + } else if (coin is Monero || coin is Wownero) { final double highestPercent = - (ref.watch(pWallets).getWallet(widget.walletId) as WowneroWallet) + (ref.read(pWallets).getWallet(widget.walletId) as LibMoneroWallet) + .highestPercentCached; + if (_percent < highestPercent) { + _percent = highestPercent.clamp(0.0, 1.0); + } + } else if (coin is Litecoin) { + final double highestPercent = + (ref.watch(pWallets).getWallet(widget.walletId) as MwebInterface) .highestPercentCached; if (_percent < highestPercent) { _percent = highestPercent.clamp(0.0, 1.0); @@ -637,7 +653,14 @@ class _WalletNetworkSettingsViewState ), if (coin is Monero || coin is Wownero || - coin is Epiccash) + coin is Epiccash || + coin is Salvium || + (coin is Litecoin && + ref.watch( + pWalletInfo( + widget.walletId, + ).select((s) => s.isMwebEnabled), + ))) Text( " (Blocks to go: ${_blocksRemaining == -1 ? "?" : _blocksRemaining})", style: STextStyles.syncPercent( diff --git a/lib/pages_desktop_specific/mweb_utxos_view.dart b/lib/pages_desktop_specific/mweb_utxos_view.dart new file mode 100644 index 000000000..0c28cdf66 --- /dev/null +++ b/lib/pages_desktop_specific/mweb_utxos_view.dart @@ -0,0 +1,281 @@ +/* + * 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 2025-06-12 + * + */ + +import 'dart:async'; + +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/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../widgets/background.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 '../db/drift/database.dart'; +import '../providers/providers.dart'; +import '../widgets/detail_item.dart'; +import '../widgets/rounded_white_container.dart'; + +class MwebUtxosView extends ConsumerWidget { + const MwebUtxosView({super.key, required this.walletId}); + + static const title = "MWEB outputs"; + static const String routeName = "/mwebUtxosView"; + + final String walletId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) { + return DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: Row( + children: [ + const SizedBox(width: 32), + 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, + ), + const SizedBox(width: 12), + Text(title, style: STextStyles.desktopH3(context)), + const Spacer(), + ], + ), + ), + useSpacers: false, + isCompactHeight: true, + ), + body: Padding(padding: const EdgeInsets.all(24), child: child), + ); + }, + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + automaticallyImplyLeading: false, + leading: AppBarBackButton( + onPressed: () => Navigator.of(context).pop(), + ), + title: Text(title, style: STextStyles.navBarTitle(context)), + ), + body: SafeArea(child: child), + ), + ); + }, + child: StreamViewList( + itemName: title, + stream: + ref + .read(pDrift(walletId)) + .select(ref.read(pDrift(walletId)).mwebUtxos) + .watch(), + itemBuilder: (MwebUtxo? coin) { + return [ + ("Output Id", coin?.outputId ?? "", 9), + ("Address", coin?.address ?? "", 9), + ("Value (sats)", coin?.value.toString() ?? "", 3), + ("Height", coin?.height.toString() ?? "", 2), + ("Block time", coin?.blockTime.toString() ?? "", 2), + ("Blocked", coin?.blocked.toString() ?? "", 2), + ("Used", coin?.used.toString() ?? "", 2), + ]; + }, + ), + ), + ); + } +} + +class StreamViewList extends StatefulWidget { + const StreamViewList({ + super.key, + required this.stream, + required this.itemBuilder, + required this.itemName, + }); + + final Stream> stream; + final String itemName; + final List<(String title, String value, int flex)> Function(T?) itemBuilder; + + @override + State> createState() => _StreamViewListState(); +} + +class _StreamViewListState extends State> { + List _items = []; + + late final StreamSubscription> _streamSubscription; + + void _onMwebUtxossCollectionWatcherEvent(List items) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _items = items; + }); + } + }); + } + + @override + void initState() { + super.initState(); + + _streamSubscription = widget.stream.listen( + (data) => _onMwebUtxossCollectionWatcherEvent(data), + ); + } + + @override + void dispose() { + _streamSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Row( + children: [ + Text( + "Total ${widget.itemName}: ${_items.length}", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Row( + children: [ + ...widget + .itemBuilder(null) + .map( + (e) => Expanded( + flex: e.$3, + child: Text( + e.$1, + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ), + ), + ], + ), + ), + ), + Expanded( + child: ListView.separated( + shrinkWrap: true, + itemCount: _items.length, + separatorBuilder: + (_, __) => Container( + height: 1, + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, + ), + itemBuilder: + (_, index) => Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ...widget + .itemBuilder(_items[index]) + .map( + (e) => Expanded( + flex: e.$3, + child: SelectableText( + e.$2, + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.left, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ); + } else { + return ListView.builder( + itemCount: _items.length + 1, + itemBuilder: (ctx, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 16, left: 16, right: 16), + child: RoundedWhiteContainer( + child: + index == 0 + ? Row( + children: [ + Text( + "Total ${widget.itemName}: ${_items.length}", + style: STextStyles.itemSubtitle(context), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...widget + .itemBuilder(_items[index - 1]) + .map( + (e) => DetailItem(title: e.$1, detail: e.$2), + ), + ], + ), + ), + ); + }, + ); + } + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 51422b09c..20e780c97 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -30,6 +30,7 @@ import '../../../../utilities/assets.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; @@ -40,6 +41,7 @@ import '../../../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../../widgets/conditional_parent.dart'; @@ -71,6 +73,7 @@ class _DesktopReceiveState extends ConsumerState { late final String walletId; late final ClipboardInterface clipboard; late final bool supportsSpark; + late bool supportsMweb; late final bool showMultiType; int _currentIndex = 0; @@ -196,19 +199,50 @@ class _DesktopReceiveState extends ConsumerState { } } + Future
_generateNewMwebAddress() async { + final wallet = ref.read(pWallets).getWallet(walletId) as MwebInterface; + + final address = await wallet.generateNextMwebAddress(); + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.addresses.put(address); + }); + + return address; + } + + Future generateNewMwebAddress() async { + final address = await showLoading
( + whileFuture: _generateNewMwebAddress(), + context: context, + message: "Generating address", + rootNavigator: Util.isDesktop, + ); + + if (mounted && address != null) { + setState(() { + _addressMap[AddressType.mweb] = address.value; + }); + } + } + @override void initState() { walletId = widget.walletId; coin = ref.read(pWalletInfo(walletId)).coin; clipboard = widget.clipboard; final wallet = ref.read(pWallets).getWallet(walletId); - supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface; + supportsSpark = wallet is SparkInterface; + supportsMweb = + wallet is MwebInterface && + !wallet.info.isViewOnly && + wallet.info.isMwebEnabled; if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { showMultiType = false; } else { showMultiType = supportsSpark || + supportsMweb || (wallet is! BCashInterface && wallet is Bip39HDWallet && wallet.supportedAddressTypes.length > 1); @@ -225,6 +259,10 @@ class _DesktopReceiveState extends ConsumerState { (e) => e != wallet.info.mainAddressType, ), ); + + if (supportsMweb) { + _walletAddressTypes.insert(0, AddressType.mweb); + } } } @@ -277,6 +315,54 @@ class _DesktopReceiveState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + ref.listen(pWalletInfo(walletId), (prev, next) { + if (prev?.isMwebEnabled != next.isMwebEnabled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + supportsMweb = next.isMwebEnabled; + + if (supportsMweb && + !_walletAddressTypes.contains(AddressType.mweb)) { + _walletAddressTypes.insert(0, AddressType.mweb); + _addressSubMap[AddressType.mweb] = ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .sortByDerivationIndexDesc() + .findFirst() + .asStream() + .listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[AddressType.mweb] = + event?.value ?? + _addressMap[AddressType.mweb] ?? + "[No address yet]"; + }); + } + }); + }); + } else { + _walletAddressTypes.remove(AddressType.mweb); + _addressSubMap[AddressType.mweb]?.cancel(); + _addressSubMap.remove(AddressType.mweb); + } + + if (_currentIndex >= _walletAddressTypes.length) { + _currentIndex = _walletAddressTypes.length - 1; + } + }); + } + }); + } + }); + final String address; if (showMultiType) { address = _addressMap[_walletAddressTypes[_currentIndex]]!; @@ -294,7 +380,8 @@ class _DesktopReceiveState extends ConsumerState { wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) { canGen = false; } else { - canGen = (wallet is MultiAddressInterface || supportsSpark); + canGen = + (wallet is MultiAddressInterface || supportsSpark || supportsMweb); } return Column( @@ -465,7 +552,10 @@ class _DesktopReceiveState extends ConsumerState { SecondaryButton( buttonHeight: ButtonHeight.l, onPressed: - supportsSpark && + supportsMweb && + _walletAddressTypes[_currentIndex] == AddressType.mweb + ? generateNewMwebAddress + : supportsSpark && _walletAddressTypes[_currentIndex] == AddressType.spark ? generateNewSparkAddress : generateNewAddress, 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 2456a909c..d06d84400 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 @@ -44,6 +44,7 @@ import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../wallets/wallet/wallet.dart' show Wallet; import '../../../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; @@ -59,6 +60,7 @@ import '../../../cashfusion/desktop_cashfusion_view.dart'; import '../../../churning/desktop_churning_view.dart'; import '../../../coin_control/desktop_coin_control_view.dart'; import '../../../desktop_menu.dart'; +import '../../../mweb_utxos_view.dart'; import '../../../ordinals/desktop_ordinals_view.dart'; import '../../../spark_coins/spark_coins_view.dart'; import '../desktop_wallet_view.dart'; @@ -74,6 +76,7 @@ enum WalletFeature { "Control, freeze, and utilize outputs at your discretion", ), sparkCoins("Spark coins", "View wallet spark coins"), + mwebUtxos("MWEB outputs", "View wallet MWEB outputs"), ordinals("Ordinals", "View and control your ordinals in ${AppConfig.prefix}"), monkey("MonKey", "Generate Banano MonKey"), fusion("Fusion", "Decentralized mixing protocol"), @@ -84,7 +87,8 @@ enum WalletFeature { // special cases clearSparkCache("", ""), rbf("", ""), - reuseAddress("", ""); + reuseAddress("", ""), + enableMweb("", ""); final String label; final String description; @@ -138,6 +142,12 @@ class _DesktopWalletFeaturesState extends ConsumerState { ).pushNamed(SparkCoinsView.routeName, arguments: widget.walletId); } + void _onMwebUtxosPressed() { + Navigator.of( + context, + ).pushNamed(MwebUtxosView.routeName, arguments: widget.walletId); + } + Future _onAnonymizeAllPressed() async { await showDialog( context: context, @@ -413,6 +423,13 @@ class _DesktopWalletFeaturesState extends ConsumerState { _onSparkCoinsPressed, ), + if (!isViewOnly && wallet is MwebInterface) + ( + WalletFeature.mwebUtxos, + Assets.svg.coinControl.gamePad, + _onMwebUtxosPressed, + ), + if (!isViewOnly && wallet is PaynymInterface) (WalletFeature.paynym, Assets.svg.robotHead, _onPaynymPressed), @@ -461,6 +478,8 @@ class _DesktopWalletFeaturesState extends ConsumerState { wallet.isViewOnly && wallet.viewOnlyType == ViewOnlyWalletType.addressOnly; + final showMwebOption = wallet is MwebInterface && !wallet.isViewOnly; + final extraOptions = [ if (wallet is SparkInterface && !isViewOnly) (WalletFeature.clearSparkCache, Assets.svg.key, () => ()), @@ -469,6 +488,8 @@ class _DesktopWalletFeaturesState extends ConsumerState { if (!isViewOnlyNoAddressGen) (WalletFeature.reuseAddress, Assets.svg.key, () => ()), + + if (showMwebOption) (WalletFeature.enableMweb, Assets.svg.key, () => ()), ]; return StaticOverflowRow( 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 ab48068b4..80f55b664 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 @@ -69,7 +69,8 @@ class _MoreFeaturesDialogState extends ConsumerState { } } - late final DSBController _switchController; + late final DSBController _switchControllerAddressReuse; + late final DSBController _switchControllerMwebToggle; bool _switchReuseAddressToggledLock = false; // Mutex. Future _switchReuseAddressToggled() async { @@ -79,7 +80,7 @@ class _MoreFeaturesDialogState extends ConsumerState { _switchReuseAddressToggledLock = true; // Lock mutex. try { - if (_switchController.isOn?.call() != true) { + if (_switchControllerAddressReuse.isOn?.call() != true) { final canContinue = await showDialog( context: context, builder: (context) { @@ -168,16 +169,123 @@ class _MoreFeaturesDialogState extends ConsumerState { isar: ref.read(mainDBProvider).isar, ); - if (_switchController.isOn != null) { - if (_switchController.isOn!.call() != shouldReuse) { - _switchController.activate?.call(); + if (_switchControllerAddressReuse.isOn != null) { + if (_switchControllerAddressReuse.isOn!.call() != shouldReuse) { + _switchControllerAddressReuse.activate?.call(); + } + } + } + + bool _switchMwebToggleToggledLock = false; // Mutex. + Future _switchMwebToggleToggled() async { + if (_switchMwebToggleToggledLock) { + return; + } + _switchMwebToggleToggledLock = true; // Lock mutex. + + try { + if (_switchControllerMwebToggle.isOn?.call() != true) { + final canContinue = await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 576, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Warning!", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 8, + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "TODO warning about extra data downloaded and initial sync/scan time", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 43), + Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(false); + }, + label: "Cancel", + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(true); + }, + label: "Continue", + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + }, + ); + + if (canContinue == true) { + await _updateMwebToggle(true); + + unawaited(ref.read(pWallets).getWallet(widget.walletId).init()); + } + } else { + await _updateMwebToggle(false); + } + } finally { + // ensure _switchMwebToggleToggledLock is set to false no matter what. + _switchMwebToggleToggledLock = false; + } + } + + Future _updateMwebToggle(bool shouldReuse) async { + await ref + .read(pWalletInfo(widget.walletId)) + .updateOtherData( + newEntries: {WalletInfoKeys.mwebEnabled: shouldReuse}, + isar: ref.read(mainDBProvider).isar, + ); + + if (_switchControllerMwebToggle.isOn != null) { + if (_switchControllerMwebToggle.isOn!.call() != shouldReuse) { + _switchControllerMwebToggle.activate?.call(); } } } @override void initState() { - _switchController = DSBController(); + _switchControllerAddressReuse = DSBController(); + _switchControllerMwebToggle = DSBController(); super.initState(); } @@ -281,7 +389,7 @@ class _MoreFeaturesDialogState extends ConsumerState { )[WalletInfoKeys.reuseAddress] as bool? ?? false, - controller: _switchController, + controller: _switchControllerAddressReuse, ), ), ), @@ -299,6 +407,43 @@ class _MoreFeaturesDialogState extends ConsumerState { ), ); + case WalletFeature.enableMweb: + return _MoreFeaturesItemBase( + onPressed: _switchMwebToggleToggled, + child: Row( + children: [ + const SizedBox(width: 3), + SizedBox( + height: 20, + width: 40, + child: IgnorePointer( + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select((value) => value.otherData), + )[WalletInfoKeys.mwebEnabled] + as bool? ?? + false, + controller: _switchControllerMwebToggle, + ), + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enable MWEB", + style: STextStyles.w600_20(context), + ), + ], + ), + ], + ), + ); + default: return _MoreFeaturesItem( label: option.$1.label, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart index 7fbcef510..7a7bc88ca 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart @@ -30,11 +30,7 @@ import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; class NetworkInfoButton extends ConsumerStatefulWidget { - const NetworkInfoButton({ - super.key, - required this.walletId, - this.eventBus, - }); + const NetworkInfoButton({super.key, required this.walletId, this.eventBus}); final String walletId; final EventBus? eventBus; @@ -74,27 +70,25 @@ class _NetworkInfoButtonState extends ConsumerState { } } - _syncStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - setState(() { - _currentSyncStatus = event.newStatus; - }); - } - }, - ); + _syncStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == widget.walletId) { + setState(() { + _currentSyncStatus = event.newStatus; + }); + } + }); - _nodeStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - setState(() { - _currentNodeStatus = event.newStatus; - }); - } - }, - ); + _nodeStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == widget.walletId) { + setState(() { + _currentNodeStatus = event.newStatus; + }); + } + }); super.initState(); } @@ -151,9 +145,9 @@ class _NetworkInfoButtonState extends ConsumerState { return Text( label, - style: STextStyles.desktopMenuItemSelected(context).copyWith( - color: _getColor(status, context), - ), + style: STextStyles.desktopMenuItemSelected( + context, + ).copyWith(color: _getColor(status, context)), ); } @@ -172,9 +166,7 @@ class _NetworkInfoButtonState extends ConsumerState { Widget build(BuildContext context) { return RawMaterialButton( hoverColor: _getColor(_currentSyncStatus, context).withOpacity(0.1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(1000), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(1000)), onPressed: () { if (Util.isDesktop) { // showDialog( @@ -220,88 +212,80 @@ class _NetworkInfoButtonState extends ConsumerState { showDialog( context: context, - builder: (context) => Navigator( - initialRoute: WalletNetworkSettingsView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - DesktopDialog( - maxHeight: null, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Network", - style: STextStyles.desktopH3(context), - ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, + builder: + (context) => Navigator( + initialRoute: WalletNetworkSettingsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Network", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], ), - ], - ), - ), - Flexible( - child: Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - right: 32, - bottom: 32, ), - child: SingleChildScrollView( - child: WalletNetworkSettingsView( - walletId: walletId, - initialSyncStatus: _currentSyncStatus, - initialNodeStatus: _currentNodeStatus, + Flexible( + child: Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 32, + ), + child: SingleChildScrollView( + child: WalletNetworkSettingsView( + walletId: walletId, + initialSyncStatus: _currentSyncStatus, + initialNodeStatus: _currentNodeStatus, + ), + ), ), ), - ), + ], ), - ], + ), + const RouteSettings( + name: WalletNetworkSettingsView.routeName, + ), ), - ), - const RouteSettings( - name: WalletNetworkSettingsView.routeName, - ), - ), - ]; - }, - ), + ]; + }, + ), ); } else { Navigator.of(context).pushNamed( WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - _currentNodeStatus, - ), + arguments: Tuple3(walletId, _currentSyncStatus, _currentNodeStatus), ); } }, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 32), child: Row( children: [ _buildNetworkIcon(_currentSyncStatus, context), - const SizedBox( - width: 6, - ), + const SizedBox(width: 6), _buildText(_currentSyncStatus, context), ], ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 7c0f4ea13..3266d1273 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -178,6 +178,7 @@ import 'pages_desktop_specific/desktop_buy/desktop_buy_view.dart'; import 'pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart'; import 'pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'pages_desktop_specific/desktop_home_view.dart'; +import 'pages_desktop_specific/mweb_utxos_view.dart'; import 'pages_desktop_specific/my_stack_view/my_stack_view.dart'; import 'pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart'; import 'pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; @@ -2227,6 +2228,16 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case MwebUtxosView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => MwebUtxosView(walletId: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopCoinControlView.routeName: if (args is String) { return getRoute( diff --git a/lib/services/mwebd_service.dart b/lib/services/mwebd_service.dart new file mode 100644 index 000000000..00ace0d24 --- /dev/null +++ b/lib/services/mwebd_service.dart @@ -0,0 +1,228 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter_mwebd/flutter_mwebd.dart'; +import 'package:mutex/mutex.dart'; +import 'package:mweb_client/mweb_client.dart'; + +import '../utilities/logger.dart'; +import '../utilities/prefs.dart'; +import '../utilities/stack_file_system.dart'; +import '../wallets/crypto_currency/crypto_currency.dart'; +import 'event_bus/events/global/mweb_status_changed_event.dart'; +import 'event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'event_bus/events/global/tor_status_changed_event.dart'; +import 'event_bus/global_event_bus.dart'; +import 'tor_service.dart'; + +final class MwebdService { + final Map + _map = {}; + + late final StreamSubscription + _torStatusListener; + late final StreamSubscription + _torPreferenceListener; + late final StreamSubscription + _mwebPreferenceListener; + + final Mutex _torConnectingLock = Mutex(); + + static final instance = MwebdService._(); + + MwebdService._() { + final bus = GlobalEventBus.instance; + + // Listen for tor status changes. + _torStatusListener = bus.on().listen(( + event, + ) async { + switch (event.newStatus) { + case TorConnectionStatus.connecting: + if (!_torConnectingLock.isLocked) { + await _torConnectingLock.acquire(); + } + break; + + case TorConnectionStatus.connected: + case TorConnectionStatus.disconnected: + if (_torConnectingLock.isLocked) { + _torConnectingLock.release(); + } + break; + } + }); + + // Listen for tor preference changes. + _torPreferenceListener = bus.on().listen(( + event, + ) async { + if (Prefs.instance.useTor) { + return await _torConnectingLock.protect(() async { + final proxyInfo = TorService.sharedInstance.getProxyInfo(); + return await _update(proxyInfo); + }); + } else { + return await _update(null); + } + }); + + // Listen for mweb preference changes. + _mwebPreferenceListener = bus.on().listen(( + event, + ) async { + switch (event.status) { + case MwebStatus.enabled: + // as we don't know which network(s) to use here + // we will rely on status checks and init as required within wallets + break; + + case MwebStatus.disabled: + final nets = _map.keys; + for (final net in nets) { + final old = _map.remove(net)!; + await old.client.cleanup(); + await old.server.stopServer(); + } + break; + } + }); + } + + // locked while mweb servers and clients are updating + final _updateLock = Mutex(); + + // update function called when Tor pref changed + Future _update(({InternetAddress host, int port})? proxyInfo) async { + await _updateLock.protect(() async { + final proxy = + proxyInfo == null + ? "" + : "${proxyInfo.host.address}:${proxyInfo.port}"; + final nets = _map.keys; + for (final net in nets) { + final old = _map.remove(net)!; + + await old.client.cleanup(); + await old.server.stopServer(); + + final port = await _getRandomUnusedPort(); + if (port == null) { + throw Exception("Could not find an unused port for mwebd"); + } + + final newServer = MwebdServer( + chain: old.server.chain, + dataDir: old.server.dataDir, + peer: old.server.peer, + proxy: proxy, + serverPort: port, + ); + await newServer.createServer(); + await newServer.startServer(); + + final newClient = MwebClient.fromHost( + "127.0.0.1", + newServer.serverPort, + ); + + _map[net] = (server: newServer, client: newClient); + } + }); + } + + Future init(CryptoCurrencyNetwork net) async { + Logging.instance.i("MwebdService init($net) called..."); + await _updateLock.protect(() async { + if (_map[net] != null) { + Logging.instance.i("MwebdService init($net) was already called."); + return; + } + + final port = await _getRandomUnusedPort(); + + if (port == null) { + throw Exception("Could not find an unused port for mwebd"); + } + + final chain = switch (net) { + CryptoCurrencyNetwork.main => "mainnet", + CryptoCurrencyNetwork.test => "testnet", + CryptoCurrencyNetwork.stage => throw UnimplementedError(), + CryptoCurrencyNetwork.test4 => throw UnimplementedError(), + }; + + final dir = await StackFileSystem.applicationMwebdDirectory(chain); + + final String proxy; + if (Prefs.instance.useTor) { + final proxyInfo = TorService.sharedInstance.getProxyInfo(); + proxy = "${proxyInfo.host.address}:${proxyInfo.port}"; + } else { + proxy = ""; + } + + final newServer = MwebdServer( + chain: chain, + dataDir: dir.path, + peer: "", + proxy: proxy, + serverPort: port, + ); + await newServer.createServer(); + await newServer.startServer(); + + final newClient = MwebClient.fromHost("127.0.0.1", newServer.serverPort); + + _map[net] = (server: newServer, client: newClient); + + Logging.instance.i("MwebdService init($net) completed!"); + }); + } + + /// Get server status. Returns null if no server was initialized. + Future getServerStatus(CryptoCurrencyNetwork net) async { + return await _updateLock.protect(() async { + return await _map[net]?.server.getStatus(); + }); + } + + /// Get client for network. Returns null if no server was initialized. + Future getClient(CryptoCurrencyNetwork net) async { + return await _updateLock.protect(() async { + return _map[net]?.client; + }); + } +} + +// ============================================================================ +Future _getRandomUnusedPort({Set excluded = const {}}) async { + const int minPort = 1024; + const int maxPort = 65535; + const int maxAttempts = 1000; + + final random = Random.secure(); + + for (int i = 0; i < maxAttempts; i++) { + final int potentialPort = minPort + random.nextInt(maxPort - minPort + 1); + + if (excluded.contains(potentialPort)) { + continue; + } + + try { + final ServerSocket socket = await ServerSocket.bind( + InternetAddress.anyIPv4, + potentialPort, + ); + await socket.close(); + return potentialPort; + } catch (_) { + excluded.add(potentialPort); + continue; + } + } + + return null; +} diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index fa3872a7c..087aaa631 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -16,7 +16,6 @@ import 'package:uuid/uuid.dart'; import '../app_config.dart'; import '../db/hive/db.dart'; -import '../services/event_bus/events/global/mweb_status_changed_event.dart'; import '../services/event_bus/events/global/tor_status_changed_event.dart'; import '../services/event_bus/global_event_bus.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; @@ -1348,37 +1347,4 @@ class Prefs extends ChangeNotifier { return Level.warning; } } - - // enabled mweb - - bool _useMweb = false; - - bool get useMweb => _useMweb; - - set useMweb(bool useMweb) { - if (_useMweb != useMweb) { - DB.instance.put( - boxName: DB.boxNamePrefs, - key: "useMweb", - value: useMweb, - ); - _useMweb = useMweb; - notifyListeners(); - GlobalEventBus.instance.fire( - MwebPreferenceChangedEvent( - status: useMweb ? MwebStatus.enabled : MwebStatus.disabled, - message: "useMweb updated in prefs", - ), - ); - } - } - - Future _getUseMweb() async { - return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "useMweb", - ) - as bool? ?? - false; - } } diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index c7b91eca1..e13557739 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -143,7 +143,7 @@ abstract class StackFileSystem { final root = await applicationRootDirectory(); final dir = Directory(path.join(root.path, "mwebd", network)); if (!dir.existsSync()) { - await dir.create(); + await dir.create(recursive: true); } return dir; } diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index bb4dd4ab2..6df8dc456 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -139,6 +139,10 @@ class WalletInfo implements IsarId { bool get isDuressVisible => otherData[WalletInfoKeys.duressMarkedVisibleWalletKey] as bool? ?? false; + @ignore + bool get isMwebEnabled => + otherData[WalletInfoKeys.mwebEnabled] as bool? ?? false; + //============================================================================ //============= Updaters ================================================ @@ -392,6 +396,16 @@ class WalletInfo implements IsarId { ); } + Future setMwebEnabled({ + required bool newValue, + required Isar isar, + }) async { + await updateOtherData( + newEntries: {WalletInfoKeys.mwebEnabled: newValue}, + isar: isar, + ); + } + //============================================================================ WalletInfo({ @@ -505,4 +519,6 @@ abstract class WalletInfoKeys { static const String viewOnlyTypeIndexKey = "viewOnlyTypeIndexKey"; static const String duressMarkedVisibleWalletKey = "duressMarkedVisibleWalletKey"; + static const String mwebEnabled = "mwebEnabledKey"; + static const String mwebScanHeight = "mwebScanHeightKey"; } diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 28b985be4..6353a330a 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -3,6 +3,7 @@ import 'package:cs_salvium/cs_salvium.dart' as lib_salvium; import 'package:tezart/tezart.dart' as tezart; import 'package:web3dart/web3dart.dart' as web3dart; +import '../../db/drift/database.dart'; import '../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/paynym/paynym_account_lite.dart'; @@ -82,6 +83,11 @@ class TxData { // Namecoin Name related final NameOpState? opNameState; + // MWEB + final bool isMweb; + final Set? mwebUtxos; + final List? usedMwebUtxos; + TxData({ this.feeRateType, this.feeRateAmount, @@ -116,6 +122,9 @@ class TxData { this.ignoreCachedBalanceChecks = false, this.opNameState, this.sparkNameInfo, + this.isMweb = false, + this.mwebUtxos, + this.usedMwebUtxos, }); Amount? get amount { @@ -263,6 +272,9 @@ class TxData { int validBlocks, })? sparkNameInfo, + bool? isMweb, + Set? mwebUtxos, + List? usedMwebUtxos, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -300,6 +312,9 @@ class TxData { ignoreCachedBalanceChecks ?? this.ignoreCachedBalanceChecks, opNameState: opNameState ?? this.opNameState, sparkNameInfo: sparkNameInfo ?? this.sparkNameInfo, + isMweb: isMweb ?? this.isMweb, + mwebUtxos: mwebUtxos ?? this.mwebUtxos, + usedMwebUtxos: usedMwebUtxos ?? this.usedMwebUtxos, ); } @@ -338,5 +353,8 @@ class TxData { 'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, ' 'opNameState: $opNameState, ' 'sparkNameInfo: $sparkNameInfo, ' + 'isMweb: $isMweb, ' + 'mwebUtxos: $mwebUtxos, ' + 'usedMwebUtxos: $usedMwebUtxos, ' '}'; } diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart index 7f0c4fd8b..db22a5b55 100644 --- a/lib/wallets/wallet/impl/litecoin_wallet.dart +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -53,6 +53,8 @@ class LitecoinWallet .not() .group( (q) => q + .typeEqualTo(AddressType.mweb) + .or() .typeEqualTo(AddressType.nonWallet) .or() .subTypeEqualTo(AddressSubType.nonWallet), diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart index 1de1577cc..b30fc1a30 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -1,13 +1,40 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:drift/drift.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:isar/isar.dart'; +import 'package:mweb_client/mweb_client.dart'; +import '../../../db/drift/database.dart'; +import '../../../models/balance.dart'; import '../../../models/isar/models/isar_models.dart'; +import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; +import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; +import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import '../../../services/event_bus/global_event_bus.dart'; +import '../../../services/mwebd_service.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/extensions/extensions.dart'; +import '../../../utilities/logger.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../../isar/models/wallet_info.dart'; +import '../../models/tx_data.dart'; import 'electrumx_interface.dart'; mixin MwebInterface on ElectrumXInterface { // TODO + StreamSubscription? _mwebUtxoSubscription; + + Future get _scanSecret async => + (await getRootHDNode()).derivePath("m/1000'/0'").privateKey.data; + Future get _spendSecret async => + (await getRootHDNode()).derivePath("m/1000'/1'").privateKey.data; + Future get _spendPub async => + (await getRootHDNode()).derivePath("m/1000'/1'").publicKey.data; + Future getCurrentReceivingMwebAddress() async { return await mainDB.isar.addresses .where() @@ -17,4 +44,529 @@ mixin MwebInterface .sortByDerivationIndexDesc() .findFirst(); } + + Future get _client async { + final client = await MwebdService.instance.getClient( + cryptoCurrency.network, + ); + if (client == null) { + throw Exception("Fetched mweb client returned null"); + } + return client; + } + + WalletSyncStatus? _syncStatus; + Timer? _mwebdPolling; + int currentKnownChainHeight = 0; + double highestPercentCached = 0; + void _startPollingMwebd() async { + _mwebdPolling?.cancel(); + _mwebdPolling = Timer.periodic(const Duration(seconds: 5), (_) async { + try { + final status = await MwebdService.instance.getServerStatus( + cryptoCurrency.network, + ); + + Logging.instance.i("_polling mwebd status: $status"); + + if (status == null) { + throw Exception( + "Mwebd server status is null. Was mwebd initialized?", + ); + } + + final currentKnownChainHeight = await chainHeight; + + final ({int remaining, double percent})? syncInfo; + + if (status.blockHeaderHeight < currentKnownChainHeight) { + syncInfo = ( + remaining: currentKnownChainHeight - status.blockHeaderHeight, + percent: status.blockHeaderHeight / currentKnownChainHeight, + ); + } else if (status.mwebHeaderHeight < currentKnownChainHeight) { + syncInfo = ( + remaining: currentKnownChainHeight - status.mwebHeaderHeight, + percent: status.mwebHeaderHeight / currentKnownChainHeight, + ); + } else if (status.mwebUtxosHeight < currentKnownChainHeight) { + syncInfo = (remaining: 1, percent: 0.99); + } else { + syncInfo = null; + } + + WalletSyncStatus? syncStatus; + + if (syncInfo != null) { + final previous = highestPercentCached; + highestPercentCached = math.max( + highestPercentCached, + syncInfo.percent, + ); + + if (previous != highestPercentCached) { + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(highestPercentCached, walletId), + ); + GlobalEventBus.instance.fire( + BlocksRemainingEvent(syncInfo.remaining, walletId), + ); + } + + syncStatus = WalletSyncStatus.syncing; + } else { + final walletMwebScanHeight = + info.otherData[WalletInfoKeys.mwebScanHeight] as int? ?? + info.restoreHeight; + + if (status.mwebUtxosHeight > walletMwebScanHeight) { + // TODO: check utxos and transactions? + + // then + await info.updateOtherData( + newEntries: { + WalletInfoKeys.mwebScanHeight: status.mwebUtxosHeight, + }, + isar: mainDB.isar, + ); + } + + syncStatus = WalletSyncStatus.synced; + } + + _syncStatus = syncStatus; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent(syncStatus, walletId, info.coin), + ); + } catch (e, s) { + Logging.instance.e( + "mweb wallet polling error", + error: e, + stackTrace: s, + ); + _syncStatus = WalletSyncStatus.unableToSync; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent(_syncStatus!, walletId, info.coin), + ); + } + }); + } + + Future _startUpdateMwebUtxos() async { + final client = await _client; + + Logging.instance.i("info.restoreHeight: ${info.restoreHeight}"); + final fromHeight = info.restoreHeight; + + final request = UtxosRequest( + fromHeight: fromHeight, + scanSecret: await _scanSecret, + ); + + await _mwebUtxoSubscription?.cancel(); + final db = Drift.get(walletId); + _mwebUtxoSubscription = (await client.utxos(request)).listen((utxo) async { + Logging.instance.i( + "Found UTXO in stream: Utxo(" + "height: ${utxo.height}, " + "value: ${utxo.value}, " + "address: ${utxo.address}, " + "outputId: ${utxo.outputId}, " + "blockTime: ${utxo.blockTime}" + ")", + ); + + if (utxo.address.isNotEmpty && utxo.outputId.isNotEmpty) { + try { + await db.transaction(() async { + final prev = + await (db.select(db.mwebUtxos)..where( + (e) => e.outputId.equals(utxo.outputId), + )).getSingleOrNull(); + + await db + .into(db.mwebUtxos) + .insertOnConflictUpdate( + MwebUtxosCompanion( + outputId: Value(prev?.outputId ?? utxo.outputId), + address: Value(prev?.address ?? utxo.address), + value: Value(utxo.value.toInt()), + height: Value(utxo.height), + blockTime: Value(utxo.blockTime), + blocked: Value(prev?.blocked ?? false), + used: Value(prev?.used ?? false), + ), + ); + }); + } catch (e, s) { + Logging.instance.f( + "Failed to insert/update mweb utxo", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w("Empty mweb utxo not added to db... ??"); + } + }); + } + + Future _initMweb() async { + try { + // check server is up + final status = await MwebdService.instance.getServerStatus( + cryptoCurrency.network, + ); + if (status == null) { + await MwebdService.instance.init(cryptoCurrency.network); + } + + _startPollingMwebd(); + } catch (e, s) { + Logging.instance.e("testing initMweb failed", error: e, stackTrace: s); + } + } + + Future
generateNextMwebAddress() async { + if (!info.isMwebEnabled) { + throw Exception( + "Tried calling generateNextMwebAddress with mweb disabled for $walletId ${info.name}", + ); + } + final highestStoredIndex = + (await getCurrentReceivingMwebAddress())?.derivationIndex ?? -1; + + final nextIndex = highestStoredIndex + 1; + + final client = await _client; + + final response = await client.address( + await _scanSecret, + await _spendPub, + nextIndex, + ); + + return Address( + walletId: walletId, + value: response, + publicKey: [], + derivationIndex: nextIndex, + derivationPath: null, + type: AddressType.mweb, + subType: AddressSubType.receiving, + ); + } + + Future estimateFeeForMweb(Amount amount) async { + if (!info.isMwebEnabled) { + throw Exception( + "Tried calling estimateFeeForMweb with mweb disabled for $walletId ${info.name}", + ); + } + throw UnimplementedError(); + } + + Future prepareSendMweb({required TxData txData}) async { + if (!info.isMwebEnabled) { + throw Exception( + "Tried calling prepareSendMweb with mweb disabled for $walletId ${info.name}", + ); + } + if (!txData.isMweb) { + throw Exception("Invalid mweb flagged tx data"); + } + + final client = await _client; + + final response = await client.create( + CreateRequest( + rawTx: txData.raw!.toUint8ListFromHex, + scanSecret: await _scanSecret, + spendSecret: await _spendSecret, + feeRatePerKb: Int64(txData.feeRateAmount!.toInt()), + dryRun: false, + ), + ); + + return txData.copyWith(raw: Uint8List.fromList(response.rawTx).toHex); + } + + Future confirmSendMweb({required TxData txData}) async { + if (!info.isMwebEnabled) { + throw Exception( + "Tried calling confirmSendMweb with mweb disabled for $walletId ${info.name}", + ); + } + + try { + Logging.instance.d("confirmSend txData: $txData"); + + final txHash = await electrumXClient.broadcastTransaction( + rawTx: txData.raw!, + ); + Logging.instance.d("Sent txHash: $txHash"); + + txData = txData.copyWith(txHash: txHash, txid: txHash); + + // Update used mweb utxos as used in database. They should already have + // been marked as isUsed. + if (txData.usedMwebUtxos != null && txData.usedMwebUtxos!.isNotEmpty) { + final db = Drift.get(walletId); + await db.transaction(() async { + for (final utxo in txData.usedMwebUtxos!) { + await db + .into(db.mwebUtxos) + .insertOnConflictUpdate(utxo.toCompanion(false)); + } + }); + } else { + Logging.instance.w( + "txData.usedMwebUtxos is empty or null when it very " + "likely should not be!", + ); + } + + return await updateSentCachedTxData(txData: txData); + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from confirmSendMweb(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + /// Should only be called within the standard wallet [recover] function due to + /// mutex locking. Otherwise behaviour MAY be undefined. + Future recoverMweb() async { + if (!info.isMwebEnabled) { + Logging.instance.e( + "Tried calling recoverMweb with mweb disabled for $walletId ${info.name}", + ); + return; + } + + throw UnimplementedError(); + } + + Future anonymizeAllMweb() async { + if (!info.isMwebEnabled) { + Logging.instance.e( + "Tried calling anonymizeAllMweb with mweb disabled for $walletId ${info.name}", + ); + return; + } + + try { + final currentHeight = await chainHeight; + + final spendableUtxos = + await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .isBlockedEqualTo(false) + .and() + .group((q) => q.usedEqualTo(false).or().usedIsNull()) + .and() + .valueGreaterThan(0) + .findAll(); + + spendableUtxos.removeWhere( + (e) => + !e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), + ); + + if (spendableUtxos.isEmpty) { + throw Exception("No available UTXOs found to anonymize"); + } + + // TODO finish + final txData = await prepareSendMweb( + txData: TxData(utxos: spendableUtxos.toSet()), + ); + + await confirmSendMweb(txData: txData); + } catch (e, s) { + Logging.instance.w( + "Exception caught in anonymizeAllMweb(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + // =========================================================================== + + @override + Future init() async { + if (info.isMwebEnabled) { + try { + await _initMweb(); + Address? address = await getCurrentReceivingMwebAddress(); + if (address == null) { + address = await generateNextMwebAddress(); + await mainDB.putAddress(address); + } + + unawaited(_startUpdateMwebUtxos()); + } catch (e, s) { + // do nothing, still allow user into wallet + Logging.instance.e( + "$runtimeType init() failed", + error: e, + stackTrace: s, + ); + } + } + + // await info.updateReceivingAddress( + // newAddress: address.value, + // isar: mainDB.isar, + // ); + + await super.init(); + } + + @override + Future updateBalance() async { + // call to super to update transparent balance + final normalBalanceFuture = super.updateBalance(); + + if (info.isMwebEnabled) { + final start = DateTime.now(); + try { + final currentHeight = await chainHeight; + final db = Drift.get(walletId); + final mwebUtxos = + await (db.select(db.mwebUtxos) + ..where((e) => e.used.equals(false))).get(); + + Amount satoshiBalanceTotal = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalancePending = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalanceSpendable = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalanceBlocked = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + + for (final utxo in mwebUtxos) { + final utxoAmount = Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + satoshiBalanceTotal += utxoAmount; + + if (utxo.blocked) { + satoshiBalanceBlocked += utxoAmount; + } else { + if (utxo.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + // overrideMinConfirms: TODO: set this??? + )) { + satoshiBalanceSpendable += utxoAmount; + } else { + satoshiBalancePending += utxoAmount; + } + } + } + + final balance = Balance( + total: satoshiBalanceTotal, + spendable: satoshiBalanceSpendable, + blockedTotal: satoshiBalanceBlocked, + pendingSpendable: satoshiBalancePending, + ); + + await info.updateBalanceSecondary( + newBalance: balance, + isar: mainDB.isar, + ); + } catch (e, s) { + Logging.instance.e( + "$runtimeType updateBalance mweb $walletId ${info.name}: ", + error: e, + stackTrace: s, + ); + } finally { + Logging.instance.d( + "${info.name} updateBalance mweb duration:" + " ${DateTime.now().difference(start)}", + ); + } + } + + // wait for normalBalanceFuture to complete before returning + await normalBalanceFuture; + } + + @override + Future refresh() async { + if (isViewOnly || !info.isMwebEnabled) { + await super.refresh(); + return; + } + + // Awaiting this lock could be dangerous. + // Since refresh is periodic (generally) + if (refreshMutex.isLocked) { + return; + } + + // TODO + + // final node = getCurrentNode(); + // + // if (_torNodeMismatchGuard(node)) { + // throw Exception("TOR – clearnet mismatch"); + // } + // + // // this acquire should be almost instant due to above check. + // // Slight possibility of race but should be irrelevant + // await refreshMutex.acquire(); + // + // libMoneroWallet?.startSyncing(); + // _setSyncStatus(lib_monero_compat.StartingSyncStatus()); + // + // await updateTransactions(); + // await updateBalance(); + // + // if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + // await checkReceivingAddressForTransactions(); + // } + // + // if (refreshMutex.isLocked) { + // refreshMutex.release(); + // } + // + // final synced = await libMoneroWallet?.isSynced(); + // + // if (synced == true) { + // _setSyncStatus(lib_monero_compat.SyncedSyncStatus()); + // } + } + + @override + Future exit() async { + _mwebdPolling?.cancel(); + _mwebdPolling = null; + await super.exit(); + } } diff --git a/pubspec.lock b/pubspec.lock index 563118121..4db79d477 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -884,7 +884,7 @@ packages: source: git version: "8.3.1" fixnum: - dependency: transitive + dependency: "direct main" description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be @@ -1566,10 +1566,10 @@ packages: dependency: "direct main" description: name: mweb_client - sha256: "8c9498adaaa1166a49ee114b03498268aff176778eeb311ed049fd22657feacb" + sha256: "263ba560dab7e63a1d03875d455a19cc4a1ab9720786cd9d6ffcc42127d06732" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.2.0" namecoin: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 3fa0f3077..de8d4dadc 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -222,7 +222,8 @@ dependencies: cs_salvium: ^1.2.0 cs_salvium_flutter_libs: ^1.0.2 flutter_mwebd: ^0.0.1-pre.1 - mweb_client: ^0.1.0 + mweb_client: ^0.2.0 + fixnum: ^1.1.1 dev_dependencies: flutter_test: From 77522108417e877012498884b1ea7ad130901d82 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 13 Jun 2025 11:29:25 -0600 Subject: [PATCH 09/42] WIP: mweb recovery --- .../models/blockchain_data/transaction.dart | 22 +- .../wallet_view/desktop_wallet_view.dart | 30 +- .../mweb_desktop_wallet_summary.dart | 206 ++++++++++++++ .../electrumx_interface.dart | 2 + .../mweb_interface.dart | 264 +++++++++++++++--- 5 files changed, 464 insertions(+), 60 deletions(-) create mode 100644 lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index d5b3572fe..417856079 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -151,7 +151,8 @@ class Transaction { } @override - toString() => "{ " + toString() => + "{ " "id: $id, " "walletId: $walletId, " "txid: $txid, " @@ -217,12 +218,14 @@ class Transaction { slateId: json["slateId"] as String?, otherData: json["otherData"] as String?, nonce: json["nonce"] as int?, - inputs: List.from(json["inputs"] as List) - .map((e) => Input.fromJsonString(e)) - .toList(), - outputs: List.from(json["outputs"] as List) - .map((e) => Output.fromJsonString(e)) - .toList(), + inputs: + List.from( + json["inputs"] as List, + ).map((e) => Input.fromJsonString(e)).toList(), + outputs: + List.from( + json["outputs"] as List, + ).map((e) => Output.fromJsonString(e)).toList(), numberOfMessages: json["numberOfMessages"] as int, ); if (json["address"] == null) { @@ -241,7 +244,7 @@ enum TransactionType { outgoing, incoming, sentToSelf, // should we keep this? - unknown; + unknown, } // Used in Isar db and stored there as int indexes so adding/removing values @@ -256,5 +259,6 @@ enum TransactionSubType { cashFusion, sparkMint, // firo specific sparkSpend, // firo specific - ordinal; + ordinal, + mweb, } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index e35e6ed34..96363f1e2 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -28,7 +28,6 @@ import '../../../pages/wallet_view/sub_widgets/transactions_list.dart'; import '../../../pages/wallet_view/transaction_views/all_transactions_view.dart'; import '../../../pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart'; import '../../../pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/active_wallet_provider.dart'; import '../../../providers/global/auto_swb_service_provider.dart'; import '../../../providers/providers.dart'; @@ -46,6 +45,7 @@ import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/wallet/impl/banano_wallet.dart'; import '../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_buttons/blue_text_button.dart'; @@ -57,6 +57,7 @@ import '../../coin_control/desktop_coin_control_use_dialog.dart'; import 'sub_widgets/desktop_wallet_features.dart'; import 'sub_widgets/desktop_wallet_summary.dart'; import 'sub_widgets/firo_desktop_wallet_summary.dart'; +import 'sub_widgets/mweb_desktop_wallet_summary.dart'; import 'sub_widgets/my_wallet.dart'; import 'sub_widgets/network_info_button.dart'; import 'sub_widgets/wallet_keys_button.dart'; @@ -448,13 +449,26 @@ class DesktopWalletHeaderRow extends ConsumerWidget { ), if (wallet is! FiroWallet) - DesktopWalletSummary( - walletId: wallet.walletId, - initialSyncStatus: - wallet.refreshMutex.isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), + wallet is MwebInterface && + ref.watch( + pWalletInfo( + wallet.walletId, + ).select((s) => s.isMwebEnabled), + ) + ? MwebDesktopWalletSummary( + walletId: wallet.walletId, + initialSyncStatus: + wallet.refreshMutex.isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ) + : DesktopWalletSummary( + walletId: wallet.walletId, + initialSyncStatus: + wallet.refreshMutex.isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), Expanded(child: DesktopWalletFeatures(walletId: wallet.walletId)), ], ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart new file mode 100644 index 000000000..942d08fa1 --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart @@ -0,0 +1,206 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2025 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2025-06-13 + * + */ + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; +import '../../../../providers/providers.dart'; +import '../../../../providers/wallet/wallet_balance_toggle_state_provider.dart'; +import '../../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/amount/amount.dart'; +import '../../../../utilities/amount/amount_formatter.dart'; +import '../../../../utilities/enums/wallet_balance_toggle_state.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../../../wallets/isar/providers/wallet_info_provider.dart'; +import 'desktop_balance_toggle_button.dart'; + +class MwebDesktopWalletSummary extends ConsumerStatefulWidget { + const MwebDesktopWalletSummary({ + super.key, + required this.walletId, + required this.initialSyncStatus, + }); + + final String walletId; + final WalletSyncStatus initialSyncStatus; + + @override + ConsumerState createState() => + _WMwebDesktopWalletSummaryState(); +} + +class _WMwebDesktopWalletSummaryState + extends ConsumerState { + late final String walletId; + + late final CryptoCurrency coin; + late final bool isMweb; + + @override + void initState() { + super.initState(); + walletId = widget.walletId; + coin = ref.read(pWalletCoin(widget.walletId)); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + Decimal? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = + ref + .watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ) + ?.value; + } + + final _showAvailable = + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available; + + final balance0 = ref.watch(pWalletBalanceSecondary(walletId)); + final balanceToShowSpark = + _showAvailable ? balance0.spendable : balance0.total; + + final balance2 = ref.watch(pWalletBalance(walletId)); + final balanceToShowPublic = + _showAvailable ? balance2.spendable : balance2.total; + + return Consumer( + builder: (context, ref, __) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Table( + columnWidths: { + 0: const IntrinsicColumnWidth(), + 1: const IntrinsicColumnWidth(), + if (price != null) 2: const IntrinsicColumnWidth(), + }, + children: [ + TableRow( + children: [ + const _Prefix(isMweb: true), + _Balance(coin: coin, amount: balanceToShowSpark), + if (price != null) + _Price( + coin: coin, + amount: balanceToShowSpark, + price: price, + ), + ], + ), + + TableRow( + children: [ + const _Prefix(isMweb: false), + _Balance(coin: coin, amount: balanceToShowPublic), + if (price != null) + _Price( + coin: coin, + amount: balanceToShowPublic, + price: price, + ), + ], + ), + ], + ), + + const SizedBox(width: 8), + WalletRefreshButton( + walletId: walletId, + initialSyncStatus: widget.initialSyncStatus, + ), + const SizedBox(width: 8), + const DesktopBalanceToggleButton(), + ], + ); + }, + ); + } +} + +class _Prefix extends StatelessWidget { + const _Prefix({super.key, required this.isMweb}); + + final bool isMweb; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SelectableText( + isMweb ? "MWEB" : "", + style: STextStyles.w500_24(context), + ), + ], + ), + ); + } +} + +class _Balance extends ConsumerWidget { + const _Balance({super.key, required this.coin, required this.amount}); + + final CryptoCurrency coin; + final Amount amount; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SelectableText( + ref.watch(pAmountFormatter(coin)).format(amount, ethContract: null), + style: STextStyles.desktopH3(context), + textAlign: TextAlign.end, + ); + } +} + +class _Price extends ConsumerWidget { + const _Price({ + super.key, + required this.coin, + required this.amount, + required this.price, + }); + + final CryptoCurrency coin; + final Amount amount; + final Decimal price; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.only(left: 16), + child: SelectableText( + "${Amount.fromDecimal(price * amount.decimal, fractionDigits: 2).fiatString(locale: ref.watch(localeServiceChangeNotifierProvider.select((value) => value.locale)))} " + "${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle1, + ), + + textAlign: TextAlign.end, + ), + ); + } +} diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 8fe157a61..dda6b2876 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:isar/isar.dart'; +import 'package:meta/meta.dart'; import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/client_manager.dart'; @@ -1788,6 +1789,7 @@ mixin ElectrumXInterface } } + @mustCallSuper @override Future init() async { try { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart index b30fc1a30..f812faba8 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -1,13 +1,17 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:math' as math; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:isar/isar.dart'; +import 'package:meta/meta.dart'; import 'package:mweb_client/mweb_client.dart'; import '../../../db/drift/database.dart'; import '../../../models/balance.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 '../../../services/event_bus/events/global/blocks_remaining_event.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -67,7 +71,9 @@ mixin MwebInterface cryptoCurrency.network, ); - Logging.instance.i("_polling mwebd status: $status"); + Logging.instance.t( + "$walletId ${info.name} _polling mwebd status: $status", + ); if (status == null) { throw Exception( @@ -115,22 +121,6 @@ mixin MwebInterface syncStatus = WalletSyncStatus.syncing; } else { - final walletMwebScanHeight = - info.otherData[WalletInfoKeys.mwebScanHeight] as int? ?? - info.restoreHeight; - - if (status.mwebUtxosHeight > walletMwebScanHeight) { - // TODO: check utxos and transactions? - - // then - await info.updateOtherData( - newEntries: { - WalletInfoKeys.mwebScanHeight: status.mwebUtxosHeight, - }, - isar: mainDB.isar, - ); - } - syncStatus = WalletSyncStatus.synced; } @@ -152,21 +142,29 @@ mixin MwebInterface }); } + Future _stopUpdateMwebUtxos() async => + await _mwebUtxoSubscription?.cancel(); Future _startUpdateMwebUtxos() async { + await _stopUpdateMwebUtxos(); + final client = await _client; Logging.instance.i("info.restoreHeight: ${info.restoreHeight}"); - final fromHeight = info.restoreHeight; + Logging.instance.i( + "info.otherData[WalletInfoKeys.mwebScanHeight]: ${info.otherData[WalletInfoKeys.mwebScanHeight]}", + ); + final fromHeight = + info.otherData[WalletInfoKeys.mwebScanHeight] as int? ?? + info.restoreHeight; final request = UtxosRequest( fromHeight: fromHeight, scanSecret: await _scanSecret, ); - await _mwebUtxoSubscription?.cancel(); final db = Drift.get(walletId); _mwebUtxoSubscription = (await client.utxos(request)).listen((utxo) async { - Logging.instance.i( + Logging.instance.t( "Found UTXO in stream: Utxo(" "height: ${utxo.height}, " "value: ${utxo.value}, " @@ -198,6 +196,50 @@ mixin MwebInterface ), ); }); + + // TODO get real txid one day + final fakeTxid = "mweb_outputId_${utxo.outputId}"; + + final tx = TransactionV2( + walletId: walletId, + blockHash: null, // ?? + hash: "", + txid: fakeTxid, + timestamp: utxo.blockTime, + height: utxo.height, + inputs: [], + outputs: [ + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "", + valueStringSats: utxo.value.toString(), + addresses: [utxo.address], + walletOwns: true, + ), + ], + version: 2, // probably + type: TransactionType.incoming, + subType: TransactionSubType.mweb, + otherData: jsonEncode({ + TxV2OdKeys.overrideFee: + Amount( + rawValue: + BigInt + .zero, // TODO fill in correctly when we have a real txid + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + }), + ); + + await mainDB.updateOrPutTransactionV2s([tx]); + + await updateBalance(mwebOnly: true); + + if (utxo.height > fromHeight) { + await info.updateOtherData( + newEntries: {WalletInfoKeys.mwebScanHeight: utxo.height}, + isar: mainDB.isar, + ); + } } catch (e, s) { Logging.instance.f( "Failed to insert/update mweb utxo", @@ -337,19 +379,6 @@ mixin MwebInterface } } - /// Should only be called within the standard wallet [recover] function due to - /// mutex locking. Otherwise behaviour MAY be undefined. - Future recoverMweb() async { - if (!info.isMwebEnabled) { - Logging.instance.e( - "Tried calling recoverMweb with mweb disabled for $walletId ${info.name}", - ); - return; - } - - throw UnimplementedError(); - } - Future anonymizeAllMweb() async { if (!info.isMwebEnabled) { Logging.instance.e( @@ -404,6 +433,7 @@ mixin MwebInterface // =========================================================================== + @mustCallSuper @override Future init() async { if (info.isMwebEnabled) { @@ -426,18 +456,16 @@ mixin MwebInterface } } - // await info.updateReceivingAddress( - // newAddress: address.value, - // isar: mainDB.isar, - // ); - await super.init(); } @override - Future updateBalance() async { - // call to super to update transparent balance - final normalBalanceFuture = super.updateBalance(); + Future updateBalance({bool mwebOnly = false}) async { + Future? normalBalanceFuture; + if (!mwebOnly) { + // call to super to update transparent balance + normalBalanceFuture = super.updateBalance(); + } if (info.isMwebEnabled) { final start = DateTime.now(); @@ -513,8 +541,10 @@ mixin MwebInterface } } - // wait for normalBalanceFuture to complete before returning - await normalBalanceFuture; + if (!mwebOnly) { + // wait for normalBalanceFuture to complete before returning + await normalBalanceFuture; + } } @override @@ -563,6 +593,154 @@ mixin MwebInterface // } } + @override + Future recover({required bool isRescan}) async { + if (isViewOnly) { + await recoverViewOnly(isRescan: isRescan); + return; + } + + final start = DateTime.now(); + final root = await getRootHDNode(); + + final List addresses})>> receiveFutures = + []; + final List addresses})>> changeFutures = + []; + + const receiveChain = 0; + const changeChain = 1; + + const txCountBatchSize = 12; + + try { + await refreshMutex.protect(() async { + if (isRescan) { + // clear cache + await electrumXCachedClient.clearSharedTransactionCache( + cryptoCurrency: info.coin, + ); + // clear blockchain info + await mainDB.deleteWalletBlockchainData(walletId); + + // reset scan/listen height + await info.updateOtherData( + newEntries: {WalletInfoKeys.mwebScanHeight: info.restoreHeight}, + isar: mainDB.isar, + ); + + // reset balance to 0 + await info.updateBalanceSecondary( + newBalance: Balance.zeroFor(currency: cryptoCurrency), + isar: mainDB.isar, + ); + + // clear all mweb utxos + final db = Drift.get(walletId); + await db.transaction(() async => await db.delete(db.mwebUtxos).go()); + + await _stopUpdateMwebUtxos(); + if (info.isMwebEnabled) { + // only restart scanning if mweb enabled + unawaited(_startUpdateMwebUtxos()); + } + } + + // receiving addresses + Logging.instance.i("checking receiving addresses..."); + + final canBatch = await serverCanBatch; + + for (final type in cryptoCurrency.supportedDerivationPathTypes) { + receiveFutures.add( + canBatch + ? checkGapsBatched(txCountBatchSize, root, type, receiveChain) + : checkGapsLinearly(root, type, receiveChain), + ); + } + + // change addresses + Logging.instance.d("checking change addresses..."); + for (final type in cryptoCurrency.supportedDerivationPathTypes) { + changeFutures.add( + canBatch + ? checkGapsBatched(txCountBatchSize, root, type, changeChain) + : checkGapsLinearly(root, type, changeChain), + ); + } + + // io limitations may require running these linearly instead + final futuresResult = await Future.wait([ + Future.wait(receiveFutures), + Future.wait(changeFutures), + ]); + + final receiveResults = futuresResult[0]; + final changeResults = futuresResult[1]; + + final List
addressesToStore = []; + + int highestReceivingIndexWithHistory = 0; + + for (final tuple in receiveResults) { + if (tuple.addresses.isEmpty) { + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await checkReceivingAddressForTransactions(); + } + } else { + highestReceivingIndexWithHistory = math.max( + tuple.index, + highestReceivingIndexWithHistory, + ); + addressesToStore.addAll(tuple.addresses); + } + } + + int highestChangeIndexWithHistory = 0; + // If restoring a wallet that never sent any funds with change, then set changeArray + // manually. If we didn't do this, it'd store an empty array. + for (final tuple in changeResults) { + if (tuple.addresses.isEmpty) { + await checkChangeAddressForTransactions(); + } else { + highestChangeIndexWithHistory = math.max( + tuple.index, + highestChangeIndexWithHistory, + ); + addressesToStore.addAll(tuple.addresses); + } + } + + // remove extra addresses to help minimize risk of creating a large gap + addressesToStore.removeWhere( + (e) => + e.subType == AddressSubType.change && + e.derivationIndex > highestChangeIndexWithHistory, + ); + addressesToStore.removeWhere( + (e) => + e.subType == AddressSubType.receiving && + e.derivationIndex > highestReceivingIndexWithHistory, + ); + + await mainDB.updateOrPutAddresses(addressesToStore); + }); + + unawaited(refresh()); + Logging.instance.i( + "Mweb recover for " + "${info.name}: ${DateTime.now().difference(start)}", + ); + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from mweb_interface recover(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + @override Future exit() async { _mwebdPolling?.cancel(); From 7e1743610199f6ee81256bfb03492f7adc7b2059 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 19 Jun 2025 08:48:32 -0600 Subject: [PATCH 10/42] mwebd service clean up --- .../more_features/more_features_dialog.dart | 17 ++++++--- .../global/mweb_service_provider.dart | 7 ++++ lib/providers/providers.dart | 1 + .../global/mweb_status_changed_event.dart | 24 ------------- lib/services/mwebd_service.dart | 35 ++++++------------- 5 files changed, 30 insertions(+), 54 deletions(-) create mode 100644 lib/providers/global/mweb_service_provider.dart delete mode 100644 lib/services/event_bus/events/global/mweb_status_changed_event.dart 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 80f55b664..3df3c9504 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 @@ -16,8 +16,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../../../../db/sqlite/firo_cache.dart'; -import '../../../../../providers/db/main_db_provider.dart'; -import '../../../../../providers/global/wallets_provider.dart'; +import '../../../../../providers/providers.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../themes/theme_providers.dart'; import '../../../../../utilities/assets.dart'; @@ -267,16 +266,24 @@ class _MoreFeaturesDialogState extends ConsumerState { } } - Future _updateMwebToggle(bool shouldReuse) async { + Future _updateMwebToggle(bool value) async { + if (value) { + unawaited( + ref + .read(pMwebService) + .init(ref.read(pWalletCoin(widget.walletId)).network), + ); + } + await ref .read(pWalletInfo(widget.walletId)) .updateOtherData( - newEntries: {WalletInfoKeys.mwebEnabled: shouldReuse}, + newEntries: {WalletInfoKeys.mwebEnabled: value}, isar: ref.read(mainDBProvider).isar, ); if (_switchControllerMwebToggle.isOn != null) { - if (_switchControllerMwebToggle.isOn!.call() != shouldReuse) { + if (_switchControllerMwebToggle.isOn!.call() != value) { _switchControllerMwebToggle.activate?.call(); } } diff --git a/lib/providers/global/mweb_service_provider.dart b/lib/providers/global/mweb_service_provider.dart new file mode 100644 index 000000000..1cddcb407 --- /dev/null +++ b/lib/providers/global/mweb_service_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../services/mwebd_service.dart'; + +final pMwebService = StateProvider( + (ref) => MwebdService.instance, +); diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 3db18af2f..d86e4de3b 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -23,6 +23,7 @@ export './global/barcode_scanner_provider.dart'; export './global/clipboard_provider.dart'; export './global/duress_provider.dart'; export './global/locale_provider.dart'; +export './global/mweb_service_provider.dart'; export './global/node_service_provider.dart'; export './global/notifications_provider.dart'; export './global/prefs_provider.dart'; diff --git a/lib/services/event_bus/events/global/mweb_status_changed_event.dart b/lib/services/event_bus/events/global/mweb_status_changed_event.dart deleted file mode 100644 index 70dd4f3d6..000000000 --- a/lib/services/event_bus/events/global/mweb_status_changed_event.dart +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 '../../../../utilities/logger.dart'; - -enum MwebStatus { enabled, disabled } - -class MwebPreferenceChangedEvent { - String? message; - MwebStatus status; - - MwebPreferenceChangedEvent({required this.status, this.message}) { - Logging.instance.w( - "MwebPreferenceChangedEvent changed to \"$status\" with message: $message", - ); - } -} diff --git a/lib/services/mwebd_service.dart b/lib/services/mwebd_service.dart index 00ace0d24..d912f8ff0 100644 --- a/lib/services/mwebd_service.dart +++ b/lib/services/mwebd_service.dart @@ -10,13 +10,19 @@ import '../utilities/logger.dart'; import '../utilities/prefs.dart'; import '../utilities/stack_file_system.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; -import 'event_bus/events/global/mweb_status_changed_event.dart'; import 'event_bus/events/global/tor_connection_status_changed_event.dart'; import 'event_bus/events/global/tor_status_changed_event.dart'; import 'event_bus/global_event_bus.dart'; import 'tor_service.dart'; final class MwebdService { + static String defaultPeer(CryptoCurrencyNetwork net) => switch (net) { + CryptoCurrencyNetwork.main => "litecoin.stackwallet.com:9333", + CryptoCurrencyNetwork.test => "litecoin.stackwallet.com:19335", + CryptoCurrencyNetwork.stage => throw UnimplementedError(), + CryptoCurrencyNetwork.test4 => throw UnimplementedError(), + }; + final Map _map = {}; @@ -24,8 +30,6 @@ final class MwebdService { _torStatusListener; late final StreamSubscription _torPreferenceListener; - late final StreamSubscription - _mwebPreferenceListener; final Mutex _torConnectingLock = Mutex(); @@ -67,27 +71,6 @@ final class MwebdService { return await _update(null); } }); - - // Listen for mweb preference changes. - _mwebPreferenceListener = bus.on().listen(( - event, - ) async { - switch (event.status) { - case MwebStatus.enabled: - // as we don't know which network(s) to use here - // we will rely on status checks and init as required within wallets - break; - - case MwebStatus.disabled: - final nets = _map.keys; - for (final net in nets) { - final old = _map.remove(net)!; - await old.client.cleanup(); - await old.server.stopServer(); - } - break; - } - }); } // locked while mweb servers and clients are updating @@ -133,6 +116,8 @@ final class MwebdService { } Future init(CryptoCurrencyNetwork net) async { + if (net == CryptoCurrencyNetwork.test) return; + Logging.instance.i("MwebdService init($net) called..."); await _updateLock.protect(() async { if (_map[net] != null) { @@ -166,7 +151,7 @@ final class MwebdService { final newServer = MwebdServer( chain: chain, dataDir: dir.path, - peer: "", + peer: defaultPeer(net), proxy: proxy, serverPort: port, ); From 6824774921391edd3a73901ae1ec0660fc30162a Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 19 Jun 2025 08:54:04 -0600 Subject: [PATCH 11/42] refactor firo balance type to general pub/priv balance type for use with mweb as well as firo --- .../send_view/confirm_transaction_view.dart | 8 +-- .../send_view/frost_ms/frost_send_view.dart | 2 +- lib/pages/send_view/send_view.dart | 45 +++++++------- ...dart => dual_balance_selection_sheet.dart} | 24 +++---- .../transaction_fee_selection_sheet.dart | 12 ++-- .../wallet_balance_toggle_sheet.dart | 62 ++++++++++--------- .../sub_widgets/wallet_summary_info.dart | 8 +-- .../desktop_balance_toggle_button.dart | 6 +- .../wallet_view/sub_widgets/desktop_send.dart | 54 +++++++++------- .../sub_widgets/desktop_send_fee_form.dart | 6 +- .../sub_widgets/desktop_wallet_summary.dart | 4 +- .../firo_desktop_wallet_summary.dart | 10 +-- .../ui/preview_tx_button_state_provider.dart | 4 +- ...public_private_balance_state_provider.dart | 6 +- lib/widgets/desktop/desktop_fee_dialog.dart | 12 ++-- 15 files changed, 137 insertions(+), 126 deletions(-) rename lib/pages/send_view/sub_widgets/{firo_balance_selection_sheet.dart => dual_balance_selection_sheet.dart} (92%) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index d34306cf4..220d0e04f 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -140,7 +140,7 @@ class _ConfirmTransactionViewState } else { if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (widget.txData.sparkMints == null) { txDataFuture = wallet.confirmSend(txData: widget.txData); } else { @@ -150,7 +150,7 @@ class _ConfirmTransactionViewState } break; - case FiroType.spark: + case BalanceType.private: txDataFuture = wallet.confirmSendSpark(txData: widget.txData); break; } @@ -346,7 +346,7 @@ class _ConfirmTransactionViewState if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (widget.txData.sparkMints != null) { fee = widget.txData.sparkMints! .map((e) => e.fee!) @@ -360,7 +360,7 @@ class _ConfirmTransactionViewState } break; - case FiroType.spark: + case BalanceType.private: fee = widget.txData.fee; amountWithoutChange = (widget.txData.amountWithoutChange ?? diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 22c290036..729bf7c69 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -230,7 +230,7 @@ class _FrostSendViewState extends ConsumerState { ), ) && (coin is Firo - ? ref.watch(publicPrivateBalanceStateProvider) == FiroType.public + ? ref.watch(publicPrivateBalanceStateProvider) == BalanceType.public : true); return ConditionalParent( diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 3fca255c7..be21c99d6 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -73,7 +73,7 @@ import '../address_book_views/address_book_view.dart'; import '../coin_control/coin_control_view.dart'; import 'confirm_transaction_view.dart'; import 'sub_widgets/building_transaction_dialog.dart'; -import 'sub_widgets/firo_balance_selection_sheet.dart'; +import 'sub_widgets/dual_balance_selection_sheet.dart'; import 'sub_widgets/transaction_fee_selection_sheet.dart'; class SendView extends ConsumerStatefulWidget { @@ -479,7 +479,8 @@ class _SendViewState extends ConsumerState { ref.read(pIsExchangeAddress.state).state = (coin as Firo) .isExchangeAddress(address ?? ""); - if (ref.read(publicPrivateBalanceStateProvider) == FiroType.spark && + if (ref.read(publicPrivateBalanceStateProvider) == + BalanceType.private && ref.read(pIsExchangeAddress) && !_isFiroExWarningDisplayed) { _isFiroExWarningDisplayed = true; @@ -508,12 +509,12 @@ class _SendViewState extends ConsumerState { if (isFiro) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (cachedFiroPublicFees[amount] != null) { return cachedFiroPublicFees[amount]!; } break; - case FiroType.spark: + case BalanceType.private: if (cachedFiroSparkFees[amount] != null) { return cachedFiroSparkFees[amount]!; } @@ -572,14 +573,14 @@ class _SendViewState extends ConsumerState { final firoWallet = wallet as FiroWallet; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: fee = await firoWallet.estimateFeeFor(amount, feeRate); cachedFiroPublicFees[amount] = ref .read(pAmountFormatter(coin)) .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFiroPublicFees[amount]!; - case FiroType.spark: + case BalanceType.private: fee = await firoWallet.estimateFeeForSpark(amount); cachedFiroSparkFees[amount] = ref .read(pAmountFormatter(coin)) @@ -606,10 +607,10 @@ class _SendViewState extends ConsumerState { final Amount availableBalance; if (isFiro) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: availableBalance = wallet.info.cachedBalance.spendable; break; - case FiroType.spark: + case BalanceType.private: availableBalance = wallet.info.cachedBalanceTertiary.spendable; break; } @@ -691,7 +692,7 @@ class _SendViewState extends ConsumerState { isSpark: wallet is FiroWallet && ref.read(publicPrivateBalanceStateProvider.state).state == - FiroType.spark, + BalanceType.private, onCancel: () { wasCancelled = true; @@ -731,7 +732,7 @@ class _SendViewState extends ConsumerState { ); } else if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (ref.read(pValidSparkSendToAddress)) { txDataFuture = wallet.prepareSparkMintTransaction( txData: TxData( @@ -768,7 +769,7 @@ class _SendViewState extends ConsumerState { } break; - case FiroType.spark: + case BalanceType.private: txDataFuture = wallet.prepareSendSpark( txData: TxData( recipients: @@ -925,11 +926,11 @@ class _SendViewState extends ConsumerState { amount = _selectedUtxosAmount(selectedUTXOs); } else if (isFiro) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: amount = ref.read(pWalletBalance(walletId)).spendable; break; - case FiroType.spark: + case BalanceType.private: amount = ref.read(pWalletBalanceTertiary(walletId)).spendable; break; } @@ -1159,7 +1160,7 @@ class _SendViewState extends ConsumerState { ), ) && (coin is Firo - ? ref.watch(publicPrivateBalanceStateProvider) == FiroType.public + ? ref.watch(publicPrivateBalanceStateProvider) == BalanceType.public : true); if (isFiro) { @@ -1181,7 +1182,7 @@ class _SendViewState extends ConsumerState { } if (previous != next && - next == FiroType.spark && + next == BalanceType.private && isExchangeAddress && !_isFiroExWarningDisplayed) { _isFiroExWarningDisplayed = true; @@ -1321,7 +1322,7 @@ class _SendViewState extends ConsumerState { .state, ) .state) { - case FiroType.public: + case BalanceType.public: amount = ref .read( @@ -1332,7 +1333,7 @@ class _SendViewState extends ConsumerState { .spendable; break; - case FiroType.spark: + case BalanceType.private: amount = ref .read( @@ -1761,7 +1762,7 @@ class _SendViewState extends ConsumerState { ), ), builder: - (_) => FiroBalanceSelectionSheet( + (_) => DualBalanceSelectionSheet( walletId: walletId, ), ); @@ -1789,7 +1790,7 @@ class _SendViewState extends ConsumerState { .state, ) .state) { - case FiroType.public: + case BalanceType.public: amount = ref .watch( @@ -1799,7 +1800,7 @@ class _SendViewState extends ConsumerState { ) .spendable; break; - case FiroType.spark: + case BalanceType.private: amount = ref .watch( @@ -2229,7 +2230,7 @@ class _SendViewState extends ConsumerState { .state, ) .state != - FiroType.public + BalanceType.public ? null : _onFeeSelectPressed, child: @@ -2240,7 +2241,7 @@ class _SendViewState extends ConsumerState { .state, ) .state != - FiroType.public) + BalanceType.public) ? Row( children: [ FutureBuilder( diff --git a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart similarity index 92% rename from lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart rename to lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart index 87c6da542..c8006b713 100644 --- a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart @@ -19,18 +19,18 @@ import '../../../utilities/constants.dart'; import '../../../utilities/text_styles.dart'; import '../../../wallets/wallet/impl/firo_wallet.dart'; -class FiroBalanceSelectionSheet extends ConsumerStatefulWidget { - const FiroBalanceSelectionSheet({super.key, required this.walletId}); +class DualBalanceSelectionSheet extends ConsumerStatefulWidget { + const DualBalanceSelectionSheet({super.key, required this.walletId}); final String walletId; @override - ConsumerState createState() => + ConsumerState createState() => _FiroBalanceSelectionSheetState(); } class _FiroBalanceSelectionSheetState - extends ConsumerState { + extends ConsumerState { late final String walletId; @override @@ -90,9 +90,9 @@ class _FiroBalanceSelectionSheetState onTap: () { final state = ref.read(publicPrivateBalanceStateProvider.state).state; - if (state != FiroType.spark) { + if (state != BalanceType.private) { ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; } Navigator.of(context).pop(); }, @@ -112,7 +112,7 @@ class _FiroBalanceSelectionSheetState Theme.of(context) .extension()! .radioButtonIconEnabled, - value: FiroType.spark, + value: BalanceType.private, groupValue: ref .watch( @@ -125,7 +125,7 @@ class _FiroBalanceSelectionSheetState .read( publicPrivateBalanceStateProvider.state, ) - .state = FiroType.spark; + .state = BalanceType.private; Navigator.of(context).pop(); }, @@ -173,9 +173,9 @@ class _FiroBalanceSelectionSheetState onTap: () { final state = ref.read(publicPrivateBalanceStateProvider.state).state; - if (state != FiroType.public) { + if (state != BalanceType.public) { ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; } Navigator.of(context).pop(); }, @@ -194,7 +194,7 @@ class _FiroBalanceSelectionSheetState Theme.of(context) .extension()! .radioButtonIconEnabled, - value: FiroType.public, + value: BalanceType.public, groupValue: ref .watch( @@ -207,7 +207,7 @@ class _FiroBalanceSelectionSheetState .read( publicPrivateBalanceStateProvider.state, ) - .state = FiroType.public; + .state = BalanceType.public; Navigator.of(context).pop(); }, ), diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart index 2257af491..3ec7048a3 100644 --- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart @@ -97,11 +97,11 @@ class _TransactionFeeSelectionSheetState } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, @@ -134,11 +134,11 @@ class _TransactionFeeSelectionSheetState } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, @@ -170,11 +170,11 @@ class _TransactionFeeSelectionSheetState } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index bbe90033e..120b99b25 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -23,7 +23,7 @@ import '../../../utilities/text_styles.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; -enum _BalanceType { available, full, sparkAvailable, sparkFull } +enum _BalanceType { available, full, privateAvailable, privateFull } class WalletBalanceToggleSheet extends ConsumerWidget { const WalletBalanceToggleSheet({super.key, required this.walletId}); @@ -35,7 +35,10 @@ class WalletBalanceToggleSheet extends ConsumerWidget { final maxHeight = MediaQuery.of(context).size.height * 0.90; final coin = ref.watch(pWalletCoin(walletId)); - final isFiro = coin is Firo; + final isMweb = ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + ); + final hasPrivate = isMweb || coin is Firo; final balance = ref.watch(pWalletBalance(walletId)); @@ -45,16 +48,19 @@ class WalletBalanceToggleSheet extends ConsumerWidget { ? _BalanceType.available : _BalanceType.full; - Balance? balanceTertiary; - if (isFiro) { - balanceTertiary = ref.watch(pWalletBalanceTertiary(walletId)); + Balance? balancePrivate; + if (hasPrivate) { + balancePrivate = + isMweb + ? ref.watch(pWalletBalanceSecondary(walletId)) + : ref.watch(pWalletBalanceTertiary(walletId)); if (ref.watch(publicPrivateBalanceStateProvider.state).state == - FiroType.spark) { + BalanceType.private) { _bal = _bal == _BalanceType.available - ? _BalanceType.sparkAvailable - : _BalanceType.sparkFull; + ? _BalanceType.privateAvailable + : _BalanceType.privateFull; } } @@ -102,21 +108,21 @@ class WalletBalanceToggleSheet extends ConsumerWidget { ), const SizedBox(height: 24), BalanceSelector( - title: "Available${isFiro ? " public" : ""} balance", + title: "Available${hasPrivate ? " public" : ""} balance", coin: coin, balance: balance.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, value: _BalanceType.available, @@ -124,70 +130,70 @@ class WalletBalanceToggleSheet extends ConsumerWidget { ), const SizedBox(height: 12), BalanceSelector( - title: "Full${isFiro ? " public" : ""} balance", + title: "Full${hasPrivate ? " public" : ""} balance", coin: coin, balance: balance.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, value: _BalanceType.full, groupValue: _bal, ), - if (balanceTertiary != null) const SizedBox(height: 12), - if (balanceTertiary != null) + if (balancePrivate != null) const SizedBox(height: 12), + if (balancePrivate != null) BalanceSelector( - title: "Available Spark balance", + title: "Available Private balance", coin: coin, - balance: balanceTertiary.spendable, + balance: balancePrivate.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, - value: _BalanceType.sparkAvailable, + value: _BalanceType.privateAvailable, groupValue: _bal, ), - if (balanceTertiary != null) const SizedBox(height: 12), - if (balanceTertiary != null) + if (balancePrivate != null) const SizedBox(height: 12), + if (balancePrivate != null) BalanceSelector( - title: "Full Spark balance", + title: "Full Private balance", coin: coin, - balance: balanceTertiary.total, + balance: balancePrivate.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, - value: _BalanceType.sparkFull, + value: _BalanceType.privateFull, groupValue: _bal, ), const SizedBox(height: 40), diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart index 7661bffdd..3b8e255e8 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -85,10 +85,6 @@ class WalletSummaryInfo extends ConsumerWidget { ); } - final priceTuple = ref.watch( - priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin)), - ); - final _showAvailable = ref.watch(walletBalanceToggleStateProvider) == WalletBalanceToggleState.available; @@ -104,12 +100,12 @@ class WalletSummaryInfo extends ConsumerWidget { title = "${_showAvailable ? "Available" : "Full"} ${type.name.capitalize()} balance"; switch (type) { - case FiroType.spark: + case BalanceType.private: final balance = ref.watch(pWalletBalanceTertiary(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; - case FiroType.public: + case BalanceType.public: final balance = ref.watch(pWalletBalance(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart index 73a6bd742..f2c5429db 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart @@ -89,12 +89,12 @@ class DesktopPrivateBalanceToggleButton extends ConsumerWidget { color: Theme.of(context).extension()!.buttonBackSecondary, splashColor: Theme.of(context).extension()!.highlight, onPressed: () { - if (currentType != FiroType.spark) { + if (currentType != BalanceType.private) { ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; } else { ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; } onPressed?.call(); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 206e7ce6d..bf07ef580 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -165,11 +165,14 @@ class _DesktopSendState extends ConsumerState { final Amount availableBalance; if ((coin is Firo)) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: availableBalance = wallet.info.cachedBalance.spendable; break; - case FiroType.spark: - availableBalance = wallet.info.cachedBalanceTertiary.spendable; + case BalanceType.private: + availableBalance = + coin is Firo + ? wallet.info.cachedBalanceTertiary.spendable + : wallet.info.cachedBalanceSecondary.spendable; break; } } else { @@ -280,7 +283,7 @@ class _DesktopSendState extends ConsumerState { ref .read(publicPrivateBalanceStateProvider.state) .state == - FiroType.spark, + BalanceType.private, onCancel: () { wasCancelled = true; @@ -325,7 +328,7 @@ class _DesktopSendState extends ConsumerState { ); } else if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (ref.read(pValidSparkSendToAddress)) { txDataFuture = wallet.prepareSparkMintTransaction( txData: TxData( @@ -364,7 +367,7 @@ class _DesktopSendState extends ConsumerState { } break; - case FiroType.spark: + case BalanceType.private: txDataFuture = wallet.prepareSendSpark( txData: TxData( recipients: @@ -624,7 +627,8 @@ class _DesktopSendState extends ConsumerState { ref.read(pIsExchangeAddress.state).state = (coin as Firo) .isExchangeAddress(address ?? ""); - if (ref.read(publicPrivateBalanceStateProvider) == FiroType.spark && + if (ref.read(publicPrivateBalanceStateProvider) == + BalanceType.private && ref.read(pIsExchangeAddress) && !_isFiroExWarningDisplayed) { _isFiroExWarningDisplayed = true; @@ -833,10 +837,10 @@ class _DesktopSendState extends ConsumerState { amount = _selectedUtxosAmount(ref.read(desktopUseUTXOs)); } else if (coin is Firo) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: amount = ref.read(pWalletBalance(walletId)).spendable; break; - case FiroType.spark: + case BalanceType.private: amount = ref.read(pWalletBalanceTertiary(walletId)).spendable; break; } @@ -969,12 +973,12 @@ class _DesktopSendState extends ConsumerState { }); } - final firoType = ref.watch(publicPrivateBalanceStateProvider); + final balType = ref.watch(publicPrivateBalanceStateProvider); final isExchangeAddress = ref.watch(pIsExchangeAddress); ref.listen(publicPrivateBalanceStateProvider, (previous, next) { if (previous != next && - next == FiroType.spark && + next == BalanceType.private && isExchangeAddress && !_isFiroExWarningDisplayed) { _isFiroExWarningDisplayed = true; @@ -994,13 +998,13 @@ class _DesktopSendState extends ConsumerState { ), ) && ref.watch(pWallets).getWallet(walletId) is CoinControlInterface && - (coin is Firo ? firoType == FiroType.public : true); + (coin is Firo ? balType == BalanceType.public : true); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), - if (coin is Firo) + if (showPrivateBalance) Text( "Send from", style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -1011,19 +1015,19 @@ class _DesktopSendState extends ConsumerState { ), textAlign: TextAlign.left, ), - if (coin is Firo) const SizedBox(height: 10), - if (coin is Firo) + if (showPrivateBalance) const SizedBox(height: 10), + if (showPrivateBalance) DropdownButtonHideUnderline( child: DropdownButton2( isExpanded: true, - value: firoType, + value: balType, items: [ DropdownMenuItem( - value: FiroType.spark, + value: BalanceType.private, child: Row( children: [ Text( - "Spark balance", + "Private balance", style: STextStyles.itemSubtitle12(context), ), const SizedBox(width: 10), @@ -1032,7 +1036,11 @@ class _DesktopSendState extends ConsumerState { .watch(pAmountFormatter(coin)) .format( ref - .watch(pWalletBalanceTertiary(walletId)) + .watch( + isMwebEnabled + ? pWalletBalanceSecondary(walletId) + : pWalletBalanceTertiary(walletId), + ) .spendable, ), style: STextStyles.itemSubtitle(context), @@ -1041,7 +1049,7 @@ class _DesktopSendState extends ConsumerState { ), ), DropdownMenuItem( - value: FiroType.public, + value: BalanceType.public, child: Row( children: [ Text( @@ -1062,8 +1070,8 @@ class _DesktopSendState extends ConsumerState { ), ], onChanged: (value) { - if (value is FiroType) { - if (value != FiroType.public) { + if (value is BalanceType) { + if (value != BalanceType.public) { ref.read(desktopUseUTXOs.state).state = {}; } setState(() { @@ -1098,7 +1106,7 @@ class _DesktopSendState extends ConsumerState { ), ), ), - if (coin is Firo) const SizedBox(height: 20), + if (showPrivateBalance) const SizedBox(height: 20), if (isPaynymSend) Text( "Send to PayNym address", diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart index 1eb046484..6b08923ab 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart @@ -71,7 +71,7 @@ class _DesktopSendFeeFormState extends ConsumerState { (cryptoCurrency is ElectrumXCurrencyInterface && !(((cryptoCurrency is Firo) && (ref.watch(publicPrivateBalanceStateProvider.state).state == - FiroType.spark)))); + BalanceType.private)))); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -183,7 +183,7 @@ class _DesktopSendFeeFormState extends ConsumerState { .state, ) .state != - FiroType.public) { + BalanceType.public) { final firoWallet = wallet as FiroWallet; if (ref @@ -192,7 +192,7 @@ class _DesktopSendFeeFormState extends ConsumerState { .state, ) .state == - FiroType.spark) { + BalanceType.private) { ref .read(feeSheetSessionCacheProvider) .average[amount] = await firoWallet diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index 84c52c870..c6ad2694d 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -104,12 +104,12 @@ class _WDesktopWalletSummaryState extends ConsumerState { final Amount balanceToShow; if (isFiro) { switch (ref.watch(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: final balance = ref.watch(pWalletBalanceTertiary(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; - case FiroType.public: + case BalanceType.public: final balance = ref.watch(pWalletBalance(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart index 2de6daf39..650f67f68 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart @@ -104,7 +104,7 @@ class _WFiroDesktopWalletSummaryState children: [ TableRow( children: [ - const _Prefix(type: FiroType.spark), + const _Prefix(type: BalanceType.private), _Balance(coin: coin, amount: balanceToShowSpark), if (price != null) _Price( @@ -117,7 +117,7 @@ class _WFiroDesktopWalletSummaryState TableRow( children: [ - const _Prefix(type: FiroType.public), + const _Prefix(type: BalanceType.public), _Balance(coin: coin, amount: balanceToShowPublic), if (price != null) _Price( @@ -147,13 +147,13 @@ class _WFiroDesktopWalletSummaryState class _Prefix extends StatelessWidget { const _Prefix({super.key, required this.type}); - final FiroType type; + final BalanceType type; String get asset { switch (type) { - case FiroType.public: + case BalanceType.public: return Assets.png.glasses; - case FiroType.spark: + case BalanceType.private: return Assets.svg.spark; } } diff --git a/lib/providers/ui/preview_tx_button_state_provider.dart b/lib/providers/ui/preview_tx_button_state_provider.dart index 35daf888f..9ff71aca7 100644 --- a/lib/providers/ui/preview_tx_button_state_provider.dart +++ b/lib/providers/ui/preview_tx_button_state_provider.dart @@ -27,13 +27,13 @@ final pPreviewTxButtonEnabled = Provider.autoDispose if (coin is Firo) { final firoType = ref.watch(publicPrivateBalanceStateProvider); switch (firoType) { - case FiroType.spark: + case BalanceType.private: return (ref.watch(pValidSendToAddress) || ref.watch(pValidSparkSendToAddress)) && !ref.watch(pIsExchangeAddress) && amount > Amount.zero; - case FiroType.public: + case BalanceType.public: return ref.watch(pValidSendToAddress) && amount > Amount.zero; } } else { diff --git a/lib/providers/wallet/public_private_balance_state_provider.dart b/lib/providers/wallet/public_private_balance_state_provider.dart index 7484db9f0..39ce0ae88 100644 --- a/lib/providers/wallet/public_private_balance_state_provider.dart +++ b/lib/providers/wallet/public_private_balance_state_provider.dart @@ -10,8 +10,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -enum FiroType { public, spark } +enum BalanceType { public, private } -final publicPrivateBalanceStateProvider = StateProvider( - (_) => FiroType.spark, +final publicPrivateBalanceStateProvider = StateProvider( + (_) => BalanceType.private, ); diff --git a/lib/widgets/desktop/desktop_fee_dialog.dart b/lib/widgets/desktop/desktop_fee_dialog.dart index 3252be98e..82cc82e0b 100644 --- a/lib/widgets/desktop/desktop_fee_dialog.dart +++ b/lib/widgets/desktop/desktop_fee_dialog.dart @@ -69,11 +69,11 @@ class _DesktopFeeDialogState extends ConsumerState { } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, @@ -119,11 +119,11 @@ class _DesktopFeeDialogState extends ConsumerState { } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, @@ -169,11 +169,11 @@ class _DesktopFeeDialogState extends ConsumerState { } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, From a8fc2f90e3fd8528318417f41be4efbdc51369a8 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 19 Jun 2025 09:41:52 -0600 Subject: [PATCH 12/42] refactor signing data --- lib/models/signing_data.dart | 49 ++++++++++--- lib/wallets/wallet/impl/namecoin_wallet.dart | 15 ++-- lib/wallets/wallet/impl/particl_wallet.dart | 8 +-- .../bcash_interface.dart | 33 +++++---- .../electrumx_interface.dart | 70 +++++++++++-------- .../paynym_interface.dart | 27 +++---- .../spark_interface.dart | 20 +++--- 7 files changed, 135 insertions(+), 87 deletions(-) diff --git a/lib/models/signing_data.dart b/lib/models/signing_data.dart index 265a021db..a6e8036a3 100644 --- a/lib/models/signing_data.dart +++ b/lib/models/signing_data.dart @@ -9,26 +9,53 @@ */ import 'package:coinlib_flutter/coinlib_flutter.dart'; -import 'isar/models/isar_models.dart'; + +import '../db/drift/database.dart'; import '../utilities/enums/derive_path_type_enum.dart'; +import 'isar/models/isar_models.dart'; + +abstract class BaseInput { + BaseInput(this._utxo, {this.key}); + + final Object _utxo; + HDKey? key; + + @override + String toString() { + return "BaseInput{\n" + " _utxo: $_utxo,\n" + " key: $key,\n" + "}"; + } +} -class SigningData { - SigningData({ - required this.derivePathType, - required this.utxo, - this.keyPair, - }); +class StandardInput extends BaseInput { + StandardInput(UTXO super.utxo, {required this.derivePathType, super.key}); final DerivePathType derivePathType; - final UTXO utxo; - HDPrivateKey? keyPair; + + UTXO get utxo => _utxo as UTXO; @override String toString() { - return "SigningData{\n" + return "StandardInput{\n" " derivePathType: $derivePathType,\n" " utxo: $utxo,\n" - " keyPair: $keyPair,\n" + " key: $key,\n" + "}"; + } +} + +class MwebInput extends BaseInput { + MwebInput(MwebUtxo super.utxo); + + MwebUtxo get utxo => _utxo as MwebUtxo; + + @override + String toString() { + return "MwebInput{\n" + " utxo: $utxo,\n" + " key: $key,\n" "}"; } } diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index bb3f020f3..a1970b974 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -627,7 +627,7 @@ class NamecoinWallet /// Builds and signs a transaction Future _createNameTx({ required TxData txData, - required List utxoSigningData, + required List utxoSigningData, required bool isForFeeCalcPurposesOnly, }) async { Logging.instance.d("Starting _createNameTx ----------"); @@ -692,7 +692,7 @@ class NamecoinWallet case DerivePathType.bip44: input = coinlib.P2PKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: utxoSigningData[i].key!.publicKey, sequence: sequence, ); @@ -710,7 +710,7 @@ class NamecoinWallet case DerivePathType.bip84: input = coinlib.P2WPKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: utxoSigningData[i].key!.publicKey, sequence: sequence, ); @@ -810,11 +810,11 @@ class NamecoinWallet // Sign the transaction accordingly for (int i = 0; i < utxoSigningData.length; i++) { final value = BigInt.from(utxoSigningData[i].utxo.value); - final key = utxoSigningData[i].keyPair!.privateKey; + final key = utxoSigningData[i].key!.privateKey!; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( - internalKey: utxoSigningData[i].keyPair!.publicKey, + internalKey: utxoSigningData[i].key!.publicKey, ); clTx = clTx.signTaproot( @@ -1115,7 +1115,10 @@ class NamecoinWallet final List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data - final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + final utxoSigningData = + (await fetchBuildTxData( + utxoObjectsToUse, + )).whereType().toList(); final int vSizeForOneOutput; try { diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index 1188625cb..19a9a9e20 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -352,7 +352,7 @@ class ParticlWallet @override Future buildTransaction({ required TxData txData, - required List utxoSigningData, + required covariant List utxoSigningData, }) async { Logging.instance.d("Starting Particl buildTransaction ----------"); @@ -374,7 +374,7 @@ class ParticlWallet for (int i = 0; i < utxoSigningData.length; i++) { final sd = utxoSigningData[i]; - final pubKey = sd.keyPair!.publicKey.data; + final pubKey = sd.key!.publicKey.data; final bitcoindart.PaymentData? data; Uint8List? redeem, output; @@ -512,9 +512,9 @@ class ParticlWallet txb.sign( vin: i, keyPair: bitcoindart.ECPair.fromPrivateKey( - utxoSigningData[i].keyPair!.privateKey.data, + utxoSigningData[i].key!.privateKey!.data, network: convertedNetwork, - compressed: utxoSigningData[i].keyPair!.privateKey.compressed, + compressed: utxoSigningData[i].key!.privateKey!.compressed, ), witnessValue: utxoSigningData[i].utxo.value, redeemScript: extraData[i].redeem, diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart index 371236180..492f727b0 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart @@ -18,7 +18,7 @@ mixin BCashInterface @override Future buildTransaction({ required TxData txData, - required List utxoSigningData, + required covariant List utxoSigningData, }) async { Logging.instance.d("Starting buildTransaction ----------"); @@ -50,9 +50,10 @@ mixin BCashInterface txid: utxoSigningData[i].utxo.txid, vout: utxoSigningData[i].utxo.vout, ), - addresses: utxoSigningData[i].utxo.address == null - ? [] - : [utxoSigningData[i].utxo.address!], + addresses: + utxoSigningData[i].utxo.address == null + ? [] + : [utxoSigningData[i].utxo.address!], valueStringSats: utxoSigningData[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, @@ -73,10 +74,9 @@ mixin BCashInterface OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "000000", valueStringSats: txData.recipients![i].amount.raw.toString(), - addresses: [ - txData.recipients![i].address.toString(), - ], - walletOwns: (await mainDB.isar.addresses + addresses: [txData.recipients![i].address.toString()], + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -94,7 +94,7 @@ mixin BCashInterface // Sign the transaction accordingly for (int i = 0; i < utxoSigningData.length; i++) { final bitboxEC = bitbox.ECPair.fromPrivateKey( - utxoSigningData[i].keyPair!.privateKey.data, + utxoSigningData[i].key!.privateKey!.data, network: bitbox_utils.Network( cryptoCurrency.networkParams.privHDPrefix, cryptoCurrency.networkParams.pubHDPrefix, @@ -103,18 +103,17 @@ mixin BCashInterface cryptoCurrency.networkParams.wifPrefix, cryptoCurrency.networkParams.p2pkhPrefix, ), - compressed: utxoSigningData[i].keyPair!.privateKey.compressed, + compressed: utxoSigningData[i].key!.privateKey!.compressed, ); - builder.sign( - i, - bitboxEC, - utxoSigningData[i].utxo.value, - ); + builder.sign(i, bitboxEC, utxoSigningData[i].utxo.value); } } catch (e, s) { - Logging.instance.e("Caught exception while signing transaction: ", - error: e, stackTrace: s); + Logging.instance.e( + "Caught exception while signing transaction: ", + error: e, + stackTrace: s, + ); rethrow; } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index dda6b2876..6a505c60c 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -325,7 +325,11 @@ mixin ElectrumXInterface rawValue: feeForOneOutput, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: + utxoSigningData + .whereType() + .map((e) => e.utxo) + .toList(), ); } @@ -418,7 +422,11 @@ mixin ElectrumXInterface rawValue: feeBeingPaid, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: + utxoSigningData + .whereType() + .map((e) => e.utxo) + .toList(), ); } else { // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize @@ -438,7 +446,7 @@ mixin ElectrumXInterface required String recipientAddress, required BigInt satoshiAmountToSend, required BigInt satoshisBeingUsed, - required List utxoSigningData, + required List utxoSigningData, required int? satsPerVByte, required BigInt feeRatePerKB, }) async { @@ -491,13 +499,17 @@ mixin ElectrumXInterface rawValue: feeForOneOutput, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: + utxoSigningData + .whereType() + .map((e) => e.utxo) + .toList(), ); } - Future> fetchBuildTxData(List utxosToUse) async { + Future> fetchBuildTxData(List utxosToUse) async { // return data - final List signingData = []; + final List signingData = []; try { // Populating the addresses to check @@ -507,7 +519,7 @@ mixin ElectrumXInterface ); signingData.add( - SigningData(derivePathType: derivePathType, utxo: utxosToUse[i]), + StandardInput(utxosToUse[i], derivePathType: derivePathType), ); } @@ -552,7 +564,7 @@ mixin ElectrumXInterface ); } - sd.keyPair = keys; + sd.key = keys; } return signingData; @@ -565,7 +577,7 @@ mixin ElectrumXInterface /// Builds and signs a transaction Future buildTransaction({ required TxData txData, - required List utxoSigningData, + required List utxoSigningData, }) async { Logging.instance.d("Starting buildTransaction ----------"); @@ -587,20 +599,22 @@ mixin ElectrumXInterface ? 0xffffffff - 10 : 0xffffffff - 1; + final standardInputs = utxoSigningData.whereType().toList(); + // Add transaction inputs - for (var i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (var i = 0; i < standardInputs.length; i++) { + final txid = standardInputs[i].utxo.txid; final hash = Uint8List.fromList( txid.toUint8ListFromHex.reversed.toList(), ); - final prevOutpoint = coinlib.OutPoint(hash, utxoSigningData[i].utxo.vout); + final prevOutpoint = coinlib.OutPoint(hash, standardInputs[i].utxo.vout); final prevOutput = coinlib.Output.fromAddress( - BigInt.from(utxoSigningData[i].utxo.value), + BigInt.from(standardInputs[i].utxo.value), coinlib.Address.fromString( - utxoSigningData[i].utxo.address!, + standardInputs[i].utxo.address!, cryptoCurrency.networkParams, ), ); @@ -609,12 +623,12 @@ mixin ElectrumXInterface final coinlib.Input input; - switch (utxoSigningData[i].derivePathType) { + switch (standardInputs[i].derivePathType) { case DerivePathType.bip44: case DerivePathType.bch44: input = coinlib.P2PKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: standardInputs[i].key!.publicKey, sequence: sequence, ); @@ -624,7 +638,7 @@ mixin ElectrumXInterface // input = coinlib.P2SHMultisigInput( // prevOut: prevOutpoint, // program: coinlib.MultisigProgram.decompile( - // utxoSigningData[i].redeemScript!, + // standardInputs[i].redeemScript!, // ), // sequence: sequence, // ); @@ -632,7 +646,7 @@ mixin ElectrumXInterface case DerivePathType.bip84: input = coinlib.P2WPKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: standardInputs[i].key!.publicKey, sequence: sequence, ); @@ -641,7 +655,7 @@ mixin ElectrumXInterface default: throw UnsupportedError( - "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + "Unknown derivation path type found: ${standardInputs[i].derivePathType}", ); } @@ -653,14 +667,14 @@ mixin ElectrumXInterface scriptSigAsm: null, sequence: sequence, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + txid: standardInputs[i].utxo.txid, + vout: standardInputs[i].utxo.vout, ), addresses: - utxoSigningData[i].utxo.address == null + standardInputs[i].utxo.address == null ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), + : [standardInputs[i].utxo.address!], + valueStringSats: standardInputs[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -715,13 +729,13 @@ mixin ElectrumXInterface try { // Sign the transaction accordingly - for (var i = 0; i < utxoSigningData.length; i++) { - final value = BigInt.from(utxoSigningData[i].utxo.value); - final key = utxoSigningData[i].keyPair!.privateKey; + for (var i = 0; i < standardInputs.length; i++) { + final value = BigInt.from(standardInputs[i].utxo.value); + final key = standardInputs[i].key!.privateKey!; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( - internalKey: utxoSigningData[i].keyPair!.publicKey, + internalKey: standardInputs[i].key!.publicKey, ); clTx = clTx.signTaproot( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart index 1bfb3a2a3..5d38c3d87 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart @@ -526,7 +526,10 @@ mixin PaynymInterface } // gather required signing data - final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + final utxoSigningData = + (await fetchBuildTxData( + utxoObjectsToUse, + )).whereType().toList(); final vSizeForNoChange = BigInt.from( (await _createNotificationTx( @@ -706,7 +709,7 @@ mixin PaynymInterface // equal to its vSize Future> _createNotificationTx({ required String targetPaymentCodeString, - required List utxoSigningData, + required List utxoSigningData, required BigInt change, BigInt? overrideAmountForTesting, }) async { @@ -726,10 +729,10 @@ mixin PaynymInterface final buffer = rev.buffer.asByteData(); buffer.setUint32(txPoint.length, txPointIndex, Endian.little); - final myKeyPair = utxoSigningData.first.keyPair!; + final myKeyPair = utxoSigningData.first.key!; final S = SecretPoint( - myKeyPair.privateKey.data, + myKeyPair.privateKey!.data, targetPaymentCode.notificationPublicKey(), ); @@ -785,7 +788,7 @@ mixin PaynymInterface case DerivePathType.bch44: input = coinlib.P2PKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: utxoSigningData[i].key!.publicKey, sequence: 0xffffffff - 1, ); @@ -803,7 +806,7 @@ mixin PaynymInterface case DerivePathType.bip84: input = coinlib.P2WPKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: utxoSigningData[i].key!.publicKey, sequence: 0xffffffff - 1, ); @@ -860,21 +863,21 @@ mixin PaynymInterface clTx = clTx.signTaproot( inputN: 0, - key: taproot.tweakPrivateKey(myKeyPair.privateKey), + key: taproot.tweakPrivateKey(myKeyPair.privateKey!), prevOuts: prevOuts, ); } else if (clTx.inputs[0] is coinlib.LegacyWitnessInput) { clTx = clTx.signLegacyWitness( inputN: 0, - key: myKeyPair.privateKey, + key: myKeyPair.privateKey!, value: BigInt.from(utxo.value), ); } else if (clTx.inputs[0] is coinlib.LegacyInput) { - clTx = clTx.signLegacy(inputN: 0, key: myKeyPair.privateKey); + clTx = clTx.signLegacy(inputN: 0, key: myKeyPair.privateKey!); } else if (clTx.inputs[0] is coinlib.TaprootSingleScriptSigInput) { clTx = clTx.signTaprootSingleScriptSig( inputN: 0, - key: myKeyPair.privateKey, + key: myKeyPair.privateKey!, prevOuts: prevOuts, ); } else { @@ -886,11 +889,11 @@ mixin PaynymInterface // sign rest of possible inputs for (int i = 1; i < utxoSigningData.length; i++) { final value = BigInt.from(utxoSigningData[i].utxo.value); - final key = utxoSigningData[i].keyPair!.privateKey; + final key = utxoSigningData[i].key!.privateKey!; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( - internalKey: utxoSigningData[i].keyPair!.publicKey, + internalKey: utxoSigningData[i].key!.publicKey, ); clTx = clTx.signTaproot( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 21bd7a74a..02a257ea8 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1413,7 +1413,7 @@ mixin SparkInterface ? max(0, currentHeight - random.nextInt(100)) : currentHeight; const txVersion = 1; - final List vin = []; + final List vin = []; final List<(dynamic, int, String?)> vout = []; BigInt nFeeRet = BigInt.zero; @@ -1426,7 +1426,7 @@ mixin SparkInterface } BigInt nValueToSelect, mintedValue; - final List setCoins = []; + final List setCoins = []; bool skipCoin = false; // Start with no fee and loop until there is enough fee @@ -1555,7 +1555,9 @@ mixin SparkInterface BigInt nValueIn = BigInt.zero; for (final utxo in itr) { if (nValueToSelect > nValueIn) { - setCoins.add((await fetchBuildTxData([utxo])).first); + setCoins.add( + (await fetchBuildTxData([utxo])).whereType().first, + ); nValueIn += BigInt.from(utxo.value); } } @@ -1595,7 +1597,7 @@ mixin SparkInterface for (final sd in setCoins) { vin.add(sd); - final pubKey = sd.keyPair!.publicKey.data; + final pubKey = sd.key!.publicKey.data; final btc.PaymentData? data; switch (sd.derivePathType) { @@ -1659,9 +1661,9 @@ mixin SparkInterface dummyTxb.sign( vin: i, keyPair: btc.ECPair.fromPrivateKey( - setCoins[i].keyPair!.privateKey.data, + setCoins[i].key!.privateKey!.data, network: _bitcoinDartNetwork, - compressed: setCoins[i].keyPair!.privateKey.compressed, + compressed: setCoins[i].key!.privateKey!.compressed, ), witnessValue: setCoins[i].utxo.value, @@ -1756,7 +1758,7 @@ mixin SparkInterface txb.setVersion(txVersion); txb.setLockTime(lockTime); for (final input in vin) { - final pubKey = input.keyPair!.publicKey.data; + final pubKey = input.key!.publicKey.data; final btc.PaymentData? data; switch (input.derivePathType) { @@ -1867,9 +1869,9 @@ mixin SparkInterface txb.sign( vin: i, keyPair: btc.ECPair.fromPrivateKey( - vin[i].keyPair!.privateKey.data, + vin[i].key!.privateKey!.data, network: _bitcoinDartNetwork, - compressed: vin[i].keyPair!.privateKey.compressed, + compressed: vin[i].key!.privateKey!.compressed, ), witnessValue: vin[i].utxo.value, From 1054b5a996b07cdabfa137194fbac68e37ebbd9b Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 20 Jun 2025 11:50:40 -0600 Subject: [PATCH 13/42] WIP broken mweb transactions --- lib/models/signing_data.dart | 56 +- .../exchange_step_views/step_4_view.dart | 8 +- lib/pages/exchange_view/send_from_view.dart | 12 +- lib/pages/namecoin_names/buy_domain_view.dart | 320 ++++++----- .../sub_widgets/transfer_option_widget.dart | 2 +- .../sub_widgets/update_option_widget.dart | 2 +- .../addresses/address_details_view.dart | 474 ++++++++-------- .../send_view/frost_ms/frost_send_view.dart | 8 +- .../send_steps/frost_send_step_1b.dart | 83 +-- .../send_steps/frost_send_step_3.dart | 97 ++-- lib/pages/send_view/send_view.dart | 60 ++- lib/pages/send_view/token_send_view.dart | 4 +- .../desktop_coin_control_use_dialog.dart | 505 +++++++++--------- .../wallet_view/sub_widgets/desktop_send.dart | 63 ++- .../sub_widgets/desktop_token_send.dart | 4 +- .../sub_widgets/desktop_wallet_features.dart | 15 +- .../more_features/more_features_dialog.dart | 2 +- lib/services/frost.dart | 217 ++++---- lib/services/mwebd_service.dart | 217 +++++++- .../crypto_currency/coins/litecoin.dart | 2 + lib/wallets/models/tx_data.dart | 13 +- lib/wallets/models/tx_recipient.dart | 5 + .../wallet/impl/bitcoin_frost_wallet.dart | 12 +- lib/wallets/wallet/impl/cardano_wallet.dart | 2 +- lib/wallets/wallet/impl/epiccash_wallet.dart | 5 +- lib/wallets/wallet/impl/litecoin_wallet.dart | 4 +- lib/wallets/wallet/impl/namecoin_wallet.dart | 10 +- lib/wallets/wallet/impl/tezos_wallet.dart | 2 +- lib/wallets/wallet/impl/wownero_wallet.dart | 2 +- lib/wallets/wallet/impl/xelis_wallet.dart | 12 +- .../intermediate/lib_monero_wallet.dart | 36 +- .../intermediate/lib_salvium_wallet.dart | 38 +- lib/wallets/wallet/wallet.dart | 95 ++-- .../electrumx_interface.dart | 411 ++++++++------ .../mweb_interface.dart | 325 +++++++---- .../paynym_interface.dart | 16 +- .../rbf_interface.dart | 91 ++-- .../spark_interface.dart | 32 +- pubspec.lock | 32 +- 39 files changed, 1929 insertions(+), 1365 deletions(-) diff --git a/lib/models/signing_data.dart b/lib/models/signing_data.dart index a6e8036a3..2fcfde38d 100644 --- a/lib/models/signing_data.dart +++ b/lib/models/signing_data.dart @@ -20,6 +20,14 @@ abstract class BaseInput { final Object _utxo; HDKey? key; + String get id; + + String? get address; + + BigInt get value; + + int? get blockTime; + @override String toString() { return "BaseInput{\n" @@ -30,12 +38,24 @@ abstract class BaseInput { } class StandardInput extends BaseInput { - StandardInput(UTXO super.utxo, {required this.derivePathType, super.key}); + StandardInput(UTXO super.utxo, {this.derivePathType, super.key}); - final DerivePathType derivePathType; + final DerivePathType? derivePathType; UTXO get utxo => _utxo as UTXO; + @override + String get id => utxo.txid; + + @override + String? get address => utxo.address; + + @override + BigInt get value => BigInt.from(utxo.value); + + @override + int? get blockTime => utxo.blockTime; + @override String toString() { return "StandardInput{\n" @@ -44,6 +64,18 @@ class StandardInput extends BaseInput { " key: $key,\n" "}"; } + + @override + bool operator ==(Object other) { + return other is StandardInput && + other.utxo.walletId == utxo.walletId && + other.utxo.txid == utxo.txid && + other.utxo.vout == utxo.vout && + other.derivePathType == derivePathType; + } + + @override + int get hashCode => Object.hashAll([utxo.walletId, utxo.txid, utxo.vout]); } class MwebInput extends BaseInput { @@ -51,6 +83,18 @@ class MwebInput extends BaseInput { MwebUtxo get utxo => _utxo as MwebUtxo; + @override + String get id => utxo.outputId; + + @override + String get address => utxo.address; + + @override + BigInt get value => BigInt.from(utxo.value); + + @override + int? get blockTime => utxo.blockTime < 1 ? null : utxo.blockTime; + @override String toString() { return "MwebInput{\n" @@ -58,4 +102,12 @@ class MwebInput extends BaseInput { " key: $key,\n" "}"; } + + @override + bool operator ==(Object other) { + return other is MwebInput && other.utxo == utxo; + } + + @override + int get hashCode => Object.hashAll([utxo.hashCode]); } diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index 72d2ff241..11bc9fe82 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -233,7 +233,9 @@ class _Step4ViewState extends ConsumerState { if (wallet is FiroWallet && !firoPublicSend) { txDataFuture = wallet.prepareSendSpark( txData: TxData( - recipients: [(address: address, amount: amount, isChange: false)], + recipients: [ + TxRecipient(address: address, amount: amount, isChange: false), + ], note: "${model.trade!.payInCurrency.toUpperCase()}/" "${model.trade!.payOutCurrency.toUpperCase()} exchange", @@ -248,7 +250,9 @@ class _Step4ViewState extends ConsumerState { : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [(address: address, amount: amount, isChange: false)], + recipients: [ + TxRecipient(address: address, amount: amount, isChange: false), + ], memo: memo, feeRateType: FeeRateType.average, note: diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 8f397e79e..dd98dce23 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -293,7 +293,9 @@ class _SendFromCardState extends ConsumerState { : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [(address: address, amount: amount, isChange: false)], + recipients: [ + TxRecipient(address: address, amount: amount, isChange: false), + ], memo: memo, feeRateType: FeeRateType.average, ), @@ -304,14 +306,18 @@ class _SendFromCardState extends ConsumerState { if (shouldSendPublicFiroFunds) { txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [(address: address, amount: amount, isChange: false)], + recipients: [ + TxRecipient(address: address, amount: amount, isChange: false), + ], feeRateType: FeeRateType.average, ), ); } else { txDataFuture = firoWallet.prepareSendSpark( txData: TxData( - recipients: [(address: address, amount: amount, isChange: false)], + recipients: [ + TxRecipient(address: address, amount: amount, isChange: false), + ], // feeRateType: FeeRateType.average, ), ); diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart index d0dddda3e..6b2695ef5 100644 --- a/lib/pages/namecoin_names/buy_domain_view.dart +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -103,7 +103,7 @@ class _BuyDomainWidgetState extends ConsumerState { note: "Reserve ${widget.domainName.substring(2)}.bit", feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( + TxRecipient( address: myAddress.value, isChange: false, amount: Amount( @@ -123,28 +123,30 @@ class _BuyDomainWidgetState extends ConsumerState { if (_preRegLock) return; _preRegLock = true; try { - final txData = (await showLoading( - whileFuture: _preRegFuture(), - context: context, - message: "Preparing transaction...", - onException: (e) { - throw e; - }, - ))!; + 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, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), ), - ), - ), ); } else { await Navigator.of(context).pushNamed( @@ -164,12 +166,13 @@ class _BuyDomainWidgetState extends ConsumerState { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: err, - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: + (_) => StackOkDialog( + title: "Error", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -192,50 +195,48 @@ class _BuyDomainWidgetState extends ConsumerState { 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(); - }, + 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), ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, ), - child: AddDnsStep1( - name: _getNameFormattedForInternal(), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: AddDnsStep1( + name: _getNameFormattedForInternal(), ), - ], - ), + ), + ], ), - ) + ), + ) : StackDialogBase( - child: AddDnsStep1( - name: _getNameFormattedForInternal(), - ), - ); + child: AddDnsStep1( + name: _getNameFormattedForInternal(), + ), + ); }, ); }, @@ -254,11 +255,12 @@ class _BuyDomainWidgetState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Add DNS record failed", - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: + (_) => StackOkDialog( + title: "Add DNS record failed", + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -289,8 +291,9 @@ class _BuyDomainWidgetState extends ConsumerState { builder: (ctx, constraints) { return SingleChildScrollView( child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight), + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), child: IntrinsicHeight( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -306,114 +309,120 @@ class _BuyDomainWidgetState extends ConsumerState { ); }, child: Column( - crossAxisAlignment: Util.isDesktop - ? CrossAxisAlignment.start - : CrossAxisAlignment.stretch, + crossAxisAlignment: + Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ if (!Util.isDesktop) Text( "Buy domain", - style: Util.isDesktop - ? STextStyles.desktopH3(context) - : STextStyles.pageTitleH2(context), + style: + Util.isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH2(context), ), - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), Row( - mainAxisAlignment: Util.isDesktop - ? MainAxisAlignment.center - : MainAxisAlignment.start, + 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, - ), + 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, - ), + 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, - ), + 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), + style: + Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), ), ], ), ), - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), + 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, - ), + 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( + ref + .watch(pAmountFormatter(coin)) + .format( Amount( rawValue: BigInt.from(kNameNewAmountSats), fractionDigits: coin.fractionDigits, ), ), - style: Util.isDesktop - ? STextStyles.w500_14(context) - : STextStyles.w500_12(context), + style: + Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), ), ], ), ), - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), ConditionalParent( condition: !Util.isDesktop, - builder: (child) => Row( - children: [child], - ), + builder: (child) => Row(children: [child]), child: CustomTextButton( text: _settingsHidden ? "More settings" : "Hide settings", onTap: () { @@ -423,10 +432,7 @@ class _BuyDomainWidgetState extends ConsumerState { }, ), ), - if (!_settingsHidden) - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + if (!_settingsHidden) SizedBox(height: Util.isDesktop ? 24 : 16), if (!_settingsHidden) if (_dnsRecords.isEmpty) RoundedWhiteContainer( @@ -436,9 +442,10 @@ class _BuyDomainWidgetState extends ConsumerState { Text( "Add DNS records to your domain name", style: STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ], @@ -455,27 +462,25 @@ class _BuyDomainWidgetState extends ConsumerState { (e) => DNSRecordCard( key: ValueKey(e), record: e, - onRemoveTapped: () => setState(() { - _dnsRecords.remove(e); - }), + onRemoveTapped: + () => setState(() { + _dnsRecords.remove(e); + }), ), ), - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), + SizedBox(height: Util.isDesktop ? 16 : 8), SecondaryButton( - label: _dnsRecords.isEmpty - ? "Add DNS record" - : "Add another DNS record", + label: + _dnsRecords.isEmpty + ? "Add DNS record" + : "Add another DNS record", buttonHeight: Util.isDesktop ? ButtonHeight.l : null, onPressed: _addRecord, ), ], ), ), - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), if (!Util.isDesktop && _settingsHidden) const Spacer(), PrimaryButton( label: "Buy", @@ -483,9 +488,7 @@ class _BuyDomainWidgetState extends ConsumerState { buttonHeight: Util.isDesktop ? ButtonHeight.l : null, onPressed: _preRegister, ), - SizedBox( - height: Util.isDesktop ? 32 : 16, - ), + SizedBox(height: Util.isDesktop ? 32 : 16), ], ), ); @@ -524,13 +527,8 @@ class DNSRecordCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "${record.type.name}$_extraInfo", - ), - CustomTextButton( - text: "Remove", - onTap: onRemoveTapped, - ), + Text("${record.type.name}$_extraInfo"), + CustomTextButton(text: "Remove", onTap: onRemoveTapped), ], ), Text(record.getValueString()), diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart index 6aaad59c0..db869b5f3 100644 --- a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -133,7 +133,7 @@ class _TransferOptionWidgetState extends ConsumerState { txData: TxData( feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( + TxRecipient( address: _address!, isChange: false, amount: Amount( diff --git a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart index e438e420c..ac9f665d6 100644 --- a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart @@ -147,7 +147,7 @@ class _BuyDomainWidgetState extends ConsumerState { txData: TxData( feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( + TxRecipient( address: _address!.value, isChange: false, amount: Amount( diff --git a/lib/pages/receive_view/addresses/address_details_view.dart b/lib/pages/receive_view/addresses/address_details_view.dart index 25e18becb..96c2e66a6 100644 --- a/lib/pages/receive_view/addresses/address_details_view.dart +++ b/lib/pages/receive_view/addresses/address_details_view.dart @@ -70,60 +70,60 @@ class _AddressDetailsViewState extends ConsumerState { void _showDesktopAddressQrCode() { showDialog( context: context, - builder: (context) => DesktopDialog( - maxWidth: 480, - maxHeight: 400, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 480, + maxHeight: 400, + child: Column( children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Address QR code", - style: STextStyles.desktopH3(context), - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Address QR code", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center( - child: RepaintBoundary( - key: _qrKey, - child: QR( - data: AddressUtils.buildUriString( - ref.watch(pWalletCoin(widget.walletId)).uriScheme, - address.value, - {}, + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: RepaintBoundary( + key: _qrKey, + child: QR( + data: AddressUtils.buildUriString( + ref.watch(pWalletCoin(widget.walletId)).uriScheme, + address.value, + {}, + ), + size: 220, + ), ), - size: 220, ), - ), + ], ), - ], - ), - ), - const SizedBox( - height: 32, + ), + const SizedBox(height: 32), + ], ), - ], - ), - ), + ), ); } @override void initState() { - address = MainDB.instance.isar.addresses - .where() - .idEqualTo(widget.addressId) - .findFirstSync()!; + address = + MainDB.instance.isar.addresses + .where() + .idEqualTo(widget.addressId) + .findFirstSync()!; label = MainDB.instance.getAddressLabelSync(widget.walletId, address.value); Id? id = label?.id; @@ -132,9 +132,10 @@ class _AddressDetailsViewState extends ConsumerState { walletId: widget.walletId, addressString: address.value, value: "", - tags: address.subType == AddressSubType.receiving - ? ["receiving"] - : address.subType == AddressSubType.change + tags: + address.subType == AddressSubType.receiving + ? ["receiving"] + : address.subType == AddressSubType.change ? ["change"] : null, ); @@ -151,43 +152,46 @@ class _AddressDetailsViewState extends ConsumerState { final wallet = ref.watch(pWallets).getWallet(widget.walletId); return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.backgroundAppBar, - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Text( - "Address details", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ); - }, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Address details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ); + }, + ), + ), ), ), - ), - ), child: StreamBuilder( stream: stream, builder: (context, snapshot) { @@ -200,9 +204,7 @@ class _AddressDetailsViewState extends ConsumerState { builder: (child) { return Column( children: [ - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), RoundedWhiteContainer( padding: const EdgeInsets.all(24), child: Column( @@ -215,9 +217,10 @@ class _AddressDetailsViewState extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), CustomTextButton( @@ -226,19 +229,16 @@ class _AddressDetailsViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), RoundedWhiteContainer( padding: EdgeInsets.zero, - borderColor: Theme.of(context) - .extension()! - .backgroundAppBar, + borderColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, child: child, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -247,34 +247,35 @@ class _AddressDetailsViewState extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( padding: EdgeInsets.zero, - borderColor: Theme.of(context) - .extension()! - .backgroundAppBar, - child: ref - .watch(pWallets) - .getWallet(widget.walletId) - .isarTransactionVersion == - 2 - ? _AddressDetailsTxV2List( - walletId: widget.walletId, - address: address, - ) - : _AddressDetailsTxList( - walletId: widget.walletId, - address: address, - ), + borderColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, + child: + ref + .watch(pWallets) + .getWallet(widget.walletId) + .isarTransactionVersion == + 2 + ? _AddressDetailsTxV2List( + walletId: widget.walletId, + address: address, + ) + : _AddressDetailsTxList( + walletId: widget.walletId, + address: address, + ), ), ], ), @@ -299,24 +300,16 @@ class _AddressDetailsViewState extends ConsumerState { ), ), ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), DetailItem( title: "Address", detail: address.value, - button: isDesktop - ? IconCopyButton( - data: address.value, - ) - : SimpleCopyButton( - data: address.value, - ), - ), - const _Div( - height: 12, + button: + isDesktop + ? IconCopyButton(data: address.value) + : SimpleCopyButton(data: address.value), ), + const _Div(height: 12), DetailItem( title: "Label", detail: label!.value, @@ -325,59 +318,47 @@ class _AddressDetailsViewState extends ConsumerState { editLabel: 'label', onValueChanged: (value) { MainDB.instance.putAddressLabel( - label!.copyWith( - label: value, - ), + label!.copyWith(label: value), ); }, ), ), - const _Div( - height: 12, - ), - _Tags( - tags: label!.tags, - ), - if (address.derivationPath != null) - const _Div( - height: 12, - ), + const _Div(height: 12), + _Tags(tags: label!.tags), + if (address.derivationPath != null) const _Div(height: 12), if (address.derivationPath != null) DetailItem( title: "Derivation path", detail: address.derivationPath!.value, button: Container(), ), - if (address.type == AddressType.spark) - const _Div( - height: 12, - ), + if (address.type == AddressType.spark) const _Div(height: 12), if (address.type == AddressType.spark) DetailItem( title: "Diversifier", detail: address.derivationIndex.toString(), button: Container(), ), - const _Div( - height: 12, - ), + if (address.type == AddressType.mweb) const _Div(height: 12), + if (address.type == AddressType.mweb) + DetailItem( + title: "Index", + detail: address.derivationIndex.toString(), + button: Container(), + ), + const _Div(height: 12), DetailItem( title: "Type", detail: address.type.readableName, button: Container(), ), - const _Div( - height: 12, - ), + const _Div(height: 12), DetailItem( title: "Sub type", detail: address.subType.prettyName, button: Container(), ), - if (kDebugMode) - const _Div( - height: 12, - ), + if (kDebugMode) const _Div(height: 12), if (kDebugMode) DetailItem( title: "frost secure (kDebugMode)", @@ -385,18 +366,13 @@ class _AddressDetailsViewState extends ConsumerState { button: Container(), ), if (wallet is Bip39HDWallet && !wallet.isViewOnly) - const _Div( - height: 12, - ), + const _Div(height: 12), if (wallet is Bip39HDWallet && !wallet.isViewOnly) AddressPrivateKey( walletId: widget.walletId, address: address, ), - if (!isDesktop) - const SizedBox( - height: 20, - ), + if (!isDesktop) const SizedBox(height: 20), if (!isDesktop) Text( "Transactions", @@ -406,10 +382,7 @@ class _AddressDetailsViewState extends ConsumerState { Theme.of(context).extension()!.textDark3, ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), + if (!isDesktop) const SizedBox(height: 12), if (!isDesktop) ref .watch(pWallets) @@ -417,13 +390,13 @@ class _AddressDetailsViewState extends ConsumerState { .isarTransactionVersion == 2 ? _AddressDetailsTxV2List( - walletId: widget.walletId, - address: address, - ) + walletId: widget.walletId, + address: address, + ) : _AddressDetailsTxList( - walletId: widget.walletId, - address: address, - ), + walletId: widget.walletId, + address: address, + ), ], ), ); @@ -458,10 +431,9 @@ class _AddressDetailsTxList extends StatelessWidget { return ListView.separated( shrinkWrap: true, primary: false, - itemBuilder: (_, index) => TransactionCard( - transaction: txns[index], - walletId: walletId, - ), + itemBuilder: + (_, index) => + TransactionCard(transaction: txns[index], walletId: walletId), separatorBuilder: (_, __) => const _Div(height: 1), itemCount: count, ); @@ -470,15 +442,14 @@ class _AddressDetailsTxList extends StatelessWidget { padding: EdgeInsets.zero, child: Column( mainAxisSize: MainAxisSize.min, - children: query - .findAllSync() - .map( - (e) => TransactionCard( - transaction: e, - walletId: walletId, - ), - ) - .toList(), + children: + query + .findAllSync() + .map( + (e) => + TransactionCard(transaction: e, walletId: walletId), + ) + .toList(), ), ); } @@ -503,40 +474,35 @@ class _AddressDetailsTxV2List extends ConsumerWidget { final walletTxFilter = ref.watch(pWallets).getWallet(walletId).transactionFilterOperation; - final query = - ref.watch(mainDBProvider).isar.transactionV2s.buildQuery( - whereClauses: [ - IndexWhereClause.equalTo( - indexName: 'walletId', - value: [walletId], + final query = ref + .watch(mainDBProvider) + .isar + .transactionV2s + .buildQuery( + whereClauses: [ + IndexWhereClause.equalTo(indexName: 'walletId', value: [walletId]), + ], + filter: FilterGroup.and([ + if (walletTxFilter != null) walletTxFilter, + FilterGroup.or([ + ObjectFilter( + property: 'inputs', + filter: FilterCondition.contains( + property: "addresses", + value: address.value, ), - ], - filter: FilterGroup.and([ - if (walletTxFilter != null) walletTxFilter, - FilterGroup.or([ - ObjectFilter( - property: 'inputs', - filter: FilterCondition.contains( - property: "addresses", - value: address.value, - ), - ), - ObjectFilter( - property: 'outputs', - filter: FilterCondition.contains( - property: "addresses", - value: address.value, - ), - ), - ]), - ]), - sortBy: [ - const SortProperty( - property: "timestamp", - sort: Sort.desc, + ), + ObjectFilter( + property: 'outputs', + filter: FilterCondition.contains( + property: "addresses", + value: address.value, ), - ], - ); + ), + ]), + ]), + sortBy: [const SortProperty(property: "timestamp", sort: Sort.desc)], + ); final count = query.countSync(); @@ -546,9 +512,8 @@ class _AddressDetailsTxV2List extends ConsumerWidget { return ListView.separated( shrinkWrap: true, primary: false, - itemBuilder: (_, index) => TransactionCardV2( - transaction: txns[index], - ), + itemBuilder: + (_, index) => TransactionCardV2(transaction: txns[index]), separatorBuilder: (_, __) => const _Div(height: 1), itemCount: count, ); @@ -557,14 +522,11 @@ class _AddressDetailsTxV2List extends ConsumerWidget { padding: EdgeInsets.zero, child: Column( mainAxisSize: MainAxisSize.min, - children: query - .findAllSync() - .map( - (e) => TransactionCardV2( - transaction: e, - ), - ) - .toList(), + children: + query + .findAllSync() + .map((e) => TransactionCardV2(transaction: e)) + .toList(), ), ); } @@ -575,10 +537,7 @@ class _AddressDetailsTxV2List extends ConsumerWidget { } class _Div extends StatelessWidget { - const _Div({ - super.key, - required this.height, - }); + const _Div({super.key, required this.height}); final double height; @@ -591,18 +550,13 @@ class _Div extends StatelessWidget { width: double.infinity, ); } else { - return SizedBox( - height: height, - ); + return SizedBox(height: height); } } } class _Tags extends StatelessWidget { - const _Tags({ - super.key, - required this.tags, - }); + const _Tags({super.key, required this.tags}); final List? tags; @@ -615,10 +569,7 @@ class _Tags extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Tags", - style: STextStyles.itemSubtitle(context), - ), + Text("Tags", style: STextStyles.itemSubtitle(context)), Container(), // SimpleEditButton( // onPressedOverride: () { @@ -627,29 +578,20 @@ class _Tags extends StatelessWidget { // ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), tags != null && tags!.isNotEmpty ? Wrap( - spacing: 10, - runSpacing: 10, - children: tags! - .map( - (e) => AddressTag( - tag: e, - ), - ) - .toList(), - ) + spacing: 10, + runSpacing: 10, + children: tags!.map((e) => AddressTag(tag: e)).toList(), + ) : Text( - "Tags will appear here", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, - ), + "Tags will appear here", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle3, ), + ), ], ), ); diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 729bf7c69..70f9f964b 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -83,7 +83,13 @@ class _FrostSendViewState extends ConsumerState { final recipients = recipientWidgetIndexes .map((i) => ref.read(pRecipient(i).state).state) - .map((e) => (address: e!.address, amount: e!.amount!, isChange: false)) + .map( + (e) => TxRecipient( + address: e!.address, + amount: e.amount!, + isChange: false, + ), + ) .toList(growable: false); final txData = await wallet.frostCreateSignConfig( diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart index ade993b22..21d85fe9c 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart @@ -4,6 +4,7 @@ import 'package:isar/isar.dart'; import '../../../../frost_route_generator.dart'; import '../../../../models/isar/models/isar_models.dart'; +import '../../../../models/signing_data.dart'; import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/frost_wallet/frost_wallet_providers.dart'; import '../../../../providers/global/wallets_provider.dart'; @@ -60,9 +61,9 @@ class _FrostSendStep1bState extends ConsumerState { } final config = configFieldController.text; - final wallet = ref.read(pWallets).getWallet( - ref.read(pFrostScaffoldArgs)!.walletId!, - ) as BitcoinFrostWallet; + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; final data = Frost.extractDataFromSignConfig( signConfig: config, @@ -70,28 +71,36 @@ class _FrostSendStep1bState extends ConsumerState { serializedKeys: (await wallet.getSerializedKeys())!, ); - final utxos = await ref - .read(mainDBProvider) - .getUTXOs(wallet.walletId) - .filter() - .anyOf( - data.inputs, - (q, e) => q - .txidEqualTo(Format.uint8listToString(e.hash)) - .and() - .valueEqualTo(e.value) - .and() - .voutEqualTo(e.vout), - ) - .findAll(); + final utxos = + await ref + .read(mainDBProvider) + .getUTXOs(wallet.walletId) + .filter() + .anyOf( + data.inputs, + (q, e) => q + .txidEqualTo(Format.uint8listToString(e.hash)) + .and() + .valueEqualTo(e.value) + .and() + .voutEqualTo(e.vout), + ) + .findAll(); // TODO add more data from 'data' and display to user ? ref.read(pFrostTxData.notifier).state = TxData( frostMSConfig: config, - recipients: data.recipients - .map((e) => (address: e.address, amount: e.amount, isChange: false)) - .toList(), - utxos: utxos.toSet(), + recipients: + data.recipients + .map( + (e) => TxRecipient( + address: e.address, + amount: e.amount, + isChange: false, + ), + ) + .toList(), + utxos: utxos.map((e) => StandardInput(e)).toSet(), ); final attemptSignRes = await wallet.frostAttemptSignConfig( @@ -112,11 +121,12 @@ class _FrostSendStep1bState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Import and attempt sign config failed", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), + builder: + (_) => StackOkDialog( + title: "Import and attempt sign config failed", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), ); } } finally { @@ -128,9 +138,9 @@ class _FrostSendStep1bState extends ConsumerState { void initState() { configFieldController = TextEditingController(); configFocusNode = FocusNode(); - final wallet = ref.read(pWallets).getWallet( - ref.read(pFrostScaffoldArgs)!.walletId!, - ) as BitcoinFrostWallet; + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(pFrostMyName.state).state = wallet.frostInfo.myName; }); @@ -152,9 +162,7 @@ class _FrostSendStep1bState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const FrostStepUserSteps( - userSteps: info, - ), + const FrostStepUserSteps(userSteps: info), const SizedBox(height: 20), FrostStepField( controller: configFieldController, @@ -169,11 +177,10 @@ class _FrostSendStep1bState extends ConsumerState { }, ), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), CheckboxTextButton( - label: "I have verified that everyone has imported he config and" + label: + "I have verified that everyone has imported he config and" " is ready to sign", onChanged: (value) { setState(() { @@ -181,9 +188,7 @@ class _FrostSendStep1bState extends ConsumerState { }); }, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), PrimaryButton( label: "Start signing", enabled: !_configEmpty && _userVerifyContinue, diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart index 6ce475a0b..f3ddc90cc 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart @@ -51,9 +51,9 @@ class _FrostSendStep3State extends ConsumerState { @override void initState() { - final wallet = ref.read(pWallets).getWallet( - ref.read(pFrostScaffoldArgs)!.walletId!, - ) as BitcoinFrostWallet; + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; final frostInfo = wallet.frostInfo; @@ -62,12 +62,13 @@ class _FrostSendStep3State extends ConsumerState { myIndex = frostInfo.participants.indexOf(frostInfo.myName); myShare = ref.read(pFrostContinueSignData.state).state!.share; - participantsWithoutMe = frostInfo.participants - .toSet() - .intersection( - ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet(), - ) - .toList(); + participantsWithoutMe = + frostInfo.participants + .toSet() + .intersection( + ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet(), + ) + .toList(); participantsWithoutMe.remove(myName); @@ -98,46 +99,28 @@ class _FrostSendStep3State extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const FrostStepUserSteps( - userSteps: info, - ), - const SizedBox( - height: 12, - ), + const FrostStepUserSteps(userSteps: info), + const SizedBox(height: 12), DetailItem( title: "My name", detail: myName, - button: Util.isDesktop - ? IconCopyButton( - data: myName, - ) - : SimpleCopyButton( - data: myName, - ), - ), - const SizedBox( - height: 12, + button: + Util.isDesktop + ? IconCopyButton(data: myName) + : SimpleCopyButton(data: myName), ), + const SizedBox(height: 12), DetailItem( title: "My share", detail: myShare, - button: Util.isDesktop - ? IconCopyButton( - data: myShare, - ) - : SimpleCopyButton( - data: myShare, - ), - ), - const SizedBox( - height: 12, - ), - FrostQrDialogPopupButton( - data: myShare, - ), - const SizedBox( - height: 12, + button: + Util.isDesktop + ? IconCopyButton(data: myShare) + : SimpleCopyButton(data: myShare), ), + const SizedBox(height: 12), + FrostQrDialogPopupButton(data: myShare), + const SizedBox(height: 12), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -158,9 +141,7 @@ class _FrostSendStep3State extends ConsumerState { ], ), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), CheckboxTextButton( label: "I have verified that everyone has my share", onChanged: (value) { @@ -169,12 +150,11 @@ class _FrostSendStep3State extends ConsumerState { }); }, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), PrimaryButton( label: "Generate transaction", - enabled: _userVerifyContinue && + enabled: + _userVerifyContinue && !fieldIsEmptyFlags.fold(false, (v, e) => v |= e), onPressed: () async { // collect Share strings @@ -206,23 +186,21 @@ class _FrostSendStep3State extends ConsumerState { final inputTotal = Amount( rawValue: txData.utxos! - .map((e) => BigInt.from(e.value)) + .map((e) => e.value) .reduce((v, e) => v += e), fractionDigits: fractionDigits, ); final outputTotal = Amount( - rawValue: - tx.outputs.map((e) => e.value).reduce((v, e) => v += e), + rawValue: tx.outputs + .map((e) => e.value) + .reduce((v, e) => v += e), fractionDigits: fractionDigits, ); ref.read(pFrostTxData.state).state = txData.copyWith( raw: rawTx, fee: inputTotal - outputTotal, - frostSigners: [ - myName, - ...participantsWithoutMe, - ], + frostSigners: [myName, ...participantsWithoutMe], ); ref.read(pFrostCreateCurrentStep.state).state = 4; @@ -233,14 +211,15 @@ class _FrostSendStep3State extends ConsumerState { .routeName, ); } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); if (context.mounted) { return await showDialog( context: context, - builder: (_) => const FrostErrorDialog( - title: "Failed to complete signing process", - ), + builder: + (_) => const FrostErrorDialog( + title: "Failed to complete signing process", + ), ); } } diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index be21c99d6..63df318e8 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -22,6 +22,7 @@ import 'package:tuple/tuple.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/paynym/paynym_account_lite.dart'; import '../../models/send_view_auto_fill_data.dart'; +import '../../models/signing_data.dart'; import '../../providers/providers.dart'; import '../../providers/ui/fee_rate_type_state_provider.dart'; import '../../providers/ui/preview_tx_button_state_provider.dart'; @@ -52,6 +53,7 @@ import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/models/tx_data.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../widgets/animated_text.dart'; @@ -141,7 +143,7 @@ class _SendViewState extends ConsumerState { bool _cryptoAmountChangeLock = false; late VoidCallback onCryptoAmountChanged; - Set selectedUTXOs = {}; + Set selectedUTXOs = {}; void _applyUri(PaymentUriData paymentData) { try { @@ -714,7 +716,7 @@ class _SendViewState extends ConsumerState { txData: TxData( paynymAccountLite: widget.accountLite!, recipients: [ - ( + TxRecipient( address: widget.accountLite!.code, amount: amount, isChange: false, @@ -756,7 +758,11 @@ class _SendViewState extends ConsumerState { txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - (address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + ), ], feeRateType: ref.read(feeRateTypeMobileStateProvider), satsPerVByte: isCustomFee.value ? customFeeRate : null, @@ -776,7 +782,11 @@ class _SendViewState extends ConsumerState { ref.read(pValidSparkSendToAddress) ? null : [ - (address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + ), ], sparkRecipients: ref.read(pValidSparkSendToAddress) @@ -793,11 +803,31 @@ class _SendViewState extends ConsumerState { ); break; } + } else if (wallet is MwebInterface && + ref.read(publicPrivateBalanceStateProvider) == BalanceType.private) { + txDataFuture = wallet.prepareSend( + txData: TxData( + isMweb: true, + recipients: [ + TxRecipient(address: _address!, amount: amount, isChange: false), + ], + feeRateType: ref.read(feeRateTypeDesktopStateProvider), + satsPerVByte: isCustomFee.value ? customFeeRate : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, + ), + ); } else { final memo = coin is Stellar ? memoController.text : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [(address: _address!, amount: amount, isChange: false)], + recipients: [ + TxRecipient(address: _address!, amount: amount, isChange: false), + ], memo: memo, feeRateType: ref.read(feeRateTypeMobileStateProvider), satsPerVByte: isCustomFee.value ? customFeeRate : null, @@ -906,7 +936,10 @@ class _SendViewState extends ConsumerState { } } - String _getSendAllTitle(bool showCoinControl, Set selectedUTXOs) { + String _getSendAllTitle( + bool showCoinControl, + Set selectedUTXOs, + ) { if (showCoinControl && selectedUTXOs.isNotEmpty) { return "Send all selected"; } @@ -914,8 +947,8 @@ class _SendViewState extends ConsumerState { return "Send all ${coin.ticker}"; } - Amount _selectedUtxosAmount(Set utxos) => Amount( - rawValue: utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e), + Amount _selectedUtxosAmount(Set utxos) => Amount( + rawValue: utxos.map((e) => e.value).reduce((v, e) => v += e), fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, ); @@ -2065,13 +2098,20 @@ class _SendViewState extends ConsumerState { walletId, CoinControlViewType.use, amount, - selectedUTXOs, + selectedUTXOs + .map((e) => e.utxo) + .toSet(), ), ); if (result is Set) { setState(() { - selectedUTXOs = result; + selectedUTXOs = + result + .map( + (e) => StandardInput(e), + ) + .toSet(); }); } } diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index fe7f4bb46..70f076c01 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -491,7 +491,9 @@ class _TokenSendViewState extends ConsumerState { txDataFuture = tokenWallet.prepareSend( txData: TxData( - recipients: [(address: _address!, amount: amount, isChange: false)], + recipients: [ + TxRecipient(address: _address!, amount: amount, isChange: false), + ], feeRateType: ref.read(feeRateTypeMobileStateProvider), note: noteController.text, ethEIP1559Fee: ethFee, diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart index c66fb387d..4f8e8576f 100644 --- a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart @@ -17,6 +17,7 @@ import 'package:isar/isar.dart'; import '../../db/isar/main_db.dart'; import '../../models/isar/models/blockchain_data/utxo.dart'; +import '../../models/signing_data.dart'; import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; @@ -41,6 +42,9 @@ import '../../widgets/toggle.dart'; import 'utxo_row.dart'; final desktopUseUTXOs = StateProvider((ref) => {}); +final pDesktopUseUTXOs = Provider( + (ref) => ref.watch(desktopUseUTXOs).map((e) => StandardInput(e)).toSet(), +); class DesktopCoinControlUseDialog extends ConsumerStatefulWidget { const DesktopCoinControlUseDialog({ @@ -124,21 +128,22 @@ class _DesktopCoinControlUseDialogState ); } - final Amount selectedSum = _selectedUTXOs.map((e) => e.value).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: coin.fractionDigits, - ), - (value, element) => value += Amount( - rawValue: BigInt.from(element), - fractionDigits: coin.fractionDigits, - ), + final Amount selectedSum = _selectedUTXOs + .map((e) => e.value) + .fold( + Amount(rawValue: BigInt.zero, fractionDigits: coin.fractionDigits), + (value, element) => + value += Amount( + rawValue: BigInt.from(element), + fractionDigits: coin.fractionDigits, + ), ); - final enableApply = widget.amountToSend == null - ? selectedChanged(_selectedUTXOs) - : selectedChanged(_selectedUTXOs) && - widget.amountToSend! <= selectedSum; + final enableApply = + widget.amountToSend == null + ? selectedChanged(_selectedUTXOs) + : selectedChanged(_selectedUTXOs) && + widget.amountToSend! <= selectedSum; return DesktopDialog( maxWidth: 700, @@ -147,14 +152,8 @@ class _DesktopCoinControlUseDialogState children: [ Row( children: [ - const AppBarBackButton( - size: 40, - iconSize: 24, - ), - Text( - "Coin control", - style: STextStyles.desktopH3(context), - ), + const AppBarBackButton(size: 40, iconSize: 24), + Text("Coin control", style: STextStyles.desktopH3(context)), ], ), Expanded( @@ -164,24 +163,24 @@ class _DesktopCoinControlUseDialogState children: [ RoundedContainer( color: Colors.transparent, - borderColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + borderColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "This option allows you to control, freeze, and utilize " "outputs at your discretion.", - style: - STextStyles.desktopTextExtraExtraSmall(context), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ], ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( children: [ Expanded( @@ -199,11 +198,13 @@ class _DesktopCoinControlUseDialogState _searchString = value; }); }, - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, height: 1.8, ), decoration: standardInputDecoration( @@ -223,44 +224,47 @@ class _DesktopCoinControlUseDialogState height: 20, ), ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], + suffixIcon: + _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 0, ), - ), - ) - : null, + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SizedBox( height: 56, width: 240, child: Toggle( isOn: _filter == CCFilter.frozen, - onColor: Theme.of(context) - .extension()! - .rateTypeToggleDesktopColorOn, - offColor: Theme.of(context) - .extension()! - .rateTypeToggleDesktopColorOff, + onColor: + Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOn, + offColor: + Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOff, onIcon: Assets.svg.coinControl.unBlocked, onText: "Available", offIcon: Assets.svg.coinControl.blocked, @@ -281,9 +285,7 @@ class _DesktopCoinControlUseDialogState }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), JDropdownIconButton( redrawOnScreenSizeChanged: true, groupValue: _sort, @@ -299,164 +301,169 @@ class _DesktopCoinControlUseDialogState ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Expanded( - child: _list != null - ? ListView.separated( - shrinkWrap: true, - primary: false, - itemCount: _list!.length, - separatorBuilder: (context, _) => const SizedBox( - height: 10, - ), - itemBuilder: (context, index) { - final utxo = MainDB.instance.isar.utxos - .where() - .idEqualTo(_list![index]) - .findFirstSync()!; - final data = UtxoRowData(utxo.id, false); - data.selected = _selectedUTXOsData.contains(data); + child: + _list != null + ? ListView.separated( + shrinkWrap: true, + primary: false, + itemCount: _list!.length, + separatorBuilder: + (context, _) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final utxo = + MainDB.instance.isar.utxos + .where() + .idEqualTo(_list![index]) + .findFirstSync()!; + final data = UtxoRowData(utxo.id, false); + data.selected = _selectedUTXOsData.contains( + data, + ); - return UtxoRow( - key: Key( - "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", - ), - data: data, - compact: true, - walletId: widget.walletId, - onSelectionChanged: (value) { - setState(() { - if (data.selected) { - _selectedUTXOsData.add(value); - _selectedUTXOs.add(utxo); + return UtxoRow( + key: Key( + "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", + ), + data: data, + compact: true, + walletId: widget.walletId, + onSelectionChanged: (value) { + setState(() { + if (data.selected) { + _selectedUTXOsData.add(value); + _selectedUTXOs.add(utxo); + } else { + _selectedUTXOsData.remove(value); + _selectedUTXOs.remove(utxo); + } + }); + }, + ); + }, + ) + : ListView.separated( + itemCount: _map!.entries.length, + separatorBuilder: + (context, _) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final entry = _map!.entries.elementAt(index); + final _controller = RotateIconController(); + + return Expandable2( + border: + Theme.of(context) + .extension()! + .backgroundAppBar, + background: + Theme.of( + context, + ).extension()!.popupBG, + animationDurationMultiplier: + 0.2 * entry.value.length, + onExpandWillChange: (state) { + if (state == Expandable2State.expanded) { + _controller.forward?.call(); } else { - _selectedUTXOsData.remove(value); - _selectedUTXOs.remove(utxo); + _controller.reverse?.call(); } - }); - }, - ); - }, - ) - : ListView.separated( - itemCount: _map!.entries.length, - separatorBuilder: (context, _) => const SizedBox( - height: 10, - ), - itemBuilder: (context, index) { - final entry = _map!.entries.elementAt(index); - final _controller = RotateIconController(); - - return Expandable2( - border: Theme.of(context) - .extension()! - .backgroundAppBar, - background: Theme.of(context) - .extension()! - .popupBG, - animationDurationMultiplier: - 0.2 * entry.value.length, - onExpandWillChange: (state) { - if (state == Expandable2State.expanded) { - _controller.forward?.call(); - } else { - _controller.reverse?.call(); - } - }, - header: RoundedContainer( - padding: const EdgeInsets.all(20), - color: Colors.transparent, - child: Row( - children: [ - SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), + }, + header: RoundedContainer( + padding: const EdgeInsets.all(20), + color: Colors.transparent, + child: Row( + children: [ + SvgPicture.file( + File( + ref.watch(coinIconProvider(coin)), + ), + width: 24, + height: 24, ), - width: 24, - height: 24, - ), - const SizedBox( - width: 12, - ), - Expanded( - flex: 3, - child: Text( - entry.key, - style: STextStyles.w600_14(context), + const SizedBox(width: 12), + Expanded( + flex: 3, + child: Text( + entry.key, + style: STextStyles.w600_14(context), + ), ), - ), - Expanded( - child: Text( - "${entry.value.length} " - "output${entry.value.length > 1 ? "s" : ""}", - style: STextStyles - .desktopTextExtraExtraSmall( - context, + Expanded( + child: Text( + "${entry.value.length} " + "output${entry.value.length > 1 ? "s" : ""}", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ), - ), - RotateIcon( - animationDurationMultiplier: - 0.2 * entry.value.length, - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 14, - color: Theme.of(context) - .extension()! - .textSubtitle1, + RotateIcon( + animationDurationMultiplier: + 0.2 * entry.value.length, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + color: + Theme.of(context) + .extension()! + .textSubtitle1, + ), + curve: Curves.easeInOut, + controller: _controller, ), - curve: Curves.easeInOut, - controller: _controller, - ), - ], + ], + ), ), - ), - children: entry.value.map( - (id) { - final utxo = MainDB.instance.isar.utxos - .where() - .idEqualTo(id) - .findFirstSync()!; - final data = UtxoRowData(utxo.id, false); - data.selected = - _selectedUTXOsData.contains(data); + children: + entry.value.map((id) { + final utxo = + MainDB.instance.isar.utxos + .where() + .idEqualTo(id) + .findFirstSync()!; + final data = UtxoRowData( + utxo.id, + false, + ); + data.selected = _selectedUTXOsData + .contains(data); - return UtxoRow( - key: Key( - "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", - ), - data: data, - compact: true, - compactWithBorder: false, - raiseOnSelected: false, - walletId: widget.walletId, - onSelectionChanged: (value) { - setState(() { - if (data.selected) { - _selectedUTXOsData.add(value); - _selectedUTXOs.add(utxo); - } else { - _selectedUTXOsData.remove(value); - _selectedUTXOs.remove(utxo); - } - }); - }, - ); - }, - ).toList(), - ); - }, - ), - ), - const SizedBox( - height: 16, + return UtxoRow( + key: Key( + "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", + ), + data: data, + compact: true, + compactWithBorder: false, + raiseOnSelected: false, + walletId: widget.walletId, + onSelectionChanged: (value) { + setState(() { + if (data.selected) { + _selectedUTXOsData.add(value); + _selectedUTXOs.add(utxo); + } else { + _selectedUTXOsData.remove( + value, + ); + _selectedUTXOs.remove(utxo); + } + }); + }, + ); + }).toList(), + ); + }, + ), ), + const SizedBox(height: 16), RoundedContainer( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, padding: EdgeInsets.zero, child: ConditionalParent( condition: widget.amountToSend != null, @@ -467,9 +474,10 @@ class _DesktopCoinControlUseDialogState child, Container( height: 1.2, - color: Theme.of(context) - .extension()! - .popupBG, + color: + Theme.of( + context, + ).extension()!.popupBG, ), Padding( padding: const EdgeInsets.all(16), @@ -481,26 +489,26 @@ class _DesktopCoinControlUseDialogState "Amount to send", style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ), ), SelectableText( - "${widget.amountToSend!.decimal.toStringAsFixed( - coin.fractionDigits, - )}" + "${widget.amountToSend!.decimal.toStringAsFixed(coin.fractionDigits)}" " ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ), ), ], ), @@ -518,23 +526,23 @@ class _DesktopCoinControlUseDialogState style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), SelectableText( - "${selectedSum.decimal.toStringAsFixed( - coin.fractionDigits, - )} ${coin.ticker}", + "${selectedSum.decimal.toStringAsFixed(coin.fractionDigits)} ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: widget.amountToSend == null - ? Theme.of(context) - .extension()! - .textDark - : selectedSum < widget.amountToSend! + color: + widget.amountToSend == null + ? Theme.of( + context, + ).extension()!.textDark + : selectedSum < widget.amountToSend! ? Theme.of(context) .extension()! .accentColorRed @@ -548,18 +556,17 @@ class _DesktopCoinControlUseDialogState ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( children: [ Expanded( child: SecondaryButton( enabled: _selectedUTXOsData.isNotEmpty, buttonHeight: ButtonHeight.l, - label: _selectedUTXOsData.isEmpty - ? "Clear selection" - : "Clear selection (${_selectedUTXOsData.length})", + label: + _selectedUTXOsData.isEmpty + ? "Clear selection" + : "Clear selection (${_selectedUTXOsData.length})", onPressed: () { setState(() { _selectedUTXOsData.clear(); @@ -568,9 +575,7 @@ class _DesktopCoinControlUseDialogState }, ), ), - const SizedBox( - width: 20, - ), + const SizedBox(width: 20), Expanded( child: PrimaryButton( enabled: enableApply, @@ -586,9 +591,7 @@ class _DesktopCoinControlUseDialogState ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index bf07ef580..b5cb4e7c9 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -49,6 +49,7 @@ import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/models/tx_data.dart'; import '../../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../widgets/custom_buttons/blue_text_button.dart'; @@ -163,7 +164,7 @@ class _DesktopSendState extends ConsumerState { final Amount amount = ref.read(pSendAmount)!; final Amount availableBalance; - if ((coin is Firo)) { + if (coin is Firo || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case BalanceType.public: availableBalance = wallet.info.cachedBalance.spendable; @@ -310,7 +311,7 @@ class _DesktopSendState extends ConsumerState { txData: TxData( paynymAccountLite: widget.accountLite!, recipients: [ - ( + TxRecipient( address: widget.accountLite!.code, amount: amount, isChange: false, @@ -321,8 +322,8 @@ class _DesktopSendState extends ConsumerState { utxos: (wallet is CoinControlInterface && coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) : null, ), ); @@ -344,8 +345,8 @@ class _DesktopSendState extends ConsumerState { satsPerVByte: isCustomFee ? customFeeRate : null, utxos: (coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) : null, ), ); @@ -353,14 +354,18 @@ class _DesktopSendState extends ConsumerState { txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - (address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + ), ], feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, utxos: (coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) : null, ), ); @@ -374,7 +379,11 @@ class _DesktopSendState extends ConsumerState { ref.read(pValidSparkSendToAddress) ? null : [ - (address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + ), ], sparkRecipients: ref.read(pValidSparkSendToAddress) @@ -391,11 +400,31 @@ class _DesktopSendState extends ConsumerState { ); break; } + } else if (wallet is MwebInterface && + ref.read(publicPrivateBalanceStateProvider) == BalanceType.private) { + txDataFuture = wallet.prepareSend( + txData: TxData( + isMweb: true, + recipients: [ + TxRecipient(address: _address!, amount: amount, isChange: false), + ], + feeRateType: ref.read(feeRateTypeDesktopStateProvider), + satsPerVByte: isCustomFee ? customFeeRate : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) + : null, + ), + ); } else { final memo = isStellar ? memoController.text : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [(address: _address!, amount: amount, isChange: false)], + recipients: [ + TxRecipient(address: _address!, amount: amount, isChange: false), + ], memo: memo, feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, @@ -406,8 +435,8 @@ class _DesktopSendState extends ConsumerState { utxos: (wallet is CoinControlInterface && coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) : null, ethEIP1559Fee: ethFee, ), @@ -835,7 +864,7 @@ class _DesktopSendState extends ConsumerState { if (showCoinControl && ref.read(desktopUseUTXOs).isNotEmpty) { amount = _selectedUtxosAmount(ref.read(desktopUseUTXOs)); - } else if (coin is Firo) { + } else if (coin is Firo || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case BalanceType.public: amount = ref.read(pWalletBalance(walletId)).spendable; @@ -975,6 +1004,10 @@ class _DesktopSendState extends ConsumerState { final balType = ref.watch(publicPrivateBalanceStateProvider); + final isMwebEnabled = ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + ); + final showPrivateBalance = coin is Firo || isMwebEnabled; final isExchangeAddress = ref.watch(pIsExchangeAddress); ref.listen(publicPrivateBalanceStateProvider, (previous, next) { if (previous != next && @@ -998,7 +1031,7 @@ class _DesktopSendState extends ConsumerState { ), ) && ref.watch(pWallets).getWallet(walletId) is CoinControlInterface && - (coin is Firo ? balType == BalanceType.public : true); + balType == BalanceType.public; return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index aac8ec01d..581b5a824 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -232,7 +232,9 @@ class _DesktopTokenSendState extends ConsumerState { txDataFuture = tokenWallet.prepareSend( txData: TxData( - recipients: [(address: _address!, amount: amount, isChange: false)], + recipients: [ + TxRecipient(address: _address!, amount: amount, isChange: false), + ], feeRateType: ref.read(feeRateTypeDesktopStateProvider), nonce: int.tryParse(nonceController.text), ethEIP1559Fee: ethFee, 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 d06d84400..17c055614 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 @@ -213,10 +213,9 @@ class _DesktopWalletFeaturesState extends ConsumerState { ), ), ); - final firoWallet = - ref.read(pWallets).getWallet(widget.walletId) as FiroWallet; - final publicBalance = firoWallet.info.cachedBalance.spendable; + final wallet = ref.read(pWallets).getWallet(widget.walletId); + final publicBalance = wallet.info.cachedBalance.spendable; if (publicBalance <= Amount.zero) { shouldPop = true; if (context.mounted) { @@ -236,7 +235,11 @@ class _DesktopWalletFeaturesState extends ConsumerState { } try { - await firoWallet.anonymizeAllSpark(); + if (wallet is MwebInterface && wallet.info.isMwebEnabled) { + await wallet.anonymizeAllMweb(); + } else { + await (wallet as FiroWallet).anonymizeAllSpark(); + } shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: true).pop(); @@ -391,7 +394,9 @@ class _DesktopWalletFeaturesState extends ConsumerState { final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly; return [ - if (!isViewOnly && coin is Firo) + if (!isViewOnly && + (coin is Firo || + (wallet is MwebInterface && wallet.info.isMwebEnabled))) ( WalletFeature.anonymizeFunds, Assets.svg.recycle, 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 3df3c9504..b3510022c 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 @@ -271,7 +271,7 @@ class _MoreFeaturesDialogState extends ConsumerState { unawaited( ref .read(pMwebService) - .init(ref.read(pWalletCoin(widget.walletId)).network), + .initService(ref.read(pWalletCoin(widget.walletId)).network), ); } diff --git a/lib/services/frost.dart b/lib/services/frost.dart index 76c0b5db9..33066de92 100644 --- a/lib/services/frost.dart +++ b/lib/services/frost.dart @@ -12,12 +12,11 @@ import '../utilities/amount/amount.dart'; import '../utilities/extensions/extensions.dart'; import '../utilities/logger.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; +import '../wallets/models/tx_recipient.dart'; abstract class Frost { //==================== utility =============================================== - static List getParticipants({ - required String multisigConfig, - }) { + static List getParticipants({required String multisigConfig}) { try { final numberOfParticipants = multisigParticipants( multisigConfig: multisigConfig, @@ -26,10 +25,7 @@ abstract class Frost { final List participants = []; for (int i = 0; i < numberOfParticipants; i++) { participants.add( - multisigParticipant( - multisigConfig: multisigConfig, - index: i, - ), + multisigParticipant(multisigConfig: multisigConfig, index: i), ); } @@ -45,18 +41,18 @@ abstract class Frost { decodeMultisigConfig(multisigConfig: encodedConfig); return true; } catch (e, s) { - Logging.instance.f("validateEncodedMultisigConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "validateEncodedMultisigConfig failed: ", + error: e, + stackTrace: s, + ); return false; } } - static int getThreshold({ - required String multisigConfig, - }) { + static int getThreshold({required String multisigConfig}) { try { - final threshold = multisigThreshold( - multisigConfig: multisigConfig, - ); + final threshold = multisigThreshold(multisigConfig: multisigConfig); return threshold; } catch (e, s) { @@ -70,7 +66,8 @@ abstract class Frost { String changeAddress, int feePerWeight, List inputs, - }) extractDataFromSignConfig({ + }) + extractDataFromSignConfig({ required String serializedKeys, required String signConfig, required CryptoCurrency coin, @@ -85,8 +82,9 @@ abstract class Frost { ); // get various data from config - final feePerWeight = - signFeePerWeight(signConfigPointer: signConfigPointer); + final feePerWeight = signFeePerWeight( + signConfigPointer: signConfigPointer, + ); final changeAddress = signChange(signConfigPointer: signConfigPointer); final recipientsCount = signPayments( signConfigPointer: signConfigPointer, @@ -103,15 +101,13 @@ abstract class Frost { signConfigPointer: signConfigPointer, index: i, ); - recipients.add( - ( - address: address, - amount: Amount( - rawValue: BigInt.from(amount), - fractionDigits: coin.fractionDigits, - ), + recipients.add(( + address: address, + amount: Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.fractionDigits, ), - ); + )); } // get utxos @@ -135,7 +131,11 @@ abstract class Frost { inputs: outputs, ); } catch (e, s) { - Logging.instance.f("extractDataFromSignConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "extractDataFromSignConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -156,7 +156,11 @@ abstract class Frost { return config; } catch (e, s) { - Logging.instance.f("createMultisigConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "createMultisigConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -166,10 +170,8 @@ abstract class Frost { String commitments, Pointer multisigConfigWithNamePtr, Pointer secretShareMachineWrapperPtr, - }) startKeyGeneration({ - required String multisigConfig, - required String myName, - }) { + }) + startKeyGeneration({required String multisigConfig, required String myName}) { try { final startKeyGenResPtr = startKeyGen( multisigConfig: multisigConfig, @@ -189,15 +191,17 @@ abstract class Frost { secretShareMachineWrapperPtr: machinePtr, ); } catch (e, s) { - Logging.instance.f("startKeyGeneration failed: ", error: e, stackTrace: s); + Logging.instance.f( + "startKeyGeneration failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static ({ - String share, - Pointer secretSharesResPtr, - }) generateSecretShares({ + static ({String share, Pointer secretSharesResPtr}) + generateSecretShares({ required Pointer multisigConfigWithNamePtr, required String mySeed, required Pointer secretShareMachineWrapperPtr, @@ -216,16 +220,17 @@ abstract class Frost { return (share: share, secretSharesResPtr: secretSharesResPtr); } catch (e, s) { - Logging.instance.f("generateSecretShares failed: ", error: e, stackTrace: s); + Logging.instance.f( + "generateSecretShares failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static ({ - Uint8List multisigId, - String recoveryString, - String serializedKeys, - }) completeKeyGeneration({ + static ({Uint8List multisigId, String recoveryString, String serializedKeys}) + completeKeyGeneration({ required Pointer multisigConfigWithNamePtr, required Pointer secretSharesResPtr, required List shares, @@ -254,7 +259,11 @@ abstract class Frost { serializedKeys: serializedKeys, ); } catch (e, s) { - Logging.instance.f("completeKeyGeneration failed: ", error: e, stackTrace: s); + Logging.instance.f( + "completeKeyGeneration failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -265,13 +274,14 @@ abstract class Frost { required String serializedKeys, required int network, required List< - ({ - UTXO utxo, - Uint8List scriptPubKey, - AddressDerivationData addressDerivationData - })> - inputs, - required List<({String address, Amount amount, bool isChange})> outputs, + ({ + UTXO utxo, + Uint8List scriptPubKey, + AddressDerivationData addressDerivationData, + }) + > + inputs, + required List outputs, required String changeAddress, required int feePerWeight, }) { @@ -279,17 +289,18 @@ abstract class Frost { final signConfig = newSignConfig( thresholdKeysWrapperPointer: deserializeKeys(keys: serializedKeys), network: network, - outputs: inputs - .map( - (e) => Output( - hash: e.utxo.txid.toUint8ListFromHex, - vout: e.utxo.vout, - value: e.utxo.value, - scriptPubKey: e.scriptPubKey, - addressDerivationData: e.addressDerivationData, - ), - ) - .toList(), + outputs: + inputs + .map( + (e) => Output( + hash: e.utxo.txid.toUint8ListFromHex, + vout: e.utxo.vout, + value: e.utxo.value, + scriptPubKey: e.scriptPubKey, + addressDerivationData: e.addressDerivationData, + ), + ) + .toList(), paymentAddresses: outputs.map((e) => e.address).toList(), paymentAmounts: outputs.map((e) => e.amount.raw.toInt()).toList(), change: changeAddress, @@ -306,7 +317,8 @@ abstract class Frost { static ({ Pointer machinePtr, String preprocess, - }) attemptSignConfig({ + }) + attemptSignConfig({ required int network, required String config, required String serializedKeys, @@ -333,7 +345,8 @@ abstract class Frost { static ({ Pointer machinePtr, String share, - }) continueSigning({ + }) + continueSigning({ required Pointer machinePtr, required List preprocesses, }) { @@ -358,10 +371,7 @@ abstract class Frost { required List shares, }) { try { - final rawTransaction = completeSign( - machine: machinePtr, - shares: shares, - ); + final rawTransaction = completeSign(machine: machinePtr, shares: shares); return rawTransaction; } catch (e, s) { @@ -404,28 +414,24 @@ abstract class Frost { return config; } catch (e, s) { - Logging.instance.f("createResharerConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "createResharerConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static ({ - String resharerStart, - Pointer machine, - }) beginResharer({ - required String serializedKeys, - required String config, - }) { + static ({String resharerStart, Pointer machine}) + beginResharer({required String serializedKeys, required String config}) { try { final result = startResharer( serializedKeys: serializedKeys, config: config, ); - return ( - resharerStart: result.encoded, - machine: result.machine, - ); + return (resharerStart: result.encoded, machine: result.machine); } catch (e, s) { Logging.instance.f("beginResharer failed: ", error: e, stackTrace: s); rethrow; @@ -433,10 +439,8 @@ abstract class Frost { } /// expects [resharerStarts] of length equal to resharers. - static ({ - String resharedStart, - Pointer prior, - }) beginReshared({ + static ({String resharedStart, Pointer prior}) + beginReshared({ required String myName, required String resharerConfig, required List resharerStarts, @@ -448,10 +452,7 @@ abstract class Frost { resharerConfig: resharerConfig, resharerStarts: resharerStarts, ); - return ( - resharedStart: result.encoded, - prior: result.machine, - ); + return (resharedStart: result.encoded, prior: result.machine); } catch (e, s) { Logging.instance.f("beginReshared failed: ", error: e, stackTrace: s); rethrow; @@ -476,11 +477,8 @@ abstract class Frost { } /// expects [resharerCompletes] of length equal to resharers - static ({ - String multisigConfig, - String serializedKeys, - String resharedId, - }) finishReshared({ + static ({String multisigConfig, String serializedKeys, String resharedId}) + finishReshared({ required StartResharedRes prior, required List resharerCompletes, }) { @@ -504,7 +502,11 @@ abstract class Frost { return config; } catch (e, s) { - Logging.instance.f("decodedResharerConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "decodedResharerConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -513,9 +515,8 @@ abstract class Frost { int newThreshold, Map resharers, List newParticipants, - }) extractResharerConfigData({ - required String rConfig, - }) { + }) + extractResharerConfigData({required String rConfig}) { final decoded = _decodeRConfigWithResharers(rConfig); final resharerConfig = decoded.config; @@ -564,8 +565,9 @@ abstract class Frost { for (final resharer in resharers) { resharersMap[decoded.resharers.entries - .firstWhere((e) => e.value == resharer) - .key] = resharer; + .firstWhere((e) => e.value == resharer) + .key] = + resharer; } return ( @@ -574,28 +576,25 @@ abstract class Frost { newParticipants: newParticipants, ); } catch (e, s) { - Logging.instance.f("extractResharerConfigData failed: ", error: e, stackTrace: s); + Logging.instance.f( + "extractResharerConfigData failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static String encodeRConfig( - String config, - Map resharers, - ) { + static String encodeRConfig(String config, Map resharers) { return base64Encode("$config@${jsonEncode(resharers)}".toUint8ListFromUtf8); } - static String decodeRConfig( - String rConfig, - ) { + static String decodeRConfig(String rConfig) { return base64Decode(rConfig).toUtf8String.split("@").first; } static ({Map resharers, String config}) - _decodeRConfigWithResharers( - String rConfig, - ) { + _decodeRConfigWithResharers(String rConfig) { final parts = base64Decode(rConfig).toUtf8String.split("@"); final config = parts[0]; diff --git a/lib/services/mwebd_service.dart b/lib/services/mwebd_service.dart index d912f8ff0..8f60a3d6f 100644 --- a/lib/services/mwebd_service.dart +++ b/lib/services/mwebd_service.dart @@ -115,9 +115,7 @@ final class MwebdService { }); } - Future init(CryptoCurrencyNetwork net) async { - if (net == CryptoCurrencyNetwork.test) return; - + Future initService(CryptoCurrencyNetwork net) async { Logging.instance.i("MwebdService init($net) called..."); await _updateLock.protect(() async { if (_map[net] != null) { @@ -125,6 +123,22 @@ final class MwebdService { return; } + if (_map.isNotEmpty) { + for (final old in _map.values) { + try { + await old.client.cleanup(); + await old.server.stopServer(); + } catch (e, s) { + Logging.instance.i( + "Switching mwebd chain. Error likely expected here.", + error: e, + stackTrace: s, + ); + } + } + _map.clear(); + } + final port = await _getRandomUnusedPort(); if (port == null) { @@ -211,3 +225,200 @@ Future _getRandomUnusedPort({Set excluded = const {}}) async { return null; } + +// final class MwebdService { +// static String defaultPeer(CryptoCurrencyNetwork net) => switch (net) { +// CryptoCurrencyNetwork.main => "litecoin.stackwallet.com:9333", +// CryptoCurrencyNetwork.test => "litecoin.stackwallet.com:19335", +// CryptoCurrencyNetwork.stage => throw UnimplementedError(), +// CryptoCurrencyNetwork.test4 => throw UnimplementedError(), +// }; +// +// final Map +// _map = {}; +// +// late final StreamSubscription +// _torStatusListener; +// late final StreamSubscription +// _torPreferenceListener; +// +// final Mutex _torConnectingLock = Mutex(); +// +// static final instance = MwebdService._(); +// +// MwebdService._() { +// final bus = GlobalEventBus.instance; +// +// // Listen for tor status changes. +// _torStatusListener = bus.on().listen(( +// event, +// ) async { +// switch (event.newStatus) { +// case TorConnectionStatus.connecting: +// if (!_torConnectingLock.isLocked) { +// await _torConnectingLock.acquire(); +// } +// break; +// +// case TorConnectionStatus.connected: +// case TorConnectionStatus.disconnected: +// if (_torConnectingLock.isLocked) { +// _torConnectingLock.release(); +// } +// break; +// } +// }); +// +// // Listen for tor preference changes. +// _torPreferenceListener = bus.on().listen(( +// event, +// ) async { +// if (Prefs.instance.useTor) { +// return await _torConnectingLock.protect(() async { +// final proxyInfo = TorService.sharedInstance.getProxyInfo(); +// return await _update(proxyInfo); +// }); +// } else { +// return await _update(null); +// } +// }); +// } +// +// // locked while mweb servers and clients are updating +// final _updateLock = Mutex(); +// +// // update function called when Tor pref changed +// Future _update(({InternetAddress host, int port})? proxyInfo) async { +// await _updateLock.protect(() async { +// final proxy = +// proxyInfo == null +// ? "" +// : "${proxyInfo.host.address}:${proxyInfo.port}"; +// final nets = _map.keys; +// for (final net in nets) { +// final old = _map.remove(net)!; +// +// await old.client.cleanup(); +// await old.server.stopServer(); +// +// final port = await _getRandomUnusedPort(); +// if (port == null) { +// throw Exception("Could not find an unused port for mwebd"); +// } +// +// final newServer = MwebdServer( +// chain: old.server.chain, +// dataDir: old.server.dataDir, +// peer: old.server.peer, +// proxy: proxy, +// serverPort: port, +// ); +// await newServer.createServer(); +// await newServer.startServer(); +// +// final newClient = MwebClient.fromHost( +// "127.0.0.1", +// newServer.serverPort, +// ); +// +// _map[net] = (server: newServer, client: newClient); +// } +// }); +// } +// +// Future init(CryptoCurrencyNetwork net) async { +// if (net == CryptoCurrencyNetwork.test) return; +// +// Logging.instance.i("MwebdService init($net) called..."); +// await _updateLock.protect(() async { +// if (_map[net] != null) { +// Logging.instance.i("MwebdService init($net) was already called."); +// return; +// } +// +// final port = await _getRandomUnusedPort(); +// +// if (port == null) { +// throw Exception("Could not find an unused port for mwebd"); +// } +// +// final chain = switch (net) { +// CryptoCurrencyNetwork.main => "mainnet", +// CryptoCurrencyNetwork.test => "testnet", +// CryptoCurrencyNetwork.stage => throw UnimplementedError(), +// CryptoCurrencyNetwork.test4 => throw UnimplementedError(), +// }; +// +// final dir = await StackFileSystem.applicationMwebdDirectory(chain); +// +// final String proxy; +// if (Prefs.instance.useTor) { +// final proxyInfo = TorService.sharedInstance.getProxyInfo(); +// proxy = "${proxyInfo.host.address}:${proxyInfo.port}"; +// } else { +// proxy = ""; +// } +// +// final newServer = MwebdServer( +// chain: chain, +// dataDir: dir.path, +// peer: defaultPeer(net), +// proxy: proxy, +// serverPort: port, +// ); +// await newServer.createServer(); +// await newServer.startServer(); +// +// final newClient = MwebClient.fromHost("127.0.0.1", newServer.serverPort); +// +// _map[net] = (server: newServer, client: newClient); +// +// Logging.instance.i("MwebdService init($net) completed!"); +// }); +// } +// +// /// Get server status. Returns null if no server was initialized. +// Future getServerStatus(CryptoCurrencyNetwork net) async { +// return await _updateLock.protect(() async { +// return await _map[net]?.server.getStatus(); +// }); +// } +// +// /// Get client for network. Returns null if no server was initialized. +// Future getClient(CryptoCurrencyNetwork net) async { +// return await _updateLock.protect(() async { +// return _map[net]?.client; +// }); +// } +// } +// +// // ============================================================================ +// Future _getRandomUnusedPort({Set excluded = const {}}) async { +// const int minPort = 1024; +// const int maxPort = 65535; +// const int maxAttempts = 1000; +// +// final random = Random.secure(); +// +// for (int i = 0; i < maxAttempts; i++) { +// final int potentialPort = minPort + random.nextInt(maxPort - minPort + 1); +// +// if (excluded.contains(potentialPort)) { +// continue; +// } +// +// try { +// final ServerSocket socket = await ServerSocket.bind( +// InternetAddress.anyIPv4, +// potentialPort, +// ); +// await socket.close(); +// return potentialPort; +// } catch (_) { +// excluded.add(potentialPort); +// continue; +// } +// } +// +// return null; +// } diff --git a/lib/wallets/crypto_currency/coins/litecoin.dart b/lib/wallets/crypto_currency/coins/litecoin.dart index 4ab8ec0c2..1b830c4f9 100644 --- a/lib/wallets/crypto_currency/coins/litecoin.dart +++ b/lib/wallets/crypto_currency/coins/litecoin.dart @@ -91,6 +91,7 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { privHDPrefix: 0x0488ade4, pubHDPrefix: 0x0488b21e, bech32Hrp: "ltc", + mwebBech32Hrp: "ltcmweb", messagePrefix: '\x19Litecoin Signed Message:\n', minFee: BigInt.from(1), // Not used in stack wallet currently minOutput: dustLimit.raw, // Not used in stack wallet currently @@ -104,6 +105,7 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { privHDPrefix: 0x04358394, pubHDPrefix: 0x043587cf, bech32Hrp: "tltc", + mwebBech32Hrp: "tmweb", messagePrefix: "\x19Litecoin Signed Message:\n", minFee: BigInt.from(1), // Not used in stack wallet currently minOutput: dustLimit.raw, // Not used in stack wallet currently diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 6353a330a..bb2eb2cd1 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -7,13 +7,15 @@ import '../../db/drift/database.dart'; import '../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/paynym/paynym_account_lite.dart'; +import '../../models/signing_data.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; import '../../widgets/eth_fee_form.dart'; import '../isar/models/spark_coin.dart'; import 'name_op_state.dart'; +import 'tx_recipient.dart'; -typedef TxRecipient = ({String address, Amount amount, bool isChange}); +export 'tx_recipient.dart'; class TxData { final FeeRateType? feeRateType; @@ -34,7 +36,7 @@ class TxData { final String? memo; final List? recipients; - final Set? utxos; + final Set? utxos; final List? usedUTXOs; final String? changeAddress; @@ -85,7 +87,6 @@ class TxData { // MWEB final bool isMweb; - final Set? mwebUtxos; final List? usedMwebUtxos; TxData({ @@ -123,7 +124,6 @@ class TxData { this.opNameState, this.sparkNameInfo, this.isMweb = false, - this.mwebUtxos, this.usedMwebUtxos, }); @@ -238,7 +238,7 @@ class TxData { String? noteOnChain, String? memo, String? otherData, - Set? utxos, + Set? utxos, List? usedUTXOs, List? recipients, String? frostMSConfig, @@ -273,7 +273,6 @@ class TxData { })? sparkNameInfo, bool? isMweb, - Set? mwebUtxos, List? usedMwebUtxos, }) { return TxData( @@ -313,7 +312,6 @@ class TxData { opNameState: opNameState ?? this.opNameState, sparkNameInfo: sparkNameInfo ?? this.sparkNameInfo, isMweb: isMweb ?? this.isMweb, - mwebUtxos: mwebUtxos ?? this.mwebUtxos, usedMwebUtxos: usedMwebUtxos ?? this.usedMwebUtxos, ); } @@ -354,7 +352,6 @@ class TxData { 'opNameState: $opNameState, ' 'sparkNameInfo: $sparkNameInfo, ' 'isMweb: $isMweb, ' - 'mwebUtxos: $mwebUtxos, ' 'usedMwebUtxos: $usedMwebUtxos, ' '}'; } diff --git a/lib/wallets/models/tx_recipient.dart b/lib/wallets/models/tx_recipient.dart index 8c5e9a9d4..8dd0c3011 100644 --- a/lib/wallets/models/tx_recipient.dart +++ b/lib/wallets/models/tx_recipient.dart @@ -1,11 +1,16 @@ +import '../../models/isar/models/blockchain_data/address.dart'; import '../../utilities/amount/amount.dart'; class TxRecipient { final String address; final Amount amount; + final bool isChange; + final AddressType? addressType; TxRecipient({ required this.address, required this.amount, + required this.isChange, + this.addressType, }); } diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 3b0c36ac0..33b6d3a0d 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -18,6 +18,7 @@ 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/paymint/fee_object_model.dart'; +import '../../../models/signing_data.dart'; import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import '../../../services/event_bus/global_event_bus.dart'; import '../../../services/frost.dart'; @@ -235,7 +236,10 @@ class BitcoinFrostWallet extends Wallet } } - return txData.copyWith(frostMSConfig: config, utxos: utxosToUse); + return txData.copyWith( + frostMSConfig: config, + utxos: utxosToUse.map((e) => StandardInput(e)).toSet(), + ); } catch (_) { rethrow; } @@ -676,11 +680,13 @@ class BitcoinFrostWallet extends Wallet Logging.instance.d("Sent txHash: $txHash"); // mark utxos as used - final usedUTXOs = txData.utxos!.map((e) => e.copyWith(used: true)); + final usedUTXOs = txData.utxos!.whereType().map( + (e) => e.utxo.copyWith(used: true), + ); await mainDB.putUTXOs(usedUTXOs.toList()); txData = txData.copyWith( - utxos: usedUTXOs.toSet(), + utxos: usedUTXOs.map((e) => StandardInput(e)).toSet(), txHash: txHash, txid: txHash, ); diff --git a/lib/wallets/wallet/impl/cardano_wallet.dart b/lib/wallets/wallet/impl/cardano_wallet.dart index 2cad7c514..5f4fdc37c 100644 --- a/lib/wallets/wallet/impl/cardano_wallet.dart +++ b/lib/wallets/wallet/impl/cardano_wallet.dart @@ -235,7 +235,7 @@ class CardanoWallet extends Bip39Wallet { // Check if we are sending all balance, which means no change and only one output for recipient. if (totalBalance == txData.amount!.raw) { final List newRecipients = [ - ( + TxRecipient( address: txData.recipients!.first.address, amount: Amount( rawValue: txData.amount!.raw - fee, diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 215f41917..9032a0c82 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -631,8 +631,7 @@ class EpiccashWallet extends Bip39Wallet { throw Exception("Epic cash prepare send requires a single recipient!"); } - ({String address, Amount amount, bool isChange}) recipient = - txData.recipients!.first; + TxRecipient recipient = txData.recipients!.first; final int realFee = await _nativeFee(recipient.amount.raw.toInt()); final feeAmount = Amount( @@ -647,7 +646,7 @@ class EpiccashWallet extends Bip39Wallet { } if (info.cachedBalance.spendable == recipient.amount) { - recipient = ( + recipient = TxRecipient( address: recipient.address, amount: recipient.amount - feeAmount, isChange: recipient.isChange, diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart index db22a5b55..c8b2673f6 100644 --- a/lib/wallets/wallet/impl/litecoin_wallet.dart +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -13,6 +13,7 @@ import '../../../utilities/logger.dart'; import '../../crypto_currency/crypto_currency.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../intermediate/bip39_hd_wallet.dart'; +import '../intermediate/external_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; import '../wallet_mixin_interfaces/extended_keys_interface.dart'; @@ -28,7 +29,8 @@ class LitecoinWallet CoinControlInterface, RbfInterface, OrdinalsInterface, - MwebInterface { + MwebInterface + implements ExternalWallet { @override int get isarTransactionVersion => 2; diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index a1970b974..5cee5ce22 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -579,7 +579,7 @@ class NamecoinWallet } TxData txData = TxData( - utxos: {utxo}, + utxos: {StandardInput(utxo)}, opNameState: NameOpState( name: data.name, saltHex: data.salt, @@ -593,7 +593,7 @@ class NamecoinWallet note: "Purchase $noteName", feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( + TxRecipient( address: (await getCurrentReceivingAddress())!.value, isChange: false, amount: Amount( @@ -902,7 +902,7 @@ class NamecoinWallet if (customSatsPerVByte != null) { final result = await coinSelectionName( txData: txData.copyWith(feeRateAmount: BigInt.from(-1)), - utxos: utxos?.toList(), + utxos: utxos?.whereType().map((e) => e.utxo).toList(), coinControl: coinControl, ); @@ -940,7 +940,7 @@ class NamecoinWallet final result = await coinSelectionName( txData: txData.copyWith(feeRateAmount: rate), - utxos: utxos?.toList(), + utxos: utxos?.whereType().map((e) => e.utxo).toList(), coinControl: coinControl, ); @@ -1117,7 +1117,7 @@ class NamecoinWallet // gather required signing data final utxoSigningData = (await fetchBuildTxData( - utxoObjectsToUse, + utxoObjectsToUse.map((e) => StandardInput(e)).toList(), )).whereType().toList(); final int vSizeForOneOutput; diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart index 53afa8df9..d5028093b 100644 --- a/lib/wallets/wallet/impl/tezos_wallet.dart +++ b/lib/wallets/wallet/impl/tezos_wallet.dart @@ -252,7 +252,7 @@ class TezosWallet extends Bip39Wallet { return txData.copyWith( recipients: [ - ( + TxRecipient( amount: sendAmount, address: txData.recipients!.first.address, isChange: txData.recipients!.first.isChange, diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index e1c09b693..b632943d0 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -54,7 +54,7 @@ class WowneroWallet extends LibMoneroWallet { txData: TxData( recipients: [ // This address is only used for getting an approximate fee, never for sending - ( + TxRecipient( address: "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", amount: amount, diff --git a/lib/wallets/wallet/impl/xelis_wallet.dart b/lib/wallets/wallet/impl/xelis_wallet.dart index 2605d40ca..cc11b48f2 100644 --- a/lib/wallets/wallet/impl/xelis_wallet.dart +++ b/lib/wallets/wallet/impl/xelis_wallet.dart @@ -416,7 +416,10 @@ class XelisWallet extends LibXelisWallet { asset: xelis_sdk.xelisAsset, ); - fee = Amount(rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits); + fee = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); outputs.add( OutputV2.isarCantDoRequiredInDefaultConstructor( @@ -477,7 +480,10 @@ class XelisWallet extends LibXelisWallet { asset: transfer.asset, ); - fee = Amount(rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits); + fee = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); outputs.add( OutputV2.isarCantDoRequiredInDefaultConstructor( @@ -719,7 +725,7 @@ class XelisWallet extends LibXelisWallet { recipients.isNotEmpty ? recipients : [ - ( + TxRecipient( address: 'xel:xz9574c80c4xegnvurazpmxhw5dlg2n0g9qm60uwgt75uqyx3pcsqzzra9m', amount: amount, diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 82f3bce3c..763162fd0 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -21,6 +21,7 @@ import '../../../models/keys/cw_key_data.dart'; import '../../../models/keys/view_only_wallet_data.dart'; import '../../../models/node_model.dart'; import '../../../models/paymint/fee_object_model.dart'; +import '../../../models/signing_data.dart'; import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; @@ -502,9 +503,10 @@ abstract class LibMoneroWallet final host = node.host.endsWith(".onion") ? node.host : Uri.parse(node.host).host; ({InternetAddress host, int port})? proxy; - proxy = prefs.useTor && !node.forceNoTor - ? TorService.sharedInstance.getProxyInfo() - : null; + proxy = + prefs.useTor && !node.forceNoTor + ? TorService.sharedInstance.getProxyInfo() + : null; _setSyncStatus(lib_monero_compat.ConnectingSyncStatus()); try { @@ -991,7 +993,8 @@ abstract class LibMoneroWallet bool _torNodeMismatchGuard(NodeModel node) { _canPing = true; // Reset. - final bool mismatch = (prefs.useTor && node.clearnetEnabled && !node.torEnabled) || + final bool mismatch = + (prefs.useTor && node.clearnetEnabled && !node.torEnabled) || (!prefs.useTor && !node.clearnetEnabled && node.torEnabled); if (mismatch) { @@ -1290,7 +1293,7 @@ abstract class LibMoneroWallet } else { final totalInputsValue = txData.utxos! .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + BigInt.from(e)); + .fold(BigInt.zero, (p, e) => p + e); sweep = txData.amount!.raw == totalInputsValue; } @@ -1317,22 +1320,23 @@ abstract class LibMoneroWallet final height = await chainHeight; final inputs = txData.utxos - ?.map( + ?.whereType() + .map( (e) => lib_monero.Output( address: e.address!, - hash: e.txid, - keyImage: e.keyImage!, - value: BigInt.from(e.value), - isFrozen: e.isBlocked, + hash: e.utxo.txid, + keyImage: e.utxo.keyImage!, + value: e.value, + isFrozen: e.utxo.isBlocked, isUnlocked: - e.blockHeight != null && - (height - (e.blockHeight ?? 0)) >= + e.utxo.blockHeight != null && + (height - (e.utxo.blockHeight ?? 0)) >= cryptoCurrency.minConfirms, - height: e.blockHeight ?? 0, - vout: e.vout, - spent: e.used ?? false, + height: e.utxo.blockHeight ?? 0, + vout: e.utxo.vout, + spent: e.utxo.used ?? false, spentHeight: null, // doesn't matter here - coinbase: e.isCoinbase, + coinbase: e.utxo.isCoinbase, ), ) .toList(); diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index 7f7eea6f0..2c47a843d 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -19,6 +19,7 @@ import '../../../models/keys/cw_key_data.dart'; import '../../../models/keys/view_only_wallet_data.dart'; import '../../../models/node_model.dart'; import '../../../models/paymint/fee_object_model.dart'; +import '../../../models/signing_data.dart'; import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; @@ -480,9 +481,10 @@ abstract class LibSalviumWallet final host = node.host.endsWith(".onion") ? node.host : Uri.parse(node.host).host; ({InternetAddress host, int port})? proxy; - proxy = prefs.useTor && !node.forceNoTor - ? TorService.sharedInstance.getProxyInfo() - : null; + proxy = + prefs.useTor && !node.forceNoTor + ? TorService.sharedInstance.getProxyInfo() + : null; _setSyncStatus(ConnectingSyncStatus()); try { @@ -956,7 +958,8 @@ abstract class LibSalviumWallet bool _torNodeMismatchGuard(NodeModel node) { _canPing = true; // Reset. - final bool mismatch = (prefs.useTor && node.clearnetEnabled && !node.torEnabled) || + final bool mismatch = + (prefs.useTor && node.clearnetEnabled && !node.torEnabled) || (!prefs.useTor && !node.clearnetEnabled && node.torEnabled); if (mismatch) { @@ -1255,7 +1258,7 @@ abstract class LibSalviumWallet } else { final totalInputsValue = txData.utxos! .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + BigInt.from(e)); + .fold(BigInt.zero, (p, e) => p + e); sweep = txData.amount!.raw == totalInputsValue; } @@ -1282,22 +1285,23 @@ abstract class LibSalviumWallet final height = await chainHeight; final inputs = txData.utxos - ?.map( + ?.whereType() + .map( (e) => lib_salvium.Output( - address: e.address!, - hash: e.txid, - keyImage: e.keyImage!, - value: BigInt.from(e.value), - isFrozen: e.isBlocked, + address: e.utxo.address!, + hash: e.utxo.txid, + keyImage: e.utxo.keyImage!, + value: e.value, + isFrozen: e.utxo.isBlocked, isUnlocked: - e.blockHeight != null && - (height - (e.blockHeight ?? 0)) >= + e.utxo.blockHeight != null && + (height - (e.utxo.blockHeight ?? 0)) >= cryptoCurrency.minConfirms, - height: e.blockHeight ?? 0, - vout: e.vout, - spent: e.used ?? false, + height: e.utxo.blockHeight ?? 0, + vout: e.utxo.vout, + spent: e.utxo.used ?? false, spentHeight: null, // doesn't matter here - coinbase: e.isCoinbase, + coinbase: e.utxo.isCoinbase, ), ) .toList(); diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index c2bfa5b41..554e04313 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -431,20 +431,24 @@ abstract class Wallet { hasNetwork ? NodeConnectionStatus.connected : NodeConnectionStatus.disconnected; - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent(status, walletId, cryptoCurrency), - ); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent(status, walletId, cryptoCurrency), + ); + } _isConnected = hasNetwork; if (status == NodeConnectionStatus.disconnected) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - cryptoCurrency, - ), - ); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + cryptoCurrency, + ), + ); + } } if (hasNetwork) { @@ -511,19 +515,22 @@ abstract class Wallet { return node; } + bool doNotFireRefreshEvents = false; + // Should fire events Future refresh() async { final refreshCompleter = Completer(); final future = refreshCompleter.future.then( (_) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - cryptoCurrency, - ), - ); - + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + cryptoCurrency, + ), + ); + } if (shouldAutoSync) { _periodicRefreshTimer ??= Timer.periodic(const Duration(seconds: 150), ( timer, @@ -541,20 +548,22 @@ abstract class Wallet { } }, onError: (Object e, StackTrace s) { - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - NodeConnectionStatus.disconnected, - walletId, - cryptoCurrency, - ), - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - cryptoCurrency, - ), - ); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + cryptoCurrency, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + cryptoCurrency, + ), + ); + } Logging.instance.e( "Caught exception in refreshWalletData()", error: e, @@ -572,7 +581,11 @@ abstract class Wallet { if (this is ElectrumXInterface) { (this as ElectrumXInterface?)?.refreshingPercent = percent; } - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(percent, walletId)); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(percent, walletId), + ); + } } // Should fire events @@ -593,13 +606,15 @@ abstract class Wallet { // Slight possibility of race but should be irrelevant await refreshMutex.acquire(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - cryptoCurrency, - ), - ); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + cryptoCurrency, + ), + ); + } // add some small buffer before making calls. // this can probably be removed in the future but was added as a diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 6a505c60c..877cb0783 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -6,6 +6,7 @@ import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:isar/isar.dart'; import 'package:meta/meta.dart'; +import '../../../db/drift/database.dart'; import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/client_manager.dart'; import '../../../electrumx_rpc/electrumx_client.dart'; @@ -32,6 +33,7 @@ import '../impl/firo_wallet.dart'; import '../impl/peercoin_wallet.dart'; import '../intermediate/bip39_hd_wallet.dart'; import 'cpfp_interface.dart'; +import 'mweb_interface.dart'; import 'paynym_interface.dart'; import 'rbf_interface.dart'; import 'view_only_option_interface.dart'; @@ -76,29 +78,33 @@ mixin ElectrumXInterface return false; } - Future> - helperRecipientsConvert(List addrs, List satValues) async { - final List<({String address, Amount amount, bool isChange})> results = []; + Future> helperRecipientsConvert( + List addrs, + List satValues, + ) async { + final List results = []; for (int i = 0; i < addrs.length; i++) { - results.add(( - address: addrs[i], - amount: Amount( - rawValue: satValues[i], - fractionDigits: cryptoCurrency.fractionDigits, + results.add( + TxRecipient( + address: addrs[i], + amount: Amount( + rawValue: satValues[i], + fractionDigits: cryptoCurrency.fractionDigits, + ), + isChange: + (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .subTypeEqualTo(AddressSubType.change) + .and() + .valueEqualTo(addrs[i]) + .valueProperty() + .findFirst()) != + null, ), - isChange: - (await mainDB.isar.addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .subTypeEqualTo(AddressSubType.change) - .and() - .valueEqualTo(addrs[i]) - .valueProperty() - .findFirst()) != - null, - )); + ); } return results; @@ -110,7 +116,7 @@ mixin ElectrumXInterface required bool isSendAll, required bool isSendAllCoinControlUtxos, int additionalOutputs = 0, - List? utxos, + List? utxos, }) async { Logging.instance.d("Starting coinSelection ----------"); @@ -126,29 +132,44 @@ mixin ElectrumXInterface final int? satsPerVByte = txData.satsPerVByte; final selectedTxFeeRate = txData.feeRateAmount!; - final List availableOutputs = - utxos ?? await mainDB.getUTXOs(walletId).findAll(); + final List availableOutputs = + utxos ?? + (await mainDB.getUTXOs(walletId).findAll()) + .map((e) => StandardInput(e)) + .toList(); + if (this is MwebInterface && utxos == null) { + final db = Drift.get(walletId); + final mwebUtxos = + await (db.select(db.mwebUtxos) + ..where((e) => e.used.equals(false))).get(); + + availableOutputs.addAll(mwebUtxos.map((e) => MwebInput(e))); + } + 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(); + availableOutputs.where((e) { + if (e is StandardInput) { + return !e.utxo.isBlocked && + (e.utxo.used != true) && + (canCPFP || + e.utxo.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + )); + } else if (e is MwebInput) { + return !e.utxo.blocked && !e.utxo.used; + } else { + return false; + } + }).toList(); final spendableSatoshiValue = spendableOutputs.fold( BigInt.zero, - (p, e) => p + BigInt.from(e.value), + (p, e) => p + e.value, ); if (spendableSatoshiValue < satoshiAmountToSend) { @@ -181,7 +202,7 @@ mixin ElectrumXInterface BigInt satoshisBeingUsed = BigInt.zero; int inputsBeingConsumed = 0; - final List utxoObjectsToUse = []; + final List utxoObjectsToUse = []; if (!coinControl) { for ( @@ -190,7 +211,7 @@ mixin ElectrumXInterface i++ ) { utxoObjectsToUse.add(spendableOutputs[i]); - satoshisBeingUsed += BigInt.from(spendableOutputs[i].value); + satoshisBeingUsed += spendableOutputs[i].value; inputsBeingConsumed += 1; } for ( @@ -199,9 +220,7 @@ mixin ElectrumXInterface i++ ) { utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += BigInt.from( - spendableOutputs[inputsBeingConsumed].value, - ); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; inputsBeingConsumed += 1; } } else { @@ -507,25 +526,32 @@ mixin ElectrumXInterface ); } - Future> fetchBuildTxData(List utxosToUse) async { + Future> fetchBuildTxData(List utxosToUse) async { // return data - final List signingData = []; + final List signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final derivePathType = cryptoCurrency.addressType( - address: utxosToUse[i].address!, - ); + final input = utxosToUse[i]; + if (input is MwebInput) { + signingData.add(input); + } else if (input is StandardInput) { + final derivePathType = cryptoCurrency.addressType( + address: input.address!, + ); - signingData.add( - StandardInput(utxosToUse[i], derivePathType: derivePathType), - ); + signingData.add( + StandardInput(input.utxo, derivePathType: derivePathType), + ); + } else { + throw Exception("Unknown input type ${input.runtimeType}"); + } } final root = await getRootHDNode(); - for (final sd in signingData) { + for (final sd in signingData.whereType()) { coinlib.HDPrivateKey? keys; final address = await mainDB.getAddress(walletId, sd.utxo.address!); if (address?.derivationPath != null) { @@ -587,8 +613,16 @@ mixin ElectrumXInterface final List prevOuts = []; + final hasMwebInputs = utxoSigningData.whereType().isNotEmpty; + final hasMwebOutputs = + txData.recipients! + .where((e) => e.addressType == AddressType.mweb) + .isNotEmpty; + + final isMweb = hasMwebOutputs || hasMwebInputs; + coinlib.Transaction clTx = coinlib.Transaction( - version: cryptoCurrency.transactionVersion, + version: isMweb ? 2 : cryptoCurrency.transactionVersion, inputs: [], outputs: [], ); @@ -599,88 +633,125 @@ mixin ElectrumXInterface ? 0xffffffff - 10 : 0xffffffff - 1; - final standardInputs = utxoSigningData.whereType().toList(); - // Add transaction inputs - for (var i = 0; i < standardInputs.length; i++) { - final txid = standardInputs[i].utxo.txid; + for (var i = 0; i < utxoSigningData.length; i++) { + final data = utxoSigningData[i]; + if (data is MwebInput) { + final address = data.address; + + final addr = await mainDB.getAddress(walletId, address); + final index = addr!.derivationIndex; + + final input = coinlib.RawInput( + prevOut: coinlib.OutPoint( + Uint8List.fromList( + data.utxo.outputId.toUint8ListFromHex.reversed.toList(), + ), + index, + ), + scriptSig: Uint8List(0), + ); - final hash = Uint8List.fromList( - txid.toUint8ListFromHex.reversed.toList(), - ); + clTx = clTx.addInput(input); - final prevOutpoint = coinlib.OutPoint(hash, standardInputs[i].utxo.vout); + tempInputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: input.scriptSig.toHex, + scriptSigAsm: null, + sequence: sequence, + outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: data.utxo.outputId, + vout: index, + ), + addresses: [address], + valueStringSats: utxoSigningData[i].value.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ), + ); + } else if (data is StandardInput) { + final txid = data.utxo.txid; - final prevOutput = coinlib.Output.fromAddress( - BigInt.from(standardInputs[i].utxo.value), - coinlib.Address.fromString( - standardInputs[i].utxo.address!, - cryptoCurrency.networkParams, - ), - ); + final hash = Uint8List.fromList( + txid.toUint8ListFromHex.reversed.toList(), + ); - prevOuts.add(prevOutput); + final prevOutpoint = coinlib.OutPoint(hash, data.utxo.vout); - final coinlib.Input input; + final prevOutput = coinlib.Output.fromAddress( + BigInt.from(data.utxo.value), + coinlib.Address.fromString( + data.utxo.address!, + cryptoCurrency.networkParams, + ), + ); - switch (standardInputs[i].derivePathType) { - case DerivePathType.bip44: - case DerivePathType.bch44: - input = coinlib.P2PKHInput( - prevOut: prevOutpoint, - publicKey: standardInputs[i].key!.publicKey, - sequence: sequence, - ); + prevOuts.add(prevOutput); - // TODO: fix this as it is (probably) wrong! - case DerivePathType.bip49: - throw Exception("TODO p2sh"); - // input = coinlib.P2SHMultisigInput( - // prevOut: prevOutpoint, - // program: coinlib.MultisigProgram.decompile( - // standardInputs[i].redeemScript!, - // ), - // sequence: sequence, - // ); - - case DerivePathType.bip84: - input = coinlib.P2WPKHInput( - prevOut: prevOutpoint, - publicKey: standardInputs[i].key!.publicKey, - sequence: sequence, - ); + final coinlib.Input input; - case DerivePathType.bip86: - input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); + switch (data.derivePathType) { + case DerivePathType.bip44: + case DerivePathType.bch44: + input = coinlib.P2PKHInput( + prevOut: prevOutpoint, + publicKey: data.key!.publicKey, + sequence: sequence, + ); - default: - throw UnsupportedError( - "Unknown derivation path type found: ${standardInputs[i].derivePathType}", - ); - } + // TODO: fix this as it is (probably) wrong! + case DerivePathType.bip49: + throw Exception("TODO p2sh"); + // input = coinlib.P2SHMultisigInput( + // prevOut: prevOutpoint, + // program: coinlib.MultisigProgram.decompile( + // data.redeemScript!, + // ), + // sequence: sequence, + // ); + + case DerivePathType.bip84: + input = coinlib.P2WPKHInput( + prevOut: prevOutpoint, + publicKey: data.key!.publicKey, + sequence: sequence, + ); - clTx = clTx.addInput(input); + case DerivePathType.bip86: + input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); - tempInputs.add( - InputV2.isarCantDoRequiredInDefaultConstructor( - scriptSigHex: input.scriptSig.toHex, - scriptSigAsm: null, - sequence: sequence, - outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: standardInputs[i].utxo.txid, - vout: standardInputs[i].utxo.vout, + default: + throw UnsupportedError( + "Unknown derivation path type found: ${data.derivePathType}", + ); + } + + clTx = clTx.addInput(input); + + tempInputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: input.scriptSig.toHex, + scriptSigAsm: null, + sequence: sequence, + outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: data.utxo.txid, + vout: data.utxo.vout, + ), + addresses: data.utxo.address == null ? [] : [data.utxo.address!], + valueStringSats: data.utxo.value.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, ), - addresses: - standardInputs[i].utxo.address == null - ? [] - : [standardInputs[i].utxo.address!], - valueStringSats: standardInputs[i].utxo.value.toString(), - witness: null, - innerRedeemScriptAsm: null, - coinbase: null, - walletOwns: true, - ), - ); + ); + } else { + throw Exception( + "Unknown input type: ${utxoSigningData[i].runtimeType}", + ); + } } // Add transaction output @@ -702,10 +773,18 @@ mixin ElectrumXInterface rethrow; } } - final output = coinlib.Output.fromAddress( - txData.recipients![i].amount.raw, - address, - ); + final coinlib.Output output; + if (address is coinlib.MwebAddress) { + output = coinlib.Output.fromProgram( + txData.recipients![i].amount.raw, + address.program, + ); + } else { + output = coinlib.Output.fromAddress( + txData.recipients![i].amount.raw, + address, + ); + } clTx = clTx.addOutput(output); @@ -729,33 +808,40 @@ mixin ElectrumXInterface try { // Sign the transaction accordingly - for (var i = 0; i < standardInputs.length; i++) { - final value = BigInt.from(standardInputs[i].utxo.value); - final key = standardInputs[i].key!.privateKey!; - - if (clTx.inputs[i] is coinlib.TaprootKeyInput) { - final taproot = coinlib.Taproot( - internalKey: standardInputs[i].key!.publicKey, - ); - - clTx = clTx.signTaproot( - inputN: i, - key: taproot.tweakPrivateKey(key), - prevOuts: prevOuts, - ); - } else if (clTx.inputs[i] is coinlib.LegacyWitnessInput) { - clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); - } else if (clTx.inputs[i] is coinlib.LegacyInput) { - clTx = clTx.signLegacy(inputN: i, key: key); - } else if (clTx.inputs[i] is coinlib.TaprootSingleScriptSigInput) { - clTx = clTx.signTaprootSingleScriptSig( - inputN: i, - key: key, - prevOuts: prevOuts, - ); + for (var i = 0; i < utxoSigningData.length; i++) { + final data = utxoSigningData[i]; + + if (data is MwebInput) { + // do nothing + } else if (data is StandardInput) { + final value = BigInt.from(data.utxo.value); + final key = data.key!.privateKey!; + if (clTx.inputs[i] is coinlib.TaprootKeyInput) { + final taproot = coinlib.Taproot(internalKey: data.key!.publicKey); + + clTx = clTx.signTaproot( + inputN: i, + key: taproot.tweakPrivateKey(key), + prevOuts: prevOuts, + ); + } else if (clTx.inputs[i] is coinlib.LegacyWitnessInput) { + clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); + } else if (clTx.inputs[i] is coinlib.LegacyInput) { + clTx = clTx.signLegacy(inputN: i, key: key); + } else if (clTx.inputs[i] is coinlib.TaprootSingleScriptSigInput) { + clTx = clTx.signTaprootSingleScriptSig( + inputN: i, + key: key, + prevOuts: prevOuts, + ); + } else { + throw Exception( + "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + ); + } } else { throw Exception( - "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + "Unknown input type: ${utxoSigningData[i].runtimeType}", ); } } @@ -769,6 +855,7 @@ mixin ElectrumXInterface } return txData.copyWith( + isMweb: isMweb, raw: clTx.toHex(), // dirty shortcut for peercoin's weirdness vSize: this is PeercoinWallet ? clTx.size : clTx.vSize(), @@ -1707,9 +1794,7 @@ mixin ElectrumXInterface final isSendAllCoinControlUtxos = coinControl && txData.amount!.raw == - utxos - .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + BigInt.from(e)); + utxos.map((e) => e.value).fold(BigInt.zero, (p, e) => p + e); if (customSatsPerVByte != null) { // check for send all @@ -1789,6 +1874,26 @@ mixin ElectrumXInterface ); } + // mweb + if (result.isMweb) { + final mwebData = await coinSelection( + txData: result.copyWith( + recipients: + result.recipients! + .where( + (e) => + !(e.isChange && e.addressType == AddressType.mweb), + ) + .toList(), + ), + coinControl: coinControl, + isSendAll: isSendAll, + isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, + ); + + return await (this as MwebInterface).processMwebTransaction(mwebData); + } + return result; } else { throw ArgumentError("Invalid fee rate argument provided!"); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart index f812faba8..61ac5f030 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; import 'package:mweb_client/mweb_client.dart'; import '../../../db/drift/database.dart'; @@ -13,21 +13,25 @@ import '../../../models/balance.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 '../../../services/event_bus/events/global/blocks_remaining_event.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import '../../../services/event_bus/global_event_bus.dart'; import '../../../services/mwebd_service.dart'; import '../../../utilities/amount/amount.dart'; +import '../../../utilities/enums/fee_rate_type_enum.dart'; import '../../../utilities/extensions/extensions.dart'; import '../../../utilities/logger.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../../isar/models/wallet_info.dart'; import '../../models/tx_data.dart'; +import '../intermediate/external_wallet.dart'; import 'electrumx_interface.dart'; mixin MwebInterface - on ElectrumXInterface { + on ElectrumXInterface + implements ExternalWallet { // TODO StreamSubscription? _mwebUtxoSubscription; @@ -45,10 +49,25 @@ mixin MwebInterface .walletIdEqualTo(walletId) .filter() .typeEqualTo(AddressType.mweb) + .and() + .subTypeEqualTo(AddressSubType.receiving) .sortByDerivationIndexDesc() .findFirst(); } + Future getMwebChangeAddress() async { + return await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .and() + .subTypeEqualTo(AddressSubType.change) + .and() + .derivationIndexEqualTo(0) + .findFirst(); + } + Future get _client async { final client = await MwebdService.instance.getClient( cryptoCurrency.network, @@ -59,7 +78,23 @@ mixin MwebInterface return client; } - WalletSyncStatus? _syncStatus; + WalletSyncStatus? _syncStatusMwebCache; + WalletSyncStatus? get _syncStatusMweb => _syncStatusMwebCache; + set _syncStatusMweb(WalletSyncStatus? newValue) { + switch (newValue) { + case null: + doNotFireRefreshEvents = true; + case WalletSyncStatus.unableToSync: + doNotFireRefreshEvents = true; + case WalletSyncStatus.synced: + doNotFireRefreshEvents = false; + case WalletSyncStatus.syncing: + doNotFireRefreshEvents = true; + } + + _syncStatusMwebCache = newValue; + } + Timer? _mwebdPolling; int currentKnownChainHeight = 0; double highestPercentCached = 0; @@ -124,7 +159,7 @@ mixin MwebInterface syncStatus = WalletSyncStatus.synced; } - _syncStatus = syncStatus; + _syncStatusMweb = syncStatus; GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent(syncStatus, walletId, info.coin), ); @@ -134,9 +169,9 @@ mixin MwebInterface error: e, stackTrace: s, ); - _syncStatus = WalletSyncStatus.unableToSync; + _syncStatusMweb = WalletSyncStatus.unableToSync; GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent(_syncStatus!, walletId, info.coin), + WalletSyncStatusChangedEvent(_syncStatusMweb!, walletId, info.coin), ); } }); @@ -260,7 +295,7 @@ mixin MwebInterface cryptoCurrency.network, ); if (status == null) { - await MwebdService.instance.init(cryptoCurrency.network); + await MwebdService.instance.initService(cryptoCurrency.network); } _startPollingMwebd(); @@ -269,16 +304,23 @@ mixin MwebInterface } } - Future
generateNextMwebAddress() async { + /// [isChange] will always return the change address at index 0 !!!!! + Future
generateNextMwebAddress({bool isChange = false}) async { if (!info.isMwebEnabled) { throw Exception( "Tried calling generateNextMwebAddress with mweb disabled for $walletId ${info.name}", ); } - final highestStoredIndex = - (await getCurrentReceivingMwebAddress())?.derivationIndex ?? -1; - final nextIndex = highestStoredIndex + 1; + final int nextIndex; + if (isChange) { + nextIndex = 0; + } else { + final highestStoredIndex = + (await getCurrentReceivingMwebAddress())?.derivationIndex ?? 0; + + nextIndex = highestStoredIndex + 1; + } final client = await _client; @@ -295,31 +337,12 @@ mixin MwebInterface derivationIndex: nextIndex, derivationPath: null, type: AddressType.mweb, - subType: AddressSubType.receiving, + subType: isChange ? AddressSubType.change : AddressSubType.receiving, ); } - Future estimateFeeForMweb(Amount amount) async { - if (!info.isMwebEnabled) { - throw Exception( - "Tried calling estimateFeeForMweb with mweb disabled for $walletId ${info.name}", - ); - } - throw UnimplementedError(); - } - - Future prepareSendMweb({required TxData txData}) async { - if (!info.isMwebEnabled) { - throw Exception( - "Tried calling prepareSendMweb with mweb disabled for $walletId ${info.name}", - ); - } - if (!txData.isMweb) { - throw Exception("Invalid mweb flagged tx data"); - } - + Future processMwebTransaction(TxData txData) async { final client = await _client; - final response = await client.create( CreateRequest( rawTx: txData.raw!.toUint8ListFromHex, @@ -333,19 +356,23 @@ mixin MwebInterface return txData.copyWith(raw: Uint8List.fromList(response.rawTx).toHex); } - Future confirmSendMweb({required TxData txData}) async { + Future _confirmSendMweb({required TxData txData}) async { if (!info.isMwebEnabled) { throw Exception( - "Tried calling confirmSendMweb with mweb disabled for $walletId ${info.name}", + "Tried calling _confirmSendMweb with mweb disabled for $walletId ${info.name}", ); } try { - Logging.instance.d("confirmSend txData: $txData"); + Logging.instance.d("_confirmSendMweb txData: $txData"); - final txHash = await electrumXClient.broadcastTransaction( - rawTx: txData.raw!, + final client = await _client; + + final response = await client.broadcast( + BroadcastRequest(rawTx: txData.raw!.toUint8ListFromHex), ); + + final txHash = response.txid; Logging.instance.d("Sent txHash: $txHash"); txData = txData.copyWith(txHash: txHash, txid: txHash); @@ -371,7 +398,7 @@ mixin MwebInterface return await updateSentCachedTxData(txData: txData); } catch (e, s) { Logging.instance.e( - "Exception rethrown from confirmSendMweb(): ", + "Exception rethrown from _confirmSendMweb(): ", error: e, stackTrace: s, ); @@ -416,11 +443,34 @@ mixin MwebInterface } // TODO finish - final txData = await prepareSendMweb( - txData: TxData(utxos: spendableUtxos.toSet()), + final txData = await prepareSend( + txData: TxData( + isMweb: true, + feeRateType: FeeRateType.average, + utxos: spendableUtxos.map((e) => StandardInput(e)).toSet(), + recipients: [ + TxRecipient( + address: (await getCurrentReceivingMwebAddress())!.value, + amount: spendableUtxos.fold( + Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits), + (p, e) => + p + + Amount( + rawValue: BigInt.from(e.value), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ), + + isChange: false, + addressType: AddressType.mweb, + ), + ], + ), ); - await confirmSendMweb(txData: txData); + final processed = await processMwebTransaction(txData); + + await _confirmSendMweb(txData: processed); } catch (e, s) { Logging.instance.w( "Exception caught in anonymizeAllMweb(): ", @@ -431,19 +481,71 @@ mixin MwebInterface } } + Future _checkSpentMwebUtxos() async { + try { + final db = Drift.get(walletId); + final mwebUtxos = + await (db.select(db.mwebUtxos) + ..where((e) => e.used.equals(false))).get(); + + final client = await _client; + + final spent = await client.spent( + SpentRequest(outputId: mwebUtxos.map((e) => e.outputId)), + ); + + final updated = mwebUtxos.where( + (e) => spent.outputId.contains(e.outputId), + ); + + await db.transaction(() async { + for (final utxo in updated) { + await db + .into(db.mwebUtxos) + .insertOnConflictUpdate( + utxo.toCompanion(false).copyWith(used: const Value(true)), + ); + } + }); + } catch (e, s) { + Logging.instance.e("_checkSpentMwebUtxos()", error: e, stackTrace: s); + } + } + + Future _checkAddresses() async { + // check change first as it is index 0 + Address? changeAddress = await getMwebChangeAddress(); + if (changeAddress == null) { + changeAddress = await generateNextMwebAddress(isChange: true); + await mainDB.putAddress(changeAddress); + } + + // check recieving + Address? address = await getCurrentReceivingMwebAddress(); + if (address == null) { + address = await generateNextMwebAddress(); + await mainDB.putAddress(address); + } + } + // =========================================================================== - @mustCallSuper @override - Future init() async { + Future confirmSend({required TxData txData}) async { + if (txData.isMweb) { + return await _confirmSendMweb(txData: txData); + } else { + return await super.confirmSend(txData: txData); + } + } + + @override + Future open() async { if (info.isMwebEnabled) { try { await _initMweb(); - Address? address = await getCurrentReceivingMwebAddress(); - if (address == null) { - address = await generateNextMwebAddress(); - await mainDB.putAddress(address); - } + + await _checkAddresses(); unawaited(_startUpdateMwebUtxos()); } catch (e, s) { @@ -455,8 +557,6 @@ mixin MwebInterface ); } } - - await super.init(); } @override @@ -470,6 +570,8 @@ mixin MwebInterface if (info.isMwebEnabled) { final start = DateTime.now(); try { + await _checkSpentMwebUtxos(); + final currentHeight = await chainHeight; final db = Drift.get(walletId); final mwebUtxos = @@ -547,52 +649,6 @@ mixin MwebInterface } } - @override - Future refresh() async { - if (isViewOnly || !info.isMwebEnabled) { - await super.refresh(); - return; - } - - // Awaiting this lock could be dangerous. - // Since refresh is periodic (generally) - if (refreshMutex.isLocked) { - return; - } - - // TODO - - // final node = getCurrentNode(); - // - // if (_torNodeMismatchGuard(node)) { - // throw Exception("TOR – clearnet mismatch"); - // } - // - // // this acquire should be almost instant due to above check. - // // Slight possibility of race but should be irrelevant - // await refreshMutex.acquire(); - // - // libMoneroWallet?.startSyncing(); - // _setSyncStatus(lib_monero_compat.StartingSyncStatus()); - // - // await updateTransactions(); - // await updateBalance(); - // - // if (info.otherData[WalletInfoKeys.reuseAddress] != true) { - // await checkReceivingAddressForTransactions(); - // } - // - // if (refreshMutex.isLocked) { - // refreshMutex.release(); - // } - // - // final synced = await libMoneroWallet?.isSynced(); - // - // if (synced == true) { - // _setSyncStatus(lib_monero_compat.SyncedSyncStatus()); - // } - } - @override Future recover({required bool isRescan}) async { if (isViewOnly) { @@ -616,6 +672,8 @@ mixin MwebInterface try { await refreshMutex.protect(() async { if (isRescan) { + await _stopUpdateMwebUtxos(); + // clear cache await electrumXCachedClient.clearSharedTransactionCache( cryptoCurrency: info.coin, @@ -639,8 +697,9 @@ mixin MwebInterface final db = Drift.get(walletId); await db.transaction(() async => await db.delete(db.mwebUtxos).go()); - await _stopUpdateMwebUtxos(); if (info.isMwebEnabled) { + await _checkAddresses(); + // only restart scanning if mweb enabled unawaited(_startUpdateMwebUtxos()); } @@ -747,4 +806,76 @@ mixin MwebInterface _mwebdPolling = null; await super.exit(); } + + bool isMwebAddress(String address) { + try { + cl.MwebAddress.fromString(address, network: cryptoCurrency.networkParams); + return true; + } catch (_) { + return false; + } + } + + // TODO: this is broken + Future mwebFee({required TxData txData}) async { + final client = await _client; + + final resp = await client.create( + CreateRequest( + rawTx: txData.raw!.toUint8ListFromHex, + scanSecret: await _scanSecret, + spendSecret: await _spendSecret, + feeRatePerKb: Int64(txData.feeRateAmount!.toInt()), + dryRun: false, + ), + ); + + final tx = cl.Transaction.fromBytes(Uint8List.fromList(resp.rawTx)); + + final used = [ + ...txData.usedUTXOs?.map((e) => StandardInput(e)) ?? [], + ...txData.usedMwebUtxos?.map((e) => MwebInput(e)) ?? [], + ]; + + final posUtxos = + used + .where( + (utxo) => tx.inputs.any( + (input) => + input.prevOut.hash.toHex == + Uint8List.fromList( + utxo.id.toUint8ListFromHex.reversed.toList(), + ).toHex, + ), + ) + .toList(); + + final preInSum = used.fold(BigInt.zero, (p, e) => p + e.value); + final posOutputSum = tx.outputs.fold( + BigInt.zero, + (acc, output) => acc + output.value, + ); + final mwebInputSum = + preInSum - posUtxos.fold(BigInt.zero, (p, e) => p + e.value); + + final preOutputSum = txData.recipients!.fold( + BigInt.zero, + (p, e) => p + e.amount.raw, + ); + + final difference = preOutputSum - mwebInputSum; + + final expectedPegin = difference > BigInt.zero ? difference : BigInt.zero; + + final fee = preInSum - preOutputSum; + + BigInt feeIncrease = posOutputSum - expectedPegin; + if (expectedPegin > BigInt.zero && fee == BigInt.zero) { + feeIncrease += txData.fee!.raw + txData.feeRateAmount! * BigInt.from(41); + } + return Amount( + rawValue: fee + feeIncrease, + fractionDigits: cryptoCurrency.fractionDigits, + ); + } } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart index 5d38c3d87..1a62c46ef 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart @@ -379,7 +379,7 @@ mixin PaynymInterface return prepareSend( txData: txData.copyWith( recipients: [ - ( + TxRecipient( address: sendToAddress.value, amount: txData.recipients!.first.amount, isChange: false, @@ -528,7 +528,7 @@ mixin PaynymInterface // gather required signing data final utxoSigningData = (await fetchBuildTxData( - utxoObjectsToUse, + utxoObjectsToUse.map((e) => StandardInput(e)).toList(), )).whereType().toList(); final vSizeForNoChange = BigInt.from( @@ -608,7 +608,7 @@ mixin PaynymInterface final txData = TxData( raw: txn.item1, recipients: [ - ( + TxRecipient( address: targetPaymentCodeString, amount: amountToSend, isChange: false, @@ -619,7 +619,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.map((e) => e.utxo).toSet(), + utxos: utxoSigningData.toSet(), note: "PayNym connect", ); @@ -638,7 +638,7 @@ mixin PaynymInterface final txData = TxData( raw: txn.item1, recipients: [ - ( + TxRecipient( address: targetPaymentCodeString, amount: amountToSend, isChange: false, @@ -649,7 +649,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.map((e) => e.utxo).toSet(), + utxos: utxoSigningData.toSet(), note: "PayNym connect", ); @@ -669,7 +669,7 @@ mixin PaynymInterface final txData = TxData( raw: txn.item1, recipients: [ - ( + TxRecipient( address: targetPaymentCodeString, amount: amountToSend, isChange: false, @@ -680,7 +680,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.map((e) => e.utxo).toSet(), + utxos: utxoSigningData.toSet(), note: "PayNym connect", ); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart index 2e609aeb0..0fad30a5e 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart @@ -4,6 +4,7 @@ import 'package:isar/isar.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/fee_rate_type_enum.dart'; import '../../../utilities/logger.dart'; @@ -46,14 +47,15 @@ mixin RbfInterface required TransactionV2 oldTransaction, required int newRate, }) async { - final note = await mainDB.isar.transactionNotes - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(oldTransaction.txid) - .findFirst(); + final note = + await mainDB.isar.transactionNotes + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(oldTransaction.txid) + .findFirst(); - final Set utxos = {}; + final Set utxos = {}; for (final input in oldTransaction.inputs) { final utxo = UTXO( walletId: walletId, @@ -71,7 +73,7 @@ mixin RbfInterface address: input.addresses.first, ); - utxos.add(utxo); + utxos.add(StandardInput(utxo)); } final List recipients = []; @@ -86,43 +88,43 @@ mixin RbfInterface final isChange = addressModel?.subType == AddressSubType.change; recipients.add( - ( + TxRecipient( address: address, amount: Amount( - rawValue: output.value, - fractionDigits: cryptoCurrency.fractionDigits), + rawValue: output.value, + fractionDigits: cryptoCurrency.fractionDigits, + ), isChange: isChange, ), ); } - final oldFee = oldTransaction - .getFee(fractionDigits: cryptoCurrency.fractionDigits) - .raw; - final inSum = utxos - .map((e) => BigInt.from(e.value)) - .fold(BigInt.zero, (p, e) => p + e); + final oldFee = + oldTransaction + .getFee(fractionDigits: cryptoCurrency.fractionDigits) + .raw; + final inSum = utxos.map((e) => e.value).fold(BigInt.zero, (p, e) => p + e); final noChange = recipients.map((e) => e.isChange).fold(false, (p, e) => p || e) == - false; - final otherAvailableUtxos = await mainDB - .getUTXOs(walletId) - .filter() - .isBlockedEqualTo(false) - .and() - .group( - (q) => q.usedIsNull().or().usedEqualTo(false), - ) - .findAll(); + false; + final otherAvailableUtxos = + await mainDB + .getUTXOs(walletId) + .filter() + .isBlockedEqualTo(false) + .and() + .group((q) => q.usedIsNull().or().usedEqualTo(false)) + .findAll(); final height = await chainHeight; otherAvailableUtxos.removeWhere( - (e) => !e.isConfirmed( - height, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - ), + (e) => + !e.isConfirmed( + height, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), ); TxData txData = TxData( @@ -138,7 +140,7 @@ mixin RbfInterface // safe to assume send all? txData = txData.copyWith( recipients: [ - ( + TxRecipient( address: recipients.first.address, amount: Amount( rawValue: inSum, @@ -159,8 +161,9 @@ mixin RbfInterface throw Exception("New fee in RBF has not changed at all"); } - final indexOfChangeOutput = - txData.recipients!.indexWhere((e) => e.isChange); + final indexOfChangeOutput = txData.recipients!.indexWhere( + (e) => e.isChange, + ); final removed = txData.recipients!.removeAt(indexOfChangeOutput); @@ -172,7 +175,7 @@ mixin RbfInterface // update recipients txData.recipients!.insert( indexOfChangeOutput, - ( + TxRecipient( address: removed.address, amount: Amount( rawValue: newChangeAmount, @@ -200,7 +203,11 @@ mixin RbfInterface } return await buildTransaction( txData: txData.copyWith( - usedUTXOs: txData.utxos!.toList(), + usedUTXOs: + txData.utxos! + .whereType() + .map((e) => e.utxo) + .toList(), fee: Amount( rawValue: newFee, fractionDigits: cryptoCurrency.fractionDigits, @@ -232,7 +239,7 @@ mixin RbfInterface } txData.recipients!.insert( indexOfChangeOutput, - ( + TxRecipient( address: removed.address, amount: Amount( rawValue: newChangeAmount, @@ -243,7 +250,7 @@ mixin RbfInterface ); final newUtxoSet = { - ...txData.utxos!, + ...txData.utxos!.whereType().map((e) => e.utxo), ...extraUtxos, }; @@ -257,14 +264,16 @@ mixin RbfInterface return await buildTransaction( txData: txData.copyWith( - utxos: newUtxoSet, + utxos: newUtxoSet.map((e) => StandardInput(e)).toSet(), usedUTXOs: newUtxoSet.toList(), fee: Amount( rawValue: newFee, fractionDigits: cryptoCurrency.fractionDigits, ), ), - utxoSigningData: await fetchBuildTxData(newUtxoSet.toList()), + utxoSigningData: await fetchBuildTxData( + newUtxoSet.map((e) => StandardInput(e)).toList(), + ), ); } } else { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 02a257ea8..42cc50a79 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -480,8 +480,7 @@ mixin SparkInterface txb.setLockTime(await chainHeight); txb.setVersion(3 | (9 << 16)); - List<({String address, Amount amount, bool isChange})>? - recipientsWithFeeSubtracted; + List? recipientsWithFeeSubtracted; List<({String address, Amount amount, String memo, bool isChange})>? sparkRecipientsWithFeeSubtracted; final recipientCount = @@ -535,16 +534,18 @@ mixin SparkInterface if (txData.recipients![i].amount.raw == BigInt.zero) { continue; } - recipientsWithFeeSubtracted!.add(( - address: txData.recipients![i].address, - amount: Amount( - rawValue: - txData.recipients![i].amount.raw - - (estimatedFee ~/ BigInt.from(totalRecipientCount)), - fractionDigits: cryptoCurrency.fractionDigits, + recipientsWithFeeSubtracted!.add( + TxRecipient( + address: txData.recipients![i].address, + amount: Amount( + rawValue: + txData.recipients![i].amount.raw - + (estimatedFee ~/ BigInt.from(totalRecipientCount)), + fractionDigits: cryptoCurrency.fractionDigits, + ), + isChange: txData.recipients![i].isChange, ), - isChange: txData.recipients![i].isChange, - )); + ); final scriptPubKey = btc.Address.addressToOutputScript( txData.recipients![i].address, @@ -1556,7 +1557,9 @@ mixin SparkInterface for (final utxo in itr) { if (nValueToSelect > nValueIn) { setCoins.add( - (await fetchBuildTxData([utxo])).whereType().first, + (await fetchBuildTxData([ + StandardInput(utxo), + ])).whereType().first, ); nValueIn += BigInt.from(utxo.value); } @@ -2073,7 +2076,8 @@ mixin SparkInterface throw Exception("Attempted send of zero amount"); } - final utxos = txData.utxos; + final utxos = + txData.utxos?.whereType().map((e) => e.utxo).toList(); final bool coinControl = utxos != null; final utxosTotal = @@ -2228,7 +2232,7 @@ mixin SparkInterface txData: TxData( sparkNameInfo: data, recipients: [ - ( + TxRecipient( address: destinationAddress, amount: Amount.fromDecimal( Decimal.fromInt(kStandardSparkNamesFee[name.length] * years), diff --git a/pubspec.lock b/pubspec.lock index 4db79d477..9cbfc4ab4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -352,21 +352,17 @@ packages: coinlib: dependency: "direct overridden" description: - path: coinlib - ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 - resolved-ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 - url: "https://www.github.com/julian-CStack/coinlib" - source: git - version: "3.1.0" + path: "../coinlib/coinlib" + relative: true + source: path + version: "4.1.0" coinlib_flutter: dependency: "direct main" description: - path: coinlib_flutter - ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 - resolved-ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 - url: "https://www.github.com/julian-CStack/coinlib" - source: git - version: "3.0.0" + path: "../coinlib/coinlib_flutter" + relative: true + source: path + version: "4.0.0" collection: dependency: transitive description: @@ -1285,10 +1281,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.19.0" io: dependency: transitive description: @@ -1891,14 +1887,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" - rxdart: - dependency: "direct main" - description: - name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.dev" - source: hosted - version: "0.27.7" sec: dependency: transitive description: From 921287cee7520233a645f23373f1f9eb33b36816 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 24 Jun 2025 09:52:25 -0600 Subject: [PATCH 14/42] temp hacks --- .../electrumx_interface.dart | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 877cb0783..96f13af8b 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -85,6 +85,18 @@ mixin ElectrumXInterface final List results = []; for (int i = 0; i < addrs.length; i++) { + // temp mweb hack + AddressType? type; + if (this is MwebInterface) { + final addr = coinlib.Address.fromString( + addrs[i], + cryptoCurrency.networkParams, + ); + if (addr is coinlib.MwebAddress) { + type = AddressType.mweb; + } + } + results.add( TxRecipient( address: addrs[i], @@ -103,6 +115,7 @@ mixin ElectrumXInterface .valueProperty() .findFirst()) != null, + addressType: type, ), ); } @@ -143,7 +156,9 @@ mixin ElectrumXInterface await (db.select(db.mwebUtxos) ..where((e) => e.used.equals(false))).get(); - availableOutputs.addAll(mwebUtxos.map((e) => MwebInput(e))); + for (final e in mwebUtxos) { + availableOutputs.add(MwebInput(e)); + } } final currentChainHeight = await chainHeight; @@ -241,7 +256,7 @@ mixin ElectrumXInterface final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); if (isSendAll || isSendAllCoinControlUtxos) { - if (satoshiAmountToSend != satoshisBeingUsed) { + if (satoshiAmountToSend != satoshisBeingUsed && !txData.isMweb) { throw Exception( "Something happened that should never actually happen. " "Please report this error to the developers.", @@ -621,6 +636,9 @@ mixin ElectrumXInterface final isMweb = hasMwebOutputs || hasMwebInputs; + print("IS MEB hasMwebInputs: $hasMwebInputs"); + print("IS MEB hasMwebOutputs: $hasMwebOutputs"); + coinlib.Transaction clTx = coinlib.Transaction( version: isMweb ? 2 : cryptoCurrency.transactionVersion, inputs: [], @@ -1784,6 +1802,36 @@ mixin ElectrumXInterface throw Exception("No recipients in attempted transaction!"); } + if (this is MwebInterface) { + bool hasMwebOutputs = false; + final recipients = + txData.recipients!.map((e) { + if (e.addressType == null) { + final addr = coinlib.Address.fromString( + e.address, + cryptoCurrency.networkParams, + ); + if (addr is coinlib.MwebAddress) { + hasMwebOutputs = true; + return TxRecipient( + address: e.address, + amount: e.amount, + isChange: e.isChange, + addressType: AddressType.mweb, + ); + } + } + return e; + }).toList(); + txData = txData.copyWith( + recipients: recipients, + isMweb: txData.isMweb || hasMwebOutputs, + ); + print("IS MEB FLAG AAAA: ${txData.isMweb}"); + } + + print("IS MEB FLAG: ${txData.isMweb}"); + final feeRateType = txData.feeRateType; final customSatsPerVByte = txData.satsPerVByte; final feeRateAmount = txData.feeRateAmount; From 15f84b483e69781819250dac496d157668589c96 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 26 Jun 2025 16:18:23 -0600 Subject: [PATCH 15/42] no need to update if empty list passed in --- lib/db/isar/main_db.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index cb0163503..c8f7fc493 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -312,6 +312,9 @@ class MainDB { Future updateUTXOs(String walletId, List utxos) async { bool newUTXO = false; + + if (utxos.isEmpty) return newUTXO; + await isar.writeTxn(() async { final set = utxos.toSet(); for (final utxo in utxos) { From 11d6b2cc132eac430b68c6892aedc486ed854e14 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 26 Jun 2025 16:18:46 -0600 Subject: [PATCH 16/42] fix: respect verbose flag --- lib/electrumx_rpc/electrumx_client.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 3852d9be9..da5cf2778 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -813,7 +813,10 @@ class ElectrumXClient { }) async { Logging.instance.d("attempting to fetch blockchain.transaction.get..."); await checkElectrumAdapter(); - final dynamic response = await getElectrumAdapter()!.getTransaction(txHash); + final dynamic response = await getElectrumAdapter()!.request( + 'blockchain.transaction.get', + [txHash, verbose], + ); Logging.instance.d("Fetching blockchain.transaction.get finished"); if (!verbose) { From e9f021cb51d18158a2bf56314ed9a2dfd2ed3246 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 27 Jun 2025 12:33:23 -0600 Subject: [PATCH 17/42] mweb (and pegin/pegout) transactions (with some bugs) and some relatively major refactoring in general --- lib/models/{signing_data.dart => input.dart} | 0 .../exchange_step_views/step_4_view.dart | 15 +- lib/pages/exchange_view/send_from_view.dart | 19 +- lib/pages/namecoin_names/buy_domain_view.dart | 1 + .../sub_widgets/transfer_option_widget.dart | 1 + .../sub_widgets/update_option_widget.dart | 1 + .../send_view/frost_ms/frost_send_view.dart | 1 + .../send_steps/frost_send_step_1b.dart | 4 +- lib/pages/send_view/send_view.dart | 40 +- lib/pages/send_view/token_send_view.dart | 8 +- .../desktop_coin_control_use_dialog.dart | 2 +- .../wallet_view/sub_widgets/desktop_send.dart | 38 +- .../sub_widgets/desktop_token_send.dart | 8 +- lib/wallets/crypto_currency/coins/banano.dart | 8 + .../crypto_currency/coins/bitcoin_frost.dart | 17 + .../crypto_currency/coins/cardano.dart | 8 + .../crypto_currency/coins/epiccash.dart | 8 + .../crypto_currency/coins/ethereum.dart | 8 + lib/wallets/crypto_currency/coins/nano.dart | 8 + lib/wallets/crypto_currency/coins/solana.dart | 8 + .../crypto_currency/coins/stellar.dart | 8 + lib/wallets/crypto_currency/coins/tezos.dart | 8 + lib/wallets/crypto_currency/coins/xelis.dart | 8 + .../crypto_currency/crypto_currency.dart | 1 + .../electrumx_currency_interface.dart | 20 + .../intermediate/cryptonote_currency.dart | 8 + .../intermediate/nano_currency.dart | 12 +- lib/wallets/models/tx_data.dart | 37 +- lib/wallets/models/tx_recipient.dart | 40 +- .../wallet/impl/bitcoin_frost_wallet.dart | 2 +- lib/wallets/wallet/impl/cardano_wallet.dart | 4 +- lib/wallets/wallet/impl/epiccash_wallet.dart | 6 +- lib/wallets/wallet/impl/litecoin_wallet.dart | 49 ++- lib/wallets/wallet/impl/namecoin_wallet.dart | 63 +-- lib/wallets/wallet/impl/particl_wallet.dart | 32 +- lib/wallets/wallet/impl/tezos_wallet.dart | 8 +- lib/wallets/wallet/impl/wownero_wallet.dart | 2 + lib/wallets/wallet/impl/xelis_wallet.dart | 1 + .../intermediate/lib_monero_wallet.dart | 2 +- .../intermediate/lib_salvium_wallet.dart | 2 +- .../bcash_interface.dart | 28 +- .../electrumx_interface.dart | 393 +++++++++--------- .../mweb_interface.dart | 190 +++++++-- .../paynym_interface.dart | 60 +-- .../rbf_interface.dart | 30 +- .../spark_interface.dart | 11 +- 46 files changed, 765 insertions(+), 463 deletions(-) rename lib/models/{signing_data.dart => input.dart} (100%) diff --git a/lib/models/signing_data.dart b/lib/models/input.dart similarity index 100% rename from lib/models/signing_data.dart rename to lib/models/input.dart diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index 11bc9fe82..d244ded98 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -230,12 +230,17 @@ class _Step4ViewState extends ConsumerState { Future txDataFuture; + final recipient = TxRecipient( + address: address, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(address)!, + ); + if (wallet is FiroWallet && !firoPublicSend) { txDataFuture = wallet.prepareSendSpark( txData: TxData( - recipients: [ - TxRecipient(address: address, amount: amount, isChange: false), - ], + recipients: [recipient], note: "${model.trade!.payInCurrency.toUpperCase()}/" "${model.trade!.payOutCurrency.toUpperCase()} exchange", @@ -250,9 +255,7 @@ class _Step4ViewState extends ConsumerState { : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [ - TxRecipient(address: address, amount: amount, isChange: false), - ], + recipients: [recipient], memo: memo, feeRateType: FeeRateType.average, note: diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index dd98dce23..fa0283ff4 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -283,6 +283,13 @@ class _SendFromCardState extends ConsumerState { TxData txData; Future txDataFuture; + final recipient = TxRecipient( + address: address, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(address)!, + ); + // if not firo then do normal send if (shouldSendPublicFiroFunds == null) { final memo = @@ -293,9 +300,7 @@ class _SendFromCardState extends ConsumerState { : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [ - TxRecipient(address: address, amount: amount, isChange: false), - ], + recipients: [recipient], memo: memo, feeRateType: FeeRateType.average, ), @@ -306,18 +311,14 @@ class _SendFromCardState extends ConsumerState { if (shouldSendPublicFiroFunds) { txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [ - TxRecipient(address: address, amount: amount, isChange: false), - ], + recipients: [recipient], feeRateType: FeeRateType.average, ), ); } else { txDataFuture = firoWallet.prepareSendSpark( txData: TxData( - recipients: [ - TxRecipient(address: address, amount: amount, isChange: false), - ], + recipients: [recipient], // feeRateType: FeeRateType.average, ), ); diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart index 6b2695ef5..2943c6ec1 100644 --- a/lib/pages/namecoin_names/buy_domain_view.dart +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -110,6 +110,7 @@ class _BuyDomainWidgetState extends ConsumerState { rawValue: BigInt.from(kNameNewAmountSats), fractionDigits: wallet.cryptoCurrency.fractionDigits, ), + addressType: myAddress.type, ), ], ); diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart index db869b5f3..89be6129c 100644 --- a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -140,6 +140,7 @@ class _TransferOptionWidgetState extends ConsumerState { rawValue: BigInt.from(kNameAmountSats), fractionDigits: wallet.cryptoCurrency.fractionDigits, ), + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, ), ], note: "Transfer ${opName.constructedName}", diff --git a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart index ac9f665d6..4fd78fb70 100644 --- a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart @@ -154,6 +154,7 @@ class _BuyDomainWidgetState extends ConsumerState { rawValue: BigInt.from(kNameAmountSats), fractionDigits: wallet.cryptoCurrency.fractionDigits, ), + addressType: _address.type, ), ], note: "Update ${opName.constructedName} (${opName.fullname})", diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 70f9f964b..4b1014158 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -88,6 +88,7 @@ class _FrostSendViewState extends ConsumerState { address: e!.address, amount: e.amount!, isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(e.address)!, ), ) .toList(growable: false); diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart index 21d85fe9c..e5f6581a1 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart @@ -3,8 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import '../../../../frost_route_generator.dart'; +import '../../../../models/input.dart'; import '../../../../models/isar/models/isar_models.dart'; -import '../../../../models/signing_data.dart'; import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/frost_wallet/frost_wallet_providers.dart'; import '../../../../providers/global/wallets_provider.dart'; @@ -97,6 +97,8 @@ class _FrostSendStep1bState extends ConsumerState { address: e.address, amount: e.amount, isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType(e.address)!, ), ) .toList(), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 63df318e8..8c3af28e7 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -19,10 +19,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:tuple/tuple.dart'; +import '../../models/input.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/paynym/paynym_account_lite.dart'; import '../../models/send_view_auto_fill_data.dart'; -import '../../models/signing_data.dart'; import '../../providers/providers.dart'; import '../../providers/ui/fee_rate_type_state_provider.dart'; import '../../providers/ui/preview_tx_button_state_provider.dart'; @@ -720,6 +720,7 @@ class _SendViewState extends ConsumerState { address: widget.accountLite!.code, amount: amount, isChange: false, + addressType: AddressType.unknown, ), ], satsPerVByte: isCustomFee.value ? customFeeRate : null, @@ -762,6 +763,8 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType(_address!)!, ), ], feeRateType: ref.read(feeRateTypeMobileStateProvider), @@ -786,6 +789,10 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType( + _address!, + )!, ), ], sparkRecipients: @@ -805,20 +812,26 @@ class _SendViewState extends ConsumerState { } } else if (wallet is MwebInterface && ref.read(publicPrivateBalanceStateProvider) == BalanceType.private) { - txDataFuture = wallet.prepareSend( + txDataFuture = wallet.prepareSendMweb( txData: TxData( - isMweb: true, recipients: [ - TxRecipient(address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, + ), ], feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee.value ? customFeeRate : null, - utxos: - (wallet is CoinControlInterface && - coinControlEnabled && - selectedUTXOs.isNotEmpty) - ? selectedUTXOs - : null, + + // these will need to be mweb utxos + // utxos: + // (wallet is CoinControlInterface && + // coinControlEnabled && + // ref.read(pDesktopUseUTXOs).isNotEmpty) + // ? ref.read(pDesktopUseUTXOs) + // : null, ), ); } else { @@ -826,7 +839,12 @@ class _SendViewState extends ConsumerState { txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - TxRecipient(address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, + ), ], memo: memo, feeRateType: ref.read(feeRateTypeMobileStateProvider), diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 70f076c01..fdf78136e 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -492,7 +492,13 @@ class _TokenSendViewState extends ConsumerState { txDataFuture = tokenWallet.prepareSend( txData: TxData( recipients: [ - TxRecipient(address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: + tokenWallet.cryptoCurrency.getAddressType(_address!)!, + ), ], feeRateType: ref.read(feeRateTypeMobileStateProvider), note: noteController.text, diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart index 4f8e8576f..a5e0ba924 100644 --- a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart @@ -16,8 +16,8 @@ import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; import '../../db/isar/main_db.dart'; +import '../../models/input.dart'; import '../../models/isar/models/blockchain_data/utxo.dart'; -import '../../models/signing_data.dart'; import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index b5cb4e7c9..a2cf27fb5 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -17,6 +17,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../../../../models/isar/models/blockchain_data/address.dart'; import '../../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../../models/isar/models/contact_entry.dart'; import '../../../../models/paynym/paynym_account_lite.dart'; @@ -315,6 +316,7 @@ class _DesktopSendState extends ConsumerState { address: widget.accountLite!.code, amount: amount, isChange: false, + addressType: AddressType.unknown, ), ], satsPerVByte: isCustomFee ? customFeeRate : null, @@ -358,6 +360,8 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType(_address!)!, ), ], feeRateType: ref.read(feeRateTypeDesktopStateProvider), @@ -383,6 +387,10 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType( + _address!, + )!, ), ], sparkRecipients: @@ -402,20 +410,25 @@ class _DesktopSendState extends ConsumerState { } } else if (wallet is MwebInterface && ref.read(publicPrivateBalanceStateProvider) == BalanceType.private) { - txDataFuture = wallet.prepareSend( + txDataFuture = wallet.prepareSendMweb( txData: TxData( - isMweb: true, recipients: [ - TxRecipient(address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, + ), ], feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: - (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(pDesktopUseUTXOs).isNotEmpty) - ? ref.read(pDesktopUseUTXOs) - : null, + // these will need to be mweb utxos + // utxos: + // (wallet is CoinControlInterface && + // coinControlEnabled && + // ref.read(pDesktopUseUTXOs).isNotEmpty) + // ? ref.read(pDesktopUseUTXOs) + // : null, ), ); } else { @@ -423,7 +436,12 @@ class _DesktopSendState extends ConsumerState { txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - TxRecipient(address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, + ), ], memo: memo, feeRateType: ref.read(feeRateTypeDesktopStateProvider), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index 581b5a824..bf57331ea 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -233,7 +233,13 @@ class _DesktopTokenSendState extends ConsumerState { txDataFuture = tokenWallet.prepareSend( txData: TxData( recipients: [ - TxRecipient(address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: + tokenWallet.cryptoCurrency.getAddressType(_address!)!, + ), ], feeRateType: ref.read(feeRateTypeDesktopStateProvider), nonce: int.tryParse(nonceController.text), diff --git a/lib/wallets/crypto_currency/coins/banano.dart b/lib/wallets/crypto_currency/coins/banano.dart index 2a62acb57..eccb6fe38 100644 --- a/lib/wallets/crypto_currency/coins/banano.dart +++ b/lib/wallets/crypto_currency/coins/banano.dart @@ -102,4 +102,12 @@ class Banano extends NanoCurrency { throw UnsupportedError( "$runtimeType does not use bitcoin style derivation paths", ); + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.banano; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart index 330d3cb86..3edaea351 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import '../../../models/isar/models/blockchain_data/address.dart'; @@ -247,4 +248,20 @@ class BitcoinFrost extends FrostCurrency { // @override BigInt get defaultFeeRate => BigInt.from(1000); // https://github.com/bitcoin/bitcoin/blob/feab35189bc00bc4cf15e9dcb5cf6b34ff3a1e91/test/functional/mempool_limit.py#L259 + + @override + AddressType? getAddressType(String address) { + try { + final clAddress = cl.Address.fromString(address, networkParams); + + return switch (clAddress) { + cl.P2PKHAddress() => AddressType.p2pkh, + cl.P2WSHAddress() => AddressType.p2sh, + cl.P2WPKHAddress() => AddressType.p2wpkh, + _ => null, + }; + } catch (_) { + return null; + } + } } diff --git a/lib/wallets/crypto_currency/coins/cardano.dart b/lib/wallets/crypto_currency/coins/cardano.dart index 976093a03..a59669eb7 100644 --- a/lib/wallets/crypto_currency/coins/cardano.dart +++ b/lib/wallets/crypto_currency/coins/cardano.dart @@ -128,4 +128,12 @@ class Cardano extends Bip39Currency { throw Exception("Unsupported network: $network"); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.cardanoShelley; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/epiccash.dart b/lib/wallets/crypto_currency/coins/epiccash.dart index d35c0fb74..b715f9623 100644 --- a/lib/wallets/crypto_currency/coins/epiccash.dart +++ b/lib/wallets/crypto_currency/coins/epiccash.dart @@ -129,4 +129,12 @@ class Epiccash extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.mimbleWimble; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/ethereum.dart b/lib/wallets/crypto_currency/coins/ethereum.dart index f6f3601a1..448f4c9ff 100644 --- a/lib/wallets/crypto_currency/coins/ethereum.dart +++ b/lib/wallets/crypto_currency/coins/ethereum.dart @@ -113,4 +113,12 @@ class Ethereum extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.ethereum; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/nano.dart b/lib/wallets/crypto_currency/coins/nano.dart index 07f464926..04622c54d 100644 --- a/lib/wallets/crypto_currency/coins/nano.dart +++ b/lib/wallets/crypto_currency/coins/nano.dart @@ -102,4 +102,12 @@ class Nano extends NanoCurrency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.nano; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/solana.dart b/lib/wallets/crypto_currency/coins/solana.dart index 39d243a1c..03331a922 100644 --- a/lib/wallets/crypto_currency/coins/solana.dart +++ b/lib/wallets/crypto_currency/coins/solana.dart @@ -122,4 +122,12 @@ class Solana extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.solana; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/stellar.dart b/lib/wallets/crypto_currency/coins/stellar.dart index 077564a50..1aa701f87 100644 --- a/lib/wallets/crypto_currency/coins/stellar.dart +++ b/lib/wallets/crypto_currency/coins/stellar.dart @@ -139,4 +139,12 @@ class Stellar extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.stellar; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/tezos.dart b/lib/wallets/crypto_currency/coins/tezos.dart index 78d3c02ad..179ae2ce1 100644 --- a/lib/wallets/crypto_currency/coins/tezos.dart +++ b/lib/wallets/crypto_currency/coins/tezos.dart @@ -220,4 +220,12 @@ class Tezos extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.tezos; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/xelis.dart b/lib/wallets/crypto_currency/coins/xelis.dart index fc1eaaa20..62b33326b 100644 --- a/lib/wallets/crypto_currency/coins/xelis.dart +++ b/lib/wallets/crypto_currency/coins/xelis.dart @@ -141,4 +141,12 @@ class Xelis extends ElectrumCurrency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.xelis; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 4f6c232e6..0c2f2cb83 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -69,6 +69,7 @@ abstract class CryptoCurrency { String get genesisHash; bool validateAddress(String address); + AddressType? getAddressType(String address); NodeModel defaultNode({required bool isPrimary}); diff --git a/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart b/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart index 387bf4454..c9d0da2b9 100644 --- a/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart +++ b/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart @@ -1,3 +1,6 @@ +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; + +import '../../../models/isar/models/blockchain_data/address.dart'; import '../intermediate/bip39_hd_currency.dart'; mixin ElectrumXCurrencyInterface on Bip39HDCurrency { @@ -5,4 +8,21 @@ mixin ElectrumXCurrencyInterface on Bip39HDCurrency { /// The default fee rate in satoshis per kilobyte. BigInt get defaultFeeRate; + + @override + AddressType? getAddressType(String address) { + try { + final clAddress = cl.Address.fromString(address, networkParams); + + return switch (clAddress) { + cl.P2PKHAddress() => AddressType.p2pkh, + cl.P2WSHAddress() => AddressType.p2sh, + cl.P2WPKHAddress() => AddressType.p2wpkh, + cl.MwebAddress() => AddressType.mweb, + _ => null, + }; + } catch (_) { + return null; + } + } } diff --git a/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart b/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart index 319a501ac..a0069e09b 100644 --- a/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart @@ -13,4 +13,12 @@ abstract class CryptonoteCurrency extends CryptoCurrency @override AddressType get defaultAddressType => AddressType.cryptonote; + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.cryptonote; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/intermediate/nano_currency.dart b/lib/wallets/crypto_currency/intermediate/nano_currency.dart index a04cd57a0..a553a6d6c 100644 --- a/lib/wallets/crypto_currency/intermediate/nano_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/nano_currency.dart @@ -1,4 +1,5 @@ import 'package:nanodart/nanodart.dart'; + import 'bip39_currency.dart'; abstract class NanoCurrency extends Bip39Currency { @@ -24,13 +25,10 @@ abstract class NanoCurrency extends Bip39Currency { List get possibleMnemonicLengths => [defaultSeedPhraseLength, 12]; @override - bool validateAddress(String address) => NanoAccounts.isValid( - nanoAccountType, - address, - ); + bool validateAddress(String address) => + NanoAccounts.isValid(nanoAccountType, address); @override - String get genesisHash => throw UnimplementedError( - "Not used in nano based coins", - ); + String get genesisHash => + throw UnimplementedError("Not used in nano based coins"); } diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index bb2eb2cd1..9a8dffd21 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -3,11 +3,10 @@ import 'package:cs_salvium/cs_salvium.dart' as lib_salvium; import 'package:tezart/tezart.dart' as tezart; import 'package:web3dart/web3dart.dart' as web3dart; -import '../../db/drift/database.dart'; +import '../../models/input.dart'; import '../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/paynym/paynym_account_lite.dart'; -import '../../models/signing_data.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; import '../../widgets/eth_fee_form.dart'; @@ -17,6 +16,20 @@ import 'tx_recipient.dart'; export 'tx_recipient.dart'; +enum TxType { + regular, + mweb, + mwebPegIn, + mwebPegOut; + + bool isMweb() => switch (this) { + TxType.mweb => true, + TxType.mwebPegIn => true, + TxType.mwebPegOut => true, + _ => false, + }; +} + class TxData { final FeeRateType? feeRateType; final BigInt? feeRateAmount; @@ -37,7 +50,7 @@ class TxData { final List? recipients; final Set? utxos; - final List? usedUTXOs; + final List? usedUTXOs; final String? changeAddress; @@ -85,9 +98,7 @@ class TxData { // Namecoin Name related final NameOpState? opNameState; - // MWEB - final bool isMweb; - final List? usedMwebUtxos; + final TxType type; TxData({ this.feeRateType, @@ -123,8 +134,7 @@ class TxData { this.ignoreCachedBalanceChecks = false, this.opNameState, this.sparkNameInfo, - this.isMweb = false, - this.usedMwebUtxos, + this.type = TxType.regular, }); Amount? get amount { @@ -239,7 +249,7 @@ class TxData { String? memo, String? otherData, Set? utxos, - List? usedUTXOs, + List? usedUTXOs, List? recipients, String? frostMSConfig, List? frostSigners, @@ -272,8 +282,7 @@ class TxData { int validBlocks, })? sparkNameInfo, - bool? isMweb, - List? usedMwebUtxos, + TxType? type, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -311,8 +320,7 @@ class TxData { ignoreCachedBalanceChecks ?? this.ignoreCachedBalanceChecks, opNameState: opNameState ?? this.opNameState, sparkNameInfo: sparkNameInfo ?? this.sparkNameInfo, - isMweb: isMweb ?? this.isMweb, - usedMwebUtxos: usedMwebUtxos ?? this.usedMwebUtxos, + type: type ?? this.type, ); } @@ -351,7 +359,6 @@ class TxData { 'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, ' 'opNameState: $opNameState, ' 'sparkNameInfo: $sparkNameInfo, ' - 'isMweb: $isMweb, ' - 'usedMwebUtxos: $usedMwebUtxos, ' + 'type: $type, ' '}'; } diff --git a/lib/wallets/models/tx_recipient.dart b/lib/wallets/models/tx_recipient.dart index 8dd0c3011..9173864d3 100644 --- a/lib/wallets/models/tx_recipient.dart +++ b/lib/wallets/models/tx_recipient.dart @@ -5,12 +5,48 @@ class TxRecipient { final String address; final Amount amount; final bool isChange; - final AddressType? addressType; + final AddressType addressType; TxRecipient({ required this.address, required this.amount, required this.isChange, - this.addressType, + required this.addressType, }); + + TxRecipient copyWith({ + String? address, + Amount? amount, + bool? isChange, + AddressType? addressType, + }) { + return TxRecipient( + address: address ?? this.address, + amount: amount ?? this.amount, + isChange: isChange ?? this.isChange, + addressType: addressType ?? this.addressType, + ); + } + + @override + String toString() { + return "TxRecipient{" + "address: $address, " + "amount: $amount, " + "isChange: $isChange, " + "addressType: $addressType" + "}"; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TxRecipient && + address == other.address && + amount == other.amount && + isChange == other.isChange && + addressType == other.addressType; + + @override + int get hashCode => Object.hash(address, amount, isChange, addressType); } diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 33b6d3a0d..95f015c01 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -11,6 +11,7 @@ import 'package:isar/isar.dart'; import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/electrumx_client.dart'; import '../../../models/balance.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/utxo.dart'; @@ -18,7 +19,6 @@ 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/paymint/fee_object_model.dart'; -import '../../../models/signing_data.dart'; import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import '../../../services/event_bus/global_event_bus.dart'; import '../../../services/frost.dart'; diff --git a/lib/wallets/wallet/impl/cardano_wallet.dart b/lib/wallets/wallet/impl/cardano_wallet.dart index 5f4fdc37c..3909c5b4b 100644 --- a/lib/wallets/wallet/impl/cardano_wallet.dart +++ b/lib/wallets/wallet/impl/cardano_wallet.dart @@ -235,13 +235,11 @@ class CardanoWallet extends Bip39Wallet { // Check if we are sending all balance, which means no change and only one output for recipient. if (totalBalance == txData.amount!.raw) { final List newRecipients = [ - TxRecipient( - address: txData.recipients!.first.address, + txData.recipients!.first.copyWith( amount: Amount( rawValue: txData.amount!.raw - fee, fractionDigits: cryptoCurrency.fractionDigits, ), - isChange: txData.recipients!.first.isChange, ), ]; return txData.copyWith( diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 9032a0c82..ea6654b12 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -646,11 +646,7 @@ class EpiccashWallet extends Bip39Wallet { } if (info.cachedBalance.spendable == recipient.amount) { - recipient = TxRecipient( - address: recipient.address, - amount: recipient.amount - feeAmount, - isChange: recipient.isChange, - ); + recipient = recipient.copyWith(amount: recipient.amount - feeAmount); } return txData.copyWith(recipients: [recipient], fee: feeAmount); diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart index c8b2673f6..82769ee29 100644 --- a/lib/wallets/wallet/impl/litecoin_wallet.dart +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -207,30 +207,35 @@ class LitecoinWallet // Parse outputs. final List outputs = []; for (final outputJson in txData["vout"] as List) { - OutputV2 output = OutputV2.fromElectrumXJson( - Map.from(outputJson as Map), - decimalPlaces: cryptoCurrency.fractionDigits, - isFullAmountNotSats: true, - // Need addresses before we can know if the wallet owns this input. - walletOwns: false, - ); + try { + OutputV2 output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); - // If output was to my wallet, add value to amount received. - if (receivingAddresses - .intersection(output.addresses.toSet()) - .isNotEmpty) { - wasReceivedInThisWallet = true; - amountReceivedInThisWallet += output.value; - output = output.copyWith(walletOwns: true); - } else if (changeAddresses - .intersection(output.addresses.toSet()) - .isNotEmpty) { - wasReceivedInThisWallet = true; - changeAmountReceivedInThisWallet += output.value; - output = output.copyWith(walletOwns: true); - } + // If output was to my wallet, add value to amount received. + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } - outputs.add(output); + outputs.add(output); + } catch (e, s) { + // TODO: mweb output parsing + Logging.instance.e("TODO", error: e, stackTrace: s); + } } final totalOut = outputs diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 5cee5ce22..abf598c6f 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -5,11 +5,11 @@ import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:isar/isar.dart'; import 'package:namecoin/namecoin.dart'; +import '../../../models/input.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'; @@ -578,6 +578,8 @@ class NamecoinWallet noteName += ".bit"; } + final receivingAddress = (await getCurrentReceivingAddress())!; + TxData txData = TxData( utxos: {StandardInput(utxo)}, opNameState: NameOpState( @@ -594,12 +596,13 @@ class NamecoinWallet feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ TxRecipient( - address: (await getCurrentReceivingAddress())!.value, + address: receivingAddress.value, isChange: false, amount: Amount( rawValue: BigInt.from(kNameAmountSats), fractionDigits: cryptoCurrency.fractionDigits, ), + addressType: receivingAddress.type, ), ], ); @@ -627,7 +630,7 @@ class NamecoinWallet /// Builds and signs a transaction Future _createNameTx({ required TxData txData, - required List utxoSigningData, + required List inputsWithKeys, required bool isForFeeCalcPurposesOnly, }) async { Logging.instance.d("Starting _createNameTx ----------"); @@ -667,19 +670,19 @@ class NamecoinWallet : 0xffffffff - 1; // Add transaction inputs - for (int i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (int i = 0; i < inputsWithKeys.length; i++) { + final txid = inputsWithKeys[i].utxo.txid; final hash = Uint8List.fromList( txid.toUint8ListFromHex.reversed.toList(), ); - final prevOutpoint = coinlib.OutPoint(hash, utxoSigningData[i].utxo.vout); + final prevOutpoint = coinlib.OutPoint(hash, inputsWithKeys[i].utxo.vout); final prevOutput = coinlib.Output.fromAddress( - BigInt.from(utxoSigningData[i].utxo.value), + BigInt.from(inputsWithKeys[i].utxo.value), coinlib.Address.fromString( - utxoSigningData[i].utxo.address!, + inputsWithKeys[i].utxo.address!, cryptoCurrency.networkParams, ), ); @@ -688,11 +691,11 @@ class NamecoinWallet final coinlib.Input input; - switch (utxoSigningData[i].derivePathType) { + switch (inputsWithKeys[i].derivePathType) { case DerivePathType.bip44: input = coinlib.P2PKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].key!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: sequence, ); @@ -710,7 +713,7 @@ class NamecoinWallet case DerivePathType.bip84: input = coinlib.P2WPKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].key!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: sequence, ); @@ -719,7 +722,7 @@ class NamecoinWallet default: throw UnsupportedError( - "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + "Unknown derivation path type found: ${inputsWithKeys[i].derivePathType}", ); } @@ -731,14 +734,14 @@ class NamecoinWallet scriptSigAsm: null, sequence: sequence, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + txid: inputsWithKeys[i].utxo.txid, + vout: inputsWithKeys[i].utxo.vout, ), addresses: - utxoSigningData[i].utxo.address == null + inputsWithKeys[i].utxo.address == null ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), + : [inputsWithKeys[i].utxo.address!], + valueStringSats: inputsWithKeys[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -808,13 +811,13 @@ class NamecoinWallet try { // Sign the transaction accordingly - for (int i = 0; i < utxoSigningData.length; i++) { - final value = BigInt.from(utxoSigningData[i].utxo.value); - final key = utxoSigningData[i].key!.privateKey!; + for (int i = 0; i < inputsWithKeys.length; i++) { + final value = BigInt.from(inputsWithKeys[i].utxo.value); + final key = inputsWithKeys[i].key!.privateKey!; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( - internalKey: utxoSigningData[i].key!.publicKey, + internalKey: inputsWithKeys[i].key!.publicKey, ); clTx = clTx.signTaproot( @@ -1115,8 +1118,8 @@ class NamecoinWallet final List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data - final utxoSigningData = - (await fetchBuildTxData( + final inputsWithKeys = + (await addSigningKeys( utxoObjectsToUse.map((e) => StandardInput(e)).toList(), )).whereType().toList(); @@ -1124,7 +1127,7 @@ class NamecoinWallet try { vSizeForOneOutput = (await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: true, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1145,7 +1148,7 @@ class NamecoinWallet try { vSizeForTwoOutPuts = (await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: true, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1197,7 +1200,7 @@ class NamecoinWallet ); final txnData = await _createNameTx( isForFeeCalcPurposesOnly: false, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, @@ -1210,7 +1213,7 @@ class NamecoinWallet rawValue: feeForOneOutput, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } @@ -1259,7 +1262,7 @@ class NamecoinWallet ); TxData txnData = await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: false, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1285,7 +1288,7 @@ class NamecoinWallet ); txnData = await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: false, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1301,7 +1304,7 @@ class NamecoinWallet rawValue: feeBeingPaid, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } else { // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index 19a9a9e20..08e27cc18 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -3,12 +3,12 @@ import 'dart:typed_data'; import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; import 'package:isar/isar.dart'; +import '../../../models/input.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/signing_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/extensions/impl/uint8_list.dart'; @@ -352,7 +352,7 @@ class ParticlWallet @override Future buildTransaction({ required TxData txData, - required covariant List utxoSigningData, + required covariant List inputsWithKeys, }) async { Logging.instance.d("Starting Particl buildTransaction ----------"); @@ -371,8 +371,8 @@ class ParticlWallet ); final List<({Uint8List? output, Uint8List? redeem})> extraData = []; - for (int i = 0; i < utxoSigningData.length; i++) { - final sd = utxoSigningData[i]; + for (int i = 0; i < inputsWithKeys.length; i++) { + final sd = inputsWithKeys[i]; final pubKey = sd.key!.publicKey.data; final bitcoindart.PaymentData? data; @@ -448,11 +448,11 @@ class ParticlWallet final List tempOutputs = []; // Add inputs. - for (var i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (var i = 0; i < inputsWithKeys.length; i++) { + final txid = inputsWithKeys[i].utxo.txid; txb.addInput( txid, - utxoSigningData[i].utxo.vout, + inputsWithKeys[i].utxo.vout, null, extraData[i].output!, cryptoCurrency.networkParams.bech32Hrp, @@ -464,14 +464,14 @@ class ParticlWallet scriptSigAsm: null, sequence: 0xffffffff - 1, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + txid: inputsWithKeys[i].utxo.txid, + vout: inputsWithKeys[i].utxo.vout, ), addresses: - utxoSigningData[i].utxo.address == null + inputsWithKeys[i].utxo.address == null ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), + : [inputsWithKeys[i].utxo.address!], + valueStringSats: inputsWithKeys[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -508,15 +508,15 @@ class ParticlWallet // Sign. try { - for (var i = 0; i < utxoSigningData.length; i++) { + for (var i = 0; i < inputsWithKeys.length; i++) { txb.sign( vin: i, keyPair: bitcoindart.ECPair.fromPrivateKey( - utxoSigningData[i].key!.privateKey!.data, + inputsWithKeys[i].key!.privateKey!.data, network: convertedNetwork, - compressed: utxoSigningData[i].key!.privateKey!.compressed, + compressed: inputsWithKeys[i].key!.privateKey!.compressed, ), - witnessValue: utxoSigningData[i].utxo.value, + witnessValue: inputsWithKeys[i].utxo.value, redeemScript: extraData[i].redeem, overridePrefix: cryptoCurrency.networkParams.bech32Hrp, ); diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart index d5028093b..2843d4a1a 100644 --- a/lib/wallets/wallet/impl/tezos_wallet.dart +++ b/lib/wallets/wallet/impl/tezos_wallet.dart @@ -251,13 +251,7 @@ class TezosWallet extends Bip39Wallet { await opList.simulate(); return txData.copyWith( - recipients: [ - TxRecipient( - amount: sendAmount, - address: txData.recipients!.first.address, - isChange: txData.recipients!.first.isChange, - ), - ], + recipients: [txData.recipients!.first.copyWith(amount: sendAmount)], // fee: fee, fee: Amount( rawValue: opList.operations diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index b632943d0..a8d5e1c65 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:compat/compat.dart' as lib_monero_compat; import 'package:cs_monero/cs_monero.dart' as lib_monero; +import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/fee_rate_type_enum.dart'; import '../../crypto_currency/crypto_currency.dart'; @@ -59,6 +60,7 @@ class WowneroWallet extends LibMoneroWallet { "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", amount: amount, isChange: false, + addressType: AddressType.cryptonote, ), ], feeRateType: feeRateType, diff --git a/lib/wallets/wallet/impl/xelis_wallet.dart b/lib/wallets/wallet/impl/xelis_wallet.dart index cc11b48f2..eea19dbdb 100644 --- a/lib/wallets/wallet/impl/xelis_wallet.dart +++ b/lib/wallets/wallet/impl/xelis_wallet.dart @@ -730,6 +730,7 @@ class XelisWallet extends LibXelisWallet { 'xel:xz9574c80c4xegnvurazpmxhw5dlg2n0g9qm60uwgt75uqyx3pcsqzzra9m', amount: amount, isChange: false, + addressType: AddressType.xelis, ), ]; diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 763162fd0..d29e97976 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -11,6 +11,7 @@ import 'package:stack_wallet_backup/generate_password.dart'; import '../../../db/hive/db.dart'; import '../../../models/balance.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/utxo.dart'; @@ -21,7 +22,6 @@ import '../../../models/keys/cw_key_data.dart'; import '../../../models/keys/view_only_wallet_data.dart'; import '../../../models/node_model.dart'; import '../../../models/paymint/fee_object_model.dart'; -import '../../../models/signing_data.dart'; import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index 2c47a843d..f18cba8d0 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -9,6 +9,7 @@ import 'package:mutex/mutex.dart'; import 'package:stack_wallet_backup/generate_password.dart'; import '../../../models/balance.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/utxo.dart'; @@ -19,7 +20,6 @@ import '../../../models/keys/cw_key_data.dart'; import '../../../models/keys/view_only_wallet_data.dart'; import '../../../models/node_model.dart'; import '../../../models/paymint/fee_object_model.dart'; -import '../../../models/signing_data.dart'; import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart index 492f727b0..410f11d2e 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart @@ -2,11 +2,11 @@ import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitbox/src/utils/network.dart' as bitbox_utils; import 'package:isar/isar.dart'; +import '../../../models/input.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/logger.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../../models/tx_data.dart'; @@ -18,7 +18,7 @@ mixin BCashInterface @override Future buildTransaction({ required TxData txData, - required covariant List utxoSigningData, + required covariant List inputsWithKeys, }) async { Logging.instance.d("Starting buildTransaction ----------"); @@ -35,10 +35,10 @@ mixin BCashInterface final List tempOutputs = []; // Add transaction inputs - for (int i = 0; i < utxoSigningData.length; i++) { + for (int i = 0; i < inputsWithKeys.length; i++) { builder.addInput( - utxoSigningData[i].utxo.txid, - utxoSigningData[i].utxo.vout, + inputsWithKeys[i].utxo.txid, + inputsWithKeys[i].utxo.vout, ); tempInputs.add( @@ -47,14 +47,14 @@ mixin BCashInterface scriptSigAsm: null, sequence: 0xffffffff - 1, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + txid: inputsWithKeys[i].utxo.txid, + vout: inputsWithKeys[i].utxo.vout, ), addresses: - utxoSigningData[i].utxo.address == null + inputsWithKeys[i].utxo.address == null ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), + : [inputsWithKeys[i].utxo.address!], + valueStringSats: inputsWithKeys[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -92,9 +92,9 @@ mixin BCashInterface try { // Sign the transaction accordingly - for (int i = 0; i < utxoSigningData.length; i++) { + for (int i = 0; i < inputsWithKeys.length; i++) { final bitboxEC = bitbox.ECPair.fromPrivateKey( - utxoSigningData[i].key!.privateKey!.data, + inputsWithKeys[i].key!.privateKey!.data, network: bitbox_utils.Network( cryptoCurrency.networkParams.privHDPrefix, cryptoCurrency.networkParams.pubHDPrefix, @@ -103,10 +103,10 @@ mixin BCashInterface cryptoCurrency.networkParams.wifPrefix, cryptoCurrency.networkParams.p2pkhPrefix, ), - compressed: utxoSigningData[i].key!.privateKey!.compressed, + compressed: inputsWithKeys[i].key!.privateKey!.compressed, ); - builder.sign(i, bitboxEC, utxoSigningData[i].utxo.value); + builder.sign(i, bitboxEC, inputsWithKeys[i].utxo.value); } } catch (e, s) { Logging.instance.e( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 96f13af8b..05ad0a58a 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -11,13 +11,13 @@ import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/client_manager.dart'; import '../../../electrumx_rpc/electrumx_client.dart'; import '../../../models/coinlib/exp2pkh_address.dart'; +import '../../../models/input.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/keys/view_only_wallet_data.dart'; import '../../../models/paymint/fee_object_model.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'; @@ -85,17 +85,10 @@ mixin ElectrumXInterface final List results = []; for (int i = 0; i < addrs.length; i++) { - // temp mweb hack - AddressType? type; - if (this is MwebInterface) { - final addr = coinlib.Address.fromString( - addrs[i], - cryptoCurrency.networkParams, - ); - if (addr is coinlib.MwebAddress) { - type = AddressType.mweb; - } - } + // assume address is valid at this point so if getAddressType fails for + // some reason default to unknown + final type = + cryptoCurrency.getAddressType(addrs[i]) ?? AddressType.unknown; results.add( TxRecipient( @@ -130,6 +123,7 @@ mixin ElectrumXInterface required bool isSendAllCoinControlUtxos, int additionalOutputs = 0, List? utxos, + BigInt? overrideFeeAmount, }) async { Logging.instance.d("Starting coinSelection ----------"); @@ -145,20 +139,25 @@ mixin ElectrumXInterface final int? satsPerVByte = txData.satsPerVByte; final selectedTxFeeRate = txData.feeRateAmount!; - final List availableOutputs = - utxos ?? - (await mainDB.getUTXOs(walletId).findAll()) - .map((e) => StandardInput(e)) - .toList(); - if (this is MwebInterface && utxos == null) { - final db = Drift.get(walletId); - final mwebUtxos = - await (db.select(db.mwebUtxos) - ..where((e) => e.used.equals(false))).get(); - - for (final e in mwebUtxos) { - availableOutputs.add(MwebInput(e)); + final List availableOutputs; + + if (txData.type == TxType.mweb || txData.type == TxType.mwebPegOut) { + if (utxos == null) { + final db = Drift.get(walletId); + final mwebUtxos = + await (db.select(db.mwebUtxos) + ..where((e) => e.used.equals(false))).get(); + + availableOutputs = mwebUtxos.map((e) => MwebInput(e)).toList(); + } else { + availableOutputs = utxos; } + } else { + availableOutputs = + utxos ?? + (await mainDB.getUTXOs(walletId).findAll()) + .map((e) => StandardInput(e)) + .toList(); } final currentChainHeight = await chainHeight; @@ -253,10 +252,10 @@ mixin ElectrumXInterface final List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data - final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + final inputsWithKeys = await addSigningKeys(utxoObjectsToUse); if (isSendAll || isSendAllCoinControlUtxos) { - if (satoshiAmountToSend != satoshisBeingUsed && !txData.isMweb) { + if (satoshiAmountToSend != satoshisBeingUsed) { throw Exception( "Something happened that should never actually happen. " "Please report this error to the developers.", @@ -267,9 +266,10 @@ mixin ElectrumXInterface recipientAddress: recipientAddress, satoshiAmountToSend: satoshiAmountToSend, satoshisBeingUsed: satoshisBeingUsed, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, satsPerVByte: satsPerVByte, feeRatePerKB: selectedTxFeeRate, + overrideFeeAmount: overrideFeeAmount, ); } @@ -277,7 +277,7 @@ mixin ElectrumXInterface try { vSizeForOneOutput = (await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( [recipientAddress], @@ -297,7 +297,7 @@ mixin ElectrumXInterface try { vSizeForTwoOutPuts = (await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( [recipientAddress, (await getCurrentChangeAddress())!.value], @@ -317,23 +317,27 @@ mixin ElectrumXInterface } // Assume 1 output, only for recipient and no change - final feeForOneOutput = BigInt.from( - satsPerVByte != null - ? (satsPerVByte * vSizeForOneOutput) - : estimateTxFee( - vSize: vSizeForOneOutput, - feeRatePerKB: selectedTxFeeRate, - ), - ); + final feeForOneOutput = + overrideFeeAmount ?? + 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, - ), - ); + final feeForTwoOutputs = + overrideFeeAmount ?? + BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForTwoOutPuts) + : estimateTxFee( + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ), + ); Logging.instance.d("feeForTwoOutputs: $feeForTwoOutputs"); Logging.instance.d("feeForOneOutput: $feeForOneOutput"); @@ -346,7 +350,7 @@ mixin ElectrumXInterface Logging.instance.d('Fee being paid: $difference sats'); Logging.instance.d('Estimated fee: $feeForOneOutput'); final txnData = await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, @@ -359,11 +363,7 @@ mixin ElectrumXInterface rawValue: feeForOneOutput, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: - utxoSigningData - .whereType() - .map((e) => e.utxo) - .toList(), + usedUTXOs: inputsWithKeys, ); } @@ -412,12 +412,13 @@ mixin ElectrumXInterface Logging.instance.d('Estimated fee: $feeForTwoOutputs'); TxData txnData = await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), + usedUTXOs: inputsWithKeys, ), ); @@ -441,12 +442,13 @@ mixin ElectrumXInterface Logging.instance.d('Adjusted Estimated fee: $feeForTwoOutputs'); txnData = await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), + usedUTXOs: inputsWithKeys, ), ); } @@ -456,11 +458,7 @@ mixin ElectrumXInterface rawValue: feeBeingPaid, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: - utxoSigningData - .whereType() - .map((e) => e.utxo) - .toList(), + usedUTXOs: inputsWithKeys, ); } else { // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize @@ -480,37 +478,46 @@ mixin ElectrumXInterface required String recipientAddress, required BigInt satoshiAmountToSend, required BigInt satoshisBeingUsed, - required List utxoSigningData, + required List inputsWithKeys, required int? satsPerVByte, required BigInt feeRatePerKB, + BigInt? overrideFeeAmount, }) async { Logging.instance.d("Attempting to send all $cryptoCurrency"); if (txData.recipients!.length != 1) { throw Exception("Send all to more than one recipient not yet supported"); } - final int vSizeForOneOutput = - (await buildTransaction( - utxoSigningData: utxoSigningData, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress], - [satoshisBeingUsed - BigInt.one], + BigInt feeForOneOutput; + if (overrideFeeAmount == null) { + final int vSizeForOneOutput = + (await buildTransaction( + inputsWithKeys: inputsWithKeys, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshisBeingUsed - BigInt.one], + ), ), - ), - )).vSize!; - BigInt feeForOneOutput = BigInt.from( - satsPerVByte != null - ? (satsPerVByte * vSizeForOneOutput) - : estimateTxFee(vSize: vSizeForOneOutput, feeRatePerKB: feeRatePerKB), - ); + )).vSize!; + feeForOneOutput = BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForOneOutput) + : estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: feeRatePerKB, + ), + ); - if (satsPerVByte == null) { - final roughEstimate = - roughFeeEstimate(utxoSigningData.length, 1, feeRatePerKB).raw; - if (feeForOneOutput < roughEstimate) { - feeForOneOutput = roughEstimate; + if (satsPerVByte == null) { + final roughEstimate = + roughFeeEstimate(inputsWithKeys.length, 1, feeRatePerKB).raw; + if (feeForOneOutput < roughEstimate) { + feeForOneOutput = roughEstimate; + } } + } else { + feeForOneOutput = overrideFeeAmount; } final amount = satoshiAmountToSend - feeForOneOutput; @@ -525,7 +532,7 @@ mixin ElectrumXInterface txData: txData.copyWith( recipients: await helperRecipientsConvert([recipientAddress], [amount]), ), - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, ); return data.copyWith( @@ -533,30 +540,26 @@ mixin ElectrumXInterface rawValue: feeForOneOutput, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: - utxoSigningData - .whereType() - .map((e) => e.utxo) - .toList(), + usedUTXOs: inputsWithKeys, ); } - Future> fetchBuildTxData(List utxosToUse) async { + Future> addSigningKeys(List utxosToUse) async { // return data - final List signingData = []; + final List inputsWithKeys = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { final input = utxosToUse[i]; if (input is MwebInput) { - signingData.add(input); + inputsWithKeys.add(input); } else if (input is StandardInput) { final derivePathType = cryptoCurrency.addressType( address: input.address!, ); - signingData.add( + inputsWithKeys.add( StandardInput(input.utxo, derivePathType: derivePathType), ); } else { @@ -566,7 +569,7 @@ mixin ElectrumXInterface final root = await getRootHDNode(); - for (final sd in signingData.whereType()) { + for (final sd in inputsWithKeys.whereType()) { coinlib.HDPrivateKey? keys; final address = await mainDB.getAddress(walletId, sd.utxo.address!); if (address?.derivationPath != null) { @@ -608,7 +611,7 @@ mixin ElectrumXInterface sd.key = keys; } - return signingData; + return inputsWithKeys; } catch (e, s) { Logging.instance.e("fetchBuildTxData() threw", error: e, stackTrace: s); rethrow; @@ -618,7 +621,7 @@ mixin ElectrumXInterface /// Builds and signs a transaction Future buildTransaction({ required TxData txData, - required List utxoSigningData, + required List inputsWithKeys, }) async { Logging.instance.d("Starting buildTransaction ----------"); @@ -628,19 +631,8 @@ mixin ElectrumXInterface final List prevOuts = []; - final hasMwebInputs = utxoSigningData.whereType().isNotEmpty; - final hasMwebOutputs = - txData.recipients! - .where((e) => e.addressType == AddressType.mweb) - .isNotEmpty; - - final isMweb = hasMwebOutputs || hasMwebInputs; - - print("IS MEB hasMwebInputs: $hasMwebInputs"); - print("IS MEB hasMwebOutputs: $hasMwebOutputs"); - coinlib.Transaction clTx = coinlib.Transaction( - version: isMweb ? 2 : cryptoCurrency.transactionVersion, + version: txData.type.isMweb() ? 2 : cryptoCurrency.transactionVersion, inputs: [], outputs: [], ); @@ -652,8 +644,8 @@ mixin ElectrumXInterface : 0xffffffff - 1; // Add transaction inputs - for (var i = 0; i < utxoSigningData.length; i++) { - final data = utxoSigningData[i]; + for (var i = 0; i < inputsWithKeys.length; i++) { + final data = inputsWithKeys[i]; if (data is MwebInput) { final address = data.address; @@ -682,7 +674,7 @@ mixin ElectrumXInterface vout: index, ), addresses: [address], - valueStringSats: utxoSigningData[i].value.toString(), + valueStringSats: inputsWithKeys[i].value.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -766,9 +758,7 @@ mixin ElectrumXInterface ), ); } else { - throw Exception( - "Unknown input type: ${utxoSigningData[i].runtimeType}", - ); + throw Exception("Unknown input type: ${inputsWithKeys[i].runtimeType}"); } } @@ -826,8 +816,8 @@ mixin ElectrumXInterface try { // Sign the transaction accordingly - for (var i = 0; i < utxoSigningData.length; i++) { - final data = utxoSigningData[i]; + for (var i = 0; i < inputsWithKeys.length; i++) { + final data = inputsWithKeys[i]; if (data is MwebInput) { // do nothing @@ -859,7 +849,7 @@ mixin ElectrumXInterface } } else { throw Exception( - "Unknown input type: ${utxoSigningData[i].runtimeType}", + "Unknown input type: ${inputsWithKeys[i].runtimeType}", ); } } @@ -873,28 +863,32 @@ mixin ElectrumXInterface } return txData.copyWith( - isMweb: isMweb, raw: clTx.toHex(), // dirty shortcut for peercoin's weirdness vSize: this is PeercoinWallet ? clTx.size : clTx.vSize(), - 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, - ), + tempTx: + txData.type.isMweb() + ? null + : 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, + ), ); } @@ -1775,14 +1769,30 @@ mixin ElectrumXInterface txData = txData.copyWith( usedUTXOs: - txData.usedUTXOs!.map((e) => e.copyWith(used: true)).toList(), + txData.usedUTXOs!.map((e) { + if (e is StandardInput) { + return StandardInput( + e.utxo.copyWith(used: true), + derivePathType: e.derivePathType, + ); + } else if (e is MwebInput) { + return MwebInput(e.utxo.copyWith(used: true)); + } else { + return e; + } + }).toList(), // TODO revisit setting these both txHash: txHash, txid: txHash, ); // mark utxos as used - await mainDB.putUTXOs(txData.usedUTXOs!); + await mainDB.putUTXOs( + txData.usedUTXOs! + .whereType() + .map((e) => e.utxo) + .toList(), + ); return await updateSentCachedTxData(txData: txData); } catch (e, s) { @@ -1802,41 +1812,13 @@ mixin ElectrumXInterface throw Exception("No recipients in attempted transaction!"); } - if (this is MwebInterface) { - bool hasMwebOutputs = false; - final recipients = - txData.recipients!.map((e) { - if (e.addressType == null) { - final addr = coinlib.Address.fromString( - e.address, - cryptoCurrency.networkParams, - ); - if (addr is coinlib.MwebAddress) { - hasMwebOutputs = true; - return TxRecipient( - address: e.address, - amount: e.amount, - isChange: e.isChange, - addressType: AddressType.mweb, - ); - } - } - return e; - }).toList(); - txData = txData.copyWith( - recipients: recipients, - isMweb: txData.isMweb || hasMwebOutputs, - ); - print("IS MEB FLAG AAAA: ${txData.isMweb}"); - } - - print("IS MEB FLAG: ${txData.isMweb}"); - final feeRateType = txData.feeRateType; final customSatsPerVByte = txData.satsPerVByte; final feeRateAmount = txData.feeRateAmount; final utxos = txData.utxos; + bool isSendAll = false; + final bool coinControl = utxos != null; final isSendAllCoinControlUtxos = @@ -1844,9 +1826,11 @@ mixin ElectrumXInterface txData.amount!.raw == utxos.map((e) => e.value).fold(BigInt.zero, (p, e) => p + e); + final TxData result; + if (customSatsPerVByte != null) { // check for send all - bool isSendAll = false; + isSendAll = false; if (txData.ignoreCachedBalanceChecks || txData.amount == info.cachedBalance.spendable) { isSendAll = true; @@ -1860,23 +1844,13 @@ mixin ElectrumXInterface isSendAll = true; } - final result = await coinSelection( + result = await coinSelection( txData: txData.copyWith(feeRateAmount: BigInt.from(-1)), isSendAll: isSendAll, utxos: utxos?.toList(), coinControl: coinControl, isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, ); - - Logging.instance.d("PREPARE 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 BigInt) { late final BigInt rate; if (feeRateType is FeeRateType) { @@ -1901,51 +1875,58 @@ mixin ElectrumXInterface } // check for send all - bool isSendAll = false; + isSendAll = false; if (txData.amount == info.cachedBalance.spendable) { isSendAll = true; } - final result = await coinSelection( + result = await coinSelection( txData: txData.copyWith(feeRateAmount: rate), isSendAll: isSendAll, utxos: utxos?.toList(), coinControl: coinControl, isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, ); + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } - 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})", - ); - } + 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})", + ); + } - // mweb - if (result.isMweb) { - final mwebData = await coinSelection( - txData: result.copyWith( - recipients: - result.recipients! - .where( - (e) => - !(e.isChange && e.addressType == AddressType.mweb), - ) - .toList(), - ), - coinControl: coinControl, - isSendAll: isSendAll, - isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, - ); + // mweb + if (result.type.isMweb()) { + final fee = await (this as MwebInterface).mwebFee(txData: result); - return await (this as MwebInterface).processMwebTransaction(mwebData); - } + TxData mwebData = await coinSelection( + txData: result.copyWith( + recipients: result.recipients!.where((e) => !(e.isChange)).toList(), + ), + coinControl: coinControl, + isSendAll: isSendAll, + isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, + overrideFeeAmount: fee.raw, + ); - return result; - } else { - throw ArgumentError("Invalid fee rate argument provided!"); + if (mwebData.type == TxType.mwebPegIn) { + mwebData = await buildTransaction( + txData: mwebData, + inputsWithKeys: mwebData.usedUTXOs!, + ); + } + final data = await (this as MwebInterface).processMwebTransaction( + mwebData, + ); + return data.copyWith(fee: fee); } + + Logging.instance.d("prepare send: $result"); + + return result; } catch (e, s) { Logging.instance.e( "Exception rethrown from prepareSend(): ", diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart index 61ac5f030..16ccb0eaf 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -10,10 +10,10 @@ import 'package:mweb_client/mweb_client.dart'; import '../../../db/drift/database.dart'; import '../../../models/balance.dart'; +import '../../../models/input.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 '../../../services/event_bus/events/global/blocks_remaining_event.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; @@ -240,7 +240,10 @@ mixin MwebInterface blockHash: null, // ?? hash: "", txid: fakeTxid, - timestamp: utxo.blockTime, + timestamp: + utxo.height < 1 + ? DateTime.now().millisecondsSinceEpoch ~/ 1000 + : utxo.blockTime, height: utxo.height, inputs: [], outputs: [ @@ -353,7 +356,68 @@ mixin MwebInterface ), ); - return txData.copyWith(raw: Uint8List.fromList(response.rawTx).toHex); + if (txData.type == TxType.mwebPegIn) { + cl.Transaction clTx = cl.Transaction.fromBytes( + Uint8List.fromList(response.rawTx), + ); + + assert(response.rawTx.toString() == clTx.toBytes().toList().toString()); + final List prevOuts = []; + + for (int i = 0; i < txData.usedUTXOs!.length; i++) { + final data = txData.usedUTXOs![i]; + if (data is StandardInput) { + final prevOutput = cl.Output.fromAddress( + BigInt.from(data.utxo.value), + cl.Address.fromString( + data.utxo.address!, + cryptoCurrency.networkParams, + ), + ); + + prevOuts.add(prevOutput); + } + } + + for (int i = 0; i < txData.usedUTXOs!.length; i++) { + final data = txData.usedUTXOs![i]; + + if (data is MwebInput) { + // do nothing + } else if (data is StandardInput) { + final value = BigInt.from(data.utxo.value); + final key = data.key!.privateKey!; + if (clTx.inputs[i] is cl.TaprootKeyInput) { + final taproot = cl.Taproot(internalKey: data.key!.publicKey); + + clTx = clTx.signTaproot( + inputN: i, + key: taproot.tweakPrivateKey(key), + prevOuts: prevOuts, + ); + } else if (clTx.inputs[i] is cl.LegacyWitnessInput) { + clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); + } else if (clTx.inputs[i] is cl.LegacyInput) { + clTx = clTx.signLegacy(inputN: i, key: key); + } else if (clTx.inputs[i] is cl.TaprootSingleScriptSigInput) { + clTx = clTx.signTaprootSingleScriptSig( + inputN: i, + key: key, + prevOuts: prevOuts, + ); + } else { + throw Exception( + "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + ); + } + } else { + throw Exception("Unknown input type: ${data.runtimeType}"); + } + } + return txData.copyWith(raw: clTx.toHex()); + } else { + return txData.copyWith(raw: Uint8List.fromList(response.rawTx).toHex); + } } Future _confirmSendMweb({required TxData txData}) async { @@ -375,17 +439,41 @@ mixin MwebInterface final txHash = response.txid; Logging.instance.d("Sent txHash: $txHash"); - txData = txData.copyWith(txHash: txHash, txid: txHash); + txData = txData.copyWith( + usedUTXOs: + txData.usedUTXOs!.map((e) { + if (e is StandardInput) { + return StandardInput( + e.utxo.copyWith(used: true), + derivePathType: e.derivePathType, + ); + } else if (e is MwebInput) { + return MwebInput(e.utxo.copyWith(used: true)); + } else { + return e; + } + }).toList(), + txHash: txHash, + txid: txHash, + ); + + // mark utxos as used + await mainDB.putUTXOs( + txData.usedUTXOs! + .whereType() + .map((e) => e.utxo) + .toList(), + ); // Update used mweb utxos as used in database. They should already have // been marked as isUsed. - if (txData.usedMwebUtxos != null && txData.usedMwebUtxos!.isNotEmpty) { + if (txData.usedUTXOs!.whereType().isNotEmpty) { final db = Drift.get(walletId); await db.transaction(() async { - for (final utxo in txData.usedMwebUtxos!) { + for (final utxo in txData.usedUTXOs!.whereType()) { await db .into(db.mwebUtxos) - .insertOnConflictUpdate(utxo.toCompanion(false)); + .insertOnConflictUpdate(utxo.utxo.toCompanion(false)); } }); } else { @@ -406,6 +494,34 @@ mixin MwebInterface } } + @override + Future prepareSend({required TxData txData}) async { + final hasMwebOutputs = + txData.recipients! + .where((e) => e.addressType == AddressType.mweb) + .isNotEmpty; + if (hasMwebOutputs) { + // assume pegin tx + txData = txData.copyWith(type: TxType.mwebPegIn); + } + + return super.prepareSend(txData: txData); + } + + /// prepare mweb transaction where spending mweb outputs + Future prepareSendMweb({required TxData txData}) async { + final hasMwebOutputs = + txData.recipients! + .where((e) => e.addressType == AddressType.mweb) + .isNotEmpty; + + final type = hasMwebOutputs ? TxType.mweb : TxType.mwebPegOut; + + txData = txData.copyWith(type: type); + + return super.prepareSend(txData: txData); + } + Future anonymizeAllMweb() async { if (!info.isMwebEnabled) { Logging.instance.e( @@ -445,7 +561,7 @@ mixin MwebInterface // TODO finish final txData = await prepareSend( txData: TxData( - isMweb: true, + type: TxType.mwebPegIn, feeRateType: FeeRateType.average, utxos: spendableUtxos.map((e) => StandardInput(e)).toSet(), recipients: [ @@ -460,7 +576,6 @@ mixin MwebInterface fractionDigits: cryptoCurrency.fractionDigits, ), ), - isChange: false, addressType: AddressType.mweb, ), @@ -468,9 +583,7 @@ mixin MwebInterface ), ); - final processed = await processMwebTransaction(txData); - - await _confirmSendMweb(txData: processed); + await _confirmSendMweb(txData: txData); } catch (e, s) { Logging.instance.w( "Exception caught in anonymizeAllMweb(): ", @@ -532,7 +645,7 @@ mixin MwebInterface @override Future confirmSend({required TxData txData}) async { - if (txData.isMweb) { + if (txData.type.isMweb()) { return await _confirmSendMweb(txData: txData); } else { return await super.confirmSend(txData: txData); @@ -816,8 +929,15 @@ mixin MwebInterface } } - // TODO: this is broken Future mwebFee({required TxData txData}) async { + final outputs = txData.recipients!; + final utxos = txData.usedUTXOs!; + + final sumOfUtxosValue = utxos.fold(BigInt.zero, (p, e) => p + e.value); + + final preOutputSum = outputs.fold(BigInt.zero, (p, e) => p + e.amount.raw); + final fee = sumOfUtxosValue - preOutputSum; + final client = await _client; final resp = await client.create( @@ -826,21 +946,19 @@ mixin MwebInterface scanSecret: await _scanSecret, spendSecret: await _spendSecret, feeRatePerKb: Int64(txData.feeRateAmount!.toInt()), - dryRun: false, + dryRun: true, ), ); - final tx = cl.Transaction.fromBytes(Uint8List.fromList(resp.rawTx)); - - final used = [ - ...txData.usedUTXOs?.map((e) => StandardInput(e)) ?? [], - ...txData.usedMwebUtxos?.map((e) => MwebInput(e)) ?? [], - ]; + final processedTx = cl.Transaction.fromBytes( + Uint8List.fromList(resp.rawTx), + ); + BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b; final posUtxos = - used + utxos .where( - (utxo) => tx.inputs.any( + (utxo) => processedTx.inputs.any( (input) => input.prevOut.hash.toHex == Uint8List.fromList( @@ -850,29 +968,21 @@ mixin MwebInterface ) .toList(); - final preInSum = used.fold(BigInt.zero, (p, e) => p + e.value); - final posOutputSum = tx.outputs.fold( + final posOutputSum = processedTx.outputs.fold( BigInt.zero, (acc, output) => acc + output.value, ); final mwebInputSum = - preInSum - posUtxos.fold(BigInt.zero, (p, e) => p + e.value); - - final preOutputSum = txData.recipients!.fold( - BigInt.zero, - (p, e) => p + e.amount.raw, - ); - - final difference = preOutputSum - mwebInputSum; - - final expectedPegin = difference > BigInt.zero ? difference : BigInt.zero; - - final fee = preInSum - preOutputSum; - + sumOfUtxosValue - posUtxos.fold(BigInt.zero, (p, e) => p + e.value); + final expectedPegin = maxBI(BigInt.zero, (preOutputSum - mwebInputSum)); BigInt feeIncrease = posOutputSum - expectedPegin; - if (expectedPegin > BigInt.zero && fee == BigInt.zero) { - feeIncrease += txData.fee!.raw + txData.feeRateAmount! * BigInt.from(41); + + if (expectedPegin > BigInt.zero) { + feeIncrease += BigInt.from( + (txData.feeRateAmount! / BigInt.from(1000) * 41).ceil(), + ); } + return Amount( rawValue: fee + feeIncrease, fractionDigits: cryptoCurrency.fractionDigits, diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart index 1a62c46ef..3ad0425d5 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart @@ -14,11 +14,11 @@ import 'package:tuple/tuple.dart'; import '../../../exceptions/wallet/insufficient_balance_exception.dart'; import '../../../exceptions/wallet/paynym_send_exception.dart'; +import '../../../models/input.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/bip32_utils.dart'; import '../../../utilities/bip47_utils.dart'; @@ -383,6 +383,7 @@ mixin PaynymInterface address: sendToAddress.value, amount: txData.recipients!.first.amount, isChange: false, + addressType: sendToAddress.type, ), ], ), @@ -526,15 +527,15 @@ mixin PaynymInterface } // gather required signing data - final utxoSigningData = - (await fetchBuildTxData( + final inputsWithKeys = + (await addSigningKeys( utxoObjectsToUse.map((e) => StandardInput(e)).toList(), )).whereType().toList(); final vSizeForNoChange = BigInt.from( (await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: BigInt.zero, // override amount to get around absurd fees error overrideAmountForTesting: satoshisBeingUsed, @@ -544,7 +545,7 @@ mixin PaynymInterface final vSizeForWithChange = BigInt.from( (await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: satoshisBeingUsed - amountToSend.raw, )).item2, ); @@ -587,7 +588,7 @@ mixin PaynymInterface feeForWithChange) { var txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: changeAmount, ); @@ -600,7 +601,7 @@ mixin PaynymInterface feeBeingPaid += BigInt.one; txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: changeAmount, ); } @@ -612,6 +613,7 @@ mixin PaynymInterface address: targetPaymentCodeString, amount: amountToSend, isChange: false, + addressType: AddressType.unknown, ), ], fee: Amount( @@ -619,7 +621,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.toSet(), + utxos: inputsWithKeys.toSet(), note: "PayNym connect", ); @@ -629,7 +631,7 @@ mixin PaynymInterface // than the dust limit. Try without change final txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: BigInt.zero, ); @@ -642,6 +644,7 @@ mixin PaynymInterface address: targetPaymentCodeString, amount: amountToSend, isChange: false, + addressType: AddressType.unknown, ), ], fee: Amount( @@ -649,7 +652,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.toSet(), + utxos: inputsWithKeys.toSet(), note: "PayNym connect", ); @@ -660,7 +663,7 @@ mixin PaynymInterface // build without change here final txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: BigInt.zero, ); @@ -673,6 +676,7 @@ mixin PaynymInterface address: targetPaymentCodeString, amount: amountToSend, isChange: false, + addressType: AddressType.unknown, ), ], fee: Amount( @@ -680,7 +684,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.toSet(), + utxos: inputsWithKeys.toSet(), note: "PayNym connect", ); @@ -709,7 +713,7 @@ mixin PaynymInterface // equal to its vSize Future> _createNotificationTx({ required String targetPaymentCodeString, - required List utxoSigningData, + required List inputsWithKeys, required BigInt change, BigInt? overrideAmountForTesting, }) async { @@ -720,7 +724,7 @@ mixin PaynymInterface ); final myCode = await getPaymentCode(isSegwit: false); - final utxo = utxoSigningData.first.utxo; + final utxo = inputsWithKeys.first.utxo; final txPoint = utxo.txid.toUint8ListFromHex.reversed.toList(); final txPointIndex = utxo.vout; @@ -729,7 +733,7 @@ mixin PaynymInterface final buffer = rev.buffer.asByteData(); buffer.setUint32(txPoint.length, txPointIndex, Endian.little); - final myKeyPair = utxoSigningData.first.key!; + final myKeyPair = inputsWithKeys.first.key!; final S = SecretPoint( myKeyPair.privateKey!.data, @@ -759,8 +763,8 @@ mixin PaynymInterface outputs: [], ); - for (var i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (var i = 0; i < inputsWithKeys.length; i++) { + final txid = inputsWithKeys[i].utxo.txid; final hash = Uint8List.fromList( txid.toUint8ListFromHex.reversed.toList(), @@ -768,13 +772,13 @@ mixin PaynymInterface final prevOutpoint = coinlib.OutPoint( hash, - utxoSigningData[i].utxo.vout, + inputsWithKeys[i].utxo.vout, ); final prevOutput = coinlib.Output.fromAddress( - BigInt.from(utxoSigningData[i].utxo.value), + BigInt.from(inputsWithKeys[i].utxo.value), coinlib.Address.fromString( - utxoSigningData[i].utxo.address!, + inputsWithKeys[i].utxo.address!, cryptoCurrency.networkParams, ), ); @@ -783,12 +787,12 @@ mixin PaynymInterface final coinlib.Input input; - switch (utxoSigningData[i].derivePathType) { + switch (inputsWithKeys[i].derivePathType) { case DerivePathType.bip44: case DerivePathType.bch44: input = coinlib.P2PKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].key!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: 0xffffffff - 1, ); @@ -806,7 +810,7 @@ mixin PaynymInterface case DerivePathType.bip84: input = coinlib.P2WPKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].key!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: 0xffffffff - 1, ); @@ -815,7 +819,7 @@ mixin PaynymInterface default: throw UnsupportedError( - "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + "Unknown derivation path type found: ${inputsWithKeys[i].derivePathType}", ); } @@ -887,13 +891,13 @@ mixin PaynymInterface } // sign rest of possible inputs - for (int i = 1; i < utxoSigningData.length; i++) { - final value = BigInt.from(utxoSigningData[i].utxo.value); - final key = utxoSigningData[i].key!.privateKey!; + for (int i = 1; i < inputsWithKeys.length; i++) { + final value = BigInt.from(inputsWithKeys[i].utxo.value); + final key = inputsWithKeys[i].key!.privateKey!; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( - internalKey: utxoSigningData[i].key!.publicKey, + internalKey: inputsWithKeys[i].key!.publicKey, ); clTx = clTx.signTaproot( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart index 0fad30a5e..03a8b6dd6 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart @@ -2,9 +2,9 @@ import 'dart:convert'; import 'package:isar/isar.dart'; +import '../../../models/input.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/fee_rate_type_enum.dart'; import '../../../utilities/logger.dart'; @@ -95,6 +95,7 @@ mixin RbfInterface fractionDigits: cryptoCurrency.fractionDigits, ), isChange: isChange, + addressType: cryptoCurrency.getAddressType(address)!, ), ); } @@ -147,6 +148,7 @@ mixin RbfInterface fractionDigits: cryptoCurrency.fractionDigits, ), isChange: false, + addressType: recipients.first.addressType, ), ], ); @@ -175,13 +177,11 @@ mixin RbfInterface // update recipients txData.recipients!.insert( indexOfChangeOutput, - TxRecipient( - address: removed.address, + removed.copyWith( amount: Amount( rawValue: newChangeAmount, fractionDigits: cryptoCurrency.fractionDigits, ), - isChange: removed.isChange, ), ); Logging.instance.d( @@ -203,17 +203,13 @@ mixin RbfInterface } return await buildTransaction( txData: txData.copyWith( - usedUTXOs: - txData.utxos! - .whereType() - .map((e) => e.utxo) - .toList(), + usedUTXOs: txData.utxos!.toList(), fee: Amount( rawValue: newFee, fractionDigits: cryptoCurrency.fractionDigits, ), ), - utxoSigningData: await fetchBuildTxData(txData.utxos!.toList()), + inputsWithKeys: await addSigningKeys(txData.utxos!.toList()), ); // if change amount is negative @@ -239,19 +235,17 @@ mixin RbfInterface } txData.recipients!.insert( indexOfChangeOutput, - TxRecipient( - address: removed.address, + removed.copyWith( amount: Amount( rawValue: newChangeAmount, fractionDigits: cryptoCurrency.fractionDigits, ), - isChange: removed.isChange, ), ); final newUtxoSet = { - ...txData.utxos!.whereType().map((e) => e.utxo), - ...extraUtxos, + ...txData.utxos!.whereType(), + ...extraUtxos.map((e) => StandardInput(e)), }; // TODO: remove assert @@ -264,16 +258,14 @@ mixin RbfInterface return await buildTransaction( txData: txData.copyWith( - utxos: newUtxoSet.map((e) => StandardInput(e)).toSet(), + utxos: newUtxoSet, usedUTXOs: newUtxoSet.toList(), fee: Amount( rawValue: newFee, fractionDigits: cryptoCurrency.fractionDigits, ), ), - utxoSigningData: await fetchBuildTxData( - newUtxoSet.map((e) => StandardInput(e)).toList(), - ), + inputsWithKeys: await addSigningKeys(newUtxoSet.toList()), ); } } else { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 42cc50a79..d5d5e756d 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -15,11 +15,11 @@ import 'package:logger/logger.dart'; import '../../../db/drift/database.dart' show Drift; import '../../../db/sqlite/firo_cache.dart'; import '../../../models/balance.dart'; +import '../../../models/input.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 '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; import '../../../services/event_bus/global_event_bus.dart'; import '../../../services/spark_names_service.dart'; @@ -535,15 +535,13 @@ mixin SparkInterface continue; } recipientsWithFeeSubtracted!.add( - TxRecipient( - address: txData.recipients![i].address, + txData.recipients![i].copyWith( amount: Amount( rawValue: txData.recipients![i].amount.raw - (estimatedFee ~/ BigInt.from(totalRecipientCount)), fractionDigits: cryptoCurrency.fractionDigits, ), - isChange: txData.recipients![i].isChange, ), ); @@ -1557,7 +1555,7 @@ mixin SparkInterface for (final utxo in itr) { if (nValueToSelect > nValueIn) { setCoins.add( - (await fetchBuildTxData([ + (await addSigningKeys([ StandardInput(utxo), ])).whereType().first, ); @@ -1921,7 +1919,7 @@ mixin SparkInterface rawValue: nFeeRet, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: vin.map((e) => e.utxo).toList(), + usedUTXOs: vin, tempTx: TransactionV2( walletId: walletId, blockHash: null, @@ -2239,6 +2237,7 @@ mixin SparkInterface fractionDigits: cryptoCurrency.fractionDigits, ), isChange: false, + addressType: cryptoCurrency.getAddressType(destinationAddress)!, ), ], ), From 9fdcdc2edd10fcd445d73594ff366aae88705303 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 27 Jun 2025 14:54:15 -0600 Subject: [PATCH 18/42] balance fixes --- .../sub_widgets/favorite_card.dart | 5 +++ .../mweb_interface.dart | 37 +++++-------------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 1133731dc..aad2dc5db 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -219,6 +219,11 @@ class _FavoriteCardState extends ConsumerState { ref.watch(pWalletBalanceSecondary(walletId)).total; total += ref.watch(pWalletBalanceTertiary(walletId)).total; + } else if (ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + )) { + total += + ref.watch(pWalletBalanceSecondary(walletId)).total; } Amount fiatTotal = Amount.zero; diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart index 16ccb0eaf..226a9a547 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -32,8 +32,6 @@ import 'electrumx_interface.dart'; mixin MwebInterface on ElectrumXInterface implements ExternalWallet { - // TODO - StreamSubscription? _mwebUtxoSubscription; Future get _scanSecret async => @@ -465,23 +463,8 @@ mixin MwebInterface .toList(), ); - // Update used mweb utxos as used in database. They should already have - // been marked as isUsed. - if (txData.usedUTXOs!.whereType().isNotEmpty) { - final db = Drift.get(walletId); - await db.transaction(() async { - for (final utxo in txData.usedUTXOs!.whereType()) { - await db - .into(db.mwebUtxos) - .insertOnConflictUpdate(utxo.utxo.toCompanion(false)); - } - }); - } else { - Logging.instance.w( - "txData.usedMwebUtxos is empty or null when it very " - "likely should not be!", - ); - } + // Update used mweb utxos as used in database + await _checkSpentMwebUtxos(); return await updateSentCachedTxData(txData: txData); } catch (e, s) { @@ -597,9 +580,7 @@ mixin MwebInterface Future _checkSpentMwebUtxos() async { try { final db = Drift.get(walletId); - final mwebUtxos = - await (db.select(db.mwebUtxos) - ..where((e) => e.used.equals(false))).get(); + final mwebUtxos = await db.select(db.mwebUtxos).get(); final client = await _client; @@ -607,16 +588,16 @@ mixin MwebInterface SpentRequest(outputId: mwebUtxos.map((e) => e.outputId)), ); - final updated = mwebUtxos.where( - (e) => spent.outputId.contains(e.outputId), - ); - await db.transaction(() async { - for (final utxo in updated) { + for (final utxo in mwebUtxos) { await db .into(db.mwebUtxos) .insertOnConflictUpdate( - utxo.toCompanion(false).copyWith(used: const Value(true)), + utxo + .toCompanion(false) + .copyWith( + used: Value(spent.outputId.contains(utxo.outputId)), + ), ); } }); From d41b3de12a60ef3afef8c826a1b840e1c9237d2f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 27 Jun 2025 15:03:43 -0600 Subject: [PATCH 19/42] use mweb enabled coinlib --- pubspec.lock | 24 +++++++++++-------- scripts/app_config/templates/pubspec.template | 13 +++++----- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 9cbfc4ab4..840c40fee 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -151,10 +151,10 @@ packages: dependency: "direct main" description: name: blockchain_utils - sha256: ebb6c336ba0120de0982c50d8bc597cb494a530bd22bd462895bb5cebde405af + sha256: "1e4f30b98d92f7ccf2eda009a23b53871a1c9b8b6dfa00bb1eb17ec00ae5eeeb" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.6.0" boolean_selector: dependency: transitive description: @@ -352,16 +352,20 @@ packages: coinlib: dependency: "direct overridden" description: - path: "../coinlib/coinlib" - relative: true - source: path + path: coinlib + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 + resolved-ref: da1b3659e296660ac2b36f81d155d2362a2b3195 + url: "https://www.github.com/julian-CStack/coinlib" + source: git version: "4.1.0" coinlib_flutter: dependency: "direct main" description: - path: "../coinlib/coinlib_flutter" - relative: true - source: path + path: coinlib_flutter + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 + resolved-ref: da1b3659e296660ac2b36f81d155d2362a2b3195 + url: "https://www.github.com/julian-CStack/coinlib" + source: git version: "4.0.0" collection: dependency: transitive @@ -1281,10 +1285,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.17.0" io: dependency: transitive description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index de8d4dadc..494224d6a 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -77,7 +77,6 @@ dependencies: local_auth: ^2.3.0 permission_handler: ^12.0.0+1 flutter_local_notifications: ^17.2.2 - rxdart: ^0.27.3 zxcvbn: ^1.0.0 dart_numerics: ^0.0.6 @@ -174,7 +173,11 @@ dependencies: convert: ^3.1.1 flutter_hooks: ^0.20.3 meta: ^1.9.1 - coinlib_flutter: ^3.0.0 + coinlib_flutter: + git: + url: https://www.github.com/julian-CStack/coinlib + path: coinlib_flutter + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 electrum_adapter: git: url: https://github.com/cypherstack/electrum_adapter.git @@ -264,19 +267,17 @@ dependency_overrides: win32: ^5.5.4 # namecoin names lib needs to be updated - #coinlib: ^3.0.0 - #coinlib_flutter: ^3.0.0 coinlib: git: url: https://www.github.com/julian-CStack/coinlib path: coinlib - ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 coinlib_flutter: git: url: https://www.github.com/julian-CStack/coinlib path: coinlib_flutter - ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 bip47: git: From 6fa12ce33657ae784b918a4e420f730f716de781 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 27 Jun 2025 15:24:25 -0600 Subject: [PATCH 20/42] fix: mweb tx change --- .../electrumx_interface.dart | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 05ad0a58a..73fb6bb36 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -134,6 +134,14 @@ mixin ElectrumXInterface throw Exception("Coin control used where utxos is null!"); } + Future
changeAddress() async { + if (txData.type == TxType.mweb || txData.type == TxType.mwebPegOut) { + return (await (this as MwebInterface).getMwebChangeAddress())!; + } else { + return (await getCurrentChangeAddress())!; + } + } + final recipientAddress = txData.recipients!.first.address; final satoshiAmountToSend = txData.amount!.raw; final int? satsPerVByte = txData.satsPerVByte; @@ -300,7 +308,7 @@ mixin ElectrumXInterface inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( - [recipientAddress, (await getCurrentChangeAddress())!.value], + [recipientAddress, (await changeAddress()).value], [ satoshiAmountToSend, maxBI( @@ -393,15 +401,17 @@ mixin ElectrumXInterface // 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; + if (!(txData.type == TxType.mweb || + txData.type == TxType.mwebPegOut)) { + // generate new change address if current change address has been used + await checkChangeAddressForTransactions(); + } + final newChangeAddress = await changeAddress(); BigInt feeBeingPaid = difference - changeOutputSize; // add change output - recipientsArray.add(newChangeAddress); + recipientsArray.add(newChangeAddress.value); recipientsAmtArray.add(changeOutputSize); Logging.instance.d('2 outputs in tx'); From e2da4711597a59cf8a035f1027f65a42e57a7dd4 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 30 Jun 2025 09:27:39 -0600 Subject: [PATCH 21/42] fix: mweb spend all --- .../wallet_view/sub_widgets/desktop_send.dart | 5 +++- .../electrumx_interface.dart | 26 +++++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index a2cf27fb5..567f3a94a 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -888,7 +888,10 @@ class _DesktopSendState extends ConsumerState { amount = ref.read(pWalletBalance(walletId)).spendable; break; case BalanceType.private: - amount = ref.read(pWalletBalanceTertiary(walletId)).spendable; + amount = + coin is Firo + ? ref.read(pWalletBalanceTertiary(walletId)).spendable + : ref.read(pWalletBalanceSecondary(walletId)).spendable; break; } } else { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 73fb6bb36..f36e882d7 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -263,7 +263,8 @@ mixin ElectrumXInterface final inputsWithKeys = await addSigningKeys(utxoObjectsToUse); if (isSendAll || isSendAllCoinControlUtxos) { - if (satoshiAmountToSend != satoshisBeingUsed) { + if ((overrideFeeAmount ?? BigInt.zero) + satoshiAmountToSend != + satoshisBeingUsed) { throw Exception( "Something happened that should never actually happen. " "Please report this error to the developers.", @@ -272,7 +273,6 @@ mixin ElectrumXInterface return await _sendAllBuilder( txData: txData, recipientAddress: recipientAddress, - satoshiAmountToSend: satoshiAmountToSend, satoshisBeingUsed: satoshisBeingUsed, inputsWithKeys: inputsWithKeys, satsPerVByte: satsPerVByte, @@ -486,7 +486,6 @@ mixin ElectrumXInterface Future _sendAllBuilder({ required TxData txData, required String recipientAddress, - required BigInt satoshiAmountToSend, required BigInt satoshisBeingUsed, required List inputsWithKeys, required int? satsPerVByte, @@ -530,9 +529,9 @@ mixin ElectrumXInterface feeForOneOutput = overrideFeeAmount; } - final amount = satoshiAmountToSend - feeForOneOutput; + final satoshiAmountToSend = satoshisBeingUsed - feeForOneOutput; - if (amount.isNegative) { + if (satoshiAmountToSend.isNegative) { throw Exception( "Estimated fee ($feeForOneOutput sats) is greater than balance!", ); @@ -540,7 +539,10 @@ mixin ElectrumXInterface final data = await buildTransaction( txData: txData.copyWith( - recipients: await helperRecipientsConvert([recipientAddress], [amount]), + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshiAmountToSend], + ), ), inputsWithKeys: inputsWithKeys, ); @@ -1822,6 +1824,10 @@ mixin ElectrumXInterface throw Exception("No recipients in attempted transaction!"); } + final balance = + txData.type == TxType.mweb || txData.type == TxType.mwebPegOut + ? info.cachedBalanceSecondary + : info.cachedBalance; final feeRateType = txData.feeRateType; final customSatsPerVByte = txData.satsPerVByte; final feeRateAmount = txData.feeRateAmount; @@ -1842,15 +1848,13 @@ mixin ElectrumXInterface // check for send all isSendAll = false; if (txData.ignoreCachedBalanceChecks || - txData.amount == info.cachedBalance.spendable) { + txData.amount == balance.spendable) { isSendAll = true; } if (coinControl && this is CpfpInterface && - txData.amount == - (info.cachedBalance.spendable + - info.cachedBalance.pendingSpendable)) { + txData.amount == (balance.spendable + balance.pendingSpendable)) { isSendAll = true; } @@ -1886,7 +1890,7 @@ mixin ElectrumXInterface // check for send all isSendAll = false; - if (txData.amount == info.cachedBalance.spendable) { + if (txData.amount == balance.spendable) { isSendAll = true; } From 74d3c9fd107df24bb138c79466cfabc813f9839e Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 30 Jun 2025 10:45:16 -0600 Subject: [PATCH 22/42] fix: mweb anon all (pegin everything) --- .../electrumx_interface.dart | 11 ++++++---- .../mweb_interface.dart | 21 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index f36e882d7..3e0328108 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -265,10 +265,13 @@ mixin ElectrumXInterface if (isSendAll || isSendAllCoinControlUtxos) { if ((overrideFeeAmount ?? BigInt.zero) + satoshiAmountToSend != satoshisBeingUsed) { - throw Exception( - "Something happened that should never actually happen. " - "Please report this error to the developers.", - ); + // hack check + if (txData.type != TxType.mwebPegIn) { + throw Exception( + "Something happened that should never actually happen. " + "Please report this error to the developers.", + ); + } } return await _sendAllBuilder( txData: txData, diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart index 226a9a547..dadaf6f5f 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -541,24 +541,25 @@ mixin MwebInterface throw Exception("No available UTXOs found to anonymize"); } + final amount = spendableUtxos.fold( + Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits), + (p, e) => + p + + Amount( + rawValue: BigInt.from(e.value), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + // TODO finish final txData = await prepareSend( txData: TxData( type: TxType.mwebPegIn, feeRateType: FeeRateType.average, - utxos: spendableUtxos.map((e) => StandardInput(e)).toSet(), recipients: [ TxRecipient( address: (await getCurrentReceivingMwebAddress())!.value, - amount: spendableUtxos.fold( - Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits), - (p, e) => - p + - Amount( - rawValue: BigInt.from(e.value), - fractionDigits: cryptoCurrency.fractionDigits, - ), - ), + amount: amount, isChange: false, addressType: AddressType.mweb, ), From 0cfa2701fb159b7fb72ff0191f9ed94712ddb953 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 30 Jun 2025 12:19:30 -0600 Subject: [PATCH 23/42] prevent mweb pegin transactions with non witness inputs from being broadcast (and getting dropped by network) --- .../electrumx_interface.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 3e0328108..7e669b07e 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -658,10 +658,14 @@ mixin ElectrumXInterface ? 0xffffffff - 10 : 0xffffffff - 1; + bool isMweb = false; + bool hasNonWitnessInput = false; + // Add transaction inputs for (var i = 0; i < inputsWithKeys.length; i++) { final data = inputsWithKeys[i]; if (data is MwebInput) { + isMweb = true; final address = data.address; final addr = await mainDB.getAddress(walletId, address); @@ -753,6 +757,10 @@ mixin ElectrumXInterface ); } + if (input is! coinlib.WitnessInput) { + hasNonWitnessInput = true; + } + clTx = clTx.addInput(input); tempInputs.add( @@ -798,6 +806,7 @@ mixin ElectrumXInterface } final coinlib.Output output; if (address is coinlib.MwebAddress) { + isMweb = true; output = coinlib.Output.fromProgram( txData.recipients![i].amount.raw, address.program, @@ -829,6 +838,12 @@ mixin ElectrumXInterface ); } + if (isMweb) { + if (hasNonWitnessInput) { + throw Exception("Found non witness input in mweb tx"); + } + } + try { // Sign the transaction accordingly for (var i = 0; i < inputsWithKeys.length; i++) { From 5073be77afdba4ba15d86cb38d6ad6f2102a385d Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 30 Jun 2025 13:14:08 -0600 Subject: [PATCH 24/42] update generated and handle mweb outputs in litecoin electrumx tx parsing --- lib/db/drift/database.g.dart | 953 +++++++----------- .../models/blockchain_data/transaction.g.dart | 2 + .../blockchain_data/v2/transaction_v2.g.dart | 2 + .../tx_v2/transaction_v2_details_view.dart | 427 +++++--- lib/wallets/wallet/impl/litecoin_wallet.dart | 24 +- .../mweb_interface.dart | 42 +- test/cached_electrumx_test.mocks.dart | 15 - .../pages/send_view/send_view_test.mocks.dart | 15 - .../exchange/exchange_view_test.mocks.dart | 15 - .../managed_favorite_test.mocks.dart | 15 - .../node_options_sheet_test.mocks.dart | 15 - .../transaction_card_test.mocks.dart | 15 - 12 files changed, 705 insertions(+), 835 deletions(-) diff --git a/lib/db/drift/database.g.dart b/lib/db/drift/database.g.dart index 416fa4170..67660d3a6 100644 --- a/lib/db/drift/database.g.dart +++ b/lib/db/drift/database.g.dart @@ -12,97 +12,66 @@ class $SparkNamesTable extends SparkNames static const VerificationMeta _nameMeta = const VerificationMeta('name'); @override late final GeneratedColumn name = GeneratedColumn( - 'name', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'UNIQUE NOT NULL COLLATE NOCASE', - ); - static const VerificationMeta _addressMeta = const VerificationMeta( - 'address', - ); + 'name', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'UNIQUE NOT NULL COLLATE NOCASE'); + static const VerificationMeta _addressMeta = + const VerificationMeta('address'); @override late final GeneratedColumn address = GeneratedColumn( - 'address', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _validUntilMeta = const VerificationMeta( - 'validUntil', - ); + 'address', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _validUntilMeta = + const VerificationMeta('validUntil'); @override late final GeneratedColumn validUntil = GeneratedColumn( - 'valid_until', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); - static const VerificationMeta _additionalInfoMeta = const VerificationMeta( - 'additionalInfo', - ); + 'valid_until', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _additionalInfoMeta = + const VerificationMeta('additionalInfo'); @override late final GeneratedColumn additionalInfo = GeneratedColumn( - 'additional_info', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); + 'additional_info', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); @override - List get $columns => [ - name, - address, - validUntil, - additionalInfo, - ]; + List get $columns => + [name, address, validUntil, additionalInfo]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'spark_names'; @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('name')) { context.handle( - _nameMeta, - name.isAcceptableOrUnknown(data['name']!, _nameMeta), - ); + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); } else if (isInserting) { context.missing(_nameMeta); } if (data.containsKey('address')) { - context.handle( - _addressMeta, - address.isAcceptableOrUnknown(data['address']!, _addressMeta), - ); + context.handle(_addressMeta, + address.isAcceptableOrUnknown(data['address']!, _addressMeta)); } else if (isInserting) { context.missing(_addressMeta); } if (data.containsKey('valid_until')) { context.handle( - _validUntilMeta, - validUntil.isAcceptableOrUnknown(data['valid_until']!, _validUntilMeta), - ); + _validUntilMeta, + validUntil.isAcceptableOrUnknown( + data['valid_until']!, _validUntilMeta)); } else if (isInserting) { context.missing(_validUntilMeta); } if (data.containsKey('additional_info')) { context.handle( - _additionalInfoMeta, - additionalInfo.isAcceptableOrUnknown( - data['additional_info']!, _additionalInfoMeta, - ), - ); + additionalInfo.isAcceptableOrUnknown( + data['additional_info']!, _additionalInfoMeta)); } return context; } @@ -113,25 +82,14 @@ class $SparkNamesTable extends SparkNames SparkName map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return SparkName( - name: - attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}name'], - )!, - address: - attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}address'], - )!, - validUntil: - attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}valid_until'], - )!, - additionalInfo: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}additional_info'], - ), + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + address: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}address'])!, + validUntil: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}valid_until'])!, + additionalInfo: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}additional_info']), ); } @@ -146,12 +104,11 @@ class SparkName extends DataClass implements Insertable { final String address; final int validUntil; final String? additionalInfo; - const SparkName({ - required this.name, - required this.address, - required this.validUntil, - this.additionalInfo, - }); + const SparkName( + {required this.name, + required this.address, + required this.validUntil, + this.additionalInfo}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -169,17 +126,14 @@ class SparkName extends DataClass implements Insertable { name: Value(name), address: Value(address), validUntil: Value(validUntil), - additionalInfo: - additionalInfo == null && nullToAbsent - ? const Value.absent() - : Value(additionalInfo), + additionalInfo: additionalInfo == null && nullToAbsent + ? const Value.absent() + : Value(additionalInfo), ); } - factory SparkName.fromJson( - Map json, { - ValueSerializer? serializer, - }) { + factory SparkName.fromJson(Map json, + {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return SparkName( name: serializer.fromJson(json['name']), @@ -199,28 +153,27 @@ class SparkName extends DataClass implements Insertable { }; } - SparkName copyWith({ - String? name, - String? address, - int? validUntil, - Value additionalInfo = const Value.absent(), - }) => SparkName( - name: name ?? this.name, - address: address ?? this.address, - validUntil: validUntil ?? this.validUntil, - additionalInfo: - additionalInfo.present ? additionalInfo.value : this.additionalInfo, - ); + SparkName copyWith( + {String? name, + String? address, + int? validUntil, + Value additionalInfo = const Value.absent()}) => + SparkName( + name: name ?? this.name, + address: address ?? this.address, + validUntil: validUntil ?? this.validUntil, + additionalInfo: + additionalInfo.present ? additionalInfo.value : this.additionalInfo, + ); SparkName copyWithCompanion(SparkNamesCompanion data) { return SparkName( name: data.name.present ? data.name.value : this.name, address: data.address.present ? data.address.value : this.address, validUntil: data.validUntil.present ? data.validUntil.value : this.validUntil, - additionalInfo: - data.additionalInfo.present - ? data.additionalInfo.value - : this.additionalInfo, + additionalInfo: data.additionalInfo.present + ? data.additionalInfo.value + : this.additionalInfo, ); } @@ -266,9 +219,9 @@ class SparkNamesCompanion extends UpdateCompanion { required int validUntil, this.additionalInfo = const Value.absent(), this.rowid = const Value.absent(), - }) : name = Value(name), - address = Value(address), - validUntil = Value(validUntil); + }) : name = Value(name), + address = Value(address), + validUntil = Value(validUntil); static Insertable custom({ Expression? name, Expression? address, @@ -285,13 +238,12 @@ class SparkNamesCompanion extends UpdateCompanion { }); } - SparkNamesCompanion copyWith({ - Value? name, - Value? address, - Value? validUntil, - Value? additionalInfo, - Value? rowid, - }) { + SparkNamesCompanion copyWith( + {Value? name, + Value? address, + Value? validUntil, + Value? additionalInfo, + Value? rowid}) { return SparkNamesCompanion( name: name ?? this.name, address: address ?? this.address, @@ -341,158 +293,103 @@ class $MwebUtxosTable extends MwebUtxos final GeneratedDatabase attachedDatabase; final String? _alias; $MwebUtxosTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _outputIdMeta = const VerificationMeta( - 'outputId', - ); + static const VerificationMeta _outputIdMeta = + const VerificationMeta('outputId'); @override late final GeneratedColumn outputId = GeneratedColumn( - 'output_id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _addressMeta = const VerificationMeta( - 'address', - ); + 'output_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _addressMeta = + const VerificationMeta('address'); @override late final GeneratedColumn address = GeneratedColumn( - 'address', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); + 'address', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); static const VerificationMeta _valueMeta = const VerificationMeta('value'); @override late final GeneratedColumn value = GeneratedColumn( - 'value', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); + 'value', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); static const VerificationMeta _heightMeta = const VerificationMeta('height'); @override late final GeneratedColumn height = GeneratedColumn( - 'height', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); - static const VerificationMeta _blockTimeMeta = const VerificationMeta( - 'blockTime', - ); + 'height', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _blockTimeMeta = + const VerificationMeta('blockTime'); @override late final GeneratedColumn blockTime = GeneratedColumn( - 'block_time', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); - static const VerificationMeta _blockedMeta = const VerificationMeta( - 'blocked', - ); + 'block_time', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _blockedMeta = + const VerificationMeta('blocked'); @override late final GeneratedColumn blocked = GeneratedColumn( - 'blocked', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("blocked" IN (0, 1))', - ), - ); + 'blocked', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("blocked" IN (0, 1))')); static const VerificationMeta _usedMeta = const VerificationMeta('used'); @override late final GeneratedColumn used = GeneratedColumn( - 'used', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("used" IN (0, 1))', - ), - ); + 'used', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("used" IN (0, 1))')); @override - List get $columns => [ - outputId, - address, - value, - height, - blockTime, - blocked, - used, - ]; + List get $columns => + [outputId, address, value, height, blockTime, blocked, used]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'mweb_utxos'; @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('output_id')) { - context.handle( - _outputIdMeta, - outputId.isAcceptableOrUnknown(data['output_id']!, _outputIdMeta), - ); + context.handle(_outputIdMeta, + outputId.isAcceptableOrUnknown(data['output_id']!, _outputIdMeta)); } else if (isInserting) { context.missing(_outputIdMeta); } if (data.containsKey('address')) { - context.handle( - _addressMeta, - address.isAcceptableOrUnknown(data['address']!, _addressMeta), - ); + context.handle(_addressMeta, + address.isAcceptableOrUnknown(data['address']!, _addressMeta)); } else if (isInserting) { context.missing(_addressMeta); } if (data.containsKey('value')) { context.handle( - _valueMeta, - value.isAcceptableOrUnknown(data['value']!, _valueMeta), - ); + _valueMeta, value.isAcceptableOrUnknown(data['value']!, _valueMeta)); } else if (isInserting) { context.missing(_valueMeta); } if (data.containsKey('height')) { - context.handle( - _heightMeta, - height.isAcceptableOrUnknown(data['height']!, _heightMeta), - ); + context.handle(_heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta)); } else if (isInserting) { context.missing(_heightMeta); } if (data.containsKey('block_time')) { - context.handle( - _blockTimeMeta, - blockTime.isAcceptableOrUnknown(data['block_time']!, _blockTimeMeta), - ); + context.handle(_blockTimeMeta, + blockTime.isAcceptableOrUnknown(data['block_time']!, _blockTimeMeta)); } else if (isInserting) { context.missing(_blockTimeMeta); } if (data.containsKey('blocked')) { - context.handle( - _blockedMeta, - blocked.isAcceptableOrUnknown(data['blocked']!, _blockedMeta), - ); + context.handle(_blockedMeta, + blocked.isAcceptableOrUnknown(data['blocked']!, _blockedMeta)); } else if (isInserting) { context.missing(_blockedMeta); } if (data.containsKey('used')) { context.handle( - _usedMeta, - used.isAcceptableOrUnknown(data['used']!, _usedMeta), - ); + _usedMeta, used.isAcceptableOrUnknown(data['used']!, _usedMeta)); } else if (isInserting) { context.missing(_usedMeta); } @@ -505,41 +402,20 @@ class $MwebUtxosTable extends MwebUtxos MwebUtxo map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return MwebUtxo( - outputId: - attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}output_id'], - )!, - address: - attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}address'], - )!, - value: - attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}value'], - )!, - height: - attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}height'], - )!, - blockTime: - attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}block_time'], - )!, - blocked: - attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}blocked'], - )!, - used: - attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}used'], - )!, + outputId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}output_id'])!, + address: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}address'])!, + value: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}value'])!, + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height'])!, + blockTime: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}block_time'])!, + blocked: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}blocked'])!, + used: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}used'])!, ); } @@ -557,15 +433,14 @@ class MwebUtxo extends DataClass implements Insertable { final int blockTime; final bool blocked; final bool used; - const MwebUtxo({ - required this.outputId, - required this.address, - required this.value, - required this.height, - required this.blockTime, - required this.blocked, - required this.used, - }); + const MwebUtxo( + {required this.outputId, + required this.address, + required this.value, + required this.height, + required this.blockTime, + required this.blocked, + required this.used}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -591,10 +466,8 @@ class MwebUtxo extends DataClass implements Insertable { ); } - factory MwebUtxo.fromJson( - Map json, { - ValueSerializer? serializer, - }) { + factory MwebUtxo.fromJson(Map json, + {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return MwebUtxo( outputId: serializer.fromJson(json['outputId']), @@ -620,23 +493,23 @@ class MwebUtxo extends DataClass implements Insertable { }; } - MwebUtxo copyWith({ - String? outputId, - String? address, - int? value, - int? height, - int? blockTime, - bool? blocked, - bool? used, - }) => MwebUtxo( - outputId: outputId ?? this.outputId, - address: address ?? this.address, - value: value ?? this.value, - height: height ?? this.height, - blockTime: blockTime ?? this.blockTime, - blocked: blocked ?? this.blocked, - used: used ?? this.used, - ); + MwebUtxo copyWith( + {String? outputId, + String? address, + int? value, + int? height, + int? blockTime, + bool? blocked, + bool? used}) => + MwebUtxo( + outputId: outputId ?? this.outputId, + address: address ?? this.address, + value: value ?? this.value, + height: height ?? this.height, + blockTime: blockTime ?? this.blockTime, + blocked: blocked ?? this.blocked, + used: used ?? this.used, + ); MwebUtxo copyWithCompanion(MwebUtxosCompanion data) { return MwebUtxo( outputId: data.outputId.present ? data.outputId.value : this.outputId, @@ -707,13 +580,13 @@ class MwebUtxosCompanion extends UpdateCompanion { required bool blocked, required bool used, this.rowid = const Value.absent(), - }) : outputId = Value(outputId), - address = Value(address), - value = Value(value), - height = Value(height), - blockTime = Value(blockTime), - blocked = Value(blocked), - used = Value(used); + }) : outputId = Value(outputId), + address = Value(address), + value = Value(value), + height = Value(height), + blockTime = Value(blockTime), + blocked = Value(blocked), + used = Value(used); static Insertable custom({ Expression? outputId, Expression? address, @@ -736,16 +609,15 @@ class MwebUtxosCompanion extends UpdateCompanion { }); } - MwebUtxosCompanion copyWith({ - Value? outputId, - Value? address, - Value? value, - Value? height, - Value? blockTime, - Value? blocked, - Value? used, - Value? rowid, - }) { + MwebUtxosCompanion copyWith( + {Value? outputId, + Value? address, + Value? value, + Value? height, + Value? blockTime, + Value? blocked, + Value? used, + Value? rowid}) { return MwebUtxosCompanion( outputId: outputId ?? this.outputId, address: address ?? this.address, @@ -816,22 +688,20 @@ abstract class _$WalletDatabase extends GeneratedDatabase { List get allSchemaEntities => [sparkNames, mwebUtxos]; } -typedef $$SparkNamesTableCreateCompanionBuilder = - SparkNamesCompanion Function({ - required String name, - required String address, - required int validUntil, - Value additionalInfo, - Value rowid, - }); -typedef $$SparkNamesTableUpdateCompanionBuilder = - SparkNamesCompanion Function({ - Value name, - Value address, - Value validUntil, - Value additionalInfo, - Value rowid, - }); +typedef $$SparkNamesTableCreateCompanionBuilder = SparkNamesCompanion Function({ + required String name, + required String address, + required int validUntil, + Value additionalInfo, + Value rowid, +}); +typedef $$SparkNamesTableUpdateCompanionBuilder = SparkNamesCompanion Function({ + Value name, + Value address, + Value validUntil, + Value additionalInfo, + Value rowid, +}); class $$SparkNamesTableFilterComposer extends Composer<_$WalletDatabase, $SparkNamesTable> { @@ -843,24 +713,17 @@ class $$SparkNamesTableFilterComposer super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get name => $composableBuilder( - column: $table.name, - builder: (column) => ColumnFilters(column), - ); + column: $table.name, builder: (column) => ColumnFilters(column)); ColumnFilters get address => $composableBuilder( - column: $table.address, - builder: (column) => ColumnFilters(column), - ); + column: $table.address, builder: (column) => ColumnFilters(column)); ColumnFilters get validUntil => $composableBuilder( - column: $table.validUntil, - builder: (column) => ColumnFilters(column), - ); + column: $table.validUntil, builder: (column) => ColumnFilters(column)); ColumnFilters get additionalInfo => $composableBuilder( - column: $table.additionalInfo, - builder: (column) => ColumnFilters(column), - ); + column: $table.additionalInfo, + builder: (column) => ColumnFilters(column)); } class $$SparkNamesTableOrderingComposer @@ -873,24 +736,17 @@ class $$SparkNamesTableOrderingComposer super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get name => $composableBuilder( - column: $table.name, - builder: (column) => ColumnOrderings(column), - ); + column: $table.name, builder: (column) => ColumnOrderings(column)); ColumnOrderings get address => $composableBuilder( - column: $table.address, - builder: (column) => ColumnOrderings(column), - ); + column: $table.address, builder: (column) => ColumnOrderings(column)); ColumnOrderings get validUntil => $composableBuilder( - column: $table.validUntil, - builder: (column) => ColumnOrderings(column), - ); + column: $table.validUntil, builder: (column) => ColumnOrderings(column)); ColumnOrderings get additionalInfo => $composableBuilder( - column: $table.additionalInfo, - builder: (column) => ColumnOrderings(column), - ); + column: $table.additionalInfo, + builder: (column) => ColumnOrderings(column)); } class $$SparkNamesTableAnnotationComposer @@ -909,127 +765,101 @@ class $$SparkNamesTableAnnotationComposer $composableBuilder(column: $table.address, builder: (column) => column); GeneratedColumn get validUntil => $composableBuilder( - column: $table.validUntil, - builder: (column) => column, - ); + column: $table.validUntil, builder: (column) => column); GeneratedColumn get additionalInfo => $composableBuilder( - column: $table.additionalInfo, - builder: (column) => column, - ); + column: $table.additionalInfo, builder: (column) => column); } -class $$SparkNamesTableTableManager - extends - RootTableManager< - _$WalletDatabase, - $SparkNamesTable, - SparkName, - $$SparkNamesTableFilterComposer, - $$SparkNamesTableOrderingComposer, - $$SparkNamesTableAnnotationComposer, - $$SparkNamesTableCreateCompanionBuilder, - $$SparkNamesTableUpdateCompanionBuilder, - ( - SparkName, - BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>, - ), - SparkName, - PrefetchHooks Function() - > { +class $$SparkNamesTableTableManager extends RootTableManager< + _$WalletDatabase, + $SparkNamesTable, + SparkName, + $$SparkNamesTableFilterComposer, + $$SparkNamesTableOrderingComposer, + $$SparkNamesTableAnnotationComposer, + $$SparkNamesTableCreateCompanionBuilder, + $$SparkNamesTableUpdateCompanionBuilder, + (SparkName, BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>), + SparkName, + PrefetchHooks Function()> { $$SparkNamesTableTableManager(_$WalletDatabase db, $SparkNamesTable table) - : super( - TableManagerState( + : super(TableManagerState( db: db, table: table, - createFilteringComposer: - () => $$SparkNamesTableFilterComposer($db: db, $table: table), - createOrderingComposer: - () => $$SparkNamesTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: - () => $$SparkNamesTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: - ({ - Value name = const Value.absent(), - Value address = const Value.absent(), - Value validUntil = const Value.absent(), - Value additionalInfo = const Value.absent(), - Value rowid = const Value.absent(), - }) => SparkNamesCompanion( - name: name, - address: address, - validUntil: validUntil, - additionalInfo: additionalInfo, - rowid: rowid, - ), - createCompanionCallback: - ({ - required String name, - required String address, - required int validUntil, - Value additionalInfo = const Value.absent(), - Value rowid = const Value.absent(), - }) => SparkNamesCompanion.insert( - name: name, - address: address, - validUntil: validUntil, - additionalInfo: additionalInfo, - rowid: rowid, - ), - withReferenceMapper: - (p0) => - p0 - .map( - (e) => ( - e.readTable(table), - BaseReferences(db, table, e), - ), - ) - .toList(), + createFilteringComposer: () => + $$SparkNamesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SparkNamesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SparkNamesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value name = const Value.absent(), + Value address = const Value.absent(), + Value validUntil = const Value.absent(), + Value additionalInfo = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SparkNamesCompanion( + name: name, + address: address, + validUntil: validUntil, + additionalInfo: additionalInfo, + rowid: rowid, + ), + createCompanionCallback: ({ + required String name, + required String address, + required int validUntil, + Value additionalInfo = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SparkNamesCompanion.insert( + name: name, + address: address, + validUntil: validUntil, + additionalInfo: additionalInfo, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), prefetchHooksCallback: null, - ), - ); + )); } -typedef $$SparkNamesTableProcessedTableManager = - ProcessedTableManager< - _$WalletDatabase, - $SparkNamesTable, - SparkName, - $$SparkNamesTableFilterComposer, - $$SparkNamesTableOrderingComposer, - $$SparkNamesTableAnnotationComposer, - $$SparkNamesTableCreateCompanionBuilder, - $$SparkNamesTableUpdateCompanionBuilder, - ( - SparkName, - BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>, - ), - SparkName, - PrefetchHooks Function() - >; -typedef $$MwebUtxosTableCreateCompanionBuilder = - MwebUtxosCompanion Function({ - required String outputId, - required String address, - required int value, - required int height, - required int blockTime, - required bool blocked, - required bool used, - Value rowid, - }); -typedef $$MwebUtxosTableUpdateCompanionBuilder = - MwebUtxosCompanion Function({ - Value outputId, - Value address, - Value value, - Value height, - Value blockTime, - Value blocked, - Value used, - Value rowid, - }); +typedef $$SparkNamesTableProcessedTableManager = ProcessedTableManager< + _$WalletDatabase, + $SparkNamesTable, + SparkName, + $$SparkNamesTableFilterComposer, + $$SparkNamesTableOrderingComposer, + $$SparkNamesTableAnnotationComposer, + $$SparkNamesTableCreateCompanionBuilder, + $$SparkNamesTableUpdateCompanionBuilder, + (SparkName, BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>), + SparkName, + PrefetchHooks Function()>; +typedef $$MwebUtxosTableCreateCompanionBuilder = MwebUtxosCompanion Function({ + required String outputId, + required String address, + required int value, + required int height, + required int blockTime, + required bool blocked, + required bool used, + Value rowid, +}); +typedef $$MwebUtxosTableUpdateCompanionBuilder = MwebUtxosCompanion Function({ + Value outputId, + Value address, + Value value, + Value height, + Value blockTime, + Value blocked, + Value used, + Value rowid, +}); class $$MwebUtxosTableFilterComposer extends Composer<_$WalletDatabase, $MwebUtxosTable> { @@ -1041,39 +871,25 @@ class $$MwebUtxosTableFilterComposer super.$removeJoinBuilderFromRootComposer, }); ColumnFilters get outputId => $composableBuilder( - column: $table.outputId, - builder: (column) => ColumnFilters(column), - ); + column: $table.outputId, builder: (column) => ColumnFilters(column)); ColumnFilters get address => $composableBuilder( - column: $table.address, - builder: (column) => ColumnFilters(column), - ); + column: $table.address, builder: (column) => ColumnFilters(column)); ColumnFilters get value => $composableBuilder( - column: $table.value, - builder: (column) => ColumnFilters(column), - ); + column: $table.value, builder: (column) => ColumnFilters(column)); ColumnFilters get height => $composableBuilder( - column: $table.height, - builder: (column) => ColumnFilters(column), - ); + column: $table.height, builder: (column) => ColumnFilters(column)); ColumnFilters get blockTime => $composableBuilder( - column: $table.blockTime, - builder: (column) => ColumnFilters(column), - ); + column: $table.blockTime, builder: (column) => ColumnFilters(column)); ColumnFilters get blocked => $composableBuilder( - column: $table.blocked, - builder: (column) => ColumnFilters(column), - ); + column: $table.blocked, builder: (column) => ColumnFilters(column)); ColumnFilters get used => $composableBuilder( - column: $table.used, - builder: (column) => ColumnFilters(column), - ); + column: $table.used, builder: (column) => ColumnFilters(column)); } class $$MwebUtxosTableOrderingComposer @@ -1086,39 +902,25 @@ class $$MwebUtxosTableOrderingComposer super.$removeJoinBuilderFromRootComposer, }); ColumnOrderings get outputId => $composableBuilder( - column: $table.outputId, - builder: (column) => ColumnOrderings(column), - ); + column: $table.outputId, builder: (column) => ColumnOrderings(column)); ColumnOrderings get address => $composableBuilder( - column: $table.address, - builder: (column) => ColumnOrderings(column), - ); + column: $table.address, builder: (column) => ColumnOrderings(column)); ColumnOrderings get value => $composableBuilder( - column: $table.value, - builder: (column) => ColumnOrderings(column), - ); + column: $table.value, builder: (column) => ColumnOrderings(column)); ColumnOrderings get height => $composableBuilder( - column: $table.height, - builder: (column) => ColumnOrderings(column), - ); + column: $table.height, builder: (column) => ColumnOrderings(column)); ColumnOrderings get blockTime => $composableBuilder( - column: $table.blockTime, - builder: (column) => ColumnOrderings(column), - ); + column: $table.blockTime, builder: (column) => ColumnOrderings(column)); ColumnOrderings get blocked => $composableBuilder( - column: $table.blocked, - builder: (column) => ColumnOrderings(column), - ); + column: $table.blocked, builder: (column) => ColumnOrderings(column)); ColumnOrderings get used => $composableBuilder( - column: $table.used, - builder: (column) => ColumnOrderings(column), - ); + column: $table.used, builder: (column) => ColumnOrderings(column)); } class $$MwebUtxosTableAnnotationComposer @@ -1152,104 +954,87 @@ class $$MwebUtxosTableAnnotationComposer $composableBuilder(column: $table.used, builder: (column) => column); } -class $$MwebUtxosTableTableManager - extends - RootTableManager< - _$WalletDatabase, - $MwebUtxosTable, - MwebUtxo, - $$MwebUtxosTableFilterComposer, - $$MwebUtxosTableOrderingComposer, - $$MwebUtxosTableAnnotationComposer, - $$MwebUtxosTableCreateCompanionBuilder, - $$MwebUtxosTableUpdateCompanionBuilder, - ( - MwebUtxo, - BaseReferences<_$WalletDatabase, $MwebUtxosTable, MwebUtxo>, - ), - MwebUtxo, - PrefetchHooks Function() - > { +class $$MwebUtxosTableTableManager extends RootTableManager< + _$WalletDatabase, + $MwebUtxosTable, + MwebUtxo, + $$MwebUtxosTableFilterComposer, + $$MwebUtxosTableOrderingComposer, + $$MwebUtxosTableAnnotationComposer, + $$MwebUtxosTableCreateCompanionBuilder, + $$MwebUtxosTableUpdateCompanionBuilder, + (MwebUtxo, BaseReferences<_$WalletDatabase, $MwebUtxosTable, MwebUtxo>), + MwebUtxo, + PrefetchHooks Function()> { $$MwebUtxosTableTableManager(_$WalletDatabase db, $MwebUtxosTable table) - : super( - TableManagerState( + : super(TableManagerState( db: db, table: table, - createFilteringComposer: - () => $$MwebUtxosTableFilterComposer($db: db, $table: table), - createOrderingComposer: - () => $$MwebUtxosTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: - () => $$MwebUtxosTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: - ({ - Value outputId = const Value.absent(), - Value address = const Value.absent(), - Value value = const Value.absent(), - Value height = const Value.absent(), - Value blockTime = const Value.absent(), - Value blocked = const Value.absent(), - Value used = const Value.absent(), - Value rowid = const Value.absent(), - }) => MwebUtxosCompanion( - outputId: outputId, - address: address, - value: value, - height: height, - blockTime: blockTime, - blocked: blocked, - used: used, - rowid: rowid, - ), - createCompanionCallback: - ({ - required String outputId, - required String address, - required int value, - required int height, - required int blockTime, - required bool blocked, - required bool used, - Value rowid = const Value.absent(), - }) => MwebUtxosCompanion.insert( - outputId: outputId, - address: address, - value: value, - height: height, - blockTime: blockTime, - blocked: blocked, - used: used, - rowid: rowid, - ), - withReferenceMapper: - (p0) => - p0 - .map( - (e) => ( - e.readTable(table), - BaseReferences(db, table, e), - ), - ) - .toList(), + createFilteringComposer: () => + $$MwebUtxosTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MwebUtxosTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MwebUtxosTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value outputId = const Value.absent(), + Value address = const Value.absent(), + Value value = const Value.absent(), + Value height = const Value.absent(), + Value blockTime = const Value.absent(), + Value blocked = const Value.absent(), + Value used = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MwebUtxosCompanion( + outputId: outputId, + address: address, + value: value, + height: height, + blockTime: blockTime, + blocked: blocked, + used: used, + rowid: rowid, + ), + createCompanionCallback: ({ + required String outputId, + required String address, + required int value, + required int height, + required int blockTime, + required bool blocked, + required bool used, + Value rowid = const Value.absent(), + }) => + MwebUtxosCompanion.insert( + outputId: outputId, + address: address, + value: value, + height: height, + blockTime: blockTime, + blocked: blocked, + used: used, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), prefetchHooksCallback: null, - ), - ); + )); } -typedef $$MwebUtxosTableProcessedTableManager = - ProcessedTableManager< - _$WalletDatabase, - $MwebUtxosTable, - MwebUtxo, - $$MwebUtxosTableFilterComposer, - $$MwebUtxosTableOrderingComposer, - $$MwebUtxosTableAnnotationComposer, - $$MwebUtxosTableCreateCompanionBuilder, - $$MwebUtxosTableUpdateCompanionBuilder, - (MwebUtxo, BaseReferences<_$WalletDatabase, $MwebUtxosTable, MwebUtxo>), - MwebUtxo, - PrefetchHooks Function() - >; +typedef $$MwebUtxosTableProcessedTableManager = ProcessedTableManager< + _$WalletDatabase, + $MwebUtxosTable, + MwebUtxo, + $$MwebUtxosTableFilterComposer, + $$MwebUtxosTableOrderingComposer, + $$MwebUtxosTableAnnotationComposer, + $$MwebUtxosTableCreateCompanionBuilder, + $$MwebUtxosTableUpdateCompanionBuilder, + (MwebUtxo, BaseReferences<_$WalletDatabase, $MwebUtxosTable, MwebUtxo>), + MwebUtxo, + PrefetchHooks Function()>; class $WalletDatabaseManager { final _$WalletDatabase _db; diff --git a/lib/models/isar/models/blockchain_data/transaction.g.dart b/lib/models/isar/models/blockchain_data/transaction.g.dart index 0d34d133d..3d9a3d409 100644 --- a/lib/models/isar/models/blockchain_data/transaction.g.dart +++ b/lib/models/isar/models/blockchain_data/transaction.g.dart @@ -368,6 +368,7 @@ const _TransactionsubTypeEnumValueMap = { 'sparkMint': 6, 'sparkSpend': 7, 'ordinal': 8, + 'mweb': 9, }; const _TransactionsubTypeValueEnumMap = { 0: TransactionSubType.none, @@ -379,6 +380,7 @@ const _TransactionsubTypeValueEnumMap = { 6: TransactionSubType.sparkMint, 7: TransactionSubType.sparkSpend, 8: TransactionSubType.ordinal, + 9: TransactionSubType.mweb, }; const _TransactiontypeEnumValueMap = { 'outgoing': 0, diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart index 68d8a18c5..6c2ffde6d 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart @@ -389,6 +389,7 @@ const _TransactionV2subTypeEnumValueMap = { 'sparkMint': 6, 'sparkSpend': 7, 'ordinal': 8, + 'mweb': 9, }; const _TransactionV2subTypeValueEnumMap = { 0: TransactionSubType.none, @@ -400,6 +401,7 @@ const _TransactionV2subTypeValueEnumMap = { 6: TransactionSubType.sparkMint, 7: TransactionSubType.sparkSpend, 8: TransactionSubType.ordinal, + 9: TransactionSubType.mweb, }; const _TransactionV2typeEnumValueMap = { 'outgoing': 0, diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart index c6ff6c0d5..c2a7e9adf 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart @@ -1852,147 +1852,306 @@ class _TransactionV2DetailsViewState isDesktop ? const _Divider() : const SizedBox(height: 12), - RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - ConditionalParent( - condition: !isDesktop, - builder: - (child) => Row( - children: [ - Expanded(child: child), - SimpleCopyButton( - data: _transaction.txid, + + _transaction.txid.startsWith("mweb_outputId_") && + _transaction.subType == + TransactionSubType.mweb + ? RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: !isDesktop, + builder: + (child) => Row( + children: [ + Expanded(child: child), + SimpleCopyButton( + data: _transaction + .txid + .replaceFirst( + "mweb_outputId_", + "", + ), + ), + ], ), - ], + child: Text( + "MWEB Output ID", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), - child: Text( - "Transaction ID", - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), + ), + const SizedBox(height: 8), + SelectableText( + _transaction.txid.replaceFirst( + "mweb_outputId_", + "", + ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + // if (coin is Litecoin && + // coin.network == + // CryptoCurrencyNetwork + // .main) + // const SizedBox(height: 8), + // if (coin is Litecoin && + // coin.network == + // CryptoCurrencyNetwork + // .main) + // CustomTextButton( + // text: + // "Open in block explorer", + // onTap: () async { + // final uri = + // getBlockExplorerTransactionUrlFor( + // coin: coin, + // txid: _transaction + // .txid + // .replaceFirst( + // "mweb_outputId_", + // "", + // ), + // ); + // + // if (ref + // .read( + // prefsChangeNotifierProvider, + // ) + // .hideBlockExplorerWarning == + // false) { + // final shouldContinue = + // await showExplorerWarning( + // "${uri.scheme}://${uri.host}", + // ); + // + // if (!shouldContinue) { + // return; + // } + // } + // try { + // await launchUrl( + // uri, + // mode: + // LaunchMode + // .externalApplication, + // ); + // } catch (_) { + // if (context.mounted) { + // unawaited( + // showDialog( + // context: context, + // builder: + // ( + // _, + // ) => StackOkDialog( + // title: + // "Could not open in block explorer", + // message: + // "Failed to open \"${uri.toString()}\"", + // ), + // ), + // ); + // } + // } + // }, + // ), + ], ), - const SizedBox(height: 8), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.txid, - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of(context) - .extension< - StackColors - >()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + ), + if (isDesktop) + const SizedBox(width: 12), + if (isDesktop) + IconCopyButton( + data: _transaction.txid + .replaceFirst( + "mweb_outputId_", + "", + ), ), - if (coin is! Epiccash) - const SizedBox(height: 8), - if (coin is! Epiccash) - CustomTextButton( - text: "Open in block explorer", - onTap: () async { - final uri = - getBlockExplorerTransactionUrlFor( - coin: coin, - txid: _transaction.txid, - ); - - if (ref - .read( - prefsChangeNotifierProvider, + ], + ), + ) + : RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: !isDesktop, + builder: + (child) => Row( + children: [ + Expanded(child: child), + SimpleCopyButton( + data: + _transaction.txid, + ), + ], + ), + child: Text( + "Transaction ID", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + ), + const SizedBox(height: 8), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.txid, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ) - .hideBlockExplorerWarning == - false) { - final shouldContinue = - await showExplorerWarning( - "${uri.scheme}://${uri.host}", - ); + : STextStyles.itemSubtitle12( + context, + ), + ), + if (coin is! Epiccash) + const SizedBox(height: 8), + if (coin is! Epiccash) + CustomTextButton( + text: + "Open in block explorer", + onTap: () async { + final uri = + getBlockExplorerTransactionUrlFor( + coin: coin, + txid: + _transaction.txid, + ); + + if (ref + .read( + prefsChangeNotifierProvider, + ) + .hideBlockExplorerWarning == + false) { + final shouldContinue = + await showExplorerWarning( + "${uri.scheme}://${uri.host}", + ); - if (!shouldContinue) { - return; - } - } - - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - try { - await launchUrl( - uri, - mode: - LaunchMode - .externalApplication, - ); - } catch (_) { - if (context.mounted) { - unawaited( - showDialog( - context: context, - builder: - ( - _, - ) => StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", - ), - ), - ); - } - } finally { - // Future.delayed( - // const Duration(seconds: 1), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - } - }, - ), - // ), - // ), - ], - ), + if (!shouldContinue) { + return; + } + } + + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + try { + await launchUrl( + uri, + mode: + LaunchMode + .externalApplication, + ); + } catch (_) { + if (context.mounted) { + unawaited( + showDialog( + context: context, + builder: + ( + _, + ) => StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), + ), + ); + } + } finally { + // Future.delayed( + // const Duration(seconds: 1), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + } + }, + ), + // ), + // ), + ], + ), + ), + if (isDesktop) + const SizedBox(width: 12), + if (isDesktop) + IconCopyButton( + data: _transaction.txid, + ), + ], ), - if (isDesktop) const SizedBox(width: 12), - if (isDesktop) - IconCopyButton(data: _transaction.txid), - ], - ), - ), + ), // if ((coin is FiroTestNet || coin is Firo) && // _transaction.subType == "mint") // const SizedBox( diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart index 82769ee29..af69c019d 100644 --- a/lib/wallets/wallet/impl/litecoin_wallet.dart +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:isar/isar.dart'; +import '../../../db/drift/database.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'; @@ -232,9 +233,26 @@ class LitecoinWallet } outputs.add(output); - } catch (e, s) { - // TODO: mweb output parsing - Logging.instance.e("TODO", error: e, stackTrace: s); + } catch (_) { + if (outputJson["ismweb"] == true) { + final outputId = outputJson["output_id"] as String; + + final db = Drift.get(walletId); + + final mwebUtxo = + await (db.select( + db.mwebUtxos, + )..where((e) => e.outputId.equals(outputId))).getSingleOrNull(); + + final output = OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "mweb", + scriptPubKeyAsm: null, + valueStringSats: mwebUtxo?.value.toString() ?? "0", + addresses: [outputId], + walletOwns: mwebUtxo != null, + ); + outputs.add(output); + } } } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart index dadaf6f5f..d37f74fa1 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -177,6 +177,7 @@ mixin MwebInterface Future _stopUpdateMwebUtxos() async => await _mwebUtxoSubscription?.cancel(); + Future _startUpdateMwebUtxos() async { await _stopUpdateMwebUtxos(); @@ -215,19 +216,17 @@ mixin MwebInterface (e) => e.outputId.equals(utxo.outputId), )).getSingleOrNull(); - await db - .into(db.mwebUtxos) - .insertOnConflictUpdate( - MwebUtxosCompanion( - outputId: Value(prev?.outputId ?? utxo.outputId), - address: Value(prev?.address ?? utxo.address), - value: Value(utxo.value.toInt()), - height: Value(utxo.height), - blockTime: Value(utxo.blockTime), - blocked: Value(prev?.blocked ?? false), - used: Value(prev?.used ?? false), - ), - ); + final newUtxo = MwebUtxosCompanion( + outputId: Value(prev?.outputId ?? utxo.outputId), + address: Value(prev?.address ?? utxo.address), + value: Value(utxo.value.toInt()), + height: Value(utxo.height), + blockTime: Value(utxo.blockTime), + blocked: Value(prev?.blocked ?? false), + used: Value(prev?.used ?? false), + ); + + await db.into(db.mwebUtxos).insertOnConflictUpdate(newUtxo); }); // TODO get real txid one day @@ -268,7 +267,7 @@ mixin MwebInterface await mainDB.updateOrPutTransactionV2s([tx]); - await updateBalance(mwebOnly: true); + await updateBalance(); if (utxo.height > fromHeight) { await info.updateOtherData( @@ -655,12 +654,9 @@ mixin MwebInterface } @override - Future updateBalance({bool mwebOnly = false}) async { - Future? normalBalanceFuture; - if (!mwebOnly) { - // call to super to update transparent balance - normalBalanceFuture = super.updateBalance(); - } + Future updateBalance() async { + // call to super to update transparent balance + final normalBalanceFuture = super.updateBalance(); if (info.isMwebEnabled) { final start = DateTime.now(); @@ -738,10 +734,8 @@ mixin MwebInterface } } - if (!mwebOnly) { - // wait for normalBalanceFuture to complete before returning - await normalBalanceFuture; - } + // wait for normalBalanceFuture to complete before returning + await normalBalanceFuture; } @override diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 0d5b6a2f4..22ab2a712 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -1261,21 +1261,6 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { returnValueForMissingStub: null, ); - @override - bool get useMweb => (super.noSuchMethod( - Invocation.getter(#useMweb), - returnValue: false, - ) as bool); - - @override - set useMweb(bool? useMweb) => super.noSuchMethod( - Invocation.setter( - #useMweb, - useMweb, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 84a91812a..7ea2055fc 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -1181,21 +1181,6 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); - @override - bool get useMweb => (super.noSuchMethod( - Invocation.getter(#useMweb), - returnValue: false, - ) as bool); - - @override - set useMweb(bool? useMweb) => super.noSuchMethod( - Invocation.setter( - #useMweb, - useMweb, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 1f78d9ce4..643d6cc6b 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -622,21 +622,6 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); - @override - bool get useMweb => (super.noSuchMethod( - Invocation.getter(#useMweb), - returnValue: false, - ) as bool); - - @override - set useMweb(bool? useMweb) => super.noSuchMethod( - Invocation.setter( - #useMweb, - useMweb, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 14bc27820..410094cdf 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -905,21 +905,6 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); - @override - bool get useMweb => (super.noSuchMethod( - Invocation.getter(#useMweb), - returnValue: false, - ) as bool); - - @override - set useMweb(bool? useMweb) => super.noSuchMethod( - Invocation.setter( - #useMweb, - useMweb, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index a346758aa..9b7d99828 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -780,21 +780,6 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); - @override - bool get useMweb => (super.noSuchMethod( - Invocation.getter(#useMweb), - returnValue: false, - ) as bool); - - @override - set useMweb(bool? useMweb) => super.noSuchMethod( - Invocation.setter( - #useMweb, - useMweb, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 939703910..cd8edb2ab 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -878,21 +878,6 @@ class MockPrefs extends _i1.Mock implements _i13.Prefs { returnValueForMissingStub: null, ); - @override - bool get useMweb => (super.noSuchMethod( - Invocation.getter(#useMweb), - returnValue: false, - ) as bool); - - @override - set useMweb(bool? useMweb) => super.noSuchMethod( - Invocation.setter( - #useMweb, - useMweb, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), From f00e85ffeffc7e5ab1894113117b59931c0af6a3 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 1 Jul 2025 09:13:50 -0600 Subject: [PATCH 25/42] mweb toggle dialog --- .../wallet_settings_wallet_settings_view.dart | 123 ++++++++++++++++++ .../more_features/more_features_dialog.dart | 12 +- 2 files changed, 132 insertions(+), 3 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart index a1f9f19dc..312a61a77 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart @@ -8,6 +8,8 @@ * */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -23,12 +25,15 @@ import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_dialog.dart'; import '../../../pinpad_views/lock_screen_view.dart'; @@ -53,6 +58,7 @@ class WalletSettingsWalletSettingsView extends ConsumerStatefulWidget { class _WalletSettingsWalletSettingsViewState extends ConsumerState { late final DSBController _switchController; + late final DSBController _switchControllerMwebToggle; bool _switchDuressToggleLock = false; // Mutex. Future _switchDuressToggled() async { @@ -135,6 +141,72 @@ class _WalletSettingsWalletSettingsViewState } } + bool _switchMwebToggleToggledLock = false; // Mutex. + Future _switchMwebToggleToggled() async { + if (_switchMwebToggleToggledLock) { + return; + } + _switchMwebToggleToggledLock = true; // Lock mutex. + + try { + if (_switchControllerMwebToggle.isOn?.call() != true) { + final canContinue = await showDialog( + context: context, + builder: (context) { + return StackDialog( + title: "Notice", + message: + "Activating MWEB requires some synchronization with the p2p network. " + "This process currently takes around 30 minutes to complete, " + "as it involves synchronizing on-chain MWEB related data.", + leftButton: SecondaryButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + label: "Cancel", + ), + rightButton: PrimaryButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + label: "Continue", + ), + ); + }, + ); + + if (canContinue == true) { + await _updateMwebToggle(true); + + unawaited( + (ref.read(pWallets).getWallet(widget.walletId) as MwebInterface) + .open(), + ); + } + } else { + await _updateMwebToggle(false); + } + } finally { + // ensure _switchMwebToggleToggledLock is set to false no matter what. + _switchMwebToggleToggledLock = false; + } + } + + Future _updateMwebToggle(bool value) async { + await ref + .read(pWalletInfo(widget.walletId)) + .updateOtherData( + newEntries: {WalletInfoKeys.mwebEnabled: value}, + isar: ref.read(mainDBProvider).isar, + ); + + if (_switchControllerMwebToggle.isOn != null) { + if (_switchControllerMwebToggle.isOn!.call() != value) { + _switchControllerMwebToggle.activate?.call(); + } + } + } + Future _updateAddressReuse(bool shouldReuse) async { await ref .read(pWalletInfo(widget.walletId)) @@ -153,6 +225,7 @@ class _WalletSettingsWalletSettingsViewState @override void initState() { _switchController = DSBController(); + _switchControllerMwebToggle = DSBController(); super.initState(); } @@ -304,6 +377,56 @@ class _WalletSettingsWalletSettingsViewState ), ), ), + if (wallet is MwebInterface) const SizedBox(height: 8), + if (wallet is MwebInterface) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: _switchMwebToggleToggled, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable MWEB", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: IgnorePointer( + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select( + (value) => value.otherData, + ), + )[WalletInfoKeys.mwebEnabled] + as bool? ?? + false, + controller: _switchControllerMwebToggle, + ), + ), + ), + ], + ), + ), + ), + ), if (!ref.watch(pDuress)) const SizedBox(height: 8), if (!ref.watch(pDuress)) RoundedWhiteContainer( 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 b3510022c..cac59a502 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 @@ -24,6 +24,7 @@ import '../../../../../utilities/text_styles.dart'; 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/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../../widgets/custom_buttons/draggable_switch_button.dart'; import '../../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../../widgets/desktop/desktop_dialog_close_button.dart'; @@ -198,7 +199,7 @@ class _MoreFeaturesDialogState extends ConsumerState { Padding( padding: const EdgeInsets.only(left: 32), child: Text( - "Warning!", + "Notice", style: STextStyles.desktopH3(context), ), ), @@ -216,7 +217,9 @@ class _MoreFeaturesDialogState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ Text( - "TODO warning about extra data downloaded and initial sync/scan time", + "Activating MWEB requires some synchronization with the p2p network. " + "This process currently takes around 30 minutes to complete, " + "as it involves synchronizing on-chain MWEB related data.", style: STextStyles.desktopTextSmall(context), ), const SizedBox(height: 43), @@ -255,7 +258,10 @@ class _MoreFeaturesDialogState extends ConsumerState { if (canContinue == true) { await _updateMwebToggle(true); - unawaited(ref.read(pWallets).getWallet(widget.walletId).init()); + unawaited( + (ref.read(pWallets).getWallet(widget.walletId) as MwebInterface) + .open(), + ); } } else { await _updateMwebToggle(false); From 95d3772e03990b725385fc82843abf1b4baa111c Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 1 Jul 2025 09:27:12 -0600 Subject: [PATCH 26/42] mweb address on mobile receive view --- lib/pages/receive_view/receive_view.dart | 94 +++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 9c8abcb6c..35d5c8ad9 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -28,6 +28,7 @@ import '../../utilities/assets.dart'; import '../../utilities/clipboard_interface.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/derive_path_type_enum.dart'; +import '../../utilities/show_loading.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; @@ -36,6 +37,7 @@ import '../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../widgets/background.dart'; @@ -74,6 +76,7 @@ class _ReceiveViewState extends ConsumerState { late final ClipboardInterface clipboard; late final bool _supportsSpark; late final bool _showMultiType; + late bool supportsMweb; int _currentIndex = 0; @@ -202,6 +205,31 @@ class _ReceiveViewState extends ConsumerState { } } + Future
_generateNewMwebAddress() async { + final wallet = ref.read(pWallets).getWallet(walletId) as MwebInterface; + + final address = await wallet.generateNextMwebAddress(); + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.addresses.put(address); + }); + + return address; + } + + Future generateNewMwebAddress() async { + final address = await showLoading
( + whileFuture: _generateNewMwebAddress(), + context: context, + message: "Generating address", + ); + + if (mounted && address != null) { + setState(() { + _addressMap[AddressType.mweb] = address.value; + }); + } + } + @override void initState() { walletId = widget.walletId; @@ -209,12 +237,17 @@ class _ReceiveViewState extends ConsumerState { clipboard = widget.clipboard; final wallet = ref.read(pWallets).getWallet(walletId); _supportsSpark = wallet is SparkInterface; + supportsMweb = + wallet is MwebInterface && + !wallet.info.isViewOnly && + wallet.info.isMwebEnabled; if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { _showMultiType = false; } else { _showMultiType = _supportsSpark || + supportsMweb || (wallet is! BCashInterface && wallet is Bip39HDWallet && wallet.supportedAddressTypes.length > 1); @@ -231,6 +264,10 @@ class _ReceiveViewState extends ConsumerState { (e) => e != wallet.info.mainAddressType, ), ); + + if (supportsMweb) { + _walletAddressTypes.insert(0, AddressType.mweb); + } } } @@ -285,6 +322,54 @@ class _ReceiveViewState extends ConsumerState { final ticker = widget.tokenContract?.symbol ?? coin.ticker; + ref.listen(pWalletInfo(walletId), (prev, next) { + if (prev?.isMwebEnabled != next.isMwebEnabled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + supportsMweb = next.isMwebEnabled; + + if (supportsMweb && + !_walletAddressTypes.contains(AddressType.mweb)) { + _walletAddressTypes.insert(0, AddressType.mweb); + _addressSubMap[AddressType.mweb] = ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .sortByDerivationIndexDesc() + .findFirst() + .asStream() + .listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[AddressType.mweb] = + event?.value ?? + _addressMap[AddressType.mweb] ?? + "[No address yet]"; + }); + } + }); + }); + } else { + _walletAddressTypes.remove(AddressType.mweb); + _addressSubMap[AddressType.mweb]?.cancel(); + _addressSubMap.remove(AddressType.mweb); + } + + if (_currentIndex >= _walletAddressTypes.length) { + _currentIndex = _walletAddressTypes.length - 1; + } + }); + } + }); + } + }); + final String address; if (_showMultiType) { address = _addressMap[_walletAddressTypes[_currentIndex]]!; @@ -302,7 +387,8 @@ class _ReceiveViewState extends ConsumerState { wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) { canGen = false; } else { - canGen = (wallet is MultiAddressInterface || _supportsSpark); + canGen = + (wallet is MultiAddressInterface || _supportsSpark || supportsMweb); } return Background( @@ -590,7 +676,11 @@ class _ReceiveViewState extends ConsumerState { SecondaryButton( label: "Generate new address", onPressed: - _supportsSpark && + supportsMweb && + _walletAddressTypes[_currentIndex] == + AddressType.mweb + ? generateNewMwebAddress + : _supportsSpark && _walletAddressTypes[_currentIndex] == AddressType.spark ? generateNewSparkAddress From e8b2c597cdecb923fb81fc68c8ec8e201fc5bdbc Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 1 Jul 2025 10:08:26 -0600 Subject: [PATCH 27/42] adjust mweb toggle dialog info text --- .../wallet_settings_wallet_settings_view.dart | 5 ++--- .../sub_widgets/more_features/more_features_dialog.dart | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart index 312a61a77..7792fb2e1 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart @@ -156,9 +156,8 @@ class _WalletSettingsWalletSettingsViewState return StackDialog( title: "Notice", message: - "Activating MWEB requires some synchronization with the p2p network. " - "This process currently takes around 30 minutes to complete, " - "as it involves synchronizing on-chain MWEB related data.", + "Activating MWEB requires synchronizing on-chain MWEB related data. " + "This currently requires about 800 MB of storage.", leftButton: SecondaryButton( onPressed: () { Navigator.of(context).pop(false); 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 cac59a502..20f2524a6 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 @@ -217,9 +217,8 @@ class _MoreFeaturesDialogState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ Text( - "Activating MWEB requires some synchronization with the p2p network. " - "This process currently takes around 30 minutes to complete, " - "as it involves synchronizing on-chain MWEB related data.", + "Activating MWEB requires synchronizing on-chain MWEB related data. " + "This currently requires about 800 MB of storage.", style: STextStyles.desktopTextSmall(context), ), const SizedBox(height: 43), From e0dfeb9acd3acf89f3f575d07becfacc9da5f425 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 1 Jul 2025 16:48:57 -0600 Subject: [PATCH 28/42] mwebd logs stream in debug mode --- lib/main.dart | 21 ++++++++++++++ lib/services/mwebd_service.dart | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index c521236fb..3ffda4012 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -59,6 +59,7 @@ import 'providers/providers.dart'; import 'route_generator.dart'; import 'services/exchange/exchange_data_loading_service.dart'; import 'services/locale_service.dart'; +import 'services/mwebd_service.dart'; import 'services/node_service.dart'; import 'services/notifications_api.dart'; import 'services/notifications_service.dart'; @@ -73,6 +74,7 @@ import 'utilities/logger.dart'; import 'utilities/prefs.dart'; import 'utilities/stack_file_system.dart'; import 'utilities/util.dart'; +import 'wallets/crypto_currency/crypto_currency.dart'; import 'wallets/isar/providers/all_wallets_info_provider.dart'; import 'wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'widgets/crypto_notifications.dart'; @@ -214,6 +216,25 @@ void main(List args) async { await CampfireMigration.init(); } + if (kDebugMode) { + unawaited( + MwebdService.instance + .logsStream(CryptoCurrencyNetwork.main) + .then( + (stream) => + stream.listen((line) => print("[MWEBD: MAINNET]: $line")), + ), + ); + unawaited( + MwebdService.instance + .logsStream(CryptoCurrencyNetwork.test) + .then( + (stream) => + stream.listen((line) => print("[MWEBD: TESTNET]: $line")), + ), + ); + } + // TODO: // This should be moved to happen during the loading animation instead of // showing a blank screen for 4-10 seconds. diff --git a/lib/services/mwebd_service.dart b/lib/services/mwebd_service.dart index 8f60a3d6f..f8b3a6a32 100644 --- a/lib/services/mwebd_service.dart +++ b/lib/services/mwebd_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:math'; @@ -193,6 +194,54 @@ final class MwebdService { return _map[net]?.client; }); } + + Future> logsStream( + CryptoCurrencyNetwork net, { + Duration pollInterval = const Duration(milliseconds: 200), + }) async { + final controller = StreamController(); + int offset = 0; + String leftover = ''; + Timer? timer; + + final path = + "${(await StackFileSystem.applicationMwebdDirectory(net == CryptoCurrencyNetwork.main ? "mainnet" : "testnet")).path}" + "${Platform.pathSeparator}logs" + "${Platform.pathSeparator}debug.log"; + + Future poll() async { + if (!controller.isClosed) { + final file = File(path); + final length = await file.length(); + + if (length > offset) { + final raf = await file.open(); + await raf.setPosition(offset); + final bytes = await raf.read(length - offset); + await raf.close(); + + final chunk = utf8.decode(bytes); + final lines = (leftover + chunk).split('\n'); + leftover = lines.removeLast(); // possibly incomplete + + for (final line in lines) { + controller.add(line); + } + + offset = length; + } + } + } + + timer = Timer.periodic(pollInterval, (_) => poll()); + + controller.onCancel = () { + timer?.cancel(); + controller.close(); + }; + + return controller.stream; + } } // ============================================================================ From 53caa9b95fda1ba64ea0f8382ed7e8c716f2989e Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 1 Jul 2025 16:49:14 -0600 Subject: [PATCH 29/42] mweb mobile updates and fixes --- lib/pages/send_view/send_view.dart | 127 ++++++++++-------- .../dual_balance_selection_sheet.dart | 30 +++-- .../sub_widgets/wallet_summary_info.dart | 7 +- lib/pages/wallet_view/wallet_view.dart | 18 ++- lib/wallets/wallet/impl/litecoin_wallet.dart | 4 + 5 files changed, 109 insertions(+), 77 deletions(-) diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 8c3af28e7..9ac52806f 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -607,13 +607,16 @@ class _SendViewState extends ConsumerState { final Amount amount = ref.read(pSendAmount)!; final Amount availableBalance; - if (isFiro) { + if (isFiro || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case BalanceType.public: availableBalance = wallet.info.cachedBalance.spendable; break; case BalanceType.private: - availableBalance = wallet.info.cachedBalanceTertiary.spendable; + availableBalance = + isFiro + ? wallet.info.cachedBalanceTertiary.spendable + : wallet.info.cachedBalanceSecondary.spendable; break; } } else { @@ -975,14 +978,17 @@ class _SendViewState extends ConsumerState { if (showCoinControl && selectedUTXOs.isNotEmpty) { amount = _selectedUtxosAmount(selectedUTXOs); - } else if (isFiro) { + } else if (isFiro || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case BalanceType.public: amount = ref.read(pWalletBalance(walletId)).spendable; break; case BalanceType.private: - amount = ref.read(pWalletBalanceTertiary(walletId)).spendable; + amount = + isFiro + ? ref.read(pWalletBalanceTertiary(walletId)).spendable + : ref.read(pWalletBalanceSecondary(walletId)).spendable; break; } } else { @@ -1198,54 +1204,56 @@ class _SendViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final wallet = ref.watch(pWallets).getWallet(walletId); final String locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale), ); + final balType = ref.watch(publicPrivateBalanceStateProvider); + + final isMwebEnabled = ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + ); + final showPrivateBalance = coin is Firo || isMwebEnabled; + final showCoinControl = - wallet is CoinControlInterface && ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, ), ) && - (coin is Firo - ? ref.watch(publicPrivateBalanceStateProvider) == BalanceType.public - : true); + ref.watch(pWallets).getWallet(walletId) is CoinControlInterface && + balType == BalanceType.public; - if (isFiro) { - final isExchangeAddress = ref.watch(pIsExchangeAddress); + final isExchangeAddress = ref.watch(pIsExchangeAddress); - ref.listen(publicPrivateBalanceStateProvider, (previous, next) { - selectedUTXOs = {}; - - if (ref.read(pSendAmount) == null) { - setState(() { - _calculateFeesFuture = calculateFees( - 0.toAmountAsRaw(fractionDigits: coin.fractionDigits), - ); - }); - } else { - setState(() { - _calculateFeesFuture = calculateFees(ref.read(pSendAmount)!); - }); - } + ref.listen(publicPrivateBalanceStateProvider, (previous, next) { + selectedUTXOs = {}; - if (previous != next && - next == BalanceType.private && - isExchangeAddress && - !_isFiroExWarningDisplayed) { - _isFiroExWarningDisplayed = true; - WidgetsBinding.instance.addPostFrameCallback( - (_) => showFiroExchangeAddressWarning( - context, - () => _isFiroExWarningDisplayed = false, - ), + if (ref.read(pSendAmount) == null) { + setState(() { + _calculateFeesFuture = calculateFees( + 0.toAmountAsRaw(fractionDigits: coin.fractionDigits), ); - } - }); - } + }); + } else { + setState(() { + _calculateFeesFuture = calculateFees(ref.read(pSendAmount)!); + }); + } + + if (previous != next && + next == BalanceType.private && + isExchangeAddress && + !_isFiroExWarningDisplayed) { + _isFiroExWarningDisplayed = true; + WidgetsBinding.instance.addPostFrameCallback( + (_) => showFiroExchangeAddressWarning( + context, + () => _isFiroExWarningDisplayed = false, + ), + ); + } + }); // add listener for epic cash to strip http:// and https:// prefixes if the address also ocntains an @ symbol (indicating an epicbox address) if (coin is Epiccash) { @@ -1346,9 +1354,9 @@ class _SendViewState extends ConsumerState { // const SizedBox( // height: 2, // ), - if (isFiro) + if (isFiro || isMwebEnabled) Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state.name.capitalize()} balance", + "${balType.name.capitalize()} balance", style: STextStyles.label( context, ).copyWith(fontSize: 10), @@ -1366,13 +1374,8 @@ class _SendViewState extends ConsumerState { Builder( builder: (context) { final Amount amount; - if (isFiro) { - switch (ref - .watch( - publicPrivateBalanceStateProvider - .state, - ) - .state) { + if (showPrivateBalance) { + switch (balType) { case BalanceType.public: amount = ref @@ -1388,9 +1391,13 @@ class _SendViewState extends ConsumerState { amount = ref .read( - pWalletBalanceTertiary( - walletId, - ), + isMwebEnabled + ? pWalletBalanceSecondary( + walletId, + ) + : pWalletBalanceTertiary( + walletId, + ), ) .spendable; break; @@ -1771,15 +1778,17 @@ class _SendViewState extends ConsumerState { } }, ), - if (isFiro) const SizedBox(height: 12), - if (isFiro) + if (isFiro || isMwebEnabled) + const SizedBox(height: 12), + if (isFiro || isMwebEnabled) Text( "Send from", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (isFiro) const SizedBox(height: 8), - if (isFiro) + if (isFiro || isMwebEnabled) + const SizedBox(height: 8), + if (isFiro || isMwebEnabled) Stack( children: [ TextField( @@ -1855,9 +1864,13 @@ class _SendViewState extends ConsumerState { amount = ref .watch( - pWalletBalanceTertiary( - walletId, - ), + isFiro + ? pWalletBalanceTertiary( + walletId, + ) + : pWalletBalanceSecondary( + walletId, + ), ) .spendable; break; diff --git a/lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart index c8006b713..fbd9ad996 100644 --- a/lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart @@ -11,13 +11,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../providers/providers.dart'; import '../../../providers/wallet/public_private_balance_state_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount_formatter.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/text_styles.dart'; -import '../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../wallets/crypto_currency/coins/firo.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; class DualBalanceSelectionSheet extends ConsumerStatefulWidget { const DualBalanceSelectionSheet({super.key, required this.walletId}); @@ -43,12 +43,7 @@ class _FiroBalanceSelectionSheetState Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final wallet = ref.watch( - pWallets.select((value) => value.getWallet(walletId)), - ); - final firoWallet = wallet as FiroWallet; - - final coin = wallet.info.coin; + final coin = ref.watch(pWalletCoin(walletId)); return Container( decoration: BoxDecoration( @@ -141,7 +136,7 @@ class _FiroBalanceSelectionSheetState // Row( // children: [ Text( - "Spark balance", + "Private balance", style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), @@ -150,9 +145,16 @@ class _FiroBalanceSelectionSheetState ref .watch(pAmountFormatter(coin)) .format( - firoWallet - .info - .cachedBalanceTertiary + ref + .watch( + coin is Firo + ? pWalletBalanceTertiary( + walletId, + ) + : pWalletBalanceSecondary( + walletId, + ), + ) .spendable, ), style: STextStyles.itemSubtitle(context), @@ -231,7 +233,9 @@ class _FiroBalanceSelectionSheetState ref .watch(pAmountFormatter(coin)) .format( - firoWallet.info.cachedBalance.spendable, + ref + .watch(pWalletBalance(walletId)) + .spendable, ), style: STextStyles.itemSubtitle(context), textAlign: TextAlign.left, diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart index 3b8e255e8..68e0123bc 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -94,14 +94,17 @@ class WalletSummaryInfo extends ConsumerWidget { final bool toggleBalance; - if (coin is Firo) { + if (coin is Firo || ref.watch(pWalletInfo(walletId)).isMwebEnabled) { toggleBalance = false; final type = ref.watch(publicPrivateBalanceStateProvider.state).state; title = "${_showAvailable ? "Available" : "Full"} ${type.name.capitalize()} balance"; switch (type) { case BalanceType.private: - final balance = ref.watch(pWalletBalanceTertiary(walletId)); + final balance = + coin is Firo + ? ref.watch(pWalletBalanceTertiary(walletId)) + : ref.watch(pWalletBalanceSecondary(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 06697a1d9..1f8ecff20 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -56,6 +56,7 @@ import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; @@ -432,9 +433,9 @@ class _WalletViewState extends ConsumerState { ), ), ); - final firoWallet = ref.read(pWallets).getWallet(walletId) as FiroWallet; + final wallet = ref.read(pWallets).getWallet(walletId); - final Amount publicBalance = firoWallet.info.cachedBalance.spendable; + final Amount publicBalance = wallet.info.cachedBalance.spendable; if (publicBalance <= Amount.zero) { shouldPop = true; if (mounted) { @@ -453,7 +454,11 @@ class _WalletViewState extends ConsumerState { } try { - await firoWallet.anonymizeAllSpark(); + if (wallet is MwebInterface && wallet.info.isMwebEnabled) { + await wallet.anonymizeAllMweb(); + } else { + await (wallet as FiroWallet).anonymizeAllSpark(); + } shouldPop = true; if (mounted) { Navigator.of( @@ -811,8 +816,11 @@ class _WalletViewState extends ConsumerState { ), ), ), - if (isSparkWallet) const SizedBox(height: 10), - if (isSparkWallet) + if (isSparkWallet || + ref.watch(pWalletInfo(walletId)).isMwebEnabled) + const SizedBox(height: 10), + if (isSparkWallet || + ref.watch(pWalletInfo(walletId)).isMwebEnabled) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart index af69c019d..76205c252 100644 --- a/lib/wallets/wallet/impl/litecoin_wallet.dart +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -292,6 +292,10 @@ class LitecoinWallet .isNotEmpty(); if (hasOrdinal) { subType = TransactionSubType.ordinal; + } else { + if (outputs.any((e) => e.scriptPubKeyHex == "mweb")) { + subType = TransactionSubType.mweb; + } } // making API calls for every output in every transaction is too expensive From 32bd98f0833b125be9edc9ba54006ae2ce76800c Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 2 Jul 2025 09:04:17 -0600 Subject: [PATCH 30/42] flutter_mwebd go requirement docs update and bump flutter_mwebd version with ios/macos build fix --- docs/building.md | 1 + scripts/app_config/templates/pubspec.template | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index 3ee68cf98..a50abbeb3 100644 --- a/docs/building.md +++ b/docs/building.md @@ -7,6 +7,7 @@ Here you will find instructions on how to install the necessary tools for buildi - The only OS supported for building Android and Linux desktop is Ubuntu 20.04. Windows builds require using Ubuntu 20.04 on WSL2. macOS builds for itself and iOS. Advanced users may also be able to build on other Debian-based distributions like Linux Mint. - Android setup ([Android Studio](https://developer.android.com/studio) and subsequent dependencies) - 100 GB of storage +- Install go: [https://go.dev/doc/install](https://go.dev/doc/install) ## Linux host diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 494224d6a..e9b29aad1 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -224,7 +224,7 @@ dependencies: path: ^1.9.1 cs_salvium: ^1.2.0 cs_salvium_flutter_libs: ^1.0.2 - flutter_mwebd: ^0.0.1-pre.1 + flutter_mwebd: ^0.0.1-pre.2 mweb_client: ^0.2.0 fixnum: ^1.1.1 From 01b067681a0d8451329ccca1eb7bdfb7391bd391 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 3 Jul 2025 08:52:28 -0600 Subject: [PATCH 31/42] fix utxos update "optimization" --- lib/db/isar/main_db.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index c8f7fc493..0a0269025 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -313,8 +313,6 @@ class MainDB { Future updateUTXOs(String walletId, List utxos) async { bool newUTXO = false; - if (utxos.isEmpty) return newUTXO; - await isar.writeTxn(() async { final set = utxos.toSet(); for (final utxo in utxos) { @@ -343,7 +341,9 @@ class MainDB { } await isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); - await isar.utxos.putAll(set.toList()); + if (set.isNotEmpty) { + await isar.utxos.putAll(set.toList()); + } }); return newUTXO; From ec630cffe146a8bfe12cca3af0e34a8b31cd7c6a Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 3 Jul 2025 08:56:03 -0600 Subject: [PATCH 32/42] bump flutter_mwebd version again (with ios build fix) --- pubspec.lock | 4 ++-- scripts/app_config/templates/pubspec.template | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 840c40fee..7ed655329 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -985,10 +985,10 @@ packages: dependency: "direct main" description: name: flutter_mwebd - sha256: b390f5c99aaddec5d4c785c6c3d96b786b4eb333e5e1fd0051079cec1aacf40a + sha256: f3b2f86b9b7352e1a6a33d2f4ed098d454a8841929b767e9390b14b66e243274 url: "https://pub.dev" source: hosted - version: "0.0.1-pre.1" + version: "0.0.1-pre.3" flutter_native_splash: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index e9b29aad1..e2a31c8ce 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -224,7 +224,7 @@ dependencies: path: ^1.9.1 cs_salvium: ^1.2.0 cs_salvium_flutter_libs: ^1.0.2 - flutter_mwebd: ^0.0.1-pre.2 + flutter_mwebd: ^0.0.1-pre.3 mweb_client: ^0.2.0 fixnum: ^1.1.1 From 8b19fcbaba37e53104092d2e9314ae5331cc1663 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 3 Jul 2025 15:07:27 -0600 Subject: [PATCH 33/42] bump flutter_mwebd version yet again --- pubspec.lock | 4 ++-- scripts/app_config/templates/pubspec.template | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 7ed655329..5932dd93b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -985,10 +985,10 @@ packages: dependency: "direct main" description: name: flutter_mwebd - sha256: f3b2f86b9b7352e1a6a33d2f4ed098d454a8841929b767e9390b14b66e243274 + sha256: bc664ef047b5364c02fa1d75aa3b0d73c1ae888530857fb8a646e31e4dd3ce7c url: "https://pub.dev" source: hosted - version: "0.0.1-pre.3" + version: "0.0.1-pre.4" flutter_native_splash: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index e2a31c8ce..6f7f3cac2 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -224,7 +224,7 @@ dependencies: path: ^1.9.1 cs_salvium: ^1.2.0 cs_salvium_flutter_libs: ^1.0.2 - flutter_mwebd: ^0.0.1-pre.3 + flutter_mwebd: ^0.0.1-pre.4 mweb_client: ^0.2.0 fixnum: ^1.1.1 From 483a41e38d21bbdcb1422bfa426e5b349992c73b Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 4 Jul 2025 10:41:27 -0600 Subject: [PATCH 34/42] and again bump flutter_mwebd version, this time with windows build fix --- docs/building.md | 2 ++ scripts/app_config/templates/pubspec.template | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index a50abbeb3..6aa647e53 100644 --- a/docs/building.md +++ b/docs/building.md @@ -164,6 +164,8 @@ cd scripts/windows ./deps.sh ``` +install go in WSL [https://go.dev/doc/install](https://go.dev/doc/install) (follow linux instructions) and ensure you have `x86_64-w64-mingw32-gcc` + and use `scripts/build_app.sh` to build plugins: ``` cd .. diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 6f7f3cac2..2a323afc7 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -224,7 +224,7 @@ dependencies: path: ^1.9.1 cs_salvium: ^1.2.0 cs_salvium_flutter_libs: ^1.0.2 - flutter_mwebd: ^0.0.1-pre.4 + flutter_mwebd: ^0.0.1-pre.5 mweb_client: ^0.2.0 fixnum: ^1.1.1 From 338f6613e72b892f72f66bc98c2012285e13e15f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 4 Jul 2025 12:50:38 -0600 Subject: [PATCH 35/42] again, but this time with linux fix --- pubspec.lock | 4 ++-- scripts/app_config/templates/pubspec.template | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 5932dd93b..66cfe0e85 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -985,10 +985,10 @@ packages: dependency: "direct main" description: name: flutter_mwebd - sha256: bc664ef047b5364c02fa1d75aa3b0d73c1ae888530857fb8a646e31e4dd3ce7c + sha256: f6daecf6a4e10dde0a2fbfe026d801cd41864c6464923d9a26a92c613c893173 url: "https://pub.dev" source: hosted - version: "0.0.1-pre.4" + version: "0.0.1-pre.6" flutter_native_splash: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 2a323afc7..4e60e7857 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -224,7 +224,7 @@ dependencies: path: ^1.9.1 cs_salvium: ^1.2.0 cs_salvium_flutter_libs: ^1.0.2 - flutter_mwebd: ^0.0.1-pre.5 + flutter_mwebd: ^0.0.1-pre.6 mweb_client: ^0.2.0 fixnum: ^1.1.1 From abeb82406068ef32ed6d98fb6c1efae4692c7713 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 4 Jul 2025 14:43:58 -0600 Subject: [PATCH 36/42] mweb related ui tweaks --- lib/pages/wallet_view/wallet_view.dart | 10 +++++----- .../sub_widgets/desktop_wallet_features.dart | 15 ++++++++------- .../sub_widgets/mweb_desktop_wallet_summary.dart | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 1f8ecff20..dd4c29560 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -445,7 +445,7 @@ class _WalletViewState extends ConsumerState { unawaited( showFloatingFlushBar( type: FlushBarType.info, - message: "No funds available to anonymize!", + message: "No funds available to privatize!", context: context, ), ); @@ -467,7 +467,7 @@ class _WalletViewState extends ConsumerState { unawaited( showFloatingFlushBar( type: FlushBarType.success, - message: "Anonymize transaction submitted", + message: "Privatize transaction submitted", context: context, ), ); @@ -482,7 +482,7 @@ class _WalletViewState extends ConsumerState { context: context, builder: (_) => StackOkDialog( - title: "Anonymize all failed", + title: "Privatize all failed", message: "Reason: $e", ), ); @@ -839,7 +839,7 @@ class _WalletViewState extends ConsumerState { (context) => StackDialog( title: "Attention!", message: - "You're about to anonymize all of your public funds.", + "You're about to privatize all of your public funds.", leftButton: TextButton( onPressed: () { Navigator.of(context).pop(); @@ -880,7 +880,7 @@ class _WalletViewState extends ConsumerState { ); }, child: Text( - "Anonymize funds", + "Privatize funds", style: STextStyles.button( context, ).copyWith( 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 17c055614..63722608b 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 @@ -11,6 +11,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -67,7 +68,7 @@ import '../desktop_wallet_view.dart'; import 'more_features/more_features_dialog.dart'; enum WalletFeature { - anonymizeFunds("Anonymize funds", "Anonymize funds"), + anonymizeFunds("Privatize funds", "Privatize funds"), swap("Swap", ""), buy("Buy", "Buy cryptocurrency"), paynym("PayNym", "Increased address privacy using BIP47"), @@ -163,7 +164,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { Text("Attention!", style: STextStyles.desktopH2(context)), const SizedBox(height: 16), Text( - "You're about to anonymize all of your public funds.", + "You're about to privatize all of your public funds.", style: STextStyles.desktopTextSmall(context), ), const SizedBox(height: 32), @@ -206,7 +207,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { builder: (context) => WillPopScope( child: const CustomLoadingOverlay( - message: "Anonymizing balance", + message: "Privatizing balance", eventBus: null, ), onWillPop: () async => shouldPop, @@ -226,7 +227,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { unawaited( showFloatingFlushBar( type: FlushBarType.info, - message: "No funds available to anonymize!", + message: "No funds available to privatize!", context: context, ), ); @@ -249,7 +250,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { unawaited( showFloatingFlushBar( type: FlushBarType.success, - message: "Anonymize transaction submitted", + message: "Privatize transaction submitted", context: context, ), ); @@ -273,7 +274,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Anonymize all failed", + "Privatize all failed", style: STextStyles.desktopH3(context), ), const Spacer(flex: 1), @@ -428,7 +429,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { _onSparkCoinsPressed, ), - if (!isViewOnly && wallet is MwebInterface) + if (kDebugMode && !isViewOnly && wallet is MwebInterface) ( WalletFeature.mwebUtxos, Assets.svg.coinControl.gamePad, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart index 942d08fa1..6ce1d19e4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart @@ -151,7 +151,7 @@ class _Prefix extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ SelectableText( - isMweb ? "MWEB" : "", + isMweb ? "Private" : "Public", style: STextStyles.w500_24(context), ), ], From 219b4d1a6142195b17151f38592bb7a87ea061f7 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 4 Jul 2025 15:28:09 -0600 Subject: [PATCH 37/42] fix coin control pegin --- .../wallet/wallet_mixin_interfaces/electrumx_interface.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 7e669b07e..e032cfce2 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1938,6 +1938,7 @@ mixin ElectrumXInterface txData: result.copyWith( recipients: result.recipients!.where((e) => !(e.isChange)).toList(), ), + utxos: utxos?.toList(), coinControl: coinControl, isSendAll: isSendAll, isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, From e7e053fa782823cba19ee9e662bb3ee3dbc3e7b5 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 4 Jul 2025 16:28:55 -0600 Subject: [PATCH 38/42] ensure wallet has address of mweb output in local db --- .../wallet/wallet_mixin_interfaces/mweb_interface.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart index d37f74fa1..a870aaa1e 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -229,6 +229,12 @@ mixin MwebInterface await db.into(db.mwebUtxos).insertOnConflictUpdate(newUtxo); }); + Address? addr = await mainDB.getAddress(walletId, utxo.address); + while (addr == null || addr.value != utxo.address) { + addr = await generateNextMwebAddress(); + await mainDB.updateOrPutAddresses([addr]); + } + // TODO get real txid one day final fakeTxid = "mweb_outputId_${utxo.outputId}"; From 6cb870e738f2c5e47b9c9c60ba3f783406eb4af5 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 4 Jul 2025 16:29:15 -0600 Subject: [PATCH 39/42] fix mweb -> mweb send all --- .../wallet_mixin_interfaces/electrumx_interface.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index e032cfce2..e978a0bd7 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -265,8 +265,18 @@ mixin ElectrumXInterface if (isSendAll || isSendAllCoinControlUtxos) { if ((overrideFeeAmount ?? BigInt.zero) + satoshiAmountToSend != satoshisBeingUsed) { + Logging.instance.d("txData.type: ${txData.type}"); + Logging.instance.d("isSendAll: $isSendAll"); + Logging.instance.d( + "isSendAllCoinControlUtxos: $isSendAllCoinControlUtxos", + ); + Logging.instance.d("overrideFeeAmount: $overrideFeeAmount"); + Logging.instance.d("satoshiAmountToSend: $satoshiAmountToSend"); + Logging.instance.d("satoshisBeingUsed: $satoshisBeingUsed"); + // hack check - if (txData.type != TxType.mwebPegIn) { + if (!(txData.type == TxType.mwebPegIn || + (txData.type == TxType.mweb && overrideFeeAmount != null))) { throw Exception( "Something happened that should never actually happen. " "Please report this error to the developers.", From fe6ef0a06cae7127f29a4a099f4080393eacc5b1 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 7 Jul 2025 08:05:22 -0600 Subject: [PATCH 40/42] fix receiving address query listener --- lib/pages/receive_view/receive_view.dart | 6 ++++++ .../wallet_view/sub_widgets/desktop_receive.dart | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 35d5c8ad9..fe44acc9b 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -289,6 +289,9 @@ class _ReceiveViewState extends ConsumerState { .walletIdEqualTo(walletId) .filter() .typeEqualTo(type) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) .sortByDerivationIndexDesc() .findFirst() .asStream() @@ -340,6 +343,9 @@ class _ReceiveViewState extends ConsumerState { .walletIdEqualTo(walletId) .filter() .typeEqualTo(AddressType.mweb) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) .sortByDerivationIndexDesc() .findFirst() .asStream() diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 20e780c97..5cea18ff2 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -284,6 +284,9 @@ class _DesktopReceiveState extends ConsumerState { .walletIdEqualTo(walletId) .filter() .typeEqualTo(type) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) .sortByDerivationIndexDesc() .findFirst() .asStream() @@ -333,6 +336,9 @@ class _DesktopReceiveState extends ConsumerState { .walletIdEqualTo(walletId) .filter() .typeEqualTo(AddressType.mweb) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) .sortByDerivationIndexDesc() .findFirst() .asStream() From 3c05f17640a89f92e2464ae4957705e9c7049b75 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 7 Jul 2025 11:55:59 -0600 Subject: [PATCH 41/42] salvium block explorer uri fix --- lib/wallets/crypto_currency/coins/salvium.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallets/crypto_currency/coins/salvium.dart b/lib/wallets/crypto_currency/coins/salvium.dart index d7618094c..db794dc53 100644 --- a/lib/wallets/crypto_currency/coins/salvium.dart +++ b/lib/wallets/crypto_currency/coins/salvium.dart @@ -116,7 +116,7 @@ class Salvium extends CryptonoteCurrency { Uri defaultBlockExplorer(String txid) { switch (network) { case CryptoCurrencyNetwork.main: - return Uri.parse("https://explorer.salvium.io//tx/$txid"); + return Uri.parse("https://explorer.salvium.io/tx/$txid"); default: throw Exception( "Unsupported network for defaultBlockExplorer(): $network", From 401af1f8043c2c0fa49780b5d161f7bcfb2ad518 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 7 Jul 2025 11:57:37 -0600 Subject: [PATCH 42/42] salvium price fix --- lib/services/price.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/services/price.dart b/lib/services/price.dart index 8dc686fa1..9641d0328 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -49,6 +49,7 @@ class PriceAPI { Nano: "nano", Banano: "banano", Xelis: "xelis", + Salvium: "salvium", }; static const refreshInterval = 60;