2024-02-05 18:09:45 +00:00
|
|
|
import 'dart:async';
|
|
|
|
|
2024-02-17 08:47:53 +00:00
|
|
|
import 'package:electrum_adapter/electrum_adapter.dart';
|
2024-02-05 18:09:45 +00:00
|
|
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
2024-02-19 21:11:10 +00:00
|
|
|
import 'package:stackwallet/utilities/logger.dart';
|
2024-02-05 18:09:45 +00:00
|
|
|
|
2024-02-17 08:47:53 +00:00
|
|
|
/// Manage chain height subscriptions for each coin.
|
|
|
|
abstract class ChainHeightServiceManager {
|
2024-02-19 20:32:43 +00:00
|
|
|
// A map of chain height services for each coin.
|
2024-02-17 08:47:53 +00:00
|
|
|
static final Map<Coin, ChainHeightService> _services = {};
|
2024-02-19 20:32:43 +00:00
|
|
|
// Map<Coin, ChainHeightService> get services => _services;
|
2024-02-14 17:07:27 +00:00
|
|
|
|
2024-02-19 20:32:43 +00:00
|
|
|
// Get the chain height service for a specific coin.
|
2024-02-17 08:47:53 +00:00
|
|
|
static ChainHeightService? getService(Coin coin) {
|
|
|
|
return _services[coin];
|
|
|
|
}
|
|
|
|
|
2024-02-19 20:32:43 +00:00
|
|
|
// Add a chain height service for a specific coin.
|
2024-02-17 08:47:53 +00:00
|
|
|
static void add(ChainHeightService service, Coin coin) {
|
2024-02-19 20:32:43 +00:00
|
|
|
// Don't add a new service if one already exists.
|
2024-02-17 08:47:53 +00:00
|
|
|
if (_services[coin] == null) {
|
|
|
|
_services[coin] = service;
|
|
|
|
} else {
|
|
|
|
throw Exception("Chain height service for $coin already managed");
|
|
|
|
}
|
|
|
|
}
|
2024-02-19 20:32:43 +00:00
|
|
|
|
|
|
|
// 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.
|
2024-02-19 21:18:29 +00:00
|
|
|
//
|
|
|
|
// 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) {
|
2024-02-19 20:32:43 +00:00
|
|
|
final ChainHeightService? service = getService(coin);
|
|
|
|
await service?.cancelListen();
|
|
|
|
remove(coin);
|
|
|
|
}
|
|
|
|
}
|
2024-02-17 08:47:53 +00:00
|
|
|
}
|
|
|
|
|
2024-02-19 20:32:43 +00:00
|
|
|
/// A service to fetch and listen for chain height updates.
|
|
|
|
///
|
|
|
|
/// TODO: Add error handling and branching to handle various other scenarios.
|
2024-02-17 08:47:53 +00:00
|
|
|
class ChainHeightService {
|
2024-02-19 20:32:43 +00:00
|
|
|
// The electrum_adapter client to use for fetching chain height updates.
|
2024-02-17 08:47:53 +00:00
|
|
|
ElectrumClient client;
|
|
|
|
|
2024-02-19 20:32:43 +00:00
|
|
|
// The subscription to listen for chain height updates.
|
2024-02-17 08:47:53 +00:00
|
|
|
StreamSubscription<dynamic>? _subscription;
|
2024-02-19 20:32:43 +00:00
|
|
|
|
|
|
|
// Whether the service has started listening for updates.
|
2024-02-17 08:47:53 +00:00
|
|
|
bool get started => _subscription != null;
|
|
|
|
|
2024-02-19 20:32:43 +00:00
|
|
|
// The current chain height.
|
2024-02-17 08:47:53 +00:00
|
|
|
int? _height;
|
|
|
|
int? get height => _height;
|
|
|
|
|
2024-02-19 21:11:10 +00:00
|
|
|
// 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);
|
|
|
|
|
2024-02-17 08:47:53 +00:00
|
|
|
ChainHeightService({required this.client});
|
|
|
|
|
2024-02-19 20:32:43 +00:00
|
|
|
/// Fetch the current chain height and start listening for updates.
|
2024-02-17 08:47:53 +00:00
|
|
|
Future<int> fetchHeightAndStartListenForUpdates() async {
|
2024-02-19 20:32:43 +00:00
|
|
|
// Don't start a new subscription if one already exists.
|
2024-02-17 08:47:53 +00:00
|
|
|
if (_subscription != null) {
|
|
|
|
throw Exception(
|
|
|
|
"Attempted to start a chain height service where an existing"
|
|
|
|
" subscription already exists!",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-02-19 20:32:43 +00:00
|
|
|
// A completer to wait for the current chain height to be fetched.
|
2024-02-17 08:47:53 +00:00
|
|
|
final completer = Completer<int>();
|
2024-02-19 20:32:43 +00:00
|
|
|
|
|
|
|
// Fetch the current chain height.
|
|
|
|
_subscription = client.subscribeHeaders().listen((BlockHeader event) {
|
2024-02-17 08:47:53 +00:00
|
|
|
_height = event.height;
|
2024-02-19 20:32:43 +00:00
|
|
|
|
2024-02-17 08:47:53 +00:00
|
|
|
if (!completer.isCompleted) {
|
|
|
|
completer.complete(_height);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-02-19 21:11:10 +00:00
|
|
|
_subscription?.onError((dynamic error) {
|
|
|
|
_handleError(error);
|
|
|
|
});
|
|
|
|
|
2024-02-19 20:32:43 +00:00
|
|
|
// Wait for the current chain height to be fetched.
|
2024-02-17 08:47:53 +00:00
|
|
|
return completer.future;
|
|
|
|
}
|
|
|
|
|
2024-02-19 21:11:10 +00:00
|
|
|
/// 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;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-02-19 20:32:43 +00:00
|
|
|
/// Stop listening for chain height updates.
|
2024-02-19 21:11:10 +00:00
|
|
|
Future<void> cancelListen() async {
|
|
|
|
await _subscription?.cancel();
|
|
|
|
_subscription = null;
|
|
|
|
_reconnectTimer?.cancel();
|
|
|
|
}
|
2024-02-05 18:09:45 +00:00
|
|
|
}
|