From 2502a14b50df1bbd67d13da4ed82c3053416fa25 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 22 Apr 2025 18:47:13 -0300 Subject: [PATCH] fix: discovery & startSync [skip ci] --- cw_bitcoin/lib/bitcoin_wallet.dart | 128 ++++---- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 8 +- cw_bitcoin/lib/electrum_wallet.dart | 287 ++++++++++++------ cw_bitcoin/lib/electrum_wallet_addresses.dart | 108 ++++--- .../lib/electrum_worker/methods/methods.dart | 1 + .../methods/scripthashes_subscribe.dart | 18 +- .../dashboard/pages/transactions_page.dart | 7 - 7 files changed, 337 insertions(+), 220 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 896405bd6..0917a4b21 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -114,68 +114,92 @@ abstract class BitcoinWalletBase extends ElectrumWallet } @override - void initAddresses() async { - // if (didInitAddresses) return; + Future initAddresses([bool? sync]) async { + var isDiscovered = await super.initAddresses(sync); + bool? discovered; - // If already loaded, no need to generate/discover all initial addresses - // so skip - // if (!walletAddresses.loadedFromNewSnapshot) { - for (final walletAddressType in walletAddresses.walletAddressTypes) { - if (isHardwareWallet && walletAddressType != SegwitAddressType.p2wpkh) continue; + // NOTE: will initiate by priority from the first walletAddressTypes + // then proceeds to following ones after got fully discovered response from worker response + if (isDiscovered != false) { + for (final addressType in walletAddresses.walletAddressTypes) { + if (isHardwareWallet && addressType != SegwitAddressType.p2wpkh) continue; - for (final seedBytesType in hdWallets.keys) { - generateInitialAddresses( - addressType: walletAddressType, - seedBytesType: seedBytesType, - ); - } - } + for (final seedBytesType in hdWallets.keys) { + // p2wpkh has always had the right derivations, skip if creating old derivations + if (seedBytesType.isOldDerivation && addressType == SegwitAddressType.p2wpkh) { + continue; + } - // } - } + final bitcoinDerivationInfo = BitcoinAddressUtils.getDerivationFromType( + addressType, + network: network, + isElectrum: seedBytesType.isElectrum, + ); - @override - @action - void generateInitialAddresses({ - required BitcoinAddressType addressType, - required SeedBytesType seedBytesType, - BitcoinDerivationInfo? bitcoinDerivationInfo, - }) { - // p2wpkh has always had the right derivations, skip if creating old derivations - if (seedBytesType.isOldDerivation && addressType == SegwitAddressType.p2wpkh) { - return; - } + bool alreadyDidDerivation = false; - final bitcoinDerivationInfo = BitcoinAddressUtils.getDerivationFromType( - addressType, - network: network, - isElectrum: seedBytesType.isElectrum, - ); + for (final derivationInfo in [ + bitcoinDerivationInfo, + BitcoinDerivationInfos.BIP84, + BitcoinDerivationInfos.ELECTRUM, + ]) { + final derivationPath = derivationInfo.derivationPath.toString(); - if (seedBytesType.isOldDerivation) { - for (final derivationInfo in [ - bitcoinDerivationInfo, - BitcoinDerivationInfos.ELECTRUM, - BitcoinDerivationInfos.BIP84, - ]) { - if (derivationInfo.derivationPath.toString() == - bitcoinDerivationInfo.derivationPath.toString()) { - continue; + if (alreadyDidDerivation && + derivationPath == bitcoinDerivationInfo.derivationPath.toString()) { + continue; + } + + alreadyDidDerivation = true; + + for (final isChange in [true, false]) { + isDiscovered = walletAddresses.discoveredAddressesRecord.getIsDiscovered( + addressType: addressType, + seedBytesType: seedBytesType, + derivationPath: derivationPath, + isChange: isChange, + ); + + if (isDiscovered == false) { + break; + } else if (sync == true) + subscribeForStatuses( + walletAddresses.addressesRecords + .getRecords( + addressType: addressType, + seedBytesType: seedBytesType, + derivationPath: derivationPath, + isChange: isChange, + ) + .whereType() + .toList(), + ); + } + + if (isDiscovered == false) { + discovered = await generateInitialAddresses( + addressType: addressType, + seedBytesType: seedBytesType, + bitcoinDerivationInfo: derivationInfo, + ); + break; + } + } + + if (isDiscovered == false) break; } - super.generateInitialAddresses( - addressType: addressType, - seedBytesType: seedBytesType, - bitcoinDerivationInfo: derivationInfo, - ); + if (isDiscovered == false) break; } - } else { - super.generateInitialAddresses( - addressType: addressType, - seedBytesType: seedBytesType, - bitcoinDerivationInfo: bitcoinDerivationInfo, - ); } + + if (isDiscovered == true && sync == false) + initAddresses(true); + else if (isDiscovered == true) + syncStatus = SyncedSyncStatus(); + else if (isDiscovered == false && discovered == false) initAddresses(sync); + + return isDiscovered; } static Future create({ diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index f6d77a467..46bbd640f 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -44,10 +44,10 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override final walletAddressTypes = [ SegwitAddressType.p2wpkh, - // SegwitAddressType.p2tr, - // P2shAddressType.p2wpkhInP2sh, - // P2pkhAddressType.p2pkh, - // SegwitAddressType.p2wsh, + SegwitAddressType.p2tr, + P2shAddressType.p2wpkhInP2sh, + P2pkhAddressType.p2pkh, + SegwitAddressType.p2wsh, ]; @observable diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 6b7e33b48..b1fd99839 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -181,7 +181,7 @@ abstract class ElectrumWalletBase break; case ElectrumRequestMethods.scripthashesSubscribeMethod: final response = ElectrumWorkerScripthashesSubscribeResponse.fromJson(messageJson); - await onScripthashesStatusResponse(response.result.last); + await onScripthashesStatusResponse(response.result); break; case ElectrumRequestMethods.getBalanceMethod: final response = ElectrumWorkerGetBalanceResponse.fromJson(messageJson); @@ -309,8 +309,44 @@ abstract class ElectrumWalletBase Timer? _updateFeeRateTimer; static const int _autoSaveInterval = 1; - void initAddresses() { - throw UnimplementedError(); + Future initAddresses([bool? sync]) async { + bool? isDiscovered = null; + + // NOTE: will initiate by priority from the first walletAddressTypes + // then proceeds to following ones after got fully discovered response from worker response + for (final addressType in walletAddresses.walletAddressTypes) { + if (isHardwareWallet && addressType != SegwitAddressType.p2wpkh) continue; + + final bitcoinDerivationInfo = BitcoinAddressUtils.getDerivationFromType( + addressType, + network: network, + isElectrum: walletAddresses.walletSeedBytesType.isElectrum, + ); + + for (final isChange in [true, false]) { + isDiscovered = walletAddresses.discoveredAddressesRecord.getIsDiscovered( + addressType: addressType, + seedBytesType: walletAddresses.walletSeedBytesType, + derivationPath: bitcoinDerivationInfo.derivationPath.toString(), + isChange: isChange, + ); + + if (isDiscovered == false) { + break; + } + } + + if (isDiscovered == false) { + await generateInitialAddresses( + addressType: addressType, + seedBytesType: walletAddresses.walletSeedBytesType, + bitcoinDerivationInfo: bitcoinDerivationInfo, + ); + break; + } + } + + return isDiscovered; } Future init() async { @@ -324,49 +360,16 @@ abstract class ElectrumWalletBase @override Future startSync() async { try { - // if (syncStatus is SynchronizingSyncStatus) { - // return; - // } + syncStatus = SynchronizingSyncStatus(); - // syncStatus = SynchronizingSyncStatus(); + await subscribeForHeaders(true); - // // INFO: FIRST (always): Call subscribe for headers, wait for completion to update currentChainTip (needed for other methods) - // await subscribeForHeaders(true); + await initAddresses(false); - // final responses = await subscribeForStatuses(addresses, true); + await updateFeeRates(); - // final addressesWithHistory = walletAddresses.allAddresses - // .where((e) => scripthashesWithStatus.contains(e.scriptHash)) - // .toList(); - - // // INFO: SECOND: Start loading transaction histories for every address, this will help discover addresses until the unused gap limit has been reached, which will help finding the full balance and unspents next - // await updateTransactions(addressesWithHistory); - - // // INFO: THIRD: Get the full wallet's balance with all addresses considered - // await updateBalance(scripthashesWithStatus, true); - - // await updateAllUnspents(scripthashesWithStatus, true); - - // syncStatus = SyncedSyncStatus(); - - // // INFO: FOURTH: Get the latest recommended fee rates and start update timer - // await updateFeeRates(); - // _updateFeeRateTimer ??= - // Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); - - // await save(); - } catch (e, stacktrace) { - printV(stacktrace); - printV("startSync $e"); - syncStatus = FailedSyncStatus(); - } - } - - Future syncAddresses(List addresses) async { - try { - updateTransactions(addresses); - updateBalance(scripthashesWithStatus); - updateAllUnspents(scripthashesWithStatus); + _updateFeeRateTimer ??= + Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); await save(); } catch (e, stacktrace) { @@ -1239,12 +1242,26 @@ abstract class ElectrumWalletBase } @action - Future onScripthashesStatusResponse(ElectrumWorkerScripthashesResponse result) async { - if (result.status == null) { + Future onScripthashesStatusResponse( + List result, + ) async { + final noItemsWithStatus = result.length == 1 && result.first.status == null; + if (noItemsWithStatus) { return; } - scripthashesWithStatus.add(result.scripthash); + final addresses = walletAddresses.allAddresses + .where( + (address) => result.any((e) => e.status != null && e.scripthash == address.scriptHash), + ) + .toList(); + + final scripthashesWithStatus = + result.where((e) => e.status != null).map((e) => e.scripthash).toList(); + + updateTransactions(addresses); + updateBalance(scripthashesWithStatus); + updateAllUnspents(scripthashesWithStatus); } @action @@ -1292,7 +1309,7 @@ abstract class ElectrumWalletBase final addressesWithHistory = []; if (histories.isNotEmpty) { - await Future.wait(histories.map((addressHistory) async { + for (final addressHistory in histories) { final txs = addressHistory.txs; if (txs.isNotEmpty) { @@ -1302,20 +1319,11 @@ abstract class ElectrumWalletBase transactionHistory.addOne(tx); } } - })); + } if (addressesWithHistory.isNotEmpty) { - walletAddresses.updateAddresses(addressesWithHistory); + walletAddresses.addAddresses(addressesWithHistory); } - } else if (response.completed) { - final firstAddress = addressesWithHistory.first; - - discoverNewAddresses( - seedBytesType: firstAddress.seedBytesType!, - isChange: firstAddress.isChange, - addressType: firstAddress.type, - derivationInfo: firstAddress.derivationInfo, - ); } } @@ -1675,7 +1683,7 @@ abstract class ElectrumWalletBase } @action - Future> subscribeForStatuses([ + Future?> subscribeForStatuses([ List? addresses, bool? wait, ]) async { @@ -1686,14 +1694,24 @@ abstract class ElectrumWalletBase addressByScripthashes[addressRecord.scriptHash] = addressRecord.address; }); - return ElectrumWorkerScripthashesSubscribeResponse.fromJson( - await waitSendWorker( + if (wait == true) + return ElectrumWorkerScripthashesSubscribeResponse.fromJson( + await waitSendWorker( + ElectrumWorkerScripthashesSubscribeRequest( + scripthashByAddress: scripthashByAddress, + addressByScripthashes: addressByScripthashes, + ), + ), + ).result; + else + sendWorker( ElectrumWorkerScripthashesSubscribeRequest( scripthashByAddress: scripthashByAddress, addressByScripthashes: addressByScripthashes, ), - ), - ).result; + ); + + return null; } @action @@ -1823,9 +1841,6 @@ abstract class ElectrumWalletBase syncStatus = ConnectedSyncStatus(); } - // TODO: only once - initAddresses(); - break; case ConnectionStatus.disconnected: if (syncStatus is! NotConnectedSyncStatus && @@ -1933,29 +1948,69 @@ abstract class ElectrumWalletBase @action Future _onAddressesDiscovered(List addresses) async { - try { - final scripthashByAddress = await subscribeForStatuses(addresses, true); + final scripthashByAddress = await subscribeForStatuses(addresses, true); + final noItemsWithStatus = + scripthashByAddress!.length == 1 && scripthashByAddress.first.status == null; - print("addresses: ${addresses.first.address}"); + final firstAddress = addresses.first; - if (addresses.first.seedBytesType!.isOldDerivation && scripthashByAddress.length == 1) { - // Wrong derivation address with no history, discard and do not add to wallet addresses - // Was only used to find transactions if any - await save(); - return; - } + // NOTE: Did not find any status for old/wrong derivation addresses, discard them + // (don't add to addresses list) + if (firstAddress.seedBytesType!.isOldDerivation && noItemsWithStatus) { + walletAddresses.discoveredAddressesRecord.addDiscovered( + addressType: firstAddress.type, + seedBytesType: firstAddress.seedBytesType!, + derivationPath: firstAddress.derivationInfo.derivationPath.toString(), + isChange: firstAddress.isChange, + discovered: true, + ); + initAddresses(); + return; + } - walletAddresses.addAddresses(addresses); - await syncAddresses(addresses); - } catch (_) {} + walletAddresses.addAddresses(addresses); + walletAddresses.discoveredAddressesRecord.addDiscovered( + addressType: firstAddress.type, + seedBytesType: firstAddress.seedBytesType!, + derivationPath: firstAddress.derivationInfo.derivationPath.toString(), + isChange: firstAddress.isChange, + discovered: true, + ); + + // NOTE: Has items with status under gap limit, continue discovering + if (!noItemsWithStatus) + discoverNewAddresses( + seedBytesType: firstAddress.seedBytesType!, + isChange: firstAddress.isChange, + addressType: firstAddress.type, + derivationInfo: firstAddress.derivationInfo, + scripthashStatuses: scripthashByAddress, + ); + // NOTE: Otherwise, sync all the discovered addresses so far + else { + subscribeForStatuses( + walletAddresses.addressesRecords + .getRecords( + seedBytesType: firstAddress.seedBytesType!, + addressType: firstAddress.type, + derivationPath: firstAddress.derivationInfo.derivationPath.toString(), + isChange: firstAddress.isChange, + ) + .whereType() + .toList(), + ); + initAddresses(); + } } @action - void generateInitialAddresses({ + Future generateInitialAddresses({ required BitcoinAddressType addressType, required SeedBytesType seedBytesType, BitcoinDerivationInfo? bitcoinDerivationInfo, - }) { + }) async { + bool discovered = false; + bitcoinDerivationInfo ??= BitcoinAddressUtils.getDerivationFromType( addressType, network: network, @@ -1968,15 +2023,32 @@ abstract class ElectrumWalletBase derivationPath: bitcoinDerivationInfo.derivationPath.toString(), isChange: false, ); + final discoveredExistingReceiveAddresses = + walletAddresses.discoveredAddressesRecord.getIsDiscovered( + addressType: addressType, + seedBytesType: seedBytesType, + derivationPath: bitcoinDerivationInfo.derivationPath.toString(), + isChange: false, + ); - if (existingReceiveAddresses.length < ElectrumWalletAddressesBase.INITIAL_RECEIVE_COUNT) { + if (!discoveredExistingReceiveAddresses && + existingReceiveAddresses.length < ElectrumWalletAddressesBase.INITIAL_RECEIVE_COUNT) { discoverNewAddresses( - seedBytesType: seedBytesType, - isChange: false, addressType: addressType, + seedBytesType: seedBytesType, derivationInfo: bitcoinDerivationInfo, + isChange: false, startIndex: existingReceiveAddresses.length, ); + discovered = true; + } else { + walletAddresses.discoveredAddressesRecord.addDiscovered( + addressType: addressType, + seedBytesType: seedBytesType, + derivationPath: bitcoinDerivationInfo.derivationPath.toString(), + isChange: false, + discovered: true, + ); } final existingChangeAddresses = walletAddresses.addressesRecords.getRecords( @@ -1985,16 +2057,35 @@ abstract class ElectrumWalletBase derivationPath: bitcoinDerivationInfo.derivationPath.toString(), isChange: true, ); + final discoveredExistingChangeAddresses = + walletAddresses.discoveredAddressesRecord.getIsDiscovered( + addressType: addressType, + seedBytesType: seedBytesType, + derivationPath: bitcoinDerivationInfo.derivationPath.toString(), + isChange: true, + ); - if (existingChangeAddresses.length < ElectrumWalletAddressesBase.INITIAL_CHANGE_COUNT) { + if (!discoveredExistingChangeAddresses && + existingChangeAddresses.length < ElectrumWalletAddressesBase.INITIAL_CHANGE_COUNT) { discoverNewAddresses( - seedBytesType: seedBytesType, - isChange: true, addressType: addressType, + seedBytesType: seedBytesType, derivationInfo: bitcoinDerivationInfo, + isChange: true, startIndex: existingChangeAddresses.length, ); + discovered = true; + } else { + walletAddresses.discoveredAddressesRecord.addDiscovered( + addressType: addressType, + seedBytesType: seedBytesType, + derivationPath: bitcoinDerivationInfo.derivationPath.toString(), + isChange: true, + discovered: true, + ); } + + return discovered; } void discoverNewAddresses({ @@ -2003,8 +2094,9 @@ abstract class ElectrumWalletBase required BitcoinAddressType addressType, required BitcoinDerivationInfo derivationInfo, int? startIndex, + List? scripthashStatuses, }) async { - final count = isChange + final countToDiscover = isChange ? ElectrumWalletAddressesBase.INITIAL_CHANGE_COUNT : ElectrumWalletAddressesBase.INITIAL_RECEIVE_COUNT; @@ -2018,20 +2110,21 @@ abstract class ElectrumWalletBase startIndex ??= recordList.length; late bool needsToDiscover; - print(addressType); - print(seedBytesType); - print(derivationInfo.derivationPath.toString()); - print(isChange); - print([addressType, recordList.length]); - if (recordList.length < count) { + if (recordList.length < countToDiscover) { needsToDiscover = true; - } else if (recordList.length == count) { + } else if (recordList.length == countToDiscover) { needsToDiscover = recordList.any((record) => !record.getIsUsed()); } else { - needsToDiscover = recordList.sublist(recordList.length - count).any( - (record) => record.getIsUsed(), - ); + needsToDiscover = recordList.sublist(recordList.length - countToDiscover).any( + (record) { + return scripthashStatuses?.any( + (scripthashStatus) => + scripthashStatus.scripthash == (record as BitcoinAddressRecord).scriptHash, + ) ?? + record.getIsUsed(); + }, + ); } if (!needsToDiscover) { @@ -2041,7 +2134,7 @@ abstract class ElectrumWalletBase workerSendPort!.send( ElectrumWorkerDiscoverAddressesRequest( id: _messageId, - count: count, + count: countToDiscover, walletType: type, startIndex: startIndex, seedBytesType: seedBytesType, diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 97ded4f7f..9fcd09e21 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -28,7 +28,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { BitcoinAddressType? initialAddressPageType, }) : addressesRecords = initialAddressesRecords ?? BitcoinAddressRecordMap(), activeIndexByType = initialActiveAddressIndex ?? {}, - discoveredAddressTypes = initialDiscoveredAddresses ?? BitcoinDiscoveredAddressesMap(), + discoveredAddressesRecord = initialDiscoveredAddresses ?? BitcoinDiscoveredAddressesMap(), addressPageType = initialAddressPageType ?? (walletInfo.addressPageType != null ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) @@ -49,6 +49,18 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { String get xpub => hdWallet.publicKey.toExtended; String get xpriv => hdWallet.privateKey.toExtended; + // NOTE: order matters in priority + List get seedBytesTypes { + final seedBytesTypes = []; + if (hdWallets.containsKey(SeedBytesType.bip39)) { + seedBytesTypes.add(SeedBytesType.bip39); + } + if (hdWallets.containsKey(SeedBytesType.electrum)) { + seedBytesTypes.add(SeedBytesType.electrum); + } + return seedBytesTypes; + } + SeedBytesType get walletSeedBytesType => hdWallets.containsKey(SeedBytesType.bip39) ? SeedBytesType.bip39 : (hdWallets.containsKey(SeedBytesType.electrum) @@ -83,11 +95,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { bool isEnabledAutoGenerateNewAddress = true; @observable - BitcoinDiscoveredAddressesMap discoveredAddressTypes; + BitcoinDiscoveredAddressesMap discoveredAddressesRecord; @observable BitcoinAddressRecordMap addressesRecords; + Set get hiddenAddresses => + allAddresses.where((address) => address.isHidden).map((address) => address.address).toSet(); + @observable List _allAddresses = []; @computed @@ -257,12 +272,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action void resetActiveAddress() { - final activeReceiveAddresses = selectedReceiveAddresses.whereType(); + try { + final activeReceiveAddresses = selectedReceiveAddresses.whereType(); - activeBitcoinAddress = activeReceiveAddresses.firstWhereOrNull( - (addressRecord) => addressRecord.index == activeIndexByType[addressPageType], - ) ?? - activeReceiveAddresses.first; + activeBitcoinAddress = activeReceiveAddresses.firstWhereOrNull( + (addressRecord) => addressRecord.index == activeIndexByType[addressPageType], + ) ?? + activeReceiveAddresses.first; + } catch (_) {} } @override @@ -272,11 +289,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return activeBitcoinAddress!.address; } - final receiveAddress = selectedReceiveAddresses - .firstWhereOrNull( - (addr) => addr.getIsStillReceiveable(isEnabledAutoGenerateNewAddress) && !addr.isChange, - ) - ?.address; + final receiveAddress = nextReceiveAddress?.address; return receiveAddress ?? ''; } @@ -442,22 +455,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } } - @action - void updateAddresses(Iterable newAddresses) { - final replacedAddresses = newAddresses.toList(); - for (final address in newAddresses) { - final index = _allAddresses.indexWhere((element) => element.address == address.address); - if (index >= 0) { - _allAddresses.replaceRange(index, index + 1, [address]); - replacedAddresses.remove(address); - } - } - - if (replacedAddresses.isNotEmpty) { - _allAddresses.addAll(replacedAddresses); - } - } - @action void addAddresses(List addresses) { final firstAddress = addresses.first; @@ -469,28 +466,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { isChange: firstAddress.isChange, addressRecords: addresses, ); - print("addressesRecords.allRecords().length"); - print(firstAddress.type); - print(firstAddress.seedBytesType!); - print(firstAddress.derivationInfo.derivationPath.toString()); - print(firstAddress.isChange); - - print(addressesRecords - .getRecords( - addressType: firstAddress.type, - seedBytesType: firstAddress.seedBytesType!, - derivationPath: firstAddress.derivationInfo.derivationPath.toString(), - isChange: firstAddress.isChange, - ) - .length); - updateAllAddresses(); } Map toJson() { final json = {}; json['addressesRecords'] = addressesRecords.toJson(); - json['discoveredAddressTypes'] = discoveredAddressTypes.toJson(); + json['discoveredAddressesRecord'] = discoveredAddressesRecord.toJson(); json['addressPageType'] = addressPageType.toString(); json['activeIndexByType'] = activeIndexByType.map( (key, value) => MapEntry(key.toString(), value), @@ -508,7 +490,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return { 'allAddresses': addresses, 'addressesRecords': data['addressesRecords'] as Map?, - 'discoveredAddressTypes': data['discoveredAddressTypes'] as Map?, + 'discoveredAddressesRecord': data['discoveredAddressesRecord'] as Map?, 'addressPageType': data['addressPageType'] as String?, 'activeIndexByType': (data['activeIndexByType'] as Map?) ?? {}, }; @@ -529,9 +511,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { json['addressesRecords'] as Map, ); - if (json['discoveredAddressTypes'] != null) + if (json['discoveredAddressesRecord'] != null) initialDiscoveredAddresses ??= BitcoinDiscoveredAddressesMap.fromJson( - json['discoveredAddressTypes'] as Map, + json['discoveredAddressesRecord'] as Map, ); return ElectrumWalletAddresses( @@ -612,7 +594,24 @@ class BitcoinAddressRecordMap extends ItemsRecordMap { ); _data[addressType]![seedBytesType]![derivationPath]!.putIfAbsent(isChange, () => []); - _data[addressType]![seedBytesType]![derivationPath]![isChange]!.addAll(addressRecords); + final recordsList = _data[addressType]![seedBytesType]![derivationPath]![isChange]!; + + if (recordsList.isEmpty) { + recordsList.addAll(addressRecords); + } else { + for (final addressRecord in addressRecords) { + final existingRecordIndex = + recordsList.indexWhere((record) => record.address == addressRecord.address); + + if (existingRecordIndex >= 0) { + recordsList.replaceRange(existingRecordIndex, existingRecordIndex + 1, [addressRecord]); + } else { + recordsList.add(addressRecord); + } + } + } + + _data[addressType]![seedBytesType]![derivationPath]![isChange] = recordsList; } List allRecords() { @@ -697,31 +696,30 @@ class BitcoinDiscoveredAddressesMap extends ItemsRecordMap { seedBytesType: { - derivationPath: {addressRecord.isChange: discovered}, + derivationPath: {isChange: discovered}, }, }, ); _data[addressType]!.putIfAbsent( seedBytesType, () => { - derivationPath: {addressRecord.isChange: discovered}, + derivationPath: {isChange: discovered}, }, ); _data[addressType]![seedBytesType]!.putIfAbsent( derivationPath, - () => {addressRecord.isChange: discovered}, + () => {isChange: discovered}, ); - _data[addressType]![seedBytesType]![derivationPath]! - .putIfAbsent(addressRecord.isChange, () => discovered); + _data[addressType]![seedBytesType]![derivationPath]!.putIfAbsent(isChange, () => discovered); - _data[addressType]![seedBytesType]![derivationPath]![addressRecord.isChange] = discovered; + _data[addressType]![seedBytesType]![derivationPath]![isChange] = discovered; } bool getIsDiscovered({ diff --git a/cw_bitcoin/lib/electrum_worker/methods/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart index 12840b06c..d402d1a73 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/methods.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; diff --git a/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart index 18ee9dd7f..0df8f414a 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart @@ -65,8 +65,8 @@ class ElectrumWorkerScripthashesResponse { static ElectrumWorkerScripthashesResponse fromJson(Map json) { return ElectrumWorkerScripthashesResponse( - address: json['address'] as String, - scripthash: json['scripthash'] as String, + address: json['address'] as String? ?? '', + scripthash: json['scripthash'] as String? ?? '', status: json['status'] as String?, ); } @@ -89,9 +89,17 @@ class ElectrumWorkerScripthashesSubscribeResponse extends ElectrumWorkerResponse @override factory ElectrumWorkerScripthashesSubscribeResponse.fromJson(Map json) { return ElectrumWorkerScripthashesSubscribeResponse( - result: (json['result'] as List) - .map((e) => ElectrumWorkerScripthashesResponse.fromJson(e as Map)) - .toList(), + result: (json['result'] as List).map((e) { + if (e is String) { + return ElectrumWorkerScripthashesResponse.fromJson( + jsonDecode(e) as Map, + ); + } + + return ElectrumWorkerScripthashesResponse.fromJson( + e as Map, + ); + }).toList(), error: json['error'] as String?, id: json['id'] as int?, completed: json['completed'] as bool? ?? false, diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index c41590741..9c6ae77dd 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -66,13 +66,6 @@ class TransactionsPage extends StatelessWidget { dashboardViewModel: dashboardViewModel, key: ValueKey('transactions_page_header_row_key'), ), - Center( - child: CircularProgressIndicator( - backgroundColor: Theme.of(context).extension()!.textColor, - valueColor: AlwaysStoppedAnimation( - Theme.of(context).extension()!.firstGradientBottomPanelColor, - ), - )), Expanded( child: Observer( builder: (_) {