From 6ddef9f077d9117becce3dcb5de251c79fe9a51f Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Nov 2023 10:13:10 -0600 Subject: [PATCH 01/77] add lib spark local dep for testing --- .../spark_interface.dart | 20 +++++++++++++------ linux/flutter/generated_plugin_registrant.cc | 4 ++++ linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ pubspec.lock | 7 +++++++ pubspec.yaml | 3 +++ .../flutter/generated_plugin_registrant.cc | 3 +++ windows/flutter/generated_plugins.cmake | 1 + 8 files changed, 35 insertions(+), 6 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 678ddb77f..6cc61f252 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1,8 +1,10 @@ import 'dart:typed_data'; +import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; @@ -27,22 +29,28 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { throw UnimplementedError(); } - Future
generateNextSparkAddress() async { + Future
generateNextSparkAddress({int index = 1}) async { final highestStoredDiversifier = (await getCurrentReceivingSparkAddress())?.derivationIndex; // default to starting at 1 if none found final int diversifier = (highestStoredDiversifier ?? 0) + 1; - // TODO: use real data - final String derivationPath = ""; - final Uint8List publicKey = Uint8List(0); // incomingViewKey? - final String addressString = ""; + final root = await getRootHDNode(); + final derivationPath = "$kSparkBaseDerivationPath$index"; + final keys = root.derivePath(derivationPath); + + final String addressString = await LibSpark.getAddress( + privateKey: keys.privateKey.data, + index: index, + diversifier: diversifier, + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, + ); return Address( walletId: walletId, value: addressString, - publicKey: publicKey, + publicKey: keys.publicKey.data, derivationIndex: diversifier, derivationPath: DerivationPath()..value = derivationPath, type: AddressType.spark, diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index b174939fa..333af74ca 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_libmonero_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLibmoneroPlugin"); flutter_libmonero_plugin_register_with_registrar(flutter_libmonero_registrar); + g_autoptr(FlPluginRegistrar) flutter_libsparkmobile_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLibsparkmobilePlugin"); + flutter_libsparkmobile_plugin_register_with_registrar(flutter_libsparkmobile_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 3a02b87cf..042fc932e 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST devicelocale flutter_libepiccash flutter_libmonero + flutter_libsparkmobile flutter_secure_storage_linux isar_flutter_libs stack_wallet_backup diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b044b4b00..1168bb3ed 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,6 +13,7 @@ import desktop_drop import device_info_plus import devicelocale import flutter_libepiccash +import flutter_libsparkmobile import flutter_local_notifications import flutter_secure_storage_macos import isar_flutter_libs @@ -34,6 +35,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) FlutterLibepiccashPlugin.register(with: registry.registrar(forPlugin: "FlutterLibepiccashPlugin")) + FlutterLibsparkmobilePlugin.register(with: registry.registrar(forPlugin: "FlutterLibsparkmobilePlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) diff --git a/pubspec.lock b/pubspec.lock index c4572ea32..68b474ced 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -659,6 +659,13 @@ packages: relative: true source: path version: "0.0.1" + flutter_libsparkmobile: + dependency: "direct main" + description: + path: "../flutter_libsparkmobile" + relative: true + source: path + version: "0.0.1" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 57ac07659..a6d48be44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,9 @@ dependencies: lelantus: path: ./crypto_plugins/flutter_liblelantus + flutter_libsparkmobile: + path: ../flutter_libsparkmobile + flutter_libmonero: path: ./crypto_plugins/flutter_libmonero diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index f886b2cc1..7ef8abf80 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DesktopDropPlugin")); FlutterLibepiccashPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterLibepiccashPluginCApi")); + FlutterLibsparkmobilePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterLibsparkmobilePluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); IsarFlutterLibsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 701de9701..633570bee 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus desktop_drop flutter_libepiccash + flutter_libsparkmobile flutter_secure_storage_windows isar_flutter_libs permission_handler_windows From 734e9d90b1b47285665d311b0d41273543fd5734 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 29 Nov 2023 09:53:30 -0600 Subject: [PATCH 02/77] WIP basic PoC showing firo spark address in stack wallet --- .../sub_widgets/desktop_receive.dart | 104 ++++++++++++++++++ lib/wallets/wallet/impl/bitcoin_wallet.dart | 4 +- .../wallet/impl/bitcoincash_wallet.dart | 4 +- lib/wallets/wallet/impl/dogecoin_wallet.dart | 4 +- lib/wallets/wallet/impl/ecash_wallet.dart | 4 +- lib/wallets/wallet/impl/firo_wallet.dart | 20 +--- lib/wallets/wallet/wallet.dart | 5 +- .../electrumx_interface.dart | 4 +- .../spark_interface.dart | 41 ++++++- linux/flutter/generated_plugin_registrant.cc | 4 - linux/flutter/generated_plugins.cmake | 2 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 - .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 2 +- 14 files changed, 160 insertions(+), 43 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 5a04541d9..5f34f6ae0 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 @@ -15,6 +15,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; @@ -30,6 +31,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -57,6 +59,7 @@ class _DesktopReceiveState extends ConsumerState { late final Coin coin; late final String walletId; late final ClipboardInterface clipboard; + late final bool supportsSpark; Future generateNewAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); @@ -98,6 +101,7 @@ class _DesktopReceiveState extends ConsumerState { walletId = widget.walletId; coin = ref.read(pWalletInfo(walletId)).coin; clipboard = widget.clipboard; + supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface; super.initState(); } @@ -111,6 +115,106 @@ class _DesktopReceiveState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + if (supportsSpark) + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + clipboard.setData( + ClipboardData(text: receivingAddress), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context) + .extension()! + .backgroundAppBar, + width: 1, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: RoundedWhiteContainer( + child: Column( + children: [ + Row( + children: [ + Text( + "Your ${widget.contractAddress == null ? coin.ticker : ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.symbol, + ), + )} SPARK address", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 15, + height: 15, + color: Theme.of(context) + .extension()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + Expanded( + child: FutureBuilder( + future: (ref.watch(pWallets).getWallet(walletId) + as SparkInterface) + .getCurrentReceivingSparkAddress(), + builder: (context, snapshot) { + String addressString = "Error"; + if (snapshot.hasData) { + addressString = snapshot.data!.value; + } + + return Text( + addressString, + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ); + }, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( diff --git a/lib/wallets/wallet/impl/bitcoin_wallet.dart b/lib/wallets/wallet/impl/bitcoin_wallet.dart index f16f0c15e..d458d3736 100644 --- a/lib/wallets/wallet/impl/bitcoin_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_wallet.dart @@ -28,7 +28,7 @@ class BitcoinWallet extends Bip39HDWallet // =========================================================================== @override - Future> fetchAllOwnAddresses() async { + Future> fetchAddressesForElectrumXScan() async { final allAddresses = await mainDB .getAddresses(walletId) .filter() @@ -51,7 +51,7 @@ class BitcoinWallet extends Bip39HDWallet // TODO: [prio=med] switch to V2 transactions final data = await fetchTransactionsV1( - addresses: await fetchAllOwnAddresses(), + addresses: await fetchAddressesForElectrumXScan(), currentChainHeight: currentChainHeight, ); diff --git a/lib/wallets/wallet/impl/bitcoincash_wallet.dart b/lib/wallets/wallet/impl/bitcoincash_wallet.dart index 0e35577c4..533d97b53 100644 --- a/lib/wallets/wallet/impl/bitcoincash_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoincash_wallet.dart @@ -63,7 +63,7 @@ class BitcoincashWallet extends Bip39HDWallet // =========================================================================== @override - Future> fetchAllOwnAddresses() async { + Future> fetchAddressesForElectrumXScan() async { final allAddresses = await mainDB .getAddresses(walletId) .filter() @@ -94,7 +94,7 @@ class BitcoincashWallet extends Bip39HDWallet @override Future updateTransactions() async { - List
allAddressesOld = await fetchAllOwnAddresses(); + List
allAddressesOld = await fetchAddressesForElectrumXScan(); Set receivingAddresses = allAddressesOld .where((e) => e.subType == AddressSubType.receiving) diff --git a/lib/wallets/wallet/impl/dogecoin_wallet.dart b/lib/wallets/wallet/impl/dogecoin_wallet.dart index 27d0e90e0..611b80d29 100644 --- a/lib/wallets/wallet/impl/dogecoin_wallet.dart +++ b/lib/wallets/wallet/impl/dogecoin_wallet.dart @@ -24,7 +24,7 @@ class DogecoinWallet extends Bip39HDWallet // =========================================================================== @override - Future> fetchAllOwnAddresses() async { + Future> fetchAddressesForElectrumXScan() async { final allAddresses = await mainDB .getAddresses(walletId) .filter() @@ -47,7 +47,7 @@ class DogecoinWallet extends Bip39HDWallet // TODO: [prio=med] switch to V2 transactions final data = await fetchTransactionsV1( - addresses: await fetchAllOwnAddresses(), + addresses: await fetchAddressesForElectrumXScan(), currentChainHeight: currentChainHeight, ); diff --git a/lib/wallets/wallet/impl/ecash_wallet.dart b/lib/wallets/wallet/impl/ecash_wallet.dart index 39c21aae2..fcfda3e0b 100644 --- a/lib/wallets/wallet/impl/ecash_wallet.dart +++ b/lib/wallets/wallet/impl/ecash_wallet.dart @@ -58,7 +58,7 @@ class EcashWallet extends Bip39HDWallet // =========================================================================== @override - Future> fetchAllOwnAddresses() async { + Future> fetchAddressesForElectrumXScan() async { final allAddresses = await mainDB .getAddresses(walletId) .filter() @@ -87,7 +87,7 @@ class EcashWallet extends Bip39HDWallet @override Future updateTransactions() async { - List
allAddressesOld = await fetchAllOwnAddresses(); + List
allAddressesOld = await fetchAddressesForElectrumXScan(); Set receivingAddresses = allAddressesOld .where((e) => e.subType == AddressSubType.receiving) diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index bcb90ac1b..1dfc75094 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -37,24 +37,6 @@ class FiroWallet extends Bip39HDWallet // =========================================================================== - @override - Future> fetchAllOwnAddresses() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); - return allAddresses; - } - - // =========================================================================== - bool _duplicateTxCheck( List> allTransactions, String txid) { for (int i = 0; i < allTransactions.length; i++) { @@ -67,7 +49,7 @@ class FiroWallet extends Bip39HDWallet @override Future updateTransactions() async { - final allAddresses = await fetchAllOwnAddresses(); + final allAddresses = await fetchAddressesForElectrumXScan(); Set receivingAddresses = allAddresses .where((e) => e.subType == AddressSubType.receiving) diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index ac5e602f9..3d790e5b9 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -289,7 +289,10 @@ abstract class Wallet { // listen to changes in db and updated wallet info property as required void _watchWalletInfo() { - _walletInfoStream = mainDB.isar.walletInfo.watchObject(_walletInfo.id); + _walletInfoStream = mainDB.isar.walletInfo.watchObject( + _walletInfo.id, + fireImmediately: true, + ); _walletInfoStream.forEach((element) { if (element != null) { _walletInfo = element; diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index be786ff52..7146454f1 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1641,7 +1641,7 @@ mixin ElectrumXInterface on Bip39HDWallet { @override Future updateUTXOs() async { - final allAddresses = await fetchAllOwnAddresses(); + final allAddresses = await fetchAddressesForElectrumXScan(); try { final fetchedUtxoList = >>[]; @@ -1856,7 +1856,7 @@ mixin ElectrumXInterface on Bip39HDWallet { int estimateTxFee({required int vSize, required int feeRatePerKB}); Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB); - Future> fetchAllOwnAddresses(); + Future> fetchAddressesForElectrumXScan(); /// Certain coins need to check if the utxo should be marked /// as blocked as well as give a reason. diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 6cc61f252..65c556039 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -10,6 +10,40 @@ import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { + @override + Future init() async { + Address? address = await getCurrentReceivingSparkAddress(); + if (address == null) { + address = await generateNextSparkAddress(); + await mainDB.putAddress(address); + } // TODO add other address types to wallet info? + + // await info.updateReceivingAddress( + // newAddress: address.value, + // isar: mainDB.isar, + // ); + + await super.init(); + } + + @override + Future> fetchAddressesForElectrumXScan() async { + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.spark) + .or() + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); + return allAddresses; + } + Future getCurrentReceivingSparkAddress() async { return await mainDB.isar.addresses .where() @@ -29,15 +63,18 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { throw UnimplementedError(); } - Future
generateNextSparkAddress({int index = 1}) async { + Future
generateNextSparkAddress() async { final highestStoredDiversifier = (await getCurrentReceivingSparkAddress())?.derivationIndex; // default to starting at 1 if none found final int diversifier = (highestStoredDiversifier ?? 0) + 1; + // TODO: check that this stays constant and only the diversifier changes? + const index = 1; + final root = await getRootHDNode(); - final derivationPath = "$kSparkBaseDerivationPath$index"; + const derivationPath = "$kSparkBaseDerivationPath$index"; final keys = root.derivePath(derivationPath); final String addressString = await LibSpark.getAddress( diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 333af74ca..b174939fa 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -30,9 +29,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_libmonero_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLibmoneroPlugin"); flutter_libmonero_plugin_register_with_registrar(flutter_libmonero_registrar); - g_autoptr(FlPluginRegistrar) flutter_libsparkmobile_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLibsparkmobilePlugin"); - flutter_libsparkmobile_plugin_register_with_registrar(flutter_libsparkmobile_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 042fc932e..bb9965d23 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -7,7 +7,6 @@ list(APPEND FLUTTER_PLUGIN_LIST devicelocale flutter_libepiccash flutter_libmonero - flutter_libsparkmobile flutter_secure_storage_linux isar_flutter_libs stack_wallet_backup @@ -17,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST coinlib_flutter + flutter_libsparkmobile tor_ffi_plugin ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 1168bb3ed..b044b4b00 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,7 +13,6 @@ import desktop_drop import device_info_plus import devicelocale import flutter_libepiccash -import flutter_libsparkmobile import flutter_local_notifications import flutter_secure_storage_macos import isar_flutter_libs @@ -35,7 +34,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) FlutterLibepiccashPlugin.register(with: registry.registrar(forPlugin: "FlutterLibepiccashPlugin")) - FlutterLibsparkmobilePlugin.register(with: registry.registrar(forPlugin: "FlutterLibsparkmobilePlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 7ef8abf80..f886b2cc1 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -25,8 +24,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DesktopDropPlugin")); FlutterLibepiccashPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterLibepiccashPluginCApi")); - FlutterLibsparkmobilePluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterLibsparkmobilePluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); IsarFlutterLibsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 633570bee..f11baedd5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus desktop_drop flutter_libepiccash - flutter_libsparkmobile flutter_secure_storage_windows isar_flutter_libs permission_handler_windows @@ -17,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_libsparkmobile tor_ffi_plugin ) From 9ad723a5b2e9c7b5986d8b2c3e3e99ff6a97ef21 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 4 Dec 2023 09:35:59 -0600 Subject: [PATCH 03/77] WIP database schema for spark coin data --- lib/wallets/isar/models/spark_coin.dart | 126 + lib/wallets/isar/models/spark_coin.g.dart | 2828 +++++++++++++++++ .../spark_interface.dart | 81 +- 3 files changed, 3023 insertions(+), 12 deletions(-) create mode 100644 lib/wallets/isar/models/spark_coin.dart create mode 100644 lib/wallets/isar/models/spark_coin.g.dart diff --git a/lib/wallets/isar/models/spark_coin.dart b/lib/wallets/isar/models/spark_coin.dart new file mode 100644 index 000000000..dfbf4c6e6 --- /dev/null +++ b/lib/wallets/isar/models/spark_coin.dart @@ -0,0 +1,126 @@ +import 'package:isar/isar.dart'; + +part 'spark_coin.g.dart'; + +enum SparkCoinType { + mint(0), + spend(1); + + const SparkCoinType(this.value); + + final int value; +} + +@Collection() +class SparkCoin { + Id id = Isar.autoIncrement; + + @Index( + unique: true, + replace: true, + composite: [ + CompositeIndex("lTagHash"), + ], + ) + final String walletId; + + @enumerated + final SparkCoinType type; + + final bool isUsed; + + final List? k; // TODO: proper name (not single char!!) is this nonce??? + + final String address; + final String txHash; + + final String valueIntString; + + final String? memo; + final List? serialContext; + + final String diversifierIntString; + final List? encryptedDiversifier; + + final List? serial; + final List? tag; + + final String lTagHash; + + @ignore + BigInt get value => BigInt.parse(valueIntString); + + @ignore + BigInt get diversifier => BigInt.parse(diversifierIntString); + + SparkCoin({ + required this.walletId, + required this.type, + required this.isUsed, + this.k, + required this.address, + required this.txHash, + required this.valueIntString, + this.memo, + this.serialContext, + required this.diversifierIntString, + this.encryptedDiversifier, + this.serial, + this.tag, + required this.lTagHash, + }); + + SparkCoin copyWith({ + SparkCoinType? type, + bool? isUsed, + List? k, + String? address, + String? txHash, + BigInt? value, + String? memo, + List? serialContext, + BigInt? diversifier, + List? encryptedDiversifier, + List? serial, + List? tag, + String? lTagHash, + }) { + return SparkCoin( + walletId: walletId, + type: type ?? this.type, + isUsed: isUsed ?? this.isUsed, + k: k ?? this.k, + address: address ?? this.address, + txHash: txHash ?? this.txHash, + valueIntString: value?.toString() ?? this.value.toString(), + memo: memo ?? this.memo, + serialContext: serialContext ?? this.serialContext, + diversifierIntString: + diversifier?.toString() ?? this.diversifier.toString(), + encryptedDiversifier: encryptedDiversifier ?? this.encryptedDiversifier, + serial: serial ?? this.serial, + tag: tag ?? this.tag, + lTagHash: lTagHash ?? this.lTagHash, + ); + } + + @override + String toString() { + return 'SparkCoin(' + ', walletId: $walletId' + ', type: $type' + ', isUsed: $isUsed' + ', k: $k' + ', address: $address' + ', txHash: $txHash' + ', value: $value' + ', memo: $memo' + ', serialContext: $serialContext' + ', diversifier: $diversifier' + ', encryptedDiversifier: $encryptedDiversifier' + ', serial: $serial' + ', tag: $tag' + ', lTagHash: $lTagHash' + ')'; + } +} diff --git a/lib/wallets/isar/models/spark_coin.g.dart b/lib/wallets/isar/models/spark_coin.g.dart new file mode 100644 index 000000000..6f5a87ea7 --- /dev/null +++ b/lib/wallets/isar/models/spark_coin.g.dart @@ -0,0 +1,2828 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'spark_coin.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters + +extension GetSparkCoinCollection on Isar { + IsarCollection get sparkCoins => this.collection(); +} + +const SparkCoinSchema = CollectionSchema( + name: r'SparkCoin', + id: -187103855721793545, + properties: { + r'address': PropertySchema( + id: 0, + name: r'address', + type: IsarType.string, + ), + r'diversifierIntString': PropertySchema( + id: 1, + name: r'diversifierIntString', + type: IsarType.string, + ), + r'encryptedDiversifier': PropertySchema( + id: 2, + name: r'encryptedDiversifier', + type: IsarType.longList, + ), + r'isUsed': PropertySchema( + id: 3, + name: r'isUsed', + type: IsarType.bool, + ), + r'k': PropertySchema( + id: 4, + name: r'k', + type: IsarType.longList, + ), + r'lTagHash': PropertySchema( + id: 5, + name: r'lTagHash', + type: IsarType.string, + ), + r'memo': PropertySchema( + id: 6, + name: r'memo', + type: IsarType.string, + ), + r'serial': PropertySchema( + id: 7, + name: r'serial', + type: IsarType.longList, + ), + r'serialContext': PropertySchema( + id: 8, + name: r'serialContext', + type: IsarType.longList, + ), + r'tag': PropertySchema( + id: 9, + name: r'tag', + type: IsarType.longList, + ), + r'txHash': PropertySchema( + id: 10, + name: r'txHash', + type: IsarType.string, + ), + r'type': PropertySchema( + id: 11, + name: r'type', + type: IsarType.byte, + enumMap: _SparkCointypeEnumValueMap, + ), + r'valueIntString': PropertySchema( + id: 12, + name: r'valueIntString', + type: IsarType.string, + ), + r'walletId': PropertySchema( + id: 13, + name: r'walletId', + type: IsarType.string, + ) + }, + estimateSize: _sparkCoinEstimateSize, + serialize: _sparkCoinSerialize, + deserialize: _sparkCoinDeserialize, + deserializeProp: _sparkCoinDeserializeProp, + idName: r'id', + indexes: { + r'walletId_lTagHash': IndexSchema( + id: 3478068730295484116, + name: r'walletId_lTagHash', + unique: true, + replace: true, + properties: [ + IndexPropertySchema( + name: r'walletId', + type: IndexType.hash, + caseSensitive: true, + ), + IndexPropertySchema( + name: r'lTagHash', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _sparkCoinGetId, + getLinks: _sparkCoinGetLinks, + attach: _sparkCoinAttach, + version: '3.0.5', +); + +int _sparkCoinEstimateSize( + SparkCoin object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.address.length * 3; + bytesCount += 3 + object.diversifierIntString.length * 3; + { + final value = object.encryptedDiversifier; + if (value != null) { + bytesCount += 3 + value.length * 8; + } + } + { + final value = object.k; + if (value != null) { + bytesCount += 3 + value.length * 8; + } + } + bytesCount += 3 + object.lTagHash.length * 3; + { + final value = object.memo; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.serial; + if (value != null) { + bytesCount += 3 + value.length * 8; + } + } + { + final value = object.serialContext; + if (value != null) { + bytesCount += 3 + value.length * 8; + } + } + { + final value = object.tag; + if (value != null) { + bytesCount += 3 + value.length * 8; + } + } + bytesCount += 3 + object.txHash.length * 3; + bytesCount += 3 + object.valueIntString.length * 3; + bytesCount += 3 + object.walletId.length * 3; + return bytesCount; +} + +void _sparkCoinSerialize( + SparkCoin object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.address); + writer.writeString(offsets[1], object.diversifierIntString); + writer.writeLongList(offsets[2], object.encryptedDiversifier); + writer.writeBool(offsets[3], object.isUsed); + writer.writeLongList(offsets[4], object.k); + writer.writeString(offsets[5], object.lTagHash); + writer.writeString(offsets[6], object.memo); + writer.writeLongList(offsets[7], object.serial); + writer.writeLongList(offsets[8], object.serialContext); + writer.writeLongList(offsets[9], object.tag); + writer.writeString(offsets[10], object.txHash); + writer.writeByte(offsets[11], object.type.index); + writer.writeString(offsets[12], object.valueIntString); + writer.writeString(offsets[13], object.walletId); +} + +SparkCoin _sparkCoinDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = SparkCoin( + address: reader.readString(offsets[0]), + diversifierIntString: reader.readString(offsets[1]), + encryptedDiversifier: reader.readLongList(offsets[2]), + isUsed: reader.readBool(offsets[3]), + k: reader.readLongList(offsets[4]), + lTagHash: reader.readString(offsets[5]), + memo: reader.readStringOrNull(offsets[6]), + serial: reader.readLongList(offsets[7]), + serialContext: reader.readLongList(offsets[8]), + tag: reader.readLongList(offsets[9]), + txHash: reader.readString(offsets[10]), + type: _SparkCointypeValueEnumMap[reader.readByteOrNull(offsets[11])] ?? + SparkCoinType.mint, + valueIntString: reader.readString(offsets[12]), + walletId: reader.readString(offsets[13]), + ); + object.id = id; + return object; +} + +P _sparkCoinDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readString(offset)) as P; + case 1: + return (reader.readString(offset)) as P; + case 2: + return (reader.readLongList(offset)) as P; + case 3: + return (reader.readBool(offset)) as P; + case 4: + return (reader.readLongList(offset)) as P; + case 5: + return (reader.readString(offset)) as P; + case 6: + return (reader.readStringOrNull(offset)) as P; + case 7: + return (reader.readLongList(offset)) as P; + case 8: + return (reader.readLongList(offset)) as P; + case 9: + return (reader.readLongList(offset)) as P; + case 10: + return (reader.readString(offset)) as P; + case 11: + return (_SparkCointypeValueEnumMap[reader.readByteOrNull(offset)] ?? + SparkCoinType.mint) as P; + case 12: + return (reader.readString(offset)) as P; + case 13: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +const _SparkCointypeEnumValueMap = { + 'mint': 0, + 'spend': 1, +}; +const _SparkCointypeValueEnumMap = { + 0: SparkCoinType.mint, + 1: SparkCoinType.spend, +}; + +Id _sparkCoinGetId(SparkCoin object) { + return object.id; +} + +List> _sparkCoinGetLinks(SparkCoin object) { + return []; +} + +void _sparkCoinAttach(IsarCollection col, Id id, SparkCoin object) { + object.id = id; +} + +extension SparkCoinByIndex on IsarCollection { + Future getByWalletIdLTagHash(String walletId, String lTagHash) { + return getByIndex(r'walletId_lTagHash', [walletId, lTagHash]); + } + + SparkCoin? getByWalletIdLTagHashSync(String walletId, String lTagHash) { + return getByIndexSync(r'walletId_lTagHash', [walletId, lTagHash]); + } + + Future deleteByWalletIdLTagHash(String walletId, String lTagHash) { + return deleteByIndex(r'walletId_lTagHash', [walletId, lTagHash]); + } + + bool deleteByWalletIdLTagHashSync(String walletId, String lTagHash) { + return deleteByIndexSync(r'walletId_lTagHash', [walletId, lTagHash]); + } + + Future> getAllByWalletIdLTagHash( + List walletIdValues, List lTagHashValues) { + final len = walletIdValues.length; + assert(lTagHashValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([walletIdValues[i], lTagHashValues[i]]); + } + + return getAllByIndex(r'walletId_lTagHash', values); + } + + List getAllByWalletIdLTagHashSync( + List walletIdValues, List lTagHashValues) { + final len = walletIdValues.length; + assert(lTagHashValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([walletIdValues[i], lTagHashValues[i]]); + } + + return getAllByIndexSync(r'walletId_lTagHash', values); + } + + Future deleteAllByWalletIdLTagHash( + List walletIdValues, List lTagHashValues) { + final len = walletIdValues.length; + assert(lTagHashValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([walletIdValues[i], lTagHashValues[i]]); + } + + return deleteAllByIndex(r'walletId_lTagHash', values); + } + + int deleteAllByWalletIdLTagHashSync( + List walletIdValues, List lTagHashValues) { + final len = walletIdValues.length; + assert(lTagHashValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([walletIdValues[i], lTagHashValues[i]]); + } + + return deleteAllByIndexSync(r'walletId_lTagHash', values); + } + + Future putByWalletIdLTagHash(SparkCoin object) { + return putByIndex(r'walletId_lTagHash', object); + } + + Id putByWalletIdLTagHashSync(SparkCoin object, {bool saveLinks = true}) { + return putByIndexSync(r'walletId_lTagHash', object, saveLinks: saveLinks); + } + + Future> putAllByWalletIdLTagHash(List objects) { + return putAllByIndex(r'walletId_lTagHash', objects); + } + + List putAllByWalletIdLTagHashSync(List objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'walletId_lTagHash', objects, + saveLinks: saveLinks); + } +} + +extension SparkCoinQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension SparkCoinQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder idGreaterThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + walletIdEqualToAnyLTagHash(String walletId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'walletId_lTagHash', + value: [walletId], + )); + }); + } + + QueryBuilder + walletIdNotEqualToAnyLTagHash(String walletId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId_lTagHash', + lower: [], + upper: [walletId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId_lTagHash', + lower: [walletId], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId_lTagHash', + lower: [walletId], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId_lTagHash', + lower: [], + upper: [walletId], + includeUpper: false, + )); + } + }); + } + + QueryBuilder walletIdLTagHashEqualTo( + String walletId, String lTagHash) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'walletId_lTagHash', + value: [walletId, lTagHash], + )); + }); + } + + QueryBuilder + walletIdEqualToLTagHashNotEqualTo(String walletId, String lTagHash) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId_lTagHash', + lower: [walletId], + upper: [walletId, lTagHash], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId_lTagHash', + lower: [walletId, lTagHash], + includeLower: false, + upper: [walletId], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId_lTagHash', + lower: [walletId, lTagHash], + includeLower: false, + upper: [walletId], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId_lTagHash', + lower: [walletId], + upper: [walletId, lTagHash], + includeUpper: false, + )); + } + }); + } +} + +extension SparkCoinQueryFilter + on QueryBuilder { + QueryBuilder addressEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'address', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'address', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'address', + value: '', + )); + }); + } + + QueryBuilder + addressIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'address', + value: '', + )); + }); + } + + QueryBuilder + diversifierIntStringEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'diversifierIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + diversifierIntStringGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'diversifierIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + diversifierIntStringLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'diversifierIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + diversifierIntStringBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'diversifierIntString', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + diversifierIntStringStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'diversifierIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + diversifierIntStringEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'diversifierIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + diversifierIntStringContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'diversifierIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + diversifierIntStringMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'diversifierIntString', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + diversifierIntStringIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'diversifierIntString', + value: '', + )); + }); + } + + QueryBuilder + diversifierIntStringIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'diversifierIntString', + value: '', + )); + }); + } + + QueryBuilder + encryptedDiversifierIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'encryptedDiversifier', + )); + }); + } + + QueryBuilder + encryptedDiversifierIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'encryptedDiversifier', + )); + }); + } + + QueryBuilder + encryptedDiversifierElementEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'encryptedDiversifier', + value: value, + )); + }); + } + + QueryBuilder + encryptedDiversifierElementGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'encryptedDiversifier', + value: value, + )); + }); + } + + QueryBuilder + encryptedDiversifierElementLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'encryptedDiversifier', + value: value, + )); + }); + } + + QueryBuilder + encryptedDiversifierElementBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'encryptedDiversifier', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + encryptedDiversifierLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'encryptedDiversifier', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder + encryptedDiversifierIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'encryptedDiversifier', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder + encryptedDiversifierIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'encryptedDiversifier', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + encryptedDiversifierLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'encryptedDiversifier', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + encryptedDiversifierLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'encryptedDiversifier', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder + encryptedDiversifierLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'encryptedDiversifier', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder idEqualTo( + Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder isUsedEqualTo( + bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isUsed', + value: value, + )); + }); + } + + QueryBuilder kIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'k', + )); + }); + } + + QueryBuilder kIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'k', + )); + }); + } + + QueryBuilder kElementEqualTo( + int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'k', + value: value, + )); + }); + } + + QueryBuilder kElementGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'k', + value: value, + )); + }); + } + + QueryBuilder kElementLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'k', + value: value, + )); + }); + } + + QueryBuilder kElementBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'k', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder kLengthEqualTo( + int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'k', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder kIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'k', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder kIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'k', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder kLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'k', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder kLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'k', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder kLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'k', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder lTagHashEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'lTagHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder lTagHashGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'lTagHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder lTagHashLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'lTagHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder lTagHashBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'lTagHash', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder lTagHashStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'lTagHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder lTagHashEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'lTagHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder lTagHashContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'lTagHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder lTagHashMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'lTagHash', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder lTagHashIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'lTagHash', + value: '', + )); + }); + } + + QueryBuilder + lTagHashIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'lTagHash', + value: '', + )); + }); + } + + QueryBuilder memoIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'memo', + )); + }); + } + + QueryBuilder memoIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'memo', + )); + }); + } + + QueryBuilder memoEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'memo', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder memoGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'memo', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder memoLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'memo', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder memoBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'memo', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder memoStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'memo', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder memoEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'memo', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder memoContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'memo', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder memoMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'memo', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder memoIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'memo', + value: '', + )); + }); + } + + QueryBuilder memoIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'memo', + value: '', + )); + }); + } + + QueryBuilder serialIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'serial', + )); + }); + } + + QueryBuilder serialIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'serial', + )); + }); + } + + QueryBuilder + serialElementEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'serial', + value: value, + )); + }); + } + + QueryBuilder + serialElementGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'serial', + value: value, + )); + }); + } + + QueryBuilder + serialElementLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'serial', + value: value, + )); + }); + } + + QueryBuilder + serialElementBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'serial', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder serialLengthEqualTo( + int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serial', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder serialIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serial', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder serialIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serial', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + serialLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serial', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + serialLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serial', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder serialLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serial', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder + serialContextIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'serialContext', + )); + }); + } + + QueryBuilder + serialContextIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'serialContext', + )); + }); + } + + QueryBuilder + serialContextElementEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'serialContext', + value: value, + )); + }); + } + + QueryBuilder + serialContextElementGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'serialContext', + value: value, + )); + }); + } + + QueryBuilder + serialContextElementLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'serialContext', + value: value, + )); + }); + } + + QueryBuilder + serialContextElementBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'serialContext', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + serialContextLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serialContext', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder + serialContextIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serialContext', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder + serialContextIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serialContext', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + serialContextLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serialContext', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + serialContextLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serialContext', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder + serialContextLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'serialContext', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder tagIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'tag', + )); + }); + } + + QueryBuilder tagIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'tag', + )); + }); + } + + QueryBuilder tagElementEqualTo( + int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'tag', + value: value, + )); + }); + } + + QueryBuilder + tagElementGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'tag', + value: value, + )); + }); + } + + QueryBuilder tagElementLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'tag', + value: value, + )); + }); + } + + QueryBuilder tagElementBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'tag', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder tagLengthEqualTo( + int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'tag', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder tagIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'tag', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder tagIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'tag', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder tagLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'tag', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + tagLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'tag', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder tagLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'tag', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder txHashEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'txHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txHashGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'txHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txHashLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'txHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txHashBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'txHash', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txHashStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'txHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txHashEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'txHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txHashContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'txHash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txHashMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'txHash', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txHashIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'txHash', + value: '', + )); + }); + } + + QueryBuilder txHashIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'txHash', + value: '', + )); + }); + } + + QueryBuilder typeEqualTo( + SparkCoinType value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'type', + value: value, + )); + }); + } + + QueryBuilder typeGreaterThan( + SparkCoinType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'type', + value: value, + )); + }); + } + + QueryBuilder typeLessThan( + SparkCoinType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'type', + value: value, + )); + }); + } + + QueryBuilder typeBetween( + SparkCoinType lower, + SparkCoinType upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'type', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + valueIntStringEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'valueIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + valueIntStringGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'valueIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + valueIntStringLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'valueIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + valueIntStringBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'valueIntString', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + valueIntStringStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'valueIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + valueIntStringEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'valueIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + valueIntStringContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'valueIntString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + valueIntStringMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'valueIntString', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + valueIntStringIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'valueIntString', + value: '', + )); + }); + } + + QueryBuilder + valueIntStringIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'valueIntString', + value: '', + )); + }); + } + + QueryBuilder walletIdEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'walletId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'walletId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: '', + )); + }); + } + + QueryBuilder + walletIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'walletId', + value: '', + )); + }); + } +} + +extension SparkCoinQueryObject + on QueryBuilder {} + +extension SparkCoinQueryLinks + on QueryBuilder {} + +extension SparkCoinQuerySortBy on QueryBuilder { + QueryBuilder sortByAddress() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.asc); + }); + } + + QueryBuilder sortByAddressDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.desc); + }); + } + + QueryBuilder + sortByDiversifierIntString() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'diversifierIntString', Sort.asc); + }); + } + + QueryBuilder + sortByDiversifierIntStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'diversifierIntString', Sort.desc); + }); + } + + QueryBuilder sortByIsUsed() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isUsed', Sort.asc); + }); + } + + QueryBuilder sortByIsUsedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isUsed', Sort.desc); + }); + } + + QueryBuilder sortByLTagHash() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lTagHash', Sort.asc); + }); + } + + QueryBuilder sortByLTagHashDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lTagHash', Sort.desc); + }); + } + + QueryBuilder sortByMemo() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'memo', Sort.asc); + }); + } + + QueryBuilder sortByMemoDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'memo', Sort.desc); + }); + } + + QueryBuilder sortByTxHash() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txHash', Sort.asc); + }); + } + + QueryBuilder sortByTxHashDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txHash', Sort.desc); + }); + } + + QueryBuilder sortByType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.asc); + }); + } + + QueryBuilder sortByTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.desc); + }); + } + + QueryBuilder sortByValueIntString() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'valueIntString', Sort.asc); + }); + } + + QueryBuilder sortByValueIntStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'valueIntString', Sort.desc); + }); + } + + QueryBuilder sortByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder sortByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension SparkCoinQuerySortThenBy + on QueryBuilder { + QueryBuilder thenByAddress() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.asc); + }); + } + + QueryBuilder thenByAddressDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.desc); + }); + } + + QueryBuilder + thenByDiversifierIntString() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'diversifierIntString', Sort.asc); + }); + } + + QueryBuilder + thenByDiversifierIntStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'diversifierIntString', Sort.desc); + }); + } + + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenByIsUsed() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isUsed', Sort.asc); + }); + } + + QueryBuilder thenByIsUsedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isUsed', Sort.desc); + }); + } + + QueryBuilder thenByLTagHash() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lTagHash', Sort.asc); + }); + } + + QueryBuilder thenByLTagHashDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lTagHash', Sort.desc); + }); + } + + QueryBuilder thenByMemo() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'memo', Sort.asc); + }); + } + + QueryBuilder thenByMemoDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'memo', Sort.desc); + }); + } + + QueryBuilder thenByTxHash() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txHash', Sort.asc); + }); + } + + QueryBuilder thenByTxHashDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txHash', Sort.desc); + }); + } + + QueryBuilder thenByType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.asc); + }); + } + + QueryBuilder thenByTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.desc); + }); + } + + QueryBuilder thenByValueIntString() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'valueIntString', Sort.asc); + }); + } + + QueryBuilder thenByValueIntStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'valueIntString', Sort.desc); + }); + } + + QueryBuilder thenByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder thenByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension SparkCoinQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByAddress( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'address', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByDiversifierIntString( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'diversifierIntString', + caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByEncryptedDiversifier() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'encryptedDiversifier'); + }); + } + + QueryBuilder distinctByIsUsed() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isUsed'); + }); + } + + QueryBuilder distinctByK() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'k'); + }); + } + + QueryBuilder distinctByLTagHash( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'lTagHash', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByMemo( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'memo', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctBySerial() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'serial'); + }); + } + + QueryBuilder distinctBySerialContext() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'serialContext'); + }); + } + + QueryBuilder distinctByTag() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'tag'); + }); + } + + QueryBuilder distinctByTxHash( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'txHash', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByType() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'type'); + }); + } + + QueryBuilder distinctByValueIntString( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'valueIntString', + caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByWalletId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'walletId', caseSensitive: caseSensitive); + }); + } +} + +extension SparkCoinQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder addressProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'address'); + }); + } + + QueryBuilder + diversifierIntStringProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'diversifierIntString'); + }); + } + + QueryBuilder?, QQueryOperations> + encryptedDiversifierProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'encryptedDiversifier'); + }); + } + + QueryBuilder isUsedProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isUsed'); + }); + } + + QueryBuilder?, QQueryOperations> kProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'k'); + }); + } + + QueryBuilder lTagHashProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'lTagHash'); + }); + } + + QueryBuilder memoProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'memo'); + }); + } + + QueryBuilder?, QQueryOperations> serialProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'serial'); + }); + } + + QueryBuilder?, QQueryOperations> + serialContextProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'serialContext'); + }); + } + + QueryBuilder?, QQueryOperations> tagProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'tag'); + }); + } + + QueryBuilder txHashProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'txHash'); + }); + } + + QueryBuilder typeProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'type'); + }); + } + + QueryBuilder valueIntStringProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'valueIntString'); + }); + } + + QueryBuilder walletIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'walletId'); + }); + } +} diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 65c556039..eb2363df7 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -4,7 +4,9 @@ import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/extensions/impl/uint8_list.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; @@ -151,26 +153,81 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // have not yet parsed. Future refreshSparkData() async { try { + final privateKeyHex = "TODO"; + final index = 0; + final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); - // TODO improve performance by adding this call to the cached client - final anonymitySet = await electrumXClient.getSparkAnonymitySet( - coinGroupId: latestSparkCoinId.toString(), - ); + // TODO improve performance by adding these calls to the cached client + final futureResults = await Future.wait([ + electrumXClient.getSparkAnonymitySet( + coinGroupId: latestSparkCoinId.toString(), + ), + electrumXClient.getSparkUsedCoinsTags( + startNumber: 0, + ), + ]); - // TODO loop over set and see which coins are ours using the FFI call `identifyCoin` - List myCoins = []; + final anonymitySet = futureResults[0]; + final spentCoinTags = List.from( + futureResults[1]["tags"] as List, + ).toSet(); - // fetch metadata for myCoins + // find our coins + final List myCoins = []; + for (final data + in List>.from(anonymitySet["coin"] as List)) { + if (data.length != 2) { + throw Exception("Unexpected serialized coin info found"); + } - // check against spent list (this call could possibly also be cached later on) - final spentCoinTags = await electrumXClient.getSparkUsedCoinsTags( - startNumber: 0, - ); + final serializedCoin = data.first; + final txHash = data.last; - // create list of Spark Coin isar objects + final coin = LibSpark.identifyAndRecoverCoin( + serializedCoin, + privateKeyHex: privateKeyHex, + index: index, + ); + + // its ours + if (coin != null) { + final SparkCoinType coinType; + switch (coin.type.value) { + case 0: + coinType = SparkCoinType.mint; + case 1: + coinType = SparkCoinType.spend; + default: + throw Exception("Unknown spark coin type detected"); + } + myCoins.add( + SparkCoin( + walletId: walletId, + type: coinType, + isUsed: spentCoinTags + .contains(coin.lTagHash!.toHex), // TODO: is hex right? + address: coin.address!, + txHash: txHash, + valueIntString: coin.value!.toString(), + lTagHash: coin.lTagHash!.toHex, // TODO: is hex right? + tag: coin.tag, + memo: coin.memo, + serial: coin.serial, + serialContext: coin.serialContext, + diversifierIntString: coin.diversifier!.toString(), + encryptedDiversifier: coin.encryptedDiversifier, + ), + ); + } + } // update wallet spark coins in isar + if (myCoins.isNotEmpty) { + await mainDB.isar.writeTxn(() async { + await mainDB.isar.sparkCoins.putAll(myCoins); + }); + } // refresh spark balance? From fd8ca3edf8cd0d975619aab2e2668eb30991974c Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Nov 2023 09:43:40 -0600 Subject: [PATCH 04/77] tx v2 sent amount calc fix --- .../isar/models/blockchain_data/v2/transaction_v2.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index 61aea5bf0..d373d670f 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -84,7 +84,14 @@ class TransactionV2 { .where((e) => e.walletOwns) .fold(BigInt.zero, (p, e) => p + e.value); - return Amount(rawValue: inSum, fractionDigits: coin.decimals); + return Amount( + rawValue: inSum, + fractionDigits: coin.decimals, + ) - + getAmountReceivedThisWallet( + coin: coin, + ) - + getFee(coin: coin); } Set associatedAddresses() => { From 780a34b7dc005801adf40e7aa63a0f58b1a8b5f6 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 4 Dec 2023 10:46:34 -0600 Subject: [PATCH 05/77] eth token api endpoint update --- lib/services/ethereum/ethereum_api.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 8f0024774..b9a118352 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -611,7 +611,8 @@ abstract class EthereumAPI { try { final response = await client.get( url: Uri.parse( - "$stackBaseServer/tokens?addrs=$contractAddress&parts=all", + // "$stackBaseServer/tokens?addrs=$contractAddress&parts=all", + "$stackBaseServer/names?terms=$contractAddress", ), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.getProxyInfo() @@ -621,6 +622,10 @@ abstract class EthereumAPI { if (response.code == 200) { final json = jsonDecode(response.body) as Map; if (json["data"] is List) { + if ((json["data"] as List).isEmpty) { + throw EthApiException("Unknown token"); + } + final map = Map.from(json["data"].first as Map); EthContract? token; if (map["isErc20"] == true) { From 9ff323393e2eaddedf0a8ae13fcdd4d26d3c71de Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 31 Oct 2023 09:17:49 -0600 Subject: [PATCH 06/77] firo testnet testing enable --- .../add_wallet_views/add_wallet_view/add_wallet_view.dart | 2 +- lib/utilities/enums/coin_enum.dart | 2 +- lib/wallets/wallet/wallet.dart | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 419aa6d00..ff769f691 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -126,7 +126,7 @@ class _AddWalletViewState extends ConsumerState { void initState() { _searchFieldController = TextEditingController(); _searchFocusNode = FocusNode(); - _coinsTestnet.remove(Coin.firoTestNet); + // _coinsTestnet.remove(Coin.firoTestNet); if (Platform.isWindows) { _coins.remove(Coin.monero); _coins.remove(Coin.wownero); diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 67872e3b9..3bc1c1dd9 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -42,7 +42,7 @@ enum Coin { stellarTestnet, } -final int kTestNetCoinCount = 5; // Util.isDesktop ? 5 : 4; +final int kTestNetCoinCount = 6; // Util.isDesktop ? 5 : 4; // remove firotestnet for now extension CoinExt on Coin { diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 3d790e5b9..b4b1340ef 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -37,6 +37,7 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_int import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; abstract class Wallet { // default to Transaction class. For TransactionV2 set to 2 @@ -270,7 +271,7 @@ abstract class Wallet { case Coin.firo: return FiroWallet(CryptoCurrencyNetwork.main); case Coin.firoTestNet: - return FiroWallet(CryptoCurrencyNetwork.main); + return FiroWallet(CryptoCurrencyNetwork.test); case Coin.nano: return NanoWallet(CryptoCurrencyNetwork.main); @@ -439,6 +440,9 @@ abstract class Wallet { if (this is LelantusInterface) { await (this as LelantusInterface).refreshLelantusData(); } + if (this is SparkInterface) { + await (this as SparkInterface).refreshSparkData(); + } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.90, walletId)); await updateBalance(); From 656b3017540492031dfafa41db58dc36d94ad107 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 4 Dec 2023 16:12:52 -0600 Subject: [PATCH 07/77] remove unnecessary toHex cleaning up diff for stashing etc purposes --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index eb2363df7..f50c91781 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -4,7 +4,6 @@ import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; -import 'package:stackwallet/utilities/extensions/impl/uint8_list.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; @@ -205,12 +204,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { SparkCoin( walletId: walletId, type: coinType, - isUsed: spentCoinTags - .contains(coin.lTagHash!.toHex), // TODO: is hex right? + isUsed: spentCoinTags.contains(coin.lTagHash!), address: coin.address!, txHash: txHash, valueIntString: coin.value!.toString(), - lTagHash: coin.lTagHash!.toHex, // TODO: is hex right? + lTagHash: coin.lTagHash!, tag: coin.tag, memo: coin.memo, serial: coin.serial, From eaf14c2e8af4916f3969ca15eb2795b1636fe040 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 5 Dec 2023 00:00:30 -0600 Subject: [PATCH 08/77] hardcode key from test --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index f50c91781..0b44e083b 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -152,8 +152,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // have not yet parsed. Future refreshSparkData() async { try { - final privateKeyHex = "TODO"; - final index = 0; + const privateKeyHex = + "8acecb5844fbcb700706a90385d20e4752ec8aecb2d13b8f03d685374e2539b2"; + // This corresponds to the `jazz settle...` test mnemonic included in the + // plugin integration test. TODO move to test. + const index = 1; + // final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); From 0b0774b0b8822a187c2083df582c854c9693174f Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 5 Dec 2023 00:00:58 -0600 Subject: [PATCH 09/77] testnet --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 0b44e083b..7814f5528 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -178,8 +178,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // find our coins final List myCoins = []; - for (final data - in List>.from(anonymitySet["coin"] as List)) { + // for (final data + // in List>.from(anonymitySet["coin"] as List)) { + for (final data in anonymitySet["coins"] as List) { if (data.length != 2) { throw Exception("Unexpected serialized coin info found"); } @@ -188,9 +189,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final txHash = data.last; final coin = LibSpark.identifyAndRecoverCoin( - serializedCoin, + "$serializedCoin", privateKeyHex: privateKeyHex, index: index, + isTestNet: true, ); // its ours From 71e89b489f6fc69d6cbf3bb0f39dca33325d507b Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 5 Dec 2023 12:31:45 -0600 Subject: [PATCH 10/77] WIP spark scanning --- .../spark_interface.dart | 110 +++++++++++------- 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index eb2363df7..1521361aa 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -4,7 +4,7 @@ import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; -import 'package:stackwallet/utilities/extensions/impl/uint8_list.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; @@ -76,7 +76,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { const index = 1; final root = await getRootHDNode(); - const derivationPath = "$kSparkBaseDerivationPath$index"; + final String derivationPath; + if (cryptoCurrency.network == CryptoCurrencyNetwork.test) { + derivationPath = "$kSparkBaseDerivationPathTestnet$index"; + } else { + derivationPath = "$kSparkBaseDerivationPath$index"; + } final keys = root.derivePath(derivationPath); final String addressString = await LibSpark.getAddress( @@ -152,9 +157,20 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // recoverSparkWallet but only fetch and check anonymity set data that we // have not yet parsed. Future refreshSparkData() async { + final sparkAddresses = await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.spark) + .findAll(); + + final Set paths = + sparkAddresses.map((e) => e.derivationPath!.value).toSet(); + try { - final privateKeyHex = "TODO"; - final index = 0; + const index = 1; + + final root = await getRootHDNode(); final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); @@ -175,50 +191,58 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // find our coins final List myCoins = []; - for (final data - in List>.from(anonymitySet["coin"] as List)) { - if (data.length != 2) { - throw Exception("Unexpected serialized coin info found"); - } - final serializedCoin = data.first; - final txHash = data.last; + for (final path in paths) { + final keys = root.derivePath(path); - final coin = LibSpark.identifyAndRecoverCoin( - serializedCoin, - privateKeyHex: privateKeyHex, - index: index, - ); + final privateKeyHex = keys.privateKey.data.toHex; - // its ours - if (coin != null) { - final SparkCoinType coinType; - switch (coin.type.value) { - case 0: - coinType = SparkCoinType.mint; - case 1: - coinType = SparkCoinType.spend; - default: - throw Exception("Unknown spark coin type detected"); + for (final dynData in anonymitySet["coins"] as List) { + final data = List.from(dynData as List); + + if (data.length != 2) { + throw Exception("Unexpected serialized coin info found"); } - myCoins.add( - SparkCoin( - walletId: walletId, - type: coinType, - isUsed: spentCoinTags - .contains(coin.lTagHash!.toHex), // TODO: is hex right? - address: coin.address!, - txHash: txHash, - valueIntString: coin.value!.toString(), - lTagHash: coin.lTagHash!.toHex, // TODO: is hex right? - tag: coin.tag, - memo: coin.memo, - serial: coin.serial, - serialContext: coin.serialContext, - diversifierIntString: coin.diversifier!.toString(), - encryptedDiversifier: coin.encryptedDiversifier, - ), + + final serializedCoin = data.first; + final txHash = data.last; + + final coin = LibSpark.identifyAndRecoverCoin( + serializedCoin, + privateKeyHex: privateKeyHex, + index: index, + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, ); + + // its ours + if (coin != null) { + final SparkCoinType coinType; + switch (coin.type.value) { + case 0: + coinType = SparkCoinType.mint; + case 1: + coinType = SparkCoinType.spend; + default: + throw Exception("Unknown spark coin type detected"); + } + myCoins.add( + SparkCoin( + walletId: walletId, + type: coinType, + isUsed: spentCoinTags.contains(coin.lTagHash!), + address: coin.address!, + txHash: txHash, + valueIntString: coin.value!.toString(), + lTagHash: coin.lTagHash!, + tag: coin.tag, + memo: coin.memo, + serial: coin.serial, + serialContext: coin.serialContext, + diversifierIntString: coin.diversifier!.toString(), + encryptedDiversifier: coin.encryptedDiversifier, + ), + ); + } } } From 051bd7db4851e9167dd7c8f1178b046907dfeb6e Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 5 Dec 2023 14:44:50 -0600 Subject: [PATCH 11/77] WIP spark scanning txhash correction --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 1521361aa..cf276b664 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; @@ -205,7 +206,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } final serializedCoin = data.first; - final txHash = data.last; + final txHash = base64ToReverseHex(data.last); final coin = LibSpark.identifyAndRecoverCoin( serializedCoin, @@ -332,3 +333,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { await normalBalanceFuture; } } + +String base64ToReverseHex(String source) => + base64Decode(LineSplitter.split(source).join()) + .reversed + .map((e) => e.toRadixString(16).padLeft(2, '0')) + .join(); From 658901ff038893f954d69f558603b5099995da5c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 5 Dec 2023 16:55:38 -0600 Subject: [PATCH 12/77] WIP spark scanning --- .../spark_interface.dart | 112 +++++++++++------- 1 file changed, 66 insertions(+), 46 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 7814f5528..1521361aa 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -4,6 +4,7 @@ import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; @@ -75,7 +76,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { const index = 1; final root = await getRootHDNode(); - const derivationPath = "$kSparkBaseDerivationPath$index"; + final String derivationPath; + if (cryptoCurrency.network == CryptoCurrencyNetwork.test) { + derivationPath = "$kSparkBaseDerivationPathTestnet$index"; + } else { + derivationPath = "$kSparkBaseDerivationPath$index"; + } final keys = root.derivePath(derivationPath); final String addressString = await LibSpark.getAddress( @@ -151,13 +157,20 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // recoverSparkWallet but only fetch and check anonymity set data that we // have not yet parsed. Future refreshSparkData() async { + final sparkAddresses = await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.spark) + .findAll(); + + final Set paths = + sparkAddresses.map((e) => e.derivationPath!.value).toSet(); + try { - const privateKeyHex = - "8acecb5844fbcb700706a90385d20e4752ec8aecb2d13b8f03d685374e2539b2"; - // This corresponds to the `jazz settle...` test mnemonic included in the - // plugin integration test. TODO move to test. const index = 1; - // + + final root = await getRootHDNode(); final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); @@ -178,51 +191,58 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // find our coins final List myCoins = []; - // for (final data - // in List>.from(anonymitySet["coin"] as List)) { - for (final data in anonymitySet["coins"] as List) { - if (data.length != 2) { - throw Exception("Unexpected serialized coin info found"); - } - final serializedCoin = data.first; - final txHash = data.last; + for (final path in paths) { + final keys = root.derivePath(path); - final coin = LibSpark.identifyAndRecoverCoin( - "$serializedCoin", - privateKeyHex: privateKeyHex, - index: index, - isTestNet: true, - ); + final privateKeyHex = keys.privateKey.data.toHex; - // its ours - if (coin != null) { - final SparkCoinType coinType; - switch (coin.type.value) { - case 0: - coinType = SparkCoinType.mint; - case 1: - coinType = SparkCoinType.spend; - default: - throw Exception("Unknown spark coin type detected"); + for (final dynData in anonymitySet["coins"] as List) { + final data = List.from(dynData as List); + + if (data.length != 2) { + throw Exception("Unexpected serialized coin info found"); } - myCoins.add( - SparkCoin( - walletId: walletId, - type: coinType, - isUsed: spentCoinTags.contains(coin.lTagHash!), - address: coin.address!, - txHash: txHash, - valueIntString: coin.value!.toString(), - lTagHash: coin.lTagHash!, - tag: coin.tag, - memo: coin.memo, - serial: coin.serial, - serialContext: coin.serialContext, - diversifierIntString: coin.diversifier!.toString(), - encryptedDiversifier: coin.encryptedDiversifier, - ), + + final serializedCoin = data.first; + final txHash = data.last; + + final coin = LibSpark.identifyAndRecoverCoin( + serializedCoin, + privateKeyHex: privateKeyHex, + index: index, + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, ); + + // its ours + if (coin != null) { + final SparkCoinType coinType; + switch (coin.type.value) { + case 0: + coinType = SparkCoinType.mint; + case 1: + coinType = SparkCoinType.spend; + default: + throw Exception("Unknown spark coin type detected"); + } + myCoins.add( + SparkCoin( + walletId: walletId, + type: coinType, + isUsed: spentCoinTags.contains(coin.lTagHash!), + address: coin.address!, + txHash: txHash, + valueIntString: coin.value!.toString(), + lTagHash: coin.lTagHash!, + tag: coin.tag, + memo: coin.memo, + serial: coin.serial, + serialContext: coin.serialContext, + diversifierIntString: coin.diversifier!.toString(), + encryptedDiversifier: coin.encryptedDiversifier, + ), + ); + } } } From 56e11400a275db4fb0a4cfa9358f36f2da83f22a Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 5 Dec 2023 14:44:50 -0600 Subject: [PATCH 13/77] WIP spark scanning txhash correction --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 1521361aa..cf276b664 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; @@ -205,7 +206,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } final serializedCoin = data.first; - final txHash = data.last; + final txHash = base64ToReverseHex(data.last); final coin = LibSpark.identifyAndRecoverCoin( serializedCoin, @@ -332,3 +333,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { await normalBalanceFuture; } } + +String base64ToReverseHex(String source) => + base64Decode(LineSplitter.split(source).join()) + .reversed + .map((e) => e.toRadixString(16).padLeft(2, '0')) + .join(); From 883a5e67e63a19065cc196b581e140b32b648858 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 7 Dec 2023 10:56:45 -0600 Subject: [PATCH 14/77] WIP spark mint transaction --- .../wallet_mixin_interfaces/spark_interface.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index cf276b664..133a09787 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -318,6 +318,18 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // first then we generate spark related data. And we sign like regular // transactions at the end. + List serialContext = []; + + final mintRecipients = LibSpark.createSparkMintRecipients( + outputs: txData.utxos! + .map((e) => ( + sparkAddress: e.address!, + value: e.value, + memo: "Stackwallet spark mint" + )) + .toList(), + serialContext: Uint8List.fromList(serialContext), + ); throw UnimplementedError(); } From 46796f02ddd60b6f6ff783b4d88d41d27268122d Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 7 Dec 2023 10:57:54 -0600 Subject: [PATCH 15/77] WIP spark mint transaction fix --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 133a09787..2e0d0f2c0 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -321,10 +321,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { List serialContext = []; final mintRecipients = LibSpark.createSparkMintRecipients( - outputs: txData.utxos! + outputs: txData.recipients! .map((e) => ( - sparkAddress: e.address!, - value: e.value, + sparkAddress: e.address, + value: e.amount.raw.toInt(), memo: "Stackwallet spark mint" )) .toList(), From 095bfc2ff3ea424a8eb4c044f587a783d0a9fcb7 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 7 Dec 2023 10:56:45 -0600 Subject: [PATCH 16/77] WIP spark mint transaction --- .../wallet_mixin_interfaces/spark_interface.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index cf276b664..133a09787 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -318,6 +318,18 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // first then we generate spark related data. And we sign like regular // transactions at the end. + List serialContext = []; + + final mintRecipients = LibSpark.createSparkMintRecipients( + outputs: txData.utxos! + .map((e) => ( + sparkAddress: e.address!, + value: e.value, + memo: "Stackwallet spark mint" + )) + .toList(), + serialContext: Uint8List.fromList(serialContext), + ); throw UnimplementedError(); } From 2e19dd8545b927b656774e0cd659b768acaa4b37 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 7 Dec 2023 10:57:54 -0600 Subject: [PATCH 17/77] WIP spark mint transaction fix --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 133a09787..2e0d0f2c0 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -321,10 +321,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { List serialContext = []; final mintRecipients = LibSpark.createSparkMintRecipients( - outputs: txData.utxos! + outputs: txData.recipients! .map((e) => ( - sparkAddress: e.address!, - value: e.value, + sparkAddress: e.address, + value: e.amount.raw.toInt(), memo: "Stackwallet spark mint" )) .toList(), From dd01444ff5ec9ac73843c6f8429d36dfbe209ecf Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 7 Dec 2023 14:46:46 -0600 Subject: [PATCH 18/77] add refresh spark data hidden button --- .../global_settings_view/hidden_settings.dart | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 04bdc18e3..d14a6fd5b 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -23,8 +23,10 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/onetime_popups/tor_has_been_add_dialog.dart'; @@ -552,6 +554,66 @@ class HiddenSettings extends StatelessWidget { ); }, ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + try { + // Run refreshSparkData. + // + // Search wallets for a Firo testnet wallet. + for (final wallet + in ref.read(pWallets).wallets) { + if (!(wallet.info.coin == + Coin.firoTestNet)) { + continue; + } + // This is a Firo testnet wallet. + final walletId = wallet.info.walletId; + + // // Search for `circle chunk...` mnemonic. + // final potentialWallet = + // ref.read(pWallets).getWallet(walletId) + // as MnemonicInterface; + // final mnemonic = await potentialWallet + // .getMnemonicAsWords(); + // if (!(mnemonic[0] == "circle" && + // mnemonic[1] == "chunk)")) { + // // That ain't it. Skip this one. + // return; + // } + // Hardcode key in refreshSparkData instead. + + // Get a Spark interface. + final fusionWallet = ref + .read(pWallets) + .getWallet(walletId) as SparkInterface; + + // Refresh Spark data. + await fusionWallet.refreshSparkData(); + + // We only need to run this once. + break; + } + } catch (e, s) { + print("$e\n$s"); + } + }, + child: RoundedWhiteContainer( + child: Text( + "Refresh Spark wallet", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ); + }, + ), // const SizedBox( // height: 12, // ), From f30e9966553a5da2b52b3b1a16a96049273c5e53 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 7 Dec 2023 14:55:40 -0600 Subject: [PATCH 19/77] dummy hidden settings prepare spark mint button --- .../global_settings_view/hidden_settings.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index d14a6fd5b..a430f3f63 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -614,6 +615,55 @@ class HiddenSettings extends StatelessWidget { ); }, ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + try { + // Run prepareSparkMintTransaction. + for (final wallet + in ref.read(pWallets).wallets) { + // Prepare tx with a Firo testnet wallet. + if (!(wallet.info.coin == + Coin.firoTestNet)) { + continue; + } + final walletId = wallet.info.walletId; + + // Get a Spark interface. + final fusionWallet = ref + .read(pWallets) + .getWallet(walletId) as SparkInterface; + + // Make a dummy TxData. + TxData txData = TxData(); // TODO + + await fusionWallet + .prepareSparkMintTransaction( + txData: txData); + + // We only need to run this once. + break; + } + } catch (e, s) { + print("$e\n$s"); + } + }, + child: RoundedWhiteContainer( + child: Text( + "Prepare Spark mint transaction", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ); + }, + ), // const SizedBox( // height: 12, // ), From 5567d96f5a8690e94278fe7630e8302f0fbb92bd Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 7 Dec 2023 15:05:27 -0600 Subject: [PATCH 20/77] confirmSparkMintTransaction --- .../spark_interface.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 2e0d0f2c0..7424047d6 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -333,6 +333,23 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { throw UnimplementedError(); } + /// Broadcast a tx and TODO update Spark balance. + Future confirmSparkMintTransaction({required TxData txData}) async { + // Broadcast tx. + final txid = await electrumXClient.broadcastTransaction( + rawTx: txData.raw!, + ); + + // Check txid. + assert(txid == txData.txid!); + + // TODO update spark balance. + + return txData.copyWith( + txid: txid, + ); + } + @override Future updateBalance() async { // call to super to update transparent balance (and lelantus balance if From 6507ebd346a3c5d2fdd4cd0e0cfa8f54ecd0435b Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 7 Dec 2023 15:30:35 -0600 Subject: [PATCH 21/77] correct comment --- lib/electrumx_rpc/electrumx_client.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 00ec67602..b6df39e35 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -881,7 +881,7 @@ class ElectrumXClient { /// /// Returns blockHash (last block hash), /// setHash (hash of current set) - /// and mints (the list of pairs serialized coin and tx hash) + /// and coins (the list of pairs serialized coin and tx hash) Future> getSparkAnonymitySet({ String coinGroupId = "1", String startBlockHash = "", From 5f4ef72e6409854a2b5b1ac0f07497d0673aff3d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 7 Dec 2023 15:58:23 -0600 Subject: [PATCH 22/77] validation in prepareSparkMintTransaction and TODOs --- .../spark_interface.dart | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 7424047d6..13bfc2e5f 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -300,12 +300,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } } - /// Transparent to Spark (mint) transaction creation + /// Transparent to Spark (mint) transaction creation. Future prepareSparkMintTransaction({required TxData txData}) async { // https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o/edit // this kind of transaction is generated like a regular transaction, but in - // place of regulart outputs we put spark outputs, so for that we call + // place of regular outputs we put spark outputs, so for that we call // createSparkMintRecipients function, we get spark related data, // everything else we do like for regular transaction, and we put CRecipient // object as a tx outputs, we need to keep the order.. @@ -318,8 +318,35 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // first then we generate spark related data. And we sign like regular // transactions at the end. - List serialContext = []; + // Validate inputs. + // There should be at least one recipient. + if (txData.recipients == null || txData.recipients!.isEmpty) { + throw Exception("No recipients provided"); + } + + // For now let's limit to one recipient. + if (txData.recipients!.length > 1) { + throw Exception("Only one recipient supported"); + } + + // Limit outputs per tx to 16. + // + // See SPARK_OUT_LIMIT_PER_TX at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16 + // TODO limit outputs to 16. + + // Limit spend value per tx to 1000000000000 satoshis. + // + // See SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L17 + // and COIN https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L17 + // Note that as MAX_MONEY is greater than this limit, we can ignore it. See https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L31 + // TODO limit spend value per tx to 1000000000000 satoshis. + + // Create the serial context. + List serialContext = []; + // TODO set serialContext to the serialized inputs. + + // Create outputs. final mintRecipients = LibSpark.createSparkMintRecipients( outputs: txData.recipients! .map((e) => ( @@ -330,6 +357,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .toList(), serialContext: Uint8List.fromList(serialContext), ); + + // TODO Add outputs to txData. + throw UnimplementedError(); } From b1e4627837c3f19f5b5cacfaf6775da4fcab36cd Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 13 Dec 2023 11:26:30 -0600 Subject: [PATCH 23/77] WIP spark spend --- lib/wallets/models/tx_data.dart | 20 ++ .../spark_interface.dart | 286 +++++++++++++++++- 2 files changed, 295 insertions(+), 11 deletions(-) diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 455790f67..0cb1bcf49 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -55,6 +55,15 @@ class TxData { // tezos specific final tezart.OperationsList? tezosOperationsList; + // firo spark specific + final List< + ({ + String address, + Amount amount, + bool subtractFeeFromAmount, + String memo, + })>? sparkRecipients; + TxData({ this.feeRateType, this.feeRateAmount, @@ -85,6 +94,7 @@ class TxData { this.txSubType, this.mintsMapLelantus, this.tezosOperationsList, + this.sparkRecipients, }); Amount? get amount => recipients != null && recipients!.isNotEmpty @@ -127,6 +137,14 @@ class TxData { TransactionSubType? txSubType, List>? mintsMapLelantus, tezart.OperationsList? tezosOperationsList, + List< + ({ + String address, + Amount amount, + bool subtractFeeFromAmount, + String memo, + })>? + sparkRecipients, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -159,6 +177,7 @@ class TxData { txSubType: txSubType ?? this.txSubType, mintsMapLelantus: mintsMapLelantus ?? this.mintsMapLelantus, tezosOperationsList: tezosOperationsList ?? this.tezosOperationsList, + sparkRecipients: sparkRecipients ?? this.sparkRecipients, ); } @@ -193,5 +212,6 @@ class TxData { 'txSubType: $txSubType, ' 'mintsMapLelantus: $mintsMapLelantus, ' 'tezosOperationsList: $tezosOperationsList, ' + 'sparkRecipients: $sparkRecipients, ' '}'; } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 2e0d0f2c0..b1c1245d4 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:bitcoindart/bitcoindart.dart' as btc; +import 'package:bitcoindart/src/utils/script.dart' as bscript; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; @@ -57,15 +59,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .findFirst(); } - Future _getSpendKey() async { - final mnemonic = await getMnemonic(); - final mnemonicPassphrase = await getMnemonicPassphrase(); - - // TODO call ffi lib to generate spend key - - throw UnimplementedError(); - } - Future

generateNextSparkAddress() async { final highestStoredDiversifier = (await getCurrentReceivingSparkAddress())?.derivationIndex; @@ -111,11 +104,46 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { Future prepareSendSpark({ required TxData txData, }) async { + // todo fetch + final List serializedMintMetas = []; + final List myCoins = []; + + final currentId = await electrumXClient.getSparkLatestCoinId(); + final List> setMaps = []; + for (int i = 0; i <= currentId; i++) { + final set = await electrumXClient.getSparkAnonymitySet( + coinGroupId: i.toString(), + ); + set["coinGroupID"] = i; + setMaps.add(set); + } + + final allAnonymitySets = setMaps + .map((e) => ( + setId: e["coinGroupID"] as int, + setHash: e["setHash"] as String, + set: (e["coins"] as List) + .map((e) => ( + serializedCoin: e[0] as String, + txHash: e[1] as String, + )) + .toList(), + )) + .toList(); + // https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o/edit // To generate a spark spend we need to call createSparkSpendTransaction, // first unlock the wallet and generate all 3 spark keys, - final spendKey = await _getSpendKey(); + const index = 1; + final root = await getRootHDNode(); + final String derivationPath; + if (cryptoCurrency.network == CryptoCurrencyNetwork.test) { + derivationPath = "$kSparkBaseDerivationPathTestnet$index"; + } else { + derivationPath = "$kSparkBaseDerivationPath$index"; + } + final privateKey = root.derivePath(derivationPath).privateKey.data; // // recipients is a list of pairs of amounts and bools, this is for transparent // outputs, first how much to send and second, subtractFeeFromAmount argument @@ -144,7 +172,227 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // outputScripts is a output data, it is a list of scripts, which we need // to put in separate tx outputs, and keep the order, - throw UnimplementedError(); + // Amount vOut = Amount( + // rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits); + // Amount mintVOut = Amount( + // rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits); + // int recipientsToSubtractFee = 0; + // + // for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { + // vOut += txData.recipients![i].amount; + // } + // + // if (vOut.raw > BigInt.from(SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION)) { + // throw Exception( + // "Spend to transparent address limit exceeded (10,000 Firo per transaction).", + // ); + // } + // + // for (int i = 0; i < (txData.sparkRecipients?.length ?? 0); i++) { + // mintVOut += txData.sparkRecipients![i].amount; + // if (txData.sparkRecipients![i].subtractFeeFromAmount) { + // recipientsToSubtractFee++; + // } + // } + // + // int fee; + + final txb = btc.TransactionBuilder( + network: btc.NetworkType( + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: btc.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, + ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ), + ); + txb.setLockTime(await chainHeight); + txb.setVersion(3 | (9 << 16)); + + // final estimated = LibSpark.selectSparkCoins( + // requiredAmount: mintVOut.raw.toInt(), + // subtractFeeFromAmount: recipientsToSubtractFee > 0, + // coins: myCoins, + // privateRecipientsCount: txData.sparkRecipients?.length ?? 0, + // ); + // + // fee = estimated.fee; + // bool remainderSubtracted = false; + + // for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { + // + // + // if (recipient.fSubtractFeeFromAmount) { + // // Subtract fee equally from each selected recipient. + // recipient.nAmount -= fee / recipientsToSubtractFee; + // + // if (!remainderSubtracted) { + // // First receiver pays the remainder not divisible by output count. + // recipient.nAmount -= fee % recipientsToSubtractFee; + // remainderSubtracted = true; + // } + // } + // } + + // outputs + + // for (int i = 0; i < (txData.sparkRecipients?.length ?? 0); i++) { + // if (txData.sparkRecipients![i].subtractFeeFromAmount) { + // BigInt amount = txData.sparkRecipients![i].amount.raw; + // + // // Subtract fee equally from each selected recipient. + // amount -= BigInt.from(fee / recipientsToSubtractFee); + // + // if (!remainderSubtracted) { + // // First receiver pays the remainder not divisible by output count. + // amount -= BigInt.from(fee % recipientsToSubtractFee); + // remainderSubtracted = true; + // } + // + // txData.sparkRecipients![i] = ( + // address: txData.sparkRecipients![i].address, + // amount: Amount( + // rawValue: amount, + // fractionDigits: cryptoCurrency.fractionDigits, + // ), + // subtractFeeFromAmount: + // txData.sparkRecipients![i].subtractFeeFromAmount, + // memo: txData.sparkRecipients![i].memo, + // ); + // } + // } + // + // int spendInCurrentTx = 0; + // for (final spendCoin in estimated.coins) { + // spendInCurrentTx += spendCoin.value?.toInt() ?? 0; + // } + // spendInCurrentTx -= fee; + // + // int transparentOut = 0; + + for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { + if (txData.recipients![i].amount.raw == BigInt.zero) { + continue; + } + if (txData.recipients![i].amount < cryptoCurrency.dustLimit) { + throw Exception("Output below dust limit"); + } + // + // transparentOut += txData.recipients![i].amount.raw.toInt(); + txb.addOutput( + txData.recipients![i].address, + txData.recipients![i].amount.raw.toInt(), + ); + } + + // // spendInCurrentTx -= transparentOut; + // final List<({String address, int amount, String memo})> privOutputs = []; + // + // for (int i = 0; i < (txData.sparkRecipients?.length ?? 0); i++) { + // if (txData.sparkRecipients![i].amount.raw == BigInt.zero) { + // continue; + // } + // + // final recipientAmount = txData.sparkRecipients![i].amount.raw.toInt(); + // // spendInCurrentTx -= recipientAmount; + // + // privOutputs.add( + // ( + // address: txData.sparkRecipients![i].address, + // amount: recipientAmount, + // memo: txData.sparkRecipients![i].memo, + // ), + // ); + // } + + // if (spendInCurrentTx < 0) { + // throw Exception("Unable to create spend transaction."); + // } + // + // if (privOutputs.isEmpty || spendInCurrentTx > 0) { + // final changeAddress = await LibSpark.getAddress( + // privateKey: privateKey, + // index: index, + // diversifier: kSparkChange, + // ); + // + // privOutputs.add( + // ( + // address: changeAddress, + // amount: spendInCurrentTx > 0 ? spendInCurrentTx : 0, + // memo: "", + // ), + // ); + // } + + // inputs + + final opReturnScript = bscript.compile([ + 0xd3, // OP_SPARKSPEND + Uint8List(0), + ]); + + txb.addInput( + '0000000000000000000000000000000000000000000000000000000000000000', + 0xffffffff, + 0xffffffff, + opReturnScript, + ); + + // final sig = extractedTx.getId(); + + // for (final coin in estimated.coins) { + // final groupId = coin.id!; + // } + + final spend = LibSpark.createSparkSendTransaction( + privateKeyHex: privateKey.toHex, + index: index, + recipients: [], + privateRecipients: txData.sparkRecipients + ?.map((e) => ( + sparkAddress: e.address, + amount: e.amount.raw.toInt(), + subtractFeeFromAmount: e.subtractFeeFromAmount, + memo: e.memo, + )) + .toList() ?? + [], + serializedMintMetas: serializedMintMetas, + allAnonymitySets: allAnonymitySets, + ); + + print("SPARK SPEND ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); + print("fee: ${spend.fee}"); + print("spend: ${spend.serializedSpendPayload}"); + print("scripts:"); + spend.outputScripts.forEach(print); + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); + + for (final outputScript in spend.outputScripts) { + txb.addOutput(outputScript, 0); + } + + final extractedTx = txb.buildIncomplete(); + + // TODO: verify encoding + extractedTx.setPayload(spend.serializedSpendPayload.toUint8ListFromUtf8); + + final rawTxHex = extractedTx.toHex(); + + return txData.copyWith( + raw: rawTxHex, + vSize: extractedTx.virtualSize(), + fee: Amount( + rawValue: BigInt.from(spend.fee), + fractionDigits: cryptoCurrency.fractionDigits, + ), + // TODO used coins + ); } // this may not be needed for either mints or spends or both @@ -247,6 +495,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } } + print("FOUND COINS: $myCoins"); + // update wallet spark coins in isar if (myCoins.isNotEmpty) { await mainDB.isar.writeTxn(() async { @@ -256,6 +506,20 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // refresh spark balance? + await prepareSendSpark( + txData: TxData( + sparkRecipients: [ + ( + address: (await getCurrentReceivingSparkAddress())!.value, + amount: Amount( + rawValue: BigInt.from(100000000), + fractionDigits: cryptoCurrency.fractionDigits), + subtractFeeFromAmount: true, + memo: "LOL MEMO OPK", + ), + ], + )); + throw UnimplementedError(); } catch (e, s) { // todo logging From 7dcac56a5ad2f6a6ce5dacde8a3ee5512e69922c Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 13 Dec 2023 11:36:37 -0600 Subject: [PATCH 24/77] WIP cached spark anon set electrumx call --- lib/db/hive/db.dart | 13 +++++ .../cached_electrumx_client.dart | 53 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/lib/db/hive/db.dart b/lib/db/hive/db.dart index 00fdc13f7..85e6c9550 100644 --- a/lib/db/hive/db.dart +++ b/lib/db/hive/db.dart @@ -53,6 +53,8 @@ class DB { // firo only String _boxNameSetCache({required Coin coin}) => "${coin.name}_anonymitySetCache"; + String _boxNameSetSparkCache({required Coin coin}) => + "${coin.name}_anonymitySetSparkCache"; String _boxNameUsedSerialsCache({required Coin coin}) => "${coin.name}_usedSerialsCache"; @@ -75,6 +77,7 @@ class DB { final Map> _txCacheBoxes = {}; final Map> _setCacheBoxes = {}; + final Map> _setSparkCacheBoxes = {}; final Map> _usedSerialsCacheBoxes = {}; // exposed for monero @@ -197,6 +200,15 @@ class DB { await Hive.openBox(_boxNameSetCache(coin: coin)); } + Future> getSparkAnonymitySetCacheBox( + {required Coin coin}) async { + if (_setSparkCacheBoxes[coin]?.isOpen != true) { + _setSparkCacheBoxes.remove(coin); + } + return _setSparkCacheBoxes[coin] ??= + await Hive.openBox(_boxNameSetSparkCache(coin: coin)); + } + Future closeAnonymitySetCacheBox({required Coin coin}) async { await _setCacheBoxes[coin]?.close(); } @@ -218,6 +230,7 @@ class DB { await deleteAll(boxName: _boxNameTxCache(coin: coin)); if (coin == Coin.firo) { await deleteAll(boxName: _boxNameSetCache(coin: coin)); + await deleteAll(boxName: _boxNameSetSparkCache(coin: coin)); await deleteAll(boxName: _boxNameUsedSerialsCache(coin: coin)); } } diff --git a/lib/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index 0cec664b8..b1bfadc76 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -107,6 +107,59 @@ class CachedElectrumXClient { } } + Future> getSparkAnonymitySet({ + required String groupId, + String blockhash = "", + required Coin coin, + }) async { + try { + final box = await DB.instance.getSparkAnonymitySetCacheBox(coin: coin); + final cachedSet = box.get(groupId) as Map?; + + Map set; + + // null check to see if there is a cached set + if (cachedSet == null) { + set = { + "setId": groupId, + "blockHash": blockhash, + "setHash": "", + "coins": [], + }; + } else { + set = Map.from(cachedSet); + } + + final newSet = await electrumXClient.getSparkAnonymitySet( + coinGroupId: groupId, + startBlockHash: set["blockHash"] as String, + ); + + // update set with new data + if (newSet["setHash"] != "" && set["setHash"] != newSet["setHash"]) { + set["setHash"] = newSet["setHash"]; + set["blockHash"] = newSet["blockHash"]; + for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) { + // TODO verify this is correct (or append?) + set["coins"].insert(0, newSet["coins"][i]); + } + // save set to db + await box.put(groupId, set); + Logging.instance.log( + "Updated current anonymity set for ${coin.name} with group ID $groupId", + level: LogLevel.Info, + ); + } + + return set; + } catch (e, s) { + Logging.instance.log( + "Failed to process CachedElectrumX.getAnonymitySet(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + String base64ToHex(String source) => base64Decode(LineSplitter.split(source).join()) .map((e) => e.toRadixString(16).padLeft(2, '0')) From e1241372bff35a919c352ee8cba98f65ebbdb6c8 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 13 Dec 2023 14:13:11 -0600 Subject: [PATCH 25/77] cached spark anon set electrumx call fixes and usage --- lib/db/hive/db.dart | 2 +- .../cached_electrumx_client.dart | 3 ++- .../spark_interface.dart | 20 ++++++++++--------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/db/hive/db.dart b/lib/db/hive/db.dart index 85e6c9550..8c90593f5 100644 --- a/lib/db/hive/db.dart +++ b/lib/db/hive/db.dart @@ -228,7 +228,7 @@ class DB { /// Clear all cached transactions for the specified coin Future clearSharedTransactionCache({required Coin coin}) async { await deleteAll(boxName: _boxNameTxCache(coin: coin)); - if (coin == Coin.firo) { + if (coin == Coin.firo || coin == Coin.firoTestNet) { await deleteAll(boxName: _boxNameSetCache(coin: coin)); await deleteAll(boxName: _boxNameSetSparkCache(coin: coin)); await deleteAll(boxName: _boxNameUsedSerialsCache(coin: coin)); diff --git a/lib/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index b1bfadc76..87a330bc2 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -121,7 +121,7 @@ class CachedElectrumXClient { // null check to see if there is a cached set if (cachedSet == null) { set = { - "setId": groupId, + "coinGroupID": int.parse(groupId), "blockHash": blockhash, "setHash": "", "coins": [], @@ -259,6 +259,7 @@ class CachedElectrumXClient { /// Clear all cached transactions for the specified coin Future clearSharedTransactionCache({required Coin coin}) async { + await DB.instance.clearSharedTransactionCache(coin: coin); await DB.instance.closeAnonymitySetCacheBox(coin: coin); } } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index b1c1245d4..e1d583ca7 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -110,9 +110,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final currentId = await electrumXClient.getSparkLatestCoinId(); final List> setMaps = []; - for (int i = 0; i <= currentId; i++) { - final set = await electrumXClient.getSparkAnonymitySet( - coinGroupId: i.toString(), + // for (int i = 0; i <= currentId; i++) { + for (int i = currentId; i <= currentId; i++) { + final set = await electrumXCachedClient.getSparkAnonymitySet( + groupId: i.toString(), + coin: info.coin, ); set["coinGroupID"] = i; setMaps.add(set); @@ -423,10 +425,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); - // TODO improve performance by adding these calls to the cached client final futureResults = await Future.wait([ - electrumXClient.getSparkAnonymitySet( - coinGroupId: latestSparkCoinId.toString(), + electrumXCachedClient.getSparkAnonymitySet( + groupId: latestSparkCoinId.toString(), + coin: info.coin, ), electrumXClient.getSparkUsedCoinsTags( startNumber: 0, @@ -542,9 +544,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); - // TODO improve performance by adding this call to the cached client - final anonymitySet = await electrumXClient.getSparkAnonymitySet( - coinGroupId: latestSparkCoinId.toString(), + final anonymitySet = await electrumXCachedClient.getSparkAnonymitySet( + groupId: latestSparkCoinId.toString(), + coin: info.coin, ); // TODO loop over set and see which coins are ours using the FFI call `identifyCoin` From cf2114b7a3911dd4769a2d28424b679b1f23eb3f Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 13 Dec 2023 16:15:59 -0600 Subject: [PATCH 26/77] cached spark used coin tags electrumx call --- lib/db/hive/db.dart | 15 ++++++ .../cached_electrumx_client.dart | 53 +++++++++++++++++-- lib/electrumx_rpc/electrumx_client.dart | 5 +- .../global_settings_view/hidden_settings.dart | 2 +- .../spark_interface.dart | 10 ++-- 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/lib/db/hive/db.dart b/lib/db/hive/db.dart index 8c90593f5..1ae6de2c2 100644 --- a/lib/db/hive/db.dart +++ b/lib/db/hive/db.dart @@ -57,6 +57,8 @@ class DB { "${coin.name}_anonymitySetSparkCache"; String _boxNameUsedSerialsCache({required Coin coin}) => "${coin.name}_usedSerialsCache"; + String _boxNameSparkUsedCoinsTagsCache({required Coin coin}) => + "${coin.name}_sparkUsedCoinsTagsCache"; Box? _boxNodeModels; Box? _boxPrimaryNodes; @@ -79,6 +81,7 @@ class DB { final Map> _setCacheBoxes = {}; final Map> _setSparkCacheBoxes = {}; final Map> _usedSerialsCacheBoxes = {}; + final Map> _getSparkUsedCoinsTagsCacheBoxes = {}; // exposed for monero Box get moneroWalletInfoBox => _walletInfoSource!; @@ -221,6 +224,16 @@ class DB { await Hive.openBox(_boxNameUsedSerialsCache(coin: coin)); } + Future> getSparkUsedCoinsTagsCacheBox( + {required Coin coin}) async { + if (_getSparkUsedCoinsTagsCacheBoxes[coin]?.isOpen != true) { + _getSparkUsedCoinsTagsCacheBoxes.remove(coin); + } + return _getSparkUsedCoinsTagsCacheBoxes[coin] ??= + await Hive.openBox( + _boxNameSparkUsedCoinsTagsCache(coin: coin)); + } + Future closeUsedSerialsCacheBox({required Coin coin}) async { await _usedSerialsCacheBoxes[coin]?.close(); } @@ -232,6 +245,8 @@ class DB { await deleteAll(boxName: _boxNameSetCache(coin: coin)); await deleteAll(boxName: _boxNameSetSparkCache(coin: coin)); await deleteAll(boxName: _boxNameUsedSerialsCache(coin: coin)); + await deleteAll( + boxName: _boxNameSparkUsedCoinsTagsCache(coin: coin)); } } diff --git a/lib/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index 87a330bc2..036517698 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -154,7 +154,7 @@ class CachedElectrumXClient { return set; } catch (e, s) { Logging.instance.log( - "Failed to process CachedElectrumX.getAnonymitySet(): $e\n$s", + "Failed to process CachedElectrumX.getSparkAnonymitySet(): $e\n$s", level: LogLevel.Error); rethrow; } @@ -251,8 +251,55 @@ class CachedElectrumXClient { return resultingList; } catch (e, s) { Logging.instance.log( - "Failed to process CachedElectrumX.getTransaction(): $e\n$s", - level: LogLevel.Error); + "Failed to process CachedElectrumX.getUsedCoinSerials(): $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + Future> getSparkUsedCoinsTags({ + required Coin coin, + }) async { + try { + final box = await DB.instance.getSparkUsedCoinsTagsCacheBox(coin: coin); + + final _list = box.get("tags") as List?; + + Set cachedTags = + _list == null ? {} : List.from(_list).toSet(); + + final startNumber = max( + 0, + cachedTags.length - 100, // 100 being some arbitrary buffer + ); + + final tags = await electrumXClient.getSparkUsedCoinsTags( + startNumber: startNumber, + ); + + // final newSerials = List.from(serials["serials"] as List) + // .map((e) => !isHexadecimal(e) ? base64ToHex(e) : e) + // .toSet(); + + // ensure we are getting some overlap so we know we are not missing any + if (cachedTags.isNotEmpty && tags.isNotEmpty) { + assert(cachedTags.intersection(tags).isNotEmpty); + } + + cachedTags.addAll(tags); + + await box.put( + "tags", + cachedTags.toList(), + ); + + return cachedTags; + } catch (e, s) { + Logging.instance.log( + "Failed to process CachedElectrumX.getSparkUsedCoinsTags(): $e\n$s", + level: LogLevel.Error, + ); rethrow; } } diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index b6df39e35..e0f2a7bdb 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -908,7 +908,7 @@ class ElectrumXClient { /// Takes [startNumber], if it is 0, we get the full set, /// otherwise the used tags after that number - Future> getSparkUsedCoinsTags({ + Future> getSparkUsedCoinsTags({ String? requestID, required int startNumber, }) async { @@ -921,7 +921,8 @@ class ElectrumXClient { ], requestTimeout: const Duration(minutes: 2), ); - return Map.from(response["result"] as Map); + final map = Map.from(response["result"] as Map); + return Set.from(map["tags"] as List); } catch (e) { Logging.instance.log(e, level: LogLevel.Error); rethrow; diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 04bdc18e3..5310caf8f 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -441,7 +441,7 @@ class HiddenSettings extends StatelessWidget { .getSparkUsedCoinsTags(startNumber: 0); print( - "usedCoinsTags['tags'].length: ${usedCoinsTags["tags"].length}"); + "usedCoinsTags['tags'].length: ${usedCoinsTags.length}"); Util.printJson( usedCoinsTags, "usedCoinsTags"); } catch (e, s) { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index e1d583ca7..3547b6284 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -430,15 +430,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { groupId: latestSparkCoinId.toString(), coin: info.coin, ), - electrumXClient.getSparkUsedCoinsTags( - startNumber: 0, - ), + electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin), ]); - final anonymitySet = futureResults[0]; - final spentCoinTags = List.from( - futureResults[1]["tags"] as List, - ).toSet(); + final anonymitySet = futureResults[0] as Map; + final spentCoinTags = futureResults[1] as Set; // find our coins final List myCoins = []; From 04bceb17553912719acac795183d43838691f38d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 13 Dec 2023 20:12:12 -0600 Subject: [PATCH 27/77] prepareSparkMintTransaction i/o validation (WIP) --- .../spark_interface.dart | 103 ++++++++++++++---- 1 file changed, 79 insertions(+), 24 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 60eafbffa..4d454f265 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -563,52 +563,106 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } /// Transparent to Spark (mint) transaction creation. + /// + /// See https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o Future prepareSparkMintTransaction({required TxData txData}) async { - // https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o/edit - - // this kind of transaction is generated like a regular transaction, but in - // place of regular outputs we put spark outputs, so for that we call - // createSparkMintRecipients function, we get spark related data, - // everything else we do like for regular transaction, and we put CRecipient - // object as a tx outputs, we need to keep the order.. - // First we pass spark::MintedCoinData>, has following members, Address - // which is any spark address, amount (v) how much we want to send, and - // memo which can be any string with 32 length (any string we want to send - // to receiver), serial_context is a byte array, which should be unique for - // each transaction, and for that we serialize and put all inputs into - // serial_context vector. So we construct the input part of the transaction - // first then we generate spark related data. And we sign like regular - // transactions at the end. + // "this kind of transaction is generated like a regular transaction, but in + // place of [regular] outputs we put spark outputs... we construct the input + // part of the transaction first then we generate spark related data [and] + // we sign like regular transactions at the end." // Validate inputs. - // There should be at least one recipient. - if (txData.recipients == null || txData.recipients!.isEmpty) { - throw Exception("No recipients provided"); + // There should be at least one input. + if (txData.utxos == null || txData.utxos!.isEmpty) { + throw Exception("No inputs provided."); } - // For now let's limit to one recipient. + // For now let's limit to one input. + if (txData.utxos!.length > 1) { + throw Exception("Only one input supported."); + // TODO remove and test with multiple inputs. + } + + // Validate individual inputs. + for (final utxo in txData.utxos!) { + // Input amount must be greater than zero. + if (utxo.value == 0) { + throw Exception("Input value cannot be zero."); + } + + // Input value must be greater than dust limit. + if (BigInt.from(utxo.value) < cryptoCurrency.dustLimit.raw) { + throw Exception("Input value below dust limit."); + } + } + + // Validate outputs. + + // There should be at least one output. + if (txData.recipients == null || txData.recipients!.isEmpty) { + throw Exception("No recipients provided."); + } + + // For now let's limit to one output. if (txData.recipients!.length > 1) { - throw Exception("Only one recipient supported"); + throw Exception("Only one recipient supported."); + // TODO remove and test with multiple recipients. } // Limit outputs per tx to 16. // // See SPARK_OUT_LIMIT_PER_TX at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16 - // TODO limit outputs to 16. + if (txData.recipients!.length > 16) { + throw Exception("Too many recipients."); + } // Limit spend value per tx to 1000000000000 satoshis. // // See SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L17 // and COIN https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L17 // Note that as MAX_MONEY is greater than this limit, we can ignore it. See https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L31 - // TODO limit spend value per tx to 1000000000000 satoshis. + // + // This will be added to and checked as we validate outputs. + Amount amountSent = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + + // Validate individual outputs. + for (final recipient in txData.recipients!) { + // Output amount must be greater than zero. + if (recipient.amount.raw == BigInt.zero) { + throw Exception("Output amount cannot be zero."); + // Could refactor this for loop to use an index and remove this output. + } + + // Output amount must be greater than dust limit. + if (recipient.amount < cryptoCurrency.dustLimit) { + throw Exception("Output below dust limit."); + } + + // Do not add outputs that would exceed the spend limit. + amountSent += recipient.amount; + if (amountSent.raw > BigInt.from(1000000000000)) { + throw Exception( + "Spend limit exceeded (10,000 FIRO per tx).", + ); + } + } + + // TODO create a transaction builder and add inputs. // Create the serial context. + // + // "...serial_context is a byte array, which should be unique for each + // transaction, and for that we serialize and put all inputs into + // serial_context vector. So we construct the input part of the transaction + // first then we generate spark related data." List serialContext = []; // TODO set serialContext to the serialized inputs. - // Create outputs. + // Create mint recipients. final mintRecipients = LibSpark.createSparkMintRecipients( outputs: txData.recipients! .map((e) => ( @@ -618,9 +672,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { )) .toList(), serialContext: Uint8List.fromList(serialContext), + // generate: true // TODO is this needed? ); - // TODO Add outputs to txData. + // TODO finish. throw UnimplementedError(); } From 1d6ca55a36d32d83ff16d3acb85dc364cd5e23f5 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 13 Dec 2023 20:25:13 -0600 Subject: [PATCH 28/77] add WIP transaction builder --- .../spark_interface.dart | 61 ++++++++++++++++--- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 4d454f265..2f37f3f8c 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -624,7 +624,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // Note that as MAX_MONEY is greater than this limit, we can ignore it. See https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L31 // // This will be added to and checked as we validate outputs. - Amount amountSent = Amount( + Amount totalAmount = Amount( rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits, ); @@ -643,24 +643,61 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } // Do not add outputs that would exceed the spend limit. - amountSent += recipient.amount; - if (amountSent.raw > BigInt.from(1000000000000)) { + totalAmount += recipient.amount; + if (totalAmount.raw > BigInt.from(1000000000000)) { throw Exception( "Spend limit exceeded (10,000 FIRO per tx).", ); } } - // TODO create a transaction builder and add inputs. + // Create a transaction builder and set locktime and version. + final txb = btc.TransactionBuilder( + network: btc.NetworkType( + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: btc.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, + ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ), + ); + txb.setLockTime(await chainHeight); + txb.setVersion(3 | (9 << 16)); + + // Create a mint script. + final mintScript = bscript.compile([ + 0xd1, // OP_SPARKMINT. + Uint8List(0), + ]); + + // Add inputs. + for (final utxo in txData.utxos!) { + txb.addInput( + utxo.txid, + utxo.vout, + 0xffffffff, + mintScript, + ); + } // Create the serial context. // // "...serial_context is a byte array, which should be unique for each // transaction, and for that we serialize and put all inputs into - // serial_context vector. So we construct the input part of the transaction - // first then we generate spark related data." + // serial_context vector." List serialContext = []; - // TODO set serialContext to the serialized inputs. + for (final utxo in txData.utxos!) { + serialContext.addAll( + bscript.compile([ + utxo.txid, + utxo.vout, + ]), + ); + } // Create mint recipients. final mintRecipients = LibSpark.createSparkMintRecipients( @@ -675,7 +712,15 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // generate: true // TODO is this needed? ); - // TODO finish. + // Add mint output(s). + for (final mint in mintRecipients) { + txb.addOutput( + mint.scriptPubKey, + mint.amount, + ); + } + + // TODO Sign the transaction. throw UnimplementedError(); } From f83fb76bd87930ac01c33481068d0974894d85c4 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 14 Dec 2023 08:31:15 -0600 Subject: [PATCH 29/77] clean docs/comments --- lib/electrumx_rpc/electrumx_client.dart | 72 ++++++++++++------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index e0f2a7bdb..bf432894b 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -467,9 +467,9 @@ class ElectrumXClient { /// and the binary header as a hexadecimal string. /// Ex: /// { - // "height": 520481, - // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" - // } + /// "height": 520481, + /// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" + /// } Future> getBlockHeadTip({String? requestID}) async { try { final response = await request( @@ -493,15 +493,15 @@ class ElectrumXClient { /// /// Returns a map with server information /// Ex: - // { - // "genesis_hash": "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", - // "hosts": {"14.3.140.101": {"tcp_port": 51001, "ssl_port": 51002}}, - // "protocol_max": "1.0", - // "protocol_min": "1.0", - // "pruning": null, - // "server_version": "ElectrumX 1.0.17", - // "hash_function": "sha256" - // } + /// { + /// "genesis_hash": "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", + /// "hosts": {"14.3.140.101": {"tcp_port": 51001, "ssl_port": 51002}}, + /// "protocol_max": "1.0", + /// "protocol_min": "1.0", + /// "pruning": null, + /// "server_version": "ElectrumX 1.0.17", + /// "hash_function": "sha256" + /// } Future> getServerFeatures({String? requestID}) async { try { final response = await request( @@ -567,15 +567,15 @@ class ElectrumXClient { /// Returns a list of maps that contain the tx_hash and height of the tx. /// Ex: /// [ - // { - // "height": 200004, - // "tx_hash": "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" - // }, - // { - // "height": 215008, - // "tx_hash": "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" - // } - // ] + /// { + /// "height": 200004, + /// "tx_hash": "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" + /// }, + /// { + /// "height": 215008, + /// "tx_hash": "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" + /// } + /// ] Future>> getHistory({ required String scripthash, String? requestID, @@ -627,19 +627,19 @@ class ElectrumXClient { /// Returns a list of maps. /// Ex: /// [ - // { - // "tx_pos": 0, - // "value": 45318048, - // "tx_hash": "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf", - // "height": 437146 - // }, - // { - // "tx_pos": 0, - // "value": 919195, - // "tx_hash": "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f", - // "height": 441696 - // } - // ] + /// { + /// "tx_pos": 0, + /// "value": 45318048, + /// "tx_hash": "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf", + /// "height": 437146 + /// }, + /// { + /// "tx_pos": 0, + /// "value": 919195, + /// "tx_hash": "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f", + /// "height": 441696 + /// } + /// ] Future>> getUTXOs({ required String scripthash, String? requestID, @@ -985,8 +985,8 @@ class ElectrumXClient { /// Returns a map with the kay "rate" that corresponds to the free rate in satoshis /// Ex: /// { - // "rate": 1000, - // } + /// "rate": 1000, + /// } Future> getFeeRate({String? requestID}) async { try { final response = await request( From 4010605bb7e65941afd0f883fbbf15fa7e107ff8 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 14 Dec 2023 09:15:11 -0600 Subject: [PATCH 30/77] spark mint tx version --- lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 2f37f3f8c..ee16e94fc 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -666,7 +666,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ), ); txb.setLockTime(await chainHeight); - txb.setVersion(3 | (9 << 16)); + txb.setVersion(1); // Create a mint script. final mintScript = bscript.compile([ From a3bfec5d5ca1606f6a47eaf3f186490fb19148a4 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 14 Dec 2023 09:46:39 -0600 Subject: [PATCH 31/77] build runner for firo related tx updates --- .../models/blockchain_data/transaction.dart | 4 +- .../models/blockchain_data/transaction.g.dart | 4 + .../blockchain_data/v2/transaction_v2.dart | 3 + .../blockchain_data/v2/transaction_v2.g.dart | 266 ++++++++++++++++-- test/cached_electrumx_test.mocks.dart | 7 +- ...allet_settings_view_screen_test.mocks.dart | 29 ++ .../bitcoin/bitcoin_wallet_test.mocks.dart | 36 ++- .../bitcoincash_wallet_test.mocks.dart | 36 ++- .../dogecoin/dogecoin_wallet_test.mocks.dart | 36 ++- .../coins/firo/firo_wallet_test.mocks.dart | 36 ++- .../namecoin/namecoin_wallet_test.mocks.dart | 36 ++- .../particl/particl_wallet_test.mocks.dart | 36 ++- 12 files changed, 473 insertions(+), 56 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index ecd7d51c8..0991624a8 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -252,5 +252,7 @@ enum TransactionSubType { mint, // firo specific join, // firo specific ethToken, // eth token - cashFusion; + cashFusion, + sparkMint, // firo specific + sparkSpend; // firo specific } diff --git a/lib/models/isar/models/blockchain_data/transaction.g.dart b/lib/models/isar/models/blockchain_data/transaction.g.dart index cd9132576..2c37f365b 100644 --- a/lib/models/isar/models/blockchain_data/transaction.g.dart +++ b/lib/models/isar/models/blockchain_data/transaction.g.dart @@ -365,6 +365,8 @@ const _TransactionsubTypeEnumValueMap = { 'join': 3, 'ethToken': 4, 'cashFusion': 5, + 'sparkMint': 6, + 'sparkSpend': 7, }; const _TransactionsubTypeValueEnumMap = { 0: TransactionSubType.none, @@ -373,6 +375,8 @@ const _TransactionsubTypeValueEnumMap = { 3: TransactionSubType.join, 4: TransactionSubType.ethToken, 5: TransactionSubType.cashFusion, + 6: TransactionSubType.sparkMint, + 7: TransactionSubType.sparkSpend, }; const _TransactiontypeEnumValueMap = { 'outgoing': 0, diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index d373d670f..f543636ff 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -37,6 +37,8 @@ class TransactionV2 { @enumerated final TransactionSubType subType; + final String? otherData; + TransactionV2({ required this.walletId, required this.blockHash, @@ -49,6 +51,7 @@ class TransactionV2 { required this.version, required this.type, required this.subType, + required this.otherData, }); int getConfirmations(int currentChainHeight) { 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 47fd5b936..c9182bc0a 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 @@ -38,41 +38,46 @@ const TransactionV2Schema = CollectionSchema( type: IsarType.objectList, target: r'InputV2', ), - r'outputs': PropertySchema( + r'otherData': PropertySchema( id: 4, + name: r'otherData', + type: IsarType.string, + ), + r'outputs': PropertySchema( + id: 5, name: r'outputs', type: IsarType.objectList, target: r'OutputV2', ), r'subType': PropertySchema( - id: 5, + id: 6, name: r'subType', type: IsarType.byte, enumMap: _TransactionV2subTypeEnumValueMap, ), r'timestamp': PropertySchema( - id: 6, + id: 7, name: r'timestamp', type: IsarType.long, ), r'txid': PropertySchema( - id: 7, + id: 8, name: r'txid', type: IsarType.string, ), r'type': PropertySchema( - id: 8, + id: 9, name: r'type', type: IsarType.byte, enumMap: _TransactionV2typeEnumValueMap, ), r'version': PropertySchema( - id: 9, + id: 10, name: r'version', type: IsarType.long, ), r'walletId': PropertySchema( - id: 10, + id: 11, name: r'walletId', type: IsarType.string, ) @@ -161,6 +166,12 @@ int _transactionV2EstimateSize( bytesCount += InputV2Schema.estimateSize(value, offsets, allOffsets); } } + { + final value = object.otherData; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } bytesCount += 3 + object.outputs.length * 3; { final offsets = allOffsets[OutputV2]!; @@ -189,18 +200,19 @@ void _transactionV2Serialize( InputV2Schema.serialize, object.inputs, ); + writer.writeString(offsets[4], object.otherData); writer.writeObjectList( - offsets[4], + offsets[5], allOffsets, OutputV2Schema.serialize, object.outputs, ); - writer.writeByte(offsets[5], object.subType.index); - writer.writeLong(offsets[6], object.timestamp); - writer.writeString(offsets[7], object.txid); - writer.writeByte(offsets[8], object.type.index); - writer.writeLong(offsets[9], object.version); - writer.writeString(offsets[10], object.walletId); + writer.writeByte(offsets[6], object.subType.index); + writer.writeLong(offsets[7], object.timestamp); + writer.writeString(offsets[8], object.txid); + writer.writeByte(offsets[9], object.type.index); + writer.writeLong(offsets[10], object.version); + writer.writeString(offsets[11], object.walletId); } TransactionV2 _transactionV2Deserialize( @@ -220,22 +232,23 @@ TransactionV2 _transactionV2Deserialize( InputV2(), ) ?? [], + otherData: reader.readStringOrNull(offsets[4]), outputs: reader.readObjectList( - offsets[4], + offsets[5], OutputV2Schema.deserialize, allOffsets, OutputV2(), ) ?? [], subType: - _TransactionV2subTypeValueEnumMap[reader.readByteOrNull(offsets[5])] ?? + _TransactionV2subTypeValueEnumMap[reader.readByteOrNull(offsets[6])] ?? TransactionSubType.none, - timestamp: reader.readLong(offsets[6]), - txid: reader.readString(offsets[7]), - type: _TransactionV2typeValueEnumMap[reader.readByteOrNull(offsets[8])] ?? + timestamp: reader.readLong(offsets[7]), + txid: reader.readString(offsets[8]), + type: _TransactionV2typeValueEnumMap[reader.readByteOrNull(offsets[9])] ?? TransactionType.outgoing, - version: reader.readLong(offsets[9]), - walletId: reader.readString(offsets[10]), + version: reader.readLong(offsets[10]), + walletId: reader.readString(offsets[11]), ); object.id = id; return object; @@ -263,6 +276,8 @@ P _transactionV2DeserializeProp

( ) ?? []) as P; case 4: + return (reader.readStringOrNull(offset)) as P; + case 5: return (reader.readObjectList( offset, OutputV2Schema.deserialize, @@ -270,20 +285,20 @@ P _transactionV2DeserializeProp

( OutputV2(), ) ?? []) as P; - case 5: + case 6: return (_TransactionV2subTypeValueEnumMap[ reader.readByteOrNull(offset)] ?? TransactionSubType.none) as P; - case 6: - return (reader.readLong(offset)) as P; case 7: - return (reader.readString(offset)) as P; + return (reader.readLong(offset)) as P; case 8: + return (reader.readString(offset)) as P; + case 9: return (_TransactionV2typeValueEnumMap[reader.readByteOrNull(offset)] ?? TransactionType.outgoing) as P; - case 9: - return (reader.readLong(offset)) as P; case 10: + return (reader.readLong(offset)) as P; + case 11: return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -297,6 +312,8 @@ const _TransactionV2subTypeEnumValueMap = { 'join': 3, 'ethToken': 4, 'cashFusion': 5, + 'sparkMint': 6, + 'sparkSpend': 7, }; const _TransactionV2subTypeValueEnumMap = { 0: TransactionSubType.none, @@ -305,6 +322,8 @@ const _TransactionV2subTypeValueEnumMap = { 3: TransactionSubType.join, 4: TransactionSubType.ethToken, 5: TransactionSubType.cashFusion, + 6: TransactionSubType.sparkMint, + 7: TransactionSubType.sparkSpend, }; const _TransactionV2typeEnumValueMap = { 'outgoing': 0, @@ -1244,6 +1263,160 @@ extension TransactionV2QueryFilter }); } + QueryBuilder + otherDataIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'otherData', + )); + }); + } + + QueryBuilder + otherDataIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'otherData', + )); + }); + } + + QueryBuilder + otherDataEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'otherData', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'otherData', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'otherData', + value: '', + )); + }); + } + + QueryBuilder + otherDataIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'otherData', + value: '', + )); + }); + } + QueryBuilder outputsLengthEqualTo(int length) { return QueryBuilder.apply(this, (query) { @@ -1887,6 +2060,19 @@ extension TransactionV2QuerySortBy }); } + QueryBuilder sortByOtherData() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.asc); + }); + } + + QueryBuilder + sortByOtherDataDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.desc); + }); + } + QueryBuilder sortBySubType() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'subType', Sort.asc); @@ -2013,6 +2199,19 @@ extension TransactionV2QuerySortThenBy }); } + QueryBuilder thenByOtherData() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.asc); + }); + } + + QueryBuilder + thenByOtherDataDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.desc); + }); + } + QueryBuilder thenBySubType() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'subType', Sort.asc); @@ -2110,6 +2309,13 @@ extension TransactionV2QueryWhereDistinct }); } + QueryBuilder distinctByOtherData( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'otherData', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctBySubType() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'subType'); @@ -2182,6 +2388,12 @@ extension TransactionV2QueryProperty }); } + QueryBuilder otherDataProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'otherData'); + }); + } + QueryBuilder, QQueryOperations> outputsProperty() { return QueryBuilder.apply(this, (query) { diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index e41279674..53ddd8cd6 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -384,7 +384,7 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { _i5.Future>.value({}), ) as _i5.Future>); @override - _i5.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -397,9 +397,8 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override _i5.Future>> getSparkMintMetaData({ String? requestID, 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 9e15dcc99..f7cd4c34f 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 @@ -74,6 +74,25 @@ class MockCachedElectrumXClient extends _i1.Mock _i4.Future>.value({}), ) as _i4.Future>); @override + _i4.Future> getSparkAnonymitySet({ + required String? groupId, + String? blockhash = r'', + required _i5.Coin? coin, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkAnonymitySet, + [], + { + #groupId: groupId, + #blockhash: blockhash, + #coin: coin, + }, + ), + returnValue: + _i4.Future>.value({}), + ) as _i4.Future>); + @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( #base64ToHex, @@ -125,6 +144,16 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: _i4.Future>.value([]), ) as _i4.Future>); @override + _i4.Future> getSparkUsedCoinsTags({required _i5.Coin? coin}) => + (super.noSuchMethod( + Invocation.method( + #getSparkUsedCoinsTags, + [], + {#coin: coin}, + ), + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); + @override _i4.Future clearSharedTransactionCache({required _i5.Coin? coin}) => (super.noSuchMethod( Invocation.method( diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index ad95b7baf..f3b618de7 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -381,7 +381,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { _i4.Future>.value({}), ) as _i4.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i4.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,9 +394,8 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); @override _i4.Future>> getSparkMintMetaData({ String? requestID, @@ -516,6 +515,25 @@ class MockCachedElectrumXClient extends _i1.Mock _i4.Future>.value({}), ) as _i4.Future>); @override + _i4.Future> getSparkAnonymitySet({ + required String? groupId, + String? blockhash = r'', + required _i6.Coin? coin, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkAnonymitySet, + [], + { + #groupId: groupId, + #blockhash: blockhash, + #coin: coin, + }, + ), + returnValue: + _i4.Future>.value({}), + ) as _i4.Future>); + @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( #base64ToHex, @@ -567,6 +585,16 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: _i4.Future>.value([]), ) as _i4.Future>); @override + _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + (super.noSuchMethod( + Invocation.method( + #getSparkUsedCoinsTags, + [], + {#coin: coin}, + ), + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); + @override _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => (super.noSuchMethod( Invocation.method( diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index b636ed2a4..3a2d12610 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -381,7 +381,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { _i4.Future>.value({}), ) as _i4.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i4.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,9 +394,8 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); @override _i4.Future>> getSparkMintMetaData({ String? requestID, @@ -516,6 +515,25 @@ class MockCachedElectrumXClient extends _i1.Mock _i4.Future>.value({}), ) as _i4.Future>); @override + _i4.Future> getSparkAnonymitySet({ + required String? groupId, + String? blockhash = r'', + required _i6.Coin? coin, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkAnonymitySet, + [], + { + #groupId: groupId, + #blockhash: blockhash, + #coin: coin, + }, + ), + returnValue: + _i4.Future>.value({}), + ) as _i4.Future>); + @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( #base64ToHex, @@ -567,6 +585,16 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: _i4.Future>.value([]), ) as _i4.Future>); @override + _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + (super.noSuchMethod( + Invocation.method( + #getSparkUsedCoinsTags, + [], + {#coin: coin}, + ), + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); + @override _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => (super.noSuchMethod( Invocation.method( diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 3accb6115..87341f635 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -381,7 +381,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { _i4.Future>.value({}), ) as _i4.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i4.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,9 +394,8 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); @override _i4.Future>> getSparkMintMetaData({ String? requestID, @@ -516,6 +515,25 @@ class MockCachedElectrumXClient extends _i1.Mock _i4.Future>.value({}), ) as _i4.Future>); @override + _i4.Future> getSparkAnonymitySet({ + required String? groupId, + String? blockhash = r'', + required _i6.Coin? coin, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkAnonymitySet, + [], + { + #groupId: groupId, + #blockhash: blockhash, + #coin: coin, + }, + ), + returnValue: + _i4.Future>.value({}), + ) as _i4.Future>); + @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( #base64ToHex, @@ -567,6 +585,16 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: _i4.Future>.value([]), ) as _i4.Future>); @override + _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + (super.noSuchMethod( + Invocation.method( + #getSparkUsedCoinsTags, + [], + {#coin: coin}, + ), + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); + @override _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => (super.noSuchMethod( Invocation.method( diff --git a/test/services/coins/firo/firo_wallet_test.mocks.dart b/test/services/coins/firo/firo_wallet_test.mocks.dart index f75e1f4f2..213c594db 100644 --- a/test/services/coins/firo/firo_wallet_test.mocks.dart +++ b/test/services/coins/firo/firo_wallet_test.mocks.dart @@ -411,7 +411,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { _i5.Future>.value({}), ) as _i5.Future>); @override - _i5.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -424,9 +424,8 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override _i5.Future>> getSparkMintMetaData({ String? requestID, @@ -546,6 +545,25 @@ class MockCachedElectrumXClient extends _i1.Mock _i5.Future>.value({}), ) as _i5.Future>); @override + _i5.Future> getSparkAnonymitySet({ + required String? groupId, + String? blockhash = r'', + required _i7.Coin? coin, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkAnonymitySet, + [], + { + #groupId: groupId, + #blockhash: blockhash, + #coin: coin, + }, + ), + returnValue: + _i5.Future>.value({}), + ) as _i5.Future>); + @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( #base64ToHex, @@ -597,6 +615,16 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: _i5.Future>.value([]), ) as _i5.Future>); @override + _i5.Future> getSparkUsedCoinsTags({required _i7.Coin? coin}) => + (super.noSuchMethod( + Invocation.method( + #getSparkUsedCoinsTags, + [], + {#coin: coin}, + ), + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); + @override _i5.Future clearSharedTransactionCache({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index f1445d743..1102ebf10 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -381,7 +381,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { _i4.Future>.value({}), ) as _i4.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i4.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,9 +394,8 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); @override _i4.Future>> getSparkMintMetaData({ String? requestID, @@ -516,6 +515,25 @@ class MockCachedElectrumXClient extends _i1.Mock _i4.Future>.value({}), ) as _i4.Future>); @override + _i4.Future> getSparkAnonymitySet({ + required String? groupId, + String? blockhash = r'', + required _i6.Coin? coin, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkAnonymitySet, + [], + { + #groupId: groupId, + #blockhash: blockhash, + #coin: coin, + }, + ), + returnValue: + _i4.Future>.value({}), + ) as _i4.Future>); + @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( #base64ToHex, @@ -567,6 +585,16 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: _i4.Future>.value([]), ) as _i4.Future>); @override + _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + (super.noSuchMethod( + Invocation.method( + #getSparkUsedCoinsTags, + [], + {#coin: coin}, + ), + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); + @override _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => (super.noSuchMethod( Invocation.method( diff --git a/test/services/coins/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart index 051cb4bbd..b8ef7a694 100644 --- a/test/services/coins/particl/particl_wallet_test.mocks.dart +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -381,7 +381,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { _i4.Future>.value({}), ) as _i4.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i4.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,9 +394,8 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); @override _i4.Future>> getSparkMintMetaData({ String? requestID, @@ -516,6 +515,25 @@ class MockCachedElectrumXClient extends _i1.Mock _i4.Future>.value({}), ) as _i4.Future>); @override + _i4.Future> getSparkAnonymitySet({ + required String? groupId, + String? blockhash = r'', + required _i6.Coin? coin, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkAnonymitySet, + [], + { + #groupId: groupId, + #blockhash: blockhash, + #coin: coin, + }, + ), + returnValue: + _i4.Future>.value({}), + ) as _i4.Future>); + @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( #base64ToHex, @@ -567,6 +585,16 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: _i4.Future>.value([]), ) as _i4.Future>); @override + _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + (super.noSuchMethod( + Invocation.method( + #getSparkUsedCoinsTags, + [], + {#coin: coin}, + ), + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); + @override _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => (super.noSuchMethod( Invocation.method( From b180b8632ea3746d4a4250d779d49414a84eca92 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 14 Dec 2023 09:48:49 -0600 Subject: [PATCH 32/77] add missing required parameter --- lib/services/mixins/electrum_x_parsing.dart | 1 + lib/wallets/wallet/impl/bitcoincash_wallet.dart | 1 + lib/wallets/wallet/impl/ecash_wallet.dart | 1 + 3 files changed, 3 insertions(+) diff --git a/lib/services/mixins/electrum_x_parsing.dart b/lib/services/mixins/electrum_x_parsing.dart index bcd9c3ed4..030adc964 100644 --- a/lib/services/mixins/electrum_x_parsing.dart +++ b/lib/services/mixins/electrum_x_parsing.dart @@ -113,6 +113,7 @@ mixin ElectrumXParsing { outputs: List.unmodifiable(outputs), subType: TransactionSubType.none, type: TransactionType.unknown, + otherData: null, ); } diff --git a/lib/wallets/wallet/impl/bitcoincash_wallet.dart b/lib/wallets/wallet/impl/bitcoincash_wallet.dart index 533d97b53..0edf16cb1 100644 --- a/lib/wallets/wallet/impl/bitcoincash_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoincash_wallet.dart @@ -293,6 +293,7 @@ class BitcoincashWallet extends Bip39HDWallet outputs: List.unmodifiable(outputs), type: type, subType: subType, + otherData: null, ); txns.add(tx); diff --git a/lib/wallets/wallet/impl/ecash_wallet.dart b/lib/wallets/wallet/impl/ecash_wallet.dart index fcfda3e0b..739ba0a0c 100644 --- a/lib/wallets/wallet/impl/ecash_wallet.dart +++ b/lib/wallets/wallet/impl/ecash_wallet.dart @@ -288,6 +288,7 @@ class EcashWallet extends Bip39HDWallet outputs: List.unmodifiable(outputs), type: type, subType: subType, + otherData: null, ); txns.add(tx); From a25c00476859de855ef5532f487d131b42cc2ebd Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 14 Dec 2023 10:44:41 -0600 Subject: [PATCH 33/77] WIP firo transactions v2 w/ spark --- lib/wallets/wallet/impl/firo_wallet.dart | 428 ++++++++++++++++++++++- 1 file changed, 412 insertions(+), 16 deletions(-) diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 1dfc75094..820b7836a 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -1,15 +1,16 @@ +import 'dart:convert'; import 'dart:math'; import 'package:decimal/decimal.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/hive/db.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/input.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/output.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; -import 'package:stackwallet/models/isar/models/firo_specific/lelantus_coin.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; @@ -27,6 +28,9 @@ class FiroWallet extends Bip39HDWallet FiroWallet(CryptoCurrencyNetwork network) : super(Firo(network)); + @override + int get isarTransactionVersion => 2; + @override FilterOperation? get changeAddressFilterOperation => FilterGroup.and(standardChangeAddressFilters); @@ -37,18 +41,376 @@ class FiroWallet extends Bip39HDWallet // =========================================================================== - bool _duplicateTxCheck( - List> allTransactions, String txid) { - for (int i = 0; i < allTransactions.length; i++) { - if (allTransactions[i]["txid"] == txid) { - return true; - } - } - return false; - } - @override Future updateTransactions() async { + List

allAddressesOld = await fetchAddressesForElectrumXScan(); + + Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => convertAddressString(e.value)) + .toSet(); + + Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => convertAddressString(e.value)) + .toSet(); + + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + + final List> allTxHashes = + await fetchHistory(allAddressesSet); + + List> allTransactions = []; + + // some lelantus transactions aren't fetched via wallet addresses so they + // will never show as confirmed in the gui. + final unconfirmedTransactions = await mainDB + .getTransactions(walletId) + .filter() + .heightIsNull() + .findAll(); + for (final tx in unconfirmedTransactions) { + final txn = await electrumXCachedClient.getTransaction( + txHash: tx.txid, + verbose: true, + coin: info.coin, + ); + final height = txn["height"] as int?; + + if (height != null) { + // tx was mined + // add to allTxHashes + final info = { + "tx_hash": tx.txid, + "height": height, + "address": tx.address.value?.value, + }; + allTxHashes.add(info); + } + } + + for (final txHash in allTxHashes) { + // final storedTx = await db + // .getTransactions(walletId) + // .filter() + // .txidEqualTo(txHash["tx_hash"] as String) + // .findFirst(); + + // if (storedTx == null || + // !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) { + final tx = await electrumXCachedClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: info.coin, + ); + + // check for duplicates before adding to list + if (allTransactions + .indexWhere((e) => e["txid"] == tx["txid"] as String) == + -1) { + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + // } + } + + final List txns = []; + + for (final txData in allTransactions) { + // set to true if any inputs were detected as owned by this wallet + bool wasSentFromThisWallet = false; + + // set to true if any outputs were detected as owned by this wallet + bool wasReceivedInThisWallet = false; + BigInt amountReceivedInThisWallet = BigInt.zero; + BigInt changeAmountReceivedInThisWallet = BigInt.zero; + + Amount? anonFees; + + bool isMint = false; + bool isJMint = false; + bool isSparkMint = false; + bool isMasterNodePayment = false; + final bool isSparkSpend = txData["type"] == 9 && txData["version"] == 3; + + if (txData.toString().contains("spark")) { + Util.printJson(txData); + } + + // parse outputs + final List outputs = []; + for (final outputJson in txData["vout"] as List) { + final outMap = Map.from(outputJson as Map); + if (outMap["scriptPubKey"]?["type"] == "lelantusmint") { + final asm = outMap["scriptPubKey"]?["asm"] as String?; + if (asm != null) { + if (asm.startsWith("OP_LELANTUSJMINT")) { + isJMint = true; + } else if (asm.startsWith("OP_LELANTUSMINT")) { + isMint = true; + } else { + Logging.instance.log( + "Unknown mint op code found for lelantusmint tx: ${txData["txid"]}", + level: LogLevel.Error, + ); + } + } else { + Logging.instance.log( + "ASM for lelantusmint tx: ${txData["txid"]} is null!", + level: LogLevel.Error, + ); + } + } + if (outMap["scriptPubKey"]?["type"] == "sparkmint") { + final asm = outMap["scriptPubKey"]?["asm"] as String?; + if (asm != null) { + if (asm.startsWith("OP_SPARKMINT")) { + isSparkMint = true; + } else { + Logging.instance.log( + "Unknown mint op code found for sparkmint tx: ${txData["txid"]}", + level: LogLevel.Error, + ); + } + } else { + Logging.instance.log( + "ASM for sparkmint tx: ${txData["txid"]} is null!", + level: LogLevel.Error, + ); + } + } + + if (isSparkSpend) { + // TODO + } else if (isSparkMint) { + // TODO + } else if (isMint || isJMint) { + // do nothing extra ? + } else { + // TODO + } + + OutputV2 output = OutputV2.fromElectrumXJson( + outMap, + decimalPlaces: cryptoCurrency.fractionDigits, + // don't know yet if wallet owns. Need addresses first + 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); + } + + outputs.add(output); + } + + if (isJMint || isSparkSpend) { + anonFees = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + // parse inputs + final List inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map.from(jsonInput as Map); + + final List addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; + + final coinbase = map["coinbase"] as String?; + + final txid = map["txid"] as String?; + final vout = map["vout"] as int?; + if (txid != null && vout != null) { + outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: txid, + vout: vout, + ); + } + + if (isSparkSpend) { + // anon fees + final nFee = Decimal.tryParse(map["nFees"].toString()); + if (nFee != null) { + final fees = Amount.fromDecimal( + nFee, + fractionDigits: cryptoCurrency.fractionDigits, + ); + + anonFees = anonFees! + fees; + } + } else if (isSparkMint) { + final address = map["address"] as String?; + final value = map["valueSat"] as int?; + + if (address != null && value != null) { + valueStringSats = value.toString(); + addresses.add(address); + } + } else if (isMint) { + // We should be able to assume this belongs to this wallet + final address = map["address"] as String?; + final value = map["valueSat"] as int?; + + if (address != null && value != null) { + valueStringSats = value.toString(); + addresses.add(address); + } + } else if (isJMint) { + // anon fees + final nFee = Decimal.tryParse(map["nFees"].toString()); + if (nFee != null) { + final fees = Amount.fromDecimal( + nFee, + fractionDigits: cryptoCurrency.fractionDigits, + ); + + anonFees = anonFees! + fees; + } + } else if (coinbase == null && txid != null && vout != null) { + final inputTx = await electrumXCachedClient.getTransaction( + txHash: txid, + coin: cryptoCurrency.coin, + ); + + final prevOutJson = Map.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) + as Map); + + final prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: cryptoCurrency.fractionDigits, + walletOwns: false, // doesn't matter here as this is not saved + ); + + valueStringSats = prevOut.valueStringSats; + addresses.addAll(prevOut.addresses); + } else if (coinbase == null) { + Util.printJson(map, "NON TXID INPUT"); + } + + InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: map["scriptSig"]?["hex"] as String?, + sequence: map["sequence"] as int?, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + witness: map["witness"] as String?, + coinbase: coinbase, + innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?, + // don't know yet if wallet owns. Need addresses first + walletOwns: false, + ); + + if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { + wasSentFromThisWallet = true; + input = input.copyWith(walletOwns: true); + } + + inputs.add(input); + } + + final totalOut = outputs + .map((e) => e.value) + .fold(BigInt.zero, (value, element) => value + element); + + TransactionType type; + TransactionSubType subType = TransactionSubType.none; + + // TODO integrate the following with the next bit + if (isSparkSpend) { + subType = TransactionSubType.sparkSpend; + } else if (isSparkMint) { + subType = TransactionSubType.sparkMint; + } else if (isMint) { + subType = TransactionSubType.mint; + } else if (isJMint) { + subType = TransactionSubType.join; + } + + // at least one input was owned by this wallet + if (wasSentFromThisWallet) { + type = TransactionType.outgoing; + + if (wasReceivedInThisWallet) { + if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet == + totalOut) { + // definitely sent all to self + type = TransactionType.sentToSelf; + } else if (amountReceivedInThisWallet == BigInt.zero) { + // most likely just a typical send + // do nothing here yet + } + + // check vout 0 for special scripts + if (outputs.isNotEmpty) { + final output = outputs.first; + + // // check for fusion + // if (BchUtils.isFUZE(output.scriptPubKeyHex.toUint8ListFromHex)) { + // subType = TransactionSubType.cashFusion; + // } else { + // // check other cases here such as SLP or cash tokens etc + // } + } + } + } else if (wasReceivedInThisWallet) { + // only found outputs owned by this wallet + type = TransactionType.incoming; + } else { + Logging.instance.log( + "Unexpected tx found (ignoring it): $txData", + level: LogLevel.Error, + ); + continue; + } + + String? otherData; + if (anonFees != null) { + otherData = jsonEncode( + { + "anonFees": anonFees.toJsonString(), + }, + ); + } + + final tx = TransactionV2( + walletId: walletId, + blockHash: txData["blockhash"] as String?, + hash: txData["hash"] as String, + txid: txData["txid"] as String, + height: txData["height"] as int?, + version: txData["version"] as int, + timestamp: txData["blocktime"] as int? ?? + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + type: type, + subType: subType, + otherData: otherData, + ); + + txns.add(tx); + } + + await mainDB.updateOrPutTransactionV2s(txns); + } + + Future updateTransactionsOLD() async { final allAddresses = await fetchAddressesForElectrumXScan(); Set receivingAddresses = allAddresses @@ -109,7 +471,9 @@ class FiroWallet extends Bip39HDWallet coin: info.coin, ); - if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { + if (allTransactions + .indexWhere((e) => e["txid"] == tx["txid"] as String) == + -1) { tx["address"] = await mainDB .getAddresses(walletId) .filter() @@ -129,6 +493,8 @@ class FiroWallet extends Bip39HDWallet bool isMint = false; bool isJMint = false; + bool isSparkMint = false; + bool isSparkSpend = false; // check if tx is Mint or jMint for (final output in outputList) { @@ -154,6 +520,32 @@ class FiroWallet extends Bip39HDWallet ); } } + if (output["scriptPubKey"]?["type"] == "sparkmint") { + final asm = output["scriptPubKey"]?["asm"] as String?; + if (asm != null) { + if (asm.startsWith("OP_SPARKMINT")) { + isSparkMint = true; + break; + } else if (asm.startsWith("OP_SPARKSPEND")) { + isSparkSpend = true; + break; + } else { + Logging.instance.log( + "Unknown mint op code found for lelantusmint tx: ${txObject["txid"]}", + level: LogLevel.Error, + ); + } + } else { + Logging.instance.log( + "ASM for sparkmint tx: ${txObject["txid"]} is null!", + level: LogLevel.Error, + ); + } + } + } + + if (isSparkSpend || isSparkMint) { + continue; } Set inputAddresses = {}; @@ -483,6 +875,10 @@ class FiroWallet extends Bip39HDWallet } } + if (input['txid'] == null) { + continue; + } + ins.add( Input( txid: input['txid'] as String, From 69860843e04af55b60199bc74058702ac9528f7c Mon Sep 17 00:00:00 2001 From: Julian Date: Thu, 14 Dec 2023 20:51:09 -0600 Subject: [PATCH 34/77] id coins tweak --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index ee16e94fc..c732af769 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -447,17 +447,18 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { for (final dynData in anonymitySet["coins"] as List) { final data = List.from(dynData as List); - if (data.length != 2) { + if (data.length != 3) { throw Exception("Unexpected serialized coin info found"); } - final serializedCoin = data.first; - final txHash = base64ToReverseHex(data.last); + final serializedCoin = data[0]; + final txHash = base64ToReverseHex(data[1]); final coin = LibSpark.identifyAndRecoverCoin( serializedCoin, privateKeyHex: privateKeyHex, index: index, + context: base64Decode(data[2]), isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, ); From 7f45c0c37cb9fa28299fe86e8f572fbbd2f810d5 Mon Sep 17 00:00:00 2001 From: Julian Date: Thu, 14 Dec 2023 20:51:57 -0600 Subject: [PATCH 35/77] podfile.lock --- macos/Podfile.lock | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 69c91af48..f76e35a57 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,7 @@ PODS: + - coinlib_flutter (0.3.2): + - Flutter + - FlutterMacOS - connectivity_plus (0.0.1): - FlutterMacOS - ReachabilitySwift @@ -22,14 +25,11 @@ PODS: - cw_shared_external (0.0.1): - cw_shared_external/Boost (= 0.0.1) - cw_shared_external/OpenSSL (= 0.0.1) - - cw_shared_external/Sodium (= 0.0.1) - FlutterMacOS - cw_shared_external/Boost (0.0.1): - FlutterMacOS - cw_shared_external/OpenSSL (0.0.1): - FlutterMacOS - - cw_shared_external/Sodium (0.0.1): - - FlutterMacOS - cw_wownero (0.0.1): - cw_wownero/Boost (= 0.0.1) - cw_wownero/OpenSSL (= 0.0.1) @@ -55,6 +55,8 @@ PODS: - FlutterMacOS - flutter_libepiccash (0.0.1): - FlutterMacOS + - flutter_libsparkmobile (0.0.1): + - FlutterMacOS - flutter_local_notifications (0.0.1): - FlutterMacOS - flutter_secure_storage_macos (6.1.1): @@ -74,6 +76,7 @@ PODS: - FlutterMacOS - stack_wallet_backup (0.0.1): - FlutterMacOS + - tor_ffi_plugin (0.0.1) - url_launcher_macos (0.0.1): - FlutterMacOS - wakelock_macos (0.0.1): @@ -82,6 +85,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - coinlib_flutter (from `Flutter/ephemeral/.symlinks/plugins/coinlib_flutter/darwin`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - cw_monero (from `Flutter/ephemeral/.symlinks/plugins/cw_monero/macos`) - cw_shared_external (from `Flutter/ephemeral/.symlinks/plugins/cw_shared_external/macos`) @@ -90,6 +94,7 @@ DEPENDENCIES: - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) - flutter_libepiccash (from `Flutter/ephemeral/.symlinks/plugins/flutter_libepiccash/macos`) + - flutter_libsparkmobile (from `Flutter/ephemeral/.symlinks/plugins/flutter_libsparkmobile/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) @@ -99,6 +104,7 @@ DEPENDENCIES: - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - stack_wallet_backup (from `Flutter/ephemeral/.symlinks/plugins/stack_wallet_backup/macos`) + - tor_ffi_plugin (from `Flutter/ephemeral/.symlinks/plugins/tor_ffi_plugin/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) @@ -108,6 +114,8 @@ SPEC REPOS: - ReachabilitySwift EXTERNAL SOURCES: + coinlib_flutter: + :path: Flutter/ephemeral/.symlinks/plugins/coinlib_flutter/darwin connectivity_plus: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos cw_monero: @@ -124,6 +132,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/devicelocale/macos flutter_libepiccash: :path: Flutter/ephemeral/.symlinks/plugins/flutter_libepiccash/macos + flutter_libsparkmobile: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_libsparkmobile/macos flutter_local_notifications: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos flutter_secure_storage_macos: @@ -142,6 +152,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos stack_wallet_backup: :path: Flutter/ephemeral/.symlinks/plugins/stack_wallet_backup/macos + tor_ffi_plugin: + :path: Flutter/ephemeral/.symlinks/plugins/tor_ffi_plugin/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos wakelock_macos: @@ -150,25 +162,28 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos SPEC CHECKSUMS: + coinlib_flutter: 6abec900d67762a6e7ccfd567a3cd3ae00bbee35 connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - cw_monero: a3442556ad3c06365c912735e4a23942a28692b1 - cw_shared_external: 1f631d1132521baac5f4caed43176fa10d4e0d8b - cw_wownero: b4adb1e701fc363de27fa222fcaf4eff6f5fa63a + cw_monero: 7acce7238d217e3993ecac6ec2dec07be728769a + cw_shared_external: c6adfd29c9be4d64f84e1fa9c541ccbcbdb6b457 + cw_wownero: bcd7f2ad6c0a3e8e2a51756fb14f0579b6f8b4ff desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 - flutter_libepiccash: 9113ac75dd325f8bcf00bc3ab583c7fc2780cf3c + flutter_libepiccash: be1560a04150c5cc85bcf08d236ec2b3d1f5d8da + flutter_libsparkmobile: 8ae86b0ccc7e52c9db6b53e258ee2977deb184ab flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a - lelantus: 3dfbf92b1e66b3573494dfe3d6a21c4988b5361b + lelantus: 308e42c5a648598936a07a234471dd8cf8e687a0 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 stack_wallet_backup: 6ebc60b1bdcf11cf1f1cbad9aa78332e1e15778c - url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 + tor_ffi_plugin: 2566c1ed174688cca560fa0c64b7a799c66f07cb + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 From 3cbc866fe900e2de4b20263ed2be53e9c006a9c0 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 15 Dec 2023 08:16:51 -0600 Subject: [PATCH 36/77] update isar spark coin schema --- lib/db/isar/main_db.dart | 2 + lib/wallets/isar/models/spark_coin.dart | 18 +- lib/wallets/isar/models/spark_coin.g.dart | 523 +++++++++++------- .../spark_interface.dart | 8 +- 4 files changed, 339 insertions(+), 212 deletions(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 2602e03c1..ee7cac6b0 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -21,6 +21,7 @@ import 'package:stackwallet/models/isar/stack_theme.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; +import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; import 'package:tuple/tuple.dart'; @@ -61,6 +62,7 @@ class MainDB { LelantusCoinSchema, WalletInfoSchema, TransactionV2Schema, + SparkCoinSchema, ], directory: (await StackFileSystem.applicationIsarDirectory()).path, // inspector: kDebugMode, diff --git a/lib/wallets/isar/models/spark_coin.dart b/lib/wallets/isar/models/spark_coin.dart index dfbf4c6e6..7f0179e52 100644 --- a/lib/wallets/isar/models/spark_coin.dart +++ b/lib/wallets/isar/models/spark_coin.dart @@ -29,7 +29,7 @@ class SparkCoin { final bool isUsed; - final List? k; // TODO: proper name (not single char!!) is this nonce??? + final List? nonce; final String address; final String txHash; @@ -47,6 +47,8 @@ class SparkCoin { final String lTagHash; + final int? height; + @ignore BigInt get value => BigInt.parse(valueIntString); @@ -57,7 +59,7 @@ class SparkCoin { required this.walletId, required this.type, required this.isUsed, - this.k, + this.nonce, required this.address, required this.txHash, required this.valueIntString, @@ -68,12 +70,13 @@ class SparkCoin { this.serial, this.tag, required this.lTagHash, + this.height, }); SparkCoin copyWith({ SparkCoinType? type, bool? isUsed, - List? k, + List? nonce, String? address, String? txHash, BigInt? value, @@ -84,12 +87,13 @@ class SparkCoin { List? serial, List? tag, String? lTagHash, + int? height, }) { return SparkCoin( walletId: walletId, type: type ?? this.type, isUsed: isUsed ?? this.isUsed, - k: k ?? this.k, + nonce: nonce ?? this.nonce, address: address ?? this.address, txHash: txHash ?? this.txHash, valueIntString: value?.toString() ?? this.value.toString(), @@ -101,16 +105,17 @@ class SparkCoin { serial: serial ?? this.serial, tag: tag ?? this.tag, lTagHash: lTagHash ?? this.lTagHash, + height: height ?? this.height, ); } @override String toString() { return 'SparkCoin(' - ', walletId: $walletId' + 'walletId: $walletId' ', type: $type' ', isUsed: $isUsed' - ', k: $k' + ', k: $nonce' ', address: $address' ', txHash: $txHash' ', value: $value' @@ -121,6 +126,7 @@ class SparkCoin { ', serial: $serial' ', tag: $tag' ', lTagHash: $lTagHash' + ', height: $height' ')'; } } diff --git a/lib/wallets/isar/models/spark_coin.g.dart b/lib/wallets/isar/models/spark_coin.g.dart index 6f5a87ea7..5ea6a30a9 100644 --- a/lib/wallets/isar/models/spark_coin.g.dart +++ b/lib/wallets/isar/models/spark_coin.g.dart @@ -32,16 +32,16 @@ const SparkCoinSchema = CollectionSchema( name: r'encryptedDiversifier', type: IsarType.longList, ), - r'isUsed': PropertySchema( + r'height': PropertySchema( id: 3, + name: r'height', + type: IsarType.long, + ), + r'isUsed': PropertySchema( + id: 4, name: r'isUsed', type: IsarType.bool, ), - r'k': PropertySchema( - id: 4, - name: r'k', - type: IsarType.longList, - ), r'lTagHash': PropertySchema( id: 5, name: r'lTagHash', @@ -52,39 +52,44 @@ const SparkCoinSchema = CollectionSchema( name: r'memo', type: IsarType.string, ), - r'serial': PropertySchema( + r'nonce': PropertySchema( id: 7, + name: r'nonce', + type: IsarType.longList, + ), + r'serial': PropertySchema( + id: 8, name: r'serial', type: IsarType.longList, ), r'serialContext': PropertySchema( - id: 8, + id: 9, name: r'serialContext', type: IsarType.longList, ), r'tag': PropertySchema( - id: 9, + id: 10, name: r'tag', type: IsarType.longList, ), r'txHash': PropertySchema( - id: 10, + id: 11, name: r'txHash', type: IsarType.string, ), r'type': PropertySchema( - id: 11, + id: 12, name: r'type', type: IsarType.byte, enumMap: _SparkCointypeEnumValueMap, ), r'valueIntString': PropertySchema( - id: 12, + id: 13, name: r'valueIntString', type: IsarType.string, ), r'walletId': PropertySchema( - id: 13, + id: 14, name: r'walletId', type: IsarType.string, ) @@ -136,12 +141,6 @@ int _sparkCoinEstimateSize( bytesCount += 3 + value.length * 8; } } - { - final value = object.k; - if (value != null) { - bytesCount += 3 + value.length * 8; - } - } bytesCount += 3 + object.lTagHash.length * 3; { final value = object.memo; @@ -149,6 +148,12 @@ int _sparkCoinEstimateSize( bytesCount += 3 + value.length * 3; } } + { + final value = object.nonce; + if (value != null) { + bytesCount += 3 + value.length * 8; + } + } { final value = object.serial; if (value != null) { @@ -182,17 +187,18 @@ void _sparkCoinSerialize( writer.writeString(offsets[0], object.address); writer.writeString(offsets[1], object.diversifierIntString); writer.writeLongList(offsets[2], object.encryptedDiversifier); - writer.writeBool(offsets[3], object.isUsed); - writer.writeLongList(offsets[4], object.k); + writer.writeLong(offsets[3], object.height); + writer.writeBool(offsets[4], object.isUsed); writer.writeString(offsets[5], object.lTagHash); writer.writeString(offsets[6], object.memo); - writer.writeLongList(offsets[7], object.serial); - writer.writeLongList(offsets[8], object.serialContext); - writer.writeLongList(offsets[9], object.tag); - writer.writeString(offsets[10], object.txHash); - writer.writeByte(offsets[11], object.type.index); - writer.writeString(offsets[12], object.valueIntString); - writer.writeString(offsets[13], object.walletId); + writer.writeLongList(offsets[7], object.nonce); + writer.writeLongList(offsets[8], object.serial); + writer.writeLongList(offsets[9], object.serialContext); + writer.writeLongList(offsets[10], object.tag); + writer.writeString(offsets[11], object.txHash); + writer.writeByte(offsets[12], object.type.index); + writer.writeString(offsets[13], object.valueIntString); + writer.writeString(offsets[14], object.walletId); } SparkCoin _sparkCoinDeserialize( @@ -205,18 +211,19 @@ SparkCoin _sparkCoinDeserialize( address: reader.readString(offsets[0]), diversifierIntString: reader.readString(offsets[1]), encryptedDiversifier: reader.readLongList(offsets[2]), - isUsed: reader.readBool(offsets[3]), - k: reader.readLongList(offsets[4]), + height: reader.readLongOrNull(offsets[3]), + isUsed: reader.readBool(offsets[4]), lTagHash: reader.readString(offsets[5]), memo: reader.readStringOrNull(offsets[6]), - serial: reader.readLongList(offsets[7]), - serialContext: reader.readLongList(offsets[8]), - tag: reader.readLongList(offsets[9]), - txHash: reader.readString(offsets[10]), - type: _SparkCointypeValueEnumMap[reader.readByteOrNull(offsets[11])] ?? + nonce: reader.readLongList(offsets[7]), + serial: reader.readLongList(offsets[8]), + serialContext: reader.readLongList(offsets[9]), + tag: reader.readLongList(offsets[10]), + txHash: reader.readString(offsets[11]), + type: _SparkCointypeValueEnumMap[reader.readByteOrNull(offsets[12])] ?? SparkCoinType.mint, - valueIntString: reader.readString(offsets[12]), - walletId: reader.readString(offsets[13]), + valueIntString: reader.readString(offsets[13]), + walletId: reader.readString(offsets[14]), ); object.id = id; return object; @@ -236,9 +243,9 @@ P _sparkCoinDeserializeProp

( case 2: return (reader.readLongList(offset)) as P; case 3: - return (reader.readBool(offset)) as P; + return (reader.readLongOrNull(offset)) as P; case 4: - return (reader.readLongList(offset)) as P; + return (reader.readBool(offset)) as P; case 5: return (reader.readString(offset)) as P; case 6: @@ -250,14 +257,16 @@ P _sparkCoinDeserializeProp

( case 9: return (reader.readLongList(offset)) as P; case 10: - return (reader.readString(offset)) as P; + return (reader.readLongList(offset)) as P; case 11: + return (reader.readString(offset)) as P; + case 12: return (_SparkCointypeValueEnumMap[reader.readByteOrNull(offset)] ?? SparkCoinType.mint) as P; - case 12: - return (reader.readString(offset)) as P; case 13: return (reader.readString(offset)) as P; + case 14: + return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -971,6 +980,75 @@ extension SparkCoinQueryFilter }); } + QueryBuilder heightIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'height', + )); + }); + } + + QueryBuilder heightIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'height', + )); + }); + } + + QueryBuilder heightEqualTo( + int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'height', + value: value, + )); + }); + } + + QueryBuilder heightGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'height', + value: value, + )); + }); + } + + QueryBuilder heightLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'height', + value: value, + )); + }); + } + + QueryBuilder heightBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'height', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder idEqualTo( Id value) { return QueryBuilder.apply(this, (query) { @@ -1034,159 +1112,6 @@ extension SparkCoinQueryFilter }); } - QueryBuilder kIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'k', - )); - }); - } - - QueryBuilder kIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'k', - )); - }); - } - - QueryBuilder kElementEqualTo( - int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'k', - value: value, - )); - }); - } - - QueryBuilder kElementGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'k', - value: value, - )); - }); - } - - QueryBuilder kElementLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'k', - value: value, - )); - }); - } - - QueryBuilder kElementBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'k', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder kLengthEqualTo( - int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'k', - length, - true, - length, - true, - ); - }); - } - - QueryBuilder kIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'k', - 0, - true, - 0, - true, - ); - }); - } - - QueryBuilder kIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'k', - 0, - false, - 999999, - true, - ); - }); - } - - QueryBuilder kLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'k', - 0, - true, - length, - include, - ); - }); - } - - QueryBuilder kLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'k', - length, - include, - 999999, - true, - ); - }); - } - - QueryBuilder kLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'k', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - QueryBuilder lTagHashEqualTo( String value, { bool caseSensitive = true, @@ -1464,6 +1389,162 @@ extension SparkCoinQueryFilter }); } + QueryBuilder nonceIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'nonce', + )); + }); + } + + QueryBuilder nonceIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'nonce', + )); + }); + } + + QueryBuilder nonceElementEqualTo( + int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'nonce', + value: value, + )); + }); + } + + QueryBuilder + nonceElementGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'nonce', + value: value, + )); + }); + } + + QueryBuilder + nonceElementLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'nonce', + value: value, + )); + }); + } + + QueryBuilder nonceElementBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'nonce', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder nonceLengthEqualTo( + int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'nonce', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder nonceIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'nonce', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder nonceIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'nonce', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder nonceLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'nonce', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + nonceLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'nonce', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder nonceLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'nonce', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + QueryBuilder serialIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -2424,6 +2505,18 @@ extension SparkCoinQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByHeight() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'height', Sort.asc); + }); + } + + QueryBuilder sortByHeightDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'height', Sort.desc); + }); + } + QueryBuilder sortByIsUsed() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'isUsed', Sort.asc); @@ -2537,6 +2630,18 @@ extension SparkCoinQuerySortThenBy }); } + QueryBuilder thenByHeight() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'height', Sort.asc); + }); + } + + QueryBuilder thenByHeightDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'height', Sort.desc); + }); + } + QueryBuilder thenById() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.asc); @@ -2658,15 +2763,15 @@ extension SparkCoinQueryWhereDistinct }); } - QueryBuilder distinctByIsUsed() { + QueryBuilder distinctByHeight() { return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isUsed'); + return query.addDistinctBy(r'height'); }); } - QueryBuilder distinctByK() { + QueryBuilder distinctByIsUsed() { return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'k'); + return query.addDistinctBy(r'isUsed'); }); } @@ -2684,6 +2789,12 @@ extension SparkCoinQueryWhereDistinct }); } + QueryBuilder distinctByNonce() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'nonce'); + }); + } + QueryBuilder distinctBySerial() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'serial'); @@ -2759,15 +2870,15 @@ extension SparkCoinQueryProperty }); } - QueryBuilder isUsedProperty() { + QueryBuilder heightProperty() { return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isUsed'); + return query.addPropertyName(r'height'); }); } - QueryBuilder?, QQueryOperations> kProperty() { + QueryBuilder isUsedProperty() { return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'k'); + return query.addPropertyName(r'isUsed'); }); } @@ -2783,6 +2894,12 @@ extension SparkCoinQueryProperty }); } + QueryBuilder?, QQueryOperations> nonceProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'nonce'); + }); + } + QueryBuilder?, QQueryOperations> serialProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'serial'); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index c732af769..5f6917529 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -478,16 +478,18 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { walletId: walletId, type: coinType, isUsed: spentCoinTags.contains(coin.lTagHash!), + nonce: coin.nonceHex?.toUint8ListFromHex, address: coin.address!, txHash: txHash, valueIntString: coin.value!.toString(), - lTagHash: coin.lTagHash!, - tag: coin.tag, memo: coin.memo, - serial: coin.serial, serialContext: coin.serialContext, diversifierIntString: coin.diversifier!.toString(), encryptedDiversifier: coin.encryptedDiversifier, + serial: coin.serial, + tag: coin.tag, + lTagHash: coin.lTagHash!, + height: coin.height, ), ); } From cae0bada66e40ff370d25e596a3ef4febf97a91d Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 15 Dec 2023 08:47:46 -0600 Subject: [PATCH 37/77] update spark balance based on identified coins --- .../spark_interface.dart | 78 +++++++++++++++---- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 5f6917529..33771aace 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -5,6 +5,7 @@ import 'package:bitcoindart/bitcoindart.dart' as btc; import 'package:bitcoindart/src/utils/script.dart' as bscript; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; +import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; @@ -496,8 +497,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } } - print("FOUND COINS: $myCoins"); - // update wallet spark coins in isar if (myCoins.isNotEmpty) { await mainDB.isar.writeTxn(() async { @@ -505,23 +504,68 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { }); } - // refresh spark balance? + final coinsToCheck = await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .heightIsNull() + .findAll(); + final List updatedCoins = []; + for (final coin in coinsToCheck) { + final storedTx = await mainDB.getTransaction(walletId, coin.txHash); + if (storedTx?.height != null) { + updatedCoins.add(coin.copyWith(height: storedTx!.height!)); + } else { + // TODO fetch tx from electrumx (and parse it to db?) + } + } - await prepareSendSpark( - txData: TxData( - sparkRecipients: [ - ( - address: (await getCurrentReceivingSparkAddress())!.value, - amount: Amount( - rawValue: BigInt.from(100000000), - fractionDigits: cryptoCurrency.fractionDigits), - subtractFeeFromAmount: true, - memo: "LOL MEMO OPK", - ), - ], - )); + // update wallet spark coins in isar + if (updatedCoins.isNotEmpty) { + await mainDB.isar.writeTxn(() async { + await mainDB.isar.sparkCoins.putAll(updatedCoins); + }); + } - throw UnimplementedError(); + // refresh spark balance + final currentHeight = await chainHeight; + final unusedCoins = await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .findAll(); + + final total = Amount( + rawValue: unusedCoins + .map((e) => e.value) + .fold(BigInt.zero, (prev, e) => prev + e), + fractionDigits: cryptoCurrency.fractionDigits, + ); + final spendable = Amount( + rawValue: unusedCoins + .where((e) => + e.height != null && + e.height! + cryptoCurrency.minConfirms >= currentHeight) + .map((e) => e.value) + .fold(BigInt.zero, (prev, e) => prev + e), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + final sparkBalance = Balance( + total: total, + spendable: spendable, + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + pendingSpendable: total - spendable, + ); + + await info.updateBalanceTertiary( + newBalance: sparkBalance, + isar: mainDB.isar, + ); } catch (e, s) { // todo logging From 2469c3eb91f708aeac8a6f8c20b0088fe33ff30e Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 15 Dec 2023 13:30:51 -0600 Subject: [PATCH 38/77] small tweaks mainly targeting firo transaction parsing --- .../isar/models/blockchain_data/v2/output_v2.dart | 14 ++++++++------ lib/wallets/wallet/impl/ecash_wallet.dart | 4 ++-- lib/wallets/wallet/impl/firo_wallet.dart | 5 +++++ lib/wallets/wallet/wallet.dart | 7 ++++--- .../wallet_mixin_interfaces/spark_interface.dart | 14 +++++++------- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/v2/output_v2.dart b/lib/models/isar/models/blockchain_data/v2/output_v2.dart index e8f84c54a..2b9ee84fb 100644 --- a/lib/models/isar/models/blockchain_data/v2/output_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/output_v2.dart @@ -46,7 +46,7 @@ class OutputV2 { Map json, { required bool walletOwns, required int decimalPlaces, - bool isECashFullAmountNotSats = false, + bool isFullAmountNotSats = false, }) { try { List addresses = []; @@ -61,9 +61,11 @@ class OutputV2 { return OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: json["scriptPubKey"]["hex"] as String, - valueStringSats: parseOutputAmountString(json["value"].toString(), - decimalPlaces: decimalPlaces, - isECashFullAmountNotSats: isECashFullAmountNotSats), + valueStringSats: parseOutputAmountString( + json["value"].toString(), + decimalPlaces: decimalPlaces, + isFullAmountNotSats: isFullAmountNotSats, + ), addresses: addresses, walletOwns: walletOwns, ); @@ -75,7 +77,7 @@ class OutputV2 { static String parseOutputAmountString( String amount, { required int decimalPlaces, - bool isECashFullAmountNotSats = false, + bool isFullAmountNotSats = false, }) { final temp = Decimal.parse(amount); if (temp < Decimal.zero) { @@ -83,7 +85,7 @@ class OutputV2 { } final String valueStringSats; - if (isECashFullAmountNotSats) { + if (isFullAmountNotSats) { valueStringSats = temp.shift(decimalPlaces).toBigInt().toString(); } else if (temp.isInteger) { valueStringSats = temp.toString(); diff --git a/lib/wallets/wallet/impl/ecash_wallet.dart b/lib/wallets/wallet/impl/ecash_wallet.dart index 739ba0a0c..802c21b77 100644 --- a/lib/wallets/wallet/impl/ecash_wallet.dart +++ b/lib/wallets/wallet/impl/ecash_wallet.dart @@ -169,7 +169,7 @@ class EcashWallet extends Bip39HDWallet final prevOut = OutputV2.fromElectrumXJson( prevOutJson, decimalPlaces: cryptoCurrency.fractionDigits, - isECashFullAmountNotSats: true, + isFullAmountNotSats: true, walletOwns: false, // doesn't matter here as this is not saved ); @@ -208,7 +208,7 @@ class EcashWallet extends Bip39HDWallet OutputV2 output = OutputV2.fromElectrumXJson( Map.from(outputJson as Map), decimalPlaces: cryptoCurrency.fractionDigits, - isECashFullAmountNotSats: true, + isFullAmountNotSats: true, // don't know yet if wallet owns. Need addresses first walletOwns: false, ); diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 820b7836a..2628b0d94 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -193,6 +193,7 @@ class FiroWallet extends Bip39HDWallet OutputV2 output = OutputV2.fromElectrumXJson( outMap, decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, // don't know yet if wallet owns. Need addresses first walletOwns: false, ); @@ -294,6 +295,7 @@ class FiroWallet extends Bip39HDWallet final prevOut = OutputV2.fromElectrumXJson( prevOutJson, decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, walletOwns: false, // doesn't matter here as this is not saved ); @@ -351,6 +353,9 @@ class FiroWallet extends Bip39HDWallet totalOut) { // definitely sent all to self type = TransactionType.sentToSelf; + } else if (isSparkMint) { + // probably sent to self + type = TransactionType.sentToSelf; } else if (amountReceivedInThisWallet == BigInt.zero) { // most likely just a typical send // do nothing here yet diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index b4b1340ef..54a1b0aa5 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -421,6 +421,10 @@ abstract class Wallet { .checkChangeAddressForTransactions(); } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); + if (this is SparkInterface) { + // this should be called before updateTransactions() + await (this as SparkInterface).refreshSparkData(); + } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.50, walletId)); final fetchFuture = updateTransactions(); @@ -440,9 +444,6 @@ abstract class Wallet { if (this is LelantusInterface) { await (this as LelantusInterface).refreshLelantusData(); } - if (this is SparkInterface) { - await (this as SparkInterface).refreshSparkData(); - } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.90, walletId)); await updateBalance(); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 33771aace..3895a6e37 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -504,6 +504,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { }); } + // update wallet spark coin height final coinsToCheck = await mainDB.isar.sparkCoins .where() .walletIdEqualToAnyLTagHash(walletId) @@ -512,15 +513,14 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .findAll(); final List updatedCoins = []; for (final coin in coinsToCheck) { - final storedTx = await mainDB.getTransaction(walletId, coin.txHash); - if (storedTx?.height != null) { - updatedCoins.add(coin.copyWith(height: storedTx!.height!)); - } else { - // TODO fetch tx from electrumx (and parse it to db?) + final tx = await electrumXCachedClient.getTransaction( + txHash: coin.txHash, + coin: info.coin, + ); + if (tx["height"] is int) { + updatedCoins.add(coin.copyWith(height: tx["height"] as int)); } } - - // update wallet spark coins in isar if (updatedCoins.isNotEmpty) { await mainDB.isar.writeTxn(() async { await mainDB.isar.sparkCoins.putAll(updatedCoins); From 8336712a2366ceef12e6338f0c5186ff6d1c1bdf Mon Sep 17 00:00:00 2001 From: julian Date: Sat, 16 Dec 2023 10:19:50 -0600 Subject: [PATCH 39/77] some firo transaction display fixes --- .../blockchain_data/v2/transaction_v2.dart | 11 + .../wallet_view/sub_widgets/tx_icon.dart | 11 + .../tx_v2/transaction_v2_card.dart | 4 +- .../tx_v2/transaction_v2_details_view.dart | 7 +- lib/wallets/wallet/impl/firo_wallet.dart | 623 +----------------- 5 files changed, 49 insertions(+), 607 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index f543636ff..067069b50 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:math'; import 'package:isar/isar.dart'; @@ -102,6 +103,15 @@ class TransactionV2 { ...outputs.map((e) => e.addresses).expand((e) => e), }; + Amount? getAnonFee() { + try { + final map = jsonDecode(otherData!) as Map; + return Amount.fromSerializedJsonString(map["anonFees"] as String); + } catch (_) { + return null; + } + } + @override String toString() { return 'TransactionV2(\n' @@ -116,6 +126,7 @@ class TransactionV2 { ' version: $version,\n' ' inputs: $inputs,\n' ' outputs: $outputs,\n' + ' otherData: $otherData,\n' ')'; } } diff --git a/lib/pages/wallet_view/sub_widgets/tx_icon.dart b/lib/pages/wallet_view/sub_widgets/tx_icon.dart index 37ab9617c..c942bb621 100644 --- a/lib/pages/wallet_view/sub_widgets/tx_icon.dart +++ b/lib/pages/wallet_view/sub_widgets/tx_icon.dart @@ -56,6 +56,17 @@ class TxIcon extends ConsumerWidget { return Assets.svg.anonymize; } + if (subType == TransactionSubType.mint || + subType == TransactionSubType.sparkMint) { + if (isCancelled) { + return Assets.svg.anonymizeFailed; + } + if (isPending) { + return Assets.svg.anonymizePending; + } + return Assets.svg.anonymize; + } + if (isReceived) { if (isCancelled) { return assets.receiveCancelled; diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart index f191a3439..7ec4e1b78 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart @@ -50,7 +50,9 @@ class _TransactionCardStateV2 extends ConsumerState { ref.read(pWallets).getWallet(walletId).cryptoCurrency.minConfirms, ); - if (_transaction.subType == TransactionSubType.cashFusion) { + if (_transaction.subType == TransactionSubType.cashFusion || + _transaction.subType == TransactionSubType.sparkMint || + _transaction.subType == TransactionSubType.mint) { if (confirmedStatus) { return "Anonymized"; } else { 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 f20b2a299..4eada7202 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 @@ -95,7 +95,12 @@ class _TransactionV2DetailsViewState minConfirms = ref.read(pWallets).getWallet(walletId).cryptoCurrency.minConfirms; - fee = _transaction.getFee(coin: coin); + if (_transaction.subType == TransactionSubType.join || + _transaction.subType == TransactionSubType.sparkSpend) { + fee = _transaction.getAnonFee()!; + } else { + fee = _transaction.getFee(coin: coin); + } if (_transaction.subType == TransactionSubType.cashFusion || _transaction.type == TransactionType.sentToSelf) { diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 2628b0d94..8ab99b011 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -13,11 +13,11 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; -import 'package:tuple/tuple.dart'; const sparkStartBlock = 819300; // (approx 18 Jan 2024) @@ -60,6 +60,22 @@ class FiroWallet extends Bip39HDWallet final List> allTxHashes = await fetchHistory(allAddressesSet); + final sparkTxids = await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .txHashProperty() + .findAll(); + + for (final txid in sparkTxids) { + // check for duplicates before adding to list + if (allTxHashes.indexWhere((e) => e["tx_hash"] == txid) == -1) { + final info = { + "tx_hash": txid, + }; + allTxHashes.add(info); + } + } + List> allTransactions = []; // some lelantus transactions aren't fetched via wallet addresses so they @@ -108,7 +124,7 @@ class FiroWallet extends Bip39HDWallet if (allTransactions .indexWhere((e) => e["txid"] == tx["txid"] as String) == -1) { - tx["height"] = txHash["height"]; + tx["height"] ??= txHash["height"]; allTransactions.add(tx); } // } @@ -133,10 +149,6 @@ class FiroWallet extends Bip39HDWallet bool isMasterNodePayment = false; final bool isSparkSpend = txData["type"] == 9 && txData["version"] == 3; - if (txData.toString().contains("spark")) { - Util.printJson(txData); - } - // parse outputs final List outputs = []; for (final outputJson in txData["vout"] as List) { @@ -415,605 +427,6 @@ class FiroWallet extends Bip39HDWallet await mainDB.updateOrPutTransactionV2s(txns); } - Future updateTransactionsOLD() async { - final allAddresses = await fetchAddressesForElectrumXScan(); - - Set receivingAddresses = allAddresses - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - Set changeAddresses = allAddresses - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); - - final List> allTxHashes = - await fetchHistory(allAddresses.map((e) => e.value).toList()); - - List> allTransactions = []; - - // some lelantus transactions aren't fetched via wallet addresses so they - // will never show as confirmed in the gui. - final unconfirmedTransactions = await mainDB - .getTransactions(walletId) - .filter() - .heightIsNull() - .findAll(); - for (final tx in unconfirmedTransactions) { - final txn = await electrumXCachedClient.getTransaction( - txHash: tx.txid, - verbose: true, - coin: info.coin, - ); - final height = txn["height"] as int?; - - if (height != null) { - // tx was mined - // add to allTxHashes - final info = { - "tx_hash": tx.txid, - "height": height, - "address": tx.address.value?.value, - }; - allTxHashes.add(info); - } - } - - // final currentHeight = await chainHeight; - - for (final txHash in allTxHashes) { - // final storedTx = await db - // .getTransactions(walletId) - // .filter() - // .txidEqualTo(txHash["tx_hash"] as String) - // .findFirst(); - - // if (storedTx == null || - // !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) { - final tx = await electrumXCachedClient.getTransaction( - txHash: txHash["tx_hash"] as String, - verbose: true, - coin: info.coin, - ); - - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == - -1) { - tx["address"] = await mainDB - .getAddresses(walletId) - .filter() - .valueEqualTo(txHash["address"] as String) - .findFirst(); - tx["height"] = txHash["height"]; - allTransactions.add(tx); - } - // } - } - - final List> txnsData = []; - - for (final txObject in allTransactions) { - final inputList = txObject["vin"] as List; - final outputList = txObject["vout"] as List; - - bool isMint = false; - bool isJMint = false; - bool isSparkMint = false; - bool isSparkSpend = false; - - // check if tx is Mint or jMint - for (final output in outputList) { - if (output["scriptPubKey"]?["type"] == "lelantusmint") { - final asm = output["scriptPubKey"]?["asm"] as String?; - if (asm != null) { - if (asm.startsWith("OP_LELANTUSJMINT")) { - isJMint = true; - break; - } else if (asm.startsWith("OP_LELANTUSMINT")) { - isMint = true; - break; - } else { - Logging.instance.log( - "Unknown mint op code found for lelantusmint tx: ${txObject["txid"]}", - level: LogLevel.Error, - ); - } - } else { - Logging.instance.log( - "ASM for lelantusmint tx: ${txObject["txid"]} is null!", - level: LogLevel.Error, - ); - } - } - if (output["scriptPubKey"]?["type"] == "sparkmint") { - final asm = output["scriptPubKey"]?["asm"] as String?; - if (asm != null) { - if (asm.startsWith("OP_SPARKMINT")) { - isSparkMint = true; - break; - } else if (asm.startsWith("OP_SPARKSPEND")) { - isSparkSpend = true; - break; - } else { - Logging.instance.log( - "Unknown mint op code found for lelantusmint tx: ${txObject["txid"]}", - level: LogLevel.Error, - ); - } - } else { - Logging.instance.log( - "ASM for sparkmint tx: ${txObject["txid"]} is null!", - level: LogLevel.Error, - ); - } - } - } - - if (isSparkSpend || isSparkMint) { - continue; - } - - Set inputAddresses = {}; - Set outputAddresses = {}; - - Amount totalInputValue = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - Amount totalOutputValue = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - - Amount amountSentFromWallet = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - Amount amountReceivedInWallet = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - Amount changeAmount = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // Parse mint transaction ================================================ - // We should be able to assume this belongs to this wallet - if (isMint) { - List ins = []; - - // Parse inputs - for (final input in inputList) { - // Both value and address should not be null for a mint - final address = input["address"] as String?; - final value = input["valueSat"] as int?; - - // We should not need to check whether the mint belongs to this - // wallet as any tx we look up will be looked up by one of this - // wallet's addresses - if (address != null && value != null) { - totalInputValue += value.toAmountAsRaw( - fractionDigits: cryptoCurrency.fractionDigits, - ); - } - - ins.add( - Input( - txid: input['txid'] as String? ?? "", - vout: input['vout'] as int? ?? -1, - scriptSig: input['scriptSig']?['hex'] as String?, - scriptSigAsm: input['scriptSig']?['asm'] as String?, - isCoinbase: input['is_coinbase'] as bool?, - sequence: input['sequence'] as int?, - innerRedeemScriptAsm: input['innerRedeemscriptAsm'] as String?, - ), - ); - } - - // Parse outputs - for (final output in outputList) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // add value to total - totalOutputValue += value; - } - - final fee = totalInputValue - totalOutputValue; - final tx = Transaction( - walletId: walletId, - txid: txObject["txid"] as String, - timestamp: txObject["blocktime"] as int? ?? - (DateTime.now().millisecondsSinceEpoch ~/ 1000), - type: TransactionType.sentToSelf, - subType: TransactionSubType.mint, - amount: totalOutputValue.raw.toInt(), - amountString: totalOutputValue.toJsonString(), - fee: fee.raw.toInt(), - height: txObject["height"] as int?, - isCancelled: false, - isLelantus: true, - slateId: null, - otherData: null, - nonce: null, - inputs: ins, - outputs: [], - numberOfMessages: null, - ); - - txnsData.add(Tuple2(tx, null)); - - // Otherwise parse JMint transaction =================================== - } else if (isJMint) { - Amount jMintFees = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // Parse inputs - List ins = []; - for (final input in inputList) { - // JMint fee - final nFee = Decimal.tryParse(input["nFees"].toString()); - if (nFee != null) { - final fees = Amount.fromDecimal( - nFee, - fractionDigits: cryptoCurrency.fractionDigits, - ); - - jMintFees += fees; - } - - ins.add( - Input( - txid: input['txid'] as String? ?? "", - vout: input['vout'] as int? ?? -1, - scriptSig: input['scriptSig']?['hex'] as String?, - scriptSigAsm: input['scriptSig']?['asm'] as String?, - isCoinbase: input['is_coinbase'] as bool?, - sequence: input['sequence'] as int?, - innerRedeemScriptAsm: input['innerRedeemscriptAsm'] as String?, - ), - ); - } - - bool nonWalletAddressFoundInOutputs = false; - - // Parse outputs - List outs = []; - for (final output in outputList) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // add value to total - totalOutputValue += value; - - final address = output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output['scriptPubKey']?['address'] as String?; - - if (address != null) { - outputAddresses.add(address); - if (receivingAddresses.contains(address) || - changeAddresses.contains(address)) { - amountReceivedInWallet += value; - } else { - nonWalletAddressFoundInOutputs = true; - } - } - - outs.add( - Output( - scriptPubKey: output['scriptPubKey']?['hex'] as String?, - scriptPubKeyAsm: output['scriptPubKey']?['asm'] as String?, - scriptPubKeyType: output['scriptPubKey']?['type'] as String?, - scriptPubKeyAddress: address ?? "jmint", - value: value.raw.toInt(), - ), - ); - } - final txid = txObject["txid"] as String; - - const subType = TransactionSubType.join; - - final type = nonWalletAddressFoundInOutputs - ? TransactionType.outgoing - : (await mainDB.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(txid) - .findFirst()) == - null - ? TransactionType.incoming - : TransactionType.sentToSelf; - - final amount = nonWalletAddressFoundInOutputs - ? totalOutputValue - : amountReceivedInWallet; - - final possibleNonWalletAddresses = - receivingAddresses.difference(outputAddresses); - final possibleReceivingAddresses = - receivingAddresses.intersection(outputAddresses); - - final transactionAddress = nonWalletAddressFoundInOutputs - ? Address( - walletId: walletId, - value: possibleNonWalletAddresses.first, - derivationIndex: -1, - derivationPath: null, - type: AddressType.nonWallet, - subType: AddressSubType.nonWallet, - publicKey: [], - ) - : allAddresses.firstWhere( - (e) => e.value == possibleReceivingAddresses.first, - ); - - final tx = Transaction( - walletId: walletId, - txid: txid, - timestamp: txObject["blocktime"] as int? ?? - (DateTime.now().millisecondsSinceEpoch ~/ 1000), - type: type, - subType: subType, - amount: amount.raw.toInt(), - amountString: amount.toJsonString(), - fee: jMintFees.raw.toInt(), - height: txObject["height"] as int?, - isCancelled: false, - isLelantus: true, - slateId: null, - otherData: null, - nonce: null, - inputs: ins, - outputs: outs, - numberOfMessages: null, - ); - - txnsData.add(Tuple2(tx, transactionAddress)); - - // Master node payment ===================================== - } else if (inputList.length == 1 && - inputList.first["coinbase"] is String) { - List ins = [ - Input( - txid: inputList.first["coinbase"] as String, - vout: -1, - scriptSig: null, - scriptSigAsm: null, - isCoinbase: true, - sequence: inputList.first['sequence'] as int?, - innerRedeemScriptAsm: null, - ), - ]; - - // parse outputs - List outs = []; - for (final output in outputList) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // get output address - final address = output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]?["address"] as String?; - if (address != null) { - outputAddresses.add(address); - - // if output was to my wallet, add value to amount received - if (receivingAddresses.contains(address)) { - amountReceivedInWallet += value; - } - } - - outs.add( - Output( - scriptPubKey: output['scriptPubKey']?['hex'] as String?, - scriptPubKeyAsm: output['scriptPubKey']?['asm'] as String?, - scriptPubKeyType: output['scriptPubKey']?['type'] as String?, - scriptPubKeyAddress: address ?? "", - value: value.raw.toInt(), - ), - ); - } - - // this is the address initially used to fetch the txid - Address transactionAddress = txObject["address"] as Address; - - final tx = Transaction( - walletId: walletId, - txid: txObject["txid"] as String, - timestamp: txObject["blocktime"] as int? ?? - (DateTime.now().millisecondsSinceEpoch ~/ 1000), - type: TransactionType.incoming, - subType: TransactionSubType.none, - // amount may overflow. Deprecated. Use amountString - amount: amountReceivedInWallet.raw.toInt(), - amountString: amountReceivedInWallet.toJsonString(), - fee: 0, - height: txObject["height"] as int?, - isCancelled: false, - isLelantus: false, - slateId: null, - otherData: null, - nonce: null, - inputs: ins, - outputs: outs, - numberOfMessages: null, - ); - - txnsData.add(Tuple2(tx, transactionAddress)); - - // Assume non lelantus transaction ===================================== - } else { - // parse inputs - List ins = []; - for (final input in inputList) { - final valueSat = input["valueSat"] as int?; - final address = input["address"] as String? ?? - input["scriptPubKey"]?["address"] as String? ?? - input["scriptPubKey"]?["addresses"]?[0] as String?; - - if (address != null && valueSat != null) { - final value = valueSat.toAmountAsRaw( - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // add value to total - totalInputValue += value; - inputAddresses.add(address); - - // if input was from my wallet, add value to amount sent - if (receivingAddresses.contains(address) || - changeAddresses.contains(address)) { - amountSentFromWallet += value; - } - } - - if (input['txid'] == null) { - continue; - } - - ins.add( - Input( - txid: input['txid'] as String, - vout: input['vout'] as int? ?? -1, - scriptSig: input['scriptSig']?['hex'] as String?, - scriptSigAsm: input['scriptSig']?['asm'] as String?, - isCoinbase: input['is_coinbase'] as bool?, - sequence: input['sequence'] as int?, - innerRedeemScriptAsm: input['innerRedeemscriptAsm'] as String?, - ), - ); - } - - // parse outputs - List outs = []; - for (final output in outputList) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // add value to total - totalOutputValue += value; - - // get output address - final address = output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]?["address"] as String?; - if (address != null) { - outputAddresses.add(address); - - // if output was to my wallet, add value to amount received - if (receivingAddresses.contains(address)) { - amountReceivedInWallet += value; - } else if (changeAddresses.contains(address)) { - changeAmount += value; - } - } - - outs.add( - Output( - scriptPubKey: output['scriptPubKey']?['hex'] as String?, - scriptPubKeyAsm: output['scriptPubKey']?['asm'] as String?, - scriptPubKeyType: output['scriptPubKey']?['type'] as String?, - scriptPubKeyAddress: address ?? "", - value: value.raw.toInt(), - ), - ); - } - - final mySentFromAddresses = [ - ...receivingAddresses.intersection(inputAddresses), - ...changeAddresses.intersection(inputAddresses) - ]; - final myReceivedOnAddresses = - receivingAddresses.intersection(outputAddresses); - final myChangeReceivedOnAddresses = - changeAddresses.intersection(outputAddresses); - - final fee = totalInputValue - totalOutputValue; - - // this is the address initially used to fetch the txid - Address transactionAddress = txObject["address"] as Address; - - TransactionType type; - Amount amount; - if (mySentFromAddresses.isNotEmpty && - myReceivedOnAddresses.isNotEmpty) { - // tx is sent to self - type = TransactionType.sentToSelf; - - // should be 0 - amount = amountSentFromWallet - - amountReceivedInWallet - - fee - - changeAmount; - } else if (mySentFromAddresses.isNotEmpty) { - // outgoing tx - type = TransactionType.outgoing; - amount = amountSentFromWallet - changeAmount - fee; - - final possible = - outputAddresses.difference(myChangeReceivedOnAddresses).first; - - if (transactionAddress.value != possible) { - transactionAddress = Address( - walletId: walletId, - value: possible, - derivationIndex: -1, - derivationPath: null, - subType: AddressSubType.nonWallet, - type: AddressType.nonWallet, - publicKey: [], - ); - } - } else { - // incoming tx - type = TransactionType.incoming; - amount = amountReceivedInWallet; - } - - final tx = Transaction( - walletId: walletId, - txid: txObject["txid"] as String, - timestamp: txObject["blocktime"] as int? ?? - (DateTime.now().millisecondsSinceEpoch ~/ 1000), - type: type, - subType: TransactionSubType.none, - // amount may overflow. Deprecated. Use amountString - amount: amount.raw.toInt(), - amountString: amount.toJsonString(), - fee: fee.raw.toInt(), - height: txObject["height"] as int?, - isCancelled: false, - isLelantus: false, - slateId: null, - otherData: null, - nonce: null, - inputs: ins, - outputs: outs, - numberOfMessages: null, - ); - - txnsData.add(Tuple2(tx, transactionAddress)); - } - } - - await mainDB.addNewTransactionData(txnsData, walletId); - } - @override ({String? blockedReason, bool blocked}) checkBlockUTXO( Map jsonUTXO, From c1640331af8b38b70312b83915bcf171c2041546 Mon Sep 17 00:00:00 2001 From: julian Date: Sat, 16 Dec 2023 14:26:23 -0600 Subject: [PATCH 40/77] spark coins ui view --- .../sub_widgets/wallet_options_button.dart | 57 ++++- .../spark_coins/spark_coins_view.dart | 236 ++++++++++++++++++ lib/route_generator.dart | 15 ++ .../spark_interface.dart | 41 ++- 4 files changed, 325 insertions(+), 24 deletions(-) create mode 100644 lib/pages_desktop_specific/spark_coins/spark_coins_view.dart diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart index 20b3278da..f495475db 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart @@ -19,6 +19,7 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_set import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart'; import 'package:stackwallet/pages_desktop_specific/lelantus_coins/lelantus_coins_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; +import 'package:stackwallet/pages_desktop_specific/spark_coins/spark_coins_view.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -32,7 +33,8 @@ enum _WalletOptions { deleteWallet, changeRepresentative, showXpub, - lelantusCoins; + lelantusCoins, + sparkCoins; String get prettyName { switch (this) { @@ -46,6 +48,8 @@ enum _WalletOptions { return "Show xPub"; case _WalletOptions.lelantusCoins: return "Lelantus Coins"; + case _WalletOptions.sparkCoins: + return "Spark Coins"; } } } @@ -89,6 +93,9 @@ class WalletOptionsButton extends StatelessWidget { onFiroShowLelantusCoins: () async { Navigator.of(context).pop(_WalletOptions.lelantusCoins); }, + onFiroShowSparkCoins: () async { + Navigator.of(context).pop(_WalletOptions.sparkCoins); + }, walletId: walletId, ); }, @@ -191,6 +198,15 @@ class WalletOptionsButton extends StatelessWidget { ), ); break; + + case _WalletOptions.sparkCoins: + unawaited( + Navigator.of(context).pushNamed( + SparkCoinsView.routeName, + arguments: walletId, + ), + ); + break; } } }, @@ -224,6 +240,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget { required this.onShowXpubPressed, required this.onChangeRepPressed, required this.onFiroShowLelantusCoins, + required this.onFiroShowSparkCoins, required this.walletId, }) : super(key: key); @@ -232,6 +249,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget { final VoidCallback onShowXpubPressed; final VoidCallback onChangeRepPressed; final VoidCallback onFiroShowLelantusCoins; + final VoidCallback onFiroShowSparkCoins; final String walletId; @override @@ -374,6 +392,43 @@ class WalletOptionsPopupMenu extends ConsumerWidget { ), ), ), + if (firoDebug) + const SizedBox( + height: 8, + ), + if (firoDebug) + TransparentButton( + onPressed: onFiroShowSparkCoins, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.eye, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + const SizedBox(width: 14), + Expanded( + child: Text( + _WalletOptions.sparkCoins.prettyName, + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ], + ), + ), + ), if (xpubEnabled) const SizedBox( height: 8, diff --git a/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart b/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart new file mode 100644 index 000000000..73ef56912 --- /dev/null +++ b/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart @@ -0,0 +1,236 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class SparkCoinsView extends ConsumerStatefulWidget { + const SparkCoinsView({ + Key? key, + required this.walletId, + }) : super(key: key); + + static const String routeName = "/sparkCoinsView"; + + final String walletId; + + @override + ConsumerState createState() => _SparkCoinsViewState(); +} + +class _SparkCoinsViewState extends ConsumerState { + List _coins = []; + + Stream>? sparkCoinsCollectionWatcher; + + void _onSparkCoinsCollectionWatcherEvent(List coins) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _coins = coins; + }); + } + }); + } + + @override + void initState() { + sparkCoinsCollectionWatcher = ref + .read(mainDBProvider) + .isar + .sparkCoins + .where() + .walletIdEqualToAnyLTagHash(widget.walletId) + .sortByHeightDesc() + .watch(fireImmediately: true); + sparkCoinsCollectionWatcher! + .listen((data) => _onSparkCoinsCollectionWatcherEvent(data)); + + super.initState(); + } + + @override + void dispose() { + sparkCoinsCollectionWatcher = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + 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( + "Spark Coins", + style: STextStyles.desktopH3(context), + ), + const Spacer(), + ], + ), + ), + useSpacers: false, + isCompactHeight: true, + ), + body: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Row( + children: [ + Expanded( + flex: 9, + child: Text( + "TXID", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ), + Expanded( + flex: 3, + child: Text( + "Value (sats)", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.right, + ), + ), + Expanded( + flex: 2, + child: Text( + "Height", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.right, + ), + ), + Expanded( + flex: 2, + child: Text( + "Type", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.right, + ), + ), + Expanded( + flex: 2, + child: Text( + "Used", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.right, + ), + ), + ], + ), + ), + ), + Expanded( + child: ListView.separated( + shrinkWrap: true, + itemCount: _coins.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: [ + Expanded( + flex: 9, + child: SelectableText( + _coins[index].txHash, + style: STextStyles.itemSubtitle12(context), + ), + ), + Expanded( + flex: 3, + child: SelectableText( + _coins[index].value.toString(), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ), + Expanded( + flex: 2, + child: SelectableText( + _coins[index].height.toString(), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ), + Expanded( + flex: 2, + child: SelectableText( + _coins[index].type.name, + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ), + Expanded( + flex: 2, + child: SelectableText( + _coins[index].isUsed.toString(), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 289a8bb37..11c8f6237 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -175,6 +175,7 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/nodes_ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/tor_settings/tor_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/spark_coins/spark_coins_view.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -1858,6 +1859,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case SparkCoinsView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SparkCoinsView( + 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/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 3895a6e37..c6da88803 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -715,34 +715,29 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { txb.setLockTime(await chainHeight); txb.setVersion(1); - // Create a mint script. - final mintScript = bscript.compile([ - 0xd1, // OP_SPARKMINT. - Uint8List(0), - ]); - - // Add inputs. - for (final utxo in txData.utxos!) { - txb.addInput( - utxo.txid, - utxo.vout, - 0xffffffff, - mintScript, - ); - } + final signingData = await fetchBuildTxData(txData.utxos!.toList()); // Create the serial context. // // "...serial_context is a byte array, which should be unique for each // transaction, and for that we serialize and put all inputs into // serial_context vector." - List serialContext = []; - for (final utxo in txData.utxos!) { - serialContext.addAll( - bscript.compile([ - utxo.txid, - utxo.vout, - ]), + final serialContext = LibSpark.serializeMintContext( + inputs: signingData + .map((e) => ( + e.utxo.txid, + e.utxo.vout, + )) + .toList(), + ); + + // Add inputs. + for (final sd in signingData) { + txb.addInput( + sd.utxo.txid, + sd.utxo.vout, + null, + sd.output, ); } @@ -756,7 +751,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { )) .toList(), serialContext: Uint8List.fromList(serialContext), - // generate: true // TODO is this needed? + generate: true, ); // Add mint output(s). From e4bb2aeca7b29f4fdf95133a91823cc52f60bf14 Mon Sep 17 00:00:00 2001 From: julian Date: Sat, 16 Dec 2023 14:28:04 -0600 Subject: [PATCH 41/77] WIP spark mints (broken) --- .../global_settings_view/hidden_settings.dart | 123 +++++++++++++----- .../spark_interface.dart | 49 +++++-- 2 files changed, 134 insertions(+), 38 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 0d06fe7e6..96d41cd6e 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -14,12 +14,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:hive_flutter/hive_flutter.dart'; +import 'package:isar/isar.dart'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; @@ -27,6 +31,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -664,35 +669,95 @@ class HiddenSettings extends StatelessWidget { ); }, ), - // const SizedBox( - // height: 12, - // ), - // GestureDetector( - // onTap: () async { - // showDialog( - // context: context, - // builder: (_) { - // return StackDialogBase( - // child: SizedBox( - // width: 300, - // child: Lottie.asset( - // Assets.lottie.plain(Coin.bitcoincash), - // ), - // ), - // ); - // }, - // ); - // }, - // child: RoundedWhiteContainer( - // child: Text( - // "Lottie test", - // style: STextStyles.button(context).copyWith( - // color: Theme.of(context) - // .extension()! - // .accentColorDark), - // ), - // ), - // ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + const enableBurningMints = false; + + try { + if (enableBurningMints) { + final wallet = ref + .read(pWallets) + .wallets + .firstWhere((e) => + e.info.name == "circle chunk") + as FiroWallet; + + final utxos = await ref + .read(mainDBProvider) + .isar + .utxos + .where() + .walletIdEqualTo(wallet.walletId) + .findAll(); + + final Set utxosToUse = {}; + + for (final u in utxos) { + if (u.used != true && + u.value < 500000000 && + u.value > 9000000) { + utxosToUse.add(u); + break; + } + if (utxosToUse.length > 2) { + break; + } + } + + print("utxosToUse: $utxosToUse"); + + final inputData = TxData( + utxos: utxosToUse, + recipients: [ + ( + address: (await wallet + .getCurrentReceivingSparkAddress())! + .value, + amount: Amount( + rawValue: BigInt.from(utxosToUse + .map((e) => e.value) + .fold(0, (p, e) => p + e) - + 20000), + fractionDigits: 8, + ), + ), + ], + ); + + final mint = await wallet + .prepareSparkMintTransaction( + txData: inputData, + ); + + print("MINT: $mint"); + + print("Submitting..."); + final result = await wallet + .confirmSparkMintTransaction( + txData: mint); + print("Submitted result: $result"); + } + } catch (e, s) { + print("$e\n$s"); + } + }, + child: RoundedWhiteContainer( + child: Text( + "💣💣💣 DANGER 💣💣💣** Random Spark mint **💣💣💣 DANGER 💣💣💣 ", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ); + }, + ), ], ), ), diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index c6da88803..337daf46c 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -9,6 +9,7 @@ import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; +import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; @@ -625,12 +626,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { throw Exception("No inputs provided."); } - // For now let's limit to one input. - if (txData.utxos!.length > 1) { - throw Exception("Only one input supported."); - // TODO remove and test with multiple inputs. - } - // Validate individual inputs. for (final utxo in txData.utxos!) { // Input amount must be greater than zero. @@ -762,9 +757,41 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); } - // TODO Sign the transaction. + try { + // Sign the transaction accordingly + for (var i = 0; i < signingData.length; i++) { + txb.sign( + vin: i, + keyPair: signingData[i].keyPair!, + witnessValue: signingData[i].utxo.value, + redeemScript: signingData[i].redeemScript, + ); + } + } catch (e, s) { + Logging.instance.log( + "Caught exception while signing spark mint transaction: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } - throw UnimplementedError(); + final builtTx = txb.build(); + + // TODO any changes to this txData object required? + return txData.copyWith( + // recipients: [ + // ( + // amount: Amount( + // rawValue: BigInt.from(incomplete.outs[0].value!), + // fractionDigits: cryptoCurrency.fractionDigits, + // ), + // address: "no address for lelantus mints", + // ) + // ], + vSize: builtTx.virtualSize(), + txid: builtTx.getId(), + raw: builtTx.toHex(), + ); } /// Broadcast a tx and TODO update Spark balance. @@ -775,7 +802,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); // Check txid. - assert(txid == txData.txid!); + if (txid == txData.txid!) { + print("SPARK TXIDS MATCH!!"); + } else { + print("SUBMITTED SPARK TXID DOES NOT MATCH WHAT WE GENERATED"); + } // TODO update spark balance. From 4e96ce5438808c1f6b9bb98e83b4132978610efd Mon Sep 17 00:00:00 2001 From: julian Date: Sat, 16 Dec 2023 15:01:47 -0600 Subject: [PATCH 42/77] empty memo (just like firo-qt) --- lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 337daf46c..feb6f5a66 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -742,7 +742,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .map((e) => ( sparkAddress: e.address, value: e.amount.raw.toInt(), - memo: "Stackwallet spark mint" + memo: "", )) .toList(), serialContext: Uint8List.fromList(serialContext), From cdd9b30cb7a1acd6393dd6ab6a74dd6c97fa2c69 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 18 Dec 2023 12:53:11 -0600 Subject: [PATCH 43/77] standard firo send fixes --- .../wallet_view/sub_widgets/desktop_send.dart | 15 +++++++-------- .../electrumx_interface.dart | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 10 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 7cd3782e1..1affff3b8 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 @@ -142,15 +142,14 @@ class _DesktopSendState extends ConsumerState { if ((coin == Coin.firo || coin == Coin.firoTestNet)) { if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { - availableBalance = wallet.info.cachedBalance.spendable; + availableBalance = wallet.info.cachedBalanceSecondary.spendable; // (manager.wallet as FiroWallet).availablePrivateBalance(); } else { - availableBalance = wallet.info.cachedBalanceSecondary.spendable; + availableBalance = wallet.info.cachedBalance.spendable; // (manager.wallet as FiroWallet).availablePublicBalance(); } } else { availableBalance = wallet.info.cachedBalance.spendable; - ; } final coinControlEnabled = @@ -821,7 +820,7 @@ class _DesktopSendState extends ConsumerState { const SizedBox( height: 4, ), - if (coin == Coin.firo) + if (coin == Coin.firo || coin == Coin.firoTestNet) Text( "Send from", style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -831,11 +830,11 @@ class _DesktopSendState extends ConsumerState { ), textAlign: TextAlign.left, ), - if (coin == Coin.firo) + if (coin == Coin.firo || coin == Coin.firoTestNet) const SizedBox( height: 10, ), - if (coin == Coin.firo) + if (coin == Coin.firo || coin == Coin.firoTestNet) DropdownButtonHideUnderline( child: DropdownButton2( isExpanded: true, @@ -917,7 +916,7 @@ class _DesktopSendState extends ConsumerState { ), ), ), - if (coin == Coin.firo) + if (coin == Coin.firo || coin == Coin.firoTestNet) const SizedBox( height: 20, ), @@ -1486,7 +1485,7 @@ class _DesktopSendState extends ConsumerState { .read( publicPrivateBalanceStateProvider .state) - .state != + .state == "Private") { throw UnimplementedError("FIXME"); // TODO: [prio=high] firo fee fix diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 7146454f1..cd7e590ec 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -625,9 +625,19 @@ mixin ElectrumXInterface on Bip39HDWallet { // TODO: use coinlib final txb = bitcoindart.TransactionBuilder( - network: bitcoindart.testnet, + network: bitcoindart.NetworkType( + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: bitcoindart.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, + ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ), ); - txb.setVersion(1); + txb.setVersion(1); // TODO possibly override this for certain coins? // Add transaction inputs for (var i = 0; i < utxoSigningData.length; i++) { From 1c0b9bec1b428a9a837ab460be73bcfdf7f7cb54 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 18 Dec 2023 12:56:27 -0600 Subject: [PATCH 44/77] spark mint sequence fix --- lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index feb6f5a66..d5288df8b 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -731,7 +731,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { txb.addInput( sd.utxo.txid, sd.utxo.vout, - null, + 0xffffffff - 1, sd.output, ); } From f8a5e44d7b5a63e0a1581a7c27a9c44d7d7e2b9a Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 18 Dec 2023 14:05:22 -0600 Subject: [PATCH 45/77] optimize spark coin refresh, refactor and clean up spark wallet recovery, and add extra data fields to the spark coin schema --- lib/db/isar/main_db.dart | 6 + lib/wallets/isar/models/spark_coin.dart | 11 + lib/wallets/isar/models/spark_coin.g.dart | 525 ++++++++++++++++-- lib/wallets/wallet/impl/firo_wallet.dart | 35 +- .../spark_interface.dart | 385 +++++++------ 5 files changed, 736 insertions(+), 226 deletions(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index ee7cac6b0..19bcb16ae 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -484,6 +484,12 @@ class MainDB { // .findAll(); // await isar.lelantusCoins.deleteAll(lelantusCoinIds); // } + + // spark coins + await isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .deleteAll(); }); } diff --git a/lib/wallets/isar/models/spark_coin.dart b/lib/wallets/isar/models/spark_coin.dart index 7f0179e52..9c11c4556 100644 --- a/lib/wallets/isar/models/spark_coin.dart +++ b/lib/wallets/isar/models/spark_coin.dart @@ -49,6 +49,9 @@ class SparkCoin { final int? height; + final String? serializedCoinB64; + final String? contextB64; + @ignore BigInt get value => BigInt.parse(valueIntString); @@ -71,6 +74,8 @@ class SparkCoin { this.tag, required this.lTagHash, this.height, + this.serializedCoinB64, + this.contextB64, }); SparkCoin copyWith({ @@ -88,6 +93,8 @@ class SparkCoin { List? tag, String? lTagHash, int? height, + String? serializedCoinB64, + String? contextB64, }) { return SparkCoin( walletId: walletId, @@ -106,6 +113,8 @@ class SparkCoin { tag: tag ?? this.tag, lTagHash: lTagHash ?? this.lTagHash, height: height ?? this.height, + serializedCoinB64: serializedCoinB64 ?? this.serializedCoinB64, + contextB64: contextB64 ?? this.contextB64, ); } @@ -127,6 +136,8 @@ class SparkCoin { ', tag: $tag' ', lTagHash: $lTagHash' ', height: $height' + ', serializedCoinB64: $serializedCoinB64' + ', contextB64: $contextB64' ')'; } } diff --git a/lib/wallets/isar/models/spark_coin.g.dart b/lib/wallets/isar/models/spark_coin.g.dart index 5ea6a30a9..e402c59eb 100644 --- a/lib/wallets/isar/models/spark_coin.g.dart +++ b/lib/wallets/isar/models/spark_coin.g.dart @@ -22,74 +22,84 @@ const SparkCoinSchema = CollectionSchema( name: r'address', type: IsarType.string, ), - r'diversifierIntString': PropertySchema( + r'contextB64': PropertySchema( id: 1, + name: r'contextB64', + type: IsarType.string, + ), + r'diversifierIntString': PropertySchema( + id: 2, name: r'diversifierIntString', type: IsarType.string, ), r'encryptedDiversifier': PropertySchema( - id: 2, + id: 3, name: r'encryptedDiversifier', type: IsarType.longList, ), r'height': PropertySchema( - id: 3, + id: 4, name: r'height', type: IsarType.long, ), r'isUsed': PropertySchema( - id: 4, + id: 5, name: r'isUsed', type: IsarType.bool, ), r'lTagHash': PropertySchema( - id: 5, + id: 6, name: r'lTagHash', type: IsarType.string, ), r'memo': PropertySchema( - id: 6, + id: 7, name: r'memo', type: IsarType.string, ), r'nonce': PropertySchema( - id: 7, + id: 8, name: r'nonce', type: IsarType.longList, ), r'serial': PropertySchema( - id: 8, + id: 9, name: r'serial', type: IsarType.longList, ), r'serialContext': PropertySchema( - id: 9, + id: 10, name: r'serialContext', type: IsarType.longList, ), + r'serializedCoinB64': PropertySchema( + id: 11, + name: r'serializedCoinB64', + type: IsarType.string, + ), r'tag': PropertySchema( - id: 10, + id: 12, name: r'tag', type: IsarType.longList, ), r'txHash': PropertySchema( - id: 11, + id: 13, name: r'txHash', type: IsarType.string, ), r'type': PropertySchema( - id: 12, + id: 14, name: r'type', type: IsarType.byte, enumMap: _SparkCointypeEnumValueMap, ), r'valueIntString': PropertySchema( - id: 13, + id: 15, name: r'valueIntString', type: IsarType.string, ), r'walletId': PropertySchema( - id: 14, + id: 16, name: r'walletId', type: IsarType.string, ) @@ -134,6 +144,12 @@ int _sparkCoinEstimateSize( ) { var bytesCount = offsets.last; bytesCount += 3 + object.address.length * 3; + { + final value = object.contextB64; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } bytesCount += 3 + object.diversifierIntString.length * 3; { final value = object.encryptedDiversifier; @@ -166,6 +182,12 @@ int _sparkCoinEstimateSize( bytesCount += 3 + value.length * 8; } } + { + final value = object.serializedCoinB64; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } { final value = object.tag; if (value != null) { @@ -185,20 +207,22 @@ void _sparkCoinSerialize( Map> allOffsets, ) { writer.writeString(offsets[0], object.address); - writer.writeString(offsets[1], object.diversifierIntString); - writer.writeLongList(offsets[2], object.encryptedDiversifier); - writer.writeLong(offsets[3], object.height); - writer.writeBool(offsets[4], object.isUsed); - writer.writeString(offsets[5], object.lTagHash); - writer.writeString(offsets[6], object.memo); - writer.writeLongList(offsets[7], object.nonce); - writer.writeLongList(offsets[8], object.serial); - writer.writeLongList(offsets[9], object.serialContext); - writer.writeLongList(offsets[10], object.tag); - writer.writeString(offsets[11], object.txHash); - writer.writeByte(offsets[12], object.type.index); - writer.writeString(offsets[13], object.valueIntString); - writer.writeString(offsets[14], object.walletId); + writer.writeString(offsets[1], object.contextB64); + writer.writeString(offsets[2], object.diversifierIntString); + writer.writeLongList(offsets[3], object.encryptedDiversifier); + writer.writeLong(offsets[4], object.height); + writer.writeBool(offsets[5], object.isUsed); + writer.writeString(offsets[6], object.lTagHash); + writer.writeString(offsets[7], object.memo); + writer.writeLongList(offsets[8], object.nonce); + writer.writeLongList(offsets[9], object.serial); + writer.writeLongList(offsets[10], object.serialContext); + writer.writeString(offsets[11], object.serializedCoinB64); + writer.writeLongList(offsets[12], object.tag); + writer.writeString(offsets[13], object.txHash); + writer.writeByte(offsets[14], object.type.index); + writer.writeString(offsets[15], object.valueIntString); + writer.writeString(offsets[16], object.walletId); } SparkCoin _sparkCoinDeserialize( @@ -209,21 +233,23 @@ SparkCoin _sparkCoinDeserialize( ) { final object = SparkCoin( address: reader.readString(offsets[0]), - diversifierIntString: reader.readString(offsets[1]), - encryptedDiversifier: reader.readLongList(offsets[2]), - height: reader.readLongOrNull(offsets[3]), - isUsed: reader.readBool(offsets[4]), - lTagHash: reader.readString(offsets[5]), - memo: reader.readStringOrNull(offsets[6]), - nonce: reader.readLongList(offsets[7]), - serial: reader.readLongList(offsets[8]), - serialContext: reader.readLongList(offsets[9]), - tag: reader.readLongList(offsets[10]), - txHash: reader.readString(offsets[11]), - type: _SparkCointypeValueEnumMap[reader.readByteOrNull(offsets[12])] ?? + contextB64: reader.readStringOrNull(offsets[1]), + diversifierIntString: reader.readString(offsets[2]), + encryptedDiversifier: reader.readLongList(offsets[3]), + height: reader.readLongOrNull(offsets[4]), + isUsed: reader.readBool(offsets[5]), + lTagHash: reader.readString(offsets[6]), + memo: reader.readStringOrNull(offsets[7]), + nonce: reader.readLongList(offsets[8]), + serial: reader.readLongList(offsets[9]), + serialContext: reader.readLongList(offsets[10]), + serializedCoinB64: reader.readStringOrNull(offsets[11]), + tag: reader.readLongList(offsets[12]), + txHash: reader.readString(offsets[13]), + type: _SparkCointypeValueEnumMap[reader.readByteOrNull(offsets[14])] ?? SparkCoinType.mint, - valueIntString: reader.readString(offsets[13]), - walletId: reader.readString(offsets[14]), + valueIntString: reader.readString(offsets[15]), + walletId: reader.readString(offsets[16]), ); object.id = id; return object; @@ -239,19 +265,19 @@ P _sparkCoinDeserializeProp

( case 0: return (reader.readString(offset)) as P; case 1: - return (reader.readString(offset)) as P; - case 2: - return (reader.readLongList(offset)) as P; - case 3: - return (reader.readLongOrNull(offset)) as P; - case 4: - return (reader.readBool(offset)) as P; - case 5: - return (reader.readString(offset)) as P; - case 6: return (reader.readStringOrNull(offset)) as P; - case 7: + case 2: + return (reader.readString(offset)) as P; + case 3: return (reader.readLongList(offset)) as P; + case 4: + return (reader.readLongOrNull(offset)) as P; + case 5: + return (reader.readBool(offset)) as P; + case 6: + return (reader.readString(offset)) as P; + case 7: + return (reader.readStringOrNull(offset)) as P; case 8: return (reader.readLongList(offset)) as P; case 9: @@ -259,13 +285,17 @@ P _sparkCoinDeserializeProp

( case 10: return (reader.readLongList(offset)) as P; case 11: - return (reader.readString(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 12: - return (_SparkCointypeValueEnumMap[reader.readByteOrNull(offset)] ?? - SparkCoinType.mint) as P; + return (reader.readLongList(offset)) as P; case 13: return (reader.readString(offset)) as P; case 14: + return (_SparkCointypeValueEnumMap[reader.readByteOrNull(offset)] ?? + SparkCoinType.mint) as P; + case 15: + return (reader.readString(offset)) as P; + case 16: return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -681,6 +711,157 @@ extension SparkCoinQueryFilter }); } + QueryBuilder contextB64IsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'contextB64', + )); + }); + } + + QueryBuilder + contextB64IsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'contextB64', + )); + }); + } + + QueryBuilder contextB64EqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'contextB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + contextB64GreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'contextB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contextB64LessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'contextB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contextB64Between( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'contextB64', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + contextB64StartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'contextB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contextB64EndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'contextB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contextB64Contains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'contextB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contextB64Matches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'contextB64', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + contextB64IsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'contextB64', + value: '', + )); + }); + } + + QueryBuilder + contextB64IsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'contextB64', + value: '', + )); + }); + } + QueryBuilder diversifierIntStringEqualTo( String value, { @@ -1866,6 +2047,160 @@ extension SparkCoinQueryFilter }); } + QueryBuilder + serializedCoinB64IsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'serializedCoinB64', + )); + }); + } + + QueryBuilder + serializedCoinB64IsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'serializedCoinB64', + )); + }); + } + + QueryBuilder + serializedCoinB64EqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'serializedCoinB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serializedCoinB64GreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'serializedCoinB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serializedCoinB64LessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'serializedCoinB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serializedCoinB64Between( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'serializedCoinB64', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serializedCoinB64StartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'serializedCoinB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serializedCoinB64EndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'serializedCoinB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serializedCoinB64Contains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'serializedCoinB64', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serializedCoinB64Matches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'serializedCoinB64', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serializedCoinB64IsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'serializedCoinB64', + value: '', + )); + }); + } + + QueryBuilder + serializedCoinB64IsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'serializedCoinB64', + value: '', + )); + }); + } + QueryBuilder tagIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -2491,6 +2826,18 @@ extension SparkCoinQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByContextB64() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'contextB64', Sort.asc); + }); + } + + QueryBuilder sortByContextB64Desc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'contextB64', Sort.desc); + }); + } + QueryBuilder sortByDiversifierIntString() { return QueryBuilder.apply(this, (query) { @@ -2553,6 +2900,19 @@ extension SparkCoinQuerySortBy on QueryBuilder { }); } + QueryBuilder sortBySerializedCoinB64() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serializedCoinB64', Sort.asc); + }); + } + + QueryBuilder + sortBySerializedCoinB64Desc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serializedCoinB64', Sort.desc); + }); + } + QueryBuilder sortByTxHash() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'txHash', Sort.asc); @@ -2616,6 +2976,18 @@ extension SparkCoinQuerySortThenBy }); } + QueryBuilder thenByContextB64() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'contextB64', Sort.asc); + }); + } + + QueryBuilder thenByContextB64Desc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'contextB64', Sort.desc); + }); + } + QueryBuilder thenByDiversifierIntString() { return QueryBuilder.apply(this, (query) { @@ -2690,6 +3062,19 @@ extension SparkCoinQuerySortThenBy }); } + QueryBuilder thenBySerializedCoinB64() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serializedCoinB64', Sort.asc); + }); + } + + QueryBuilder + thenBySerializedCoinB64Desc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'serializedCoinB64', Sort.desc); + }); + } + QueryBuilder thenByTxHash() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'txHash', Sort.asc); @@ -2748,6 +3133,13 @@ extension SparkCoinQueryWhereDistinct }); } + QueryBuilder distinctByContextB64( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'contextB64', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByDiversifierIntString( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -2807,6 +3199,14 @@ extension SparkCoinQueryWhereDistinct }); } + QueryBuilder distinctBySerializedCoinB64( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'serializedCoinB64', + caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByTag() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'tag'); @@ -2856,6 +3256,12 @@ extension SparkCoinQueryProperty }); } + QueryBuilder contextB64Property() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'contextB64'); + }); + } + QueryBuilder diversifierIntStringProperty() { return QueryBuilder.apply(this, (query) { @@ -2913,6 +3319,13 @@ extension SparkCoinQueryProperty }); } + QueryBuilder + serializedCoinB64Property() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'serializedCoinB64'); + }); + } + QueryBuilder?, QQueryOperations> tagProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'tag'); diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 8ab99b011..0047262e9 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -485,6 +485,7 @@ class FiroWallet extends Bip39HDWallet await mainDB.deleteWalletBlockchainData(walletId); } + // lelantus final latestSetId = await electrumXClient.getLelantusLatestCoinId(); final setDataMapFuture = getSetDataMap(latestSetId); final usedSerialNumbersFuture = @@ -492,6 +493,17 @@ class FiroWallet extends Bip39HDWallet coin: info.coin, ); + // spark + final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); + final sparkAnonSetFuture = electrumXCachedClient.getSparkAnonymitySet( + groupId: latestSparkCoinId.toString(), + coin: info.coin, + ); + final sparkUsedCoinTagsFuture = + electrumXCachedClient.getSparkUsedCoinsTags( + coin: info.coin, + ); + // receiving addresses Logging.instance.log( "checking receiving addresses...", @@ -595,16 +607,29 @@ class FiroWallet extends Bip39HDWallet final futureResults = await Future.wait([ usedSerialNumbersFuture, setDataMapFuture, + sparkAnonSetFuture, + sparkUsedCoinTagsFuture, ]); + // lelantus final usedSerialsSet = (futureResults[0] as List).toSet(); final setDataMap = futureResults[1] as Map; - await recoverLelantusWallet( - latestSetId: latestSetId, - usedSerialNumbers: usedSerialsSet, - setDataMap: setDataMap, - ); + // spark + final sparkAnonymitySet = futureResults[2] as Map; + final sparkSpentCoinTags = futureResults[3] as Set; + + await Future.wait([ + recoverLelantusWallet( + latestSetId: latestSetId, + usedSerialNumbers: usedSerialsSet, + setDataMap: setDataMap, + ), + recoverSparkWallet( + anonymitySet: sparkAnonymitySet, + spentCoinTags: sparkSpentCoinTags, + ), + ]); }); await refresh(); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index d5288df8b..e2a16fafb 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -16,6 +16,8 @@ import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; +const kDefaultSparkIndex = 1; + mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { @override Future init() async { @@ -68,21 +70,18 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // default to starting at 1 if none found final int diversifier = (highestStoredDiversifier ?? 0) + 1; - // TODO: check that this stays constant and only the diversifier changes? - const index = 1; - final root = await getRootHDNode(); final String derivationPath; if (cryptoCurrency.network == CryptoCurrencyNetwork.test) { - derivationPath = "$kSparkBaseDerivationPathTestnet$index"; + derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex"; } else { - derivationPath = "$kSparkBaseDerivationPath$index"; + derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex"; } final keys = root.derivePath(derivationPath); final String addressString = await LibSpark.getAddress( privateKey: keys.privateKey.data, - index: index, + index: kDefaultSparkIndex, diversifier: diversifier, isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, ); @@ -138,14 +137,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o/edit // To generate a spark spend we need to call createSparkSpendTransaction, // first unlock the wallet and generate all 3 spark keys, - const index = 1; final root = await getRootHDNode(); final String derivationPath; if (cryptoCurrency.network == CryptoCurrencyNetwork.test) { - derivationPath = "$kSparkBaseDerivationPathTestnet$index"; + derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex"; } else { - derivationPath = "$kSparkBaseDerivationPath$index"; + derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex"; } final privateKey = root.derivePath(derivationPath).privateKey.data; // @@ -355,7 +353,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final spend = LibSpark.createSparkSendTransaction( privateKeyHex: privateKey.toHex, - index: index, + index: kDefaultSparkIndex, recipients: [], privateRecipients: txData.sparkRecipients ?.map((e) => ( @@ -366,7 +364,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { )) .toList() ?? [], - serializedMintMetas: serializedMintMetas, + serializedCoins: serializedCoins, allAnonymitySets: allAnonymitySets, ); @@ -421,152 +419,41 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { sparkAddresses.map((e) => e.derivationPath!.value).toSet(); try { - const index = 1; - - final root = await getRootHDNode(); - final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); + final blockHash = await _getCachedSparkBlockHash(); + final futureResults = await Future.wait([ - electrumXCachedClient.getSparkAnonymitySet( - groupId: latestSparkCoinId.toString(), - coin: info.coin, - ), + blockHash == null + ? electrumXCachedClient.getSparkAnonymitySet( + groupId: latestSparkCoinId.toString(), + coin: info.coin, + ) + : electrumXClient.getSparkAnonymitySet( + coinGroupId: latestSparkCoinId.toString(), + startBlockHash: blockHash, + ), electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin), ]); final anonymitySet = futureResults[0] as Map; final spentCoinTags = futureResults[1] as Set; - // find our coins - final List myCoins = []; - - for (final path in paths) { - final keys = root.derivePath(path); - - final privateKeyHex = keys.privateKey.data.toHex; - - for (final dynData in anonymitySet["coins"] as List) { - final data = List.from(dynData as List); - - if (data.length != 3) { - throw Exception("Unexpected serialized coin info found"); - } - - final serializedCoin = data[0]; - final txHash = base64ToReverseHex(data[1]); - - final coin = LibSpark.identifyAndRecoverCoin( - serializedCoin, - privateKeyHex: privateKeyHex, - index: index, - context: base64Decode(data[2]), - isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, - ); - - // its ours - if (coin != null) { - final SparkCoinType coinType; - switch (coin.type.value) { - case 0: - coinType = SparkCoinType.mint; - case 1: - coinType = SparkCoinType.spend; - default: - throw Exception("Unknown spark coin type detected"); - } - myCoins.add( - SparkCoin( - walletId: walletId, - type: coinType, - isUsed: spentCoinTags.contains(coin.lTagHash!), - nonce: coin.nonceHex?.toUint8ListFromHex, - address: coin.address!, - txHash: txHash, - valueIntString: coin.value!.toString(), - memo: coin.memo, - serialContext: coin.serialContext, - diversifierIntString: coin.diversifier!.toString(), - encryptedDiversifier: coin.encryptedDiversifier, - serial: coin.serial, - tag: coin.tag, - lTagHash: coin.lTagHash!, - height: coin.height, - ), - ); - } - } - } + final myCoins = await _identifyCoins( + anonymitySet: anonymitySet, + spentCoinTags: spentCoinTags, + sparkAddressDerivationPaths: paths, + ); // update wallet spark coins in isar - if (myCoins.isNotEmpty) { - await mainDB.isar.writeTxn(() async { - await mainDB.isar.sparkCoins.putAll(myCoins); - }); - } + await _addOrUpdateSparkCoins(myCoins); - // update wallet spark coin height - final coinsToCheck = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .heightIsNull() - .findAll(); - final List updatedCoins = []; - for (final coin in coinsToCheck) { - final tx = await electrumXCachedClient.getTransaction( - txHash: coin.txHash, - coin: info.coin, - ); - if (tx["height"] is int) { - updatedCoins.add(coin.copyWith(height: tx["height"] as int)); - } - } - if (updatedCoins.isNotEmpty) { - await mainDB.isar.writeTxn(() async { - await mainDB.isar.sparkCoins.putAll(updatedCoins); - }); - } + // update blockHash in cache + final String newBlockHash = anonymitySet["blockHash"] as String; + await _setCachedSparkBlockHash(newBlockHash); // refresh spark balance - final currentHeight = await chainHeight; - final unusedCoins = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .isUsedEqualTo(false) - .findAll(); - - final total = Amount( - rawValue: unusedCoins - .map((e) => e.value) - .fold(BigInt.zero, (prev, e) => prev + e), - fractionDigits: cryptoCurrency.fractionDigits, - ); - final spendable = Amount( - rawValue: unusedCoins - .where((e) => - e.height != null && - e.height! + cryptoCurrency.minConfirms >= currentHeight) - .map((e) => e.value) - .fold(BigInt.zero, (prev, e) => prev + e), - fractionDigits: cryptoCurrency.fractionDigits, - ); - - final sparkBalance = Balance( - total: total, - spendable: spendable, - blockedTotal: Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - pendingSpendable: total - spendable, - ); - - await info.updateBalanceTertiary( - newBalance: sparkBalance, - isar: mainDB.isar, - ); + await refreshSparkBalance(); } catch (e, s) { // todo logging @@ -574,35 +461,85 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } } + Future refreshSparkBalance() async { + final currentHeight = await chainHeight; + final unusedCoins = await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .findAll(); + + final total = Amount( + rawValue: unusedCoins + .map((e) => e.value) + .fold(BigInt.zero, (prev, e) => prev + e), + fractionDigits: cryptoCurrency.fractionDigits, + ); + final spendable = Amount( + rawValue: unusedCoins + .where((e) => + e.height != null && + e.height! + cryptoCurrency.minConfirms >= currentHeight) + .map((e) => e.value) + .fold(BigInt.zero, (prev, e) => prev + e), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + final sparkBalance = Balance( + total: total, + spendable: spendable, + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + pendingSpendable: total - spendable, + ); + + await info.updateBalanceTertiary( + newBalance: sparkBalance, + isar: mainDB.isar, + ); + } + /// Should only be called within the standard wallet [recover] function due to /// mutex locking. Otherwise behaviour MAY be undefined. - Future recoverSparkWallet( - // { - // required int latestSetId, - // required Map setDataMap, - // required Set usedSerialNumbers, - // } - ) async { + Future recoverSparkWallet({ + required Map anonymitySet, + required Set spentCoinTags, + }) async { + // generate spark addresses if non existing + if (await getCurrentReceivingSparkAddress() == null) { + final address = await generateNextSparkAddress(); + await mainDB.putAddress(address); + } + + final sparkAddresses = await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.spark) + .findAll(); + + final Set paths = + sparkAddresses.map((e) => e.derivationPath!.value).toSet(); + try { - // do we need to generate any spark address(es) here? - - final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); - - final anonymitySet = await electrumXCachedClient.getSparkAnonymitySet( - groupId: latestSparkCoinId.toString(), - coin: info.coin, + final myCoins = await _identifyCoins( + anonymitySet: anonymitySet, + spentCoinTags: spentCoinTags, + sparkAddressDerivationPaths: paths, ); - // TODO loop over set and see which coins are ours using the FFI call `identifyCoin` - List myCoins = []; - - // fetch metadata for myCoins - - // create list of Spark Coin isar objects - // update wallet spark coins in isar + await _addOrUpdateSparkCoins(myCoins); - throw UnimplementedError(); + // update blockHash in cache + final String newBlockHash = anonymitySet["blockHash"] as String; + await _setCachedSparkBlockHash(newBlockHash); + + // refresh spark balance + await refreshSparkBalance(); } catch (e, s) { // todo logging @@ -826,6 +763,124 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // wait for normalBalanceFuture to complete before returning await normalBalanceFuture; } + + // ====================== Private ============================================ + + final _kSparkAnonSetCachedBlockHashKey = "SparkAnonSetCachedBlockHashKey"; + + Future _getCachedSparkBlockHash() async { + return info.otherData[_kSparkAnonSetCachedBlockHashKey] as String?; + } + + Future _setCachedSparkBlockHash(String blockHash) async { + await info.updateOtherData( + newEntries: {_kSparkAnonSetCachedBlockHashKey: blockHash}, + isar: mainDB.isar, + ); + } + + Future> _identifyCoins({ + required Map anonymitySet, + required Set spentCoinTags, + required Set sparkAddressDerivationPaths, + }) async { + final root = await getRootHDNode(); + + final List myCoins = []; + + for (final path in sparkAddressDerivationPaths) { + final keys = root.derivePath(path); + + final privateKeyHex = keys.privateKey.data.toHex; + + for (final dynData in anonymitySet["coins"] as List) { + final data = List.from(dynData as List); + + if (data.length != 3) { + throw Exception("Unexpected serialized coin info found"); + } + + final serializedCoinB64 = data[0]; + final txHash = base64ToReverseHex(data[1]); + final contextB64 = data[2]; + + final coin = LibSpark.identifyAndRecoverCoin( + serializedCoinB64, + privateKeyHex: privateKeyHex, + index: kDefaultSparkIndex, + context: base64Decode(contextB64), + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, + ); + + // its ours + if (coin != null) { + final SparkCoinType coinType; + switch (coin.type.value) { + case 0: + coinType = SparkCoinType.mint; + case 1: + coinType = SparkCoinType.spend; + default: + throw Exception("Unknown spark coin type detected"); + } + myCoins.add( + SparkCoin( + walletId: walletId, + type: coinType, + isUsed: spentCoinTags.contains(coin.lTagHash!), + nonce: coin.nonceHex?.toUint8ListFromHex, + address: coin.address!, + txHash: txHash, + valueIntString: coin.value!.toString(), + memo: coin.memo, + serialContext: coin.serialContext, + diversifierIntString: coin.diversifier!.toString(), + encryptedDiversifier: coin.encryptedDiversifier, + serial: coin.serial, + tag: coin.tag, + lTagHash: coin.lTagHash!, + height: coin.height, + serializedCoinB64: serializedCoinB64, + contextB64: contextB64, + ), + ); + } + } + } + + return myCoins; + } + + Future _addOrUpdateSparkCoins(List coins) async { + if (coins.isNotEmpty) { + await mainDB.isar.writeTxn(() async { + await mainDB.isar.sparkCoins.putAll(coins); + }); + } + + // update wallet spark coin height + final coinsToCheck = await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .heightIsNull() + .findAll(); + final List updatedCoins = []; + for (final coin in coinsToCheck) { + final tx = await electrumXCachedClient.getTransaction( + txHash: coin.txHash, + coin: info.coin, + ); + if (tx["height"] is int) { + updatedCoins.add(coin.copyWith(height: tx["height"] as int)); + } + } + if (updatedCoins.isNotEmpty) { + await mainDB.isar.writeTxn(() async { + await mainDB.isar.sparkCoins.putAll(updatedCoins); + }); + } + } } String base64ToReverseHex(String source) => From 11edcf30cf89211d30c85d7ee6e0ec26d4838e62 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 18 Dec 2023 15:12:16 -0600 Subject: [PATCH 46/77] format unused wallet coins for spark spend --- .../wallet_mixin_interfaces/spark_interface.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index e2a16fafb..a70245c9a 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -105,9 +105,15 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { Future prepareSendSpark({ required TxData txData, }) async { - // todo fetch - final List serializedMintMetas = []; - final List myCoins = []; + final coins = await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .findAll(); + + final serializedCoins = + coins.map((e) => (e.serializedCoinB64!, e.contextB64!)).toList(); final currentId = await electrumXClient.getSparkLatestCoinId(); final List> setMaps = []; From 0f9eff679267762a8dee599e7dce8b14b19976e5 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 18 Dec 2023 16:48:41 -0600 Subject: [PATCH 47/77] desktop spark address display --- .../sub_widgets/desktop_receive.dart | 411 ++++++++++++------ 1 file changed, 279 insertions(+), 132 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 5f34f6ae0..283567df4 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 @@ -10,15 +10,18 @@ import 'dart:async'; +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:isar/isar.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -30,8 +33,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; -import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -61,9 +65,13 @@ class _DesktopReceiveState extends ConsumerState { late final ClipboardInterface clipboard; late final bool supportsSpark; + String? _sparkAddress; + String? _qrcodeContent; + bool _showSparkAddress = true; + Future generateNewAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); - if (wallet is Bip39HDWallet) { + if (wallet is MultiAddressInterface) { bool shouldPop = false; unawaited( showDialog( @@ -96,6 +104,51 @@ class _DesktopReceiveState extends ConsumerState { } } + Future generateNewSparkAddress() async { + final wallet = ref.read(pWallets).getWallet(walletId); + if (wallet is SparkInterface) { + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (_) { + return WillPopScope( + onWillPop: () async => shouldPop, + child: Container( + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.5), + child: const CustomLoadingOverlay( + message: "Generating address", + eventBus: null, + ), + ), + ); + }, + ), + ); + + final address = await wallet.generateNextSparkAddress(); + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.addresses.put(address); + }); + + shouldPop = true; + + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); + if (_sparkAddress != address.value) { + setState(() { + _sparkAddress = address.value; + }); + } + } + } + } + + StreamSubscription? _streamSub; + @override void initState() { walletId = widget.walletId; @@ -103,25 +156,221 @@ class _DesktopReceiveState extends ConsumerState { clipboard = widget.clipboard; supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface; + if (supportsSpark) { + _streamSub = ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.spark) + .sortByDerivationIndexDesc() + .findFirst() + .asStream() + .listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _sparkAddress = event?.value; + }); + } + }); + }); + } + super.initState(); } + @override + void dispose() { + _streamSub?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final receivingAddress = ref.watch(pWalletReceivingAddress(walletId)); + if (supportsSpark) { + if (_showSparkAddress) { + _qrcodeContent = _sparkAddress; + } else { + _qrcodeContent = ref.watch(pWalletReceivingAddress(walletId)); + } + } else { + _qrcodeContent = ref.watch(pWalletReceivingAddress(walletId)); + } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (supportsSpark) - MouseRegion( + ConditionalParent( + condition: supportsSpark, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DropdownButtonHideUnderline( + child: DropdownButton2( + value: _showSparkAddress, + items: [ + DropdownMenuItem( + value: true, + child: Text( + "Spark address", + style: STextStyles.desktopTextMedium(context), + ), + ), + DropdownMenuItem( + value: false, + child: Text( + "Transparent address", + style: STextStyles.desktopTextMedium(context), + ), + ), + ], + onChanged: (value) { + if (value is bool && value != _showSparkAddress) { + setState(() { + _showSparkAddress = 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, + ), + ), + ), + 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, + ), + ), + ), + ), + const SizedBox( + height: 12, + ), + if (_showSparkAddress) + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + clipboard.setData( + ClipboardData(text: _sparkAddress ?? "Error"), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context) + .extension()! + .backgroundAppBar, + width: 1, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: RoundedWhiteContainer( + child: Column( + children: [ + Row( + children: [ + Text( + "Your ${widget.contractAddress == null ? coin.ticker : ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.symbol, + ), + )} SPARK address", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 15, + height: 15, + color: Theme.of(context) + .extension()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + Expanded( + child: Text( + _sparkAddress ?? "Error", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + if (!_showSparkAddress) child, + ], + ), + child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { clipboard.setData( - ClipboardData(text: receivingAddress), + ClipboardData( + text: ref.watch(pWalletReceivingAddress(walletId))), ); showFloatingFlushBar( type: FlushBarType.info, @@ -152,7 +401,7 @@ class _DesktopReceiveState extends ConsumerState { tokenServiceProvider.select( (value) => value!.tokenContract.symbol, ), - )} SPARK address", + )} address", style: STextStyles.itemSubtitle(context), ), const Spacer(), @@ -183,27 +432,15 @@ class _DesktopReceiveState extends ConsumerState { Row( children: [ Expanded( - child: FutureBuilder( - future: (ref.watch(pWallets).getWallet(walletId) - as SparkInterface) - .getCurrentReceivingSparkAddress(), - builder: (context, snapshot) { - String addressString = "Error"; - if (snapshot.hasData) { - addressString = snapshot.data!.value; - } - - return Text( - addressString, - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ); - }, + child: Text( + ref.watch(pWalletReceivingAddress(walletId)), + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), ), ), ], @@ -214,113 +451,23 @@ class _DesktopReceiveState extends ConsumerState { ), ), ), - - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - clipboard.setData( - ClipboardData(text: receivingAddress), - ); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - width: 1, - ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: RoundedWhiteContainer( - child: Column( - children: [ - Row( - children: [ - Text( - "Your ${widget.contractAddress == null ? coin.ticker : ref.watch( - tokenServiceProvider.select( - (value) => value!.tokenContract.symbol, - ), - )} address", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 15, - height: 15, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ], - ), - const SizedBox( - height: 8, - ), - Row( - children: [ - Expanded( - child: Text( - receivingAddress, - style: - STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), ), - if (coin != Coin.epicCash && - coin != Coin.ethereum && - coin != Coin.banano && - coin != Coin.nano && - coin != Coin.stellar && - coin != Coin.stellarTestnet && - coin != Coin.tezos) + + if (ref.watch(pWallets.select((value) => value.getWallet(walletId))) + is MultiAddressInterface || + supportsSpark) const SizedBox( height: 20, ), - if (coin != Coin.epicCash && - coin != Coin.ethereum && - coin != Coin.banano && - coin != Coin.nano && - coin != Coin.stellar && - coin != Coin.stellarTestnet && - coin != Coin.tezos) + + if (ref.watch(pWallets.select((value) => value.getWallet(walletId))) + is MultiAddressInterface || + supportsSpark) SecondaryButton( buttonHeight: ButtonHeight.l, - onPressed: generateNewAddress, + onPressed: supportsSpark && _showSparkAddress + ? generateNewSparkAddress + : generateNewAddress, label: "Generate new address", ), const SizedBox( @@ -330,7 +477,7 @@ class _DesktopReceiveState extends ConsumerState { child: QrImageView( data: AddressUtils.buildUriString( coin, - receivingAddress, + _qrcodeContent ?? "", {}, ), size: 200, @@ -371,7 +518,7 @@ class _DesktopReceiveState extends ConsumerState { RouteGenerator.generateRoute( RouteSettings( name: GenerateUriQrCodeView.routeName, - arguments: Tuple2(coin, receivingAddress), + arguments: Tuple2(coin, _qrcodeContent ?? ""), ), ), ], @@ -388,7 +535,7 @@ class _DesktopReceiveState extends ConsumerState { shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, builder: (_) => GenerateUriQrCodeView( coin: coin, - receivingAddress: receivingAddress, + receivingAddress: _qrcodeContent ?? "", ), settings: const RouteSettings( name: GenerateUriQrCodeView.routeName, From a2e36f06ded833d8d176f7322eae6dbb84ff34d8 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 18 Dec 2023 16:49:39 -0600 Subject: [PATCH 48/77] show diversifier in address details --- .../receive_view/addresses/address_details_view.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/pages/receive_view/addresses/address_details_view.dart b/lib/pages/receive_view/addresses/address_details_view.dart index 64cfe41cd..ce7f84fc4 100644 --- a/lib/pages/receive_view/addresses/address_details_view.dart +++ b/lib/pages/receive_view/addresses/address_details_view.dart @@ -352,6 +352,16 @@ class _AddressDetailsViewState extends ConsumerState { data: address.derivationPath!.value, button: Container(), ), + if (address.type == AddressType.spark) + const _Div( + height: 12, + ), + if (address.type == AddressType.spark) + _Item( + title: "Diversifier", + data: address.derivationIndex.toString(), + button: Container(), + ), const _Div( height: 12, ), From 65e93c7f48780cd64667e04648501a28cd0dc1dc Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 Dec 2023 09:20:50 -0600 Subject: [PATCH 49/77] add spark address validation --- lib/wallets/crypto_currency/coins/firo.dart | 11 +++++++++-- .../wallet_mixin_interfaces/spark_interface.dart | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index bccdc950c..82eb8ab39 100644 --- a/lib/wallets/crypto_currency/coins/firo.dart +++ b/lib/wallets/crypto_currency/coins/firo.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; class Firo extends Bip39HDCurrency { Firo(super.network) { @@ -132,9 +133,15 @@ class Firo extends Bip39HDCurrency { coinlib.Address.fromString(address, networkParams); return true; } catch (_) { - return false; + return validateSparkAddress(address); } - // TODO: implement validateAddress for spark addresses? + } + + bool validateSparkAddress(String address) { + return SparkInterface.validateSparkAddress( + address: address, + isTestNet: network == CryptoCurrencyNetwork.test, + ); } @override diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index a70245c9a..69e391453 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -19,6 +19,12 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_int const kDefaultSparkIndex = 1; mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { + static bool validateSparkAddress({ + required String address, + required bool isTestNet, + }) => + LibSpark.validateAddress(address: address, isTestNet: isTestNet); + @override Future init() async { Address? address = await getCurrentReceivingSparkAddress(); From 311b2adfd98ec65e23e7e76dae3fb792f0fcb2fa Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 Dec 2023 12:06:05 -0600 Subject: [PATCH 50/77] offload coin identification to separate isolate --- .../spark_interface.dart | 225 ++++++++++-------- 1 file changed, 123 insertions(+), 102 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 69e391453..75ac5b59e 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1,8 +1,8 @@ import 'dart:convert'; -import 'dart:typed_data'; import 'package:bitcoindart/bitcoindart.dart' as btc; import 'package:bitcoindart/src/utils/script.dart' as bscript; +import 'package:flutter/foundation.dart'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/balance.dart'; @@ -435,34 +435,47 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final blockHash = await _getCachedSparkBlockHash(); - final futureResults = await Future.wait([ - blockHash == null - ? electrumXCachedClient.getSparkAnonymitySet( - groupId: latestSparkCoinId.toString(), - coin: info.coin, - ) - : electrumXClient.getSparkAnonymitySet( - coinGroupId: latestSparkCoinId.toString(), - startBlockHash: blockHash, - ), - electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin), - ]); + final anonymitySet = blockHash == null + ? await electrumXCachedClient.getSparkAnonymitySet( + groupId: latestSparkCoinId.toString(), + coin: info.coin, + ) + : await electrumXClient.getSparkAnonymitySet( + coinGroupId: latestSparkCoinId.toString(), + startBlockHash: blockHash, + ); - final anonymitySet = futureResults[0] as Map; - final spentCoinTags = futureResults[1] as Set; + if (anonymitySet["coins"] is List && + (anonymitySet["coins"] as List).isNotEmpty) { + final spentCoinTags = + await electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin); - final myCoins = await _identifyCoins( - anonymitySet: anonymitySet, - spentCoinTags: spentCoinTags, - sparkAddressDerivationPaths: paths, - ); + final root = await getRootHDNode(); + final privateKeyHexSet = paths + .map( + (e) => root.derivePath(e).privateKey.data.toHex, + ) + .toSet(); - // update wallet spark coins in isar - await _addOrUpdateSparkCoins(myCoins); + final myCoins = await compute( + _identifyCoins, + ( + anonymitySetCoins: anonymitySet["coins"] as List, + spentCoinTags: spentCoinTags, + privateKeyHexSet: privateKeyHexSet, + walletId: walletId, + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, + ), + ); - // update blockHash in cache - final String newBlockHash = anonymitySet["blockHash"] as String; - await _setCachedSparkBlockHash(newBlockHash); + // update wallet spark coins in isar + await _addOrUpdateSparkCoins(myCoins); + + // update blockHash in cache + final String newBlockHash = + base64ToReverseHex(anonymitySet["blockHash"] as String); + await _setCachedSparkBlockHash(newBlockHash); + } // refresh spark balance await refreshSparkBalance(); @@ -537,10 +550,19 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { sparkAddresses.map((e) => e.derivationPath!.value).toSet(); try { - final myCoins = await _identifyCoins( - anonymitySet: anonymitySet, - spentCoinTags: spentCoinTags, - sparkAddressDerivationPaths: paths, + final root = await getRootHDNode(); + final privateKeyHexSet = + paths.map((e) => root.derivePath(e).privateKey.data.toHex).toSet(); + + final myCoins = await compute( + _identifyCoins, + ( + anonymitySetCoins: anonymitySet["coins"] as List, + spentCoinTags: spentCoinTags, + privateKeyHexSet: privateKeyHexSet, + walletId: walletId, + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, + ), ); // update wallet spark coins in isar @@ -680,7 +702,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { txb.addInput( sd.utxo.txid, sd.utxo.vout, - 0xffffffff - 1, + 0xffffffff - + 1, // minus 1 is important. 0xffffffff on its own will burn funds sd.output, ); } @@ -791,78 +814,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); } - Future> _identifyCoins({ - required Map anonymitySet, - required Set spentCoinTags, - required Set sparkAddressDerivationPaths, - }) async { - final root = await getRootHDNode(); - - final List myCoins = []; - - for (final path in sparkAddressDerivationPaths) { - final keys = root.derivePath(path); - - final privateKeyHex = keys.privateKey.data.toHex; - - for (final dynData in anonymitySet["coins"] as List) { - final data = List.from(dynData as List); - - if (data.length != 3) { - throw Exception("Unexpected serialized coin info found"); - } - - final serializedCoinB64 = data[0]; - final txHash = base64ToReverseHex(data[1]); - final contextB64 = data[2]; - - final coin = LibSpark.identifyAndRecoverCoin( - serializedCoinB64, - privateKeyHex: privateKeyHex, - index: kDefaultSparkIndex, - context: base64Decode(contextB64), - isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, - ); - - // its ours - if (coin != null) { - final SparkCoinType coinType; - switch (coin.type.value) { - case 0: - coinType = SparkCoinType.mint; - case 1: - coinType = SparkCoinType.spend; - default: - throw Exception("Unknown spark coin type detected"); - } - myCoins.add( - SparkCoin( - walletId: walletId, - type: coinType, - isUsed: spentCoinTags.contains(coin.lTagHash!), - nonce: coin.nonceHex?.toUint8ListFromHex, - address: coin.address!, - txHash: txHash, - valueIntString: coin.value!.toString(), - memo: coin.memo, - serialContext: coin.serialContext, - diversifierIntString: coin.diversifier!.toString(), - encryptedDiversifier: coin.encryptedDiversifier, - serial: coin.serial, - tag: coin.tag, - lTagHash: coin.lTagHash!, - height: coin.height, - serializedCoinB64: serializedCoinB64, - contextB64: contextB64, - ), - ); - } - } - } - - return myCoins; - } - Future _addOrUpdateSparkCoins(List coins) async { if (coins.isNotEmpty) { await mainDB.isar.writeTxn(() async { @@ -900,3 +851,73 @@ String base64ToReverseHex(String source) => .reversed .map((e) => e.toRadixString(16).padLeft(2, '0')) .join(); + +/// Top level function which should be called wrapped in [compute] +Future> _identifyCoins( + ({ + List anonymitySetCoins, + Set spentCoinTags, + Set privateKeyHexSet, + String walletId, + bool isTestNet, + }) args) async { + final List myCoins = []; + + for (final privateKeyHex in args.privateKeyHexSet) { + for (final dynData in args.anonymitySetCoins) { + final data = List.from(dynData as List); + + if (data.length != 3) { + throw Exception("Unexpected serialized coin info found"); + } + + final serializedCoinB64 = data[0]; + final txHash = base64ToReverseHex(data[1]); + final contextB64 = data[2]; + + final coin = LibSpark.identifyAndRecoverCoin( + serializedCoinB64, + privateKeyHex: privateKeyHex, + index: kDefaultSparkIndex, + context: base64Decode(contextB64), + isTestNet: args.isTestNet, + ); + + // its ours + if (coin != null) { + final SparkCoinType coinType; + switch (coin.type.value) { + case 0: + coinType = SparkCoinType.mint; + case 1: + coinType = SparkCoinType.spend; + default: + throw Exception("Unknown spark coin type detected"); + } + myCoins.add( + SparkCoin( + walletId: args.walletId, + type: coinType, + isUsed: args.spentCoinTags.contains(coin.lTagHash!), + nonce: coin.nonceHex?.toUint8ListFromHex, + address: coin.address!, + txHash: txHash, + valueIntString: coin.value!.toString(), + memo: coin.memo, + serialContext: coin.serialContext, + diversifierIntString: coin.diversifier!.toString(), + encryptedDiversifier: coin.encryptedDiversifier, + serial: coin.serial, + tag: coin.tag, + lTagHash: coin.lTagHash!, + height: coin.height, + serializedCoinB64: serializedCoinB64, + contextB64: contextB64, + ), + ); + } + } + } + + return myCoins; +} From acb0157d8a7ace335bc11b8cfb917ae642318b91 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 19 Dec 2023 18:34:20 -0600 Subject: [PATCH 51/77] Firo balance type toggle ui and send from balance type switching --- lib/db/migrate_wallets_to_isar.dart | 1 + lib/pages/send_view/send_view.dart | 364 +++++++++++------- .../firo_balance_selection_sheet.dart | 97 ++++- .../wallet_balance_toggle_sheet.dart | 118 ++++-- .../sub_widgets/wallet_summary_info.dart | 39 +- lib/pages/wallet_view/wallet_view.dart | 32 +- .../desktop_balance_toggle_button.dart | 35 +- .../wallet_view/sub_widgets/desktop_send.dart | 204 +++++++--- .../sub_widgets/desktop_wallet_summary.dart | 41 +- ...public_private_balance_state_provider.dart | 8 +- .../wallet_balance_toggle_state_provider.dart | 4 - .../spark_interface.dart | 11 +- 12 files changed, 674 insertions(+), 280 deletions(-) diff --git a/lib/db/migrate_wallets_to_isar.dart b/lib/db/migrate_wallets_to_isar.dart index 9f46c274a..893a85094 100644 --- a/lib/db/migrate_wallets_to_isar.dart +++ b/lib/db/migrate_wallets_to_isar.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_exten Future migrateWalletsToIsar({ required SecureStorageInterface secureStore, }) async { + await MainDB.instance.initMainDB(); final allWalletsBox = await Hive.openBox(DB.boxNameAllWalletsData); final names = DB.instance diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 73b596690..0e1a8d5e3 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -15,6 +15,7 @@ import 'package:cw_core/monero_transaction_priority.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_native_splash/cli_commands.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; @@ -49,10 +50,12 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -116,12 +119,14 @@ class _SendViewState extends ConsumerState { final _memoFocus = FocusNode(); late final bool isStellar; + late final bool isFiro; Amount? _amountToSend; Amount? _cachedAmountToSend; String? _address; bool _addressToggleFlag = false; + bool _isSparkAddress = false; bool _cryptoAmountChangeLock = false; late VoidCallback onCryptoAmountChanged; @@ -241,11 +246,17 @@ class _SendViewState extends ConsumerState { ref.read(previewTxButtonStateProvider.state).state = (amount != null && amount > Amount.zero); } else { - final isValidAddress = ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(address ?? ""); + final walletCurrency = + ref.read(pWallets).getWallet(walletId).cryptoCurrency; + final isValidAddress = walletCurrency.validateAddress(address ?? ""); + + _isSparkAddress = isValidAddress + ? SparkInterface.validateSparkAddress( + address: address!, + isTestNet: walletCurrency.network == CryptoCurrencyNetwork.test, + ) + : false; + ref.read(previewTxButtonStateProvider.state).state = (isValidAddress && amount != null && amount > Amount.zero); } @@ -254,7 +265,8 @@ class _SendViewState extends ConsumerState { late Future _calculateFeesFuture; Map cachedFees = {}; - Map cachedFiroPrivateFees = {}; + Map cachedFiroLelantusFees = {}; + Map cachedFiroSparkFees = {}; Map cachedFiroPublicFees = {}; Future calculateFees(Amount amount) async { @@ -262,16 +274,23 @@ class _SendViewState extends ConsumerState { return "0"; } - if (coin == Coin.firo || coin == Coin.firoTestNet) { - if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - if (cachedFiroPrivateFees[amount] != null) { - return cachedFiroPrivateFees[amount]!; - } - } else { - if (cachedFiroPublicFees[amount] != null) { - return cachedFiroPublicFees[amount]!; - } + if (isFiro) { + switch (ref.read(publicPrivateBalanceStateProvider.state).state) { + case FiroType.public: + if (cachedFiroPublicFees[amount] != null) { + return cachedFiroPublicFees[amount]!; + } + break; + case FiroType.lelantus: + if (cachedFiroLelantusFees[amount] != null) { + return cachedFiroLelantusFees[amount]!; + } + break; + case FiroType.spark: + if (cachedFiroSparkFees[amount] != null) { + return cachedFiroSparkFees[amount]!; + } + break; } } else if (cachedFees[amount] != null) { return cachedFees[amount]!; @@ -321,31 +340,37 @@ class _SendViewState extends ConsumerState { ); return cachedFees[amount]!; - } else if (coin == Coin.firo || coin == Coin.firoTestNet) { - if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - fee = await wallet.estimateFeeFor(amount, feeRate); + } else if (isFiro) { + final firoWallet = wallet as FiroWallet; - cachedFiroPrivateFees[amount] = ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + switch (ref.read(publicPrivateBalanceStateProvider.state).state) { + case FiroType.public: + fee = await firoWallet.estimateFeeFor(amount, feeRate); + cachedFiroPublicFees[amount] = + ref.read(pAmountFormatter(coin)).format( + fee, + withUnitName: true, + indicatePrecisionLoss: false, + ); + return cachedFiroPublicFees[amount]!; - return cachedFiroPrivateFees[amount]!; - } else { - // TODO: [prio=high] firo public send fees refactor or something... - throw UnimplementedError("Firo pub fees todo"); - // fee = await (manager.wallet as FiroWallet) - // .estimateFeeForPublic(amount, feeRate); - // - // cachedFiroPublicFees[amount] = ref.read(pAmountFormatter(coin)).format( - // fee, - // withUnitName: true, - // indicatePrecisionLoss: false, - // ); - // - // return cachedFiroPublicFees[amount]!; + case FiroType.lelantus: + fee = await firoWallet.estimateFeeForLelantus(amount); + cachedFiroLelantusFees[amount] = + ref.read(pAmountFormatter(coin)).format( + fee, + withUnitName: true, + indicatePrecisionLoss: false, + ); + return cachedFiroLelantusFees[amount]!; + case FiroType.spark: + fee = await firoWallet.estimateFeeForSpark(amount); + cachedFiroSparkFees[amount] = ref.read(pAmountFormatter(coin)).format( + fee, + withUnitName: true, + indicatePrecisionLoss: false, + ); + return cachedFiroSparkFees[amount]!; } } else { fee = await wallet.estimateFeeFor(amount, feeRate); @@ -369,13 +394,17 @@ class _SendViewState extends ConsumerState { final Amount amount = _amountToSend!; final Amount availableBalance; - if ((coin == Coin.firo || coin == Coin.firoTestNet)) { - if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - availableBalance = ref.read(pWalletBalance(walletId)).spendable; - } else { - availableBalance = - ref.read(pWalletBalanceSecondary(walletId)).spendable; + if (isFiro) { + switch (ref.read(publicPrivateBalanceStateProvider.state).state) { + case FiroType.public: + availableBalance = wallet.info.cachedBalance.spendable; + break; + case FiroType.lelantus: + availableBalance = wallet.info.cachedBalanceSecondary.spendable; + break; + case FiroType.spark: + availableBalance = wallet.info.cachedBalanceTertiary.spendable; + break; } } else { availableBalance = ref.read(pWalletBalance(walletId)).spendable; @@ -492,14 +521,63 @@ class _SendViewState extends ConsumerState { : null, ), ); - } else if (wallet is FiroWallet && - ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - txDataFuture = wallet.prepareSendLelantus( - txData: TxData( - recipients: [(address: _address!, amount: amount)], - ), - ); + } else if (wallet is FiroWallet) { + switch (ref.read(publicPrivateBalanceStateProvider.state).state) { + case FiroType.public: + txDataFuture = wallet.prepareSend( + txData: TxData( + recipients: _isSparkAddress + ? null + : [(address: _address!, amount: amount)], + sparkRecipients: _isSparkAddress + ? [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + subtractFeeFromAmount: false, + ) + ] + : null, + feeRateType: ref.read(feeRateTypeStateProvider), + satsPerVByte: isCustomFee ? customFeeRate : null, + utxos: (wallet is CoinControlInterface && + coinControlEnabled && + selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, + ), + ); + break; + + case FiroType.lelantus: + txDataFuture = wallet.prepareSendLelantus( + txData: TxData( + recipients: [(address: _address!, amount: amount)], + ), + ); + break; + + case FiroType.spark: + txDataFuture = wallet.prepareSendSpark( + txData: TxData( + recipients: _isSparkAddress + ? null + : [(address: _address!, amount: amount)], + sparkRecipients: _isSparkAddress + ? [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + subtractFeeFromAmount: false, + ) + ] + : null, + ), + ); + break; + } } else { final memo = coin == Coin.stellar || coin == Coin.stellarTestnet ? memoController.text @@ -610,6 +688,7 @@ class _SendViewState extends ConsumerState { clipboard = widget.clipboard; scanner = widget.barcodeScanner; isStellar = coin == Coin.stellar || coin == Coin.stellarTestnet; + isFiro = coin == Coin.firo || coin == Coin.firoTestNet; sendToController = TextEditingController(); cryptoAmountController = TextEditingController(); @@ -718,7 +797,7 @@ class _SendViewState extends ConsumerState { ), ); - if (coin == Coin.firo || coin == Coin.firoTestNet) { + if (isFiro) { ref.listen(publicPrivateBalanceStateProvider, (previous, next) { if (_amountToSend == null) { setState(() { @@ -830,10 +909,9 @@ class _SendViewState extends ConsumerState { // const SizedBox( // height: 2, // ), - if (coin == Coin.firo || - coin == Coin.firoTestNet) + if (isFiro) Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", + "${ref.watch(publicPrivateBalanceStateProvider.state).state.name.capitalize()} balance", style: STextStyles.label(context) .copyWith(fontSize: 10), ), @@ -849,22 +927,29 @@ class _SendViewState extends ConsumerState { const Spacer(), Builder(builder: (context) { final Amount amount; - if (coin != Coin.firo && - coin != Coin.firoTestNet) { - if (ref - .watch( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") { - amount = ref - .read(pWalletBalance(walletId)) - .spendable; - } else { - amount = ref - .read(pWalletBalanceSecondary( - walletId)) - .spendable; + if (isFiro) { + switch (ref + .watch( + publicPrivateBalanceStateProvider + .state) + .state) { + case FiroType.public: + amount = ref + .read(pWalletBalance(walletId)) + .spendable; + break; + case FiroType.lelantus: + amount = ref + .read(pWalletBalanceSecondary( + walletId)) + .spendable; + break; + case FiroType.spark: + amount = ref + .read(pWalletBalanceTertiary( + walletId)) + .spendable; + break; } } else { amount = ref @@ -1245,7 +1330,7 @@ class _SendViewState extends ConsumerState { const SizedBox( height: 10, ), - if (isStellar) + if (isStellar || _isSparkAddress) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1355,21 +1440,21 @@ class _SendViewState extends ConsumerState { } }, ), - if (coin == Coin.firo) + if (isFiro) const SizedBox( height: 12, ), - if (coin == Coin.firo) + if (isFiro) Text( "Send from", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin == Coin.firo) + if (isFiro) const SizedBox( height: 8, ), - if (coin == Coin.firo) + if (isFiro) Stack( children: [ TextField( @@ -1414,47 +1499,53 @@ class _SendViewState extends ConsumerState { Row( children: [ Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", + "${ref.watch(publicPrivateBalanceStateProvider.state).state.name.capitalize()} balance", style: STextStyles.itemSubtitle12( context), ), const SizedBox( width: 10, ), - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") - Text( - ref - .watch( - pAmountFormatter(coin)) - .format( - ref - .watch( - pWalletBalanceSecondary( - walletId)) - .spendable, - ), - style: STextStyles.itemSubtitle( - context), - ) - else - Text( - ref - .watch( - pAmountFormatter(coin)) - .format( - ref - .watch(pWalletBalance( + Builder(builder: (_) { + final Amount amount; + switch (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state) { + case FiroType.public: + amount = ref + .watch(pWalletBalance( + walletId)) + .spendable; + break; + case FiroType.lelantus: + amount = ref + .watch( + pWalletBalanceSecondary( walletId)) - .spendable, + .spendable; + break; + case FiroType.spark: + amount = ref + .watch( + pWalletBalanceTertiary( + walletId)) + .spendable; + break; + } + + return Text( + ref + .watch( + pAmountFormatter(coin)) + .format( + amount, ), style: STextStyles.itemSubtitle( context), - ), + ); + }), ], ), SvgPicture.asset( @@ -1486,21 +1577,36 @@ class _SendViewState extends ConsumerState { CustomTextButton( text: "Send all ${coin.ticker}", onTap: () async { - if ((coin == Coin.firo || - coin == Coin.firoTestNet) && - ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Public") { + if (isFiro) { + final Amount amount; + switch (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state) { + case FiroType.public: + amount = ref + .read(pWalletBalance(walletId)) + .spendable; + break; + case FiroType.lelantus: + amount = ref + .read(pWalletBalanceSecondary( + walletId)) + .spendable; + break; + case FiroType.spark: + amount = ref + .read(pWalletBalanceTertiary( + walletId)) + .spendable; + break; + } + cryptoAmountController.text = ref .read(pAmountFormatter(coin)) .format( - ref - .read(pWalletBalanceSecondary( - walletId)) - .spendable, + amount, withUnitName: false, ); } else { @@ -1935,14 +2041,13 @@ class _SendViewState extends ConsumerState { Constants.size.circularBorderRadius, ), ), - onPressed: (coin == Coin.firo || - coin == Coin.firoTestNet) && + onPressed: isFiro && ref .watch( publicPrivateBalanceStateProvider .state) - .state == - "Private" + .state != + FiroType.public ? null : () { showModalBottomSheet( @@ -1993,14 +2098,13 @@ class _SendViewState extends ConsumerState { ), ); }, - child: ((coin == Coin.firo || - coin == Coin.firoTestNet) && + child: (isFiro && ref .watch( publicPrivateBalanceStateProvider .state) - .state == - "Private") + .state != + FiroType.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/firo_balance_selection_sheet.dart index a09f6928b..8c01cac9a 100644 --- a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart @@ -101,9 +101,9 @@ class _FiroBalanceSelectionSheetState onTap: () { final state = ref.read(publicPrivateBalanceStateProvider.state).state; - if (state != "Private") { + if (state != FiroType.spark) { ref.read(publicPrivateBalanceStateProvider.state).state = - "Private"; + FiroType.spark; } Navigator.of(context).pop(); }, @@ -122,7 +122,7 @@ class _FiroBalanceSelectionSheetState activeColor: Theme.of(context) .extension()! .radioButtonIconEnabled, - value: "Private", + value: FiroType.spark, groupValue: ref .watch( publicPrivateBalanceStateProvider.state) @@ -131,7 +131,7 @@ class _FiroBalanceSelectionSheetState ref .read(publicPrivateBalanceStateProvider .state) - .state = "Private"; + .state = FiroType.spark; Navigator.of(context).pop(); }, @@ -149,7 +149,86 @@ class _FiroBalanceSelectionSheetState // Row( // children: [ Text( - "Private balance", + "Spark balance", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + width: 2, + ), + Text( + ref.watch(pAmountFormatter(coin)).format( + firoWallet + .info.cachedBalanceTertiary.spendable, + ), + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + // ], + // ), + ) + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + GestureDetector( + onTap: () { + final state = + ref.read(publicPrivateBalanceStateProvider.state).state; + if (state != FiroType.lelantus) { + ref.read(publicPrivateBalanceStateProvider.state).state = + FiroType.lelantus; + } + Navigator.of(context).pop(); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: FiroType.lelantus, + groupValue: ref + .watch( + publicPrivateBalanceStateProvider.state) + .state, + onChanged: (x) { + ref + .read(publicPrivateBalanceStateProvider + .state) + .state = FiroType.lelantus; + + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Row( + // children: [ + Text( + "Lelantus balance", style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), @@ -180,9 +259,9 @@ class _FiroBalanceSelectionSheetState onTap: () { final state = ref.read(publicPrivateBalanceStateProvider.state).state; - if (state != "Public") { + if (state != FiroType.public) { ref.read(publicPrivateBalanceStateProvider.state).state = - "Public"; + FiroType.public; } Navigator.of(context).pop(); }, @@ -200,7 +279,7 @@ class _FiroBalanceSelectionSheetState activeColor: Theme.of(context) .extension()! .radioButtonIconEnabled, - value: "Public", + value: FiroType.public, groupValue: ref .watch( publicPrivateBalanceStateProvider.state) @@ -209,7 +288,7 @@ class _FiroBalanceSelectionSheetState ref .read(publicPrivateBalanceStateProvider .state) - .state = "Public"; + .state = FiroType.public; Navigator.of(context).pop(); }, ), 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 a0e5a29ee..8fa7eaaef 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 @@ -25,8 +25,10 @@ import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; enum _BalanceType { available, full, - privateAvailable, - privateFull; + lelantusAvailable, + lelantusFull, + sparkAvailable, + sparkFull; } class WalletBalanceToggleSheet extends ConsumerWidget { @@ -39,9 +41,10 @@ class WalletBalanceToggleSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final maxHeight = MediaQuery.of(context).size.height * 0.60; + final maxHeight = MediaQuery.of(context).size.height * 0.90; final coin = ref.watch(pWalletCoin(walletId)); + final isFiro = coin == Coin.firo || coin == Coin.firoTestNet; Balance balance = ref.watch(pWalletBalance(walletId)); @@ -52,18 +55,27 @@ class WalletBalanceToggleSheet extends ConsumerWidget { : _BalanceType.full; Balance? balanceSecondary; - if (coin == Coin.firo || coin == Coin.firoTestNet) { + Balance? balanceTertiary; + if (isFiro) { balanceSecondary = ref.watch(pWalletBalanceSecondary(walletId)); + balanceTertiary = ref.watch(pWalletBalanceTertiary(walletId)); - final temp = balance; - balance = balanceSecondary!; - balanceSecondary = temp; + switch (ref.watch(publicPrivateBalanceStateProvider.state).state) { + case FiroType.spark: + _bal = _bal == _BalanceType.available + ? _BalanceType.sparkAvailable + : _BalanceType.sparkFull; + break; - if (ref.watch(publicPrivateBalanceStateProvider.state).state == - "Private") { - _bal = _bal == _BalanceType.available - ? _BalanceType.privateAvailable - : _BalanceType.privateFull; + case FiroType.lelantus: + _bal = _bal == _BalanceType.available + ? _BalanceType.lelantusAvailable + : _BalanceType.lelantusFull; + break; + + case FiroType.public: + // already set above + break; } } @@ -116,22 +128,21 @@ class WalletBalanceToggleSheet extends ConsumerWidget { height: 24, ), BalanceSelector( - title: - "Available${balanceSecondary != null ? " public" : ""} balance", + title: "Available${isFiro ? " public" : ""} balance", coin: coin, balance: balance.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - "Public"; + FiroType.public; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - "Public"; + FiroType.public; Navigator.of(context).pop(); }, value: _BalanceType.available, @@ -141,22 +152,21 @@ class WalletBalanceToggleSheet extends ConsumerWidget { height: 12, ), BalanceSelector( - title: - "Full${balanceSecondary != null ? " public" : ""} balance", + title: "Full${isFiro ? " public" : ""} balance", coin: coin, balance: balance.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - "Public"; + FiroType.public; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - "Public"; + FiroType.public; Navigator.of(context).pop(); }, value: _BalanceType.full, @@ -168,24 +178,24 @@ class WalletBalanceToggleSheet extends ConsumerWidget { ), if (balanceSecondary != null) BalanceSelector( - title: "Available private balance", + title: "Available lelantus balance", coin: coin, balance: balanceSecondary.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - "Private"; + FiroType.lelantus; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - "Private"; + FiroType.lelantus; Navigator.of(context).pop(); }, - value: _BalanceType.privateAvailable, + value: _BalanceType.lelantusAvailable, groupValue: _bal, ), if (balanceSecondary != null) @@ -194,24 +204,76 @@ class WalletBalanceToggleSheet extends ConsumerWidget { ), if (balanceSecondary != null) BalanceSelector( - title: "Full private balance", + title: "Full lelantus balance", coin: coin, balance: balanceSecondary.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - "Private"; + FiroType.lelantus; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - "Private"; + FiroType.lelantus; Navigator.of(context).pop(); }, - value: _BalanceType.privateFull, + value: _BalanceType.lelantusFull, + groupValue: _bal, + ), + if (balanceTertiary != null) + const SizedBox( + height: 12, + ), + if (balanceTertiary != null) + BalanceSelector( + title: "Available spark balance", + coin: coin, + balance: balanceTertiary.spendable, + onPressed: () { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.available; + ref.read(publicPrivateBalanceStateProvider.state).state = + FiroType.spark; + Navigator.of(context).pop(); + }, + onChanged: (_) { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.available; + ref.read(publicPrivateBalanceStateProvider.state).state = + FiroType.spark; + Navigator.of(context).pop(); + }, + value: _BalanceType.sparkAvailable, + groupValue: _bal, + ), + if (balanceTertiary != null) + const SizedBox( + height: 12, + ), + if (balanceTertiary != null) + BalanceSelector( + title: "Full spark balance", + coin: coin, + balance: balanceTertiary.total, + onPressed: () { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + ref.read(publicPrivateBalanceStateProvider.state).state = + FiroType.spark; + Navigator.of(context).pop(); + }, + onChanged: (_) { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + ref.read(publicPrivateBalanceStateProvider.state).state = + FiroType.spark; + Navigator.of(context).pop(); + }, + value: _BalanceType.sparkFull, groupValue: _bal, ), const SizedBox( 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 b7eb4d390..aeadd6a7a 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -12,6 +12,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter_native_splash/cli_commands.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart'; @@ -29,6 +30,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; +import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; class WalletSummaryInfo extends ConsumerWidget { @@ -45,6 +47,8 @@ class WalletSummaryInfo extends ConsumerWidget { showModalBottomSheet( backgroundColor: Colors.transparent, context: context, + useSafeArea: true, + isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(20), @@ -58,10 +62,6 @@ class WalletSummaryInfo extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); - bool isMonkey = true; - - final receivingAddress = ref.watch(pWalletReceivingAddress(walletId)); - final externalCalls = ref.watch( prefsChangeNotifierProvider.select((value) => value.externalCalls)); final coin = ref.watch(pWalletCoin(walletId)); @@ -81,19 +81,28 @@ class WalletSummaryInfo extends ConsumerWidget { WalletBalanceToggleState.available; final Amount balanceToShow; - String title; + final String title; if (coin == Coin.firo || coin == Coin.firoTestNet) { - final _showPrivate = - ref.watch(publicPrivateBalanceStateProvider.state).state == "Private"; + final type = ref.watch(publicPrivateBalanceStateProvider.state).state; + title = + "${_showAvailable ? "Available" : "Full"} ${type.name.capitalize()} balance"; + switch (type) { + case FiroType.spark: + final balance = ref.watch(pWalletBalanceTertiary(walletId)); + balanceToShow = _showAvailable ? balance.spendable : balance.total; + break; - final secondaryBal = ref.watch(pWalletBalanceSecondary(walletId)); + case FiroType.lelantus: + final balance = ref.watch(pWalletBalanceSecondary(walletId)); + balanceToShow = _showAvailable ? balance.spendable : balance.total; + break; - final bal = _showPrivate ? balance : secondaryBal; - - balanceToShow = _showAvailable ? bal.spendable : bal.total; - title = _showAvailable ? "Available" : "Full"; - title += _showPrivate ? " private balance" : " public balance"; + case FiroType.public: + final balance = ref.watch(pWalletBalance(walletId)); + balanceToShow = _showAvailable ? balance.spendable : balance.total; + break; + } } else { balanceToShow = _showAvailable ? balance.spendable : balance.total; title = _showAvailable ? "Available balance" : "Full balance"; @@ -102,8 +111,8 @@ class WalletSummaryInfo extends ConsumerWidget { List? imageBytes; if (coin == Coin.banano) { - // TODO: [prio=high] fix this and uncomment: - // imageBytes = (manager.wallet as BananoWallet).getMonkeyImageBytes(); + imageBytes = (ref.watch(pWallets).getWallet(walletId) as BananoWallet) + .getMonkeyImageBytes(); } return ConditionalParent( diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 3d5577a0f..19afd86be 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -46,8 +46,6 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/providers/ui/unread_notifications_provider.dart'; import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart'; -import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; -import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; @@ -63,7 +61,6 @@ import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -951,20 +948,21 @@ class _WalletViewState extends ConsumerState { label: "Send", icon: const SendNavIcon(), onTap: () { - switch (ref - .read(walletBalanceToggleStateProvider.state) - .state) { - case WalletBalanceToggleState.full: - ref - .read(publicPrivateBalanceStateProvider.state) - .state = "Public"; - break; - case WalletBalanceToggleState.available: - ref - .read(publicPrivateBalanceStateProvider.state) - .state = "Private"; - break; - } + // not sure what this is supposed to accomplish? + // switch (ref + // .read(walletBalanceToggleStateProvider.state) + // .state) { + // case WalletBalanceToggleState.full: + // ref + // .read(publicPrivateBalanceStateProvider.state) + // .state = "Public"; + // break; + // case WalletBalanceToggleState.available: + // ref + // .read(publicPrivateBalanceStateProvider.state) + // .state = "Private"; + // break; + // } Navigator.of(context).pushNamed( SendView.routeName, arguments: Tuple2( 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 355badbd3..bd9eafd2d 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 @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -80,6 +81,8 @@ class DesktopPrivateBalanceToggleButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final currentType = ref.watch(publicPrivateBalanceStateProvider); + return SizedBox( height: 22, width: 22, @@ -87,13 +90,21 @@ class DesktopPrivateBalanceToggleButton extends ConsumerWidget { color: Theme.of(context).extension()!.buttonBackSecondary, splashColor: Theme.of(context).extension()!.highlight, onPressed: () { - if (ref.read(walletPrivateBalanceToggleStateProvider.state).state == - WalletBalanceToggleState.available) { - ref.read(walletPrivateBalanceToggleStateProvider.state).state = - WalletBalanceToggleState.full; - } else { - ref.read(walletPrivateBalanceToggleStateProvider.state).state = - WalletBalanceToggleState.available; + switch (currentType) { + case FiroType.public: + ref.read(publicPrivateBalanceStateProvider.state).state = + FiroType.lelantus; + break; + + case FiroType.lelantus: + ref.read(publicPrivateBalanceStateProvider.state).state = + FiroType.spark; + break; + + case FiroType.spark: + ref.read(publicPrivateBalanceStateProvider.state).state = + FiroType.public; + break; } onPressed?.call(); }, @@ -110,12 +121,14 @@ class DesktopPrivateBalanceToggleButton extends ConsumerWidget { child: Center( child: Image( image: AssetImage( - ref.watch(walletPrivateBalanceToggleStateProvider.state).state == - WalletBalanceToggleState.available - ? Assets.png.glassesHidden - : Assets.png.glasses, + currentType == FiroType.public + ? Assets.png.glasses + : Assets.png.glassesHidden, ), width: 16, + color: currentType == FiroType.spark + ? Theme.of(context).extension()!.accentColorYellow + : null, ), ), ), 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 1affff3b8..9bb53cba4 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 @@ -48,10 +48,12 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; @@ -123,6 +125,8 @@ class _DesktopSendState extends ConsumerState { bool get isPaynymSend => widget.accountLite != null; + bool _isSparkAddress = false; + bool isCustomFee = false; int customFeeRate = 1; (FeeRateType, String?, String?)? feeSelectionResult; @@ -140,13 +144,16 @@ class _DesktopSendState extends ConsumerState { final Amount amount = _amountToSend!; final Amount availableBalance; if ((coin == Coin.firo || coin == Coin.firoTestNet)) { - if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - availableBalance = wallet.info.cachedBalanceSecondary.spendable; - // (manager.wallet as FiroWallet).availablePrivateBalance(); - } else { - availableBalance = wallet.info.cachedBalance.spendable; - // (manager.wallet as FiroWallet).availablePublicBalance(); + switch (ref.read(publicPrivateBalanceStateProvider.state).state) { + case FiroType.public: + availableBalance = wallet.info.cachedBalance.spendable; + break; + case FiroType.lelantus: + availableBalance = wallet.info.cachedBalanceSecondary.spendable; + break; + case FiroType.spark: + availableBalance = wallet.info.cachedBalanceTertiary.spendable; + break; } } else { availableBalance = wallet.info.cachedBalance.spendable; @@ -311,14 +318,63 @@ class _DesktopSendState extends ConsumerState { : null, ), ); - } else if (wallet is FiroWallet && - ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - txDataFuture = wallet.prepareSendLelantus( - txData: TxData( - recipients: [(address: _address!, amount: amount)], - ), - ); + } else if (wallet is FiroWallet) { + switch (ref.read(publicPrivateBalanceStateProvider.state).state) { + case FiroType.public: + txDataFuture = wallet.prepareSend( + txData: TxData( + recipients: _isSparkAddress + ? null + : [(address: _address!, amount: amount)], + sparkRecipients: _isSparkAddress + ? [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + subtractFeeFromAmount: false, + ) + ] + : null, + feeRateType: ref.read(feeRateTypeStateProvider), + satsPerVByte: isCustomFee ? customFeeRate : null, + utxos: (wallet is CoinControlInterface && + coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, + ), + ); + break; + + case FiroType.lelantus: + txDataFuture = wallet.prepareSendLelantus( + txData: TxData( + recipients: [(address: _address!, amount: amount)], + ), + ); + break; + + case FiroType.spark: + txDataFuture = wallet.prepareSendSpark( + txData: TxData( + recipients: _isSparkAddress + ? null + : [(address: _address!, amount: amount)], + sparkRecipients: _isSparkAddress + ? [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + subtractFeeFromAmount: false, + ) + ] + : null, + ), + ); + break; + } } else { final memo = isStellar ? memoController.text : null; txDataFuture = wallet.prepareSend( @@ -520,11 +576,17 @@ class _DesktopSendState extends ConsumerState { ref.read(previewTxButtonStateProvider.state).state = (amount != null && amount > Amount.zero); } else { - final isValidAddress = ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(address ?? ""); + final walletCurrency = + ref.read(pWallets).getWallet(walletId).cryptoCurrency; + final isValidAddress = walletCurrency.validateAddress(address ?? ""); + + _isSparkAddress = isValidAddress + ? SparkInterface.validateSparkAddress( + address: address!, + isTestNet: walletCurrency.network == CryptoCurrencyNetwork.test, + ) + : false; + ref.read(previewTxButtonStateProvider.state).state = (isValidAddress && amount != null && amount > Amount.zero); } @@ -687,11 +749,23 @@ class _DesktopSendState extends ConsumerState { Future sendAllTapped() async { final info = ref.read(pWalletInfo(walletId)); - if ((coin == Coin.firo || coin == Coin.firoTestNet) && - ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { - cryptoAmountController.text = info - .cachedBalanceSecondary.spendable.decimal - .toStringAsFixed(coin.decimals); + if (coin == Coin.firo || coin == Coin.firoTestNet) { + switch (ref.read(publicPrivateBalanceStateProvider.state).state) { + case FiroType.public: + cryptoAmountController.text = info.cachedBalance.spendable.decimal + .toStringAsFixed(coin.decimals); + break; + case FiroType.lelantus: + cryptoAmountController.text = info + .cachedBalanceSecondary.spendable.decimal + .toStringAsFixed(coin.decimals); + break; + case FiroType.spark: + cryptoAmountController.text = info + .cachedBalanceTertiary.spendable.decimal + .toStringAsFixed(coin.decimals); + break; + } } else { cryptoAmountController.text = info.cachedBalance.spendable.decimal.toStringAsFixed(coin.decimals); @@ -841,11 +915,31 @@ class _DesktopSendState extends ConsumerState { value: ref.watch(publicPrivateBalanceStateProvider.state).state, items: [ DropdownMenuItem( - value: "Private", + value: FiroType.spark, child: Row( children: [ Text( - "Private balance", + "Spark balance", + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + Text( + ref.watch(pAmountFormatter(coin)).format(ref + .watch(pWalletBalanceTertiary(walletId)) + .spendable), + style: STextStyles.itemSubtitle(context), + ), + ], + ), + ), + DropdownMenuItem( + value: FiroType.lelantus, + child: Row( + children: [ + Text( + "Lelantus balance", style: STextStyles.itemSubtitle12(context), ), const SizedBox( @@ -861,7 +955,7 @@ class _DesktopSendState extends ConsumerState { ), ), DropdownMenuItem( - value: "Public", + value: FiroType.public, child: Row( children: [ Text( @@ -881,9 +975,9 @@ class _DesktopSendState extends ConsumerState { ), ], onChanged: (value) { - if (value is String) { + if (value is FiroType) { setState(() { - ref.watch(publicPrivateBalanceStateProvider.state).state = + ref.read(publicPrivateBalanceStateProvider.state).state = value; }); } @@ -1316,11 +1410,11 @@ class _DesktopSendState extends ConsumerState { } }, ), - if (isStellar) + if (isStellar || _isSparkAddress) const SizedBox( height: 10, ), - if (isStellar) + if (isStellar || _isSparkAddress) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1386,8 +1480,12 @@ class _DesktopSendState extends ConsumerState { ConditionalParent( condition: coin.isElectrumXCoin && !(((coin == Coin.firo || coin == Coin.firoTestNet) && - ref.read(publicPrivateBalanceStateProvider.state).state == - "Private")), + (ref.watch(publicPrivateBalanceStateProvider.state).state == + FiroType.lelantus || + ref + .watch(publicPrivateBalanceStateProvider.state) + .state == + FiroType.spark))), builder: (child) => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -1485,15 +1583,33 @@ class _DesktopSendState extends ConsumerState { .read( publicPrivateBalanceStateProvider .state) - .state == - "Private") { - throw UnimplementedError("FIXME"); - // TODO: [prio=high] firo fee fix - // ref - // .read(feeSheetSessionCacheProvider) - // .average[amount] = await (manager.wallet - // as FiroWallet) - // .estimateFeeForPublic(amount, feeRate); + .state != + FiroType.public) { + final firoWallet = wallet as FiroWallet; + + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + FiroType.lelantus) { + ref + .read(feeSheetSessionCacheProvider) + .average[amount] = + await firoWallet + .estimateFeeForLelantus(amount); + } else if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + FiroType.spark) { + ref + .read(feeSheetSessionCacheProvider) + .average[amount] = + await firoWallet + .estimateFeeForSpark(amount); + } } else { ref .read(feeSheetSessionCacheProvider) @@ -1531,7 +1647,7 @@ class _DesktopSendState extends ConsumerState { .watch( publicPrivateBalanceStateProvider.state) .state == - "Private" + FiroType.lelantus ? Text( "~${ref.watch(pAmountFormatter(coin)).format( Amount( 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 26f556fb9..b7d81c301 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 @@ -15,6 +15,7 @@ import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -61,6 +62,7 @@ class _WDesktopWalletSummaryState extends ConsumerState { ), ); final coin = ref.watch(pWalletCoin(widget.walletId)); + final isFiro = coin == Coin.firo || coin == Coin.firoTestNet; final locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale)); @@ -82,29 +84,30 @@ class _WDesktopWalletSummaryState extends ConsumerState { ref.watch(walletBalanceToggleStateProvider.state).state == WalletBalanceToggleState.available; - Balance balance = widget.isToken - ? ref.watch(tokenServiceProvider.select((value) => value!.balance)) - : ref.watch(pWalletBalance(walletId)); + final Amount balanceToShow; + if (isFiro) { + switch (ref.watch(publicPrivateBalanceStateProvider.state).state) { + case FiroType.spark: + final balance = ref.watch(pWalletBalanceTertiary(walletId)); + balanceToShow = _showAvailable ? balance.spendable : balance.total; + break; - Amount balanceToShow; - if (coin == Coin.firo || coin == Coin.firoTestNet) { - final balanceSecondary = ref.watch(pWalletBalanceSecondary(walletId)); - final showPrivate = - ref.watch(walletPrivateBalanceToggleStateProvider.state).state == - WalletBalanceToggleState.available; + case FiroType.lelantus: + final balance = ref.watch(pWalletBalanceSecondary(walletId)); + balanceToShow = _showAvailable ? balance.spendable : balance.total; + break; - if (_showAvailable) { - balanceToShow = - showPrivate ? balanceSecondary.spendable : balance.spendable; - } else { - balanceToShow = showPrivate ? balanceSecondary.total : balance.total; + case FiroType.public: + final balance = ref.watch(pWalletBalance(walletId)); + balanceToShow = _showAvailable ? balance.spendable : balance.total; + break; } } else { - if (_showAvailable) { - balanceToShow = balance.spendable; - } else { - balanceToShow = balance.total; - } + Balance balance = widget.isToken + ? ref.watch(tokenServiceProvider.select((value) => value!.balance)) + : ref.watch(pWalletBalance(walletId)); + + balanceToShow = _showAvailable ? balance.spendable : balance.total; } return Consumer( diff --git a/lib/providers/wallet/public_private_balance_state_provider.dart b/lib/providers/wallet/public_private_balance_state_provider.dart index 1fb641072..503aa40f2 100644 --- a/lib/providers/wallet/public_private_balance_state_provider.dart +++ b/lib/providers/wallet/public_private_balance_state_provider.dart @@ -10,5 +10,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +enum FiroType { + public, + lelantus, + spark; +} + final publicPrivateBalanceStateProvider = - StateProvider((_) => "Private"); + StateProvider((_) => FiroType.lelantus); diff --git a/lib/providers/wallet/wallet_balance_toggle_state_provider.dart b/lib/providers/wallet/wallet_balance_toggle_state_provider.dart index 12e6ce8e7..2a6dc41fd 100644 --- a/lib/providers/wallet/wallet_balance_toggle_state_provider.dart +++ b/lib/providers/wallet/wallet_balance_toggle_state_provider.dart @@ -14,7 +14,3 @@ import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; final walletBalanceToggleStateProvider = StateProvider.autoDispose( (ref) => WalletBalanceToggleState.full); - -final walletPrivateBalanceToggleStateProvider = - StateProvider.autoDispose( - (ref) => WalletBalanceToggleState.full); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 75ac5b59e..e00652a5a 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -104,7 +104,14 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } Future estimateFeeForSpark(Amount amount) async { - throw UnimplementedError(); + // int spendAmount = amount.raw.toInt(); + // if (spendAmount == 0) { + return Amount( + rawValue: BigInt.from(0), + fractionDigits: cryptoCurrency.fractionDigits, + ); + // } + // TODO actual fee estimation } /// Spark to Spark/Transparent (spend) creation @@ -505,7 +512,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { rawValue: unusedCoins .where((e) => e.height != null && - e.height! + cryptoCurrency.minConfirms >= currentHeight) + e.height! + cryptoCurrency.minConfirms <= currentHeight) .map((e) => e.value) .fold(BigInt.zero, (prev, e) => prev + e), fractionDigits: cryptoCurrency.fractionDigits, From 35fafb5c5dab3a4dde92d57417454bc0ee483d71 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 Dec 2023 13:11:21 -0600 Subject: [PATCH 52/77] add some send logging --- .../my_stack_view/wallet_view/sub_widgets/desktop_send.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9bb53cba4..770e60cdb 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 @@ -437,7 +437,8 @@ class _DesktopSendState extends ConsumerState { ), ); } - } catch (e) { + } catch (e, s) { + Logging.instance.log("Desktop send: $e\n$s", level: LogLevel.Warning); if (mounted) { // pop building dialog Navigator.of( From c16c97d74dac189a4a112945ae50979a1fc5cda7 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 Dec 2023 13:45:46 -0600 Subject: [PATCH 53/77] add required data to spark coin schema and some other small fixes for spark spend script creation --- lib/wallets/isar/models/spark_coin.dart | 5 + lib/wallets/isar/models/spark_coin.g.dart | 196 +++++++++++++----- .../spark_interface.dart | 34 ++- 3 files changed, 178 insertions(+), 57 deletions(-) diff --git a/lib/wallets/isar/models/spark_coin.dart b/lib/wallets/isar/models/spark_coin.dart index 9c11c4556..d3ef6825c 100644 --- a/lib/wallets/isar/models/spark_coin.dart +++ b/lib/wallets/isar/models/spark_coin.dart @@ -28,6 +28,7 @@ class SparkCoin { final SparkCoinType type; final bool isUsed; + final int groupId; final List? nonce; @@ -62,6 +63,7 @@ class SparkCoin { required this.walletId, required this.type, required this.isUsed, + required this.groupId, this.nonce, required this.address, required this.txHash, @@ -81,6 +83,7 @@ class SparkCoin { SparkCoin copyWith({ SparkCoinType? type, bool? isUsed, + int? groupId, List? nonce, String? address, String? txHash, @@ -100,6 +103,7 @@ class SparkCoin { walletId: walletId, type: type ?? this.type, isUsed: isUsed ?? this.isUsed, + groupId: groupId ?? this.groupId, nonce: nonce ?? this.nonce, address: address ?? this.address, txHash: txHash ?? this.txHash, @@ -124,6 +128,7 @@ class SparkCoin { 'walletId: $walletId' ', type: $type' ', isUsed: $isUsed' + ', groupId: $groupId' ', k: $nonce' ', address: $address' ', txHash: $txHash' diff --git a/lib/wallets/isar/models/spark_coin.g.dart b/lib/wallets/isar/models/spark_coin.g.dart index e402c59eb..75cd92b62 100644 --- a/lib/wallets/isar/models/spark_coin.g.dart +++ b/lib/wallets/isar/models/spark_coin.g.dart @@ -37,69 +37,74 @@ const SparkCoinSchema = CollectionSchema( name: r'encryptedDiversifier', type: IsarType.longList, ), - r'height': PropertySchema( + r'groupId': PropertySchema( id: 4, + name: r'groupId', + type: IsarType.long, + ), + r'height': PropertySchema( + id: 5, name: r'height', type: IsarType.long, ), r'isUsed': PropertySchema( - id: 5, + id: 6, name: r'isUsed', type: IsarType.bool, ), r'lTagHash': PropertySchema( - id: 6, + id: 7, name: r'lTagHash', type: IsarType.string, ), r'memo': PropertySchema( - id: 7, + id: 8, name: r'memo', type: IsarType.string, ), r'nonce': PropertySchema( - id: 8, + id: 9, name: r'nonce', type: IsarType.longList, ), r'serial': PropertySchema( - id: 9, + id: 10, name: r'serial', type: IsarType.longList, ), r'serialContext': PropertySchema( - id: 10, + id: 11, name: r'serialContext', type: IsarType.longList, ), r'serializedCoinB64': PropertySchema( - id: 11, + id: 12, name: r'serializedCoinB64', type: IsarType.string, ), r'tag': PropertySchema( - id: 12, + id: 13, name: r'tag', type: IsarType.longList, ), r'txHash': PropertySchema( - id: 13, + id: 14, name: r'txHash', type: IsarType.string, ), r'type': PropertySchema( - id: 14, + id: 15, name: r'type', type: IsarType.byte, enumMap: _SparkCointypeEnumValueMap, ), r'valueIntString': PropertySchema( - id: 15, + id: 16, name: r'valueIntString', type: IsarType.string, ), r'walletId': PropertySchema( - id: 16, + id: 17, name: r'walletId', type: IsarType.string, ) @@ -210,19 +215,20 @@ void _sparkCoinSerialize( writer.writeString(offsets[1], object.contextB64); writer.writeString(offsets[2], object.diversifierIntString); writer.writeLongList(offsets[3], object.encryptedDiversifier); - writer.writeLong(offsets[4], object.height); - writer.writeBool(offsets[5], object.isUsed); - writer.writeString(offsets[6], object.lTagHash); - writer.writeString(offsets[7], object.memo); - writer.writeLongList(offsets[8], object.nonce); - writer.writeLongList(offsets[9], object.serial); - writer.writeLongList(offsets[10], object.serialContext); - writer.writeString(offsets[11], object.serializedCoinB64); - writer.writeLongList(offsets[12], object.tag); - writer.writeString(offsets[13], object.txHash); - writer.writeByte(offsets[14], object.type.index); - writer.writeString(offsets[15], object.valueIntString); - writer.writeString(offsets[16], object.walletId); + writer.writeLong(offsets[4], object.groupId); + writer.writeLong(offsets[5], object.height); + writer.writeBool(offsets[6], object.isUsed); + writer.writeString(offsets[7], object.lTagHash); + writer.writeString(offsets[8], object.memo); + writer.writeLongList(offsets[9], object.nonce); + writer.writeLongList(offsets[10], object.serial); + writer.writeLongList(offsets[11], object.serialContext); + writer.writeString(offsets[12], object.serializedCoinB64); + writer.writeLongList(offsets[13], object.tag); + writer.writeString(offsets[14], object.txHash); + writer.writeByte(offsets[15], object.type.index); + writer.writeString(offsets[16], object.valueIntString); + writer.writeString(offsets[17], object.walletId); } SparkCoin _sparkCoinDeserialize( @@ -236,20 +242,21 @@ SparkCoin _sparkCoinDeserialize( contextB64: reader.readStringOrNull(offsets[1]), diversifierIntString: reader.readString(offsets[2]), encryptedDiversifier: reader.readLongList(offsets[3]), - height: reader.readLongOrNull(offsets[4]), - isUsed: reader.readBool(offsets[5]), - lTagHash: reader.readString(offsets[6]), - memo: reader.readStringOrNull(offsets[7]), - nonce: reader.readLongList(offsets[8]), - serial: reader.readLongList(offsets[9]), - serialContext: reader.readLongList(offsets[10]), - serializedCoinB64: reader.readStringOrNull(offsets[11]), - tag: reader.readLongList(offsets[12]), - txHash: reader.readString(offsets[13]), - type: _SparkCointypeValueEnumMap[reader.readByteOrNull(offsets[14])] ?? + groupId: reader.readLong(offsets[4]), + height: reader.readLongOrNull(offsets[5]), + isUsed: reader.readBool(offsets[6]), + lTagHash: reader.readString(offsets[7]), + memo: reader.readStringOrNull(offsets[8]), + nonce: reader.readLongList(offsets[9]), + serial: reader.readLongList(offsets[10]), + serialContext: reader.readLongList(offsets[11]), + serializedCoinB64: reader.readStringOrNull(offsets[12]), + tag: reader.readLongList(offsets[13]), + txHash: reader.readString(offsets[14]), + type: _SparkCointypeValueEnumMap[reader.readByteOrNull(offsets[15])] ?? SparkCoinType.mint, - valueIntString: reader.readString(offsets[15]), - walletId: reader.readString(offsets[16]), + valueIntString: reader.readString(offsets[16]), + walletId: reader.readString(offsets[17]), ); object.id = id; return object; @@ -271,32 +278,34 @@ P _sparkCoinDeserializeProp

( case 3: return (reader.readLongList(offset)) as P; case 4: - return (reader.readLongOrNull(offset)) as P; + return (reader.readLong(offset)) as P; case 5: - return (reader.readBool(offset)) as P; + return (reader.readLongOrNull(offset)) as P; case 6: - return (reader.readString(offset)) as P; + return (reader.readBool(offset)) as P; case 7: - return (reader.readStringOrNull(offset)) as P; + return (reader.readString(offset)) as P; case 8: - return (reader.readLongList(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 9: return (reader.readLongList(offset)) as P; case 10: return (reader.readLongList(offset)) as P; case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: return (reader.readLongList(offset)) as P; + case 12: + return (reader.readStringOrNull(offset)) as P; case 13: - return (reader.readString(offset)) as P; + return (reader.readLongList(offset)) as P; case 14: + return (reader.readString(offset)) as P; + case 15: return (_SparkCointypeValueEnumMap[reader.readByteOrNull(offset)] ?? SparkCoinType.mint) as P; - case 15: - return (reader.readString(offset)) as P; case 16: return (reader.readString(offset)) as P; + case 17: + return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -1161,6 +1170,59 @@ extension SparkCoinQueryFilter }); } + QueryBuilder groupIdEqualTo( + int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'groupId', + value: value, + )); + }); + } + + QueryBuilder groupIdGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'groupId', + value: value, + )); + }); + } + + QueryBuilder groupIdLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'groupId', + value: value, + )); + }); + } + + QueryBuilder groupIdBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'groupId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder heightIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -2852,6 +2914,18 @@ extension SparkCoinQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByGroupId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'groupId', Sort.asc); + }); + } + + QueryBuilder sortByGroupIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'groupId', Sort.desc); + }); + } + QueryBuilder sortByHeight() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'height', Sort.asc); @@ -3002,6 +3076,18 @@ extension SparkCoinQuerySortThenBy }); } + QueryBuilder thenByGroupId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'groupId', Sort.asc); + }); + } + + QueryBuilder thenByGroupIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'groupId', Sort.desc); + }); + } + QueryBuilder thenByHeight() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'height', Sort.asc); @@ -3155,6 +3241,12 @@ extension SparkCoinQueryWhereDistinct }); } + QueryBuilder distinctByGroupId() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'groupId'); + }); + } + QueryBuilder distinctByHeight() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'height'); @@ -3276,6 +3368,12 @@ extension SparkCoinQueryProperty }); } + QueryBuilder groupIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'groupId'); + }); + } + QueryBuilder heightProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'height'); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index e00652a5a..d5268e1bd 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -123,21 +123,35 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .walletIdEqualToAnyLTagHash(walletId) .filter() .isUsedEqualTo(false) + .and() + .heightIsNotNull() .findAll(); - final serializedCoins = - coins.map((e) => (e.serializedCoinB64!, e.contextB64!)).toList(); + final serializedCoins = coins + .map((e) => ( + serializedCoin: e.serializedCoinB64!, + serializedCoinContext: e.contextB64!, + groupId: e.groupId, + height: e.height!, + )) + .toList(); final currentId = await electrumXClient.getSparkLatestCoinId(); final List> setMaps = []; - // for (int i = 0; i <= currentId; i++) { - for (int i = currentId; i <= currentId; i++) { + final List<({int groupId, String blockHash})> idAndBlockHashes = []; + for (int i = 1; i <= currentId; i++) { final set = await electrumXCachedClient.getSparkAnonymitySet( groupId: i.toString(), coin: info.coin, ); set["coinGroupID"] = i; setMaps.add(set); + idAndBlockHashes.add( + ( + groupId: i, + blockHash: set["blockHash"] as String, + ), + ); } final allAnonymitySets = setMaps @@ -385,6 +399,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { [], serializedCoins: serializedCoins, allAnonymitySets: allAnonymitySets, + idAndBlockHashes: idAndBlockHashes + .map((e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash))) + .toList(), ); print("SPARK SPEND ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); @@ -399,10 +416,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } final extractedTx = txb.buildIncomplete(); - - // TODO: verify encoding - extractedTx.setPayload(spend.serializedSpendPayload.toUint8ListFromUtf8); - + extractedTx.setPayload(spend.serializedSpendPayload); final rawTxHex = extractedTx.toHex(); return txData.copyWith( @@ -468,6 +482,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { _identifyCoins, ( anonymitySetCoins: anonymitySet["coins"] as List, + groupId: latestSparkCoinId, spentCoinTags: spentCoinTags, privateKeyHexSet: privateKeyHexSet, walletId: walletId, @@ -565,6 +580,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { _identifyCoins, ( anonymitySetCoins: anonymitySet["coins"] as List, + groupId: anonymitySet["coinGroupID"] as int, spentCoinTags: spentCoinTags, privateKeyHexSet: privateKeyHexSet, walletId: walletId, @@ -863,6 +879,7 @@ String base64ToReverseHex(String source) => Future> _identifyCoins( ({ List anonymitySetCoins, + int groupId, Set spentCoinTags, Set privateKeyHexSet, String walletId, @@ -906,6 +923,7 @@ Future> _identifyCoins( walletId: args.walletId, type: coinType, isUsed: args.spentCoinTags.contains(coin.lTagHash!), + groupId: args.groupId, nonce: coin.nonceHex?.toUint8ListFromHex, address: coin.address!, txHash: txHash, From f61acd90b7dc9f8cae2939e4f53d3b2f31afbd2f Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 Dec 2023 17:46:48 -0600 Subject: [PATCH 54/77] hash used spark tags --- lib/electrumx_rpc/electrumx_client.dart | 9 +++- .../spark_coins/spark_coins_view.dart | 31 +++++++++++++ .../spark_interface.dart | 43 +++++++++++++++---- 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index bf432894b..21126c5d1 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -15,6 +15,8 @@ import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:decimal/decimal.dart'; import 'package:event_bus/event_bus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:mutex/mutex.dart'; import 'package:stackwallet/electrumx_rpc/rpc.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; @@ -922,7 +924,8 @@ class ElectrumXClient { requestTimeout: const Duration(minutes: 2), ); final map = Map.from(response["result"] as Map); - return Set.from(map["tags"] as List); + final set = Set.from(map["tags"] as List); + return await compute(_ffiHashTagsComputeWrapper, set); } catch (e) { Logging.instance.log(e, level: LogLevel.Error); rethrow; @@ -1036,3 +1039,7 @@ class ElectrumXClient { } } } + +Set _ffiHashTagsComputeWrapper(Set base64Tags) { + return LibSpark.hashTags(base64Tags: base64Tags); +} diff --git a/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart b/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart index 73ef56912..5bc9bbb32 100644 --- a/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart +++ b/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart @@ -131,6 +131,14 @@ class _SparkCoinsViewState extends ConsumerState { textAlign: TextAlign.left, ), ), + Expanded( + flex: 9, + child: Text( + "LTag Hash", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ), Expanded( flex: 3, child: Text( @@ -147,6 +155,14 @@ class _SparkCoinsViewState extends ConsumerState { textAlign: TextAlign.right, ), ), + Expanded( + flex: 2, + child: Text( + "Group Id", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.right, + ), + ), Expanded( flex: 2, child: Text( @@ -190,6 +206,13 @@ class _SparkCoinsViewState extends ConsumerState { style: STextStyles.itemSubtitle12(context), ), ), + Expanded( + flex: 9, + child: SelectableText( + _coins[index].lTagHash, + style: STextStyles.itemSubtitle12(context), + ), + ), Expanded( flex: 3, child: SelectableText( @@ -206,6 +229,14 @@ class _SparkCoinsViewState extends ConsumerState { textAlign: TextAlign.right, ), ), + Expanded( + flex: 2, + child: SelectableText( + _coins[index].groupId.toString(), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ), Expanded( flex: 2, child: SelectableText( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index d5268e1bd..863250e9c 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -456,21 +456,31 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final blockHash = await _getCachedSparkBlockHash(); - final anonymitySet = blockHash == null - ? await electrumXCachedClient.getSparkAnonymitySet( + final anonymitySetFuture = blockHash == null + ? electrumXCachedClient.getSparkAnonymitySet( groupId: latestSparkCoinId.toString(), coin: info.coin, ) - : await electrumXClient.getSparkAnonymitySet( + : electrumXClient.getSparkAnonymitySet( coinGroupId: latestSparkCoinId.toString(), startBlockHash: blockHash, ); + final spentCoinTagsFuture = + electrumXClient.getSparkUsedCoinsTags(startNumber: 0); + // electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin); + + final futureResults = await Future.wait([ + anonymitySetFuture, + spentCoinTagsFuture, + ]); + + final anonymitySet = futureResults[0] as Map; + final spentCoinTags = futureResults[1] as Set; + + final List myCoins = []; if (anonymitySet["coins"] is List && (anonymitySet["coins"] as List).isNotEmpty) { - final spentCoinTags = - await electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin); - final root = await getRootHDNode(); final privateKeyHexSet = paths .map( @@ -478,7 +488,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ) .toSet(); - final myCoins = await compute( + final identifiedCoins = await compute( _identifyCoins, ( anonymitySetCoins: anonymitySet["coins"] as List, @@ -490,8 +500,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ), ); - // update wallet spark coins in isar - await _addOrUpdateSparkCoins(myCoins); + myCoins.addAll(identifiedCoins); // update blockHash in cache final String newBlockHash = @@ -499,6 +508,22 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { await _setCachedSparkBlockHash(newBlockHash); } + // check current coins + final currentCoins = await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .findAll(); + for (final coin in currentCoins) { + if (spentCoinTags.contains(coin.lTagHash)) { + myCoins.add(coin.copyWith(isUsed: true)); + } + } + + // update wallet spark coins in isar + await _addOrUpdateSparkCoins(myCoins); + // refresh spark balance await refreshSparkBalance(); } catch (e, s) { From d1321162820177a7ff023cf17965e8f79cf72049 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 20 Dec 2023 18:00:02 -0600 Subject: [PATCH 55/77] WIP spark spend progress --- .../send_view/confirm_transaction_view.dart | 13 ++++++-- lib/wallets/models/tx_data.dart | 7 +++++ .../spark_interface.dart | 31 +++++++------------ 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 9bcaf1b01..173722fa2 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -625,7 +625,9 @@ class _ConfirmTransactionViewState ), Builder( builder: (context) { - final amount = widget.txData.amount!; + // TODO: [prio=high] spark transaction specifics - better handling + final amount = widget.txData.amount ?? + widget.txData.amountSpark!; final externalCalls = ref.watch( prefsChangeNotifierProvider.select( (value) => value.externalCalls)); @@ -723,9 +725,12 @@ class _ConfirmTransactionViewState height: 2, ), SelectableText( + // TODO: [prio=high] spark transaction specifics - better handling widget.isPaynymTransaction ? widget.txData.paynymAccountLite!.nymName - : widget.txData.recipients!.first.address, + : widget.txData.recipients?.first.address ?? + widget.txData.sparkRecipients!.first + .address, style: STextStyles.desktopTextExtraExtraSmall( context) .copyWith( @@ -1072,7 +1077,9 @@ class _ConfirmTransactionViewState Builder(builder: (context) { final fee = widget.txData.fee!; - final amount = widget.txData.amount!; + // TODO: [prio=high] spark transaction specifics - better handling + final amount = + widget.txData.amount ?? widget.txData.amountSpark!; return SelectableText( ref .watch(pAmountFormatter(coin)) diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 0cb1bcf49..f1b8bfa54 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -103,6 +103,13 @@ class TxData { .reduce((total, amount) => total += amount) : null; + Amount? get amountSpark => + sparkRecipients != null && sparkRecipients!.isNotEmpty + ? sparkRecipients! + .map((e) => e.amount) + .reduce((total, amount) => total += amount) + : null; + int? get estimatedSatsPerVByte => fee != null && vSize != null ? (fee!.raw ~/ BigInt.from(vSize!)).toInt() : null; diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 863250e9c..382c53a7b 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:bitcoindart/bitcoindart.dart' as btc; -import 'package:bitcoindart/src/utils/script.dart' as bscript; import 'package:flutter/foundation.dart'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; @@ -118,6 +117,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { Future prepareSendSpark({ required TxData txData, }) async { + // fetch spendable spark coins final coins = await mainDB.isar.sparkCoins .where() .walletIdEqualToAnyLTagHash(walletId) @@ -127,6 +127,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .heightIsNotNull() .findAll(); + // prepare coin data for ffi final serializedCoins = coins .map((e) => ( serializedCoin: e.serializedCoinB64!, @@ -366,18 +367,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // inputs - final opReturnScript = bscript.compile([ - 0xd3, // OP_SPARKSPEND - Uint8List(0), - ]); - - txb.addInput( - '0000000000000000000000000000000000000000000000000000000000000000', - 0xffffffff, - 0xffffffff, - opReturnScript, - ); - // final sig = extractedTx.getId(); // for (final coin in estimated.coins) { @@ -404,18 +393,20 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .toList(), ); - print("SPARK SPEND ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); - print("fee: ${spend.fee}"); - print("spend: ${spend.serializedSpendPayload}"); - print("scripts:"); - spend.outputScripts.forEach(print); - print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); - for (final outputScript in spend.outputScripts) { txb.addOutput(outputScript, 0); } final extractedTx = txb.buildIncomplete(); + + extractedTx.addInput( + '0000000000000000000000000000000000000000000000000000000000000000' + .toUint8ListFromHex, + 0xffffffff, + 0xffffffff, + "d3".toUint8ListFromHex, // OP_SPARKSPEND + ); + extractedTx.setPayload(spend.serializedSpendPayload); final rawTxHex = extractedTx.toHex(); From 1d3b07490db92fce869b24c7563709fdadc17942 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 21 Dec 2023 10:23:55 -0600 Subject: [PATCH 56/77] successful spark to spark send --- .../send_view/confirm_transaction_view.dart | 18 +- .../spark_interface.dart | 214 ++++-------------- 2 files changed, 52 insertions(+), 180 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 173722fa2..0b58c1fea 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -140,10 +140,20 @@ class _ConfirmTransactionViewState } else if (widget.isPaynymTransaction) { txDataFuture = wallet.confirmSend(txData: widget.txData); } else { - if (wallet is FiroWallet && - ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - txDataFuture = wallet.confirmSendLelantus(txData: widget.txData); + if (wallet is FiroWallet) { + switch (ref.read(publicPrivateBalanceStateProvider.state).state) { + case FiroType.public: + txDataFuture = wallet.confirmSend(txData: widget.txData); + break; + + case FiroType.lelantus: + txDataFuture = wallet.confirmSendLelantus(txData: widget.txData); + break; + + case FiroType.spark: + txDataFuture = wallet.confirmSendSpark(txData: widget.txData); + break; + } } else { if (coin == Coin.epicCash) { txDataFuture = wallet.confirmSend( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 382c53a7b..47eb71716 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -168,10 +168,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { )) .toList(); - // https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o/edit - // To generate a spark spend we need to call createSparkSpendTransaction, - // first unlock the wallet and generate all 3 spark keys, - final root = await getRootHDNode(); final String derivationPath; if (cryptoCurrency.network == CryptoCurrencyNetwork.test) { @@ -180,58 +176,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex"; } final privateKey = root.derivePath(derivationPath).privateKey.data; - // - // recipients is a list of pairs of amounts and bools, this is for transparent - // outputs, first how much to send and second, subtractFeeFromAmount argument - // for each receiver. - // - // privateRecipients is again the list of pairs, first the receiver data - // which has following members, Address which is any spark address, - // amount (v) how much we want to send, and memo which can be any string - // with 32 length (any string we want to send to receiver), and the second - // subtractFeeFromAmount, - // - // coins is the list of all our available spark coins - // - // cover_set_data_all is the list of all anonymity sets, - // - // idAndBlockHashes_all is the list of block hashes for each anonymity set - // - // txHashSig is the transaction hash only without spark data, tx version, - // type, transparent outputs and everything else should be set before generating it. - // - // fee is a output data - // - // serializedSpend is a output data, byte array with spark spend, we need - // to put it into vExtraPayload (this naming can be different in your codebase) - // - // outputScripts is a output data, it is a list of scripts, which we need - // to put in separate tx outputs, and keep the order, - - // Amount vOut = Amount( - // rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits); - // Amount mintVOut = Amount( - // rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits); - // int recipientsToSubtractFee = 0; - // - // for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { - // vOut += txData.recipients![i].amount; - // } - // - // if (vOut.raw > BigInt.from(SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION)) { - // throw Exception( - // "Spend to transparent address limit exceeded (10,000 Firo per transaction).", - // ); - // } - // - // for (int i = 0; i < (txData.sparkRecipients?.length ?? 0); i++) { - // mintVOut += txData.sparkRecipients![i].amount; - // if (txData.sparkRecipients![i].subtractFeeFromAmount) { - // recipientsToSubtractFee++; - // } - // } - // - // int fee; final txb = btc.TransactionBuilder( network: btc.NetworkType( @@ -249,67 +193,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { txb.setLockTime(await chainHeight); txb.setVersion(3 | (9 << 16)); - // final estimated = LibSpark.selectSparkCoins( - // requiredAmount: mintVOut.raw.toInt(), - // subtractFeeFromAmount: recipientsToSubtractFee > 0, - // coins: myCoins, - // privateRecipientsCount: txData.sparkRecipients?.length ?? 0, - // ); - // - // fee = estimated.fee; - // bool remainderSubtracted = false; - - // for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { - // - // - // if (recipient.fSubtractFeeFromAmount) { - // // Subtract fee equally from each selected recipient. - // recipient.nAmount -= fee / recipientsToSubtractFee; - // - // if (!remainderSubtracted) { - // // First receiver pays the remainder not divisible by output count. - // recipient.nAmount -= fee % recipientsToSubtractFee; - // remainderSubtracted = true; - // } - // } - // } - - // outputs - - // for (int i = 0; i < (txData.sparkRecipients?.length ?? 0); i++) { - // if (txData.sparkRecipients![i].subtractFeeFromAmount) { - // BigInt amount = txData.sparkRecipients![i].amount.raw; - // - // // Subtract fee equally from each selected recipient. - // amount -= BigInt.from(fee / recipientsToSubtractFee); - // - // if (!remainderSubtracted) { - // // First receiver pays the remainder not divisible by output count. - // amount -= BigInt.from(fee % recipientsToSubtractFee); - // remainderSubtracted = true; - // } - // - // txData.sparkRecipients![i] = ( - // address: txData.sparkRecipients![i].address, - // amount: Amount( - // rawValue: amount, - // fractionDigits: cryptoCurrency.fractionDigits, - // ), - // subtractFeeFromAmount: - // txData.sparkRecipients![i].subtractFeeFromAmount, - // memo: txData.sparkRecipients![i].memo, - // ); - // } - // } - // - // int spendInCurrentTx = 0; - // for (final spendCoin in estimated.coins) { - // spendInCurrentTx += spendCoin.value?.toInt() ?? 0; - // } - // spendInCurrentTx -= fee; - // - // int transparentOut = 0; - for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { if (txData.recipients![i].amount.raw == BigInt.zero) { continue; @@ -325,53 +208,15 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); } - // // spendInCurrentTx -= transparentOut; - // final List<({String address, int amount, String memo})> privOutputs = []; - // - // for (int i = 0; i < (txData.sparkRecipients?.length ?? 0); i++) { - // if (txData.sparkRecipients![i].amount.raw == BigInt.zero) { - // continue; - // } - // - // final recipientAmount = txData.sparkRecipients![i].amount.raw.toInt(); - // // spendInCurrentTx -= recipientAmount; - // - // privOutputs.add( - // ( - // address: txData.sparkRecipients![i].address, - // amount: recipientAmount, - // memo: txData.sparkRecipients![i].memo, - // ), - // ); - // } - - // if (spendInCurrentTx < 0) { - // throw Exception("Unable to create spend transaction."); - // } - // - // if (privOutputs.isEmpty || spendInCurrentTx > 0) { - // final changeAddress = await LibSpark.getAddress( - // privateKey: privateKey, - // index: index, - // diversifier: kSparkChange, - // ); - // - // privOutputs.add( - // ( - // address: changeAddress, - // amount: spendInCurrentTx > 0 ? spendInCurrentTx : 0, - // memo: "", - // ), - // ); - // } - - // inputs - - // final sig = extractedTx.getId(); - - // for (final coin in estimated.coins) { - // final groupId = coin.id!; - // } + final extractedTx = txb.buildIncomplete(); + extractedTx.addInput( + '0000000000000000000000000000000000000000000000000000000000000000' + .toUint8ListFromHex, + 0xffffffff, + 0xffffffff, + "d3".toUint8ListFromHex, // OP_SPARKSPEND + ); + extractedTx.setPayload(Uint8List(0)); final spend = LibSpark.createSparkSendTransaction( privateKeyHex: privateKey.toHex, @@ -391,22 +236,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { idAndBlockHashes: idAndBlockHashes .map((e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash))) .toList(), + txHash: extractedTx.getHash(), ); for (final outputScript in spend.outputScripts) { - txb.addOutput(outputScript, 0); + extractedTx.addOutput(outputScript, 0); } - final extractedTx = txb.buildIncomplete(); - - extractedTx.addInput( - '0000000000000000000000000000000000000000000000000000000000000000' - .toUint8ListFromHex, - 0xffffffff, - 0xffffffff, - "d3".toUint8ListFromHex, // OP_SPARKSPEND - ); - extractedTx.setPayload(spend.serializedSpendPayload); final rawTxHex = extractedTx.toHex(); @@ -425,7 +261,33 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { Future confirmSendSpark({ required TxData txData, }) async { - throw UnimplementedError(); + try { + Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); + + final txHash = await electrumXClient.broadcastTransaction( + rawTx: txData.raw!, + ); + Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + + txData = txData.copyWith( + // TODO mark spark coins as spent locally and update balance before waiting to check via electrumx? + + // usedUTXOs: + // txData.usedUTXOs!.map((e) => e.copyWith(used: true)).toList(), + + // TODO revisit setting these both + txHash: txHash, + txid: txHash, + ); + // mark utxos as used + await mainDB.putUTXOs(txData.usedUTXOs!); + + return txData; + } catch (e, s) { + Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } } // TODO lots of room for performance improvements here. Should be similar to From 1e1a472d42856084d233d0c26b1f63a6b6a78a9b Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 21 Dec 2023 10:24:22 -0600 Subject: [PATCH 57/77] fix transaction broadcast error text overflow on desktop --- lib/pages/send_view/confirm_transaction_view.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 0b58c1fea..2fa7332bc 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -243,9 +243,13 @@ class _ConfirmTransactionViewState const SizedBox( height: 24, ), - Text( - e.toString(), - style: STextStyles.smallMed14(context), + Flexible( + child: SingleChildScrollView( + child: SelectableText( + e.toString(), + style: STextStyles.smallMed14(context), + ), + ), ), const SizedBox( height: 56, From b441157398a87852f045344c9bcd488519b10f09 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 21 Dec 2023 14:41:29 -0600 Subject: [PATCH 58/77] handle send all spark properly --- .../cached_electrumx_client.dart | 6 +- lib/pages/send_view/send_view.dart | 2 - .../wallet_view/sub_widgets/desktop_send.dart | 2 - lib/wallets/models/tx_data.dart | 2 - .../spark_interface.dart | 137 +++++++++++++++--- 5 files changed, 120 insertions(+), 29 deletions(-) diff --git a/lib/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index 036517698..337539412 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -141,7 +141,11 @@ class CachedElectrumXClient { set["blockHash"] = newSet["blockHash"]; for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) { // TODO verify this is correct (or append?) - set["coins"].insert(0, newSet["coins"][i]); + if ((set["coins"] as List) + .where((e) => e[0] == newSet["coins"][i][0]) + .isEmpty) { + set["coins"].insert(0, newSet["coins"][i]); + } } // save set to db await box.put(groupId, set); diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 0e1a8d5e3..5dd8e0ae3 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -535,7 +535,6 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, memo: memoController.text, - subtractFeeFromAmount: false, ) ] : null, @@ -570,7 +569,6 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, memo: memoController.text, - subtractFeeFromAmount: false, ) ] : null, 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 770e60cdb..cd3f1ee6d 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 @@ -332,7 +332,6 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, memo: memoController.text, - subtractFeeFromAmount: false, ) ] : null, @@ -367,7 +366,6 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, memo: memoController.text, - subtractFeeFromAmount: false, ) ] : null, diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index f1b8bfa54..9602a4e11 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -60,7 +60,6 @@ class TxData { ({ String address, Amount amount, - bool subtractFeeFromAmount, String memo, })>? sparkRecipients; @@ -148,7 +147,6 @@ class TxData { ({ String address, Amount amount, - bool subtractFeeFromAmount, String memo, })>? sparkRecipients, diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 47eb71716..24ac235e2 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -125,8 +125,32 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .isUsedEqualTo(false) .and() .heightIsNotNull() + .and() + .not() + .valueIntStringEqualTo("0") .findAll(); + final available = info.cachedBalanceTertiary.spendable; + + final txAmount = (txData.recipients ?? []).map((e) => e.amount).fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e) + + (txData.sparkRecipients ?? []).map((e) => e.amount).fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e); + + if (txAmount > available) { + throw Exception("Insufficient Spark balance"); + } + + final bool isSendAll = available == txAmount; + // prepare coin data for ffi final serializedCoins = coins .map((e) => ( @@ -177,34 +201,89 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } final privateKey = root.derivePath(derivationPath).privateKey.data; - final txb = btc.TransactionBuilder( - network: btc.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: btc.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, + final btcDartNetwork = btc.NetworkType( + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: btc.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ); + final txb = btc.TransactionBuilder( + network: btcDartNetwork, ); txb.setLockTime(await chainHeight); txb.setVersion(3 | (9 << 16)); + final List<({String address, Amount amount})> recipientsWithFeeSubtracted = + []; + final List< + ({ + String address, + Amount amount, + String memo, + })> sparkRecipientsWithFeeSubtracted = []; + final outputCount = (txData.recipients + ?.where( + (e) => e.amount.raw > BigInt.zero, + ) + .length ?? + 0) + + (txData.sparkRecipients?.length ?? 0); + final BigInt estimatedFee; + if (isSendAll) { + final estFee = LibSpark.estimateSparkFee( + privateKeyHex: privateKey.toHex, + index: kDefaultSparkIndex, + sendAmount: txAmount.raw.toInt(), + subtractFeeFromAmount: true, + serializedCoins: serializedCoins, + privateRecipientsCount: (txData.sparkRecipients?.length ?? 0), + ); + estimatedFee = BigInt.from(estFee); + } else { + estimatedFee = BigInt.zero; + } + + for (int i = 0; i < (txData.sparkRecipients?.length ?? 0); i++) { + sparkRecipientsWithFeeSubtracted.add( + ( + address: txData.sparkRecipients![i].address, + amount: Amount( + rawValue: txData.sparkRecipients![i].amount.raw - + (estimatedFee ~/ BigInt.from(outputCount)), + fractionDigits: cryptoCurrency.fractionDigits, + ), + memo: txData.sparkRecipients![i].memo, + ), + ); + } + for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { if (txData.recipients![i].amount.raw == BigInt.zero) { continue; } - if (txData.recipients![i].amount < cryptoCurrency.dustLimit) { - throw Exception("Output below dust limit"); - } - // - // transparentOut += txData.recipients![i].amount.raw.toInt(); - txb.addOutput( + recipientsWithFeeSubtracted.add( + ( + address: txData.recipients![i].address, + amount: Amount( + rawValue: txData.recipients![i].amount.raw - + (estimatedFee ~/ BigInt.from(outputCount)), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ), + ); + + final scriptPubKey = btc.Address.addressToOutputScript( txData.recipients![i].address, - txData.recipients![i].amount.raw.toInt(), + btcDartNetwork, + ); + txb.addOutput( + scriptPubKey, + recipientsWithFeeSubtracted[i].amount.raw.toInt(), ); } @@ -221,12 +300,19 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final spend = LibSpark.createSparkSendTransaction( privateKeyHex: privateKey.toHex, index: kDefaultSparkIndex, - recipients: [], + recipients: txData.recipients + ?.map((e) => ( + address: e.address, + amount: e.amount.raw.toInt(), + subtractFeeFromAmount: isSendAll, + )) + .toList() ?? + [], privateRecipients: txData.sparkRecipients ?.map((e) => ( sparkAddress: e.address, amount: e.amount.raw.toInt(), - subtractFeeFromAmount: e.subtractFeeFromAmount, + subtractFeeFromAmount: isSendAll, memo: e.memo, )) .toList() ?? @@ -246,6 +332,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { extractedTx.setPayload(spend.serializedSpendPayload); final rawTxHex = extractedTx.toHex(); + if (isSendAll) { + txData = txData.copyWith( + recipients: recipientsWithFeeSubtracted, + sparkRecipients: sparkRecipientsWithFeeSubtracted, + ); + } + return txData.copyWith( raw: rawTxHex, vSize: extractedTx.virtualSize(), @@ -279,8 +372,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { txHash: txHash, txid: txHash, ); - // mark utxos as used - await mainDB.putUTXOs(txData.usedUTXOs!); + // // mark utxos as used + // await mainDB.putUTXOs(txData.usedUTXOs!); return txData; } catch (e, s) { From 94e69f193b98361847df21d5d1431efd6b39e213 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 21 Dec 2023 16:04:49 -0600 Subject: [PATCH 59/77] send all spark tweaks --- .../spark_interface.dart | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 24ac235e2..e38cf0f73 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -218,21 +218,21 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { txb.setLockTime(await chainHeight); txb.setVersion(3 | (9 << 16)); - final List<({String address, Amount amount})> recipientsWithFeeSubtracted = - []; - final List< + List<({String address, Amount amount})>? recipientsWithFeeSubtracted; + List< ({ String address, Amount amount, String memo, - })> sparkRecipientsWithFeeSubtracted = []; - final outputCount = (txData.recipients - ?.where( - (e) => e.amount.raw > BigInt.zero, - ) - .length ?? - 0) + - (txData.sparkRecipients?.length ?? 0); + })>? sparkRecipientsWithFeeSubtracted; + final recipientCount = (txData.recipients + ?.where( + (e) => e.amount.raw > BigInt.zero, + ) + .length ?? + 0); + final totalRecipientCount = + recipientCount + (txData.sparkRecipients?.length ?? 0); final BigInt estimatedFee; if (isSendAll) { final estFee = LibSpark.estimateSparkFee( @@ -248,13 +248,20 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { estimatedFee = BigInt.zero; } + if ((txData.sparkRecipients?.length ?? 0) > 0) { + sparkRecipientsWithFeeSubtracted = []; + } + if (recipientCount > 0) { + recipientsWithFeeSubtracted = []; + } + for (int i = 0; i < (txData.sparkRecipients?.length ?? 0); i++) { - sparkRecipientsWithFeeSubtracted.add( + sparkRecipientsWithFeeSubtracted!.add( ( address: txData.sparkRecipients![i].address, amount: Amount( rawValue: txData.sparkRecipients![i].amount.raw - - (estimatedFee ~/ BigInt.from(outputCount)), + (estimatedFee ~/ BigInt.from(totalRecipientCount)), fractionDigits: cryptoCurrency.fractionDigits, ), memo: txData.sparkRecipients![i].memo, @@ -266,12 +273,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { if (txData.recipients![i].amount.raw == BigInt.zero) { continue; } - recipientsWithFeeSubtracted.add( + recipientsWithFeeSubtracted!.add( ( address: txData.recipients![i].address, amount: Amount( rawValue: txData.recipients![i].amount.raw - - (estimatedFee ~/ BigInt.from(outputCount)), + (estimatedFee ~/ BigInt.from(totalRecipientCount)), fractionDigits: cryptoCurrency.fractionDigits, ), ), From c640d3e4cccd5daff480d60021b4c6fadb75f603 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 21 Dec 2023 16:18:12 -0600 Subject: [PATCH 60/77] run createSparkSend in isolate --- .../spark_interface.dart | 113 ++++++++++++++---- 1 file changed, 87 insertions(+), 26 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index e38cf0f73..20bba6013 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -304,32 +304,36 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); extractedTx.setPayload(Uint8List(0)); - final spend = LibSpark.createSparkSendTransaction( - privateKeyHex: privateKey.toHex, - index: kDefaultSparkIndex, - recipients: txData.recipients - ?.map((e) => ( - address: e.address, - amount: e.amount.raw.toInt(), - subtractFeeFromAmount: isSendAll, - )) - .toList() ?? - [], - privateRecipients: txData.sparkRecipients - ?.map((e) => ( - sparkAddress: e.address, - amount: e.amount.raw.toInt(), - subtractFeeFromAmount: isSendAll, - memo: e.memo, - )) - .toList() ?? - [], - serializedCoins: serializedCoins, - allAnonymitySets: allAnonymitySets, - idAndBlockHashes: idAndBlockHashes - .map((e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash))) - .toList(), - txHash: extractedTx.getHash(), + final spend = await compute( + _createSparkSend, + ( + privateKeyHex: privateKey.toHex, + index: kDefaultSparkIndex, + recipients: txData.recipients + ?.map((e) => ( + address: e.address, + amount: e.amount.raw.toInt(), + subtractFeeFromAmount: isSendAll, + )) + .toList() ?? + [], + privateRecipients: txData.sparkRecipients + ?.map((e) => ( + sparkAddress: e.address, + amount: e.amount.raw.toInt(), + subtractFeeFromAmount: isSendAll, + memo: e.memo, + )) + .toList() ?? + [], + serializedCoins: serializedCoins, + allAnonymitySets: allAnonymitySets, + idAndBlockHashes: idAndBlockHashes + .map( + (e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash))) + .toList(), + txHash: extractedTx.getHash(), + ), ); for (final outputScript in spend.outputScripts) { @@ -853,6 +857,63 @@ String base64ToReverseHex(String source) => .map((e) => e.toRadixString(16).padLeft(2, '0')) .join(); +/// Top level function which should be called wrapped in [compute] +Future< + ({ + Uint8List serializedSpendPayload, + List outputScripts, + int fee, + })> _createSparkSend( + ({ + String privateKeyHex, + int index, + List< + ({ + String address, + int amount, + bool subtractFeeFromAmount + })> recipients, + List< + ({ + String sparkAddress, + int amount, + bool subtractFeeFromAmount, + String memo + })> privateRecipients, + List< + ({ + String serializedCoin, + String serializedCoinContext, + int groupId, + int height, + })> serializedCoins, + List< + ({ + int setId, + String setHash, + List<({String serializedCoin, String txHash})> set + })> allAnonymitySets, + List< + ({ + int setId, + Uint8List blockHash, + })> idAndBlockHashes, + Uint8List txHash, + }) args) async { + final spend = LibSpark.createSparkSendTransaction( + privateKeyHex: args.privateKeyHex, + index: args.index, + recipients: args.recipients, + privateRecipients: args.privateRecipients, + serializedCoins: args.serializedCoins, + allAnonymitySets: args.allAnonymitySets, + idAndBlockHashes: args.idAndBlockHashes, + txHash: args.txHash, + ); + + return spend; +} + /// Top level function which should be called wrapped in [compute] Future> _identifyCoins( ({ From 73f213174dd3698e81c5852a5bcf40b696503702 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 22 Dec 2023 18:15:44 -0600 Subject: [PATCH 61/77] WIP spark mint all --- lib/pages/wallet_view/wallet_view.dart | 3 +- .../sub_widgets/desktop_wallet_features.dart | 3 +- .../lelantus_interface.dart | 2 +- .../spark_interface.dart | 529 +++++++++++++++++- 4 files changed, 509 insertions(+), 28 deletions(-) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 19afd86be..9a61dc61f 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -430,7 +430,8 @@ class _WalletViewState extends ConsumerState { } try { - await firoWallet.anonymizeAllPublicFunds(); + // await firoWallet.anonymizeAllLelantus(); + await firoWallet.anonymizeAllSpark(); shouldPop = true; if (mounted) { Navigator.of(context).popUntil( 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 b6dee78d9..ff04edd77 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 @@ -198,7 +198,8 @@ class _DesktopWalletFeaturesState extends ConsumerState { } try { - await firoWallet.anonymizeAllPublicFunds(); + // await firoWallet.anonymizeAllLelantus(); + await firoWallet.anonymizeAllSpark(); shouldPop = true; if (context.mounted) { Navigator.of(context, rootNavigator: true).pop(); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart index fed2c47e0..de3192195 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart @@ -1047,7 +1047,7 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface { return mints; } - Future anonymizeAllPublicFunds() async { + Future anonymizeAllLelantus() async { try { final mintResult = await _mintSelection(); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 20bba6013..226388f9b 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1,11 +1,13 @@ import 'dart:convert'; +import 'dart:math'; import 'package:bitcoindart/bitcoindart.dart' as btc; import 'package:flutter/foundation.dart'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/balance.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -17,6 +19,12 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_int const kDefaultSparkIndex = 1; +const MAX_STANDARD_TX_WEIGHT = 400000; + +const OP_SPARKMINT = 0xd1; +const OP_SPARKSMINT = 0xd2; +const OP_SPARKSPEND = 0xd3; + mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { static bool validateSparkAddress({ required String address, @@ -201,19 +209,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } final privateKey = root.derivePath(derivationPath).privateKey.data; - final btcDartNetwork = btc.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: btc.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ); final txb = btc.TransactionBuilder( - network: btcDartNetwork, + network: _bitcoinDartNetwork, ); txb.setLockTime(await chainHeight); txb.setVersion(3 | (9 << 16)); @@ -286,7 +283,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final scriptPubKey = btc.Address.addressToOutputScript( txData.recipients![i].address, - btcDartNetwork, + _bitcoinDartNetwork, ); txb.addOutput( scriptPubKey, @@ -586,6 +583,466 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } } + Future> createSparkMintTransactions({ + required List availableUtxos, + required List outputs, + required bool subtractFeeFromAmount, + required bool autoMintAll, + }) async { + // pre checks + if (outputs.isEmpty) { + throw Exception("Cannot mint without some recipients"); + } + BigInt valueToMint = + outputs.map((e) => e.value).reduce((value, element) => value + element); + + if (valueToMint <= BigInt.zero) { + throw Exception("Cannot mint amount=$valueToMint"); + } + final totalUtxosValue = _sum(availableUtxos); + if (valueToMint > totalUtxosValue) { + throw Exception("Insufficient balance to create spark mint(s)"); + } + + // organise utxos + Map> utxosByAddress = {}; + for (final utxo in availableUtxos) { + utxosByAddress[utxo.address!] ??= []; + utxosByAddress[utxo.address!]!.add(utxo); + } + final valueAndUTXOs = utxosByAddress.values.toList(); + + // setup some vars + int nChangePosInOut = -1; + int nChangePosRequest = nChangePosInOut; + List outputs_ = outputs.toList(); + final currentHeight = await chainHeight; + final random = Random.secure(); + final List results = []; + + valueAndUTXOs.shuffle(random); + + while (valueAndUTXOs.isNotEmpty) { + final lockTime = random.nextInt(10) == 0 + ? max(0, currentHeight - random.nextInt(100)) + : currentHeight; + const txVersion = 1; + final List vin = []; + final List<(dynamic, int)> vout = []; + + BigInt nFeeRet = BigInt.zero; + + final itr = valueAndUTXOs.first; + BigInt valueToMintInTx = _sum(itr); + + if (!autoMintAll) { + valueToMintInTx = _min(valueToMintInTx, valueToMint); + } + + BigInt nValueToSelect, mintedValue; + final List setCoins = []; + bool skipCoin = false; + + // Start with no fee and loop until there is enough fee + while (true) { + mintedValue = valueToMintInTx; + + if (subtractFeeFromAmount) { + nValueToSelect = mintedValue; + } else { + nValueToSelect = mintedValue + nFeeRet; + } + + // if not enough coins in this group then subtract fee from mint + if (nValueToSelect > _sum(itr) && !subtractFeeFromAmount) { + nValueToSelect = mintedValue; + mintedValue -= nFeeRet; + } + + // if (!MoneyRange(mintedValue) || mintedValue == 0) { + if (mintedValue == BigInt.zero) { + valueAndUTXOs.remove(itr); + skipCoin = true; + break; + } + + nChangePosInOut = nChangePosRequest; + vin.clear(); + vout.clear(); + setCoins.clear(); + final remainingOutputs = outputs_.toList(); + final List singleTxOutputs = []; + if (autoMintAll) { + singleTxOutputs.add( + MutableSparkRecipient( + (await getCurrentReceivingSparkAddress())!.value, + mintedValue, + "", + ), + ); + } else { + BigInt remainingMintValue = mintedValue; + while (remainingMintValue > BigInt.zero) { + final singleMintValue = + _min(remainingMintValue, remainingOutputs.first.value); + singleTxOutputs.add( + MutableSparkRecipient( + remainingOutputs.first.address, + singleMintValue, + remainingOutputs.first.memo, + ), + ); + + // subtract minted amount from remaining value + remainingMintValue -= singleMintValue; + remainingOutputs.first.value -= singleMintValue; + + if (remainingOutputs.first.value == BigInt.zero) { + remainingOutputs.remove(remainingOutputs.first); + } + } + } + + if (subtractFeeFromAmount) { + final BigInt singleFee = + nFeeRet ~/ BigInt.from(singleTxOutputs.length); + BigInt remainder = nFeeRet % BigInt.from(singleTxOutputs.length); + + for (int i = 0; i < singleTxOutputs.length; ++i) { + if (singleTxOutputs[i].value <= singleFee) { + singleTxOutputs.removeAt(i); + remainder += singleTxOutputs[i].value - singleFee; + --i; + } + singleTxOutputs[i].value -= singleFee; + if (remainder > BigInt.zero && + singleTxOutputs[i].value > + nFeeRet % BigInt.from(singleTxOutputs.length)) { + // first receiver pays the remainder not divisible by output count + singleTxOutputs[i].value -= remainder; + remainder = BigInt.zero; + } + } + } + + // Generate dummy mint coins to save time + final dummyRecipients = LibSpark.createSparkMintRecipients( + outputs: singleTxOutputs + .map((e) => ( + sparkAddress: e.address, + value: e.value.toInt(), + memo: "", + )) + .toList(), + serialContext: Uint8List(0), + generate: false, + ); + + final dummyTxb = btc.TransactionBuilder(network: _bitcoinDartNetwork); + dummyTxb.setVersion(txVersion); + dummyTxb.setLockTime(lockTime); + for (final recipient in dummyRecipients) { + if (recipient.amount < cryptoCurrency.dustLimit.raw.toInt()) { + throw Exception("Output amount too small"); + } + vout.add(( + recipient.scriptPubKey, + recipient.amount, + )); + } + + // Choose coins to use + BigInt nValueIn = BigInt.zero; + for (final utxo in itr) { + if (nValueToSelect > nValueIn) { + setCoins.add((await fetchBuildTxData([utxo])).first); + nValueIn += BigInt.from(utxo.value); + } + } + if (nValueIn < nValueToSelect) { + throw Exception("Insufficient funds"); + } + + // priority stuff??? + + BigInt nChange = nValueIn - nValueToSelect; + if (nChange > BigInt.zero) { + if (nChange < cryptoCurrency.dustLimit.raw) { + nChangePosInOut = -1; + nFeeRet += nChange; + } else { + if (nChangePosInOut == -1) { + nChangePosInOut = random.nextInt(vout.length + 1); + } else if (nChangePosInOut > vout.length) { + throw Exception("Change index out of range"); + } + + final changeAddress = await getCurrentChangeAddress(); + vout.insert( + nChangePosInOut, + (changeAddress!.value, nChange.toInt()), + ); + } + } + + // add outputs for dummy tx to check fees + for (final out in vout) { + dummyTxb.addOutput(out.$1, out.$2); + } + + // fill vin + for (final sd in setCoins) { + vin.add(sd); + + // add to dummy tx + dummyTxb.addInput( + sd.utxo.txid, + sd.utxo.vout, + 0xffffffff - + 1, // minus 1 is important. 0xffffffff on its own will burn funds + sd.output, + ); + } + + // sign dummy tx + for (var i = 0; i < setCoins.length; i++) { + dummyTxb.sign( + vin: i, + keyPair: setCoins[i].keyPair!, + witnessValue: setCoins[i].utxo.value, + redeemScript: setCoins[i].redeemScript, + ); + } + + final dummyTx = dummyTxb.build(); + final nBytes = dummyTx.virtualSize(); + + if (dummyTx.weight() > MAX_STANDARD_TX_WEIGHT) { + throw Exception("Transaction too large"); + } + + final nFeeNeeded = + BigInt.from(nBytes); // One day we'll do this properly + + if (nFeeRet >= nFeeNeeded) { + for (final usedCoin in setCoins) { + itr.removeWhere((e) => e == usedCoin.utxo); + } + if (itr.isEmpty) { + final preLength = valueAndUTXOs.length; + valueAndUTXOs.remove(itr); + assert(preLength - 1 == valueAndUTXOs.length); + } + + // Generate real mint coins + final serialContext = LibSpark.serializeMintContext( + inputs: setCoins + .map((e) => ( + e.utxo.txid, + e.utxo.vout, + )) + .toList(), + ); + final recipients = LibSpark.createSparkMintRecipients( + outputs: singleTxOutputs + .map( + (e) => ( + sparkAddress: e.address, + memo: e.memo, + value: e.value.toInt(), + ), + ) + .toList(), + serialContext: serialContext, + generate: true, + ); + + int i = 0; + for (final recipient in recipients) { + final out = (recipient.scriptPubKey, recipient.amount); + while (i < vout.length) { + if (vout[i].$1 is Uint8List && + (vout[i].$1 as Uint8List).isNotEmpty && + (vout[i].$1 as Uint8List)[0] == OP_SPARKMINT) { + vout[i] = out; + break; + } + ++i; + } + ++i; + } + + outputs_ = remainingOutputs; + + break; // Done, enough fee included. + } + + // Include more fee and try again. + nFeeRet = nFeeNeeded; + continue; + } + + if (skipCoin) { + continue; + } + + // sign + final txb = btc.TransactionBuilder(network: _bitcoinDartNetwork); + txb.setVersion(txVersion); + txb.setLockTime(lockTime); + for (final input in vin) { + txb.addInput( + input.utxo.txid, + input.utxo.vout, + 0xffffffff - + 1, // minus 1 is important. 0xffffffff on its own will burn funds + input.output, + ); + } + + for (final output in vout) { + txb.addOutput(output.$1, output.$2); + } + + try { + for (var i = 0; i < vin.length; i++) { + txb.sign( + vin: i, + keyPair: vin[i].keyPair!, + witnessValue: vin[i].utxo.value, + redeemScript: vin[i].redeemScript, + ); + } + } catch (e, s) { + Logging.instance.log( + "Caught exception while signing spark mint transaction: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + final builtTx = txb.build(); + final data = TxData( + // TODO: add fee output to recipients? + sparkRecipients: vout + .map( + (e) => ( + address: "lol", + memo: "", + amount: Amount( + rawValue: BigInt.from(e.$2), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ), + ) + .toList(), + vSize: builtTx.virtualSize(), + txid: builtTx.getId(), + raw: builtTx.toHex(), + fee: Amount( + rawValue: nFeeRet, + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + + results.add(data); + + if (nChangePosInOut >= 0) { + final vOut = vout[nChangePosInOut]; + assert(vOut.$1 is String); // check to make sure is change address + + final out = UTXO( + walletId: walletId, + txid: data.txid!, + vout: nChangePosInOut, + value: vOut.$2, + address: vOut.$1 as String, + name: "Spark mint change", + isBlocked: false, + blockedReason: null, + isCoinbase: false, + blockHash: null, + blockHeight: null, + blockTime: null, + ); + + bool added = false; + for (final utxos in valueAndUTXOs) { + if (utxos.first.address == out.address) { + utxos.add(out); + added = true; + } + } + + if (!added) { + valueAndUTXOs.add([out]); + } + } + + if (!autoMintAll) { + valueToMint -= mintedValue; + if (valueToMint == BigInt.zero) { + break; + } + } + } + + if (!autoMintAll && valueToMint > BigInt.zero) { + // TODO: Is this a valid error message? + throw Exception("Failed to mint expected amounts"); + } + + return results; + } + + Future anonymizeAllSpark() async { + const subtractFeeFromAmount = true; // must be true for mint all + final currentHeight = await chainHeight; + + // TODO: this is broken? + final spendableUtxos = await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .isBlockedEqualTo(false) + .and() + .valueGreaterThan(0) + .and() + .usedEqualTo(false) + .and() + .blockHeightIsNotNull() + .and() + .blockHeightLessThan( + currentHeight + cryptoCurrency.minConfirms, + include: true, + ) + .findAll(); + + if (spendableUtxos.isEmpty) { + throw Exception("No available UTXOs found to anonymize"); + } + + final results = await createSparkMintTransactions( + subtractFeeFromAmount: subtractFeeFromAmount, + autoMintAll: true, + availableUtxos: spendableUtxos, + outputs: [ + MutableSparkRecipient( + (await getCurrentReceivingSparkAddress())!.value, + spendableUtxos + .map((e) => BigInt.from(e.value)) + .fold(BigInt.zero, (p, e) => p + e), + "", + ), + ], + ); + + int i = 0; + for (final data in results) { + print("Results data $i=$data"); + i++; + } + } + /// Transparent to Spark (mint) transaction creation. /// /// See https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o @@ -671,17 +1128,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // Create a transaction builder and set locktime and version. final txb = btc.TransactionBuilder( - network: btc.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: btc.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ), + network: _bitcoinDartNetwork, ); txb.setLockTime(await chainHeight); txb.setVersion(1); @@ -849,6 +1296,18 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { }); } } + + btc.NetworkType get _bitcoinDartNetwork => btc.NetworkType( + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: btc.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, + ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ); } String base64ToReverseHex(String source) => @@ -985,3 +1444,23 @@ Future> _identifyCoins( return myCoins; } + +BigInt _min(BigInt a, BigInt b) { + if (a <= b) { + return a; + } else { + return b; + } +} + +BigInt _sum(List utxos) => utxos + .map((e) => BigInt.from(e.value)) + .fold(BigInt.zero, (previousValue, element) => previousValue + element); + +class MutableSparkRecipient { + String address; + BigInt value; + String memo; + + MutableSparkRecipient(this.address, this.value, this.memo); +} From 8cc72f3448e2f7ea3bc64962bd2bfa8855ac0bec Mon Sep 17 00:00:00 2001 From: julian Date: Sun, 24 Dec 2023 10:51:08 -0600 Subject: [PATCH 62/77] spark anonymize all --- .../lelantus_interface.dart | 2 +- .../spark_interface.dart | 100 ++++++++++-------- 2 files changed, 58 insertions(+), 44 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart index de3192195..a4dec9157 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart @@ -1056,7 +1056,7 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface { unawaited(refresh()); } catch (e, s) { Logging.instance.log( - "Exception caught in anonymizeAllPublicFunds(): $e\n$s", + "Exception caught in anonymizeAllLelantus(): $e\n$s", level: LogLevel.Warning, ); rethrow; diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 226388f9b..33fb946bf 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -616,6 +616,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { int nChangePosInOut = -1; int nChangePosRequest = nChangePosInOut; List outputs_ = outputs.toList(); + final feesObject = await fees; final currentHeight = await chainHeight; final random = Random.secure(); final List results = []; @@ -821,8 +822,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { throw Exception("Transaction too large"); } - final nFeeNeeded = - BigInt.from(nBytes); // One day we'll do this properly + final nFeeNeeded = BigInt.from( + estimateTxFee( + vSize: nBytes, + feeRatePerKB: feesObject.medium, + ), + ); // One day we'll do this properly if (nFeeRet >= nFeeNeeded) { for (final usedCoin in setCoins) { @@ -944,6 +949,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ), ); + if (nFeeRet.toInt() < data.vSize!) { + throw Exception("fee is less than vSize"); + } + results.add(data); if (nChangePosInOut >= 0) { @@ -995,51 +1004,56 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } Future anonymizeAllSpark() async { - const subtractFeeFromAmount = true; // must be true for mint all - final currentHeight = await chainHeight; + try { + const subtractFeeFromAmount = true; // must be true for mint all + final currentHeight = await chainHeight; - // TODO: this is broken? - final spendableUtxos = await mainDB.isar.utxos - .where() - .walletIdEqualTo(walletId) - .filter() - .isBlockedEqualTo(false) - .and() - .valueGreaterThan(0) - .and() - .usedEqualTo(false) - .and() - .blockHeightIsNotNull() - .and() - .blockHeightLessThan( - currentHeight + cryptoCurrency.minConfirms, - include: true, - ) - .findAll(); + final spendableUtxos = await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .isBlockedEqualTo(false) + .and() + .group((q) => q.usedEqualTo(false).or().usedIsNull()) + .and() + .valueGreaterThan(0) + .findAll(); - if (spendableUtxos.isEmpty) { - throw Exception("No available UTXOs found to anonymize"); - } - - final results = await createSparkMintTransactions( - subtractFeeFromAmount: subtractFeeFromAmount, - autoMintAll: true, - availableUtxos: spendableUtxos, - outputs: [ - MutableSparkRecipient( - (await getCurrentReceivingSparkAddress())!.value, - spendableUtxos - .map((e) => BigInt.from(e.value)) - .fold(BigInt.zero, (p, e) => p + e), - "", + spendableUtxos.removeWhere( + (e) => !e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, ), - ], - ); + ); - int i = 0; - for (final data in results) { - print("Results data $i=$data"); - i++; + if (spendableUtxos.isEmpty) { + throw Exception("No available UTXOs found to anonymize"); + } + + final results = await createSparkMintTransactions( + subtractFeeFromAmount: subtractFeeFromAmount, + autoMintAll: true, + availableUtxos: spendableUtxos, + outputs: [ + MutableSparkRecipient( + (await getCurrentReceivingSparkAddress())!.value, + spendableUtxos + .map((e) => BigInt.from(e.value)) + .fold(BigInt.zero, (p, e) => p + e), + "", + ), + ], + ); + + for (final data in results) { + await confirmSparkMintTransaction(txData: data); + } + } catch (e, s) { + Logging.instance.log( + "Exception caught in anonymizeAllSpark(): $e\n$s", + level: LogLevel.Warning, + ); + rethrow; } } From cb46c2fa3aa5dc7b2a7fc140757eed3acde3600a Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 27 Dec 2023 09:07:40 -0600 Subject: [PATCH 63/77] add named constructor that should have been done ages ago --- lib/utilities/amount/amount.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/utilities/amount/amount.dart b/lib/utilities/amount/amount.dart index 0014a4eab..2ce3fbd56 100644 --- a/lib/utilities/amount/amount.dart +++ b/lib/utilities/amount/amount.dart @@ -26,6 +26,10 @@ class Amount { fractionDigits: 0, ); + Amount.zeroWith({required this.fractionDigits}) + : assert(fractionDigits >= 0), + _value = BigInt.zero; + /// truncate decimal value to [fractionDigits] places Amount.fromDecimal(Decimal amount, {required this.fractionDigits}) : assert(fractionDigits >= 0), From 953acb493c518e308e41f1d2d865885de96275b9 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 27 Dec 2023 10:01:13 -0600 Subject: [PATCH 64/77] spark spend from transparent and various clean up --- lib/pages/receive_view/receive_view.dart | 390 ++++++++++++++---- .../send_view/confirm_transaction_view.dart | 170 ++++---- lib/pages/send_view/send_view.dart | 56 +-- lib/pages/wallet_view/wallet_view.dart | 9 +- .../wallet_view/sub_widgets/desktop_send.dart | 70 ++-- lib/wallets/models/tx_data.dart | 5 + .../spark_interface.dart | 374 ++++++++--------- 7 files changed, 673 insertions(+), 401 deletions(-) diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 83a55092e..56a341f5d 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -10,15 +10,18 @@ import 'dart:async'; +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:isar/isar.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.dart'; import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -30,7 +33,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; @@ -58,6 +63,11 @@ class _ReceiveViewState extends ConsumerState { late final Coin coin; late final String walletId; late final ClipboardInterface clipboard; + late final bool supportsSpark; + + String? _sparkAddress; + String? _qrcodeContent; + bool _showSparkAddress = true; Future generateNewAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); @@ -96,23 +106,106 @@ class _ReceiveViewState extends ConsumerState { } } + Future generateNewSparkAddress() async { + final wallet = ref.read(pWallets).getWallet(walletId); + if (wallet is SparkInterface) { + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (_) { + return WillPopScope( + onWillPop: () async => shouldPop, + child: Container( + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.5), + child: const CustomLoadingOverlay( + message: "Generating address", + eventBus: null, + ), + ), + ); + }, + ), + ); + + final address = await wallet.generateNextSparkAddress(); + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.addresses.put(address); + }); + + shouldPop = true; + + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); + if (_sparkAddress != address.value) { + setState(() { + _sparkAddress = address.value; + }); + } + } + } + } + + StreamSubscription? _streamSub; + @override void initState() { walletId = widget.walletId; coin = ref.read(pWalletCoin(walletId)); clipboard = widget.clipboard; + supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface; + + if (supportsSpark) { + _streamSub = ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.spark) + .sortByDerivationIndexDesc() + .findFirst() + .asStream() + .listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _sparkAddress = event?.value; + }); + } + }); + }); + } super.initState(); } + @override + void dispose() { + _streamSub?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final receivingAddress = ref.watch(pWalletReceivingAddress(walletId)); - final ticker = widget.tokenContract?.symbol ?? coin.ticker; + if (supportsSpark) { + if (_showSparkAddress) { + _qrcodeContent = _sparkAddress; + } else { + _qrcodeContent = ref.watch(pWalletReceivingAddress(walletId)); + } + } else { + _qrcodeContent = ref.watch(pWalletReceivingAddress(walletId)); + } + return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -225,86 +318,239 @@ class _ReceiveViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - GestureDetector( - onTap: () { - HapticFeedback.lightImpact(); - clipboard.setData( - ClipboardData(text: receivingAddress), - ); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: RoundedWhiteContainer( - child: Column( - children: [ - Row( - children: [ - Text( - "Your $ticker address", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 10, - height: 10, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ], - ), - const SizedBox( - height: 4, - ), - Row( - children: [ - Expanded( + ConditionalParent( + condition: supportsSpark, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DropdownButtonHideUnderline( + child: DropdownButton2( + value: _showSparkAddress, + items: [ + DropdownMenuItem( + value: true, child: Text( - receivingAddress, - style: STextStyles.itemSubtitle12(context), + "Spark address", + style: STextStyles.desktopTextMedium(context), + ), + ), + DropdownMenuItem( + value: false, + child: Text( + "Transparent address", + style: STextStyles.desktopTextMedium(context), ), ), ], + onChanged: (value) { + if (value is bool && value != _showSparkAddress) { + setState(() { + _showSparkAddress = 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, + ), + ), + ), + 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, + ), + ), ), - ], + ), + const SizedBox( + height: 12, + ), + if (_showSparkAddress) + GestureDetector( + onTap: () { + clipboard.setData( + ClipboardData(text: _sparkAddress ?? "Error"), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context) + .extension()! + .backgroundAppBar, + width: 1, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: RoundedWhiteContainer( + child: Column( + children: [ + Row( + children: [ + Text( + "Your ${coin.ticker} SPARK address", + style: + STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 15, + height: 15, + color: Theme.of(context) + .extension()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + Expanded( + child: Text( + _sparkAddress ?? "Error", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + if (!_showSparkAddress) child, + ], + ), + child: GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + clipboard.setData( + ClipboardData( + text: + ref.watch(pWalletReceivingAddress(walletId))), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: RoundedWhiteContainer( + child: Column( + children: [ + Row( + children: [ + Text( + "Your $ticker address", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 10, + height: 10, + color: Theme.of(context) + .extension()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ], + ), + const SizedBox( + height: 4, + ), + Row( + children: [ + Expanded( + child: Text( + ref.watch( + pWalletReceivingAddress(walletId)), + style: STextStyles.itemSubtitle12(context), + ), + ), + ], + ), + ], + ), ), ), ), - if (coin != Coin.epicCash && - coin != Coin.ethereum && - coin != Coin.banano && - coin != Coin.nano && - coin != Coin.stellar && - coin != Coin.stellarTestnet && - coin != Coin.tezos) + if (ref.watch(pWallets + .select((value) => value.getWallet(walletId))) + is MultiAddressInterface || + supportsSpark) const SizedBox( height: 12, ), - if (coin != Coin.epicCash && - coin != Coin.ethereum && - coin != Coin.banano && - coin != Coin.nano && - coin != Coin.stellar && - coin != Coin.stellarTestnet && - coin != Coin.tezos) + if (ref.watch(pWallets + .select((value) => value.getWallet(walletId))) + is MultiAddressInterface || + supportsSpark) TextButton( - onPressed: generateNewAddress, + onPressed: supportsSpark && _showSparkAddress + ? generateNewSparkAddress + : generateNewAddress, style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle(context), @@ -328,7 +574,7 @@ class _ReceiveViewState extends ConsumerState { QrImageView( data: AddressUtils.buildUriString( coin, - receivingAddress, + _qrcodeContent ?? "", {}, ), size: MediaQuery.of(context).size.width / 2, @@ -347,7 +593,7 @@ class _ReceiveViewState extends ConsumerState { RouteGenerator.useMaterialPageRoute, builder: (_) => GenerateUriQrCodeView( coin: coin, - receivingAddress: receivingAddress, + receivingAddress: _qrcodeContent ?? "", ), settings: const RouteSettings( name: GenerateUriQrCodeView.routeName, diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 2fa7332bc..1fe6078d9 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -120,7 +120,7 @@ class _ConfirmTransactionViewState ), ); - late String txid; + final List txids = []; Future txDataFuture; final note = noteController.text; @@ -143,7 +143,12 @@ class _ConfirmTransactionViewState if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.public: - txDataFuture = wallet.confirmSend(txData: widget.txData); + if (widget.txData.sparkMints == null) { + txDataFuture = wallet.confirmSend(txData: widget.txData); + } else { + txDataFuture = + wallet.confirmSparkMintTransactions(txData: widget.txData); + } break; case FiroType.lelantus: @@ -175,17 +180,24 @@ class _ConfirmTransactionViewState sendProgressController.triggerSuccess?.call(); await Future.delayed(const Duration(seconds: 5)); - txid = (results.first as TxData).txid!; + if (wallet is FiroWallet && + (results.first as TxData).sparkMints != null) { + txids.addAll((results.first as TxData).sparkMints!.map((e) => e.txid!)); + } else { + txids.add((results.first as TxData).txid!); + } ref.refresh(desktopUseUTXOs); // save note - await ref.read(mainDBProvider).putTransactionNote( - TransactionNote( - walletId: walletId, - txid: txid, - value: note, - ), - ); + for (final txid in txids) { + await ref.read(mainDBProvider).putTransactionNote( + TransactionNote( + walletId: walletId, + txid: txid, + value: note, + ), + ); + } if (widget.isTokenTx) { unawaited(ref.read(tokenServiceProvider)!.refresh()); @@ -333,6 +345,48 @@ class _ConfirmTransactionViewState } else { unit = coin.ticker; } + + final Amount? fee; + final Amount amount; + + final wallet = ref.watch(pWallets).getWallet(walletId); + + if (wallet is FiroWallet) { + switch (ref.read(publicPrivateBalanceStateProvider.state).state) { + case FiroType.public: + if (widget.txData.sparkMints != null) { + fee = widget.txData.sparkMints! + .map((e) => e.fee!) + .reduce((value, element) => value += element); + amount = widget.txData.sparkMints! + .map((e) => e.amountSpark!) + .reduce((value, element) => value += element); + } else { + fee = widget.txData.fee; + amount = widget.txData.amount!; + } + break; + + case FiroType.lelantus: + fee = widget.txData.fee; + amount = widget.txData.amount!; + break; + + case FiroType.spark: + fee = widget.txData.fee; + amount = (widget.txData.amount ?? + Amount.zeroWith( + fractionDigits: wallet.cryptoCurrency.fractionDigits)) + + (widget.txData.amountSpark ?? + Amount.zeroWith( + fractionDigits: wallet.cryptoCurrency.fractionDigits)); + break; + } + } else { + fee = widget.txData.fee; + amount = widget.txData.amount!; + } + return ConditionalParent( condition: !isDesktop, builder: (child) => Background( @@ -438,7 +492,8 @@ class _ConfirmTransactionViewState Text( widget.isPaynymTransaction ? widget.txData.paynymAccountLite!.nymName - : widget.txData.recipients!.first.address, + : widget.txData.recipients?.first.address ?? + widget.txData.sparkRecipients!.first.address, style: STextStyles.itemSubtitle12(context), ), ], @@ -457,7 +512,7 @@ class _ConfirmTransactionViewState ), SelectableText( ref.watch(pAmountFormatter(coin)).format( - widget.txData.amount!, + amount, ethContract: ref .watch(tokenServiceProvider) ?.tokenContract, @@ -482,9 +537,7 @@ class _ConfirmTransactionViewState style: STextStyles.smallMed12(context), ), SelectableText( - ref - .watch(pAmountFormatter(coin)) - .format(widget.txData.fee!), + ref.watch(pAmountFormatter(coin)).format(fee!), style: STextStyles.itemSubtitle12(context), textAlign: TextAlign.right, ), @@ -508,7 +561,7 @@ class _ConfirmTransactionViewState height: 4, ), SelectableText( - "~${widget.txData.fee!.raw.toInt() ~/ widget.txData.vSize!}", + "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle12(context), ), ], @@ -639,9 +692,6 @@ class _ConfirmTransactionViewState ), Builder( builder: (context) { - // TODO: [prio=high] spark transaction specifics - better handling - final amount = widget.txData.amount ?? - widget.txData.amountSpark!; final externalCalls = ref.watch( prefsChangeNotifierProvider.select( (value) => value.externalCalls)); @@ -778,24 +828,15 @@ class _ConfirmTransactionViewState const SizedBox( height: 2, ), - Builder( - builder: (context) { - final fee = widget.txData.fee!; - - return SelectableText( - ref - .watch(pAmountFormatter(coin)) - .format(fee), - style: - STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ); - }, + SelectableText( + ref.watch(pAmountFormatter(coin)).format(fee!), + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), ), ], ), @@ -1000,15 +1041,9 @@ class _ConfirmTransactionViewState color: Theme.of(context) .extension()! .textFieldDefaultBG, - child: Builder( - builder: (context) { - final fee = widget.txData.fee!; - - return SelectableText( - ref.watch(pAmountFormatter(coin)).format(fee), - style: STextStyles.itemSubtitle(context), - ); - }, + child: SelectableText( + ref.watch(pAmountFormatter(coin)).format(fee!), + style: STextStyles.itemSubtitle(context), ), ), ), @@ -1044,7 +1079,7 @@ class _ConfirmTransactionViewState .extension()! .textFieldDefaultBG, child: SelectableText( - "~${widget.txData.fee!.raw.toInt() ~/ widget.txData.vSize!}", + "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle(context), ), ), @@ -1088,31 +1123,22 @@ class _ConfirmTransactionViewState .textConfirmTotalAmount, ), ), - Builder(builder: (context) { - final fee = widget.txData.fee!; - - // TODO: [prio=high] spark transaction specifics - better handling - final amount = - widget.txData.amount ?? widget.txData.amountSpark!; - return SelectableText( - ref - .watch(pAmountFormatter(coin)) - .format(amount + fee), - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), - textAlign: TextAlign.right, - ); - }), + SelectableText( + ref.watch(pAmountFormatter(coin)).format(amount + fee!), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ), ], ), ), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 5dd8e0ae3..b9878e5cd 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -524,29 +524,39 @@ class _SendViewState extends ConsumerState { } else if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.public: - txDataFuture = wallet.prepareSend( - txData: TxData( - recipients: _isSparkAddress - ? null - : [(address: _address!, amount: amount)], - sparkRecipients: _isSparkAddress - ? [ - ( - address: _address!, - amount: amount, - memo: memoController.text, - ) - ] - : null, - feeRateType: ref.read(feeRateTypeStateProvider), - satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - selectedUTXOs.isNotEmpty) - ? selectedUTXOs - : null, - ), - ); + if (_isSparkAddress) { + txDataFuture = wallet.prepareSparkMintTransaction( + txData: TxData( + sparkRecipients: [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + ) + ], + feeRateType: ref.read(feeRateTypeStateProvider), + satsPerVByte: isCustomFee ? customFeeRate : null, + utxos: (wallet is CoinControlInterface && + coinControlEnabled && + selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, + ), + ); + } else { + txDataFuture = wallet.prepareSend( + txData: TxData( + recipients: [(address: _address!, amount: amount)], + feeRateType: ref.read(feeRateTypeStateProvider), + satsPerVByte: isCustomFee ? customFeeRate : null, + utxos: (wallet is CoinControlInterface && + coinControlEnabled && + selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, + ), + ); + } break; case FiroType.lelantus: diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 9a61dc61f..7a8e02844 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -68,6 +68,7 @@ import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -115,6 +116,8 @@ class _WalletViewState extends ConsumerState { late final String walletId; late final Coin coin; + late final bool isSparkWallet; + late final bool _shouldDisableAutoSyncOnLogOut; late WalletSyncStatus _currentSyncStatus; @@ -171,6 +174,8 @@ class _WalletViewState extends ConsumerState { _shouldDisableAutoSyncOnLogOut = false; } + isSparkWallet = wallet is SparkInterface; + if (coin == Coin.firo && (wallet as FiroWallet).lelantusCoinIsarRescanRequired) { _rescanningOnOpen = true; @@ -758,11 +763,11 @@ class _WalletViewState extends ConsumerState { ), ), ), - if (coin == Coin.firo) + if (isSparkWallet) const SizedBox( height: 10, ), - if (coin == Coin.firo) + if (isSparkWallet) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( 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 cd3f1ee6d..dd596037e 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 @@ -321,29 +321,39 @@ class _DesktopSendState extends ConsumerState { } else if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.public: - txDataFuture = wallet.prepareSend( - txData: TxData( - recipients: _isSparkAddress - ? null - : [(address: _address!, amount: amount)], - sparkRecipients: _isSparkAddress - ? [ - ( - address: _address!, - amount: amount, - memo: memoController.text, - ) - ] - : null, - feeRateType: ref.read(feeRateTypeStateProvider), - satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, - ), - ); + if (_isSparkAddress) { + txDataFuture = wallet.prepareSparkMintTransaction( + txData: TxData( + sparkRecipients: [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + ) + ], + feeRateType: ref.read(feeRateTypeStateProvider), + satsPerVByte: isCustomFee ? customFeeRate : null, + utxos: (wallet is CoinControlInterface && + coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, + ), + ); + } else { + txDataFuture = wallet.prepareSend( + txData: TxData( + recipients: [(address: _address!, amount: amount)], + feeRateType: ref.read(feeRateTypeStateProvider), + satsPerVByte: isCustomFee ? customFeeRate : null, + utxos: (wallet is CoinControlInterface && + coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, + ), + ); + } break; case FiroType.lelantus: @@ -579,7 +589,9 @@ class _DesktopSendState extends ConsumerState { ref.read(pWallets).getWallet(walletId).cryptoCurrency; final isValidAddress = walletCurrency.validateAddress(address ?? ""); - _isSparkAddress = isValidAddress + _isSparkAddress = isValidAddress && + ref.read(publicPrivateBalanceStateProvider.state).state != + FiroType.lelantus ? SparkInterface.validateSparkAddress( address: address!, isTestNet: walletCurrency.network == CryptoCurrencyNetwork.test, @@ -1409,11 +1421,17 @@ class _DesktopSendState extends ConsumerState { } }, ), - if (isStellar || _isSparkAddress) + if (isStellar || + (_isSparkAddress && + ref.watch(publicPrivateBalanceStateProvider) != + FiroType.public)) const SizedBox( height: 10, ), - if (isStellar || _isSparkAddress) + if (isStellar || + (_isSparkAddress && + ref.watch(publicPrivateBalanceStateProvider) != + FiroType.public)) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 9602a4e11..9148d182e 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -62,6 +62,7 @@ class TxData { Amount amount, String memo, })>? sparkRecipients; + final List? sparkMints; TxData({ this.feeRateType, @@ -94,6 +95,7 @@ class TxData { this.mintsMapLelantus, this.tezosOperationsList, this.sparkRecipients, + this.sparkMints, }); Amount? get amount => recipients != null && recipients!.isNotEmpty @@ -150,6 +152,7 @@ class TxData { String memo, })>? sparkRecipients, + List? sparkMints, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -183,6 +186,7 @@ class TxData { mintsMapLelantus: mintsMapLelantus ?? this.mintsMapLelantus, tezosOperationsList: tezosOperationsList ?? this.tezosOperationsList, sparkRecipients: sparkRecipients ?? this.sparkRecipients, + sparkMints: sparkMints ?? this.sparkMints, ); } @@ -218,5 +222,6 @@ class TxData { 'mintsMapLelantus: $mintsMapLelantus, ' 'tezosOperationsList: $tezosOperationsList, ' 'sparkRecipients: $sparkRecipients, ' + 'sparkMints: $sparkMints, ' '}'; } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 33fb946bf..d5900cbd9 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:bitcoindart/bitcoindart.dart' as btc; +import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; @@ -19,8 +20,12 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_int const kDefaultSparkIndex = 1; +// TODO dart style constants. Maybe move to spark lib? const MAX_STANDARD_TX_WEIGHT = 400000; +//https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16 +const SPARK_OUT_LIMIT_PER_TX = 16; + const OP_SPARKMINT = 0xd1; const OP_SPARKSMINT = 0xd2; const OP_SPARKSPEND = 0xd3; @@ -125,6 +130,47 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { Future prepareSendSpark({ required TxData txData, }) async { + // There should be at least one output. + if (!(txData.recipients?.isNotEmpty == true || + txData.sparkRecipients?.isNotEmpty == true)) { + throw Exception("No recipients provided."); + } + + if (txData.sparkRecipients?.isNotEmpty == true && + txData.sparkRecipients!.length >= SPARK_OUT_LIMIT_PER_TX - 1) { + throw Exception("Spark shielded output limit exceeded."); + } + + final transparentSumOut = + (txData.recipients ?? []).map((e) => e.amount).fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e); + + // See SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L17 + // and COIN https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L17 + // Note that as MAX_MONEY is greater than this limit, we can ignore it. See https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L31 + if (transparentSumOut > + Amount.fromDecimal( + Decimal.parse("10000"), + fractionDigits: cryptoCurrency.fractionDigits, + )) { + throw Exception( + "Spend to transparent address limit exceeded (10,000 Firo per transaction)."); + } + + final sparkSumOut = + (txData.sparkRecipients ?? []).map((e) => e.amount).fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e); + + final txAmount = transparentSumOut + sparkSumOut; + // fetch spendable spark coins final coins = await mainDB.isar.sparkCoins .where() @@ -140,19 +186,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final available = info.cachedBalanceTertiary.spendable; - final txAmount = (txData.recipients ?? []).map((e) => e.amount).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - (p, e) => p + e) + - (txData.sparkRecipients ?? []).map((e) => e.amount).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - (p, e) => p + e); - if (txAmount > available) { throw Exception("Insufficient Spark balance"); } @@ -583,7 +616,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } } - Future> createSparkMintTransactions({ + // modelled on CSparkWallet::CreateSparkMintTransactions https://github.com/firoorg/firo/blob/39c41e5e7ec634ced3700fe3f4f5509dc2e480d0/src/spark/sparkwallet.cpp#L752 + Future> _createSparkMintTransactions({ required List availableUtxos, required List outputs, required bool subtractFeeFromAmount, @@ -593,6 +627,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { if (outputs.isEmpty) { throw Exception("Cannot mint without some recipients"); } + + // TODO remove when multiple recipients gui is added. Will need to handle + // addresses when confirming the transactions later as well + assert(outputs.length == 1); + BigInt valueToMint = outputs.map((e) => e.value).reduce((value, element) => value + element); @@ -615,7 +654,9 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // setup some vars int nChangePosInOut = -1; int nChangePosRequest = nChangePosInOut; - List outputs_ = outputs.toList(); + List outputs_ = outputs + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); // deep copy final feesObject = await fees; final currentHeight = await chainHeight; final random = Random.secure(); @@ -671,8 +712,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { vin.clear(); vout.clear(); setCoins.clear(); - final remainingOutputs = outputs_.toList(); + + // deep copy + final remainingOutputs = outputs_ + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); final List singleTxOutputs = []; + if (autoMintAll) { singleTxOutputs.add( MutableSparkRecipient( @@ -682,7 +728,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ), ); } else { - BigInt remainingMintValue = mintedValue; + BigInt remainingMintValue = BigInt.parse(mintedValue.toString()); + while (remainingMintValue > BigInt.zero) { final singleMintValue = _min(remainingMintValue, remainingOutputs.first.value); @@ -877,7 +924,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ++i; } - outputs_ = remainingOutputs; + // deep copy + outputs_ = remainingOutputs + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); break; // Done, enough fee included. } @@ -926,12 +976,17 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { rethrow; } final builtTx = txb.build(); + + // TODO: see todo at top of this function + assert(outputs.length == 1); + final data = TxData( - // TODO: add fee output to recipients? sparkRecipients: vout + .where((e) => e.$1 is Uint8List) // ignore change .map( (e) => ( - address: "lol", + address: outputs.first + .address, // for display purposes on confirm tx screen. See todos above memo: "", amount: Amount( rawValue: BigInt.from(e.$2), @@ -947,6 +1002,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { rawValue: nFeeRet, fractionDigits: cryptoCurrency.fractionDigits, ), + usedUTXOs: vin.map((e) => e.utxo).toList(), ); if (nFeeRet.toInt() < data.vSize!) { @@ -1030,7 +1086,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { throw Exception("No available UTXOs found to anonymize"); } - final results = await createSparkMintTransactions( + final mints = await _createSparkMintTransactions( subtractFeeFromAmount: subtractFeeFromAmount, autoMintAll: true, availableUtxos: spendableUtxos, @@ -1045,9 +1101,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ], ); - for (final data in results) { - await confirmSparkMintTransaction(txData: data); - } + await confirmSparkMintTransactions(txData: TxData(sparkMints: mints)); } catch (e, s) { Logging.instance.log( "Exception caught in anonymizeAllSpark(): $e\n$s", @@ -1061,196 +1115,98 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { /// /// See https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o Future prepareSparkMintTransaction({required TxData txData}) async { - // "this kind of transaction is generated like a regular transaction, but in - // place of [regular] outputs we put spark outputs... we construct the input - // part of the transaction first then we generate spark related data [and] - // we sign like regular transactions at the end." - - // Validate inputs. - - // There should be at least one input. - if (txData.utxos == null || txData.utxos!.isEmpty) { - throw Exception("No inputs provided."); - } - - // Validate individual inputs. - for (final utxo in txData.utxos!) { - // Input amount must be greater than zero. - if (utxo.value == 0) { - throw Exception("Input value cannot be zero."); - } - - // Input value must be greater than dust limit. - if (BigInt.from(utxo.value) < cryptoCurrency.dustLimit.raw) { - throw Exception("Input value below dust limit."); - } - } - - // Validate outputs. - - // There should be at least one output. - if (txData.recipients == null || txData.recipients!.isEmpty) { - throw Exception("No recipients provided."); - } - - // For now let's limit to one output. - if (txData.recipients!.length > 1) { - throw Exception("Only one recipient supported."); - // TODO remove and test with multiple recipients. - } - - // Limit outputs per tx to 16. - // - // See SPARK_OUT_LIMIT_PER_TX at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16 - if (txData.recipients!.length > 16) { - throw Exception("Too many recipients."); - } - - // Limit spend value per tx to 1000000000000 satoshis. - // - // See SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L17 - // and COIN https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L17 - // Note that as MAX_MONEY is greater than this limit, we can ignore it. See https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L31 - // - // This will be added to and checked as we validate outputs. - Amount totalAmount = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // Validate individual outputs. - for (final recipient in txData.recipients!) { - // Output amount must be greater than zero. - if (recipient.amount.raw == BigInt.zero) { - throw Exception("Output amount cannot be zero."); - // Could refactor this for loop to use an index and remove this output. - } - - // Output amount must be greater than dust limit. - if (recipient.amount < cryptoCurrency.dustLimit) { - throw Exception("Output below dust limit."); - } - - // Do not add outputs that would exceed the spend limit. - totalAmount += recipient.amount; - if (totalAmount.raw > BigInt.from(1000000000000)) { - throw Exception( - "Spend limit exceeded (10,000 FIRO per tx).", - ); - } - } - - // Create a transaction builder and set locktime and version. - final txb = btc.TransactionBuilder( - network: _bitcoinDartNetwork, - ); - txb.setLockTime(await chainHeight); - txb.setVersion(1); - - final signingData = await fetchBuildTxData(txData.utxos!.toList()); - - // Create the serial context. - // - // "...serial_context is a byte array, which should be unique for each - // transaction, and for that we serialize and put all inputs into - // serial_context vector." - final serialContext = LibSpark.serializeMintContext( - inputs: signingData - .map((e) => ( - e.utxo.txid, - e.utxo.vout, - )) - .toList(), - ); - - // Add inputs. - for (final sd in signingData) { - txb.addInput( - sd.utxo.txid, - sd.utxo.vout, - 0xffffffff - - 1, // minus 1 is important. 0xffffffff on its own will burn funds - sd.output, - ); - } - - // Create mint recipients. - final mintRecipients = LibSpark.createSparkMintRecipients( - outputs: txData.recipients! - .map((e) => ( - sparkAddress: e.address, - value: e.amount.raw.toInt(), - memo: "", - )) - .toList(), - serialContext: Uint8List.fromList(serialContext), - generate: true, - ); - - // Add mint output(s). - for (final mint in mintRecipients) { - txb.addOutput( - mint.scriptPubKey, - mint.amount, - ); - } - try { - // Sign the transaction accordingly - for (var i = 0; i < signingData.length; i++) { - txb.sign( - vin: i, - keyPair: signingData[i].keyPair!, - witnessValue: signingData[i].utxo.value, - redeemScript: signingData[i].redeemScript, - ); + if (txData.sparkRecipients?.isNotEmpty != true) { + throw Exception("Missing spark recipients."); } + final recipients = txData.sparkRecipients! + .map( + (e) => MutableSparkRecipient( + e.address, + e.amount.raw, + e.memo, + ), + ) + .toList(); + + final total = recipients + .map((e) => e.value) + .reduce((value, element) => value += element); + + if (total < BigInt.zero) { + throw Exception("Attempted send of negative amount"); + } else if (total == BigInt.zero) { + throw Exception("Attempted send of zero amount"); + } + + final currentHeight = await chainHeight; + + // coin control not enabled for firo currently so we can ignore this + // final utxosToUse = txData.utxos?.toList() ?? await mainDB.isar.utxos + // .where() + // .walletIdEqualTo(walletId) + // .filter() + // .isBlockedEqualTo(false) + // .and() + // .group((q) => q.usedEqualTo(false).or().usedIsNull()) + // .and() + // .valueGreaterThan(0) + // .findAll(); + 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, + ), + ); + + if (spendableUtxos.isEmpty) { + throw Exception("No available UTXOs found to anonymize"); + } + + final available = spendableUtxos + .map((e) => BigInt.from(e.value)) + .reduce((value, element) => value += element); + + final bool subtractFeeFromAmount; + if (available < total) { + throw Exception("Insufficient balance"); + } else if (available == total) { + subtractFeeFromAmount = true; + } else { + subtractFeeFromAmount = false; + } + + final mints = await _createSparkMintTransactions( + subtractFeeFromAmount: subtractFeeFromAmount, + autoMintAll: false, + availableUtxos: spendableUtxos, + outputs: recipients, + ); + + return txData.copyWith(sparkMints: mints); } catch (e, s) { Logging.instance.log( - "Caught exception while signing spark mint transaction: $e\n$s", - level: LogLevel.Error, + "Exception caught in prepareSparkMintTransaction(): $e\n$s", + level: LogLevel.Warning, ); rethrow; } - - final builtTx = txb.build(); - - // TODO any changes to this txData object required? - return txData.copyWith( - // recipients: [ - // ( - // amount: Amount( - // rawValue: BigInt.from(incomplete.outs[0].value!), - // fractionDigits: cryptoCurrency.fractionDigits, - // ), - // address: "no address for lelantus mints", - // ) - // ], - vSize: builtTx.virtualSize(), - txid: builtTx.getId(), - raw: builtTx.toHex(), - ); } - /// Broadcast a tx and TODO update Spark balance. - Future confirmSparkMintTransaction({required TxData txData}) async { - // Broadcast tx. - final txid = await electrumXClient.broadcastTransaction( - rawTx: txData.raw!, - ); - - // Check txid. - if (txid == txData.txid!) { - print("SPARK TXIDS MATCH!!"); - } else { - print("SUBMITTED SPARK TXID DOES NOT MATCH WHAT WE GENERATED"); - } - - // TODO update spark balance. - - return txData.copyWith( - txid: txid, - ); + Future confirmSparkMintTransactions({required TxData txData}) async { + final futures = txData.sparkMints!.map((e) => confirmSend(txData: e)); + return txData.copyWith(sparkMints: await Future.wait(futures)); } @override @@ -1259,7 +1215,8 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // what ever class this mixin is used on uses LelantusInterface as well) final normalBalanceFuture = super.updateBalance(); - // todo: spark balance aka update info.tertiaryBalance + // todo: spark balance aka update info.tertiaryBalance here? + // currently happens on spark coins update/refresh // wait for normalBalanceFuture to complete before returning await normalBalanceFuture; @@ -1477,4 +1434,9 @@ class MutableSparkRecipient { String memo; MutableSparkRecipient(this.address, this.value, this.memo); + + @override + String toString() { + return 'MutableSparkRecipient{ address: $address, value: $value, memo: $memo }'; + } } From 0fc68a37021ce7311f2306e58b3668376eaa7206 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 27 Dec 2023 10:01:21 -0600 Subject: [PATCH 65/77] clean up --- .../global_settings_view/hidden_settings.dart | 490 +----------------- 1 file changed, 1 insertion(+), 489 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 96d41cd6e..6cc47a0d4 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -14,25 +14,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:isar/isar.dart'; import 'package:stackwallet/db/hive/db.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; -import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/wallets/models/tx_data.dart'; -import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; -import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/onetime_popups/tor_has_been_add_dialog.dart'; @@ -225,98 +215,7 @@ class HiddenSettings extends StatelessWidget { ), ); }), - // const SizedBox( - // height: 12, - // ), - // Consumer(builder: (_, ref, __) { - // return GestureDetector( - // onTap: () async { - // final x = - // await MajesticBankAPI.instance.getRates(); - // print(x); - // }, - // child: RoundedWhiteContainer( - // child: Text( - // "Click me", - // style: STextStyles.button(context).copyWith( - // color: Theme.of(context) - // .extension()! - // .accentColorDark), - // ), - // ), - // ); - // }), - // const SizedBox( - // height: 12, - // ), - // Consumer(builder: (_, ref, __) { - // return GestureDetector( - // onTap: () async { - // ref - // .read(priceAnd24hChangeNotifierProvider) - // .tokenContractAddressesToCheck - // .add( - // "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); - // ref - // .read(priceAnd24hChangeNotifierProvider) - // .tokenContractAddressesToCheck - // .add( - // "0xdAC17F958D2ee523a2206206994597C13D831ec7"); - // await ref - // .read(priceAnd24hChangeNotifierProvider) - // .updatePrice(); - // - // final x = ref - // .read(priceAnd24hChangeNotifierProvider) - // .getTokenPrice( - // "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); - // - // print( - // "PRICE 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48: $x"); - // }, - // child: RoundedWhiteContainer( - // child: Text( - // "Click me", - // style: STextStyles.button(context).copyWith( - // color: Theme.of(context) - // .extension()! - // .accentColorDark), - // ), - // ), - // ); - // }), - // const SizedBox( - // height: 12, - // ), - // Consumer(builder: (_, ref, __) { - // return GestureDetector( - // onTap: () async { - // // final erc20 = Erc20ContractInfo( - // // contractAddress: 'some con', - // // name: "loonamsn", - // // symbol: "DD", - // // decimals: 19, - // // ); - // // - // // final json = erc20.toJson(); - // // - // // print(json); - // // - // // final ee = EthContractInfo.fromJson(json); - // // - // // print(ee); - // }, - // child: RoundedWhiteContainer( - // child: Text( - // "Click me", - // style: STextStyles.button(context).copyWith( - // color: Theme.of(context) - // .extension()! - // .accentColorDark), - // ), - // ), - // ); - // }), + const SizedBox( height: 12, ), @@ -353,9 +252,6 @@ class HiddenSettings extends StatelessWidget { } }, ), - const SizedBox( - height: 12, - ), Consumer( builder: (_, ref, __) { return GestureDetector( @@ -374,390 +270,6 @@ class HiddenSettings extends StatelessWidget { ); }, ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - try { - final n = DefaultNodes.firoTestnet; - - final e = ElectrumXClient.from( - node: ElectrumXNode( - address: n.host, - port: n.port, - name: n.name, - id: n.id, - useSSL: n.useSSL, - ), - prefs: - ref.read(prefsChangeNotifierProvider), - failovers: [], - ); - - // Call and print getSparkAnonymitySet. - final anonymitySet = - await e.getSparkAnonymitySet( - coinGroupId: "1", - startBlockHash: "", - ); - - Util.printJson(anonymitySet, "anonymitySet"); - } catch (e, s) { - print("$e\n$s"); - } - }, - child: RoundedWhiteContainer( - child: Text( - "Spark getSparkAnonymitySet", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - try { - final n = DefaultNodes.firoTestnet; - - final e = ElectrumXClient.from( - node: ElectrumXNode( - address: n.host, - port: n.port, - name: n.name, - id: n.id, - useSSL: n.useSSL, - ), - prefs: - ref.read(prefsChangeNotifierProvider), - failovers: [], - ); - - // Call and print getUsedCoinsTags. - final usedCoinsTags = await e - .getSparkUsedCoinsTags(startNumber: 0); - - print( - "usedCoinsTags['tags'].length: ${usedCoinsTags.length}"); - Util.printJson( - usedCoinsTags, "usedCoinsTags"); - } catch (e, s) { - print("$e\n$s"); - } - }, - child: RoundedWhiteContainer( - child: Text( - "Spark getSparkUsedCoinsTags", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - try { - final n = DefaultNodes.firoTestnet; - - final e = ElectrumXClient.from( - node: ElectrumXNode( - address: n.host, - port: n.port, - name: n.name, - id: n.id, - useSSL: n.useSSL, - ), - prefs: - ref.read(prefsChangeNotifierProvider), - failovers: [], - ); - - // Call and print getSparkMintMetaData. - final mintMetaData = - await e.getSparkMintMetaData( - sparkCoinHashes: [ - "b476ed2b374bb081ea51d111f68f0136252521214e213d119b8dc67b92f5a390", - ], - ); - - Util.printJson(mintMetaData, "mintMetaData"); - } catch (e, s) { - print("$e\n$s"); - } - }, - child: RoundedWhiteContainer( - child: Text( - "Spark getSparkMintMetaData", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - try { - final n = DefaultNodes.firoTestnet; - - final e = ElectrumXClient.from( - node: ElectrumXNode( - address: n.host, - port: n.port, - name: n.name, - id: n.id, - useSSL: n.useSSL, - ), - prefs: - ref.read(prefsChangeNotifierProvider), - failovers: [], - ); - - // Call and print getSparkLatestCoinId. - final latestCoinId = - await e.getSparkLatestCoinId(); - - Util.printJson(latestCoinId, "latestCoinId"); - } catch (e, s) { - print("$e\n$s"); - } - }, - child: RoundedWhiteContainer( - child: Text( - "Spark getSparkLatestCoinId", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - try { - // Run refreshSparkData. - // - // Search wallets for a Firo testnet wallet. - for (final wallet - in ref.read(pWallets).wallets) { - if (!(wallet.info.coin == - Coin.firoTestNet)) { - continue; - } - // This is a Firo testnet wallet. - final walletId = wallet.info.walletId; - - // // Search for `circle chunk...` mnemonic. - // final potentialWallet = - // ref.read(pWallets).getWallet(walletId) - // as MnemonicInterface; - // final mnemonic = await potentialWallet - // .getMnemonicAsWords(); - // if (!(mnemonic[0] == "circle" && - // mnemonic[1] == "chunk)")) { - // // That ain't it. Skip this one. - // return; - // } - // Hardcode key in refreshSparkData instead. - - // Get a Spark interface. - final fusionWallet = ref - .read(pWallets) - .getWallet(walletId) as SparkInterface; - - // Refresh Spark data. - await fusionWallet.refreshSparkData(); - - // We only need to run this once. - break; - } - } catch (e, s) { - print("$e\n$s"); - } - }, - child: RoundedWhiteContainer( - child: Text( - "Refresh Spark wallet", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - try { - // Run prepareSparkMintTransaction. - for (final wallet - in ref.read(pWallets).wallets) { - // Prepare tx with a Firo testnet wallet. - if (!(wallet.info.coin == - Coin.firoTestNet)) { - continue; - } - final walletId = wallet.info.walletId; - - // Get a Spark interface. - final fusionWallet = ref - .read(pWallets) - .getWallet(walletId) as SparkInterface; - - // Make a dummy TxData. - TxData txData = TxData(); // TODO - - await fusionWallet - .prepareSparkMintTransaction( - txData: txData); - - // We only need to run this once. - break; - } - } catch (e, s) { - print("$e\n$s"); - } - }, - child: RoundedWhiteContainer( - child: Text( - "Prepare Spark mint transaction", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - const enableBurningMints = false; - - try { - if (enableBurningMints) { - final wallet = ref - .read(pWallets) - .wallets - .firstWhere((e) => - e.info.name == "circle chunk") - as FiroWallet; - - final utxos = await ref - .read(mainDBProvider) - .isar - .utxos - .where() - .walletIdEqualTo(wallet.walletId) - .findAll(); - - final Set utxosToUse = {}; - - for (final u in utxos) { - if (u.used != true && - u.value < 500000000 && - u.value > 9000000) { - utxosToUse.add(u); - break; - } - if (utxosToUse.length > 2) { - break; - } - } - - print("utxosToUse: $utxosToUse"); - - final inputData = TxData( - utxos: utxosToUse, - recipients: [ - ( - address: (await wallet - .getCurrentReceivingSparkAddress())! - .value, - amount: Amount( - rawValue: BigInt.from(utxosToUse - .map((e) => e.value) - .fold(0, (p, e) => p + e) - - 20000), - fractionDigits: 8, - ), - ), - ], - ); - - final mint = await wallet - .prepareSparkMintTransaction( - txData: inputData, - ); - - print("MINT: $mint"); - - print("Submitting..."); - final result = await wallet - .confirmSparkMintTransaction( - txData: mint); - print("Submitted result: $result"); - } - } catch (e, s) { - print("$e\n$s"); - } - }, - child: RoundedWhiteContainer( - child: Text( - "💣💣💣 DANGER 💣💣💣** Random Spark mint **💣💣💣 DANGER 💣💣💣 ", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }, - ), ], ), ), From 4074023a8814d7a789a1e60a06c912520ddb20dc Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 29 Dec 2023 09:24:25 -0600 Subject: [PATCH 66/77] remove sparkData from tx data before caching as we currently don't need it and its quite large --- lib/electrumx_rpc/cached_electrumx_client.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index 337539412..021bdf065 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -193,6 +193,7 @@ class CachedElectrumXClient { result.remove("hex"); result.remove("lelantusData"); + result.remove("sparkData"); if (result["confirmations"] != null && result["confirmations"] as int > minCacheConfirms) { From f697aeb043626864f516e5d18ada3a582d72169e Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 29 Dec 2023 09:26:32 -0600 Subject: [PATCH 67/77] WIP handle spark transaction parsing --- lib/wallets/wallet/impl/firo_wallet.dart | 130 +++++++++++++----- .../spark_interface.dart | 30 ++++ 2 files changed, 128 insertions(+), 32 deletions(-) diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 0047262e9..97f1b192a 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:decimal/decimal.dart'; +import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; @@ -9,6 +10,7 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; @@ -60,17 +62,20 @@ class FiroWallet extends Bip39HDWallet final List> allTxHashes = await fetchHistory(allAddressesSet); - final sparkTxids = await mainDB.isar.sparkCoins + final sparkCoins = await mainDB.isar.sparkCoins .where() .walletIdEqualToAnyLTagHash(walletId) - .txHashProperty() .findAll(); - for (final txid in sparkTxids) { + final Set sparkTxids = {}; + + for (final coin in sparkCoins) { + sparkTxids.add(coin.txHash); // check for duplicates before adding to list - if (allTxHashes.indexWhere((e) => e["tx_hash"] == txid) == -1) { + if (allTxHashes.indexWhere((e) => e["tx_hash"] == coin.txHash) == -1) { final info = { - "tx_hash": txid, + "tx_hash": coin.txHash, + "height": coin.height, }; allTxHashes.add(info); } @@ -148,6 +153,17 @@ class FiroWallet extends Bip39HDWallet bool isSparkMint = false; bool isMasterNodePayment = false; final bool isSparkSpend = txData["type"] == 9 && txData["version"] == 3; + final bool isMySpark = sparkTxids.contains(txData["txid"] as String); + + final sparkCoinsInvolved = + sparkCoins.where((e) => e.txHash == txData["txid"]); + if (isMySpark && sparkCoinsInvolved.isEmpty) { + Logging.instance.log( + "sparkCoinsInvolved is empty and should not be! (ignoring tx parsing)", + level: LogLevel.Error, + ); + continue; + } // parse outputs final List outputs = []; @@ -173,10 +189,12 @@ class FiroWallet extends Bip39HDWallet ); } } - if (outMap["scriptPubKey"]?["type"] == "sparkmint") { + if (outMap["scriptPubKey"]?["type"] == "sparkmint" || + outMap["scriptPubKey"]?["type"] == "sparksmint") { final asm = outMap["scriptPubKey"]?["asm"] as String?; if (asm != null) { - if (asm.startsWith("OP_SPARKMINT")) { + if (asm.startsWith("OP_SPARKMINT") || + asm.startsWith("OP_SPARKSMINT")) { isSparkMint = true; } else { Logging.instance.log( @@ -192,16 +210,6 @@ class FiroWallet extends Bip39HDWallet } } - if (isSparkSpend) { - // TODO - } else if (isSparkMint) { - // TODO - } else if (isMint || isJMint) { - // do nothing extra ? - } else { - // TODO - } - OutputV2 output = OutputV2.fromElectrumXJson( outMap, decimalPlaces: cryptoCurrency.fractionDigits, @@ -210,6 +218,46 @@ class FiroWallet extends Bip39HDWallet walletOwns: false, ); + // if (isSparkSpend) { + // // TODO? + // } else + if (isSparkMint) { + if (isMySpark) { + if (output.addresses.isEmpty && + output.scriptPubKeyHex.length >= 488) { + // likely spark related + final opByte = output.scriptPubKeyHex + .substring(0, 2) + .toUint8ListFromHex + .first; + if (opByte == OP_SPARKMINT || opByte == OP_SPARKSMINT) { + final serCoin = base64Encode(output.scriptPubKeyHex + .substring(2, 488) + .toUint8ListFromHex); + final coin = sparkCoinsInvolved + .where((e) => e.serializedCoinB64!.startsWith(serCoin)) + .firstOrNull; + + if (coin == null) { + // not ours + } else { + output = output.copyWith( + walletOwns: true, + valueStringSats: coin.value.toString(), + addresses: [ + coin.address, + ], + ); + } + } + } + } + } else if (isMint || isJMint) { + // do nothing extra ? + } else { + // TODO? + } + // if output was to my wallet, add value to amount received if (receivingAddresses .intersection(output.addresses.toSet()) @@ -223,6 +271,13 @@ class FiroWallet extends Bip39HDWallet wasReceivedInThisWallet = true; changeAmountReceivedInThisWallet += output.value; output = output.copyWith(walletOwns: true); + } else if (isSparkMint && isMySpark) { + wasReceivedInThisWallet = true; + if (output.addresses.contains(sparkChangeAddress)) { + changeAmountReceivedInThisWallet += output.value; + } else { + amountReceivedInThisWallet += output.value; + } } outputs.add(output); @@ -333,6 +388,32 @@ class FiroWallet extends Bip39HDWallet if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { wasSentFromThisWallet = true; input = input.copyWith(walletOwns: true); + } else if (isMySpark) { + final lTags = map["lTags"] as List?; + + if (lTags?.isNotEmpty == true) { + final List usedCoins = []; + for (final tag in lTags!) { + final components = (tag as String).split(","); + final x = components[0].substring(1); + final y = components[1].substring(0, components[1].length - 1); + + final hash = LibSpark.hashTag(x, y); + usedCoins.addAll(sparkCoins.where((e) => e.lTagHash == hash)); + } + + if (usedCoins.isNotEmpty) { + input = input.copyWith( + addresses: usedCoins.map((e) => e.address).toList(), + valueStringSats: usedCoins + .map((e) => e.value) + .reduce((value, element) => value += element) + .toString(), + walletOwns: true, + ); + wasSentFromThisWallet = true; + } + } } inputs.add(input); @@ -365,25 +446,10 @@ class FiroWallet extends Bip39HDWallet totalOut) { // definitely sent all to self type = TransactionType.sentToSelf; - } else if (isSparkMint) { - // probably sent to self - type = TransactionType.sentToSelf; } else if (amountReceivedInThisWallet == BigInt.zero) { // most likely just a typical send // do nothing here yet } - - // check vout 0 for special scripts - if (outputs.isNotEmpty) { - final output = outputs.first; - - // // check for fusion - // if (BchUtils.isFUZE(output.scriptPubKeyHex.toUint8ListFromHex)) { - // subType = TransactionSubType.cashFusion; - // } else { - // // check other cases here such as SLP or cash tokens etc - // } - } } } else if (wasReceivedInThisWallet) { // only found outputs owned by this wallet diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index d5900cbd9..c27b662ed 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -23,6 +23,8 @@ const kDefaultSparkIndex = 1; // TODO dart style constants. Maybe move to spark lib? const MAX_STANDARD_TX_WEIGHT = 400000; +const SPARK_CHANGE_D = 0x270F; + //https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16 const SPARK_OUT_LIMIT_PER_TX = 16; @@ -31,6 +33,16 @@ const OP_SPARKSMINT = 0xd2; const OP_SPARKSPEND = 0xd3; mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { + String? _sparkChangeAddressCached; + + /// Spark change address. Should generally not be exposed to end users. + String get sparkChangeAddress { + if (_sparkChangeAddressCached == null) { + throw Exception("_sparkChangeAddressCached was not initialized"); + } + return _sparkChangeAddressCached!; + } + static bool validateSparkAddress({ required String address, required bool isTestNet, @@ -45,6 +57,24 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { await mainDB.putAddress(address); } // TODO add other address types to wallet info? + if (_sparkChangeAddressCached == null) { + final root = await getRootHDNode(); + final String derivationPath; + if (cryptoCurrency.network == CryptoCurrencyNetwork.test) { + derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex"; + } else { + derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex"; + } + final keys = root.derivePath(derivationPath); + + _sparkChangeAddressCached = await LibSpark.getAddress( + privateKey: keys.privateKey.data, + index: kDefaultSparkIndex, + diversifier: SPARK_CHANGE_D, + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, + ); + } + // await info.updateReceivingAddress( // newAddress: address.value, // isar: mainDB.isar, From 202ca5941075f5407a36c712564d1acbb396b326 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 29 Dec 2023 10:30:37 -0600 Subject: [PATCH 68/77] tx status and icon fixes --- .../blockchain_data/v2/transaction_v2.dart | 82 +++++++++++++++++- .../wallet_view/sub_widgets/tx_icon.dart | 18 ++-- .../tx_v2/all_transactions_v2_view.dart | 44 +++------- .../tx_v2/transaction_v2_card.dart | 51 +++--------- .../tx_v2/transaction_v2_details_view.dart | 83 +++---------------- 5 files changed, 119 insertions(+), 159 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index 067069b50..9acb5f9ee 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -7,6 +7,8 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart' import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; part 'transaction_v2.g.dart'; @@ -75,7 +77,7 @@ class TransactionV2 { return Amount(rawValue: inSum - outSum, fractionDigits: coin.decimals); } - Amount getAmountReceivedThisWallet({required Coin coin}) { + Amount getAmountReceivedInThisWallet({required Coin coin}) { final outSum = outputs .where((e) => e.walletOwns) .fold(BigInt.zero, (p, e) => p + e.value); @@ -83,6 +85,15 @@ class TransactionV2 { return Amount(rawValue: outSum, fractionDigits: coin.decimals); } + Amount getAmountSparkSelfMinted({required Coin coin}) { + final outSum = outputs.where((e) { + final op = e.scriptPubKeyHex.substring(0, 2).toUint8ListFromHex.first; + return e.walletOwns && (op == OP_SPARKMINT); + }).fold(BigInt.zero, (p, e) => p + e.value); + + return Amount(rawValue: outSum, fractionDigits: coin.decimals); + } + Amount getAmountSentFromThisWallet({required Coin coin}) { final inSum = inputs .where((e) => e.walletOwns) @@ -92,7 +103,7 @@ class TransactionV2 { rawValue: inSum, fractionDigits: coin.decimals, ) - - getAmountReceivedThisWallet( + getAmountReceivedInThisWallet( coin: coin, ) - getFee(coin: coin); @@ -112,6 +123,73 @@ class TransactionV2 { } } + String statusLabel({ + required int currentChainHeight, + required int minConfirms, + }) { + if (subType == TransactionSubType.cashFusion || + subType == TransactionSubType.mint || + (subType == TransactionSubType.sparkMint && + type == TransactionType.sentToSelf)) { + if (isConfirmed(currentChainHeight, minConfirms)) { + return "Anonymized"; + } else { + return "Anonymizing"; + } + } + + // if (coin == Coin.epicCash) { + // if (_transaction.isCancelled) { + // return "Cancelled"; + // } else if (type == TransactionType.incoming) { + // if (isConfirmed(height, minConfirms)) { + // return "Received"; + // } else { + // if (_transaction.numberOfMessages == 1) { + // return "Receiving (waiting for sender)"; + // } else if ((_transaction.numberOfMessages ?? 0) > 1) { + // return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no) + // } else { + // return "Receiving"; + // } + // } + // } else if (type == TransactionType.outgoing) { + // if (isConfirmed(height, minConfirms)) { + // return "Sent (confirmed)"; + // } else { + // if (_transaction.numberOfMessages == 1) { + // return "Sending (waiting for receiver)"; + // } else if ((_transaction.numberOfMessages ?? 0) > 1) { + // return "Sending (waiting for confirmations)"; + // } else { + // return "Sending"; + // } + // } + // } + // } + + if (type == TransactionType.incoming) { + // if (_transaction.isMinting) { + // return "Minting"; + // } else + if (isConfirmed(currentChainHeight, minConfirms)) { + return "Received"; + } else { + return "Receiving"; + } + } else if (type == TransactionType.outgoing) { + if (isConfirmed(currentChainHeight, minConfirms)) { + return "Sent"; + } else { + return "Sending"; + } + } else if (type == TransactionType.sentToSelf) { + return "Sent to self"; + } else { + return type.name; + } + } + @override String toString() { return 'TransactionV2(\n' diff --git a/lib/pages/wallet_view/sub_widgets/tx_icon.dart b/lib/pages/wallet_view/sub_widgets/tx_icon.dart index c942bb621..11920f7c2 100644 --- a/lib/pages/wallet_view/sub_widgets/tx_icon.dart +++ b/lib/pages/wallet_view/sub_widgets/tx_icon.dart @@ -40,24 +40,16 @@ class TxIcon extends ConsumerWidget { bool isReceived, bool isPending, TransactionSubType subType, + TransactionType type, IThemeAssets assets, ) { if (subType == TransactionSubType.cashFusion) { return Assets.svg.txCashFusion; } - if (!isReceived && subType == TransactionSubType.mint) { - if (isCancelled) { - return Assets.svg.anonymizeFailed; - } - if (isPending) { - return Assets.svg.anonymizePending; - } - return Assets.svg.anonymize; - } - - if (subType == TransactionSubType.mint || - subType == TransactionSubType.sparkMint) { + if ((!isReceived && subType == TransactionSubType.mint) || + (subType == TransactionSubType.sparkMint && + type == TransactionType.sentToSelf)) { if (isCancelled) { return Assets.svg.anonymizeFailed; } @@ -102,6 +94,7 @@ class TxIcon extends ConsumerWidget { ref.watch(pWallets).getWallet(tx.walletId).cryptoCurrency.minConfirms, ), tx.subType, + tx.type, ref.watch(themeAssetsProvider), ); } else if (transaction is TransactionV2) { @@ -115,6 +108,7 @@ class TxIcon extends ConsumerWidget { ref.watch(pWallets).getWallet(tx.walletId).cryptoCurrency.minConfirms, ), tx.subType, + tx.type, ref.watch(themeAssetsProvider), ); } else { diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart index c1aa9c826..3953bfc7b 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart @@ -31,7 +31,6 @@ import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -843,34 +842,10 @@ class _DesktopTransactionCardRowState late final String walletId; late final int minConfirms; - String whatIsIt(TransactionType type, Coin coin, int height) { - if (_transaction.subType == TransactionSubType.mint || - _transaction.subType == TransactionSubType.cashFusion) { - if (_transaction.isConfirmed(height, minConfirms)) { - return "Anonymized"; - } else { - return "Anonymizing"; - } - } - - if (type == TransactionType.incoming) { - if (_transaction.isConfirmed(height, minConfirms)) { - return "Received"; - } else { - return "Receiving"; - } - } else if (type == TransactionType.outgoing) { - if (_transaction.isConfirmed(height, minConfirms)) { - return "Sent"; - } else { - return "Sending"; - } - } else if (type == TransactionType.sentToSelf) { - return "Sent to self"; - } else { - return type.name; - } - } + String whatIsIt(TransactionV2 tx, int height) => tx.statusLabel( + currentChainHeight: height, + minConfirms: minConfirms, + ); @override void initState() { @@ -917,7 +892,7 @@ class _DesktopTransactionCardRowState final Amount amount; if (_transaction.subType == TransactionSubType.cashFusion) { - amount = _transaction.getAmountReceivedThisWallet(coin: coin); + amount = _transaction.getAmountReceivedInThisWallet(coin: coin); } else { switch (_transaction.type) { case TransactionType.outgoing: @@ -926,7 +901,11 @@ class _DesktopTransactionCardRowState case TransactionType.incoming: case TransactionType.sentToSelf: - amount = _transaction.getAmountReceivedThisWallet(coin: coin); + if (_transaction.subType == TransactionSubType.sparkMint) { + amount = _transaction.getAmountSparkSelfMinted(coin: coin); + } else { + amount = _transaction.getAmountReceivedInThisWallet(coin: coin); + } break; case TransactionType.unknown: @@ -994,8 +973,7 @@ class _DesktopTransactionCardRowState flex: 3, child: Text( whatIsIt( - _transaction.type, - coin, + _transaction, currentHeight, ), style: diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart index 7ec4e1b78..89dd74f8b 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart @@ -44,43 +44,12 @@ class _TransactionCardStateV2 extends ConsumerState { String whatIsIt( Coin coin, int currentHeight, - ) { - final confirmedStatus = _transaction.isConfirmed( - currentHeight, - ref.read(pWallets).getWallet(walletId).cryptoCurrency.minConfirms, - ); - - if (_transaction.subType == TransactionSubType.cashFusion || - _transaction.subType == TransactionSubType.sparkMint || - _transaction.subType == TransactionSubType.mint) { - if (confirmedStatus) { - return "Anonymized"; - } else { - return "Anonymizing"; - } - } - - if (_transaction.type == TransactionType.incoming) { - // if (_transaction.isMinting) { - // return "Minting"; - // } else - if (confirmedStatus) { - return "Received"; - } else { - return "Receiving"; - } - } else if (_transaction.type == TransactionType.outgoing) { - if (confirmedStatus) { - return "Sent"; - } else { - return "Sending"; - } - } else if (_transaction.type == TransactionType.sentToSelf) { - return "Sent to self"; - } else { - return _transaction.type.name; - } - } + ) => + _transaction.statusLabel( + currentChainHeight: currentHeight, + minConfirms: + ref.read(pWallets).getWallet(walletId).cryptoCurrency.minConfirms, + ); @override void initState() { @@ -123,7 +92,7 @@ class _TransactionCardStateV2 extends ConsumerState { final Amount amount; if (_transaction.subType == TransactionSubType.cashFusion) { - amount = _transaction.getAmountReceivedThisWallet(coin: coin); + amount = _transaction.getAmountReceivedInThisWallet(coin: coin); } else { switch (_transaction.type) { case TransactionType.outgoing: @@ -132,7 +101,11 @@ class _TransactionCardStateV2 extends ConsumerState { case TransactionType.incoming: case TransactionType.sentToSelf: - amount = _transaction.getAmountReceivedThisWallet(coin: coin); + if (_transaction.subType == TransactionSubType.sparkMint) { + amount = _transaction.getAmountSparkSelfMinted(coin: coin); + } else { + amount = _transaction.getAmountReceivedInThisWallet(coin: coin); + } break; case TransactionType.unknown: 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 4eada7202..205f331d0 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 @@ -112,7 +112,7 @@ class _TransactionV2DetailsViewState unit = coin.ticker; if (_transaction.subType == TransactionSubType.cashFusion) { - amount = _transaction.getAmountReceivedThisWallet(coin: coin); + amount = _transaction.getAmountReceivedInThisWallet(coin: coin); data = _transaction.outputs .where((e) => e.walletOwns) .map((e) => ( @@ -136,7 +136,11 @@ class _TransactionV2DetailsViewState case TransactionType.incoming: case TransactionType.sentToSelf: - amount = _transaction.getAmountReceivedThisWallet(coin: coin); + if (_transaction.subType == TransactionSubType.sparkMint) { + amount = _transaction.getAmountSparkSelfMinted(coin: coin); + } else { + amount = _transaction.getAmountReceivedInThisWallet(coin: coin); + } data = _transaction.outputs .where((e) => e.walletOwns) .map((e) => ( @@ -169,77 +173,10 @@ class _TransactionV2DetailsViewState super.dispose(); } - String whatIsIt(TransactionV2 tx, int height) { - final type = tx.type; - if (coin == Coin.firo || coin == Coin.firoTestNet) { - if (tx.subType == TransactionSubType.mint) { - if (tx.isConfirmed(height, minConfirms)) { - return "Minted"; - } else { - return "Minting"; - } - } - } - - // if (coin == Coin.epicCash) { - // if (_transaction.isCancelled) { - // return "Cancelled"; - // } else if (type == TransactionType.incoming) { - // if (tx.isConfirmed(height, minConfirms)) { - // return "Received"; - // } else { - // if (_transaction.numberOfMessages == 1) { - // return "Receiving (waiting for sender)"; - // } else if ((_transaction.numberOfMessages ?? 0) > 1) { - // return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no) - // } else { - // return "Receiving"; - // } - // } - // } else if (type == TransactionType.outgoing) { - // if (tx.isConfirmed(height, minConfirms)) { - // return "Sent (confirmed)"; - // } else { - // if (_transaction.numberOfMessages == 1) { - // return "Sending (waiting for receiver)"; - // } else if ((_transaction.numberOfMessages ?? 0) > 1) { - // return "Sending (waiting for confirmations)"; - // } else { - // return "Sending"; - // } - // } - // } - // } - - if (tx.subType == TransactionSubType.cashFusion) { - if (tx.isConfirmed(height, minConfirms)) { - return "Anonymized"; - } else { - return "Anonymizing"; - } - } - - if (type == TransactionType.incoming) { - // if (_transaction.isMinting) { - // return "Minting"; - // } else - if (tx.isConfirmed(height, minConfirms)) { - return "Received"; - } else { - return "Receiving"; - } - } else if (type == TransactionType.outgoing) { - if (tx.isConfirmed(height, minConfirms)) { - return "Sent"; - } else { - return "Sending"; - } - } else if (type == TransactionType.sentToSelf) { - return "Sent to self"; - } else { - return type.name; - } - } + String whatIsIt(TransactionV2 tx, int height) => tx.statusLabel( + currentChainHeight: height, + minConfirms: minConfirms, + ); Future fetchContactNameFor(String address) async { if (address.isEmpty) { From 97ff9ecf8b6a5b8a29fa86b43ca73281add04cf2 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 29 Dec 2023 10:34:41 -0600 Subject: [PATCH 69/77] const app dir name --- lib/utilities/stack_file_system.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index 4cbbbf437..56e55fe40 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -20,16 +20,18 @@ abstract class StackFileSystem { static Future applicationRootDirectory() async { Directory appDirectory; + // if this is changed, the directories in libmonero must also be changed!!!!! + const dirName = "stackwallet"; + // todo: can merge and do same as regular linux home dir? if (Logging.isArmLinux) { appDirectory = await getApplicationDocumentsDirectory(); - appDirectory = Directory("${appDirectory.path}/.stackwallet"); + appDirectory = Directory("${appDirectory.path}/.$dirName"); } else if (Platform.isLinux) { if (overrideDir != null) { appDirectory = Directory(overrideDir!); } else { - appDirectory = - Directory("${Platform.environment['HOME']}/.stackwallet"); + appDirectory = Directory("${Platform.environment['HOME']}/.$dirName"); } } else if (Platform.isWindows) { if (overrideDir != null) { @@ -42,7 +44,7 @@ abstract class StackFileSystem { appDirectory = Directory(overrideDir!); } else { appDirectory = await getLibraryDirectory(); - appDirectory = Directory("${appDirectory.path}/stackwallet"); + appDirectory = Directory("${appDirectory.path}/$dirName"); } } else if (Platform.isIOS) { // todo: check if we need different behaviour here From 02cb79c6a30b7722e33f9cbed09b21c559867727 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 29 Dec 2023 18:12:13 -0600 Subject: [PATCH 70/77] refactor send screen address validation to take into account not being able to send from lelantus to spark directly --- lib/pages/send_view/send_view.dart | 479 +++++++++--------- lib/pages/send_view/token_send_view.dart | 8 +- .../wallet_view/sub_widgets/desktop_send.dart | 203 ++++---- .../ui/preview_tx_button_state_provider.dart | 27 +- 4 files changed, 376 insertions(+), 341 deletions(-) diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index b9878e5cd..ea82e5b65 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -121,38 +121,173 @@ class _SendViewState extends ConsumerState { late final bool isStellar; late final bool isFiro; - Amount? _amountToSend; Amount? _cachedAmountToSend; String? _address; bool _addressToggleFlag = false; - bool _isSparkAddress = false; bool _cryptoAmountChangeLock = false; late VoidCallback onCryptoAmountChanged; Set selectedUTXOs = {}; + Future _scanQr() async { + try { + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + + // Future.delayed( + // const Duration(seconds: 2), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + + Logging.instance.log("qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = AddressUtils.parseUri(qrResult.rawContent); + + Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info); + + if (results.isNotEmpty && results["scheme"] == coin.uriScheme) { + // auto fill address + _address = (results["address"] ?? "").trim(); + sendToController.text = _address!; + + // autofill notes field + if (results["message"] != null) { + noteController.text = results["message"]!; + } else if (results["label"] != null) { + noteController.text = results["label"]!; + } + + // autofill amount field + if (results["amount"] != null) { + final Amount amount = Decimal.parse(results["amount"]!).toAmount( + fractionDigits: coin.decimals, + ); + cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( + amount, + withUnitName: false, + ); + ref.read(pSendAmount.notifier).state = amount; + } + + _setValidAddressProviders(_address); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else if (ref + .read(pWallets) + .getWallet(walletId) + .cryptoCurrency + .validateAddress(qrResult.rawContent)) { + _address = qrResult.rawContent.trim(); + sendToController.text = _address ?? ""; + + _setValidAddressProviders(_address); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true; + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning); + } + } + + void _fiatFieldChanged(String baseAmountString) { + final baseAmount = Amount.tryParseFiatString( + baseAmountString, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); + final Amount? amount; + if (baseAmount != null) { + final Decimal _price = + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + + if (_price == Decimal.zero) { + amount = 0.toAmountAsRaw(fractionDigits: coin.decimals); + } else { + amount = baseAmount <= Amount.zero + ? 0.toAmountAsRaw(fractionDigits: coin.decimals) + : (baseAmount.decimal / _price) + .toDecimal( + scaleOnInfinitePrecision: coin.decimals, + ) + .toAmount(fractionDigits: coin.decimals); + } + if (_cachedAmountToSend != null && _cachedAmountToSend == amount) { + return; + } + _cachedAmountToSend = amount; + Logging.instance + .log("it changed $amount $_cachedAmountToSend", level: LogLevel.Info); + + final amountString = ref.read(pAmountFormatter(coin)).format( + amount, + withUnitName: false, + ); + + _cryptoAmountChangeLock = true; + cryptoAmountController.text = amountString; + _cryptoAmountChangeLock = false; + } else { + amount = 0.toAmountAsRaw(fractionDigits: coin.decimals); + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ""; + _cryptoAmountChangeLock = false; + } + // setState(() { + // _calculateFeesFuture = calculateFees( + // Format.decimalAmountToSatoshis( + // _amountToSend!)); + // }); + ref.read(pSendAmount.notifier).state = amount; + } + void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( cryptoAmountController.text, ); + final Amount? amount; if (cryptoAmount != null) { - _amountToSend = cryptoAmount; - if (_cachedAmountToSend != null && - _cachedAmountToSend == _amountToSend) { + amount = cryptoAmount; + if (_cachedAmountToSend != null && _cachedAmountToSend == amount) { return; } - _cachedAmountToSend = _amountToSend; - Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + _cachedAmountToSend = amount; + Logging.instance.log("it changed $amount $_cachedAmountToSend", level: LogLevel.Info); final price = ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; if (price > Decimal.zero) { - baseAmountController.text = (_amountToSend!.decimal * price) + baseAmountController.text = (amount!.decimal * price) .toAmount( fractionDigits: 2, ) @@ -161,20 +296,20 @@ class _SendViewState extends ConsumerState { ); } } else { - _amountToSend = null; + amount = null; baseAmountController.text = ""; } - _updatePreviewButtonState(_address, _amountToSend); + ref.read(pSendAmount.notifier).state = amount; _cryptoAmountChangedFeeUpdateTimer?.cancel(); _cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { if (coin != Coin.epicCash && !_baseFocus.hasFocus) { setState(() { _calculateFeesFuture = calculateFees( - _amountToSend == null + amount == null ? 0.toAmountAsRaw(fractionDigits: coin.decimals) - : _amountToSend!, + : amount!, ); }); } @@ -193,9 +328,9 @@ class _SendViewState extends ConsumerState { if (coin != Coin.epicCash && !_cryptoFocus.hasFocus) { setState(() { _calculateFeesFuture = calculateFees( - _amountToSend == null + ref.read(pSendAmount) == null ? 0.toAmountAsRaw(fractionDigits: coin.decimals) - : _amountToSend!, + : ref.read(pSendAmount)!, ); }); } @@ -230,6 +365,7 @@ class _SendViewState extends ConsumerState { if (_data != null && _data!.contactLabel == address) { return null; } + if (address.isNotEmpty && !ref .read(pWallets) @@ -241,24 +377,22 @@ class _SendViewState extends ConsumerState { return null; } - void _updatePreviewButtonState(String? address, Amount? amount) { + void _setValidAddressProviders(String? address) { if (isPaynymSend) { - ref.read(previewTxButtonStateProvider.state).state = - (amount != null && amount > Amount.zero); + ref.read(pValidSendToAddress.notifier).state = true; } else { - final walletCurrency = - ref.read(pWallets).getWallet(walletId).cryptoCurrency; - final isValidAddress = walletCurrency.validateAddress(address ?? ""); + final wallet = ref.read(pWallets).getWallet(walletId); + if (wallet is SparkInterface) { + ref.read(pValidSparkSendToAddress.notifier).state = + SparkInterface.validateSparkAddress( + address: address ?? "", + isTestNet: + wallet.cryptoCurrency.network == CryptoCurrencyNetwork.test, + ); + } - _isSparkAddress = isValidAddress - ? SparkInterface.validateSparkAddress( - address: address!, - isTestNet: walletCurrency.network == CryptoCurrencyNetwork.test, - ) - : false; - - ref.read(previewTxButtonStateProvider.state).state = - (isValidAddress && amount != null && amount > Amount.zero); + ref.read(pValidSendToAddress.notifier).state = + wallet.cryptoCurrency.validateAddress(address ?? ""); } } @@ -392,7 +526,7 @@ class _SendViewState extends ConsumerState { ); final wallet = ref.read(pWallets).getWallet(walletId); - final Amount amount = _amountToSend!; + final Amount amount = ref.read(pSendAmount)!; final Amount availableBalance; if (isFiro) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { @@ -524,7 +658,7 @@ class _SendViewState extends ConsumerState { } else if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.public: - if (_isSparkAddress) { + if (ref.read(pValidSparkSendToAddress)) { txDataFuture = wallet.prepareSparkMintTransaction( txData: TxData( sparkRecipients: [ @@ -570,10 +704,10 @@ class _SendViewState extends ConsumerState { case FiroType.spark: txDataFuture = wallet.prepareSendSpark( txData: TxData( - recipients: _isSparkAddress + recipients: ref.read(pValidSparkSendToAddress) ? null : [(address: _address!, amount: amount)], - sparkRecipients: _isSparkAddress + sparkRecipients: ref.read(pValidSparkSendToAddress) ? [ ( address: _address!, @@ -807,7 +941,7 @@ class _SendViewState extends ConsumerState { if (isFiro) { ref.listen(publicPrivateBalanceStateProvider, (previous, next) { - if (_amountToSend == null) { + if (ref.read(pSendAmount) == null) { setState(() { _calculateFeesFuture = calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals)); @@ -815,7 +949,7 @@ class _SendViewState extends ConsumerState { } else { setState(() { _calculateFeesFuture = calculateFees( - _amountToSend!, + ref.read(pSendAmount)!, ); }); } @@ -1077,8 +1211,7 @@ class _SendViewState extends ConsumerState { ), onChanged: (newValue) { _address = newValue.trim(); - _updatePreviewButtonState( - _address, _amountToSend); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = newValue.isNotEmpty; @@ -1115,9 +1248,8 @@ class _SendViewState extends ConsumerState { onTap: () { sendToController.text = ""; _address = ""; - _updatePreviewButtonState( - _address, - _amountToSend); + _setValidAddressProviders( + _address); setState(() { _addressToggleFlag = false; @@ -1159,9 +1291,8 @@ class _SendViewState extends ConsumerState { content.trim(); _address = content.trim(); - _updatePreviewButtonState( - _address, - _amountToSend); + _setValidAddressProviders( + _address); setState(() { _addressToggleFlag = sendToController @@ -1195,139 +1326,9 @@ class _SendViewState extends ConsumerState { "Scan QR Button. Opens Camera For Scanning QR Code.", key: const Key( "sendViewScanQrButtonKey"), - onTap: () async { - try { - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await scanner.scan(); - - // Future.delayed( - // const Duration(seconds: 2), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - - Logging.instance.log( - "qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); - - final results = - AddressUtils.parseUri( - qrResult.rawContent); - - Logging.instance.log( - "qrResult parsed: $results", - level: LogLevel.Info); - - if (results.isNotEmpty && - results["scheme"] == - coin.uriScheme) { - // auto fill address - _address = - (results["address"] ?? - "") - .trim(); - sendToController.text = - _address!; - - // autofill notes field - if (results["message"] != - null) { - noteController.text = - results["message"]!; - } else if (results[ - "label"] != - null) { - noteController.text = - results["label"]!; - } - - // autofill amount field - if (results["amount"] != - null) { - final Amount amount = - Decimal.parse(results[ - "amount"]!) - .toAmount( - fractionDigits: - coin.decimals, - ); - cryptoAmountController - .text = - ref - .read( - pAmountFormatter( - coin)) - .format( - amount, - withUnitName: - false, - ); - _amountToSend = amount; - } - - _updatePreviewButtonState( - _address, - _amountToSend); - setState(() { - _addressToggleFlag = - sendToController - .text.isNotEmpty; - }); - - // now check for non standard encoded basic address - } else if (ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(qrResult - .rawContent)) { - _address = qrResult - .rawContent - .trim(); - sendToController.text = - _address ?? ""; - - _updatePreviewButtonState( - _address, - _amountToSend); - setState(() { - _addressToggleFlag = - sendToController - .text.isNotEmpty; - }); - } - } on PlatformException catch (e, s) { - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true; - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); - } - }, + onTap: _scanQr, child: const QrCodeIcon(), - ) + ), ], ), ), @@ -1338,7 +1339,11 @@ class _SendViewState extends ConsumerState { const SizedBox( height: 10, ), - if (isStellar || _isSparkAddress) + if (isStellar || + (ref.watch(pValidSparkSendToAddress) && + ref.watch( + publicPrivateBalanceStateProvider) != + FiroType.lelantus)) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1419,9 +1424,50 @@ class _SendViewState extends ConsumerState { ), Builder( builder: (_) { - final error = _updateInvalidAddressText( - _address ?? "", - ); + final String? error; + + if (_address == null || _address!.isEmpty) { + error = null; + } else if (isFiro) { + if (ref.watch( + publicPrivateBalanceStateProvider) == + FiroType.lelantus) { + if (_data != null && + _data!.contactLabel == _address) { + error = SparkInterface.validateSparkAddress( + address: _data!.address, + isTestNet: coin.isTestNet) + ? "Unsupported" + : null; + } else if (ref + .watch(pValidSparkSendToAddress)) { + error = "Unsupported"; + } else { + error = ref.watch(pValidSendToAddress) + ? null + : "Invalid address"; + } + } else { + if (_data != null && + _data!.contactLabel == _address) { + error = null; + } else if (!ref.watch(pValidSendToAddress) && + !ref.watch(pValidSparkSendToAddress)) { + error = "Invalid address"; + } else { + error = null; + } + } + } else { + if (_data != null && + _data!.contactLabel == _address) { + error = null; + } else if (!ref.watch(pValidSendToAddress)) { + error = "Invalid address"; + } else { + error = null; + } + } if (error == null || error.isEmpty) { return Container(); @@ -1737,65 +1783,7 @@ class _SendViewState extends ConsumerState { // ? newValue // : oldValue), ], - onChanged: (baseAmountString) { - final baseAmount = Amount.tryParseFiatString( - baseAmountString, - locale: locale, - ); - if (baseAmount != null) { - final Decimal _price = ref - .read(priceAnd24hChangeNotifierProvider) - .getPrice(coin) - .item1; - - if (_price == Decimal.zero) { - _amountToSend = 0.toAmountAsRaw( - fractionDigits: coin.decimals); - } else { - _amountToSend = baseAmount <= Amount.zero - ? 0.toAmountAsRaw( - fractionDigits: coin.decimals) - : (baseAmount.decimal / _price) - .toDecimal( - scaleOnInfinitePrecision: - coin.decimals, - ) - .toAmount( - fractionDigits: coin.decimals); - } - if (_cachedAmountToSend != null && - _cachedAmountToSend == _amountToSend) { - return; - } - _cachedAmountToSend = _amountToSend; - Logging.instance.log( - "it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); - - final amountString = - ref.read(pAmountFormatter(coin)).format( - _amountToSend!, - withUnitName: false, - ); - - _cryptoAmountChangeLock = true; - cryptoAmountController.text = amountString; - _cryptoAmountChangeLock = false; - } else { - _amountToSend = 0.toAmountAsRaw( - fractionDigits: coin.decimals); - _cryptoAmountChangeLock = true; - cryptoAmountController.text = ""; - _cryptoAmountChangeLock = false; - } - // setState(() { - // _calculateFeesFuture = calculateFees( - // Format.decimalAmountToSatoshis( - // _amountToSend!)); - // }); - _updatePreviewButtonState( - _address, _amountToSend); - }, + onChanged: _fiatFieldChanged, decoration: InputDecoration( contentPadding: const EdgeInsets.only( top: 12, @@ -1860,8 +1848,8 @@ class _SendViewState extends ConsumerState { .spendable; Amount? amount; - if (_amountToSend != null) { - amount = _amountToSend!; + if (ref.read(pSendAmount) != null) { + amount = ref.read(pSendAmount)!; if (spendable == amount) { // this is now a send all @@ -2075,7 +2063,8 @@ class _SendViewState extends ConsumerState { amount: (Decimal.tryParse( cryptoAmountController .text) ?? - _amountToSend + ref + .watch(pSendAmount) ?.decimal ?? Decimal.zero) .toAmount( @@ -2239,14 +2228,10 @@ class _SendViewState extends ConsumerState { height: 12, ), TextButton( - onPressed: ref - .watch(previewTxButtonStateProvider.state) - .state + onPressed: ref.watch(pPreviewTxButtonEnabled(coin)) ? _previewTransaction : null, - style: ref - .watch(previewTxButtonStateProvider.state) - .state + style: ref.watch(pPreviewTxButtonEnabled(coin)) ? Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context) diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index b05beeb25..c1ea39345 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -348,7 +348,7 @@ class _TokenSendViewState extends ConsumerState { .getWallet(walletId) .cryptoCurrency .validateAddress(address ?? ""); - ref.read(previewTxButtonStateProvider.state).state = + ref.read(previewTokenTxButtonStateProvider.state).state = (isValidAddress && amount != null && amount > Amount.zero); } @@ -1227,12 +1227,14 @@ class _TokenSendViewState extends ConsumerState { ), TextButton( onPressed: ref - .watch(previewTxButtonStateProvider.state) + .watch( + previewTokenTxButtonStateProvider.state) .state ? _previewTransaction : null, style: ref - .watch(previewTxButtonStateProvider.state) + .watch( + previewTokenTxButtonStateProvider.state) .state ? Theme.of(context) .extension()! 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 dd596037e..d638c8690 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 @@ -114,7 +114,6 @@ class _DesktopSendState extends ConsumerState { String? _note; String? _onChainNote; - Amount? _amountToSend; Amount? _cachedAmountToSend; String? _address; @@ -125,8 +124,6 @@ class _DesktopSendState extends ConsumerState { bool get isPaynymSend => widget.accountLite != null; - bool _isSparkAddress = false; - bool isCustomFee = false; int customFeeRate = 1; (FeeRateType, String?, String?)? feeSelectionResult; @@ -141,7 +138,7 @@ class _DesktopSendState extends ConsumerState { Future previewSend() async { final wallet = ref.read(pWallets).getWallet(walletId); - final Amount amount = _amountToSend!; + final Amount amount = ref.read(pSendAmount)!; final Amount availableBalance; if ((coin == Coin.firo || coin == Coin.firoTestNet)) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { @@ -321,7 +318,7 @@ class _DesktopSendState extends ConsumerState { } else if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.public: - if (_isSparkAddress) { + if (ref.read(pValidSparkSendToAddress)) { txDataFuture = wallet.prepareSparkMintTransaction( txData: TxData( sparkRecipients: [ @@ -367,10 +364,10 @@ class _DesktopSendState extends ConsumerState { case FiroType.spark: txDataFuture = wallet.prepareSendSpark( txData: TxData( - recipients: _isSparkAddress + recipients: ref.read(pValidSparkSendToAddress) ? null : [(address: _address!, amount: amount)], - sparkRecipients: _isSparkAddress + sparkRecipients: ref.read(pValidSparkSendToAddress) ? [ ( address: _address!, @@ -533,21 +530,21 @@ class _DesktopSendState extends ConsumerState { final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( cryptoAmountController.text, ); + final Amount? amount; if (cryptoAmount != null) { - _amountToSend = cryptoAmount; - if (_cachedAmountToSend != null && - _cachedAmountToSend == _amountToSend) { + amount = cryptoAmount; + if (_cachedAmountToSend != null && _cachedAmountToSend == amount) { return; } - Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + Logging.instance.log("it changed $amount $_cachedAmountToSend", level: LogLevel.Info); - _cachedAmountToSend = _amountToSend; + _cachedAmountToSend = amount; final price = ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; if (price > Decimal.zero) { - final String fiatAmountString = (_amountToSend!.decimal * price) + final String fiatAmountString = (amount!.decimal * price) .toAmount(fractionDigits: 2) .fiatString( locale: ref.read(localeServiceChangeNotifierProvider).locale, @@ -556,52 +553,29 @@ class _DesktopSendState extends ConsumerState { baseAmountController.text = fiatAmountString; } } else { - _amountToSend = null; + amount = null; _cachedAmountToSend = null; baseAmountController.text = ""; } - _updatePreviewButtonState(_address, _amountToSend); + ref.read(pSendAmount.notifier).state = amount; } } - String? _updateInvalidAddressText(String address) { - if (_data != null && _data!.contactLabel == address) { - return null; - } - if (address.isNotEmpty && - !ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(address)) { - return "Invalid address"; - } - return null; - } - - void _updatePreviewButtonState(String? address, Amount? amount) { - if (isPaynymSend) { - ref.read(previewTxButtonStateProvider.state).state = - (amount != null && amount > Amount.zero); - } else { - final walletCurrency = - ref.read(pWallets).getWallet(walletId).cryptoCurrency; - final isValidAddress = walletCurrency.validateAddress(address ?? ""); - - _isSparkAddress = isValidAddress && - ref.read(publicPrivateBalanceStateProvider.state).state != - FiroType.lelantus - ? SparkInterface.validateSparkAddress( - address: address!, - isTestNet: walletCurrency.network == CryptoCurrencyNetwork.test, - ) - : false; - - ref.read(previewTxButtonStateProvider.state).state = - (isValidAddress && amount != null && amount > Amount.zero); - } - } + // String? _updateInvalidAddressText(String address) { + // if (_data != null && _data!.contactLabel == address) { + // return null; + // } + // if (address.isNotEmpty && + // !ref + // .read(pWallets) + // .getWallet(walletId) + // .cryptoCurrency + // .validateAddress(address)) { + // return "Invalid address"; + // } + // return null; + // } Future scanQr() async { try { @@ -639,10 +613,9 @@ class _DesktopSendState extends ConsumerState { cryptoAmountController.text = ref .read(pAmountFormatter(coin)) .format(amount, withUnitName: false); - _amountToSend = amount; + ref.read(pSendAmount.notifier).state = amount; } - _updatePreviewButtonState(_address, _amountToSend); setState(() { _addressToggleFlag = sendToController.text.isNotEmpty; }); @@ -656,7 +629,7 @@ class _DesktopSendState extends ConsumerState { _address = qrResult.rawContent; sendToController.text = _address ?? ""; - _updatePreviewButtonState(_address, _amountToSend); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = sendToController.text.isNotEmpty; }); @@ -670,6 +643,25 @@ class _DesktopSendState extends ConsumerState { } } + void _setValidAddressProviders(String? address) { + if (isPaynymSend) { + ref.read(pValidSendToAddress.notifier).state = true; + } else { + final wallet = ref.read(pWallets).getWallet(walletId); + if (wallet is SparkInterface) { + ref.read(pValidSparkSendToAddress.notifier).state = + SparkInterface.validateSparkAddress( + address: address ?? "", + isTestNet: + wallet.cryptoCurrency.network == CryptoCurrencyNetwork.test, + ); + } + + ref.read(pValidSendToAddress.notifier).state = + wallet.cryptoCurrency.validateAddress(address ?? ""); + } + } + Future pasteAddress() async { final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); if (data?.text != null && data!.text!.isNotEmpty) { @@ -686,7 +678,7 @@ class _DesktopSendState extends ConsumerState { sendToController.text = content; _address = content; - _updatePreviewButtonState(_address, _amountToSend); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = sendToController.text.isNotEmpty; }); @@ -715,28 +707,29 @@ class _DesktopSendState extends ConsumerState { baseAmountString, locale: ref.read(localeServiceChangeNotifierProvider).locale, ); + final Amount? amount; if (baseAmount != null) { final _price = ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; if (_price == Decimal.zero) { - _amountToSend = Decimal.zero.toAmount(fractionDigits: coin.decimals); + amount = Decimal.zero.toAmount(fractionDigits: coin.decimals); } else { - _amountToSend = baseAmount <= Amount.zero + amount = baseAmount <= Amount.zero ? Decimal.zero.toAmount(fractionDigits: coin.decimals) : (baseAmount.decimal / _price) .toDecimal(scaleOnInfinitePrecision: coin.decimals) .toAmount(fractionDigits: coin.decimals); } - if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { + if (_cachedAmountToSend != null && _cachedAmountToSend == amount) { return; } - _cachedAmountToSend = _amountToSend; - Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); + _cachedAmountToSend = amount; + Logging.instance + .log("it changed $amount $_cachedAmountToSend", level: LogLevel.Info); final amountString = ref.read(pAmountFormatter(coin)).format( - _amountToSend!, + amount!, withUnitName: false, ); @@ -744,7 +737,7 @@ class _DesktopSendState extends ConsumerState { cryptoAmountController.text = amountString; _cryptoAmountChangeLock = false; } else { - _amountToSend = Decimal.zero.toAmount(fractionDigits: coin.decimals); + amount = Decimal.zero.toAmount(fractionDigits: coin.decimals); _cryptoAmountChangeLock = true; cryptoAmountController.text = ""; _cryptoAmountChangeLock = false; @@ -754,7 +747,7 @@ class _DesktopSendState extends ConsumerState { // Format.decimalAmountToSatoshis( // _amountToSend!)); // }); - _updatePreviewButtonState(_address, _amountToSend); + ref.read(pSendAmount.notifier).state = amount; } Future sendAllTapped() async { @@ -784,11 +777,12 @@ class _DesktopSendState extends ConsumerState { } void _showDesktopCoinControl() async { + final amount = ref.read(pSendAmount); await showDialog( context: context, builder: (context) => DesktopCoinControlUseDialog( walletId: widget.walletId, - amountToSend: _amountToSend, + amountToSend: amount, ), ); } @@ -797,7 +791,8 @@ class _DesktopSendState extends ConsumerState { void initState() { WidgetsBinding.instance.addPostFrameCallback((_) { ref.refresh(feeSheetSessionCacheProvider); - ref.read(previewTxButtonStateProvider.state).state = false; + ref.read(pValidSendToAddress.state).state = false; + ref.read(pValidSparkSendToAddress.state).state = false; }); // _calculateFeesFuture = calculateFees(0); @@ -832,20 +827,20 @@ class _DesktopSendState extends ConsumerState { _cryptoFocus.addListener(() { if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { - if (_amountToSend == null) { + if (ref.read(pSendAmount) == null) { ref.refresh(sendAmountProvider); } else { - ref.read(sendAmountProvider.state).state = _amountToSend!; + ref.read(sendAmountProvider.state).state = ref.read(pSendAmount)!; } } }); _baseFocus.addListener(() { if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { - if (_amountToSend == null) { + if (ref.read(pSendAmount) == null) { ref.refresh(sendAmountProvider); } else { - ref.read(sendAmountProvider.state).state = _amountToSend!; + ref.read(sendAmountProvider.state).state = ref.read(pSendAmount)!; } } }); @@ -1263,7 +1258,7 @@ class _DesktopSendState extends ConsumerState { ), onChanged: (newValue) { _address = newValue; - _updatePreviewButtonState(_address, _amountToSend); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = newValue.isNotEmpty; @@ -1303,8 +1298,7 @@ class _DesktopSendState extends ConsumerState { onTap: () { sendToController.text = ""; _address = ""; - _updatePreviewButtonState( - _address, _amountToSend); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = false; }); @@ -1365,10 +1359,7 @@ class _DesktopSendState extends ConsumerState { _address = entry.address; - _updatePreviewButtonState( - _address, - _amountToSend, - ); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = true; @@ -1393,9 +1384,44 @@ class _DesktopSendState extends ConsumerState { if (!isPaynymSend) Builder( builder: (_) { - final error = _updateInvalidAddressText( - _address ?? "", - ); + final String? error; + + if (_address == null || _address!.isEmpty) { + error = null; + } else if (coin == Coin.firo || coin == Coin.firoTestNet) { + if (ref.watch(publicPrivateBalanceStateProvider) == + FiroType.lelantus) { + if (_data != null && _data!.contactLabel == _address) { + error = SparkInterface.validateSparkAddress( + address: _data!.address, isTestNet: coin.isTestNet) + ? "Lelantus to Spark not supported" + : null; + } else if (ref.watch(pValidSparkSendToAddress)) { + error = "Lelantus to Spark not supported"; + } else { + error = ref.watch(pValidSendToAddress) + ? null + : "Invalid address"; + } + } else { + if (_data != null && _data!.contactLabel == _address) { + error = null; + } else if (!ref.watch(pValidSendToAddress) && + !ref.watch(pValidSparkSendToAddress)) { + error = "Invalid address"; + } else { + error = null; + } + } + } else { + if (_data != null && _data!.contactLabel == _address) { + error = null; + } else if (!ref.watch(pValidSendToAddress)) { + error = "Invalid address"; + } else { + error = null; + } + } if (error == null || error.isEmpty) { return Container(); @@ -1422,16 +1448,16 @@ class _DesktopSendState extends ConsumerState { }, ), if (isStellar || - (_isSparkAddress && + (ref.watch(pValidSparkSendToAddress) && ref.watch(publicPrivateBalanceStateProvider) != - FiroType.public)) + FiroType.lelantus)) const SizedBox( height: 10, ), if (isStellar || - (_isSparkAddress && + (ref.watch(pValidSparkSendToAddress) && ref.watch(publicPrivateBalanceStateProvider) != - FiroType.public)) + FiroType.lelantus)) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1727,10 +1753,9 @@ class _DesktopSendState extends ConsumerState { PrimaryButton( buttonHeight: ButtonHeight.l, label: "Preview send", - enabled: ref.watch(previewTxButtonStateProvider.state).state, - onPressed: ref.watch(previewTxButtonStateProvider.state).state - ? previewSend - : null, + enabled: ref.watch(pPreviewTxButtonEnabled(coin)), + onPressed: + ref.watch(pPreviewTxButtonEnabled(coin)) ? previewSend : null, ) ], ); diff --git a/lib/providers/ui/preview_tx_button_state_provider.dart b/lib/providers/ui/preview_tx_button_state_provider.dart index 842ac5658..768edf301 100644 --- a/lib/providers/ui/preview_tx_button_state_provider.dart +++ b/lib/providers/ui/preview_tx_button_state_provider.dart @@ -9,9 +9,32 @@ */ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; -final previewTxButtonStateProvider = StateProvider.autoDispose((_) { - return false; +final pSendAmount = StateProvider.autoDispose((_) => null); +final pValidSendToAddress = StateProvider.autoDispose((_) => false); +final pValidSparkSendToAddress = StateProvider.autoDispose((_) => false); + +final pPreviewTxButtonEnabled = + Provider.autoDispose.family((ref, coin) { + final amount = ref.watch(pSendAmount) ?? Amount.zero; + + // TODO [prio=low]: move away from Coin + if (coin == Coin.firo || coin == Coin.firoTestNet) { + if (ref.watch(publicPrivateBalanceStateProvider) == FiroType.lelantus) { + return ref.watch(pValidSendToAddress) && + !ref.watch(pValidSparkSendToAddress) && + amount > Amount.zero; + } else { + return (ref.watch(pValidSendToAddress) || + ref.watch(pValidSparkSendToAddress)) && + amount > Amount.zero; + } + } else { + return ref.watch(pValidSendToAddress) && amount > Amount.zero; + } }); final previewTokenTxButtonStateProvider = StateProvider.autoDispose((_) { From ce0b8712847a7ee97a5ad106572fc5f5c7401faf Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 3 Jan 2024 08:38:01 -0600 Subject: [PATCH 71/77] fix txns v2 not showing up right away on refresh --- .../tx_v2/transaction_v2_list.dart | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart index ad2f31024..ed3c3cdd0 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart @@ -8,6 +8,8 @@ * */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; @@ -40,6 +42,9 @@ class _TransactionsV2ListState extends ConsumerState { bool _hasLoaded = false; List _transactions = []; + late final StreamSubscription> _subscription; + late final QueryBuilder _query; + BorderRadius get _borderRadiusFirst { return BorderRadius.only( topLeft: Radius.circular( @@ -62,19 +67,39 @@ class _TransactionsV2ListState extends ConsumerState { ); } + @override + void initState() { + _query = ref + .read(mainDBProvider) + .isar + .transactionV2s + .where() + .walletIdEqualTo(widget.walletId) + .sortByTimestampDesc(); + + _subscription = _query.watch().listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _transactions = event; + }); + }); + }); + + super.initState(); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { final coin = ref.watch(pWallets).getWallet(widget.walletId).info.coin; return FutureBuilder( - future: ref - .watch(mainDBProvider) - .isar - .transactionV2s - .where() - .walletIdEqualTo(widget.walletId) - .sortByTimestampDesc() - .findAll(), + future: _query.findAll(), builder: (fbContext, AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { From 86be1444ea1a045e1d43ba7300c15ed15e202143 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 3 Jan 2024 09:37:50 -0600 Subject: [PATCH 72/77] critical desktop password related button/function locks --- .../password/create_password_view.dart | 11 +++++++++++ .../password/desktop_login_view.dart | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/pages_desktop_specific/password/create_password_view.dart b/lib/pages_desktop_specific/password/create_password_view.dart index 2986cd0da..a8c129f90 100644 --- a/lib/pages_desktop_specific/password/create_password_view.dart +++ b/lib/pages_desktop_specific/password/create_password_view.dart @@ -65,7 +65,14 @@ class _CreatePasswordViewState extends ConsumerState { bool get fieldsMatch => passwordController.text == passwordRepeatController.text; + bool _nextLock = false; + void onNextPressed() async { + if (_nextLock) { + return; + } + _nextLock = true; + final String passphrase = passwordController.text; final String repeatPassphrase = passwordRepeatController.text; @@ -75,6 +82,7 @@ class _CreatePasswordViewState extends ConsumerState { message: "A password is required", context: context, )); + _nextLock = false; return; } if (passphrase != repeatPassphrase) { @@ -83,6 +91,7 @@ class _CreatePasswordViewState extends ConsumerState { message: "Password does not match", context: context, )); + _nextLock = false; return; } @@ -106,6 +115,7 @@ class _CreatePasswordViewState extends ConsumerState { message: "Error: $e", context: context, )); + _nextLock = false; return; } @@ -132,6 +142,7 @@ class _CreatePasswordViewState extends ConsumerState { context: context, )); } + _nextLock = false; } @override diff --git a/lib/pages_desktop_specific/password/desktop_login_view.dart b/lib/pages_desktop_specific/password/desktop_login_view.dart index cdd6c8c63..2597704fe 100644 --- a/lib/pages_desktop_specific/password/desktop_login_view.dart +++ b/lib/pages_desktop_specific/password/desktop_login_view.dart @@ -79,11 +79,18 @@ class _DesktopLoginViewState extends ConsumerState { } } + bool _loginLock = false; Future login() async { + if (_loginLock) { + return; + } + _loginLock = true; + try { unawaited( showDialog( context: context, + barrierDismissible: false, builder: (context) => const Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, @@ -138,6 +145,8 @@ class _DesktopLoginViewState extends ConsumerState { context: context, ); } + } finally { + _loginLock = false; } } From 89c781ef23e236a718cc9e7da115a8f74d440c7b Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 3 Jan 2024 09:47:36 -0600 Subject: [PATCH 73/77] fix initial wallet not showing up on creation --- lib/main.dart | 3 ++- lib/pages/wallets_view/wallets_view.dart | 3 ++- lib/pages_desktop_specific/my_stack_view/my_stack_view.dart | 4 ++-- lib/services/wallets.dart | 2 -- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 7ac3170b6..478315623 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -72,6 +72,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/providers/all_wallets_info_provider.dart'; import 'package:stackwallet/widgets/crypto_notifications.dart'; import 'package:window_size/window_size.dart'; @@ -747,7 +748,7 @@ class _MaterialAppWithThemeState extends ConsumerState builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { // FlutterNativeSplash.remove(); - if (ref.read(pWallets).hasWallets || + if (ref.read(pAllWalletsInfo).isNotEmpty || ref.read(prefsChangeNotifierProvider).hasPin) { // return HomeView(); diff --git a/lib/pages/wallets_view/wallets_view.dart b/lib/pages/wallets_view/wallets_view.dart index 53f6f1a64..be33af927 100644 --- a/lib/pages/wallets_view/wallets_view.dart +++ b/lib/pages/wallets_view/wallets_view.dart @@ -16,6 +16,7 @@ import 'package:stackwallet/pages/wallets_view/sub_widgets/empty_wallets.dart'; import 'package:stackwallet/pages/wallets_view/sub_widgets/favorite_wallets.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/themes/theme_providers.dart'; +import 'package:stackwallet/wallets/isar/providers/all_wallets_info_provider.dart'; class WalletsView extends ConsumerWidget { const WalletsView({Key? key}) : super(key: key); @@ -25,7 +26,7 @@ class WalletsView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); - final hasWallets = ref.watch(pWallets).hasWallets; + final hasWallets = ref.watch(pAllWalletsInfo).isNotEmpty; final showFavorites = ref.watch(prefsChangeNotifierProvider .select((value) => value.showFavoriteWallets)); diff --git a/lib/pages_desktop_specific/my_stack_view/my_stack_view.dart b/lib/pages_desktop_specific/my_stack_view/my_stack_view.dart index f9fb117bf..bb9f9a646 100644 --- a/lib/pages_desktop_specific/my_stack_view/my_stack_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/my_stack_view.dart @@ -16,9 +16,9 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/hidden_settings.dart'; import 'package:stackwallet/pages/wallets_view/sub_widgets/empty_wallets.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_wallets.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/themes/theme_providers.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/isar/providers/all_wallets_info_provider.dart'; import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; @@ -36,7 +36,7 @@ class _MyStackViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final hasWallets = ref.watch(pWallets).hasWallets; + final hasWallets = ref.watch(pAllWalletsInfo).isNotEmpty; return Background( child: Column( diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 3e8ebc46f..e26eaf0ba 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -35,8 +35,6 @@ class Wallets { late NodeService nodeService; late MainDB mainDB; - bool get hasWallets => _wallets.isNotEmpty; - List get wallets => _wallets.values.toList(); static bool hasLoaded = false; From 555448a0e90dfb994028284ea2cb22814ae4ffee Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 3 Jan 2024 10:52:48 -0600 Subject: [PATCH 74/77] use non local flutter_libsparkmobile --- pubspec.lock | 8 +++++--- pubspec.yaml | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 68b474ced..e270744d3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -662,9 +662,11 @@ packages: flutter_libsparkmobile: dependency: "direct main" description: - path: "../flutter_libsparkmobile" - relative: true - source: path + path: "." + ref: c08c24e744f03c00c2b6fd72ae1a657d2c90f036 + resolved-ref: c08c24e744f03c00c2b6fd72ae1a657d2c90f036 + url: "https://github.com/cypherstack/flutter_libsparkmobile.git" + source: git version: "0.0.1" flutter_lints: dependency: "direct dev" diff --git a/pubspec.yaml b/pubspec.yaml index a6d48be44..961c6167a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,9 @@ dependencies: path: ./crypto_plugins/flutter_liblelantus flutter_libsparkmobile: - path: ../flutter_libsparkmobile + git: + url: https://github.com/cypherstack/flutter_libsparkmobile.git + ref: c08c24e744f03c00c2b6fd72ae1a657d2c90f036 flutter_libmonero: path: ./crypto_plugins/flutter_libmonero From 07b21a42c66fb26737f8b55b73a4a3cec5c807ee Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 3 Jan 2024 11:01:04 -0600 Subject: [PATCH 75/77] check change address diversifier on spark address generate --- .../wallet_mixin_interfaces/spark_interface.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index c27b662ed..56b188e41 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -23,8 +23,6 @@ const kDefaultSparkIndex = 1; // TODO dart style constants. Maybe move to spark lib? const MAX_STANDARD_TX_WEIGHT = 400000; -const SPARK_CHANGE_D = 0x270F; - //https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16 const SPARK_OUT_LIMIT_PER_TX = 16; @@ -70,7 +68,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { _sparkChangeAddressCached = await LibSpark.getAddress( privateKey: keys.privateKey.data, index: kDefaultSparkIndex, - diversifier: SPARK_CHANGE_D, + diversifier: kSparkChange, isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, ); } @@ -116,7 +114,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { (await getCurrentReceivingSparkAddress())?.derivationIndex; // default to starting at 1 if none found - final int diversifier = (highestStoredDiversifier ?? 0) + 1; + int diversifier = (highestStoredDiversifier ?? 0) + 1; + // change address check + if (diversifier == kSparkChange) { + diversifier++; + } final root = await getRootHDNode(); final String derivationPath; From c8817371dcedd66d170be8c57d698311f3bf9f4d Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 3 Jan 2024 17:08:34 -0600 Subject: [PATCH 76/77] update ref --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e270744d3..34b432940 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -663,8 +663,8 @@ packages: dependency: "direct main" description: path: "." - ref: c08c24e744f03c00c2b6fd72ae1a657d2c90f036 - resolved-ref: c08c24e744f03c00c2b6fd72ae1a657d2c90f036 + ref: "6e2b2650a84fc5832dc676f8d016e530ae77851f" + resolved-ref: "6e2b2650a84fc5832dc676f8d016e530ae77851f" url: "https://github.com/cypherstack/flutter_libsparkmobile.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 961c6167a..dcdf22bd2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,7 @@ dependencies: flutter_libsparkmobile: git: url: https://github.com/cypherstack/flutter_libsparkmobile.git - ref: c08c24e744f03c00c2b6fd72ae1a657d2c90f036 + ref: 6e2b2650a84fc5832dc676f8d016e530ae77851f flutter_libmonero: path: ./crypto_plugins/flutter_libmonero From 7870eb3e215f1ef45b0ecbf2e64c17f43d72dc24 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 4 Jan 2024 10:52:30 -0600 Subject: [PATCH 77/77] update ref --- pubspec.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index dcdf22bd2..3e566a466 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,7 @@ dependencies: flutter_libsparkmobile: git: url: https://github.com/cypherstack/flutter_libsparkmobile.git - ref: 6e2b2650a84fc5832dc676f8d016e530ae77851f + ref: d54b4a1f492e48696c3df27eb8c31131a681cbc2 flutter_libmonero: path: ./crypto_plugins/flutter_libmonero @@ -165,7 +165,11 @@ dependencies: url: https://github.com/cypherstack/tezart.git ref: 8a7070f533e63dd150edae99476f6853bfb25913 socks5_proxy: ^1.0.3+dev.3 - coinlib_flutter: ^1.0.0 + coinlib_flutter: + git: + url: https://github.com/cypherstack/coinlib.git + path: coinlib_flutter + ref: 4f549b8b511a63fdc1f44796ab43b10f586635cd convert: ^3.1.1 flutter_hooks: ^0.20.3 meta: ^1.9.1