From 997201e0809351de005f8de76d6017e9edb5e35a Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Thu, 28 May 2026 17:15:04 +0200 Subject: [PATCH] Cw 1438 v2 (#3252) * refactor address discovery and response handling * refactor input tx fetching and mweb tagging * add batch unspent fetching * add batch balance fetching * extend batch fetching to all Electrum wallets * minor fixes * [skip ci] Update cw_bitcoin/lib/electrum_wallet.dart Co-authored-by: Omar Hatem * Revert "minor fixes" This reverts commit 631e6258d25cc8717178bbc2b39407443f030677. * used addresses anywhere in gap * refresh receive and change addresses on set * add isLegacyDerivation flag to address flows --------- Co-authored-by: Serhii <17529954+serhii-bor@users.noreply.github.com> --- cw_bitcoin/lib/electrum.dart | 75 +++ cw_bitcoin/lib/electrum_wallet.dart | 463 ++++++++++++------ cw_bitcoin/lib/electrum_wallet_addresses.dart | 20 +- cw_bitcoin/lib/litecoin_wallet.dart | 1 + 4 files changed, 398 insertions(+), 161 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 095183426..052d18ef0 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -173,6 +173,7 @@ class ElectrumClient { if (isJSONStringCorrect(unterminatedString)) { final response = json.decode(unterminatedString); + _handleResponse(response); // unterminatedString = null; unterminatedString = ''; } @@ -340,6 +341,80 @@ class ElectrumClient { return historyMap; } + Future>>> getBatchUnspent( + List scriptHashes, { + int timeout = 10000, + }) async { + final paramsList = scriptHashes.map((h) => [h]).toList(growable: false); + + final batchResults = await callBatchWithTimeout( + method: 'blockchain.scripthash.listunspent', + paramsList: paramsList, + timeout: timeout, + ); + + final unspentMap = >>{}; + + for (int i = 0; i < scriptHashes.length; i++) { + final sh = scriptHashes[i]; + + if (i >= batchResults.length) { + unspentMap[sh] = const []; + continue; + } + + final result = batchResults[i]; + + if (result is List) { + unspentMap[sh] = result + .whereType>() + .map((m) => m.map((k, v) => MapEntry(k.toString(), v))) + .cast>() + .toList(); + } else { + unspentMap[sh] = const []; + } + } + + return unspentMap; + } + + Future>> getBatchBalance( + List scriptHashes, { + int timeout = 10000, + }) async { + final paramsList = scriptHashes.map((h) => [h]).toList(growable: false); + + final batchResults = await callBatchWithTimeout( + method: 'blockchain.scripthash.get_balance', + paramsList: paramsList, + timeout: timeout, + ); + + final balanceMap = >{}; + + for (int i = 0; i < scriptHashes.length; i++) { + final sh = scriptHashes[i]; + + if (i >= batchResults.length) { + balanceMap[sh] = {}; + continue; + } + + final result = batchResults[i]; + + if (result is Map) { + balanceMap[sh] = result; + } else if (result is Map) { + balanceMap[sh] = Map.from(result); + } else { + balanceMap[sh] = {}; + } + } + + return balanceMap; + } + Future>> getBatchTransactionVerbose( List hashes, { int timeout = 10000, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 842fe7e38..1b10235ea 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -367,6 +367,8 @@ abstract class ElectrumWalletBase String get xpub => accountHD.publicKey.toExtended; + bool get shouldUseBatchFetching => useBatchForHistory && _isBatchSupported == true; + @override String? get seed => _mnemonic; @@ -708,7 +710,7 @@ abstract class ElectrumWalletBase } await subscribeForUpdates(); - await _checkIfBatchSupported(); + await checkIfBatchSupported(); await updateTransactions(); await updateAllUnspents(); @@ -1793,18 +1795,21 @@ abstract class ElectrumWalletBase } // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating - walletAddresses.allAddresses - .where((element) => element.type != SegwitAddresType.mweb) - .forEach((addr) { - if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; - }); - final addressFutures = walletAddresses.allAddresses + final targetAddresses = walletAddresses.allAddresses .where((element) => element.type != SegwitAddresType.mweb) - .map((address) => fetchUnspent(address)) .toList(); - final results = await Future.wait(addressFutures); + for (final addr in targetAddresses) { + if (addr is! BitcoinSilentPaymentAddressRecord) { + addr.balance = 0; + } + } + + final results = shouldUseBatchFetching + ? await _fetchUnspentsBatch(targetAddresses) + : await _fetchUnspentsRegular(targetAddresses); + final failedCount = results.where((result) => result == null).length; if (failedCount == 0) { @@ -1836,6 +1841,74 @@ abstract class ElectrumWalletBase await _refreshUnspentCoinsInfo(); } + Future?>> _fetchUnspentsRegular( + List addresses, + ) async { + final addressFutures = addresses.map((address) => fetchUnspent(address)).toList(); + return Future.wait(addressFutures); + } + + + Future?>> _fetchUnspentsBatch( + List addresses, + ) async { + final byScriptHash = { + for (final address in addresses) address.getScriptHash(network): address, + }; + + final scriptHashes = byScriptHash.keys.toList(); + + try { + final unspentByScriptHash = + await _processChunksToMap>>( + items: scriptHashes, + chunkSize: addressHistoryChunkSize, + processChunk: _getListUnspentBatch, + ); + + final txHashes = {}; + final coinsByScriptHash = >{}; + + for (final entry in unspentByScriptHash.entries) { + final addressRecord = byScriptHash[entry.key]; + if (addressRecord == null) continue; + + final coins = []; + + for (final unspent in entry.value) { + final coin = BitcoinUnspent.fromJSON(addressRecord, unspent); + coin.isChange = addressRecord.isHidden; + coins.add(coin); + txHashes.add(coin.hash); + } + + coinsByScriptHash[entry.key] = coins; + } + + final txInfoByHash = await fetchTransactionInfoBatch( + hashes: txHashes.toList(), + retryOnFailure: true, + retryDelay: const Duration(seconds: 1), + ); + + for (final coins in coinsByScriptHash.values) { + for (final coin in coins) { + final tx = txInfoByHash[coin.hash]; + coin.confirmations = tx?.confirmations; + coin.isPegOut = tx?.isHogEx; + } + } + + return addresses.map((address) { + final scriptHash = address.getScriptHash(network); + return coinsByScriptHash[scriptHash] ?? []; + }).toList(); + } catch (e) { + printV('fetchUnspentsBatch failed: $e'); + return List?>.filled(addresses.length, null); + } + } + List handleFailedUtxoFetch({ required int failedCount, required List previousUnspentCoins, @@ -2395,25 +2468,30 @@ abstract class ElectrumWalletBase @override Future> fetchTransactions() async { try { - final Map historiesWithDetails = {}; - final shouldUseBatchForHistory = useBatchForHistory && _isBatchSupported == true; + final Map historiesWithDetails = {};; - printV('[BATCH_TEST] Fetching transactions with batch: $shouldUseBatchForHistory'); + printV('[BATCH_TEST] Fetching transactions with batch: $shouldUseBatchFetching'); if (type == WalletType.bitcoin) { - await Future.wait(BITCOIN_ADDRESS_TYPES.map((type) => shouldUseBatchForHistory + await Future.wait(BITCOIN_ADDRESS_TYPES.map((type) => shouldUseBatchFetching ? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type) : fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.bitcoinCash) { await Future.wait(BITCOIN_CASH_ADDRESS_TYPES - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); + .map((type) => shouldUseBatchFetching + ? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type) + : fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.litecoin) { await Future.wait(LITECOIN_ADDRESS_TYPES .where((type) => type != SegwitAddresType.mweb) - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); + .map((type) => shouldUseBatchFetching + ? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type) + : fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.dogecoin) { await Future.wait(DOGECOIN_ADDRESS_TYPES - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); + .map((type) => shouldUseBatchFetching + ? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type) + : fetchTransactionsForAddressType(historiesWithDetails, type))); } transactionHistory.transactions.values.forEach((tx) async { @@ -2457,13 +2535,13 @@ abstract class ElectrumWalletBase historiesWithDetails.addAll(history); final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses; - final isUsedAddressUnderGap = matchedAddresses.toList().indexOf(addressRecord) >= + final isUsedAddressAboveGap = matchedAddresses.toList().indexOf(addressRecord) >= matchedAddresses.length - (addressRecord.isHidden ? ElectrumWalletAddressesBase.defaultChangeAddressesCount : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); - if (isUsedAddressUnderGap) { + if (isUsedAddressAboveGap) { final prevLength = walletAddresses.allAddresses.length; // Discover new addresses for the same address type until the gap limit is respected @@ -2538,19 +2616,7 @@ abstract class ElectrumWalletBase // Got a new transaction fetched, add it to the transaction history // instead of waiting all to finish, and next time it will be faster - if (this is LitecoinWallet) { - // if we have a peg out transaction with the same value - // that matches this received transaction, mark it as being from a peg out: - for (final tx2 in transactionHistory.transactions.values) { - final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs(); - // this isn't a perfect matching algorithm since we don't have the right input/output information from these transaction models (the addresses are in different formats), but this should be more than good enough for now as it's extremely unlikely a user receives the EXACT same amount from 2 different sources and one of them is a peg out and the other isn't WITHIN 5 blocks of each other - if (tx2.additionalInfo["isPegOut"] == true && - tx2.amount == tx.amount && - heightDiff <= 5) { - tx.additionalInfo["fromPegOut"] = true; - } - } - } + _applyLitecoinPegOutTag(tx); transactionHistory.addOne(tx); await transactionHistory.save(); } @@ -2572,65 +2638,128 @@ abstract class ElectrumWalletBase } Future fetchTransactionsForAddressTypeBatch( - Map historiesWithDetails, BitcoinAddressType type) async { + Map historiesWithDetails, + BitcoinAddressType type) async { final addressesByType = - walletAddresses.allAddresses.where((addr) => addr.type == type).toList(); - final hiddenAddresses = addressesByType.where((addr) => addr.isHidden).toList(); + walletAddresses.allAddresses.where((addr) => addr.type == type).toList(); final receiveAddresses = addressesByType.where((addr) => !addr.isHidden).toList(); + final hiddenAddresses = addressesByType.where((addr) => addr.isHidden).toList(); + walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address)); await walletAddresses.saveAddressesInBox(); - final tip = await getCurrentChainTip(); - - final addressHistory = await _processChunksToMap( - items: addressesByType, - chunkSize: addressHistoryChunkSize, - processChunk: (chunk) => _fetchBatchAddressHistory(chunk, tip, addressHistoryChunkSize), + await fetchTransactionsForAddressesBranchBatch( + historiesWithDetails, + type, + receiveAddresses, + isHidden: false, + isLegacyDerivation: false, ); - if (addressHistory.isNotEmpty) historiesWithDetails.addAll(addressHistory); + await fetchTransactionsForAddressesBranchBatch( + historiesWithDetails, + type, + hiddenAddresses, + isHidden: true, + isLegacyDerivation: false, + ); - for (final addressRecord in addressesByType) { - final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses; + await fetchTransactionsForAddressesBranchBatch( + historiesWithDetails, + type, + receiveAddresses, + isHidden: false, + isLegacyDerivation: true, + ); - final isUsedAddressUnderGap = - matchedAddresses.indexOf(addressRecord) >= - matchedAddresses.length - ElectrumWalletAddressesBase.gap; - - if (isUsedAddressUnderGap && addressRecord.isUsed) { - final prevLength = walletAddresses.allAddresses.length; + await fetchTransactionsForAddressesBranchBatch( + historiesWithDetails, + type, + hiddenAddresses, + isHidden: true, + isLegacyDerivation: true, + ); + } - await walletAddresses.discoverAddressesBatch( - matchedAddresses, - addressRecord.isHidden, - (newAddresses) async { - await _fetchBatchAddressHistory( - newAddresses, - tip, - discoveryHistoryChunkSize, - ); + Future fetchTransactionsForAddressesBranchBatch( + Map historiesWithDetails, + BitcoinAddressType type, + List branchAddresses, { + required bool isHidden, + required bool isLegacyDerivation, + }) async { + if (branchAddresses.isEmpty) return; - return newAddresses - .where((addressRecord) => addressRecord.isUsed) - .map((addressRecord) => addressRecord.address) - .toSet(); - }, - type: type, + final tip = await getCurrentChainTip(); + final currentBranch = [...branchAddresses]; + + final initialHistory = + await _processChunksToMap( + items: currentBranch, + chunkSize: addressHistoryChunkSize, + processChunk: (chunk) => _fetchBatchAddressHistory( + chunk, + tip, + addressHistoryChunkSize, + ), + ); + + if (initialHistory.isNotEmpty) { + historiesWithDetails.addAll(initialHistory); + } + + final gapLimit = isHidden + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount; + + final highestUsedIndex = _highestUsedIndex(currentBranch); + final shouldDiscover = + highestUsedIndex >= 0 && highestUsedIndex >= currentBranch.length - gapLimit; + + if (!shouldDiscover) return; + + + final newAddresses = await walletAddresses.discoverAddressesBatch( + currentBranch, + isHidden, + (newAddresses) async { + final newHistory = await _fetchBatchAddressHistory( + newAddresses, + tip, + discoveryHistoryChunkSize, ); - final newLength = walletAddresses.allAddresses.length; - - if (newLength > prevLength) { - await fetchTransactionsForAddressTypeBatch( - historiesWithDetails, - type); - return; + if (newHistory.isNotEmpty) { + historiesWithDetails.addAll(newHistory); } + + return newAddresses + .where((addressRecord) => addressRecord.isUsed) + .map((addressRecord) => addressRecord.address) + .toSet(); + }, + type: type, + isLegacyDerivation: isLegacyDerivation, + ); + + if (newAddresses.isNotEmpty) { + currentBranch.addAll(newAddresses); + + if (isHidden) { + walletAddresses.hiddenAddresses.addAll(newAddresses.map((e) => e.address)); + await walletAddresses.saveAddressesInBox(); } } } + int _highestUsedIndex(List addresses) { + for (int i = addresses.length - 1; i >= 0; i--) { + if (addresses[i].isUsed) return i; + } + return -1; + } + Future> _fetchBatchAddressHistory( List addressRecords, int? currentHeight, @@ -2762,17 +2891,7 @@ abstract class ElectrumWalletBase historiesWithDetails[tx.id] = tx; - // Litecoin peg-out tagging - if (this is LitecoinWallet) { - for (final tx2 in transactionHistory.transactions.values) { - final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs(); - if (tx2.additionalInfo["isPegOut"] == true && - tx2.amount == tx.amount && - heightDiff <= 5) { - tx.additionalInfo["fromPegOut"] = true; - } - } - } + _applyLitecoinPegOutTag(tx); transactionHistory.addOne(tx); didUpdateHistory = true; @@ -2819,6 +2938,22 @@ abstract class ElectrumWalletBase ); } + Future>>> _getListUnspentBatch( + List scriptHashes) { + return electrumClient.getBatchUnspent( + scriptHashes, + timeout: transactionBatchTimeoutMs, + ); + } + + Future>> _getBalanceBatch( + List scriptHashes) { + return electrumClient.getBatchBalance( + scriptHashes, + timeout: transactionBatchTimeoutMs, + ); + } + Future> fetchTransactionInfoBatch({ required List hashes, Map? heightsByHash, @@ -2906,8 +3041,16 @@ abstract class ElectrumWalletBase final inputTxIdsByHash = _collectInputTxIdsByHash(originalByHash); - final inputVerboseByTxId = await _fetchInputTransactionVerboseBatch( - inputTxIdsByHash); + final allInputTxids = {}; + for (final txids in inputTxIdsByHash.values) { + allInputTxids.addAll(txids); + } + + final inputTxIds = allInputTxids.toList(growable: false); + + final inputVerboseByTxId = inputTxIds.isEmpty + ? >{} + : await _fetchTransactionVerboseBatch(inputTxIds); final parsedInputTxById = _parseTransactions(inputVerboseByTxId); @@ -2999,65 +3142,6 @@ abstract class ElectrumWalletBase return inputTxIdsByHash; } - - Future>> _fetchInputTransactionVerboseBatch( - Map> inputTxidsByHash) async { - final allInputTxids = {}; - for (final txids in inputTxidsByHash.values) { - allInputTxids.addAll(txids); - } - - final inputTxIds = allInputTxids.toList(growable: false); - - final verboseTransactionByHash = - await _processChunksToMap>( - items: inputTxIds, - chunkSize: inputTransactionChunkSize, - processChunk: _getTransactionVerboseBatch, - onChunkError: (chunk, error) { - if (error is electrum.RequestFailedTimeoutException) { - printV( - 'fetchInputTransactionVerboseBatch timeout for ${chunk.length} txs: ${error.method}', - ); - } else { - printV( - 'fetchInputTransactionVerboseBatch failed for ${chunk.length} txs: $error,', - ); - } - }, - ); - - final emptyHex = []; - for (final txId in inputTxIds) { - final vTx = verboseTransactionByHash[txId]; - if (vTx == null || vTx.isEmpty || vTx['hex'] == null) { - emptyHex.add(txId); - } - } - - final hexByHash = await _processChunksToMap( - items: emptyHex, - chunkSize: inputTransactionChunkSize, - processChunk: _getTransactionHexBatch, - ); - - for (final txId in inputTxIds) { - final verbose = verboseTransactionByHash[txId] ?? {}; - if ((verbose['hex'] as String?) == null) { - final hex = hexByHash[txId]; - if (hex != null && hex.isNotEmpty) { - verboseTransactionByHash[txId] = { - ...verbose, - 'hex': hex, - }; - } - } - } - - return verboseTransactionByHash; - } - - Future> _buildTransactionBundlesBatch({ required List unique, required Map? heightsByHash, @@ -3290,18 +3374,64 @@ abstract class ElectrumWalletBase })); } + Future>> fetchBalancesBatch( + List addresses, + ) async { + final scriptHashes = addresses.map((address) => address.getScriptHash(network)).toList(); + + if (scriptHashes.isEmpty) { + return >[]; + } + + try { + final balancesByScriptHash = + await _processChunksToMap>( + items: scriptHashes, + chunkSize: addressHistoryChunkSize, + processChunk: _getBalanceBatch, + ); + + final balances = scriptHashes + .map((scriptHash) => balancesByScriptHash[scriptHash] ?? {}) + .toList(); + + final hasMissingBalance = balances.any((balance) => balance['confirmed'] == null); + if (hasMissingBalance) { + printV('fetchBalancesBatch returned missing balances, falling back to regular flow'); + return fetchBalancesRegular(addresses); + } + + return balances; + } catch (e) { + printV('fetchBalancesBatch failed, falling back to regular flow: $e'); + return fetchBalancesRegular(addresses); + } + } + + Future>> fetchBalancesRegular( + List addresses, + ) async { + final balanceFutures = >>[]; + + for (final address in addresses) { + final sh = address.getScriptHash(network); + balanceFutures.add(electrumClient.getBalance(sh)); + } + + return Future.wait(balanceFutures); + } + Future fetchBalances() async { final addresses = walletAddresses.allAddresses .where((address) => address.address.isNotEmpty) .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) .toList(); - final balanceFutures = >>[]; - for (var i = 0; i < addresses.length; i++) { - final addressRecord = addresses[i]; - final sh = addressRecord.getScriptHash(network); - final balanceFuture = electrumClient.getBalance(sh); - balanceFutures.add(balanceFuture); - } + + final balances = shouldUseBatchFetching + ? await fetchBalancesBatch(addresses) + : await fetchBalancesRegular(addresses); + + printV('Fetched balances for ${addresses.length} addresses. Batch fetching: $shouldUseBatchFetching'); var totalFrozen = 0; var totalConfirmed = 0; @@ -3337,8 +3467,6 @@ abstract class ElectrumWalletBase }); }); - final balances = await Future.wait(balanceFutures); - if (balances.isNotEmpty && balances.first['confirmed'] == null) { // if we got null balance responses from the server, set our connection status to lost and return our last known balance: printV("got null balance responses from the server, setting connection status to lost"); @@ -3420,7 +3548,23 @@ abstract class ElectrumWalletBase return base64Encode(decodedSig); } - Future _checkIfBatchSupported() async { + void _applyLitecoinPegOutTag(ElectrumTransactionInfo tx) { + if (this is! LitecoinWallet) return; + + // if we have a peg out transaction with the same value + // that matches this received transaction, mark it as being from a peg out: + for (final tx2 in transactionHistory.transactions.values) { + final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs(); + // this isn't a perfect matching algorithm since we don't have the right input/output information from these transaction models (the addresses are in different formats), but this should be more than good enough for now as it's extremely unlikely a user receives the EXACT same amount from 2 different sources and one of them is a peg out and the other isn't WITHIN 5 blocks of each other + if (tx2.additionalInfo["isPegOut"] == true && + tx2.amount == tx.amount && + heightDiff <= 5) { + tx.additionalInfo["fromPegOut"] = true; + } + } + } + + Future checkIfBatchSupported() async { if (_isBatchSupported != null) { printV('[BATCH_TEST] Already checked: $_isBatchSupported'); @@ -3440,12 +3584,23 @@ abstract class ElectrumWalletBase printV('[BATCH_TEST] Start: hashes=${hashes.length}, timeout=${batchTestTimeoutMs}ms'); - await electrumClient.callBatchWithTimeout( + final result = await electrumClient.callBatchWithTimeout( method: 'blockchain.scripthash.get_history', paramsList: paramsList, timeout: batchTestTimeoutMs, ); + final hasError = result.any((item) => + item is Map && + item.containsKey('error') && + item['error'] != null); + + if (hasError) { + _isBatchSupported = false; + printV('[BATCH_TEST] Result: supported=false (server returned error)'); + return; + } + _isBatchSupported = true; printV('[BATCH_TEST] Result: supported=true'); } on electrum.RequestFailedTimeoutException catch (e) { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index d3763386d..e692816b6 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -715,38 +715,42 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future discoverAddressesBatch( + Future> discoverAddressesBatch( List addressList, bool isHidden, Future> Function(List) getUsedAddresses, { BitcoinAddressType type = SegwitAddresType.p2wpkh, + required bool isLegacyDerivation, }) async { final newAddresses = await _createNewAddresses( gap, startIndex: addressList.length, isHidden: isHidden, type: type, + isLegacyDerivation: isLegacyDerivation, ); addAddresses(newAddresses); final usedAddresses = await getUsedAddresses(newAddresses); - final isLastAddressUsed = - newAddresses.isNotEmpty && usedAddresses.contains(newAddresses.last.address); - if (!isLastAddressUsed) { - return false; + final hasUsedAddressInGap = newAddresses.any( + (addressRecord) => usedAddresses.contains(addressRecord.address)); + + if (!hasUsedAddressInGap) { + return newAddresses; } final updatedAddressList = [...addressList, ...newAddresses]; - await discoverAddressesBatch( + final moreNewAddresses = await discoverAddressesBatch( updatedAddressList, isHidden, getUsedAddresses, type: type, + isLegacyDerivation: isLegacyDerivation, ); - return true; + return [...newAddresses, ...moreNewAddresses]; } @@ -818,6 +822,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { this._addresses.clear(); this._addresses.addAll(addressesSet); updateAddressesByMatch(); + updateReceiveAddresses(); + updateChangeAddresses(); } @action diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 3cee6c9da..3a363aa4a 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -381,6 +381,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } catch (e) { printV("failed to subscribe for updates: $e"); } + await checkIfBatchSupported(); updateFeeRates(); _feeRatesTimer?.cancel(); _feeRatesTimer =