some firo transaction display fixes

This commit is contained in:
julian 2023-12-16 10:19:50 -06:00
parent 2469c3eb91
commit 8336712a23
5 changed files with 49 additions and 607 deletions

View file

@ -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'
')';
}
}

View file

@ -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;

View file

@ -50,7 +50,9 @@ class _TransactionCardStateV2 extends ConsumerState<TransactionCardV2> {
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 {

View file

@ -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) {

View file

@ -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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<OutputV2> outputs = [];
for (final outputJson in txData["vout"] as List) {
@ -415,605 +427,6 @@ class FiroWallet extends Bip39HDWallet
await mainDB.updateOrPutTransactionV2s(txns);
}
Future<void> updateTransactionsOLD() async {
final allAddresses = await fetchAddressesForElectrumXScan();
Set<String> receivingAddresses = allAddresses
.where((e) => e.subType == AddressSubType.receiving)
.map((e) => e.value)
.toSet();
Set<String> changeAddresses = allAddresses
.where((e) => e.subType == AddressSubType.change)
.map((e) => e.value)
.toSet();
final List<Map<String, dynamic>> allTxHashes =
await fetchHistory(allAddresses.map((e) => e.value).toList());
List<Map<String, dynamic>> 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<Tuple2<Transaction, Address?>> 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<String> inputAddresses = {};
Set<String> 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<Input> 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<Input> 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<Output> 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<Input> 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<Output> 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<Input> 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<Output> 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<String, dynamic> jsonUTXO,