From 8e3e1db3cd02e51bb1ae0fdb6b808539f077e741 Mon Sep 17 00:00:00 2001
From: julian <julian@cypherstack.com>
Date: Wed, 18 Oct 2023 15:50:14 -0600
Subject: [PATCH] WIP txn V2

---
 .../blockchain_data/v2/transaction_v2.dart    | 240 ++++++++++++++++++
 lib/services/mixins/electrum_x_parsing.dart   |   5 +-
 2 files changed, 244 insertions(+), 1 deletion(-)
 create mode 100644 lib/models/isar/models/blockchain_data/v2/transaction_v2.dart

diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart
new file mode 100644
index 000000000..a77cacdea
--- /dev/null
+++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart
@@ -0,0 +1,240 @@
+import 'package:decimal/decimal.dart';
+
+class TransactionV2 {
+  final String hash;
+  final String txid;
+
+  final int size;
+  final int lockTime;
+
+  final DateTime? blockTime;
+  final String? blockHash;
+
+  final List<InputV2> inputs;
+  final List<OutputV2> outputs;
+
+  TransactionV2({
+    required this.blockHash,
+    required this.hash,
+    required this.txid,
+    required this.lockTime,
+    required this.size,
+    required this.blockTime,
+    required this.inputs,
+    required this.outputs,
+  });
+
+  static TransactionV2 fromElectrumXJson(Map<String, dynamic> json) {
+    try {
+      final inputs = (json["vin"] as List).map(
+        (e) => InputV2.fromElectrumXJson(
+          Map<String, dynamic>.from(e as Map),
+        ),
+      );
+      final outputs = (json["vout"] as List).map(
+        (e) => OutputV2.fromElectrumXJson(
+          Map<String, dynamic>.from(e as Map),
+        ),
+      );
+
+      final blockTimeUnix = json["blocktime"] as int?;
+      DateTime? blockTime;
+      if (blockTimeUnix != null) {
+        blockTime = DateTime.fromMillisecondsSinceEpoch(
+          blockTimeUnix * 1000,
+          isUtc: true,
+        );
+      }
+
+      return TransactionV2(
+        blockHash: json["blockhash"] as String?,
+        hash: json["hash"] as String,
+        txid: json["txid"] as String,
+        lockTime: json["locktime"] as int,
+        size: json["size"] as int,
+        blockTime: blockTime,
+        inputs: List.unmodifiable(inputs),
+        outputs: List.unmodifiable(outputs),
+      );
+    } catch (e) {
+      throw Exception(
+        "Failed to parse TransactionV2 for txid=${json["txid"]}: $e",
+      );
+    }
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is TransactionV2 &&
+        other.hash == hash &&
+        other.txid == txid &&
+        other.size == size &&
+        other.lockTime == lockTime &&
+        other.blockTime == blockTime &&
+        other.blockHash == blockHash &&
+        _listEquals(other.inputs, inputs) &&
+        _listEquals(other.outputs, outputs);
+  }
+
+  @override
+  int get hashCode => Object.hash(
+        hash,
+        txid,
+        size,
+        lockTime,
+        blockTime,
+        blockHash,
+        inputs,
+        outputs,
+      );
+
+  @override
+  String toString() {
+    return 'TransactionV2(\n'
+        '  hash: $hash,\n'
+        '  txid: $txid,\n'
+        '  size: $size,\n'
+        '  lockTime: $lockTime,\n'
+        '  blockTime: $blockTime,\n'
+        '  blockHash: $blockHash,\n'
+        '  inputs: $inputs,\n'
+        '  outputs: $outputs,\n'
+        ')';
+  }
+}
+
+class InputV2 {
+  final String scriptSigHex;
+  final int sequence;
+  final String txid;
+  final int vout;
+
+  InputV2({
+    required this.scriptSigHex,
+    required this.sequence,
+    required this.txid,
+    required this.vout,
+  });
+
+  static InputV2 fromElectrumXJson(Map<String, dynamic> json) {
+    try {
+      return InputV2(
+          scriptSigHex: json["scriptSig"]["hex"] as String,
+          sequence: json["sequence"] as int,
+          txid: json["txid"] as String,
+          vout: json["vout"] as int);
+    } catch (e) {
+      throw Exception("Failed to parse InputV2 from $json");
+    }
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is InputV2 &&
+        other.scriptSigHex == scriptSigHex &&
+        other.sequence == sequence &&
+        other.txid == txid &&
+        other.vout == vout;
+  }
+
+  @override
+  int get hashCode => Object.hash(
+        scriptSigHex,
+        sequence,
+        txid,
+        vout,
+      );
+
+  @override
+  String toString() {
+    return 'InputV2(\n'
+        '  scriptSigHex: $scriptSigHex,\n'
+        '  sequence: $sequence,\n'
+        '  txid: $txid,\n'
+        '  vout: $vout,\n'
+        ')';
+  }
+}
+
+class OutputV2 {
+  final String scriptPubKeyHex;
+  final String valueStringSats;
+
+  BigInt get value => BigInt.parse(valueStringSats);
+
+  OutputV2({
+    required this.scriptPubKeyHex,
+    required this.valueStringSats,
+  });
+
+  // TODO: move this to a subclass based on coin since we don't know if the value will be sats or a decimal amount
+  // For now assume 8 decimal places
+  @Deprecated("See TODO and comments")
+  static OutputV2 fromElectrumXJson(Map<String, dynamic> json) {
+    try {
+      final temp = Decimal.parse(json["value"].toString());
+      if (temp < Decimal.zero) {
+        throw Exception("Negative value found");
+      }
+
+      final String valueStringSats;
+      if (temp.isInteger) {
+        valueStringSats = temp.toString();
+      } else {
+        valueStringSats = temp.shift(8).toBigInt().toString();
+      }
+
+      return OutputV2(
+        scriptPubKeyHex: json["scriptPubKey"]["hex"] as String,
+        valueStringSats: valueStringSats,
+      );
+    } catch (e) {
+      throw Exception("Failed to parse OutputV2 from $json");
+    }
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is OutputV2 &&
+        other.scriptPubKeyHex == scriptPubKeyHex &&
+        other.valueStringSats == valueStringSats;
+  }
+
+  @override
+  int get hashCode => Object.hash(
+        scriptPubKeyHex,
+        valueStringSats,
+      );
+
+  @override
+  String toString() {
+    return 'OutputV2(\n'
+        '  scriptPubKeyHex: $scriptPubKeyHex,\n'
+        '  value: $value,\n'
+        ')';
+  }
+}
+
+bool _listEquals<T, U>(List<T> a, List<U> b) {
+  if (T != U) {
+    return false;
+  }
+
+  if (a.length != b.length) {
+    return false;
+  }
+
+  for (int i = 0; i < a.length; i++) {
+    if (a[i] != b[i]) {
+      return false;
+    }
+  }
+
+  return true;
+}
diff --git a/lib/services/mixins/electrum_x_parsing.dart b/lib/services/mixins/electrum_x_parsing.dart
index 224073574..c811b2e36 100644
--- a/lib/services/mixins/electrum_x_parsing.dart
+++ b/lib/services/mixins/electrum_x_parsing.dart
@@ -12,6 +12,7 @@ import 'dart:convert';
 
 import 'package:bip47/src/util.dart';
 import 'package:decimal/decimal.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/services/mixins/paynym_wallet_interface.dart';
 import 'package:stackwallet/utilities/amount/amount.dart';
@@ -24,13 +25,15 @@ class TT with ElectrumXParsing {
 }
 
 mixin ElectrumXParsing {
-  Future<dynamic> parseBchTx(
+  Future<TransactionV2> parseBchTx(
     Map<String, dynamic> json, [
     String? debugTitle,
   ]) async {
     print("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
     util.Util.printJson(json, debugTitle);
     print("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
+
+    return TransactionV2.fromElectrumXJson(json);
   }
 
   Future<Tuple2<Transaction, Address>> parseTransaction(