mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-18 16:44:32 +00:00
paynymn fixes and clean up
This commit is contained in:
parent
4b14cd861e
commit
7af2fea977
7 changed files with 387 additions and 289 deletions
|
@ -4,6 +4,7 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:stackwallet/db/hive/db.dart';
|
import 'package:stackwallet/db/hive/db.dart';
|
||||||
import 'package:stackwallet/db/isar/main_db.dart';
|
import 'package:stackwallet/db/isar/main_db.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/models/isar/models/isar_models.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
||||||
|
@ -16,6 +17,11 @@ Future<void> migrateWalletsToIsar({
|
||||||
required SecureStorageInterface secureStore,
|
required SecureStorageInterface secureStore,
|
||||||
}) async {
|
}) async {
|
||||||
await MainDB.instance.initMainDB();
|
await MainDB.instance.initMainDB();
|
||||||
|
|
||||||
|
// ensure fresh
|
||||||
|
await MainDB.instance.isar
|
||||||
|
.writeTxn(() async => await MainDB.instance.isar.transactionV2s.clear());
|
||||||
|
|
||||||
final allWalletsBox = await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
|
final allWalletsBox = await Hive.openBox<dynamic>(DB.boxNameAllWalletsData);
|
||||||
|
|
||||||
final names = DB.instance
|
final names = DB.instance
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
part 'input_v2.g.dart';
|
part 'input_v2.g.dart';
|
||||||
|
@ -86,6 +88,38 @@ class InputV2 {
|
||||||
..coinbase = coinbase
|
..coinbase = coinbase
|
||||||
..walletOwns = walletOwns;
|
..walletOwns = walletOwns;
|
||||||
|
|
||||||
|
static InputV2 fromElectrumxJson({
|
||||||
|
required Map<String, dynamic> json,
|
||||||
|
required OutpointV2? outpoint,
|
||||||
|
required List<String> addresses,
|
||||||
|
required String valueStringSats,
|
||||||
|
required String? coinbase,
|
||||||
|
required bool walletOwns,
|
||||||
|
}) {
|
||||||
|
final dynamicWitness = json["witness"] ?? json["txinwitness"];
|
||||||
|
|
||||||
|
final String? witness;
|
||||||
|
if (dynamicWitness is Map || dynamicWitness is List) {
|
||||||
|
witness = jsonEncode(dynamicWitness);
|
||||||
|
} else if (dynamicWitness is String) {
|
||||||
|
witness = dynamicWitness;
|
||||||
|
} else {
|
||||||
|
witness = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return InputV2()
|
||||||
|
..scriptSigHex = json["scriptSig"]?["hex"] as String?
|
||||||
|
..scriptSigAsm = json["scriptSig"]?["asm"] as String?
|
||||||
|
..sequence = json["sequence"] as int?
|
||||||
|
..outpoint = outpoint
|
||||||
|
..addresses = List.unmodifiable(addresses)
|
||||||
|
..valueStringSats = valueStringSats
|
||||||
|
..witness = witness
|
||||||
|
..innerRedeemScriptAsm = json["innerRedeemscriptAsm"] as String?
|
||||||
|
..coinbase = coinbase
|
||||||
|
..walletOwns = walletOwns;
|
||||||
|
}
|
||||||
|
|
||||||
InputV2 copyWith({
|
InputV2 copyWith({
|
||||||
String? scriptSigHex,
|
String? scriptSigHex,
|
||||||
String? scriptSigAsm,
|
String? scriptSigAsm,
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:stackwallet/models/paynym/created_paynym.dart';
|
import 'package:stackwallet/models/paynym/created_paynym.dart';
|
||||||
import 'package:stackwallet/models/paynym/paynym_account.dart';
|
import 'package:stackwallet/models/paynym/paynym_account.dart';
|
||||||
import 'package:stackwallet/models/paynym/paynym_claim.dart';
|
import 'package:stackwallet/models/paynym/paynym_claim.dart';
|
||||||
|
@ -57,11 +56,11 @@ class PaynymIsApi {
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint("Paynym request uri: $uri");
|
// debugPrint("Paynym request uri: $uri");
|
||||||
debugPrint("Paynym request body: $body");
|
// debugPrint("Paynym request body: $body");
|
||||||
debugPrint("Paynym request headers: $headers");
|
// debugPrint("Paynym request headers: $headers");
|
||||||
debugPrint("Paynym response code: ${response.code}");
|
// debugPrint("Paynym response code: ${response.code}");
|
||||||
debugPrint("Paynym response body: ${response.body}");
|
// debugPrint("Paynym response body: ${response.body}");
|
||||||
|
|
||||||
return Tuple2(
|
return Tuple2(
|
||||||
jsonDecode(response.body) as Map<String, dynamic>,
|
jsonDecode(response.body) as Map<String, dynamic>,
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
import 'package:bip47/src/util.dart';
|
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
|
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
|
||||||
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.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/utilities/amount/amount.dart';
|
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||||
import 'package:stackwallet/utilities/extensions/extensions.dart';
|
|
||||||
import 'package:stackwallet/utilities/logger.dart';
|
|
||||||
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
|
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
|
||||||
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
||||||
import 'package:stackwallet/wallets/crypto_currency/interfaces/paynym_currency_interface.dart';
|
import 'package:stackwallet/wallets/crypto_currency/interfaces/paynym_currency_interface.dart';
|
||||||
|
@ -51,266 +44,6 @@ class BitcoinWallet<T extends PaynymCurrencyInterface> extends Bip39HDWallet<T>
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> updateTransactions() async {
|
|
||||||
// Get all addresses.
|
|
||||||
List<Address> allAddressesOld = await fetchAddressesForElectrumXScan();
|
|
||||||
|
|
||||||
// Separate receiving and change addresses.
|
|
||||||
Set<String> receivingAddresses = allAddressesOld
|
|
||||||
.where((e) => e.subType == AddressSubType.receiving)
|
|
||||||
.map((e) => e.value)
|
|
||||||
.toSet();
|
|
||||||
Set<String> changeAddresses = allAddressesOld
|
|
||||||
.where((e) => e.subType == AddressSubType.change)
|
|
||||||
.map((e) => e.value)
|
|
||||||
.toSet();
|
|
||||||
|
|
||||||
// Remove duplicates.
|
|
||||||
final allAddressesSet = {...receivingAddresses, ...changeAddresses};
|
|
||||||
|
|
||||||
// Fetch history from ElectrumX.
|
|
||||||
final List<Map<String, dynamic>> allTxHashes =
|
|
||||||
await fetchHistory(allAddressesSet);
|
|
||||||
|
|
||||||
// Only parse new txs (not in db yet).
|
|
||||||
List<Map<String, dynamic>> allTransactions = [];
|
|
||||||
for (final txHash in allTxHashes) {
|
|
||||||
// Check for duplicates by searching for tx by tx_hash in db.
|
|
||||||
final storedTx = await mainDB.isar.transactionV2s
|
|
||||||
.where()
|
|
||||||
.txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId)
|
|
||||||
.findFirst();
|
|
||||||
|
|
||||||
if (storedTx == null ||
|
|
||||||
storedTx.height == null ||
|
|
||||||
(storedTx.height != null && storedTx.height! <= 0)) {
|
|
||||||
// Tx not in db yet.
|
|
||||||
final tx = await electrumXCachedClient.getTransaction(
|
|
||||||
txHash: txHash["tx_hash"] as String,
|
|
||||||
verbose: true,
|
|
||||||
coin: cryptoCurrency.coin,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only tx to list once.
|
|
||||||
if (allTransactions
|
|
||||||
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
|
|
||||||
-1) {
|
|
||||||
tx["height"] = txHash["height"];
|
|
||||||
allTransactions.add(tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse all new txs.
|
|
||||||
final List<TransactionV2> txns = [];
|
|
||||||
for (final txData in allTransactions) {
|
|
||||||
bool wasSentFromThisWallet = false;
|
|
||||||
// Set to true if any inputs were detected as owned by this wallet.
|
|
||||||
|
|
||||||
bool wasReceivedInThisWallet = false;
|
|
||||||
// Set to true if any outputs were detected as owned by this wallet.
|
|
||||||
|
|
||||||
// Parse inputs.
|
|
||||||
BigInt amountReceivedInThisWallet = BigInt.zero;
|
|
||||||
BigInt changeAmountReceivedInThisWallet = BigInt.zero;
|
|
||||||
final List<InputV2> inputs = [];
|
|
||||||
for (final jsonInput in txData["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) {
|
|
||||||
// Not a coinbase (ie a typical input).
|
|
||||||
final txid = map["txid"] as String;
|
|
||||||
final vout = map["vout"] as int;
|
|
||||||
|
|
||||||
final inputTx = await electrumXCachedClient.getTransaction(
|
|
||||||
txHash: txid,
|
|
||||||
coin: cryptoCurrency.coin,
|
|
||||||
);
|
|
||||||
|
|
||||||
final prevOutJson = Map<String, dynamic>.from(
|
|
||||||
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
|
|
||||||
as Map);
|
|
||||||
|
|
||||||
final prevOut = OutputV2.fromElectrumXJson(
|
|
||||||
prevOutJson,
|
|
||||||
decimalPlaces: cryptoCurrency.fractionDigits,
|
|
||||||
isFullAmountNotSats: true,
|
|
||||||
walletOwns: false, // Doesn't matter here as this is not saved.
|
|
||||||
);
|
|
||||||
|
|
||||||
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
|
|
||||||
txid: txid,
|
|
||||||
vout: vout,
|
|
||||||
);
|
|
||||||
valueStringSats = prevOut.valueStringSats;
|
|
||||||
addresses.addAll(prevOut.addresses);
|
|
||||||
}
|
|
||||||
|
|
||||||
InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor(
|
|
||||||
scriptSigHex: map["scriptSig"]?["hex"] as String?,
|
|
||||||
scriptSigAsm: map["scriptSig"]?["asm"] 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?,
|
|
||||||
// Need addresses before we can know if the wallet owns this input.
|
|
||||||
walletOwns: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if input was from this wallet.
|
|
||||||
if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) {
|
|
||||||
wasSentFromThisWallet = true;
|
|
||||||
input = input.copyWith(walletOwns: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
inputs.add(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse outputs.
|
|
||||||
final List<OutputV2> outputs = [];
|
|
||||||
for (final outputJson in txData["vout"] as List) {
|
|
||||||
OutputV2 output = OutputV2.fromElectrumXJson(
|
|
||||||
Map<String, dynamic>.from(outputJson as Map),
|
|
||||||
decimalPlaces: cryptoCurrency.fractionDigits,
|
|
||||||
isFullAmountNotSats: true,
|
|
||||||
// Need addresses before we can know if the wallet owns this input.
|
|
||||||
walletOwns: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If output was to my wallet, add value to amount received.
|
|
||||||
if (receivingAddresses
|
|
||||||
.intersection(output.addresses.toSet())
|
|
||||||
.isNotEmpty) {
|
|
||||||
wasReceivedInThisWallet = true;
|
|
||||||
amountReceivedInThisWallet += output.value;
|
|
||||||
output = output.copyWith(walletOwns: true);
|
|
||||||
} else if (changeAddresses
|
|
||||||
.intersection(output.addresses.toSet())
|
|
||||||
.isNotEmpty) {
|
|
||||||
wasReceivedInThisWallet = true;
|
|
||||||
changeAmountReceivedInThisWallet += output.value;
|
|
||||||
output = output.copyWith(walletOwns: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
outputs.add(output);
|
|
||||||
}
|
|
||||||
|
|
||||||
final totalOut = outputs
|
|
||||||
.map((e) => e.value)
|
|
||||||
.fold(BigInt.zero, (value, element) => value + element);
|
|
||||||
|
|
||||||
TransactionType type;
|
|
||||||
TransactionSubType subType = TransactionSubType.none;
|
|
||||||
if (outputs.length > 1 && inputs.isNotEmpty) {
|
|
||||||
for (int i = 0; i < outputs.length; i++) {
|
|
||||||
List<String>? scriptChunks = outputs[i].scriptPubKeyAsm?.split(" ");
|
|
||||||
if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
|
|
||||||
final blindedPaymentCode = scriptChunks![1];
|
|
||||||
final bytes = blindedPaymentCode.fromHex;
|
|
||||||
|
|
||||||
// https://en.bitcoin.it/wiki/BIP_0047#Sending
|
|
||||||
if (bytes.length == 80 && bytes.first == 1) {
|
|
||||||
subType = TransactionSubType.bip47Notification;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// At least one input was owned by this wallet.
|
|
||||||
if (wasSentFromThisWallet) {
|
|
||||||
type = TransactionType.outgoing;
|
|
||||||
|
|
||||||
if (wasReceivedInThisWallet) {
|
|
||||||
if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet ==
|
|
||||||
totalOut) {
|
|
||||||
// Definitely sent all to self.
|
|
||||||
type = TransactionType.sentToSelf;
|
|
||||||
} else if (amountReceivedInThisWallet == BigInt.zero) {
|
|
||||||
// Most likely just a typical send, do nothing here yet.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (wasReceivedInThisWallet) {
|
|
||||||
// Only found outputs owned by this wallet.
|
|
||||||
type = TransactionType.incoming;
|
|
||||||
|
|
||||||
// TODO: [prio=none] Check for special Bitcoin outputs like ordinals.
|
|
||||||
} else {
|
|
||||||
Logging.instance.log(
|
|
||||||
"Unexpected tx found (ignoring it): $txData",
|
|
||||||
level: LogLevel.Error,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final tx = TransactionV2(
|
|
||||||
walletId: walletId,
|
|
||||||
blockHash: txData["blockhash"] as String?,
|
|
||||||
hash: txData["hash"] as String,
|
|
||||||
txid: txData["txid"] as String,
|
|
||||||
height: txData["height"] as int?,
|
|
||||||
version: txData["version"] as int,
|
|
||||||
timestamp: txData["blocktime"] as int? ??
|
|
||||||
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
|
|
||||||
inputs: List.unmodifiable(inputs),
|
|
||||||
outputs: List.unmodifiable(outputs),
|
|
||||||
type: type,
|
|
||||||
subType: subType,
|
|
||||||
otherData: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
txns.add(tx);
|
|
||||||
}
|
|
||||||
|
|
||||||
await mainDB.updateOrPutTransactionV2s(txns);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<({String? blockedReason, bool blocked, String? utxoLabel})>
|
|
||||||
checkBlockUTXO(
|
|
||||||
Map<String, dynamic> jsonUTXO,
|
|
||||||
String? scriptPubKeyHex,
|
|
||||||
Map<String, dynamic>? jsonTX,
|
|
||||||
String? utxoOwnerAddress,
|
|
||||||
) async {
|
|
||||||
bool blocked = false;
|
|
||||||
String? blockedReason;
|
|
||||||
|
|
||||||
if (jsonTX != null) {
|
|
||||||
// check for bip47 notification
|
|
||||||
final outputs = jsonTX["vout"] as List;
|
|
||||||
for (final output in outputs) {
|
|
||||||
List<String>? scriptChunks =
|
|
||||||
(output['scriptPubKey']?['asm'] as String?)?.split(" ");
|
|
||||||
if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
|
|
||||||
final blindedPaymentCode = scriptChunks![1];
|
|
||||||
final bytes = blindedPaymentCode.toUint8ListFromHex;
|
|
||||||
|
|
||||||
// https://en.bitcoin.it/wiki/BIP_0047#Sending
|
|
||||||
if (bytes.length == 80 && bytes.first == 1) {
|
|
||||||
blocked = true;
|
|
||||||
blockedReason = "Paynym notification output. Incautious "
|
|
||||||
"handling of outputs from notification transactions "
|
|
||||||
"may cause unintended loss of privacy.";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) {
|
Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) {
|
||||||
return Amount(
|
return Amount(
|
||||||
|
|
|
@ -782,6 +782,7 @@ class FiroWallet extends Bip39HDWallet
|
||||||
static const String _lelantusCoinIsarRescanRequired =
|
static const String _lelantusCoinIsarRescanRequired =
|
||||||
"lelantusCoinIsarRescanRequired";
|
"lelantusCoinIsarRescanRequired";
|
||||||
|
|
||||||
|
// TODO: [prio=high]
|
||||||
Future<void> setLelantusCoinIsarRescanRequiredDone() async {
|
Future<void> setLelantusCoinIsarRescanRequiredDone() async {
|
||||||
await DB.instance.put<dynamic>(
|
await DB.instance.put<dynamic>(
|
||||||
boxName: walletId,
|
boxName: walletId,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
@ -19,9 +20,11 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
|
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
|
||||||
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
|
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
|
||||||
import 'package:stackwallet/utilities/logger.dart';
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/paynym_is_api.dart';
|
||||||
import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart';
|
import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart';
|
||||||
import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart';
|
import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart';
|
||||||
import 'package:stackwallet/wallets/models/tx_data.dart';
|
import 'package:stackwallet/wallets/models/tx_data.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
|
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
@ -1748,9 +1751,49 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
||||||
e.derivationIndex > highestReceivingIndexWithHistory);
|
e.derivationIndex > highestReceivingIndexWithHistory);
|
||||||
|
|
||||||
await mainDB.updateOrPutAddresses(addressesToStore);
|
await mainDB.updateOrPutAddresses(addressesToStore);
|
||||||
|
|
||||||
|
if (this is PaynymInterface) {
|
||||||
|
final notificationAddress =
|
||||||
|
await (this as PaynymInterface).getMyNotificationAddress();
|
||||||
|
|
||||||
|
await (this as BitcoinWallet)
|
||||||
|
.updateTransactions(overrideAddresses: [notificationAddress]);
|
||||||
|
|
||||||
|
// get own payment code
|
||||||
|
// isSegwit does not matter here at all
|
||||||
|
final myCode =
|
||||||
|
await (this as PaynymInterface).getPaymentCode(isSegwit: false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Set<String> codesToCheck = {};
|
||||||
|
final nym = await PaynymIsApi().nym(myCode.toString());
|
||||||
|
if (nym.value != null) {
|
||||||
|
for (final follower in nym.value!.followers) {
|
||||||
|
codesToCheck.add(follower.code);
|
||||||
|
}
|
||||||
|
for (final following in nym.value!.following) {
|
||||||
|
codesToCheck.add(following.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore paynym transactions
|
||||||
|
await (this as PaynymInterface).restoreAllHistory(
|
||||||
|
maxUnusedAddressGap: 20,
|
||||||
|
maxNumberOfIndexesToCheck: 10000,
|
||||||
|
paymentCodeStrings: codesToCheck,
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to check paynym.is followers/following for history during "
|
||||||
|
"bitcoin wallet ($walletId ${info.name}) "
|
||||||
|
"_recoverWalletFromBIP32SeedPhrase: $e/n$s",
|
||||||
|
level: LogLevel.Error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await refresh();
|
unawaited(refresh());
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
"Exception rethrown from electrumx_mixin recover(): $e\n$s",
|
"Exception rethrown from electrumx_mixin recover(): $e\n$s",
|
||||||
|
|
|
@ -4,7 +4,6 @@ import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:bip32/bip32.dart' as bip32;
|
import 'package:bip32/bip32.dart' as bip32;
|
||||||
import 'package:bip47/bip47.dart';
|
import 'package:bip47/bip47.dart';
|
||||||
import 'package:bip47/src/util.dart';
|
|
||||||
import 'package:bitcoindart/bitcoindart.dart' as btc_dart;
|
import 'package:bitcoindart/bitcoindart.dart' as btc_dart;
|
||||||
import 'package:bitcoindart/src/utils/constants/op.dart' as op;
|
import 'package:bitcoindart/src/utils/constants/op.dart' as op;
|
||||||
import 'package:bitcoindart/src/utils/script.dart' as bscript;
|
import 'package:bitcoindart/src/utils/script.dart' as bscript;
|
||||||
|
@ -13,6 +12,7 @@ import 'package:pointycastle/digests/sha256.dart';
|
||||||
import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart';
|
import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart';
|
||||||
import 'package:stackwallet/exceptions/wallet/paynym_send_exception.dart';
|
import 'package:stackwallet/exceptions/wallet/paynym_send_exception.dart';
|
||||||
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.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/blockchain_data/v2/transaction_v2.dart';
|
||||||
import 'package:stackwallet/models/isar/models/isar_models.dart';
|
import 'package:stackwallet/models/isar/models/isar_models.dart';
|
||||||
import 'package:stackwallet/models/signing_data.dart';
|
import 'package:stackwallet/models/signing_data.dart';
|
||||||
|
@ -20,6 +20,7 @@ import 'package:stackwallet/utilities/amount/amount.dart';
|
||||||
import 'package:stackwallet/utilities/bip32_utils.dart';
|
import 'package:stackwallet/utilities/bip32_utils.dart';
|
||||||
import 'package:stackwallet/utilities/bip47_utils.dart';
|
import 'package:stackwallet/utilities/bip47_utils.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
import 'package:stackwallet/utilities/extensions/extensions.dart';
|
||||||
import 'package:stackwallet/utilities/format.dart';
|
import 'package:stackwallet/utilities/format.dart';
|
||||||
import 'package:stackwallet/utilities/logger.dart';
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
import 'package:stackwallet/wallets/crypto_currency/interfaces/paynym_currency_interface.dart';
|
import 'package:stackwallet/wallets/crypto_currency/interfaces/paynym_currency_interface.dart';
|
||||||
|
@ -689,11 +690,11 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
|
||||||
final myCode = await getPaymentCode(isSegwit: false);
|
final myCode = await getPaymentCode(isSegwit: false);
|
||||||
|
|
||||||
final utxo = utxoSigningData.first.utxo;
|
final utxo = utxoSigningData.first.utxo;
|
||||||
final txPoint = utxo.txid.fromHex.reversed.toList();
|
final txPoint = utxo.txid.toUint8ListFromHex.reversed.toList();
|
||||||
final txPointIndex = utxo.vout;
|
final txPointIndex = utxo.vout;
|
||||||
|
|
||||||
final rev = Uint8List(txPoint.length + 4);
|
final rev = Uint8List(txPoint.length + 4);
|
||||||
Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length);
|
_copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length);
|
||||||
final buffer = rev.buffer.asByteData();
|
final buffer = rev.buffer.asByteData();
|
||||||
buffer.setUint32(txPoint.length, txPointIndex, Endian.little);
|
buffer.setUint32(txPoint.length, txPointIndex, Endian.little);
|
||||||
|
|
||||||
|
@ -923,16 +924,16 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
|
||||||
Uint8List? _pubKeyFromInput(InputV2 input) {
|
Uint8List? _pubKeyFromInput(InputV2 input) {
|
||||||
final scriptSigComponents = input.scriptSigAsm?.split(" ") ?? [];
|
final scriptSigComponents = input.scriptSigAsm?.split(" ") ?? [];
|
||||||
if (scriptSigComponents.length > 1) {
|
if (scriptSigComponents.length > 1) {
|
||||||
return scriptSigComponents[1].fromHex;
|
return scriptSigComponents[1].toUint8ListFromHex;
|
||||||
}
|
}
|
||||||
if (input.witness != null) {
|
if (input.witness != null) {
|
||||||
try {
|
try {
|
||||||
final witnessComponents = jsonDecode(input.witness!) as List;
|
final witnessComponents = jsonDecode(input.witness!) as List;
|
||||||
if (witnessComponents.length == 2) {
|
if (witnessComponents.length == 2) {
|
||||||
return (witnessComponents[1] as String).fromHex;
|
return (witnessComponents[1] as String).toUint8ListFromHex;
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (e, s) {
|
||||||
//
|
Logging.instance.log("_pubKeyFromInput: $e\n$s", level: LogLevel.Info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -952,11 +953,12 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
|
||||||
|
|
||||||
final designatedInput = transaction.inputs.first;
|
final designatedInput = transaction.inputs.first;
|
||||||
|
|
||||||
final txPoint = designatedInput.outpoint!.txid.fromHex.reversed.toList();
|
final txPoint =
|
||||||
|
designatedInput.outpoint!.txid.toUint8ListFromHex.reversed.toList();
|
||||||
final txPointIndex = designatedInput.outpoint!.vout;
|
final txPointIndex = designatedInput.outpoint!.vout;
|
||||||
|
|
||||||
final rev = Uint8List(txPoint.length + 4);
|
final rev = Uint8List(txPoint.length + 4);
|
||||||
Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length);
|
_copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length);
|
||||||
final buffer = rev.buffer.asByteData();
|
final buffer = rev.buffer.asByteData();
|
||||||
buffer.setUint32(txPoint.length, txPointIndex, Endian.little);
|
buffer.setUint32(txPoint.length, txPointIndex, Endian.little);
|
||||||
|
|
||||||
|
@ -980,9 +982,9 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
|
||||||
);
|
);
|
||||||
|
|
||||||
return unBlindedPaymentCode;
|
return unBlindedPaymentCode;
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
"unBlindedPaymentCodeFromTransaction() failed: $e\nFor tx: $transaction",
|
"unBlindedPaymentCodeFromTransaction() failed: $e\n$s\nFor tx: $transaction",
|
||||||
level: LogLevel.Warning,
|
level: LogLevel.Warning,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
|
@ -1003,11 +1005,12 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
|
||||||
|
|
||||||
final designatedInput = transaction.inputs.first;
|
final designatedInput = transaction.inputs.first;
|
||||||
|
|
||||||
final txPoint = designatedInput.outpoint!.txid.fromHex.toList();
|
final txPoint =
|
||||||
|
designatedInput.outpoint!.txid.toUint8ListFromHex.toList();
|
||||||
final txPointIndex = designatedInput.outpoint!.vout;
|
final txPointIndex = designatedInput.outpoint!.vout;
|
||||||
|
|
||||||
final rev = Uint8List(txPoint.length + 4);
|
final rev = Uint8List(txPoint.length + 4);
|
||||||
Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length);
|
_copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length);
|
||||||
final buffer = rev.buffer.asByteData();
|
final buffer = rev.buffer.asByteData();
|
||||||
buffer.setUint32(txPoint.length, txPointIndex, Endian.little);
|
buffer.setUint32(txPoint.length, txPointIndex, Endian.little);
|
||||||
|
|
||||||
|
@ -1185,7 +1188,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
|
||||||
final List<Future<void>> futures = [];
|
final List<Future<void>> futures = [];
|
||||||
for (final code in codes) {
|
for (final code in codes) {
|
||||||
futures.add(
|
futures.add(
|
||||||
restoreHistoryWith(
|
_restoreHistoryWith(
|
||||||
other: code,
|
other: code,
|
||||||
maxUnusedAddressGap: maxUnusedAddressGap,
|
maxUnusedAddressGap: maxUnusedAddressGap,
|
||||||
maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
|
maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
|
||||||
|
@ -1197,7 +1200,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
|
||||||
await Future.wait(futures);
|
await Future.wait(futures);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restoreHistoryWith({
|
Future<void> _restoreHistoryWith({
|
||||||
required PaymentCode other,
|
required PaymentCode other,
|
||||||
required bool checkSegwitAsWell,
|
required bool checkSegwitAsWell,
|
||||||
required int maxUnusedAddressGap,
|
required int maxUnusedAddressGap,
|
||||||
|
@ -1441,6 +1444,19 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _copyBytes(
|
||||||
|
Uint8List source,
|
||||||
|
int sourceStartingIndex,
|
||||||
|
Uint8List destination,
|
||||||
|
int destinationStartingIndex,
|
||||||
|
int numberOfBytes,
|
||||||
|
) {
|
||||||
|
for (int i = 0; i < numberOfBytes; i++) {
|
||||||
|
destination[i + destinationStartingIndex] =
|
||||||
|
source[i + sourceStartingIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generate a new payment code string storage key
|
/// generate a new payment code string storage key
|
||||||
String _generateKey() {
|
String _generateKey() {
|
||||||
final bytes = _randomBytes(24);
|
final bytes = _randomBytes(24);
|
||||||
|
@ -1454,4 +1470,270 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
|
||||||
return Uint8List.fromList(
|
return Uint8List.fromList(
|
||||||
List<int>.generate(n, (_) => rng.nextInt(0xFF + 1)));
|
List<int>.generate(n, (_) => rng.nextInt(0xFF + 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================== Overrides ==============================================
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateTransactions({List<Address>? overrideAddresses}) async {
|
||||||
|
// Get all addresses.
|
||||||
|
List<Address> allAddressesOld =
|
||||||
|
overrideAddresses ?? await fetchAddressesForElectrumXScan();
|
||||||
|
|
||||||
|
// Separate receiving and change addresses.
|
||||||
|
Set<String> receivingAddresses = allAddressesOld
|
||||||
|
.where((e) =>
|
||||||
|
e.subType == AddressSubType.receiving ||
|
||||||
|
e.subType == AddressSubType.paynymNotification ||
|
||||||
|
e.subType == AddressSubType.paynymReceive)
|
||||||
|
.map((e) => e.value)
|
||||||
|
.toSet();
|
||||||
|
Set<String> changeAddresses = allAddressesOld
|
||||||
|
.where((e) => e.subType == AddressSubType.change)
|
||||||
|
.map((e) => e.value)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
// Remove duplicates.
|
||||||
|
final allAddressesSet = {...receivingAddresses, ...changeAddresses};
|
||||||
|
|
||||||
|
// Fetch history from ElectrumX.
|
||||||
|
final List<Map<String, dynamic>> allTxHashes =
|
||||||
|
await fetchHistory(allAddressesSet);
|
||||||
|
|
||||||
|
// Only parse new txs (not in db yet).
|
||||||
|
List<Map<String, dynamic>> allTransactions = [];
|
||||||
|
for (final txHash in allTxHashes) {
|
||||||
|
// Check for duplicates by searching for tx by tx_hash in db.
|
||||||
|
// final storedTx = await mainDB.isar.transactionV2s
|
||||||
|
// .where()
|
||||||
|
// .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId)
|
||||||
|
// .findFirst();
|
||||||
|
//
|
||||||
|
// if (storedTx == null ||
|
||||||
|
// storedTx.height == null ||
|
||||||
|
// (storedTx.height != null && storedTx.height! <= 0)) {
|
||||||
|
// Tx not in db yet.
|
||||||
|
final tx = await electrumXCachedClient.getTransaction(
|
||||||
|
txHash: txHash["tx_hash"] as String,
|
||||||
|
verbose: true,
|
||||||
|
coin: cryptoCurrency.coin,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only tx to list once.
|
||||||
|
if (allTransactions
|
||||||
|
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
|
||||||
|
-1) {
|
||||||
|
tx["height"] = txHash["height"];
|
||||||
|
allTransactions.add(tx);
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse all new txs.
|
||||||
|
final List<TransactionV2> txns = [];
|
||||||
|
for (final txData in allTransactions) {
|
||||||
|
bool wasSentFromThisWallet = false;
|
||||||
|
// Set to true if any inputs were detected as owned by this wallet.
|
||||||
|
|
||||||
|
bool wasReceivedInThisWallet = false;
|
||||||
|
// Set to true if any outputs were detected as owned by this wallet.
|
||||||
|
|
||||||
|
// Parse inputs.
|
||||||
|
BigInt amountReceivedInThisWallet = BigInt.zero;
|
||||||
|
BigInt changeAmountReceivedInThisWallet = BigInt.zero;
|
||||||
|
final List<InputV2> inputs = [];
|
||||||
|
for (final jsonInput in txData["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) {
|
||||||
|
// Not a coinbase (ie a typical input).
|
||||||
|
final txid = map["txid"] as String;
|
||||||
|
final vout = map["vout"] as int;
|
||||||
|
|
||||||
|
final inputTx = await electrumXCachedClient.getTransaction(
|
||||||
|
txHash: txid,
|
||||||
|
coin: cryptoCurrency.coin,
|
||||||
|
);
|
||||||
|
|
||||||
|
final prevOutJson = Map<String, dynamic>.from(
|
||||||
|
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
|
||||||
|
as Map);
|
||||||
|
|
||||||
|
final prevOut = OutputV2.fromElectrumXJson(
|
||||||
|
prevOutJson,
|
||||||
|
decimalPlaces: cryptoCurrency.fractionDigits,
|
||||||
|
isFullAmountNotSats: true,
|
||||||
|
walletOwns: false, // Doesn't matter here as this is not saved.
|
||||||
|
);
|
||||||
|
|
||||||
|
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
|
||||||
|
txid: txid,
|
||||||
|
vout: vout,
|
||||||
|
);
|
||||||
|
valueStringSats = prevOut.valueStringSats;
|
||||||
|
addresses.addAll(prevOut.addresses);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputV2 input = InputV2.fromElectrumxJson(
|
||||||
|
json: map,
|
||||||
|
outpoint: outpoint,
|
||||||
|
valueStringSats: valueStringSats,
|
||||||
|
addresses: addresses,
|
||||||
|
coinbase: coinbase,
|
||||||
|
// Need addresses before we can know if the wallet owns this input.
|
||||||
|
walletOwns: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if input was from this wallet.
|
||||||
|
if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) {
|
||||||
|
wasSentFromThisWallet = true;
|
||||||
|
input = input.copyWith(walletOwns: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs.add(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse outputs.
|
||||||
|
final List<OutputV2> outputs = [];
|
||||||
|
for (final outputJson in txData["vout"] as List) {
|
||||||
|
OutputV2 output = OutputV2.fromElectrumXJson(
|
||||||
|
Map<String, dynamic>.from(outputJson as Map),
|
||||||
|
decimalPlaces: cryptoCurrency.fractionDigits,
|
||||||
|
isFullAmountNotSats: true,
|
||||||
|
// Need addresses before we can know if the wallet owns this input.
|
||||||
|
walletOwns: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If output was to my wallet, add value to amount received.
|
||||||
|
if (receivingAddresses
|
||||||
|
.intersection(output.addresses.toSet())
|
||||||
|
.isNotEmpty) {
|
||||||
|
wasReceivedInThisWallet = true;
|
||||||
|
amountReceivedInThisWallet += output.value;
|
||||||
|
output = output.copyWith(walletOwns: true);
|
||||||
|
} else if (changeAddresses
|
||||||
|
.intersection(output.addresses.toSet())
|
||||||
|
.isNotEmpty) {
|
||||||
|
wasReceivedInThisWallet = true;
|
||||||
|
changeAmountReceivedInThisWallet += output.value;
|
||||||
|
output = output.copyWith(walletOwns: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs.add(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
final totalOut = outputs
|
||||||
|
.map((e) => e.value)
|
||||||
|
.fold(BigInt.zero, (value, element) => value + element);
|
||||||
|
|
||||||
|
TransactionType type;
|
||||||
|
TransactionSubType subType = TransactionSubType.none;
|
||||||
|
if (outputs.length > 1 && inputs.isNotEmpty) {
|
||||||
|
for (int i = 0; i < outputs.length; i++) {
|
||||||
|
List<String>? scriptChunks = outputs[i].scriptPubKeyAsm?.split(" ");
|
||||||
|
if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
|
||||||
|
final blindedPaymentCode = scriptChunks![1];
|
||||||
|
final bytes = blindedPaymentCode.toUint8ListFromHex;
|
||||||
|
|
||||||
|
// https://en.bitcoin.it/wiki/BIP_0047#Sending
|
||||||
|
if (bytes.length == 80 && bytes.first == 1) {
|
||||||
|
subType = TransactionSubType.bip47Notification;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least one input was owned by this wallet.
|
||||||
|
if (wasSentFromThisWallet) {
|
||||||
|
type = TransactionType.outgoing;
|
||||||
|
|
||||||
|
if (wasReceivedInThisWallet) {
|
||||||
|
if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet ==
|
||||||
|
totalOut) {
|
||||||
|
// Definitely sent all to self.
|
||||||
|
type = TransactionType.sentToSelf;
|
||||||
|
} else if (amountReceivedInThisWallet == BigInt.zero) {
|
||||||
|
// Most likely just a typical send, do nothing here yet.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (wasReceivedInThisWallet) {
|
||||||
|
// Only found outputs owned by this wallet.
|
||||||
|
type = TransactionType.incoming;
|
||||||
|
|
||||||
|
// TODO: [prio=none] Check for special Bitcoin outputs like ordinals.
|
||||||
|
} else {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Unexpected tx found (ignoring it): $txData",
|
||||||
|
level: LogLevel.Error,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final tx = TransactionV2(
|
||||||
|
walletId: walletId,
|
||||||
|
blockHash: txData["blockhash"] as String?,
|
||||||
|
hash: txData["hash"] as String,
|
||||||
|
txid: txData["txid"] as String,
|
||||||
|
height: txData["height"] as int?,
|
||||||
|
version: txData["version"] as int,
|
||||||
|
timestamp: txData["blocktime"] as int? ??
|
||||||
|
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
inputs: List.unmodifiable(inputs),
|
||||||
|
outputs: List.unmodifiable(outputs),
|
||||||
|
type: type,
|
||||||
|
subType: subType,
|
||||||
|
otherData: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
txns.add(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
await mainDB.updateOrPutTransactionV2s(txns);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<
|
||||||
|
({
|
||||||
|
String? blockedReason,
|
||||||
|
bool blocked,
|
||||||
|
String? utxoLabel,
|
||||||
|
})> checkBlockUTXO(
|
||||||
|
Map<String, dynamic> jsonUTXO,
|
||||||
|
String? scriptPubKeyHex,
|
||||||
|
Map<String, dynamic>? jsonTX,
|
||||||
|
String? utxoOwnerAddress,
|
||||||
|
) async {
|
||||||
|
bool blocked = false;
|
||||||
|
String? blockedReason;
|
||||||
|
|
||||||
|
if (jsonTX != null) {
|
||||||
|
// check for bip47 notification
|
||||||
|
final outputs = jsonTX["vout"] as List;
|
||||||
|
for (final output in outputs) {
|
||||||
|
List<String>? scriptChunks =
|
||||||
|
(output['scriptPubKey']?['asm'] as String?)?.split(" ");
|
||||||
|
if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
|
||||||
|
final blindedPaymentCode = scriptChunks![1];
|
||||||
|
final bytes = blindedPaymentCode.toUint8ListFromHex;
|
||||||
|
|
||||||
|
// https://en.bitcoin.it/wiki/BIP_0047#Sending
|
||||||
|
if (bytes.length == 80 && bytes.first == 1) {
|
||||||
|
blocked = true;
|
||||||
|
blockedReason = "Paynym notification output. Incautious "
|
||||||
|
"handling of outputs from notification transactions "
|
||||||
|
"may cause unintended loss of privacy.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue