diff --git a/lib/db/hive/db.dart b/lib/db/hive/db.dart index 00fdc13f7..1ae6de2c2 100644 --- a/lib/db/hive/db.dart +++ b/lib/db/hive/db.dart @@ -53,8 +53,12 @@ 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"; + String _boxNameSparkUsedCoinsTagsCache({required Coin coin}) => + "${coin.name}_sparkUsedCoinsTagsCache"; Box? _boxNodeModels; Box? _boxPrimaryNodes; @@ -75,7 +79,9 @@ class DB { final Map> _txCacheBoxes = {}; final Map> _setCacheBoxes = {}; + final Map> _setSparkCacheBoxes = {}; final Map> _usedSerialsCacheBoxes = {}; + final Map> _getSparkUsedCoinsTagsCacheBoxes = {}; // exposed for monero Box get moneroWalletInfoBox => _walletInfoSource!; @@ -197,6 +203,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(); } @@ -209,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(); } @@ -216,9 +241,12 @@ 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)); + await deleteAll( + boxName: _boxNameSparkUsedCoinsTagsCache(coin: coin)); } } diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 2602e03c1..19bcb16ae 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, @@ -482,6 +484,12 @@ class MainDB { // .findAll(); // await isar.lelantusCoins.deleteAll(lelantusCoinIds); // } + + // spark coins + await isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .deleteAll(); }); } 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/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index 0cec664b8..021bdf065 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -107,6 +107,63 @@ 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 = { + "coinGroupID": int.parse(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?) + 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); + 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.getSparkAnonymitySet(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + String base64ToHex(String source) => base64Decode(LineSplitter.split(source).join()) .map((e) => e.toRadixString(16).padLeft(2, '0')) @@ -136,6 +193,7 @@ class CachedElectrumXClient { result.remove("hex"); result.remove("lelantusData"); + result.remove("sparkData"); if (result["confirmations"] != null && result["confirmations"] as int > minCacheConfirms) { @@ -198,14 +256,62 @@ 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; } } /// 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/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 00ec67602..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'; @@ -467,9 +469,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 +495,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 +569,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 +629,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, @@ -881,7 +883,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 = "", @@ -908,7 +910,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 +923,9 @@ class ElectrumXClient { ], requestTimeout: const Duration(minutes: 2), ); - return Map.from(response["result"] as Map); + final map = Map.from(response["result"] as Map); + final set = Set.from(map["tags"] as List); + return await compute(_ffiHashTagsComputeWrapper, set); } catch (e) { Logging.instance.log(e, level: LogLevel.Error); rethrow; @@ -984,8 +988,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( @@ -1035,3 +1039,7 @@ class ElectrumXClient { } } } + +Set _ffiHashTagsComputeWrapper(Set base64Tags) { + return LibSpark.hashTags(base64Tags: base64Tags); +} 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/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/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/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index 61aea5bf0..9acb5f9ee 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'; @@ -6,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'; @@ -37,6 +40,8 @@ class TransactionV2 { @enumerated final TransactionSubType subType; + final String? otherData; + TransactionV2({ required this.walletId, required this.blockHash, @@ -49,6 +54,7 @@ class TransactionV2 { required this.version, required this.type, required this.subType, + required this.otherData, }); int getConfirmations(int currentChainHeight) { @@ -71,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); @@ -79,12 +85,28 @@ 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) .fold(BigInt.zero, (p, e) => p + e.value); - return Amount(rawValue: inSum, fractionDigits: coin.decimals); + return Amount( + rawValue: inSum, + fractionDigits: coin.decimals, + ) - + getAmountReceivedInThisWallet( + coin: coin, + ) - + getFee(coin: coin); } Set associatedAddresses() => { @@ -92,6 +114,82 @@ 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; + } + } + + 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' @@ -106,6 +204,7 @@ class TransactionV2 { ' version: $version,\n' ' inputs: $inputs,\n' ' outputs: $outputs,\n' + ' otherData: $otherData,\n' ')'; } } 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/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/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, ), 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 9bcaf1b01..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; @@ -140,10 +140,25 @@ 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: + if (widget.txData.sparkMints == null) { + txDataFuture = wallet.confirmSend(txData: widget.txData); + } else { + txDataFuture = + wallet.confirmSparkMintTransactions(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( @@ -165,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()); @@ -233,9 +255,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, @@ -319,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( @@ -424,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), ), ], @@ -443,7 +512,7 @@ class _ConfirmTransactionViewState ), SelectableText( ref.watch(pAmountFormatter(coin)).format( - widget.txData.amount!, + amount, ethContract: ref .watch(tokenServiceProvider) ?.tokenContract, @@ -468,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, ), @@ -494,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), ), ], @@ -625,7 +692,6 @@ class _ConfirmTransactionViewState ), Builder( builder: (context) { - final amount = widget.txData.amount!; final externalCalls = ref.watch( prefsChangeNotifierProvider.select( (value) => value.externalCalls)); @@ -723,9 +789,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( @@ -759,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, + ), ), ], ), @@ -981,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), ), ), ), @@ -1025,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), ), ), @@ -1069,29 +1123,22 @@ class _ConfirmTransactionViewState .textConfirmTotalAmount, ), ), - Builder(builder: (context) { - final fee = widget.txData.fee!; - - final amount = widget.txData.amount!; - 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 73b596690..ea82e5b65 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,8 +119,8 @@ class _SendViewState extends ConsumerState { final _memoFocus = FocusNode(); late final bool isStellar; + late final bool isFiro; - Amount? _amountToSend; Amount? _cachedAmountToSend; String? _address; @@ -128,26 +131,163 @@ class _SendViewState extends ConsumerState { 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, ) @@ -156,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!, ); }); } @@ -188,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)!, ); }); } @@ -225,6 +365,7 @@ class _SendViewState extends ConsumerState { if (_data != null && _data!.contactLabel == address) { return null; } + if (address.isNotEmpty && !ref .read(pWallets) @@ -236,25 +377,30 @@ 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 isValidAddress = ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(address ?? ""); - ref.read(previewTxButtonStateProvider.state).state = - (isValidAddress && amount != null && amount > Amount.zero); + 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 ?? ""); } } late Future _calculateFeesFuture; Map cachedFees = {}; - Map cachedFiroPrivateFees = {}; + Map cachedFiroLelantusFees = {}; + Map cachedFiroSparkFees = {}; Map cachedFiroPublicFees = {}; Future calculateFees(Amount amount) async { @@ -262,16 +408,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 +474,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); @@ -367,15 +526,19 @@ 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 ((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 +655,71 @@ 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: + if (ref.read(pValidSparkSendToAddress)) { + 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: + txDataFuture = wallet.prepareSendLelantus( + txData: TxData( + recipients: [(address: _address!, amount: amount)], + ), + ); + break; + + case FiroType.spark: + txDataFuture = wallet.prepareSendSpark( + txData: TxData( + recipients: ref.read(pValidSparkSendToAddress) + ? null + : [(address: _address!, amount: amount)], + sparkRecipients: ref.read(pValidSparkSendToAddress) + ? [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + ) + ] + : null, + ), + ); + break; + } } else { final memo = coin == Coin.stellar || coin == Coin.stellarTestnet ? memoController.text @@ -610,6 +830,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,9 +939,9 @@ class _SendViewState extends ConsumerState { ), ); - if (coin == Coin.firo || coin == Coin.firoTestNet) { + if (isFiro) { ref.listen(publicPrivateBalanceStateProvider, (previous, next) { - if (_amountToSend == null) { + if (ref.read(pSendAmount) == null) { setState(() { _calculateFeesFuture = calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals)); @@ -728,7 +949,7 @@ class _SendViewState extends ConsumerState { } else { setState(() { _calculateFeesFuture = calculateFees( - _amountToSend!, + ref.read(pSendAmount)!, ); }); } @@ -830,10 +1051,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 +1069,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 @@ -984,8 +1211,7 @@ class _SendViewState extends ConsumerState { ), onChanged: (newValue) { _address = newValue.trim(); - _updatePreviewButtonState( - _address, _amountToSend); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = newValue.isNotEmpty; @@ -1022,9 +1248,8 @@ class _SendViewState extends ConsumerState { onTap: () { sendToController.text = ""; _address = ""; - _updatePreviewButtonState( - _address, - _amountToSend); + _setValidAddressProviders( + _address); setState(() { _addressToggleFlag = false; @@ -1066,9 +1291,8 @@ class _SendViewState extends ConsumerState { content.trim(); _address = content.trim(); - _updatePreviewButtonState( - _address, - _amountToSend); + _setValidAddressProviders( + _address); setState(() { _addressToggleFlag = sendToController @@ -1102,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(), - ) + ), ], ), ), @@ -1245,7 +1339,11 @@ class _SendViewState extends ConsumerState { const SizedBox( height: 10, ), - if (isStellar) + if (isStellar || + (ref.watch(pValidSparkSendToAddress) && + ref.watch( + publicPrivateBalanceStateProvider) != + FiroType.lelantus)) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1326,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(); @@ -1355,21 +1494,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 +1553,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 +1631,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 { @@ -1623,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, @@ -1746,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 @@ -1935,14 +2037,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( @@ -1962,7 +2063,8 @@ class _SendViewState extends ConsumerState { amount: (Decimal.tryParse( cryptoAmountController .text) ?? - _amountToSend + ref + .watch(pSendAmount) ?.decimal ?? Decimal.zero) .toAmount( @@ -1993,14 +2095,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( @@ -2127,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/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/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/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 04bdc18e3..6cc47a0d4 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -15,14 +15,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:stackwallet/db/hive/db.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; import 'package:stackwallet/notifications/show_flush_bar.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/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -217,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, ), @@ -345,9 +252,6 @@ class HiddenSettings extends StatelessWidget { } }, ), - const SizedBox( - height: 12, - ), Consumer( builder: (_, ref, __) { return GestureDetector( @@ -366,221 +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["tags"].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, - // ), - // 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), - // ), - // ), - // ), ], ), ), diff --git a/lib/pages/wallet_view/sub_widgets/tx_icon.dart b/lib/pages/wallet_view/sub_widgets/tx_icon.dart index 37ab9617c..11920f7c2 100644 --- a/lib/pages/wallet_view/sub_widgets/tx_icon.dart +++ b/lib/pages/wallet_view/sub_widgets/tx_icon.dart @@ -40,13 +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 ((!isReceived && subType == TransactionSubType.mint) || + (subType == TransactionSubType.sparkMint && + type == TransactionType.sentToSelf)) { if (isCancelled) { return Assets.svg.anonymizeFailed; } @@ -91,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) { @@ -104,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/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/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 f191a3439..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,41 +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) { - 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() { @@ -121,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: @@ -130,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 f20b2a299..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 @@ -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) { @@ -107,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) => ( @@ -131,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) => ( @@ -164,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) { 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) { diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 3d5577a0f..7a8e02844 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'; @@ -71,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'; @@ -118,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; @@ -174,6 +174,8 @@ class _WalletViewState extends ConsumerState { _shouldDisableAutoSyncOnLogOut = false; } + isSparkWallet = wallet is SparkInterface; + if (coin == Coin.firo && (wallet as FiroWallet).lelantusCoinIsarRescanRequired) { _rescanningOnOpen = true; @@ -433,7 +435,8 @@ class _WalletViewState extends ConsumerState { } try { - await firoWallet.anonymizeAllPublicFunds(); + // await firoWallet.anonymizeAllLelantus(); + await firoWallet.anonymizeAllSpark(); shouldPop = true; if (mounted) { Navigator.of(context).popUntil( @@ -760,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( @@ -951,20 +954,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/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/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_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 5a04541d9..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,14 +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'; @@ -29,7 +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'; @@ -57,10 +63,15 @@ class _DesktopReceiveState 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); - if (wallet is Bip39HDWallet) { + if (wallet is MultiAddressInterface) { bool shouldPop = false; unawaited( showDialog( @@ -93,130 +104,370 @@ 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; coin = ref.read(pWalletInfo(walletId)).coin; 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: [ - 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, + 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, + ), + ), ), ), - 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 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, ), - const Spacer(), - Row( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: RoundedWhiteContainer( + child: Column( children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 15, - height: 15, - color: Theme.of(context) - .extension()! - .infoItemIcons, + 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( - width: 4, + height: 8, ), - Text( - "Copy", - style: STextStyles.link2(context), + Row( + children: [ + Expanded( + child: Text( + _sparkAddress ?? "Error", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ], ), ], ), - ], + ), ), - const SizedBox( - height: 8, - ), - Row( - children: [ - Expanded( - child: Text( - receivingAddress, - 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: ref.watch(pWalletReceivingAddress(walletId))), + ); + 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( + ref.watch(pWalletReceivingAddress(walletId)), + 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( @@ -226,7 +477,7 @@ class _DesktopReceiveState extends ConsumerState { child: QrImageView( data: AddressUtils.buildUriString( coin, - receivingAddress, + _qrcodeContent ?? "", {}, ), size: 200, @@ -267,7 +518,7 @@ class _DesktopReceiveState extends ConsumerState { RouteGenerator.generateRoute( RouteSettings( name: GenerateUriQrCodeView.routeName, - arguments: Tuple2(coin, receivingAddress), + arguments: Tuple2(coin, _qrcodeContent ?? ""), ), ), ], @@ -284,7 +535,7 @@ class _DesktopReceiveState extends ConsumerState { shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, builder: (_) => GenerateUriQrCodeView( coin: coin, - receivingAddress: receivingAddress, + receivingAddress: _qrcodeContent ?? "", ), settings: const RouteSettings( name: GenerateUriQrCodeView.routeName, 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..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 @@ -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'; @@ -112,7 +114,6 @@ class _DesktopSendState extends ConsumerState { String? _note; String? _onChainNote; - Amount? _amountToSend; Amount? _cachedAmountToSend; String? _address; @@ -137,20 +138,22 @@ 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)) { - if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - availableBalance = wallet.info.cachedBalance.spendable; - // (manager.wallet as FiroWallet).availablePrivateBalance(); - } else { - availableBalance = wallet.info.cachedBalanceSecondary.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; - ; } final coinControlEnabled = @@ -312,14 +315,71 @@ 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: + if (ref.read(pValidSparkSendToAddress)) { + 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: + txDataFuture = wallet.prepareSendLelantus( + txData: TxData( + recipients: [(address: _address!, amount: amount)], + ), + ); + break; + + case FiroType.spark: + txDataFuture = wallet.prepareSendSpark( + txData: TxData( + recipients: ref.read(pValidSparkSendToAddress) + ? null + : [(address: _address!, amount: amount)], + sparkRecipients: ref.read(pValidSparkSendToAddress) + ? [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + ) + ] + : null, + ), + ); + break; + } } else { final memo = isStellar ? memoController.text : null; txDataFuture = wallet.prepareSend( @@ -382,7 +442,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( @@ -469,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, @@ -492,44 +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 isValidAddress = ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(address ?? ""); - 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 { @@ -567,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; }); @@ -584,7 +629,7 @@ class _DesktopSendState extends ConsumerState { _address = qrResult.rawContent; sendToController.text = _address ?? ""; - _updatePreviewButtonState(_address, _amountToSend); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = sendToController.text.isNotEmpty; }); @@ -598,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) { @@ -614,7 +678,7 @@ class _DesktopSendState extends ConsumerState { sendToController.text = content; _address = content; - _updatePreviewButtonState(_address, _amountToSend); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = sendToController.text.isNotEmpty; }); @@ -643,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, ); @@ -672,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; @@ -682,17 +747,29 @@ class _DesktopSendState extends ConsumerState { // Format.decimalAmountToSatoshis( // _amountToSend!)); // }); - _updatePreviewButtonState(_address, _amountToSend); + ref.read(pSendAmount.notifier).state = amount; } 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); @@ -700,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, ), ); } @@ -713,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); @@ -748,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)!; } } }); @@ -821,7 +900,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,22 +910,42 @@ 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, 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( @@ -862,7 +961,7 @@ class _DesktopSendState extends ConsumerState { ), ), DropdownMenuItem( - value: "Public", + value: FiroType.public, child: Row( children: [ Text( @@ -882,9 +981,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; }); } @@ -917,7 +1016,7 @@ class _DesktopSendState extends ConsumerState { ), ), ), - if (coin == Coin.firo) + if (coin == Coin.firo || coin == Coin.firoTestNet) const SizedBox( height: 20, ), @@ -1159,7 +1258,7 @@ class _DesktopSendState extends ConsumerState { ), onChanged: (newValue) { _address = newValue; - _updatePreviewButtonState(_address, _amountToSend); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = newValue.isNotEmpty; @@ -1199,8 +1298,7 @@ class _DesktopSendState extends ConsumerState { onTap: () { sendToController.text = ""; _address = ""; - _updatePreviewButtonState( - _address, _amountToSend); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = false; }); @@ -1261,10 +1359,7 @@ class _DesktopSendState extends ConsumerState { _address = entry.address; - _updatePreviewButtonState( - _address, - _amountToSend, - ); + _setValidAddressProviders(_address); setState(() { _addressToggleFlag = true; @@ -1289,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(); @@ -1317,11 +1447,17 @@ class _DesktopSendState extends ConsumerState { } }, ), - if (isStellar) + if (isStellar || + (ref.watch(pValidSparkSendToAddress) && + ref.watch(publicPrivateBalanceStateProvider) != + FiroType.lelantus)) const SizedBox( height: 10, ), - if (isStellar) + if (isStellar || + (ref.watch(pValidSparkSendToAddress) && + ref.watch(publicPrivateBalanceStateProvider) != + FiroType.lelantus)) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1387,8 +1523,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: [ @@ -1487,14 +1627,32 @@ class _DesktopSendState extends ConsumerState { 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); + 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) @@ -1532,7 +1690,7 @@ class _DesktopSendState extends ConsumerState { .watch( publicPrivateBalanceStateProvider.state) .state == - "Private" + FiroType.lelantus ? Text( "~${ref.watch(pAmountFormatter(coin)).format( Amount( @@ -1595,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/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/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/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/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; } } 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..5bc9bbb32 --- /dev/null +++ b/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart @@ -0,0 +1,267 @@ +/* + * 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: 9, + child: Text( + "LTag Hash", + 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( + "Group Id", + 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: 9, + child: SelectableText( + _coins[index].lTagHash, + 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].groupId.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/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((_) { 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/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/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) { 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/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; 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), 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/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 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/isar/models/spark_coin.dart b/lib/wallets/isar/models/spark_coin.dart new file mode 100644 index 000000000..d3ef6825c --- /dev/null +++ b/lib/wallets/isar/models/spark_coin.dart @@ -0,0 +1,148 @@ +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 int groupId; + + final List? 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; + + final int? height; + + final String? serializedCoinB64; + final String? contextB64; + + @ignore + BigInt get value => BigInt.parse(valueIntString); + + @ignore + BigInt get diversifier => BigInt.parse(diversifierIntString); + + SparkCoin({ + required this.walletId, + required this.type, + required this.isUsed, + required this.groupId, + this.nonce, + 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, + this.height, + this.serializedCoinB64, + this.contextB64, + }); + + SparkCoin copyWith({ + SparkCoinType? type, + bool? isUsed, + int? groupId, + List? nonce, + String? address, + String? txHash, + BigInt? value, + String? memo, + List? serialContext, + BigInt? diversifier, + List? encryptedDiversifier, + List? serial, + List? tag, + String? lTagHash, + int? height, + String? serializedCoinB64, + String? contextB64, + }) { + return 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, + 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, + height: height ?? this.height, + serializedCoinB64: serializedCoinB64 ?? this.serializedCoinB64, + contextB64: contextB64 ?? this.contextB64, + ); + } + + @override + String toString() { + return 'SparkCoin(' + 'walletId: $walletId' + ', type: $type' + ', isUsed: $isUsed' + ', groupId: $groupId' + ', k: $nonce' + ', address: $address' + ', txHash: $txHash' + ', value: $value' + ', memo: $memo' + ', serialContext: $serialContext' + ', diversifier: $diversifier' + ', encryptedDiversifier: $encryptedDiversifier' + ', serial: $serial' + ', 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 new file mode 100644 index 000000000..75cd92b62 --- /dev/null +++ b/lib/wallets/isar/models/spark_coin.g.dart @@ -0,0 +1,3456 @@ +// 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'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: 3, + name: r'encryptedDiversifier', + type: IsarType.longList, + ), + 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: 6, + name: r'isUsed', + type: IsarType.bool, + ), + r'lTagHash': PropertySchema( + id: 7, + name: r'lTagHash', + type: IsarType.string, + ), + r'memo': PropertySchema( + id: 8, + name: r'memo', + type: IsarType.string, + ), + r'nonce': PropertySchema( + id: 9, + name: r'nonce', + type: IsarType.longList, + ), + r'serial': PropertySchema( + id: 10, + name: r'serial', + type: IsarType.longList, + ), + r'serialContext': PropertySchema( + id: 11, + name: r'serialContext', + type: IsarType.longList, + ), + r'serializedCoinB64': PropertySchema( + id: 12, + name: r'serializedCoinB64', + type: IsarType.string, + ), + r'tag': PropertySchema( + id: 13, + name: r'tag', + type: IsarType.longList, + ), + r'txHash': PropertySchema( + id: 14, + name: r'txHash', + type: IsarType.string, + ), + r'type': PropertySchema( + id: 15, + name: r'type', + type: IsarType.byte, + enumMap: _SparkCointypeEnumValueMap, + ), + r'valueIntString': PropertySchema( + id: 16, + name: r'valueIntString', + type: IsarType.string, + ), + r'walletId': PropertySchema( + id: 17, + 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; + { + final value = object.contextB64; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + bytesCount += 3 + object.diversifierIntString.length * 3; + { + final value = object.encryptedDiversifier; + 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.nonce; + if (value != null) { + bytesCount += 3 + value.length * 8; + } + } + { + 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.serializedCoinB64; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + 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.contextB64); + writer.writeString(offsets[2], object.diversifierIntString); + writer.writeLongList(offsets[3], object.encryptedDiversifier); + 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( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = SparkCoin( + address: reader.readString(offsets[0]), + contextB64: reader.readStringOrNull(offsets[1]), + diversifierIntString: reader.readString(offsets[2]), + encryptedDiversifier: reader.readLongList(offsets[3]), + 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[16]), + walletId: reader.readString(offsets[17]), + ); + 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.readStringOrNull(offset)) as P; + case 2: + return (reader.readString(offset)) as P; + case 3: + return (reader.readLongList(offset)) as P; + case 4: + return (reader.readLong(offset)) as P; + case 5: + return (reader.readLongOrNull(offset)) as P; + case 6: + return (reader.readBool(offset)) as P; + case 7: + return (reader.readString(offset)) as P; + case 8: + 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.readLongList(offset)) as P; + case 12: + return (reader.readStringOrNull(offset)) as P; + case 13: + 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 16: + return (reader.readString(offset)) as P; + case 17: + 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 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, { + 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 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( + 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) { + 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 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 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( + 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 + 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( + 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 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) { + return query.addSortBy(r'diversifierIntString', Sort.asc); + }); + } + + QueryBuilder + sortByDiversifierIntStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'diversifierIntString', Sort.desc); + }); + } + + 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); + }); + } + + 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); + }); + } + + 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 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); + }); + } + + 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 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) { + return query.addSortBy(r'diversifierIntString', Sort.asc); + }); + } + + QueryBuilder + thenByDiversifierIntStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'diversifierIntString', Sort.desc); + }); + } + + 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); + }); + } + + 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); + }); + } + + 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 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); + }); + } + + 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 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) { + return query.addDistinctBy(r'diversifierIntString', + caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByEncryptedDiversifier() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'encryptedDiversifier'); + }); + } + + QueryBuilder distinctByGroupId() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'groupId'); + }); + } + + QueryBuilder distinctByHeight() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'height'); + }); + } + + QueryBuilder distinctByIsUsed() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isUsed'); + }); + } + + 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 distinctByNonce() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'nonce'); + }); + } + + 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 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'); + }); + } + + 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 contextB64Property() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'contextB64'); + }); + } + + 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 groupIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'groupId'); + }); + } + + QueryBuilder heightProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'height'); + }); + } + + QueryBuilder isUsedProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isUsed'); + }); + } + + 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> nonceProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'nonce'); + }); + } + + 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 + serializedCoinB64Property() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'serializedCoinB64'); + }); + } + + 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/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 455790f67..9148d182e 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, + String memo, + })>? sparkRecipients; + final List? sparkMints; + TxData({ this.feeRateType, this.feeRateAmount, @@ -85,6 +94,8 @@ class TxData { this.txSubType, this.mintsMapLelantus, this.tezosOperationsList, + this.sparkRecipients, + this.sparkMints, }); Amount? get amount => recipients != null && recipients!.isNotEmpty @@ -93,6 +104,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; @@ -127,6 +145,14 @@ class TxData { TransactionSubType? txSubType, List>? mintsMapLelantus, tezart.OperationsList? tezosOperationsList, + List< + ({ + String address, + Amount amount, + String memo, + })>? + sparkRecipients, + List? sparkMints, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -159,6 +185,8 @@ class TxData { txSubType: txSubType ?? this.txSubType, mintsMapLelantus: mintsMapLelantus ?? this.mintsMapLelantus, tezosOperationsList: tezosOperationsList ?? this.tezosOperationsList, + sparkRecipients: sparkRecipients ?? this.sparkRecipients, + sparkMints: sparkMints ?? this.sparkMints, ); } @@ -193,5 +221,7 @@ class TxData { 'txSubType: $txSubType, ' 'mintsMapLelantus: $mintsMapLelantus, ' 'tezosOperationsList: $tezosOperationsList, ' + 'sparkRecipients: $sparkRecipients, ' + 'sparkMints: $sparkMints, ' '}'; } 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..0edf16cb1 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) @@ -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/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..802c21b77 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) @@ -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, ); @@ -288,6 +288,7 @@ class EcashWallet extends Bip39HDWallet outputs: List.unmodifiable(outputs), type: type, subType: subType, + otherData: null, ); txns.add(tx); diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index bcb90ac1b..97f1b192a 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -1,22 +1,25 @@ +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/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/extensions/extensions.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/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) @@ -27,6 +30,9 @@ class FiroWallet extends Bip39HDWallet FiroWallet(CryptoCurrencyNetwork network) : super(Firo(network)); + @override + int get isarTransactionVersion => 2; + @override FilterOperation? get changeAddressFilterOperation => FilterGroup.and(standardChangeAddressFilters); @@ -37,49 +43,43 @@ 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++) { - if (allTransactions[i]["txid"] == txid) { - return true; - } - } - return false; - } - @override Future updateTransactions() async { - final allAddresses = await fetchAllOwnAddresses(); + List
allAddressesOld = await fetchAddressesForElectrumXScan(); - Set receivingAddresses = allAddresses + Set receivingAddresses = allAddressesOld .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) + .map((e) => convertAddressString(e.value)) .toSet(); - Set changeAddresses = allAddresses + + Set changeAddresses = allAddressesOld .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) + .map((e) => convertAddressString(e.value)) .toSet(); + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + final List> allTxHashes = - await fetchHistory(allAddresses.map((e) => e.value).toList()); + await fetchHistory(allAddressesSet); + + final sparkCoins = await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .findAll(); + + 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"] == coin.txHash) == -1) { + final info = { + "tx_hash": coin.txHash, + "height": coin.height, + }; + allTxHashes.add(info); + } + } List> allTransactions = []; @@ -110,8 +110,6 @@ class FiroWallet extends Bip39HDWallet } } - // final currentHeight = await chainHeight; - for (final txHash in allTxHashes) { // final storedTx = await db // .getTransactions(walletId) @@ -127,508 +125,372 @@ class FiroWallet extends Bip39HDWallet coin: info.coin, ); - if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { - tx["address"] = await mainDB - .getAddresses(walletId) - .filter() - .valueEqualTo(txHash["address"] as String) - .findFirst(); - tx["height"] = txHash["height"]; + // 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> txnsData = []; + final List txns = []; - for (final txObject in allTransactions) { - final inputList = txObject["vin"] as List; - final outputList = txObject["vout"] as List; + 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; + final bool isMySpark = sparkTxids.contains(txData["txid"] as String); - // check if tx is Mint or jMint - for (final output in outputList) { - if (output["scriptPubKey"]?["type"] == "lelantusmint") { - final asm = output["scriptPubKey"]?["asm"] 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 = []; + 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; - break; } else if (asm.startsWith("OP_LELANTUSMINT")) { isMint = true; - break; } else { Logging.instance.log( - "Unknown mint op code found for lelantusmint tx: ${txObject["txid"]}", + "Unknown mint op code found for lelantusmint tx: ${txData["txid"]}", level: LogLevel.Error, ); } } else { Logging.instance.log( - "ASM for lelantusmint tx: ${txObject["txid"]} is null!", + "ASM for lelantusmint tx: ${txData["txid"]} is null!", level: LogLevel.Error, ); } } - } - - 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, + if (outMap["scriptPubKey"]?["type"] == "sparkmint" || + outMap["scriptPubKey"]?["type"] == "sparksmint") { + final asm = outMap["scriptPubKey"]?["asm"] as String?; + if (asm != null) { + if (asm.startsWith("OP_SPARKMINT") || + asm.startsWith("OP_SPARKSMINT")) { + 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, ); } - - 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, + OutputV2 output = OutputV2.fromElectrumXJson( + outMap, + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // don't know yet if wallet owns. Need addresses first + walletOwns: false, ); - txnsData.add(Tuple2(tx, null)); + // 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; - // Otherwise parse JMint transaction =================================== - } else if (isJMint) { - Amount jMintFees = Amount( + 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()) + .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); + } else if (isSparkMint && isMySpark) { + wasReceivedInThisWallet = true; + if (output.addresses.contains(sparkChangeAddress)) { + changeAmountReceivedInThisWallet += output.value; + } else { + amountReceivedInThisWallet += output.value; + } + } + + outputs.add(output); + } + + if (isJMint || isSparkSpend) { + anonFees = 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()); + // 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, ); - jMintFees += fees; + anonFees = anonFees! + fees; } + } else if (isSparkMint) { + final address = map["address"] as String?; + final value = map["valueSat"] as int?; - 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; - } + 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?; - 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; - } + if (address != null && value != null) { + valueStringSats = value.toString(); + addresses.add(address); } - - 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( + } else if (isJMint) { + // anon fees + final nFee = Decimal.tryParse(map["nFees"].toString()); + if (nFee != null) { + final fees = Amount.fromDecimal( + nFee, 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; - } + anonFees = anonFees! + 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?, - ), - ); - } - - // parse outputs - List outs = []; - for (final output in outputList) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, + } else if (coinbase == null && txid != null && vout != null) { + final inputTx = await electrumXCachedClient.getTransaction( + txHash: txid, + coin: cryptoCurrency.coin, ); - // add value to total - totalOutputValue += value; + final prevOutJson = Map.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) + as Map); - // 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 prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + 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"); } - 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, + 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, ); - txnsData.add(Tuple2(tx, transactionAddress)); + 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); } + + 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 + } + } + } 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.addNewTransactionData(txnsData, walletId); + await mainDB.updateOrPutTransactionV2s(txns); } @override @@ -689,6 +551,7 @@ class FiroWallet extends Bip39HDWallet await mainDB.deleteWalletBlockchainData(walletId); } + // lelantus final latestSetId = await electrumXClient.getLelantusLatestCoinId(); final setDataMapFuture = getSetDataMap(latestSetId); final usedSerialNumbersFuture = @@ -696,6 +559,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...", @@ -799,16 +673,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.dart b/lib/wallets/wallet/wallet.dart index ac5e602f9..54a1b0aa5 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); @@ -289,7 +290,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; @@ -417,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(); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index be786ff52..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++) { @@ -1641,7 +1651,7 @@ mixin ElectrumXInterface on Bip39HDWallet { @override Future updateUTXOs() async { - final allAddresses = await fetchAllOwnAddresses(); + final allAddresses = await fetchAddressesForElectrumXScan(); try { final fetchedUtxoList = >>[]; @@ -1856,7 +1866,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/lelantus_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart index fed2c47e0..a4dec9157 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(); @@ -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 678ddb77f..56b188e41 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -1,13 +1,104 @@ -import 'dart:typed_data'; +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'; -import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/balance.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'; +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'; +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; + 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, + }) => + LibSpark.validateAddress(address: address, isTestNet: isTestNet); + + @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? + + 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: kSparkChange, + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, + ); + } + + // 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() @@ -18,31 +109,37 @@ 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; // 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++; + } - // TODO: use real data - final String derivationPath = ""; - final Uint8List publicKey = Uint8List(0); // incomingViewKey? - final String addressString = ""; + final root = await getRootHDNode(); + final String derivationPath; + if (cryptoCurrency.network == CryptoCurrencyNetwork.test) { + derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex"; + } else { + derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex"; + } + final keys = root.derivePath(derivationPath); + + final String addressString = await LibSpark.getAddress( + privateKey: keys.privateKey.data, + index: kDefaultSparkIndex, + 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, @@ -51,85 +148,403 @@ 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 Future prepareSendSpark({ required TxData txData, }) async { - // 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(); + // There should be at least one output. + if (!(txData.recipients?.isNotEmpty == true || + txData.sparkRecipients?.isNotEmpty == true)) { + throw Exception("No recipients provided."); + } - // - // 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, + if (txData.sparkRecipients?.isNotEmpty == true && + txData.sparkRecipients!.length >= SPARK_OUT_LIMIT_PER_TX - 1) { + throw Exception("Spark shielded output limit exceeded."); + } - throw UnimplementedError(); + 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() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .and() + .heightIsNotNull() + .and() + .not() + .valueIntStringEqualTo("0") + .findAll(); + + final available = info.cachedBalanceTertiary.spendable; + + if (txAmount > available) { + throw Exception("Insufficient Spark balance"); + } + + final bool isSendAll = available == txAmount; + + // prepare coin data for ffi + 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 = []; + 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 + .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(); + + final root = await getRootHDNode(); + final String derivationPath; + if (cryptoCurrency.network == CryptoCurrencyNetwork.test) { + derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex"; + } else { + derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex"; + } + final privateKey = root.derivePath(derivationPath).privateKey.data; + + final txb = btc.TransactionBuilder( + network: _bitcoinDartNetwork, + ); + txb.setLockTime(await chainHeight); + txb.setVersion(3 | (9 << 16)); + + List<({String address, Amount amount})>? recipientsWithFeeSubtracted; + List< + ({ + String address, + Amount amount, + String memo, + })>? 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( + 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; + } + + if ((txData.sparkRecipients?.length ?? 0) > 0) { + sparkRecipientsWithFeeSubtracted = []; + } + if (recipientCount > 0) { + recipientsWithFeeSubtracted = []; + } + + 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(totalRecipientCount)), + 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; + } + recipientsWithFeeSubtracted!.add( + ( + address: txData.recipients![i].address, + amount: Amount( + rawValue: txData.recipients![i].amount.raw - + (estimatedFee ~/ BigInt.from(totalRecipientCount)), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ), + ); + + final scriptPubKey = btc.Address.addressToOutputScript( + txData.recipients![i].address, + _bitcoinDartNetwork, + ); + txb.addOutput( + scriptPubKey, + recipientsWithFeeSubtracted[i].amount.raw.toInt(), + ); + } + + final extractedTx = txb.buildIncomplete(); + extractedTx.addInput( + '0000000000000000000000000000000000000000000000000000000000000000' + .toUint8ListFromHex, + 0xffffffff, + 0xffffffff, + "d3".toUint8ListFromHex, // OP_SPARKSPEND + ); + extractedTx.setPayload(Uint8List(0)); + + 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) { + extractedTx.addOutput(outputScript, 0); + } + + 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(), + 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 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 // 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 latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); - // TODO improve performance by adding this call to the cached client - final anonymitySet = await electrumXClient.getSparkAnonymitySet( - coinGroupId: latestSparkCoinId.toString(), - ); + final blockHash = await _getCachedSparkBlockHash(); - // TODO loop over set and see which coins are ours using the FFI call `identifyCoin` - List myCoins = []; + final anonymitySetFuture = blockHash == null + ? electrumXCachedClient.getSparkAnonymitySet( + groupId: latestSparkCoinId.toString(), + coin: info.coin, + ) + : electrumXClient.getSparkAnonymitySet( + coinGroupId: latestSparkCoinId.toString(), + startBlockHash: blockHash, + ); + final spentCoinTagsFuture = + electrumXClient.getSparkUsedCoinsTags(startNumber: 0); + // electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin); - // fetch metadata for myCoins + final futureResults = await Future.wait([ + anonymitySetFuture, + spentCoinTagsFuture, + ]); - // check against spent list (this call could possibly also be cached later on) - final spentCoinTags = await electrumXClient.getSparkUsedCoinsTags( - startNumber: 0, - ); + final anonymitySet = futureResults[0] as Map; + final spentCoinTags = futureResults[1] as Set; - // create list of Spark Coin isar objects + final List myCoins = []; + + if (anonymitySet["coins"] is List && + (anonymitySet["coins"] as List).isNotEmpty) { + final root = await getRootHDNode(); + final privateKeyHexSet = paths + .map( + (e) => root.derivePath(e).privateKey.data.toHex, + ) + .toSet(); + + final identifiedCoins = await compute( + _identifyCoins, + ( + anonymitySetCoins: anonymitySet["coins"] as List, + groupId: latestSparkCoinId, + spentCoinTags: spentCoinTags, + privateKeyHexSet: privateKeyHexSet, + walletId: walletId, + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, + ), + ); + + myCoins.addAll(identifiedCoins); + + // update blockHash in cache + final String newBlockHash = + base64ToReverseHex(anonymitySet["blockHash"] as String); + 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? - - throw UnimplementedError(); + // refresh spark balance + await refreshSparkBalance(); } catch (e, s) { // todo logging @@ -137,35 +552,95 @@ 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 root = await getRootHDNode(); + final privateKeyHexSet = + paths.map((e) => root.derivePath(e).privateKey.data.toHex).toSet(); - 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 myCoins = await compute( + _identifyCoins, + ( + anonymitySetCoins: anonymitySet["coins"] as List, + groupId: anonymitySet["coinGroupID"] as int, + spentCoinTags: spentCoinTags, + privateKeyHexSet: privateKeyHexSet, + walletId: walletId, + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, + ), ); - // 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 @@ -173,25 +648,597 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } } - /// Transparent to Spark (mint) transaction creation + // 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, + required bool autoMintAll, + }) async { + // pre checks + 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); + + 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 + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); // deep copy + final feesObject = await fees; + 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(); + + // deep copy + final remainingOutputs = outputs_ + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); + final List singleTxOutputs = []; + + if (autoMintAll) { + singleTxOutputs.add( + MutableSparkRecipient( + (await getCurrentReceivingSparkAddress())!.value, + mintedValue, + "", + ), + ); + } else { + BigInt remainingMintValue = BigInt.parse(mintedValue.toString()); + + 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( + estimateTxFee( + vSize: nBytes, + feeRatePerKB: feesObject.medium, + ), + ); // 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; + } + + // deep copy + outputs_ = remainingOutputs + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); + + 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(); + + // TODO: see todo at top of this function + assert(outputs.length == 1); + + final data = TxData( + sparkRecipients: vout + .where((e) => e.$1 is Uint8List) // ignore change + .map( + (e) => ( + address: outputs.first + .address, // for display purposes on confirm tx screen. See todos above + 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, + ), + usedUTXOs: vin.map((e) => e.utxo).toList(), + ); + + if (nFeeRet.toInt() < data.vSize!) { + throw Exception("fee is less than vSize"); + } + + 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 { + try { + const subtractFeeFromAmount = true; // must be true for mint all + final currentHeight = await chainHeight; + + final spendableUtxos = await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .isBlockedEqualTo(false) + .and() + .group((q) => q.usedEqualTo(false).or().usedIsNull()) + .and() + .valueGreaterThan(0) + .findAll(); + + spendableUtxos.removeWhere( + (e) => !e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + ), + ); + + if (spendableUtxos.isEmpty) { + throw Exception("No available UTXOs found to anonymize"); + } + + final mints = 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), + "", + ), + ], + ); + + await confirmSparkMintTransactions(txData: TxData(sparkMints: mints)); + } catch (e, s) { + Logging.instance.log( + "Exception caught in anonymizeAllSpark(): $e\n$s", + level: LogLevel.Warning, + ); + rethrow; + } + } + + /// 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 + try { + 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(); - // 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 - // 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. + final total = recipients + .map((e) => e.value) + .reduce((value, element) => value += element); - throw UnimplementedError(); + 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( + "Exception caught in prepareSparkMintTransaction(): $e\n$s", + level: LogLevel.Warning, + ); + rethrow; + } + } + + Future confirmSparkMintTransactions({required TxData txData}) async { + final futures = txData.sparkMints!.map((e) => confirmSend(txData: e)); + return txData.copyWith(sparkMints: await Future.wait(futures)); } @override @@ -200,9 +1247,228 @@ 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; } + + // ====================== 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 _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); + }); + } + } + + 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) => + base64Decode(LineSplitter.split(source).join()) + .reversed + .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( + ({ + List anonymitySetCoins, + int groupId, + 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!), + groupId: args.groupId, + 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; +} + +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); + + @override + String toString() { + return 'MutableSparkRecipient{ address: $address, value: $value, memo: $memo }'; + } } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 3a02b87cf..bb9965d23 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -16,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/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 diff --git a/pubspec.lock b/pubspec.lock index c4572ea32..34b432940 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -659,6 +659,15 @@ packages: relative: true source: path version: "0.0.1" + flutter_libsparkmobile: + dependency: "direct main" + description: + path: "." + ref: "6e2b2650a84fc5832dc676f8d016e530ae77851f" + resolved-ref: "6e2b2650a84fc5832dc676f8d016e530ae77851f" + url: "https://github.com/cypherstack/flutter_libsparkmobile.git" + source: git + version: "0.0.1" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 57ac07659..3e566a466 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,11 @@ dependencies: lelantus: path: ./crypto_plugins/flutter_liblelantus + flutter_libsparkmobile: + git: + url: https://github.com/cypherstack/flutter_libsparkmobile.git + ref: d54b4a1f492e48696c3df27eb8c31131a681cbc2 + flutter_libmonero: path: ./crypto_plugins/flutter_libmonero @@ -160,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 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( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 701de9701..f11baedd5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_libsparkmobile tor_ffi_plugin )