mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-17 17:57:40 +00:00
ff1f746a76
* WIP: Fix particl for non standart txs send * Use stackwallet default node and fix txs all appearing as received with incorrect amounts * Clean up logging, fix all txs showing as received
438 lines
14 KiB
Dart
438 lines
14 KiB
Dart
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<TransactionChunk> txChunks;
|
|
|
|
TransactionData({this.txChunks = const []});
|
|
|
|
factory TransactionData.fromJson(Map<String, dynamic> json) {
|
|
var dateTimeChunks = json['dateTimeChunks'] as List;
|
|
List<TransactionChunk> chunksList = dateTimeChunks
|
|
.map((txChunk) =>
|
|
TransactionChunk.fromJson(txChunk as Map<String, dynamic>))
|
|
.toList();
|
|
|
|
return TransactionData(txChunks: chunksList);
|
|
}
|
|
|
|
factory TransactionData.fromMap(Map<String, Transaction> transactions) {
|
|
Map<String, List<Transaction>> chunks = {};
|
|
transactions.forEach((key, value) {
|
|
String date = extractDateFromTimestamp(value.timestamp);
|
|
if (!chunks.containsKey(date)) {
|
|
chunks[date] = [];
|
|
}
|
|
chunks[date]!.add(value);
|
|
});
|
|
List<TransactionChunk> 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<String, Transaction> getAllTransactions() {
|
|
Map<String, Transaction> 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<Transaction> transactions;
|
|
|
|
TransactionChunk({required this.timestamp, required this.transactions});
|
|
|
|
factory TransactionChunk.fromJson(Map<String, dynamic> json) {
|
|
var txArray = json['transactions'] as List;
|
|
List<Transaction> txList = txArray
|
|
.map((tx) => Transaction.fromJson(tx as Map<String, dynamic>))
|
|
.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<dynamic> 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<Input> inputs;
|
|
// @HiveField(12)
|
|
final List<Output> 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<String, dynamic> json) {
|
|
var inputArray = json['inputs'] as List;
|
|
var outputArray = json['outputs'] as List;
|
|
|
|
List<Input> inputList = inputArray
|
|
.map((input) => Input.fromJson(Map<String, dynamic>.from(input as Map)))
|
|
.toList();
|
|
List<Output> outputList = outputArray
|
|
.map((output) =>
|
|
Output.fromJson(Map<String, dynamic>.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<String, dynamic> 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<dynamic>? aliens,
|
|
String? worthNow,
|
|
String? worthAtBlockTimestamp,
|
|
int? fees,
|
|
int? inputSize,
|
|
int? outputSize,
|
|
List<Input>? inputs,
|
|
List<Output>? 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<dynamic>? 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<String, dynamic> 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<String, dynamic> json) {
|
|
// TODO determine if any of this code is needed.
|
|
try {
|
|
// 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: "",
|
|
// scriptpubkeyAsm: "",
|
|
// scriptpubkeyType: "",
|
|
// 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());
|
|
// }
|
|
} catch (s, e) {
|
|
return Output(
|
|
// Return output object with null values; allows wallet history to be built
|
|
scriptpubkey: "",
|
|
scriptpubkeyAsm: "",
|
|
scriptpubkeyType: "",
|
|
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());
|
|
}
|
|
}
|
|
}
|