mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
Chore: Moving the Spectrum extension to a core extension (#2011)
* Adding spectrum without tests * added spectrum ext specific tests * fix reflection test
This commit is contained in:
parent
77d27cf8af
commit
ef4012d4b1
27 changed files with 1778 additions and 1 deletions
0
src/cryptoadvance/specterext/spectrum/__init__.py
Normal file
0
src/cryptoadvance/specterext/spectrum/__init__.py
Normal file
107
src/cryptoadvance/specterext/spectrum/bridge_rpc.py
Normal file
107
src/cryptoadvance/specterext/spectrum/bridge_rpc.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import datetime
|
||||
import errno
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
from cryptoadvance.specter.helpers import is_ip_private
|
||||
from cryptoadvance.specter.specter_error import SpecterError, handle_exception
|
||||
from cryptoadvance.specter.rpc import BitcoinRPC
|
||||
from cryptoadvance.specter.rpc import RpcError as SpecterRpcError
|
||||
from cryptoadvance.spectrum.spectrum import RPCError as SpectrumRpcError
|
||||
from cryptoadvance.specter.specter_error import BrokenCoreConnectionException
|
||||
|
||||
from cryptoadvance.spectrum.spectrum import Spectrum
|
||||
|
||||
from flask import has_app_context
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO: redefine __dir__ and help
|
||||
|
||||
|
||||
class BridgeRPC(BitcoinRPC):
|
||||
"""A class which behaves like a BitcoinRPC but internally bridges to Spectrum.jsonrpc"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
spectrum,
|
||||
app=None,
|
||||
wallet_name=None,
|
||||
):
|
||||
self.spectrum: Spectrum = spectrum
|
||||
self.wallet_name = wallet_name
|
||||
self._app = app
|
||||
|
||||
def wallet(self, name=""):
|
||||
return type(self)(
|
||||
self.spectrum,
|
||||
wallet_name=name,
|
||||
)
|
||||
|
||||
def clone(self):
|
||||
"""
|
||||
Returns a clone of self.
|
||||
Useful if you want to mess with the properties
|
||||
"""
|
||||
return self.__class__(self, self.spectrum, wallet=self.wallet)
|
||||
|
||||
def multi(self, calls: list, **kwargs):
|
||||
"""Makes batch request to Core"""
|
||||
if self.spectrum is None:
|
||||
raise BrokenCoreConnectionException
|
||||
type(self).counter += len(calls)
|
||||
headers = {"content-type": "application/json"}
|
||||
payload = [
|
||||
{
|
||||
"method": method,
|
||||
"params": args if args != [None] else [],
|
||||
"jsonrpc": "2.0",
|
||||
"id": i,
|
||||
}
|
||||
for i, (method, *args) in enumerate(calls)
|
||||
]
|
||||
timeout = self.timeout
|
||||
if "timeout" in kwargs:
|
||||
timeout = kwargs["timeout"]
|
||||
|
||||
if kwargs.get("no_wait"):
|
||||
# Zero is treated like None, i.e. infinite wait
|
||||
timeout = 0.001
|
||||
|
||||
# Spectrum uses a DB and access to it needs an app-context. In order to keep that implementation
|
||||
# detail within spectrum, we're establishing a context as needed.
|
||||
try:
|
||||
if not has_app_context() and self._app is not None:
|
||||
with self._app.app_context():
|
||||
result = [
|
||||
self.spectrum.jsonrpc(
|
||||
item, wallet_name=self.wallet_name, catch_exceptions=False
|
||||
)
|
||||
for item in payload
|
||||
]
|
||||
else:
|
||||
result = [
|
||||
self.spectrum.jsonrpc(
|
||||
item, wallet_name=self.wallet_name, catch_exceptions=False
|
||||
)
|
||||
for item in payload
|
||||
]
|
||||
return result
|
||||
|
||||
except ValueError as ve:
|
||||
mock_response = object()
|
||||
mock_response.status_code = 500
|
||||
mock_response.text = ve
|
||||
raise SpecterRpcError(f"Request error: {ve}", mock_response)
|
||||
except SpectrumRpcError as se:
|
||||
raise SpecterRpcError(
|
||||
str(se), status_code=500, error_code=se.code, error_msg=se.message
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<BridgeRPC {self.spectrum}>"
|
||||
94
src/cryptoadvance/specterext/spectrum/config.py
Normal file
94
src/cryptoadvance/specterext/spectrum/config.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
""" A config module contains static configuration """
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import datetime
|
||||
import secrets
|
||||
from flask import current_app as app
|
||||
from cryptoadvance.specter.config import _get_bool_env_var
|
||||
|
||||
try:
|
||||
# Python 2.7
|
||||
import ConfigParser as configparser
|
||||
except ImportError:
|
||||
# Python 3
|
||||
import configparser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseConfig(object):
|
||||
"""Base configuration. Does not allow e.g. SECRET_KEY, so redefining here"""
|
||||
|
||||
USERNAME = "admin"
|
||||
SPECTRUM_DATADIR = "data" # used for sqlite but also for txs-cache
|
||||
|
||||
# The prepopulated options
|
||||
ELECTRUM_OPTIONS = {
|
||||
"electrum.emzy.de": {"host": "electrum.emzy.de", "port": 50002, "ssl": True},
|
||||
"electrum.blockstream.info": {
|
||||
"host": "electrum.blockstream.info",
|
||||
"port": 50002,
|
||||
"ssl": True,
|
||||
},
|
||||
}
|
||||
|
||||
# The one which is chosen at startup
|
||||
ELECTRUM_DEFAULT_OPTION = "electrum.emzy.de"
|
||||
|
||||
|
||||
# Level 1: How does persistence work?
|
||||
# Convention: BlaConfig
|
||||
|
||||
|
||||
class LiteConfig(BaseConfig):
|
||||
# The Folder to store the DB into is chosen here NOT to be spectrum-extension specific.
|
||||
# We're using Flask-Sqlalchemy and so we can only use one DB per App so we assume that
|
||||
# the DB is shared between different Extensions.
|
||||
# Instead, the tables are all prefixed with "spectrum_"
|
||||
# ToDo: separate the other stuff /txs) in a separate directory
|
||||
|
||||
# SPECTRUM_DATADIR cannot specified here as the app.config would throw a RuntimeError: Working outside of application context.
|
||||
# So this key need to be defined in service.callback_after_serverpy_init_app
|
||||
# SPECTRUM_DATADIR=os.path.join(app.config["SPECTER_DATA_FOLDER"], "sqlite")
|
||||
# DATABASE=os.path.abspath(os.path.join(SPECTRUM_DATADIR, "db.sqlite"))
|
||||
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + DATABASE
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
|
||||
# Level 2: Where do we get an electrum from ?
|
||||
# Convention: Prefix a level 1 config with the electrum solution
|
||||
class NigiriLocalElectrumLiteConfig(LiteConfig):
|
||||
ELECTRUM_HOST = "127.0.0.1"
|
||||
ELECTRUM_PORT = 50000
|
||||
ELECTRUM_USES_SSL = _get_bool_env_var("ELECTRUM_USES_SSL", default="false")
|
||||
|
||||
|
||||
class EmzyElectrumLiteConfig(LiteConfig):
|
||||
ELECTRUM_HOST = os.environ.get("ELECTRUM_HOST", default="electrum.emzy.de")
|
||||
ELECTRUM_PORT = int(os.environ.get("ELECTRUM_PORT", default="50002"))
|
||||
ELECTRUM_USES_SSL = _get_bool_env_var("ELECTRUM_USES_SSL", default="true")
|
||||
|
||||
|
||||
# Level 3: Back to the problem-Space.
|
||||
# Convention: ProblemConfig where problem is usually one of Test/Production or so
|
||||
|
||||
|
||||
class TestConfig(NigiriLocalElectrumLiteConfig):
|
||||
pass
|
||||
|
||||
|
||||
class DevelopmentConfig(EmzyElectrumLiteConfig):
|
||||
pass
|
||||
|
||||
|
||||
class Development2Config(EmzyElectrumLiteConfig):
|
||||
ELECTRUM_HOST = os.environ.get("ELECTRUM_HOST", default="kirsche.emzy.de")
|
||||
ELECTRUM_PORT = int(os.environ.get("ELECTRUM_PORT", default="50002"))
|
||||
ELECTRUM_USES_SSL = _get_bool_env_var("ELECTRUM_USES_SSL", default="true")
|
||||
|
||||
|
||||
class ProductionConfig(EmzyElectrumLiteConfig):
|
||||
"""Not sure whether we're production ready, though"""
|
||||
|
||||
pass
|
||||
168
src/cryptoadvance/specterext/spectrum/controller.py
Normal file
168
src/cryptoadvance/specterext/spectrum/controller.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import logging
|
||||
from flask import redirect, render_template, request, url_for, flash
|
||||
from flask import current_app as app
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from cryptoadvance.specter.services.controller import user_secret_decrypted_required
|
||||
from cryptoadvance.specter.user import User
|
||||
from cryptoadvance.specter.wallet import Wallet
|
||||
from cryptoadvance.specter.specter_error import SpecterError
|
||||
|
||||
from cryptoadvance.specterext.spectrum.spectrum_node import SpectrumNode
|
||||
from .service import SpectrumService
|
||||
from .controller_helpers import (
|
||||
ext,
|
||||
specter,
|
||||
evaluate_current_status,
|
||||
check_for_node_on_same_network,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
spectrum_endpoint = SpectrumService.blueprint
|
||||
|
||||
|
||||
@spectrum_endpoint.route("/")
|
||||
@login_required
|
||||
def index(node_alias=None):
|
||||
if node_alias is not None and node_alias != "spectrum_node":
|
||||
raise SpecterError(f"Unknown Spectrum Node: {node_alias}")
|
||||
return render_template(
|
||||
"spectrum/index.jinja",
|
||||
)
|
||||
|
||||
|
||||
@spectrum_endpoint.route("node/<node_alias>/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def node_settings(node_alias=None):
|
||||
if node_alias is not None and node_alias != "spectrum_node":
|
||||
raise SpecterError(f"Unknown Spectrum Node: {node_alias}")
|
||||
return redirect(url_for("spectrum_endpoint.settings_get"))
|
||||
|
||||
|
||||
@spectrum_endpoint.route("/settings", methods=["GET"])
|
||||
@login_required
|
||||
def settings_get():
|
||||
# Show current configuration
|
||||
if ext().id in specter().user.services:
|
||||
show_menu = "yes"
|
||||
else:
|
||||
show_menu = "no"
|
||||
electrum_options = app.config["ELECTRUM_OPTIONS"]
|
||||
elec_chosen_option = "manual"
|
||||
spectrum_node: SpectrumNode = ext().spectrum_node
|
||||
if spectrum_node is not None:
|
||||
host = spectrum_node.host
|
||||
port = spectrum_node.port
|
||||
ssl = spectrum_node.ssl
|
||||
for opt_key, elec in electrum_options.items():
|
||||
if elec["host"] == host and elec["port"] == port and elec["ssl"] == ssl:
|
||||
elec_chosen_option = opt_key
|
||||
return render_template(
|
||||
"spectrum/settings.jinja",
|
||||
elec_options=electrum_options,
|
||||
elec_chosen_option=elec_chosen_option,
|
||||
host=host,
|
||||
port=port,
|
||||
ssl=ssl,
|
||||
show_menu=show_menu,
|
||||
)
|
||||
else:
|
||||
return render_template(
|
||||
"spectrum/settings.jinja",
|
||||
elec_options=electrum_options,
|
||||
elec_chosen_option="list",
|
||||
show_menu=show_menu,
|
||||
)
|
||||
|
||||
|
||||
@spectrum_endpoint.route("/settings", methods=["POST"])
|
||||
@login_required
|
||||
def settings_post():
|
||||
# Node status before saving the settings
|
||||
node_is_running_before_request = False
|
||||
host_before_request = None
|
||||
if ext().is_spectrum_node_available:
|
||||
node_is_running_before_request = ext().spectrum_node.is_running
|
||||
host_before_request = ext().spectrum_node.host
|
||||
logger.debug(f"The host before saving the new settings: {host_before_request}")
|
||||
logger.debug(
|
||||
f"Node running before updating settings: {node_is_running_before_request}"
|
||||
)
|
||||
|
||||
# Gather the Electrum server settings from the form and update with them
|
||||
success = False
|
||||
host = request.form.get("host")
|
||||
try:
|
||||
port = int(request.form.get("port"))
|
||||
except ValueError:
|
||||
port = 0
|
||||
ssl = request.form.get("ssl") == "on"
|
||||
option_mode = request.form.get("option_mode")
|
||||
electrum_options = app.config["ELECTRUM_OPTIONS"]
|
||||
elec_option = request.form.get("elec_option")
|
||||
if option_mode == "list":
|
||||
host = electrum_options[elec_option]["host"]
|
||||
port = electrum_options[elec_option]["port"]
|
||||
ssl = electrum_options[elec_option]["ssl"]
|
||||
# If there is already a Spectrum node, just update with the new values (restarts Spectrum)
|
||||
if ext().is_spectrum_node_available:
|
||||
ext().update_electrum(host, port, ssl)
|
||||
# Otherwise, create the Spectrum node and then start Spectrum
|
||||
else:
|
||||
ext().enable_spectrum(host, port, ssl, activate_spectrum_node=False)
|
||||
# Make the Spectrum node the new active node and save it to disk, but only if the connection is working"""
|
||||
# BETA_VERSION: Additional check that there is no Bitcoin Core node for the same network alongside the Spectrum node
|
||||
spectrum_node = ext().spectrum_node
|
||||
|
||||
if check_for_node_on_same_network(spectrum_node, specter()):
|
||||
# Delete Spectrum node again (it wasn't saved to disk yet)
|
||||
del specter().node_manager.nodes[spectrum_node.alias]
|
||||
return render_template(
|
||||
"spectrum/spectrum_setup_beta.jinja", core_node_exists=True
|
||||
)
|
||||
|
||||
if ext().spectrum_node.is_running:
|
||||
logger.debug("Activating Spectrum node ...")
|
||||
ext().activate_spectrum_node()
|
||||
success = True
|
||||
|
||||
# Set the menu item
|
||||
show_menu = request.form["show_menu"]
|
||||
user = specter().user_manager.get_user()
|
||||
if show_menu == "yes":
|
||||
user.add_service(ext().id)
|
||||
else:
|
||||
user.remove_service(ext().id)
|
||||
|
||||
# Determine changes for better feedback message in the jinja template
|
||||
logger.debug(f"Node running after updating settings: {success}")
|
||||
host_after_request = ext().spectrum_node.host
|
||||
logger.debug(f"The host after saving the new settings: {host_after_request}")
|
||||
|
||||
if (
|
||||
node_is_running_before_request == success
|
||||
and success == True
|
||||
and host_before_request == host_after_request
|
||||
):
|
||||
# Case 1: We changed a setting that didn't impact the Spectrum node, currently only the menu item setting
|
||||
return redirect(
|
||||
url_for(f"{ SpectrumService.get_blueprint_name()}.settings_get")
|
||||
)
|
||||
|
||||
changed_host, check_port_and_ssl = evaluate_current_status(
|
||||
node_is_running_before_request,
|
||||
success,
|
||||
host_before_request,
|
||||
host_after_request,
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"spectrum/spectrum_setup.jinja",
|
||||
success=success,
|
||||
node_is_running_before_request=node_is_running_before_request,
|
||||
changed_host=changed_host,
|
||||
host_type=option_mode,
|
||||
check_port_and_ssl=check_port_and_ssl,
|
||||
)
|
||||
83
src/cryptoadvance/specterext/spectrum/controller_helpers.py
Normal file
83
src/cryptoadvance/specterext/spectrum/controller_helpers.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import logging
|
||||
|
||||
from cryptoadvance.specter.specter import Specter
|
||||
from flask import current_app as app
|
||||
|
||||
from .service import SpectrumService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def ext() -> SpectrumService:
|
||||
"""convenience for getting the extension-object"""
|
||||
return app.specter.ext["spectrum"]
|
||||
|
||||
|
||||
def specter() -> Specter:
|
||||
"""convenience for getting the specter-object"""
|
||||
return app.specter
|
||||
|
||||
|
||||
def check_for_node_on_same_network(spectrum_node, specter: Specter):
|
||||
if spectrum_node is not None:
|
||||
current_spectrum_chain = spectrum_node.chain
|
||||
nodes_current_chain = specter.node_manager.nodes_by_chain(
|
||||
current_spectrum_chain
|
||||
)
|
||||
# Check whether there is a Bitcoin Core node for the same network:
|
||||
core_node_exists = False
|
||||
for node in nodes_current_chain:
|
||||
logger.debug(node)
|
||||
if (
|
||||
node.fqcn
|
||||
!= "cryptoadvance.specterext.spectrum.spectrum_node.SpectrumNode"
|
||||
and not node.is_liquid
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def evaluate_current_status(
|
||||
node_is_running_before_request, success, host_before_request, host_after_request
|
||||
):
|
||||
"""Figures out whether the:
|
||||
* the user changed the host and/or
|
||||
* the user changed the port/ssl
|
||||
and returns two booleans: changed_host, check_port_and_ssl
|
||||
useful for user-feedback.
|
||||
"""
|
||||
changed_host = False
|
||||
check_port_and_ssl = False
|
||||
if (
|
||||
node_is_running_before_request == success
|
||||
and success == True
|
||||
and host_before_request != host_after_request
|
||||
):
|
||||
# Case 2: We changed the host but switched from one working connection to another one
|
||||
changed_host = True
|
||||
if node_is_running_before_request and not success:
|
||||
# Case 3: We changed the host from a working to a broken connection
|
||||
if host_before_request != host_after_request:
|
||||
changed_host = True
|
||||
# Case 4: We didn't change the host but probably other configs such as port and / or ssl which are likely the reason for the broken connection
|
||||
# TODO: Worth it to also check for changes in the port / ssl configs?
|
||||
else:
|
||||
check_port_and_ssl = True
|
||||
if not node_is_running_before_request and success:
|
||||
# Case 5: We changed the host from a broken to a working connection
|
||||
if host_before_request != host_after_request and host_before_request != None:
|
||||
changed_host = True
|
||||
# Case 6: We didn't change the host but only the port and / or ssl config which did the trick
|
||||
else:
|
||||
# Not necessary since this is set to False by default, just to improve readability
|
||||
check_port_and_ssl = False
|
||||
if not node_is_running_before_request and not success:
|
||||
# Case 7: We don't get a connection running for the current host, perhaps it is due to the port / ssl
|
||||
if host_before_request == host_after_request and host_before_request != None:
|
||||
check_port_and_ssl = True
|
||||
# Case 7: Unclear what the issue is, best to check everything
|
||||
if host_before_request != host_after_request:
|
||||
changed_host = True
|
||||
check_port_and_ssl = True
|
||||
|
||||
return changed_host, check_port_and_ssl
|
||||
155
src/cryptoadvance/specterext/spectrum/service.py
Normal file
155
src/cryptoadvance/specterext/spectrum/service.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from cryptoadvance.specter.managers.node_manager import NodeManager
|
||||
from cryptoadvance.specter.services.service import (
|
||||
Service,
|
||||
devstatus_prod,
|
||||
devstatus_alpha,
|
||||
)
|
||||
|
||||
# A SpecterError can be raised and will be shown to the user as a red banner
|
||||
from cryptoadvance.specter.specter_error import SpecterError
|
||||
from flask import current_app as app
|
||||
from flask import url_for
|
||||
from flask_apscheduler import APScheduler
|
||||
from cryptoadvance.specterext.spectrum.spectrum_node import SpectrumNode
|
||||
from cryptoadvance.spectrum.server import init_app, Spectrum
|
||||
from cryptoadvance.spectrum.db import db
|
||||
from cryptoadvance.specter.specter_error import BrokenCoreConnectionException
|
||||
from cryptoadvance.specter.server_endpoints.welcome.welcome_vm import WelcomeVm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
spectrum_node_alias = "spectrum_node"
|
||||
|
||||
|
||||
class SpectrumService(Service):
|
||||
id = "spectrum"
|
||||
name = "Spectrum"
|
||||
icon = "spectrum/img/logo.svg"
|
||||
logo = "spectrum/img/logo.svg"
|
||||
desc = "An electrum hidden behind a core API"
|
||||
has_blueprint = True
|
||||
blueprint_module = "cryptoadvance.specterext.spectrum.controller"
|
||||
devstatus = devstatus_alpha
|
||||
isolated_client = False
|
||||
|
||||
# TODO: As more Services are integrated, we'll want more robust categorization and sorting logic
|
||||
sort_priority = 2
|
||||
|
||||
@property
|
||||
def spectrum_node(self):
|
||||
"""Iterates all nodes and returns the spectrum Node or None if it doesn't exist"""
|
||||
for node in app.specter.node_manager.nodes.values():
|
||||
if (
|
||||
node.fqcn
|
||||
== "cryptoadvance.specterext.spectrum.spectrum_node.SpectrumNode"
|
||||
):
|
||||
return node
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_spectrum_node_available(self):
|
||||
"""Whether there is a spectrum Node available (activated or not)"""
|
||||
return not self.spectrum_node is None
|
||||
|
||||
def callback_specter_added_to_flask_app(self):
|
||||
logger.debug("Setting up Spectrum ...")
|
||||
# See comments in config.py which would be the natural place to define SPECTRUM_DATADIR
|
||||
# but we want to avoid RuntimeError: Working outside of application context.
|
||||
app.config["SPECTRUM_DATADIR"] = os.path.join(
|
||||
app.config["SPECTER_DATA_FOLDER"], "sqlite"
|
||||
)
|
||||
app.config["DATABASE"] = os.path.abspath(
|
||||
os.path.join(app.config["SPECTRUM_DATADIR"], "db.sqlite")
|
||||
)
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + app.config["DATABASE"]
|
||||
|
||||
if not os.path.exists(app.config["SPECTRUM_DATADIR"]):
|
||||
os.makedirs(app.config["SPECTRUM_DATADIR"])
|
||||
logger.info(
|
||||
f"Intitializing Database in {app.config['SQLALCHEMY_DATABASE_URI']}"
|
||||
)
|
||||
db.init_app(app)
|
||||
db.create_all()
|
||||
# Check whether there is a Spectrum node in the node manager of Specter
|
||||
if self.is_spectrum_node_available:
|
||||
try:
|
||||
self.spectrum_node.start_spectrum(app, self.data_folder)
|
||||
except BrokenCoreConnectionException as e:
|
||||
logger.error(e)
|
||||
|
||||
# TODO: Refactor this or the next function to only have one
|
||||
def enable_default_spectrum(self):
|
||||
"""* Creates and saves a Spectrum node if there is none with the default config values ("ELECTRUM_DEFAULT_OPTION")
|
||||
* Starts Spectrum
|
||||
* Switches to the Spectrum node
|
||||
"""
|
||||
if not self.is_spectrum_node_available:
|
||||
# No SpectrumNode yet created. Let's do that.
|
||||
default_electrum = app.config["ELECTRUM_DEFAULT_OPTION"]
|
||||
spectrum_node = SpectrumNode(
|
||||
host=app.config["ELECTRUM_OPTIONS"][default_electrum]["host"],
|
||||
port=app.config["ELECTRUM_OPTIONS"][default_electrum]["port"],
|
||||
ssl=app.config["ELECTRUM_OPTIONS"][default_electrum]["ssl"],
|
||||
)
|
||||
app.specter.node_manager.nodes[spectrum_node_alias] = spectrum_node
|
||||
app.specter.node_manager.save_node(spectrum_node)
|
||||
self.spectrum_node.start_spectrum(app, self.data_folder)
|
||||
self.activate_spectrum_node()
|
||||
|
||||
def enable_spectrum(self, host, port, ssl, activate_spectrum_node=False):
|
||||
"""* Creates a Spectrum node if there is none
|
||||
* Starts Spectrum
|
||||
* Does by default NOT yet switch to the Spectrum node nor yet save the node to disk
|
||||
"""
|
||||
if not self.is_spectrum_node_available:
|
||||
# No SpectrumNode yet created. Let's do that.
|
||||
logger.debug("Creating a Spectrum node ...")
|
||||
spectrum_node = SpectrumNode(host=host, port=port, ssl=ssl)
|
||||
app.specter.node_manager.nodes[spectrum_node_alias] = spectrum_node
|
||||
self.spectrum_node.start_spectrum(app, self.data_folder)
|
||||
if activate_spectrum_node:
|
||||
self.activate_spectrum_node()
|
||||
|
||||
def disable_spectrum(self):
|
||||
"""Stops Spectrum and deletes the Spectrum node"""
|
||||
self.spectrum_node.stop_spectrum()
|
||||
spectrum_node = None
|
||||
if self.is_spectrum_node_available:
|
||||
app.specter.node_manager.delete_node(self.spectrum_node, app.specter)
|
||||
logger.info("Spectrum disabled")
|
||||
|
||||
def update_electrum(self, host, port, ssl):
|
||||
if not self.is_spectrum_node_available:
|
||||
raise Exception("No Spectrum node available. Cannot start Spectrum.")
|
||||
logger.info(f"Updating Spectrum node with {host}:{port} (ssl: {ssl})")
|
||||
self.spectrum_node.update_electrum(host, port, ssl, app, self.data_folder)
|
||||
|
||||
def activate_spectrum_node(self):
|
||||
"""Makes the Spectrum node the new active node and saves it to disk"""
|
||||
logger.info("Activating Spectrum node.")
|
||||
if not self.is_spectrum_node_available:
|
||||
raise Exception("Spectrum is not enabled. Cannot start Electrum")
|
||||
nm: NodeManager = app.specter.node_manager
|
||||
if self.spectrum_node.is_running:
|
||||
app.specter.update_active_node(spectrum_node_alias)
|
||||
app.specter.node_manager.save_node(self.spectrum_node)
|
||||
logger.info(
|
||||
f"Activated node {self.spectrum_node} with rpc {self.spectrum_node.rpc}"
|
||||
)
|
||||
else:
|
||||
raise SpecterError(
|
||||
"Trying to switch Spectrum node but there seems to be a connection problem."
|
||||
)
|
||||
|
||||
def callback_adjust_view_model(self, view_model: WelcomeVm):
|
||||
if view_model.__class__.__name__ == "WelcomeVm":
|
||||
# potentially, we could make a reidrect here:
|
||||
# view_model.about_redirect=url_for("spectrum_endpoint.some_enpoint_here")
|
||||
# but we do it small here and only replace a specific component:
|
||||
view_model.get_started_include = (
|
||||
"spectrum/welcome/components/get_started.jinja"
|
||||
)
|
||||
return view_model
|
||||
210
src/cryptoadvance/specterext/spectrum/spectrum_node.py
Normal file
210
src/cryptoadvance/specterext/spectrum/spectrum_node.py
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import logging
|
||||
from cryptoadvance.specterext.spectrum.bridge_rpc import BridgeRPC
|
||||
from cryptoadvance.specter.helpers import deep_update
|
||||
from cryptoadvance.specter.node import AbstractNode
|
||||
from cryptoadvance.specter.devices.bitcoin_core import BitcoinCore
|
||||
from cryptoadvance.specter.specter_error import BrokenCoreConnectionException
|
||||
from cryptoadvance.spectrum.spectrum import Spectrum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SpectrumNode(AbstractNode):
|
||||
"""A Node implementation which returns a bridge_rpc class to connect to a spectrum"""
|
||||
|
||||
external_node = True
|
||||
# logging.getLogger("cryptoadvance.spectrum.spectrum").setLevel(logging.INFO)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name="Spectrum Node",
|
||||
alias="spectrum_node",
|
||||
spectrum=None,
|
||||
bridge=None,
|
||||
host=None,
|
||||
port=None,
|
||||
ssl=None,
|
||||
):
|
||||
self._spectrum = spectrum
|
||||
self.bridge = bridge
|
||||
self.name = "Spectrum Node"
|
||||
self.alias = "spectrum_node" # used for the file: nodes/spectrum_node.json
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._ssl = ssl
|
||||
|
||||
# ToDo: Should not be necessary
|
||||
self._rpc = None
|
||||
|
||||
@property
|
||||
def datadir(self):
|
||||
"""Spectrum doesn't need or have a datadirectory but the deletion-process is demanding to have one
|
||||
"" is a magic-value which prevents stupid things to happen until we have refactored that process
|
||||
"""
|
||||
return ""
|
||||
|
||||
def start_spectrum(self, app, datadir):
|
||||
if self._host is None or self._port is None or self._ssl is None:
|
||||
raise BrokenCoreConnectionException(
|
||||
f"Cannot start Spectrum without host ({self._host}), port ({self._port}) or ssl ({self._ssl})"
|
||||
)
|
||||
try:
|
||||
logger.debug(f"Spectrum node is creating a Spectrum instance.")
|
||||
self.spectrum = Spectrum(
|
||||
self._host,
|
||||
self._port,
|
||||
self._ssl,
|
||||
datadir=datadir,
|
||||
app=app,
|
||||
)
|
||||
logger.debug(f"{self.name} is instantiating its BridgeRPC.")
|
||||
self.bridge = BridgeRPC(self.spectrum, app=app)
|
||||
self.spectrum.sync()
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
def stop_spectrum(self):
|
||||
if self.spectrum:
|
||||
self.spectrum.stop()
|
||||
self.spectrum = None
|
||||
|
||||
def update_electrum(self, host, port, ssl, app, datadir):
|
||||
if host is None or port is None or ssl is None:
|
||||
raise BrokenCoreConnectionException(
|
||||
f"Cannot start Spectrum without host ({host}), port ({port}) or ssl ({ssl})"
|
||||
)
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._ssl = ssl
|
||||
self.stop_spectrum()
|
||||
self.start_spectrum(app, datadir)
|
||||
|
||||
# TODO fullpath is not implemented which is necessary to delete the node
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, node_dict, *args, **kwargs):
|
||||
"""Create a Node from json"""
|
||||
name = node_dict.get("name", "")
|
||||
alias = node_dict.get("alias", "")
|
||||
host = node_dict.get("host", None)
|
||||
port = node_dict.get("port", None)
|
||||
ssl = node_dict.get("ssl", None)
|
||||
|
||||
return cls(name, alias, host=host, port=port, ssl=ssl)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
"""Get a json-representation of this Node"""
|
||||
node_json = super().json
|
||||
return deep_update(
|
||||
node_json,
|
||||
{
|
||||
"name": self.name,
|
||||
"alias": self.alias,
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"ssl": self.ssl,
|
||||
},
|
||||
)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
if self.spectrum:
|
||||
return self.spectrum.is_connected()
|
||||
else:
|
||||
# If there is no Spectrum object, there can't be a (socket) connection
|
||||
return False
|
||||
|
||||
def check_blockheight(self):
|
||||
"""This naive implementation always returns True: Claiming that new blocks have arrived, we're forcing
|
||||
the caller to always recalculate everything.
|
||||
That's possible because calling those rpc-calls on spectrum's side is cheap.
|
||||
It might not be cheap on Specter's side but that's for Specter to optimize!
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def spectrum(self):
|
||||
"""Returns None if the Spectrum node has no Spectrum object"""
|
||||
if self._spectrum:
|
||||
return self._spectrum
|
||||
else:
|
||||
return None
|
||||
|
||||
@spectrum.setter
|
||||
def spectrum(self, value):
|
||||
self._spectrum = value
|
||||
if self._spectrum is not None:
|
||||
self._rpc = BridgeRPC(self.spectrum)
|
||||
else:
|
||||
self._rpc = None
|
||||
|
||||
@property
|
||||
def bridge(self):
|
||||
return self._bridge
|
||||
|
||||
@bridge.setter
|
||||
def bridge(self, value):
|
||||
self._bridge = value
|
||||
if self._bridge is not None:
|
||||
self._rpc = self._bridge
|
||||
else:
|
||||
logger.debug(f"No BridgeRPC for Spectrum node, setting rpc to None ...")
|
||||
self._rpc = None
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
if self.spectrum:
|
||||
return self.spectrum.host
|
||||
return self._host
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
if self.spectrum:
|
||||
return self.spectrum.port
|
||||
return self._port
|
||||
|
||||
@property
|
||||
def ssl(self):
|
||||
if self.spectrum:
|
||||
return self.spectrum.ssl
|
||||
return self._ssl
|
||||
|
||||
@property
|
||||
def rpc(self):
|
||||
if self._rpc is None:
|
||||
self._rpc = BridgeRPC(
|
||||
self.spectrum
|
||||
) # TODO: If "app" is used for BridgeRPC in the end, it is missing here. Also, better to use the bridge setter perhaps?
|
||||
return self._rpc
|
||||
|
||||
def get_rpc(self):
|
||||
"""
|
||||
return ta BridgeRPC
|
||||
"""
|
||||
return self.rpc
|
||||
|
||||
def update_rpc(self):
|
||||
"""No need to do anything"""
|
||||
pass
|
||||
|
||||
def is_device_supported(self, device_class_or_device_instance):
|
||||
"""Returns False if a device is not supported for Spectrum nodes, True otherwise.
|
||||
Currently, Bitcoin Core hot wallets are not supported"""
|
||||
# If a device class is passed as argument, take that, otherwise derive the class from the instance
|
||||
if device_class_or_device_instance.__class__ == type:
|
||||
device_class = device_class_or_device_instance
|
||||
else:
|
||||
device_class = device_class_or_device_instance.__class__
|
||||
if device_class == BitcoinCore:
|
||||
return False
|
||||
return True
|
||||
|
||||
def node_info_template(self):
|
||||
return "spectrum/components/spectrum_info.jinja"
|
||||
|
||||
def node_logo_template(self):
|
||||
return "spectrum/components/spectrum_node_logo.jinja"
|
||||
|
||||
def node_connection_template(self):
|
||||
return "spectrum/components/spectrum_node_connection.jinja"
|
||||
|
|
@ -0,0 +1 @@
|
|||
/* This is the place to put all your styles */
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
width="779.8111"
|
||||
height="779.8111"
|
||||
id="svg2"
|
||||
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)"
|
||||
sodipodi:docname="electrum_lightblue.svg"
|
||||
inkscape:export-filename="/home/voegtlin/logos/electrum_blue.png"
|
||||
inkscape:export-xdpi="10.014582"
|
||||
inkscape:export-ydpi="10.014582">
|
||||
<metadata
|
||||
id="metadata53">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1027"
|
||||
id="namedview51"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:zoom="0.1767767"
|
||||
inkscape:cx="637.59035"
|
||||
inkscape:cy="804.37887"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2"
|
||||
inkscape:showpageshadow="false"
|
||||
showborder="false"
|
||||
inkscape:document-rotation="0"
|
||||
fit-margin-top="43.37159"
|
||||
fit-margin-left="5"
|
||||
fit-margin-right="5"
|
||||
fit-margin-bottom="43.37159" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<linearGradient
|
||||
id="linearGradient3987">
|
||||
<stop
|
||||
style="stop-color:#41b3ec;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4032" />
|
||||
<stop
|
||||
style="stop-color:#0581c4;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3991" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3987"
|
||||
id="linearGradient3996"
|
||||
x1="11.848"
|
||||
y1="179.9725"
|
||||
x2="263.60077"
|
||||
y2="179.9725"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(8.6924921,11.686933)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3987"
|
||||
id="linearGradient4010"
|
||||
x1="11.348"
|
||||
y1="179.9725"
|
||||
x2="264.10077"
|
||||
y2="179.9725"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(8.6924921,11.686933)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3987"
|
||||
id="linearGradient4012"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="254.73778"
|
||||
y1="334.82437"
|
||||
x2="9.1209993"
|
||||
y2="-4.0966849"
|
||||
gradientTransform="translate(8.6924921,11.686933)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3987"
|
||||
id="linearGradient4014"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="254.73778"
|
||||
y1="334.82437"
|
||||
x2="9.1209993"
|
||||
y2="-4.0966849"
|
||||
gradientTransform="translate(8.6924921,11.686933)" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3987"
|
||||
id="radialGradient3786"
|
||||
cx="523.18433"
|
||||
cy="732.08508"
|
||||
fx="523.18433"
|
||||
fy="732.08508"
|
||||
r="146.04732"
|
||||
gradientTransform="matrix(-0.89061708,0.86297075,-0.78291311,-0.80799475,1427.0215,361.93369)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3987"
|
||||
id="radialGradient3796"
|
||||
cx="529.57996"
|
||||
cy="728.15845"
|
||||
fx="529.57996"
|
||||
fy="728.15845"
|
||||
r="146.04732"
|
||||
gradientTransform="matrix(-0.87083717,0.55292729,-0.71897156,-1.1320773,466.9435,499.54917)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3987"
|
||||
id="radialGradient3800"
|
||||
cx="505.65533"
|
||||
cy="726.3916"
|
||||
fx="505.65533"
|
||||
fy="726.3916"
|
||||
r="146.04732"
|
||||
gradientTransform="matrix(-0.94821917,1.3404019,-1.4302101,-1.0037148,1646.3219,-657.0863)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<ellipse
|
||||
style="fill:none;fill-opacity:0;stroke:url(#radialGradient3786);stroke-width:37.5001;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:none"
|
||||
id="path3803"
|
||||
inkscape:export-filename="/home/voegtlin/logos/electrum_light.png"
|
||||
inkscape:export-xdpi="15.13"
|
||||
inkscape:export-ydpi="15.13"
|
||||
cx="389.90555"
|
||||
cy="389.90555"
|
||||
rx="366.15549"
|
||||
ry="173.5993" />
|
||||
<ellipse
|
||||
transform="matrix(-0.50141536,-0.8652067,0.86684024,-0.498586,0,0)"
|
||||
id="path4023"
|
||||
style="fill:none;fill-opacity:0;stroke:url(#radialGradient3796);stroke-width:37.5002;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:none"
|
||||
inkscape:export-filename="/home/voegtlin/logos/electrum_light.png"
|
||||
inkscape:export-xdpi="15.13"
|
||||
inkscape:export-ydpi="15.13"
|
||||
cx="-532.39014"
|
||||
cy="141.84499"
|
||||
rx="365.12195"
|
||||
ry="174.09163" />
|
||||
<ellipse
|
||||
style="fill:none;fill-opacity:0;stroke:url(#radialGradient3800);stroke-width:37.5002;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:none"
|
||||
id="path4029"
|
||||
transform="matrix(-0.50141531,0.86520673,-0.86684024,-0.498586,0,0)"
|
||||
inkscape:export-filename="/home/voegtlin/logos/electrum_light.png"
|
||||
inkscape:export-xdpi="15.13"
|
||||
inkscape:export-ydpi="15.13"
|
||||
cx="143.58514"
|
||||
cy="-532.85632"
|
||||
rx="365.12195"
|
||||
ry="174.09163" />
|
||||
<g
|
||||
id="g5"
|
||||
transform="matrix(0.69025084,0,0,0.68452521,303.02483,261.95235)"
|
||||
style="fill:url(#linearGradient4010);fill-opacity:1;stroke:url(#linearGradient3996)"
|
||||
inkscape:export-filename="/home/voegtlin/logos/electrum_light.png"
|
||||
inkscape:export-xdpi="15.13"
|
||||
inkscape:export-ydpi="15.13">
|
||||
<path
|
||||
id="path7"
|
||||
d="m 217.021,167.042 c 18.631,-9.483 30.288,-26.184 27.565,-54.007 C 240.919,75.012 208.06,62.262 166.58,58.631 L 166.572,5.89 h -32.139 l -0.009,51.354 c -8.456,0 -17.076,0.166 -25.657,0.338 L 108.76,5.897 76.65,5.894 76.644,58.622 c -6.959,0.142 -13.793,0.277 -20.466,0.277 v -0.156 l -44.33,-0.018 0.006,34.282 c 0,0 23.734,-0.446 23.343,-0.013 13.013,0.009 17.262,7.559 18.484,14.076 l 0.01,60.083 v 84.397 c -0.573,4.09 -2.984,10.625 -12.083,10.637 0.414,0.364 -23.379,-0.004 -23.379,-0.004 l -6.375,38.335 h 41.817 c 7.792,0.009 15.448,0.13 22.959,0.19 l 0.028,53.338 32.102,0.009 -0.009,-52.779 c 8.832,0.18 17.357,0.258 25.684,0.247 l -0.009,52.532 h 32.138 l 0.018,-53.249 c 54.022,-3.1 91.842,-16.697 96.544,-67.385 3.79,-40.809 -15.434,-59.025 -46.105,-66.379 z M 109.535,95.321 c 18.126,0 75.132,-5.767 75.14,32.064 -0.008,36.269 -56.996,32.032 -75.14,32.032 V 95.321 Z m -0.014,167.126 0.014,-70.672 c 21.778,-0.006 90.085,-6.261 90.094,35.32 0.009,39.876 -68.316,35.336 -90.108,35.352 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:url(#linearGradient4012);fill-opacity:1;stroke:url(#linearGradient4014)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
|
|
@ -0,0 +1,187 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="210mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 210 297"
|
||||
version="1.1"
|
||||
id="svg156"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="logo.svg">
|
||||
<defs
|
||||
id="defs150">
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3987"
|
||||
id="radialGradient3800"
|
||||
cx="505.65533"
|
||||
cy="726.3916"
|
||||
fx="505.65533"
|
||||
fy="726.3916"
|
||||
r="146.04732"
|
||||
gradientTransform="matrix(-0.25088299,0.35464801,-0.37840975,-0.2655662,470.84911,-198.20295)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient3987">
|
||||
<stop
|
||||
style="stop-color:#41b3ec;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4032" />
|
||||
<stop
|
||||
style="stop-color:#0581c4;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3991" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3987"
|
||||
id="radialGradient3796"
|
||||
cx="529.57996"
|
||||
cy="728.15845"
|
||||
fx="529.57996"
|
||||
fy="728.15845"
|
||||
r="146.04732"
|
||||
gradientTransform="matrix(-0.230409,0.14629535,-0.1902279,-0.29952879,84.868575,113.75328)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3987"
|
||||
id="radialGradient3786"
|
||||
cx="523.18433"
|
||||
cy="732.08508"
|
||||
fx="523.18433"
|
||||
fy="732.08508"
|
||||
r="146.04732"
|
||||
gradientTransform="matrix(-0.23564243,0.22832767,-0.20714576,-0.21378194,380.99287,138.40863)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB;"
|
||||
inkscape:label="Invert"
|
||||
id="filter73">
|
||||
<feColorMatrix
|
||||
type="hueRotate"
|
||||
values="180"
|
||||
result="color1"
|
||||
id="feColorMatrix69" />
|
||||
<feColorMatrix
|
||||
values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0.21 0.72 0.07 0.61 0 "
|
||||
result="color2"
|
||||
id="feColorMatrix71" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="576.448"
|
||||
inkscape:cy="373.34524"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="3768"
|
||||
inkscape:window-height="2096"
|
||||
inkscape:window-x="72"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata153">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g31"
|
||||
style="filter:url(#filter73)">
|
||||
<path
|
||||
id="path23"
|
||||
d="m 119.10644,99.61967 c 6.01079,2.97349 9.86483,8.01438 13.11271,13.71251 2.43749,4.52405 2.8012,9.38628 2.85863,14.42717 0.083,6.81478 -0.21695,13.63594 -0.0511,20.44434 0.0893,5.09832 2.24607,9.13742 4.00082,13.81461 1.87597,4.98346 4.93241,9.00979 7.37629,13.76993 1.35275,2.7374 3.94977,5.52585 4.31986,8.56953 -1.15494,1.58245 -3.04368,2.64806 -4.95156,2.99263 -5.8066,0.53599 -11.86206,-3.9051 -17.73885,-4.09652 -7.21678,-0.56152 -13.30414,3.87957 -20.44434,4.01995 -3.86043,0.0957 -7.688959,-0.26162 -11.338838,-1.59522 -3.777465,-1.30808 -8.218549,-3.13939 -12.276813,-2.44388 -5.28973,0.65085 -10.132798,4.21776 -15.320481,3.95615 -2.813936,-0.0383 -4.830352,-1.44208 -7.08276,-2.90968 2.788398,-3.75196 5.117459,-7.68259 6.955139,-11.9833 2.297128,-5.27698 5.238723,-10.18388 7.478416,-15.46725 1.978054,-4.83033 1.627119,-10.29236 1.812142,-15.43535 -0.0061,-6.04269 0.255243,-12.07901 0.446696,-18.11532 0.267994,-7.81019 4.549534,-14.64412 10.215737,-19.74882 4.779304,-4.04548 10.426385,-4.70271 16.424412,-5.43651 4.90689,-0.48495 9.73084,-0.98266 14.20384,1.52503 z"
|
||||
style="fill:#000000;stroke-width:0.63808793"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path25"
|
||||
d="m 116.13295,117.20538 c 4.31347,-0.29991 8.71628,3.13939 10.33702,6.99982 2.4375,5.98527 -2.32902,13.22756 -8.66523,13.77632 -5.86403,0.54237 -11.56854,-4.19224 -11.51111,-10.21579 -0.0893,-5.49394 4.52404,-9.99884 9.83932,-10.56035 z"
|
||||
style="fill:#ffffff;stroke-width:0.63808793"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path27"
|
||||
d="m 63.45878,119.45145 c 1.767528,2.94158 2.411961,6.04269 4.894146,8.63332 2.105676,2.43112 5.359916,3.52225 7.50392,5.80023 0.599784,1.88874 0.561493,4.03271 0.644469,6.0044 -0.03215,4.94518 0.236099,9.48837 -1.607976,14.17832 -4.30072,-3.03092 -7.988903,-6.9743 -11.281434,-11.05169 -3.030892,-3.92424 -4.466598,-9.35437 -5.666203,-14.10174 -0.650861,-3.01815 -1.39102,-6.66802 0.599821,-9.35437 1.052838,-1.60798 3.981683,-2.09931 4.913257,-0.10847 z"
|
||||
style="fill:#000000;stroke-width:0.63808793"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path29"
|
||||
d="m 156.00068,123.31826 c 0.59981,8.69076 -4.38366,15.84373 -9.23951,22.53726 -2.00997,2.39922 -4.88775,6.29793 -8.35895,6.15117 -2.25245,-2.62892 -1.57608,-5.71726 -1.67179,-8.92685 0.11486,-2.55873 -0.083,-5.46841 0.79122,-7.89952 1.69732,-2.55236 4.93243,-3.8732 6.73821,-6.43193 2.12484,-2.75654 2.54598,-7.35078 5.33442,-9.40542 2.57788,-1.66541 6.34897,1.11665 6.4064,3.97529 z"
|
||||
style="fill:#000000;stroke-width:0.63808793"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path31"
|
||||
d="m 89.984114,122.26541 c 3.943371,-0.75932 7.944184,3.15854 6.565916,7.18487 -1.269783,4.29434 -7.982476,4.70909 -10.049896,0.8997 -1.684557,-3.11386 0.172304,-7.08277 3.48398,-8.08457 z"
|
||||
style="fill:#ffffff;stroke-width:0.63808793"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path33"
|
||||
d="m 103.77317,143.11175 c 7.59326,0.22333 15.20565,-0.45943 22.77976,-0.1659 -2.4694,2.80758 -5.51308,4.79204 -8.70353,6.68077 0.44667,4.96434 1.38466,10.87941 -0.1978,15.67145 -1.1358,3.43291 -4.24329,4.44109 -7.39544,5.3727 -5.16851,1.48674 -11.77909,-0.71466 -14.688771,-5.28975 -1.66541,-4.09653 -0.146778,-9.62237 -0.561549,-13.96775 -0.140367,-1.59522 -1.952551,-2.62254 -3.101107,-3.49034 -2.201403,-1.42932 -4.153954,-2.99901 -5.927805,-4.94518 5.991633,-0.8678 11.670594,0.24247 17.796242,0.134 z"
|
||||
style="fill:#ffffff;stroke-width:0.63808793"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path35"
|
||||
d="m 115.29067,151.91736 c 1.18685,4.13481 1.66541,9.79466 -0.57428,13.61042 -4.17948,4.99623 -12.35337,3.22234 -16.181892,-1.24427 -1.812187,-3.05645 -1.473984,-7.58049 -1.020975,-10.9815 0.09572,-2.49492 4.307097,-3.41377 5.615207,-1.33998 2.06101,2.60978 2.82671,6.27878 4.28156,9.26503 0.58703,-3.1011 0.65722,-7.38267 2.69911,-9.92864 1.40379,-1.63989 4.48575,-1.72923 5.18127,0.61894 z"
|
||||
style="fill:#000000;stroke-width:0.63808793"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<ellipse
|
||||
style="fill:none;fill-opacity:0;stroke:url(#radialGradient3786);stroke-width:9.9219017;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:none"
|
||||
id="path3803"
|
||||
inkscape:export-filename="/home/voegtlin/logos/electrum_light.png"
|
||||
inkscape:export-xdpi="15.13"
|
||||
inkscape:export-ydpi="15.13"
|
||||
cx="106.58929"
|
||||
cy="145.80954"
|
||||
rx="96.878639"
|
||||
ry="45.931484" />
|
||||
<ellipse
|
||||
transform="matrix(-0.50141536,-0.8652067,0.86684024,-0.498586,0,0)"
|
||||
id="path4023"
|
||||
style="fill:none;fill-opacity:0;stroke:url(#radialGradient3796);stroke-width:9.92192745;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:none"
|
||||
inkscape:export-filename="/home/voegtlin/logos/electrum_light.png"
|
||||
inkscape:export-xdpi="15.13"
|
||||
inkscape:export-ydpi="15.13"
|
||||
cx="-179.53844"
|
||||
cy="19.110722"
|
||||
rx="96.605179"
|
||||
ry="46.061745" />
|
||||
<ellipse
|
||||
style="fill:none;fill-opacity:0;stroke:url(#radialGradient3800);stroke-width:9.92192745;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:none"
|
||||
id="path4029"
|
||||
transform="matrix(-0.50141531,0.86520673,-0.86684024,-0.498586,0,0)"
|
||||
inkscape:export-filename="/home/voegtlin/logos/electrum_light.png"
|
||||
inkscape:export-xdpi="15.13"
|
||||
inkscape:export-ydpi="15.13"
|
||||
cx="73.250038"
|
||||
cy="-165.33377"
|
||||
rx="96.605179"
|
||||
ry="46.061745" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
{% extends "base.jinja" %}
|
||||
{% block head %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for(service.id +'_endpoint' + '.static', filename='spectrum/css/styles.css') }}">
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<h1>{{ _("Spectrum Info") }}:</h1>
|
||||
{% if specter.chain %}
|
||||
|
||||
<p class="warning" id="type_reminder">
|
||||
<img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>
|
||||
{{ _("Connecting to public electrum servers may expose some information including your ip address and transaction history.") }}
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<tr> <td style="text-align: left;">{{ _("Host") }}:</td> <td style="text-align: right;" id="node-info-specter-chain">{{specter.node.host}}</td> </tr>
|
||||
<tr> <td style="text-align: left;">{{ _("Port") }}:</td> <td style="text-align: right;" id="node-info-specter-chain">{{specter.node.port}}</td> </tr>
|
||||
<tr> <td style="text-align: left;">{{ _("SSL") }}:</td> <td style="text-align: right;" id="node-info-specter-chain">{{specter.node.ssl}}</td> </tr>
|
||||
<tr> <td style="text-align: left;">{{ _("Network") }}:</td> <td style="text-align: right;" id="node-info-specter-chain">{{specter.chain}}</td> </tr>
|
||||
<tr> <td style="text-align: left;">{{ _("Blocks count") }}:</td> <td style="text-align: right;">{{specter.info['blocks']}}</td> </tr>
|
||||
<tr> <td style="text-align: left;">{{ _("Last block hash") }}:</td> <td style="text-align: right"><code style="word-break: break-word;">{{specter.info['bestblockhash']}}</code></td> </tr>
|
||||
<tr> <td style="text-align: left;">{{ _("Node uptime") }}:</td> <td style="text-align: right;">~ {{(specter.info['uptime'] / 60 // 60) | int }} {{ _("Hours") }}</td> </tr>
|
||||
{% if specter.info['pruned'] %}
|
||||
<tr> <td style="text-align: left;">{{ _("Automatic pruning") }}:</td> <td style="text-align: right;">{{specter.info['automatic_pruning']}}</td> </tr>
|
||||
<tr> <td style="text-align: left;">{{ _("Prune height") }}:</td> <td style="text-align: right;">{{specter.info['pruneheight']}}</td> </tr>
|
||||
<tr> <td style="text-align: left;">{{ _("Prune target size") }}:</td> <td style="text-align: right;">{{specter.info['prune_target_size']}}</td> </tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
|
||||
<p id="total_supply" style="line-height: 2.5;"></p>
|
||||
<div class="row">
|
||||
<button type="button" onclick="fetchTotalSupply()" class="btn centered">
|
||||
{{ _("Run the numbers!") }}
|
||||
<tool-tip width="200px">
|
||||
<h4 slot="title">{{ _("Calculate the total Bitcoin supply") }}</h4>
|
||||
<span slot="paragraph">
|
||||
{{ _("This will run the ") }} <code>gettxoutsetinfo</code> {{_(" command which will calculate the total amount of Bitcoin's UTXO set.") }}<br>
|
||||
{{ _("This might take a few minutes...") }}
|
||||
</span>
|
||||
</tool-tip>
|
||||
</button>
|
||||
</div><br>
|
||||
<script>
|
||||
let totalUserBalance = parseFloat(parseFloat("{{ specter.wallet_manager.wallets.values() | sum(attribute='fullbalance') }}").toFixed(8));
|
||||
async function fetchTotalSupply() {
|
||||
document.getElementById('total_supply').innerHTML = `{{ _("Running the numbers... (this might take a few minutes)") }}`;
|
||||
try {
|
||||
const response = await fetch(
|
||||
"{{ url_for('wallets_endpoint_api.txout_set_info') }}",
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
).catch((err) => {
|
||||
showError(err)
|
||||
return
|
||||
});
|
||||
let result = await response.json();
|
||||
console.log(result)
|
||||
if (result.error) {
|
||||
showError(result.error)
|
||||
return
|
||||
}
|
||||
if (totalUserBalance==0) {
|
||||
document.getElementById('total_supply').innerHTML = `{{ _("Your wallet holds 0 BTC. Get off zero!)") }} `
|
||||
return
|
||||
}
|
||||
let userBalanceFromTotal = parseFloat((100 / (result.total_amount / totalUserBalance)).toFixed(8));
|
||||
document.getElementById('total_supply').innerHTML = `{{ _("Bitcoin Total Supply") }}: ${result.total_amount} BTC<br>` +
|
||||
`<span class="note" style="margin: 7px auto;">{{ _("Your wallets hold") }} ` +
|
||||
`${totalUserBalance} BTC (~${userBalanceFromTotal.toFixed(8)}% ` +
|
||||
`{{ _("from the total supply") }}</span>`
|
||||
} catch(e) {
|
||||
console.log('Caught error:', e);
|
||||
showError(e)
|
||||
return { success: false, error: e };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% if current_user.is_admin %}
|
||||
<div class="row">
|
||||
<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 Electrum Connection") }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{% from 'components/menu_item.jinja' import menu_item %}
|
||||
|
||||
{#
|
||||
spectrum_menu - Tabs menu to navigate between the spectrum screens.
|
||||
Parameters:
|
||||
- active_menuitem: Current active tab. Options: 'general', 'settings', ...
|
||||
#}
|
||||
{% macro spectrum_menu(active_menuitem) -%}
|
||||
<nav class="row collapse-on-mobile">
|
||||
{{ menu_item(service.id, 'index', 'Main', active_menuitem, isLeft=true) }}
|
||||
{{ menu_item(service.id, 'settings_get', 'Settings', active_menuitem, isRight=true) }}
|
||||
<a href="javascript:void(0);" class="mobile-nav-icon" onclick="toggleMobileNav(this, `{{ url_for('static', filename='img/expand-more.svg') }}`, `{{ url_for('static', filename='img/expand-less.svg') }}`)">
|
||||
<img style="width: 36px;" src="{{ url_for('static', filename='img/expand-more.svg') }}"/>
|
||||
</a>
|
||||
</nav>
|
||||
{%- endmacro %}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{% set node = specter.node %}
|
||||
{% if specter.node.is_running %}
|
||||
<div id="active-node" onclick="showPageOverlay('bitcoin_core_info');document.getElementById('side-content').classList.remove('active');" class="item core" style="cursor: pointer;">
|
||||
<img src="{{ url_for('spectrum_endpoint.static', filename='spectrum/img/logo.svg') }}" width="40"/>
|
||||
{% include "includes/sidebar/components/bitcoin_core_info.jinja" %}
|
||||
<div>
|
||||
<span style="max-width: 140px;">{{ node.name }}</span> <br>
|
||||
<small>
|
||||
{% if specter.chain %}
|
||||
{{node.chain | title}} node
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<a id="no-node-connection" class="no-connection" href="{{ url_for('nodes_endpoint.node_settings', node_alias=specter.node.alias) }}">
|
||||
<div style="display: flex; gap: 5px">
|
||||
<span>{{ _("Spectrum not connected") }}</span>
|
||||
<img src="{{ url_for('static', filename='img/broken-connection.svg') }}" style="width: 15px;">
|
||||
</div>
|
||||
<div id="configure-click" class="configure-click">{{ _("Click to configure") }}</div>
|
||||
</a>
|
||||
{% endif%}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<img src="{{ url_for('spectrum_endpoint.static', filename='spectrum/img/logo.svg') }}" width="40"/>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{% extends "spectrum/base.jinja" %}
|
||||
{% block main %}
|
||||
<img src="{{ url_for(service.id +'_endpoint' + '.static', filename=service.logo) }}" width="100"/>
|
||||
{% from 'spectrum/components/spectrum_menu.jinja' import spectrum_menu with context %}
|
||||
{{ spectrum_menu(tab) }}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "spectrum/components/spectrum_tab.jinja" %}
|
||||
{% block title %}Settings{% endblock %}
|
||||
{% set tab = 'index' %}
|
||||
{% block content %}
|
||||
<br><br>
|
||||
<div class="card">
|
||||
<h1>{{ _("Spectrum - Connecting to Electrum server") }}</h1>
|
||||
<div class="note">
|
||||
{{ _("- Spectrum lets you use Specter with an Electrum server.") }}<br/>
|
||||
{{ _("- You can connect Specter to your own Electrum Server or public servers.") }}<br/>
|
||||
{{ _("- In the alpha version of Spectrum, it's not supported to use a Bitcoin Core node alongside the Spectrum node.") }}<br/>
|
||||
</div>
|
||||
<div class="note">
|
||||
<b>{{ _("To activate Spectrum, choose an electrum server from the list in the settings and save.") }}</b>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
{% extends "spectrum/components/spectrum_tab.jinja" %}
|
||||
|
||||
{% block title %}Settings{% endblock %}
|
||||
{% set tab = 'settings_get' %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h1>{{ _("Configure Spectrum") }}</h1>
|
||||
<div class="note">
|
||||
{{ _("- Here you can choose from a list of Electrum servers or configure the connection to your own server.") }}<br/>
|
||||
{{ _("- Connecting to a third-party without using Tor leaks private information. Be aware of that.") }}<br/>
|
||||
</div>
|
||||
<form action="{{ url_for(service.get_blueprint_name() + '.settings_post') }}" method="POST" role="form">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div style="margin-top: 20px">Show Menu Item:</div>
|
||||
<select name="show_menu">
|
||||
<option value="yes" {% if show_menu == 'yes' %}selected{% endif %}>Yes</option>
|
||||
<option value="no" {% if show_menu == 'no' %}selected{% endif %}>No</option>
|
||||
</select>
|
||||
<div style="margin-top: 20px;">
|
||||
{{ _("Electrum Server:") }}
|
||||
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px;" id="elec_option_list" name="option_mode" value="list" >{{ _("list") }}</label>
|
||||
<label><input type="radio" class="inline" style="margin: 0 10px 0 20px" id="elec_option_manual" name="option_mode" value="manual" >{{ _("manual") }}</label>
|
||||
</div>
|
||||
<div id = "elec_manual_container" style="display: none">
|
||||
<div style="margin-top: 20px">
|
||||
Host:<br><input id="host-input" type="text" name="host" type="text" value="{{ host }}" placeholder="127.0.0.1">
|
||||
</div>
|
||||
<div style="margin-top: 20px; margin-bottom: 10px">
|
||||
Port:<br><input id="port-input" type="text" name="port" type="text" value="{{ port }}" placeholder="50000">
|
||||
</div>
|
||||
<label style="font-size: 1em;">{{ _("Use ssl:") }} </label>
|
||||
<label class="switch">
|
||||
<input id="ssl-input" type="checkbox" name="ssl" style="margin: auto;" value="on" {% if ssl %} checked {% endif %}>
|
||||
<span id="ssl-slider" class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id ="elec_list_container" style="display: none; margin-top: 20px;">
|
||||
{% if elec_chosen_option in elec_options %}
|
||||
Currently selected:
|
||||
{% else %}
|
||||
Select server:
|
||||
{% endif %}
|
||||
<select id="server-list" name="elec_option">
|
||||
<option selected style='display: none'></option>
|
||||
{% for electrum_option in elec_options %}
|
||||
<option value="{{ electrum_option }}" {% if elec_chosen_option == electrum_option %}selected{% endif %}>{{ electrum_option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 25px">
|
||||
<button type="submit" class="btn">{{ _("Save") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
this.elecOptionList = document.getElementById("elec_option_list")
|
||||
this.elecOptionManual = document.getElementById("elec_option_manual")
|
||||
|
||||
this.elecManualContainer = document.getElementById("elec_manual_container")
|
||||
this.elecListContainer = document.getElementById("elec_list_container")
|
||||
|
||||
let hostInput = document.getElementById('host-input')
|
||||
let portInput = document.getElementById('port-input')
|
||||
let sslInput = document.getElementById('ssl-input')
|
||||
|
||||
function showElecOption(option) {
|
||||
if (option == 'list') {
|
||||
this.elecOptionList.checked = true
|
||||
this.elecManualContainer.style.display ='none'
|
||||
this.elecListContainer.style.display = 'block'
|
||||
} else {
|
||||
this.elecOptionManual.checked = true
|
||||
this.elecManualContainer.style.display ='block'
|
||||
this.elecListContainer.style.display = 'none'
|
||||
}
|
||||
}
|
||||
{% if elec_chosen_option == "manual" %}
|
||||
showElecOption("manual")
|
||||
{% else %}
|
||||
showElecOption("list")
|
||||
{% endif %}
|
||||
|
||||
// Click event handler for manual configuration
|
||||
elec_option_manual = document.getElementById("elec_option_manual")
|
||||
elec_option_manual.addEventListener('click', (event) => {
|
||||
this.showElecOption("manual")
|
||||
// Clear out the input fields, otherwise it is using the values from the server from the list
|
||||
hostInput.value = ""
|
||||
portInput.value = ""
|
||||
sslInput.checked = false
|
||||
});
|
||||
|
||||
// Click event handler for configuration from list
|
||||
this.elec_option_list = document.getElementById("elec_option_list")
|
||||
this.elec_option_list.addEventListener('click', (event) => {
|
||||
this.showElecOption("list")
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
{% extends "base.jinja" %}
|
||||
{% block main %}
|
||||
<style>
|
||||
.feedback-icon {
|
||||
width: 50px;
|
||||
margin-bottom: 15px
|
||||
}
|
||||
.feedback-text {
|
||||
font-size: 1.1em;
|
||||
color: #ccc;
|
||||
}
|
||||
.feedback-container {
|
||||
max-width: 100%;
|
||||
width: 580px;
|
||||
border: 1px solid var(--cmap-border);
|
||||
border-radius: 4px;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.button-container {
|
||||
margin-top: 25px;
|
||||
}
|
||||
</style>
|
||||
<div class="feedback-container">
|
||||
<h1>{{ _("Spectrum - Connecting Specter to Electrum") }}</h1><br>
|
||||
<div style="text-align: center">
|
||||
{% if host_type == "list" %}
|
||||
{% if success %}
|
||||
<img class="feedback-icon" src="{{ url_for('static', filename='img/party.png') }}"/><br>
|
||||
{# TODO: Adding the exact name of the server? #}
|
||||
{% if changed_host %} <span class="feedback-text">{{ _("You switched the Electrum server successfully:") }}</span><br> {% endif %}
|
||||
<span class="feedback-text">{{ _("Specter is connected via Spectrum to a public Electrum server!") }}</span>
|
||||
{% else %}
|
||||
<img class="feedback-icon" src="{{ url_for('static', filename='img/failed.svg') }}"/><br>
|
||||
{% if changed_host and node_is_running_before_request %}
|
||||
<span class="feedback-text">{{ _("Cannot connect to the public Electrum server. You changed the settings from a working connection to a server that is not responding.
|
||||
Consider switching back to the one you had chosen before.") }}</span>
|
||||
{% else %}
|
||||
<span class="feedback-text">{{ _("Cannot connect to the public Electrum server.") }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if success %}
|
||||
<img class="feedback-icon" src="{{ url_for('static', filename='img/party.png') }}"/><br>
|
||||
{% if changed_host %} <span class="feedback-text">{{ _("You switched the Electrum server successfully:") }}</span><br> {% endif %}
|
||||
<span class="feedback-text">{{ _("Specter is connected via Spectrum to a manually configured Electrum server!") }}</span>
|
||||
{% else %}
|
||||
<img class="feedback-icon" src="{{ url_for('static', filename='img/failed.svg') }}"/><br>
|
||||
{% if changed_host and node_is_running_before_request %}
|
||||
<span class="feedback-text">{{ _("Cannot connect to the manually configured Electrum server. You changed the settings from a working connection to a server that is not responding.
|
||||
Consider switching back to the one you had chosen before.") }}</span>
|
||||
{% elif not changed_host and check_port_and_ssl %}
|
||||
<span class="feedback-text">{{ _("Cannot connect to the manually configured Electrum server. Double-check that the port and SSL settings are correct.") }}</span>
|
||||
{% elif changed_host and check_port_and_ssl %}
|
||||
<span class="feedback-text">{{ _("Cannot connect to the manually configured Electrum server. Double-check all the configuration settings.") }}</span>
|
||||
{% else %}
|
||||
<span class="feedback-text">{{ _("Cannot connect to the manually configured Electrum server.") }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="button-container">
|
||||
{% if success %}
|
||||
<a class="btn" href="{{ url_for('welcome_endpoint.index') }}" >{{ _("Let's go!") }}</a>
|
||||
{% else %}
|
||||
<a class="btn" id="back-button">{{ _("Back to Spectrum settings") }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let backButton = document.getElementById('back-button');
|
||||
backButton.setAttribute('href', document.referrer); // Just to have the link preview and open in a new tab functionality, probably not needed here
|
||||
backButton.addEventListener('click', () => {
|
||||
history.back();
|
||||
return false;
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
{% extends "base.jinja" %}
|
||||
{% block main %}
|
||||
<style>
|
||||
.feedback-icon {
|
||||
width: 60px;
|
||||
margin-bottom: 15px
|
||||
}
|
||||
.feedback-text {
|
||||
font-size: 1.1em;
|
||||
color: #ccc;
|
||||
}
|
||||
.feedback-container {
|
||||
max-width: 100%;
|
||||
width: 580px;
|
||||
border: 1px solid var(--cmap-border);
|
||||
border-radius: 4px;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.button-container {
|
||||
margin-top: 25px;
|
||||
}
|
||||
</style>
|
||||
<div class="feedback-container">
|
||||
<h1>{{ _("Spectrum - Connecting Specter to Electrum") }}</h1><br>
|
||||
<div style="text-align: center">
|
||||
{% if core_node_exists %}
|
||||
<img class="feedback-icon" src="{{ url_for('static', filename='img/not-supported.svg') }}"/><br>
|
||||
<span class="feedback-text">{{ _("In the alpha version of Spectrum, it's not supported to use a Bitcoin Core node alongside the Spectrum node. Your Spectrum node was not saved! Delete the Bitcoin Core node and try again.") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="button-container">
|
||||
{% if core_node_exists %}
|
||||
<a class="btn" id="back-button">{{ _("Back to Spectrum settings") }}</a>
|
||||
{% else %}
|
||||
<a class="btn" href="{{ url_for('welcome_endpoint.index') }}" >{{ _("Let's go!") }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let backButton = document.getElementById('back-button');
|
||||
backButton.setAttribute('href', document.referrer); // Just to have the link preview and open in a new tab functionality, probably not needed here
|
||||
backButton.addEventListener('click', () => {
|
||||
history.back();
|
||||
return false;
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
<div style="display: flex; justify-content: center; align-items: center;">
|
||||
<div class="main-description-box" style="margin: 20px">
|
||||
<p style="font-size: 1.1em; margin: 1em auto 1em auto; text-align: left;">{{ _("Use a public Electrum server for the easiest setup. This exposes your transaction data. You can use your own Electrum server to avoid this.") }} <br><br>
|
||||
<a href="{{url_for('spectrum_endpoint.index')}}" class="btn action centered" style="max-width: 200px;">{{ _('Get started!') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="main-description-box" style="margin: 20px">
|
||||
<p style="font-size: 1.1em; margin: 1em auto 1em auto; text-align: left;">{{ _("Set up your own node. This provides a high degree of privacy but takes more time.") }} <br><br>
|
||||
<a href="{{url_for('setup_endpoint.' + specter.setup_status.stage)}}" class="btn action centered" style="max-width: 200px;">{% if specter.setup_status.stage == "start" %}{{ _('Get started!') }}{% else %}{{ _('Continue setup') }}{% endif %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<br><br>
|
||||
|
||||
38
tests/test_specterext_spectrum_bridge_rpc.py
Normal file
38
tests/test_specterext_spectrum_bridge_rpc.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
from cryptoadvance.specterext.spectrum.bridge_rpc import BridgeRPC
|
||||
from flask import Flask
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_getmininginfobridge(caplog, app: Flask):
|
||||
print(app.spectrum)
|
||||
brpc = BridgeRPC(app.spectrum)
|
||||
|
||||
data = brpc.getmininginfo()
|
||||
assert data["blocks"] >= 0
|
||||
data = brpc.getblockchaininfo()
|
||||
assert data["blocks"] >= 0
|
||||
data = brpc.getnetworkinfo()
|
||||
assert data["version"] == 230000
|
||||
data = brpc.getmempoolinfo()
|
||||
assert data["mempoolminfee"] == 0.00001000 # bad, needs a fix!
|
||||
data = brpc.uptime()
|
||||
assert data >= 0 # in seconds and therefore almost zero
|
||||
|
||||
data = brpc.getblockcount()
|
||||
assert data >= 0 # hmmm, why is that?
|
||||
with app.app_context():
|
||||
data = brpc.listwallets()
|
||||
assert data == []
|
||||
data = brpc.createwallet("some_test_wallet_name_123")
|
||||
assert data["name"] == "some_test_wallet_name_123"
|
||||
data = brpc.listwallets()
|
||||
assert data == ["some_test_wallet_name_123"]
|
||||
|
||||
wbrpc = brpc.wallet("some_test_wallet_name_123")
|
||||
assert wbrpc is not None
|
||||
data = wbrpc.getwalletinfo()
|
||||
print(data)
|
||||
assert data["walletname"] == "some_test_wallet_name_123"
|
||||
|
||||
# assert False
|
||||
88
tests/test_specterext_spectrum_controller_helpers.py
Normal file
88
tests/test_specterext_spectrum_controller_helpers.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
from cryptoadvance.specterext.spectrum.controller_helpers import (
|
||||
evaluate_current_status,
|
||||
check_for_node_on_same_network,
|
||||
)
|
||||
from mock import Mock
|
||||
|
||||
|
||||
def test_evaluate_current_status():
|
||||
# We changed the host and made the connection work again with it
|
||||
node_is_running_before_request, success, host_before_request, host_after_request = (
|
||||
False,
|
||||
True,
|
||||
"127.0.0.1",
|
||||
"electrum.emzy.de",
|
||||
)
|
||||
changed_host, check_port_and_ssl = evaluate_current_status(
|
||||
node_is_running_before_request, success, host_before_request, host_after_request
|
||||
)
|
||||
assert changed_host == True
|
||||
assert check_port_and_ssl == False
|
||||
|
||||
# We didn't change anything
|
||||
node_is_running_before_request, success, host_before_request, host_after_request = (
|
||||
True,
|
||||
True,
|
||||
"127.0.0.1",
|
||||
"127.0.0.1",
|
||||
)
|
||||
changed_host, check_port_and_ssl = evaluate_current_status(
|
||||
node_is_running_before_request, success, host_before_request, host_after_request
|
||||
)
|
||||
assert changed_host == False
|
||||
assert check_port_and_ssl == False
|
||||
|
||||
# We didn't change the host but the connection got lost, which indicates that the ssl / port config was changed
|
||||
node_is_running_before_request, success, host_before_request, host_after_request = (
|
||||
True,
|
||||
False,
|
||||
"127.0.0.1",
|
||||
"127.0.0.1",
|
||||
)
|
||||
changed_host, check_port_and_ssl = evaluate_current_status(
|
||||
node_is_running_before_request, success, host_before_request, host_after_request
|
||||
)
|
||||
assert changed_host == False
|
||||
assert check_port_and_ssl == True
|
||||
|
||||
# We didn't change the host but the connection can still not be established
|
||||
node_is_running_before_request, success, host_before_request, host_after_request = (
|
||||
False,
|
||||
False,
|
||||
"127.0.0.1",
|
||||
"127.0.0.1",
|
||||
)
|
||||
changed_host, check_port_and_ssl = evaluate_current_status(
|
||||
node_is_running_before_request, success, host_before_request, host_after_request
|
||||
)
|
||||
assert changed_host == False
|
||||
assert check_port_and_ssl == True
|
||||
|
||||
# We did change the host but the connection can still not be established
|
||||
node_is_running_before_request, success, host_before_request, host_after_request = (
|
||||
False,
|
||||
False,
|
||||
"127.0.0.1",
|
||||
"electrum.emzy.de",
|
||||
)
|
||||
changed_host, check_port_and_ssl = evaluate_current_status(
|
||||
node_is_running_before_request, success, host_before_request, host_after_request
|
||||
)
|
||||
assert changed_host == True
|
||||
assert check_port_and_ssl == True
|
||||
|
||||
|
||||
def test_check_for_node_on_same_network():
|
||||
spectrum_node_mock = Mock()
|
||||
spectrum_node_mock.fqcn = (
|
||||
"cryptoadvance.specterext.spectrum.spectrum_node.SpectrumNode"
|
||||
)
|
||||
bitcoin_core_node_mock = Mock()
|
||||
bitcoin_core_node_mock.fqcn = "cryptoadvance.specter.node.Node"
|
||||
bitcoin_core_node_mock.is_liquid = False
|
||||
specter_mock = Mock()
|
||||
specter_mock.node_manager.nodes_by_chain.return_value = [
|
||||
spectrum_node_mock,
|
||||
bitcoin_core_node_mock,
|
||||
]
|
||||
assert check_for_node_on_same_network(spectrum_node_mock, specter_mock) == True
|
||||
42
tests/test_specterext_spectrum_node.py
Normal file
42
tests/test_specterext_spectrum_node.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import logging
|
||||
import pytest
|
||||
from cryptoadvance.specterext.spectrum.spectrum_node import SpectrumNode
|
||||
from cryptoadvance.spectrum.util import SpectrumException
|
||||
from cryptoadvance.specter.specter_error import BrokenCoreConnectionException
|
||||
from cryptoadvance.specter.persistence import PersistentObject
|
||||
|
||||
|
||||
def test_SpectrumNode(caplog):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
# Instantiate directly:
|
||||
sn = SpectrumNode("Some name")
|
||||
# An AbstractNode return None if the connection is broken for:
|
||||
assert sn.chain == None
|
||||
# Empty dict for info:
|
||||
assert sn.info == {}
|
||||
assert sn.network_info == {"subversion": "", "version": 999999}
|
||||
assert sn.bitcoin_core_version_raw == 99999
|
||||
assert sn.is_running == False
|
||||
assert (
|
||||
type(sn.network_parameters) == dict
|
||||
) # a huge dict {'Yprv': b'\x02B\x85\xb5', 'Ypub': b'\x02B\x89\xef', ...}
|
||||
with pytest.raises(BrokenCoreConnectionException):
|
||||
sn.uptime()
|
||||
|
||||
a_dict = {
|
||||
"python_class": "cryptoadvance.specterext.spectrum.spectrum_node.SpectrumNode",
|
||||
"name": "Spectrum Node",
|
||||
"alias": "spectrum_node",
|
||||
"host": "kirsche.emzy.de",
|
||||
"port": 5002,
|
||||
"ssl": True,
|
||||
}
|
||||
|
||||
# Instantiate via PersistentObject:
|
||||
sn = PersistentObject.from_json(a_dict)
|
||||
|
||||
assert type(sn) == SpectrumNode
|
||||
assert sn.host == "kirsche.emzy.de"
|
||||
assert sn.port == 5002
|
||||
assert sn.ssl == True
|
||||
|
|
@ -88,7 +88,7 @@ def test_get_subclasses_for_class(caplog):
|
|||
classlist = get_subclasses_for_clazz(SpecterMigration)
|
||||
assert SpecterMigration_0000 in classlist
|
||||
classlist = get_subclasses_for_clazz(Service)
|
||||
assert len(classlist) == 3 # Happy to remove that at some point
|
||||
assert len(classlist) == 4 # Happy to remove that at some point
|
||||
assert SwanService in classlist
|
||||
assert ElectrumService in classlist
|
||||
assert DevhelpService in classlist
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue