diff --git a/lib/models/isar/models/blockchain_data/v2/input_v2.dart b/lib/models/isar/models/blockchain_data/v2/input_v2.dart index 7880472b0..f604b75af 100644 --- a/lib/models/isar/models/blockchain_data/v2/input_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/input_v2.dart @@ -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 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 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 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' ')'; } } diff --git a/lib/models/isar/models/blockchain_data/v2/output_v2.dart b/lib/models/isar/models/blockchain_data/v2/output_v2.dart index 344b15cca..b65dc4b23 100644 --- a/lib/models/isar/models/blockchain_data/v2/output_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/output_v2.dart @@ -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 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 json) { + static OutputV2 isarCantDoRequiredInDefaultConstructor({ + required String scriptPubKeyHex, + required String valueStringSats, + required List addresses, + }) => + OutputV2() + ..scriptPubKeyHex = scriptPubKeyHex + ..valueStringSats = valueStringSats + ..addresses = List.unmodifiable(addresses); + + static OutputV2 fromElectrumXJson( + Map json, { + required int decimalPlaces, + }) { try { - final temp = Decimal.parse(json["value"].toString()); - if (temp < Decimal.zero) { - throw Exception("Negative value found"); + List 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(List a, List 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/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index 3c8cc4fde..f98ca53ea 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -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 inputs; final List 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 json) { - try { - final inputs = (json["vin"] as List).map( - (e) => InputV2.fromElectrumXJson( - Map.from(e as Map), - ), - ); - final outputs = (json["vout"] as List).map( - (e) => OutputV2.fromElectrumXJson( - Map.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(List a, List 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/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 075018b13..2f34a89a9 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -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"); } diff --git a/lib/services/mixins/electrum_x_parsing.dart b/lib/services/mixins/electrum_x_parsing.dart index c811b2e36..5fe38dfe3 100644 --- a/lib/services/mixins/electrum_x_parsing.dart +++ b/lib/services/mixins/electrum_x_parsing.dart @@ -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 parseBchTx( - Map json, [ + Future 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 inputs = []; + for (final jsonInput in jsonTx["vin"] as List) { + final map = Map.from(jsonInput as Map); + + final List 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.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 outputs = []; + for (final outputJson in jsonTx["vout"] as List) { + final output = OutputV2.fromElectrumXJson( + Map.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> parseTransaction(