mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
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:
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user