From 3ee881805e8a080e27176f87743fc114bd8078c6 Mon Sep 17 00:00:00 2001 From: k9ert Date: Tue, 31 Jan 2023 11:06:29 +0100 Subject: [PATCH] Chore: Porting chore/rid_of_defaultnode to uiux-revamp (#2098) * Chore: Remove default node (#2072) * change passwordaspin auth implementation * env-var for RASPIBLITZ_SPECTER_RPC_LOGIN_BITCOIN_CONF_LOCATION * remove default * fix tests * fix the tests part2 * fix cypress tests * docstrings and other default removals * improve error_handling * do not raise exception * fixed debugging artifact * Spinning refresh icon if node is still syncing * Cypress test for sync status * remove notification of fully synced node * Set timeout for sync check to 5min --------- Co-authored-by: moneymanolis Co-authored-by: Manolis Mandrapilias <70536101+moneymanolis@users.noreply.github.com> --- cypress/integration/spec_connections.js | 13 +- src/cryptoadvance/specter/config.py | 7 + src/cryptoadvance/specter/device.py | 3 +- .../specter/managers/config_manager.py | 2 +- .../specter/managers/node_manager.py | 119 +++--- .../specter/managers/wallet_manager.py | 15 +- src/cryptoadvance/specter/node.py | 89 ++++- src/cryptoadvance/specter/rpc.py | 3 +- .../specter/server_endpoints/auth.py | 137 ++++--- .../specter/server_endpoints/nodes.py | 10 + src/cryptoadvance/specter/specter.py | 31 +- src/cryptoadvance/specter/specter_error.py | 9 +- src/cryptoadvance/specter/static/helpers.js | 2 +- src/cryptoadvance/specter/static/output.css | 30 ++ src/cryptoadvance/specter/static/style.css | 4 + .../sidebar/components/node_connection.jinja | 42 +- .../templates/includes/sidebar/sidebar.jinja | 8 - .../node/components/bitcoin_core_info.jinja | 20 +- src/cryptoadvance/specter/user.py | 1 - .../specterext/spectrum/service.py | 4 +- .../components/spectrum_node_connection.jinja | 3 + tests/conftest.py | 135 +------ tests/fix_devices_and_wallets.py | 82 +++- tests/misc_testdata/specter_device.json | 41 ++ tests/misc_testdata/trezor_device.json | 360 ++++++++++++++++++ tests/test_ep_wallet_api.py | 6 +- tests/test_managers_device.py | 1 - tests/test_managers_node.py | 45 ++- tests/test_managers_wallet.py | 34 +- tests/test_node.py | 3 +- tests/test_services.py | 15 +- tests/test_specter.py | 1 - tests/test_specter_migrator.py | 2 +- tests/test_specterext_swan_service.py | 25 +- 34 files changed, 946 insertions(+), 356 deletions(-) create mode 100644 tests/misc_testdata/specter_device.json create mode 100644 tests/misc_testdata/trezor_device.json diff --git a/cypress/integration/spec_connections.js b/cypress/integration/spec_connections.js index 531cc23e2..e8ad8181d 100644 --- a/cypress/integration/spec_connections.js +++ b/cypress/integration/spec_connections.js @@ -45,9 +45,16 @@ describe('Connecting nodes', () => { // TODO: For testing the deletion we could delete the Liquid connection here if we don't run the Liquid tests it('Select Bitcoin Core connection', () => { - cy.get('#node-switch-icon').click() - cy.contains('Bitcoin Core').click() - cy.contains('Switched to use Bitcoin Core as connection') + cy.get('#node-switch-icon').click() + cy.contains('Bitcoin Core').click() + cy.contains('Switched to use Bitcoin Core as connection') + }) + + it('Check sync status of Bitcoin Core node', () => { + cy.intercept("GET", "/nodes/sync_status/", {'fullySynced': false}); + cy.visit('/') + cy.contains('Your Bitcoin node is syncing.') + cy.get('[data-cy="unfinished-sync-indicator"]').should('be.visible') }) }) diff --git a/src/cryptoadvance/specter/config.py b/src/cryptoadvance/specter/config.py index f17cf7069..8ab5b1bbf 100644 --- a/src/cryptoadvance/specter/config.py +++ b/src/cryptoadvance/specter/config.py @@ -77,6 +77,13 @@ class BaseConfig(object): # CERT and KEY is for running self-signed-ssl-certs. Check cli_server for details CERT = os.getenv("CERT", None) KEY = os.getenv("KEY", None) + + # This will be used to search for a bitcoin.conf in order to enable the + # auth method "RPC password as pin" + RASPIBLITZ_SPECTER_RPC_LOGIN_BITCOIN_CONF_LOCATION = os.getenv( + "RASPIBLITZ_SPECTER_RPC_LOGIN_BITCOIN_CONF_LOCATION", "/mnt/hdd/bitcoin" + ) + # This will get passed to initialize the specter-object DEFAULT_SPECTER_CONFIG = {} diff --git a/src/cryptoadvance/specter/device.py b/src/cryptoadvance/specter/device.py index de6f2b828..b602c338c 100644 --- a/src/cryptoadvance/specter/device.py +++ b/src/cryptoadvance/specter/device.py @@ -3,6 +3,7 @@ from typing import Type from cryptoadvance.specter.util.reflection import get_subclasses_for_clazz from .key import Key +from typing import List from .persistence import read_json_file, write_json_file import logging from .helpers import is_testnet, is_liquid @@ -44,7 +45,7 @@ class Device: """ self.name = name self.alias = alias - self.keys = keys + self.keys: List[Key] = keys self.fullpath = fullpath self.blinding_key = blinding_key self.manager = manager diff --git a/src/cryptoadvance/specter/managers/config_manager.py b/src/cryptoadvance/specter/managers/config_manager.py index 29dbfbf65..49ff45f2c 100644 --- a/src/cryptoadvance/specter/managers/config_manager.py +++ b/src/cryptoadvance/specter/managers/config_manager.py @@ -46,7 +46,7 @@ class ConfigManager(GenericDataManager): "asset_labels": { "liquidv1": {}, }, - "active_node_alias": "default", + "active_node_alias": None, "proxy_url": "socks5h://localhost:9050", # Tor proxy URL "only_tor": False, "tor_control_port": "", diff --git a/src/cryptoadvance/specter/managers/node_manager.py b/src/cryptoadvance/specter/managers/node_manager.py index 0a5545a2c..fc302ba4f 100644 --- a/src/cryptoadvance/specter/managers/node_manager.py +++ b/src/cryptoadvance/specter/managers/node_manager.py @@ -8,7 +8,7 @@ from ..rpc import get_default_datadir, RPC_PORTS from ..specter_error import SpecterError, SpecterInternalException from ..persistence import PersistentObject, write_node, delete_file from ..helpers import alias, calc_fullpath, load_jsons -from ..node import Node +from ..node import Node, NonExistingNode from ..internal_node import InternalNode from ..services import callbacks from ..managers.service_manager import ServiceManager @@ -18,14 +18,11 @@ logger = logging.getLogger(__name__) class NodeManager: - # chain is required to manage wallets when bitcoind is not running - DEFAULT_ALIAS = "default" - def __init__( self, proxy_url="socks5h://localhost:9050", only_tor=False, - active_node="default", + active_node=None, bitcoind_path="", internal_bitcoind_version="", data_folder="", @@ -56,6 +53,7 @@ class NodeManager: if not os.path.isdir(data_folder): os.mkdir(data_folder) nodes_files = load_jsons(self.data_folder, key="alias") + logger.debug(nodes_files) for node_alias in nodes_files: try: valid_node = True @@ -101,49 +99,15 @@ class NodeManager: port=7041, host="localhost", protocol="http", - default_alias=self.DEFAULT_ALIAS, ) - logger.debug( - "Creating an external BTC node with the initial configuration." - ) - self.add_external_node( - node_type="BTC", - name="", - autodetect=False, - datadir=get_default_datadir(), - user="", - password="", - port=8332, - host="localhost", - protocol="http", - default_alias=self.DEFAULT_ALIAS, - ) - - # Make sure we always have the default node - # (needed for the rpc-as-pin-authentication used on Raspiblitz) - has_default_node = False - for node in self.nodes.values(): - if node.alias == self.DEFAULT_ALIAS: - has_default_node = True - # Recreate the default node if it doesn't exist anymore - if not has_default_node: - logger.debug("Recreating the default node.") - self.add_external_node( - node_type="BTC", - name="", - autodetect=False, - datadir=get_default_datadir(), - user="", - password="", - port=8332, - host="localhost", - protocol="http", - default_alias=self.DEFAULT_ALIAS, - ) @property def active_node(self) -> Node: - return self.get_by_alias(self._active_node) + """returns the current active node or a NonExistingNode + if no node is active, currently. + """ + active_node = self.get_by_alias(self._active_node) + return active_node if active_node else NonExistingNode() @property def nodes_names(self) -> list: @@ -155,26 +119,37 @@ class NodeManager: return [node for node in self.nodes.values() if node.chain == chain] def switch_node(self, node_alias: str): - # This will throw an error if the node doesn't exist + """This will throw an SpecterError if the node doesn't exist. + It won't persist anything! Use specter.update_active_node to persist! + """ + new_node = self.get_by_alias(node_alias) + if not new_node: + raise SpecterError(f"Node alias {node_alias} does not exist!") logger.debug(f"Switching from {self._active_node} to {node_alias}.") - self._active_node = self.get_by_alias(node_alias).alias - - def default_node(self) -> Node: - return self.get_by_alias(self.DEFAULT_ALIAS) + self._active_node = node_alias def get_by_alias(self, alias: str) -> Node: + """Returns a Node instance for the given alias. + None if a node with that alias doesn't exist + """ for node in self.nodes.values(): if node.alias == alias: return node - raise SpecterError("Node alias %s does not exist!" % alias) + return None def get_by_name(self, name: str) -> Node: + """Returns a Node instance for the given alias. + raises an SpecterError if it doesn't exist + """ for node in self.nodes.values(): if node.name == name: return node raise SpecterError("Node name %s does not exist!" % name) def get_name_from_alias(self, alias: str) -> str: + """Returns the name for a specific node alias + raises an SpecterError if it doesn't exist + """ for node in self.nodes.values(): if node.alias == alias: return node.name @@ -200,26 +175,25 @@ class NodeManager: def add_external_node( self, - node_type, - name, - autodetect, + node_type: str, + name: str, + autodetect: bool, datadir, - user, - password, - port, - host, - protocol, - default_alias=None, + user: str, + password: str, + port: str, + host: str, + protocol: str, ): - """Adding a node. Params: - :param node_type: Either BTC or ELM, used to distinguish between Bitcoin and Liquid nodes. + """Adding a node and saves it to disk as well. Params: + * node_type: only valid for autodetect. Either BTC or ELM + * name: A nice name for this node. The alias will get calculated out of that + * autodetect (boolean): whether this node should get autodetected + * datadir: questionable! Why is that here needed?! This should only be used for an external node. Use add_internal_node for internal node and if you have defined your own node type, use save_node directly to save the node (and create it yourself) """ - if not default_alias: - node_alias = alias(name) - else: - node_alias = default_alias + node_alias = alias(name) fullpath = os.path.join(self.data_folder, "%s.json" % node_alias) i = 2 while os.path.isfile(fullpath): @@ -247,6 +221,8 @@ class NodeManager: return node def save_node(self, node): + """writes the node to disk. Will also apply a fullpath based on the datadir of the + NodeManager if the node doesn't have one.""" if not hasattr(node, "fullpath"): node.fullpath = calc_fullpath(self.data_folder, node.alias) write_node(node, node.fullpath) @@ -254,20 +230,16 @@ class NodeManager: def add_internal_node( self, - name, + name: str, network="main", - port=None, - default_alias=None, + port: str = None, datadir=None, ): """Adding an internal node. Params: This should only be used for internal nodes. Use add__External_node for external nodes and if you have defined your own node-type, use save_node directly. to save the node (and create it yourself) """ - if not default_alias: - node_alias = alias(name) - else: - node_alias = default_alias + node_alias = alias(name) fullpath = os.path.join(self.data_folder, "%s.json" % node_alias) i = 2 while os.path.isfile(fullpath): @@ -298,6 +270,7 @@ class NodeManager: return node def delete_node(self, node, specter): + """Deletes the node. Also from the disk.""" logger.info("Deleting {}".format(node.alias)) try: # Delete from wallet manager @@ -306,7 +279,7 @@ class NodeManager: delete_file(node.fullpath) delete_file(node.fullpath + ".bkp") # Update the active node - if self._active_node == node.alias: + if self._active_node == node.alias and len(self.nodes) > 0: specter.update_active_node( next(iter(self.nodes.values())).alias ) # This switches to the first node in the node list, which is usually the default node diff --git a/src/cryptoadvance/specter/managers/wallet_manager.py b/src/cryptoadvance/specter/managers/wallet_manager.py index 8c27f531d..301ad8404 100644 --- a/src/cryptoadvance/specter/managers/wallet_manager.py +++ b/src/cryptoadvance/specter/managers/wallet_manager.py @@ -13,7 +13,7 @@ from cryptoadvance.specter.key import Key from ..helpers import add_dicts, alias, is_liquid, load_jsons from ..liquid.wallet import LWallet from ..persistence import delete_folder -from ..rpc import RpcError, get_default_datadir +from ..rpc import RpcError, get_default_datadir, BrokenCoreConnectionException from ..specter_error import SpecterError, SpecterInternalException, handle_exception from ..util.flask import FlaskThread from ..wallet import ( # TODO: `purposes` unused here, but other files rely on this import @@ -31,7 +31,6 @@ class WalletManager: # chain is required to manage wallets when bitcoind is not running def __init__( self, - bitcoin_core_version_raw, data_folder, rpc, chain, @@ -50,8 +49,6 @@ class WalletManager: # key is the name of the wallet, value is the actual instance self.wallets = {} - # A way to communicate failed wallets to the outside - self.bitcoin_core_version_raw = bitcoin_core_version_raw self.allow_threading_for_testing = allow_threading_for_testing # define different wallet classes for liquid and bitcoin self.WalletClass = LWallet if is_liquid(chain) else Wallet @@ -333,6 +330,16 @@ class WalletManager: self._wallets[self.chain] = {} self._wallets[self.chain] = value + @property + def bitcoin_core_version_raw(self): + try: + bitcoin_core_version_raw = self.rpc.getnetworkinfo()["version"] + return bitcoin_core_version_raw or 200000 + except BrokenCoreConnectionException: + # In good faith and in order to keep the tests running, we assume + # a reasonable core version + return 200000 + def create_wallet(self, name, sigs_required, key_type, keys, devices, **kwargs): try: walletsindir = [ diff --git a/src/cryptoadvance/specter/node.py b/src/cryptoadvance/specter/node.py index bef8c9591..913068fda 100644 --- a/src/cryptoadvance/specter/node.py +++ b/src/cryptoadvance/specter/node.py @@ -28,7 +28,94 @@ from .device import Device logger = logging.getLogger(__name__) -class AbstractNode(PersistentObject): +class NonExistingNode(PersistentObject): + """A kind of Null-object as it represents a non-existing Node. It's deriving from PersistentObject but is not meant to be + persisted. Instead, it's be created on the fly from the NodeManager if it doesn't have a reasonable node available. + It also works as some kind of minimal implementation so that specter doesn't fail gracefully. + """ + + @property + def info(self): + return {} + + @property + def network_info(self): + return {} + + @property + def uptime(self): + return -1 + + @property + def chain(self): + return None + + @property + def bitcoin_core_version_raw(self): + return 9999999 + + def update_rpc(self): + pass + + @property + def is_running(self): + return False + + @property + def rpc(self): + return None + + def check_blockheight(self): + """check_blockheight is a method which is probably deprecated. + It should return True if there are new blocks available since check_info has been called + (which updates the cached _info[] dict) + """ + raise NotImplemented( + "A Node Implementation need to implement the check_blockheight method" + ) + + def is_device_supported(self, device_class_or_device_instance): + """Lets the node deactivate specific devices. The parameter could be a device or a device_type + You have to check yourself if overriding this method. + e.g. + if device_instance_or_device_class.__class__ == type: + device_class = device_instance_or_device_class + else: + device_class = device_instance_or_device_class.__class__ + # example: + # if BitcoinCore == device_class: + # return False + return True + """ + return True + + def node_info_template(self): + """This should return the path to a Info template as string""" + return "node/components/bitcoin_core_info.jinja" + + def node_logo_template(self): + """This should return the path to a Logo template as string + The template should contain the logo independent from the + status of the node. It's used in the node-selector + """ + return "includes/sidebar/components/node_logo.jinja" + + def node_connection_template(self): + """This should return the path to a connection template as string""" + return "includes/sidebar/components/node_connection.jinja" + + def delete_wallet_file(self, wallet) -> bool: + """Deleting the wallet file located on the node. This only works if the node is on the same machine as Specter. + Returns True if the wallet file could be deleted, otherwise returns False. + + In the case of an Abtract Node, we consider that method as an edge-case anyway and we just return False here. + That is the normal usage if you don't have access to the internals of your Bitcoin Core. + Overwrite as necessary. + """ + return False + + +class AbstractNode(NonExistingNode): """This is a Node class worth deriving from. It tries to define as many attributes as possible which are needed but probably in a very inefficient way, e.g. without any caching. Feel free to improve that in subclasses and you might get inspired by existing sublasses """ diff --git a/src/cryptoadvance/specter/rpc.py b/src/cryptoadvance/specter/rpc.py index 0fe4c0b28..f749c8b2a 100644 --- a/src/cryptoadvance/specter/rpc.py +++ b/src/cryptoadvance/specter/rpc.py @@ -501,7 +501,8 @@ class BitcoinRPC: r = self.multi([(method, *args)], **kwargs)[0] if r["error"] is not None: raise RpcError( - f"Request error for method {method}: {r['error']['message']}", r + f"Request error for method {method}{args}: {r['error']['message']}", + r, ) return r["result"] diff --git a/src/cryptoadvance/specter/server_endpoints/auth.py b/src/cryptoadvance/specter/server_endpoints/auth.py index 8f59311a2..40b02ddd1 100644 --- a/src/cryptoadvance/specter/server_endpoints/auth.py +++ b/src/cryptoadvance/specter/server_endpoints/auth.py @@ -7,10 +7,13 @@ from flask import jsonify, redirect, render_template, request, url_for from flask_babel import lazy_gettext as _ from flask_login import current_user, login_required, logout_user +from cryptoadvance.specter.specter import Specter + from ..helpers import alias from ..server_endpoints import flash from ..services import ExtensionException from ..user import User, hash_password, verify_password +from ..rpc import BitcoinRPC, _detect_rpc_confs_via_datadir rand = random.randint(0, 1e32) # to force style refresh last_sensitive_request = 0 # to rate limit sensitive requests @@ -28,60 +31,13 @@ def login(): auth = app.specter.config["auth"] if auth["method"] == "none": - app.login("admin") - app.logger.info("AUDIT: Successful Login no credentials") - return redirect_login(request) - - if auth["method"] == "rpcpasswordaspin": - # TODO: check the password via RPC-call - if ( - app.specter.default_node.rpc is None - or not app.specter.default_node.rpc.test_connection() - ): - if app.specter.default_node.password == request.form["password"]: - app.login("admin", request.form["password"]) - app.logger.info( - "AUDIT: Successfull Login via RPC-credentials (node disconnected)" - ) - return redirect_login(request) - - flash( - _( - "We could not check your password, maybe Bitcoin Core is not running or not configured?" - ), - "error", - ) - app.logger.info("AUDIT: Failed to check password") - return ( - render_template( - "login.jinja", - specter=app.specter, - data={"controller": "controller.login"}, - ), - 401, - ) - rpc = app.specter.default_node.rpc.clone() - rpc.password = request.form["password"] - if rpc.test_connection(): - app.login("admin", request.form["password"]) - app.logger.info("AUDIT: Successfull Login via RPC-credentials") - return redirect_login(request) - + return login_method_none() + elif auth["method"] == "rpcpasswordaspin": + return login_method_rpcpasswordaspin() elif auth["method"] == "passwordonly": - password = request.form["password"] - if verify_password(app.specter.user_manager.admin.password_hash, password): - app.login("admin", request.form["password"]) - return redirect_login(request) - + return login_method_passwordonly() elif auth["method"] == "usernamepassword": - # TODO: This way both "User" and "user" will pass as usernames, should there be strict check on that here? Or should we keep it like this? - username = request.form["username"] - password = request.form["password"] - user = app.specter.user_manager.get_user_by_username(username) - if user: - if verify_password(user.password_hash, password): - app.login(user.id, request.form["password"]) - return redirect_login(request) + return login_method_usernamepassword() # Either invalid method or incorrect credentials flash(_("Invalid username or password"), "error") @@ -103,6 +59,83 @@ def login(): ) +def deny_login(comment=""): + flash("Invalid user or password!") + app.logger.info("AUDIT: Invalid password login attempt") + return redirect("login") + + +def login_method_none(): + app.login("admin") + app.logger.info("AUDIT: Successful Login no credentials") + return redirect_login(request) + + +def login_method_rpcpasswordaspin(): + # This authentiaction method has been especially created for Raspiblitz + # We assume here, that no preconfigured node-connection is available + # otherwise, which configured node should we use? + # We want to get rid of the default node, so we can't use that + # Instead we use a default location where we assume the bitcoin.conf + # This can be overridden via ENV_var. + + specter: Specter = app.specter + if specter.node and specter.node._get_rpc().test_connection(): + rpc = specter.node._get_rpc().clone() + else: + confs = _detect_rpc_confs_via_datadir( + None, + datadir=app.config["RASPIBLITZ_SPECTER_RPC_LOGIN_BITCOIN_CONF_LOCATION"], + ) + if len(confs) == 0: + flash( + "No RPC connection to Bitcoin Core found. Cannot Log you in.", "error" + ) + return redirect(url_for("login")) + conf = confs[0] + rpc = BitcoinRPC(**conf) + + # A bit redundant as this has been checked before but let's be sure + if not rpc.test_connection(): + flash( + "It seems that there is no working RPC connection to Bitcoin Core. Cannot Log you in." + ) + return redirect(url_for("login")) + orig_password = rpc.password + rpc.password = request.form["password"] + if rpc.password == request.form["password"] and rpc.test_connection(): + app.login("admin", request.form["password"]) + app.logger.info("AUDIT: Successfull Login via RPC-credentials") + return redirect_login(request) + if orig_password == request.form["password"]: + app.login("admin", request.form["password"]) + app.logger.info( + f"AUDIT: Successfull Login via RPC-credentials (node not reachable) {rpc.password}" + ) + return redirect_login(request) + return deny_login(comment="Pin") + + +def login_method_passwordonly(): + password = request.form["password"] + if verify_password(app.specter.user_manager.admin.password_hash, password): + app.login("admin", request.form["password"]) + return redirect_login(request) + return deny_login(comment="passwordonly") + + +def login_method_usernamepassword(): + # TODO: This way both "User" and "user" will pass as usernames, should there be strict check on that here? Or should we keep it like this? + username = request.form["username"] + password = request.form["password"] + user = app.specter.user_manager.get_user_by_username(username) + if user: + if verify_password(user.password_hash, password): + app.login(user.id, request.form["password"]) + return redirect_login(request) + return deny_login(comment="usernamepassword") + + @auth_endpoint.route("/register", methods=["GET", "POST"]) def register(): """register""" diff --git a/src/cryptoadvance/specter/server_endpoints/nodes.py b/src/cryptoadvance/specter/server_endpoints/nodes.py index c3244a7b1..adf65c970 100644 --- a/src/cryptoadvance/specter/server_endpoints/nodes.py +++ b/src/cryptoadvance/specter/server_endpoints/nodes.py @@ -553,3 +553,13 @@ def rename_node(): node.rename(new_name) response = {"nameChanged": True, "error": None} return jsonify(response) + + +@nodes_endpoint.route("sync_status/", methods=["GET"]) +@login_required +def check_sync_status(): + if app.specter.info.get("initialblockdownload") == True: + response = {"fullySynced": False} + else: + response = {"fullySynced": True} + return jsonify(response) diff --git a/src/cryptoadvance/specter/specter.py b/src/cryptoadvance/specter/specter.py index a615b6d74..e907a9fd4 100644 --- a/src/cryptoadvance/specter/specter.py +++ b/src/cryptoadvance/specter/specter.py @@ -127,10 +127,8 @@ class Specter: ) except SpecterError as e: if str(e).endswith("does not exist!"): - logger.warning( - f"Current Node doesn't exist. Switching over to node {self.node_manager.DEFAULT_ALIAS}." - ) - self.update_active_node(self.node_manager.DEFAULT_ALIAS) + if len(self.node_manager.nodes) > 0: + self.update_active_node(next(iter(self.nodes.values())).alias) else: raise e @@ -237,13 +235,7 @@ class Specter: @property def node(self) -> Node: - try: - return self.node_manager.active_node - except SpecterError as e: - logger.error("SpecterError while accessing active_node") - logger.exception(e) - self.update_active_node(list(self.node_manager.nodes.values())[0].alias) - return self.node_manager.active_node + return self.node_manager.active_node @property def default_node(self): @@ -565,7 +557,7 @@ class Specter: @property def active_node_alias(self): - return self.user_config.get("active_node_alias", "default") + return self.user_config.get("active_node_alias", None) @property def explorer(self): @@ -745,7 +737,7 @@ class Specter: old_internal_rpc = self.config.get("internal_node", None) if old_internal_rpc and os.path.isfile(self.bitcoind_path): internal_node = InternalNode( - "Specter Bitcoin", + "Specter Bitcoin internal", "specter_bitcoin", old_internal_rpc.get("autodetect", False), old_internal_rpc.get("datadir", get_default_datadir()), @@ -755,7 +747,7 @@ class Specter: old_internal_rpc.get("host", "localhost"), old_internal_rpc.get("protocol", "http"), os.path.join( - os.path.join(self.data_folder, "nodes"), "specter_bitcoin.json" + os.path.join(self.data_folder, "nodes"), "bitcoin_core.json" ), self, self.bitcoind_path, @@ -776,7 +768,7 @@ class Specter: if old_rpc: node = Node( "Bitcoin Core", - "default", + "bitcoin_core", old_rpc.get("autodetect", True), old_rpc.get("datadir", get_default_datadir()), old_rpc.get("user", ""), @@ -784,16 +776,21 @@ class Specter: old_rpc.get("port", None), old_rpc.get("host", "localhost"), old_rpc.get("protocol", "http"), - os.path.join(os.path.join(self.data_folder, "nodes"), "default.json"), + os.path.join( + os.path.join(self.data_folder, "nodes"), "bitcoin_core.json" + ), "BTC", self, ) logger.info(f"persisting {node} in migrate_old_node_format") write_node( node, - os.path.join(os.path.join(self.data_folder, "nodes"), "default.json"), + os.path.join( + os.path.join(self.data_folder, "nodes"), "bitcoin_core.json" + ), ) del self.config["rpc"] + self.config_manager.update_active_node("bitcoin_core") self._save() diff --git a/src/cryptoadvance/specter/specter_error.py b/src/cryptoadvance/specter/specter_error.py index 187a53481..4f84515e3 100644 --- a/src/cryptoadvance/specter/specter_error.py +++ b/src/cryptoadvance/specter/specter_error.py @@ -48,8 +48,13 @@ class ExtProcTimeoutException(SpecterInternalException): def handle_exception(exception, user=None): """prints the exception and most important the stacktrace""" - if app.config["SPECTER_CONFIGURATION_CLASS_FULLNAME"].endswith("DevelopmentConfig"): - raise exception + try: + if app.config["SPECTER_CONFIGURATION_CLASS_FULLNAME"].endswith( + "DevelopmentConfig" + ): + raise exception + except RuntimeError: # Application context might be missing + pass logger.error("Unexpected error:") logger.error( "----START-TRACEBACK-----------------------------------------------------------------" diff --git a/src/cryptoadvance/specter/static/helpers.js b/src/cryptoadvance/specter/static/helpers.js index 19692f7d7..2ab5706a2 100644 --- a/src/cryptoadvance/specter/static/helpers.js +++ b/src/cryptoadvance/specter/static/helpers.js @@ -208,7 +208,7 @@ async function send_request(url, method_str, csrf_token, formData) { return jsonResponse } catch(error) { - showError(`Failed to fetch transactions list: ${error}`) + showError(`Error occured durch fetch call: ${error}`) return { 'error': error} } } \ No newline at end of file diff --git a/src/cryptoadvance/specter/static/output.css b/src/cryptoadvance/specter/static/output.css index 146ad21e6..31c848123 100644 --- a/src/cryptoadvance/specter/static/output.css +++ b/src/cryptoadvance/specter/static/output.css @@ -863,6 +863,10 @@ select { background-color: rgb(27 28 33 / var(--tw-bg-opacity)); } +.animate-spin-slow { + animation: spin 1.5s linear infinite; +} + *, ::before, ::after { --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; @@ -1114,6 +1118,10 @@ input[type="number"]::-webkit-outer-spin-button, margin: auto; } +.m-3 { + margin: 0.75rem; +} + .m-4 { margin: 1rem; } @@ -1740,6 +1748,11 @@ input[type="number"]::-webkit-outer-spin-button, border-radius: 0.375rem; } +.rounded-l-md { + border-top-left-radius: 0.375rem; + border-bottom-left-radius: 0.375rem; +} + .rounded-t-xl { border-top-left-radius: 0.75rem; border-top-right-radius: 0.75rem; @@ -1896,6 +1909,10 @@ input[type="number"]::-webkit-outer-spin-button, padding: 1.25rem; } +.p-2 { + padding: 0.5rem; +} + .px-\[calc\(\(100\%-700px\)\/2\)\] { padding-left: calc((100% - 700px) / 2); padding-right: calc((100% - 700px) / 2); @@ -2023,6 +2040,11 @@ input[type="number"]::-webkit-outer-spin-button, line-height: 1.25rem; } +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + .font-medium { font-weight: 500; } @@ -2171,10 +2193,18 @@ input[type="number"]::-webkit-outer-spin-button, transition-duration: 200ms; } +.duration-1000 { + transition-duration: 1000ms; +} + .ease-out { transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + * { font-family: "Social"; font-style: normal; diff --git a/src/cryptoadvance/specter/static/style.css b/src/cryptoadvance/specter/static/style.css index d0b2d5a25..65a03844e 100644 --- a/src/cryptoadvance/specter/static/style.css +++ b/src/cryptoadvance/specter/static/style.css @@ -185,6 +185,10 @@ .selection-button { @apply rounded-lg w-full h-10 bg-dark-800 border-2 border-dark-700 text-center block text-lg py-1 hover:bg-dark-700; } + + .animate-spin-slow { + animation: spin 1.5s linear infinite; + } } @layer components { diff --git a/src/cryptoadvance/specter/templates/includes/sidebar/components/node_connection.jinja b/src/cryptoadvance/specter/templates/includes/sidebar/components/node_connection.jinja index 296f338d1..09c322148 100644 --- a/src/cryptoadvance/specter/templates/includes/sidebar/components/node_connection.jinja +++ b/src/cryptoadvance/specter/templates/includes/sidebar/components/node_connection.jinja @@ -1,8 +1,13 @@ {% set node = specter.node %} - {% if node.is_running %} -
- +
+ {% if specter.info.get("initialblockdownload") == True %} + + + {% else %} + + + {% endif %}
@@ -20,4 +25,35 @@ document.addEventListener('connectionRenamed', (e) => { sidebarConnectionNameElement.innerText = e.detail.newName }) + + document.addEventListener("DOMContentLoaded", () => { + {% if node.is_running %} + checkSyncStatus() + {% endif %} + }) + + let count = 0 + async function checkSyncStatus() { + let url = `{{ url_for('nodes_endpoint.check_sync_status') }}`; + const response = await send_request(url, 'GET', "{{ csrf_token() }}"); + if (response.fullySynced === true) { + console.log('IBD is finished, Node is fully synced.') + document.getElementById('unfinished-ibd-indicator').classList.remove('animate-spin') + document.getElementById("unfinished-ibd-indicator").classList.add('hidden') + document.getElementById('connected-icon').classList.remove('hidden') + } + else { + console.log('IBD is not finished yet.') + document.getElementById('unfinished-ibd-indicator').classList.remove('hidden') + document.getElementById('unfinished-ibd-indicator').classList.add('animate-spin') + document.getElementById('connected-icon').classList.add('hidden') + console.log(count) + // Show message only once + if (count === 0 ) { + showNotification(`{{ _("Your Bitcoin node is syncing. Check the progress by clicking on the name of the node.") }}`) + } + count++ + setTimeout(checkSyncStatus, 5*60000); + } + } diff --git a/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja b/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja index 22ad52fd1..a26197eb0 100644 --- a/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja +++ b/src/cryptoadvance/specter/templates/includes/sidebar/sidebar.jinja @@ -4,14 +4,6 @@