diff --git a/.gitmodules b/.gitmodules index 7474c8a54..95b02e580 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 \ No newline at end of file diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index c976dcfc7..9eb24dd00 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit c976dcfc7786bbf7091e310eb877f5c685352903 +Subproject commit 9eb24dd00cd0e1df08624ece1ca47090c158c08c diff --git a/lib/db/db_version_migration.dart b/lib/db/db_version_migration.dart index 21241f630..e81c01c76 100644 --- a/lib/db/db_version_migration.dart +++ b/lib/db/db_version_migration.dart @@ -85,6 +85,7 @@ class DbVersionMigrator with WalletDB { useSSL: node.useSSL), prefs: prefs, failovers: failovers, + coin: Coin.firo, ); try { diff --git a/lib/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index 021bdf065..b64e2ec7d 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -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 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 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> getAnonymitySet({ required String groupId, String blockhash = "", @@ -56,9 +80,12 @@ class CachedElectrumXClient { set = Map.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.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 result = await electrumXClient - .getTransaction(txHash: txHash, verbose: verbose); + await _checkElectrumAdapterClient(); + + final Map 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 tags to a Set. + 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. + final Iterable iterableTags = newTags.map((e) => e.toString()); + + cachedTags.addAll(iterableTags); await box.put( "tags", diff --git a/lib/electrumx_rpc/electrumx_chain_height_service.dart b/lib/electrumx_rpc/electrumx_chain_height_service.dart new file mode 100644 index 000000000..3696e78f9 --- /dev/null +++ b/lib/electrumx_rpc/electrumx_chain_height_service.dart @@ -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 _services = {}; + // Map 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 dispose() async { + // Close each subscription. + // + // Create a list of keys to avoid concurrent modification during iteration + var keys = List.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? _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 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(); + + // 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 cancelListen() async { + await _subscription?.cancel(); + _subscription = null; + _reconnectTimer?.cancel(); + } +} diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 21126c5d1..98c6614f9 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -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? get electrumAdapterChannel => _electrumAdapterChannel; + StreamChannel? _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 _finalizer = Finalizer( @@ -101,7 +114,7 @@ class ElectrumXClient { required bool useSSL, required Prefs prefs, required List 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().listen( (event) async { switch (event.newStatus) { @@ -133,6 +148,8 @@ class ElectrumXClient { } }, ); + + // Listen for tor preference changes. _torPreferenceListener = bus.on().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 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 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 /// /// returns a list of json response objects if no errors were found - Future>> batchRequest({ + Future> batchRequest({ required String command, - required Map> args, + required List 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 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 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 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 = >[]; + _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 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>.from(response, growable: false); + return response; } on WifiOnlyException { rethrow; } on SocketException { @@ -451,13 +474,23 @@ class ElectrumXClient { /// Returns true if ping succeeded Future 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).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; - 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.from(response["result"] as Map); + return Map.from(response as Map); } catch (e) { rethrow; } @@ -510,7 +543,7 @@ class ElectrumXClient { requestID: requestID, command: 'server.features', ); - return Map.from(response["result"] as Map); + return Map.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.from(response["result"] as Map); + return Map.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>>> getBatchHistory( - {required Map> args}) async { + Future>>> getBatchHistory({ + required List args, + }) async { try { final response = await batchRequest( command: 'blockchain.scripthash.get_history', args: args, ); - final Map>> result = {}; + final List>> result = []; for (int i = 0; i < response.length; i++) { - result[response[i]["id"] as String] = - List>.from(response[i]["result"] as List); + result.add(List>.from(response[i] as List)); } return result; } catch (e) { @@ -654,23 +686,37 @@ class ElectrumXClient { scripthash, ], ); - return List>.from(response["result"] as List); + return List>.from(response as List); } catch (e) { rethrow; } } - Future>>> getBatchUTXOs( - {required Map> args}) async { + Future>>> getBatchUTXOs({ + required List args, + }) async { try { final response = await batchRequest( command: 'blockchain.scripthash.listunspent', args: args, ); - final Map>> result = {}; + final List>> result = []; for (int i = 0; i < response.length; i++) { - result[response[i]["id"] as String] = - List>.from(response[i]["result"] as List); + if ((response[i] as List).isNotEmpty) { + try { + final data = List>.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.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.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.from(response["result"] as Map); - } catch (e) { - rethrow; - } + Logging.instance.log("attempting to fetch lelantus.getanonymityset...", + level: LogLevel.Info); + await checkElectrumAdapter(); + Map 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.from(result as Map); - } catch (e) { - Logging.instance.log(e, level: LogLevel.Error); - rethrow; + retryCount--; } + + return Map.from(response as Map); } /// Returns the latest Lelantus set id /// /// ex: 1 Future 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 response = + await (_electrumAdapterClient as FiroElectrumClient) + .getSparkAnonymitySet( + coinGroupId: coinGroupId, startBlockHash: startBlockHash); Logging.instance.log("Fetching spark.getsparkanonymityset finished", level: LogLevel.Info); - return Map.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.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 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.from(response); final set = Set.from(map["tags"] as List); return await compute(_ffiHashTagsComputeWrapper, set); } catch (e) { @@ -947,16 +955,15 @@ class ElectrumXClient { required List sparkCoinHashes, }) async { try { - final response = await request( - requestID: requestID, - command: 'spark.getsparkmintmetadata', - args: [ - { - "coinHashes": sparkCoinHashes, - }, - ], - ); - return List>.from(response["result"] as List); + Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...", + level: LogLevel.Info); + await checkElectrumAdapter(); + List response = + await (_electrumAdapterClient as FiroElectrumClient) + .getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes); + Logging.instance.log("Fetching spark.getsparkmintmetadata finished", + level: LogLevel.Info); + return List>.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> getFeeRate({String? requestID}) async { - try { - final response = await request( - requestID: requestID, - command: 'blockchain.getfeerate', - ); - return Map.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; } diff --git a/lib/electrumx_rpc/rpc.dart b/lib/electrumx_rpc/rpc.dart index 89c1735c2..f2044a141 100644 --- a/lib/electrumx_rpc/rpc.dart +++ b/lib/electrumx_rpc/rpc.dart @@ -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, diff --git a/lib/electrumx_rpc/subscribable_electrumx_client.dart b/lib/electrumx_rpc/subscribable_electrumx_client.dart index b7da56a52..f06771906 100644 --- a/lib/electrumx_rpc/subscribable_electrumx_client.dart +++ b/lib/electrumx_rpc/subscribable_electrumx_client.dart @@ -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 _controller = +// StreamController(); // TODO controller params +// +// Stream get responseStream => _controller.stream; +// +// void addToStream(dynamic data) => _controller.add(data); // } // // class SocketTask { @@ -40,67 +49,430 @@ // final Map _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? _torPreferenceListener; +// StreamSubscription? _torStatusListener; +// final Mutex _torConnectingLock = Mutex(); +// bool _requireMutex = false; +// +// List? failovers; +// int currentFailoverIndex = -1; +// // SubscribableElectrumXClient({ -// bool useSSL = true, +// required bool useSSL, +// required Prefs prefs, +// required List 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().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().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 connect({required String host, required int port}) async { -// try { -// await _socket?.close(); -// } catch (_) {} +// factory SubscribableElectrumXClient.from({ +// required ElectrumXNode node, +// required Prefs prefs, +// required List 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 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 _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 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 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 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 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 data) { // _responseData.addAll(data); // @@ -137,6 +511,7 @@ // } // } // +// /// Called when the socket has a response. // void _responseHandler(Map 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 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 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 _call({ // required String method, // List 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(); // _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 _callWithTimeout({ // required String method, // List 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(); // _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 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 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: [], // ); diff --git a/lib/models/isar/models/blockchain_data/v2/output_v2.dart b/lib/models/isar/models/blockchain_data/v2/output_v2.dart index f096d8a90..45a8b1329 100644 --- a/lib/models/isar/models/blockchain_data/v2/output_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/output_v2.dart @@ -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, ), diff --git a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart index 29b40f34b..ef4d55319 100644 --- a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart @@ -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, - ), - ], + ), ), ), ); diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index b88078780..9d40e4106 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -726,12 +726,13 @@ class _RestoreWalletViewState extends ConsumerState { color: Theme.of(context).extension()!.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 { }, ), ), - if (isDesktop) + /*if (isDesktop) const Spacer( flex: 15, - ), + ),*/ if (!isDesktop) Expanded( child: SingleChildScrollView( @@ -1174,6 +1175,7 @@ class _RestoreWalletViewState extends ConsumerState { ), ), ), + ), ); } } diff --git a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart index ea77f9d33..02f4e714b 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart @@ -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 { 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(); + } } }, ), diff --git a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart index 2448b1e33..a061f1ed5 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart @@ -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.from(_addresses); + tempAddresses.remove(entry); ContactEntry editedContact = - contact.copyWith(addresses: _addresses); + contact.copyWith(addresses: tempAddresses); if (await ref .read(addressBookServiceProvider) .editContact(editedContact)) { diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index e03c3ab21..ddac06aca 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -177,6 +177,7 @@ class _AddEditNodeViewState extends ConsumerState { useSSL: formData.useSSL!, failovers: [], prefs: ref.read(prefsChangeNotifierProvider), + coin: coin, ); try { diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 6bf0092e8..bd974b6ec 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -154,6 +154,7 @@ class _NodeDetailsViewState extends ConsumerState { useSSL: node.useSSL, failovers: [], prefs: ref.read(prefsChangeNotifierProvider), + coin: coin, ); try { diff --git a/lib/pages/special/firo_rescan_recovery_error_dialog.dart b/lib/pages/special/firo_rescan_recovery_error_dialog.dart index d062b62d5..a812c377d 100644 --- a/lib/pages/special/firo_rescan_recovery_error_dialog.dart +++ b/lib/pages/special/firo_rescan_recovery_error_dialog.dart @@ -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 diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index 8fa7eaaef..45483e9d0 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -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: () { diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart index d32bd7d08..d44588b3c 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart @@ -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 ), ), diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index bbb688f01..07d592b33 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -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 { 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) { diff --git a/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_address_card.dart index fc626f290..13cdc24e4 100644 --- a/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_address_card.dart +++ b/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_address_card.dart @@ -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()!.textDark, ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 832038005..70766661b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -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 { 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 { ), ), ), + 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: [ diff --git a/lib/pages_desktop_specific/settings/desktop_settings_view.dart b/lib/pages_desktop_specific/settings/desktop_settings_view.dart index 6a785fc8a..5d95cd960 100644 --- a/lib/pages_desktop_specific/settings/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/settings/desktop_settings_view.dart @@ -105,7 +105,12 @@ class _DesktopSettingsViewState extends ConsumerState { children: [ const Padding( padding: EdgeInsets.all(15.0), - child: SettingsMenu(), + child: Align( + alignment: Alignment.topLeft, + child: SingleChildScrollView( + child: SettingsMenu(), + ), + ), ), Expanded( child: contentViews[ diff --git a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart index 1a11320c0..102a547d6 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart @@ -36,305 +36,308 @@ class _AdvancedSettings extends ConsumerState { @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()! - .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()! - .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()! - .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()! + .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( - 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()! - .textDark), - textAlign: TextAlign.left, ), - PrimaryButton( - buttonHeight: ButtonHeight.xs, - label: "Edit", - width: 101, - onPressed: () async { - await showDialog( - 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()! + .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()! + .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( + 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()! - .textDark), - textAlign: TextAlign.left, - ), - PrimaryButton( - buttonHeight: ButtonHeight.xs, - label: "Edit", - width: 101, - onPressed: () async { - await showDialog( - 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()! + .textDark), + textAlign: TextAlign.left, + ), + PrimaryButton( + buttonHeight: ButtonHeight.xs, + label: "Edit", + width: 101, + onPressed: () async { + await showDialog( + 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()! - .textDark), - textAlign: TextAlign.left, - ), - PrimaryButton( - buttonHeight: ButtonHeight.xs, - label: "Show logs", - width: 101, - onPressed: () async { - await showDialog( - 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()! + .textDark), + textAlign: TextAlign.left, + ), + PrimaryButton( + buttonHeight: ButtonHeight.xs, + label: "Edit", + width: 101, + onPressed: () async { + await showDialog( + 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()! + .textDark), + textAlign: TextAlign.left, + ), + PrimaryButton( + buttonHeight: ButtonHeight.xs, + label: "Show logs", + width: 101, + onPressed: () async { + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const DebugInfoDialog(); + }, + ); + }, + ), + ], + ), + ), + const SizedBox( + height: 10, + ), + ], + ), ), ), - ), - ], + ], + ), ); } } diff --git a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/debug_info_dialog.dart b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/debug_info_dialog.dart index 0ae221621..c9a476d04 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/debug_info_dialog.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/debug_info_dialog.dart @@ -98,7 +98,10 @@ class _DebugInfoDialog extends ConsumerState { @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: [ diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index 1512c12c6..e6c49f47f 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -146,6 +146,7 @@ class NotificationsService extends ChangeNotifier { node: eNode, failovers: failovers, prefs: prefs, + coin: coin, ); final tx = await client.getTransaction(txHash: txid); diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index b56a6b090..729b863ee 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -42,7 +42,13 @@ class Wallets { final Map _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) { diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 4ed59213b..0e766d28e 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -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); diff --git a/lib/utilities/default_epicboxes.dart b/lib/utilities/default_epicboxes.dart index 567ef7cb1..6f0fbc7c5 100644 --- a/lib/utilities/default_epicboxes.dart +++ b/lib/utilities/default_epicboxes.dart @@ -17,7 +17,7 @@ abstract class DefaultEpicBoxes { static List get defaultIds => ['americas', 'asia', 'europe']; static EpicBoxServerModel get americas => EpicBoxServerModel( - host: 'epicbox.epic.tech', + host: 'stackwallet.epicbox.com', port: 443, name: 'Americas', id: 'americas', diff --git a/lib/wallets/wallet/impl/bitcoincash_wallet.dart b/lib/wallets/wallet/impl/bitcoincash_wallet.dart index d617dc64f..2b9355e2d 100644 --- a/lib/wallets/wallet/impl/bitcoincash_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoincash_wallet.dart @@ -119,28 +119,28 @@ class BitcoincashWallet extends Bip39HDWallet List> 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 txns = []; @@ -174,22 +174,28 @@ class BitcoincashWallet extends Bip39HDWallet coin: cryptoCurrency.coin, ); - final prevOutJson = Map.from( - (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) - as Map); + try { + final prevOutJson = Map.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 diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart index 1bde36803..d1df08502 100644 --- a/lib/wallets/wallet/impl/tezos_wallet.dart +++ b/lib/wallets/wallet/impl/tezos_wallet.dart @@ -429,14 +429,14 @@ class TezosWallet extends Bip39Wallet { 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) { diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index fe26a508f..2f1691b0a 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -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 { Future 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 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 diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 0b74f4ed6..a7bb2a1af 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -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 on Bip39HDWallet { 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 on Bip39HDWallet { // 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 on Bip39HDWallet { Future 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 on Bip39HDWallet { return transactions.length; } - Future> fetchTxCountBatched({ - required Map addresses, + /// Should return a list of tx counts matching the list of addresses given + Future> fetchTxCountBatched({ + required List addresses, }) async { try { - final Map> 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 result = {}; - for (final entry in response.entries) { - result[entry.key] = entry.value.length; + final List result = []; + for (final entry in response) { + result.add(entry.length); } return result; } catch (e, s) { @@ -857,14 +890,64 @@ mixin ElectrumXInterface on Bip39HDWallet { .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 on Bip39HDWallet { index < cryptoCurrency.maxNumberOfIndexesToCheck && gapCounter < cryptoCurrency.maxUnusedAddressGap; index += txCountBatchSize) { - List iterationsAddressArray = []; Logging.instance.log( "index: $index, \t GapCounter $chain ${type.name}: $gapCounter", level: LogLevel.Info); - final _id = "k_$index"; - Map txCountCallArgs = {}; + List txCountCallArgs = []; for (int j = 0; j < txCountBatchSize; j++) { final derivePath = cryptoCurrency.constructDerivePath( @@ -922,9 +1003,9 @@ mixin ElectrumXInterface on Bip39HDWallet { addressArray.add(address); - txCountCallArgs.addAll({ - "${_id}_$j": addressString, - }); + txCountCallArgs.add( + addressString, + ); } // get address tx counts @@ -932,10 +1013,9 @@ mixin ElectrumXInterface on Bip39HDWallet { // 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 on Bip39HDWallet { List> allTxHashes = []; if (serverCanBatch) { - final Map>> batches = {}; - final Map requestIdToAddressMap = {}; + final Map>> batches = {}; + final Map> 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 on Bip39HDWallet { 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 on Bip39HDWallet { 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 on Bip39HDWallet { await updateElectrumX(newNode: node); } + Future 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 on Bip39HDWallet { 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 on Bip39HDWallet { final fetchedUtxoList = >>[]; if (serverCanBatch) { - final Map>> batches = {}; + final Map>> 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 on Bip39HDWallet { 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; diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 7576999de..bc7233065 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -175,6 +175,7 @@ class _NodeCardState extends ConsumerState { useSSL: node.useSSL, failovers: [], prefs: ref.read(prefsChangeNotifierProvider), + coin: widget.coin, ); try { diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index c14b6a2ec..0a2e5ac4c 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -158,6 +158,7 @@ class NodeOptionsSheet extends ConsumerWidget { failovers: [], prefs: ref.read(prefsChangeNotifierProvider), torService: ref.read(pTorService), + coin: coin, ); try { diff --git a/pubspec.lock b/pubspec.lock index 840efc472..2bba3135a 100644 --- a/pubspec.lock +++ b/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" diff --git a/pubspec.yaml b/pubspec.yaml index a06ed1073..223c9a95b 100644 --- a/pubspec.yaml +++ b/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: