stack_wallet/lib/services/node_service.dart

327 lines
9.5 KiB
Dart

/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
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';
import '../utilities/default_nodes.dart';
import '../utilities/flutter_secure_storage_interface.dart';
import '../utilities/logger.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
const kStackCommunityNodesEndpoint = "https://extras.stackwallet.com";
class NodeService extends ChangeNotifier {
final SecureStorageInterface secureStorageInterface;
/// Exposed [secureStorageInterface] in order to inject mock for tests
NodeService({
required this.secureStorageInterface,
});
Future<void> updateDefaults() async {
// hack
if (AppConfig.coins.where((e) => e.identifier == "firo").isNotEmpty) {
final others = [
"electrumx01.firo.org",
"electrumx02.firo.org",
"electrumx03.firo.org",
"electrumx.firo.org",
];
const port = 50002;
const idPrefix = "not_a_real_default_but_temp";
for (final host in others) {
final _id = "${idPrefix}_$host";
NodeModel? node = DB.instance.get<NodeModel>(
boxName: DB.boxNameNodeModels,
key: _id,
);
if (node == null) {
node = NodeModel(
host: host,
port: port,
name: host,
id: _id,
useSSL: true,
enabled: true,
coinName: "firo",
isFailover: true,
isDown: false,
torEnabled: true,
clearnetEnabled: true,
);
await DB.instance.put<NodeModel>(
boxName: DB.boxNameNodeModels,
key: _id,
value: node,
);
}
}
}
for (final defaultNode in AppConfig.coins.map(
(e) => e.defaultNode,
)) {
final savedNode = DB.instance
.get<NodeModel>(boxName: DB.boxNameNodeModels, key: defaultNode.id);
if (savedNode == null) {
// save the default node to hive only if no other nodes for the specific coin exist
if (getNodesFor(
AppConfig.getCryptoCurrencyByPrettyName(
defaultNode.coinName,
),
).isEmpty) {
await DB.instance.put<NodeModel>(
boxName: DB.boxNameNodeModels,
key: defaultNode.id,
value: defaultNode,
);
}
} else {
// update all fields but copy over previously set enabled and trusted states
await DB.instance.put<NodeModel>(
boxName: DB.boxNameNodeModels,
key: savedNode.id,
value: defaultNode.copyWith(
enabled: savedNode.enabled,
isFailover: savedNode.isFailover,
trusted: savedNode.trusted,
torEnabled: savedNode.torEnabled,
clearnetEnabled: savedNode.clearnetEnabled,
),
);
}
// check if a default node is the primary node for the crypto currency
// and update it if needed
final coin =
AppConfig.getCryptoCurrencyByPrettyName(defaultNode.coinName);
final primaryNode = getPrimaryNodeFor(currency: coin);
if (primaryNode != null && primaryNode.id == defaultNode.id) {
await setPrimaryNodeFor(
coin: coin,
node: defaultNode.copyWith(
enabled: primaryNode.enabled,
isFailover: primaryNode.isFailover,
trusted: primaryNode.trusted,
torEnabled: primaryNode.torEnabled,
clearnetEnabled: primaryNode.clearnetEnabled,
),
);
}
}
}
Future<void> setPrimaryNodeFor({
required CryptoCurrency coin,
required NodeModel node,
bool shouldNotifyListeners = false,
}) async {
await DB.instance.put<NodeModel>(
boxName: DB.boxNamePrimaryNodes,
key: coin.identifier,
value: node,
);
if (shouldNotifyListeners) {
notifyListeners();
}
}
NodeModel? getPrimaryNodeFor({required CryptoCurrency currency}) {
return DB.instance.get<NodeModel>(
boxName: DB.boxNamePrimaryNodes,
key: currency.identifier,
);
}
List<NodeModel> get primaryNodes {
return DB.instance.values<NodeModel>(boxName: DB.boxNamePrimaryNodes);
}
List<NodeModel> get nodes {
return DB.instance.values<NodeModel>(boxName: DB.boxNameNodeModels);
}
List<NodeModel> getNodesFor(CryptoCurrency coin) {
final list = DB.instance
.values<NodeModel>(boxName: DB.boxNameNodeModels)
.where(
(e) =>
e.coinName == coin.identifier &&
!e.id.startsWith(DefaultNodes.defaultNodeIdPrefix),
)
.toList();
// add default to end of list
list.addAll(
DB.instance
.values<NodeModel>(boxName: DB.boxNameNodeModels)
.where(
(e) =>
e.coinName == coin.identifier &&
e.id.startsWith(DefaultNodes.defaultNodeIdPrefix),
)
.toList(),
);
// return reversed list so default node appears at beginning
return list.reversed.toList();
}
NodeModel? getNodeById({required String id}) {
return DB.instance.get<NodeModel>(boxName: DB.boxNameNodeModels, key: id);
}
List<NodeModel> failoverNodesFor({required CryptoCurrency currency}) {
return getNodesFor(currency)
.where((e) => e.isFailover && !e.isDown)
.toList();
}
// should probably just combine this and edit into a save() func at some point
/// Over write node in hive if a node with existing id already exists.
/// Otherwise add node to hive
Future<void> add(
NodeModel node,
String? password,
bool shouldNotifyListeners,
) async {
await DB.instance.put<NodeModel>(
boxName: DB.boxNameNodeModels,
key: node.id,
value: node,
);
if (password != null) {
await secureStorageInterface.write(
key: "${node.id}_nodePW",
value: password,
);
}
if (shouldNotifyListeners) {
notifyListeners();
}
}
Future<void> delete(String id, bool shouldNotifyListeners) async {
await DB.instance.delete<NodeModel>(boxName: DB.boxNameNodeModels, key: id);
await secureStorageInterface.delete(key: "${id}_nodePW");
if (shouldNotifyListeners) {
notifyListeners();
}
}
Future<void> setEnabledState(
String id,
bool enabled,
bool shouldNotifyListeners,
) async {
final model = DB.instance.get<NodeModel>(
boxName: DB.boxNameNodeModels,
key: id,
)!;
await DB.instance.put<NodeModel>(
boxName: DB.boxNameNodeModels,
key: model.id,
value: model.copyWith(enabled: enabled),
);
if (shouldNotifyListeners) {
notifyListeners();
}
}
/// convenience wrapper for add
Future<void> edit(
NodeModel editedNode,
String? password,
bool shouldNotifyListeners,
) async {
// check if the node being edited is the primary one; if it is, setPrimaryNodeFor coin
final coin = AppConfig.getCryptoCurrencyByPrettyName(editedNode.coinName);
final primaryNode = getPrimaryNodeFor(currency: coin);
if (primaryNode?.id == editedNode.id) {
await setPrimaryNodeFor(
coin: coin,
node: editedNode,
shouldNotifyListeners: true,
);
}
return add(editedNode, password, shouldNotifyListeners);
}
//============================================================================
Future<void> updateCommunityNodes() async {
final Client client = Client();
try {
final uri = Uri.parse("$kStackCommunityNodesEndpoint/getNodes");
final response = await client.post(
uri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
"jsonrpc": "2.0",
"id": "0",
}),
);
final json = jsonDecode(response.body) as Map;
final result = jsonDecode(json['result'] as String);
final map = jsonDecode(result as String);
Logging.instance.log(map, level: LogLevel.Info);
for (final coin in AppConfig.coins) {
final nodeList = List<Map<String, dynamic>>.from(
map["nodes"][coin.identifier] as List? ?? [],
);
for (final nodeMap in nodeList) {
NodeModel node = NodeModel(
host: nodeMap["host"] as String,
port: nodeMap["port"] as int,
name: nodeMap["name"] as String,
id: nodeMap["id"] as String,
useSSL: nodeMap["useSSL"] == "true",
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) {
node = currentNode.copyWith(
host: node.host,
port: node.port,
name: node.name,
useSSL: node.useSSL,
coinName: node.coinName,
isDown: node.isDown,
);
}
await add(node, null, false);
}
}
} catch (e, s) {
Logging.instance
.log("updateCommunityNodes() failed: $e\n$s", level: LogLevel.Error);
}
}
}