WIP txV2 for bch for now

This commit is contained in:
julian 2023-10-19 11:08:14 -06:00
parent 94fd8bc0ec
commit 45737084fd
5 changed files with 296 additions and 151 deletions

View file

@ -1,28 +1,84 @@
class InputV2 {
final String scriptSigHex;
final int sequence;
final String txid;
final int vout;
import 'package:isar/isar.dart';
InputV2({
required this.scriptSigHex,
required this.sequence,
required this.txid,
required this.vout,
});
part 'input_v2.g.dart';
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");
}
@Embedded()
class OutpointV2 {
late final String txid;
late final int vout;
OutpointV2();
static OutpointV2 isarCantDoRequiredInDefaultConstructor({
required String txid,
required int vout,
}) =>
OutpointV2()
..vout = vout
..txid = txid;
@override
String toString() {
return 'OutpointV2(\n'
' txid: $txid,\n'
' vout: $vout,\n'
')';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is OutpointV2 && other.txid == txid && other.vout == vout;
}
@override
int get hashCode {
return Object.hash(
txid.hashCode,
vout.hashCode,
);
}
}
@Embedded()
class InputV2 {
late final String? scriptSigHex;
late final int? sequence;
late final OutpointV2? outpoint;
late final List<String> addresses;
late final String valueStringSats;
late final String? coinbase;
late final String? witness;
late final String? innerRedeemScriptAsm;
@ignore
BigInt get value => BigInt.parse(valueStringSats);
InputV2();
static InputV2 isarCantDoRequiredInDefaultConstructor({
required String? scriptSigHex,
required int? sequence,
required OutpointV2? outpoint,
required List<String> addresses,
required String valueStringSats,
required String? witness,
required String? innerRedeemScriptAsm,
required String? coinbase,
}) =>
InputV2()
..scriptSigHex = scriptSigHex
..sequence = sequence
..sequence = sequence
..addresses = List.unmodifiable(addresses)
..valueStringSats = valueStringSats
..witness = witness
..innerRedeemScriptAsm = innerRedeemScriptAsm
..coinbase = coinbase;
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
@ -30,16 +86,14 @@ class InputV2 {
return other is InputV2 &&
other.scriptSigHex == scriptSigHex &&
other.sequence == sequence &&
other.txid == txid &&
other.vout == vout;
other.outpoint == outpoint;
}
@override
int get hashCode => Object.hash(
scriptSigHex,
sequence,
txid,
vout,
outpoint,
);
@override
@ -47,8 +101,12 @@ class InputV2 {
return 'InputV2(\n'
' scriptSigHex: $scriptSigHex,\n'
' sequence: $sequence,\n'
' txid: $txid,\n'
' vout: $vout,\n'
' outpoint: $outpoint,\n'
' addresses: $addresses,\n'
' valueStringSats: $valueStringSats,\n'
' coinbase: $coinbase,\n'
' witness: $witness,\n'
' innerRedeemScriptAsm: $innerRedeemScriptAsm,\n'
')';
}
}

View file

