import 'package:dart_numerics/dart_numerics.dart'; import 'package:decimal/decimal.dart'; import 'package:hive/hive.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; part '../type_adaptors/transactions_model.g.dart'; String extractDateFromTimestamp(int? timestamp) { if (timestamp == 0 || timestamp == null) { return 'Now...'; } final int day = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000).day; final int month = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000).month; final int year = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000).year; return '$year${month < 10 ? "0$month" : month.toString()}${day < 10 ? "0$day" : day.toString()}'; } // @HiveType(typeId: 1) class TransactionData { // @HiveField(0) final List txChunks; TransactionData({this.txChunks = const []}); factory TransactionData.fromJson(Map json) { var dateTimeChunks = json['dateTimeChunks'] as List; List chunksList = dateTimeChunks .map((txChunk) => TransactionChunk.fromJson(txChunk as Map)) .toList(); return TransactionData(txChunks: chunksList); } factory TransactionData.fromMap(Map transactions) { Map> chunks = {}; transactions.forEach((key, value) { String date = extractDateFromTimestamp(value.timestamp); if (!chunks.containsKey(date)) { chunks[date] = []; } chunks[date]!.add(value); }); List chunksList = []; chunks.forEach((key, value) { value.sort((a, b) => b.timestamp.compareTo(a.timestamp)); chunksList.add( TransactionChunk(timestamp: value[0].timestamp, transactions: value)); }); chunksList.sort((a, b) => b.timestamp.compareTo(a.timestamp)); return TransactionData(txChunks: chunksList); } Transaction? findTransaction(String txid) { for (var i = 0; i < txChunks.length; i++) { var txChunk = txChunks[i].transactions; for (var j = 0; j < txChunk.length; j++) { var tx = txChunk[j]; if (tx.txid == txid) { return tx; } } } return null; } Map getAllTransactions() { Map transactions = {}; for (var i = 0; i < txChunks.length; i++) { var txChunk = txChunks[i].transactions; for (var j = 0; j < txChunk.length; j++) { var tx = txChunk[j]; transactions[tx.txid] = tx; } } return transactions; } } // @HiveType(typeId: 2) class TransactionChunk { // @HiveField(0) final int timestamp; // @HiveField(1) final List transactions; TransactionChunk({required this.timestamp, required this.transactions}); factory TransactionChunk.fromJson(Map json) { var txArray = json['transactions'] as List; List txList = txArray .map((tx) => Transaction.fromJson(tx as Map)) .toList(); return TransactionChunk( timestamp: json['timestamp'] as int, transactions: txList); } @override String toString() { String transaction = "timestamp: $timestamp transactions: [\n"; for (final tx in transactions) { transaction += " $tx \n"; } transaction += "]"; return transaction; } } // @HiveType(typeId: 3) class Transaction { // @HiveField(0) final String txid; // @HiveField(1) final bool confirmedStatus; // @HiveField(2) final int timestamp; // @HiveField(3) final String txType; // @HiveField(4) final int amount; // @HiveField(5) final List aliens; // @HiveField(6) final String worthNow; // @HiveField(7) final String worthAtBlockTimestamp; // @HiveField(8) final int fees; // @HiveField(9) final int inputSize; // @HiveField(10) final int outputSize; // @HiveField(11) final List inputs; // @HiveField(12) final List outputs; // @HiveField(13) final String address; // @HiveField(14) final int height; // @HiveField(15) final String subType; // @HiveField(16) final int confirmations; // @HiveField(17) final bool isCancelled; // @HiveField(18) final String? slateId; // @HiveField(18) final String? otherData; Transaction({ required this.txid, required this.confirmedStatus, required this.timestamp, required this.txType, required this.amount, this.aliens = const [], required this.worthNow, required this.worthAtBlockTimestamp, required this.fees, required this.inputSize, required this.outputSize, required this.inputs, required this.outputs, required this.address, required this.height, this.subType = "", required this.confirmations, this.isCancelled = false, this.slateId, this.otherData, }); factory Transaction.fromJson(Map json) { var inputArray = json['inputs'] as List; var outputArray = json['outputs'] as List; List inputList = inputArray .map((input) => Input.fromJson(Map.from(input as Map))) .toList(); List outputList = outputArray .map((output) => Output.fromJson(Map.from(output as Map))) .toList(); return Transaction( txid: json['txid'] as String, confirmedStatus: json['confirmed_status'] as bool, timestamp: json['timestamp'] as int, txType: json['txType'] as String, amount: json['amount'] as int, aliens: json['aliens'] as List, worthNow: json['worthNow'] as String? ?? "", worthAtBlockTimestamp: json['worthAtBlockTimestamp'] as String? ?? "", fees: json['fees'] as int, inputSize: json['inputSize'] as int, outputSize: json['outputSize'] as int, inputs: inputList, outputs: outputList, address: json['address'] as String, height: json['height'] as int? ?? -1, subType: json["subType"] as String? ?? "", confirmations: json['confirmations'] as int? ?? 0, isCancelled: json["isCancelled"] as bool? ?? false, slateId: json["slateId"] as String?, otherData: json["otherData"] as String?, ); } factory Transaction.fromLelantusJson(Map json) { return Transaction( txid: json['txid'] as String, confirmedStatus: json['confirmed_status'] as bool? ?? false, timestamp: json['timestamp'] as int? ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000), txType: json['txType'] as String, amount: (Decimal.parse(json["amount"].toString()) * Decimal.fromInt(Constants.satsPerCoin(Coin .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure .toBigInt() .toInt(), aliens: [], worthNow: json['worthNow'] as String, worthAtBlockTimestamp: json['worthAtBlockTimestamp'] as String? ?? "0", fees: (Decimal.parse(json["fees"].toString()) * Decimal.fromInt(Constants.satsPerCoin(Coin .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure .toBigInt() .toInt(), inputSize: json['inputSize'] as int? ?? 0, outputSize: json['outputSize'] as int? ?? 0, inputs: [], outputs: [], address: json["address"] as String, height: json["height"] as int? ?? int64MaxValue, subType: json["subType"] as String? ?? "", confirmations: json["confirmations"] as int? ?? 0, otherData: json["otherData"] as String?, ); } bool get isMinting => subType.toLowerCase() == "mint" && !confirmedStatus; Transaction copyWith({ String? txid, bool? confirmedStatus, int? timestamp, String? txType, int? amount, List? aliens, String? worthNow, String? worthAtBlockTimestamp, int? fees, int? inputSize, int? outputSize, List? inputs, List? outputs, String? address, int? height, String? subType, int? confirmations, bool? isCancelled, String? slateId, String? otherData, }) { return Transaction( txid: txid ?? this.txid, confirmedStatus: confirmedStatus ?? this.confirmedStatus, timestamp: timestamp ?? this.timestamp, txType: txType ?? this.txType, amount: amount ?? this.amount, aliens: aliens ?? this.aliens, worthNow: worthNow ?? this.worthNow, worthAtBlockTimestamp: worthAtBlockTimestamp ?? this.worthAtBlockTimestamp, fees: fees ?? this.fees, inputSize: inputSize ?? this.inputSize, outputSize: outputSize ?? this.outputSize, inputs: inputs ?? this.inputs, outputs: outputs ?? this.outputs, address: address ?? this.address, height: height ?? this.height, subType: subType ?? this.subType, confirmations: confirmations ?? this.confirmations, isCancelled: isCancelled ?? this.isCancelled, slateId: slateId ?? this.slateId, otherData: otherData ?? this.otherData, ); } @override String toString() { String transaction = "{txid: $txid, type: $txType, subType: $subType, value: $amount, fee: $fees, height: $height, confirm: $confirmedStatus, confirmations: $confirmations, address: $address, timestamp: $timestamp, worthNow: $worthNow, inputs: $inputs, slateid: $slateId }"; return transaction; } } // @HiveType(typeId: 4) class Input { // @HiveField(0) final String txid; // @HiveField(1) final int vout; // // @HiveField(2) final Output? prevout; // @HiveField(3) final String? scriptsig; // @HiveField(4) final String? scriptsigAsm; // @HiveField(5) final List? witness; // @HiveField(6) final bool? isCoinbase; // @HiveField(7) final int? sequence; // @HiveField(8) final String? innerRedeemscriptAsm; Input({ required this.txid, required this.vout, this.prevout, this.scriptsig, this.scriptsigAsm, this.witness, this.isCoinbase, this.sequence, this.innerRedeemscriptAsm, }); factory Input.fromJson(Map json) { bool iscoinBase = json['coinbase'] != null; return Input( txid: json['txid'] as String? ?? "", vout: json['vout'] as int? ?? -1, // electrumx calls do not return prevout so we set this to null for now prevout: null, //Output.fromJson(json['prevout']), scriptsig: iscoinBase ? "" : json['scriptSig']['hex'] as String?, scriptsigAsm: iscoinBase ? "" : json['scriptSig']['asm'] as String?, witness: json['witness'] as List? ?? [], isCoinbase: iscoinBase ? iscoinBase : json['is_coinbase'] as bool?, sequence: json['sequence'] as int?, innerRedeemscriptAsm: json['innerRedeemscriptAsm'] as String? ?? "", ); } @override String toString() { String transaction = "{txid: $txid}"; return transaction; } } // @HiveType(typeId: 5) class Output { // @HiveField(0) final String? scriptpubkey; // @HiveField(1) final String? scriptpubkeyAsm; // @HiveField(2) final String? scriptpubkeyType; // @HiveField(3) final String scriptpubkeyAddress; // @HiveField(4) final int value; Output( {this.scriptpubkey, this.scriptpubkeyAsm, this.scriptpubkeyType, required this.scriptpubkeyAddress, required this.value}); factory Output.fromJson(Map json) { // TODO determine if any of this code is needed. // Particl has different tx types that need to be detected and handled here if (json.containsKey('scriptPubKey') as bool) { // output is transparent final address = json["scriptPubKey"]["addresses"] == null ? json['scriptPubKey']['type'] as String : json["scriptPubKey"]["addresses"][0] as String; return Output( scriptpubkey: json['scriptPubKey']['hex'] as String?, scriptpubkeyAsm: json['scriptPubKey']['asm'] as String?, scriptpubkeyType: json['scriptPubKey']['type'] as String?, scriptpubkeyAddress: address, value: (Decimal.parse(json["value"].toString()) * Decimal.fromInt(Constants.satsPerCoin(Coin .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure .toBigInt() .toInt(), ); } /* else if (json.containsKey('ct_fee') as bool) { // or type: data // output is blinded (CT) } else if (json.containsKey('rangeproof') as bool) { // or valueCommitment or type: anon // output is private (RingCT) } */ else { // TODO detect staking // TODO handle CT, RingCT, and staking accordingly // print("transaction not supported: ${json}"); return Output( // Return output object with null values; allows wallet history to be built scriptpubkey: null, scriptpubkeyAsm: null, scriptpubkeyType: null, scriptpubkeyAddress: "", value: (Decimal.parse(0.toString()) * Decimal.fromInt(Constants.satsPerCoin(Coin .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure .toBigInt() .toInt()); } } }