centralized electrum client management

This commit is contained in:
julian 2024-04-18 17:17:45 -06:00
parent a2f74ced8a
commit 622740a8c0
14 changed files with 327 additions and 901 deletions

View file

@ -33,6 +33,8 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:tuple/tuple.dart';
class DbVersionMigrator with WalletDB {
@ -85,7 +87,7 @@ class DbVersionMigrator with WalletDB {
useSSL: node.useSSL),
prefs: prefs,
failovers: failovers,
coin: Coin.firo,
cryptoCurrency: Firo(CryptoCurrencyNetwork.main),
);
try {

View file

@ -11,9 +11,6 @@
import 'dart:convert';
import 'dart:math';
import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter;
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:electrum_adapter/methods/specific/firo.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -22,41 +19,18 @@ import 'package:string_validator/string_validator.dart';
class CachedElectrumXClient {
final ElectrumXClient electrumXClient;
ElectrumClient electrumAdapterClient;
final Future<ElectrumClient> Function() electrumAdapterUpdateCallback;
static const minCacheConfirms = 30;
CachedElectrumXClient({
required this.electrumXClient,
required this.electrumAdapterClient,
required this.electrumAdapterUpdateCallback,
});
CachedElectrumXClient({required this.electrumXClient});
factory CachedElectrumXClient.from({
required ElectrumXClient electrumXClient,
required ElectrumClient electrumAdapterClient,
required Future<ElectrumClient> Function() electrumAdapterUpdateCallback,
}) =>
CachedElectrumXClient(
electrumXClient: electrumXClient,
electrumAdapterClient: electrumAdapterClient,
electrumAdapterUpdateCallback: electrumAdapterUpdateCallback,
);
/// If the client is closed, use the callback to update it.
_checkElectrumAdapterClient() async {
if (electrumAdapterClient.peer.isClosed) {
Logging.instance.log(
"ElectrumAdapterClient is closed, reopening it...",
level: LogLevel.Info,
);
ElectrumClient? _electrumAdapterClient =
await electrumAdapterUpdateCallback.call();
electrumAdapterClient = _electrumAdapterClient;
}
}
Future<Map<String, dynamic>> getAnonymitySet({
required String groupId,
String blockhash = "",
@ -80,12 +54,9 @@ class CachedElectrumXClient {
set = Map<String, dynamic>.from(cachedSet);
}
await _checkElectrumAdapterClient();
final newSet = await (electrumAdapterClient as FiroElectrumClient)
.getLelantusAnonymitySet(
final newSet = await electrumXClient.getLelantusAnonymitySet(
groupId: groupId,
blockHash: set["blockHash"] as String,
blockhash: set["blockHash"] as String,
);
// update set with new data
@ -157,10 +128,7 @@ class CachedElectrumXClient {
set = Map<String, dynamic>.from(cachedSet);
}
await _checkElectrumAdapterClient();
final newSet = await (electrumAdapterClient as FiroElectrumClient)
.getSparkAnonymitySet(
final newSet = await electrumXClient.getSparkAnonymitySet(
coinGroupId: groupId,
startBlockHash: set["blockHash"] as String,
);
@ -218,10 +186,11 @@ class CachedElectrumXClient {
final cachedTx = box.get(txHash) as Map?;
if (cachedTx == null) {
await _checkElectrumAdapterClient();
final Map<String, dynamic> result =
await electrumAdapterClient.getTransaction(txHash);
await electrumXClient.getTransaction(
txHash: txHash,
verbose: verbose,
);
result.remove("hex");
result.remove("lelantusData");
@ -263,10 +232,7 @@ class CachedElectrumXClient {
cachedSerials.length - 100, // 100 being some arbitrary buffer
);
await _checkElectrumAdapterClient();
final serials = await (electrumAdapterClient as FiroElectrumClient)
.getLelantusUsedCoinSerials(
final serials = await electrumXClient.getLelantusUsedCoinSerials(
startNumber: startNumber,
);
@ -314,22 +280,12 @@ class CachedElectrumXClient {
cachedTags.length - 100, // 100 being some arbitrary buffer
);
await _checkElectrumAdapterClient();
final tags =
await (electrumAdapterClient as FiroElectrumClient).getUsedCoinsTags(
final newTags = await electrumXClient.getSparkUsedCoinsTags(
startNumber: startNumber,
);
// final newSerials = List<String>.from(serials["serials"] as List)
// .map((e) => !isHexadecimal(e) ? base64ToHex(e) : e)
// .toSet();
// Convert the Map<String, dynamic> tags to a Set<Object?>.
final newTags = (tags["tags"] as List).toSet();
// ensure we are getting some overlap so we know we are not missing any
if (cachedTags.isNotEmpty && tags.isNotEmpty) {
if (cachedTags.isNotEmpty && newTags.isNotEmpty) {
assert(cachedTags.intersection(newTags).isNotEmpty);
}

View file

@ -0,0 +1,96 @@
import 'dart:async';
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
class ClientManager {
ClientManager._();
static final ClientManager sharedInstance = ClientManager._();
final Map<String, ElectrumClient> _map = {};
final Map<String, int> _heights = {};
final Map<String, StreamSubscription<BlockHeader>> _subscriptions = {};
final Map<String, Completer<int>> _heightCompleters = {};
String _keyHelper(CryptoCurrency cryptoCurrency) {
return "${cryptoCurrency.runtimeType}_${cryptoCurrency.network.name}";
}
final Finalizer<ClientManager> _finalizer = Finalizer((manager) async {
await manager._kill();
});
ElectrumClient? getClient({
required CryptoCurrency cryptoCurrency,
}) =>
_map[_keyHelper(cryptoCurrency)];
void addClient(
ElectrumClient client, {
required CryptoCurrency cryptoCurrency,
}) {
final key = _keyHelper(cryptoCurrency);
if (_map[key] != null) {
throw Exception("ElectrumX Client for $key already exists.");
} else {
_map[key] = client;
}
_heightCompleters[key] = Completer<int>();
_subscriptions[key] = client.subscribeHeaders().listen((event) {
_heights[key] = event.height;
if (!_heightCompleters[key]!.isCompleted) {
_heightCompleters[key]!.complete(event.height);
}
});
}
Future<int> getChainHeightFor(CryptoCurrency cryptoCurrency) async {
final key = _keyHelper(cryptoCurrency);
if (_map[key] == null) {
throw Exception(
"No managed ElectrumClient for $cryptoCurrency found.",
);
}
if (_heightCompleters[key] == null) {
throw Exception(
"No managed _heightCompleters for $cryptoCurrency found.",
);
}
return _heights[key] ?? await _heightCompleters[key]!.future;
}
Future<ElectrumClient?> remove({
required CryptoCurrency cryptoCurrency,
}) async {
final key = _keyHelper(cryptoCurrency);
await _subscriptions[key]?.cancel();
_subscriptions.remove(key);
_heights.remove(key);
_heightCompleters.remove(key);
return _map.remove(key);
}
Future<void> closeAll() async {
await _kill();
_finalizer.detach(this);
}
Future<void> _kill() async {
for (final sub in _subscriptions.values) {
await sub.cancel();
}
for (final client in _map.values) {
await client.close();
}
_heightCompleters.clear();
_heights.clear();
_subscriptions.clear();
_map.clear();
}
}

View file

@ -1,149 +0,0 @@
import 'dart:async';
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
/// Manage chain height subscriptions for each coin.
abstract class ChainHeightServiceManager {
// A map of chain height services for each coin.
static final Map<Coin, ChainHeightService> _services = {};
// Map<Coin, ChainHeightService> get services => _services;
// Get the chain height service for a specific coin.
static ChainHeightService? getService(Coin coin) {
return _services[coin];
}
// Add a chain height service for a specific coin.
static void add(ChainHeightService service, Coin coin) {
// Don't add a new service if one already exists.
if (_services[coin] == null) {
_services[coin] = service;
} else {
throw Exception("Chain height service for $coin already managed");
}
}
// Remove a chain height service for a specific coin.
static void remove(Coin coin) {
_services.remove(coin);
}
// Close all subscriptions and clean up resources.
static Future<void> dispose() async {
// Close each subscription.
//
// Create a list of keys to avoid concurrent modification during iteration
var keys = List<Coin>.from(_services.keys);
// Iterate over the copy of the keys
for (final coin in keys) {
final ChainHeightService? service = getService(coin);
await service?.cancelListen();
remove(coin);
}
}
}
/// A service to fetch and listen for chain height updates.
///
/// TODO: Add error handling and branching to handle various other scenarios.
class ChainHeightService {
// The electrum_adapter client to use for fetching chain height updates.
ElectrumClient client;
// The subscription to listen for chain height updates.
StreamSubscription<dynamic>? _subscription;
// Whether the service has started listening for updates.
bool get started => _subscription != null;
// The current chain height.
int? _height;
int? get height => _height;
// Whether the service is currently reconnecting.
bool _isReconnecting = false;
// The reconnect timer.
Timer? _reconnectTimer;
// The reconnection timeout duration.
static const Duration _connectionTimeout = Duration(seconds: 10);
ChainHeightService({required this.client});
/// Fetch the current chain height and start listening for updates.
Future<int> fetchHeightAndStartListenForUpdates() async {
// Don't start a new subscription if one already exists.
if (_subscription != null) {
throw Exception(
"Attempted to start a chain height service where an existing"
" subscription already exists!",
);
}
// A completer to wait for the current chain height to be fetched.
final completer = Completer<int>();
// Fetch the current chain height.
_subscription = client.subscribeHeaders().listen((BlockHeader event) {
_height = event.height;
if (!completer.isCompleted) {
completer.complete(_height);
}
});
_subscription?.onError((dynamic error) {
_handleError(error);
});
// Wait for the current chain height to be fetched.
return completer.future;
}
/// Handle an error from the subscription.
void _handleError(dynamic error) {
Logging.instance.log(
"Error reconnecting for chain height: ${error.toString()}",
level: LogLevel.Error,
);
_subscription?.cancel();
_subscription = null;
_attemptReconnect();
}
/// Attempt to reconnect to the electrum server.
void _attemptReconnect() {
// Avoid multiple reconnection attempts.
if (_isReconnecting) return;
_isReconnecting = true;
// Attempt to reconnect.
unawaited(fetchHeightAndStartListenForUpdates().then((_) {
_isReconnecting = false;
}));
// Set a timer to on the reconnection attempt and clean up if it fails.
_reconnectTimer?.cancel();
_reconnectTimer = Timer(_connectionTimeout, () async {
if (_subscription == null) {
await _subscription?.cancel();
_subscription = null; // Will also occur on an error via handleError.
_reconnectTimer?.cancel();
_reconnectTimer = null;
_isReconnecting = false;
}
});
}
/// Stop listening for chain height updates.
Future<void> cancelListen() async {
await _subscription?.cancel();
_subscription = null;
_reconnectTimer?.cancel();
}
}

View file

@ -20,16 +20,17 @@ import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
import 'package:mutex/mutex.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart';
import 'package:stackwallet/electrumx_rpc/rpc.dart';
import 'package:stackwallet/electrumx_rpc/client_manager.dart';
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/dogecoin.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stream_channel/stream_channel.dart';
class WifiOnlyException implements Exception {}
@ -65,6 +66,8 @@ class ElectrumXNode {
}
class ElectrumXClient {
final CryptoCurrency cryptoCurrency;
String get host => _host;
late String _host;
@ -74,14 +77,13 @@ class ElectrumXClient {
bool get useSSL => _useSSL;
late bool _useSSL;
JsonRPC? get rpcClient => _rpcClient;
JsonRPC? _rpcClient;
StreamChannel<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
// StreamChannel<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
StreamChannel<dynamic>? _electrumAdapterChannel;
ElectrumClient? get electrumAdapterClient => _electrumAdapterClient;
ElectrumClient? _electrumAdapterClient;
ElectrumClient? getElectrumAdapter() =>
ClientManager.sharedInstance.getClient(
cryptoCurrency: cryptoCurrency,
);
late Prefs _prefs;
late TorService _torService;
@ -91,9 +93,6 @@ class ElectrumXClient {
final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
Coin? get coin => _coin;
late Coin? _coin;
// add finalizer to cancel stream subscription when all references to an
// instance of ElectrumX becomes inaccessible
static final Finalizer<ElectrumXClient> _finalizer = Finalizer(
@ -114,7 +113,7 @@ class ElectrumXClient {
required bool useSSL,
required Prefs prefs,
required List<ElectrumXNode> failovers,
Coin? coin,
required this.cryptoCurrency,
this.connectionTimeoutForSpecialCaseJsonRPCClients =
const Duration(seconds: 60),
TorService? torService,
@ -125,7 +124,6 @@ class ElectrumXClient {
_host = host;
_port = port;
_useSSL = useSSL;
_coin = coin;
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
@ -161,10 +159,12 @@ class ElectrumXClient {
// setting to null should force the creation of a new json rpc client
// on the next request sent through this electrumx instance
_electrumAdapterChannel = null;
_electrumAdapterClient = null;
await (await ClientManager.sharedInstance
.remove(cryptoCurrency: cryptoCurrency))
?.close();
// Also close any chain height services that are currently open.
await ChainHeightServiceManager.dispose();
// await ChainHeightServiceManager.dispose();
},
);
}
@ -173,7 +173,7 @@ class ElectrumXClient {
required ElectrumXNode node,
required Prefs prefs,
required List<ElectrumXNode> failovers,
required Coin coin,
required CryptoCurrency cryptoCurrency,
TorService? torService,
EventBus? globalEventBusForTesting,
}) {
@ -185,7 +185,7 @@ class ElectrumXClient {
torService: torService,
failovers: failovers,
globalEventBusForTesting: globalEventBusForTesting,
coin: coin,
cryptoCurrency: cryptoCurrency,
);
}
@ -197,7 +197,11 @@ class ElectrumXClient {
return true;
}
Future<void> checkElectrumAdapter() async {
Future<void> closeAdapter() async {
await getElectrumAdapter()?.close();
}
Future<void> _checkElectrumAdapter() async {
({InternetAddress host, int port})? proxyInfo;
// If we're supposed to use Tor...
@ -223,75 +227,60 @@ class ElectrumXClient {
}
}
// TODO [prio=med]: Add proxyInfo to StreamChannel (or add to wrapper).
// if (_electrumAdapter!.proxyInfo != proxyInfo) {
// _electrumAdapter!.proxyInfo = proxyInfo;
// _electrumAdapter!.disconnect(
// reason: "Tor proxyInfo does not match current info",
// );
// }
// If the current ElectrumAdapterClient is closed, create a new one.
if (_electrumAdapterClient != null &&
_electrumAdapterClient!.peer.isClosed) {
if (getElectrumAdapter() != null && getElectrumAdapter()!.peer.isClosed) {
_electrumAdapterChannel = null;
_electrumAdapterClient = null;
ClientManager.sharedInstance.remove(cryptoCurrency: cryptoCurrency);
}
final String useHost;
final int usePort;
final bool useUseSSL;
if (currentFailoverIndex == -1) {
_electrumAdapterChannel ??= await electrum_adapter.connect(
host,
port: port,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
acceptUnverified: true,
useSSL: useSSL,
proxyInfo: proxyInfo,
);
if (_coin == Coin.firo || _coin == Coin.firoTestNet) {
_electrumAdapterClient ??= FiroElectrumClient(
_electrumAdapterChannel!,
host,
port,
useSSL,
proxyInfo,
);
} else {
_electrumAdapterClient ??= ElectrumClient(
_electrumAdapterChannel!,
host,
port,
useSSL,
proxyInfo,
);
}
useHost = host;
usePort = port;
useUseSSL = useSSL;
} else {
_electrumAdapterChannel ??= await electrum_adapter.connect(
failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
acceptUnverified: true,
useSSL: failovers![currentFailoverIndex].useSSL,
proxyInfo: proxyInfo,
);
if (_coin == Coin.firo || _coin == Coin.firoTestNet) {
_electrumAdapterClient ??= FiroElectrumClient(
useHost = failovers![currentFailoverIndex].address;
usePort = failovers![currentFailoverIndex].port;
useUseSSL = failovers![currentFailoverIndex].useSSL;
}
_electrumAdapterChannel ??= await electrum_adapter.connect(
useHost,
port: usePort,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
acceptUnverified: true,
useSSL: useUseSSL,
proxyInfo: proxyInfo,
);
if (getElectrumAdapter() == null) {
final ElectrumClient newClient;
if (cryptoCurrency is Firo) {
newClient = FiroElectrumClient(
_electrumAdapterChannel!,
failovers![currentFailoverIndex].address,
failovers![currentFailoverIndex].port,
failovers![currentFailoverIndex].useSSL,
useHost,
usePort,
useUseSSL,
proxyInfo,
);
} else {
_electrumAdapterClient ??= ElectrumClient(
newClient = ElectrumClient(
_electrumAdapterChannel!,
failovers![currentFailoverIndex].address,
failovers![currentFailoverIndex].port,
failovers![currentFailoverIndex].useSSL,
useHost,
usePort,
useUseSSL,
proxyInfo,
);
}
ClientManager.sharedInstance.addClient(
newClient,
cryptoCurrency: cryptoCurrency,
);
}
return;
@ -311,13 +300,13 @@ class ElectrumXClient {
if (_requireMutex) {
await _torConnectingLock
.protect(() async => await checkElectrumAdapter());
.protect(() async => await _checkElectrumAdapter());
} else {
await checkElectrumAdapter();
await _checkElectrumAdapter();
}
try {
final response = await _electrumAdapterClient!.request(
final response = await getElectrumAdapter()!.request(
command,
args,
);
@ -397,16 +386,16 @@ class ElectrumXClient {
if (_requireMutex) {
await _torConnectingLock
.protect(() async => await checkElectrumAdapter());
.protect(() async => await _checkElectrumAdapter());
} else {
await checkElectrumAdapter();
await _checkElectrumAdapter();
}
try {
var futures = <Future<dynamic>>[];
_electrumAdapterClient!.peer.withBatch(() {
getElectrumAdapter()!.peer.withBatch(() {
for (final arg in args) {
futures.add(_electrumAdapterClient!.request(command, arg));
futures.add(getElectrumAdapter()!.request(command, arg));
}
});
final response = await Future.wait(futures);
@ -778,8 +767,8 @@ class ElectrumXClient {
}) async {
Logging.instance.log("attempting to fetch blockchain.transaction.get...",
level: LogLevel.Info);
await checkElectrumAdapter();
dynamic response = await _electrumAdapterClient!.getTransaction(txHash);
await _checkElectrumAdapter();
dynamic response = await getElectrumAdapter()!.getTransaction(txHash);
Logging.instance.log("Fetching blockchain.transaction.get finished",
level: LogLevel.Info);
@ -811,9 +800,9 @@ class ElectrumXClient {
}) async {
Logging.instance.log("attempting to fetch lelantus.getanonymityset...",
level: LogLevel.Info);
await checkElectrumAdapter();
await _checkElectrumAdapter();
Map<String, dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)!
await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash);
Logging.instance.log("Fetching lelantus.getanonymityset finished",
level: LogLevel.Info);
@ -830,8 +819,8 @@ class ElectrumXClient {
}) async {
Logging.instance.log("attempting to fetch lelantus.getmintmetadata...",
level: LogLevel.Info);
await checkElectrumAdapter();
dynamic response = await (_electrumAdapterClient as FiroElectrumClient)!
await _checkElectrumAdapter();
dynamic response = await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusMintData(mints: mints);
Logging.instance.log("Fetching lelantus.getmintmetadata finished",
level: LogLevel.Info);
@ -846,13 +835,13 @@ class ElectrumXClient {
}) async {
Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...",
level: LogLevel.Info);
await checkElectrumAdapter();
await _checkElectrumAdapter();
int retryCount = 3;
dynamic response;
while (retryCount > 0 && response is! List) {
response = await (_electrumAdapterClient as FiroElectrumClient)!
response = await (getElectrumAdapter() as FiroElectrumClient)
.getLelantusUsedCoinSerials(startNumber: startNumber);
// TODO add 2 minute timeout.
Logging.instance.log("Fetching lelantus.getusedcoinserials finished",
@ -870,9 +859,9 @@ class ElectrumXClient {
Future<int> getLelantusLatestCoinId({String? requestID}) async {
Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...",
level: LogLevel.Info);
await checkElectrumAdapter();
await _checkElectrumAdapter();
int response =
await (_electrumAdapterClient as FiroElectrumClient).getLatestCoinId();
await (getElectrumAdapter() as FiroElectrumClient).getLatestCoinId();
Logging.instance.log("Fetching lelantus.getlatestcoinid finished",
level: LogLevel.Info);
return response;
@ -901,9 +890,9 @@ class ElectrumXClient {
try {
Logging.instance.log("attempting to fetch spark.getsparkanonymityset...",
level: LogLevel.Info);
await checkElectrumAdapter();
await _checkElectrumAdapter();
Map<String, dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)
await (getElectrumAdapter() as FiroElectrumClient)
.getSparkAnonymitySet(
coinGroupId: coinGroupId, startBlockHash: startBlockHash);
Logging.instance.log("Fetching spark.getsparkanonymityset finished",
@ -924,11 +913,12 @@ class ElectrumXClient {
// Use electrum_adapter package's getSparkUsedCoinsTags method.
Logging.instance.log("attempting to fetch spark.getusedcoinstags...",
level: LogLevel.Info);
await checkElectrumAdapter();
await _checkElectrumAdapter();
Map<String, dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)
await (getElectrumAdapter() as FiroElectrumClient)
.getUsedCoinsTags(startNumber: startNumber);
// TODO: Add 2 minute timeout.
// Why 2 minutes?
Logging.instance.log("Fetching spark.getusedcoinstags finished",
level: LogLevel.Info);
final map = Map<String, dynamic>.from(response);
@ -957,9 +947,9 @@ class ElectrumXClient {
try {
Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...",
level: LogLevel.Info);
await checkElectrumAdapter();
await _checkElectrumAdapter();
List<dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)
await (getElectrumAdapter() as FiroElectrumClient)
.getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
Logging.instance.log("Fetching spark.getsparkmintmetadata finished",
level: LogLevel.Info);
@ -979,8 +969,8 @@ class ElectrumXClient {
try {
Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...",
level: LogLevel.Info);
await checkElectrumAdapter();
int response = await (_electrumAdapterClient as FiroElectrumClient)
await _checkElectrumAdapter();
int response = await (getElectrumAdapter() as FiroElectrumClient)
.getSparkLatestCoinId();
Logging.instance.log("Fetching spark.getsparklatestcoinid finished",
level: LogLevel.Info);
@ -1001,8 +991,8 @@ class ElectrumXClient {
/// "rate": 1000,
/// }
Future<Map<String, dynamic>> getFeeRate({String? requestID}) async {
await checkElectrumAdapter();
return await _electrumAdapterClient!.getFeeRate();
await _checkElectrumAdapter();
return await getElectrumAdapter()!.getFeeRate();
}
/// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of [blocks].
@ -1022,7 +1012,7 @@ class ElectrumXClient {
try {
// If the response is -1 or null, return a temporary hardcoded value for
// Dogecoin. This is a temporary fix until the fee estimation is fixed.
if (coin == Coin.dogecoin &&
if (cryptoCurrency is Dogecoin &&
(response == null ||
response == -1 ||
Decimal.parse(response.toString()) == Decimal.parse("-1"))) {
@ -1035,7 +1025,7 @@ class ElectrumXClient {
return Decimal.parse(response.toString());
} catch (e, s) {
final String msg = "Error parsing fee rate. Response: $response"
"\nResult: ${response}\nError: $e\nStack trace: $s";
"\nResult: $response\nError: $e\nStack trace: $s";
Logging.instance.log(msg, level: LogLevel.Fatal);
throw Exception(msg);
}

View file

@ -1,413 +0,0 @@
/*
* 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:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:mutex/mutex.dart';
import 'package:stackwallet/exceptions/json_rpc/json_rpc_exception.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:tor_ffi_plugin/socks_socket.dart';
// Json RPC class to handle connecting to electrumx servers
class JsonRPC {
JsonRPC({
required this.host,
required this.port,
this.useSSL = false,
this.connectionTimeout = const Duration(seconds: 60),
required ({InternetAddress host, int port})? proxyInfo,
});
final bool useSSL;
final String host;
final int port;
final Duration connectionTimeout;
({InternetAddress host, int port})? proxyInfo;
final _requestMutex = Mutex();
final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue();
Socket? _socket;
SOCKSSocket? _socksSocket;
StreamSubscription<List<int>>? _subscription;
void _dataHandler(List<int> data) {
_requestQueue.nextIncompleteReq.then((req) {
if (req != null) {
req.appendDataAndCheckIfComplete(data);
if (req.isComplete) {
_onReqCompleted(req);
}
} else {
Logging.instance.log(
"_dataHandler found a null req!",
level: LogLevel.Warning,
);
}
});
}
void _errorHandler(Object error, StackTrace trace) {
_requestQueue.nextIncompleteReq.then((req) {
if (req != null) {
req.completer.completeError(error, trace);
_onReqCompleted(req);
}
});
}
void _doneHandler() {
disconnect(reason: "JsonRPC _doneHandler() called");
}
void _onReqCompleted(_JsonRPCRequest req) {
_requestQueue.remove(req).then((_) {
// attempt to send next request
_sendNextAvailableRequest();
});
}
void _sendNextAvailableRequest() {
_requestQueue.nextIncompleteReq.then((req) {
if (req != null) {
if (!Prefs.instance.useTor) {
if (_socket == null) {
Logging.instance.log(
"JsonRPC _sendNextAvailableRequest attempted with"
" _socket=null on $host:$port",
level: LogLevel.Error,
);
}
// \r\n required by electrumx server
_socket!.write('${req.jsonRequest}\r\n');
} else {
if (_socksSocket == null) {
Logging.instance.log(
"JsonRPC _sendNextAvailableRequest attempted with"
" _socksSocket=null on $host:$port",
level: LogLevel.Error,
);
}
// \r\n required by electrumx server
_socksSocket?.write('${req.jsonRequest}\r\n');
}
// TODO different timeout length?
req.initiateTimeout(
onTimedOut: () {
_onReqCompleted(req);
},
);
}
});
}
Future<JsonRPCResponse> request(
String jsonRpcRequest,
Duration requestTimeout,
) async {
await _requestMutex.protect(() async {
if (!Prefs.instance.useTor) {
if (_socket == null) {
Logging.instance.log(
"JsonRPC request: opening socket $host:$port",
level: LogLevel.Info,
);
await _connect().timeout(requestTimeout, onTimeout: () {
throw Exception("Request timeout: $jsonRpcRequest");
});
}
} else {
if (_socksSocket == null) {
Logging.instance.log(
"JsonRPC request: opening SOCKS socket to $host:$port",
level: LogLevel.Info,
);
await _connect().timeout(requestTimeout, onTimeout: () {
throw Exception("Request timeout: $jsonRpcRequest");
});
}
}
});
final req = _JsonRPCRequest(
jsonRequest: jsonRpcRequest,
requestTimeout: requestTimeout,
completer: Completer<JsonRPCResponse>(),
);
final future = req.completer.future.onError(
(error, stackTrace) async {
await disconnect(
reason: "return req.completer.future.onError: $error\n$stackTrace",
);
return JsonRPCResponse(
exception: error is JsonRpcException
? error
: JsonRpcException(
"req.completer.future.onError: $error\n$stackTrace",
),
);
},
);
// if this is the only/first request then send it right away
await _requestQueue.add(
req,
onInitialRequestAdded: _sendNextAvailableRequest,
);
return future;
}
/// DO NOT set [ignoreMutex] to true unless fully aware of the consequences
Future<void> disconnect({
required String reason,
bool ignoreMutex = false,
}) async {
if (ignoreMutex) {
await _disconnectHelper(reason: reason);
} else {
await _requestMutex.protect(() async {
await _disconnectHelper(reason: reason);
});
}
}
Future<void> _disconnectHelper({required String reason}) async {
await _subscription?.cancel();
_subscription = null;
_socket?.destroy();
_socket = null;
await _socksSocket?.close();
_socksSocket = null;
// clean up remaining queue
await _requestQueue.completeRemainingWithError(
"JsonRPC disconnect() called with reason: \"$reason\"",
);
}
Future<void> _connect() async {
// ignore mutex is set to true here as _connect is already called within
// the mutex.protect block. Setting to false here leads to a deadlock
await disconnect(
reason: "New connection requested",
ignoreMutex: true,
);
if (!Prefs.instance.useTor) {
if (useSSL) {
_socket = await SecureSocket.connect(
host,
port,
timeout: connectionTimeout,
onBadCertificate: (_) => true,
); // TODO do not automatically trust bad certificates.
} else {
_socket = await Socket.connect(
host,
port,
timeout: connectionTimeout,
);
}
_subscription = _socket!.listen(
_dataHandler,
onError: _errorHandler,
onDone: _doneHandler,
cancelOnError: true,
);
} else {
if (proxyInfo == null) {
throw JsonRpcException(
"JsonRPC.connect failed with useTor=${Prefs.instance.useTor} and proxyInfo is null");
}
// instantiate a socks socket at localhost and on the port selected by the tor service
_socksSocket = await SOCKSSocket.create(
proxyHost: proxyInfo!.host.address,
proxyPort: proxyInfo!.port,
sslEnabled: useSSL,
);
try {
Logging.instance.log(
"JsonRPC.connect(): connecting to SOCKS socket at $proxyInfo (SSL $useSSL)...",
level: LogLevel.Info);
await _socksSocket?.connect();
Logging.instance.log(
"JsonRPC.connect(): connected to SOCKS socket at $proxyInfo...",
level: LogLevel.Info);
} catch (e) {
Logging.instance.log(
"JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e",
level: LogLevel.Error);
throw JsonRpcException(
"JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e");
}
try {
Logging.instance.log(
"JsonRPC.connect(): connecting to $host:$port over SOCKS socket at $proxyInfo...",
level: LogLevel.Info);
await _socksSocket?.connectTo(host, port);
Logging.instance.log(
"JsonRPC.connect(): connected to $host:$port over SOCKS socket at $proxyInfo",
level: LogLevel.Info);
} catch (e) {
Logging.instance.log(
"JsonRPC.connect(): failed to connect to $host over tor proxy at $proxyInfo, $e",
level: LogLevel.Error);
throw JsonRpcException(
"JsonRPC.connect(): failed to connect to tor proxy, $e");
}
_subscription = _socksSocket!.listen(
_dataHandler,
onError: _errorHandler,
onDone: _doneHandler,
cancelOnError: true,
);
}
return;
}
}
class _JsonRPCRequestQueue {
final _lock = Mutex();
final List<_JsonRPCRequest> _rq = [];
Future<void> add(
_JsonRPCRequest req, {
VoidCallback? onInitialRequestAdded,
}) async {
return await _lock.protect(() async {
_rq.add(req);
if (_rq.length == 1) {
onInitialRequestAdded?.call();
}
});
}
Future<bool> remove(_JsonRPCRequest req) async {
return await _lock.protect(() async {
final result = _rq.remove(req);
return result;
});
}
Future<_JsonRPCRequest?> get nextIncompleteReq async {
return await _lock.protect(() async {
int removeCount = 0;
_JsonRPCRequest? returnValue;
for (final req in _rq) {
if (req.isComplete) {
removeCount++;
} else {
returnValue = req;
break;
}
}
_rq.removeRange(0, removeCount);
return returnValue;
});
}
Future<void> completeRemainingWithError(
String error, {
StackTrace? stackTrace,
}) async {
await _lock.protect(() async {
for (final req in _rq) {
if (!req.isComplete) {
req.completer.completeError(Exception(error), stackTrace);
}
}
_rq.clear();
});
}
Future<bool> get isEmpty async {
return await _lock.protect(() async {
return _rq.isEmpty;
});
}
}
class _JsonRPCRequest {
// 0x0A is newline
// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html
static const int separatorByte = 0x0A;
final String jsonRequest;
final Completer<JsonRPCResponse> completer;
final Duration requestTimeout;
final List<int> _responseData = [];
_JsonRPCRequest({
required this.jsonRequest,
required this.completer,
required this.requestTimeout,
});
void appendDataAndCheckIfComplete(List<int> data) {
_responseData.addAll(data);
if (data.last == separatorByte) {
try {
final response = json.decode(String.fromCharCodes(_responseData));
completer.complete(JsonRPCResponse(data: response));
} catch (e, s) {
Logging.instance.log(
"JsonRPC json.decode: $e\n$s",
level: LogLevel.Error,
);
completer.completeError(e, s);
}
}
}
void initiateTimeout({
required VoidCallback onTimedOut,
}) {
Future<void>.delayed(requestTimeout).then((_) {
if (!isComplete) {
completer.complete(
JsonRPCResponse(
data: null,
exception: JsonRpcException(
"_JsonRPCRequest timed out: $jsonRequest",
),
),
);
}
onTimedOut.call();
});
}
bool get isComplete => completer.isCompleted;
}
class JsonRPCResponse {
final dynamic data;
final JsonRpcException? exception;
JsonRPCResponse({this.data, this.exception});
}

View file

@ -14,18 +14,20 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/utilities/test_eth_node_connection.dart';
import 'package:stackwallet/utilities/test_monero_node_connection.dart';
import 'package:stackwallet/utilities/test_stellar_node_connection.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@ -173,17 +175,14 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
case Coin.bitcoincashTestnet:
case Coin.firoTestNet:
case Coin.dogecoinTestNet:
final client = ElectrumXClient(
host: formData.host!,
port: formData.port!,
useSSL: formData.useSSL!,
failovers: [],
prefs: ref.read(prefsChangeNotifierProvider),
coin: coin,
);
try {
testPassed = await client.ping();
testPassed = await checkElectrumServer(
host: formData.host!,
port: formData.port!,
useSSL: formData.useSSL!,
overridePrefs: ref.read(prefsChangeNotifierProvider),
overrideTorService: ref.read(pTorService),
);
} catch (_) {
testPassed = false;
}
@ -191,14 +190,13 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
break;
case Coin.ethereum:
// TODO fix this
// final client = Web3Client(
// "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba",
// Client());
try {
// await client.getSyncStatus();
} catch (_) {}
testPassed = await testEthNodeConnection(formData.host!);
} catch (_) {
testPassed = false;
}
break;
case Coin.stellar:
case Coin.stellarTestnet:
try {

View file

@ -13,13 +13,14 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
@ -150,17 +151,14 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
case Coin.eCash:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
final client = ElectrumXClient(
host: node!.host,
port: node.port,
useSSL: node.useSSL,
failovers: [],
prefs: ref.read(prefsChangeNotifierProvider),
coin: coin,
);
try {
testPassed = await client.ping();
testPassed = await checkElectrumServer(
host: node!.host,
port: node.port,
useSSL: node.useSSL,
overridePrefs: ref.read(prefsChangeNotifierProvider),
overrideTorService: ref.read(pTorService),
);
} catch (_) {
testPassed = false;
}

View file

@ -147,7 +147,7 @@ class NotificationsService extends ChangeNotifier {
node: eNode,
failovers: failovers,
prefs: prefs,
coin: coin,
cryptoCurrency: wallet.cryptoCurrency,
);
final tx = await client.getTransaction(txHash: txid);

View file

@ -0,0 +1,63 @@
import 'dart:io';
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
Future<bool> checkElectrumServer({
required String host,
required int port,
required bool useSSL,
Prefs? overridePrefs,
TorService? overrideTorService,
}) async {
final _prefs = overridePrefs ?? Prefs.instance;
final _torService = overrideTorService ?? TorService.sharedInstance;
({InternetAddress host, int port})? proxyInfo;
try {
// If we're supposed to use Tor...
if (_prefs.useTor) {
// But Tor isn't running...
if (_torService.status != TorConnectionStatus.connected) {
// And the killswitch isn't set...
if (!_prefs.torKillSwitch) {
// Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function.
Logging.instance.log(
"Tor preference set but Tor is not enabled, killswitch not set, connecting to Electrum adapter through clearnet",
level: LogLevel.Warning,
);
} else {
// ... But if the killswitch is set, then we throw an exception.
throw Exception(
"Tor preference and killswitch set but Tor is not enabled, not connecting to Electrum adapter");
// TODO [prio=low]: Try to start Tor.
}
} else {
// Get the proxy info from the TorService.
proxyInfo = _torService.getProxyInfo();
}
}
final client = await ElectrumClient.connect(
host: host,
port: port,
useSSL: useSSL,
proxyInfo: proxyInfo,
).timeout(
const Duration(seconds: 5),
onTimeout: () => throw Exception(
"The checkElectrumServer connect() call timed out.",
),
);
await client.ping().timeout(const Duration(seconds: 5));
return true;
} catch (_) {
return false;
}
}

View file

@ -1,8 +1,6 @@
import 'dart:async';
import 'dart:ffi';
import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter;
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:flutter/foundation.dart';
import 'package:frostdart/frostdart.dart' as frost;
import 'package:frostdart/frostdart_bindings_generated.dart';
@ -20,19 +18,15 @@ import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_currency.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stream_channel/stream_channel.dart';
class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
BitcoinFrostWallet(CryptoCurrencyNetwork network)
@ -44,8 +38,6 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
.findFirstSync()!;
late ElectrumXClient electrumXClient;
late StreamChannel electrumAdapterChannel;
late ElectrumClient electrumAdapterClient;
late CachedElectrumXClient electrumXCachedClient;
Future<void> initializeNewFrost({
@ -1102,66 +1094,29 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
final newNode = await _getCurrentElectrumXNode();
try {
await electrumXClient.electrumAdapterClient?.close();
} catch (e, s) {
await electrumXClient.closeAdapter();
} catch (e) {
if (e.toString().contains("initialized")) {
// Ignore. This should happen every first time the wallet is opened.
} else {
Logging.instance
.log("Error closing electrumXClient: $e", level: LogLevel.Error);
Logging.instance.log(
"Error closing electrumXClient: $e",
level: LogLevel.Error,
);
}
}
electrumXClient = ElectrumXClient.from(
node: newNode,
prefs: prefs,
failovers: failovers,
coin: cryptoCurrency.coin,
cryptoCurrency: cryptoCurrency,
);
electrumAdapterChannel = await electrum_adapter.connect(
newNode.address,
port: newNode.port,
acceptUnverified: true,
useSSL: newNode.useSSL,
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
);
if (electrumXClient.coin == Coin.firo ||
electrumXClient.coin == Coin.firoTestNet) {
electrumAdapterClient = FiroElectrumClient(
electrumAdapterChannel,
newNode.address,
newNode.port,
newNode.useSSL,
Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
} else {
electrumAdapterClient = ElectrumClient(
electrumAdapterChannel,
newNode.address,
newNode.port,
newNode.useSSL,
Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
}
electrumXCachedClient = CachedElectrumXClient.from(
electrumXClient: electrumXClient,
electrumAdapterClient: electrumAdapterClient,
electrumAdapterUpdateCallback: updateClient,
);
}
// TODO [prio=low]: Use ElectrumXInterface method.
Future<ElectrumClient> updateClient() async {
Logging.instance.log(
"Updating electrum node and ElectrumAdapterClient from Frost wallet.",
level: LogLevel.Info);
await updateNode();
return electrumAdapterClient;
}
bool _duplicateTxCheck(
List<Map<String, dynamic>> allTransactions, String txid) {
for (int i = 0; i < allTransactions.length; i++) {

View file

@ -3,11 +3,9 @@ import 'dart:math';
import 'dart:typed_data';
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter;
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart';
import 'package:stackwallet/electrumx_rpc/client_manager.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart';
@ -15,7 +13,6 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/models/signing_data.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
@ -23,22 +20,16 @@ import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/paynym_is_api.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
import 'package:stream_channel/stream_channel.dart';
mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
late ElectrumXClient electrumXClient;
late StreamChannel<dynamic> electrumAdapterChannel;
late ElectrumClient electrumAdapterClient;
late CachedElectrumXClient electrumXCachedClient;
// late SubscribableElectrumXClient subscribableElectrumXClient;
late ChainHeightServiceManager chainHeightServiceManager;
int? get maximumFeerate => null;
@ -706,6 +697,12 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
cryptoCurrency.networkParams,
);
print("=============================================================");
print("$i ${txData.recipients![i].amount.decimal}");
print("$i ${txData.recipients![i].amount.raw}");
print("$address");
print("=============================================================");
final output = coinlib.Output.fromAddress(
txData.recipients![i].amount.raw,
address,
@ -785,24 +782,9 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
Future<int> fetchChainHeight() async {
try {
// Get the chain height service for the current coin.
ChainHeightService? service = ChainHeightServiceManager.getService(
cryptoCurrency.coin,
return await ClientManager.sharedInstance.getChainHeightFor(
cryptoCurrency,
);
// ... or create a new one if it doesn't exist.
if (service == null) {
service = ChainHeightService(client: electrumAdapterClient);
ChainHeightServiceManager.add(service, cryptoCurrency.coin);
}
// If the service hasn't been started, start it and fetch the chain height.
if (!service.started) {
return await service.fetchHeightAndStartListenForUpdates();
}
// Return the height as per the service if available or the cached height.
return service.height ?? info.cachedChainHeight;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown in fetchChainHeight\nError: $e\nStack trace: $s",
@ -868,7 +850,7 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
final newNode = await _getCurrentElectrumXNode();
try {
await electrumXClient.electrumAdapterClient?.close();
await electrumXClient.closeAdapter();
} catch (e) {
if (e.toString().contains("initialized")) {
// Ignore. This should happen every first time the wallet is opened.
@ -881,50 +863,11 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
node: newNode,
prefs: prefs,
failovers: failovers,
coin: cryptoCurrency.coin,
cryptoCurrency: cryptoCurrency,
);
electrumAdapterChannel = await electrum_adapter.connect(
newNode.address,
port: newNode.port,
acceptUnverified: true,
useSSL: newNode.useSSL,
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
);
if (electrumXClient.coin == Coin.firo ||
electrumXClient.coin == Coin.firoTestNet) {
electrumAdapterClient = FiroElectrumClient(
electrumAdapterChannel,
newNode.address,
newNode.port,
newNode.useSSL,
Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
} else {
electrumAdapterClient = ElectrumClient(
electrumAdapterChannel,
newNode.address,
newNode.port,
newNode.useSSL,
Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
}
electrumXCachedClient = CachedElectrumXClient.from(
electrumXClient: electrumXClient,
electrumAdapterClient: electrumAdapterClient,
electrumAdapterUpdateCallback: updateClient,
);
// Replaced using electrum_adapters' SubscribableClient in fetchChainHeight.
// subscribableElectrumXClient = SubscribableElectrumXClient.from(
// node: newNode,
// prefs: prefs,
// failovers: failovers,
// );
// await subscribableElectrumXClient.connect(
// host: newNode.address, port: newNode.port);
}
//============================================================================
@ -1219,13 +1162,6 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
await updateElectrumX();
}
Future<ElectrumClient> updateClient() async {
Logging.instance.log("Updating electrum node and ElectrumAdapterClient.",
level: LogLevel.Info);
await updateNode();
return electrumAdapterClient;
}
FeeObject? _cachedFees;
@override

View file

@ -13,15 +13,16 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart';
import 'package:stackwallet/providers/global/active_wallet_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -171,17 +172,14 @@ class _NodeCardState extends ConsumerState<NodeCard> {
case Coin.eCash:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
final client = ElectrumXClient(
host: node.host,
port: node.port,
useSSL: node.useSSL,
failovers: [],
prefs: ref.read(prefsChangeNotifierProvider),
coin: widget.coin,
);
try {
testPassed = await client.ping();
testPassed = await checkElectrumServer(
host: node.host,
port: node.port,
useSSL: node.useSSL,
overridePrefs: ref.read(prefsChangeNotifierProvider),
overrideTorService: ref.read(pTorService),
);
} catch (_) {
testPassed = false;
}

View file

@ -13,7 +13,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart';
@ -23,6 +22,7 @@ import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -153,18 +153,14 @@ class NodeOptionsSheet extends ConsumerWidget {
case Coin.eCash:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
final client = ElectrumXClient(
host: node.host,
port: node.port,
useSSL: node.useSSL,
failovers: [],
prefs: ref.read(prefsChangeNotifierProvider),
torService: ref.read(pTorService),
coin: coin,
);
try {
testPassed = await client.ping();
testPassed = await checkElectrumServer(
host: node.host,
port: node.port,
useSSL: node.useSSL,
overridePrefs: ref.read(prefsChangeNotifierProvider),
overrideTorService: ref.read(pTorService),
);
} catch (_) {
testPassed = false;
}