stack_wallet/lib/electrumx_rpc/cached_electrumx_client.dart

342 lines
10 KiB
Dart
Raw Normal View History

2023-05-26 21:21:16 +00:00
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'dart:convert';
import 'dart:math';
2024-05-27 23:56:22 +00:00
import 'package:string_validator/string_validator.dart';
import '../db/hive/db.dart';
import '../utilities/logger.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
2024-05-27 23:56:22 +00:00
import 'electrumx_client.dart';
2022-08-26 08:11:35 +00:00
2023-11-14 20:31:53 +00:00
class CachedElectrumXClient {
final ElectrumXClient electrumXClient;
2022-08-26 08:11:35 +00:00
static const minCacheConfirms = 30;
2024-04-18 23:17:45 +00:00
CachedElectrumXClient({required this.electrumXClient});
2022-08-26 08:11:35 +00:00
2023-11-14 20:31:53 +00:00
factory CachedElectrumXClient.from({
required ElectrumXClient electrumXClient,
2022-08-26 08:11:35 +00:00
}) =>
2023-11-14 20:31:53 +00:00
CachedElectrumXClient(
electrumXClient: electrumXClient,
);
2022-08-26 08:11:35 +00:00
Future<Map<String, dynamic>> getAnonymitySet({
required String groupId,
String blockhash = "",
2024-05-15 21:20:45 +00:00
required CryptoCurrency cryptoCurrency,
2022-08-26 08:11:35 +00:00
}) async {
try {
2024-05-15 21:20:45 +00:00
final box =
await DB.instance.getAnonymitySetCacheBox(currency: cryptoCurrency);
final cachedSet = box.get(groupId) as Map?;
2022-08-26 08:11:35 +00:00
Map<String, dynamic> set;
// null check to see if there is a cached set
if (cachedSet == null) {
set = {
"setId": groupId,
"blockHash": blockhash,
"setHash": "",
"coins": <dynamic>[],
};
} else {
set = Map<String, dynamic>.from(cachedSet);
}
2024-04-18 23:17:45 +00:00
final newSet = await electrumXClient.getLelantusAnonymitySet(
2022-08-26 08:11:35 +00:00
groupId: groupId,
2024-04-18 23:17:45 +00:00
blockhash: set["blockHash"] as String,
2022-08-26 08:11:35 +00:00
);
// update set with new data
if (newSet["setHash"] != "" && set["setHash"] != newSet["setHash"]) {
set["setHash"] = !isHexadecimal(newSet["setHash"] as String)
2022-12-14 15:05:47 +00:00
? base64ToHex(newSet["setHash"] as String)
: newSet["setHash"];
set["blockHash"] = !isHexadecimal(newSet["blockHash"] as String)
2022-12-14 15:05:47 +00:00
? base64ToReverseHex(newSet["blockHash"] as String)
: newSet["blockHash"];
2022-08-26 08:11:35 +00:00
for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) {
2024-05-15 21:20:45 +00:00
final dynamic newCoin = newSet["coins"][i];
final List<dynamic> translatedCoin = [];
translatedCoin.add(
!isHexadecimal(newCoin[0] as String)
? base64ToHex(newCoin[0] as String)
: newCoin[0],
);
translatedCoin.add(
!isHexadecimal(newCoin[1] as String)
? base64ToReverseHex(newCoin[1] as String)
: newCoin[1],
);
try {
2024-05-15 21:20:45 +00:00
translatedCoin.add(
!isHexadecimal(newCoin[2] as String)
? base64ToHex(newCoin[2] as String)
: newCoin[2],
);
} catch (e) {
translatedCoin.add(newCoin[2]);
}
2024-05-15 21:20:45 +00:00
translatedCoin.add(
!isHexadecimal(newCoin[3] as String)
? base64ToReverseHex(newCoin[3] as String)
: newCoin[3],
);
set["coins"].insert(0, translatedCoin);
2022-08-26 08:11:35 +00:00
}
// save set to db
await box.put(groupId, set);
2022-08-26 08:11:35 +00:00
Logging.instance.log(
2024-05-15 21:20:45 +00:00
"Updated current anonymity set for ${cryptoCurrency.identifier} with group ID $groupId",
2023-05-16 17:04:45 +00:00
level: LogLevel.Info,
);
2022-08-26 08:11:35 +00:00
}
return set;
} catch (e, s) {
Logging.instance.log(
2024-05-15 21:20:45 +00:00
"Failed to process CachedElectrumX.getAnonymitySet(): $e\n$s",
level: LogLevel.Error,
);
2022-08-26 08:11:35 +00:00
rethrow;
}
}
Future<Map<String, dynamic>> getSparkAnonymitySet({
required String groupId,
String blockhash = "",
2024-05-15 21:20:45 +00:00
required CryptoCurrency cryptoCurrency,
2024-05-10 20:32:15 +00:00
required bool useOnlyCacheIfNotEmpty,
}) async {
try {
2024-05-15 21:20:45 +00:00
final box = await DB.instance.getSparkAnonymitySetCacheBox(
currency: cryptoCurrency,
);
final cachedSet = box.get(groupId) as Map?;
Map<String, dynamic> set;
// null check to see if there is a cached set
if (cachedSet == null) {
set = {
"coinGroupID": int.parse(groupId),
"blockHash": blockhash,
"setHash": "",
"coins": <dynamic>[],
};
} else {
set = Map<String, dynamic>.from(cachedSet);
2024-05-10 20:32:15 +00:00
if (useOnlyCacheIfNotEmpty) {
return set;
}
}
2024-04-18 23:17:45 +00:00
final newSet = await electrumXClient.getSparkAnonymitySet(
coinGroupId: groupId,
startBlockHash: set["blockHash"] as String,
);
// update set with new data
if (newSet["setHash"] != "" && set["setHash"] != newSet["setHash"]) {
set["setHash"] = newSet["setHash"];
set["blockHash"] = newSet["blockHash"];
for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) {
// TODO verify this is correct (or append?)
2023-12-21 20:41:29 +00:00
if ((set["coins"] as List)
.where((e) => e[0] == newSet["coins"][i][0])
.isEmpty) {
set["coins"].insert(0, newSet["coins"][i]);
}
}
// save set to db
await box.put(groupId, set);
Logging.instance.log(
2024-05-15 21:20:45 +00:00
"Updated current anonymity set for ${cryptoCurrency.identifier} with group ID $groupId",
level: LogLevel.Info,
);
}
return set;
} catch (e, s) {
Logging.instance.log(
2024-05-15 21:20:45 +00:00
"Failed to process CachedElectrumX.getSparkAnonymitySet(): $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
String base64ToHex(String source) =>
base64Decode(LineSplitter.split(source).join())
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();
String base64ToReverseHex(String source) =>
base64Decode(LineSplitter.split(source).join())
.reversed
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();
2022-08-26 08:11:35 +00:00
/// Call electrumx getTransaction on a per coin basis, storing the result in local db if not already there.
///
/// ElectrumX api only called if the tx does not exist in local db
Future<Map<String, dynamic>> getTransaction({
required String txHash,
2024-05-15 21:20:45 +00:00
required CryptoCurrency cryptoCurrency,
2022-08-26 08:11:35 +00:00
bool verbose = true,
}) async {
try {
2024-05-15 21:20:45 +00:00
final box = await DB.instance.getTxCacheBox(currency: cryptoCurrency);
final cachedTx = box.get(txHash) as Map?;
2022-08-26 08:11:35 +00:00
if (cachedTx == null) {
final Map<String, dynamic> result =
2024-04-18 23:17:45 +00:00
await electrumXClient.getTransaction(
txHash: txHash,
verbose: verbose,
);
2022-08-26 08:11:35 +00:00
result.remove("hex");
result.remove("lelantusData");
result.remove("sparkData");
2022-08-26 08:11:35 +00:00
if (result["confirmations"] != null &&
result["confirmations"] as int > minCacheConfirms) {
await box.put(txHash, result);
2022-08-26 08:11:35 +00:00
}
2023-09-28 19:23:45 +00:00
// Logging.instance.log("using fetched result", level: LogLevel.Info);
2022-08-26 08:11:35 +00:00
return result;
} else {
2023-09-28 19:23:45 +00:00
// Logging.instance.log("using cached result", level: LogLevel.Info);
2022-08-26 08:11:35 +00:00
return Map<String, dynamic>.from(cachedTx);
}
} catch (e, s) {
Logging.instance.log(
2024-05-15 21:20:45 +00:00
"Failed to process CachedElectrumX.getTransaction(): $e\n$s",
level: LogLevel.Error,
);
2022-08-26 08:11:35 +00:00
rethrow;
}
}
2023-05-16 19:29:48 +00:00
Future<List<String>> getUsedCoinSerials({
2024-05-15 21:20:45 +00:00
required CryptoCurrency cryptoCurrency,
int startNumber = 0,
2022-08-26 08:11:35 +00:00
}) async {
try {
2024-05-15 21:20:45 +00:00
final box =
await DB.instance.getUsedSerialsCacheBox(currency: cryptoCurrency);
final _list = box.get("serials") as List?;
2022-08-26 08:11:35 +00:00
2024-05-15 21:20:45 +00:00
final Set<String> cachedSerials =
_list == null ? {} : List<String>.from(_list).toSet();
2022-08-26 08:11:35 +00:00
startNumber = max(
max(0, startNumber),
cachedSerials.length - 100, // 100 being some arbitrary buffer
);
2022-08-26 08:11:35 +00:00
2024-04-18 23:17:45 +00:00
final serials = await electrumXClient.getLelantusUsedCoinSerials(
startNumber: startNumber,
);
2023-05-16 19:29:48 +00:00
final newSerials = List<String>.from(serials["serials"] as List)
.map((e) => !isHexadecimal(e) ? base64ToHex(e) : e)
.toSet();
// ensure we are getting some overlap so we know we are not missing any
if (cachedSerials.isNotEmpty && newSerials.isNotEmpty) {
2023-11-27 21:06:37 +00:00
assert(cachedSerials.intersection(newSerials).isNotEmpty);
}
cachedSerials.addAll(newSerials);
2022-08-26 08:11:35 +00:00
final resultingList = cachedSerials.toList();
await box.put(
"serials",
resultingList,
2023-05-16 19:29:48 +00:00
);
2022-08-26 08:11:35 +00:00
return resultingList;
2022-08-26 08:11:35 +00:00
} catch (e, s) {
Logging.instance.log(
"Failed to process CachedElectrumX.getUsedCoinSerials(): $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
Future<Set<String>> getSparkUsedCoinsTags({
2024-05-15 21:20:45 +00:00
required CryptoCurrency cryptoCurrency,
}) async {
try {
2024-05-15 21:20:45 +00:00
final box = await DB.instance.getSparkUsedCoinsTagsCacheBox(
currency: cryptoCurrency,
);
final _list = box.get("tags") as List?;
2024-05-15 21:20:45 +00:00
final Set<String> cachedTags =
_list == null ? {} : List<String>.from(_list).toSet();
final startNumber = max(
0,
cachedTags.length - 100, // 100 being some arbitrary buffer
);
2024-04-18 23:17:45 +00:00
final newTags = await electrumXClient.getSparkUsedCoinsTags(
startNumber: startNumber,
);
// ensure we are getting some overlap so we know we are not missing any
2024-04-18 23:17:45 +00:00
if (cachedTags.isNotEmpty && newTags.isNotEmpty) {
assert(cachedTags.intersection(newTags).isNotEmpty);
}
// Make newTags an Iterable<String>.
final Iterable<String> iterableTags = newTags.map((e) => e.toString());
cachedTags.addAll(iterableTags);
await box.put(
"tags",
cachedTags.toList(),
);
return cachedTags;
} catch (e, s) {
Logging.instance.log(
"Failed to process CachedElectrumX.getSparkUsedCoinsTags(): $e\n$s",
level: LogLevel.Error,
);
2022-08-26 08:11:35 +00:00
rethrow;
}
}
/// Clear all cached transactions for the specified coin
2024-05-27 23:56:22 +00:00
Future<void> clearSharedTransactionCache({
required CryptoCurrency cryptoCurrency,
}) async {
2024-05-15 21:20:45 +00:00
await DB.instance.clearSharedTransactionCache(currency: cryptoCurrency);
await DB.instance.closeAnonymitySetCacheBox(currency: cryptoCurrency);
2022-08-26 08:11:35 +00:00
}
}