electrum/fulcrum batching tweaks and fixes

This commit is contained in:
julian 2024-02-22 12:16:53 +07:00
parent a5299adb39
commit 725d11f9c2
2 changed files with 86 additions and 91 deletions

View file

@ -387,7 +387,7 @@ class ElectrumXClient {
/// returns a list of json response objects if no errors were found /// returns a list of json response objects if no errors were found
Future<List<dynamic>> batchRequest({ Future<List<dynamic>> batchRequest({
required String command, required String command,
required Map<String, List<dynamic>> args, required List<dynamic> args,
Duration requestTimeout = const Duration(seconds: 60), Duration requestTimeout = const Duration(seconds: 60),
int retries = 2, int retries = 2,
}) async { }) async {
@ -404,37 +404,39 @@ class ElectrumXClient {
try { try {
var futures = <Future<dynamic>>[]; var futures = <Future<dynamic>>[];
List? response;
_electrumAdapterClient!.peer.withBatch(() { _electrumAdapterClient!.peer.withBatch(() {
for (final entry in args.entries) { for (final arg in args) {
futures.add(_electrumAdapterClient!.request(command, entry.value)); futures.add(_electrumAdapterClient!.request(command, arg));
} }
}); });
response = await Future.wait(futures); final response = await Future.wait(futures);
// check for errors, format and throw if there are any // We cannot modify the response list as the order and length are related
final List<String> errors = []; // to the order and length of the batched requests!
for (int i = 0; i < response.length; i++) { //
var result = response[i]; // // check for errors, format and throw if there are any
// final List<String> errors = [];
if (result == null || (result is List && result.isEmpty)) { // for (int i = 0; i < response.length; i++) {
continue; // var result = response[i];
// TODO [prio=extreme]: Figure out if this is actually an issue. //
} // if (result == null || (result is List && result.isEmpty)) {
result = result[0]; // Unwrap the list. // continue;
if ((result is Map && result.keys.contains("error")) || // // TODO [prio=extreme]: Figure out if this is actually an issue.
result == null) { // }
errors.add(result.toString()); // result = result[0]; // Unwrap the list.
} // if ((result is Map && result.keys.contains("error")) ||
} // result == null) {
if (errors.isNotEmpty) { // errors.add(result.toString());
String error = "[\n"; // }
for (int i = 0; i < errors.length; i++) { // }
error += "${errors[i]}\n"; // if (errors.isNotEmpty) {
} // String error = "[\n";
error += "]"; // for (int i = 0; i < errors.length; i++) {
throw Exception("JSONRPC response error: $error"); // error += "${errors[i]}\n";
} // }
// error += "]";
// throw Exception("JSONRPC response error: $error");
// }
currentFailoverIndex = -1; currentFailoverIndex = -1;
return response; return response;
@ -636,16 +638,17 @@ class ElectrumXClient {
} }
} }
Future<Map<int, List<Map<String, dynamic>>>> getBatchHistory( Future<List<List<Map<String, dynamic>>>> getBatchHistory({
{required Map<String, List<dynamic>> args}) async { required List<dynamic> args,
}) async {
try { try {
final response = await batchRequest( final response = await batchRequest(
command: 'blockchain.scripthash.get_history', command: 'blockchain.scripthash.get_history',
args: args, args: args,
); );
final Map<int, List<Map<String, dynamic>>> result = {}; final List<List<Map<String, dynamic>>> result = [];
for (int i = 0; i < response.length; i++) { for (int i = 0; i < response.length; i++) {
result[i] = List<Map<String, dynamic>>.from(response[i] as List); result.add(List<Map<String, dynamic>>.from(response[i] as List));
} }
return result; return result;
} catch (e) { } catch (e) {
@ -689,23 +692,27 @@ class ElectrumXClient {
} }
} }
Future<Map<int, List<Map<String, dynamic>>>> getBatchUTXOs( Future<List<List<Map<String, dynamic>>>> getBatchUTXOs({
{required Map<String, List<dynamic>> args}) async { required List<dynamic> args,
}) async {
try { try {
final response = await batchRequest( final response = await batchRequest(
command: 'blockchain.scripthash.listunspent', command: 'blockchain.scripthash.listunspent',
args: args, args: args,
); );
final Map<int, List<Map<String, dynamic>>> result = {}; final List<List<Map<String, dynamic>>> result = [];
for (int i = 0; i < response.length; i++) { for (int i = 0; i < response.length; i++) {
if ((response[i] as List).isNotEmpty) { if ((response[i] as List).isNotEmpty) {
try { try {
// result[i] = response[i] as List<Map<String, dynamic>>; final data = List<Map<String, dynamic>>.from(response[i] as List);
result[i] = List<Map<String, dynamic>>.from(response[i] as List); result.add(data);
} catch (e) { } catch (e) {
print(response[i]); // to ensure we keep same length of responses as requests/args
// add empty list on error
result.add([]);
Logging.instance.log( Logging.instance.log(
"getBatchUTXOs failed to parse response", "getBatchUTXOs failed to parse response=${response[i]}: $e",
level: LogLevel.Error, level: LogLevel.Error,
); );
} }

View file

@ -842,21 +842,19 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
return transactions.length; return transactions.length;
} }
Future<Map<int, int>> fetchTxCountBatched({ /// Should return a list of tx counts matching the list of addresses given
required Map<String, String> addresses, Future<List<int>> fetchTxCountBatched({
required List<String> addresses,
}) async { }) async {
try { try {
final Map<String, List<dynamic>> args = {}; final response = await electrumXClient.getBatchHistory(
for (final entry in addresses.entries) { args: addresses
args[entry.key] = [ .map((e) => [cryptoCurrency.addressToScriptHash(address: e)])
cryptoCurrency.addressToScriptHash(address: entry.value), .toList(growable: false));
];
}
final response = await electrumXClient.getBatchHistory(args: args);
final Map<int, int> result = {}; final List<int> result = [];
for (final entry in response.entries) { for (final entry in response) {
result[entry.key] = entry.value.length; result.add(entry.length);
} }
return result; return result;
} catch (e, s) { } catch (e, s) {
@ -968,13 +966,11 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
index < cryptoCurrency.maxNumberOfIndexesToCheck && index < cryptoCurrency.maxNumberOfIndexesToCheck &&
gapCounter < cryptoCurrency.maxUnusedAddressGap; gapCounter < cryptoCurrency.maxUnusedAddressGap;
index += txCountBatchSize) { index += txCountBatchSize) {
List<String> iterationsAddressArray = [];
Logging.instance.log( Logging.instance.log(
"index: $index, \t GapCounter $chain ${type.name}: $gapCounter", "index: $index, \t GapCounter $chain ${type.name}: $gapCounter",
level: LogLevel.Info); level: LogLevel.Info);
final _id = "k_$index"; List<String> txCountCallArgs = [];
Map<String, String> txCountCallArgs = {};
for (int j = 0; j < txCountBatchSize; j++) { for (int j = 0; j < txCountBatchSize; j++) {
final derivePath = cryptoCurrency.constructDerivePath( final derivePath = cryptoCurrency.constructDerivePath(
@ -1007,9 +1003,9 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
addressArray.add(address); addressArray.add(address);
txCountCallArgs.addAll({ txCountCallArgs.add(
"${_id}_$j": addressString, addressString,
}); );
} }
// get address tx counts // get address tx counts
@ -1017,11 +1013,9 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
// check and add appropriate addresses // check and add appropriate addresses
for (int k = 0; k < txCountBatchSize; k++) { for (int k = 0; k < txCountBatchSize; k++) {
int count = (counts["${_id}_$k"] == null) ? 0 : counts["${_id}_$k"]!; final count = counts[k];
if (count > 0) { if (count > 0) {
iterationsAddressArray.add(txCountCallArgs["${_id}_$k"]!);
// update highest // update highest
highestIndexWithHistory = index + k; highestIndexWithHistory = index + k;
@ -1111,23 +1105,20 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
List<Map<String, dynamic>> allTxHashes = []; List<Map<String, dynamic>> allTxHashes = [];
if (serverCanBatch) { if (serverCanBatch) {
final Map<String, Map<String, List<dynamic>>> batches = {}; final Map<int, List<List<dynamic>>> batches = {};
final Map<int, String> requestIdToAddressMap = {}; final Map<int, List<String>> batchIndexToAddressListMap = {};
const batchSizeMax = 100; const batchSizeMax = 100;
int batchNumber = 0; int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) { for (int i = 0; i < allAddresses.length; i++) {
if (batches["$batchNumber"] == null) { batches[batchNumber] ??= [];
batches["$batchNumber"] = {}; batchIndexToAddressListMap[batchNumber] ??= [];
}
final address = allAddresses.elementAt(i);
final scriptHash = cryptoCurrency.addressToScriptHash( final scriptHash = cryptoCurrency.addressToScriptHash(
address: allAddresses.elementAt(i), address: address,
); );
// final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); batches[batchNumber]!.add([scriptHash]);
// TODO [prio=???]: Pass request IDs to electrum_adapter. batchIndexToAddressListMap[batchNumber]!.add(address);
requestIdToAddressMap[i] = allAddresses.elementAt(i);
batches["$batchNumber"]!.addAll({
"$i": [scriptHash]
});
if (i % batchSizeMax == batchSizeMax - 1) { if (i % batchSizeMax == batchSizeMax - 1) {
batchNumber++; batchNumber++;
} }
@ -1135,13 +1126,14 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
for (int i = 0; i < batches.length; i++) { for (int i = 0; i < batches.length; i++) {
final response = final response =
await electrumXClient.getBatchHistory(args: batches["$i"]!); await electrumXClient.getBatchHistory(args: batches[i]!);
for (final entry in response.entries) { for (int j = 0; j < response.length; j++) {
for (int j = 0; j < entry.value.length; j++) { final entry = response[j];
entry.value[j]["address"] = requestIdToAddressMap[entry.key]; for (int k = 0; k < entry.length; k++) {
if (!allTxHashes.contains(entry.value[j])) { entry[k]["address"] = batchIndexToAddressListMap[i]![j];
allTxHashes.add(entry.value[j]); // if (!allTxHashes.contains(entry[j])) {
} allTxHashes.add(entry[k]);
// }
} }
} }
} }
@ -1608,31 +1600,27 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
final fetchedUtxoList = <List<Map<String, dynamic>>>[]; final fetchedUtxoList = <List<Map<String, dynamic>>>[];
if (serverCanBatch) { if (serverCanBatch) {
final Map<int, Map<String, List<dynamic>>> batches = {}; final Map<int, List<List<dynamic>>> batchArgs = {};
const batchSizeMax = 10; const batchSizeMax = 10;
int batchNumber = 0; int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) { for (int i = 0; i < allAddresses.length; i++) {
if (batches[batchNumber] == null) { batchArgs[batchNumber] ??= [];
batches[batchNumber] = {};
}
final scriptHash = cryptoCurrency.addressToScriptHash( final scriptHash = cryptoCurrency.addressToScriptHash(
address: allAddresses[i].value, address: allAddresses[i].value,
); );
batches[batchNumber]!.addAll({ batchArgs[batchNumber]!.add([scriptHash]);
scriptHash: [scriptHash]
});
if (i % batchSizeMax == batchSizeMax - 1) { if (i % batchSizeMax == batchSizeMax - 1) {
batchNumber++; batchNumber++;
} }
} }
for (int i = 0; i < batches.length; i++) { for (int i = 0; i < batchArgs.length; i++) {
final response = final response =
await electrumXClient.getBatchUTXOs(args: batches[i]!); await electrumXClient.getBatchUTXOs(args: batchArgs[i]!);
for (final entry in response.entries) { for (final entry in response) {
if (entry.value.isNotEmpty) { if (entry.isNotEmpty) {
fetchedUtxoList.add(entry.value); fetchedUtxoList.add(entry);
} }
} }
} }