diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index b3ab5277c..42f7a68b3 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -62,8 +62,6 @@ class Node extends HiveObject with Keyable { @HiveField(6) String? socksProxyAddress; - bool isPowNode = false; - bool get isSSL => useSSL ?? false; bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty; diff --git a/cw_core/lib/pow_node.dart b/cw_core/lib/pow_node.dart new file mode 100644 index 000000000..ce7f1001e --- /dev/null +++ b/cw_core/lib/pow_node.dart @@ -0,0 +1,218 @@ +import 'dart:io'; +import 'package:cw_core/keyable.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:hive/hive.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:http/io_client.dart' as ioc; + +part 'pow_node.g.dart'; + +Uri createUriFromElectrumAddress(String address) => Uri.tryParse('tcp://$address')!; + +@HiveType(typeId: PowNode.typeId) +class PowNode extends HiveObject with Keyable { + PowNode({ + this.login, + this.password, + this.useSSL, + this.trusted = false, + this.socksProxyAddress, + String? uri, + WalletType? type, + }) { + if (uri != null) { + uriRaw = uri; + } + if (type != null) { + this.type = type; + } + } + + PowNode.fromMap(Map map) + : uriRaw = map['uri'] as String? ?? '', + login = map['login'] as String?, + password = map['password'] as String?, + useSSL = map['useSSL'] as bool?, + trusted = map['trusted'] as bool? ?? false, + socksProxyAddress = map['socksProxyPort'] as String?; + + static const typeId = NODE_TYPE_ID; + static const boxName = 'Nodes'; + + @HiveField(0, defaultValue: '') + late String uriRaw; + + @HiveField(1) + String? login; + + @HiveField(2) + String? password; + + @HiveField(3, defaultValue: 0) + late int typeRaw; + + @HiveField(4) + bool? useSSL; + + @HiveField(5, defaultValue: false) + bool trusted; + + @HiveField(6) + String? socksProxyAddress; + + bool get isSSL => useSSL ?? false; + + bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty; + + Uri get uri { + switch (type) { + case WalletType.monero: + return Uri.http(uriRaw, ''); + case WalletType.bitcoin: + return createUriFromElectrumAddress(uriRaw); + case WalletType.litecoin: + return createUriFromElectrumAddress(uriRaw); + case WalletType.haven: + return Uri.http(uriRaw, ''); + case WalletType.ethereum: + return Uri.https(uriRaw, ''); + case WalletType.nano: + case WalletType.banano: + if (uriRaw.contains("https") || uriRaw.endsWith("443") || isSSL) { + return Uri.https(uriRaw, ''); + } else { + return Uri.http(uriRaw, ''); + } + default: + throw Exception('Unexpected type ${type.toString()} for Node uri'); + } + } + + @override + bool operator ==(other) => + other is PowNode && + (other.uriRaw == uriRaw && + other.login == login && + other.password == password && + other.typeRaw == typeRaw && + other.useSSL == useSSL && + other.trusted == trusted && + other.socksProxyAddress == socksProxyAddress); + + @override + int get hashCode => + uriRaw.hashCode ^ + login.hashCode ^ + password.hashCode ^ + typeRaw.hashCode ^ + useSSL.hashCode ^ + trusted.hashCode ^ + socksProxyAddress.hashCode; + + @override + dynamic get keyIndex { + _keyIndex ??= key; + return _keyIndex; + } + + WalletType get type => deserializeFromInt(typeRaw); + + set type(WalletType type) => typeRaw = serializeToInt(type); + + dynamic _keyIndex; + + Future requestNode() async { + try { + switch (type) { + case WalletType.monero: + return useSocksProxy + ? requestNodeWithProxy(socksProxyAddress ?? '') + : requestMoneroNode(); + case WalletType.bitcoin: + return requestElectrumServer(); + case WalletType.litecoin: + return requestElectrumServer(); + case WalletType.haven: + return requestMoneroNode(); + case WalletType.ethereum: + return requestElectrumServer(); + default: + return false; + } + } catch (_) { + return false; + } + } + + Future requestMoneroNode() async { + final path = '/json_rpc'; + final rpcUri = isSSL ? Uri.https(uri.authority, path) : Uri.http(uri.authority, path); + final realm = 'monero-rpc'; + final body = {'jsonrpc': '2.0', 'id': '0', 'method': 'get_info'}; + + try { + final authenticatingClient = HttpClient(); + + authenticatingClient.addCredentials( + rpcUri, + realm, + HttpClientDigestCredentials(login ?? '', password ?? ''), + ); + + final http.Client client = ioc.IOClient(authenticatingClient); + + final response = await client.post( + rpcUri, + headers: {'Content-Type': 'application/json'}, + body: json.encode(body), + ); + + client.close(); + + final resBody = json.decode(response.body) as Map; + return !(resBody['result']['offline'] as bool); + } catch (_) { + return false; + } + } + + Future requestNodeWithProxy(String proxy) async { + if (proxy.isEmpty || !proxy.contains(':')) { + return false; + } + final proxyAddress = proxy.split(':')[0]; + final proxyPort = int.parse(proxy.split(':')[1]); + try { + final socket = await Socket.connect(proxyAddress, proxyPort, timeout: Duration(seconds: 5)); + socket.destroy(); + return true; + } catch (_) { + return false; + } + } + + Future requestElectrumServer() async { + try { + await SecureSocket.connect(uri.host, uri.port, + timeout: Duration(seconds: 5), onBadCertificate: (_) => true); + return true; + } catch (_) { + return false; + } + } + + Future requestEthereumServer() async { + try { + final response = await http.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + return response.statusCode >= 200 && response.statusCode < 300; + } catch (_) { + return false; + } + } +} diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 6be218a6c..a549eb7db 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/pow_node.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; @@ -53,7 +54,7 @@ abstract class WalletBase< Future connectToNode({required Node node}); // there is a default definition here because only coins with a pow node (nano based) need to override this - Future connectToPowNode({required Node node}) async {} + Future connectToPowNode({required PowNode node}) async {} Future startSync(); diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index 153e9bc77..97eaf4342 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/pow_node.dart'; import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_transaction_model.dart'; import 'package:cw_nano/nano_util.dart'; @@ -20,7 +21,7 @@ class NanoClient { StreamSubscription? subscription; Node? _node; - Node? _powNode; + PowNode? _powNode; bool connect(Node node) { try { @@ -31,7 +32,7 @@ class NanoClient { } } - bool connectPow(Node node) { + bool connectPow(PowNode node) { try { _powNode = node; return true; diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index e3e7dab38..b1b583ff2 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -6,6 +6,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/pow_node.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -152,7 +153,7 @@ abstract class NanoWalletBase } @override - Future connectToPowNode({required Node node}) async { + Future connectToPowNode({required PowNode node}) async { _client.connectPow(node); } diff --git a/lib/di.dart b/lib/di.dart index d483ce57c..51bbc5306 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -25,6 +25,7 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart'; import 'package:cake_wallet/src/screens/nano_accounts/nano_account_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/nano_accounts/nano_account_list_page.dart'; +import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_choose_derivation.dart'; @@ -91,6 +92,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/nano_account.dart'; +import 'package:cw_core/pow_node.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cw_core/wallet_service.dart'; @@ -225,6 +227,7 @@ final getIt = GetIt.instance; var _isSetupFinished = false; late Box _walletInfoSource; late Box _nodeSource; +late Box _powNodeSource; late Box _contactSource; late Box _tradesSource; late Box