Feature: Node manager (#1146)

* Move managers to managers folder

* Add node manager

* Fix internal node setup

* Remove old bitcoin core settings page

* Migrate old specter versions

* Fix unit tests

* Fix cypress tests

* Fix cypress tests

* Fixes

* Fixes

* Fixes

* Fix

* Fix

* Fix Cypress

* some documentation

* sorting imports and logger.error for testing tor

* get logs from tor in doubt

Co-authored-by: Kim Neunert <k9ert@gmx.de>
This commit is contained in:
benk10 2021-05-11 17:44:25 +03:00 committed by GitHub
parent d858d07f37
commit 7ac1f2bd7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1673 additions and 3054 deletions

View file

@ -7,9 +7,10 @@ describe('Completely empty specter-home', () => {
cy.viewport(1200,660)
cy.visit('/')
cy.contains('Welcome to Specter Desktop')
cy.get('#node-switch-icon').click()
cy.get('[href="/nodes/node/default/"]').first().click()
cy.contains('Bitcoin Core')
cy.get('[href="/settings/"] > img').click()
cy.contains('Bitcoin JSON-RPC')
cy.get('[href="/settings/general"]').click()
cy.contains('Backup and Restore')
cy.get('[href="/settings/auth"]').click()
cy.contains('Authentication:')
@ -29,7 +30,8 @@ describe('Completely empty specter-home', () => {
it('Configures the node in Specter', () => {
cy.viewport(1200,660)
cy.visit('/')
cy.get('[href="/settings/"] > img').click()
cy.get('#node-switch-icon').click()
cy.get('[href="/nodes/node/default/"]').first().click()
cy.get('#datadir-container').then(($datadir) => {
cy.log($datadir)
if (!Cypress.dom.isVisible($datadir)) {

View file

@ -10,7 +10,7 @@ describe('Setup Tor and test connection', () => {
cy.wait(60000)
cy.get('#tor-status-text').contains('Status: Running')
cy.get('[value="test_tor"]').click()
cy.get('[value="test_tor"]').click({ timeout: 60000 })
cy.contains('Tor requests test completed successfully!')
cy.get('[value="stoptor"]').click()
cy.get('#tor-status-text').contains('Status: Down')

View file

@ -17,14 +17,12 @@ describe('Setup wizard', () => {
cy.contains('Configure your node')
cy.get('#quicksync-switch').click()
cy.get('#setup-bitcoind-dir-button').click()
cy.wait(3000)
cy.contains('Starting up Bitcoin Core...')
cy.wait(60000)
cy.contains('Setup Completed Successfully!')
cy.contains('Setup Completed Successfully!', { timeout: 60000 })
cy.get('#finish-setup-btn').click()
cy.contains('Connect Specter with Bitcoin Core node.')
cy.get('[href="/settings/"]').click()
cy.get('#active-node').click()
cy.get('#active-node-settings-btn').click()
cy.contains('Built in Bitcoin Node Status: Running')
cy.get('[value="stopbitcoind"]').click()
cy.contains('Built in Bitcoin Node Status: Down')
@ -33,10 +31,6 @@ describe('Setup wizard', () => {
cy.contains('Built in Bitcoin Node Status: Running')
cy.get('[name="remove_datadir"]').click()
cy.get('[value="uninstall_bitcoind"]').click()
cy.contains('Specter can help you get started with your own Bitcoin Core node by setting it all up for you.')
cy.get('#external_node_view_btn').click()
cy.get('[value="useexternal"]').click()
cy.visit('/settings/tor')
cy.get('[value="starttor"]').click()
@ -45,7 +39,7 @@ describe('Setup wizard', () => {
cy.get('#tor-status-text').contains('Status: Down')
cy.get('[value="starttor"]').click()
cy.get('#tor-status-text').contains('Status: Running')
cy.get('[value="test_tor"]').click()
cy.get('[value="test_tor"]').click({ timeout: 60000 })
cy.contains('Tor requests test completed successfully!')
cy.get('[value="uninstalltor"]').click()
cy.get('#setup-tor-button').click()

View file

@ -28,6 +28,7 @@ module.exports = (on, config) => {
var rimraf = require("rimraf");
rimraf.sync(specter_home);
fs.mkdirSync(specter_home);
fs.mkdirSync(specter_home+"/nodes");
fs.mkdirSync(specter_home+"/devices");
fs.mkdirSync(specter_home+"/wallets");
return null

2074
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,148 @@
import json
import logging
import os
from .helpers import is_testnet
from .specter_error import SpecterError, ExtProcTimeoutException
from .rpc import (
BitcoinRPC,
RpcError,
autodetect_rpc_confs,
detect_rpc_confs,
get_default_datadir,
)
from .bitcoind import BitcoindPlainController
from .persistence import write_node
from .node import Node
logger = logging.getLogger(__name__)
class InternalNode(Node):
"""A Node but other than Node, this one is managed by Specter.
So it has start and stop methods and one called is_bitcoind_running
"""
def __init__(
self,
name,
alias,
autodetect,
datadir,
user,
password,
port,
host,
protocol,
fullpath,
manager,
bitcoind_path,
bitcoind_network,
version,
):
super().__init__(
name,
alias,
autodetect,
datadir,
user,
password,
port,
host,
protocol,
False,
fullpath,
manager,
)
self.bitcoind_path = bitcoind_path
self.bitcoind_network = bitcoind_network
self._bitcoind = None
self.bitcoin_pid = False
self.version = version
@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)
bitcoind_path = node_dict.get("bitcoind_path", "")
bitcoind_network = node_dict.get("bitcoind_network", "mainnet")
version = node_dict.get("version", "")
return cls(
name,
alias,
autodetect,
datadir,
user,
password,
port,
host,
protocol,
fullpath,
manager,
bitcoind_path,
bitcoind_network,
version,
)
@property
def json(self):
node_json = super().json
node_json["bitcoind_path"] = self.bitcoind_path
node_json["bitcoind_network"] = self.bitcoind_network
node_json["version"] = self.version
return node_json
def start(self, timeout=15):
try:
self.bitcoind.start_bitcoind(
datadir=os.path.expanduser(self.datadir),
timeout=timeout, # At the initial startup, we don't wait on bitcoind
)
except ExtProcTimeoutException as e:
logger.error(e)
e.check_logfile(os.path.join(self.datadir, "debug.log"))
logger.error(e.get_logger_friendly())
except SpecterError as e:
logger.error(e)
# Likely files of bitcoind were not found. Maybe deleted by the user?
finally:
try:
self.bitcoin_pid = self.bitcoind.bitcoind_proc.pid
except Exception as e:
logger.error(e)
return self.update_rpc()
def stop(self):
if self._bitcoind:
self._bitcoind.stop_bitcoind()
self.bitcoin_pid = False
@property
def bitcoind(self):
if os.path.isfile(self.bitcoind_path):
if not self._bitcoind:
self._bitcoind = BitcoindPlainController(
bitcoind_path=self.bitcoind_path,
rpcport=8332,
network="mainnet",
rpcuser=self.user,
rpcpassword=self.password,
)
return self._bitcoind
raise SpecterError(
"Bitcoin Core files missing. Make sure Bitcoin Core is installed within Specter"
)
def is_bitcoind_running(self):
return self._bitcoind and self._bitcoind.check_existing()

View file

@ -9,7 +9,6 @@ from urllib.parse import urlparse
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 @@ class ConfigManager(GenericDataManager):
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 @@ class ConfigManager(GenericDataManager):
"regtest": "CUSTOM",
"signet": "CUSTOM",
},
"active_node_alias": "default",
"proxy_url": "socks5h://localhost:9050", # Tor proxy URL
"only_tor": False,
"tor_control_port": "",
@ -80,31 +61,11 @@ class ConfigManager(GenericDataManager):
"validate_merkle_proofs": False,
"fee_estimator": "mempool",
"fee_estimator_custom_url": "",
# TODO: remove
"bitcoind": False,
"bitcoind_internal_version": "",
}
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,26 +93,9 @@ class ConfigManager(GenericDataManager):
# 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 set_bitcoind_pid(self, pid):
"""set the control pid of the bitcoind daemon"""
if self.data.get("bitcoind", False) != 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
def update_active_node(self, node_alias):
"""set the current active node to use"""
self.data["active_node_alias"] = node_alias
self._save()
def update_auth(self, method, rate_limit, registration_link_timeout):

View file

@ -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__)

View file

@ -0,0 +1,192 @@
import os
import logging
import secrets
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
from ..internal_node import InternalNode
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",
bitcoind_path="",
internal_bitcoind_version="",
data_folder="",
):
self.data_folder = data_folder
self._active_node = active_node
self.proxy_url = proxy_url
self.only_tor = only_tor
self.bitcoind_path = bitcoind_path
self.internal_bitcoind_version = internal_bitcoind_version
self.update(data_folder)
internal_nodes = [
node for node in self.nodes.values() if not node.external_node
]
for node in internal_nodes:
node.start()
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)
node_class = (
Node if nodes_files[node_alias]["external_node"] else InternalNode
)
nodes[nodes_files[node_alias]["name"]] = node_class.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
logger.info("Added new node {}".format(node.alias))
return node
def add_internal_node(
self,
name,
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 = InternalNode(
name,
node_alias,
False,
os.path.join(self.data_folder, f"{node_alias}/.bitcoin"),
"bitcoin",
secrets.token_urlsafe(16),
8332,
"localhost",
"http",
fullpath,
self,
self.bitcoind_path,
"mainnet",
self.internal_bitcoind_version,
)
write_node(node, fullpath)
self.update() # reload files
logger.info("Added new internal node {}".format(node.alias))
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()
logger.info("Node {} was deleted successfully".format(node.alias))
# TODO: Refactor out later to allow multiple built in nodes
@property
def internal_node(self):
internal_nodes = [
node for node in self.nodes.values() if not node.external_node
]
if len(internal_nodes) < 1:
return self.add_internal_node("Specter Bitcoin")
return internal_nodes[0]

View file

@ -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__)

View file

@ -8,12 +8,12 @@ import traceback
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()

View file

@ -0,0 +1,350 @@
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:
"""A NodeManager represents the connection to a Bitcoin and/o Liquid Node (Full-) node.
It can be created via Constructor or from_json, and mainly it can give you A
RPC-object to use the API.
One or many Nodes are managed via the NodeManager
"""
def __init__(
self,
name,
alias,
autodetect,
datadir,
user,
password,
port,
host,
protocol,
external_node,
fullpath,
manager,
):
"""Constructor for your Node.
:param name: arbitrary name
:param alias: Bad habit, doesn't seem to have business functionality
:param autodetect: Boolean, will use the datadir to derive config is yes
:param datadir: A directory where a bitcoin.conf can be found, relevant for autodetect
:param user: rpc-user
:param password: rpc-password
:param port: usually something like 8332 for mainnet, 18332 for testnet, 18443 for Regtest, 38332 for signet
:param host: domainname or ip-address. Don't add the protocol here
:param protocol: Usually https or http
:param external_node: should be True for Node and False for InternalNode
:param fullpath: it's assumed that you want to store it on disk AND decide about the fullpath upfront
:param manager: A NodeManager instance which will get notified if the Node's name changes, the proxy_url will get copied from the manager as well
"""
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 = self.get_rpc()
self.check_info()
@classmethod
def from_json(cls, node_dict, manager, default_alias="", default_fullpath=""):
"""Create a Node from json"""
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):
"""Get a json-representation of this Node"""
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
"""
if hasattr(self, "rpc"):
rpc = self.rpc
else:
rpc = None
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):
"""tests the rpc-connection and returns a dict which helps
to derive what might be wrong with the config
ToDo: list an example here.
"""
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):
"""use this to abort a rescan as it stores some state while rescanning"""
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)

View file

@ -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()

View file

@ -85,6 +85,7 @@ def init_app(app, hwibridge=False, specter=None):
specter = Specter(
data_folder=app.config["SPECTER_DATA_FOLDER"],
config=app.config["DEFAULT_SPECTER_CONFIG"],
internal_bitcoind_version=app.config["INTERNAL_BITCOIND_VERSION"],
)
# version checker

View file

@ -36,7 +36,7 @@ def login():
if auth["method"] == "rpcpasswordaspin":
# TODO: check the password via RPC-call
if app.specter.rpc is None:
if app.specter.config["rpc"]["password"] == request.form["password"]:
if app.specter.node.password == request.form["password"]:
app.login("admin")
app.logger.info(
"AUDIT: Successfull Login via RPC-credentials (node disconnected)"

View file

@ -27,6 +27,7 @@ app.register_blueprint(filters_bp)
# 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 @@ from ..rpc import RpcError
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")
@ -95,7 +97,12 @@ def server_error_timeout(e):
"Bitcoin Core is not coming up in time. Maybe it's just slow but please check the logs below",
"warn",
)
return redirect(url_for("settings_endpoint.bitcoin_core_internal_logs"))
return redirect(
url_for(
"node_settings.bitcoin_core_internal_logs",
node_alias=app.specter.node.alias,
)
)
@app.errorhandler(CSRFError)

View file

@ -15,9 +15,9 @@ from flask import current_app as app
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

View file

@ -0,0 +1,334 @@
import copy, random, json, time, os, shutil, logging
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
from ..specter_error import ExtProcTimeoutException
from ..util.shell import get_last_lines_from_file
logger = logging.getLogger(__name__)
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/<node_alias>/", methods=["GET", "POST"])
@login_required
def node_settings(node_alias):
if node_alias:
try:
node = app.specter.node_manager.get_by_alias(node_alias)
if not node.external_node:
return redirect(
url_for(
"nodes_endpoint.internal_node_settings",
node_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 and node_alias:
node.update_rpc()
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("specter_node/<node_alias>/", methods=["GET", "POST"])
@login_required
def internal_node_settings(node_alias):
err = None
if node_alias:
try:
node = app.specter.node_manager.get_by_alias(node_alias)
if node.external_node:
return redirect(
url_for(
"nodes_endpoint.node_settings",
node_alias=node.alias,
)
)
except:
return render_template(
"base.jinja", error="Node not found", specter=app.specter, rand=rand
)
else:
# TODO: Allow internal node setup here?
return redirect(
url_for(
"nodes_endpoint.internal_node_settings",
node_alias=node.alias,
)
)
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()
if request.method == "POST":
action = request.form["action"]
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)
if bool(request.form.get("remove_datadir", False)):
shutil.rmtree(os.path.expanduser(node.datadir), ignore_errors=True)
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 == "stopbitcoind":
try:
node.stop()
time.sleep(5)
flash("Specter stopped Bitcoin Core successfully")
except Exception as e:
try:
logger.exception(e)
flash("Stopping Bitcoin Core, this might take a few moments.")
node.rpc.stop()
except Exception as ne:
logger.exception(ne)
flash(f"Failed to stop Bitcoin Core {ne}", "error")
elif action == "startbitcoind":
if node.start(timeout=120):
flash("Specter has started Bitcoin Core")
else:
flash("Specter failed to start the node...", "error")
elif action == "uninstall_bitcoind":
try:
node.stop()
shutil.rmtree(
os.path.join(app.specter.data_folder, "bitcoin-binaries"),
ignore_errors=True,
)
if bool(request.form.get("remove_datadir", False)):
shutil.rmtree(os.path.expanduser(node.datadir), ignore_errors=True)
flash(f"Bitcoin Core uninstalled successfully")
app.specter.node_manager.delete_node(node, app.specter)
return redirect(
url_for(
"nodes_endpoint.node_settings",
node_alias=app.specter.node.alias,
)
)
except Exception as e:
flash(f"Failed to remove Bitcoin Core, error: {e}", "error")
return render_template(
"node/internal_node_settings.jinja",
node=node,
node_alias=node_alias,
specter=app.specter,
rand=rand,
)
@nodes_endpoint.route("/internal_node_logs/<node_alias>/", methods=["GET"])
@login_required
def internal_node_logs(node_alias):
node = app.specter.node_manager.get_by_alias(node_alias)
logfile_location = os.path.join(node.datadir, "debug.log")
return render_template(
"node/internal_node_logs.jinja",
node_alias=node_alias,
specter=app.specter,
loglines="".join(get_last_lines_from_file(logfile_location)),
)
@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))

View file

@ -1,20 +1,23 @@
import json, os, time, random, requests, secrets, platform, tarfile, zipfile, sys, shutil
import pgpy
import json
import logging
import os
import platform
import random
import secrets
import shutil
import sys
import tarfile
import time
import zipfile
from pathlib import Path
from flask import (
Flask,
Blueprint,
render_template,
request,
redirect,
url_for,
jsonify,
flash,
send_file,
)
from flask_login import login_required, current_user
import pgpy
import requests
from flask import Blueprint, Flask
from flask import current_app as app
from flask import flash, jsonify, redirect, render_template, request, send_file, url_for
from flask_login import current_user, login_required
from ..helpers import (
get_loglevel,
get_startblock_by_chain,
@ -22,11 +25,13 @@ from ..helpers import (
set_loglevel,
)
from ..persistence import write_devices, write_wallet
from ..specter_error import ExtProcTimeoutException, handle_exception
from ..user import hash_password
from ..util.tor import start_hidden_service, stop_hidden_services
from ..util.sha256sum import sha256sum
from ..util.shell import get_last_lines_from_file
from ..specter_error import handle_exception, ExtProcTimeoutException
from ..util.tor import start_hidden_service, stop_hidden_services
logger = logging.getLogger(__name__)
rand = random.randint(0, 1e32) # to force style refresh
@ -37,181 +42,7 @@ settings_endpoint = Blueprint("settings_endpoint", __name__)
@settings_endpoint.route("/", methods=["GET"])
@login_required
def settings():
if current_user.is_admin:
return redirect(url_for("settings_endpoint.bitcoin_core"))
else:
return redirect(url_for("settings_endpoint.general"))
@settings_endpoint.route("/bitcoin_core/internal_logs", methods=["GET"])
@login_required
def bitcoin_core_internal_logs():
logfile_location = os.path.join(
app.specter.config["internal_node"]["datadir"], "debug.log"
)
return render_template(
"settings/bitcoin_core_internal_logs.jinja",
specter=app.specter,
loglines="".join(get_last_lines_from_file(logfile_location)),
)
@settings_endpoint.route("/bitcoin_core", methods=["GET", "POST"])
@login_required
def bitcoin_core():
current_version = notify_upgrade(app, flash)
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 app.specter.rpc is None:
app.specter.check()
rpc = app.specter.config["rpc"]
user = rpc["user"]
password = rpc["password"]
port = rpc["port"]
host = rpc["host"]
protocol = "http"
autodetect = rpc["autodetect"]
datadir = rpc["datadir"]
external_node = rpc["external_node"]
node_view = "external" if external_node else "internal"
err = None
if "protocol" in rpc:
protocol = rpc["protocol"]
test = None
if request.method == "POST":
action = request.form["action"]
if action == "test" or action == "save":
autodetect = "autodetect" in request.form
if autodetect:
datadir = request.form["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 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 wron to the user gets broken
test = app.specter.test_rpc(
user=user,
password=password,
port=port,
host=host,
protocol=protocol,
autodetect=autodetect,
datadir=datadir,
)
node_view = "external"
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 current_user.is_admin:
node_view = "external"
success = app.specter.update_rpc(
user=user,
password=password,
port=port,
host=host,
protocol=protocol,
autodetect=autodetect,
datadir=datadir,
)
if not success:
flash("Failed connecting to the node", "error")
app.specter.check()
# Internal Node actions
elif action == "useinternal":
app.specter.update_use_external_node(False)
external_node = False
node_view = "internal"
elif action == "useexternal":
app.specter.update_use_external_node(True)
external_node = True
node_view = "external"
elif action == "stopbitcoind":
node_view = "internal"
try:
app.specter.bitcoind.stop_bitcoind()
app.specter.set_bitcoind_pid(False)
time.sleep(5)
flash("Specter stopped Bitcoin Core successfully")
except Exception:
try:
flash("Stopping Bitcoin Core, this might take a few moments.")
app.specter.rpc.stop()
except Exception as e:
flash(f"Failed to stop Bitcoin Core {e}", "error")
elif action == "startbitcoind":
node_view = "internal"
try:
app.specter.bitcoind.start_bitcoind(
datadir=os.path.expanduser(
app.specter.config["internal_node"]["datadir"]
)
)
except ExtProcTimeoutException as e:
e.check_logfile(
os.path.join(
app.specter.config["internal_node"]["datadir"], "debug.log"
)
)
raise e
finally:
app.specter.set_bitcoind_pid(app.specter.bitcoind.bitcoind_proc.pid)
time.sleep(15)
flash("Specter has started Bitcoin Core")
elif action == "uninstall_bitcoind":
try:
if app.specter.is_bitcoind_running():
app.specter.bitcoind.stop_bitcoind()
shutil.rmtree(os.path.join(app.specter.data_folder, "bitcoin-binaries"))
if bool(request.form.get("remove_datadir", False)):
shutil.rmtree(
os.path.expanduser(
app.specter.config["internal_node"]["datadir"]
)
)
flash(f"Bitcoin Core uninstalled successfully")
except Exception as e:
flash(f"Failed to remove Bitcoin Core, error: {e}", "error")
app.specter.check()
return render_template(
"settings/bitcoin_core_settings.jinja",
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),
is_running=app.specter.is_bitcoind_running(),
node_view=node_view,
external_node=external_node,
error=err,
rand=rand,
)
return redirect(url_for("settings_endpoint.general"))
@settings_endpoint.route("/general", methods=["GET", "POST"])
@ -372,6 +203,7 @@ def tor():
flash("Specter has started Tor")
except Exception as e:
flash(f"Failed to start Tor, error: {e}", "error")
logger.error(f"Failed to start Tor, error: {e}")
elif action == "stoptor":
try:
app.specter.tor_daemon.stop_tor_daemon()
@ -379,6 +211,7 @@ def tor():
flash("Specter stopped Tor successfully")
except Exception as e:
flash(f"Failed to stop Tor, error: {e}", "error")
logger.error(f"Failed to start Tor, error: {e}")
elif action == "uninstalltor":
try:
if app.specter.is_tor_dameon_running():
@ -387,7 +220,8 @@ def tor():
os.remove(os.path.join(app.specter.data_folder, "torrc"))
flash(f"Tor uninstalled successfully")
except Exception as e:
flash(f"Failed to stop Tor, error: {e}", "error")
flash(f"Failed to uninstall Tor, error: {e}", "error")
logger.error(f"Failed to uninstall Tor, error: {e}")
elif action == "test_tor":
try:
requests_session = requests.Session()
@ -396,14 +230,28 @@ def tor():
res = requests_session.get(
# "http://expyuzz4wqqyqhjn.onion", # Tor Project onion website (seems to be down)
"https://protonirockerxow.onion", # Proton mail onion website
timeout=10,
)
tor_connectable = res.status_code == 200
if tor_connectable:
flash("Tor requests test completed successfully!", "info")
logger.error("Tor-Logs:")
logger.error(app.specter.tor_daemon.get_logs())
else:
flash("Failed to make test request over Tor.", "error")
flash(
f"Failed to make test request over Tor. Status-Code: {res.status_code}",
"error",
)
logger.error(
f"Failed to make test request over Tor. Status-Code: {res.status_code}"
)
logger.error("Tor-Logs:")
logger.error(app.specter.tor_daemon.get_logs())
except Exception as e:
flash("Failed to make test request over Tor.\nError: %s" % e, "error")
flash(f"Failed to make test request over Tor.\nError: {e}", "error")
logger.error(f"Failed to make test request over Tor.\nError: {e}")
logger.error("Tor-Logs:")
logger.error(app.specter.tor_daemon.get_logs())
tor_connectable = False
elif action == "toggle_hidden_service":
if not app.config["DEBUG"]:

View file

@ -15,9 +15,9 @@ from flask import current_app as app
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,
@ -121,14 +121,17 @@ def setup_tor():
@setup_endpoint.route("/setup_bitcoind", methods=["POST"])
@login_required
def setup_bitcoind():
app.specter.config["internal_node"]["datadir"] = request.form.get(
"bitcoin_core_datadir", app.specter.config["internal_node"]["datadir"]
app.specter.node_manager.internal_node.update_rpc(
datadir=request.form.get(
"bitcoin_core_datadir", app.specter.node_manager.internal_node.datadir
),
)
app.specter._save()
if os.path.exists(app.specter.config["internal_node"]["datadir"]):
if os.path.exists(app.specter.node_manager.internal_node.datadir):
if request.form["override_data_folder"] != "true":
return {"error": "data folder already exists"}
shutil.rmtree(app.specter.config["internal_node"]["datadir"])
shutil.rmtree(
app.specter.node_manager.internal_node.datadir, ignore_errors=True
)
if (
not os.path.isfile(app.specter.bitcoind_path)
and app.specter.setup_status["bitcoind"]["stage_progress"] == -1

View file

@ -37,7 +37,7 @@ from ..util.descriptor import AddChecksum, Descriptor
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

View file

@ -19,10 +19,12 @@ from .tor_daemon import TorDaemonController
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 .persistence import write_json_file, read_json_file
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, write_node
from .user import User
from .util.price_providers import update_price
from .util.tor import get_tor_daemon_suffix
@ -32,70 +34,20 @@ from stem.control import Controller
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
from .node import Node
from .internal_node import InternalNode
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"""
# use this lock for all fs operations
lock = threading.Lock()
def __init__(self, data_folder="./data", config={}):
def __init__(self, data_folder="./data", config={}, internal_bitcoind_version=""):
if data_folder.startswith("~"):
data_folder = os.path.expanduser(data_folder)
data_folder = os.path.abspath(data_folder)
@ -106,33 +58,30 @@ class Specter:
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.internal_bitcoind_version = internal_bitcoind_version
# Migrating from Specter 1.3.1 and lower (prior to the node manager)
self.migrate_old_node_format()
self.node_manager = NodeManager(
proxy_url=self.proxy_url,
only_tor=self.only_tor,
active_node=self.active_node_alias,
bitcoind_path=self.bitcoind_path,
internal_bitcoind_version=internal_bitcoind_version,
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()}"
)
self.bitcoind_path = os.path.join(
self.data_folder, "bitcoin-binaries/bin/bitcoind"
)
if platform.system() == "Windows":
self.bitcoind_path += ".exe"
self._bitcoind = None
self._tor_daemon = None
self.node_status = None
self.setup_status = {
"stage": "start",
"bitcoind": {
@ -150,46 +99,13 @@ class Specter:
# 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):
try:
self.bitcoind.start_bitcoind(
datadir=os.path.expanduser(self.config["internal_node"]["datadir"]),
timeout=15, # At the initial startup, we don't wait on bitcoind
)
except ExtProcTimeoutException as e:
logger.error(e)
e.check_logfile(
os.path.join(self.config["internal_node"]["datadir"], "debug.log")
)
logger.error(e.get_logger_friendly())
except SpecterError as e:
logger.error(e)
# Likely files of bitcoind were not found. Maybe deleted by the user?
finally:
try:
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()
@ -209,9 +125,9 @@ class Specter:
logger.info("Specter exit cleanup: Stopping Tor daemon")
self._tor_daemon.stop_tor_daemon()
if self._bitcoind:
logger.info("Specter exit cleanup: Stopping bitcoind")
self._bitcoind.stop_bitcoind()
for node in self.node_manager.nodes.values():
if not node.external_node:
node.stop()
logger.info("Closing Specter after cleanup")
# For some reason we need to explicitely exit here. Otherwise it will hang
@ -229,27 +145,16 @@ class Specter:
# 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 +164,29 @@ class Specter:
for u in self.user_manager.users:
u.check()
@property
def node(self):
try:
return self.node_manager.active_node
except SpecterError as e:
self.update_active_node(list(self.node_manager.nodes.values())[0].alias)
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 +220,7 @@ class Specter:
# 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,23 +231,11 @@ class Specter:
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
# mark
def set_bitcoind_pid(self, pid):
"""set the control pid of the bitcoind daemon"""
self.config_manager.set_bitcoind_pid(pid)
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()
def update_setup_status(self, software_name, stage):
self.setup_status[software_name]["error"] = ""
@ -481,11 +270,6 @@ class Specter:
return {"installed": installed, **self.setup_status[software_name]}
# mark
def update_use_external_node(self, use_external_node):
"""set whatever specter should connect to internal or external node"""
self.config_manager.update_use_external_node(use_external_node)
# mark
def update_auth(self, method, rate_limit, registration_link_timeout):
"""simply persisting the current auth-choice"""
@ -562,28 +346,9 @@ class Specter:
"Tor daemon files missing. Make sure Tor is installed within Specter"
)
@property
def bitcoind(self):
if os.path.isfile(self.bitcoind_path):
if not self._bitcoind:
self._bitcoind = BitcoindPlainController(
bitcoind_path=self.bitcoind_path,
rpcport=8332,
network="mainnet",
rpcuser=self.config["internal_node"]["user"],
rpcpassword=self.config["internal_node"]["password"],
)
return self._bitcoind
raise SpecterError(
"Bitcoin Core files missing. Make sure Bitcoin Core is installed within Specter"
)
def is_tor_dameon_running(self):
return self._tor_daemon and self._tor_daemon.is_running()
def is_bitcoind_running(self):
return self._bitcoind and self._bitcoind.check_existing()
@property
def tor_controller(self):
if self._tor_controller:
@ -649,41 +414,45 @@ class Specter:
return self.rpc.estimatesmartfee(blocks)
@property
def is_running(self):
return self._is_running
def bitcoind_path(self):
bitcoind_path = os.path.join(self.data_folder, "bitcoin-binaries/bin/bitcoind")
@property
def is_configured(self):
return self._is_configured
if platform.system() == "Windows":
bitcoind_path += ".exe"
return bitcoind_path
@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, "")
@ -796,6 +565,63 @@ class Specter:
memory_file.seek(0)
return memory_file
# Migrating RPC nodes from Specter 1.3.1 and lower (prior to the node manager)
def migrate_old_node_format(self):
if not os.path.isdir(os.path.join(self.data_folder, "nodes")):
os.mkdir(os.path.join(self.data_folder, "nodes"))
old_rpc = self.config.get("rpc", None)
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",
old_internal_rpc.get("autodetect", False),
old_internal_rpc.get("datadir", get_default_datadir()),
old_internal_rpc.get("user", ""),
old_internal_rpc.get("password", ""),
old_internal_rpc.get("port", 8332),
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"
),
self,
self.bitcoind_path,
"mainnet",
self.internal_bitcoind_version,
)
write_node(
internal_node,
os.path.join(
os.path.join(self.data_folder, "nodes"), "specter_bitcoin.json"
),
)
del self.config["internal_node"]
if not old_rpc or not old_rpc.get("external_node", True):
self.config_manager.update_active_node("specter_bitcoin")
if old_rpc:
node = Node(
"Bitcoin Core",
"default",
old_rpc.get("autodetect", True),
old_rpc.get("datadir", get_default_datadir()),
old_rpc.get("user", ""),
old_rpc.get("password", ""),
old_rpc.get("port", None),
old_rpc.get("host", "localhost"),
old_rpc.get("protocol", "http"),
True,
os.path.join(os.path.join(self.data_folder, "nodes"), "default.json"),
self,
)
write_node(
node,
os.path.join(os.path.join(self.data_folder, "nodes"), "default.json"),
)
del self.config["rpc"]
self._save()
class SpecterConfiguration:
"""An abstract class which only holds functionality relevant for storage of information mostly

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="48px" height="48px"><path d="M0 0h24v24H0z" fill="none"/><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>

Before

Width:  |  Height:  |  Size: 281 B

View file

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.38859 16.5001L18.9999 16.5001" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.39825 19.9999L4.99986 16.4999L8.39825 12.9999" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.6112 7.50016L4.99989 7.50016" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.6015 11.0002L18.9999 7.50016L15.6015 4.00018" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 639 B

View file

@ -12,7 +12,7 @@
<form action="./" method="POST" onsubmit="{% if not device %}return checkType();{% endif %}">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
{% if not device %}
<label>Name it:<label>
<label>Name it:</label>
<div class="row">
<input type="text" id="device_name" name="device_name" value="{{ device_name }}" placeholder="Name your device">
</div>

View file

@ -171,9 +171,4 @@
hidePageOverlay();
return result;
}
// TODO: remove, legacy
function onCancelOverlay(){
}
</script>

View file

@ -1,4 +1,4 @@
<div class="page_overlay" id="page_overlay">
<div class="page_overlay" id="page_overlay" style="display: none;">
<div class="page_overlay_popup" id="page_overlay_popup">
<div id="page_overlay_popup_content"></div>
<div class="page_overlay_popup_cancel">
@ -51,6 +51,8 @@
var overlayPopup = document.getElementById("page_overlay_popup");
overlayPopup.style.display = "none";
// A file using `overlay.html` should implement `onCancelOverlay` JS function to handle popup cancel logic
onCancelOverlay();
}
}
@ -82,7 +84,5 @@
function cancelOverlay() {
hidePageOverlay();
// A file using `overlay.html` should implement `onCancelOverlay` JS function to handle popup cancel logic
onCancelOverlay();
}
</script>

View file

@ -26,7 +26,7 @@
</table>
<p id="total_supply" style="line-height: 2.5;"></p>
<div class="row">
<button type="button" onclick="fetchTotalSupply()" class="btn centered" href="{{ url_for('settings_endpoint.bitcoin_core') }}">
<button type="button" onclick="fetchTotalSupply()" class="btn centered">
Run the numbers!
<div class="tool-tip">
<i class="tool-tip__icon">i</i>
@ -68,7 +68,7 @@
</script>
{% if current_user.is_admin %}
<div class="row">
<a class="btn centered" href="{{ url_for('settings_endpoint.bitcoin_core') }}">
<a id="active-node-settings-btn" class="btn centered" href="{{ url_for('nodes_endpoint.node_settings', node_alias=specter.node.alias) }}">
<img src="{{ url_for('static', filename='img/gear.svg') }}" style="width: 26px; margin: 0px;" class="svg-white">
Configure Bitcoin Core RPC Connection
</a>

View file

@ -0,0 +1,77 @@
<style>
.node-list-item {
min-height: min-content;
justify-content: flex-start;
flex-grow: 1;
width: 100%;
padding: 10px 0;
display: flex;
flex-direction: row;
align-items: center;
border-left: 3px solid transparent;
}
.node-list-item>svg {
width: 80px;
}
.node-list-item.active{
background: #394659;
}
@media (min-width: 600px){
#node_select_popup {
width: 350px;
}
}
</style>
<div id="node_select_popup" class="hidden" style="text-align: left;">
<h1>Node configuration</h1>
{% for node_name in specter.node_manager.nodes_names %}
{% set node = specter.node_manager.nodes[node_name] %}
<form action="{{url_for('nodes_endpoint.switch_node')}}" id="{{node.alias}}-select-node-form" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="node_alias" value="{{ node.alias }}"/>
<div class="item core node-list-item {% if node.alias == specter.node.alias %}active{% endif %}" style="cursor: pointer;"
onclick="document.getElementById('{{node.alias}}-select-node-form').submit();">
{{ bitcoin_svg(node.chain, 40) }}
<div style="width: 100%;">
{{ node.name }}
<br>
<small>
{% if node.chain %}
{{node.chain | title}}, {{ node.host }}:{{ node.port }}
{% else %}
Node unreachable...
{% endif %}
</small>
</div>
{% if node.alias == specter.node.alias %}
<a href="{{ url_for('nodes_endpoint.node_settings', node_alias=node.alias) }}" style="text-align: right; width: 100%px;">
<img src="{{ url_for('static', filename='img/gear.svg')}}" class="svg-white" style="width: 40px; margin-right: 10px;"/>
</a>
{% endif %}
</div>
</form>
{% endfor %}
<br>
{{ sidebar_btn(url_for('nodes_endpoint.node_settings'), 'Connect a new node', 'btn_new_node') }}
</div>
<script type="text/javascript">
function showNodeSelectPopup() {
hidePageOverlay();
if (!e) var e = window.event;
e.cancelBubble = true;
if (e.stopPropagation) {
e.stopPropagation();
}
document.getElementById('page_overlay_popup').style.padding = '1.5em 0';
document.getElementById('page_overlay_popup_cancel_button').classList.add('hidden');
document.getElementById('side-content').classList.remove('active');
showPageOverlay('node_select_popup');
}
function onCancelOverlay() {
document.getElementById('page_overlay_popup').style.padding = '1.5em';
document.getElementById('page_overlay_popup_cancel_button').classList.remove('hidden');
}
</script>

View file

@ -1,4 +1,7 @@
{% from 'includes/sidebar/components/sidebar_btn.jinja' import sidebar_btn %}
{% from "components/bitcoin_svg.jinja" import bitcoin_svg %}
{% include "includes/sidebar/components/bitcoin_core_info.jinja" %}
{% include "includes/sidebar/components/node_select_popup.jinja" %}
<style>
#wallets_overview_link {
text-transform: none;
@ -12,6 +15,20 @@
color: #fff;
}
#node-switch-icon {
padding: 5px;
float: right;
width: 30px;
position: absolute;
left: 210px;
top: 5px;
border-radius: 10px;
}
#node-switch-icon:hover {
background-color: #737f98;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(359deg); }
@ -29,16 +46,12 @@
{% if specter.network_info.version < 190000 %}
<p class="warning"><img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/><br>Bitcoin Core version is outdated.<br>Some features might not work...<br>(minimum required: v19.0.0).</p>
{% endif %}
{% if specter.chain %}
<div onclick="showPageOverlay('bitcoin_core_info');document.getElementById('side-content').classList.remove('active');" class="item core" style="cursor: pointer;">
{% else %}
<a href="{{ url_for('settings_endpoint.settings') }}" class="item core" style="color: #fff; text-decoration: none;">
{% endif %}
{% from "components/bitcoin_svg.jinja" import bitcoin_svg %}
<div id="active-node" onclick="{% if not specter.chain %}window.location.href = `{{ url_for('nodes_endpoint.node_settings', node_alias=specter.node.alias) }}`;{% else %}hidePageOverlay();showPageOverlay('bitcoin_core_info');document.getElementById('side-content').classList.remove('active');{% endif %}" class="item core" style="cursor: pointer; position: relative;">
{{ bitcoin_svg(specter.info.chain, 40) }}
{% include "includes/sidebar/components/bitcoin_core_info.jinja" %}
<div>
Bitcoin Core<br>
<span style="max-width: 140px;">{{ specter.node.name }}</span>
<img id="node-switch-icon" src="{{ url_for('static', filename='img/flip-horizontal.svg')}}" class="svg-white" onclick="showNodeSelectPopup()"/>
<br>
<small>
{% if specter.chain %}
{% if specter.bitcoin_core_version != '' %}
@ -55,11 +68,7 @@
{% endif %}
</small>
</div>
{% if specter.chain %}
</div>
{% else %}
</a>
{% endif %}
</div>
{% if specter.info["utxorescan"] %}
<div class="rescan_progress {{ specter.info.chain }}">
<div id="utxo_rescan_progress" style='width: {{ specter.info["utxorescan"] }}%'></div>
@ -178,7 +187,7 @@
<img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/>
<br>Wallets are unavailable if Specter is not connected to Bitcoin Core!<br>
{% if current_user.is_admin %}
<a style="margin: 5px; transform: scale(0.9); font-size: 1em;" class="btn" href="{{ url_for('settings_endpoint.bitcoin_core') }}">
<a style="margin: 5px; transform: scale(0.9); font-size: 1em;" class="btn" href="{{ url_for('nodes_endpoint.node_settings', node_alias=specter.node.alias) }}">
<img src="{{ url_for('static', filename='img/gear.svg') }}" style="width: 24px; margin: 0px;" class="svg-white">Configure Node
</a>
{% endif %}

View file

@ -9,7 +9,7 @@
{{ settings_menu('bitcoin_core', current_user) }}
</form>
<br><br><br>
<a href="{{ url_for('settings_endpoint.bitcoin_core')}}" class="btn centered">Return</a>
<a href="{{ url_for('nodes_endpoint.node_settings', node_alias=node_alias) }}" class="btn centered">Return</a>
<br><br>
<div class="card" style="width:90%; max-width: 1000px;">
Bitcoin Logs:

View file

@ -0,0 +1,45 @@
{% extends "base.jinja" %}
{% block main %}
<h1 id="title">{{ node.name }}</h1>
{% from 'components/editable_title.jinja' import editable_title %}
{{ editable_title(node.name) }}
<form action="?" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="card" style="margin: 20px auto 120px; text-align: center;">
<h2>Built in Bitcoin Node Status: {% if node.bitcoind.status %}{{node.bitcoind.status}}{% else %}Down{% endif %}</h2><br>
<span class="note" style="font-size: 1.3em;">Bitcoin Core Version: {{node.version}}</span>
<br><br>
{% if node.bitcoin_pid %}
<button type="submit" class="btn centered" name="action" value="stopbitcoind">Stop Bitcoin Core</button>
{% else %}
<button type="submit" class="btn centered" name="action" value="startbitcoind">Start Bitcoin Core</button>
<p class="warning">
If this is your first time starting the node, and you haven't used the QuickSync, your node will start with Initial Block Downloading (IBD) to sync with the network.<br>
This process of IBD syncing may take several days. If you'd like the QuickSync option, please click on it before starting your node, or if it's running, stop it and the click the QuickSync.<br>
<b>(Warning: QuickSync will override any existing data of your Bitcoin Node!)</b>
</p>
{% endif %}
<br><br>
<br><br>
<div class="card">
<h1>Debug</h1>
<a class="btn centered" href="{{url_for('nodes_endpoint.internal_node_logs', node_alias=node_alias)}}">See Bitcoind Logs</a>
<br><br>
<h1>Danger Zone</h1>
{% if specter.node_manager.nodes | length > 1 and node_alias %}
<div class="row">
<button type="submit" name="action" value="forget" class="btn danger centered">Forget node</button>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
{% endif %}
<button type="submit" name="action" value="uninstall_bitcoind" class="btn danger centered">Unistall Bitcoin Core</button>
{% if specter.node_manager.nodes | length > 1 and node_alias %}
</div>
{% endif %}
<br>
<label><input type="checkbox" class="inline" name="remove_datadir"> Delete data folder?</label>
</div>
</div>
</form>
{% endblock %}
{% block scripts %}
{% endblock %}

View file

@ -0,0 +1,219 @@
{% extends "base.jinja" %}
{% block main %}
{% include "includes/qr-scanner.html" %}
{% if node_alias %}
<h1 id="title">{{ node.name }}</h1>
{% from 'components/editable_title.jinja' import editable_title %}
{{ editable_title(node.name) }}
<form action="?" method="POST">
{% else %}
<form action="?" method="POST">
<h1 id="title">Configure new node</h1>
<label style="text-align: center; display: block; margin: auto;font-size: 1.2em; margin-bottom: 7px;">Name your new node:</label>
<input type="text" id="name" name="name" value="{{ node.name }}" placeholder="Name your new node" style="width: auto; display: block; margin: auto; width: 70%;">
{% endif %}
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="card" style="margin: 20px auto 20px;">
<h2>Bitcoin JSON-RPC configuration</h2>
<br>
<label style="height: 25px; vertical-align: sub;">Auto-detect:&nbsp;</label>
<label class="switch">
<input type="checkbox" id="autodetect" name="autodetect" {% if node.autodetect %}checked{% endif %}>
<span class="slider"></span>
</label>
<span id="datadir-container">
<div class="tool-tip" style="float: right;">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Setting Bitcoin Core Data Directory<br></span><br>
When auto-detect is on, Specter will check for Environment-Variables (BTC_RPC_USER, BTC_RPC_PASSWORD, BTC_RPC_HOST, BTC_RPC_PORT) to configure the connection or
attempt to automatically locate your Bitcoin data directory and load your node configurations from it.<br><br>
However, if your Bitcoin Core data directory is not located at the default location, you will need to enter its path here so Specter will be able to locate it.<br><br>
If you are connecting to a specific remote node, you can disable the auto-detect feature and enter the node's configurations manually below.
</span>
</p>
</div>
<br><br>
Bitcoin Core data directory path:<br><input type="text" id="datadir" name="datadir" type="text" value="{{ node.datadir }}">
</span>
<br><br>
<div id="rpc_settings">
<div id="lock" class="center">
<p style="text-align: center; margin: auto; ">&#128274;</p>
<span class="note centered"> (disable auto-detect to configure manually)</span>
</div>
Username:<br><input type="text" id="username" name="username" value="{{ node.user }}">
<br><br>
Password:<br><input type="password" id="password" name="password" value="{{ node.password }}">
<br><br>
Host:<br><input type="text" id="host" name="host" type="text" value="{{ node.protocol }}://{{ node.host }}">
<br>
<qr-scanner id="scan_rpc" style="margin: 10px;">
<a slot="button" href="#" class="btn" style="padding: 7px 15px 5px 10px;">
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 30px; margin: 0px;" class="svg-white">
Connect with QR
</a>
</qr-scanner>
</div>
Port:<br><input type="text" id="port" name="port" type="text" value="{{ node.port }}">
<div class="note">
Default ports: <b>8332</b> for mainnet, <b>18332</b> for testnet, <b>18443</b> for Regtest, <b>38332</b> for signet
</div>
<br><br>
<div class="row">
<button type="submit" class="btn" name="action" value="test">Test</button>&nbsp;
<button type="submit" class="btn" name="action" value="save">Save</button>
</div>
{% if test %}
<br><div class="log"><b>Test results:</b><br><br>
<div style="display: grid;grid-template-columns: auto auto auto; line-height: 2.2;">
{% macro tick_or_cross(my_boolean) %}
<button style="background: #fff; border: none;" disabled>
{% if my_boolean %}
<div style="color: green; font-size: 1.5em;">&#x2714;</div>
{% else %}
<div style="color: red; font-size: 1.5em;">&#x274C;</div>
{% endif %}
</button>
{% endmacro %}
{% if 'connectable' in test['tests'] %}
<div>Connectable</div>
<div>{{ tick_or_cross(test['tests']['connectable']) }} </div>
{% if not test['tests']['connectable'] %}
<tool-tip title="Your Node can't be reached" style="float: right;">
There are a lot of potential issues preventing the connection.
Please doublecheck the Host and the Port. Make sure you can reach
the host and make sure that Bitcoin-Core is listening at the port
you have specified.
For more hints, please look at this <a style="color:grey" href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/connect-your-node.md" target="_blank">article</a>.
</tool-tip>
{% else %}
<div></div>
{% endif %}
{% endif %}
{% if 'credentials' in test['tests'] %}
<div>Credentials</div>
<div> {{ tick_or_cross(test['tests']['credentials']) }} </div>
{% if not test['tests']['credentials'] %}
<tool-tip title="Your Credentials don't work" style="float: right;">
Please doublecheck the user and the Password. Look at the bitcoin.conf
for the correct values in that field.
For more hints, please look at this <a style="color:grey" href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/connect-your-node.md" target="_blank">article</a>.
</tool-tip>
{% else %}
<div></div>
{% endif %}
{% endif %}
{% if 'recent_version' in test['tests'] %}
<div>Version recent enough</div>
<div> {{ tick_or_cross(test['tests']['recent_version']) }} </div>
{% if not test['tests']['recent_version'] %}
<tool-tip title="Your Core Node might be too old" style="float: right; margin-bottom: 5px;">
Specter is working well up from Bitcoin Core version 0.17.
The version of your Node is too low, unfortunatley.
Please upgrade!
</tool-tip>
{% else %}
<div></div>
{% endif %}
{% endif %}
{% if 'wallets' in test['tests'] %}
<div>Wallets enabled</div>
<div> {{ tick_or_cross(test['tests']['wallets']) }} </div>
{% if not test['tests']['wallets'] %}
<tool-tip title="Wallet Support in your Core-Node" style="float: right; margin-bottom: 5px;">
The RPC-Interface of your Core-Node is available, but the wallet-api is not.
Please make sure to have 'disablewallet=0' in your bitcoin.conf.
</tool-tip>
{% else %}
<div></div>
{% endif %}
{% endif %}
</div>
<br>
<code>
<pre>Process finished with code <b>{{ test.code }}</b>{% if test.code == 0 %} Output: {{ test.out }}{% else %}Error message: {{ test.err }}{% endif %}</pre>
</code>
</div>
{% endif %}
</div>
{% if specter.node_manager.nodes | length > 1 and node_alias %}
<button style="margin-bottom: 100px;" type="submit" name="action" value="forget" class="btn danger centered">Forget node</button>
{% endif %}
</form>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener("DOMContentLoaded", function(){
var autodetect = document.getElementById("autodetect");
if (autodetect != null) {
function toggleAutoDetectMode() {
var rpcSettings = document.getElementById("rpc_settings");
var scanRpc = document.getElementById("scan_rpc");
var lock = document.getElementById("lock");
var datadirContainer = document.getElementById("datadir-container");
if (autodetect.checked) {
lock.style.removeProperty('display');
datadirContainer.style.removeProperty('display');
rpcSettings.style['pointer-events'] = 'none';
rpcSettings.style['background-color'] = '#8881';
rpcSettings.style['padding'] = '10px';
rpcSettings.style['border-radius'] = '10px';
scanRpc.style.display = 'none';
document.getElementById("username").value = '{{ node.user }}';
document.getElementById("password").value = '{{ node.password }}';
} else {
lock.style['display'] = 'none';
datadirContainer.style['display'] = 'none';
rpcSettings.style.removeProperty('background-color');
rpcSettings.style.removeProperty('pointer-events');
rpcSettings.style.removeProperty('padding');
rpcSettings.style.removeProperty('border-radius');
scanRpc.style.removeProperty('display');
}
}
autodetect.addEventListener("change", function() {
toggleAutoDetectMode();
}, false);
toggleAutoDetectMode();
}
});
</script>
<script type="text/javascript">
let scanner = document.getElementById('scan_rpc');
if(scanner != null) {
scanner.addEventListener('scan', e=>{
let result = e.detail.result;
if(result==null){
return;
}
try {
let data = result.split('btcrpc://')[1].split(':')
let username = data[0];
let password = data[1].split('@')[0];
let host = data[1].split('@')[1];
let port = data[2].split('?')[0].split('/')[0];
document.getElementById('username').value = username;
document.getElementById('password').value = password;
document.getElementById('host').value = host;
document.getElementById('port').value = port;
} catch {
showError('Failed to read connection data from the QR', 3000)
}
});
}
</script>
{% endblock %}

View file

@ -12,7 +12,7 @@
<option value="none" {% if method=="none" %} selected="selected"{% endif %}>None</option>
<option value="passwordonly" {% if method=="passwordonly" %} selected="selected"{% endif %}>Password Protection</option>
<option value="usernamepassword" {% if method=="usernamepassword" %} selected="selected"{% endif %}>Multiple Users</option>
<option value="rpcpasswordaspin" {% if method=="rpcpasswordaspin" %} selected="selected"{% else %}{% if specter.rpc is none or specter.config.rpc.autodetect %}disabled{% endif %}{% endif %}>RPC password as Pin</option>
<option value="rpcpasswordaspin" {% if method=="rpcpasswordaspin" %} selected="selected"{% else %}{% if specter.rpc is none or specter.node.autodetect %}disabled{% endif %}{% endif %}>RPC password as Pin</option>
</select>
<br><br>
{% endif %}

View file

@ -1,285 +0,0 @@
{% extends "base.jinja" %}
{% block main %}
{% include "includes/qr-scanner.html" %}
<form action="?" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<h1 id="title" class="settings-title">Settings</h1>
{% from 'settings/components/settings_menu.jinja' import settings_menu %}
{% from 'settings/components/settings_menu_item.jinja' import settings_menu_item %}
{{ settings_menu('bitcoin_core', current_user) }}
<nav class="row">
<button type="button" id="external_node_view_btn" class="btn radio left checked" onclick="setNodeTypeView('external')"> Existing Node </button>
<button type="button" id="internal_node_view_btn" class="btn radio right" onclick="setNodeTypeView('internal')"> Built In Node </button>
</nav>
<div id="internal_node_setup_view" class="card" style="margin: 20px auto 120px; text-align: center;">
{% if external_node and bitcoind_exists %}
<button type="submit" class="btn centered" name="action" value="useinternal">Switch to built-in node</button>
<br><br>
{% endif %}
{% if bitcoind_exists %}
<h2>Built in Bitcoin Node Status: {% if specter.bitcoind.status %}{{specter.bitcoind.status}}{% else %}Down{% endif %}</h2><br>
<span class="note" style="font-size: 1.3em;">Bitcoin Core Version: {{specter.config.bitcoind_internal_version}}</span>
<br><br>
{% if not external_node %}
{% if is_running %}
<button type="submit" class="btn centered" name="action" value="stopbitcoind">Stop Bitcoin Core</button>
{% else %}
<button type="submit" class="btn centered" name="action" value="startbitcoind">Start Bitcoin Core</button>
<p class="warning">
If this is your first time starting the node, and you haven't used the QuickSync, your node will start with Initial Block Downloading (IBD) to sync with the network.<br>
This process of IBD syncing may take several days. If you'd like the QuickSync option, please click on it before starting your node, or if it's running, stop it and the click the QuickSync.<br>
<b>(Warning: QuickSync will override any existing data of your Bitcoin Node!)</b>
</p>
{% endif %}
<br><br>
{% else %}
<p>Please switch node type to start the built in node.</p>
{% endif %}
<br><br>
<div class="card">
<h1>Debug</h1>
<a class="btn centered" href="{{url_for('settings_endpoint.bitcoin_core_internal_logs')}}">See Bitcoind Logs</a>
<br><br>
<h1>Danger Zone</h1>
<button type="submit" name="action" value="uninstall_bitcoind" class="btn danger centered">Unistall Bitcoin Core</button>
<br>
<label><input type="checkbox" class="inline" name="remove_datadir"> Delete data folder?</label>
</div>
{% else %}
<div>
<h1>Setup Bitcoin Core Node</h1>
<p class="warning">
Specter can help you get started with your own Bitcoin Core node by setting it all up for you.<br><br>
If you don't yet have your own Bitcoin node, or want to switch from an external node to the Specter managed option, you can click below to start the setup.<br><br>
<a href="{{url_for('setup_endpoint.bitcoind')}}" class="btn wizard-btn" style="width: 200px; margin: auto;">Setup new Bitcoin node</a>
</p>
</div>
{% endif %}
</div>
<div id="external_node_setup_view" class="card" style="margin: 20px auto 120px;">
{% if not external_node %}
<h1>Use existing Bitcoin node</h1>
<p class="warning">
If you're already running a Bitcoin Core node and would like to use it with Specter, you can click below to setup the connection.<br><br>
<i>Note: make sure your Bitcoin node is configured with server=1 in its bitcoin.conf file.</i><br>
</p>
<br>
<button type="submit" class="btn centered" name="action" value="useexternal">Switch to external node</button>
<br>
{% else %}
<h2>Bitcoin JSON-RPC configuration</h2>
<br>
<label style="height: 25px; vertical-align: sub;">Auto-detect:&nbsp;</label>
<label class="switch">
<input type="checkbox" id="autodetect" name="autodetect" {% if autodetect %}checked{% endif %}>
<span class="slider"></span>
</label>
<span id="datadir-container">
<div class="tool-tip" style="float: right;">
<i class="tool-tip__icon">i</i>
<p class="tool-tip__info">
<span class="info">
<span class="info__title">Setting Bitcoin Core Data Directory<br></span><br>
When auto-detect is on, Specter will check for Environment-Variables (BTC_RPC_USER, BTC_RPC_PASSWORD, BTC_RPC_HOST, BTC_RPC_PORT) to configure the connection or
attempt to automatically locate your Bitcoin data directory and load your node configurations from it.<br><br>
However, if your Bitcoin Core data directory is not located at the default location, you will need to enter its path here so Specter will be able to locate it.<br><br>
If you are connecting to a specific remote node, you can disable the auto-detect feature and enter the node's configurations manually below.
</span>
</p>
</div>
<br><br>
Bitcoin Core data directory path:<br><input type="text" id="datadir" name="datadir" type="text" value="{{ datadir }}">
</span>
<br><br>
<div id="rpc_settings">
<div id="lock" class="center">
<p style="text-align: center; margin: auto; ">&#128274;</p>
<span class="note centered"> (disable auto-detect to configure manually)</span>
</div>
Username:<br><input type="text" id="username" name="username" value="{{ username }}">
<br><br>
Password:<br><input type="password" id="password" name="password" value="{{ password }}">
<br><br>
Host:<br><input type="text" id="host" name="host" type="text" value="{{ protocol }}://{{ host }}">
<br>
<qr-scanner id="scan_rpc" style="margin: 10px;">
<a slot="button" href="#" class="btn" style="padding: 7px 15px 5px 10px;">
<img src="{{ url_for('static', filename='img/qr-code.svg') }}" style="width: 30px; margin: 0px;" class="svg-white">
Connect with QR
</a>
</qr-scanner>
</div>
Port:<br><input type="text" id="port" name="port" type="text" value="{{ port }}">
<div class="note">
Default ports: <b>8332</b> for mainnet, <b>18332</b> for testnet, <b>18443</b> for Regtest, <b>38332</b> for signet
</div>
<br><br>
<div class="row">
<button type="submit" class="btn" name="action" value="test">Test</button>&nbsp;
<button type="submit" class="btn" name="action" value="save">Save</button>
</div>
{% if test %}
<br><div class="log"><b>Test results:</b><br><br>
<div style="display: grid;grid-template-columns: auto auto auto; line-height: 2.2;">
{% macro tick_or_cross(my_boolean) %}
<button style="background: #fff; border: none;" disabled>
{% if my_boolean %}
<div style="color: green; font-size: 1.5em;">&#x2714;</div>
{% else %}
<div style="color: red; font-size: 1.5em;">&#x274C;</div>
{% endif %}
</button>
{% endmacro %}
{% if 'connectable' in test['tests'] %}
<div>Connectable</div>
<div>{{ tick_or_cross(test['tests']['connectable']) }} </div>
{% if not test['tests']['connectable'] %}
<tool-tip title="Your Node can't be reached" style="float: right;">
There are a lot of potential issues preventing the connection.
Please doublecheck the Host and the Port. Make sure you can reach
the host and make sure that Bitcoin-Core is listening at the port
you have specified.
For more hints, please look at this <a style="color:grey" href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/connect-your-node.md" target="_blank">article</a>.
</tool-tip>
{% else %}
<div></div>
{% endif %}
{% endif %}
{% if 'credentials' in test['tests'] %}
<div>Credentials</div>
<div> {{ tick_or_cross(test['tests']['credentials']) }} </div>
{% if not test['tests']['credentials'] %}
<tool-tip title="Your Credentials don't work" style="float: right;">
Please doublecheck the user and the Password. Look at the bitcoin.conf
for the correct values in that field.
For more hints, please look at this <a style="color:grey" href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/connect-your-node.md" target="_blank">article</a>.
</tool-tip>
{% else %}
<div></div>
{% endif %}
{% endif %}
{% if 'recent_version' in test['tests'] %}
<div>Version recent enough</div>
<div> {{ tick_or_cross(test['tests']['recent_version']) }} </div>
{% if not test['tests']['recent_version'] %}
<tool-tip title="Your Core Node might be too old" style="float: right; margin-bottom: 5px;">
Specter is working well up from Bitcoin Core version 0.17.
The version of your Node is too low, unfortunatley.
Please upgrade!
</tool-tip>
{% else %}
<div></div>
{% endif %}
{% endif %}
{% if 'wallets' in test['tests'] %}
<div>Wallets enabled</div>
<div> {{ tick_or_cross(test['tests']['wallets']) }} </div>
{% if not test['tests']['wallets'] %}
<tool-tip title="Wallet Support in your Core-Node" style="float: right; margin-bottom: 5px;">
The RPC-Interface of your Core-Node is available, but the wallet-api is not.
Please make sure to have 'disablewallet=0' in your bitcoin.conf.
</tool-tip>
{% else %}
<div></div>
{% endif %}
{% endif %}
</div>
<br>
<code>
<pre>Process finished with code <b>{{ test.code }}</b>{% if test.code == 0 %} Output: {{ test.out }}{% else %}Error message: {{ test.err }}{% endif %}</pre>
</code>
</div>
{% endif %}
{% endif %}
</div>
</form>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener("DOMContentLoaded", function(){
setNodeTypeView("{{ node_view }}")
var autodetect = document.getElementById("autodetect");
if (autodetect != null) {
function toggleAutoDetectMode() {
var rpcSettings = document.getElementById("rpc_settings");
var scanRpc = document.getElementById("scan_rpc");
var lock = document.getElementById("lock");
var datadirContainer = document.getElementById("datadir-container");
if (autodetect.checked) {
lock.style.removeProperty('display');
datadirContainer.style.removeProperty('display');
rpcSettings.style['pointer-events'] = 'none';
rpcSettings.style['background-color'] = '#8881';
rpcSettings.style['padding'] = '10px';
rpcSettings.style['border-radius'] = '10px';
scanRpc.style.display = 'none';
document.getElementById("username").value = '{{ username }}';
document.getElementById("password").value = '{{ password }}';
} else {
lock.style['display'] = 'none';
datadirContainer.style['display'] = 'none';
rpcSettings.style.removeProperty('background-color');
rpcSettings.style.removeProperty('pointer-events');
rpcSettings.style.removeProperty('padding');
rpcSettings.style.removeProperty('border-radius');
scanRpc.style.removeProperty('display');
}
}
autodetect.addEventListener("change", function() {
toggleAutoDetectMode();
}, false);
toggleAutoDetectMode();
}
});
function setNodeTypeView(nodeType) {
if (nodeType == "external") {
document.getElementById("external_node_view_btn").classList.add('checked');
document.getElementById("internal_node_view_btn").classList.remove('checked');
document.getElementById("external_node_setup_view").classList.remove('hidden');
document.getElementById("internal_node_setup_view").classList.add('hidden');
} else {
document.getElementById("external_node_view_btn").classList.remove('checked');
document.getElementById("internal_node_view_btn").classList.add('checked');
document.getElementById("external_node_setup_view").classList.add('hidden');
document.getElementById("internal_node_setup_view").classList.remove('hidden');
}
}
</script>
<script type="text/javascript">
let scanner = document.getElementById('scan_rpc');
if(scanner != null) {
scanner.addEventListener('scan', e=>{
let result = e.detail.result;
if(result==null){
return;
}
try {
let data = result.split('btcrpc://')[1].split(':')
let username = data[0];
let password = data[1].split('@')[0];
let host = data[1].split('@')[1];
let port = data[2].split('?')[0].split('/')[0];
document.getElementById('username').value = username;
document.getElementById('password').value = password;
document.getElementById('host').value = host;
document.getElementById('port').value = port;
} catch {
showError('Failed to read connection data from the QR', 3000)
}
});
}
</script>
{% endblock %}

View file

@ -8,10 +8,7 @@
#}
{% macro settings_menu(active_menuitem, current_user) -%}
<nav class="row collapse-on-mobile">
{% if current_user.is_admin %}
{{ settings_menu_item('bitcoin_core', 'Bitcoin Core', active_menuitem, isLeft=true) }}
{% endif %}
{{ settings_menu_item('general', 'General', active_menuitem, isLeft=(not current_user.is_admin)) }}
{{ settings_menu_item('general', 'General', active_menuitem, isLeft=true) }}
{{ settings_menu_item('auth', 'Authentication', active_menuitem, isRight=false) }}
{{ settings_menu_item('hwi', 'USB Devices', active_menuitem, isRight=(not current_user.is_admin)) }}
{% if current_user.is_admin %}

View file

@ -10,7 +10,7 @@
<h2 style="text-decoration: underline;">Advanced configurations</h2><br>
<p style="text-align: left;">
Bitcoin Core data directory path:<br>
<input type="text" id="bitcoin_core_datadir" name="bitcoin_core_datadir" type="text" value="{{ specter.config.internal_node.datadir }}">
<input type="text" id="bitcoin_core_datadir" name="bitcoin_core_datadir" type="text" value="{{ specter.node_manager.internal_node.datadir }}">
</p>
</div>
<br><br>

View file

@ -11,7 +11,7 @@
</div>
<br><br>
<div class="row">
<a href="{{url_for('settings_endpoint.bitcoin_core')}}" class="btn wizard-btn">Connect existing node</a>
<a href="{{ url_for('nodes_endpoint.node_settings', node_alias=specter.node.alias) }}" class="btn wizard-btn">Connect existing node</a>
<a href="{{ url_for('setup_endpoint.bitcoind') }}" class="btn wizard-btn action" id="setup-node-btn">Setup a new node</a>
</div>
{% endblock %}

View file

@ -1,8 +1,10 @@
import logging
import subprocess
import platform
import os
import platform
import signal
import subprocess
from io import StringIO
import psutil
logger = logging.getLogger(__name__)
@ -27,11 +29,21 @@ class TorDaemonController:
self.tor_daemon_proc = subprocess.Popen(
f'{"exec " if platform.system() != "Windows" else ""}"{self.tor_daemon_path}" --defaults-torrc {self.tor_config_path}',
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
logger.debug(
"Running tor-daemon process with pid {}".format(self.tor_daemon_proc.pid)
)
def get_logs(self):
logs = ""
newline = self.tor_daemon_proc.stdout.readline().decode("ascii")
while newline != "":
logs = logs + newline
newline = self.tor_daemon_proc.stdout.readline().decode("ascii")
return logs
def get_hashed_password(self, password):
hashed_pw = subprocess.check_output(
f'{"exec " if platform.system() != "Windows" else ""}"{self.tor_daemon_path}" --hash-password {password}',

View file

@ -6,8 +6,8 @@ import json
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):
@ -90,8 +90,8 @@ class User(UserMixin):
return user_dict
def check(self):
self.check_wallet_manager()
self.check_device_manager()
self.check_wallet_manager()
def check_wallet_manager(self):
"""Updates wallet manager for this user"""

View file

@ -46,13 +46,11 @@ def setup_bitcoind_thread(specter=None, internal_bitcoind_version=""):
os.path.join(specter.data_folder, f"bitcoin-{internal_bitcoind_version}"),
bitcoin_binaries_folder,
)
if not os.path.exists(specter.config["internal_node"]["datadir"]):
if not os.path.exists(specter.node_manager.internal_node.datadir):
logger.info(
f"Creating bitcoin datadir: {specter.config['internal_node']['datadir']}"
f"Creating bitcoin datadir: {specter.node_manager.internal_node.datadir}"
)
os.makedirs(specter.config["internal_node"]["datadir"])
specter.config["bitcoind_internal_version"] = internal_bitcoind_version
specter._save()
os.makedirs(specter.node_manager.internal_node.datadir)
specter.reset_setup("bitcoind")
except Exception as e:
logger.error(f"Failed to install Bitcoin Core. Error: {e}")
@ -110,27 +108,27 @@ def setup_bitcoind_directory_thread(specter=None, quicksync=True, pruned=True):
"Failed to verify prunednode.today hash is in SHA265SUMS.asc"
)
logger.info(
f"Unpacking {prunednode_file} to {os.path.expanduser(specter.config['internal_node']['datadir'])}"
f"Unpacking {prunednode_file} to {os.path.expanduser(specter.node_manager.internal_node.datadir)}"
)
with zipfile.ZipFile(prunednode_file, "r") as zip_ref:
zip_ref.extractall(
os.path.expanduser(specter.config["internal_node"]["datadir"])
os.path.expanduser(specter.node_manager.internal_node.datadir)
)
os.remove(prunednode_file)
logger.info(f"Writing bitcoin.conf")
if not os.path.exists(specter.config["internal_node"]["datadir"]):
os.makedirs(specter.config["internal_node"]["datadir"])
if not os.path.exists(specter.node_manager.internal_node.datadir):
os.makedirs(specter.node_manager.internal_node.datadir)
with open(
os.path.join(specter.config["internal_node"]["datadir"], "bitcoin.conf"),
os.path.join(specter.node_manager.internal_node.datadir, "bitcoin.conf"),
"w+",
) as file:
salt = generate_salt(16)
password_hmac = password_to_hmac(
salt, specter.config["internal_node"]["password"]
salt, specter.node_manager.internal_node.password
)
file.write(
f'\nrpcauth={specter.config["internal_node"]["user"]}:{salt}${password_hmac}'
f"\nrpcauth={specter.node_manager.internal_node.user}:{salt}${password_hmac}"
)
file.write(f"\nserver=1")
file.write(f"\nlisten=1")
@ -145,27 +143,13 @@ def setup_bitcoind_directory_thread(specter=None, quicksync=True, pruned=True):
file.write(f"\nblockfilterindex=1")
specter.update_setup_status("bitcoind", "START_SERVICE")
specter.update_use_external_node(False)
# Specter's 'bitcoind' attribute will instantiate a BitcoindController as needed
logger.info(
f"Starting up Bitcoin Core... in {os.path.expanduser(specter.config['internal_node']['datadir'])}"
)
try:
specter.bitcoind.start_bitcoind(
datadir=os.path.expanduser(specter.config["internal_node"]["datadir"])
)
finally:
specter.set_bitcoind_pid(specter.bitcoind.bitcoind_proc.pid)
logger.info("Waiting 15 seconds ...")
time.sleep(15)
success = specter.update_rpc(
port=8332,
autodetect=False,
user=specter.config["internal_node"]["user"],
password=specter.config["internal_node"]["password"],
need_update="true",
f"Starting up Bitcoin Core... in {os.path.expanduser(specter.node_manager.internal_node.datadir)}"
)
success = specter.node_manager.internal_node.start(timeout=60)
specter.update_active_node(specter.node_manager.internal_node.alias)
if not success:
specter.update_setup_status("bitcoind", "FAILED")
logger.info("No success connecting to Bitcoin Core")
@ -174,7 +158,7 @@ def setup_bitcoind_directory_thread(specter=None, quicksync=True, pruned=True):
specter.setup_status["stage"] = "end"
except ExtProcTimeoutException as e:
e.check_logfile(
os.path.join(specter.config["internal_node"]["datadir"], "debug.log")
os.path.join(specter.node_manager.internal_node.datadir, "debug.log")
)
logger.error(f"Failed to setup Bitcoin Core. Error: {e}")
logger.error(e.get_logger_friendly())

View file

@ -13,7 +13,7 @@ from cryptoadvance.specter.bitcoind import (
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
@ -263,6 +263,7 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder):
config = {
"rpc": {
"autodetect": False,
"datadir": "",
"user": bitcoin_regtest.rpcconn.rpcuser,
"password": bitcoin_regtest.rpcconn.rpcpassword,
"port": bitcoin_regtest.rpcconn.rpcport,

View file

@ -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):

View file

@ -9,10 +9,7 @@ def test_ConfigManager(empty_data_folder):
cm = ConfigManager(data_folder=empty_data_folder)
assert os.path.isfile(os.path.join(empty_data_folder, "config.json"))
assert cm.rpc_conf["host"] == "localhost"
assert cm.bitcoin_datadir.endswith(".bitcoin")
cm.set_bitcoind_pid(123)
cm.update_use_external_node(True)
assert cm.data["auth"]["method"] == "none"
# Should probably raise an Exception!
cm.update_auth("muh", 11, 11)
user_mock = Mock()

View file

@ -3,9 +3,9 @@ from decimal import Decimal
from cryptoadvance.specter.helpers import alias, generate_mnemonic
from cryptoadvance.specter.key import Key
from cryptoadvance.specter.rpc import BitcoinRPC
from cryptoadvance.specter.specter import get_rpc, Specter
from cryptoadvance.specter.specter import 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():
@ -14,32 +14,14 @@ def test_alias():
assert alias("Wurst$ 1") == "wurst_1"
@pytest.mark.skip(reason="no idea why this does not pass on gitlab exclusively")
def test_get_rpc(specter_regtest_configured):
specter_regtest_configured.check()
rpc_config_data = {
"autodetect": False,
"user": "bitcoin",
"password": "secret",
"port": specter_regtest_configured.config["rpc"]["port"],
"host": "localhost",
"protocol": "http",
}
print("rpc_config_data: {}".format(rpc_config_data))
rpc = get_rpc(rpc_config_data)
assert rpc.getblockchaininfo()
assert isinstance(rpc, BitcoinRPC)
# ToDo test autodetection-features
def test_specter(specter_regtest_configured, caplog):
caplog.set_level(logging.DEBUG)
specter_regtest_configured.check()
assert specter_regtest_configured.wallet_manager is not None
assert specter_regtest_configured.device_manager is not None
assert specter_regtest_configured.config["rpc"]["host"] != "None"
logging.debug("out {}".format(specter_regtest_configured.test_rpc()))
json_return = json.loads(specter_regtest_configured.test_rpc()["out"])
assert specter_regtest_configured.node.host != "None"
logging.debug("out {}".format(specter_regtest_configured.node.test_rpc()))
json_return = json.loads(specter_regtest_configured.node.test_rpc()["out"])
# that might only work if your chain is fresh
# assert json_return['blocks'] == 100
assert json_return["chain"] == "regtest"
@ -85,6 +67,7 @@ def test_abandon_purged_tx(
config = {
"rpc": {
"autodetect": False,
"datadir": "",
"user": rpcconn.rpcuser,
"password": rpcconn.rpcpassword,
"port": rpcconn.rpcport,
@ -98,8 +81,7 @@ def test_abandon_purged_tx(
specter = Specter(data_folder=devices_filled_data_folder, config=config)
specter.check()
specter.check_node_info()
assert specter._info["mempool_info"]["maxmempool"] == 5 * 1000 * 1000 # 5MB
assert specter.info["mempool_info"]["maxmempool"] == 5 * 1000 * 1000 # 5MB
# Largely copy-and-paste from test_wallet_manager.test_wallet_createpsbt.
# TODO: Make a test fixture in conftest.py that sets up already funded wallets

View file

@ -4,7 +4,7 @@ from cryptoadvance.specter.rpc import RpcError
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):