From f697aeb043626864f516e5d18ada3a582d72169e Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 29 Dec 2023 09:26:32 -0600 Subject: [PATCH] WIP handle spark transaction parsing --- lib/wallets/wallet/impl/firo_wallet.dart | 130 +++++++++++++----- .../spark_interface.dart | 30 ++++ 2 files changed, 128 insertions(+), 32 deletions(-) diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 0047262e9..97f1b192a 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:decimal/decimal.dart'; +import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; @@ -9,6 +10,7 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; @@ -60,17 +62,20 @@ class FiroWallet extends Bip39HDWallet final List> allTxHashes = await fetchHistory(allAddressesSet); - final sparkTxids = await mainDB.isar.sparkCoins + final sparkCoins = await mainDB.isar.sparkCoins .where() .walletIdEqualToAnyLTagHash(walletId) - .txHashProperty() .findAll(); - for (final txid in sparkTxids) { + final Set sparkTxids = {}; + + for (final coin in sparkCoins) { + sparkTxids.add(coin.txHash); // check for duplicates before adding to list - if (allTxHashes.indexWhere((e) => e["tx_hash"] == txid) == -1) { + if (allTxHashes.indexWhere((e) => e["tx_hash"] == coin.txHash) == -1) { final info = { - "tx_hash": txid, + "tx_hash": coin.txHash, + "height": coin.height, }; allTxHashes.add(info); } @@ -148,6 +153,17 @@ class FiroWallet extends Bip39HDWallet bool isSparkMint = false; bool isMasterNodePayment = false; final bool isSparkSpend = txData["type"] == 9 && txData["version"] == 3; + final bool isMySpark = sparkTxids.contains(txData["txid"] as String); + + final sparkCoinsInvolved = + sparkCoins.where((e) => e.txHash == txData["txid"]); + if (isMySpark && sparkCoinsInvolved.isEmpty) { + Logging.instance.log( + "sparkCoinsInvolved is empty and should not be! (ignoring tx parsing)", + level: LogLevel.Error, + ); + continue; + } // parse outputs final List outputs = []; @@ -173,10 +189,12 @@ class FiroWallet extends Bip39HDWallet ); } } - if (outMap["scriptPubKey"]?["type"] == "sparkmint") { + if (outMap["scriptPubKey"]?["type"] == "sparkmint" || + outMap["scriptPubKey"]?["type"] == "sparksmint") { final asm = outMap["scriptPubKey"]?["asm"] as String?; if (asm != null) { - if (asm.startsWith("OP_SPARKMINT")) { + if (asm.startsWith("OP_SPARKMINT") || + asm.startsWith("OP_SPARKSMINT")) { isSparkMint = true; } else { Logging.instance.log( @@ -192,16 +210,6 @@ class FiroWallet extends Bip39HDWallet } } - if (isSparkSpend) { - // TODO - } else if (isSparkMint) { - // TODO - } else if (isMint || isJMint) { - // do nothing extra ? - } else { - // TODO - } - OutputV2 output = OutputV2.fromElectrumXJson( outMap, decimalPlaces: cryptoCurrency.fractionDigits, @@ -210,6 +218,46 @@ class FiroWallet extends Bip39HDWallet walletOwns: false, ); + // if (isSparkSpend) { + // // TODO? + // } else + if (isSparkMint) { + if (isMySpark) { + if (output.addresses.isEmpty && + output.scriptPubKeyHex.length >= 488) { + // likely spark related + final opByte = output.scriptPubKeyHex + .substring(0, 2) + .toUint8ListFromHex + .first; + if (opByte == OP_SPARKMINT || opByte == OP_SPARKSMINT) { + final serCoin = base64Encode(output.scriptPubKeyHex + .substring(2, 488) + .toUint8ListFromHex); + final coin = sparkCoinsInvolved + .where((e) => e.serializedCoinB64!.startsWith(serCoin)) + .firstOrNull; + + if (coin == null) { + // not ours + } else { + output = output.copyWith( + walletOwns: true, + valueStringSats: coin.value.toString(), + addresses: [ + coin.address, + ], + ); + } + } + } + } + } else if (isMint || isJMint) { + // do nothing extra ? + } else { + // TODO? + } + // if output was to my wallet, add value to amount received if (receivingAddresses .intersection(output.addresses.toSet()) @@ -223,6 +271,13 @@ class FiroWallet extends Bip39HDWallet wasReceivedInThisWallet = true; changeAmountReceivedInThisWallet += output.value; output = output.copyWith(walletOwns: true); + } else if (isSparkMint && isMySpark) { + wasReceivedInThisWallet = true; + if (output.addresses.contains(sparkChangeAddress)) { + changeAmountReceivedInThisWallet += output.value; + } else { + amountReceivedInThisWallet += output.value; + } } outputs.add(output); @@ -333,6 +388,32 @@ class FiroWallet extends Bip39HDWallet if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { wasSentFromThisWallet = true; input = input.copyWith(walletOwns: true); + } else if (isMySpark) { + final lTags = map["lTags"] as List?; + + if (lTags?.isNotEmpty == true) { + final List usedCoins = []; + for (final tag in lTags!) { + final components = (tag as String).split(","); + final x = components[0].substring(1); + final y = components[1].substring(0, components[1].length - 1); + + final hash = LibSpark.hashTag(x, y); + usedCoins.addAll(sparkCoins.where((e) => e.lTagHash == hash)); + } + + if (usedCoins.isNotEmpty) { + input = input.copyWith( + addresses: usedCoins.map((e) => e.address).toList(), + valueStringSats: usedCoins + .map((e) => e.value) + .reduce((value, element) => value += element) + .toString(), + walletOwns: true, + ); + wasSentFromThisWallet = true; + } + } } inputs.add(input); @@ -365,25 +446,10 @@ class FiroWallet extends Bip39HDWallet totalOut) { // definitely sent all to self type = TransactionType.sentToSelf; - } else if (isSparkMint) { - // probably sent to self - type = TransactionType.sentToSelf; } else if (amountReceivedInThisWallet == BigInt.zero) { // most likely just a typical send // do nothing here yet } - - // check vout 0 for special scripts - if (outputs.isNotEmpty) { - final output = outputs.first; - - // // check for fusion - // if (BchUtils.isFUZE(output.scriptPubKeyHex.toUint8ListFromHex)) { - // subType = TransactionSubType.cashFusion; - // } else { - // // check other cases here such as SLP or cash tokens etc - // } - } } } else if (wasReceivedInThisWallet) { // only found outputs owned by this wallet diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index d5900cbd9..c27b662ed 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -23,6 +23,8 @@ const kDefaultSparkIndex = 1; // TODO dart style constants. Maybe move to spark lib? const MAX_STANDARD_TX_WEIGHT = 400000; +const SPARK_CHANGE_D = 0x270F; + //https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16 const SPARK_OUT_LIMIT_PER_TX = 16; @@ -31,6 +33,16 @@ const OP_SPARKSMINT = 0xd2; const OP_SPARKSPEND = 0xd3; mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { + String? _sparkChangeAddressCached; + + /// Spark change address. Should generally not be exposed to end users. + String get sparkChangeAddress { + if (_sparkChangeAddressCached == null) { + throw Exception("_sparkChangeAddressCached was not initialized"); + } + return _sparkChangeAddressCached!; + } + static bool validateSparkAddress({ required String address, required bool isTestNet, @@ -45,6 +57,24 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { await mainDB.putAddress(address); } // TODO add other address types to wallet info? + if (_sparkChangeAddressCached == null) { + final root = await getRootHDNode(); + final String derivationPath; + if (cryptoCurrency.network == CryptoCurrencyNetwork.test) { + derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex"; + } else { + derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex"; + } + final keys = root.derivePath(derivationPath); + + _sparkChangeAddressCached = await LibSpark.getAddress( + privateKey: keys.privateKey.data, + index: kDefaultSparkIndex, + diversifier: SPARK_CHANGE_D, + isTestNet: cryptoCurrency.network == CryptoCurrencyNetwork.test, + ); + } + // await info.updateReceivingAddress( // newAddress: address.value, // isar: mainDB.isar,