-
Notifications
You must be signed in to change notification settings - Fork 244
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into fix-build-instructions
- Loading branch information
Showing
27 changed files
with
1,778 additions
and
1 deletion.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}>" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |
Oops, something went wrong.