Merge pull request #1036 from cypherstack/node-settings

Node settings
This commit is contained in:
Diego Salazar 2024-11-26 13:29:33 -07:00 committed by GitHub
commit 1ce4bee68c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 1178 additions and 120 deletions

View file

@ -77,6 +77,8 @@ class DbVersionMigrator with WalletDB {
name: e.name,
id: e.id,
useSSL: e.useSSL,
torEnabled: e.torEnabled,
clearnetEnabled: e.clearnetEnabled,
),
)
.toList();
@ -88,6 +90,8 @@ class DbVersionMigrator with WalletDB {
name: node.name,
id: node.id,
useSSL: node.useSSL,
torEnabled: node.torEnabled,
clearnetEnabled: node.clearnetEnabled,
),
prefs: prefs,
failovers: failovers,

View file

@ -3,6 +3,8 @@ import 'dart:async';
import 'package:electrum_adapter/electrum_adapter.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
import '../utilities/tor_plain_net_option_enum.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
class ClientManager {
@ -10,6 +12,7 @@ class ClientManager {
static final ClientManager sharedInstance = ClientManager._();
final Map<String, ElectrumClient> _map = {};
final Map<String, TorPlainNetworkOption> _mapNet = {};
final Map<String, int> _heights = {};
final Map<String, StreamSubscription<BlockHeader>> _subscriptions = {};
final Map<String, Completer<int>> _heightCompleters = {};
@ -24,18 +27,37 @@ class ClientManager {
ElectrumClient? getClient({
required CryptoCurrency cryptoCurrency,
}) =>
_map[_keyHelper(cryptoCurrency)];
required TorPlainNetworkOption netType,
}) {
final _key = _keyHelper(cryptoCurrency);
void addClient(
if (netType == _mapNet[_key]) {
return _map[_key];
} else {
return null;
}
}
Future<void> addClient(
ElectrumClient client, {
required CryptoCurrency cryptoCurrency,
}) {
required TorPlainNetworkOption netType,
}) async {
final key = _keyHelper(cryptoCurrency);
if (_map[key] != null) {
throw Exception("ElectrumX Client for $key already exists.");
if (_mapNet[key] == netType) {
throw Exception(
"ElectrumX Client for $key and $netType already exists.",
);
}
await remove(cryptoCurrency: cryptoCurrency);
_map[key] = client;
_mapNet[key] = netType;
} else {
_map[key] = client;
_mapNet[key] = netType;
}
_heightCompleters[key] = Completer<int>();
@ -68,10 +90,24 @@ class ClientManager {
);
}
if (Prefs.instance.useTor) {
if (_mapNet[key]! == TorPlainNetworkOption.clear) {
throw Exception(
"Non-TOR only client for $key found.",
);
}
} else {
if (_mapNet[key]! == TorPlainNetworkOption.tor) {
throw Exception(
"TOR only client for $key found.",
);
}
}
return _heights[key] ?? await _heightCompleters[key]!.future;
}
Future<ElectrumClient?> remove({
Future<(ElectrumClient?, TorPlainNetworkOption?)> remove({
required CryptoCurrency cryptoCurrency,
}) async {
final key = _keyHelper(cryptoCurrency);
@ -80,7 +116,7 @@ class ClientManager {
_heights.remove(key);
_heightCompleters.remove(key);
return _map.remove(key);
return (_map.remove(key), _mapNet.remove(key));
}
Future<void> closeAll() async {
@ -99,6 +135,7 @@ class ClientManager {
_heightCompleters.clear();
_heights.clear();
_subscriptions.clear();
_mapNet.clear();
_map.clear();
}
}

View file

@ -29,6 +29,7 @@ import '../utilities/amount/amount.dart';
import '../utilities/extensions/impl/string.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
import '../utilities/tor_plain_net_option_enum.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart';
import 'client_manager.dart';
@ -42,6 +43,10 @@ typedef SparkMempoolData = ({
class WifiOnlyException implements Exception {}
class TorOnlyException implements Exception {}
class ClearnetOnlyException implements Exception {}
class ElectrumXNode {
ElectrumXNode({
required this.address,
@ -49,12 +54,16 @@ class ElectrumXNode {
required this.name,
required this.id,
required this.useSSL,
required this.torEnabled,
required this.clearnetEnabled,
});
final String address;
final int port;
final String name;
final String id;
final bool useSSL;
final bool torEnabled;
final bool clearnetEnabled;
factory ElectrumXNode.from(ElectrumXNode node) {
return ElectrumXNode(
@ -63,6 +72,8 @@ class ElectrumXNode {
name: node.name,
id: node.id,
useSSL: node.useSSL,
torEnabled: node.torEnabled,
clearnetEnabled: node.clearnetEnabled,
);
}
@ -74,6 +85,7 @@ class ElectrumXNode {
class ElectrumXClient {
final CryptoCurrency cryptoCurrency;
final TorPlainNetworkOption netType;
String get host => _host;
late String _host;
@ -90,6 +102,7 @@ class ElectrumXClient {
ElectrumClient? getElectrumAdapter() =>
ClientManager.sharedInstance.getClient(
cryptoCurrency: cryptoCurrency,
netType: netType,
);
late Prefs _prefs;
@ -119,6 +132,7 @@ class ElectrumXClient {
required int port,
required bool useSSL,
required Prefs prefs,
required this.netType,
required List<ElectrumXNode> failovers,
required this.cryptoCurrency,
this.connectionTimeoutForSpecialCaseJsonRPCClients =
@ -168,6 +182,7 @@ class ElectrumXClient {
_electrumAdapterChannel = null;
await (await ClientManager.sharedInstance
.remove(cryptoCurrency: cryptoCurrency))
.$1
?.close();
// Also close any chain height services that are currently open.
@ -193,6 +208,10 @@ class ElectrumXClient {
failovers: failovers,
globalEventBusForTesting: globalEventBusForTesting,
cryptoCurrency: cryptoCurrency,
netType: TorPlainNetworkOption.fromNodeData(
node.torEnabled,
node.clearnetEnabled,
),
);
}
@ -236,6 +255,18 @@ class ElectrumXClient {
// Get the proxy info from the TorService.
proxyInfo = _torService.getProxyInfo();
}
if (netType == TorPlainNetworkOption.clear) {
_electrumAdapterChannel = null;
await ClientManager.sharedInstance
.remove(cryptoCurrency: cryptoCurrency);
}
} else {
if (netType == TorPlainNetworkOption.tor) {
_electrumAdapterChannel = null;
await ClientManager.sharedInstance
.remove(cryptoCurrency: cryptoCurrency);
}
}
// If the current ElectrumAdapterClient is closed, create a new one.
@ -288,9 +319,10 @@ class ElectrumXClient {
);
}
ClientManager.sharedInstance.addClient(
await ClientManager.sharedInstance.addClient(
newClient,
cryptoCurrency: cryptoCurrency,
netType: netType,
);
}
@ -352,6 +384,10 @@ class ElectrumXClient {
return response;
} on WifiOnlyException {
rethrow;
} on ClearnetOnlyException {
rethrow;
} on TorOnlyException {
rethrow;
} on SocketException {
// likely timed out so then retry
if (retries > 0) {
@ -442,6 +478,10 @@ class ElectrumXClient {
return response;
} on WifiOnlyException {
rethrow;
} on ClearnetOnlyException {
rethrow;
} on TorOnlyException {
rethrow;
} on SocketException {
// likely timed out so then retry
if (retries > 0) {
@ -488,10 +528,10 @@ class ElectrumXClient {
return await request(
requestID: requestID,
command: 'server.ping',
requestTimeout: const Duration(seconds: 2),
requestTimeout: const Duration(seconds: 3),
retries: retryCount,
).timeout(
const Duration(seconds: 2),
const Duration(seconds: 3),
onTimeout: () {
Logging.instance.log(
"ElectrumxClient.ping timed out with retryCount=$retryCount, host=$_host",

View file

@ -0,0 +1,8 @@
class NodeTorMismatchConfigException implements Exception {
final String message;
NodeTorMismatchConfigException({required this.message});
@override
String toString() => message;
}

View file

@ -9,6 +9,7 @@
*/
import 'package:hive/hive.dart';
import '../utilities/default_nodes.dart';
import '../utilities/flutter_secure_storage_interface.dart';
@ -38,6 +39,10 @@ class NodeModel {
final bool isDown;
// @HiveField(10)
final bool? trusted;
// @HiveField(11)
final bool torEnabled;
// @HiveField(12)
final bool clearnetEnabled;
NodeModel({
required this.host,
@ -49,6 +54,8 @@ class NodeModel {
required this.coinName,
required this.isFailover,
required this.isDown,
required this.torEnabled,
required this.clearnetEnabled,
this.loginName,
this.trusted,
});
@ -64,6 +71,8 @@ class NodeModel {
bool? isFailover,
bool? isDown,
bool? trusted,
bool? torEnabled,
bool? clearnetEnabled,
}) {
return NodeModel(
host: host ?? this.host,
@ -77,6 +86,8 @@ class NodeModel {
isFailover: isFailover ?? this.isFailover,
isDown: isDown ?? this.isDown,
trusted: trusted ?? this.trusted,
torEnabled: torEnabled ?? this.torEnabled,
clearnetEnabled: clearnetEnabled ?? this.clearnetEnabled,
);
}
@ -98,6 +109,8 @@ class NodeModel {
map['isFailover'] = isFailover;
map['isDown'] = isDown;
map['trusted'] = trusted;
map['torEnabled'] = torEnabled;
map['clearEnabled'] = clearnetEnabled;
return map;
}

View file

@ -28,13 +28,15 @@ class NodeModelAdapter extends TypeAdapter<NodeModel> {
isFailover: fields[8] as bool,
isDown: fields[9] as bool,
trusted: fields[10] as bool?,
torEnabled: fields[11] as bool? ?? true,
clearnetEnabled: fields[12] as bool? ?? true,
);
}
@override
void write(BinaryWriter writer, NodeModel obj) {
writer
..writeByte(11)
..writeByte(13)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@ -56,7 +58,11 @@ class NodeModelAdapter extends TypeAdapter<NodeModel> {
..writeByte(9)
..write(obj.isDown)
..writeByte(10)
..write(obj.trusted);
..write(obj.trusted)
..writeByte(11)
..write(obj.torEnabled)
..writeByte(12)
..write(obj.clearnetEnabled);
}
@override

View file

@ -16,6 +16,7 @@ import 'package:mutex/mutex.dart';
import '../../notifications/show_flush_bar.dart';
// import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart';
import '../../providers/global/node_service_provider.dart';
import '../../providers/global/prefs_provider.dart';
import '../../providers/global/secure_store_provider.dart';
import '../../providers/global/wallets_provider.dart';
@ -25,7 +26,9 @@ import '../../utilities/assets.dart';
import '../../utilities/biometrics.dart';
import '../../utilities/flutter_secure_storage_interface.dart';
import '../../utilities/show_loading.dart';
import '../../utilities/show_node_tor_settings_mismatch.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -101,6 +104,20 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> {
final walletId = widget.routeOnSuccessArguments as String;
final wallet = ref.read(pWallets).getWallet(walletId);
final canContinue = await checkShowNodeTorSettingsMismatch(
context: context,
currency: wallet.cryptoCurrency,
prefs: ref.read(prefsChangeNotifierProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
allowCancel: false,
rootNavigator: Util.isDesktop,
);
if (!canContinue) {
return;
}
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {
loadFuture =

View file

@ -18,14 +18,17 @@ import 'package:uuid/uuid.dart';
import '../../../../models/node_model.dart';
import '../../../../notifications/show_flush_bar.dart';
import '../../../../providers/global/active_wallet_provider.dart';
import '../../../../providers/global/secure_store_provider.dart';
import '../../../../providers/providers.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/assets.dart';
import '../../../../utilities/constants.dart';
import '../../../../utilities/enums/sync_type_enum.dart';
import '../../../../utilities/flutter_secure_storage_interface.dart';
import '../../../../utilities/test_node_connection.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/tor_plain_net_option_enum.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart';
@ -229,6 +232,11 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
}
}
final torEnabled = formData.netOption == TorPlainNetworkOption.tor ||
formData.netOption == TorPlainNetworkOption.both;
final plainEnabled = formData.netOption == TorPlainNetworkOption.clear ||
formData.netOption == TorPlainNetworkOption.both;
switch (viewType) {
case AddEditNodeViewType.add:
final NodeModel node = NodeModel(
@ -243,6 +251,8 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
isFailover: formData.isFailover!,
trusted: formData.trusted!,
isDown: false,
torEnabled: torEnabled,
clearnetEnabled: plainEnabled,
);
await ref.read(nodeServiceChangeNotifierProvider).add(
@ -250,6 +260,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
formData.password,
true,
);
await _notifyWalletsOfUpdatedNode();
if (mounted) {
Navigator.of(context)
.popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete));
@ -268,6 +279,8 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
isFailover: formData.isFailover!,
trusted: formData.trusted!,
isDown: false,
torEnabled: torEnabled,
clearnetEnabled: plainEnabled,
);
await ref.read(nodeServiceChangeNotifierProvider).add(
@ -275,6 +288,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
formData.password,
true,
);
await _notifyWalletsOfUpdatedNode();
if (mounted) {
Navigator.of(context)
.popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete));
@ -283,6 +297,39 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
}
}
Future<void> _notifyWalletsOfUpdatedNode() async {
final wallets =
ref.read(pWallets).wallets.where((e) => e.info.coin == widget.coin);
final prefs = ref.read(prefsChangeNotifierProvider);
switch (prefs.syncType) {
case SyncingType.currentWalletOnly:
for (final wallet in wallets) {
if (ref.read(currentWalletIdProvider) == wallet.walletId) {
unawaited(wallet.updateNode().then((value) => wallet.refresh()));
} else {
unawaited(wallet.updateNode());
}
}
break;
case SyncingType.selectedWalletsAtStartup:
final List<String> walletIdsToSync = prefs.walletIdsSyncOnStartup;
for (final wallet in wallets) {
if (walletIdsToSync.contains(wallet.walletId)) {
unawaited(wallet.updateNode().then((value) => wallet.refresh()));
} else {
unawaited(wallet.updateNode());
}
}
break;
case SyncingType.allWalletsOnStartup:
for (final wallet in wallets) {
unawaited(wallet.updateNode().then((value) => wallet.refresh()));
}
break;
}
}
@override
void initState() {
isDesktop = Util.isDesktop;
@ -422,7 +469,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
condition: isDesktop,
builder: (child) => DesktopDialog(
maxWidth: 580,
maxHeight: 500,
maxHeight: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -568,10 +615,11 @@ class NodeFormData {
String? name, host, login, password;
int? port;
bool? useSSL, isFailover, trusted;
TorPlainNetworkOption? netOption;
@override
String toString() {
return "{ name: $name, host: $host, port: $port, useSSL: $useSSL, trusted: $trusted }";
return "{ name: $name, host: $host, port: $port, useSSL: $useSSL, trusted: $trusted, netOption: $netOption }";
}
}
@ -615,6 +663,7 @@ class _NodeFormState extends ConsumerState<NodeForm> {
bool _trusted = false;
int? port;
late bool enableSSLCheckbox;
late TorPlainNetworkOption netOption;
late final bool enableAuthFields;
@ -672,6 +721,7 @@ class _NodeFormState extends ConsumerState<NodeForm> {
ref.read(nodeFormDataProvider).useSSL = _useSSL;
ref.read(nodeFormDataProvider).isFailover = _isFailover;
ref.read(nodeFormDataProvider).trusted = _trusted;
ref.read(nodeFormDataProvider).netOption = netOption;
}
@override
@ -704,6 +754,15 @@ class _NodeFormState extends ConsumerState<NodeForm> {
_useSSL = node.useSSL;
_isFailover = node.isFailover;
_trusted = node.trusted ?? false;
if (node.torEnabled && !node.clearnetEnabled) {
netOption = TorPlainNetworkOption.tor;
} else if (node.clearnetEnabled && !node.torEnabled) {
netOption = TorPlainNetworkOption.clear;
} else {
netOption = TorPlainNetworkOption.both;
}
if (widget.coin is Epiccash) {
enableSSLCheckbox = !node.host.startsWith("http");
} else {
@ -716,6 +775,7 @@ class _NodeFormState extends ConsumerState<NodeForm> {
});
} else {
enableSSLCheckbox = true;
netOption = TorPlainNetworkOption.both;
// default to port 3413
// _portController.text = "3413";
}
@ -1168,7 +1228,145 @@ class _NodeFormState extends ConsumerState<NodeForm> {
),
],
),
if (widget.coin is! Ethereum)
const SizedBox(
height: 16,
),
if (widget.coin is! Ethereum)
Row(
children: [
RadioTextButton(
label: "Only TOR traffic",
enabled: !widget.readOnly,
value: TorPlainNetworkOption.tor,
groupValue: netOption,
onChanged: (value) {
if (!widget.readOnly) {
setState(
() => netOption = TorPlainNetworkOption.tor,
);
_updateState();
}
},
),
],
),
if (widget.coin is! Ethereum)
const SizedBox(
height: 8,
),
if (widget.coin is! Ethereum)
Row(
children: [
RadioTextButton(
label: "Only non-TOR traffic",
enabled: !widget.readOnly,
value: TorPlainNetworkOption.clear,
groupValue: netOption,
onChanged: (value) {
if (!widget.readOnly) {
setState(
() => netOption = TorPlainNetworkOption.clear,
);
_updateState();
}
},
),
],
),
if (widget.coin is! Ethereum)
const SizedBox(
height: 8,
),
if (widget.coin is! Ethereum)
Row(
children: [
RadioTextButton(
label: "Allow both",
enabled: !widget.readOnly,
value: TorPlainNetworkOption.both,
groupValue: netOption,
onChanged: (value) {
if (!widget.readOnly) {
setState(
() => netOption = TorPlainNetworkOption.both,
);
_updateState();
}
},
),
],
),
],
);
}
}
class RadioTextButton<T> extends StatelessWidget {
const RadioTextButton({
super.key,
required this.value,
required this.label,
required this.groupValue,
required this.onChanged,
this.enabled = true,
});
final T value;
final String label;
final T groupValue;
final bool enabled;
final void Function(T) onChanged;
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => MouseRegion(
cursor: SystemMouseCursors.click,
child: child,
),
child: GestureDetector(
onTap: () {
if (value != groupValue) {
onChanged.call(value);
}
},
child: Container(
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Radio<T>(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: value,
groupValue: groupValue,
onChanged: !enabled
? null
: (_) {
if (value != groupValue) {
onChanged.call(value);
}
},
),
),
const SizedBox(
width: 14,
),
Text(
label,
style: STextStyles.w500_14(context),
),
],
),
),
),
);
}
}

View file

@ -13,15 +13,19 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:tuple/tuple.dart';
import '../../../../notifications/show_flush_bar.dart';
import 'add_edit_node_view.dart';
import '../../../../providers/global/active_wallet_provider.dart';
import '../../../../providers/global/secure_store_provider.dart';
import '../../../../providers/providers.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/assets.dart';
import '../../../../utilities/enums/sync_type_enum.dart';
import '../../../../utilities/flutter_secure_storage_interface.dart';
import '../../../../utilities/test_node_connection.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/tor_plain_net_option_enum.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../../widgets/background.dart';
@ -31,7 +35,7 @@ import '../../../../widgets/desktop/delete_button.dart';
import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/desktop/secondary_button.dart';
import 'package:tuple/tuple.dart';
import 'add_edit_node_view.dart';
class NodeDetailsView extends ConsumerStatefulWidget {
const NodeDetailsView({
@ -59,6 +63,39 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
bool _desktopReadOnly = true;
Future<void> _notifyWalletsOfUpdatedNode() async {
final wallets =
ref.read(pWallets).wallets.where((e) => e.info.coin == widget.coin);
final prefs = ref.read(prefsChangeNotifierProvider);
switch (prefs.syncType) {
case SyncingType.currentWalletOnly:
for (final wallet in wallets) {
if (ref.read(currentWalletIdProvider) == wallet.walletId) {
unawaited(wallet.updateNode().then((value) => wallet.refresh()));
} else {
unawaited(wallet.updateNode());
}
}
break;
case SyncingType.selectedWalletsAtStartup:
final List<String> walletIdsToSync = prefs.walletIdsSyncOnStartup;
for (final wallet in wallets) {
if (walletIdsToSync.contains(wallet.walletId)) {
unawaited(wallet.updateNode().then((value) => wallet.refresh()));
} else {
unawaited(wallet.updateNode());
}
}
break;
case SyncingType.allWalletsOnStartup:
for (final wallet in wallets) {
unawaited(wallet.updateNode().then((value) => wallet.refresh()));
}
break;
}
}
@override
initState() {
secureStore = ref.read(secureStoreProvider);
@ -265,6 +302,16 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
.read(nodeServiceChangeNotifierProvider)
.getNodeById(id: nodeId)!;
final TorPlainNetworkOption netOption;
if (ref.read(nodeFormDataProvider).netOption != null) {
netOption = ref.read(nodeFormDataProvider).netOption!;
} else {
netOption = TorPlainNetworkOption.fromNodeData(
node.torEnabled,
node.clearnetEnabled,
);
}
final nodeFormData = NodeFormData()
..useSSL = node.useSSL
..trusted = node.trusted
@ -272,7 +319,8 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
..host = node.host
..login = node.loginName
..port = node.port
..isFailover = node.isFailover;
..isFailover = node.isFailover
..netOption = netOption;
nodeFormData.password = await node.getPassword(
ref.read(secureStoreProvider),
);
@ -338,6 +386,16 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
loginName: ref.read(nodeFormDataProvider).login,
isFailover:
ref.read(nodeFormDataProvider).isFailover,
torEnabled:
ref.read(nodeFormDataProvider).netOption ==
TorPlainNetworkOption.tor ||
ref.read(nodeFormDataProvider).netOption ==
TorPlainNetworkOption.both,
clearnetEnabled:
ref.read(nodeFormDataProvider).netOption ==
TorPlainNetworkOption.clear ||
ref.read(nodeFormDataProvider).netOption ==
TorPlainNetworkOption.both,
);
await ref
@ -347,6 +405,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
ref.read(nodeFormDataProvider).password,
true,
);
await _notifyWalletsOfUpdatedNode();
}
},
)

View file

@ -1272,6 +1272,8 @@ abstract class SWB {
loginName: node['loginName'] as String?,
isFailover: node['isFailover'] as bool,
isDown: node['isDown'] as bool,
torEnabled: node['torEnabled'] as bool? ?? true,
clearnetEnabled: node['plainEnabled'] as bool? ?? true,
),
node["password"] as String?,
true,

View file

@ -22,6 +22,7 @@ import '../../../utilities/amount/amount.dart';
import '../../../utilities/amount/amount_formatter.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/show_loading.dart';
import '../../../utilities/show_node_tor_settings_mismatch.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/coins/firo.dart';
@ -117,6 +118,19 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> {
onTap: () async {
final wallet = ref.read(pWallets).getWallet(walletId);
final canContinue = await checkShowNodeTorSettingsMismatch(
context: context,
currency: coin,
prefs: ref.read(prefsChangeNotifierProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
allowCancel: true,
rootNavigator: Util.isDesktop,
);
if (!canContinue) {
return;
}
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {
loadFuture =

View file

@ -21,6 +21,7 @@ import '../../../themes/stack_colors.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/show_loading.dart';
import '../../../utilities/show_node_tor_settings_mismatch.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/crypto_currency.dart';
@ -83,6 +84,20 @@ class WalletListItem extends ConsumerWidget {
.read(pWallets)
.wallets
.firstWhere((e) => e.info.coin == coin);
final canContinue = await checkShowNodeTorSettingsMismatch(
context: context,
currency: coin,
prefs: ref.read(prefsChangeNotifierProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
allowCancel: true,
rootNavigator: Util.isDesktop,
);
if (!canContinue) {
return;
}
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {
loadFuture =

View file

@ -12,10 +12,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/global/active_wallet_provider.dart';
import '../../providers/global/node_service_provider.dart';
import '../../providers/global/prefs_provider.dart';
import '../../providers/global/wallets_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/constants.dart';
import '../../utilities/show_loading.dart';
import '../../utilities/show_node_tor_settings_mismatch.dart';
import '../../utilities/util.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
import '../../wallets/wallet/intermediate/lib_monero_wallet.dart';
@ -81,6 +84,22 @@ class CoinWalletsTable extends ConsumerWidget {
final wallet =
ref.read(pWallets).getWallet(walletIds[i]);
final canContinue =
await checkShowNodeTorSettingsMismatch(
context: context,
currency: coin,
prefs: ref.read(prefsChangeNotifierProvider),
nodeService:
ref.read(nodeServiceChangeNotifierProvider),
allowCancel: true,
rootNavigator: Util.isDesktop,
);
if (!canContinue) {
return;
}
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {
loadFuture = wallet

View file

@ -20,6 +20,7 @@ import '../../themes/coin_icon_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/amount/amount.dart';
import '../../utilities/show_loading.dart';
import '../../utilities/show_node_tor_settings_mismatch.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
@ -123,6 +124,19 @@ class _DesktopWalletSummaryRowState
final wallet = ref.read(pWallets).wallets.firstWhere(
(e) => e.cryptoCurrency.identifier == widget.coin.identifier);
final canContinue = await checkShowNodeTorSettingsMismatch(
context: context,
currency: wallet.cryptoCurrency,
prefs: ref.read(prefsChangeNotifierProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
allowCancel: true,
rootNavigator: Util.isDesktop,
);
if (!canContinue) {
return;
}
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {
loadFuture =

View file

@ -12,6 +12,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import '../app_config.dart';
import '../db/hive/db.dart';
import '../models/node_model.dart';
@ -58,6 +59,8 @@ class NodeService extends ChangeNotifier {
enabled: savedNode.enabled,
isFailover: savedNode.isFailover,
trusted: savedNode.trusted,
torEnabled: savedNode.torEnabled,
clearnetEnabled: savedNode.clearnetEnabled,
),
);
}
@ -74,6 +77,8 @@ class NodeService extends ChangeNotifier {
enabled: primaryNode.enabled,
isFailover: primaryNode.isFailover,
trusted: primaryNode.trusted,
torEnabled: primaryNode.torEnabled,
clearnetEnabled: primaryNode.clearnetEnabled,
),
);
}
@ -253,7 +258,9 @@ class NodeService extends ChangeNotifier {
enabled: true,
coinName: coin.identifier,
isFailover: true,
torEnabled: nodeMap["torEnabled"] == "true",
isDown: nodeMap["isDown"] == "true",
clearnetEnabled: nodeMap["plainEnabled"] == "true",
);
final currentNode = getNodeById(id: nodeMap["id"] as String);
if (currentNode != null) {

View file

@ -11,23 +11,23 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../app_config.dart';
import '../db/hive/db.dart';
import '../electrumx_rpc/electrumx_client.dart';
import '../exceptions/electrumx/no_such_transaction.dart';
import '../models/exchange/response_objects/trade.dart';
import '../models/notification_model.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart';
import 'exchange/exchange.dart';
import 'exchange/exchange_response.dart';
import 'node_service.dart';
import 'notifications_api.dart';
import 'trade_service.dart';
import 'wallets.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart';
import 'exchange/exchange.dart';
class NotificationsService extends ChangeNotifier {
late NodeService nodeService;
@ -136,12 +136,26 @@ class NotificationsService extends ChangeNotifier {
final node = nodeService.getPrimaryNodeFor(currency: coin);
if (node != null) {
if (wallet is ElectrumXInterface) {
if (prefs.useTor) {
if (node.clearnetEnabled && !node.torEnabled) {
// just ignore I guess??
return;
}
} else {
if (node.torEnabled && !node.clearnetEnabled) {
// just ignore I guess??
return;
}
}
final eNode = ElectrumXNode(
address: node.host,
port: node.port,
name: node.name,
id: node.id,
useSSL: node.useSSL,
torEnabled: node.torEnabled,
clearnetEnabled: node.clearnetEnabled,
);
final failovers = nodeService
.failoverNodesFor(currency: coin)
@ -152,6 +166,8 @@ class NotificationsService extends ChangeNotifier {
name: e.name,
id: e.id,
useSSL: e.useSSL,
torEnabled: node.torEnabled,
clearnetEnabled: node.clearnetEnabled,
),
)
.toList();

View file

@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import '../services/node_service.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../widgets/conditional_parent.dart';
import '../widgets/desktop/desktop_dialog.dart';
import '../widgets/desktop/primary_button.dart';
import '../widgets/desktop/secondary_button.dart';
import '../widgets/stack_dialog.dart';
import 'prefs.dart';
import 'text_styles.dart';
import 'util.dart';
Future<bool> checkShowNodeTorSettingsMismatch({
required BuildContext context,
required CryptoCurrency currency,
required Prefs prefs,
required NodeService nodeService,
required bool allowCancel,
bool rootNavigator = false,
}) async {
final node =
nodeService.getPrimaryNodeFor(currency: currency) ?? currency.defaultNode;
if (prefs.useTor) {
if (node.torEnabled) {
return true;
}
} else {
if (node.clearnetEnabled) {
return true;
}
}
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) => ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopDialog(
maxHeight: double.infinity,
child: Padding(
padding: const EdgeInsets.all(32),
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => StackDialogBase(
child: child,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Attention! Node connection issue detected. "
"The current node will not sync due to its connectivity settings. "
"Please adjust the node settings or enable/disable TOR.",
style: STextStyles.w600_16(context),
),
SizedBox(
height: Util.isDesktop ? 32 : 24,
),
Row(
children: [
allowCancel
? Expanded(
child: SecondaryButton(
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
label: "Cancel",
onPressed: () {
Navigator.of(context).pop(false);
},
),
)
: const Spacer(),
SizedBox(
width: Util.isDesktop ? 24 : 16,
),
Expanded(
child: PrimaryButton(
buttonHeight: Util.isDesktop ? ButtonHeight.l : null,
label: "Continue",
onPressed: () {
Navigator.of(context).pop(true);
},
),
),
],
),
],
),
),
),
);
return result ?? true;
}

View file

@ -15,6 +15,7 @@ import '../pages/settings_views/global_settings_view/manage_nodes_views/add_edit
import '../services/tor_service.dart';
import 'logger.dart';
import 'prefs.dart';
import 'tor_plain_net_option_enum.dart';
Future<bool> _testEpicBoxNodeConnection(Uri uri) async {
final HTTP client = HTTP();
@ -50,6 +51,17 @@ Future<NodeFormData?> testEpicNodeConnection(NodeFormData data) async {
if (data.host == null || data.port == null || data.useSSL == null) {
return null;
}
if (Prefs.instance.useTor) {
if (data.netOption == TorPlainNetworkOption.clear) {
return null;
}
} else {
if (data.netOption == TorPlainNetworkOption.tor) {
return null;
}
}
const String path_postfix = "/v1/version";
if (data.host!.startsWith("https://")) {

View file

@ -5,7 +5,6 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:on_chain/ada/ada.dart';
import 'package:on_chain/ada/src/provider/provider/provider.dart';
import 'package:socks5_proxy/socks.dart';
import '../networking/http.dart';
@ -25,6 +24,7 @@ import 'test_epic_box_connection.dart';
import 'test_eth_node_connection.dart';
import 'test_monero_node_connection.dart';
import 'test_stellar_node_connection.dart';
import 'tor_plain_net_option_enum.dart';
Future<bool> _xmrHelper(
NodeFormData nodeFormData,
@ -45,7 +45,6 @@ Future<bool> _xmrHelper(
final uriString = "${uri.scheme}://${uri.host}:${port ?? 0}$path";
if (proxyInfo == null && uri.host.endsWith(".onion")) {
return false;
}
@ -93,6 +92,24 @@ Future<bool> testNodeConnection({
}) async {
final formData = nodeFormData;
if (ref.read(prefsChangeNotifierProvider).useTor) {
if (formData.netOption! == TorPlainNetworkOption.clear) {
Logging.instance.log(
"This node is configured for non-TOR only but TOR is enabled",
level: LogLevel.Warning,
);
return false;
}
} else {
if (formData.netOption! == TorPlainNetworkOption.tor) {
Logging.instance.log(
"This node is configured for TOR only but TOR is disabled",
level: LogLevel.Warning,
);
return false;
}
}
bool testPassed = false;
switch (cryptoCurrency) {
@ -111,9 +128,7 @@ Future<bool> testNodeConnection({
case CryptonoteCurrency():
try {
final proxyInfo = ref
.read(prefsChangeNotifierProvider)
.useTor
final proxyInfo = ref.read(prefsChangeNotifierProvider).useTor
? ref.read(pTorService).getProxyInfo()
: null;
@ -186,7 +201,7 @@ Future<bool> testNodeConnection({
case Stellar():
try {
testPassed =
await testStellarNodeConnection(formData.host!, formData.port!);
await testStellarNodeConnection(formData.host!, formData.port!);
} catch (_) {}
break;
@ -202,9 +217,7 @@ Future<bool> testNodeConnection({
"action": "version",
},
),
proxyInfo: ref
.read(prefsChangeNotifierProvider)
.useTor
proxyInfo: ref.read(prefsChangeNotifierProvider).useTor
? ref.read(pTorService).getProxyInfo()
: null,
);
@ -245,9 +258,7 @@ Future<bool> testNodeConnection({
case Cardano():
try {
final client = HttpClient();
if (ref
.read(prefsChangeNotifierProvider)
.useTor) {
if (ref.read(prefsChangeNotifierProvider).useTor) {
final proxyInfo = TorService.sharedInstance.getProxyInfo();
final proxySettings = ProxySettings(
proxyInfo.host,

View file

@ -0,0 +1,23 @@
enum TorPlainNetworkOption {
tor,
clear,
both;
bool allowsTor() => this == tor || this == both;
bool allowsClear() => this == clear || this == both;
static TorPlainNetworkOption fromNodeData(
bool torEnabled,
bool clearEnabled,
) {
if (clearEnabled && torEnabled) {
return TorPlainNetworkOption.both;
} else if (torEnabled) {
return TorPlainNetworkOption.tor;
} else if (clearEnabled) {
return TorPlainNetworkOption.clear;
} else {
return TorPlainNetworkOption.both;
}
}
}

View file

@ -77,6 +77,8 @@ class Banano extends NanoCurrency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -239,6 +239,8 @@ class Bitcoin extends Bip39HDCurrency
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
case CryptoCurrencyNetwork.test:
@ -252,6 +254,8 @@ class Bitcoin extends Bip39HDCurrency
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
case CryptoCurrencyNetwork.test4:
@ -265,6 +269,8 @@ class Bitcoin extends Bip39HDCurrency
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -73,6 +73,8 @@ class BitcoinFrost extends FrostCurrency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
case CryptoCurrencyNetwork.test:
@ -86,6 +88,8 @@ class BitcoinFrost extends FrostCurrency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
case CryptoCurrencyNetwork.test4:
@ -99,6 +103,8 @@ class BitcoinFrost extends FrostCurrency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -298,6 +298,8 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
case CryptoCurrencyNetwork.test:
@ -311,6 +313,8 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -77,6 +77,8 @@ class Cardano extends Bip39Currency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:
@ -91,7 +93,8 @@ class Cardano extends Bip39Currency {
int get fractionDigits => 6;
@override
String get genesisHash => "f0f7892b5c333cffc4b3c4344de48af4cc63f55e44936196f365a9ef2244134f";
String get genesisHash =>
"f0f7892b5c333cffc4b3c4344de48af4cc63f55e44936196f365a9ef2244134f";
@override
bool get hasBuySupport => false;

View file

@ -189,6 +189,8 @@ class Dash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -189,6 +189,8 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
case CryptoCurrencyNetwork.test:
@ -202,6 +204,8 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -289,6 +289,8 @@ class Ecash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -80,6 +80,8 @@ class Epiccash extends Bip39Currency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -57,6 +57,8 @@ class Ethereum extends Bip39Currency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
@override

View file

@ -231,6 +231,8 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
case CryptoCurrencyNetwork.test:
@ -257,6 +259,8 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -220,6 +220,8 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
case CryptoCurrencyNetwork.test:
@ -233,6 +235,8 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -72,6 +72,8 @@ class Monero extends CryptonoteCurrency {
isFailover: true,
isDown: false,
trusted: true,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -102,6 +102,8 @@ class Namecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
// case CryptoCurrencyNetwork.test:
// TODO: [prio=low] Add testnet support.

View file

@ -77,6 +77,8 @@ class Nano extends NanoCurrency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -97,6 +97,8 @@ class Particl extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
// case CryptoCurrencyNetwork.test:
// TODO: [prio=low] Add testnet.

View file

@ -103,6 +103,8 @@ class Peercoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
case CryptoCurrencyNetwork.test:
@ -116,6 +118,8 @@ class Peercoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -55,6 +55,8 @@ class Solana extends Bip39Currency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:
throw Exception("Unsupported network: $network");

View file

@ -68,6 +68,8 @@ class Stellar extends Bip39Currency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
case CryptoCurrencyNetwork.test:
@ -81,6 +83,8 @@ class Stellar extends Bip39Currency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -118,6 +118,8 @@ class Tezos extends Bip39Currency {
coinName: identifier,
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -72,6 +72,8 @@ class Wownero extends CryptonoteCurrency {
isFailover: true,
isDown: false,
trusted: true,
torEnabled: true,
clearnetEnabled: true,
);
default:

View file

@ -1338,6 +1338,8 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
name: node.name,
useSSL: node.useSSL,
id: node.id,
torEnabled: node.torEnabled,
clearnetEnabled: node.clearnetEnabled,
);
}
@ -1352,6 +1354,8 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
name: e.name,
id: e.id,
useSSL: e.useSSL,
torEnabled: e.torEnabled,
clearnetEnabled: e.clearnetEnabled,
),
)
.toList();

View file

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:blockchain_utils/bip/bip/bip44/base/bip44_base.dart';
@ -7,19 +6,21 @@ import 'package:blockchain_utils/bip/cardano/cip1852/cip1852.dart';
import 'package:blockchain_utils/bip/cardano/cip1852/conf/cip1852_coins.dart';
import 'package:blockchain_utils/bip/cardano/mnemonic/cardano_icarus_seed_generator.dart';
import 'package:blockchain_utils/bip/cardano/shelley/cardano_shelley.dart';
import 'package:isar/isar.dart';
import 'package:on_chain/ada/ada.dart';
import 'package:socks5_proxy/socks.dart';
import 'package:tuple/tuple.dart';
import '../../../exceptions/wallet/node_tor_mismatch_config_exception.dart';
import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import 'package:tuple/tuple.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart' as isar;
import '../../../models/paymint/fee_object_model.dart';
import '../../../networking/http.dart';
import '../../../services/tor_service.dart';
import '../../../utilities/amount/amount.dart';
import 'package:isar/isar.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/prefs.dart';
import '../../../utilities/tor_plain_net_option_enum.dart';
import '../../api/cardano/blockfrost_http_provider.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../models/tx_data.dart';
@ -30,7 +31,6 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
// Source: https://cips.cardano.org/cip/CIP-1852
static const String _addressDerivationPath = "m/1852'/1815'/0'/0/0";
static final HTTP _httpClient = HTTP();
BlockforestProvider? blockfrostProvider;
@ -138,12 +138,12 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
final fee = params.calculateFee(284).toInt();
return FeeObject(
numberOfBlocksFast: 2,
numberOfBlocksAverage: 2,
numberOfBlocksSlow: 2,
fast: fee,
medium: fee,
slow: fee,
numberOfBlocksFast: 2,
numberOfBlocksAverage: 2,
numberOfBlocksSlow: 2,
fast: fee,
medium: fee,
slow: fee,
);
} catch (e, s) {
Logging.instance.log(
@ -184,41 +184,59 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
totalBalance += BigInt.parse(utxo.amount.first.quantity);
}
if (leftAmountForUtxos > BigInt.parse("0") || totalBalance < txData.amount!.raw) {
if (leftAmountForUtxos > BigInt.parse("0") ||
totalBalance < txData.amount!.raw) {
throw Exception("Insufficient balance");
}
final bip32 = CardanoIcarusBip32.fromSeed(CardanoIcarusSeedGenerator(await getMnemonic()).generate());
final bip32 = CardanoIcarusBip32.fromSeed(
CardanoIcarusSeedGenerator(await getMnemonic()).generate());
final spend = bip32.derivePath("1852'/1815'/0'/0/0");
final privateKey = AdaPrivateKey.fromBytes(spend.privateKey.raw);
// Calculate fees with example tx
final exampleFee = ADAHelper.toLovelaces("0.10");
final change = TransactionOutput(address: ADABaseAddress((await getCurrentReceivingAddress())!.value), amount: Value(coin: totalBalance - (txData.amount!.raw)));
final change = TransactionOutput(
address: ADABaseAddress((await getCurrentReceivingAddress())!.value),
amount: Value(coin: totalBalance - (txData.amount!.raw)));
final body = TransactionBody(
inputs: listOfUtxosToBeUsed.map((e) => TransactionInput(transactionId: TransactionHash.fromHex(e.txHash), index: e.outputIndex)).toList(),
outputs: [change, TransactionOutput(address: ADABaseAddress(txData.recipients!.first.address), amount: Value(coin: txData.amount!.raw - exampleFee))],
inputs: listOfUtxosToBeUsed
.map((e) => TransactionInput(
transactionId: TransactionHash.fromHex(e.txHash),
index: e.outputIndex))
.toList(),
outputs: [
change,
TransactionOutput(
address: ADABaseAddress(txData.recipients!.first.address),
amount: Value(coin: txData.amount!.raw - exampleFee))
],
fee: exampleFee,
);
final exampleTx = ADATransaction(
body: body,
witnessSet: TransactionWitnessSet(vKeys: [
body: body,
witnessSet: TransactionWitnessSet(
vKeys: [
privateKey.createSignatureWitness(body.toHash().data),
],)
,);
final params = await blockfrostProvider!.request(BlockfrostRequestLatestEpochProtocolParameters());
],
),
);
final params = await blockfrostProvider!
.request(BlockfrostRequestLatestEpochProtocolParameters());
final fee = params.calculateFee(exampleTx.size);
// Check if we are sending all balance, which means no change and only one output for recipient.
if (totalBalance == txData.amount!.raw) {
final List<TxRecipient> newRecipients = [(
address: txData.recipients!.first.address,
amount: Amount(
rawValue: txData.amount!.raw - fee,
fractionDigits: cryptoCurrency.fractionDigits,
),
isChange: txData.recipients!.first.isChange,
),];
final List<TxRecipient> newRecipients = [
(
address: txData.recipients!.first.address,
amount: Amount(
rawValue: txData.amount!.raw - fee,
fractionDigits: cryptoCurrency.fractionDigits,
),
isChange: txData.recipients!.first.isChange,
),
];
return txData.copyWith(
fee: Amount(
rawValue: fee,
@ -232,8 +250,10 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
}
// Minimum change in Cardano is 1 ADA and we need to have enough balance for that
if (totalBalance - (txData.amount!.raw + fee) < ADAHelper.toLovelaces("1")) {
throw Exception("Not enough balance for change. By network rules, please either send all balance or leave at least 1 ADA change.");
if (totalBalance - (txData.amount!.raw + fee) <
ADAHelper.toLovelaces("1")) {
throw Exception(
"Not enough balance for change. By network rules, please either send all balance or leave at least 1 ADA change.");
}
return txData.copyWith(
@ -266,7 +286,6 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
),
);
var leftAmountForUtxos = txData.amount!.raw + txData.fee!.raw;
final listOfUtxosToBeUsed = <ADAAccountUTXOResponse>[];
var totalBalance = BigInt.zero;
@ -285,31 +304,53 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
totalUtxoAmount += BigInt.parse(utxo.amount.first.quantity);
}
final bip32 = CardanoIcarusBip32.fromSeed(CardanoIcarusSeedGenerator(await getMnemonic()).generate());
final bip32 = CardanoIcarusBip32.fromSeed(
CardanoIcarusSeedGenerator(await getMnemonic()).generate());
final spend = bip32.derivePath("1852'/1815'/0'/0/0");
final privateKey = AdaPrivateKey.fromBytes(spend.privateKey.raw);
final change = TransactionOutput(address: ADABaseAddress((await getCurrentReceivingAddress())!.value), amount: Value(coin: totalUtxoAmount - (txData.amount!.raw + txData.fee!.raw)));
final change = TransactionOutput(
address: ADABaseAddress((await getCurrentReceivingAddress())!.value),
amount: Value(
coin: totalUtxoAmount - (txData.amount!.raw + txData.fee!.raw)));
List<TransactionOutput> outputs = [];
if (totalBalance == (txData.amount!.raw + txData.fee!.raw)) {
outputs = [TransactionOutput(address: ADABaseAddress(txData.recipients!.first.address), amount: Value(coin: txData.amount!.raw))];
outputs = [
TransactionOutput(
address: ADABaseAddress(txData.recipients!.first.address),
amount: Value(coin: txData.amount!.raw))
];
} else {
outputs = [change, TransactionOutput(address: ADABaseAddress(txData.recipients!.first.address), amount: Value(coin: txData.amount!.raw))];
outputs = [
change,
TransactionOutput(
address: ADABaseAddress(txData.recipients!.first.address),
amount: Value(coin: txData.amount!.raw))
];
}
final body = TransactionBody(
inputs: listOfUtxosToBeUsed.map((e) => TransactionInput(transactionId: TransactionHash.fromHex(e.txHash), index: e.outputIndex)).toList(),
inputs: listOfUtxosToBeUsed
.map((e) => TransactionInput(
transactionId: TransactionHash.fromHex(e.txHash),
index: e.outputIndex))
.toList(),
outputs: outputs,
fee: txData.fee!.raw,
);
final tx = ADATransaction(
body: body,
witnessSet: TransactionWitnessSet(vKeys: [
body: body,
witnessSet: TransactionWitnessSet(
vKeys: [
privateKey.createSignatureWitness(body.toHash().data),
],)
,);
],
),
);
final sentTx = await blockfrostProvider!.request(BlockfrostRequestSubmitTransaction(
transactionCborBytes: tx.serialize(),),);
final sentTx = await blockfrostProvider!.request(
BlockfrostRequestSubmitTransaction(
transactionCborBytes: tx.serialize(),
),
);
return txData.copyWith(
txid: sentTx,
);
@ -365,11 +406,13 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
final balance = Balance(
total: Amount(
rawValue: totalBalanceInLovelace,
fractionDigits: cryptoCurrency.fractionDigits,),
rawValue: totalBalanceInLovelace,
fractionDigits: cryptoCurrency.fractionDigits,
),
spendable: Amount(
rawValue: totalBalanceInLovelace,
fractionDigits: cryptoCurrency.fractionDigits,),
rawValue: totalBalanceInLovelace,
fractionDigits: cryptoCurrency.fractionDigits,
),
blockedTotal: Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits,
@ -399,8 +442,9 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
);
await info.updateCachedChainHeight(
newHeight: latestBlock.height == null ? 0 : latestBlock.height!,
isar: mainDB.isar,);
newHeight: latestBlock.height == null ? 0 : latestBlock.height!,
isar: mainDB.isar,
);
} catch (e, s) {
Logging.instance.log(
"Error updating transactions in cardano_wallet.dart: $e\n$s",
@ -473,13 +517,15 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
if (txType == isar.TransactionType.incoming) {
receiverAddr = currentAddr;
for (final output in utxoInfo.outputs) {
if (output.address == currentAddr && output.amount.first.unit == "lovelace") {
if (output.address == currentAddr &&
output.amount.first.unit == "lovelace") {
amount += int.parse(output.amount.first.quantity);
}
}
} else if (txType == isar.TransactionType.outgoing) {
for (final output in utxoInfo.outputs) {
if (output.address != currentAddr && output.amount.first.unit == "lovelace") {
if (output.address != currentAddr &&
output.amount.first.unit == "lovelace") {
receiverAddr = output.address;
amount += int.parse(output.amount.first.quantity);
}
@ -532,6 +578,8 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
}
await mainDB.addNewTransactionData(parsedTxsList, walletId);
} on NodeTorMismatchConfigException {
rethrow;
} catch (e, s) {
Logging.instance.log(
"Error updating transactions in cardano_wallet.dart: $e\n$s",
@ -548,6 +596,7 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
Future<void> updateProvider() async {
final currentNode = getCurrentNode();
final client = HttpClient();
if (prefs.useTor) {
final proxyInfo = TorService.sharedInstance.getProxyInfo();
@ -557,11 +606,45 @@ class CardanoWallet extends Bip39Wallet<Cardano> {
);
SocksTCPClient.assignToHttpClient(client, [proxySettings]);
}
blockfrostProvider = BlockforestProvider(
blockfrostProvider = CustomBlockForestProvider(
BlockfrostHttpProvider(
url: "${currentNode.host}:${currentNode.port}/",
client: client,
),
prefs,
TorPlainNetworkOption.fromNodeData(
currentNode.torEnabled,
currentNode.clearnetEnabled,
),
);
}
}
class CustomBlockForestProvider extends BlockforestProvider {
CustomBlockForestProvider(super.rpc, this.prefs, this.netOption);
final Prefs prefs;
final TorPlainNetworkOption netOption;
@override
Future<T> request<T, E>(
BlockforestRequestParam<T, E> request, [
Duration? timeout,
]) async {
if (prefs.useTor) {
if (netOption == TorPlainNetworkOption.clear) {
throw NodeTorMismatchConfigException(
message: "TOR enabled but node set to clearnet only",
);
}
} else {
if (netOption == TorPlainNetworkOption.tor) {
throw NodeTorMismatchConfigException(
message: "TOR off but node set to TOR only",
);
}
}
return super.request(request, timeout);
}
}

View file

@ -11,6 +11,7 @@ import 'package:mutex/mutex.dart';
import 'package:stack_wallet_backup/generate_password.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../../exceptions/wallet/node_tor_mismatch_config_exception.dart';
import '../../../models/balance.dart';
import '../../../models/epicbox_config_model.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
@ -32,6 +33,7 @@ import '../../../utilities/flutter_secure_storage_interface.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/stack_file_system.dart';
import '../../../utilities/test_epic_box_connection.dart';
import '../../../utilities/tor_plain_net_option_enum.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../models/tx_data.dart';
import '../intermediate/bip39_wallet.dart';
@ -82,6 +84,7 @@ class EpiccashWallet extends Bip39Wallet {
/// returns an empty String on success, error message on failure
Future<String> cancelPendingTransactionAndPost(String txSlateId) async {
try {
_hackedCheckTorNodePrefs();
final String wallet = (await secureStorageInterface.read(
key: '${walletId}_wallet',
))!;
@ -173,6 +176,7 @@ class EpiccashWallet extends Bip39Wallet {
}) async {
final wallet = await secureStorageInterface.read(key: '${walletId}_wallet');
try {
_hackedCheckTorNodePrefs();
final available = info.cachedBalance.spendable.raw.toInt();
final transactionFees = await epiccash.LibEpiccash.getTransactionFees(
@ -198,6 +202,7 @@ class EpiccashWallet extends Bip39Wallet {
}
Future<void> _startSync() async {
_hackedCheckTorNodePrefs();
Logging.instance.log("request start sync", level: LogLevel.Info);
final wallet = await secureStorageInterface.read(key: '${walletId}_wallet');
const int refreshFromNode = 1;
@ -222,6 +227,7 @@ class EpiccashWallet extends Bip39Wallet {
double spendable,
double total
})> _allWalletBalances() async {
_hackedCheckTorNodePrefs();
final wallet = await secureStorageInterface.read(key: '${walletId}_wallet');
const refreshFromNode = 0;
return await epiccash.LibEpiccash.getWalletBalances(
@ -232,6 +238,7 @@ class EpiccashWallet extends Bip39Wallet {
}
Future<bool> _testEpicboxServer(EpicBoxConfigModel epicboxConfig) async {
_hackedCheckTorNodePrefs();
final host = epicboxConfig.host;
final port = epicboxConfig.port ?? 443;
WebSocketChannel? channel;
@ -576,6 +583,7 @@ class EpiccashWallet extends Bip39Wallet {
@override
Future<TxData> confirmSend({required TxData txData}) async {
try {
_hackedCheckTorNodePrefs();
final wallet =
await secureStorageInterface.read(key: '${walletId}_wallet');
final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
@ -638,6 +646,7 @@ class EpiccashWallet extends Bip39Wallet {
@override
Future<TxData> prepareSend({required TxData txData}) async {
try {
_hackedCheckTorNodePrefs();
if (txData.recipients?.length != 1) {
throw Exception("Epic cash prepare send requires a single recipient!");
}
@ -679,6 +688,7 @@ class EpiccashWallet extends Bip39Wallet {
@override
Future<void> recover({required bool isRescan}) async {
try {
_hackedCheckTorNodePrefs();
await refreshMutex.protect(() async {
if (isRescan) {
// clear blockchain info
@ -782,6 +792,7 @@ class EpiccashWallet extends Bip39Wallet {
cryptoCurrency,
),
);
_hackedCheckTorNodePrefs();
// if (info.epicData?.creationHeight == null) {
// await info.updateExtraEpiccashWalletInfo(epicData: inf, isar: isar)
@ -880,6 +891,7 @@ class EpiccashWallet extends Bip39Wallet {
@override
Future<void> updateBalance() async {
try {
_hackedCheckTorNodePrefs();
final balances = await _allWalletBalances();
final balance = Balance(
total: Amount.fromDecimal(
@ -915,6 +927,7 @@ class EpiccashWallet extends Bip39Wallet {
@override
Future<void> updateTransactions() async {
try {
_hackedCheckTorNodePrefs();
final wallet =
await secureStorageInterface.read(key: '${walletId}_wallet');
const refreshFromNode = 1;
@ -1083,7 +1096,11 @@ class EpiccashWallet extends Bip39Wallet {
NodeFormData()
..host = node!.host
..useSSL = node.useSSL
..port = node.port,
..port = node.port
..netOption = TorPlainNetworkOption.fromNodeData(
node.torEnabled,
node.clearnetEnabled,
),
) !=
null;
} catch (e, s) {
@ -1094,6 +1111,7 @@ class EpiccashWallet extends Bip39Wallet {
@override
Future<void> updateChainHeight() async {
_hackedCheckTorNodePrefs();
final config = await _getRealConfig();
final latestHeight =
await epiccash.LibEpiccash.getChainHeight(config: config);
@ -1105,6 +1123,7 @@ class EpiccashWallet extends Bip39Wallet {
@override
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
_hackedCheckTorNodePrefs();
// setting ifErrorEstimateFee doesn't do anything as its not used in the nativeFee function?????
final int currentFee = await _nativeFee(
amount.raw.toInt(),
@ -1143,6 +1162,28 @@ class EpiccashWallet extends Bip39Wallet {
await super.exit();
Logging.instance.log("EpicCash_wallet exit finished", level: LogLevel.Info);
}
void _hackedCheckTorNodePrefs() {
final node = nodeService.getPrimaryNodeFor(currency: cryptoCurrency)!;
final netOption = TorPlainNetworkOption.fromNodeData(
node.torEnabled,
node.clearnetEnabled,
);
if (prefs.useTor) {
if (netOption == TorPlainNetworkOption.clear) {
throw NodeTorMismatchConfigException(
message: "TOR enabled but node set to clearnet only",
);
}
} else {
if (netOption == TorPlainNetworkOption.tor) {
throw NodeTorMismatchConfigException(
message: "TOR off but node set to TOR only",
);
}
}
}
}
Future<String> deleteEpicWallet({

View file

@ -9,6 +9,7 @@ import 'package:solana/dto.dart';
import 'package:solana/solana.dart';
import 'package:tuple/tuple.dart';
import '../../../exceptions/wallet/node_tor_mismatch_config_exception.dart';
import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart' as isar;
import '../../../models/isar/models/isar_models.dart';
@ -19,6 +20,7 @@ import '../../../services/tor_service.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/prefs.dart';
import '../../../utilities/tor_plain_net_option_enum.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../models/tx_data.dart';
import '../intermediate/bip39_wallet.dart';
@ -60,6 +62,7 @@ class SolanaWallet extends Bip39Wallet<Solana> {
}
Future<int?> _getEstimatedNetworkFee(Amount transferAmount) async {
_checkClient();
final latestBlockhash = await _rpcClient?.getLatestBlockhash();
final pubKey = (await _getKeyPair()).publicKey;
@ -129,11 +132,14 @@ class SolanaWallet extends Bip39Wallet<Solana> {
// Rent exemption of Solana
final accInfo = await _rpcClient?.getAccountInfo(address!.value);
if (accInfo!.value == null) {
throw Exception("Account does not appear to exist");
}
final int minimumRent =
await _rpcClient?.getMinimumBalanceForRentExemption(
accInfo!.value!.data.toString().length,
) ??
0; // TODO revisit null condition.
await _rpcClient!.getMinimumBalanceForRentExemption(
accInfo.value!.data.toString().length,
);
if (minimumRent >
((await _getCurrentBalanceInLamports()) -
txData.amount!.raw.toInt() -
@ -254,7 +260,7 @@ class SolanaWallet extends Bip39Wallet<Solana> {
return health != null;
} catch (e, s) {
Logging.instance.log(
"$runtimeType Solana pingCheck failed \"health=$health\": $e\n$s",
"$runtimeType Solana pingCheck failed \"health response=$health\": $e\n$s",
level: LogLevel.Error,
);
return Future.value(false);
@ -289,21 +295,22 @@ class SolanaWallet extends Bip39Wallet<Solana> {
@override
Future<void> updateBalance() async {
_checkClient();
try {
final address = await getCurrentReceivingAddress();
_checkClient();
final balance = await _rpcClient?.getBalance(address!.value);
// Rent exemption of Solana
final accInfo = await _rpcClient?.getAccountInfo(address!.value);
// TODO [prio=low]: handle null account info.
if (accInfo!.value == null) {
throw Exception("Account does not appear to exist");
}
final int minimumRent =
await _rpcClient?.getMinimumBalanceForRentExemption(
accInfo!.value!.data.toString().length,
) ??
0;
// TODO [prio=low]: revisit null condition.
await _rpcClient!.getMinimumBalanceForRentExemption(
accInfo.value!.data.toString().length,
);
final spendableBalance = balance!.value - minimumRent;
final newBalance = Balance(
@ -357,16 +364,19 @@ class SolanaWallet extends Bip39Wallet<Solana> {
@override
Future<void> updateNode() async {
_solNode = getCurrentNode();
_solNode = NodeService(secureStorageInterface: secureStorageInterface)
.getPrimaryNodeFor(currency: info.coin) ??
info.coin.defaultNode;
await refresh();
}
@override
NodeModel getCurrentNode() {
return _solNode ??
NodeService(secureStorageInterface: secureStorageInterface)
_solNode ??= NodeService(secureStorageInterface: secureStorageInterface)
.getPrimaryNodeFor(currency: info.coin) ??
info.coin.defaultNode;
return _solNode!;
}
@override
@ -445,6 +455,8 @@ class SolanaWallet extends Bip39Wallet<Solana> {
txsList.add(Tuple2(transaction, txAddress));
}
await mainDB.addNewTransactionData(txsList, walletId);
} on NodeTorMismatchConfigException {
rethrow;
} catch (e, s) {
Logging.instance.log(
"Error occurred in solana_wallet.dart while getting"
@ -464,6 +476,28 @@ class SolanaWallet extends Bip39Wallet<Solana> {
///
void _checkClient() {
final node = getCurrentNode();
final netOption = TorPlainNetworkOption.fromNodeData(
node.torEnabled,
node.clearnetEnabled,
);
if (prefs.useTor) {
if (netOption == TorPlainNetworkOption.clear) {
_rpcClient = null;
throw NodeTorMismatchConfigException(
message: "TOR enabled but node set to clearnet only",
);
}
} else {
if (netOption == TorPlainNetworkOption.tor) {
_rpcClient = null;
throw NodeTorMismatchConfigException(
message: "TOR off but node set to TOR only",
);
}
}
_rpcClient = createRpcClient(
node.host,
node.port,

View file

@ -7,6 +7,7 @@ import 'package:mutex/mutex.dart';
import 'package:socks5_proxy/socks.dart';
import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart' as stellar;
import '../../../exceptions/wallet/node_tor_mismatch_config_exception.dart';
import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart';
@ -22,6 +23,7 @@ import '../../../utilities/amount/amount.dart';
import '../../../utilities/enums/fee_rate_type_enum.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/test_stellar_node_connection.dart';
import '../../../utilities/tor_plain_net_option_enum.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../models/tx_data.dart';
import '../intermediate/bip39_wallet.dart';
@ -61,12 +63,40 @@ class StellarWallet extends Bip39Wallet<Stellar> {
);
}
void _hackedCheck() {
final node = getCurrentNode();
final netOption = TorPlainNetworkOption.fromNodeData(
node.torEnabled,
node.clearnetEnabled,
);
if (prefs.useTor) {
if (netOption == TorPlainNetworkOption.clear) {
_stellarSdk?.httpClient.close();
_stellarSdk = null;
throw NodeTorMismatchConfigException(
message: "TOR enabled but node set to clearnet only",
);
}
} else {
if (netOption == TorPlainNetworkOption.tor) {
_stellarSdk?.httpClient.close();
_stellarSdk = null;
throw NodeTorMismatchConfigException(
message: "TOR off but node set to TOR only",
);
}
}
}
Future<stellar.StellarSDK> get stellarSdk async {
if (_requireMutex) {
await _torConnectingLock.protect(() async {
_hackedCheck();
_stellarSdk ??= _getFreshSdk();
});
} else {
_hackedCheck();
_stellarSdk ??= _getFreshSdk();
}
return _stellarSdk!;

View file

@ -4,6 +4,7 @@ import 'package:isar/isar.dart';
import 'package:tezart/tezart.dart' as tezart;
import 'package:tuple/tuple.dart';
import '../../../exceptions/wallet/node_tor_mismatch_config_exception.dart';
import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart';
@ -14,6 +15,7 @@ import '../../../services/tor_service.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/extensions/impl/string.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/tor_plain_net_option_enum.dart';
import '../../api/tezos/tezos_account.dart';
import '../../api/tezos/tezos_api.dart';
import '../../api/tezos/tezos_rpc_api.dart';
@ -42,6 +44,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
String passphrase = "",
}) async {
try {
_hackedCheckTorNodePrefs();
for (final path in Tezos.possibleDerivationPaths) {
final ks = await _getKeyStore(path: path.value);
@ -99,6 +102,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
// Amount? customRevealFee,
}) async {
try {
_hackedCheckTorNodePrefs();
final sourceKeyStore = await _getKeyStore();
final server = (_xtzNode ?? getCurrentNode()).host;
// if (kDebugMode) {
@ -178,6 +182,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
@override
Future<TxData> prepareSend({required TxData txData}) async {
try {
_hackedCheckTorNodePrefs();
if (txData.recipients == null || txData.recipients!.length != 1) {
throw Exception("$runtimeType prepareSend requires 1 recipient");
}
@ -288,6 +293,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
@override
Future<TxData> confirmSend({required TxData txData}) async {
_hackedCheckTorNodePrefs();
await txData.tezosOperationsList!.inject();
await txData.tezosOperationsList!.monitor();
return txData.copyWith(
@ -301,6 +307,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
TezosAccount account,
String recipientAddress,
) async {
_hackedCheckTorNodePrefs();
try {
final opList = await _buildSendTransaction(
amount: Amount(
@ -356,6 +363,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
int feeRate, {
String recipientAddress = "tz1MXvDCyXSqBqXPNDcsdmVZKfoxL9FTHmp2",
}) async {
_hackedCheckTorNodePrefs();
if (info.cachedBalance.spendable.raw == BigInt.zero) {
return Amount(
rawValue: BigInt.zero,
@ -402,6 +410,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
@override
Future<bool> pingCheck() async {
_hackedCheckTorNodePrefs();
final currentNode = getCurrentNode();
return await TezosRpcAPI.testNetworkConnection(
nodeInfo: (
@ -413,6 +422,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
@override
Future<void> recover({required bool isRescan}) async {
_hackedCheckTorNodePrefs();
await refreshMutex.protect(() async {
if (isRescan) {
await mainDB.deleteWalletBlockchainData(walletId);
@ -463,6 +473,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
@override
Future<void> updateBalance() async {
try {
_hackedCheckTorNodePrefs();
final currentNode = _xtzNode ?? getCurrentNode();
final balance = await TezosRpcAPI.getBalance(
nodeInfo: (host: currentNode.host, port: currentNode.port),
@ -498,6 +509,7 @@ class TezosWallet extends Bip39Wallet<Tezos> {
@override
Future<void> updateChainHeight() async {
try {
_hackedCheckTorNodePrefs();
final currentNode = _xtzNode ?? getCurrentNode();
final height = await TezosRpcAPI.getChainHeight(
nodeInfo: (
@ -530,14 +542,15 @@ class TezosWallet extends Bip39Wallet<Tezos> {
@override
NodeModel getCurrentNode() {
return _xtzNode ??
return _xtzNode ??=
NodeService(secureStorageInterface: secureStorageInterface)
.getPrimaryNodeFor(currency: info.coin) ??
info.coin.defaultNode;
.getPrimaryNodeFor(currency: info.coin) ??
info.coin.defaultNode;
}
@override
Future<void> updateTransactions() async {
_hackedCheckTorNodePrefs();
// TODO: optimize updateTransactions and use V2
final myAddress = (await getCurrentReceivingAddress())!;
@ -615,4 +628,26 @@ class TezosWallet extends Bip39Wallet<Tezos> {
// do nothing. Not used in tezos
return false;
}
void _hackedCheckTorNodePrefs() {
final node = nodeService.getPrimaryNodeFor(currency: cryptoCurrency)!;
final netOption = TorPlainNetworkOption.fromNodeData(
node.torEnabled,
node.clearnetEnabled,
);
if (prefs.useTor) {
if (netOption == TorPlainNetworkOption.clear) {
throw NodeTorMismatchConfigException(
message: "TOR enabled but node set to clearnet only",
);
}
} else {
if (netOption == TorPlainNetworkOption.tor) {
throw NodeTorMismatchConfigException(
message: "TOR off but node set to TOR only",
);
}
}
}
}

View file

@ -467,7 +467,22 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
final host = Uri.parse(node.host).host;
({InternetAddress host, int port})? proxy;
if (prefs.useTor) {
if (node.clearnetEnabled && !node.torEnabled) {
libMoneroWallet?.stopAutoSaving();
libMoneroWallet?.stopListeners();
libMoneroWallet?.stopSyncing();
_setSyncStatus(lib_monero_compat.FailedSyncStatus());
throw Exception("TOR - clearnet mismatch");
}
proxy = TorService.sharedInstance.getProxyInfo();
} else {
if (!node.clearnetEnabled && node.torEnabled) {
libMoneroWallet?.stopAutoSaving();
libMoneroWallet?.stopListeners();
libMoneroWallet?.stopSyncing();
_setSyncStatus(lib_monero_compat.FailedSyncStatus());
throw Exception("TOR - clearnet mismatch");
}
}
_setSyncStatus(lib_monero_compat.ConnectingSyncStatus());
@ -495,6 +510,9 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
proxy == null ? null : "${proxy.host.address}:${proxy.port}",
);
}
libMoneroWallet?.startSyncing();
libMoneroWallet?.startListeners();
libMoneroWallet?.startAutoSaving();
_setSyncStatus(lib_monero_compat.ConnectedSyncStatus());
} catch (e, s) {
@ -1020,6 +1038,26 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
return;
}
final node = getCurrentNode();
if (prefs.useTor) {
if (node.clearnetEnabled && !node.torEnabled) {
libMoneroWallet?.stopAutoSaving();
libMoneroWallet?.stopListeners();
libMoneroWallet?.stopSyncing();
_setSyncStatus(lib_monero_compat.FailedSyncStatus());
throw Exception("TOR - clearnet mismatch");
}
} else {
if (!node.clearnetEnabled && node.torEnabled) {
libMoneroWallet?.stopAutoSaving();
libMoneroWallet?.stopListeners();
libMoneroWallet?.stopSyncing();
_setSyncStatus(lib_monero_compat.FailedSyncStatus());
throw Exception("TOR - clearnet mismatch");
}
}
// this acquire should be almost instant due to above check.
// Slight possibility of race but should be irrelevant
await refreshMutex.acquire();

View file

@ -906,6 +906,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
name: node.name,
useSSL: node.useSSL,
id: node.id,
torEnabled: node.torEnabled,
clearnetEnabled: node.clearnetEnabled,
);
}
@ -919,6 +921,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
name: e.name,
id: e.id,
useSSL: e.useSSL,
torEnabled: e.torEnabled,
clearnetEnabled: e.clearnetEnabled,
),
)
.toList();

View file

@ -5,6 +5,7 @@ import 'package:isar/isar.dart';
import 'package:nanodart/nanodart.dart';
import 'package:tuple/tuple.dart';
import '../../../exceptions/wallet/node_tor_mismatch_config_exception.dart';
import '../../../external_api_keys.dart';
import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
@ -18,6 +19,7 @@ import '../../../services/tor_service.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/extensions/impl/string.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/tor_plain_net_option_enum.dart';
import '../../crypto_currency/intermediate/nano_currency.dart';
import '../../models/tx_data.dart';
import '../intermediate/bip39_wallet.dart';
@ -47,6 +49,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
final _httpClient = HTTP();
Future<String?> _requestWork(String hash) async {
_hackedCheckTorNodePrefs();
return _httpClient
.post(
url: Uri.parse(_kWorkServer), // this should be a
@ -104,6 +107,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
String amountRaw,
String publicAddress,
) async {
_hackedCheckTorNodePrefs();
// TODO: the opening block of an account is a special case
bool openBlock = false;
@ -223,6 +227,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
}
Future<void> _confirmAllReceivable(String accountAddress) async {
_hackedCheckTorNodePrefs();
final node = getCurrentNode();
final receivableResponse = await _httpClient.post(
url: Uri.parse(node.host),
@ -254,6 +259,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
//========= public ===========================================================
Future<String> getCurrentRepresentative() async {
_hackedCheckTorNodePrefs();
final serverURI = Uri.parse(getCurrentNode().host);
final address =
(_cachedAddress ?? await getCurrentReceivingAddress())!.value;
@ -272,6 +278,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
Future<bool> changeRepresentative(String newRepresentative) async {
try {
_hackedCheckTorNodePrefs();
final node = getCurrentNode();
final serverURI = Uri.parse(node.host);
await updateBalance();
@ -347,6 +354,11 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
@override
Future<bool> pingCheck() async {
try {
_hackedCheckTorNodePrefs();
} catch (_) {
return false;
}
final node = getCurrentNode();
final uri = Uri.parse(node.host);
final response = await _httpClient.post(
@ -365,6 +377,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
@override
Future<TxData> prepareSend({required TxData txData}) async {
_hackedCheckTorNodePrefs();
if (txData.recipients!.length != 1) {
throw ArgumentError(
"${cryptoCurrency.runtimeType} currently only "
@ -383,6 +396,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
@override
Future<TxData> confirmSend({required TxData txData}) async {
try {
_hackedCheckTorNodePrefs();
// our address:
final String publicAddress =
(_cachedAddress ?? await getCurrentReceivingAddress())!.value;
@ -483,6 +497,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
@override
Future<void> recover({required bool isRescan}) async {
try {
_hackedCheckTorNodePrefs();
await refreshMutex.protect(() async {
if (isRescan) {
await mainDB.deleteWalletBlockchainData(walletId);
@ -505,6 +520,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
String? previous,
Map<String, dynamic>? data,
) async {
_hackedCheckTorNodePrefs();
final node = getCurrentNode();
final body = {
"action": "account_history",
@ -543,6 +559,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
@override
Future<void> updateTransactions() async {
_hackedCheckTorNodePrefs();
await updateChainHeight();
final receivingAddress =
(_cachedAddress ?? await getCurrentReceivingAddress())!;
@ -613,6 +630,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
@override
Future<void> updateBalance() async {
try {
_hackedCheckTorNodePrefs();
final addressString =
(_cachedAddress ??= (await getCurrentReceivingAddress())!).value;
final body = jsonEncode({
@ -661,6 +679,7 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
@override
Future<void> updateChainHeight() async {
try {
_hackedCheckTorNodePrefs();
final String publicAddress =
(_cachedAddress ??= (await getCurrentReceivingAddress())!).value;
@ -724,4 +743,26 @@ mixin NanoInterface<T extends NanoCurrency> on Bip39Wallet<T> {
medium: 0,
slow: 0,
);
void _hackedCheckTorNodePrefs() {
final node = getCurrentNode();
final netOption = TorPlainNetworkOption.fromNodeData(
node.torEnabled,
node.clearnetEnabled,
);
if (prefs.useTor) {
if (netOption == TorPlainNetworkOption.clear) {
throw NodeTorMismatchConfigException(
message: "TOR enabled but node set to clearnet only",
);
}
} else {
if (netOption == TorPlainNetworkOption.tor) {
throw NodeTorMismatchConfigException(
message: "TOR off but node set to TOR only",
);
}
}
}
}

View file

@ -13,6 +13,8 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:tuple/tuple.dart';
import '../notifications/show_flush_bar.dart';
import '../pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart';
import '../pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart';
@ -26,6 +28,7 @@ import '../utilities/default_nodes.dart';
import '../utilities/enums/sync_type_enum.dart';
import '../utilities/test_node_connection.dart';
import '../utilities/text_styles.dart';
import '../utilities/tor_plain_net_option_enum.dart';
import '../utilities/util.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import 'conditional_parent.dart';
@ -33,7 +36,6 @@ import 'custom_buttons/blue_text_button.dart';
import 'expandable.dart';
import 'node_options_sheet.dart';
import 'rounded_white_container.dart';
import 'package:tuple/tuple.dart';
class NodeCard extends ConsumerStatefulWidget {
const NodeCard({
@ -165,6 +167,15 @@ class _NodeCardState extends ConsumerState<NodeCard> {
text: "Connect",
enabled: _status == "Disconnected",
onTap: () async {
final TorPlainNetworkOption netOption;
if (_node.torEnabled && !_node.clearnetEnabled) {
netOption = TorPlainNetworkOption.tor;
} else if (_node.clearnetEnabled && !_node.torEnabled) {
netOption = TorPlainNetworkOption.clear;
} else {
netOption = TorPlainNetworkOption.both;
}
final nodeFormData = NodeFormData()
..useSSL = _node.useSSL
..trusted = _node.trusted
@ -172,6 +183,7 @@ class _NodeCardState extends ConsumerState<NodeCard> {
..host = _node.host
..login = _node.loginName
..port = _node.port
..netOption = netOption
..isFailover = _node.isFailover;
nodeFormData.password = await _node.getPassword(
ref.read(secureStoreProvider),

View file

@ -27,6 +27,7 @@ import '../utilities/default_nodes.dart';
import '../utilities/enums/sync_type_enum.dart';
import '../utilities/test_node_connection.dart';
import '../utilities/text_styles.dart';
import '../utilities/tor_plain_net_option_enum.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import 'rounded_white_container.dart';
@ -256,6 +257,16 @@ class NodeOptionsSheet extends ConsumerWidget {
ref.read(secureStoreProvider),
);
if (context.mounted) {
final TorPlainNetworkOption netOption;
if (node.torEnabled &&
!node.clearnetEnabled) {
netOption = TorPlainNetworkOption.tor;
} else if (node.clearnetEnabled &&
!node.torEnabled) {
netOption = TorPlainNetworkOption.clear;
} else {
netOption = TorPlainNetworkOption.both;
}
final canConnect = await testNodeConnection(
context: context,
nodeFormData: NodeFormData()
@ -266,6 +277,7 @@ class NodeOptionsSheet extends ConsumerWidget {
..port = node.port
..useSSL = node.useSSL
..isFailover = node.isFailover
..netOption = netOption
..trusted = node.trusted,
cryptoCurrency: coin,
ref: ref,

View file

@ -23,6 +23,7 @@ import '../providers/providers.dart';
import '../utilities/constants.dart';
import '../utilities/logger.dart';
import '../utilities/show_loading.dart';
import '../utilities/show_node_tor_settings_mismatch.dart';
import '../utilities/util.dart';
import '../wallets/isar/providers/eth/current_token_wallet_provider.dart';
import '../wallets/wallet/impl/ethereum_wallet.dart';
@ -95,6 +96,19 @@ class SimpleWalletCard extends ConsumerWidget {
final wallet = ref.read(pWallets).getWallet(walletId);
final canContinue = await checkShowNodeTorSettingsMismatch(
context: context,
currency: wallet.cryptoCurrency,
prefs: ref.read(prefsChangeNotifierProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
allowCancel: true,
rootNavigator: Util.isDesktop,
);
if (!canContinue) {
return;
}
if (context.mounted) {
final Future<void> loadFuture;
if (wallet is LibMoneroWallet) {

View file

@ -168,6 +168,8 @@ void main() {
name: "some name",
id: "some ID",
useSSL: true,
torEnabled: true,
clearnetEnabled: true,
);
final client =

View file

@ -1,10 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:hive/hive.dart';
import 'package:hive_test/hive_test.dart';
import 'package:stackwallet/app_config.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/app_config.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
@ -48,6 +48,8 @@ void main() {
coinName: "bitcoin",
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
await service.setPrimaryNodeFor(
coin: Bitcoin(CryptoCurrencyNetwork.main),
@ -129,6 +131,8 @@ void main() {
coinName: "bitcoin",
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
final nodeB = NodeModel(
host: "host2",
@ -140,6 +144,8 @@ void main() {
coinName: "monero",
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
final nodeC = NodeModel(
host: "host3",
@ -151,6 +157,8 @@ void main() {
coinName: "epicCash",
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
setUp(() async {

View file

@ -37,6 +37,8 @@ void main() {
coinName: "Bitcoin",
isFailover: false,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
),
);
@ -51,6 +53,8 @@ void main() {
coinName: "Bitcoin",
isFailover: false,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
),
);
@ -112,6 +116,8 @@ void main() {
coinName: "Bitcoin",
isFailover: false,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
),
);
@ -126,6 +132,8 @@ void main() {
coinName: "Bitcoin",
isFailover: false,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
),
);
@ -188,6 +196,8 @@ void main() {
coinName: "Bitcoin",
isFailover: false,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
),
);
@ -202,6 +212,8 @@ void main() {
coinName: "Bitcoin",
isFailover: false,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
),
);

View file

@ -25,17 +25,20 @@ void main() {
final mockPrefs = MockPrefs();
final mockNodeService = MockNodeService();
when(mockNodeService.getNodeById(id: "node id")).thenAnswer(
(realInvocation) => NodeModel(
host: "127.0.0.1",
port: 2000,
name: "Some other name",
id: "node id",
useSSL: true,
enabled: true,
coinName: "Bitcoin",
isFailover: false,
isDown: false));
when(mockNodeService.getNodeById(id: "node id"))
.thenAnswer((realInvocation) => NodeModel(
host: "127.0.0.1",
port: 2000,
name: "Some other name",
id: "node id",
useSSL: true,
enabled: true,
coinName: "Bitcoin",
isFailover: false,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
));
when(mockNodeService.getPrimaryNodeFor(
currency: Bitcoin(CryptoCurrencyNetwork.main)))
@ -48,6 +51,8 @@ void main() {
enabled: true,
coinName: "Bitcoin",
isFailover: false,
torEnabled: true,
clearnetEnabled: true,
isDown: false));
await tester.pumpWidget(
@ -109,6 +114,8 @@ void main() {
coinName: "Bitcoin",
isFailover: false,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
),
);
@ -125,6 +132,8 @@ void main() {
coinName: "Bitcoin",
isFailover: false,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
),
);
@ -186,6 +195,8 @@ void main() {
coinName: "Bitcoin",
isFailover: false,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
),
);
@ -202,6 +213,8 @@ void main() {
coinName: "Bitcoin",
isFailover: false,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
),
);