Skip to content

Commit

Permalink
Merge branch 'master' into fix-build-instructions
Browse files Browse the repository at this point in the history
  • Loading branch information
k9ert authored Dec 8, 2022
2 parents 915841b + ef4012d commit 9fdfa0e
Show file tree
Hide file tree
Showing 27 changed files with 1,778 additions and 1 deletion.
Empty file.
107 changes: 107 additions & 0 deletions src/cryptoadvance/specterext/spectrum/bridge_rpc.py
Original file line number Diff line number Diff line change
@@ -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 changes: 94 additions & 0 deletions src/cryptoadvance/specterext/spectrum/config.py
Original file line number Diff line number Diff line change
@@ -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 changes: 168 additions & 0 deletions src/cryptoadvance/specterext/spectrum/controller.py
Original file line number Diff line number Diff line change
@@ -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,
)
Loading

0 comments on commit 9fdfa0e

Please sign in to comment.