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:
k9ert 2022-12-08 16:01:12 +01:00 committed by GitHub
parent 77d27cf8af
commit ef4012d4b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1778 additions and 1 deletions

View 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}>"

View 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

View 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,
)

View 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

View 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

View 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"

View file

@ -0,0 +1 @@
/* This is the place to put all your styles */

View file

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

View file

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

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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%}

View file

@ -0,0 +1 @@
<img src="{{ url_for('spectrum_endpoint.static', filename='spectrum/img/logo.svg') }}" width="40"/>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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:") }}&nbsp;</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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

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

View 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

View 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

View 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

View file

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