/* * 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 updateDefaults() async { for (final defaultNode in AppConfig.coins.map( (e) => e.defaultNode, )) { final savedNode = DB.instance .get(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( 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( 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 setPrimaryNodeFor({ required CryptoCurrency coin, required NodeModel node, bool shouldNotifyListeners = false, }) async { await DB.instance.put( boxName: DB.boxNamePrimaryNodes, key: coin.identifier, value: node, ); if (shouldNotifyListeners) { notifyListeners(); } } NodeModel? getPrimaryNodeFor({required CryptoCurrency currency}) { return DB.instance.get( boxName: DB.boxNamePrimaryNodes, key: currency.identifier, ); } List get primaryNodes { return DB.instance.values(boxName: DB.boxNamePrimaryNodes); } List get nodes { return DB.instance.values(boxName: DB.boxNameNodeModels); } List getNodesFor(CryptoCurrency coin) { final list = DB.instance .values(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(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(boxName: DB.boxNameNodeModels, key: id); } List 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 add( NodeModel node, String? password, bool shouldNotifyListeners, ) async { await DB.instance.put( boxName: DB.boxNameNodeModels, key: node.id, value: node, ); if (password != null) { await secureStorageInterface.write( key: "${node.id}_nodePW", value: password, ); } if (shouldNotifyListeners) { notifyListeners(); } } Future delete(String id, bool shouldNotifyListeners) async { await DB.instance.delete(boxName: DB.boxNameNodeModels, key: id); await secureStorageInterface.delete(key: "${id}_nodePW"); if (shouldNotifyListeners) { notifyListeners(); } } Future setEnabledState( String id, bool enabled, bool shouldNotifyListeners, ) async { final model = DB.instance.get( boxName: DB.boxNameNodeModels, key: id, )!; await DB.instance.put( boxName: DB.boxNameNodeModels, key: model.id, value: model.copyWith(enabled: enabled), ); if (shouldNotifyListeners) { notifyListeners(); } } /// convenience wrapper for add Future 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 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>.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); } } }