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 <omarh.ismail1@gmail.com>

* 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>
This commit is contained in:
Omar Hatem
2026-05-28 17:15:04 +02:00
committed by GitHub
co-authored by Serhii
parent 4c95b85c46
commit 997201e080
4 changed files with 398 additions and 161 deletions
+75
View File
@@ -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<Map<String, List<Map<String, dynamic>>>> getBatchUnspent(
List<String> scriptHashes, {
int timeout = 10000,
}) async {
final paramsList = scriptHashes.map((h) => <Object>[h]).toList(growable: false);
final batchResults = await callBatchWithTimeout(
method: 'blockchain.scripthash.listunspent',
paramsList: paramsList,
timeout: timeout,
);
final unspentMap = <String, List<Map<String, dynamic>>>{};
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<dynamic, dynamic>>()
.map((m) => m.map((k, v) => MapEntry(k.toString(), v)))
.cast<Map<String, dynamic>>()
.toList();
} else {
unspentMap[sh] = const [];
}
}
return unspentMap;
}
Future<Map<String, Map<String, dynamic>>> getBatchBalance(
List<String> scriptHashes, {
int timeout = 10000,
}) async {
final paramsList = scriptHashes.map((h) => <Object>[h]).toList(growable: false);
final batchResults = await callBatchWithTimeout(
method: 'blockchain.scripthash.get_balance',
paramsList: paramsList,
timeout: timeout,
);
final balanceMap = <String, Map<String, dynamic>>{};
for (int i = 0; i < scriptHashes.length; i++) {
final sh = scriptHashes[i];
if (i >= batchResults.length) {
balanceMap[sh] = <String, dynamic>{};
continue;
}
final result = batchResults[i];
if (result is Map<String, dynamic>) {
balanceMap[sh] = result;
} else if (result is Map) {
balanceMap[sh] = Map<String, dynamic>.from(result);
} else {
balanceMap[sh] = <String, dynamic>{};
}
}
return balanceMap;
}
Future<Map<String, Map<String, dynamic>>> getBatchTransactionVerbose(
List<String> hashes, {
int timeout = 10000,
+309 -154
View File
@@ -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<List<List<BitcoinUnspent>?>> _fetchUnspentsRegular(
List<BitcoinAddressRecord> addresses,
) async {
final addressFutures = addresses.map((address) => fetchUnspent(address)).toList();
return Future.wait(addressFutures);
}
Future<List<List<BitcoinUnspent>?>> _fetchUnspentsBatch(
List<BitcoinAddressRecord> addresses,
) async {
final byScriptHash = <String, BitcoinAddressRecord>{
for (final address in addresses) address.getScriptHash(network): address,
};
final scriptHashes = byScriptHash.keys.toList();
try {
final unspentByScriptHash =
await _processChunksToMap<String, String, List<Map<String, dynamic>>>(
items: scriptHashes,
chunkSize: addressHistoryChunkSize,
processChunk: _getListUnspentBatch,
);
final txHashes = <String>{};
final coinsByScriptHash = <String, List<BitcoinUnspent>>{};
for (final entry in unspentByScriptHash.entries) {
final addressRecord = byScriptHash[entry.key];
if (addressRecord == null) continue;
final coins = <BitcoinUnspent>[];
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] ?? <BitcoinUnspent>[];
}).toList();
} catch (e) {
printV('fetchUnspentsBatch failed: $e');
return List<List<BitcoinUnspent>?>.filled(addresses.length, null);
}
}
List<BitcoinUnspent> handleFailedUtxoFetch({
required int failedCount,
required List<BitcoinUnspent> previousUnspentCoins,
@@ -2395,25 +2468,30 @@ abstract class ElectrumWalletBase
@override
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
try {
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
final shouldUseBatchForHistory = useBatchForHistory && _isBatchSupported == true;
final Map<String, ElectrumTransactionInfo> 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<void> fetchTransactionsForAddressTypeBatch(
Map<String, ElectrumTransactionInfo> historiesWithDetails, BitcoinAddressType type) async {
Map<String, ElectrumTransactionInfo> 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<BitcoinAddressRecord, String, ElectrumTransactionInfo>(
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<void> fetchTransactionsForAddressesBranchBatch(
Map<String, ElectrumTransactionInfo> historiesWithDetails,
BitcoinAddressType type,
List<BitcoinAddressRecord> 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<BitcoinAddressRecord, String, ElectrumTransactionInfo>(
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<BitcoinAddressRecord> addresses) {
for (int i = addresses.length - 1; i >= 0; i--) {
if (addresses[i].isUsed) return i;
}
return -1;
}
Future<Map<String, ElectrumTransactionInfo>> _fetchBatchAddressHistory(
List<BitcoinAddressRecord> 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<Map<String, List<Map<String, dynamic>>>> _getListUnspentBatch(
List<String> scriptHashes) {
return electrumClient.getBatchUnspent(
scriptHashes,
timeout: transactionBatchTimeoutMs,
);
}
Future<Map<String, Map<String, dynamic>>> _getBalanceBatch(
List<String> scriptHashes) {
return electrumClient.getBatchBalance(
scriptHashes,
timeout: transactionBatchTimeoutMs,
);
}
Future<Map<String, ElectrumTransactionInfo?>> fetchTransactionInfoBatch({
required List<String> hashes,
Map<String, int?>? heightsByHash,
@@ -2906,8 +3041,16 @@ abstract class ElectrumWalletBase
final inputTxIdsByHash = _collectInputTxIdsByHash(originalByHash);
final inputVerboseByTxId = await _fetchInputTransactionVerboseBatch(
inputTxIdsByHash);
final allInputTxids = <String>{};
for (final txids in inputTxIdsByHash.values) {
allInputTxids.addAll(txids);
}
final inputTxIds = allInputTxids.toList(growable: false);
final inputVerboseByTxId = inputTxIds.isEmpty
? <String, Map<String, dynamic>>{}
: await _fetchTransactionVerboseBatch(inputTxIds);
final parsedInputTxById = _parseTransactions(inputVerboseByTxId);
@@ -2999,65 +3142,6 @@ abstract class ElectrumWalletBase
return inputTxIdsByHash;
}
Future<Map<String, Map<String, dynamic>>> _fetchInputTransactionVerboseBatch(
Map<String, List<String>> inputTxidsByHash) async {
final allInputTxids = <String>{};
for (final txids in inputTxidsByHash.values) {
allInputTxids.addAll(txids);
}
final inputTxIds = allInputTxids.toList(growable: false);
final verboseTransactionByHash =
await _processChunksToMap<String, String, Map<String, dynamic>>(
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 = <String>[];
for (final txId in inputTxIds) {
final vTx = verboseTransactionByHash[txId];
if (vTx == null || vTx.isEmpty || vTx['hex'] == null) {
emptyHex.add(txId);
}
}
final hexByHash = await _processChunksToMap<String, String, String?>(
items: emptyHex,
chunkSize: inputTransactionChunkSize,
processChunk: _getTransactionHexBatch,
);
for (final txId in inputTxIds) {
final verbose = verboseTransactionByHash[txId] ?? <String, dynamic>{};
if ((verbose['hex'] as String?) == null) {
final hex = hexByHash[txId];
if (hex != null && hex.isNotEmpty) {
verboseTransactionByHash[txId] = {
...verbose,
'hex': hex,
};
}
}
}
return verboseTransactionByHash;
}
Future<Map<String, ElectrumTransactionBundle>> _buildTransactionBundlesBatch({
required List<String> unique,
required Map<String, int?>? heightsByHash,
@@ -3290,18 +3374,64 @@ abstract class ElectrumWalletBase
}));
}
Future<List<Map<String, dynamic>>> fetchBalancesBatch(
List<BitcoinAddressRecord> addresses,
) async {
final scriptHashes = addresses.map((address) => address.getScriptHash(network)).toList();
if (scriptHashes.isEmpty) {
return <Map<String, dynamic>>[];
}
try {
final balancesByScriptHash =
await _processChunksToMap<String, String, Map<String, dynamic>>(
items: scriptHashes,
chunkSize: addressHistoryChunkSize,
processChunk: _getBalanceBatch,
);
final balances = scriptHashes
.map((scriptHash) => balancesByScriptHash[scriptHash] ?? <String, dynamic>{})
.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<List<Map<String, dynamic>>> fetchBalancesRegular(
List<BitcoinAddressRecord> addresses,
) async {
final balanceFutures = <Future<Map<String, dynamic>>>[];
for (final address in addresses) {
final sh = address.getScriptHash(network);
balanceFutures.add(electrumClient.getBalance(sh));
}
return Future.wait(balanceFutures);
}
Future<ElectrumBalance> fetchBalances() async {
final addresses = walletAddresses.allAddresses
.where((address) => address.address.isNotEmpty)
.where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress)
.toList();
final balanceFutures = <Future<Map<String, dynamic>>>[];
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<void> _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<void> 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<String, dynamic> &&
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) {
+13 -7
View File
@@ -715,38 +715,42 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
@action
Future<bool> discoverAddressesBatch(
Future<List<BitcoinAddressRecord>> discoverAddressesBatch(
List<BitcoinAddressRecord> addressList,
bool isHidden,
Future<Set<String>> Function(List<BitcoinAddressRecord>) 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
+1
View File
@@ -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 =