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(