mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-17 09:47:37 +00:00
Merge pull request #773 from cypherstack/staging
Update main to v1.10.1
This commit is contained in:
commit
953a9e3e7a
36 changed files with 1965 additions and 843 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
@ -6,4 +6,4 @@
|
|||
url = https://github.com/cypherstack/flutter_libmonero.git
|
||||
[submodule "crypto_plugins/flutter_liblelantus"]
|
||||
path = crypto_plugins/flutter_liblelantus
|
||||
url = https://github.com/cypherstack/flutter_liblelantus.git
|
||||
url = https://github.com/cypherstack/flutter_liblelantus.git
|
|
@ -1 +1 @@
|
|||
Subproject commit c976dcfc7786bbf7091e310eb877f5c685352903
|
||||
Subproject commit 9eb24dd00cd0e1df08624ece1ca47090c158c08c
|
|
@ -85,6 +85,7 @@ class DbVersionMigrator with WalletDB {
|
|||
useSSL: node.useSSL),
|
||||
prefs: prefs,
|
||||
failovers: failovers,
|
||||
coin: Coin.firo,
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
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';
|
||||
|
@ -19,20 +22,41 @@ import 'package:string_validator/string_validator.dart';
|
|||
|
||||
class CachedElectrumXClient {
|
||||
final ElectrumXClient electrumXClient;
|
||||
ElectrumClient electrumAdapterClient;
|
||||
final Future<ElectrumClient> Function() electrumAdapterUpdateCallback;
|
||||
|
||||
static const minCacheConfirms = 30;
|
||||
|
||||
const CachedElectrumXClient({
|
||||
CachedElectrumXClient({
|
||||
required this.electrumXClient,
|
||||
required this.electrumAdapterClient,
|
||||
required this.electrumAdapterUpdateCallback,
|
||||
});
|
||||
|
||||
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 = "",
|
||||
|
@ -56,9 +80,12 @@ class CachedElectrumXClient {
|
|||
set = Map<String, dynamic>.from(cachedSet);
|
||||
}
|
||||
|
||||
final newSet = await electrumXClient.getLelantusAnonymitySet(
|
||||
await _checkElectrumAdapterClient();
|
||||
|
||||
final newSet = await (electrumAdapterClient as FiroElectrumClient)
|
||||
.getLelantusAnonymitySet(
|
||||
groupId: groupId,
|
||||
blockhash: set["blockHash"] as String,
|
||||
blockHash: set["blockHash"] as String,
|
||||
);
|
||||
|
||||
// update set with new data
|
||||
|
@ -82,7 +109,7 @@ class CachedElectrumXClient {
|
|||
translatedCoin.add(!isHexadecimal(newCoin[2] as String)
|
||||
? base64ToHex(newCoin[2] as String)
|
||||
: newCoin[2]);
|
||||
} catch (e, s) {
|
||||
} catch (e) {
|
||||
translatedCoin.add(newCoin[2]);
|
||||
}
|
||||
translatedCoin.add(!isHexadecimal(newCoin[3] as String)
|
||||
|
@ -130,7 +157,10 @@ class CachedElectrumXClient {
|
|||
set = Map<String, dynamic>.from(cachedSet);
|
||||
}
|
||||
|
||||
final newSet = await electrumXClient.getSparkAnonymitySet(
|
||||
await _checkElectrumAdapterClient();
|
||||
|
||||
final newSet = await (electrumAdapterClient as FiroElectrumClient)
|
||||
.getSparkAnonymitySet(
|
||||
coinGroupId: groupId,
|
||||
startBlockHash: set["blockHash"] as String,
|
||||
);
|
||||
|
@ -188,8 +218,10 @@ class CachedElectrumXClient {
|
|||
|
||||
final cachedTx = box.get(txHash) as Map?;
|
||||
if (cachedTx == null) {
|
||||
final Map<String, dynamic> result = await electrumXClient
|
||||
.getTransaction(txHash: txHash, verbose: verbose);
|
||||
await _checkElectrumAdapterClient();
|
||||
|
||||
final Map<String, dynamic> result =
|
||||
await electrumAdapterClient.getTransaction(txHash);
|
||||
|
||||
result.remove("hex");
|
||||
result.remove("lelantusData");
|
||||
|
@ -231,7 +263,10 @@ class CachedElectrumXClient {
|
|||
cachedSerials.length - 100, // 100 being some arbitrary buffer
|
||||
);
|
||||
|
||||
final serials = await electrumXClient.getLelantusUsedCoinSerials(
|
||||
await _checkElectrumAdapterClient();
|
||||
|
||||
final serials = await (electrumAdapterClient as FiroElectrumClient)
|
||||
.getLelantusUsedCoinSerials(
|
||||
startNumber: startNumber,
|
||||
);
|
||||
|
||||
|
@ -279,7 +314,10 @@ class CachedElectrumXClient {
|
|||
cachedTags.length - 100, // 100 being some arbitrary buffer
|
||||
);
|
||||
|
||||
final tags = await electrumXClient.getSparkUsedCoinsTags(
|
||||
await _checkElectrumAdapterClient();
|
||||
|
||||
final tags =
|
||||
await (electrumAdapterClient as FiroElectrumClient).getUsedCoinsTags(
|
||||
startNumber: startNumber,
|
||||
);
|
||||
|
||||
|
@ -287,12 +325,18 @@ class CachedElectrumXClient {
|
|||
// .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) {
|
||||
assert(cachedTags.intersection(tags).isNotEmpty);
|
||||
assert(cachedTags.intersection(newTags).isNotEmpty);
|
||||
}
|
||||
|
||||
cachedTags.addAll(tags);
|
||||
// Make newTags an Iterable<String>.
|
||||
final Iterable<String> iterableTags = newTags.map((e) => e.toString());
|
||||
|
||||
cachedTags.addAll(iterableTags);
|
||||
|
||||
await box.put(
|
||||
"tags",
|
||||
|
|
149
lib/electrumx_rpc/electrumx_chain_height_service.dart
Normal file
149
lib/electrumx_rpc/electrumx_chain_height_service.dart
Normal file
|
@ -0,0 +1,149 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -9,24 +9,28 @@
|
|||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:decimal/decimal.dart';
|
||||
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: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/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:uuid/uuid.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
|
||||
class WifiOnlyException implements Exception {}
|
||||
|
||||
|
@ -73,6 +77,12 @@ class ElectrumXClient {
|
|||
JsonRPC? get rpcClient => _rpcClient;
|
||||
JsonRPC? _rpcClient;
|
||||
|
||||
StreamChannel<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
|
||||
StreamChannel<dynamic>? _electrumAdapterChannel;
|
||||
|
||||
ElectrumClient? get electrumAdapterClient => _electrumAdapterClient;
|
||||
ElectrumClient? _electrumAdapterClient;
|
||||
|
||||
late Prefs _prefs;
|
||||
late TorService _torService;
|
||||
|
||||
|
@ -81,6 +91,9 @@ 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(
|
||||
|
@ -101,7 +114,7 @@ class ElectrumXClient {
|
|||
required bool useSSL,
|
||||
required Prefs prefs,
|
||||
required List<ElectrumXNode> failovers,
|
||||
JsonRPC? client,
|
||||
Coin? coin,
|
||||
this.connectionTimeoutForSpecialCaseJsonRPCClients =
|
||||
const Duration(seconds: 60),
|
||||
TorService? torService,
|
||||
|
@ -112,9 +125,11 @@ class ElectrumXClient {
|
|||
_host = host;
|
||||
_port = port;
|
||||
_useSSL = useSSL;
|
||||
_rpcClient = client;
|
||||
_coin = coin;
|
||||
|
||||
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
|
||||
|
||||
// Listen for tor status changes.
|
||||
_torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
|
||||
(event) async {
|
||||
switch (event.newStatus) {
|
||||
|
@ -133,6 +148,8 @@ class ElectrumXClient {
|
|||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Listen for tor preference changes.
|
||||
_torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen(
|
||||
(event) async {
|
||||
// not sure if we need to do anything specific here
|
||||
|
@ -141,21 +158,13 @@ class ElectrumXClient {
|
|||
// case TorStatus.disabled:
|
||||
// }
|
||||
|
||||
// might be ok to just reset/kill the current _jsonRpcClient
|
||||
|
||||
// since disconnecting is async and we want to ensure instant change over
|
||||
// we will keep temp reference to current rpc client to call disconnect
|
||||
// on before awaiting the disconnection future
|
||||
|
||||
final temp = _rpcClient;
|
||||
|
||||
// setting to null should force the creation of a new json rpc client
|
||||
// on the next request sent through this electrumx instance
|
||||
_rpcClient = null;
|
||||
_electrumAdapterChannel = null;
|
||||
_electrumAdapterClient = null;
|
||||
|
||||
await temp?.disconnect(
|
||||
reason: "Tor status changed to \"${event.status}\"",
|
||||
);
|
||||
// Also close any chain height services that are currently open.
|
||||
await ChainHeightServiceManager.dispose();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -164,6 +173,7 @@ class ElectrumXClient {
|
|||
required ElectrumXNode node,
|
||||
required Prefs prefs,
|
||||
required List<ElectrumXNode> failovers,
|
||||
required Coin coin,
|
||||
TorService? torService,
|
||||
EventBus? globalEventBusForTesting,
|
||||
}) {
|
||||
|
@ -175,6 +185,7 @@ class ElectrumXClient {
|
|||
torService: torService,
|
||||
failovers: failovers,
|
||||
globalEventBusForTesting: globalEventBusForTesting,
|
||||
coin: coin,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -186,7 +197,9 @@ class ElectrumXClient {
|
|||
return true;
|
||||
}
|
||||
|
||||
void _checkRpcClient() {
|
||||
Future<void> checkElectrumAdapter() async {
|
||||
({InternetAddress host, int port})? proxyInfo;
|
||||
|
||||
// If we're supposed to use Tor...
|
||||
if (_prefs.useTor) {
|
||||
// But Tor isn't running...
|
||||
|
@ -195,64 +208,93 @@ class ElectrumXClient {
|
|||
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 ElectrumX through clearnet",
|
||||
"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 ElectrumX");
|
||||
"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.
|
||||
final proxyInfo = _torService.getProxyInfo();
|
||||
|
||||
if (currentFailoverIndex == -1) {
|
||||
_rpcClient ??= JsonRPC(
|
||||
host: host,
|
||||
port: port,
|
||||
useSSL: useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
proxyInfo: proxyInfo,
|
||||
);
|
||||
} else {
|
||||
_rpcClient ??= JsonRPC(
|
||||
host: failovers![currentFailoverIndex].address,
|
||||
port: failovers![currentFailoverIndex].port,
|
||||
useSSL: failovers![currentFailoverIndex].useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
proxyInfo: proxyInfo,
|
||||
);
|
||||
}
|
||||
|
||||
if (_rpcClient!.proxyInfo != proxyInfo) {
|
||||
_rpcClient!.proxyInfo = proxyInfo;
|
||||
_rpcClient!.disconnect(
|
||||
reason: "Tor proxyInfo does not match current info",
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
proxyInfo = _torService.getProxyInfo();
|
||||
}
|
||||
}
|
||||
|
||||
if (currentFailoverIndex == -1) {
|
||||
_rpcClient ??= JsonRPC(
|
||||
host: host,
|
||||
port: port,
|
||||
useSSL: useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
proxyInfo: null,
|
||||
);
|
||||
} else {
|
||||
_rpcClient ??= JsonRPC(
|
||||
host: failovers![currentFailoverIndex].address,
|
||||
port: failovers![currentFailoverIndex].port,
|
||||
useSSL: failovers![currentFailoverIndex].useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
proxyInfo: null,
|
||||
);
|
||||
// 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) {
|
||||
_electrumAdapterChannel = null;
|
||||
_electrumAdapterClient = null;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
} 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(
|
||||
_electrumAdapterChannel!,
|
||||
failovers![currentFailoverIndex].address,
|
||||
failovers![currentFailoverIndex].port,
|
||||
failovers![currentFailoverIndex].useSSL,
|
||||
proxyInfo,
|
||||
);
|
||||
} else {
|
||||
_electrumAdapterClient ??= ElectrumClient(
|
||||
_electrumAdapterChannel!,
|
||||
failovers![currentFailoverIndex].address,
|
||||
failovers![currentFailoverIndex].port,
|
||||
failovers![currentFailoverIndex].useSSL,
|
||||
proxyInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// Send raw rpc command
|
||||
|
@ -268,32 +310,22 @@ class ElectrumXClient {
|
|||
}
|
||||
|
||||
if (_requireMutex) {
|
||||
await _torConnectingLock.protect(() async => _checkRpcClient());
|
||||
await _torConnectingLock
|
||||
.protect(() async => await checkElectrumAdapter());
|
||||
} else {
|
||||
_checkRpcClient();
|
||||
await checkElectrumAdapter();
|
||||
}
|
||||
|
||||
try {
|
||||
final requestId = requestID ?? const Uuid().v1();
|
||||
final jsonArgs = json.encode(args);
|
||||
final jsonRequestString = '{"jsonrpc": "2.0", '
|
||||
'"id": "$requestId",'
|
||||
'"method": "$command",'
|
||||
'"params": $jsonArgs}';
|
||||
|
||||
// Logging.instance.log("ElectrumX jsonRequestString: $jsonRequestString");
|
||||
|
||||
final response = await _rpcClient!.request(
|
||||
jsonRequestString,
|
||||
requestTimeout,
|
||||
final response = await _electrumAdapterClient!.request(
|
||||
command,
|
||||
args,
|
||||
);
|
||||
|
||||
if (response.exception != null) {
|
||||
throw response.exception!;
|
||||
}
|
||||
|
||||
if (response.data is Map && response.data["error"] != null) {
|
||||
if (response.data["error"]
|
||||
if (response is Map &&
|
||||
response.keys.contains("error") &&
|
||||
response["error"] != null) {
|
||||
if (response["error"]
|
||||
.toString()
|
||||
.contains("No such mempool or blockchain transaction")) {
|
||||
throw NoSuchTransactionException(
|
||||
|
@ -305,13 +337,19 @@ class ElectrumXClient {
|
|||
throw Exception(
|
||||
"JSONRPC response\n"
|
||||
" command: $command\n"
|
||||
" error: ${response.data}"
|
||||
" error: ${response["error"]}\n"
|
||||
" args: $args\n",
|
||||
);
|
||||
}
|
||||
|
||||
currentFailoverIndex = -1;
|
||||
return response.data;
|
||||
|
||||
// If the command is a ping, a good return should always be null.
|
||||
if (command.contains("ping")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return response;
|
||||
} on WifiOnlyException {
|
||||
rethrow;
|
||||
} on SocketException {
|
||||
|
@ -347,9 +385,9 @@ class ElectrumXClient {
|
|||
/// map of <request id string : arguments list>
|
||||
///
|
||||
/// returns a list of json response objects if no errors were found
|
||||
Future<List<Map<String, dynamic>>> batchRequest({
|
||||
Future<List<dynamic>> batchRequest({
|
||||
required String command,
|
||||
required Map<String, List<dynamic>> args,
|
||||
required List<dynamic> args,
|
||||
Duration requestTimeout = const Duration(seconds: 60),
|
||||
int retries = 2,
|
||||
}) async {
|
||||
|
@ -358,65 +396,50 @@ class ElectrumXClient {
|
|||
}
|
||||
|
||||
if (_requireMutex) {
|
||||
await _torConnectingLock.protect(() async => _checkRpcClient());
|
||||
await _torConnectingLock
|
||||
.protect(() async => await checkElectrumAdapter());
|
||||
} else {
|
||||
_checkRpcClient();
|
||||
await checkElectrumAdapter();
|
||||
}
|
||||
|
||||
try {
|
||||
final List<String> requestStrings = [];
|
||||
|
||||
for (final entry in args.entries) {
|
||||
final jsonArgs = json.encode(entry.value);
|
||||
requestStrings.add(
|
||||
'{"jsonrpc": "2.0", "id": "${entry.key}","method": "$command","params": $jsonArgs}');
|
||||
}
|
||||
|
||||
// combine request strings into json array
|
||||
String request = "[";
|
||||
for (int i = 0; i < requestStrings.length - 1; i++) {
|
||||
request += "${requestStrings[i]},";
|
||||
}
|
||||
request += "${requestStrings.last}]";
|
||||
|
||||
// Logging.instance.log("batch request: $request");
|
||||
|
||||
// send batch request
|
||||
final jsonRpcResponse =
|
||||
(await _rpcClient!.request(request, requestTimeout));
|
||||
|
||||
if (jsonRpcResponse.exception != null) {
|
||||
throw jsonRpcResponse.exception!;
|
||||
}
|
||||
|
||||
final List<dynamic> response;
|
||||
try {
|
||||
response = jsonRpcResponse.data as List;
|
||||
} catch (_) {
|
||||
throw Exception(
|
||||
"Expected json list but got a map: ${jsonRpcResponse.data}",
|
||||
);
|
||||
}
|
||||
|
||||
// check for errors, format and throw if there are any
|
||||
final List<String> errors = [];
|
||||
for (int i = 0; i < response.length; i++) {
|
||||
final result = response[i];
|
||||
if (result["error"] != null || result["result"] == null) {
|
||||
errors.add(result.toString());
|
||||
var futures = <Future<dynamic>>[];
|
||||
_electrumAdapterClient!.peer.withBatch(() {
|
||||
for (final arg in args) {
|
||||
futures.add(_electrumAdapterClient!.request(command, arg));
|
||||
}
|
||||
}
|
||||
if (errors.isNotEmpty) {
|
||||
String error = "[\n";
|
||||
for (int i = 0; i < errors.length; i++) {
|
||||
error += "${errors[i]}\n";
|
||||
}
|
||||
error += "]";
|
||||
throw Exception("JSONRPC response error: $error");
|
||||
}
|
||||
});
|
||||
final response = await Future.wait(futures);
|
||||
|
||||
// We cannot modify the response list as the order and length are related
|
||||
// to the order and length of the batched requests!
|
||||
//
|
||||
// // check for errors, format and throw if there are any
|
||||
// final List<String> errors = [];
|
||||
// for (int i = 0; i < response.length; i++) {
|
||||
// var result = response[i];
|
||||
//
|
||||
// if (result == null || (result is List && result.isEmpty)) {
|
||||
// continue;
|
||||
// // TODO [prio=extreme]: Figure out if this is actually an issue.
|
||||
// }
|
||||
// result = result[0]; // Unwrap the list.
|
||||
// if ((result is Map && result.keys.contains("error")) ||
|
||||
// result == null) {
|
||||
// errors.add(result.toString());
|
||||
// }
|
||||
// }
|
||||
// if (errors.isNotEmpty) {
|
||||
// String error = "[\n";
|
||||
// for (int i = 0; i < errors.length; i++) {
|
||||
// error += "${errors[i]}\n";
|
||||
// }
|
||||
// error += "]";
|
||||
// throw Exception("JSONRPC response error: $error");
|
||||
// }
|
||||
|
||||
currentFailoverIndex = -1;
|
||||
return List<Map<String, dynamic>>.from(response, growable: false);
|
||||
return response;
|
||||
} on WifiOnlyException {
|
||||
rethrow;
|
||||
} on SocketException {
|
||||
|
@ -451,13 +474,23 @@ class ElectrumXClient {
|
|||
/// Returns true if ping succeeded
|
||||
Future<bool> ping({String? requestID, int retryCount = 1}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
// This doesn't work because electrum_adapter only returns the result:
|
||||
// (which is always `null`).
|
||||
// await checkElectrumAdapter();
|
||||
// final response = await electrumAdapterClient!
|
||||
// .ping()
|
||||
// .timeout(const Duration(seconds: 2));
|
||||
// return (response as Map<String, dynamic>).isNotEmpty;
|
||||
|
||||
// Because request() has been updated to use electrum_adapter, and because
|
||||
// electrum_adapter returns the result of the request, request() has been
|
||||
// updated to return a bool on a server.ping command as a special case.
|
||||
return await request(
|
||||
requestID: requestID,
|
||||
command: 'server.ping',
|
||||
requestTimeout: const Duration(seconds: 2),
|
||||
retries: retryCount,
|
||||
).timeout(const Duration(seconds: 2)) as Map<String, dynamic>;
|
||||
return response.keys.contains("result") && response["result"] == null;
|
||||
).timeout(const Duration(seconds: 2)) as bool;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -478,14 +511,14 @@ class ElectrumXClient {
|
|||
requestID: requestID,
|
||||
command: 'blockchain.headers.subscribe',
|
||||
);
|
||||
if (response["result"] == null) {
|
||||
if (response == null) {
|
||||
Logging.instance.log(
|
||||
"getBlockHeadTip returned null response",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
throw 'getBlockHeadTip returned null response';
|
||||
}
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
return Map<String, dynamic>.from(response as Map);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -510,7 +543,7 @@ class ElectrumXClient {
|
|||
requestID: requestID,
|
||||
command: 'server.features',
|
||||
);
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
return Map<String, dynamic>.from(response as Map);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -531,7 +564,7 @@ class ElectrumXClient {
|
|||
rawTx,
|
||||
],
|
||||
);
|
||||
return response["result"] as String;
|
||||
return response as String;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -558,7 +591,7 @@ class ElectrumXClient {
|
|||
scripthash,
|
||||
],
|
||||
);
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
return Map<String, dynamic>.from(response as Map);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -595,8 +628,7 @@ class ElectrumXClient {
|
|||
scripthash,
|
||||
],
|
||||
);
|
||||
|
||||
result = response["result"];
|
||||
result = response;
|
||||
retryCount--;
|
||||
}
|
||||
|
||||
|
@ -606,17 +638,17 @@ class ElectrumXClient {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Map<String, List<Map<String, dynamic>>>> getBatchHistory(
|
||||
{required Map<String, List<dynamic>> args}) async {
|
||||
Future<List<List<Map<String, dynamic>>>> getBatchHistory({
|
||||
required List<dynamic> args,
|
||||
}) async {
|
||||
try {
|
||||
final response = await batchRequest(
|
||||
command: 'blockchain.scripthash.get_history',
|
||||
args: args,
|
||||
);
|
||||
final Map<String, List<Map<String, dynamic>>> result = {};
|
||||
final List<List<Map<String, dynamic>>> result = [];
|
||||
for (int i = 0; i < response.length; i++) {
|
||||
result[response[i]["id"] as String] =
|
||||
List<Map<String, dynamic>>.from(response[i]["result"] as List);
|
||||
result.add(List<Map<String, dynamic>>.from(response[i] as List));
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
|
@ -654,23 +686,37 @@ class ElectrumXClient {
|
|||
scripthash,
|
||||
],
|
||||
);
|
||||
return List<Map<String, dynamic>>.from(response["result"] as List);
|
||||
return List<Map<String, dynamic>>.from(response as List);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, List<Map<String, dynamic>>>> getBatchUTXOs(
|
||||
{required Map<String, List<dynamic>> args}) async {
|
||||
Future<List<List<Map<String, dynamic>>>> getBatchUTXOs({
|
||||
required List<dynamic> args,
|
||||
}) async {
|
||||
try {
|
||||
final response = await batchRequest(
|
||||
command: 'blockchain.scripthash.listunspent',
|
||||
args: args,
|
||||
);
|
||||
final Map<String, List<Map<String, dynamic>>> result = {};
|
||||
final List<List<Map<String, dynamic>>> result = [];
|
||||
for (int i = 0; i < response.length; i++) {
|
||||
result[response[i]["id"] as String] =
|
||||
List<Map<String, dynamic>>.from(response[i]["result"] as List);
|
||||
if ((response[i] as List).isNotEmpty) {
|
||||
try {
|
||||
final data = List<Map<String, dynamic>>.from(response[i] as List);
|
||||
result.add(data);
|
||||
} catch (e) {
|
||||
// to ensure we keep same length of responses as requests/args
|
||||
// add empty list on error
|
||||
result.add([]);
|
||||
|
||||
Logging.instance.log(
|
||||
"getBatchUTXOs failed to parse response=${response[i]}: $e",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
|
@ -730,36 +776,18 @@ class ElectrumXClient {
|
|||
bool verbose = true,
|
||||
String? requestID,
|
||||
}) async {
|
||||
dynamic response;
|
||||
try {
|
||||
response = await request(
|
||||
requestID: requestID,
|
||||
command: 'blockchain.transaction.get',
|
||||
args: [
|
||||
txHash,
|
||||
verbose,
|
||||
],
|
||||
);
|
||||
if (!verbose) {
|
||||
return {"rawtx": response["result"] as String};
|
||||
}
|
||||
Logging.instance.log("attempting to fetch blockchain.transaction.get...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
dynamic response = await _electrumAdapterClient!.getTransaction(txHash);
|
||||
Logging.instance.log("Fetching blockchain.transaction.get finished",
|
||||
level: LogLevel.Info);
|
||||
|
||||
if (response["result"] == null) {
|
||||
Logging.instance.log(
|
||||
"getTransaction($txHash) returned null response",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
throw 'getTransaction($txHash) returned null response';
|
||||
}
|
||||
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
} catch (e) {
|
||||
Logging.instance.log(
|
||||
"getTransaction($txHash) response: $response",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
if (!verbose) {
|
||||
return {"rawtx": response as String};
|
||||
}
|
||||
|
||||
return Map<String, dynamic>.from(response as Map);
|
||||
}
|
||||
|
||||
/// Returns the whole Lelantus anonymity set for denomination in the groupId.
|
||||
|
@ -781,23 +809,15 @@ class ElectrumXClient {
|
|||
String blockhash = "",
|
||||
String? requestID,
|
||||
}) async {
|
||||
try {
|
||||
Logging.instance.log("attempting to fetch lelantus.getanonymityset...",
|
||||
level: LogLevel.Info);
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'lelantus.getanonymityset',
|
||||
args: [
|
||||
groupId,
|
||||
blockhash,
|
||||
],
|
||||
);
|
||||
Logging.instance.log("Fetching lelantus.getanonymityset finished",
|
||||
level: LogLevel.Info);
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
Logging.instance.log("attempting to fetch lelantus.getanonymityset...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
Map<String, dynamic> response =
|
||||
await (_electrumAdapterClient as FiroElectrumClient)!
|
||||
.getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash);
|
||||
Logging.instance.log("Fetching lelantus.getanonymityset finished",
|
||||
level: LogLevel.Info);
|
||||
return response;
|
||||
}
|
||||
|
||||
//TODO add example to docs
|
||||
|
@ -808,18 +828,14 @@ class ElectrumXClient {
|
|||
dynamic mints,
|
||||
String? requestID,
|
||||
}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'lelantus.getmintmetadata',
|
||||
args: [
|
||||
mints,
|
||||
],
|
||||
);
|
||||
return response["result"];
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
Logging.instance.log("attempting to fetch lelantus.getmintmetadata...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
dynamic response = await (_electrumAdapterClient as FiroElectrumClient)!
|
||||
.getLelantusMintData(mints: mints);
|
||||
Logging.instance.log("Fetching lelantus.getmintmetadata finished",
|
||||
level: LogLevel.Info);
|
||||
return response;
|
||||
}
|
||||
|
||||
//TODO add example to docs
|
||||
|
@ -828,45 +844,38 @@ class ElectrumXClient {
|
|||
String? requestID,
|
||||
required int startNumber,
|
||||
}) async {
|
||||
try {
|
||||
int retryCount = 3;
|
||||
dynamic result;
|
||||
Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
|
||||
while (retryCount > 0 && result is! List) {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'lelantus.getusedcoinserials',
|
||||
args: [
|
||||
"$startNumber",
|
||||
],
|
||||
requestTimeout: const Duration(minutes: 2),
|
||||
);
|
||||
int retryCount = 3;
|
||||
dynamic response;
|
||||
|
||||
result = response["result"];
|
||||
retryCount--;
|
||||
}
|
||||
while (retryCount > 0 && response is! List) {
|
||||
response = await (_electrumAdapterClient as FiroElectrumClient)!
|
||||
.getLelantusUsedCoinSerials(startNumber: startNumber);
|
||||
// TODO add 2 minute timeout.
|
||||
Logging.instance.log("Fetching lelantus.getusedcoinserials finished",
|
||||
level: LogLevel.Info);
|
||||
|
||||
return Map<String, dynamic>.from(result as Map);
|
||||
} catch (e) {
|
||||
Logging.instance.log(e, level: LogLevel.Error);
|
||||
rethrow;
|
||||
retryCount--;
|
||||
}
|
||||
|
||||
return Map<String, dynamic>.from(response as Map);
|
||||
}
|
||||
|
||||
/// Returns the latest Lelantus set id
|
||||
///
|
||||
/// ex: 1
|
||||
Future<int> getLelantusLatestCoinId({String? requestID}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'lelantus.getlatestcoinid',
|
||||
);
|
||||
return response["result"] as int;
|
||||
} catch (e) {
|
||||
Logging.instance.log(e, level: LogLevel.Error);
|
||||
rethrow;
|
||||
}
|
||||
Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
int response =
|
||||
await (_electrumAdapterClient as FiroElectrumClient).getLatestCoinId();
|
||||
Logging.instance.log("Fetching lelantus.getlatestcoinid finished",
|
||||
level: LogLevel.Info);
|
||||
return response;
|
||||
}
|
||||
|
||||
// ============== Spark ======================================================
|
||||
|
@ -892,17 +901,14 @@ class ElectrumXClient {
|
|||
try {
|
||||
Logging.instance.log("attempting to fetch spark.getsparkanonymityset...",
|
||||
level: LogLevel.Info);
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'spark.getsparkanonymityset',
|
||||
args: [
|
||||
coinGroupId,
|
||||
startBlockHash,
|
||||
],
|
||||
);
|
||||
await checkElectrumAdapter();
|
||||
Map<String, dynamic> response =
|
||||
await (_electrumAdapterClient as FiroElectrumClient)
|
||||
.getSparkAnonymitySet(
|
||||
coinGroupId: coinGroupId, startBlockHash: startBlockHash);
|
||||
Logging.instance.log("Fetching spark.getsparkanonymityset finished",
|
||||
level: LogLevel.Info);
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
return response;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -915,15 +921,17 @@ class ElectrumXClient {
|
|||
required int startNumber,
|
||||
}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'spark.getusedcoinstags',
|
||||
args: [
|
||||
"$startNumber",
|
||||
],
|
||||
requestTimeout: const Duration(minutes: 2),
|
||||
);
|
||||
final map = Map<String, dynamic>.from(response["result"] as Map);
|
||||
// Use electrum_adapter package's getSparkUsedCoinsTags method.
|
||||
Logging.instance.log("attempting to fetch spark.getusedcoinstags...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
Map<String, dynamic> response =
|
||||
await (_electrumAdapterClient as FiroElectrumClient)
|
||||
.getUsedCoinsTags(startNumber: startNumber);
|
||||
// TODO: Add 2 minute timeout.
|
||||
Logging.instance.log("Fetching spark.getusedcoinstags finished",
|
||||
level: LogLevel.Info);
|
||||
final map = Map<String, dynamic>.from(response);
|
||||
final set = Set<String>.from(map["tags"] as List);
|
||||
return await compute(_ffiHashTagsComputeWrapper, set);
|
||||
} catch (e) {
|
||||
|
@ -947,16 +955,15 @@ class ElectrumXClient {
|
|||
required List<String> sparkCoinHashes,
|
||||
}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'spark.getsparkmintmetadata',
|
||||
args: [
|
||||
{
|
||||
"coinHashes": sparkCoinHashes,
|
||||
},
|
||||
],
|
||||
);
|
||||
return List<Map<String, dynamic>>.from(response["result"] as List);
|
||||
Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
List<dynamic> response =
|
||||
await (_electrumAdapterClient as FiroElectrumClient)
|
||||
.getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
|
||||
Logging.instance.log("Fetching spark.getsparkmintmetadata finished",
|
||||
level: LogLevel.Info);
|
||||
return List<Map<String, dynamic>>.from(response);
|
||||
} catch (e) {
|
||||
Logging.instance.log(e, level: LogLevel.Error);
|
||||
rethrow;
|
||||
|
@ -970,11 +977,14 @@ class ElectrumXClient {
|
|||
String? requestID,
|
||||
}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'spark.getsparklatestcoinid',
|
||||
);
|
||||
return response["result"] as int;
|
||||
Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
int response = await (_electrumAdapterClient as FiroElectrumClient)
|
||||
.getSparkLatestCoinId();
|
||||
Logging.instance.log("Fetching spark.getsparklatestcoinid finished",
|
||||
level: LogLevel.Info);
|
||||
return response;
|
||||
} catch (e) {
|
||||
Logging.instance.log(e, level: LogLevel.Error);
|
||||
rethrow;
|
||||
|
@ -991,15 +1001,8 @@ class ElectrumXClient {
|
|||
/// "rate": 1000,
|
||||
/// }
|
||||
Future<Map<String, dynamic>> getFeeRate({String? requestID}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'blockchain.getfeerate',
|
||||
);
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
await checkElectrumAdapter();
|
||||
return await _electrumAdapterClient!.getFeeRate();
|
||||
}
|
||||
|
||||
/// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of [blocks].
|
||||
|
@ -1016,7 +1019,26 @@ class ElectrumXClient {
|
|||
blocks,
|
||||
],
|
||||
);
|
||||
return Decimal.parse(response["result"].toString());
|
||||
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 &&
|
||||
(response == null ||
|
||||
response == -1 ||
|
||||
Decimal.parse(response.toString()) == Decimal.parse("-1"))) {
|
||||
// Return 0.05 for slow, 0.2 for average, and 1 for fast txs.
|
||||
// These numbers produce tx fees in line with txs in the wild on
|
||||
// https://dogechain.info/
|
||||
return Decimal.parse((1 / blocks).toString());
|
||||
// TODO [prio=med]: Fix fee estimation.
|
||||
}
|
||||
return Decimal.parse(response.toString());
|
||||
} catch (e, s) {
|
||||
final String msg = "Error parsing fee rate. Response: $response"
|
||||
"\nResult: ${response}\nError: $e\nStack trace: $s";
|
||||
Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
throw Exception(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -1033,7 +1055,7 @@ class ElectrumXClient {
|
|||
requestID: requestID,
|
||||
command: 'blockchain.relayfee',
|
||||
);
|
||||
return Decimal.parse(response["result"].toString());
|
||||
return Decimal.parse(response.toString());
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
|
|
@ -213,7 +213,7 @@ class JsonRPC {
|
|||
port,
|
||||
timeout: connectionTimeout,
|
||||
onBadCertificate: (_) => true,
|
||||
); // TODO do not automatically trust bad certificates
|
||||
); // TODO do not automatically trust bad certificates.
|
||||
} else {
|
||||
_socket = await Socket.connect(
|
||||
host,
|
||||
|
|
|
@ -12,16 +12,25 @@
|
|||
// import 'dart:convert';
|
||||
// import 'dart:io';
|
||||
//
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:event_bus/event_bus.dart';
|
||||
// import 'package:mutex/mutex.dart';
|
||||
// import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
|
||||
// import 'package:stackwallet/exceptions/json_rpc/json_rpc_exception.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/logger.dart';
|
||||
// import 'package:stackwallet/utilities/prefs.dart';
|
||||
// import 'package:tor_ffi_plugin/socks_socket.dart';
|
||||
//
|
||||
// class ElectrumXSubscription with ChangeNotifier {
|
||||
// dynamic _response;
|
||||
// dynamic get response => _response;
|
||||
// set response(dynamic newData) {
|
||||
// _response = newData;
|
||||
// notifyListeners();
|
||||
// }
|
||||
// class ElectrumXSubscription {
|
||||
// final StreamController<dynamic> _controller =
|
||||
// StreamController(); // TODO controller params
|
||||
//
|
||||
// Stream<dynamic> get responseStream => _controller.stream;
|
||||
//
|
||||
// void addToStream(dynamic data) => _controller.add(data);
|
||||
// }
|
||||
//
|
||||
// class SocketTask {
|
||||
|
@ -40,67 +49,430 @@
|
|||
// final Map<String, SocketTask> _tasks = {};
|
||||
// Timer? _aliveTimer;
|
||||
// Socket? _socket;
|
||||
// SOCKSSocket? _socksSocket;
|
||||
// late final bool _useSSL;
|
||||
// late final Duration _connectionTimeout;
|
||||
// late final Duration _keepAlive;
|
||||
//
|
||||
// bool get isConnected => _isConnected;
|
||||
// bool get useSSL => _useSSL;
|
||||
// // Used to reconnect.
|
||||
// String? _host;
|
||||
// int? _port;
|
||||
//
|
||||
// void Function(bool)? onConnectionStatusChanged;
|
||||
//
|
||||
// late Prefs _prefs;
|
||||
// late TorService _torService;
|
||||
// StreamSubscription<TorPreferenceChangedEvent>? _torPreferenceListener;
|
||||
// StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
|
||||
// final Mutex _torConnectingLock = Mutex();
|
||||
// bool _requireMutex = false;
|
||||
//
|
||||
// List<ElectrumXNode>? failovers;
|
||||
// int currentFailoverIndex = -1;
|
||||
//
|
||||
// SubscribableElectrumXClient({
|
||||
// bool useSSL = true,
|
||||
// required bool useSSL,
|
||||
// required Prefs prefs,
|
||||
// required List<ElectrumXNode> failovers,
|
||||
// TorService? torService,
|
||||
// this.onConnectionStatusChanged,
|
||||
// Duration connectionTimeout = const Duration(seconds: 5),
|
||||
// Duration keepAlive = const Duration(seconds: 10),
|
||||
// EventBus? globalEventBusForTesting,
|
||||
// }) {
|
||||
// _useSSL = useSSL;
|
||||
// _prefs = prefs;
|
||||
// _torService = torService ?? TorService.sharedInstance;
|
||||
// _connectionTimeout = connectionTimeout;
|
||||
// _keepAlive = keepAlive;
|
||||
//
|
||||
// // If we're testing, use the global event bus for testing.
|
||||
// final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
|
||||
//
|
||||
// // Listen to global event bus for Tor status changes.
|
||||
// _torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
|
||||
// (event) async {
|
||||
// try {
|
||||
// switch (event.newStatus) {
|
||||
// case TorConnectionStatus.connecting:
|
||||
// // If Tor is connecting, we need to wait.
|
||||
// await _torConnectingLock.acquire();
|
||||
// _requireMutex = true;
|
||||
// break;
|
||||
//
|
||||
// case TorConnectionStatus.connected:
|
||||
// case TorConnectionStatus.disconnected:
|
||||
// // If Tor is connected or disconnected, we can release the lock.
|
||||
// if (_torConnectingLock.isLocked) {
|
||||
// _torConnectingLock.release();
|
||||
// }
|
||||
// _requireMutex = false;
|
||||
// break;
|
||||
// }
|
||||
// } finally {
|
||||
// // Ensure the lock is released.
|
||||
// if (_torConnectingLock.isLocked) {
|
||||
// _torConnectingLock.release();
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
//
|
||||
// // Listen to global event bus for Tor preference changes.
|
||||
// _torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen(
|
||||
// (event) async {
|
||||
// // Close open socket (if open).
|
||||
// final tempSocket = _socket;
|
||||
// _socket = null;
|
||||
// await tempSocket?.close();
|
||||
//
|
||||
// // Close open SOCKS socket (if open).
|
||||
// final tempSOCKSSocket = _socksSocket;
|
||||
// _socksSocket = null;
|
||||
// await tempSOCKSSocket?.close();
|
||||
//
|
||||
// // Clear subscriptions.
|
||||
// _tasks.clear();
|
||||
//
|
||||
// // Cancel alive timer
|
||||
// _aliveTimer?.cancel();
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// Future<void> connect({required String host, required int port}) async {
|
||||
// try {
|
||||
// await _socket?.close();
|
||||
// } catch (_) {}
|
||||
// factory SubscribableElectrumXClient.from({
|
||||
// required ElectrumXNode node,
|
||||
// required Prefs prefs,
|
||||
// required List<ElectrumXNode> failovers,
|
||||
// TorService? torService,
|
||||
// }) {
|
||||
// return SubscribableElectrumXClient(
|
||||
// useSSL: node.useSSL,
|
||||
// prefs: prefs,
|
||||
// failovers: failovers,
|
||||
// torService: torService ?? TorService.sharedInstance,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// if (_useSSL) {
|
||||
// _socket = await SecureSocket.connect(
|
||||
// host,
|
||||
// port,
|
||||
// timeout: _connectionTimeout,
|
||||
// onBadCertificate: (_) => true,
|
||||
// );
|
||||
// } else {
|
||||
// _socket = await Socket.connect(
|
||||
// host,
|
||||
// port,
|
||||
// timeout: _connectionTimeout,
|
||||
// );
|
||||
// // Example for returning a future which completes upon connection.
|
||||
// // static Future<SubscribableElectrumXClient> from({
|
||||
// // required ElectrumXNode node,
|
||||
// // TorService? torService,
|
||||
// // }) async {
|
||||
// // final client = SubscribableElectrumXClient(
|
||||
// // useSSL: node.useSSL,
|
||||
// // );
|
||||
// //
|
||||
// // await client.connect(host: node.address, port: node.port);
|
||||
// //
|
||||
// // return client;
|
||||
// // }
|
||||
//
|
||||
// /// Check if the RPC client is connected and connect if needed.
|
||||
// ///
|
||||
// /// If Tor is enabled but not running, it will attempt to start Tor.
|
||||
// Future<void> _checkSocket({bool connecting = false}) async {
|
||||
// if (_prefs.useTor) {
|
||||
// // If we're supposed to use Tor...
|
||||
// if (_torService.status != TorConnectionStatus.connected) {
|
||||
// // ... but Tor isn't running...
|
||||
// if (!_prefs.torKillSwitch) {
|
||||
// // ... and the killswitch isn't set, then we'll just return below.
|
||||
// Logging.instance.log(
|
||||
// "Tor preference set but Tor is not enabled, killswitch not set, connecting to ElectrumX through clearnet.",
|
||||
// level: LogLevel.Warning,
|
||||
// );
|
||||
// } else {
|
||||
// // ... but if the killswitch is set, then let's try to start Tor.
|
||||
// await _torService.start();
|
||||
// // TODO [prio=low]: Attempt to restart Tor if needed. Update Tor package for restart feature.
|
||||
//
|
||||
// // Double-check that Tor is running.
|
||||
// if (_torService.status != TorConnectionStatus.connected) {
|
||||
// // If Tor still isn't running, then we'll throw an exception.
|
||||
// throw Exception("SubscribableElectrumXClient._checkRpcClient: "
|
||||
// "Tor preference and killswitch set but Tor not enabled and could not start, not connecting to ElectrumX.");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _updateConnectionStatus(true);
|
||||
//
|
||||
// _socket!.listen(
|
||||
// _dataHandler,
|
||||
// onError: _errorHandler,
|
||||
// onDone: _doneHandler,
|
||||
// cancelOnError: true,
|
||||
// );
|
||||
// // Connect if needed.
|
||||
// if (!connecting) {
|
||||
// if ((!_prefs.useTor && _socket == null) ||
|
||||
// (_prefs.useTor && _socksSocket == null)) {
|
||||
// if (currentFailoverIndex == -1) {
|
||||
// // Check if we have cached node information
|
||||
// if (_host == null && _port == null) {
|
||||
// throw Exception("SubscribableElectrumXClient._checkRpcClient: "
|
||||
// "No host or port provided and no cached node information.");
|
||||
// }
|
||||
//
|
||||
// _aliveTimer?.cancel();
|
||||
// _aliveTimer = Timer.periodic(
|
||||
// _keepAlive,
|
||||
// (_) async => _updateConnectionStatus(await ping()),
|
||||
// );
|
||||
// // Connect to the server.
|
||||
// await connect(host: _host!, port: _port!);
|
||||
// } else {
|
||||
// // Attempt to connect to the next failover server.
|
||||
// await connect(
|
||||
// host: failovers![currentFailoverIndex].address,
|
||||
// port: failovers![currentFailoverIndex].port,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Connect to the server.
|
||||
// ///
|
||||
// /// If Tor is enabled, it will attempt to connect through Tor.
|
||||
// Future<void> connect({
|
||||
// required String host,
|
||||
// required int port,
|
||||
// }) async {
|
||||
// try {
|
||||
// // Cache node information.
|
||||
// _host = host;
|
||||
// _port = port;
|
||||
//
|
||||
// // If we're already connected, disconnect first.
|
||||
// try {
|
||||
// await _socket?.close();
|
||||
// } catch (_) {}
|
||||
//
|
||||
// // If we're connecting to Tor, wait.
|
||||
// if (_requireMutex) {
|
||||
// await _torConnectingLock
|
||||
// .protect(() async => await _checkSocket(connecting: true));
|
||||
// } else {
|
||||
// await _checkSocket(connecting: true);
|
||||
// }
|
||||
//
|
||||
// if (!Prefs.instance.useTor) {
|
||||
// // If we're not supposed to use Tor, then connect directly.
|
||||
// await connectClearnet(host, port);
|
||||
// } else {
|
||||
// // If we're supposed to use Tor...
|
||||
// if (_torService.status != TorConnectionStatus.connected) {
|
||||
// // ... but Tor isn't running...
|
||||
// if (!_prefs.torKillSwitch) {
|
||||
// // ... and the killswitch isn't set, then we'll connect clearnet.
|
||||
// Logging.instance.log(
|
||||
// "Tor preference set but Tor not enabled, no killswitch set, connecting to ElectrumX through clearnet",
|
||||
// level: LogLevel.Warning,
|
||||
// );
|
||||
// await connectClearnet(host, port);
|
||||
// } else {
|
||||
// // ... but if the killswitch is set, then let's try to start Tor.
|
||||
// await _torService.start();
|
||||
// // TODO [prio=low]: Attempt to restart Tor if needed. Update Tor package for restart feature.
|
||||
//
|
||||
// // Doublecheck that Tor is running.
|
||||
// if (_torService.status != TorConnectionStatus.connected) {
|
||||
// // If Tor still isn't running, then we'll throw an exception.
|
||||
// throw Exception(
|
||||
// "Tor preference and killswitch set but Tor not enabled, not connecting to ElectrumX");
|
||||
// }
|
||||
//
|
||||
// // Connect via Tor.
|
||||
// await connectTor(host, port);
|
||||
// }
|
||||
// } else {
|
||||
// // Connect via Tor.
|
||||
// await connectTor(host, port);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// _updateConnectionStatus(true);
|
||||
//
|
||||
// if (_prefs.useTor) {
|
||||
// if (_socksSocket == null) {
|
||||
// final String msg = "SubscribableElectrumXClient.connect(): "
|
||||
// "cannot listen to $host:$port via SOCKSSocket because it is not connected.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
//
|
||||
// _socksSocket!.listen(
|
||||
// _dataHandler,
|
||||
// onError: _errorHandler,
|
||||
// onDone: _doneHandler,
|
||||
// cancelOnError: true,
|
||||
// );
|
||||
// } else {
|
||||
// if (_socket == null) {
|
||||
// final String msg = "SubscribableElectrumXClient.connect(): "
|
||||
// "cannot listen to $host:$port via socket because it is not connected.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
//
|
||||
// _socket!.listen(
|
||||
// _dataHandler,
|
||||
// onError: _errorHandler,
|
||||
// onDone: _doneHandler,
|
||||
// cancelOnError: true,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// _aliveTimer?.cancel();
|
||||
// _aliveTimer = Timer.periodic(
|
||||
// _keepAlive,
|
||||
// (_) async => _updateConnectionStatus(await ping()),
|
||||
// );
|
||||
// } catch (e, s) {
|
||||
// final msg = "SubscribableElectrumXClient.connect: "
|
||||
// "failed to connect to $host:$port."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
//
|
||||
// // Ensure cleanup is performed on failure to avoid resource leaks.
|
||||
// await disconnect(); // Use the disconnect method to clean up.
|
||||
// rethrow; // Rethrow the exception to handle it further up the call stack.
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Connect to the server directly.
|
||||
// Future<void> connectClearnet(String host, int port) async {
|
||||
// try {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectClearnet(): "
|
||||
// "creating a socket to $host:$port (SSL $useSSL)...",
|
||||
// level: LogLevel.Info);
|
||||
//
|
||||
// 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,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectClearnet(): "
|
||||
// "created socket to $host:$port...",
|
||||
// level: LogLevel.Info);
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient.connectClearnet: "
|
||||
// "failed to connect to $host (SSL: $useSSL)."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// /// Connect to the server using the Tor service.
|
||||
// Future<void> connectTor(String host, int port) async {
|
||||
// // Get the proxy info from the TorService.
|
||||
// final proxyInfo = _torService.getProxyInfo();
|
||||
//
|
||||
// try {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "creating a SOCKS socket at $proxyInfo (SSL $useSSL)...",
|
||||
// level: LogLevel.Info);
|
||||
//
|
||||
// // Create a socks socket using the Tor service's proxy info.
|
||||
// _socksSocket = await SOCKSSocket.create(
|
||||
// proxyHost: proxyInfo.host.address,
|
||||
// proxyPort: proxyInfo.port,
|
||||
// sslEnabled: useSSL,
|
||||
// );
|
||||
//
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "created SOCKS socket at $proxyInfo...",
|
||||
// level: LogLevel.Info);
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient.connectTor(): "
|
||||
// "failed to create a SOCKS socket at $proxyInfo (SSL $useSSL)..."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "connecting to SOCKS socket at $proxyInfo (SSL $useSSL)...",
|
||||
// level: LogLevel.Info);
|
||||
//
|
||||
// await _socksSocket?.connect();
|
||||
//
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "connected to SOCKS socket at $proxyInfo...",
|
||||
// level: LogLevel.Info);
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient.connectTor(): "
|
||||
// "failed to connect to SOCKS socket at $proxyInfo.."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "connecting to $host:$port over SOCKS socket at $proxyInfo...",
|
||||
// level: LogLevel.Info);
|
||||
//
|
||||
// await _socksSocket?.connectTo(host, port);
|
||||
//
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "connected to $host:$port over SOCKS socket at $proxyInfo",
|
||||
// level: LogLevel.Info);
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient.connectTor(): "
|
||||
// "failed to connect $host over tor proxy at $proxyInfo."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// /// Disconnect from the server.
|
||||
// Future<void> disconnect() async {
|
||||
// _aliveTimer?.cancel();
|
||||
// await _socket?.close();
|
||||
// _aliveTimer = null;
|
||||
//
|
||||
// try {
|
||||
// await _socket?.close();
|
||||
// } catch (e, s) {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.disconnect: failed to close socket."
|
||||
// "\nError: $e\nStack trace: $s",
|
||||
// level: LogLevel.Warning);
|
||||
// }
|
||||
// _socket = null;
|
||||
//
|
||||
// try {
|
||||
// await _socksSocket?.close();
|
||||
// } catch (e, s) {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.disconnect: failed to close SOCKS socket."
|
||||
// "\nError: $e\nStack trace: $s",
|
||||
// level: LogLevel.Warning);
|
||||
// }
|
||||
// _socksSocket = null;
|
||||
//
|
||||
// onConnectionStatusChanged = null;
|
||||
// }
|
||||
//
|
||||
// /// Format JSON request string.
|
||||
// String _buildJsonRequestString({
|
||||
// required String method,
|
||||
// required String id,
|
||||
|
@ -110,6 +482,7 @@
|
|||
// return '{"jsonrpc": "2.0", "id": "$id","method": "$method","params": $paramString}\r\n';
|
||||
// }
|
||||
//
|
||||
// /// Update the connection status and call the onConnectionStatusChanged callback if it exists.
|
||||
// void _updateConnectionStatus(bool connectionStatus) {
|
||||
// if (_isConnected != connectionStatus && onConnectionStatusChanged != null) {
|
||||
// onConnectionStatusChanged!(connectionStatus);
|
||||
|
@ -117,6 +490,7 @@
|
|||
// _isConnected = connectionStatus;
|
||||
// }
|
||||
//
|
||||
// /// Called when the socket has data.
|
||||
// void _dataHandler(List<int> data) {
|
||||
// _responseData.addAll(data);
|
||||
//
|
||||
|
@ -137,6 +511,7 @@
|
|||
// }
|
||||
// }
|
||||
//
|
||||
// /// Called when the socket has a response.
|
||||
// void _responseHandler(Map<String, dynamic> response) {
|
||||
// // subscriptions will have a method in the response
|
||||
// if (response['method'] is String) {
|
||||
|
@ -150,6 +525,7 @@
|
|||
// _complete(id, result);
|
||||
// }
|
||||
//
|
||||
// /// Called when the subscription has a response.
|
||||
// void _subscriptionHandler({
|
||||
// required Map<String, dynamic> response,
|
||||
// }) {
|
||||
|
@ -160,19 +536,20 @@
|
|||
// final scripthash = params.first as String;
|
||||
// final taskId = "blockchain.scripthash.subscribe:$scripthash";
|
||||
//
|
||||
// _tasks[taskId]?.subscription?.response = params.last;
|
||||
// _tasks[taskId]?.subscription?.addToStream(params.last);
|
||||
// break;
|
||||
// case "blockchain.headers.subscribe":
|
||||
// final params = response["params"];
|
||||
// const taskId = "blockchain.headers.subscribe";
|
||||
//
|
||||
// _tasks[taskId]?.subscription?.response = params.first;
|
||||
// _tasks[taskId]?.subscription?.addToStream(params.first);
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Called when the socket has an error.
|
||||
// void _errorHandler(Object error, StackTrace trace) {
|
||||
// _updateConnectionStatus(false);
|
||||
// Logging.instance.log(
|
||||
|
@ -180,12 +557,14 @@
|
|||
// level: LogLevel.Info);
|
||||
// }
|
||||
//
|
||||
// /// Called when the socket is closed.
|
||||
// void _doneHandler() {
|
||||
// _updateConnectionStatus(false);
|
||||
// Logging.instance.log("SubscribableElectrumXClient called _doneHandler",
|
||||
// level: LogLevel.Info);
|
||||
// }
|
||||
//
|
||||
// /// Complete a task with the given id and data.
|
||||
// void _complete(String id, dynamic data) {
|
||||
// if (_tasks[id] == null) {
|
||||
// return;
|
||||
|
@ -198,10 +577,11 @@
|
|||
// if (!(_tasks[id]?.isSubscription ?? false)) {
|
||||
// _tasks.remove(id);
|
||||
// } else {
|
||||
// _tasks[id]?.subscription?.response = data;
|
||||
// _tasks[id]?.subscription?.addToStream(data);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Add a task to the task list.
|
||||
// void _addTask({
|
||||
// required String id,
|
||||
// required Completer<dynamic> completer,
|
||||
|
@ -209,6 +589,7 @@
|
|||
// _tasks[id] = SocketTask(completer: completer, subscription: null);
|
||||
// }
|
||||
//
|
||||
// /// Add a subscription task to the task list.
|
||||
// void _addSubscriptionTask({
|
||||
// required String id,
|
||||
// required ElectrumXSubscription subscription,
|
||||
|
@ -216,83 +597,240 @@
|
|||
// _tasks[id] = SocketTask(completer: null, subscription: subscription);
|
||||
// }
|
||||
//
|
||||
// /// Write call to socket.
|
||||
// Future<dynamic> _call({
|
||||
// required String method,
|
||||
// List<dynamic> params = const [],
|
||||
// }) async {
|
||||
// // If we're connecting to Tor, wait.
|
||||
// if (_requireMutex) {
|
||||
// await _torConnectingLock.protect(() async => await _checkSocket());
|
||||
// } else {
|
||||
// await _checkSocket();
|
||||
// }
|
||||
//
|
||||
// // Check socket is connected.
|
||||
// if (_prefs.useTor) {
|
||||
// if (_socksSocket == null) {
|
||||
// final msg = "SubscribableElectrumXClient._call: "
|
||||
// "SOCKSSocket is not connected. Method $method, params $params.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// } else {
|
||||
// if (_socket == null) {
|
||||
// final msg = "SubscribableElectrumXClient._call: "
|
||||
// "Socket is not connected. Method $method, params $params.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// final completer = Completer<dynamic>();
|
||||
// _currentRequestID++;
|
||||
// final id = _currentRequestID.toString();
|
||||
// _addTask(id: id, completer: completer);
|
||||
//
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// // Write to the socket.
|
||||
// try {
|
||||
// _addTask(id: id, completer: completer);
|
||||
//
|
||||
// return completer.future;
|
||||
// if (_prefs.useTor) {
|
||||
// _socksSocket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// return completer.future;
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient._call: "
|
||||
// "failed to request $method with id $id."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Write call to socket with timeout.
|
||||
// Future<dynamic> _callWithTimeout({
|
||||
// required String method,
|
||||
// List<dynamic> params = const [],
|
||||
// Duration timeout = const Duration(seconds: 2),
|
||||
// }) async {
|
||||
// // If we're connecting to Tor, wait.
|
||||
// if (_requireMutex) {
|
||||
// await _torConnectingLock.protect(() async => await _checkSocket());
|
||||
// } else {
|
||||
// await _checkSocket();
|
||||
// }
|
||||
//
|
||||
// // Check socket is connected.
|
||||
// if (_prefs.useTor) {
|
||||
// if (_socksSocket == null) {
|
||||
// try {
|
||||
// if (_host == null || _port == null) {
|
||||
// throw Exception("No host or port provided");
|
||||
// }
|
||||
//
|
||||
// // Attempt to conect.
|
||||
// await connect(
|
||||
// host: _host!,
|
||||
// port: _port!,
|
||||
// );
|
||||
// } catch (e, s) {
|
||||
// final msg = "SubscribableElectrumXClient._callWithTimeout: "
|
||||
// "SOCKSSocket not connected and cannot connect. "
|
||||
// "Method $method, params $params."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// if (_socket == null) {
|
||||
// try {
|
||||
// if (_host == null || _port == null) {
|
||||
// throw Exception("No host or port provided");
|
||||
// }
|
||||
//
|
||||
// // Attempt to conect.
|
||||
// await connect(
|
||||
// host: _host!,
|
||||
// port: _port!,
|
||||
// );
|
||||
// } catch (e, s) {
|
||||
// final msg = "SubscribableElectrumXClient._callWithTimeout: "
|
||||
// "Socket not connected and cannot connect. "
|
||||
// "Method $method, params $params.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// final completer = Completer<dynamic>();
|
||||
// _currentRequestID++;
|
||||
// final id = _currentRequestID.toString();
|
||||
// _addTask(id: id, completer: completer);
|
||||
//
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// // Write to the socket.
|
||||
// try {
|
||||
// _addTask(id: id, completer: completer);
|
||||
//
|
||||
// Timer(timeout, () {
|
||||
// if (!completer.isCompleted) {
|
||||
// completer.completeError(
|
||||
// Exception("Request \"id: $id, method: $method\" timed out!"),
|
||||
// if (_prefs.useTor) {
|
||||
// _socksSocket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// return completer.future;
|
||||
// Timer(timeout, () {
|
||||
// if (!completer.isCompleted) {
|
||||
// completer.completeError(
|
||||
// Exception("Request \"id: $id, method: $method\" timed out!"),
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// return completer.future;
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient._callWithTimeout: "
|
||||
// "failed to request $method with id $id (timeout $timeout)."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// ElectrumXSubscription _subscribe({
|
||||
// required String taskId,
|
||||
// required String id,
|
||||
// required String method,
|
||||
// List<dynamic> params = const [],
|
||||
// }) {
|
||||
// // try {
|
||||
// final subscription = ElectrumXSubscription();
|
||||
// _addSubscriptionTask(id: taskId, subscription: subscription);
|
||||
// _currentRequestID++;
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: taskId,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// try {
|
||||
// final subscription = ElectrumXSubscription();
|
||||
// _addSubscriptionTask(id: id, subscription: subscription);
|
||||
// _currentRequestID++;
|
||||
//
|
||||
// return subscription;
|
||||
// // } catch (e, s) {
|
||||
// // Logging.instance.log("SubscribableElectrumXClient _subscribe: $e\n$s", level: LogLevel.Error);
|
||||
// // return null;
|
||||
// // }
|
||||
// // Check socket is connected.
|
||||
// if (_prefs.useTor) {
|
||||
// if (_socksSocket == null) {
|
||||
// final msg = "SubscribableElectrumXClient._call: "
|
||||
// "SOCKSSocket is not connected. Method $method, params $params.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// } else {
|
||||
// if (_socket == null) {
|
||||
// final msg = "SubscribableElectrumXClient._call: "
|
||||
// "Socket is not connected. Method $method, params $params.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Write to the socket.
|
||||
// if (_prefs.useTor) {
|
||||
// _socksSocket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// return subscription;
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient._subscribe: "
|
||||
// "failed to subscribe to $method with id $id."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Ping the server to ensure it is responding
|
||||
// ///
|
||||
// /// Returns true if ping succeeded
|
||||
// Future<bool> ping() async {
|
||||
// // If we're connecting to Tor, wait.
|
||||
// if (_requireMutex) {
|
||||
// await _torConnectingLock.protect(() async => await _checkSocket());
|
||||
// } else {
|
||||
// await _checkSocket();
|
||||
// }
|
||||
//
|
||||
// // Write to the socket.
|
||||
// try {
|
||||
// final response = (await _callWithTimeout(method: "server.ping")) as Map;
|
||||
// return response.keys.contains("result") && response["result"] == null;
|
||||
|
@ -304,7 +842,7 @@
|
|||
// /// Subscribe to a scripthash to receive notifications on status changes
|
||||
// ElectrumXSubscription subscribeToScripthash({required String scripthash}) {
|
||||
// return _subscribe(
|
||||
// taskId: 'blockchain.scripthash.subscribe:$scripthash',
|
||||
// id: 'blockchain.scripthash.subscribe:$scripthash',
|
||||
// method: 'blockchain.scripthash.subscribe',
|
||||
// params: [scripthash],
|
||||
// );
|
||||
|
@ -316,7 +854,7 @@
|
|||
// ElectrumXSubscription subscribeToBlockHeaders() {
|
||||
// return _tasks["blockchain.headers.subscribe"]?.subscription ??
|
||||
// _subscribe(
|
||||
// taskId: "blockchain.headers.subscribe",
|
||||
// id: "blockchain.headers.subscribe",
|
||||
// method: "blockchain.headers.subscribe",
|
||||
// params: [],
|
||||
// );
|
||||
|
|
|
@ -68,7 +68,7 @@ class OutputV2 {
|
|||
scriptPubKeyHex: json["scriptPubKey"]["hex"] as String,
|
||||
scriptPubKeyAsm: json["scriptPubKey"]["asm"] as String?,
|
||||
valueStringSats: parseOutputAmountString(
|
||||
json["value"].toString(),
|
||||
json["value"] != null ? json["value"].toString(): "0",
|
||||
decimalPlaces: decimalPlaces,
|
||||
isFullAmountNotSats: isFullAmountNotSats,
|
||||
),
|
||||
|
|
|
@ -45,48 +45,55 @@ class CreateOrRestoreWalletView extends StatelessWidget {
|
|||
leading: AppBarBackButton(),
|
||||
trailing: ExitToMyStackButton(),
|
||||
),
|
||||
body: SizedBox(
|
||||
width: 480,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(
|
||||
flex: 10,
|
||||
body: SingleChildScrollView(
|
||||
child: Center(
|
||||
// Center the content horizontally
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
/*const Spacer(
|
||||
flex: 10,
|
||||
),*/
|
||||
CreateRestoreWalletTitle(
|
||||
coin: entity.coin,
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
SizedBox(
|
||||
width: 324,
|
||||
child: CreateRestoreWalletSubTitle(
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
CoinImage(
|
||||
coin: entity.coin,
|
||||
width: isDesktop
|
||||
? 324
|
||||
: MediaQuery.of(context).size.width / 1.6,
|
||||
height: isDesktop
|
||||
? null
|
||||
: MediaQuery.of(context).size.width / 1.6,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
CreateWalletButtonGroup(
|
||||
coin: entity.coin,
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
/*const Spacer(
|
||||
flex: 15,
|
||||
),*/
|
||||
],
|
||||
),
|
||||
CreateRestoreWalletTitle(
|
||||
coin: entity.coin,
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
SizedBox(
|
||||
width: 324,
|
||||
child: CreateRestoreWalletSubTitle(
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
CoinImage(
|
||||
coin: entity.coin,
|
||||
width:
|
||||
isDesktop ? 324 : MediaQuery.of(context).size.width / 1.6,
|
||||
height:
|
||||
isDesktop ? null : MediaQuery.of(context).size.width / 1.6,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
CreateWalletButtonGroup(
|
||||
coin: entity.coin,
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
const Spacer(
|
||||
flex: 15,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -726,12 +726,13 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
color: Theme.of(context).extension<StackColors>()!.background,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (isDesktop)
|
||||
/*if (isDesktop)
|
||||
const Spacer(
|
||||
flex: 10,
|
||||
),
|
||||
),*/
|
||||
if (!isDesktop)
|
||||
Text(
|
||||
widget.walletName,
|
||||
|
@ -1060,10 +1061,10 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
},
|
||||
),
|
||||
),
|
||||
if (isDesktop)
|
||||
/*if (isDesktop)
|
||||
const Spacer(
|
||||
flex: 15,
|
||||
),
|
||||
),*/
|
||||
if (!isDesktop)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
|
@ -1174,6 +1175,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:stackwallet/providers/global/secure_store_provider.dart';
|
||||
import 'package:stackwallet/providers/providers.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
|
@ -65,13 +66,21 @@ class _RestoreFailedDialogState extends ConsumerState<RestoreFailedDialog> {
|
|||
style: STextStyles.itemSubtitle12(context),
|
||||
),
|
||||
onPressed: () async {
|
||||
await ref.read(pWallets).deleteWallet(
|
||||
ref.read(pWalletInfo(walletId)),
|
||||
ref.read(secureStoreProvider),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
try {
|
||||
await ref.read(pWallets).deleteWallet(
|
||||
ref.read(pWalletInfo(walletId)),
|
||||
ref.read(secureStoreProvider),
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Error while getting wallet info in restore failed dialog\n"
|
||||
"Error: $e\nStack trace: $s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
@ -234,9 +236,13 @@ class _EditContactAddressViewState
|
|||
e.coin == addressEntry.coin,
|
||||
);
|
||||
|
||||
_addresses.remove(entry);
|
||||
//Deleting an entry directly from _addresses gives error
|
||||
// "Cannot remove from a fixed-length list", so we remove the
|
||||
// entry from a copy
|
||||
var tempAddresses = List<ContactAddressEntry>.from(_addresses);
|
||||
tempAddresses.remove(entry);
|
||||
ContactEntry editedContact =
|
||||
contact.copyWith(addresses: _addresses);
|
||||
contact.copyWith(addresses: tempAddresses);
|
||||
if (await ref
|
||||
.read(addressBookServiceProvider)
|
||||
.editContact(editedContact)) {
|
||||
|
|
|
@ -177,6 +177,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
|
|||
useSSL: formData.useSSL!,
|
||||
failovers: [],
|
||||
prefs: ref.read(prefsChangeNotifierProvider),
|
||||
coin: coin,
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
|
@ -154,6 +154,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
|
|||
useSSL: node.useSSL,
|
||||
failovers: [],
|
||||
prefs: ref.read(prefsChangeNotifierProvider),
|
||||
coin: coin,
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
|
@ -209,7 +209,7 @@ class _FiroRescanRecoveryErrorViewState
|
|||
children: [
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
Text(
|
||||
"Failed to rescan firo wallet",
|
||||
"Failed to rescan Firo wallet",
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
Util.isDesktop
|
||||
|
|
|
@ -178,7 +178,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
|
|||
),
|
||||
if (balanceSecondary != null)
|
||||
BalanceSelector(
|
||||
title: "Available lelantus balance",
|
||||
title: "Available Lelantus balance",
|
||||
coin: coin,
|
||||
balance: balanceSecondary.spendable,
|
||||
onPressed: () {
|
||||
|
@ -204,7 +204,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
|
|||
),
|
||||
if (balanceSecondary != null)
|
||||
BalanceSelector(
|
||||
title: "Full lelantus balance",
|
||||
title: "Full Lelantus balance",
|
||||
coin: coin,
|
||||
balance: balanceSecondary.total,
|
||||
onPressed: () {
|
||||
|
@ -230,7 +230,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
|
|||
),
|
||||
if (balanceTertiary != null)
|
||||
BalanceSelector(
|
||||
title: "Available spark balance",
|
||||
title: "Available Spark balance",
|
||||
coin: coin,
|
||||
balance: balanceTertiary.spendable,
|
||||
onPressed: () {
|
||||
|
@ -256,7 +256,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
|
|||
),
|
||||
if (balanceTertiary != null)
|
||||
BalanceSelector(
|
||||
title: "Full spark balance",
|
||||
title: "Full Spark balance",
|
||||
coin: coin,
|
||||
balance: balanceTertiary.total,
|
||||
onPressed: () {
|
||||
|
|
|
@ -909,7 +909,58 @@ class _TransactionV2DetailsViewState
|
|||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (coin == Coin.epicCash)
|
||||
RoundedWhiteContainer(
|
||||
padding: isDesktop
|
||||
? const EdgeInsets.all(16)
|
||||
: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"On chain note",
|
||||
style: isDesktop
|
||||
? STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
: STextStyles.itemSubtitle(
|
||||
context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
SelectableText(
|
||||
_transaction.onChainNote ?? "",
|
||||
style: isDesktop
|
||||
? STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textDark,
|
||||
)
|
||||
: STextStyles.itemSubtitle12(
|
||||
context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isDesktop)
|
||||
IconCopyButton(
|
||||
data: _transaction.onChainNote ?? "",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isDesktop
|
||||
? const _Divider()
|
||||
: const SizedBox(
|
||||
|
@ -996,7 +1047,9 @@ class _TransactionV2DetailsViewState
|
|||
.watch(
|
||||
pTransactionNote(
|
||||
(
|
||||
txid: _transaction.txid,
|
||||
txid: (coin == Coin.epicCash) ?
|
||||
_transaction.slateId as String
|
||||
: _transaction.txid,
|
||||
walletId: walletId
|
||||
),
|
||||
),
|
||||
|
|
|
@ -59,6 +59,7 @@ import 'package:stackwallet/utilities/clipboard_interface.dart';
|
|||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/enums/sync_type_enum.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/show_loading.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
|
@ -305,6 +306,26 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
BackupFrequencyType.afterClosingAWallet) {
|
||||
unawaited(ref.read(autoSWBServiceProvider).doBackup());
|
||||
}
|
||||
|
||||
// Close the wallet according to syncing preferences.
|
||||
switch (ref.read(prefsChangeNotifierProvider).syncType) {
|
||||
case SyncingType.currentWalletOnly:
|
||||
// Close the wallet.
|
||||
unawaited(ref.watch(pWallets).getWallet(walletId).exit());
|
||||
// unawaited so we don't lag the UI.
|
||||
case SyncingType.selectedWalletsAtStartup:
|
||||
// Close if this wallet is not in the list to be synced.
|
||||
if (!ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.walletIdsSyncOnStartup
|
||||
.contains(widget.walletId)) {
|
||||
unawaited(ref.watch(pWallets).getWallet(walletId).exit());
|
||||
// unawaited so we don't lag the UI.
|
||||
}
|
||||
case SyncingType.allWalletsOnStartup:
|
||||
// Do nothing.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildNetworkIcon(WalletSyncStatus status) {
|
||||
|
|
|
@ -61,7 +61,7 @@ class DesktopAddressCard extends ConsumerWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(
|
||||
"${contactId == "default" ? entry.other! : entry.label} (${entry.coin.ticker})",
|
||||
"${contactId == "default" ? entry.other : entry.label} (${entry.coin.ticker})",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
|
||||
color: Theme.of(context).extension<StackColors>()!.textDark,
|
||||
),
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
@ -38,7 +38,9 @@ import 'package:stackwallet/themes/coin_icon_provider.dart';
|
|||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
|
||||
import 'package:stackwallet/utilities/enums/sync_type_enum.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
||||
|
@ -92,6 +94,26 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
|||
unawaited(ref.read(autoSWBServiceProvider).doBackup());
|
||||
}
|
||||
|
||||
// Close the wallet according to syncing preferences.
|
||||
switch (ref.read(prefsChangeNotifierProvider).syncType) {
|
||||
case SyncingType.currentWalletOnly:
|
||||
// Close the wallet.
|
||||
unawaited(wallet.exit());
|
||||
// unawaited so we don't lag the UI.
|
||||
case SyncingType.selectedWalletsAtStartup:
|
||||
// Close if this wallet is not in the list to be synced.
|
||||
if (!ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.walletIdsSyncOnStartup
|
||||
.contains(widget.walletId)) {
|
||||
unawaited(wallet.exit());
|
||||
// unawaited so we don't lag the UI.
|
||||
}
|
||||
case SyncingType.allWalletsOnStartup:
|
||||
// Do nothing.
|
||||
break;
|
||||
}
|
||||
|
||||
ref.read(currentWalletIdProvider.notifier).state = null;
|
||||
}
|
||||
|
||||
|
@ -181,6 +203,21 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (kDebugMode) const Spacer(),
|
||||
if (kDebugMode)
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
"Debug Height:",
|
||||
),
|
||||
const SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
Text(
|
||||
ref.watch(pWalletChainHeight(widget.walletId)).toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
|
|
|
@ -105,7 +105,12 @@ class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> {
|
|||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(15.0),
|
||||
child: SettingsMenu(),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SingleChildScrollView(
|
||||
child: SettingsMenu(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: contentViews[
|
||||
|
|
|
@ -36,305 +36,308 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 30,
|
||||
),
|
||||
child: RoundedWhiteContainer(
|
||||
radiusMultiplier: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SvgPicture.asset(
|
||||
Assets.svg.circleSliders,
|
||||
width: 48,
|
||||
height: 48,
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 30,
|
||||
),
|
||||
child: RoundedWhiteContainer(
|
||||
radiusMultiplier: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SvgPicture.asset(
|
||||
Assets.svg.circleSliders,
|
||||
width: 48,
|
||||
height: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.start,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Advanced",
|
||||
style: STextStyles.desktopTextSmall(context),
|
||||
),
|
||||
TextSpan(
|
||||
text:
|
||||
"\n\nConfigure these settings only if you know what you are doing!",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context),
|
||||
),
|
||||
],
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.start,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Advanced",
|
||||
style: STextStyles.desktopTextSmall(context),
|
||||
),
|
||||
TextSpan(
|
||||
text:
|
||||
"\n\nConfigure these settings only if you know what you are doing!",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Toggle testnet coins",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
prefsChangeNotifierProvider
|
||||
.select((value) => value.showTestNetCoins),
|
||||
),
|
||||
onValueChanged: (newValue) {
|
||||
ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.showTestNetCoins = newValue;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Enable coin control",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
prefsChangeNotifierProvider
|
||||
.select((value) => value.enableCoinControl),
|
||||
),
|
||||
onValueChanged: (newValue) {
|
||||
ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.enableCoinControl = newValue;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
|
||||
/// TODO: Make a dialog popup
|
||||
Consumer(builder: (_, ref, __) {
|
||||
final externalCalls = ref.watch(
|
||||
prefsChangeNotifierProvider
|
||||
.select((value) => value.externalCalls),
|
||||
);
|
||||
return Padding(
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Stack Experience",
|
||||
style:
|
||||
STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Text(
|
||||
externalCalls ? "Easy crypto" : "Incognito",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
"Toggle testnet coins",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
prefsChangeNotifierProvider.select(
|
||||
(value) => value.showTestNetCoins),
|
||||
),
|
||||
onValueChanged: (newValue) {
|
||||
ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.showTestNetCoins = newValue;
|
||||
},
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Change",
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const StackPrivacyDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Block explorers",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Edit",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const DesktopManageBlockExplorersDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Enable coin control",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
prefsChangeNotifierProvider.select(
|
||||
(value) => value.enableCoinControl),
|
||||
),
|
||||
onValueChanged: (newValue) {
|
||||
ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.enableCoinControl = newValue;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
|
||||
/// TODO: Make a dialog popup
|
||||
Consumer(builder: (_, ref, __) {
|
||||
final externalCalls = ref.watch(
|
||||
prefsChangeNotifierProvider
|
||||
.select((value) => value.externalCalls),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Stack Experience",
|
||||
style: STextStyles.desktopTextExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Text(
|
||||
externalCalls ? "Easy crypto" : "Incognito",
|
||||
style:
|
||||
STextStyles.desktopTextExtraExtraSmall(
|
||||
context),
|
||||
),
|
||||
],
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Change",
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const StackPrivacyDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Units",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Edit",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const ManageCoinUnitsView();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Block explorers",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Edit",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const DesktopManageBlockExplorersDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Debug info",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Show logs",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const DebugInfoDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Units",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Edit",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const ManageCoinUnitsView();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
],
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Debug info",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Show logs",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const DebugInfoDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,10 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DesktopDialog(
|
||||
maxHeight: 850,
|
||||
// Max height of 850 unless the screen is smaller than that.
|
||||
maxHeight: MediaQuery.of(context).size.height < 850
|
||||
? MediaQuery.of(context).size.height
|
||||
: 850,
|
||||
maxWidth: 600,
|
||||
child: Column(
|
||||
children: [
|
||||
|
|
|
@ -146,6 +146,7 @@ class NotificationsService extends ChangeNotifier {
|
|||
node: eNode,
|
||||
failovers: failovers,
|
||||
prefs: prefs,
|
||||
coin: coin,
|
||||
);
|
||||
final tx = await client.getTransaction(txHash: txid);
|
||||
|
||||
|
|
|
@ -42,7 +42,13 @@ class Wallets {
|
|||
|
||||
final Map<String, Wallet> _wallets = {};
|
||||
|
||||
Wallet getWallet(String walletId) => _wallets[walletId]!;
|
||||
Wallet getWallet(String walletId) {
|
||||
if (_wallets[walletId] != null) {
|
||||
return _wallets[walletId]!;
|
||||
} else {
|
||||
throw Exception("Wallet with id $walletId not found");
|
||||
}
|
||||
}
|
||||
|
||||
void addWallet(Wallet wallet) {
|
||||
if (_wallets[wallet.walletId] != null) {
|
||||
|
|
|
@ -14,6 +14,24 @@ import 'package:bitcoindart/bitcoindart.dart';
|
|||
import 'package:crypto/crypto.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
||||
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/dogecoin.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/ecash.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/epiccash.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/ethereum.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/litecoin.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/namecoin.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart';
|
||||
|
||||
class AddressUtils {
|
||||
static String condenseAddress(String address) {
|
||||
|
@ -49,7 +67,55 @@ class AddressUtils {
|
|||
}
|
||||
|
||||
static bool validateAddress(String address, Coin coin) {
|
||||
throw Exception("moved");
|
||||
//This calls the validate address for each crypto coin, validateAddress is
|
||||
//only used in 2 places, so I just replaced the old functionality here
|
||||
switch (coin) {
|
||||
case Coin.bitcoin:
|
||||
return Bitcoin(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.litecoin:
|
||||
return Litecoin(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.bitcoincash:
|
||||
return Bitcoincash(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.dogecoin:
|
||||
return Dogecoin(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.epicCash:
|
||||
return Epiccash(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.ethereum:
|
||||
return Ethereum(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.firo:
|
||||
return Firo(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.eCash:
|
||||
return Ecash(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.monero:
|
||||
return Monero(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.wownero:
|
||||
return Wownero(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.namecoin:
|
||||
return Namecoin(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.particl:
|
||||
return Particl(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.stellar:
|
||||
return Stellar(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.nano:
|
||||
return Nano(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.banano:
|
||||
return Banano(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.tezos:
|
||||
return Tezos(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.bitcoinTestNet:
|
||||
return Bitcoin(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
case Coin.litecoinTestNet:
|
||||
return Litecoin(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
case Coin.bitcoincashTestnet:
|
||||
return Bitcoincash(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
case Coin.firoTestNet:
|
||||
return Firo(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
case Coin.dogecoinTestNet:
|
||||
return Dogecoin(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
case Coin.stellarTestnet:
|
||||
return Stellar(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
}
|
||||
// throw Exception("moved");
|
||||
// switch (coin) {
|
||||
// case Coin.bitcoin:
|
||||
// return Address.validateAddress(address, bitcoin);
|
||||
|
|
|
@ -17,7 +17,7 @@ abstract class DefaultEpicBoxes {
|
|||
static List<String> get defaultIds => ['americas', 'asia', 'europe'];
|
||||
|
||||
static EpicBoxServerModel get americas => EpicBoxServerModel(
|
||||
host: 'epicbox.epic.tech',
|
||||
host: 'stackwallet.epicbox.com',
|
||||
port: 443,
|
||||
name: 'Americas',
|
||||
id: 'americas',
|
||||
|
|
|
@ -119,28 +119,28 @@ class BitcoincashWallet extends Bip39HDWallet
|
|||
List<Map<String, dynamic>> allTransactions = [];
|
||||
|
||||
for (final txHash in allTxHashes) {
|
||||
final storedTx = await mainDB.isar.transactionV2s
|
||||
.where()
|
||||
.txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId)
|
||||
.findFirst();
|
||||
// final storedTx = await mainDB.isar.transactionV2s
|
||||
// .where()
|
||||
// .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId)
|
||||
// .findFirst();
|
||||
//
|
||||
// if (storedTx == null ||
|
||||
// storedTx.height == null ||
|
||||
// (storedTx.height != null && storedTx.height! <= 0)) {
|
||||
final tx = await electrumXCachedClient.getTransaction(
|
||||
txHash: txHash["tx_hash"] as String,
|
||||
verbose: true,
|
||||
coin: cryptoCurrency.coin,
|
||||
);
|
||||
|
||||
if (storedTx == null ||
|
||||
storedTx.height == null ||
|
||||
(storedTx.height != null && storedTx.height! <= 0)) {
|
||||
final tx = await electrumXCachedClient.getTransaction(
|
||||
txHash: txHash["tx_hash"] as String,
|
||||
verbose: true,
|
||||
coin: cryptoCurrency.coin,
|
||||
);
|
||||
|
||||
// check for duplicates before adding to list
|
||||
if (allTransactions
|
||||
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
|
||||
-1) {
|
||||
tx["height"] = txHash["height"];
|
||||
allTransactions.add(tx);
|
||||
}
|
||||
// check for duplicates before adding to list
|
||||
if (allTransactions
|
||||
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
|
||||
-1) {
|
||||
tx["height"] = txHash["height"];
|
||||
allTransactions.add(tx);
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
final List<TransactionV2> txns = [];
|
||||
|
@ -174,22 +174,28 @@ class BitcoincashWallet extends Bip39HDWallet
|
|||
coin: cryptoCurrency.coin,
|
||||
);
|
||||
|
||||
final prevOutJson = Map<String, dynamic>.from(
|
||||
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
|
||||
as Map);
|
||||
try {
|
||||
final prevOutJson = Map<String, dynamic>.from(
|
||||
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
|
||||
as Map);
|
||||
final prevOut = OutputV2.fromElectrumXJson(
|
||||
prevOutJson,
|
||||
decimalPlaces: cryptoCurrency.fractionDigits,
|
||||
walletOwns: false, // doesn't matter here as this is not saved
|
||||
isFullAmountNotSats: true,
|
||||
);
|
||||
|
||||
final prevOut = OutputV2.fromElectrumXJson(
|
||||
prevOutJson,
|
||||
decimalPlaces: cryptoCurrency.fractionDigits,
|
||||
walletOwns: false, // doesn't matter here as this is not saved
|
||||
);
|
||||
|
||||
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
|
||||
txid: txid,
|
||||
vout: vout,
|
||||
);
|
||||
valueStringSats = prevOut.valueStringSats;
|
||||
addresses.addAll(prevOut.addresses);
|
||||
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
|
||||
txid: txid,
|
||||
vout: vout,
|
||||
);
|
||||
valueStringSats = prevOut.valueStringSats;
|
||||
addresses.addAll(prevOut.addresses);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Error getting prevOutJson: $e\nStack trace: $s",
|
||||
level: LogLevel.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor(
|
||||
|
@ -222,6 +228,7 @@ class BitcoincashWallet extends Bip39HDWallet
|
|||
decimalPlaces: cryptoCurrency.fractionDigits,
|
||||
// don't know yet if wallet owns. Need addresses first
|
||||
walletOwns: false,
|
||||
isFullAmountNotSats: true,
|
||||
);
|
||||
|
||||
// if output was to my wallet, add value to amount received
|
||||
|
|
|
@ -429,14 +429,14 @@ class TezosWallet extends Bip39Wallet<Tezos> {
|
|||
await mainDB.updateOrPutAddresses([address]);
|
||||
|
||||
// ensure we only have a single address
|
||||
await mainDB.isar.writeTxn(() async {
|
||||
await mainDB.isar.addresses
|
||||
mainDB.isar.writeTxnSync(() {
|
||||
mainDB.isar.addresses
|
||||
.where()
|
||||
.walletIdEqualTo(walletId)
|
||||
.filter()
|
||||
.not()
|
||||
.derivationPath((q) => q.valueEqualTo(derivationPath))
|
||||
.deleteAll();
|
||||
.deleteAllSync();
|
||||
});
|
||||
|
||||
if (info.cachedReceivingAddress != address.value) {
|
||||
|
|
|
@ -17,6 +17,7 @@ import 'package:stackwallet/utilities/amount/amount.dart';
|
|||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/default_nodes.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/enums/sync_type_enum.dart';
|
||||
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/paynym_is_api.dart';
|
||||
|
@ -609,7 +610,45 @@ abstract class Wallet<T extends CryptoCurrency> {
|
|||
Future<void> exit() async {
|
||||
_periodicRefreshTimer?.cancel();
|
||||
_networkAliveTimer?.cancel();
|
||||
// TODO:
|
||||
|
||||
// If the syncing pref is currentWalletOnly or selectedWalletsAtStartup (and
|
||||
// this wallet isn't in walletIdsSyncOnStartup), then we close subscriptions.
|
||||
|
||||
switch (prefs.syncType) {
|
||||
case SyncingType.currentWalletOnly:
|
||||
// Close the subscription for this coin's chain height.
|
||||
// NOTE: This does not work now that the subscription is shared
|
||||
// await (await ChainHeightServiceManager.getService(cryptoCurrency.coin))
|
||||
// ?.cancelListen();
|
||||
case SyncingType.selectedWalletsAtStartup:
|
||||
// Close the subscription if this wallet is not in the list to be synced.
|
||||
if (!prefs.walletIdsSyncOnStartup.contains(walletId)) {
|
||||
// Check if there's another wallet of this coin on the sync list.
|
||||
List<String> walletIds = [];
|
||||
for (final id in prefs.walletIdsSyncOnStartup) {
|
||||
final wallet = mainDB.isar.walletInfo
|
||||
.where()
|
||||
.walletIdEqualTo(id)
|
||||
.findFirstSync()!;
|
||||
|
||||
if (wallet.coin == cryptoCurrency.coin) {
|
||||
walletIds.add(id);
|
||||
}
|
||||
}
|
||||
// TODO [prio=low]: use a query instead of iterating thru wallets.
|
||||
|
||||
// If there are no other wallets of this coin, then close the sub.
|
||||
if (walletIds.isEmpty) {
|
||||
// NOTE: This does not work now that the subscription is shared
|
||||
// await (await ChainHeightServiceManager.getService(
|
||||
// cryptoCurrency.coin))
|
||||
// ?.cancelListen();
|
||||
}
|
||||
}
|
||||
case SyncingType.allWalletsOnStartup:
|
||||
// Do nothing.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
|
|
|
@ -4,8 +4,11 @@ import 'dart:math';
|
|||
import 'package:bip47/src/util.dart';
|
||||
import 'package:bitcoindart/bitcoindart.dart' as bitcoindart;
|
||||
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/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';
|
||||
|
@ -13,23 +16,29 @@ 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';
|
||||
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.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:uuid/uuid.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
|
||||
mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
||||
late ElectrumXClient electrumXClient;
|
||||
late StreamChannel electrumAdapterChannel;
|
||||
late ElectrumClient electrumAdapterClient;
|
||||
late CachedElectrumXClient electrumXCachedClient;
|
||||
// late SubscribableElectrumXClient subscribableElectrumXClient;
|
||||
late ChainHeightServiceManager chainHeightServiceManager;
|
||||
|
||||
int? get maximumFeerate => null;
|
||||
|
||||
|
@ -123,7 +132,12 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
// don't care about sorting if using all utxos
|
||||
if (!coinControl) {
|
||||
// sort spendable by age (oldest first)
|
||||
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
||||
spendableOutputs.sort((a, b) => (b.blockTime ?? currentChainHeight)
|
||||
.compareTo((a.blockTime ?? currentChainHeight)));
|
||||
// Null check operator changed to null assignment in order to resolve a
|
||||
// `Null check operator used on a null value` error. currentChainHeight
|
||||
// used in order to sort these unconfirmed outputs as the youngest, but we
|
||||
// could just as well use currentChainHeight + 1.
|
||||
}
|
||||
|
||||
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
||||
|
@ -794,9 +808,30 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
|
||||
Future<int> fetchChainHeight() async {
|
||||
try {
|
||||
final result = await electrumXClient.getBlockHeadTip();
|
||||
return result["height"] as int;
|
||||
} catch (e) {
|
||||
// Get the chain height service for the current coin.
|
||||
ChainHeightService? service = ChainHeightServiceManager.getService(
|
||||
cryptoCurrency.coin,
|
||||
);
|
||||
|
||||
// ... 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",
|
||||
level: LogLevel.Error);
|
||||
// completer.completeError(e, s);
|
||||
// return Future.error(e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
@ -807,21 +842,19 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
return transactions.length;
|
||||
}
|
||||
|
||||
Future<Map<String, int>> fetchTxCountBatched({
|
||||
required Map<String, String> addresses,
|
||||
/// Should return a list of tx counts matching the list of addresses given
|
||||
Future<List<int>> fetchTxCountBatched({
|
||||
required List<String> addresses,
|
||||
}) async {
|
||||
try {
|
||||
final Map<String, List<dynamic>> args = {};
|
||||
for (final entry in addresses.entries) {
|
||||
args[entry.key] = [
|
||||
cryptoCurrency.addressToScriptHash(address: entry.value),
|
||||
];
|
||||
}
|
||||
final response = await electrumXClient.getBatchHistory(args: args);
|
||||
final response = await electrumXClient.getBatchHistory(
|
||||
args: addresses
|
||||
.map((e) => [cryptoCurrency.addressToScriptHash(address: e)])
|
||||
.toList(growable: false));
|
||||
|
||||
final Map<String, int> result = {};
|
||||
for (final entry in response.entries) {
|
||||
result[entry.key] = entry.value.length;
|
||||
final List<int> result = [];
|
||||
for (final entry in response) {
|
||||
result.add(entry.length);
|
||||
}
|
||||
return result;
|
||||
} catch (e, s) {
|
||||
|
@ -857,14 +890,64 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
.toList();
|
||||
|
||||
final newNode = await getCurrentElectrumXNode();
|
||||
try {
|
||||
await electrumXClient.electrumAdapterClient?.close();
|
||||
} catch (e, s) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
electrumXClient = ElectrumXClient.from(
|
||||
node: newNode,
|
||||
prefs: prefs,
|
||||
failovers: failovers,
|
||||
coin: cryptoCurrency.coin,
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
//============================================================================
|
||||
|
@ -883,13 +966,11 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
index < cryptoCurrency.maxNumberOfIndexesToCheck &&
|
||||
gapCounter < cryptoCurrency.maxUnusedAddressGap;
|
||||
index += txCountBatchSize) {
|
||||
List<String> iterationsAddressArray = [];
|
||||
Logging.instance.log(
|
||||
"index: $index, \t GapCounter $chain ${type.name}: $gapCounter",
|
||||
level: LogLevel.Info);
|
||||
|
||||
final _id = "k_$index";
|
||||
Map<String, String> txCountCallArgs = {};
|
||||
List<String> txCountCallArgs = [];
|
||||
|
||||
for (int j = 0; j < txCountBatchSize; j++) {
|
||||
final derivePath = cryptoCurrency.constructDerivePath(
|
||||
|
@ -922,9 +1003,9 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
|
||||
addressArray.add(address);
|
||||
|
||||
txCountCallArgs.addAll({
|
||||
"${_id}_$j": addressString,
|
||||
});
|
||||
txCountCallArgs.add(
|
||||
addressString,
|
||||
);
|
||||
}
|
||||
|
||||
// get address tx counts
|
||||
|
@ -932,10 +1013,9 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
|
||||
// check and add appropriate addresses
|
||||
for (int k = 0; k < txCountBatchSize; k++) {
|
||||
int count = counts["${_id}_$k"]!;
|
||||
if (count > 0) {
|
||||
iterationsAddressArray.add(txCountCallArgs["${_id}_$k"]!);
|
||||
final count = counts[k];
|
||||
|
||||
if (count > 0) {
|
||||
// update highest
|
||||
highestIndexWithHistory = index + k;
|
||||
|
||||
|
@ -1025,22 +1105,20 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
List<Map<String, dynamic>> allTxHashes = [];
|
||||
|
||||
if (serverCanBatch) {
|
||||
final Map<int, Map<String, List<dynamic>>> batches = {};
|
||||
final Map<String, String> requestIdToAddressMap = {};
|
||||
final Map<int, List<List<dynamic>>> batches = {};
|
||||
final Map<int, List<String>> batchIndexToAddressListMap = {};
|
||||
const batchSizeMax = 100;
|
||||
int batchNumber = 0;
|
||||
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(
|
||||
address: allAddresses.elementAt(i),
|
||||
address: address,
|
||||
);
|
||||
final id = Logger.isTestEnv ? "$i" : const Uuid().v1();
|
||||
requestIdToAddressMap[id] = allAddresses.elementAt(i);
|
||||
batches[batchNumber]!.addAll({
|
||||
id: [scriptHash]
|
||||
});
|
||||
batches[batchNumber]!.add([scriptHash]);
|
||||
batchIndexToAddressListMap[batchNumber]!.add(address);
|
||||
if (i % batchSizeMax == batchSizeMax - 1) {
|
||||
batchNumber++;
|
||||
}
|
||||
|
@ -1049,12 +1127,13 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
for (int i = 0; i < batches.length; i++) {
|
||||
final response =
|
||||
await electrumXClient.getBatchHistory(args: batches[i]!);
|
||||
for (final entry in response.entries) {
|
||||
for (int j = 0; j < entry.value.length; j++) {
|
||||
entry.value[j]["address"] = requestIdToAddressMap[entry.key];
|
||||
if (!allTxHashes.contains(entry.value[j])) {
|
||||
allTxHashes.add(entry.value[j]);
|
||||
}
|
||||
for (int j = 0; j < response.length; j++) {
|
||||
final entry = response[j];
|
||||
for (int k = 0; k < entry.length; k++) {
|
||||
entry[k]["address"] = batchIndexToAddressListMap[i]![j];
|
||||
// if (!allTxHashes.contains(entry[j])) {
|
||||
allTxHashes.add(entry[k]);
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1096,6 +1175,8 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
coin: cryptoCurrency.coin,
|
||||
);
|
||||
|
||||
print("txn: $txn");
|
||||
|
||||
final vout = jsonUTXO["tx_pos"] as int;
|
||||
|
||||
final outputs = txn["vout"] as List;
|
||||
|
@ -1164,6 +1245,13 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
await updateElectrumX(newNode: node);
|
||||
}
|
||||
|
||||
Future<ElectrumClient> updateClient() async {
|
||||
Logging.instance.log("Updating electrum node and ElectrumAdapterClient.",
|
||||
level: LogLevel.Info);
|
||||
await updateNode();
|
||||
return electrumAdapterClient;
|
||||
}
|
||||
|
||||
FeeObject? _cachedFees;
|
||||
|
||||
@override
|
||||
|
@ -1196,9 +1284,9 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info);
|
||||
_cachedFees = feeObject;
|
||||
return _cachedFees!;
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Exception rethrown from _getFees(): $e",
|
||||
"Exception rethrown from _getFees(): $e\nStack trace: $s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
if (_cachedFees == null) {
|
||||
|
@ -1512,31 +1600,27 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
final fetchedUtxoList = <List<Map<String, dynamic>>>[];
|
||||
|
||||
if (serverCanBatch) {
|
||||
final Map<int, Map<String, List<dynamic>>> batches = {};
|
||||
final Map<int, List<List<dynamic>>> batchArgs = {};
|
||||
const batchSizeMax = 10;
|
||||
int batchNumber = 0;
|
||||
for (int i = 0; i < allAddresses.length; i++) {
|
||||
if (batches[batchNumber] == null) {
|
||||
batches[batchNumber] = {};
|
||||
}
|
||||
batchArgs[batchNumber] ??= [];
|
||||
final scriptHash = cryptoCurrency.addressToScriptHash(
|
||||
address: allAddresses[i].value,
|
||||
);
|
||||
|
||||
batches[batchNumber]!.addAll({
|
||||
scriptHash: [scriptHash]
|
||||
});
|
||||
batchArgs[batchNumber]!.add([scriptHash]);
|
||||
if (i % batchSizeMax == batchSizeMax - 1) {
|
||||
batchNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < batches.length; i++) {
|
||||
for (int i = 0; i < batchArgs.length; i++) {
|
||||
final response =
|
||||
await electrumXClient.getBatchUTXOs(args: batches[i]!);
|
||||
for (final entry in response.entries) {
|
||||
if (entry.value.isNotEmpty) {
|
||||
fetchedUtxoList.add(entry.value);
|
||||
await electrumXClient.getBatchUTXOs(args: batchArgs[i]!);
|
||||
for (final entry in response) {
|
||||
if (entry.isNotEmpty) {
|
||||
fetchedUtxoList.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1680,7 +1764,8 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
|||
Logging.instance.log("prepare send: $result", level: LogLevel.Info);
|
||||
if (result.fee!.raw.toInt() < result.vSize!) {
|
||||
throw Exception(
|
||||
"Error in fee calculation: Transaction fee cannot be less than vSize");
|
||||
"Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot "
|
||||
"be less than vSize (${result.vSize})");
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -175,6 +175,7 @@ class _NodeCardState extends ConsumerState<NodeCard> {
|
|||
useSSL: node.useSSL,
|
||||
failovers: [],
|
||||
prefs: ref.read(prefsChangeNotifierProvider),
|
||||
coin: widget.coin,
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
|
@ -158,6 +158,7 @@ class NodeOptionsSheet extends ConsumerWidget {
|
|||
failovers: [],
|
||||
prefs: ref.read(prefsChangeNotifierProvider),
|
||||
torService: ref.read(pTorService),
|
||||
coin: coin,
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
23
pubspec.lock
23
pubspec.lock
|
@ -524,6 +524,15 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
electrum_adapter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "0a34f7f48d921fb33f551cb11dfc9b2930522240"
|
||||
resolved-ref: "0a34f7f48d921fb33f551cb11dfc9b2930522240"
|
||||
url: "https://github.com/cypherstack/electrum_adapter.git"
|
||||
source: git
|
||||
version: "3.0.0"
|
||||
emojis:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -674,10 +683,10 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "3.0.1"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1038,10 +1047,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "3.0.0"
|
||||
local_auth:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1594,7 +1603,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.5.3"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
|
@ -1718,8 +1727,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "0a6888282f4e98401051a396e9d2293bd55ac2c2"
|
||||
resolved-ref: "0a6888282f4e98401051a396e9d2293bd55ac2c2"
|
||||
ref: e37dc4e22f7acb2746b70bdc935f0eb3c50b8b71
|
||||
resolved-ref: e37dc4e22f7acb2746b70bdc935f0eb3c50b8b71
|
||||
url: "https://github.com/cypherstack/tor.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
|
11
pubspec.yaml
11
pubspec.yaml
|
@ -11,7 +11,7 @@ description: Stack Wallet
|
|||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.9.2+201
|
||||
version: 1.10.1+209
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.2 <4.0.0"
|
||||
|
@ -65,7 +65,7 @@ dependencies:
|
|||
tor_ffi_plugin:
|
||||
git:
|
||||
url: https://github.com/cypherstack/tor.git
|
||||
ref: 0a6888282f4e98401051a396e9d2293bd55ac2c2
|
||||
ref: e37dc4e22f7acb2746b70bdc935f0eb3c50b8b71
|
||||
|
||||
fusiondart:
|
||||
git:
|
||||
|
@ -173,6 +173,11 @@ dependencies:
|
|||
url: https://github.com/cypherstack/coinlib.git
|
||||
path: coinlib_flutter
|
||||
ref: 376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7
|
||||
electrum_adapter:
|
||||
git:
|
||||
url: https://github.com/cypherstack/electrum_adapter.git
|
||||
ref: 0a34f7f48d921fb33f551cb11dfc9b2930522240
|
||||
stream_channel: ^2.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -189,7 +194,7 @@ dev_dependencies:
|
|||
# lint: ^1.10.0
|
||||
analyzer: ^5.13.0
|
||||
import_sorter: ^4.6.0
|
||||
flutter_lints: ^2.0.1
|
||||
flutter_lints: ^3.0.1
|
||||
isar_generator: 3.0.5
|
||||
|
||||
flutter_launcher_icons:
|
||||
|
|
Loading…
Reference in a new issue