diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index f543636ff..067069b50 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:math'; import 'package:isar/isar.dart'; @@ -102,6 +103,15 @@ class TransactionV2 { ...outputs.map((e) => e.addresses).expand((e) => e), }; + Amount? getAnonFee() { + try { + final map = jsonDecode(otherData!) as Map; + return Amount.fromSerializedJsonString(map["anonFees"] as String); + } catch (_) { + return null; + } + } + @override String toString() { return 'TransactionV2(\n' @@ -116,6 +126,7 @@ class TransactionV2 { ' version: $version,\n' ' inputs: $inputs,\n' ' outputs: $outputs,\n' + ' otherData: $otherData,\n' ')'; } } diff --git a/lib/pages/wallet_view/sub_widgets/tx_icon.dart b/lib/pages/wallet_view/sub_widgets/tx_icon.dart index 37ab9617c..c942bb621 100644 --- a/lib/pages/wallet_view/sub_widgets/tx_icon.dart +++ b/lib/pages/wallet_view/sub_widgets/tx_icon.dart @@ -56,6 +56,17 @@ class TxIcon extends ConsumerWidget { return Assets.svg.anonymize; } + if (subType == TransactionSubType.mint || + subType == TransactionSubType.sparkMint) { + if (isCancelled) { + return Assets.svg.anonymizeFailed; + } + if (isPending) { + return Assets.svg.anonymizePending; + } + return Assets.svg.anonymize; + } + if (isReceived) { if (isCancelled) { return assets.receiveCancelled; diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart index f191a3439..7ec4e1b78 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart @@ -50,7 +50,9 @@ class _TransactionCardStateV2 extends ConsumerState { ref.read(pWallets).getWallet(walletId).cryptoCurrency.minConfirms, ); - if (_transaction.subType == TransactionSubType.cashFusion) { + if (_transaction.subType == TransactionSubType.cashFusion || + _transaction.subType == TransactionSubType.sparkMint || + _transaction.subType == TransactionSubType.mint) { if (confirmedStatus) { return "Anonymized"; } else { diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart index f20b2a299..4eada7202 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart @@ -95,7 +95,12 @@ class _TransactionV2DetailsViewState minConfirms = ref.read(pWallets).getWallet(walletId).cryptoCurrency.minConfirms; - fee = _transaction.getFee(coin: coin); + if (_transaction.subType == TransactionSubType.join || + _transaction.subType == TransactionSubType.sparkSpend) { + fee = _transaction.getAnonFee()!; + } else { + fee = _transaction.getFee(coin: coin); + } if (_transaction.subType == TransactionSubType.cashFusion || _transaction.type == TransactionType.sentToSelf) { diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 2628b0d94..8ab99b011 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -13,11 +13,11 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/isar/models/spark_coin.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; -import 'package:tuple/tuple.dart'; const sparkStartBlock = 819300; // (approx 18 Jan 2024) @@ -60,6 +60,22 @@ class FiroWallet extends Bip39HDWallet final List> allTxHashes = await fetchHistory(allAddressesSet); + final sparkTxids = await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .txHashProperty() + .findAll(); + + for (final txid in sparkTxids) { + // check for duplicates before adding to list + if (allTxHashes.indexWhere((e) => e["tx_hash"] == txid) == -1) { + final info = { + "tx_hash": txid, + }; + allTxHashes.add(info); + } + } + List> allTransactions = []; // some lelantus transactions aren't fetched via wallet addresses so they @@ -108,7 +124,7 @@ class FiroWallet extends Bip39HDWallet if (allTransactions .indexWhere((e) => e["txid"] == tx["txid"] as String) == -1) { - tx["height"] = txHash["height"]; + tx["height"] ??= txHash["height"]; allTransactions.add(tx); } // } @@ -133,10 +149,6 @@ class FiroWallet extends Bip39HDWallet bool isMasterNodePayment = false; final bool isSparkSpend = txData["type"] == 9 && txData["version"] == 3; - if (txData.toString().contains("spark")) { - Util.printJson(txData); - } - // parse outputs final List outputs = []; for (final outputJson in txData["vout"] as List) { @@ -415,605 +427,6 @@ class FiroWallet extends Bip39HDWallet await mainDB.updateOrPutTransactionV2s(txns); } - Future updateTransactionsOLD() async { - final allAddresses = await fetchAddressesForElectrumXScan(); - - Set receivingAddresses = allAddresses - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - Set changeAddresses = allAddresses - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); - - final List> allTxHashes = - await fetchHistory(allAddresses.map((e) => e.value).toList()); - - List> allTransactions = []; - - // some lelantus transactions aren't fetched via wallet addresses so they - // will never show as confirmed in the gui. - final unconfirmedTransactions = await mainDB - .getTransactions(walletId) - .filter() - .heightIsNull() - .findAll(); - for (final tx in unconfirmedTransactions) { - final txn = await electrumXCachedClient.getTransaction( - txHash: tx.txid, - verbose: true, - coin: info.coin, - ); - final height = txn["height"] as int?; - - if (height != null) { - // tx was mined - // add to allTxHashes - final info = { - "tx_hash": tx.txid, - "height": height, - "address": tx.address.value?.value, - }; - allTxHashes.add(info); - } - } - - // final currentHeight = await chainHeight; - - for (final txHash in allTxHashes) { - // final storedTx = await db - // .getTransactions(walletId) - // .filter() - // .txidEqualTo(txHash["tx_hash"] as String) - // .findFirst(); - - // if (storedTx == null || - // !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) { - final tx = await electrumXCachedClient.getTransaction( - txHash: txHash["tx_hash"] as String, - verbose: true, - coin: info.coin, - ); - - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == - -1) { - tx["address"] = await mainDB - .getAddresses(walletId) - .filter() - .valueEqualTo(txHash["address"] as String) - .findFirst(); - tx["height"] = txHash["height"]; - allTransactions.add(tx); - } - // } - } - - final List> txnsData = []; - - for (final txObject in allTransactions) { - final inputList = txObject["vin"] as List; - final outputList = txObject["vout"] as List; - - bool isMint = false; - bool isJMint = false; - bool isSparkMint = false; - bool isSparkSpend = false; - - // check if tx is Mint or jMint - for (final output in outputList) { - if (output["scriptPubKey"]?["type"] == "lelantusmint") { - final asm = output["scriptPubKey"]?["asm"] as String?; - if (asm != null) { - if (asm.startsWith("OP_LELANTUSJMINT")) { - isJMint = true; - break; - } else if (asm.startsWith("OP_LELANTUSMINT")) { - isMint = true; - break; - } else { - Logging.instance.log( - "Unknown mint op code found for lelantusmint tx: ${txObject["txid"]}", - level: LogLevel.Error, - ); - } - } else { - Logging.instance.log( - "ASM for lelantusmint tx: ${txObject["txid"]} is null!", - level: LogLevel.Error, - ); - } - } - if (output["scriptPubKey"]?["type"] == "sparkmint") { - final asm = output["scriptPubKey"]?["asm"] as String?; - if (asm != null) { - if (asm.startsWith("OP_SPARKMINT")) { - isSparkMint = true; - break; - } else if (asm.startsWith("OP_SPARKSPEND")) { - isSparkSpend = true; - break; - } else { - Logging.instance.log( - "Unknown mint op code found for lelantusmint tx: ${txObject["txid"]}", - level: LogLevel.Error, - ); - } - } else { - Logging.instance.log( - "ASM for sparkmint tx: ${txObject["txid"]} is null!", - level: LogLevel.Error, - ); - } - } - } - - if (isSparkSpend || isSparkMint) { - continue; - } - - Set inputAddresses = {}; - Set outputAddresses = {}; - - Amount totalInputValue = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - Amount totalOutputValue = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - - Amount amountSentFromWallet = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - Amount amountReceivedInWallet = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - Amount changeAmount = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // Parse mint transaction ================================================ - // We should be able to assume this belongs to this wallet - if (isMint) { - List ins = []; - - // Parse inputs - for (final input in inputList) { - // Both value and address should not be null for a mint - final address = input["address"] as String?; - final value = input["valueSat"] as int?; - - // We should not need to check whether the mint belongs to this - // wallet as any tx we look up will be looked up by one of this - // wallet's addresses - if (address != null && value != null) { - totalInputValue += value.toAmountAsRaw( - fractionDigits: cryptoCurrency.fractionDigits, - ); - } - - ins.add( - Input( - txid: input['txid'] as String? ?? "", - vout: input['vout'] as int? ?? -1, - scriptSig: input['scriptSig']?['hex'] as String?, - scriptSigAsm: input['scriptSig']?['asm'] as String?, - isCoinbase: input['is_coinbase'] as bool?, - sequence: input['sequence'] as int?, - innerRedeemScriptAsm: input['innerRedeemscriptAsm'] as String?, - ), - ); - } - - // Parse outputs - for (final output in outputList) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // add value to total - totalOutputValue += value; - } - - final fee = totalInputValue - totalOutputValue; - final tx = Transaction( - walletId: walletId, - txid: txObject["txid"] as String, - timestamp: txObject["blocktime"] as int? ?? - (DateTime.now().millisecondsSinceEpoch ~/ 1000), - type: TransactionType.sentToSelf, - subType: TransactionSubType.mint, - amount: totalOutputValue.raw.toInt(), - amountString: totalOutputValue.toJsonString(), - fee: fee.raw.toInt(), - height: txObject["height"] as int?, - isCancelled: false, - isLelantus: true, - slateId: null, - otherData: null, - nonce: null, - inputs: ins, - outputs: [], - numberOfMessages: null, - ); - - txnsData.add(Tuple2(tx, null)); - - // Otherwise parse JMint transaction =================================== - } else if (isJMint) { - Amount jMintFees = Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // Parse inputs - List ins = []; - for (final input in inputList) { - // JMint fee - final nFee = Decimal.tryParse(input["nFees"].toString()); - if (nFee != null) { - final fees = Amount.fromDecimal( - nFee, - fractionDigits: cryptoCurrency.fractionDigits, - ); - - jMintFees += fees; - } - - ins.add( - Input( - txid: input['txid'] as String? ?? "", - vout: input['vout'] as int? ?? -1, - scriptSig: input['scriptSig']?['hex'] as String?, - scriptSigAsm: input['scriptSig']?['asm'] as String?, - isCoinbase: input['is_coinbase'] as bool?, - sequence: input['sequence'] as int?, - innerRedeemScriptAsm: input['innerRedeemscriptAsm'] as String?, - ), - ); - } - - bool nonWalletAddressFoundInOutputs = false; - - // Parse outputs - List outs = []; - for (final output in outputList) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // add value to total - totalOutputValue += value; - - final address = output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output['scriptPubKey']?['address'] as String?; - - if (address != null) { - outputAddresses.add(address); - if (receivingAddresses.contains(address) || - changeAddresses.contains(address)) { - amountReceivedInWallet += value; - } else { - nonWalletAddressFoundInOutputs = true; - } - } - - outs.add( - Output( - scriptPubKey: output['scriptPubKey']?['hex'] as String?, - scriptPubKeyAsm: output['scriptPubKey']?['asm'] as String?, - scriptPubKeyType: output['scriptPubKey']?['type'] as String?, - scriptPubKeyAddress: address ?? "jmint", - value: value.raw.toInt(), - ), - ); - } - final txid = txObject["txid"] as String; - - const subType = TransactionSubType.join; - - final type = nonWalletAddressFoundInOutputs - ? TransactionType.outgoing - : (await mainDB.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(txid) - .findFirst()) == - null - ? TransactionType.incoming - : TransactionType.sentToSelf; - - final amount = nonWalletAddressFoundInOutputs - ? totalOutputValue - : amountReceivedInWallet; - - final possibleNonWalletAddresses = - receivingAddresses.difference(outputAddresses); - final possibleReceivingAddresses = - receivingAddresses.intersection(outputAddresses); - - final transactionAddress = nonWalletAddressFoundInOutputs - ? Address( - walletId: walletId, - value: possibleNonWalletAddresses.first, - derivationIndex: -1, - derivationPath: null, - type: AddressType.nonWallet, - subType: AddressSubType.nonWallet, - publicKey: [], - ) - : allAddresses.firstWhere( - (e) => e.value == possibleReceivingAddresses.first, - ); - - final tx = Transaction( - walletId: walletId, - txid: txid, - timestamp: txObject["blocktime"] as int? ?? - (DateTime.now().millisecondsSinceEpoch ~/ 1000), - type: type, - subType: subType, - amount: amount.raw.toInt(), - amountString: amount.toJsonString(), - fee: jMintFees.raw.toInt(), - height: txObject["height"] as int?, - isCancelled: false, - isLelantus: true, - slateId: null, - otherData: null, - nonce: null, - inputs: ins, - outputs: outs, - numberOfMessages: null, - ); - - txnsData.add(Tuple2(tx, transactionAddress)); - - // Master node payment ===================================== - } else if (inputList.length == 1 && - inputList.first["coinbase"] is String) { - List ins = [ - Input( - txid: inputList.first["coinbase"] as String, - vout: -1, - scriptSig: null, - scriptSigAsm: null, - isCoinbase: true, - sequence: inputList.first['sequence'] as int?, - innerRedeemScriptAsm: null, - ), - ]; - - // parse outputs - List outs = []; - for (final output in outputList) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // get output address - final address = output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]?["address"] as String?; - if (address != null) { - outputAddresses.add(address); - - // if output was to my wallet, add value to amount received - if (receivingAddresses.contains(address)) { - amountReceivedInWallet += value; - } - } - - outs.add( - Output( - scriptPubKey: output['scriptPubKey']?['hex'] as String?, - scriptPubKeyAsm: output['scriptPubKey']?['asm'] as String?, - scriptPubKeyType: output['scriptPubKey']?['type'] as String?, - scriptPubKeyAddress: address ?? "", - value: value.raw.toInt(), - ), - ); - } - - // this is the address initially used to fetch the txid - Address transactionAddress = txObject["address"] as Address; - - final tx = Transaction( - walletId: walletId, - txid: txObject["txid"] as String, - timestamp: txObject["blocktime"] as int? ?? - (DateTime.now().millisecondsSinceEpoch ~/ 1000), - type: TransactionType.incoming, - subType: TransactionSubType.none, - // amount may overflow. Deprecated. Use amountString - amount: amountReceivedInWallet.raw.toInt(), - amountString: amountReceivedInWallet.toJsonString(), - fee: 0, - height: txObject["height"] as int?, - isCancelled: false, - isLelantus: false, - slateId: null, - otherData: null, - nonce: null, - inputs: ins, - outputs: outs, - numberOfMessages: null, - ); - - txnsData.add(Tuple2(tx, transactionAddress)); - - // Assume non lelantus transaction ===================================== - } else { - // parse inputs - List ins = []; - for (final input in inputList) { - final valueSat = input["valueSat"] as int?; - final address = input["address"] as String? ?? - input["scriptPubKey"]?["address"] as String? ?? - input["scriptPubKey"]?["addresses"]?[0] as String?; - - if (address != null && valueSat != null) { - final value = valueSat.toAmountAsRaw( - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // add value to total - totalInputValue += value; - inputAddresses.add(address); - - // if input was from my wallet, add value to amount sent - if (receivingAddresses.contains(address) || - changeAddresses.contains(address)) { - amountSentFromWallet += value; - } - } - - if (input['txid'] == null) { - continue; - } - - ins.add( - Input( - txid: input['txid'] as String, - vout: input['vout'] as int? ?? -1, - scriptSig: input['scriptSig']?['hex'] as String?, - scriptSigAsm: input['scriptSig']?['asm'] as String?, - isCoinbase: input['is_coinbase'] as bool?, - sequence: input['sequence'] as int?, - innerRedeemScriptAsm: input['innerRedeemscriptAsm'] as String?, - ), - ); - } - - // parse outputs - List outs = []; - for (final output in outputList) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, - ); - - // add value to total - totalOutputValue += value; - - // get output address - final address = output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]?["address"] as String?; - if (address != null) { - outputAddresses.add(address); - - // if output was to my wallet, add value to amount received - if (receivingAddresses.contains(address)) { - amountReceivedInWallet += value; - } else if (changeAddresses.contains(address)) { - changeAmount += value; - } - } - - outs.add( - Output( - scriptPubKey: output['scriptPubKey']?['hex'] as String?, - scriptPubKeyAsm: output['scriptPubKey']?['asm'] as String?, - scriptPubKeyType: output['scriptPubKey']?['type'] as String?, - scriptPubKeyAddress: address ?? "", - value: value.raw.toInt(), - ), - ); - } - - final mySentFromAddresses = [ - ...receivingAddresses.intersection(inputAddresses), - ...changeAddresses.intersection(inputAddresses) - ]; - final myReceivedOnAddresses = - receivingAddresses.intersection(outputAddresses); - final myChangeReceivedOnAddresses = - changeAddresses.intersection(outputAddresses); - - final fee = totalInputValue - totalOutputValue; - - // this is the address initially used to fetch the txid - Address transactionAddress = txObject["address"] as Address; - - TransactionType type; - Amount amount; - if (mySentFromAddresses.isNotEmpty && - myReceivedOnAddresses.isNotEmpty) { - // tx is sent to self - type = TransactionType.sentToSelf; - - // should be 0 - amount = amountSentFromWallet - - amountReceivedInWallet - - fee - - changeAmount; - } else if (mySentFromAddresses.isNotEmpty) { - // outgoing tx - type = TransactionType.outgoing; - amount = amountSentFromWallet - changeAmount - fee; - - final possible = - outputAddresses.difference(myChangeReceivedOnAddresses).first; - - if (transactionAddress.value != possible) { - transactionAddress = Address( - walletId: walletId, - value: possible, - derivationIndex: -1, - derivationPath: null, - subType: AddressSubType.nonWallet, - type: AddressType.nonWallet, - publicKey: [], - ); - } - } else { - // incoming tx - type = TransactionType.incoming; - amount = amountReceivedInWallet; - } - - final tx = Transaction( - walletId: walletId, - txid: txObject["txid"] as String, - timestamp: txObject["blocktime"] as int? ?? - (DateTime.now().millisecondsSinceEpoch ~/ 1000), - type: type, - subType: TransactionSubType.none, - // amount may overflow. Deprecated. Use amountString - amount: amount.raw.toInt(), - amountString: amount.toJsonString(), - fee: fee.raw.toInt(), - height: txObject["height"] as int?, - isCancelled: false, - isLelantus: false, - slateId: null, - otherData: null, - nonce: null, - inputs: ins, - outputs: outs, - numberOfMessages: null, - ); - - txnsData.add(Tuple2(tx, transactionAddress)); - } - } - - await mainDB.addNewTransactionData(txnsData, walletId); - } - @override ({String? blockedReason, bool blocked}) checkBlockUTXO( Map jsonUTXO,