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

View file

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