@ -1,48 +1,83 @@
import 'package:decimal/decimal.dart';
import 'package:isar/isar.dart';
part 'output_v2.g.dart';
@Embedded()
class OutputV2 {
final String scriptPubKeyHex;
final String valueStringSats;
late final String scriptPubKeyHex;
late final String valueStringSats;
late final List<String> addresses;
@ignore
BigInt get value => BigInt.parse(valueStringSats);
OutputV2({
required this.scriptPubKeyHex,
required this.valueStringSats,
});
OutputV2();
// 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) {
static OutputV2 isarCantDoRequiredInDefaultConstructor({
required String scriptPubKeyHex,
required String valueStringSats,
required List<String> addresses,
}) =>
OutputV2()
..scriptPubKeyHex = scriptPubKeyHex
..valueStringSats = valueStringSats
..addresses = List.unmodifiable(addresses);
static OutputV2 fromElectrumXJson(
Map<String, dynamic> json, {
required int decimalPlaces,
}) {
try {
final temp = Decimal.parse(json["value"].toString());
if (temp < Decimal.zero) {
throw Exception("Negative value found");
List<String> addresses = [];
if (json["scriptPubKey"]?["addresses"] is List) {
for (final e in json["scriptPubKey"]["addresses"] as List) {
addresses.add(e as String);
}
} else if (json["scriptPubKey"]?["address"] is String) {
addresses.add(json["scriptPubKey"]?["address"] as String);
}
final String valueStringSats;
if (temp.isInteger) {
valueStringSats = temp.toString();
} else {
valueStringSats = temp.shift(8).toBigInt().toString();
}
return OutputV2(
return OutputV2.isarCantDoRequiredInDefaultConstructor(
scriptPubKeyHex: json["scriptPubKey"]["hex"] as String,
valueStringSats: valueStringSats,
valueStringSats: parseOutputAmountString(
json["value"].toString(),
decimalPlaces: decimalPlaces,
),
addresses: addresses,
);
} catch (e) {
throw Exception("Failed to parse OutputV2 from $json");
}
}
static String parseOutputAmountString(
String amount, {
required int decimalPlaces,
}) {
final temp = Decimal.parse(amount);
if (temp < Decimal.zero) {
throw Exception("Negative value found");
}
final String valueStringSats;
if (temp.isInteger) {
valueStringSats = temp.toString();
} else {
valueStringSats = temp.shift(decimalPlaces).toBigInt().toString();
}
return valueStringSats;
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is OutputV2 &&
other.scriptPubKeyHex == scriptPubKeyHex &&
_listEquals(other.addresses, addresses) &&
other.valueStringSats == valueStringSats;
}
@ -57,6 +92,25 @@ class OutputV2 {
return 'OutputV2(\n'
' scriptPubKeyHex: $scriptPubKeyHex,\n'
' value: $value,\n'
' addresses: $addresses,\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;
}

View file

@ -1,125 +1,67 @@
import 'dart:math';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart';
part 'transaction_v2.g.dart';
@Collection()
class TransactionV2 {
final String hash;
final Id id = Isar.autoIncrement;
@Index()
final String walletId;
@Index(unique: true, composite: [CompositeIndex("walletId")])
final String txid;
final int size;
final int lockTime;
final String hash;
final DateTime? blockTime;
@Index()
late final int timestamp;
final int? height;
final String? blockHash;
final int version;
final List<InputV2> inputs;
final List<OutputV2> outputs;
TransactionV2({
required this.walletId,
required this.blockHash,
required this.hash,
required this.txid,
required this.lockTime,
required this.size,
required this.blockTime,
required this.timestamp,
required this.height,
required this.inputs,
required this.outputs,
required this.version,
});
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",
);
}
int getConfirmations(int currentChainHeight) {
if (height == null || height! <= 0) return 0;
return max(0, currentChainHeight - (height! - 1));
}
@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);
bool isConfirmed(int currentChainHeight, int minimumConfirms) {
final confirmations = getConfirmations(currentChainHeight);
return confirmations >= minimumConfirms;
}
@override
int get hashCode => Object.hash(
hash,
txid,
size,
lockTime,
blockTime,
blockHash,
inputs,
outputs,
);
@override
String toString() {
return 'TransactionV2(\n'
' walletId: $walletId,\n'
' hash: $hash,\n'
' txid: $txid,\n'
' size: $size,\n'
' lockTime: $lockTime,\n'
' blockTime: $blockTime,\n'
' timestamp: $timestamp,\n'
' height: $height,\n'
' blockHash: $blockHash,\n'
' version: $version,\n'
' inputs: $inputs,\n'
' outputs: $outputs,\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;
}

View file

@ -18,6 +18,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/debug_service_provider.dart';
@ -379,6 +380,9 @@ class HiddenSettings extends StatelessWidget {
failovers: [],
);
final ce =
CachedElectrumX(electrumXClient: e);
final txids = [
"", // cashTokenTxid
"6a0444358bc41913c5b04a8dc06896053184b3641bc62502d18f954865b6ce1e", // normalTxid
@ -400,15 +404,23 @@ class HiddenSettings extends StatelessWidget {
// // await e.getTransaction(txHash: txids[2]);
// // await p.parseBchTx(json3);
//
final json4 =
await e.getTransaction(txHash: txids[3]);
await p.parseBchTx(
json4, "SLP TOKEN SEND TXID:");
await p.getTransaction(
txids[3],
Coin.bitcoincash,
"lol",
ce,
"SLP TOKEN SEND TXID:");
await p.getTransaction(
"009d31380d2dbfb5c91500c861d55b531a8b762b0abb19353db884548dbac8b6",
Coin.bitcoincash,
"lol",
ce,
"COINBASE TXID:");
final json5 =
await e.getTransaction(txHash: txids[4]);
await p.parseBchTx(
json5, "SLP TOKEN GENESIS TXID:");
// final json5 =
// await e.getTransaction(txHash: txids[4]);
// await p.parseBchTx(
// json5, "SLP TOKEN GENESIS TXID:");
} catch (e, s) {
print("$e\n$s");
}

View file

@ -12,6 +12,9 @@ import 'dart:convert';
import 'package:bip47/src/util.dart';
import 'package:decimal/decimal.dart';
import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.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';
@ -25,15 +28,91 @@ class TT with ElectrumXParsing {
}
mixin ElectrumXParsing {
Future<TransactionV2> parseBchTx(
Map<String, dynamic> json, [
Future<TransactionV2> getTransaction(
String txHash,
Coin coin,
String walletId,
CachedElectrumX cachedElectrumX, [
String? debugTitle,
]) async {
final jsonTx = await cachedElectrumX.getTransaction(
txHash: txHash,
coin: coin,
);
print("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
util.Util.printJson(json, debugTitle);
util.Util.printJson(jsonTx, debugTitle);
print("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
return TransactionV2.fromElectrumXJson(json);
// parse inputs
final List<InputV2> inputs = [];
for (final jsonInput in jsonTx["vin"] as List) {
final map = Map<String, dynamic>.from(jsonInput as Map);
final List<String> addresses = [];
String valueStringSats = "0";
OutpointV2? outpoint;
final coinbase = map["coinbase"] as String?;
if (coinbase == null) {
final txid = map["txid"] as String;
final vout = map["vout"] as int;
final inputTx =
await cachedElectrumX.getTransaction(txHash: txid, coin: coin);
final prevOutJson = Map<String, dynamic>.from(
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) as Map);
final prevOut = OutputV2.fromElectrumXJson(
prevOutJson,
decimalPlaces: coin.decimals,
);
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
txid: txid,
vout: vout,
);
valueStringSats = prevOut.valueStringSats;
addresses.addAll(prevOut.addresses);
}
final input = InputV2.isarCantDoRequiredInDefaultConstructor(
scriptSigHex: map["scriptSig"]?["hex"] as String?,
sequence: map["sequence"] as int?,
outpoint: outpoint,
valueStringSats: valueStringSats,
addresses: addresses,
witness: map["witness"] as String?,
coinbase: coinbase,
innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?,
);
inputs.add(input);
}
// parse outputs
final List<OutputV2> outputs = [];
for (final outputJson in jsonTx["vout"] as List) {
final output = OutputV2.fromElectrumXJson(
Map<String, dynamic>.from(outputJson as Map),
decimalPlaces: coin.decimals,
);
outputs.add(output);
}
return TransactionV2(
walletId: walletId,
blockHash: jsonTx["blockhash"] as String?,
hash: jsonTx["hash"] as String,
txid: jsonTx["txid"] as String,
height: jsonTx["height"] as int?,
version: jsonTx["version"] as int,
timestamp: jsonTx["blocktime"] as int? ??
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
inputs: List.unmodifiable(inputs),
outputs: List.unmodifiable(outputs),
);
}
Future<Tuple2<Transaction, Address>> parseTransaction(