2022-12-22 01:07:13 +00:00
|
|
|
import 'dart:convert';
|
2022-12-20 23:00:03 +00:00
|
|
|
import 'dart:typed_data';
|
|
|
|
|
|
|
|
import 'package:bip47/bip47.dart';
|
2023-01-05 18:48:18 +00:00
|
|
|
import 'package:bip47/src/util.dart';
|
2022-12-20 23:00:03 +00:00
|
|
|
import 'package:bitcoindart/bitcoindart.dart';
|
2023-01-05 18:48:18 +00:00
|
|
|
import 'package:bitcoindart/src/utils/constants/op.dart' as op;
|
|
|
|
import 'package:bitcoindart/src/utils/script.dart' as bscript;
|
|
|
|
import 'package:decimal/decimal.dart';
|
2022-12-20 23:00:03 +00:00
|
|
|
import 'package:pointycastle/digests/sha256.dart';
|
|
|
|
import 'package:stackwallet/hive/db.dart';
|
2023-01-05 22:57:53 +00:00
|
|
|
import 'package:stackwallet/models/paymint/utxo_model.dart';
|
2022-12-20 23:00:03 +00:00
|
|
|
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
|
2023-01-05 18:48:18 +00:00
|
|
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
2022-12-21 23:02:14 +00:00
|
|
|
import 'package:stackwallet/utilities/format.dart';
|
2023-01-06 19:55:47 +00:00
|
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
|
|
|
|
class SWException with Exception {
|
|
|
|
SWException(this.message);
|
|
|
|
|
|
|
|
final String message;
|
|
|
|
|
|
|
|
@override
|
|
|
|
toString() => message;
|
|
|
|
}
|
|
|
|
|
|
|
|
class InsufficientBalanceException extends SWException {
|
|
|
|
InsufficientBalanceException(super.message);
|
|
|
|
}
|
2022-12-20 23:00:03 +00:00
|
|
|
|
|
|
|
extension PayNym on DogecoinWallet {
|
|
|
|
// fetch or generate this wallet's bip47 payment code
|
|
|
|
Future<PaymentCode> getPaymentCode() async {
|
|
|
|
final paymentCodeString = DB.instance
|
|
|
|
.get<dynamic>(boxName: walletId, key: "paymentCodeString") as String?;
|
|
|
|
PaymentCode paymentCode;
|
|
|
|
if (paymentCodeString == null) {
|
|
|
|
final node = getBip32Root((await mnemonic).join(" "), network)
|
|
|
|
.derivePath("m/47'/0'/0'");
|
|
|
|
paymentCode =
|
|
|
|
PaymentCode.initFromPubKey(node.publicKey, node.chainCode, network);
|
|
|
|
await DB.instance.put<dynamic>(
|
|
|
|
boxName: walletId,
|
|
|
|
key: "paymentCodeString",
|
|
|
|
value: paymentCode.toString());
|
|
|
|
} else {
|
|
|
|
paymentCode = PaymentCode.fromPaymentCode(paymentCodeString, network);
|
|
|
|
}
|
|
|
|
return paymentCode;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Uint8List> signWithNotificationKey(Uint8List data) async {
|
|
|
|
final node = getBip32Root((await mnemonic).join(" "), network)
|
|
|
|
.derivePath("m/47'/0'/0'");
|
|
|
|
final pair = ECPair.fromPrivateKey(node.privateKey!, network: network);
|
|
|
|
final signed = pair.sign(SHA256Digest().process(data));
|
|
|
|
return signed;
|
|
|
|
}
|
2022-12-21 16:17:53 +00:00
|
|
|
|
2022-12-21 23:02:14 +00:00
|
|
|
Future<String> signStringWithNotificationKey(String data) async {
|
|
|
|
final bytes =
|
2022-12-22 01:07:13 +00:00
|
|
|
await signWithNotificationKey(Uint8List.fromList(utf8.encode(data)));
|
2022-12-21 23:02:14 +00:00
|
|
|
return Format.uint8listToString(bytes);
|
|
|
|
// final bytes =
|
|
|
|
// await signWithNotificationKey(Uint8List.fromList(utf8.encode(data)));
|
|
|
|
// return Format.uint8listToString(bytes);
|
|
|
|
}
|
|
|
|
|
2022-12-21 16:17:53 +00:00
|
|
|
// Future<Map<String, dynamic>> prepareNotificationTransaction(
|
|
|
|
// String targetPaymentCode) async {}
|
2023-01-05 18:48:18 +00:00
|
|
|
|
2023-01-06 19:55:47 +00:00
|
|
|
Future<Map<String, dynamic>> buildNotificationTx({
|
|
|
|
required int selectedTxFeeRate,
|
|
|
|
required String targetPaymentCodeString,
|
|
|
|
int additionalOutputs = 0,
|
|
|
|
List<UtxoObject>? utxos,
|
|
|
|
}) async {
|
|
|
|
const amountToSend = DUST_LIMIT;
|
|
|
|
final List<UtxoObject> availableOutputs = utxos ?? outputsList;
|
|
|
|
final List<UtxoObject> spendableOutputs = [];
|
|
|
|
int spendableSatoshiValue = 0;
|
|
|
|
|
|
|
|
// Build list of spendable outputs and totaling their satoshi amount
|
|
|
|
for (var i = 0; i < availableOutputs.length; i++) {
|
|
|
|
if (availableOutputs[i].blocked == false &&
|
|
|
|
availableOutputs[i].status.confirmed == true) {
|
|
|
|
spendableOutputs.add(availableOutputs[i]);
|
|
|
|
spendableSatoshiValue += availableOutputs[i].value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (spendableSatoshiValue < amountToSend) {
|
|
|
|
// insufficient balance
|
|
|
|
throw InsufficientBalanceException(
|
|
|
|
"Spendable balance is less than the minimum required for a notification transaction.");
|
|
|
|
} else if (spendableSatoshiValue == amountToSend) {
|
|
|
|
// insufficient balance due to missing amount to cover fee
|
|
|
|
throw InsufficientBalanceException(
|
|
|
|
"Remaining balance does not cover the network fee.");
|
|
|
|
}
|
|
|
|
|
|
|
|
// sort spendable by age (oldest first)
|
|
|
|
spendableOutputs.sort(
|
|
|
|
(a, b) => b.status.confirmations.compareTo(a.status.confirmations));
|
|
|
|
|
|
|
|
int satoshisBeingUsed = 0;
|
|
|
|
int outputsBeingUsed = 0;
|
|
|
|
List<UtxoObject> utxoObjectsToUse = [];
|
|
|
|
|
|
|
|
for (int i = 0;
|
|
|
|
satoshisBeingUsed < amountToSend && i < spendableOutputs.length;
|
|
|
|
i++) {
|
|
|
|
utxoObjectsToUse.add(spendableOutputs[i]);
|
|
|
|
satoshisBeingUsed += spendableOutputs[i].value;
|
|
|
|
outputsBeingUsed += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// add additional outputs if required
|
|
|
|
for (int i = 0;
|
|
|
|
i < additionalOutputs && outputsBeingUsed < spendableOutputs.length;
|
|
|
|
i++) {
|
|
|
|
utxoObjectsToUse.add(spendableOutputs[outputsBeingUsed]);
|
|
|
|
satoshisBeingUsed += spendableOutputs[outputsBeingUsed].value;
|
|
|
|
outputsBeingUsed += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// gather required signing data
|
|
|
|
final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse);
|
|
|
|
|
|
|
|
final int vSizeForNoChange = (await _createNotificationTx(
|
|
|
|
targetPaymentCodeString: targetPaymentCodeString,
|
|
|
|
utxosToUse: utxoObjectsToUse,
|
|
|
|
utxoSigningData: utxoSigningData,
|
|
|
|
change: 0))
|
|
|
|
.item2;
|
|
|
|
|
|
|
|
final int vSizeForWithChange = (await _createNotificationTx(
|
|
|
|
targetPaymentCodeString: targetPaymentCodeString,
|
|
|
|
utxosToUse: utxoObjectsToUse,
|
|
|
|
utxoSigningData: utxoSigningData,
|
|
|
|
change: satoshisBeingUsed - amountToSend))
|
|
|
|
.item2;
|
|
|
|
|
|
|
|
// Assume 2 outputs, for recipient and payment code script
|
|
|
|
int feeForNoChange = estimateTxFee(
|
|
|
|
vSize: vSizeForNoChange,
|
|
|
|
feeRatePerKB: selectedTxFeeRate,
|
|
|
|
);
|
|
|
|
|
|
|
|
// Assume 3 outputs, for recipient, payment code script, and change
|
|
|
|
int feeForWithChange = estimateTxFee(
|
|
|
|
vSize: vSizeForWithChange,
|
|
|
|
feeRatePerKB: selectedTxFeeRate,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (feeForNoChange < vSizeForNoChange * 1000) {
|
|
|
|
feeForNoChange = vSizeForNoChange * 1000;
|
|
|
|
}
|
|
|
|
if (feeForWithChange < vSizeForWithChange * 1000) {
|
|
|
|
feeForWithChange = vSizeForWithChange * 1000;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (satoshisBeingUsed - amountToSend > feeForNoChange + DUST_LIMIT) {
|
|
|
|
// try to add change output due to "left over" amount being greater than
|
|
|
|
// the estimated fee + the dust limit
|
|
|
|
int changeAmount = satoshisBeingUsed - amountToSend - feeForWithChange;
|
|
|
|
|
|
|
|
// check estimates are correct and build notification tx
|
|
|
|
if (changeAmount >= DUST_LIMIT &&
|
|
|
|
satoshisBeingUsed - amountToSend - changeAmount == feeForWithChange) {
|
|
|
|
final txn = await _createNotificationTx(
|
|
|
|
targetPaymentCodeString: targetPaymentCodeString,
|
|
|
|
utxosToUse: utxoObjectsToUse,
|
|
|
|
utxoSigningData: utxoSigningData,
|
|
|
|
change: changeAmount,
|
|
|
|
);
|
|
|
|
|
|
|
|
int feeBeingPaid = satoshisBeingUsed - amountToSend - changeAmount;
|
|
|
|
|
|
|
|
Map<String, dynamic> transactionObject = {
|
|
|
|
"hex": txn.item1,
|
|
|
|
"recipientPaynym": targetPaymentCodeString,
|
|
|
|
// "recipientAmt": recipientsAmtArray[0],
|
|
|
|
"fee": feeBeingPaid,
|
|
|
|
"vSize": txn.item2,
|
|
|
|
};
|
|
|
|
return transactionObject;
|
|
|
|
} else {
|
|
|
|
// something broke during fee estimation or the change amount is smaller
|
|
|
|
// than the dust limit. Try without change
|
|
|
|
final txn = await _createNotificationTx(
|
|
|
|
targetPaymentCodeString: targetPaymentCodeString,
|
|
|
|
utxosToUse: utxoObjectsToUse,
|
|
|
|
utxoSigningData: utxoSigningData,
|
|
|
|
change: 0,
|
|
|
|
);
|
|
|
|
|
|
|
|
int feeBeingPaid = satoshisBeingUsed - amountToSend;
|
|
|
|
|
|
|
|
Map<String, dynamic> transactionObject = {
|
|
|
|
"hex": txn.item1,
|
|
|
|
"recipientPaynym": targetPaymentCodeString,
|
|
|
|
// "recipientAmt": recipientsAmtArray[0],
|
|
|
|
"fee": feeBeingPaid,
|
|
|
|
"vSize": txn.item2,
|
|
|
|
};
|
|
|
|
return transactionObject;
|
|
|
|
}
|
|
|
|
} else if (satoshisBeingUsed - amountToSend >= feeForNoChange) {
|
|
|
|
// since we already checked if we need to add a change output we can just
|
|
|
|
// build without change here
|
|
|
|
final txn = await _createNotificationTx(
|
|
|
|
targetPaymentCodeString: targetPaymentCodeString,
|
|
|
|
utxosToUse: utxoObjectsToUse,
|
|
|
|
utxoSigningData: utxoSigningData,
|
|
|
|
change: 0,
|
|
|
|
);
|
|
|
|
|
|
|
|
int feeBeingPaid = satoshisBeingUsed - amountToSend;
|
|
|
|
|
|
|
|
Map<String, dynamic> transactionObject = {
|
|
|
|
"hex": txn.item1,
|
|
|
|
"recipientPaynym": targetPaymentCodeString,
|
|
|
|
// "recipientAmt": recipientsAmtArray[0],
|
|
|
|
"fee": feeBeingPaid,
|
|
|
|
"vSize": txn.item2,
|
|
|
|
};
|
|
|
|
return transactionObject;
|
|
|
|
} else {
|
|
|
|
// if we get here we do not have enough funds to cover the tx total so we
|
|
|
|
// check if we have any more available outputs and try again
|
|
|
|
if (spendableOutputs.length > outputsBeingUsed) {
|
|
|
|
return buildNotificationTx(
|
|
|
|
selectedTxFeeRate: selectedTxFeeRate,
|
|
|
|
targetPaymentCodeString: targetPaymentCodeString,
|
|
|
|
additionalOutputs: additionalOutputs + 1,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
throw InsufficientBalanceException(
|
|
|
|
"Remaining balance does not cover the network fee.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// return tuple with string value equal to the raw tx hex and the int value
|
|
|
|
// equal to its vSize
|
|
|
|
Future<Tuple2<String, int>> _createNotificationTx({
|
|
|
|
required String targetPaymentCodeString,
|
|
|
|
required List<UtxoObject> utxosToUse,
|
|
|
|
required Map<String, dynamic> utxoSigningData,
|
|
|
|
required int change,
|
|
|
|
}) async {
|
2023-01-05 18:48:18 +00:00
|
|
|
final targetPaymentCode =
|
|
|
|
PaymentCode.fromPaymentCode(targetPaymentCodeString, network);
|
|
|
|
final myCode = await getPaymentCode();
|
|
|
|
|
|
|
|
final utxo = utxosToUse.first;
|
|
|
|
final txPoint = utxo.txid.fromHex.toList();
|
|
|
|
final txPointIndex = utxo.vout;
|
|
|
|
|
|
|
|
final rev = Uint8List(txPoint.length + 4);
|
|
|
|
Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length);
|
|
|
|
final buffer = rev.buffer.asByteData();
|
|
|
|
buffer.setUint32(txPoint.length, txPointIndex, Endian.little);
|
|
|
|
|
|
|
|
final myKeyPair = utxoSigningData[utxo.txid]["keyPair"] as ECPair;
|
|
|
|
|
|
|
|
final S = SecretPoint(
|
|
|
|
myKeyPair.privateKey!,
|
|
|
|
targetPaymentCode.notificationPublicKey(),
|
|
|
|
);
|
|
|
|
|
|
|
|
final blindingMask = PaymentCode.getMask(S.ecdhSecret(), rev);
|
|
|
|
|
|
|
|
final blindedPaymentCode = PaymentCode.blind(
|
|
|
|
myCode.getPayload(),
|
|
|
|
blindingMask,
|
|
|
|
);
|
|
|
|
|
|
|
|
final opReturnScript = bscript.compile([
|
|
|
|
(op.OPS["OP_RETURN"] as int),
|
|
|
|
blindedPaymentCode,
|
|
|
|
]);
|
|
|
|
|
|
|
|
final bobP2PKH = P2PKH(
|
|
|
|
data: PaymentData(
|
|
|
|
pubkey: targetPaymentCode.notificationPublicKey(),
|
|
|
|
),
|
|
|
|
).data;
|
|
|
|
final notificationScript = bscript.compile([bobP2PKH.output]);
|
|
|
|
|
|
|
|
// build a notification tx
|
|
|
|
final txb = TransactionBuilder();
|
|
|
|
txb.setVersion(1);
|
|
|
|
|
|
|
|
txb.addInput(
|
2023-01-05 22:57:53 +00:00
|
|
|
utxo.txid,
|
2023-01-05 18:48:18 +00:00
|
|
|
txPointIndex,
|
|
|
|
);
|
|
|
|
|
2023-01-06 19:55:47 +00:00
|
|
|
txb.addOutput(targetPaymentCode.notificationAddress(), DUST_LIMIT);
|
2023-01-05 22:57:53 +00:00
|
|
|
txb.addOutput(opReturnScript, 0);
|
|
|
|
|
|
|
|
// TODO: add possible change output and mark output as dangerous
|
2023-01-06 19:55:47 +00:00
|
|
|
if (change > 0) {
|
|
|
|
// generate new change address if current change address has been used
|
|
|
|
await checkChangeAddressForTransactions(DerivePathType.bip44);
|
|
|
|
final String changeAddress =
|
|
|
|
await getCurrentAddressForChain(1, DerivePathType.bip44);
|
|
|
|
txb.addOutput(changeAddress, change);
|
|
|
|
}
|
2023-01-05 18:48:18 +00:00
|
|
|
|
|
|
|
txb.sign(
|
|
|
|
vin: 0,
|
|
|
|
keyPair: myKeyPair,
|
|
|
|
);
|
|
|
|
|
2023-01-06 19:55:47 +00:00
|
|
|
// sign rest of possible inputs
|
|
|
|
for (var i = 1; i < utxosToUse.length - 1; i++) {
|
|
|
|
final txid = utxosToUse[i].txid;
|
|
|
|
txb.sign(
|
|
|
|
vin: i,
|
|
|
|
keyPair: utxoSigningData[txid]["keyPair"] as ECPair,
|
|
|
|
// witnessValue: utxosToUse[i].value,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-01-05 18:48:18 +00:00
|
|
|
final builtTx = txb.build();
|
|
|
|
|
2023-01-06 19:55:47 +00:00
|
|
|
return Tuple2(builtTx.toHex(), builtTx.virtualSize());
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<bool> hasConfirmedNotificationTxSentTo(
|
|
|
|
String paymentCodeString) async {
|
|
|
|
final targetPaymentCode =
|
|
|
|
PaymentCode.fromPaymentCode(paymentCodeString, network);
|
|
|
|
final targetNotificationAddress = targetPaymentCode.notificationAddress();
|
|
|
|
|
|
|
|
final myTxHistory = (await transactionData)
|
|
|
|
.getAllTransactions()
|
|
|
|
.entries
|
|
|
|
.map((e) => e.value)
|
|
|
|
.where((e) =>
|
|
|
|
e.txType == "Sent" && e.address == targetNotificationAddress);
|
|
|
|
|
|
|
|
return myTxHistory.isNotEmpty;
|
|
|
|
}
|
|
|
|
|
|
|
|
// fetch paynym notification tx meta data
|
|
|
|
Set<Map<String, dynamic>> getPaynymNotificationTxInfo() {
|
|
|
|
final set = DB.instance.get<dynamic>(
|
|
|
|
boxName: walletId, key: "paynymNotificationTxInfo") as Set? ??
|
|
|
|
{};
|
|
|
|
|
|
|
|
return Set<Map<String, dynamic>>.from(set);
|
|
|
|
}
|
|
|
|
|
|
|
|
// add/update paynym notification tx meta data entry
|
|
|
|
Future<void> updatePaynymNotificationInfo({
|
|
|
|
required String txid,
|
|
|
|
required bool confirmed,
|
|
|
|
required String paymentCodeString,
|
|
|
|
}) async {
|
|
|
|
final data = getPaynymNotificationTxInfo();
|
|
|
|
data.add({
|
|
|
|
"txid": txid,
|
|
|
|
"confirmed": confirmed,
|
|
|
|
"paymentCodeString": paymentCodeString,
|
|
|
|
});
|
|
|
|
await DB.instance.put<dynamic>(
|
|
|
|
boxName: walletId,
|
|
|
|
key: "paynymNotificationTxInfo",
|
|
|
|
value: data,
|
|
|
|
);
|
2023-01-05 18:48:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Map<String, dynamic>> parseTransaction(
|
|
|
|
Map<String, dynamic> txData,
|
|
|
|
dynamic electrumxClient,
|
|
|
|
Set<String> myAddresses,
|
|
|
|
Set<String> myChangeAddresses,
|
|
|
|
Coin coin,
|
|
|
|
int minConfirms,
|
|
|
|
Decimal currentPrice,
|
|
|
|
) async {
|
|
|
|
Set<String> inputAddresses = {};
|
|
|
|
Set<String> outputAddresses = {};
|
|
|
|
|
|
|
|
int totalInputValue = 0;
|
|
|
|
int totalOutputValue = 0;
|
|
|
|
|
|
|
|
int amountSentFromWallet = 0;
|
|
|
|
int amountReceivedInWallet = 0;
|
|
|
|
|
|
|
|
// parse inputs
|
|
|
|
for (final input in txData["vin"] as List) {
|
|
|
|
final prevTxid = input["txid"] as String;
|
|
|
|
final prevOut = input["vout"] as int;
|
|
|
|
|
|
|
|
// fetch input tx to get address
|
|
|
|
final inputTx = await electrumxClient.getTransaction(
|
|
|
|
txHash: prevTxid,
|
|
|
|
coin: coin,
|
|
|
|
);
|
|
|
|
|
|
|
|
for (final output in inputTx["vout"] as List) {
|
|
|
|
// check matching output
|
|
|
|
if (prevOut == output["n"]) {
|
|
|
|
// get value
|
|
|
|
final value = Format.decimalAmountToSatoshis(
|
|
|
|
Decimal.parse(output["value"].toString()),
|
|
|
|
coin,
|
|
|
|
);
|
|
|
|
|
|
|
|
// add value to total
|
|
|
|
totalInputValue += value;
|
|
|
|
|
|
|
|
// get input(prevOut) address
|
|
|
|
final address = output["scriptPubKey"]?["addresses"]?[0] as String? ??
|
|
|
|
output["scriptPubKey"]?["address"] as String?;
|
|
|
|
if (address != null) {
|
|
|
|
inputAddresses.add(address);
|
|
|
|
|
|
|
|
// if input was from my wallet, add value to amount sent
|
|
|
|
if (myAddresses.contains(address)) {
|
|
|
|
amountSentFromWallet += value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse outputs
|
|
|
|
for (final output in txData["vout"] as List) {
|
|
|
|
// get value
|
|
|
|
final value = Format.decimalAmountToSatoshis(
|
|
|
|
Decimal.parse(output["value"].toString()),
|
|
|
|
coin,
|
|
|
|
);
|
|
|
|
|
|
|
|
// add value to total
|
|
|
|
totalOutputValue += value;
|
|
|
|
|
|
|
|
// get output address
|
|
|
|
final address = output["scriptPubKey"]["addresses"][0] as String? ??
|
|
|
|
output["scriptPubKey"]["address"] as String?;
|
|
|
|
if (address != null) {
|
|
|
|
outputAddresses.add(address);
|
|
|
|
|
|
|
|
// if output was from my wallet, add value to amount received
|
|
|
|
if (myAddresses.contains(address)) {
|
|
|
|
amountReceivedInWallet += value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
final mySentFromAddresses = myAddresses.intersection(inputAddresses);
|
|
|
|
final myReceivedOnAddresses = myAddresses.intersection(outputAddresses);
|
|
|
|
|
|
|
|
final fee = totalInputValue - totalOutputValue;
|
|
|
|
|
|
|
|
// create normalized tx data map
|
|
|
|
Map<String, dynamic> normalizedTx = {};
|
|
|
|
|
|
|
|
final int confirms = txData["confirmations"] as int? ?? 0;
|
|
|
|
|
|
|
|
normalizedTx["txid"] = txData["txid"] as String;
|
|
|
|
normalizedTx["confirmed_status"] = confirms >= minConfirms;
|
|
|
|
normalizedTx["confirmations"] = confirms;
|
|
|
|
normalizedTx["timestamp"] = txData["blocktime"] as int? ??
|
|
|
|
(DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
|
|
|
normalizedTx["aliens"] = <dynamic>[];
|
|
|
|
normalizedTx["fees"] = fee;
|
|
|
|
normalizedTx["address"] = txData["address"] as String;
|
|
|
|
normalizedTx["inputSize"] = txData["vin"].length;
|
|
|
|
normalizedTx["outputSize"] = txData["vout"].length;
|
|
|
|
normalizedTx["inputs"] = txData["vin"];
|
|
|
|
normalizedTx["outputs"] = txData["vout"];
|
|
|
|
normalizedTx["height"] = txData["height"] as int;
|
|
|
|
|
|
|
|
int amount;
|
|
|
|
String type;
|
|
|
|
if (mySentFromAddresses.isNotEmpty && myReceivedOnAddresses.isNotEmpty) {
|
|
|
|
// tx is sent to self
|
|
|
|
type = "Sent to self";
|
2023-01-05 22:57:53 +00:00
|
|
|
amount = amountSentFromWallet - amountReceivedInWallet - fee;
|
2023-01-05 18:48:18 +00:00
|
|
|
} else if (mySentFromAddresses.isNotEmpty) {
|
|
|
|
// outgoing tx
|
|
|
|
type = "Sent";
|
|
|
|
amount = amountSentFromWallet;
|
|
|
|
} else {
|
|
|
|
// incoming tx
|
|
|
|
type = "Received";
|
|
|
|
amount = amountReceivedInWallet;
|
|
|
|
}
|
|
|
|
|
|
|
|
normalizedTx["txType"] = type;
|
|
|
|
normalizedTx["amount"] = amount;
|
|
|
|
normalizedTx["worthNow"] = (Format.satoshisToAmount(
|
|
|
|
amount,
|
|
|
|
coin: coin,
|
|
|
|
) *
|
|
|
|
currentPrice)
|
|
|
|
.toStringAsFixed(2);
|
|
|
|
|
|
|
|
return normalizedTx;
|
2022-12-20 23:00:03 +00:00
|
|
|
}
|