mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
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:
parent
d858d07f37
commit
7ac1f2bd7b
46 changed files with 1673 additions and 3054 deletions
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
2074
package-lock.json
generated
File diff suppressed because it is too large
Load diff
148
src/cryptoadvance/specter/internal_node.py
Normal file
148
src/cryptoadvance/specter/internal_node.py
Normal 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()
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
||||
192
src/cryptoadvance/specter/managers/node_manager.py
Normal file
192
src/cryptoadvance/specter/managers/node_manager.py
Normal 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]
|
||||
|
|
@ -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__)
|
||||
|
|
@ -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()
|
||||
|
||||
350
src/cryptoadvance/specter/node.py
Normal file
350
src/cryptoadvance/specter/node.py
Normal 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)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
334
src/cryptoadvance/specter/server_endpoints/nodes.py
Normal file
334
src/cryptoadvance/specter/server_endpoints/nodes.py
Normal 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))
|
||||
|
|
@ -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"]:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
6
src/cryptoadvance/specter/static/img/flip-horizontal.svg
Normal file
6
src/cryptoadvance/specter/static/img/flip-horizontal.svg
Normal 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 |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -171,9 +171,4 @@
|
|||
hidePageOverlay();
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: remove, legacy
|
||||
function onCancelOverlay(){
|
||||
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -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>
|
||||
{% 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 %}
|
||||
219
src/cryptoadvance/specter/templates/node/node_settings.jinja
Normal file
219
src/cryptoadvance/specter/templates/node/node_settings.jinja
Normal 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: </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; ">🔒</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>
|
||||
<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;">✔</div>
|
||||
{% else %}
|
||||
<div style="color: red; font-size: 1.5em;">❌</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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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: </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; ">🔒</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>
|
||||
<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;">✔</div>
|
||||
{% else %}
|
||||
<div style="color: red; font-size: 1.5em;">❌</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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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}',
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue