Merge branch 'staging' into add_frost

This commit is contained in:
sneurlax 2024-02-23 17:37:21 -06:00
commit 7d5cc8d8be
34 changed files with 1963 additions and 841 deletions

View file

@ -85,6 +85,7 @@ class DbVersionMigrator with WalletDB {
useSSL: node.useSSL),
prefs: prefs,
failovers: failovers,
coin: Coin.firo,
);
try {

View file

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

View file

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

View file

@ -9,24 +9,28 @@
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:decimal/decimal.dart';
import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter;
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:electrum_adapter/methods/specific/firo.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
import 'package:mutex/mutex.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart';
import 'package:stackwallet/electrumx_rpc/rpc.dart';
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:uuid/uuid.dart';
import 'package:stream_channel/stream_channel.dart';
class WifiOnlyException implements Exception {}
@ -73,6 +77,12 @@ class ElectrumXClient {
JsonRPC? get rpcClient => _rpcClient;
JsonRPC? _rpcClient;
StreamChannel<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
StreamChannel<dynamic>? _electrumAdapterChannel;
ElectrumClient? get electrumAdapterClient => _electrumAdapterClient;
ElectrumClient? _electrumAdapterClient;
late Prefs _prefs;
late TorService _torService;
@ -81,6 +91,9 @@ class ElectrumXClient {
final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
Coin? get coin => _coin;
late Coin? _coin;
// add finalizer to cancel stream subscription when all references to an
// instance of ElectrumX becomes inaccessible
static final Finalizer<ElectrumXClient> _finalizer = Finalizer(
@ -101,7 +114,7 @@ class ElectrumXClient {
required bool useSSL,
required Prefs prefs,
required List<ElectrumXNode> failovers,
JsonRPC? client,
Coin? coin,
this.connectionTimeoutForSpecialCaseJsonRPCClients =
const Duration(seconds: 60),
TorService? torService,
@ -112,9 +125,11 @@ class ElectrumXClient {
_host = host;
_port = port;
_useSSL = useSSL;
_rpcClient = client;
_coin = coin;
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
// Listen for tor status changes.
_torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
(event) async {
switch (event.newStatus) {
@ -133,6 +148,8 @@ class ElectrumXClient {
}
},
);
// Listen for tor preference changes.
_torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen(
(event) async {
// not sure if we need to do anything specific here
@ -141,21 +158,13 @@ class ElectrumXClient {
// case TorStatus.disabled:
// }
// might be ok to just reset/kill the current _jsonRpcClient
// since disconnecting is async and we want to ensure instant change over
// we will keep temp reference to current rpc client to call disconnect
// on before awaiting the disconnection future
final temp = _rpcClient;
// setting to null should force the creation of a new json rpc client
// on the next request sent through this electrumx instance
_rpcClient = null;
_electrumAdapterChannel = null;
_electrumAdapterClient = null;
await temp?.disconnect(
reason: "Tor status changed to \"${event.status}\"",
);
// Also close any chain height services that are currently open.
await ChainHeightServiceManager.dispose();
},
);
}
@ -164,6 +173,7 @@ class ElectrumXClient {
required ElectrumXNode node,
required Prefs prefs,
required List<ElectrumXNode> failovers,
required Coin coin,
TorService? torService,
EventBus? globalEventBusForTesting,
}) {
@ -175,6 +185,7 @@ class ElectrumXClient {
torService: torService,
failovers: failovers,
globalEventBusForTesting: globalEventBusForTesting,
coin: coin,
);
}
@ -186,7 +197,9 @@ class ElectrumXClient {
return true;
}
void _checkRpcClient() {
Future<void> checkElectrumAdapter() async {
({InternetAddress host, int port})? proxyInfo;
// If we're supposed to use Tor...
if (_prefs.useTor) {
// But Tor isn't running...
@ -195,64 +208,93 @@ class ElectrumXClient {
if (!_prefs.torKillSwitch) {
// Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function.
Logging.instance.log(
"Tor preference set but Tor is not enabled, killswitch not set, connecting to ElectrumX through clearnet",
"Tor preference set but Tor is not enabled, killswitch not set, connecting to Electrum adapter through clearnet",
level: LogLevel.Warning,
);
} else {
// ... But if the killswitch is set, then we throw an exception.
throw Exception(
"Tor preference and killswitch set but Tor is not enabled, not connecting to ElectrumX");
"Tor preference and killswitch set but Tor is not enabled, not connecting to Electrum adapter");
// TODO [prio=low]: Try to start Tor.
}
} else {
// Get the proxy info from the TorService.
final proxyInfo = _torService.getProxyInfo();
if (currentFailoverIndex == -1) {
_rpcClient ??= JsonRPC(
host: host,
port: port,
useSSL: useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: proxyInfo,
);
} else {
_rpcClient ??= JsonRPC(
host: failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
useSSL: failovers![currentFailoverIndex].useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: proxyInfo,
);
}
if (_rpcClient!.proxyInfo != proxyInfo) {
_rpcClient!.proxyInfo = proxyInfo;
_rpcClient!.disconnect(
reason: "Tor proxyInfo does not match current info",
);
}
return;
proxyInfo = _torService.getProxyInfo();
}
}
if (currentFailoverIndex == -1) {
_rpcClient ??= JsonRPC(
host: host,
port: port,
useSSL: useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: null,
);
} else {
_rpcClient ??= JsonRPC(
host: failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
useSSL: failovers![currentFailoverIndex].useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
proxyInfo: null,
);
// TODO [prio=med]: Add proxyInfo to StreamChannel (or add to wrapper).
// if (_electrumAdapter!.proxyInfo != proxyInfo) {
// _electrumAdapter!.proxyInfo = proxyInfo;
// _electrumAdapter!.disconnect(
// reason: "Tor proxyInfo does not match current info",
// );
// }
// If the current ElectrumAdapterClient is closed, create a new one.
if (_electrumAdapterClient != null &&
_electrumAdapterClient!.peer.isClosed) {
_electrumAdapterChannel = null;
_electrumAdapterClient = null;
}
if (currentFailoverIndex == -1) {
_electrumAdapterChannel ??= await electrum_adapter.connect(
host,
port: port,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
acceptUnverified: true,
useSSL: useSSL,
proxyInfo: proxyInfo,
);
if (_coin == Coin.firo || _coin == Coin.firoTestNet) {
_electrumAdapterClient ??= FiroElectrumClient(
_electrumAdapterChannel!,
host,
port,
useSSL,
proxyInfo,
);
} else {
_electrumAdapterClient ??= ElectrumClient(
_electrumAdapterChannel!,
host,
port,
useSSL,
proxyInfo,
);
}
} else {
_electrumAdapterChannel ??= await electrum_adapter.connect(
failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
acceptUnverified: true,
useSSL: failovers![currentFailoverIndex].useSSL,
proxyInfo: proxyInfo,
);
if (_coin == Coin.firo || _coin == Coin.firoTestNet) {
_electrumAdapterClient ??= FiroElectrumClient(
_electrumAdapterChannel!,
failovers![currentFailoverIndex].address,
failovers![currentFailoverIndex].port,
failovers![currentFailoverIndex].useSSL,
proxyInfo,
);
} else {
_electrumAdapterClient ??= ElectrumClient(
_electrumAdapterChannel!,
failovers![currentFailoverIndex].address,
failovers![currentFailoverIndex].port,
failovers![currentFailoverIndex].useSSL,
proxyInfo,
);
}
}
return;
}
/// Send raw rpc command
@ -268,32 +310,22 @@ class ElectrumXClient {
}
if (_requireMutex) {
await _torConnectingLock.protect(() async => _checkRpcClient());
await _torConnectingLock
.protect(() async => await checkElectrumAdapter());
} else {
_checkRpcClient();
await checkElectrumAdapter();
}
try {
final requestId = requestID ?? const Uuid().v1();
final jsonArgs = json.encode(args);
final jsonRequestString = '{"jsonrpc": "2.0", '
'"id": "$requestId",'
'"method": "$command",'
'"params": $jsonArgs}';
// Logging.instance.log("ElectrumX jsonRequestString: $jsonRequestString");
final response = await _rpcClient!.request(
jsonRequestString,
requestTimeout,
final response = await _electrumAdapterClient!.request(
command,
args,
);
if (response.exception != null) {
throw response.exception!;
}
if (response.data is Map && response.data["error"] != null) {
if (response.data["error"]
if (response is Map &&
response.keys.contains("error") &&
response["error"] != null) {
if (response["error"]
.toString()
.contains("No such mempool or blockchain transaction")) {
throw NoSuchTransactionException(
@ -305,13 +337,19 @@ class ElectrumXClient {
throw Exception(
"JSONRPC response\n"
" command: $command\n"
" error: ${response.data}"
" error: ${response["error"]}\n"
" args: $args\n",
);
}
currentFailoverIndex = -1;
return response.data;
// If the command is a ping, a good return should always be null.
if (command.contains("ping")) {
return true;
}
return response;
} on WifiOnlyException {
rethrow;
} on SocketException {
@ -347,9 +385,9 @@ class ElectrumXClient {
/// map of <request id string : arguments list>
///
/// returns a list of json response objects if no errors were found
Future<List<Map<String, dynamic>>> batchRequest({
Future<List<dynamic>> batchRequest({
required String command,
required Map<String, List<dynamic>> args,
required List<dynamic> args,
Duration requestTimeout = const Duration(seconds: 60),
int retries = 2,
}) async {
@ -358,65 +396,50 @@ class ElectrumXClient {
}
if (_requireMutex) {
await _torConnectingLock.protect(() async => _checkRpcClient());
await _torConnectingLock
.protect(() async => await checkElectrumAdapter());
} else {
_checkRpcClient();
await checkElectrumAdapter();
}
try {
final List<String> requestStrings = [];
for (final entry in args.entries) {
final jsonArgs = json.encode(entry.value);
requestStrings.add(
'{"jsonrpc": "2.0", "id": "${entry.key}","method": "$command","params": $jsonArgs}');
}
// combine request strings into json array
String request = "[";
for (int i = 0; i < requestStrings.length - 1; i++) {
request += "${requestStrings[i]},";
}
request += "${requestStrings.last}]";
// Logging.instance.log("batch request: $request");
// send batch request
final jsonRpcResponse =
(await _rpcClient!.request(request, requestTimeout));
if (jsonRpcResponse.exception != null) {
throw jsonRpcResponse.exception!;
}
final List<dynamic> response;
try {
response = jsonRpcResponse.data as List;
} catch (_) {
throw Exception(
"Expected json list but got a map: ${jsonRpcResponse.data}",
);
}
// check for errors, format and throw if there are any
final List<String> errors = [];
for (int i = 0; i < response.length; i++) {
final result = response[i];
if (result["error"] != null || result["result"] == null) {
errors.add(result.toString());
var futures = <Future<dynamic>>[];
_electrumAdapterClient!.peer.withBatch(() {
for (final arg in args) {
futures.add(_electrumAdapterClient!.request(command, arg));
}
}
if (errors.isNotEmpty) {
String error = "[\n";
for (int i = 0; i < errors.length; i++) {
error += "${errors[i]}\n";
}
error += "]";
throw Exception("JSONRPC response error: $error");
}
});
final response = await Future.wait(futures);
// We cannot modify the response list as the order and length are related
// to the order and length of the batched requests!
//
// // check for errors, format and throw if there are any
// final List<String> errors = [];
// for (int i = 0; i < response.length; i++) {
// var result = response[i];
//
// if (result == null || (result is List && result.isEmpty)) {
// continue;
// // TODO [prio=extreme]: Figure out if this is actually an issue.
// }
// result = result[0]; // Unwrap the list.
// if ((result is Map && result.keys.contains("error")) ||
// result == null) {
// errors.add(result.toString());
// }
// }
// if (errors.isNotEmpty) {
// String error = "[\n";
// for (int i = 0; i < errors.length; i++) {
// error += "${errors[i]}\n";
// }
// error += "]";
// throw Exception("JSONRPC response error: $error");
// }
currentFailoverIndex = -1;
return List<Map<String, dynamic>>.from(response, growable: false);
return response;
} on WifiOnlyException {
rethrow;
} on SocketException {
@ -451,13 +474,23 @@ class ElectrumXClient {
/// Returns true if ping succeeded
Future<bool> ping({String? requestID, int retryCount = 1}) async {
try {
final response = await request(
// This doesn't work because electrum_adapter only returns the result:
// (which is always `null`).
// await checkElectrumAdapter();
// final response = await electrumAdapterClient!
// .ping()
// .timeout(const Duration(seconds: 2));
// return (response as Map<String, dynamic>).isNotEmpty;
// Because request() has been updated to use electrum_adapter, and because
// electrum_adapter returns the result of the request, request() has been
// updated to return a bool on a server.ping command as a special case.
return await request(
requestID: requestID,
command: 'server.ping',
requestTimeout: const Duration(seconds: 2),
retries: retryCount,
).timeout(const Duration(seconds: 2)) as Map<String, dynamic>;
return response.keys.contains("result") && response["result"] == null;
).timeout(const Duration(seconds: 2)) as bool;
} catch (e) {
rethrow;
}
@ -478,14 +511,14 @@ class ElectrumXClient {
requestID: requestID,
command: 'blockchain.headers.subscribe',
);
if (response["result"] == null) {
if (response == null) {
Logging.instance.log(
"getBlockHeadTip returned null response",
level: LogLevel.Error,
);
throw 'getBlockHeadTip returned null response';
}
return Map<String, dynamic>.from(response["result"] as Map);
return Map<String, dynamic>.from(response as Map);
} catch (e) {
rethrow;
}
@ -510,7 +543,7 @@ class ElectrumXClient {
requestID: requestID,
command: 'server.features',
);
return Map<String, dynamic>.from(response["result"] as Map);
return Map<String, dynamic>.from(response as Map);
} catch (e) {
rethrow;
}
@ -531,7 +564,7 @@ class ElectrumXClient {
rawTx,
],
);
return response["result"] as String;
return response as String;
} catch (e) {
rethrow;
}
@ -558,7 +591,7 @@ class ElectrumXClient {
scripthash,
],
);
return Map<String, dynamic>.from(response["result"] as Map);
return Map<String, dynamic>.from(response as Map);
} catch (e) {
rethrow;
}
@ -595,8 +628,7 @@ class ElectrumXClient {
scripthash,
],
);
result = response["result"];
result = response;
retryCount--;
}
@ -606,17 +638,17 @@ class ElectrumXClient {
}
}
Future<Map<String, List<Map<String, dynamic>>>> getBatchHistory(
{required Map<String, List<dynamic>> args}) async {
Future<List<List<Map<String, dynamic>>>> getBatchHistory({
required List<dynamic> args,
}) async {
try {
final response = await batchRequest(
command: 'blockchain.scripthash.get_history',
args: args,
);
final Map<String, List<Map<String, dynamic>>> result = {};
final List<List<Map<String, dynamic>>> result = [];
for (int i = 0; i < response.length; i++) {
result[response[i]["id"] as String] =
List<Map<String, dynamic>>.from(response[i]["result"] as List);
result.add(List<Map<String, dynamic>>.from(response[i] as List));
}
return result;
} catch (e) {
@ -654,23 +686,37 @@ class ElectrumXClient {
scripthash,
],
);
return List<Map<String, dynamic>>.from(response["result"] as List);
return List<Map<String, dynamic>>.from(response as List);
} catch (e) {
rethrow;
}
}
Future<Map<String, List<Map<String, dynamic>>>> getBatchUTXOs(
{required Map<String, List<dynamic>> args}) async {
Future<List<List<Map<String, dynamic>>>> getBatchUTXOs({
required List<dynamic> args,
}) async {
try {
final response = await batchRequest(
command: 'blockchain.scripthash.listunspent',
args: args,
);
final Map<String, List<Map<String, dynamic>>> result = {};
final List<List<Map<String, dynamic>>> result = [];
for (int i = 0; i < response.length; i++) {
result[response[i]["id"] as String] =
List<Map<String, dynamic>>.from(response[i]["result"] as List);
if ((response[i] as List).isNotEmpty) {
try {
final data = List<Map<String, dynamic>>.from(response[i] as List);
result.add(data);
} catch (e) {
// to ensure we keep same length of responses as requests/args
// add empty list on error
result.add([]);
Logging.instance.log(
"getBatchUTXOs failed to parse response=${response[i]}: $e",
level: LogLevel.Error,
);
}
}
}
return result;
} catch (e) {
@ -730,36 +776,18 @@ class ElectrumXClient {
bool verbose = true,
String? requestID,
}) async {
dynamic response;
try {
response = await request(
requestID: requestID,
command: 'blockchain.transaction.get',
args: [
txHash,
verbose,
],
);
if (!verbose) {
return {"rawtx": response["result"] as String};
}
Logging.instance.log("attempting to fetch blockchain.transaction.get...",
level: LogLevel.Info);
await checkElectrumAdapter();
dynamic response = await _electrumAdapterClient!.getTransaction(txHash);
Logging.instance.log("Fetching blockchain.transaction.get finished",
level: LogLevel.Info);
if (response["result"] == null) {
Logging.instance.log(
"getTransaction($txHash) returned null response",
level: LogLevel.Error,
);
throw 'getTransaction($txHash) returned null response';
}
return Map<String, dynamic>.from(response["result"] as Map);
} catch (e) {
Logging.instance.log(
"getTransaction($txHash) response: $response",
level: LogLevel.Error,
);
rethrow;
if (!verbose) {
return {"rawtx": response as String};
}
return Map<String, dynamic>.from(response as Map);
}
/// Returns the whole Lelantus anonymity set for denomination in the groupId.
@ -781,23 +809,15 @@ class ElectrumXClient {
String blockhash = "",
String? requestID,
}) async {
try {
Logging.instance.log("attempting to fetch lelantus.getanonymityset...",
level: LogLevel.Info);
final response = await request(
requestID: requestID,
command: 'lelantus.getanonymityset',
args: [
groupId,
blockhash,
],
);
Logging.instance.log("Fetching lelantus.getanonymityset finished",
level: LogLevel.Info);
return Map<String, dynamic>.from(response["result"] as Map);
} catch (e) {
rethrow;
}
Logging.instance.log("attempting to fetch lelantus.getanonymityset...",
level: LogLevel.Info);
await checkElectrumAdapter();
Map<String, dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)!
.getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash);
Logging.instance.log("Fetching lelantus.getanonymityset finished",
level: LogLevel.Info);
return response;
}
//TODO add example to docs
@ -808,18 +828,14 @@ class ElectrumXClient {
dynamic mints,
String? requestID,
}) async {
try {
final response = await request(
requestID: requestID,
command: 'lelantus.getmintmetadata',
args: [
mints,
],
);
return response["result"];
} catch (e) {
rethrow;
}
Logging.instance.log("attempting to fetch lelantus.getmintmetadata...",
level: LogLevel.Info);
await checkElectrumAdapter();
dynamic response = await (_electrumAdapterClient as FiroElectrumClient)!
.getLelantusMintData(mints: mints);
Logging.instance.log("Fetching lelantus.getmintmetadata finished",
level: LogLevel.Info);
return response;
}
//TODO add example to docs
@ -828,45 +844,38 @@ class ElectrumXClient {
String? requestID,
required int startNumber,
}) async {
try {
int retryCount = 3;
dynamic result;
Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...",
level: LogLevel.Info);
await checkElectrumAdapter();
while (retryCount > 0 && result is! List) {
final response = await request(
requestID: requestID,
command: 'lelantus.getusedcoinserials',
args: [
"$startNumber",
],
requestTimeout: const Duration(minutes: 2),
);
int retryCount = 3;
dynamic response;
result = response["result"];
retryCount--;
}
while (retryCount > 0 && response is! List) {
response = await (_electrumAdapterClient as FiroElectrumClient)!
.getLelantusUsedCoinSerials(startNumber: startNumber);
// TODO add 2 minute timeout.
Logging.instance.log("Fetching lelantus.getusedcoinserials finished",
level: LogLevel.Info);
return Map<String, dynamic>.from(result as Map);
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
retryCount--;
}
return Map<String, dynamic>.from(response as Map);
}
/// Returns the latest Lelantus set id
///
/// ex: 1
Future<int> getLelantusLatestCoinId({String? requestID}) async {
try {
final response = await request(
requestID: requestID,
command: 'lelantus.getlatestcoinid',
);
return response["result"] as int;
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
}
Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...",
level: LogLevel.Info);
await checkElectrumAdapter();
int response =
await (_electrumAdapterClient as FiroElectrumClient).getLatestCoinId();
Logging.instance.log("Fetching lelantus.getlatestcoinid finished",
level: LogLevel.Info);
return response;
}
// ============== Spark ======================================================
@ -892,17 +901,14 @@ class ElectrumXClient {
try {
Logging.instance.log("attempting to fetch spark.getsparkanonymityset...",
level: LogLevel.Info);
final response = await request(
requestID: requestID,
command: 'spark.getsparkanonymityset',
args: [
coinGroupId,
startBlockHash,
],
);
await checkElectrumAdapter();
Map<String, dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)
.getSparkAnonymitySet(
coinGroupId: coinGroupId, startBlockHash: startBlockHash);
Logging.instance.log("Fetching spark.getsparkanonymityset finished",
level: LogLevel.Info);
return Map<String, dynamic>.from(response["result"] as Map);
return response;
} catch (e) {
rethrow;
}
@ -915,15 +921,17 @@ class ElectrumXClient {
required int startNumber,
}) async {
try {
final response = await request(
requestID: requestID,
command: 'spark.getusedcoinstags',
args: [
"$startNumber",
],
requestTimeout: const Duration(minutes: 2),
);
final map = Map<String, dynamic>.from(response["result"] as Map);
// Use electrum_adapter package's getSparkUsedCoinsTags method.
Logging.instance.log("attempting to fetch spark.getusedcoinstags...",
level: LogLevel.Info);
await checkElectrumAdapter();
Map<String, dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)
.getUsedCoinsTags(startNumber: startNumber);
// TODO: Add 2 minute timeout.
Logging.instance.log("Fetching spark.getusedcoinstags finished",
level: LogLevel.Info);
final map = Map<String, dynamic>.from(response);
final set = Set<String>.from(map["tags"] as List);
return await compute(_ffiHashTagsComputeWrapper, set);
} catch (e) {
@ -947,16 +955,15 @@ class ElectrumXClient {
required List<String> sparkCoinHashes,
}) async {
try {
final response = await request(
requestID: requestID,
command: 'spark.getsparkmintmetadata',
args: [
{
"coinHashes": sparkCoinHashes,
},
],
);
return List<Map<String, dynamic>>.from(response["result"] as List);
Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...",
level: LogLevel.Info);
await checkElectrumAdapter();
List<dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)
.getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
Logging.instance.log("Fetching spark.getsparkmintmetadata finished",
level: LogLevel.Info);
return List<Map<String, dynamic>>.from(response);
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
@ -970,11 +977,14 @@ class ElectrumXClient {
String? requestID,
}) async {
try {
final response = await request(
requestID: requestID,
command: 'spark.getsparklatestcoinid',
);
return response["result"] as int;
Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...",
level: LogLevel.Info);
await checkElectrumAdapter();
int response = await (_electrumAdapterClient as FiroElectrumClient)
.getSparkLatestCoinId();
Logging.instance.log("Fetching spark.getsparklatestcoinid finished",
level: LogLevel.Info);
return response;
} catch (e) {
Logging.instance.log(e, level: LogLevel.Error);
rethrow;
@ -991,15 +1001,8 @@ class ElectrumXClient {
/// "rate": 1000,
/// }
Future<Map<String, dynamic>> getFeeRate({String? requestID}) async {
try {
final response = await request(
requestID: requestID,
command: 'blockchain.getfeerate',
);
return Map<String, dynamic>.from(response["result"] as Map);
} catch (e) {
rethrow;
}
await checkElectrumAdapter();
return await _electrumAdapterClient!.getFeeRate();
}
/// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of [blocks].
@ -1016,7 +1019,26 @@ class ElectrumXClient {
blocks,
],
);
return Decimal.parse(response["result"].toString());
try {
// If the response is -1 or null, return a temporary hardcoded value for
// Dogecoin. This is a temporary fix until the fee estimation is fixed.
if (coin == Coin.dogecoin &&
(response == null ||
response == -1 ||
Decimal.parse(response.toString()) == Decimal.parse("-1"))) {
// Return 0.05 for slow, 0.2 for average, and 1 for fast txs.
// These numbers produce tx fees in line with txs in the wild on
// https://dogechain.info/
return Decimal.parse((1 / blocks).toString());
// TODO [prio=med]: Fix fee estimation.
}
return Decimal.parse(response.toString());
} catch (e, s) {
final String msg = "Error parsing fee rate. Response: $response"
"\nResult: ${response}\nError: $e\nStack trace: $s";
Logging.instance.log(msg, level: LogLevel.Fatal);
throw Exception(msg);
}
} catch (e) {
rethrow;
}
@ -1033,7 +1055,7 @@ class ElectrumXClient {
requestID: requestID,
command: 'blockchain.relayfee',
);
return Decimal.parse(response["result"].toString());
return Decimal.parse(response.toString());
} catch (e) {
rethrow;
}

View file

@ -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,

View file

@ -12,16 +12,25 @@
// import 'dart:convert';
// import 'dart:io';
//
// import 'package:flutter/foundation.dart';
// import 'package:event_bus/event_bus.dart';
// import 'package:mutex/mutex.dart';
// import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
// import 'package:stackwallet/exceptions/json_rpc/json_rpc_exception.dart';
// import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
// import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart';
// import 'package:stackwallet/services/event_bus/global_event_bus.dart';
// import 'package:stackwallet/services/tor_service.dart';
// import 'package:stackwallet/utilities/logger.dart';
// import 'package:stackwallet/utilities/prefs.dart';
// import 'package:tor_ffi_plugin/socks_socket.dart';
//
// class ElectrumXSubscription with ChangeNotifier {
// dynamic _response;
// dynamic get response => _response;
// set response(dynamic newData) {
// _response = newData;
// notifyListeners();
// }
// class ElectrumXSubscription {
// final StreamController<dynamic> _controller =
// StreamController(); // TODO controller params
//
// Stream<dynamic> get responseStream => _controller.stream;
//
// void addToStream(dynamic data) => _controller.add(data);
// }
//
// class SocketTask {
@ -40,67 +49,430 @@
// final Map<String, SocketTask> _tasks = {};
// Timer? _aliveTimer;
// Socket? _socket;
// SOCKSSocket? _socksSocket;
// late final bool _useSSL;
// late final Duration _connectionTimeout;
// late final Duration _keepAlive;
//
// bool get isConnected => _isConnected;
// bool get useSSL => _useSSL;
// // Used to reconnect.
// String? _host;
// int? _port;
//
// void Function(bool)? onConnectionStatusChanged;
//
// late Prefs _prefs;
// late TorService _torService;
// StreamSubscription<TorPreferenceChangedEvent>? _torPreferenceListener;
// StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
// final Mutex _torConnectingLock = Mutex();
// bool _requireMutex = false;
//
// List<ElectrumXNode>? failovers;
// int currentFailoverIndex = -1;
//
// SubscribableElectrumXClient({
// bool useSSL = true,
// required bool useSSL,
// required Prefs prefs,
// required List<ElectrumXNode> failovers,
// TorService? torService,
// this.onConnectionStatusChanged,
// Duration connectionTimeout = const Duration(seconds: 5),
// Duration keepAlive = const Duration(seconds: 10),
// EventBus? globalEventBusForTesting,
// }) {
// _useSSL = useSSL;
// _prefs = prefs;
// _torService = torService ?? TorService.sharedInstance;
// _connectionTimeout = connectionTimeout;
// _keepAlive = keepAlive;
//
// // If we're testing, use the global event bus for testing.
// final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
//
// // Listen to global event bus for Tor status changes.
// _torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
// (event) async {
// try {
// switch (event.newStatus) {
// case TorConnectionStatus.connecting:
// // If Tor is connecting, we need to wait.
// await _torConnectingLock.acquire();
// _requireMutex = true;
// break;
//
// case TorConnectionStatus.connected:
// case TorConnectionStatus.disconnected:
// // If Tor is connected or disconnected, we can release the lock.
// if (_torConnectingLock.isLocked) {
// _torConnectingLock.release();
// }
// _requireMutex = false;
// break;
// }
// } finally {
// // Ensure the lock is released.
// if (_torConnectingLock.isLocked) {
// _torConnectingLock.release();
// }
// }
// },
// );
//
// // Listen to global event bus for Tor preference changes.
// _torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen(
// (event) async {
// // Close open socket (if open).
// final tempSocket = _socket;
// _socket = null;
// await tempSocket?.close();
//
// // Close open SOCKS socket (if open).
// final tempSOCKSSocket = _socksSocket;
// _socksSocket = null;
// await tempSOCKSSocket?.close();
//
// // Clear subscriptions.
// _tasks.clear();
//
// // Cancel alive timer
// _aliveTimer?.cancel();
// },
// );
// }
//
// Future<void> connect({required String host, required int port}) async {
// try {
// await _socket?.close();
// } catch (_) {}
// factory SubscribableElectrumXClient.from({
// required ElectrumXNode node,
// required Prefs prefs,
// required List<ElectrumXNode> failovers,
// TorService? torService,
// }) {
// return SubscribableElectrumXClient(
// useSSL: node.useSSL,
// prefs: prefs,
// failovers: failovers,
// torService: torService ?? TorService.sharedInstance,
// );
// }
//
// if (_useSSL) {
// _socket = await SecureSocket.connect(
// host,
// port,
// timeout: _connectionTimeout,
// onBadCertificate: (_) => true,
// );
// } else {
// _socket = await Socket.connect(
// host,
// port,
// timeout: _connectionTimeout,
// );
// // Example for returning a future which completes upon connection.
// // static Future<SubscribableElectrumXClient> from({
// // required ElectrumXNode node,
// // TorService? torService,
// // }) async {
// // final client = SubscribableElectrumXClient(
// // useSSL: node.useSSL,
// // );
// //
// // await client.connect(host: node.address, port: node.port);
// //
// // return client;
// // }
//
// /// Check if the RPC client is connected and connect if needed.
// ///
// /// If Tor is enabled but not running, it will attempt to start Tor.
// Future<void> _checkSocket({bool connecting = false}) async {
// if (_prefs.useTor) {
// // If we're supposed to use Tor...
// if (_torService.status != TorConnectionStatus.connected) {
// // ... but Tor isn't running...
// if (!_prefs.torKillSwitch) {
// // ... and the killswitch isn't set, then we'll just return below.
// Logging.instance.log(
// "Tor preference set but Tor is not enabled, killswitch not set, connecting to ElectrumX through clearnet.",
// level: LogLevel.Warning,
// );
// } else {
// // ... but if the killswitch is set, then let's try to start Tor.
// await _torService.start();
// // TODO [prio=low]: Attempt to restart Tor if needed. Update Tor package for restart feature.
//
// // Double-check that Tor is running.
// if (_torService.status != TorConnectionStatus.connected) {
// // If Tor still isn't running, then we'll throw an exception.
// throw Exception("SubscribableElectrumXClient._checkRpcClient: "
// "Tor preference and killswitch set but Tor not enabled and could not start, not connecting to ElectrumX.");
// }
// }
// }
// }
// _updateConnectionStatus(true);
//
// _socket!.listen(
// _dataHandler,
// onError: _errorHandler,
// onDone: _doneHandler,
// cancelOnError: true,
// );
// // Connect if needed.
// if (!connecting) {
// if ((!_prefs.useTor && _socket == null) ||
// (_prefs.useTor && _socksSocket == null)) {
// if (currentFailoverIndex == -1) {
// // Check if we have cached node information
// if (_host == null && _port == null) {
// throw Exception("SubscribableElectrumXClient._checkRpcClient: "
// "No host or port provided and no cached node information.");
// }
//
// _aliveTimer?.cancel();
// _aliveTimer = Timer.periodic(
// _keepAlive,
// (_) async => _updateConnectionStatus(await ping()),
// );
// // Connect to the server.
// await connect(host: _host!, port: _port!);
// } else {
// // Attempt to connect to the next failover server.
// await connect(
// host: failovers![currentFailoverIndex].address,
// port: failovers![currentFailoverIndex].port,
// );
// }
// }
// }
// }
//
// /// Connect to the server.
// ///
// /// If Tor is enabled, it will attempt to connect through Tor.
// Future<void> connect({
// required String host,
// required int port,
// }) async {
// try {
// // Cache node information.
// _host = host;
// _port = port;
//
// // If we're already connected, disconnect first.
// try {
// await _socket?.close();
// } catch (_) {}
//
// // If we're connecting to Tor, wait.
// if (_requireMutex) {
// await _torConnectingLock
// .protect(() async => await _checkSocket(connecting: true));
// } else {
// await _checkSocket(connecting: true);
// }
//
// if (!Prefs.instance.useTor) {
// // If we're not supposed to use Tor, then connect directly.
// await connectClearnet(host, port);
// } else {
// // If we're supposed to use Tor...
// if (_torService.status != TorConnectionStatus.connected) {
// // ... but Tor isn't running...
// if (!_prefs.torKillSwitch) {
// // ... and the killswitch isn't set, then we'll connect clearnet.
// Logging.instance.log(
// "Tor preference set but Tor not enabled, no killswitch set, connecting to ElectrumX through clearnet",
// level: LogLevel.Warning,
// );
// await connectClearnet(host, port);
// } else {
// // ... but if the killswitch is set, then let's try to start Tor.
// await _torService.start();
// // TODO [prio=low]: Attempt to restart Tor if needed. Update Tor package for restart feature.
//
// // Doublecheck that Tor is running.
// if (_torService.status != TorConnectionStatus.connected) {
// // If Tor still isn't running, then we'll throw an exception.
// throw Exception(
// "Tor preference and killswitch set but Tor not enabled, not connecting to ElectrumX");
// }
//
// // Connect via Tor.
// await connectTor(host, port);
// }
// } else {
// // Connect via Tor.
// await connectTor(host, port);
// }
// }
//
// _updateConnectionStatus(true);
//
// if (_prefs.useTor) {
// if (_socksSocket == null) {
// final String msg = "SubscribableElectrumXClient.connect(): "
// "cannot listen to $host:$port via SOCKSSocket because it is not connected.";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw Exception(msg);
// }
//
// _socksSocket!.listen(
// _dataHandler,
// onError: _errorHandler,
// onDone: _doneHandler,
// cancelOnError: true,
// );
// } else {
// if (_socket == null) {
// final String msg = "SubscribableElectrumXClient.connect(): "
// "cannot listen to $host:$port via socket because it is not connected.";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw Exception(msg);
// }
//
// _socket!.listen(
// _dataHandler,
// onError: _errorHandler,
// onDone: _doneHandler,
// cancelOnError: true,
// );
// }
//
// _aliveTimer?.cancel();
// _aliveTimer = Timer.periodic(
// _keepAlive,
// (_) async => _updateConnectionStatus(await ping()),
// );
// } catch (e, s) {
// final msg = "SubscribableElectrumXClient.connect: "
// "failed to connect to $host:$port."
// "\nError: $e\nStack trace: $s";
// Logging.instance.log(msg, level: LogLevel.Fatal);
//
// // Ensure cleanup is performed on failure to avoid resource leaks.
// await disconnect(); // Use the disconnect method to clean up.
// rethrow; // Rethrow the exception to handle it further up the call stack.
// }
// }
//
// /// Connect to the server directly.
// Future<void> connectClearnet(String host, int port) async {
// try {
// Logging.instance.log(
// "SubscribableElectrumXClient.connectClearnet(): "
// "creating a socket to $host:$port (SSL $useSSL)...",
// level: LogLevel.Info);
//
// if (_useSSL) {
// _socket = await SecureSocket.connect(
// host,
// port,
// timeout: _connectionTimeout,
// onBadCertificate: (_) =>
// true, // TODO do not automatically trust bad certificates.
// );
// } else {
// _socket = await Socket.connect(
// host,
// port,
// timeout: _connectionTimeout,
// );
// }
//
// Logging.instance.log(
// "SubscribableElectrumXClient.connectClearnet(): "
// "created socket to $host:$port...",
// level: LogLevel.Info);
// } catch (e, s) {
// final String msg = "SubscribableElectrumXClient.connectClearnet: "
// "failed to connect to $host (SSL: $useSSL)."
// "\nError: $e\nStack trace: $s";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw JsonRpcException(msg);
// }
//
// return;
// }
//
// /// Connect to the server using the Tor service.
// Future<void> connectTor(String host, int port) async {
// // Get the proxy info from the TorService.
// final proxyInfo = _torService.getProxyInfo();
//
// try {
// Logging.instance.log(
// "SubscribableElectrumXClient.connectTor(): "
// "creating a SOCKS socket at $proxyInfo (SSL $useSSL)...",
// level: LogLevel.Info);
//
// // Create a socks socket using the Tor service's proxy info.
// _socksSocket = await SOCKSSocket.create(
// proxyHost: proxyInfo.host.address,
// proxyPort: proxyInfo.port,
// sslEnabled: useSSL,
// );
//
// Logging.instance.log(
// "SubscribableElectrumXClient.connectTor(): "
// "created SOCKS socket at $proxyInfo...",
// level: LogLevel.Info);
// } catch (e, s) {
// final String msg = "SubscribableElectrumXClient.connectTor(): "
// "failed to create a SOCKS socket at $proxyInfo (SSL $useSSL)..."
// "\nError: $e\nStack trace: $s";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw JsonRpcException(msg);
// }
//
// try {
// Logging.instance.log(
// "SubscribableElectrumXClient.connectTor(): "
// "connecting to SOCKS socket at $proxyInfo (SSL $useSSL)...",
// level: LogLevel.Info);
//
// await _socksSocket?.connect();
//
// Logging.instance.log(
// "SubscribableElectrumXClient.connectTor(): "
// "connected to SOCKS socket at $proxyInfo...",
// level: LogLevel.Info);
// } catch (e, s) {
// final String msg = "SubscribableElectrumXClient.connectTor(): "
// "failed to connect to SOCKS socket at $proxyInfo.."
// "\nError: $e\nStack trace: $s";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw JsonRpcException(msg);
// }
//
// try {
// Logging.instance.log(
// "SubscribableElectrumXClient.connectTor(): "
// "connecting to $host:$port over SOCKS socket at $proxyInfo...",
// level: LogLevel.Info);
//
// await _socksSocket?.connectTo(host, port);
//
// Logging.instance.log(
// "SubscribableElectrumXClient.connectTor(): "
// "connected to $host:$port over SOCKS socket at $proxyInfo",
// level: LogLevel.Info);
// } catch (e, s) {
// final String msg = "SubscribableElectrumXClient.connectTor(): "
// "failed to connect $host over tor proxy at $proxyInfo."
// "\nError: $e\nStack trace: $s";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw JsonRpcException(msg);
// }
//
// return;
// }
//
// /// Disconnect from the server.
// Future<void> disconnect() async {
// _aliveTimer?.cancel();
// await _socket?.close();
// _aliveTimer = null;
//
// try {
// await _socket?.close();
// } catch (e, s) {
// Logging.instance.log(
// "SubscribableElectrumXClient.disconnect: failed to close socket."
// "\nError: $e\nStack trace: $s",
// level: LogLevel.Warning);
// }
// _socket = null;
//
// try {
// await _socksSocket?.close();
// } catch (e, s) {
// Logging.instance.log(
// "SubscribableElectrumXClient.disconnect: failed to close SOCKS socket."
// "\nError: $e\nStack trace: $s",
// level: LogLevel.Warning);
// }
// _socksSocket = null;
//
// onConnectionStatusChanged = null;
// }
//
// /// Format JSON request string.
// String _buildJsonRequestString({
// required String method,
// required String id,
@ -110,6 +482,7 @@
// return '{"jsonrpc": "2.0", "id": "$id","method": "$method","params": $paramString}\r\n';
// }
//
// /// Update the connection status and call the onConnectionStatusChanged callback if it exists.
// void _updateConnectionStatus(bool connectionStatus) {
// if (_isConnected != connectionStatus && onConnectionStatusChanged != null) {
// onConnectionStatusChanged!(connectionStatus);
@ -117,6 +490,7 @@
// _isConnected = connectionStatus;
// }
//
// /// Called when the socket has data.
// void _dataHandler(List<int> data) {
// _responseData.addAll(data);
//
@ -137,6 +511,7 @@
// }
// }
//
// /// Called when the socket has a response.
// void _responseHandler(Map<String, dynamic> response) {
// // subscriptions will have a method in the response
// if (response['method'] is String) {
@ -150,6 +525,7 @@
// _complete(id, result);
// }
//
// /// Called when the subscription has a response.
// void _subscriptionHandler({
// required Map<String, dynamic> response,
// }) {
@ -160,19 +536,20 @@
// final scripthash = params.first as String;
// final taskId = "blockchain.scripthash.subscribe:$scripthash";
//
// _tasks[taskId]?.subscription?.response = params.last;
// _tasks[taskId]?.subscription?.addToStream(params.last);
// break;
// case "blockchain.headers.subscribe":
// final params = response["params"];
// const taskId = "blockchain.headers.subscribe";
//
// _tasks[taskId]?.subscription?.response = params.first;
// _tasks[taskId]?.subscription?.addToStream(params.first);
// break;
// default:
// break;
// }
// }
//
// /// Called when the socket has an error.
// void _errorHandler(Object error, StackTrace trace) {
// _updateConnectionStatus(false);
// Logging.instance.log(
@ -180,12 +557,14 @@
// level: LogLevel.Info);
// }
//
// /// Called when the socket is closed.
// void _doneHandler() {
// _updateConnectionStatus(false);
// Logging.instance.log("SubscribableElectrumXClient called _doneHandler",
// level: LogLevel.Info);
// }
//
// /// Complete a task with the given id and data.
// void _complete(String id, dynamic data) {
// if (_tasks[id] == null) {
// return;
@ -198,10 +577,11 @@
// if (!(_tasks[id]?.isSubscription ?? false)) {
// _tasks.remove(id);
// } else {
// _tasks[id]?.subscription?.response = data;
// _tasks[id]?.subscription?.addToStream(data);
// }
// }
//
// /// Add a task to the task list.
// void _addTask({
// required String id,
// required Completer<dynamic> completer,
@ -209,6 +589,7 @@
// _tasks[id] = SocketTask(completer: completer, subscription: null);
// }
//
// /// Add a subscription task to the task list.
// void _addSubscriptionTask({
// required String id,
// required ElectrumXSubscription subscription,
@ -216,83 +597,240 @@
// _tasks[id] = SocketTask(completer: null, subscription: subscription);
// }
//
// /// Write call to socket.
// Future<dynamic> _call({
// required String method,
// List<dynamic> params = const [],
// }) async {
// // If we're connecting to Tor, wait.
// if (_requireMutex) {
// await _torConnectingLock.protect(() async => await _checkSocket());
// } else {
// await _checkSocket();
// }
//
// // Check socket is connected.
// if (_prefs.useTor) {
// if (_socksSocket == null) {
// final msg = "SubscribableElectrumXClient._call: "
// "SOCKSSocket is not connected. Method $method, params $params.";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw Exception(msg);
// }
// } else {
// if (_socket == null) {
// final msg = "SubscribableElectrumXClient._call: "
// "Socket is not connected. Method $method, params $params.";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw Exception(msg);
// }
// }
//
// final completer = Completer<dynamic>();
// _currentRequestID++;
// final id = _currentRequestID.toString();
// _addTask(id: id, completer: completer);
//
// _socket?.write(
// _buildJsonRequestString(
// method: method,
// id: id,
// params: params,
// ),
// );
// // Write to the socket.
// try {
// _addTask(id: id, completer: completer);
//
// return completer.future;
// if (_prefs.useTor) {
// _socksSocket?.write(
// _buildJsonRequestString(
// method: method,
// id: id,
// params: params,
// ),
// );
// } else {
// _socket?.write(
// _buildJsonRequestString(
// method: method,
// id: id,
// params: params,
// ),
// );
// }
//
// return completer.future;
// } catch (e, s) {
// final String msg = "SubscribableElectrumXClient._call: "
// "failed to request $method with id $id."
// "\nError: $e\nStack trace: $s";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw JsonRpcException(msg);
// }
// }
//
// /// Write call to socket with timeout.
// Future<dynamic> _callWithTimeout({
// required String method,
// List<dynamic> params = const [],
// Duration timeout = const Duration(seconds: 2),
// }) async {
// // If we're connecting to Tor, wait.
// if (_requireMutex) {
// await _torConnectingLock.protect(() async => await _checkSocket());
// } else {
// await _checkSocket();
// }
//
// // Check socket is connected.
// if (_prefs.useTor) {
// if (_socksSocket == null) {
// try {
// if (_host == null || _port == null) {
// throw Exception("No host or port provided");
// }
//
// // Attempt to conect.
// await connect(
// host: _host!,
// port: _port!,
// );
// } catch (e, s) {
// final msg = "SubscribableElectrumXClient._callWithTimeout: "
// "SOCKSSocket not connected and cannot connect. "
// "Method $method, params $params."
// "\nError: $e\nStack trace: $s";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw Exception(msg);
// }
// }
// } else {
// if (_socket == null) {
// try {
// if (_host == null || _port == null) {
// throw Exception("No host or port provided");
// }
//
// // Attempt to conect.
// await connect(
// host: _host!,
// port: _port!,
// );
// } catch (e, s) {
// final msg = "SubscribableElectrumXClient._callWithTimeout: "
// "Socket not connected and cannot connect. "
// "Method $method, params $params.";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw Exception(msg);
// }
// }
// }
//
// final completer = Completer<dynamic>();
// _currentRequestID++;
// final id = _currentRequestID.toString();
// _addTask(id: id, completer: completer);
//
// _socket?.write(
// _buildJsonRequestString(
// method: method,
// id: id,
// params: params,
// ),
// );
// // Write to the socket.
// try {
// _addTask(id: id, completer: completer);
//
// Timer(timeout, () {
// if (!completer.isCompleted) {
// completer.completeError(
// Exception("Request \"id: $id, method: $method\" timed out!"),
// if (_prefs.useTor) {
// _socksSocket?.write(
// _buildJsonRequestString(
// method: method,
// id: id,
// params: params,
// ),
// );
// } else {
// _socket?.write(
// _buildJsonRequestString(
// method: method,
// id: id,
// params: params,
// ),
// );
// }
// });
//
// return completer.future;
// Timer(timeout, () {
// if (!completer.isCompleted) {
// completer.completeError(
// Exception("Request \"id: $id, method: $method\" timed out!"),
// );
// }
// });
//
// return completer.future;
// } catch (e, s) {
// final String msg = "SubscribableElectrumXClient._callWithTimeout: "
// "failed to request $method with id $id (timeout $timeout)."
// "\nError: $e\nStack trace: $s";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw JsonRpcException(msg);
// }
// }
//
// ElectrumXSubscription _subscribe({
// required String taskId,
// required String id,
// required String method,
// List<dynamic> params = const [],
// }) {
// // try {
// final subscription = ElectrumXSubscription();
// _addSubscriptionTask(id: taskId, subscription: subscription);
// _currentRequestID++;
// _socket?.write(
// _buildJsonRequestString(
// method: method,
// id: taskId,
// params: params,
// ),
// );
// try {
// final subscription = ElectrumXSubscription();
// _addSubscriptionTask(id: id, subscription: subscription);
// _currentRequestID++;
//
// return subscription;
// // } catch (e, s) {
// // Logging.instance.log("SubscribableElectrumXClient _subscribe: $e\n$s", level: LogLevel.Error);
// // return null;
// // }
// // Check socket is connected.
// if (_prefs.useTor) {
// if (_socksSocket == null) {
// final msg = "SubscribableElectrumXClient._call: "
// "SOCKSSocket is not connected. Method $method, params $params.";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw Exception(msg);
// }
// } else {
// if (_socket == null) {
// final msg = "SubscribableElectrumXClient._call: "
// "Socket is not connected. Method $method, params $params.";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw Exception(msg);
// }
// }
//
// // Write to the socket.
// if (_prefs.useTor) {
// _socksSocket?.write(
// _buildJsonRequestString(
// method: method,
// id: id,
// params: params,
// ),
// );
// } else {
// _socket?.write(
// _buildJsonRequestString(
// method: method,
// id: id,
// params: params,
// ),
// );
// }
//
// return subscription;
// } catch (e, s) {
// final String msg = "SubscribableElectrumXClient._subscribe: "
// "failed to subscribe to $method with id $id."
// "\nError: $e\nStack trace: $s";
// Logging.instance.log(msg, level: LogLevel.Fatal);
// throw JsonRpcException(msg);
// }
// }
//
// /// Ping the server to ensure it is responding
// ///
// /// Returns true if ping succeeded
// Future<bool> ping() async {
// // If we're connecting to Tor, wait.
// if (_requireMutex) {
// await _torConnectingLock.protect(() async => await _checkSocket());
// } else {
// await _checkSocket();
// }
//
// // Write to the socket.
// try {
// final response = (await _callWithTimeout(method: "server.ping")) as Map;
// return response.keys.contains("result") && response["result"] == null;
@ -304,7 +842,7 @@
// /// Subscribe to a scripthash to receive notifications on status changes
// ElectrumXSubscription subscribeToScripthash({required String scripthash}) {
// return _subscribe(
// taskId: 'blockchain.scripthash.subscribe:$scripthash',
// id: 'blockchain.scripthash.subscribe:$scripthash',
// method: 'blockchain.scripthash.subscribe',
// params: [scripthash],
// );
@ -316,7 +854,7 @@
// ElectrumXSubscription subscribeToBlockHeaders() {
// return _tasks["blockchain.headers.subscribe"]?.subscription ??
// _subscribe(
// taskId: "blockchain.headers.subscribe",
// id: "blockchain.headers.subscribe",
// method: "blockchain.headers.subscribe",
// params: [],
// );

View file

@ -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,
),

View file

@ -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,
),
],
),
),
),
);

View file

@ -726,12 +726,13 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
color: Theme.of(context).extension<StackColors>()!.background,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
child: SingleChildScrollView(
child: Column(
children: [
if (isDesktop)
/*if (isDesktop)
const Spacer(
flex: 10,
),
),*/
if (!isDesktop)
Text(
widget.walletName,
@ -1060,10 +1061,10 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
},
),
),
if (isDesktop)
/*if (isDesktop)
const Spacer(
flex: 15,
),
),*/
if (!isDesktop)
Expanded(
child: SingleChildScrollView(
@ -1174,6 +1175,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
),
),
),
),
);
}
}

View file

@ -13,6 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
@ -65,13 +66,21 @@ class _RestoreFailedDialogState extends ConsumerState<RestoreFailedDialog> {
style: STextStyles.itemSubtitle12(context),
),
onPressed: () async {
await ref.read(pWallets).deleteWallet(
ref.read(pWalletInfo(walletId)),
ref.read(secureStoreProvider),
);
if (mounted) {
Navigator.of(context).pop();
try {
await ref.read(pWallets).deleteWallet(
ref.read(pWalletInfo(walletId)),
ref.read(secureStoreProvider),
);
} catch (e, s) {
Logging.instance.log(
"Error while getting wallet info in restore failed dialog\n"
"Error: $e\nStack trace: $s",
level: LogLevel.Error,
);
} finally {
if (mounted) {
Navigator.of(context).pop();
}
}
},
),

View file

@ -8,6 +8,8 @@
*
*/
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
@ -234,9 +236,13 @@ class _EditContactAddressViewState
e.coin == addressEntry.coin,
);
_addresses.remove(entry);
//Deleting an entry directly from _addresses gives error
// "Cannot remove from a fixed-length list", so we remove the
// entry from a copy
var tempAddresses = List<ContactAddressEntry>.from(_addresses);
tempAddresses.remove(entry);
ContactEntry editedContact =
contact.copyWith(addresses: _addresses);
contact.copyWith(addresses: tempAddresses);
if (await ref
.read(addressBookServiceProvider)
.editContact(editedContact)) {

View file

@ -179,6 +179,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
useSSL: formData.useSSL!,
failovers: [],
prefs: ref.read(prefsChangeNotifierProvider),
coin: coin,
);
try {

View file

@ -156,6 +156,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
useSSL: node.useSSL,
failovers: [],
prefs: ref.read(prefsChangeNotifierProvider),
coin: coin,
);
try {

View file

@ -208,7 +208,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

View file

@ -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: () {

View file

@ -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
),
),

View file

@ -60,6 +60,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';
@ -307,6 +308,26 @@ class _WalletViewState extends ConsumerState<WalletView> {
BackupFrequencyType.afterClosingAWallet) {
unawaited(ref.read(autoSWBServiceProvider).doBackup());
}
// Close the wallet according to syncing preferences.
switch (ref.read(prefsChangeNotifierProvider).syncType) {
case SyncingType.currentWalletOnly:
// Close the wallet.
unawaited(ref.watch(pWallets).getWallet(walletId).exit());
// unawaited so we don't lag the UI.
case SyncingType.selectedWalletsAtStartup:
// Close if this wallet is not in the list to be synced.
if (!ref
.read(prefsChangeNotifierProvider)
.walletIdsSyncOnStartup
.contains(widget.walletId)) {
unawaited(ref.watch(pWallets).getWallet(walletId).exit());
// unawaited so we don't lag the UI.
}
case SyncingType.allWalletsOnStartup:
// Do nothing.
break;
}
}
Widget _buildNetworkIcon(WalletSyncStatus status) {

View file

@ -61,7 +61,7 @@ class DesktopAddressCard extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(
"${contactId == "default" ? entry.other! : entry.label} (${entry.coin.ticker})",
"${contactId == "default" ? entry.other : entry.label} (${entry.coin.ticker})",
style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),

View file

@ -10,9 +10,9 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
@ -38,7 +38,9 @@ import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
import 'package:stackwallet/utilities/enums/sync_type_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
@ -92,6 +94,26 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
unawaited(ref.read(autoSWBServiceProvider).doBackup());
}
// Close the wallet according to syncing preferences.
switch (ref.read(prefsChangeNotifierProvider).syncType) {
case SyncingType.currentWalletOnly:
// Close the wallet.
unawaited(wallet.exit());
// unawaited so we don't lag the UI.
case SyncingType.selectedWalletsAtStartup:
// Close if this wallet is not in the list to be synced.
if (!ref
.read(prefsChangeNotifierProvider)
.walletIdsSyncOnStartup
.contains(widget.walletId)) {
unawaited(wallet.exit());
// unawaited so we don't lag the UI.
}
case SyncingType.allWalletsOnStartup:
// Do nothing.
break;
}
ref.read(currentWalletIdProvider.notifier).state = null;
}
@ -181,6 +203,21 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
),
),
),
if (kDebugMode) const Spacer(),
if (kDebugMode)
Row(
children: [
const Text(
"Debug Height:",
),
const SizedBox(
width: 2,
),
Text(
ref.watch(pWalletChainHeight(widget.walletId)).toString(),
),
],
),
const Spacer(),
Row(
children: [

View file

@ -105,7 +105,12 @@ class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> {
children: [
const Padding(
padding: EdgeInsets.all(15.0),
child: SettingsMenu(),
child: Align(
alignment: Alignment.topLeft,
child: SingleChildScrollView(
child: SettingsMenu(),
),
),
),
Expanded(
child: contentViews[

View file

@ -36,305 +36,308 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> {
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
return Column(
children: [
Padding(
padding: const EdgeInsets.only(
right: 30,
),
child: RoundedWhiteContainer(
radiusMultiplier: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: SvgPicture.asset(
Assets.svg.circleSliders,
width: 48,
height: 48,
return SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
right: 30,
),
child: RoundedWhiteContainer(
radiusMultiplier: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: SvgPicture.asset(
Assets.svg.circleSliders,
width: 48,
height: 48,
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.all(10),
child: RichText(
textAlign: TextAlign.start,
text: TextSpan(
children: [
TextSpan(
text: "Advanced",
style: STextStyles.desktopTextSmall(context),
),
TextSpan(
text:
"\n\nConfigure these settings only if you know what you are doing!",
style: STextStyles.desktopTextExtraExtraSmall(
context),
),
],
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.all(10),
child: RichText(
textAlign: TextAlign.start,
text: TextSpan(
children: [
TextSpan(
text: "Advanced",
style: STextStyles.desktopTextSmall(context),
),
TextSpan(
text:
"\n\nConfigure these settings only if you know what you are doing!",
style: STextStyles.desktopTextExtraExtraSmall(
context),
),
],
),
),
),
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
),
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Toggle testnet coins",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
prefsChangeNotifierProvider
.select((value) => value.showTestNetCoins),
),
onValueChanged: (newValue) {
ref
.read(prefsChangeNotifierProvider)
.showTestNetCoins = newValue;
},
),
),
],
),
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Enable coin control",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
prefsChangeNotifierProvider
.select((value) => value.enableCoinControl),
),
onValueChanged: (newValue) {
ref
.read(prefsChangeNotifierProvider)
.enableCoinControl = newValue;
},
),
),
],
),
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
),
),
/// TODO: Make a dialog popup
Consumer(builder: (_, ref, __) {
final externalCalls = ref.watch(
prefsChangeNotifierProvider
.select((value) => value.externalCalls),
);
return Padding(
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Stack Experience",
style:
STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
Text(
externalCalls ? "Easy crypto" : "Incognito",
style: STextStyles.desktopTextExtraExtraSmall(
context),
),
],
Text(
"Toggle testnet coins",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.showTestNetCoins),
),
onValueChanged: (newValue) {
ref
.read(prefsChangeNotifierProvider)
.showTestNetCoins = newValue;
},
),
),
PrimaryButton(
label: "Change",
buttonHeight: ButtonHeight.xs,
width: 101,
onPressed: () async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const StackPrivacyDialog();
},
);
},
)
],
),
);
}),
],
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Block explorers",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
PrimaryButton(
buttonHeight: ButtonHeight.xs,
label: "Edit",
width: 101,
onPressed: () async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const DesktopManageBlockExplorersDialog();
},
);
},
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Enable coin control",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl),
),
onValueChanged: (newValue) {
ref
.read(prefsChangeNotifierProvider)
.enableCoinControl = newValue;
},
),
),
],
),
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
),
),
/// TODO: Make a dialog popup
Consumer(builder: (_, ref, __) {
final externalCalls = ref.watch(
prefsChangeNotifierProvider
.select((value) => value.externalCalls),
);
return Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Stack Experience",
style: STextStyles.desktopTextExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
Text(
externalCalls ? "Easy crypto" : "Incognito",
style:
STextStyles.desktopTextExtraExtraSmall(
context),
),
],
),
PrimaryButton(
label: "Change",
buttonHeight: ButtonHeight.xs,
width: 101,
onPressed: () async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const StackPrivacyDialog();
},
);
},
)
],
),
);
}),
],
),
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
),
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Units",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
PrimaryButton(
buttonHeight: ButtonHeight.xs,
label: "Edit",
width: 101,
onPressed: () async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const ManageCoinUnitsView();
},
);
},
),
],
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Block explorers",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
PrimaryButton(
buttonHeight: ButtonHeight.xs,
label: "Edit",
width: 101,
onPressed: () async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const DesktopManageBlockExplorersDialog();
},
);
},
),
],
),
),
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
),
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Debug info",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
PrimaryButton(
buttonHeight: ButtonHeight.xs,
label: "Show logs",
width: 101,
onPressed: () async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const DebugInfoDialog();
},
);
},
),
],
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Units",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
PrimaryButton(
buttonHeight: ButtonHeight.xs,
label: "Edit",
width: 101,
onPressed: () async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const ManageCoinUnitsView();
},
);
},
),
],
),
),
),
const SizedBox(
height: 10,
),
],
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Debug info",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark),
textAlign: TextAlign.left,
),
PrimaryButton(
buttonHeight: ButtonHeight.xs,
label: "Show logs",
width: 101,
onPressed: () async {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const DebugInfoDialog();
},
);
},
),
],
),
),
const SizedBox(
height: 10,
),
],
),
),
),
),
],
],
),
);
}
}

