From d35f74666a743a62b626337ee918557d8c802cad Mon Sep 17 00:00:00 2001 From: benk10 Date: Mon, 3 May 2021 10:06:57 +0300 Subject: [PATCH 01/17] Move managers to managers folder --- .../specter/{ => managers}/device_manager.py | 10 +++++----- .../specter/{ => managers}/user_manager.py | 4 ++-- .../specter/{ => managers}/wallet_manager.py | 12 ++++++------ .../specter/server_endpoints/devices.py | 4 ++-- src/cryptoadvance/specter/server_endpoints/setup.py | 4 ++-- .../specter/server_endpoints/wallets.py | 2 +- src/cryptoadvance/specter/specter.py | 10 +++++----- src/cryptoadvance/specter/user.py | 4 ++-- tests/conftest.py | 2 +- tests/test_device_manager.py | 4 ++-- tests/test_specter.py | 2 +- tests/test_wallet_manager.py | 2 +- 12 files changed, 30 insertions(+), 30 deletions(-) rename src/cryptoadvance/specter/{ => managers}/device_manager.py (92%) rename src/cryptoadvance/specter/{ => managers}/user_manager.py (96%) rename src/cryptoadvance/specter/{ => managers}/wallet_manager.py (98%) diff --git a/src/cryptoadvance/specter/device_manager.py b/src/cryptoadvance/specter/managers/device_manager.py similarity index 92% rename from src/cryptoadvance/specter/device_manager.py rename to src/cryptoadvance/specter/managers/device_manager.py index dd6dc318f1..ef4539dbe1 100644 --- a/src/cryptoadvance/specter/device_manager.py +++ b/src/cryptoadvance/specter/managers/device_manager.py @@ -1,12 +1,12 @@ import os import json import logging -from .helpers import alias, load_jsons -from .rpc import get_default_datadir +from ..helpers import alias, load_jsons +from ..rpc import get_default_datadir -from .devices import __all__ as device_classes -from .devices.generic import GenericDevice # default device type -from .persistence import write_device, delete_file, delete_folder +from ..devices import __all__ as device_classes +from ..devices.generic import GenericDevice # default device type +from ..persistence import write_device, delete_file, delete_folder logger = logging.getLogger(__name__) diff --git a/src/cryptoadvance/specter/user_manager.py b/src/cryptoadvance/specter/managers/user_manager.py similarity index 96% rename from src/cryptoadvance/specter/user_manager.py rename to src/cryptoadvance/specter/managers/user_manager.py index cd2c7f5b36..26f3078f95 100644 --- a/src/cryptoadvance/specter/user_manager.py +++ b/src/cryptoadvance/specter/managers/user_manager.py @@ -1,8 +1,8 @@ import os import json import logging -from .persistence import read_json_file, write_json_file -from .user import User, hash_password +from ..persistence import read_json_file, write_json_file +from ..user import User, hash_password from flask_login import current_user logger = logging.getLogger(__name__) diff --git a/src/cryptoadvance/specter/wallet_manager.py b/src/cryptoadvance/specter/managers/wallet_manager.py similarity index 98% rename from src/cryptoadvance/specter/wallet_manager.py rename to src/cryptoadvance/specter/managers/wallet_manager.py index 7a70212a4a..aee6139ac3 100644 --- a/src/cryptoadvance/specter/wallet_manager.py +++ b/src/cryptoadvance/specter/managers/wallet_manager.py @@ -8,12 +8,12 @@ from collections import OrderedDict from io import BytesIO -from .helpers import alias, load_jsons -from .persistence import delete_file, delete_folder -from .rpc import RpcError, get_default_datadir -from .specter_error import SpecterError -from .util.descriptor import AddChecksum -from .wallet import Wallet +from ..helpers import alias, load_jsons +from ..persistence import delete_file, delete_folder +from ..rpc import RpcError, get_default_datadir +from ..specter_error import SpecterError +from ..util.descriptor import AddChecksum +from ..wallet import Wallet logger = logging.getLogger() diff --git a/src/cryptoadvance/specter/server_endpoints/devices.py b/src/cryptoadvance/specter/server_endpoints/devices.py index b34a226bcc..3229dae120 100644 --- a/src/cryptoadvance/specter/server_endpoints/devices.py +++ b/src/cryptoadvance/specter/server_endpoints/devices.py @@ -15,9 +15,9 @@ from mnemonic import Mnemonic from ..helpers import is_testnet, generate_mnemonic from ..key import Key -from ..device_manager import get_device_class +from ..managers.device_manager import get_device_class from ..devices.bitcoin_core import BitcoinCore -from ..wallet_manager import purposes +from ..managers.wallet_manager import purposes from ..specter_error import handle_exception rand = random.randint(0, 1e32) # to force style refresh diff --git a/src/cryptoadvance/specter/server_endpoints/setup.py b/src/cryptoadvance/specter/server_endpoints/setup.py index e4a91d2059..cf15851bae 100644 --- a/src/cryptoadvance/specter/server_endpoints/setup.py +++ b/src/cryptoadvance/specter/server_endpoints/setup.py @@ -15,9 +15,9 @@ from mnemonic import Mnemonic from ..helpers import is_testnet, generate_mnemonic from ..key import Key -from ..device_manager import get_device_class +from ..managers.device_manager import get_device_class from ..devices.bitcoin_core import BitcoinCore -from ..wallet_manager import purposes +from ..managers.wallet_manager import purposes from ..specter_error import handle_exception from ..util.bitcoind_setup_tasks import ( setup_bitcoind_thread, diff --git a/src/cryptoadvance/specter/server_endpoints/wallets.py b/src/cryptoadvance/specter/server_endpoints/wallets.py index fa6d94e92a..e9ae9e07c0 100644 --- a/src/cryptoadvance/specter/server_endpoints/wallets.py +++ b/src/cryptoadvance/specter/server_endpoints/wallets.py @@ -37,7 +37,7 @@ from ..util.fee_estimation import get_fees from ..util.price_providers import get_price_at from ..util.tx import decoderawtransaction -from ..wallet_manager import purposes +from ..managers.wallet_manager import purposes rand = random.randint(0, 1e32) # to force style refresh diff --git a/src/cryptoadvance/specter/specter.py b/src/cryptoadvance/specter/specter.py index 577bf23eb9..1e9e1be59f 100644 --- a/src/cryptoadvance/specter/specter.py +++ b/src/cryptoadvance/specter/specter.py @@ -19,9 +19,11 @@ from urllib3.exceptions import NewConnectionError from requests.exceptions import ConnectionError from .rpc import BitcoinRPC -from .device_manager import DeviceManager -from .wallet_manager import WalletManager -from .user_manager import UserManager +from .managers.device_manager import DeviceManager +from .managers.wallet_manager import WalletManager +from .managers.user_manager import UserManager +from .managers.otp_manager import OtpManager +from .managers.config_manager import ConfigManager from .persistence import write_json_file, read_json_file from .user import User from .util.price_providers import update_price @@ -32,8 +34,6 @@ from .specter_error import SpecterError, ExtProcTimeoutException from sys import exit from .util.setup_states import SETUP_STATES -from .managers.otp_manager import OtpManager -from .managers.config_manager import ConfigManager logger = logging.getLogger(__name__) diff --git a/src/cryptoadvance/specter/user.py b/src/cryptoadvance/specter/user.py index 6624fd6234..ea9bcb276d 100644 --- a/src/cryptoadvance/specter/user.py +++ b/src/cryptoadvance/specter/user.py @@ -6,8 +6,8 @@ from flask_login import UserMixin from .specter_error import SpecterError from .persistence import read_json_file, write_json_file, delete_folder -from .wallet_manager import WalletManager -from .device_manager import DeviceManager +from .managers.wallet_manager import WalletManager +from .managers.device_manager import DeviceManager def hash_password(password): diff --git a/tests/conftest.py b/tests/conftest.py index def5de9249..f88f0b4754 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,7 @@ BitcoindPlainController, ) from cryptoadvance.specter.bitcoind_docker import BitcoindDockerController -from cryptoadvance.specter.device_manager import DeviceManager +from cryptoadvance.specter.managers.device_manager import DeviceManager from cryptoadvance.specter.specter import Specter from cryptoadvance.specter.server import create_app, init_app diff --git a/tests/test_device_manager.py b/tests/test_device_manager.py index 3e63f12500..8c01635049 100644 --- a/tests/test_device_manager.py +++ b/tests/test_device_manager.py @@ -1,8 +1,8 @@ import os from cryptoadvance.specter.devices.generic import GenericDevice from cryptoadvance.specter.key import Key -from cryptoadvance.specter.device_manager import DeviceManager -from cryptoadvance.specter.wallet_manager import WalletManager +from cryptoadvance.specter.managers.device_manager import DeviceManager +from cryptoadvance.specter.managers.wallet_manager import WalletManager def test_DeviceManager(empty_data_folder): diff --git a/tests/test_specter.py b/tests/test_specter.py index a17fa19ff9..e4a4146c01 100644 --- a/tests/test_specter.py +++ b/tests/test_specter.py @@ -5,7 +5,7 @@ from cryptoadvance.specter.rpc import BitcoinRPC from cryptoadvance.specter.specter import get_rpc, Specter from cryptoadvance.specter.specter_error import SpecterError -from cryptoadvance.specter.wallet_manager import WalletManager +from cryptoadvance.specter.managers.wallet_manager import WalletManager def test_alias(): diff --git a/tests/test_wallet_manager.py b/tests/test_wallet_manager.py index be61f31976..abd9ed752a 100644 --- a/tests/test_wallet_manager.py +++ b/tests/test_wallet_manager.py @@ -4,7 +4,7 @@ from cryptoadvance.specter.specter_error import SpecterError from cryptoadvance.specter.wallet import Wallet from cryptoadvance.specter.key import Key -from cryptoadvance.specter.wallet_manager import WalletManager +from cryptoadvance.specter.managers.wallet_manager import WalletManager def test_WalletManager(docker, request, devices_filled_data_folder, device_manager): From 096d92221453f7cadb0951e050141a15e110b1c3 Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 4 May 2021 17:00:22 +0300 Subject: [PATCH 02/17] Add node manager --- .../specter/managers/config_manager.py | 60 +--- .../specter/managers/node_manager.py | 129 +++++++ src/cryptoadvance/specter/node.py | 320 ++++++++++++++++++ src/cryptoadvance/specter/persistence.py | 5 + .../specter/server_endpoints/controller.py | 8 +- .../specter/server_endpoints/nodes.py | 199 +++++++++++ .../specter/server_endpoints/settings.py | 10 +- src/cryptoadvance/specter/specter.py | 272 +++------------ .../specter/static/img/file_icon.svg | 1 - .../specter/static/img/flip-horizontal.svg | 6 + .../templates/device/new_device_manual.jinja | 2 +- .../specter/templates/includes/hwi/hwi.jinja | 5 - .../templates/includes/overlay/overlay.html | 6 +- .../components/node_select_popup.jinja | 77 +++++ .../templates/includes/sidebar/sidebar.jinja | 21 +- .../templates/node/node_settings.jinja | 219 ++++++++++++ .../settings/bitcoin_core_settings.jinja | 16 +- src/cryptoadvance/specter/user.py | 2 +- 18 files changed, 1032 insertions(+), 326 deletions(-) create mode 100644 src/cryptoadvance/specter/managers/node_manager.py create mode 100644 src/cryptoadvance/specter/node.py create mode 100644 src/cryptoadvance/specter/server_endpoints/nodes.py delete mode 100644 src/cryptoadvance/specter/static/img/file_icon.svg create mode 100644 src/cryptoadvance/specter/static/img/flip-horizontal.svg create mode 100644 src/cryptoadvance/specter/templates/includes/sidebar/components/node_select_popup.jinja create mode 100644 src/cryptoadvance/specter/templates/node/node_settings.jinja diff --git a/src/cryptoadvance/specter/managers/config_manager.py b/src/cryptoadvance/specter/managers/config_manager.py index 146f06faf6..67b1228f89 100644 --- a/src/cryptoadvance/specter/managers/config_manager.py +++ b/src/cryptoadvance/specter/managers/config_manager.py @@ -9,7 +9,6 @@ from ..helpers import deep_update from ..persistence import read_json_file, write_json_file -from ..rpc import RpcError, autodetect_rpc_confs, detect_rpc_confs, get_default_datadir from ..specter_error import SpecterError from .genericdata_manager import GenericDataManager @@ -31,25 +30,6 @@ def __init__(self, data_folder, config={}): super().__init__(data_folder) self.arg_config = config self.data = { - "rpc": { - "autodetect": True, - "datadir": get_default_datadir(), - "user": "", - "password": "", - "port": "", - "host": "localhost", # localhost - "protocol": "http", # https for the future - "external_node": True, - }, - "internal_node": { - "autodetect": False, - "datadir": os.path.join(self.data_folder, ".bitcoin"), - "user": "bitcoin", - "password": secrets.token_urlsafe(16), - "host": "localhost", # localhost - "protocol": "http", # https for the future - "port": 8332, - }, "auth": { "method": "none", "password_min_chars": 6, @@ -63,6 +43,7 @@ def __init__(self, data_folder, config={}): "regtest": "CUSTOM", "signet": "CUSTOM", }, + "active_node_alias": "default", "proxy_url": "socks5h://localhost:9050", # Tor proxy URL "only_tor": False, "tor_control_port": "", @@ -85,26 +66,6 @@ def __init__(self, data_folder, config={}): } self.check_config() - @property - def rpc_conf(self): - return ( - self.data["rpc"] - if self.data["rpc"].get("external_node", True) - else self.data["internal_node"] - ) - - def update_rpc(self, **kwargs): - need_update = kwargs.get("need_update", False) - for k in kwargs: - if k != "need_update" and self.rpc_conf[k] != kwargs[k]: - self.data[ - "rpc" - if self.data["rpc"].get("external_node", True) - else "internal_node" - ][k] = kwargs[k] - need_update = True - return need_update - def check_config(self): """ Updates config if file config have changed. @@ -132,15 +93,10 @@ def check_config(self): # config from constructor overrides file config deep_update(self.data, self.arg_config) - @property - def bitcoin_datadir(self): - if "datadir" in self.data["rpc"]: - if self.data["rpc"].get("external_node", True): - return os.path.expanduser(self.data["rpc"]["datadir"]) - else: - if "datadir" in self.data["internal_node"]: - return os.path.expanduser(self.data["internal_node"]["datadir"]) - return get_default_datadir() + def update_active_node(self, node_alias): + """set the current active node to use""" + self.data["active_node_alias"] = node_alias + self._save() def set_bitcoind_pid(self, pid): """set the control pid of the bitcoind daemon""" @@ -148,12 +104,6 @@ def set_bitcoind_pid(self, pid): self.data["bitcoind"] = pid self._save() - def update_use_external_node(self, use_external_node): - """set whatever specter should connect to internal or external node""" - assert isinstance(use_external_node, bool) - self.data["rpc"]["external_node"] = use_external_node - self._save() - def update_auth(self, method, rate_limit, registration_link_timeout): """simply persisting the current auth-choice""" auth = self.data["auth"] diff --git a/src/cryptoadvance/specter/managers/node_manager.py b/src/cryptoadvance/specter/managers/node_manager.py new file mode 100644 index 0000000000..f1747cf364 --- /dev/null +++ b/src/cryptoadvance/specter/managers/node_manager.py @@ -0,0 +1,129 @@ +import os +import logging + +from ..rpc import get_default_datadir +from ..specter_error import SpecterError +from ..persistence import write_node, delete_file +from ..helpers import alias, load_jsons +from ..node import Node + +logger = logging.getLogger(__name__) + + +class NodeManager: + # chain is required to manage wallets when bitcoind is not running + def __init__( + self, + proxy_url="socks5h://localhost:9050", + only_tor=False, + active_node="default", + data_folder="", + ): + self.data_folder = data_folder + self._active_node = active_node + self.proxy_url = proxy_url + self.only_tor = only_tor + self.update(data_folder) + + def update(self, data_folder=None): + if data_folder is not None: + self.data_folder = data_folder + if data_folder.startswith("~"): + data_folder = os.path.expanduser(data_folder) + # creating folders if they don't exist + if not os.path.isdir(data_folder): + os.mkdir(data_folder) + nodes = {} + nodes_files = load_jsons(self.data_folder, key="name") + for node_alias in nodes_files: + fullpath = os.path.join(self.data_folder, "%s.json" % node_alias) + nodes[nodes_files[node_alias]["name"]] = Node.from_json( + nodes_files[node_alias], + self, + default_alias=node_alias, + default_fullpath=fullpath, + ) + if not nodes: + self.add_node( + name="Bitcoin Core", + autodetect=True, + datadir=get_default_datadir(), + user="", + password="", + port=8332, + host="localhost", + protocol="http", + external_node=True, + default_alias="default", + ) + else: + self.nodes = nodes + + @property + def active_node(self): + return self.get_by_alias(self._active_node) + + @property + def nodes_names(self): + return sorted(self.nodes.keys()) + + def switch_node(self, node_alias): + # this will throw an error if the node doesn't exist + self._active_node = self.get_by_alias(node_alias).alias + + def get_by_alias(self, alias): + for node_name in self.nodes: + if self.nodes[node_name] and self.nodes[node_name].alias == alias: + return self.nodes[node_name] + raise SpecterError("Node %s does not exist!" % alias) + + def add_node( + self, + name, + autodetect, + datadir, + user, + password, + port, + host, + protocol, + external_node, + default_alias=None, + ): + if not default_alias: + node_alias = alias(name) + else: + node_alias = default_alias + fullpath = os.path.join(self.data_folder, "%s.json" % node_alias) + i = 2 + while os.path.isfile(fullpath): + node_alias = alias("%s %d" % (name, i)) + fullpath = os.path.join(self.data_folder, "%s.json" % node_alias) + i += 1 + + node = Node( + name, + node_alias, + autodetect, + datadir, + user, + password, + port, + host, + protocol, + external_node, + fullpath, + self, + ) + write_node(node, fullpath) + self.update() # reload files + return node + + def delete_node(self, node, specter): + logger.info("Deleting {}".format(node.alias)) + # Delete files + delete_file(node.fullpath) + del self.nodes[node.name] + if self._active_node == node.alias: + specter.update_active_node(next(iter(self.nodes.values())).alias) + self.update() diff --git a/src/cryptoadvance/specter/node.py b/src/cryptoadvance/specter/node.py new file mode 100644 index 0000000000..f66e272494 --- /dev/null +++ b/src/cryptoadvance/specter/node.py @@ -0,0 +1,320 @@ +import json +import logging +import os + +from .helpers import is_testnet +from .rpc import ( + BitcoinRPC, + RpcError, + autodetect_rpc_confs, + detect_rpc_confs, + get_default_datadir, +) +from .persistence import write_node + +logger = logging.getLogger(__name__) + + +class Node: + def __init__( + self, + name, + alias, + autodetect, + datadir, + user, + password, + port, + host, + protocol, + external_node, + fullpath, + manager, + ): + self.name = name + self.alias = alias + self.autodetect = autodetect + self.datadir = datadir + self.user = user + self.password = password + self.port = port + self.host = host + self.protocol = protocol + self.external_node = external_node + self.fullpath = fullpath + self.manager = manager + self.proxy_url = manager.proxy_url + self.only_tor = manager.only_tor + self.rpc = None + self.rpc = self.get_rpc() + + self.check_info() + + @classmethod + def from_json(cls, node_dict, manager, default_alias="", default_fullpath=""): + name = node_dict.get("name", "") + alias = node_dict.get("alias", default_alias) + autodetect = node_dict.get("autodetect", True) + datadir = node_dict.get("datadir", get_default_datadir()) + user = node_dict.get("user", "") + password = node_dict.get("password", "") + port = node_dict.get("port", None) + host = node_dict.get("host", "localhost") + protocol = node_dict.get("protocol", "http") + external_node = node_dict.get("external_node", True) + fullpath = node_dict.get("fullpath", default_fullpath) + + return cls( + name, + alias, + autodetect, + datadir, + user, + password, + port, + host, + protocol, + external_node, + fullpath, + manager, + ) + + @property + def json(self): + return { + "name": self.name, + "alias": self.alias, + "autodetect": self.autodetect, + "datadir": self.datadir, + "user": self.user, + "password": self.password, + "port": self.port, + "host": self.host, + "protocol": self.protocol, + "external_node": self.external_node, + "fullpath": self.fullpath, + } + + def get_rpc(self): + """ + Checks if config have changed, compares with old rpc + and returns new one if necessary + """ + rpc = self.rpc + if self.autodetect: + if self.port: + rpc_conf_arr = autodetect_rpc_confs( + datadir=os.path.expanduser(self.datadir), port=self.port + ) + else: + rpc_conf_arr = autodetect_rpc_confs( + datadir=os.path.expanduser(self.datadir) + ) + if len(rpc_conf_arr) > 0: + rpc = BitcoinRPC( + **rpc_conf_arr[0], proxy_url=self.proxy_url, only_tor=self.only_tor + ) + else: + # if autodetect is disabled and port is not defined + # we use default port 8332 + if not self.port: + self.port = 8332 + rpc = BitcoinRPC( + user=self.user, + password=self.password, + host=self.host, + port=self.port, + protocol=self.protocol, + proxy_url=self.proxy_url, + only_tor=self.only_tor, + ) + return rpc + + def update_rpc( + self, + autodetect=None, + datadir=None, + user=None, + password=None, + port=None, + host=None, + protocol=None, + ): + update_rpc = self.rpc is None or not self.rpc.test_connection() + if autodetect is not None and self.autodetect != autodetect: + self.autodetect = autodetect + update_rpc = True + if datadir is not None and self.datadir != datadir: + self.datadir = datadir + update_rpc = True + if user is not None and self.user != user: + self.user = user + update_rpc = True + if password is not None and self.password != password: + self.password = password + update_rpc = True + if port is not None and self.port != port: + self.port = port + update_rpc = True + if host is not None and self.host != host: + self.host = host + update_rpc = True + if protocol is not None and self.protocol != protocol: + self.protocol = protocol + update_rpc = True + if update_rpc: + self.rpc = self.get_rpc() + write_node(self, self.fullpath) + self.check_info() + return False if not self.rpc else self.rpc.test_connection() + + def rename(self, new_name): + logger.info("Renaming {}".format(self.alias)) + self.name = new_name + write_node(self, self.fullpath) + self.manager.update() + + def check_info(self): + self._is_configured = self.rpc is not None + self._is_running = False + if self._is_configured: + try: + res = [ + r["result"] + for r in self.rpc.multi( + [ + ("getblockchaininfo", None), + ("getnetworkinfo", None), + ("getmempoolinfo", None), + ("uptime", None), + ("getblockhash", 0), + ("scantxoutset", "status", []), + ] + ) + ] + self._info = res[0] + self._network_info = res[1] + self._info["mempool_info"] = res[2] + self._info["uptime"] = res[3] + try: + self.rpc.getblockfilter(res[4]) + self._info["blockfilterindex"] = True + except: + self._info["blockfilterindex"] = False + self._info["utxorescan"] = ( + res[5]["progress"] + if res[5] is not None and "progress" in res[5] + else None + ) + if self._info["utxorescan"] is None: + self.utxorescanwallet = None + self._is_running = True + except Exception as e: + self._info = {"chain": None} + self._network_info = {"subversion": "", "version": 999999} + logger.error("Exception %s while check_node_info()" % e) + pass + else: + self._info = {"chain": None} + self._network_info = {"subversion": "", "version": 999999} + + if not self._is_running: + self._info["chain"] = None + + def test_rpc(self): + if self.rpc is None: + return {"out": "", "err": "autodetect failed", "code": -1} + r = {} + r["tests"] = {"connectable": False} + r["err"] = "" + r["code"] = 0 + try: + r["tests"]["recent_version"] = ( + int(self.rpc.getnetworkinfo()["version"]) >= 170000 + ) + if not r["tests"]["recent_version"]: + r["err"] = "Core Node might be too old" + + r["tests"]["connectable"] = True + r["tests"]["credentials"] = True + try: + self.rpc.listwallets() + r["tests"]["wallets"] = True + except RpcError as rpce: + logger.error(rpce) + r["tests"]["wallets"] = False + r["err"] = "Wallets disabled" + + r["out"] = json.dumps(self.rpc.getblockchaininfo(), indent=4) + except ConnectionError as e: + logger.error("Caught an ConnectionError while test_rpc: %s", e) + + r["tests"]["connectable"] = False + r["err"] = "Failed to connect!" + r["code"] = -1 + except RpcError as rpce: + logger.error("Caught an RpcError while test_rpc: %s", rpce) + logger.error(rpce.status_code) + r["tests"]["connectable"] = True + r["code"] = self.rpc.r.status_code + if rpce.status_code == 401: + r["tests"]["credentials"] = False + r["err"] = "RPC authentication failed!" + else: + r["err"] = str(rpce.status_code) + except Exception as e: + logger.error( + "Caught an exception of type {} while test_rpc: {}".format( + type(e), str(e) + ) + ) + r["out"] = "" + if self.rpc.r is not None and "error" in self.rpc.r: + r["err"] = self.rpc.r["error"] + r["code"] = self.rpc.r.status_code + else: + r["err"] = "Failed to connect" + r["code"] = -1 + return r + + def abortrescanutxo(self): + self.rpc.scantxoutset("abort", []) + # Bitcoin Core doesn't catch up right away + # so app.specter.check() doesn't work + self._info["utxorescan"] = None + self.utxorescanwallet = None + + def check_blockheight(self): + return self.info["blocks"] != self.rpc.getblockcount() + + @property + def is_running(self): + return self._is_running + + @property + def is_configured(self): + return self._is_configured + + @property + def info(self): + return self._info + + @property + def network_info(self): + return self._network_info + + @property + def bitcoin_core_version(self): + return self.network_info["subversion"].replace("/", "").replace("Satoshi:", "") + + @property + def bitcoin_core_version_raw(self): + return self.network_info["version"] + + @property + def chain(self): + return self.info["chain"] + + @property + def is_testnet(self): + return is_testnet(self.chain) diff --git a/src/cryptoadvance/specter/persistence.py b/src/cryptoadvance/specter/persistence.py index 9d3c589a82..9ccb44b4cf 100644 --- a/src/cryptoadvance/specter/persistence.py +++ b/src/cryptoadvance/specter/persistence.py @@ -128,6 +128,11 @@ def write_device(device, fullpath): storage_callback() +def write_node(node, fullpath): + _write_json_file(node.json, fullpath) + storage_callback() + + def delete_folder(path): _delete_folder(path) storage_callback() diff --git a/src/cryptoadvance/specter/server_endpoints/controller.py b/src/cryptoadvance/specter/server_endpoints/controller.py index d6d37ec32b..f9cdf69eae 100644 --- a/src/cryptoadvance/specter/server_endpoints/controller.py +++ b/src/cryptoadvance/specter/server_endpoints/controller.py @@ -27,6 +27,7 @@ # Setup specter endpoints from .auth import auth_endpoint from .devices import devices_endpoint +from .nodes import nodes_endpoint from .price import price_endpoint from .settings import settings_endpoint from .setup import setup_endpoint @@ -35,6 +36,7 @@ app.register_blueprint(auth_endpoint, url_prefix="/auth") app.register_blueprint(devices_endpoint, url_prefix="/devices") +app.register_blueprint(nodes_endpoint, url_prefix="/nodes") app.register_blueprint(price_endpoint, url_prefix="/price") app.register_blueprint(settings_endpoint, url_prefix="/settings") app.register_blueprint(setup_endpoint, url_prefix="/setup") @@ -74,9 +76,9 @@ def server_specter_error(se): def server_error(e): """Unspecific Exceptions get a 500 Error-Page""" # if rpc is not available - if app.specter.rpc is None or not app.specter.rpc.test_connection(): - # make sure specter knows that rpc is not there - app.specter.check() + # if app.specter.rpc is None or not app.specter.rpc.test_connection(): + # # make sure specter knows that rpc is not there + # app.specter.check() app.logger.error("Uncaught exception: %s" % e) trace = traceback.format_exc() app.logger.error(trace) diff --git a/src/cryptoadvance/specter/server_endpoints/nodes.py b/src/cryptoadvance/specter/server_endpoints/nodes.py new file mode 100644 index 0000000000..e5a5d7c228 --- /dev/null +++ b/src/cryptoadvance/specter/server_endpoints/nodes.py @@ -0,0 +1,199 @@ +import copy, random, json + +from flask import ( + Flask, + Blueprint, + render_template, + request, + redirect, + url_for, + jsonify, + flash, +) +from flask_login import login_required, current_user +from flask import current_app as app +from ..rpc import get_default_datadir +from ..node import Node + + +rand = random.randint(0, 1e32) # to force style refresh + +# Setup endpoint blueprint +nodes_endpoint = Blueprint("nodes_endpoint", __name__) + + +@nodes_endpoint.route( + "new_node/", defaults={"node_alias": None}, methods=["GET", "POST"] +) +@nodes_endpoint.route("node//", methods=["GET", "POST"]) +@login_required +def node_settings(node_alias): + err = None + if node_alias: + try: + node = app.specter.node_manager.get_by_alias(node_alias) + except: + return render_template( + "base.jinja", error="Node not found", specter=app.specter, rand=rand + ) + else: + node = Node.from_json( + { + "name": "New Node", + "autodetect": True, + "datadir": get_default_datadir(), + "user": "", + "password": "", + "port": 8332, + "host": "localhost", + "protocol": "http", + "external_node": True, + }, + app.specter.node_manager, + ) + + if not current_user.is_admin: + flash("Only an admin is allowed to access this page.", "error") + return redirect("") + # The node might have been down but is now up again + # (and the checker did not realized yet) and the user clicked "Configure Node" + if node.rpc is None: + node.update_rpc() + + err = None + test = None + if request.method == "POST": + action = request.form["action"] + + if action != "rename": + autodetect = "autodetect" in request.form + if autodetect: + datadir = request.form["datadir"] + else: + datadir = "" + user = request.form["username"] + password = request.form["password"] + port = request.form["port"] + host = request.form["host"].rstrip("/") + # protocol://host + if "://" in host: + arr = host.split("://") + protocol = arr[0] + host = arr[1] + if not node_alias: + node.name = request.form["name"] + + if action == "rename": + node_name = request.form["newtitle"] + if not node_name: + flash("Node name must not be empty", "error") + elif node_name == node.name: + pass + elif node_name in app.specter.device_manager.devices_names: + flash("Node with this name already exists", "error") + else: + node.rename(node_name) + elif action == "forget": + if not node_alias: + flash("Failed to deleted node. Node isn't saved", "error") + elif len(app.specter.node_manager.nodes) > 1: + app.specter.node_manager.delete_node(node, app.specter) + flash("Node deleted successfully") + return redirect( + url_for( + "nodes_endpoint.node_settings", + node_alias=app.specter.node.alias, + ) + ) + else: + flash( + "Failed to deleted node. Specter must have at least one node configured", + "error", + ) + elif action == "test": + # If this is failing, the test_rpc-method needs improvement + # Don't wrap this into a try/except otherwise the feedback + # of what's wrong to the user gets broken + node = Node( + node.name, + node.alias, + autodetect, + datadir, + user, + password, + port, + host, + protocol, + node.external_node, + node.fullpath, + node.manager, + ) + test = node.test_rpc() + + if "tests" in test: + # If any test has failed, we notify the user that the test has not passed + if False in list(test["tests"].values()): + flash(f"Test failed: {test['err']}", "error") + else: + flash("Test passed", "info") + elif action == "save": + if not node_alias: + if node.name in app.specter.node_manager.nodes: + flash( + "Node with this name already exits, please choose a different name.", + "error", + ) + return render_template( + "node/node_settings.jinja", + node=node, + node_alias=node_alias, + test=test, + specter=app.specter, + rand=rand, + ) + node = app.specter.node_manager.add_node( + node.name, + autodetect, + datadir, + user, + password, + port, + host, + protocol, + node.external_node, + ) + app.specter.update_active_node(node.alias) + return redirect( + url_for("nodes_endpoint.node_settings", node_alias=node.alias) + ) + + success = node.update_rpc( + autodetect=autodetect, + datadir=datadir, + user=user, + password=password, + port=port, + host=host, + protocol=protocol, + ) + if not success: + flash("Failed connecting to the node", "error") + if app.specter.active_node_alias == node.alias: + app.specter.check() + + return render_template( + "node/node_settings.jinja", + node=node, + node_alias=node_alias, + test=test, + specter=app.specter, + rand=rand, + ) + + +@nodes_endpoint.route("switch_node/", methods=["POST"]) +@login_required +def switch_node(): + node_alias = request.form["node_alias"] + app.specter.update_active_node(node_alias) + return redirect(url_for("nodes_endpoint.node_settings", node_alias=node_alias)) diff --git a/src/cryptoadvance/specter/server_endpoints/settings.py b/src/cryptoadvance/specter/server_endpoints/settings.py index 1fe0a7b1dd..6f2a67901f 100644 --- a/src/cryptoadvance/specter/server_endpoints/settings.py +++ b/src/cryptoadvance/specter/server_endpoints/settings.py @@ -123,7 +123,7 @@ def bitcoin_core(): elif action == "save": if current_user.is_admin: node_view = "external" - success = app.specter.update_rpc( + success = app.specter.node.update_rpc( user=user, password=password, port=port, @@ -195,14 +195,8 @@ def bitcoin_core(): return render_template( "settings/bitcoin_core_settings.jinja", + node=app.specter.node, test=test, - autodetect=autodetect, - datadir=datadir, - username=user, - password=password, - port=port, - host=host, - protocol=protocol, specter=app.specter, current_version=current_version, bitcoind_exists=os.path.isfile(app.specter.bitcoind_path), diff --git a/src/cryptoadvance/specter/specter.py b/src/cryptoadvance/specter/specter.py index 1e9e1be59f..6eface2b7f 100644 --- a/src/cryptoadvance/specter/specter.py +++ b/src/cryptoadvance/specter/specter.py @@ -34,61 +34,12 @@ from .specter_error import SpecterError, ExtProcTimeoutException from sys import exit from .util.setup_states import SETUP_STATES +from .node import Node +from .managers.node_manager import NodeManager logger = logging.getLogger(__name__) -def get_rpc( - conf, - old_rpc=None, - return_broken_instead_none=False, - proxy_url="socks5h://localhost:9050", - only_tor=False, -): - """ - Checks if config have changed, compares with old rpc - and returns new one if necessary - If there is no working rpc-connection, it has to return None - If return_broken_instead_none is True, it'll return even a broken connection. - """ - if "autodetect" not in conf: - conf["autodetect"] = True - rpc = None - if conf["autodetect"]: - if "port" in conf: - rpc_conf_arr = autodetect_rpc_confs( - datadir=os.path.expanduser(conf["datadir"]), port=conf["port"] - ) - else: - rpc_conf_arr = autodetect_rpc_confs( - datadir=os.path.expanduser(conf["datadir"]) - ) - if len(rpc_conf_arr) > 0: - rpc = BitcoinRPC(**rpc_conf_arr[0], proxy_url=proxy_url, only_tor=only_tor) - else: - # if autodetect is disabled and port is not defined - # we use default port 8332 - if not conf.get("port", None): - conf["port"] = 8332 - rpc = BitcoinRPC(**conf) - if return_broken_instead_none: - return rpc - # check if we have something to compare with - if old_rpc is None: - return rpc if rpc and rpc.test_connection() else None - # check if we have something detected - if rpc is None: - # check if old rpc is still valid - return old_rpc if old_rpc.test_connection() else None - # check if something has changed and return new rpc if so. - # RPC cookie will have a new password if bitcoind is restarted. - if rpc.url == old_rpc.url and rpc.password == old_rpc.password: - return old_rpc - else: - logger.info("rpc config have changed.") - return rpc - - class Specter: """A central Object mostly holding app-settings""" @@ -106,18 +57,17 @@ def __init__(self, data_folder="./data", config={}): self.data_folder = data_folder - # the rpc-object. Currently we only have one. If we have Node-Managers, we would need - # either many of them and register them with a keyword or something like that - self.rpc = None - - # wallet that is currently rescanning with utxorescan - # can be only one at a time - self.utxorescanwallet = None - self.user_manager = UserManager(self) self._config_manager = ConfigManager(self.data_folder, config) + self.node_manager = NodeManager( + proxy_url=self.proxy_url, + only_tor=self.only_tor, + active_node=self.active_node_alias, + data_folder=os.path.join(self.data_folder, "nodes"), + ) + self.torbrowser_path = os.path.join( self.data_folder, f"tor-binaries/tor{get_tor_daemon_suffix()}" ) @@ -132,7 +82,6 @@ def __init__(self, data_folder="./data", config={}): self._bitcoind = None self._tor_daemon = None - self.node_status = None self.setup_status = { "stage": "start", "bitcoind": { @@ -150,27 +99,14 @@ def __init__(self, data_folder="./data", config={}): # also loads and checks wallets for all users try: self.check(check_all=True) - rpc_conf = next( - ( - conf - for conf in detect_rpc_confs( - datadir=os.path.expanduser( - self.config["rpc"]["datadir"] - if self.config["rpc"].get("external_node", True) - else self.config["internal_node"]["datadir"] - ) - ) - if conf["port"] == 8332 - ), - None, - ) if os.path.isfile(self.torbrowser_path): self.tor_daemon.start_tor_daemon() except Exception as e: logger.error(e) - if not self.config_manager.data["rpc"].get("external_node", True): + # TODO: Move internal node logic to node object + if not self.node.external_node: try: self.bitcoind.start_bitcoind( datadir=os.path.expanduser(self.config["internal_node"]["datadir"]), @@ -190,6 +126,8 @@ def __init__(self, data_folder="./data", config={}): self.set_bitcoind_pid(self.bitcoind.bitcoind_proc.pid) except Exception as e: logger.error(e) + + ################################################################################ self.update_tor_controller() self.checker = Checker(lambda: self.check(check_all=True), desc="health") self.checker.start() @@ -229,27 +167,16 @@ def check(self, user=None, check_all=False): # check if config file have changed self.check_config() - # update rpc if something doesn't work - rpc = self.rpc - if rpc is None or not rpc.test_connection(): - rpc = get_rpc( - self.config_manager.rpc_conf, - self.rpc, - proxy_url=self.proxy_url, - only_tor=self.only_tor, - ) - - self.check_node_info() + self.node.update_rpc() # if rpc is not available # do checks more often, once in 20 seconds - if rpc is None or self.info.get("initialblockdownload", True): + if self.rpc is None or self.node.info.get("initialblockdownload", True): period = 20 else: period = 600 if hasattr(self, "checker") and self.checker.period != period: self.checker.period = period - self.rpc = rpc if not check_all: # find proper user @@ -259,61 +186,25 @@ def check(self, user=None, check_all=False): for u in self.user_manager.users: u.check() + @property + def node(self): + return self.node_manager.active_node + + @property + def rpc(self): + return self.node.rpc + + @property + def utxorescanwallet(self): + return self.node.utxorescanwallet + @property def config(self): """A convenience property simply redirecting to the config_manager""" return self.config_manager.data - def check_node_info(self): - self._is_configured = self.rpc is not None - self._is_running = False - if self._is_configured: - try: - res = [ - r["result"] - for r in self.rpc.multi( - [ - ("getblockchaininfo", None), - ("getnetworkinfo", None), - ("getmempoolinfo", None), - ("uptime", None), - ("getblockhash", 0), - ("scantxoutset", "status", []), - ] - ) - ] - self._info = res[0] - self._network_info = res[1] - self._info["mempool_info"] = res[2] - self._info["uptime"] = res[3] - try: - self.rpc.getblockfilter(res[4]) - self._info["blockfilterindex"] = True - except: - self._info["blockfilterindex"] = False - self._info["utxorescan"] = ( - res[5]["progress"] - if res[5] is not None and "progress" in res[5] - else None - ) - if self._info["utxorescan"] is None: - self.utxorescanwallet = None - self._is_running = True - except Exception as e: - self._info = {"chain": None} - self._network_info = {"subversion": "", "version": 999999} - logger.error("Exception %s while specter.check()" % e) - pass - else: - self._info = {"chain": None} - self._network_info = {"subversion": "", "version": 999999} - - if not self._is_running: - self._info["chain"] = None - def check_blockheight(self): - current_blockheight = self.rpc.getblockcount() - if self.info["blocks"] != current_blockheight: + if self.node.check_blockheight(): self.check(check_all=True) def get_user_folder_id(self, user=None): @@ -347,79 +238,7 @@ def delete_user(self, user): # mark @property def bitcoin_datadir(self): - return self.config_manager.bitcoin_datadir - - def abortrescanutxo(self): - self.rpc.scantxoutset("abort", []) - # Bitcoin Core doesn't catch up right away - # so app.specter.check() doesn't work - self._info["utxorescan"] = None - self.utxorescanwallet = None - - def test_rpc(self, **kwargs): - conf = copy.deepcopy(self.config_manager.data["rpc"]) - conf.update(kwargs) - - rpc = get_rpc( - conf, - return_broken_instead_none=True, - proxy_url=self.proxy_url, - only_tor=self.only_tor, - ) - if rpc is None: - return {"out": "", "err": "autodetect failed", "code": -1} - r = {} - r["tests"] = {"connectable": False} - r["err"] = "" - r["code"] = 0 - try: - r["tests"]["recent_version"] = ( - int(rpc.getnetworkinfo()["version"]) >= 170000 - ) - if not r["tests"]["recent_version"]: - r["err"] = "Core Node might be too old" - - r["tests"]["connectable"] = True - r["tests"]["credentials"] = True - try: - rpc.listwallets() - r["tests"]["wallets"] = True - except RpcError as rpce: - logger.error(rpce) - r["tests"]["wallets"] = False - r["err"] = "Wallets disabled" - - r["out"] = json.dumps(rpc.getblockchaininfo(), indent=4) - except ConnectionError as e: - logger.error("Caught an ConnectionError while test_rpc: %s", e) - - r["tests"]["connectable"] = False - r["err"] = "Failed to connect!" - r["code"] = -1 - except RpcError as rpce: - logger.error("Caught an RpcError while test_rpc: %s", rpce) - logger.error(rpce.status_code) - r["tests"]["connectable"] = True - r["code"] = rpc.r.status_code - if rpce.status_code == 401: - r["tests"]["credentials"] = False - r["err"] = "RPC authentication failed!" - else: - r["err"] = str(rpce.status_code) - except Exception as e: - logger.error( - "Caught an exception of type {} while test_rpc: {}".format( - type(e), str(e) - ) - ) - r["out"] = "" - if rpc.r is not None and "error" in rpc.r: - r["err"] = rpc.r["error"] - r["code"] = rpc.r.status_code - else: - r["err"] = "Failed to connect" - r["code"] = -1 - return r + return self.node.datadir # mark def _save(self): @@ -430,18 +249,11 @@ def config_fname(self): return os.path.join(self.data_folder, "config.json") # mark - def update_rpc(self, **kwargs): - need_update = self.config_manager.update_rpc(**kwargs) - if need_update: - self.rpc = get_rpc( - self.config_manager.rpc_conf, - None, - proxy_url=self.proxy_url, - only_tor=self.only_tor, - ) - self._save() - self.check(check_all=True) - return self.rpc is not None + def update_active_node(self, node_alias): + """update the current active node to use""" + self.config_manager.update_active_node(node_alias) + self.node_manager.switch_node(node_alias) + self.check() # mark def set_bitcoind_pid(self, pid): @@ -658,32 +470,36 @@ def is_configured(self): @property def info(self): - return self._info + return self.node.info @property def network_info(self): - return self._network_info + return self.node.network_info @property def bitcoin_core_version(self): - return self.network_info["subversion"].replace("/", "").replace("Satoshi:", "") + return self.node.bitcoin_core_version @property def bitcoin_core_version_raw(self): - return self.network_info["version"] + return self.node.bitcoin_core_version_raw @property def chain(self): - return self._info["chain"] + return self.node.chain @property def is_testnet(self): - return is_testnet(self.chain) + return self.node.is_testnet @property def user_config(self): return self.config if self.user.is_admin else self.user.config + @property + def active_node_alias(self): + return self.user_config.get("active_node_alias", "default") + @property def explorer(self): return self.user_config.get("explorers", {}).get(self.chain, "") diff --git a/src/cryptoadvance/specter/static/img/file_icon.svg b/src/cryptoadvance/specter/static/img/file_icon.svg deleted file mode 100644 index 88c4705e6b..0000000000 --- a/src/cryptoadvance/specter/static/img/file_icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/cryptoadvance/specter/static/img/flip-horizontal.svg b/src/cryptoadvance/specter/static/img/flip-horizontal.svg new file mode 100644 index 0000000000..609309958e --- /dev/null +++ b/src/cryptoadvance/specter/static/img/flip-horizontal.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/cryptoadvance/specter/templates/device/new_device_manual.jinja b/src/cryptoadvance/specter/templates/device/new_device_manual.jinja index 5d202b8244..8708a6338d 100644 --- a/src/cryptoadvance/specter/templates/device/new_device_manual.jinja +++ b/src/cryptoadvance/specter/templates/device/new_device_manual.jinja @@ -12,7 +12,7 @@
{% if not device %} -