fix: scan when switching, fix multiple unspents in same tx

This commit is contained in:
Rafael Saes 2024-02-27 19:51:53 -03:00
parent 2967809a0e
commit a44bd6b8f9
4 changed files with 105 additions and 79 deletions

View file

@ -19,7 +19,7 @@ class ElectrumTransactionBundle {
}
class ElectrumTransactionInfo extends TransactionInfo {
BitcoinUnspent? unspent;
List<BitcoinUnspent>? unspents;
ElectrumTransactionInfo(this.type,
{required String id,
@ -31,7 +31,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
required DateTime date,
required int confirmations,
String? to,
this.unspent}) {
this.unspents}) {
this.id = id;
this.height = height;
this.amount = amount;
@ -160,10 +160,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
isPending: data['isPending'] as bool,
confirmations: data['confirmations'] as int,
to: data['to'] as String?,
unspent: data['unspent'] != null
? BitcoinUnspent.fromJSON(
BitcoinAddressRecord.fromJSON(data['unspent']['address_record'] as String),
data['unspent'] as Map<String, dynamic>)
unspents: data['unspent'] != null
? (data['unspent'] as List<dynamic>)
.map((unspent) => BitcoinUnspent.fromJSON(
BitcoinAddressRecord.fromJSON(unspent['address_record'] as String),
data['unspent'] as Map<String, dynamic>))
.toList()
: null,
);
}
@ -210,11 +212,11 @@ class ElectrumTransactionInfo extends TransactionInfo {
m['confirmations'] = confirmations;
m['fee'] = fee;
m['to'] = to;
m['unspent'] = unspent?.toJson() ?? <String, dynamic>{};
m['unspent'] = unspents?.map((e) => e.toJson()) ?? [];
return m;
}
String toString() {
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspent)';
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)';
}
}

View file