View file

@ -98,7 +98,10 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> {
@override
Widget build(BuildContext context) {
return DesktopDialog(
maxHeight: 850,
// Max height of 850 unless the screen is smaller than that.
maxHeight: MediaQuery.of(context).size.height < 850
? MediaQuery.of(context).size.height
: 850,
maxWidth: 600,
child: Column(
children: [

View file

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

View file

@ -42,7 +42,13 @@ class Wallets {
final Map<String, Wallet> _wallets = {};
Wallet getWallet(String walletId) => _wallets[walletId]!;
Wallet getWallet(String walletId) {
if (_wallets[walletId] != null) {
return _wallets[walletId]!;
} else {
throw Exception("Wallet with id $walletId not found");
}
}
void addWallet(Wallet wallet) {
if (_wallets[wallet.walletId] != null) {

View file

@ -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);

View file

@ -17,7 +17,7 @@ abstract class DefaultEpicBoxes {
static List<String> get defaultIds => ['americas', 'asia', 'europe'];
static EpicBoxServerModel get americas => EpicBoxServerModel(
host: 'epicbox.epic.tech',
host: 'stackwallet.epicbox.com',
port: 443,
name: 'Americas',
id: 'americas',

View file

@ -119,28 +119,28 @@ class BitcoincashWallet extends Bip39HDWallet
List<Map<String, dynamic>> allTransactions = [];
for (final txHash in allTxHashes) {
final storedTx = await mainDB.isar.transactionV2s
.where()
.txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId)
.findFirst();
// final storedTx = await mainDB.isar.transactionV2s
// .where()
// .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId)
// .findFirst();
//
// if (storedTx == null ||
// storedTx.height == null ||
// (storedTx.height != null && storedTx.height! <= 0)) {
final tx = await electrumXCachedClient.getTransaction(
txHash: txHash["tx_hash"] as String,
verbose: true,
coin: cryptoCurrency.coin,
);
if (storedTx == null ||
storedTx.height == null ||
(storedTx.height != null && storedTx.height! <= 0)) {
final tx = await electrumXCachedClient.getTransaction(
txHash: txHash["tx_hash"] as String,
verbose: true,
coin: cryptoCurrency.coin,
);
// check for duplicates before adding to list
if (allTransactions
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
-1) {
tx["height"] = txHash["height"];
allTransactions.add(tx);
}
// check for duplicates before adding to list
if (allTransactions
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
-1) {
tx["height"] = txHash["height"];
allTransactions.add(tx);
}
// }
}
final List<TransactionV2> txns = [];
@ -174,22 +174,28 @@ class BitcoincashWallet extends Bip39HDWallet
coin: cryptoCurrency.coin,
);
final prevOutJson = Map<String, dynamic>.from(
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
as Map);
try {
final prevOutJson = Map<String, dynamic>.from(
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
as Map);
final prevOut = OutputV2.fromElectrumXJson(
prevOutJson,
decimalPlaces: cryptoCurrency.fractionDigits,
walletOwns: false, // doesn't matter here as this is not saved
isFullAmountNotSats: true,
);
final prevOut = OutputV2.fromElectrumXJson(
prevOutJson,
decimalPlaces: cryptoCurrency.fractionDigits,
walletOwns: false, // doesn't matter here as this is not saved
);
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
txid: txid,
vout: vout,
);
valueStringSats = prevOut.valueStringSats;
addresses.addAll(prevOut.addresses);
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
txid: txid,
vout: vout,
);
valueStringSats = prevOut.valueStringSats;
addresses.addAll(prevOut.addresses);
} catch (e, s) {
Logging.instance.log(
"Error getting prevOutJson: $e\nStack trace: $s",
level: LogLevel.Warning);
}
}
InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor(
@ -222,6 +228,7 @@ class BitcoincashWallet extends Bip39HDWallet
decimalPlaces: cryptoCurrency.fractionDigits,
// don't know yet if wallet owns. Need addresses first
walletOwns: false,
isFullAmountNotSats: true,
);
// if output was to my wallet, add value to amount received

View file

@ -429,14 +429,14 @@ class TezosWallet extends Bip39Wallet<Tezos> {
await mainDB.updateOrPutAddresses([address]);
// ensure we only have a single address
await mainDB.isar.writeTxn(() async {
await mainDB.isar.addresses
mainDB.isar.writeTxnSync(() {
mainDB.isar.addresses
.where()
.walletIdEqualTo(walletId)
.filter()
.not()
.derivationPath((q) => q.valueEqualTo(derivationPath))
.deleteAll();
.deleteAllSync();
});
if (info.cachedReceivingAddress != address.value) {

View file

@ -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';
@ -618,7 +619,45 @@ abstract class Wallet<T extends CryptoCurrency> {
Future<void> exit() async {
_periodicRefreshTimer?.cancel();
_networkAliveTimer?.cancel();
// TODO:
// If the syncing pref is currentWalletOnly or selectedWalletsAtStartup (and
// this wallet isn't in walletIdsSyncOnStartup), then we close subscriptions.
switch (prefs.syncType) {
case SyncingType.currentWalletOnly:
// Close the subscription for this coin's chain height.
// NOTE: This does not work now that the subscription is shared
// await (await ChainHeightServiceManager.getService(cryptoCurrency.coin))
// ?.cancelListen();
case SyncingType.selectedWalletsAtStartup:
// Close the subscription if this wallet is not in the list to be synced.
if (!prefs.walletIdsSyncOnStartup.contains(walletId)) {
// Check if there's another wallet of this coin on the sync list.
List<String> walletIds = [];
for (final id in prefs.walletIdsSyncOnStartup) {
final wallet = mainDB.isar.walletInfo
.where()
.walletIdEqualTo(id)
.findFirstSync()!;
if (wallet.coin == cryptoCurrency.coin) {
walletIds.add(id);
}
}
// TODO [prio=low]: use a query instead of iterating thru wallets.
// If there are no other wallets of this coin, then close the sub.
if (walletIds.isEmpty) {
// NOTE: This does not work now that the subscription is shared
// await (await ChainHeightServiceManager.getService(
// cryptoCurrency.coin))
// ?.cancelListen();
}
}
case SyncingType.allWalletsOnStartup:
// Do nothing.
break;
}
}
@mustCallSuper

View file

@ -4,8 +4,11 @@ import 'dart:math';
import 'package:bip47/src/util.dart';
import 'package:bitcoindart/bitcoindart.dart' as bitcoindart;
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter;
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart';
@ -13,23 +16,29 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/models/signing_data.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/paynym_is_api.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
import 'package:uuid/uuid.dart';
import 'package:stream_channel/stream_channel.dart';
mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
late ElectrumXClient electrumXClient;
late StreamChannel electrumAdapterChannel;
late ElectrumClient electrumAdapterClient;
late CachedElectrumXClient electrumXCachedClient;
// late SubscribableElectrumXClient subscribableElectrumXClient;
late ChainHeightServiceManager chainHeightServiceManager;
int? get maximumFeerate => null;
@ -123,7 +132,12 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
// don't care about sorting if using all utxos
if (!coinControl) {
// sort spendable by age (oldest first)
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
spendableOutputs.sort((a, b) => (b.blockTime ?? currentChainHeight)
.compareTo((a.blockTime ?? currentChainHeight)));
// Null check operator changed to null assignment in order to resolve a
// `Null check operator used on a null value` error. currentChainHeight
// used in order to sort these unconfirmed outputs as the youngest, but we
// could just as well use currentChainHeight + 1.
}
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
@ -794,9 +808,30 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
Future<int> fetchChainHeight() async {
try {
final result = await electrumXClient.getBlockHeadTip();
return result["height"] as int;
} catch (e) {
// Get the chain height service for the current coin.
ChainHeightService? service = ChainHeightServiceManager.getService(
cryptoCurrency.coin,
);
// ... or create a new one if it doesn't exist.
if (service == null) {
service = ChainHeightService(client: electrumAdapterClient);
ChainHeightServiceManager.add(service, cryptoCurrency.coin);
}
// If the service hasn't been started, start it and fetch the chain height.
if (!service.started) {
return await service.fetchHeightAndStartListenForUpdates();
}
// Return the height as per the service if available or the cached height.
return service.height ?? info.cachedChainHeight;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown in fetchChainHeight\nError: $e\nStack trace: $s",
level: LogLevel.Error);
// completer.completeError(e, s);
// return Future.error(e, s);
rethrow;
}
}
@ -807,21 +842,19 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
return transactions.length;
}
Future<Map<String, int>> fetchTxCountBatched({
required Map<String, String> addresses,
/// Should return a list of tx counts matching the list of addresses given
Future<List<int>> fetchTxCountBatched({
required List<String> addresses,
}) async {
try {
final Map<String, List<dynamic>> args = {};
for (final entry in addresses.entries) {
args[entry.key] = [
cryptoCurrency.addressToScriptHash(address: entry.value),
];
}
final response = await electrumXClient.getBatchHistory(args: args);
final response = await electrumXClient.getBatchHistory(
args: addresses
.map((e) => [cryptoCurrency.addressToScriptHash(address: e)])
.toList(growable: false));
final Map<String, int> result = {};
for (final entry in response.entries) {
result[entry.key] = entry.value.length;
final List<int> result = [];
for (final entry in response) {
result.add(entry.length);
}
return result;
} catch (e, s) {
@ -857,14 +890,64 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
.toList();
final newNode = await _getCurrentElectrumXNode();
try {
await electrumXClient.electrumAdapterClient?.close();
} catch (e, s) {
if (e.toString().contains("initialized")) {
// Ignore. This should happen every first time the wallet is opened.
} else {
Logging.instance
.log("Error closing electrumXClient: $e", level: LogLevel.Error);
}
}
electrumXClient = ElectrumXClient.from(
node: newNode,
prefs: prefs,
failovers: failovers,
coin: cryptoCurrency.coin,
);
electrumAdapterChannel = await electrum_adapter.connect(
newNode.address,
port: newNode.port,
acceptUnverified: true,
useSSL: newNode.useSSL,
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
);
if (electrumXClient.coin == Coin.firo ||
electrumXClient.coin == Coin.firoTestNet) {
electrumAdapterClient = FiroElectrumClient(
electrumAdapterChannel,
newNode.address,
newNode.port,
newNode.useSSL,
Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
} else {
electrumAdapterClient = ElectrumClient(
electrumAdapterChannel,
newNode.address,
newNode.port,
newNode.useSSL,
Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null);
}
electrumXCachedClient = CachedElectrumXClient.from(
electrumXClient: electrumXClient,
electrumAdapterClient: electrumAdapterClient,
electrumAdapterUpdateCallback: updateClient,
);
// Replaced using electrum_adapters' SubscribableClient in fetchChainHeight.
// subscribableElectrumXClient = SubscribableElectrumXClient.from(
// node: newNode,
// prefs: prefs,
// failovers: failovers,
// );
// await subscribableElectrumXClient.connect(
// host: newNode.address, port: newNode.port);
}
//============================================================================
@ -883,13 +966,11 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
index < cryptoCurrency.maxNumberOfIndexesToCheck &&
gapCounter < cryptoCurrency.maxUnusedAddressGap;
index += txCountBatchSize) {
List<String> iterationsAddressArray = [];
Logging.instance.log(
"index: $index, \t GapCounter $chain ${type.name}: $gapCounter",
level: LogLevel.Info);
final _id = "k_$index";
Map<String, String> txCountCallArgs = {};
List<String> txCountCallArgs = [];
for (int j = 0; j < txCountBatchSize; j++) {
final derivePath = cryptoCurrency.constructDerivePath(
@ -922,9 +1003,9 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
addressArray.add(address);
txCountCallArgs.addAll({
"${_id}_$j": addressString,
});
txCountCallArgs.add(
addressString,
);
}
// get address tx counts
@ -932,10 +1013,9 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
// check and add appropriate addresses
for (int k = 0; k < txCountBatchSize; k++) {
int count = counts["${_id}_$k"]!;
if (count > 0) {
iterationsAddressArray.add(txCountCallArgs["${_id}_$k"]!);
final count = counts[k];
if (count > 0) {
// update highest
highestIndexWithHistory = index + k;
@ -1025,22 +1105,20 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
List<Map<String, dynamic>> allTxHashes = [];
if (serverCanBatch) {
final Map<int, Map<String, List<dynamic>>> batches = {};
final Map<String, String> requestIdToAddressMap = {};
final Map<int, List<List<dynamic>>> batches = {};
final Map<int, List<String>> batchIndexToAddressListMap = {};
const batchSizeMax = 100;
int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) {
if (batches[batchNumber] == null) {
batches[batchNumber] = {};
}
batches[batchNumber] ??= [];
batchIndexToAddressListMap[batchNumber] ??= [];
final address = allAddresses.elementAt(i);
final scriptHash = cryptoCurrency.addressToScriptHash(
address: allAddresses.elementAt(i),
address: address,
);
final id = Logger.isTestEnv ? "$i" : const Uuid().v1();
requestIdToAddressMap[id] = allAddresses.elementAt(i);
batches[batchNumber]!.addAll({
id: [scriptHash]
});
batches[batchNumber]!.add([scriptHash]);
batchIndexToAddressListMap[batchNumber]!.add(address);
if (i % batchSizeMax == batchSizeMax - 1) {
batchNumber++;
}
@ -1049,12 +1127,13 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
for (int i = 0; i < batches.length; i++) {
final response =
await electrumXClient.getBatchHistory(args: batches[i]!);
for (final entry in response.entries) {
for (int j = 0; j < entry.value.length; j++) {
entry.value[j]["address"] = requestIdToAddressMap[entry.key];
if (!allTxHashes.contains(entry.value[j])) {
allTxHashes.add(entry.value[j]);
}
for (int j = 0; j < response.length; j++) {
final entry = response[j];
for (int k = 0; k < entry.length; k++) {
entry[k]["address"] = batchIndexToAddressListMap[i]![j];
// if (!allTxHashes.contains(entry[j])) {
allTxHashes.add(entry[k]);
// }
}
}
}
@ -1096,6 +1175,8 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
coin: cryptoCurrency.coin,
);
print("txn: $txn");
final vout = jsonUTXO["tx_pos"] as int;
final outputs = txn["vout"] as List;
@ -1163,6 +1244,13 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
await updateElectrumX();
}
Future<ElectrumClient> updateClient() async {
Logging.instance.log("Updating electrum node and ElectrumAdapterClient.",
level: LogLevel.Info);
await updateNode();
return electrumAdapterClient;
}
FeeObject? _cachedFees;
@override
@ -1195,9 +1283,9 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info);
_cachedFees = feeObject;
return _cachedFees!;
} catch (e) {
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _getFees(): $e",
"Exception rethrown from _getFees(): $e\nStack trace: $s",
level: LogLevel.Error,
);
if (_cachedFees == null) {
@ -1511,31 +1599,27 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
final fetchedUtxoList = <List<Map<String, dynamic>>>[];
if (serverCanBatch) {
final Map<int, Map<String, List<dynamic>>> batches = {};
final Map<int, List<List<dynamic>>> batchArgs = {};
const batchSizeMax = 10;
int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) {
if (batches[batchNumber] == null) {
batches[batchNumber] = {};
}
batchArgs[batchNumber] ??= [];
final scriptHash = cryptoCurrency.addressToScriptHash(
address: allAddresses[i].value,
);
batches[batchNumber]!.addAll({
scriptHash: [scriptHash]
});
batchArgs[batchNumber]!.add([scriptHash]);
if (i % batchSizeMax == batchSizeMax - 1) {
batchNumber++;
}
}
for (int i = 0; i < batches.length; i++) {
for (int i = 0; i < batchArgs.length; i++) {
final response =
await electrumXClient.getBatchUTXOs(args: batches[i]!);
for (final entry in response.entries) {
if (entry.value.isNotEmpty) {
fetchedUtxoList.add(entry.value);
await electrumXClient.getBatchUTXOs(args: batchArgs[i]!);
for (final entry in response) {
if (entry.isNotEmpty) {
fetchedUtxoList.add(entry);
}
}
}
@ -1679,7 +1763,8 @@ mixin ElectrumXInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
Logging.instance.log("prepare send: $result", level: LogLevel.Info);
if (result.fee!.raw.toInt() < result.vSize!) {
throw Exception(
"Error in fee calculation: Transaction fee cannot be less than vSize");
"Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot "
"be less than vSize (${result.vSize})");
}
return result;

View file

@ -177,6 +177,7 @@ class _NodeCardState extends ConsumerState<NodeCard> {
useSSL: node.useSSL,
failovers: [],
prefs: ref.read(prefsChangeNotifierProvider),
coin: widget.coin,
);
try {

View file

@ -160,6 +160,7 @@ class NodeOptionsSheet extends ConsumerWidget {
failovers: [],
prefs: ref.read(prefsChangeNotifierProvider),
torService: ref.read(pTorService),
coin: coin,
);
try {

View file

@ -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:
@ -1045,10 +1054,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:
@ -1601,7 +1610,7 @@ packages:
source: hosted
version: "1.5.3"
stream_channel:
dependency: transitive
dependency: "direct main"
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
@ -1725,8 +1734,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"

View file

@ -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"
@ -68,7 +68,7 @@ dependencies:
tor_ffi_plugin:
git:
url: https://github.com/cypherstack/tor.git
ref: 0a6888282f4e98401051a396e9d2293bd55ac2c2
ref: e37dc4e22f7acb2746b70bdc935f0eb3c50b8b71
fusiondart:
git:
@ -176,6 +176,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:
@ -192,7 +197,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: