diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 78c2c7f94..e8a39420c 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -36,6 +36,14 @@ class ElectrumBalance extends Balance { @override Money frozen; + ElectrumBalance copy() => ElectrumBalance( + confirmed: confirmed, + unconfirmed: unconfirmed, + frozen: frozen, + secondConfirmed: secondConfirmed, + secondUnconfirmed: secondUnconfirmed, + ); + static ElectrumBalance? fromJSON(String? jsonSource, Currency currency) { if (jsonSource == null) return null; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index ae36b55a0..e0f3eaebd 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -428,7 +428,7 @@ abstract class ElectrumWalletBase return; } - balance[currency] = newBalance; + balance[currency] = newBalance.copy(); } Map> get addressesSetByAccount { @@ -1845,8 +1845,8 @@ abstract class ElectrumWalletBase isViewOnly: false, )..addListener((transaction) async { transactionHistory.addOne(transaction); - await updateBalance(); await updateAllUnspents(); + await updateBalance(); }); } @@ -1950,8 +1950,8 @@ abstract class ElectrumWalletBase unspentCoins .removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); - await updateBalance(); await updateAllUnspents(); + await updateBalance(); }); } catch (e) { throw e; @@ -2736,8 +2736,8 @@ abstract class ElectrumWalletBase } }); transactionHistory.addOne(transaction); - await updateBalance(); await updateAllUnspents(); + await updateBalance(); }); } catch (e) { throw e; @@ -4037,6 +4037,10 @@ abstract class ElectrumWalletBase var totalConfirmed = 0; var totalUnconfirmed = 0; + final confirmedByAccount = {}; + final unconfirmedByAccount = {}; + final frozenByAccount = {}; + if (hasSilentPaymentsScanning) { // Add values from unspent coins that are not fetched by the address list // i.e. scanned silent payments @@ -4044,8 +4048,13 @@ abstract class ElectrumWalletBase if (tx.unspents != null) { tx.unspents!.forEach((unspent) { if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { - if (unspent.isFrozen) totalFrozen += unspent.value; + final account = unspent.bitcoinAddressRecord.accountIndex; + if (unspent.isFrozen) { + totalFrozen += unspent.value; + frozenByAccount[account] = (frozenByAccount[account] ?? 0) + unspent.value; + } totalConfirmed += unspent.value; + confirmedByAccount[account] = (confirmedByAccount[account] ?? 0) + unspent.value; } }); } @@ -4062,6 +4071,8 @@ abstract class ElectrumWalletBase element.value == info.value) { if (info.isFrozen) { totalFrozen += element.value; + final account = element.bitcoinAddressRecord.accountIndex; + frozenByAccount[account] = (frozenByAccount[account] ?? 0) + element.value; } } }); @@ -4089,6 +4100,10 @@ abstract class ElectrumWalletBase totalConfirmed += confirmed; totalUnconfirmed += unconfirmed; + final account = addressRecord.accountIndex; + confirmedByAccount[account] = (confirmedByAccount[account] ?? 0) + confirmed; + unconfirmedByAccount[account] = (unconfirmedByAccount[account] ?? 0) + unconfirmed; + addressRecord.balance = confirmed + unconfirmed; if (confirmed > 0 || unconfirmed > 0) { addressRecord.setAsUsed(); @@ -4096,6 +4111,26 @@ abstract class ElectrumWalletBase } } + if (hasAccountsSupport) { + final perAccount = {}; + final accounts = { + ...confirmedByAccount.keys, + ...unconfirmedByAccount.keys, + ...frozenByAccount.keys, + ...walletAddresses.accountIndexes, + }; + + for (final account in accounts) { + perAccount[account] = ElectrumBalance( + confirmed: Money.fromInt(confirmedByAccount[account] ?? 0, currency), + unconfirmed: Money.fromInt(unconfirmedByAccount[account] ?? 0, currency), + frozen: Money.fromInt(frozenByAccount[account] ?? 0, currency), + ); + } + + accountBalances = ObservableMap.of(perAccount); + } + return ElectrumBalance( confirmed: Money.fromInt(totalConfirmed, currency), unconfirmed: Money.fromInt(totalUnconfirmed, currency), @@ -4138,13 +4173,19 @@ abstract class ElectrumWalletBase try { final fetchedTotal = await fetchBalances(); + if (!electrumClient.isConnected || syncStatus is LostConnectionSyncStatus) { + printV("updateBalance: connection lost during fetch, keeping existing balance"); + return; + } + if (type == WalletType.bitcoin && hasAccountsSupport) { // If the wallet has accounts support, we only want to update the balance for the current account. final accountBalance = accountBalances[currentAccountIndex]; - if (accountBalance == null) { - return; + if (accountBalance != null) { + balance[currency] = accountBalance.copy(); + } else { + printV("updateBalance: no balance for account $currentAccountIndex, keeping existing"); } - balance[currency] = accountBalance; } else { balance[currency] = fetchedTotal; } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 305e1c28a..b5559ab10 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,3 +1,4 @@ +import 'dart:async' show Zone; import 'dart:io' show Platform; import 'dart:math'; import "package:collection/collection.dart"; @@ -425,7 +426,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); updateReceiveAddresses(); updateChangeAddresses(); - _validateAddresses(); + await _validateAddresses(); await updateAddressesInBox(); if (currentReceiveAddressIndex >= receiveAddresses.length) { @@ -506,7 +507,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (shouldSkipHardwareWalletType) continue; await _generateInitialAddresses(accountIndex: accountIndex, type: type); - + // Legacy derivation for these types is identical to the standard one. if (includeLegacy && !LEGACY_DUPLICATE_ADDRESS_TYPES.contains(type)) { await _generateInitialAddresses( @@ -1001,13 +1002,32 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); } - void _validateAddresses() { - _addresses.forEach((element) async { - if (element.type == SegwitAddresType.mweb) { - // this would add a ton of startup lag for mweb addresses since we have 1000 of them - return; + static const _validationTimeSlice = Duration(milliseconds: 16); + + Future _validateAddresses() async { + final addresses = _addresses.toList(); + final slice = Stopwatch()..start(); + + for (final element in addresses) { + try { + await _validateAddress(element); + } catch (e, s) { + Zone.current.handleUncaughtError(e, s); } + if (slice.elapsed >= _validationTimeSlice) { + await Future.delayed(Duration.zero); + slice.reset(); + } + } + } + + Future _validateAddress(BitcoinAddressRecord element) async { + if (element.type == SegwitAddresType.mweb) { + // this would add a ton of startup lag for mweb addresses since we have 1000 of them + return; + } + try { final mainHd = _hdForAddressGeneration( isHidden: false,