@ -142,6 +142,9 @@ abstract class ElectrumWalletBase
@observable
bool nodeSupportsSilentPayments = true;
@observable
int? currentChainTip;
@override
BitcoinWalletKeys get keys =>
BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!);
@ -197,30 +200,37 @@ abstract class ElectrumWalletBase
syncStatus = UnsupportedSyncStatus();
}
if (message is BitcoinUnspent) {
if (!unspentCoins.any((utx) =>
utx.hash.contains(message.hash) &&
utx.vout == message.vout &&
utx.address.contains(message.address))) {
unspentCoins.add(message);
if (unspentCoinsInfo.values.any((element) =>
element.walletId.contains(id) &&
element.hash.contains(message.hash) &&
element.address.contains(message.address))) {
_addCoinInfo(message);
await walletInfo.save();
await save();
}
balance[currency] = await _fetchBalances();
}
}
if (message is Map<String, ElectrumTransactionInfo>) {
transactionHistory.addMany(message);
await transactionHistory.save();
for (final map in message.entries) {
final txid = map.key;
final tx = map.value;
if (tx.unspents != null) {
tx.unspents!.forEach((unspent) => walletAddresses.addSilentAddresses(
[unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord]));
final existingTxInfo = transactionHistory.transactions[txid];
if (existingTxInfo != null) {
final newUnspents = tx.unspents!
.where((unspent) => !existingTxInfo.unspents!.any((element) =>
element.hash.contains(unspent.hash) && element.vout == unspent.vout))
.toList();
if (newUnspents.isNotEmpty) {
existingTxInfo.unspents ??= [];
existingTxInfo.unspents!.addAll(newUnspents);
existingTxInfo.amount += newUnspents.length > 1
? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent)
: newUnspents[0].value;
}
} else {
transactionHistory.addMany(message);
transactionHistory.save();
}
}
}
updateUnspent();
}
// check if is a SyncStatus type since "is SyncStatus" doesn't work here
@ -236,17 +246,16 @@ abstract class ElectrumWalletBase
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
if (hasSilentPaymentsScanning) {
try {
await _setInitialHeight();
} catch (_) {}
final currentChainTip = await electrumClient.getCurrentBlockChainTip();
if ((currentChainTip ?? 0) > walletInfo.restoreHeight) {
_setListeners(walletInfo.restoreHeight, chainTip: currentChainTip);
}
} else {
syncStatus = AttemptingSyncStatus();
}
await updateTransactions();
@ -258,9 +267,9 @@ abstract class ElectrumWalletBase
Timer.periodic(
const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates());
// if (!hasSilentPaymentsScanning) {
syncStatus = SyncedSyncStatus();
// }
if (!hasSilentPaymentsScanning || walletInfo.restoreHeight == currentChainTip) {
syncStatus = SyncedSyncStatus();
}
} catch (e, stacktrace) {
print(stacktrace);
print(e.toString());
@ -312,10 +321,22 @@ abstract class ElectrumWalletBase
leftAmount = leftAmount - utx.value;
final address = _addressTypeFromStr(utx.address, network);
final privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index,
network: network);
ECPrivate? privkey;
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
privkey = walletAddresses.primarySilentAddress!.b_spend.clone().tweakAdd(
BigintUtils.fromBytes(BytesUtils.fromHexString(
(utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord)
.silentPaymentTweak)),
);
} else {
privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index,
network: network);
}
privateKeys.add(privkey);
inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr));
@ -661,18 +682,19 @@ abstract class ElectrumWalletBase
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
Future<void> updateUnspent() async {
List<BitcoinUnspent> updatedUnspentCoins = [];
// Update unspents stored from scanned silent payment transactions
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspent != null) {
if (!unspentCoins
.any((utx) => utx.hash.contains(tx.unspent!.hash) && utx.vout == tx.unspent!.vout)) {
unspentCoins.add(tx.unspent!);
if (tx.unspents != null) {
if (!unspentCoins.any((utx) =>
tx.unspents!.any((element) => utx.hash.contains(element.hash)) &&
tx.unspents!.any((element) => utx.vout == element.vout))) {
updatedUnspentCoins.addAll(tx.unspents!);
}
}
});
List<BitcoinUnspent> updatedUnspentCoins = [];
final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient
@ -866,7 +888,7 @@ abstract class ElectrumWalletBase
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
final history = await electrumClient
.getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network)!);
.getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network));
if (history.isNotEmpty) {
addressRecord.setAsUsed();
@ -1048,8 +1070,8 @@ abstract class ElectrumWalletBase
}
if (walletInfo.restoreHeight == 0) {
final currentHeight = await electrumClient.getCurrentBlockChainTip();
if (currentHeight != null) walletInfo.restoreHeight = currentHeight;
currentChainTip = await electrumClient.getCurrentBlockChainTip();
if (currentChainTip != null) walletInfo.restoreHeight = currentChainTip!;
}
}
}
@ -1102,13 +1124,18 @@ class SyncResponse {
Future<void> startRefresh(ScanData scanData) async {
var cachedBlockchainHeight = scanData.chainTip;
Future<ElectrumClient> connect() async {
final electrumClient = scanData.electrumClient;
if (!electrumClient.isConnected) {
final node = scanData.node;
await electrumClient.connectToUri(Uri.parse(node));
}
return electrumClient;
}
Future<int> getNodeHeightOrUpdate(int baseHeight) async {
if (cachedBlockchainHeight < baseHeight || cachedBlockchainHeight == 0) {
final electrumClient = scanData.electrumClient;
if (!electrumClient.isConnected) {
final node = scanData.node;
await electrumClient.connectToUri(Uri.parse(node));
}
final electrumClient = await connect();
cachedBlockchainHeight =
await electrumClient.getCurrentBlockChainTip() ?? cachedBlockchainHeight;
@ -1151,16 +1178,8 @@ Future<void> startRefresh(ScanData scanData) async {
return;
}
print(["Scanning from height:", syncHeight]);
try {
// Get all the tweaks from the block
final electrumClient = scanData.electrumClient;
if (!electrumClient.isConnected) {
final node = scanData.node;
print(node);
await electrumClient.connectToUri(Uri.parse(node));
}
final electrumClient = await connect();
List<dynamic>? tweaks;
try {
@ -1177,7 +1196,6 @@ Future<void> startRefresh(ScanData scanData) async {
for (var i = 0; i < tweaks.length; i++) {
try {
// final txid = tweaks.keys.toList()[i];
final details = tweaks[i] as Map<String, dynamic>;
final output_pubkeys = (details["output_pubkeys"] as List<dynamic>);
final tweak = details["tweak"].toString();
@ -1193,9 +1211,7 @@ Future<void> startRefresh(ScanData scanData) async {
final result = spb.scanOutputs(
scanData.primarySilentAddress.b_scan,
scanData.primarySilentAddress.B_spend,
output_pubkeys
.map((p) => ECPublic.fromBytes(BytesUtils.fromHexString(p.toString()).sublist(2)))
.toList(),
output_pubkeys.map((output) => output.toString()).toList(),
precomputedLabels: scanData.labels,
);
@ -1206,8 +1222,7 @@ Future<void> startRefresh(ScanData scanData) async {
result.forEach((key, value) async {
final t_k = value[0];
final address =
ECPublic.fromHex(key).toTaprootAddress(tweak: false).toAddress(scanData.network);
final address = ECPublic.fromHex(key).toTaprootAddress().toAddress(scanData.network);
final listUnspent =
await electrumClient.getListUnspentWithAddress(address, scanData.network);
@ -1228,6 +1243,10 @@ Future<void> startRefresh(ScanData scanData) async {
} catch (_) {}
});
if (info == null) {
return;
}
// final tweak = value[0];
// String? label;
// if (value.length > 1) label = value[1];
@ -1237,20 +1256,16 @@ Future<void> startRefresh(ScanData scanData) async {
WalletType.bitcoin,
id: tx.hash,
height: syncHeight,
amount: tx.value,
amount: 0, // will be added later via unspent
fee: 0,
direction: TransactionDirection.incoming,
isPending: false,
date: DateTime.now(),
confirmations: currentChainTip - syncHeight,
confirmations: currentChainTip - syncHeight - 1,
to: scanData.primarySilentAddress.toString(),
unspent: tx,
unspents: [tx],
);
// final status = json.decode((await http
// .get(Uri.parse("https://blockstream.info/testnet/api/tx/$txid/outspends")))
// .body) as List<dynamic>;
// bool spent = false;
// for (final s in status) {
// if ((s["spent"] as bool) == true) {

View file

@ -35,7 +35,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
primarySilentAddress = silentAddress,
addressesByReceiveType =
ObservableList<BitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
.toSet()),
@ -408,6 +408,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
updateAddressesByMatch();
}
@action
void addSilentAddresses(Iterable<BitcoinSilentPaymentAddressRecord> addresses) {
final addressesSet = this.silentAddresses.toSet();
addressesSet.addAll(addresses);
this.silentAddresses.clear();
this.silentAddresses.addAll(addressesSet);
updateAddressesByMatch();
}
void _validateSideHdAddresses(List<BitcoinAddressRecord> addrWithTransactions) {
addrWithTransactions.forEach((element) {
if (element.address !=

View file

@ -80,7 +80,7 @@ packages:
description:
path: "."
ref: cake-update-v2
resolved-ref: be980da9ed063da13db3907ca76d534298bb1d40
resolved-ref: "7634511b15e5a48bd18e3c19f81971628090c04f"
url: "https://github.com/cake-tech/bitcoin_base.git"
source: git
version: "4.0.0"