Feature: several things for Spectrum preparation (#1913)

* change Node persistence format

* Removing the Singleton

* mainly refactoring

* intermediate commit

* fix test

* Updated diagram

* fix tests

* removing simplejsons references

* fix tests

* remove update, tests working

* abstract Node

* refining Migration framework: run migrations until they suceed

* BrokenCoreConnectionException added to handle lost RPC + some minor error handling fixes

* clicking redirects to node config if there is no node connection

* AbstractNode

* BrokenCoreConnectionException added to handle lost RPC + some minor error handling fixes

* clicking redirects to node config if there is no node connection

* handling broken connection when there is no other error superseding (i.e. no wallets)

* Abstract Node improvements and issue fixing

* improving node_info handling

* fix test results, property device_manager in user, bcce in get_rpc in node, improved rpc property in node

* typos

* provide data_folders for extensions

* parametrizing include for node_info

* Remove initial_node_contribution refactoring welcome

* adding convenience methods

* extension data-storage

* Tests green

* Better failure resistence

* Enabling Node settings

* error handling

* adjust_view_model callback

* exception Handling

* Shielding core from extension flaws

* More consistent logging

* Refactoring wallets endpoints

* Make wallet_overview extendable

* butgifex and cleanup

* feedback by Manolis

* rename BusinessObject to PersistentObject

* fix Error-management and rollback information

* Nodes.md corrections

* frontend-aspects.md (small) corrections

* fix tests and address feedback and errormanagement

* fix tests, proper error-handling

* Fix test_util_reflection

* just some little addons to test_util_reflection

* fix cleanup_on_exit

* migration fix

* remove check, add redirect after saving node

* fix test (not that cool but what to do?)

* upgrade Flask as Flask-SQLAlchemy needs higher Flask version

* Revert "upgrade Flask as Flask-SQLAlchemy needs higher Flask version"

This reverts commit bf24120f89.

* update bug fixed + node manager added to node test

* cleanup external_node

Co-authored-by: moneymanolis <moneymanolis@protonmail.com>
This commit is contained in:
k9ert 2022-10-29 15:54:34 +02:00 committed by GitHub
parent 9a3f92d618
commit de8f5af619
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1101 additions and 474 deletions

View file

@ -15,24 +15,22 @@ It is up to each `Service` implementation to decide what data is stored; the `Se
This is also where `Service`-wide configuration or other information should be stored, _**even if it is not secret**_ (see above intro about not polluting other existing data stores).
## ServiceEncryptedStorageManager
Because the `ServiceEncryptedStorage` is specific to each individual user, this manager provides convenient access to automatically retrieve the `current_user` from the Flask context and provide the correct user's `ServiceEncryptedStorage`. It is implemented as a `Singleton` which can be retrieved simply by importing the class and calling `get_instance()`.
Because the `ServiceEncryptedStorage` is specific to each individual user, this manager provides convenient access to automatically retrieve the `current_user` from the Flask context and provide the correct user's `ServiceEncryptedStorage`.
This simplifies code to just asking for:
```python
from .service_encrypted_storage import ServiceEncryptedStorageManager
ServiceEncryptedStorageManager.get_instance().get_current_user_service_data(service_id=some_service_id)
app.specter.service_encrypted_storage_manager.get_current_user_service_data(service_id=some_service_id)
```
As a further convenience, the `Service` base class itself encapsulates `Service`-aware access to this per-`User` encrypted storage:
```python
@classmethod
def get_current_user_service_data(cls) -> dict:
return ServiceEncryptedStorageManager.get_instance().get_current_user_service_data(service_id=cls.id)
return app.specter.service_encrypted_storage_manager.get_current_user_service_data(service_id=cls.id)
```
Whenever possible, external code should not directly access these `Service`-related support classes but rather should ask for them through the `Service` class.
## `ServiceUnencryptedStorage`
A disadvantage of the `ServiceEncryptedStorage` is, that the user needs to be freshly logged in in order to be able to decrypt the secrets. If you want to avoid that login but your extension should still store data on disk, you can use the `ServiceUnencryptedStorage`.
@ -49,7 +47,7 @@ _Note: current `Service` implementations have not yet needed this feature so dis
Unfortunately, the two unencrypted classes are derived from the encrypted one rather than having it the other way around or having abstract classes. This makes the diagram maybe a bit confusing.
[![](https://mermaid.ink/img/pako:eNqVVMFuwjAM_ZUqJzaVw66IIU0D7bRd0G6VItO4LFvqoCRlqhj_PpeWASLduhyiqH7v-dlOsxO5VSgmIjfg_VzD2kGZUcKr3Z-Q0Ol8DgGegWCNLpl-jcfJEt1W57ig3NWbgGoZrONoS-oJXjBfCaPcPxI-ENkAQVvyF7RHS4VeVw5WBpea1gaDpZbZZ6eT_9VyzMK18_8oZeIuE8l4fMsn4tOQRvZm7FPra24XZgLjK4--38BFTe1-uCORAe3acLOk1KSDlCOPpkgTxSBZWKPQpUlniUcnP7C-f7GENycmQYnSFvLdc7zQBk-hn1r4OxrlTxFjQY3ORKSHbUfcn3vuqTFm_MIyt4h3pX1zraTCA_0sX0b9ya5varxPB7DUKk0-wfC1HSh_PeJdFB7_Mc6sTKf--Hk2G96769lHhJrlW7xc1bJp538qGpQjJnVqhUhFia4ErfiROwhlIrxhiZmY8FFhAZUJmciogVYbHj8ulOb8YlKA8ZgKqIJd1pSLSXAVHkHdW9mh9t9YNMxZ)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqVVMFuwjAM_ZUqJzaVw66IIU0D7bRd0G6VItO4LFvqoCRlqhj_PpeWASLduhyiqH7v-dlOsxO5VSgmIjfg_VzD2kGZUcKr3Z-Q0Ol8DgGegWCNLpl-jcfJEt1W57ig3NWbgGoZrONoS-oJXjBfCaPcPxI-ENkAQVvyF7RHS4VeVw5WBpea1gaDpZbZZ6eT_9VyzMK18_8oZeIuE8l4fMsn4tOQRvZm7FPra24XZgLjK4--38BFTe1-uCORAe3acLOk1KSDlCOPpkgTxSBZWKPQpUlniUcnP7C-f7GENycmQYnSFvLdc7zQBk-hn1r4OxrlTxFjQY3ORKSHbUfcn3vuqTFm_MIyt4h3pX1zraTCA_0sX0b9ya5varxPB7DUKk0-wfC1HSh_PeJdFB7_Mc6sTKf--Hk2G96769lHhJrlW7xc1bJp538qGpQjJnVqhUhFia4ErfiROwhlIrxhiZmY8FFhAZUJmciogVYbHj8ulOb8YlKA8ZgKqIJd1pSLSXAVHkHdW9mh9t9YNMxZ)
[![](https://mermaid.ink/img/pako:eNqVVD1PwzAQ_SuRp4KSgTWCAakVEywVC4pkXeNLMTjnynaKotL_jpukTaLGEDxYdt579-4j8oHlWiBLWa7A2qWErYEyo8ivdn9CQiPzJTh4BoItmuj-O0miNZq9zHFFual3DsXaaePRVhQAR8pXwkntH4aPRNqBk5rsHMupENfOHWtWpIzdZSxKklt_In-a04igYyhaqDkd7AWeX1m04QRGNbV7M-OJBh9a-LQ4lyQd5wuLqogj4Um80EqgiaMuJd96_on1w4smvOmVBCVyXfAP6_FCKuyhSy3-Oyphe0RpEItBEG5h3wmPw5wDNU4lPkrZt8jvQlrYKOQCG_nAL6Ow2fWfNt2nhsyliKMvUArnhr8e8eE3emC8g5RsC_BNzU9l_8d5HCys7DNkMSvRlCCFfzsaXcbcO5aYsdQfBZjPjGV04lU7PxJcCem9WFqAshgzqJxe15Sz1JkKz6Tu_bmwdkBvWp_vxx_pzJiR)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqVVD1PwzAQ_SuRp4KSgTWCAakVEywVC4pkXeNLMTjnynaKotL_jpukTaLGEDxYdt579-4j8oHlWiBLWa7A2qWErYEyo8ivdn9CQiPzJTh4BoItmuj-O0miNZq9zHFFual3DsXaaePRVhQAR8pXwkntH4aPRNqBk5rsHMupENfOHWtWpIzdZSxKklt_In-a04igYyhaqDkd7AWeX1m04QRGNbV7M-OJBh9a-LQ4lyQd5wuLqogj4Um80EqgiaMuJd96_on1w4smvOmVBCVyXfAP6_FCKuyhSy3-Oyphe0RpEItBEG5h3wmPw5wDNU4lPkrZt8jvQlrYKOQCG_nAL6Ow2fWfNt2nhsyliKMvUArnhr8e8eE3emC8g5RsC_BNzU9l_8d5HCys7DNkMSvRlCCFfzsaXcbcO5aYsdQfBZjPjGV04lU7PxJcCem9WFqAshgzqJxe15Sz1JkKz6Tu_bmwdkBvWp_vxx_pzJiR)
## Implementation Notes
Efforts has been taken to provide `Service` data storage that is separate from existing data stores in order to keep those areas clean and simple. Where touchpoints are unavoidable, they are kept to the absolute bare minimum (e.g. `User.services` list in `users.json`, `Address.service_id` field).

View file

@ -41,7 +41,7 @@ In your controller, the endpoint needs to be specified like this:
ui = RubberduckService.blueprints["ui"]
```
## Templates and Static Resources
## Templates and static resources
The minimal url routes for `Service` selection and management. As usualy in Flask, `templates` and `static` resources are in their respective subfolders. Please note that there is an additional directory with the id of the extension which looks redundant at first. This is due to the way blueprints are loading templates and ensures that there are no naming collisions. Maybe at a later stage, this can be used to let plugins override other plugin's templates.
@ -66,8 +66,8 @@ You might have an extension which wants to inject e.g. JavaScript code into each
For this to work, the extension needs to be activated for the user, though.
### Extending dialogs
You can extend the settings dialog or the wallet-dialog with your own templates. To do that, create a callback method in your service like:
## Extending dialogs
You can extend the settings dialog or the wallet dialog with your own templates. To do that, create a callback method in your service like:
```python
from cryptoadvance.specter.services import callbacks
@ -120,7 +120,7 @@ The `some_settingspage.jinja` should probably look exactly like all the other se
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<h1 id="title" class="settings-title">Settings</h1>
{% from 'settings/components/settings_menu.jinja' import settings_menu %}
{{ settings_menu('myext_something', current_user, setting_exts) }}
{{ settings_menu('myext_something', current_user, ext_settingstabs) }}
<div class="card" style="margin: 20px auto;">
<h1>{{ _("Something something") }} </h1>
</div>
@ -146,3 +146,26 @@ A reasonable `mywalletdetails.jinja` would look like this:
![](./images/extensions/add_wallettabs.png)
## Extending certain pages or complete endpoints
For some endpoints, there is the possibility to extend/change parts of a page or the complete page. This works by declaring the `callback_adjust_view_model` method in your extension and modify the ViewModel which got passed into the callback. As there is only one callback for all types of ViewModels, you will need to check for the type that you're expecting and only adjust this type. Here is an example:
```python
from cryptoadvance.specter.server_endpoints.welcome.welcome_vm import WelcomeVm
class ExtensionidService(Service):
[...}
def callback_adjust_view_model(self, view_model: WelcomeVm):
if type(view_model) == WelcomeVm:
# potentially, we could make a redirect 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
```
In this example, a certain part of the page gets replaced. As you can read in the comments, you could also trigger a complete redirect to a different endpoint.
Currently, only two `ViewModels` are existing. Check them out. Don't hesitate to create an issue if you'd like to modify something where no ViewModel exists yet:
- cryptoadvance.specter.server_endpoints.welcome.welcome_vm
- cryptoadvance.specter.server_endpoints.wallets.wallets_vm

30
docs/extensions/nodes.md Normal file
View file

@ -0,0 +1,30 @@
# Adding Nodes or NodeTypes
The whole programming-model is based on the Bitcoin-Core API. So we need Bitcoin Core nodes or elements nodes or at least something which behaves like that. So for the Spectrum Integration, we made extending the node possible. This is a short description of how that has been done and which extensionpoints might be helpfull here.
So to create your own Node, derive from `AbstractNode`:
```
from cryptoadvance.specter.node import AbstractNode
class MyNode(AbstractNode):
# [...]
@classmethod
def from_json(cls, node_dict, *args, **kwargs):
[...]
def node_info_template(self):
return "spectrum/components/spectrum_info.jinja"
```
That class will need its own `fromJson` method. Overwrite the `node_info_template` method to specify your own template for the info-page which comes up if you click on a fully configured and functional node in the upper left corner. In order to smuggle your node into existence, you could potentially use the `callback_after_serverpy_init_app` callback. Have a look how the spectrum-extension did it [here](https://github.com/cryptoadvance/spectrum/pull/9/files#diff-82be7977bfa33bdbb0a448c7a03b43de90c4749565bef6737d6d516956ff0823R51-R62). Alternatively, you could create your own frontend in your controller and maybe additionally adjust the `WelcomeVm` model class as described in the frontend section.
If the `node_settings` are clicked for that node, we also expect that you have a `node_settings` endpoint in your controller. Otherwise there will be errors. Something like:
```
@yourextension_endpoint.route("node/<node_alias>/", methods=["GET", "POST"])
@login_required
def node_settings(node_alias=None):
[...]
return render_template(...
```

View file

@ -44,6 +44,7 @@ nav:
- extensions/data-storage.md
- extensions/callbacks.md
- extensions/devices.md
- extensions/nodes.md
theme:
name: readthedocs

View file

@ -121,8 +121,20 @@ def alias(name):
return "".join(x for x in name if x.isalnum() or x == "_").lower()
def fullpath(data_folder, name):
"""Quick way to get a fullpath which usually"""
return os.path.join(data_folder, f"{alias(name)}.json")
def calc_fullpath(data_folder, name):
"""Get a fullpath for a Businessobject with a name quickly"""
return os.path.join(data_folder, f"{alias(name)}.json")
def deep_update(d, u):
"""updates the dict d with the dict u"""
"""updates the dict d with the dict u
in short: second argument wins
"""
for k, v in six.iteritems(u):
dv = d.get(k, {})
if not isinstance(dv, collections.abc.Mapping):
@ -151,6 +163,9 @@ def load_jsons(folder, key=None):
d["alias"] = fname[:-5]
dd[d[key]] = d
except Exception as e:
logger.error(
f"Exception while loading json file {os.path.join(folder, fname)}"
)
logger.exception(e)
return dd

View file

@ -25,6 +25,7 @@ class InternalNode(Node):
BROKEN = "Broken"
DOWN = "Down"
RUNNING = "Running"
external_node = False
def __init__(
self,
@ -53,7 +54,6 @@ class InternalNode(Node):
port,
host,
protocol,
False,
fullpath,
"BTC",
manager,
@ -92,7 +92,6 @@ class InternalNode(Node):
port = node_dict.get("port", None)
host = node_dict.get("host", "localhost")
protocol = node_dict.get("protocol", "http")
external_node = node_dict.get("external_node", True)
fullpath = node_dict.get("fullpath", default_fullpath)
bitcoind_path = node_dict.get("bitcoind_path", "")
bitcoind_network = node_dict.get("bitcoind_network", "main")

View file

@ -1,14 +1,16 @@
from gc import callbacks
import os
import logging
import secrets
import shutil
from ..rpc import get_default_datadir, RPC_PORTS
from ..specter_error import SpecterError
from ..persistence import write_node, delete_file
from ..helpers import alias, load_jsons
from ..specter_error import SpecterError, SpecterInternalException
from ..persistence import PersistentObject, write_node, delete_file
from ..helpers import alias, calc_fullpath, load_jsons
from ..node import Node
from ..internal_node import InternalNode
from ..services import callbacks
from ..util.bitcoind_setup_tasks import setup_bitcoind_thread
logger = logging.getLogger(__name__)
@ -27,20 +29,21 @@ class NodeManager:
internal_bitcoind_version="",
data_folder="",
):
self.nodes = {}
self.data_folder = data_folder
self._active_node = active_node
self.proxy_url = proxy_url
self.only_tor = only_tor
self.bitcoind_path = bitcoind_path
self.internal_bitcoind_version = internal_bitcoind_version
self.update(data_folder)
self.load_from_disk(data_folder)
internal_nodes = [
node for node in self.nodes.values() if not node.external_node
]
for node in internal_nodes:
node.start()
def update(self, data_folder=None):
def load_from_disk(self, data_folder=None):
if data_folder is not None:
self.data_folder = data_folder
if data_folder.startswith("~"):
@ -48,22 +51,23 @@ class NodeManager:
# creating folders if they don't exist
if not os.path.isdir(data_folder):
os.mkdir(data_folder)
nodes = {}
nodes_files = load_jsons(self.data_folder, key="name")
for node_alias in nodes_files:
fullpath = os.path.join(self.data_folder, "%s.json" % node_alias)
node_class = (
Node if nodes_files[node_alias].get("external_node") else InternalNode
)
nodes[nodes_files[node_alias]["name"]] = node_class.from_json(
nodes_files[node_alias],
self,
default_alias=node_alias,
default_fullpath=fullpath,
)
if not nodes:
try:
self.nodes[
nodes_files[node_alias]["name"]
] = PersistentObject.from_json(
nodes_files[node_alias],
self,
default_alias=node_alias,
default_fullpath=calc_fullpath(self.data_folder, node_alias),
)
except SpecterInternalException as e:
logger.error(f"Skipping node {node_alias} due to {e}")
if not self.nodes:
if os.environ.get("ELM_RPC_USER"):
self.add_node(
self.add_external_node(
node_type="ELM",
name="Blockstream Liquid",
autodetect=True,
@ -73,10 +77,10 @@ class NodeManager:
port=7041,
host="localhost",
protocol="http",
external_node=True,
default_alias=self.DEFAULT_ALIAS,
)
self.add_node(
logger.info("Creating initial node-configuration")
self.add_external_node(
node_type="BTC",
name="Bitcoin Core",
autodetect=True,
@ -86,11 +90,28 @@ class NodeManager:
port=8332,
host="localhost",
protocol="http",
external_node=True,
default_alias=self.DEFAULT_ALIAS,
)
else:
self.nodes = nodes
# Just to be sure here ....
has_default_node = False
for name, node in self.nodes.items():
if node.alias == self.DEFAULT_ALIAS:
return
# Make sure we always have a default node
# (needed for the rpc-as-pin-authentication, created and used for raspiblitz)
self.add_external_node(
node_type="BTC",
name="Bitcoin Core",
autodetect=True,
datadir=get_default_datadir(),
user="",
password="",
port=8332,
host="localhost",
protocol="http",
default_alias=self.DEFAULT_ALIAS,
)
@property
def active_node(self):
@ -131,7 +152,7 @@ class NodeManager:
for node_alias in stopped_nodes:
self.get_by_alias(node_alias).start(timeout=60)
def add_node(
def add_external_node(
self,
node_type,
name,
@ -142,12 +163,12 @@ class NodeManager:
port,
host,
protocol,
external_node,
default_alias=None,
):
"""Adding a node. Params:
:param node_type: only valid for autodetect. Either BTC or ELM
This should only be used for an external node. Use add_internal_node for internal node
and if you have defined your own node-type, use save_node directly. to save the node (and create it yourself)
"""
if not default_alias:
node_alias = alias(name)
@ -170,14 +191,22 @@ class NodeManager:
port,
host,
protocol,
external_node,
fullpath,
node_type,
self,
)
logger.info(f"persisting {node} in add_node")
logger.info(f"persisting {node} in add_external_node")
self.nodes[name] = node
return self.save_node(node)
def save_node(self, node):
fullpath = (
node.fullpath
if hasattr(node, "fullpath")
else calc_fullpath(self.data_folder, node.name)
)
write_node(node, fullpath)
self.update() # reload files
logger.info("Added new node {}".format(node.alias))
return node
@ -189,6 +218,10 @@ class NodeManager:
default_alias=None,
datadir=None,
):
"""Adding an internal node. Params:
This should only be used for internal nodes. Use add__External_node for external nodes
and if you have defined your own node-type, use save_node directly. to save the node (and create it yourself)
"""
if not default_alias:
node_alias = alias(name)
else:
@ -218,11 +251,8 @@ class NodeManager:
network,
self.internal_bitcoind_version,
)
logger.info(f"persisting {node} in add_internal_node")
write_node(node, fullpath)
self.update() # reload files
logger.info("Added new internal node {}".format(node.alias))
return node
self.nodes[name] = node
return self.save_node(node)
def delete_node(self, node, specter):
logger.info("Deleting {}".format(node.alias))
@ -232,5 +262,4 @@ class NodeManager:
if self._active_node == node.alias:
specter.update_active_node(next(iter(self.nodes.values())).alias)
del self.nodes[node.name]
self.update()
logger.info("Node {} was deleted successfully".format(node.alias))

View file

@ -79,7 +79,7 @@ class ServiceManager:
logger.info(
f"Service {clazz.__name__} not activated due to devstatus ( {self.devstatus_threshold} > {clazz.devstatus} )"
)
logger.info("----> finished service processing")
logger.info("----> finished service loading")
self.execute_ext_callbacks("afterServiceManagerInit")
@classmethod
@ -283,9 +283,20 @@ class ServiceManager:
return_values = {}
for ext in self.services.values():
if hasattr(ext, f"callback_{callback_id}"):
return_values[ext.id] = getattr(ext, f"callback_{callback_id}")(
*args, **kwargs
)
try:
return_values[ext.id] = getattr(ext, f"callback_{callback_id}")(
*args, **kwargs
)
except Exception as e:
# Development should catch all errors early!
if app.config["SPECTER_CONFIGURATION_CLASS_FULLNAME"].endswith(
"DevelopmentConfig"
):
raise e
logger.error(
"Exception {e} while executing {callback_id} for extension {ext.id}"
)
logger.exception(e)
elif hasattr(ext, "callback"):
return_values[ext.id] = ext.callback(callback_id, *args, **kwargs)
# Filtering out all None return values

View file

@ -68,10 +68,6 @@ class WalletManager:
The _update internal method will resync the internal status with Bitcoin Core
use_threading : for the _update method which is heavily communicating with Bitcoin Core
"""
if (chain is None and rpc is not None) or (chain is not None and rpc is None):
raise SpecterInternalException(
f"Chain ({chain}) and rpc ({rpc}) can only be changed with one another"
)
if self.is_loading:
return
self.is_loading = True
@ -95,7 +91,8 @@ class WalletManager:
# {'Specter': {'name': 'Specter', 'alias': 'pacman', ... }, 'another_wallet': { ... } }
# It contains the same data as the JSON on disk
wallets_update_list = {}
if self.working_folder is not None and self.rpc is not None:
if self.working_folder is not None:
wallets_files = load_jsons(self.working_folder, key="name")
for wallet in wallets_files:
wallet_name = wallets_files[wallet]["name"]
@ -106,6 +103,11 @@ class WalletManager:
wallets_update_list[wallet_name]["keys_count"] = len(
wallets_files[wallet]["keys"]
)
if (
self.working_folder is not None
and self.rpc is not None
and self.chain is not None
):
if self.allow_threading and use_threading:
t = threading.Thread(
target=self._update,
@ -117,7 +119,7 @@ class WalletManager:
self._update(wallets_update_list)
else:
self.is_loading = False
logger.info(
logger.warning(
"Specter seems to be disconnected from Bitcoin Core. Skipping wallets update."
)
@ -131,6 +133,9 @@ class WalletManager:
* the unloaded wallets are loaded in Bitcoin Core
* and, on the Specter side, the wallet objects of those unloaded wallets are reinitialised
"""
logger.info(
f"Started Updating Wallets with {len(wallets_update_list.values())} wallets"
)
# list of wallets in the dict
existing_names = list(self.wallets.keys())
# list of wallet to keep
@ -138,31 +143,26 @@ class WalletManager:
try:
if wallets_update_list:
loaded_wallets = self.rpc.listwallets()
# logger.info("Getting loaded wallets list from Bitcoin Core")
for wallet in wallets_update_list:
wallet_alias = wallets_update_list[wallet]["alias"]
wallet_name = wallets_update_list[wallet]["name"]
logger.info(f" Updating wallet {wallet_name}")
# wallet from json not yet loaded in Bitcoin Core?!
if os.path.join(self.rpc_path, wallet_alias) not in loaded_wallets:
try:
logger.info(
"Loading %s to Bitcoin Core"
% wallets_update_list[wallet]["alias"]
)
logger.debug(f"Loading {wallet_name} to Bitcoin Core")
self.rpc.loadwallet(
os.path.join(self.rpc_path, wallet_alias)
)
logger.info(
"Initializing %s Wallet object"
% wallets_update_list[wallet]["alias"]
)
logger.debug("Initializing {wallet_name} Wallet object")
loaded_wallet = self.WalletClass.from_json(
wallets_update_list[wallet],
self.device_manager,
self,
)
# Lock UTXO of pending PSBTs
logger.info(
logger.debug(
"Re-locking UTXOs of wallet %s"
% wallets_update_list[wallet]["alias"]
)
@ -205,6 +205,7 @@ class WalletManager:
logger.warning(
f"Couldn't load wallet {wallet_alias}. Silently ignored! Wallet error: {e}"
)
logger.exception(e)
self._failed_load_wallets.append(
{
**wallets_update_list[wallet],
@ -236,7 +237,10 @@ class WalletManager:
# only ignore rpc errors
except RpcError as e:
logger.error(f"Failed updating wallet manager. RPC error: {e}")
# logger.info("Done updating wallet manager")
logger.info("Updating wallet manager done. Result:")
logger.info(f" * failed_load_wallets: {self._failed_load_wallets}")
logger.info(f" * loaded_wallets: {len(self.wallets)}")
wallets_update_list = {}
self.is_loading = False

View file

@ -1,13 +1,16 @@
import json
import logging
import os
from os import path
from embit.liquid.networks import get_network
from flask import render_template
from flask_babel import lazy_gettext as _
from requests.exceptions import ConnectionError
from .helpers import is_testnet, is_liquid
from .helpers import deep_update, is_liquid, is_testnet
from .liquid.rpc import LiquidRPC
from .persistence import write_node
from .persistence import PersistentObject, write_node
from .rpc import (
BitcoinRPC,
RpcError,
@ -19,7 +22,117 @@ from .specter_error import BrokenCoreConnectionException
logger = logging.getLogger(__name__)
class Node:
class AbstractNode(PersistentObject):
"""This is a Node class worth deriving from. It tries to define as many attributes as possible which are needed but probably in a very
inefficient way, e.g. without any caching. Feel free to improve that in subclasses and you might get inspired by existing sublasses
"""
# Many properties are convenience properties of informations, you get from the various info-rpc-callse, namely:
# * getblockchaininfo
# * getnetworkinfo
# * getmempoolinfo
# * uptime
# * getblockhash
# * scantxoutset
# So first, here are the one which return directly the content of one of those calls as dict:
@property
def info(self):
"""Should be a combination of various info calls from Bitcoin Core. Could have:
* all the info from https://developer.bitcoin.org/reference/rpc/getblockchaininfo.html
* plus 'mempool_info' from https://developer.bitcoin.org/reference/rpc/getmempoolinfo.html
* plus uptime
* plus other stuff
We only implement a bare minimum here. See the Node-Impl for more
This method is exception-safe and returns an empty dict if the connection is broken
"""
try:
res = [
r["result"]
for r in self.rpc.multi(
[
("getblockchaininfo", None),
("getnetworkinfo", None),
("getmempoolinfo", None),
("uptime", None),
("getblockhash", 0),
("scantxoutset", "status", []),
]
)
]
info = res[0]
info["mempool_info"] = res[2]
info["uptime"] = res[3]
return info
except BrokenCoreConnectionException:
return {}
@property
def network_info(self):
"""https://developer.bitcoin.org/reference/rpc/getnetworkinfo.html
returns an almost empty dict if Broken Connection
"""
try:
return self.rpc.getnetworkinfo()
except BrokenCoreConnectionException:
return {"subversion": "", "version": 999999}
@property
def uptime(self):
"""https://developer.bitcoin.org/reference/rpc/uptime.html"""
return self.rpc.uptime()
# Now some even more convenient properties which provide often used handpicked information from those dicts
@property
def chain(self):
"""current network name (main, test, regtest)
might have more values in elements/liquid
"""
try:
return self.info.get("chain")
except BrokenCoreConnectionException:
# This is the most important part to signal that the node-connection is broken without throwin an Exception
return None
@property
def bitcoin_core_version_raw(self):
try:
return self.rpc.getnetworkinfo()["version"]
except BrokenCoreConnectionException:
# This is the most important part to signal that the node-connection is broken without throwin an Exception
return 99999
# ... and more derived properties which already calculate stuff based on those information
@property
def network_parameters(self):
return get_network(self.chain)
@property
def is_running(self):
if self.network_info["version"] == 999999:
logger.debug(f"Node is not running")
return False
else:
return True
def check_blockheight(self):
"""check_blockheight is a method which is probably deprecated.
It should return True if there are new blocks available since check_info has been called
(which updates the cached _info[] dict)
"""
raise NotImplemented(
"A Node Implementation need to implement the check_blockheight method"
)
def node_info_template(self):
"""This should return the path to a template as string"""
return "node/components/bitcoin_core_info.jinja"
class Node(AbstractNode):
"""A Node represents the connection to a Bitcoin and/or Liquid (Full-) node.
It can be created via Constructor or from_json, and mainly it can give you A
RPC-object to use the API.
@ -28,6 +141,8 @@ class Node:
One or many Nodes are managed via the NodeManager
"""
external_node = True
def __init__(
self,
name,
@ -39,7 +154,6 @@ class Node:
port,
host,
protocol,
external_node,
fullpath,
node_type,
manager,
@ -55,7 +169,6 @@ class Node:
:param port: usually something like 8332 for mainnet, 18332 for testnet, 18443 for Regtest, 38332 for signet
:param host: domainname or ip-address. Don't add the protocol here
:param protocol: Usually https or http
:param external_node: should be True for Node and False for InternalNode
:param fullpath: it's assumed that you want to store it on disk AND decide about the fullpath upfront
:param node_type: either "ELM" or "BTC", will impact autodetection (datadir and Env-vars)
:param manager: A NodeManager instance which will get notified if the Node's name changes, the proxy_url will get copied from the manager as well
@ -69,7 +182,6 @@ class Node:
self.port = port
self.host = host
self.protocol = protocol
self.external_node = external_node
self.fullpath = fullpath
self._node_type = node_type
self.manager = manager
@ -95,7 +207,6 @@ class Node:
port = node_dict.get("port", None)
host = node_dict.get("host", "localhost")
protocol = node_dict.get("protocol", "http")
external_node = node_dict.get("external_node", True)
fullpath = node_dict.get("fullpath", default_fullpath)
return cls(
@ -108,7 +219,6 @@ class Node:
port,
host,
protocol,
external_node,
fullpath,
node_type,
manager,
@ -117,20 +227,23 @@ class Node:
@property
def json(self):
"""Get a json-representation of this Node"""
return {
"name": self.name,
"alias": self.alias,
"autodetect": self.autodetect,
"datadir": self.datadir,
"user": self.user,
"password": self.password,
"port": self.port,
"host": self.host,
"protocol": self.protocol,
"external_node": self.external_node,
"fullpath": self.fullpath,
"node_type": self.node_type,
}
node_json = super().json
return deep_update(
node_json,
{
"name": self.name,
"alias": self.alias,
"autodetect": self.autodetect,
"datadir": self.datadir,
"user": self.user,
"password": self.password,
"port": self.port,
"host": self.host,
"protocol": self.protocol,
"fullpath": self.fullpath,
"node_type": self.node_type,
},
)
def _get_rpc(self):
"""Checks if configurations have changed, compares with old rpc
@ -192,7 +305,7 @@ class Node:
logger.error(rpce)
return None
except BrokenCoreConnectionException as bcce:
logger.error(f"{bcce} while get_rpc")
logger.error(f"{bcce} while get_rpc for {rpc}")
return None
except Exception as e:
logger.exception(e)
@ -262,7 +375,6 @@ class Node:
self.name = new_name
logger.info(f"persisting {self} in rename")
write_node(self, self.fullpath)
self.manager.update()
def check_info(self):
self._is_configured = self.rpc is not None
@ -515,5 +627,7 @@ class Node:
logger.debug(f"Updating {self}.rpc {self._rpc} with None (setter)")
self._rpc = value
# UI specific stuff
def __repr__(self) -> str:
return f"<{self.__class__.__name__} name={self.name} fullpath={self.fullpath}>"

View file

@ -13,7 +13,9 @@ import threading
from flask import current_app as app
from .specter_error import SpecterError
from cryptoadvance.specter.util.reflection import get_class
from .specter_error import SpecterError, SpecterInternalException
from .services.callbacks import specter_persistence_callback
from .util.shell import run_shell
@ -23,6 +25,68 @@ fslock = threading.Lock()
pclock = threading.Lock()
class PersistentObject:
"""An object which contains reasonable infrastructure to get un-/persisted (in json)
As such, other than the name implies, it doesn't contain any business specific attributes
Its usage is not (yet?!) supported in this persistence module but let's see
"""
@property
def fqcn(self):
"""the fully qualified class Name, e.g. "cryptoadvance.specter.node.Node"""
return f"{self.__class__.__module__}.{self.__class__.__name__}"
@property
def json(self):
"""A property to easily transform your BO to a dict. Use it like:
mybo_json = super().json
return deep_update(
mybo_json,
{
"name": self.name,
"alias": self.alias,
[...]
},
)
"""
self_json = {}
self_json["python_class"] = self.fqcn
return self_json
@property
def is_specter_core_object(self):
"""Whether the class is defined in cryptoadvance.specter"""
return self.fqcn.startswith("cryptoadvance.specter.")
@property
def ext_id(self):
"""returns the third part of yourorg.specterext.ext_id"""
if self.is_specter_core_object:
return None
else:
return self.fqcn.split(".")[2]
@property
def blueprint(self):
"""returns the blueprint of the extension (assuming there is only a default one)"""
if self.is_specter_core_object:
return ""
else:
return f"{self.ext_id}_endpoint"
@classmethod
def from_json(cls, a_dict, *args, **kwargs):
"""Creates a PersistentObject of the right class
Might throw a SpecterInternalException
"""
try:
clazz = get_class(a_dict["python_class"])
except KeyError:
raise SpecterInternalException("dict does not have a python_class")
return clazz.from_json(a_dict, *args, **kwargs)
def read_json_file(path):
"""read_json_file from the .specter-directory. Don't use it for
something else"""

View file

@ -191,10 +191,15 @@ def autodetect_rpc_confs(
except requests.exceptions.RequestException as e:
pass
# no point in reporting that here
except SpecterError as e:
# Timeout
pass
except RpcError:
pass
# have to make a list of acceptable exception unfortunately
# please enlarge if you find new ones
else:
logger.info(f"No candidates for BTC-connection autodetection found")
return available_conf_arr
@ -391,6 +396,7 @@ class BitcoinRPC:
to,
)
)
logger.exception(to)
raise SpecterError(
"Timeout after {} secs while {} call({: <28}). Check the logs for more details.".format(
timeout,

View file

@ -142,16 +142,22 @@ def init_app(app: SpecterFlask, hwibridge=False, specter=None):
data_folder=app.config["SPECTER_DATA_FOLDER"],
config=app.config["DEFAULT_SPECTER_CONFIG"],
internal_bitcoind_version=app.config["INTERNAL_BITCOIND_VERSION"],
initialize=False,
)
# HWI
specter.hwi = HWIBridge()
# ServiceManager will instantiate and register blueprints for extensions
# It's an attribute to the specter but specter is not aware of it.
# However some managers are aware of it and so we need to split
# instantiation from initializing and in between attach the service_manager
specter.service_manager = ServiceManager(
specter=specter, devstatus_threshold=app.config["SERVICES_DEVSTATUS_THRESHOLD"]
)
specter.initialize()
# HWI
specter.hwi = HWIBridge()
login_manager = LoginManager()
login_manager.session_protection = app.config.get("SESSION_PROTECTION", "strong")
login_manager.init_app(app) # Enable Login
@ -259,6 +265,7 @@ def init_app(app: SpecterFlask, hwibridge=False, specter=None):
scheduler.init_app(app)
scheduler.start()
logger.info("----> starting service callback_after_serverpy_init_app ")
specter.service_manager.execute_ext_callbacks(
after_serverpy_init_app, scheduler=scheduler
)

View file

@ -41,7 +41,7 @@ from .price import price_endpoint
from .settings import settings_endpoint
from .setup import setup_endpoint
from .wallets import wallets_endpoint
from .wallets_api import wallets_endpoint_api
from .wallets.wallets_api import wallets_endpoint_api
from .welcome import welcome_endpoint
spc_prefix = app.config["SPECTER_URL_PREFIX"]
@ -170,8 +170,6 @@ def selfcheck():
"""check status before every request"""
if app.specter.rpc is not None:
type(app.specter.rpc).counter = 0
if not app.specter.chain:
app.specter.check()
if app.config.get("LOGIN_DISABLED"):
app.login("admin")

View file

@ -3,6 +3,7 @@ from flask import current_app as app
from flask import Blueprint
from jinja2 import pass_context
from ..helpers import to_ascii20
from markupsafe import Markup
from ..util.common import format_btc_amount_as_sats, format_btc_amount
filters_bp = Blueprint("filters", __name__)
@ -16,6 +17,23 @@ def ascii20(context, name):
return to_ascii20(name)
@pass_context
@filters_bp.app_template_filter("subrender")
def subrender_filter(context, value):
"""This can render a variable as it would be template-text like:
{{ "Hello, {{name}}"|subrender }}
based on the idea here:
https://stackoverflow.com/questions/8862731/jinja-nested-rendering-on-variable-content
Currently not used but tried it out for Node_sessings_rendering and kept in here
for extensions to maybe use later.
"""
_template = context.eval_ctx.environment.from_string(value)
result = _template.render(**context)
if context.eval_ctx.autoescape:
result = Markup(result)
return result
@pass_context
@filters_bp.app_template_filter("unique_len")
def unique_len(context, arr):

View file

@ -14,7 +14,11 @@ from flask_login import login_required, current_user
from flask import current_app as app
from ..rpc import get_default_datadir
from ..node import Node
from ..specter_error import ExtProcTimeoutException, BrokenCoreConnectionException
from ..specter_error import (
ExtProcTimeoutException,
BrokenCoreConnectionException,
SpecterError,
)
from ..util.shell import get_last_lines_from_file
from ..server_endpoints import flash
@ -34,7 +38,16 @@ nodes_endpoint = Blueprint("nodes_endpoint", __name__)
def node_settings(node_alias):
if node_alias:
try:
node = app.specter.node_manager.get_by_alias(node_alias)
node: Node = app.specter.node_manager.get_by_alias(node_alias)
if not node.is_specter_core_object:
return redirect(
url_for(
# This is a convention which should be documented
# Maybe we should do that differently
f"{node.blueprint}.node_settings",
node_alias=node.alias,
)
)
if not node.external_node:
return redirect(
url_for(
@ -42,7 +55,8 @@ def node_settings(node_alias):
node_alias=node.alias,
)
)
except:
except SpecterError as se:
assert str(se).endswith("does not exist!")
return render_template(
"base.jinja", error=_("Node not found"), specter=app.specter, rand=rand
)
@ -57,7 +71,6 @@ def node_settings(node_alias):
"port": 8332,
"host": "localhost",
"protocol": "http",
"external_node": True,
},
app.specter.node_manager,
)
@ -138,7 +151,6 @@ def node_settings(node_alias):
port,
host,
protocol,
node.external_node,
node.fullpath,
"BTC",
node.manager,
@ -169,7 +181,7 @@ def node_settings(node_alias):
specter=app.specter,
rand=rand,
)
node = app.specter.node_manager.add_node(
node = app.specter.node_manager.add_external_node(
"BTC",
node.name,
autodetect,
@ -179,7 +191,6 @@ def node_settings(node_alias):
port,
host,
protocol,
node.external_node,
)
app.specter.update_active_node(node.alias)
return redirect(
@ -197,8 +208,8 @@ def node_settings(node_alias):
)
if not success:
flash(_("Saving failed: no connection to node"), "error")
if app.specter.active_node_alias == node.alias:
app.specter.check()
if success:
return redirect(url_for("welcome_endpoint.index"))
return render_template(
"node/node_settings.jinja",

View file

@ -0,0 +1 @@
from .wallets import *

View file

@ -10,17 +10,19 @@ from flask import jsonify, redirect, render_template, request, url_for
from flask_babel import lazy_gettext as _
from flask_login import login_required
from ..commands.psbt_creator import PsbtCreator
from ..helpers import bcur2base64, get_devices_with_keys_by_type, get_txid
from ..key import Key
from ..managers.wallet_manager import purposes
from ..persistence import delete_file
from ..server_endpoints import flash
from ..services import callbacks
from ..specter_error import SpecterError, handle_exception
from ..util.tx import convert_rawtransaction_to_psbt, is_hex
from ..util.wallet_importer import WalletImporter
from ..wallet import Wallet
from ...commands.psbt_creator import PsbtCreator
from ...helpers import bcur2base64, get_devices_with_keys_by_type, get_txid
from ...key import Key
from ...managers.wallet_manager import purposes
from ...persistence import delete_file
from ...server_endpoints import flash
from ...services import callbacks
from ...services.callbacks import adjust_view_model
from ...specter_error import SpecterError, handle_exception
from ...util.tx import convert_rawtransaction_to_psbt, is_hex
from ...util.wallet_importer import WalletImporter
from ...wallet import Wallet
from .wallets_vm import WalletsOverviewVm
logger = logging.getLogger(__name__)
@ -68,15 +70,34 @@ def inject_common_stuff():
@login_required
def wallets_overview():
app.specter.check_blockheight()
# The execute_ext_callbacks method is not really prepared for the things we're doing here.
# that's why we need so many lines for just expressing:
# "Here is a ViewModel, adjust it if you want"
# We need to change that method to enable "middleware"
wallets_overview_vm_dict = app.specter.service_manager.execute_ext_callbacks(
adjust_view_model, WalletsOverviewVm()
)
if len(wallets_overview_vm_dict.values()) > 1:
raise logger.error(
"Seems that we have more than one WalletsOverviewVm Extension"
)
if len(wallets_overview_vm_dict.values()) == 1:
wallets_overview_vm = list(wallets_overview_vm_dict.values())[0]
else:
wallets_overview_vm = WalletsOverviewVm()
if wallets_overview_vm.wallets_overview_redirect != None:
return redirect(wallets_overview_vm.wallets_overview_redirect)
for wallet in list(app.specter.wallet_manager.wallets.values()):
wallet.update_balance()
wallet.check_utxo()
return render_template(
"wallet/wallets_overview.jinja",
"wallet/overview/wallets_overview.jinja",
specter=app.specter,
rand=rand,
services=app.specter.service_manager.services,
wallets_overview_vm=wallets_overview_vm,
)

View file

@ -20,19 +20,19 @@ from flask_babel import lazy_gettext as _
from flask_login import current_user, login_required
from werkzeug.wrappers import Response
from ..commands.psbt_creator import PsbtCreator
from ..helpers import bcur2base64
from ..rpc import RpcError
from ..server_endpoints import flash
from ..server_endpoints.filters import assetlabel
from ..specter_error import SpecterError, handle_exception
from ..util.base43 import b43_decode
from ..util.descriptor import Descriptor
from ..util.fee_estimation import FeeEstimationResultEncoder, get_fees
from ..util.mnemonic import generate_mnemonic
from ..util.price_providers import get_price_at
from ..util.tx import decoderawtransaction
from ..wallet import Wallet
from ...commands.psbt_creator import PsbtCreator
from ...helpers import bcur2base64
from ...rpc import RpcError
from ...server_endpoints import flash
from ...server_endpoints.filters import assetlabel
from ...specter_error import SpecterError, handle_exception
from ...util.base43 import b43_decode
from ...util.descriptor import Descriptor
from ...util.fee_estimation import FeeEstimationResultEncoder, get_fees
from ...util.mnemonic import generate_mnemonic
from ...util.price_providers import get_price_at
from ...util.tx import decoderawtransaction
from ...wallet import Wallet
logger = logging.getLogger(__name__)

View file

@ -0,0 +1,16 @@
from dataclasses import dataclass
@dataclass
class WalletsOverviewVm:
"""An object to control what is being displayed at the wallet_overview endpoint
If you set the different attributes, you can modify the behaviour.
e.g. setting the about_redirect to url_for(...) will cause a redirect
Check the wallets_overview.jinja template to understand exact usage
"""
wallets_overview_redirect: str = None
header_and_summary_include: str = "wallet/overview/header_and_summary.jinja"
balance_overview_include: str = "wallet/overview/balance_overview.jinja"
tx_table_include: str = "wallet/overview/tx_table.jinja"

View file

@ -0,0 +1 @@
from .welcome import *

View file

@ -8,9 +8,12 @@ from flask import make_response, redirect, render_template, request, url_for
from flask_babel import lazy_gettext as _
from flask_login import login_required
from ..helpers import notify_upgrade
from ..managers.wallet_manager import purposes
from ..server_endpoints import flash
from ...helpers import notify_upgrade
from ...managers.wallet_manager import purposes
from ...server_endpoints import flash
from ...services.callbacks import adjust_view_model
from ...specter_error import SpecterError
from .welcome_vm import WelcomeVm
logger = logging.getLogger(__name__)
@ -46,6 +49,22 @@ def index():
@login_required
def about():
notify_upgrade(app, flash)
# The execute_ext_callbacks method is not really prepared for the things we're doing here.
# that's why we need so many lines for just expressing:
# "Here is a ViewModel, adjust it if you want"
# We need to change that method to enable "middleware"
welcome_vm_dict = app.specter.service_manager.execute_ext_callbacks(
adjust_view_model, WelcomeVm()
)
if len(welcome_vm_dict.values()) > 1:
raise SpecterError("Seems that we have more than one Welcome Extension")
if len(welcome_vm_dict.values()) == 1:
welcome_vm = list(welcome_vm_dict.values())[0]
else:
welcome_vm = WelcomeVm()
if welcome_vm.about_redirect != None:
return redirect(welcome_vm.about_redirect)
if request.method == "POST":
action = request.form["action"]
if action == "cancelsetup":
@ -54,8 +73,9 @@ def about():
app.specter.reset_setup("torbrowser")
return render_template(
"base.jinja",
"welcome/welcome.jinja",
specter=app.specter,
welcome_vm=welcome_vm,
rand=rand,
supported_languages=app.supported_languages,
)

View file

@ -0,0 +1,17 @@
from dataclasses import dataclass
@dataclass
class WelcomeVm:
"""An object to control what is being displayed at the About endpoint
If you set the different attributes, you can modify the behaviour.
e.g. setting the about_redirect to url_for(...) will cause a redirect
Check the welcome.jinja template to understand exact usage
"""
about_redirect: str = None
specter_remote_include: str = "welcome/components/specter_remote.jinja"
get_started_include: str = "welcome/components/get_started.jinja"
tick_checkboxes_include: str = "welcome/components/tick_checkboxes.jinja"
remaining_remarks_include = "welcome/components/remaining_remarks.jinja"

View file

@ -35,6 +35,12 @@ add_settingstabs = "add_settingstabs"
"""
add_wallettabs = "add_wallettabs"
""" Endpoints might define their behaviour via a ViewModel. Those Models are passed here and
extensions can modify that behaviour via Modifying that model. Currently there is only:
cryptoadvance.specter.server_enpoints.welcome.welcome_vm.WelcomeVm
"""
adjust_view_model = "adjust_view_model"
"""
This one is called, whenever a file is persisted. To call external scripts in another
process, you better use the SPECTER_PERSISTENCE_CALLBACK Env Var or it's asynchronous cousin

View file

@ -47,6 +47,10 @@ class Service:
raise Exception(f"Service {self.__class__} needs name")
self.active = active
self.specter = specter
self.data_folder = os.path.join(self.specter.data_folder, "extensions", self.id)
if not os.path.exists(self.data_folder):
logger.info(f"Creating extension data_folder {self.data_folder} ")
os.makedirs(self.data_folder)
@classmethod
def _storage_manager(cls):

View file

@ -68,7 +68,12 @@ class Specter:
config={},
internal_bitcoind_version="",
checker_threads=True,
initialize=True,
):
"""Very basic Initialisation of the Specter Object. Will, by default call specter.initialize() shortly after
This might be unwanted (e.g. in server.py) in order to apply specter.service_manager so that all the
managers can make callbacks in the initialize method.
"""
if data_folder.startswith("~"):
data_folder = os.path.expanduser(data_folder)
data_folder = os.path.abspath(data_folder)
@ -78,30 +83,40 @@ class Specter:
os.makedirs(data_folder)
self.data_folder = data_folder
self._config = config
self._internal_bitcoind_version = internal_bitcoind_version
self._checker_threads = checker_threads
if initialize:
self.initialize()
def initialize(self):
"""Runs the checker_treads, instantiates all the managers and attach them to its attributes"""
self.user_manager = UserManager(
self
) # has to come before calling VersionChecker()
# version checker
# checks for new versions once per hour
logger.info("Instantiate VersionChecker")
self.version = VersionChecker(specter=self)
if checker_threads:
if self._checker_threads:
self.version.start()
self._config_manager = ConfigManager(self.data_folder, config)
logger.info("Instantiate ConfigManager")
self._config_manager = ConfigManager(self.data_folder, self._config)
self.internal_bitcoind_version = internal_bitcoind_version
self.internal_bitcoind_version = self._internal_bitcoind_version
# Migrating from Specter 1.3.1 and lower (prior to the node manager)
self.migrate_old_node_format()
logger.info("Instantiate NodeManager")
self.node_manager = NodeManager(
proxy_url=self.proxy_url,
only_tor=self.only_tor,
active_node=self.active_node_alias,
bitcoind_path=self.bitcoind_path,
internal_bitcoind_version=internal_bitcoind_version,
internal_bitcoind_version=self._internal_bitcoind_version,
data_folder=os.path.join(self.data_folder, "nodes"),
)
@ -136,12 +151,12 @@ class Specter:
self.update_tor_controller()
self.checker = Checker(lambda: self.check(check_all=True), desc="health")
if checker_threads:
if self._checker_threads:
self.checker.start()
self.price_checker = Checker(
lambda: update_price(self, self.user), desc="price"
)
if self.price_check and self.price_provider and checker_threads:
if self.price_check and self.price_provider and self._checker_threads:
self.price_checker.start()
# Configuring the two different storages (Universal json-files)
@ -206,7 +221,7 @@ class Specter:
u.check()
@property
def node(self):
def node(self) -> Node:
try:
return self.node_manager.active_node
except SpecterError as e:
@ -752,7 +767,6 @@ class Specter:
old_rpc.get("port", None),
old_rpc.get("host", "localhost"),
old_rpc.get("protocol", "http"),
True,
os.path.join(os.path.join(self.data_folder, "nodes"), "default.json"),
"BTC",
self,

View file

@ -1,5 +1,6 @@
import logging
from .util.shell import get_last_lines_from_file
from flask import current_app as app
logger = logging.getLogger(__name__)
@ -47,6 +48,8 @@ class ExtProcTimeoutException(SpecterInternalException):
def handle_exception(exception, user=None):
"""prints the exception and most important the stacktrace"""
if app.config["SPECTER_CONFIGURATION_CLASS_FULLNAME"].endswith("DevelopmentConfig"):
raise exception
logger.error("Unexpected error:")
logger.error(
"----START-TRACEBACK-----------------------------------------------------------------"

View file

@ -108,165 +108,8 @@
{% if error %}
<br><br>{{ _("Something went wrong:") }}<br><br>{{error}}
{% else %}
<br>
<div class="main-page-container">
<div style="max-width: 15em; text-align: center; margin-bottom: 2em; margin-top: .7em; margin-left: auto; margin-right: auto;">
{% include "includes/language/language_select.jinja" %}
</div>
<h1 style="font-size: 1.8em; line-height: 1em;">
{{ _('Welcome to Specter Desktop') }}
</h1>
{% if ('://localhost:' not in url_for('index', _external=True) and '://127.0.0.1:' not in url_for('index', _external=True)) and specter.hwi_bridge_url != "http://127.0.0.1:25441/hwi/api/" %}
<div style="margin: auto; display: block;">
<div class="card" style="margin: auto;padding: 5px 10px 15px; width: 90%;">
<div class="row">
<img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/>
<p style="color: #FF9A00; font-size: 1.1em; margin-left: 10px;">{{ _('Using Specter from a remote machine?') }}</p>
</div>
<p style="margin-left: 30px; margin-top: -10px; margin-right: 20px; text-align: left;">{{ _('If you run Specter on a remote machine, like Umbrel or a myNode, you need to update settings to connect hardware wallets via USB.') }}</p>
<a href="{{ url_for('settings_endpoint.hwi') }}" style="color: #fff; text-decoration: underline; cursor: pointer;">
<h4>{{ _("Update your settings") }}</h4>
</a>
</div>
<br>
</div>
{% endif %}
<div style="display: flex; justify-content: center; align-items: center;">
<div class="main-description-box">
<p style="font-size: 1.1em; margin: 1em auto 1em auto; text-align: left;">{{ _("Specter Desktop is a convenient wallet interface to better use your Bitcoin Core node. Its good for singlesig wallets, but comes with a special focus on multisignature setups for different hardware wallets and air-gapped signing devices.") }} <br><br>
&#128217; Learn <a style="color: #fff" href="https://specter-desktop-docs.netlify.app/multisig-guide/" target="_blank">{{ _("here") }}</a> more about multisig.
</p>
</div>
</div>
{% if not specter.chain %}
<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>
<br><br>
{% else %}
<h2 style="font-size: 1.6em; line-height: .5em; margin-top: 1em;">{{ _('Getting Started') }}</h2><br>
<table style="width: 90%; margin: auto;">
<tbody>
<tr class="tr-hover">
<td>
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not specter.chain else '') + 'tick.svg') }}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('setup_endpoint.node_type') }}">
<h2 style="position: relative; z-index: 1;">{{ _("Connect Specter with Bitcoin Core node.") }}</h2>
</a>
</td>
<td style="text-align: left;">
{% if not specter.chain %}
<button type="button" class="btn" onclick="showPageOverlay('connect-node-help-popup')">Need help?</button>
<div id="connect-node-help-popup" class="hidden">
<h1>{{ _("Connecting Specter to your Bitcoin Core node") }}</h1>
<div class="overflow">
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('no-node-help-popup')">{{ _("I don't have a Bitcoin Core node yet.") }}</button>
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('local-node-help-popup')">{{ _("I have Bitcoin Core on this computer.") }}</button>
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('remote-node-help-popup')">{{ _("I have a remote Bitcoin Core node.") }}</button>
</div>
</div>
<div id="no-node-help-popup" class="hidden">
<h1>{{ _("Installing and Setting Up Bitcoin Core") }}</h1>
{{ _("Check out these video tutorials on downloading and setting up Bitcoin Core with Specter Desktop") }}:<br><br>
<ul style="text-align: left; margin: auto; line-height: 2;">
<li><a href="https://www.keepitsimplebitcoin.com/setup-bitcoin-core/" target="_blank" style="color: #fff;">KIS: Setting up Bitcoin Core</a></li>
<li><a href="https://www.keepitsimplebitcoin.com/how-to-use-specter-wallet-1/" target="_blank" style="color: #fff;">KIS: Setting up Specter Desktop</a></li>
<li><a href="https://www.youtube.com/watch?v=4koKF2MDXtk&t=2s" target="_blank" style="color: #fff;">Ministry of Nodes: Setting up Bitcoin Core and Specter on Windows</a></li>
</ul>
</div>
<div id="local-node-help-popup" class="hidden">
<h1>{{ _('Connecting Specter to your local Bitcoin Core node') }}</h1>
<p>{{ _("If you're running a local Bitcoin Core node, Specter should be able to auto-detect it if its data folder is located at the default location.") }}<br>
{{ _("In case it isn't, you might need to specify the Bitcoin Core data folder path in Specter's settings page.") }}<br>
{{ _("Alternatively, you can switch off auto-detect from the settings and provide the Bitcoin Core RPC credentials and host URL manually.") }}<br><br>
<b>{{ _("Please note:")}}</b><br>{{ _('Your node must be configured with ')}}<code>server=1</code>{{ _(' line in the ')}}<code>bitcoin.conf</code>{{ _(' file.')}}<br>
{{ _("If you don't have that, add it and restart Bitcoin Core.") }}"
</p>
<br><br>
{{ _("In addition, you can check out these video tutorials for setting up Bitcoin Core with Specter Desktop") }}:<br><br>
<ul style="text-align: left; margin: auto; line-height: 2;">
<li><a href="https://www.keepitsimplebitcoin.com/setup-bitcoin-core/" target="_blank" style="color: #fff;">KIS: Setting up Bitcoin Core</a></li>
<li><a href="https://www.keepitsimplebitcoin.com/how-to-use-specter-wallet-1/" target="_blank" style="color: #fff;">KIS: Setting up Specter Desktop</a></li>
<li><a href="https://www.youtube.com/watch?v=4koKF2MDXtk&t=2s" target="_blank" style="color: #fff;">Ministry of Nodes: Setting up Bitcoin Core and Specter on Windows</a></li>
</ul>
</div>
<div id="remote-node-help-popup" class="hidden">
<h1>{{ _("Connecting Specter to your remote Bitcoin Core node") }}</h1>
<h3>{{ _('Connecting to a "node in a box"') }}</h3>
<p style="font-size: 1.05em;">
{{ _("Specter can be used with a remote Bitcoin Core node running on a machine like RaspiBlitz, myNode, Nodl, Umbrel, and Embassy.") }}<br>
{{ _("RaspiBlitz and myNode currently allow running Specter in the remote machine directly, which means it's possible to use it and skip setting up connection manually.") }}<br><br>
{{ _("If you use a different node or would still like to connect run Specter locally but connect remotely, you can do so by going to Settings, Bitcoin Core tab, and provide the Bitcoin Core RPC credentials and host URL manually.") }}"
</p>
<h3>{{ _("Connecting over Tor") }}</h3>
<p style="font-size: 1.05em;">{{ _("Connecting over Tor requires that you first configure Tor on your local machine.") }}<br>
You can checkout <a href="https://www.keepitsimplebitcoin.com/how-to-install-tor/" style="color: #fff;" target="_blank">this video tutorial</a>, or read the documentation for <a href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/tor.md" target="_blank" style="color: #fff;">Mac and Linux</a> or <a href="https://github.com/Fonta1n3/FullyNoded/blob/master/Docs/Tor/Tor.md#windows-10" target="_blank" style="color: #fff;">Window</a><br>
</p>
</div>
{% endif %}
</td>
</tr>
<tr class="tr-hover">
<td>
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not (specter.device_manager.devices | length) else '') + 'tick.svg') }}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('devices_endpoint.new_device_type') }}">
<h2 style="position: relative; z-index: 1;">{{ _("Add signing devices you wish to use.") }}</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
<tr class="tr-hover">
<td>
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not (specter.wallet_manager.wallets | length) else '') + 'tick.svg') }}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('wallets_endpoint.new_wallet_type') }}">
<h2 style="position: relative; z-index: 1;">{{ _("Create your first wallet!") }}</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
{% if specter.chain == 'main' %}
<tr class="tr-hover">
<td>
<img src="{{ url_for('static', filename='img/ghost_3d.png')}}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('welcome_endpoint.get_whitepaper') }}">
<h2>{{ _("And when you are all set: Get the whitepaper from the timechain.") }}</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
{% endif %}
</tbody>
</table>
<br>
{% endif %}
<p style="position: relative; z-index: 1; line-height: 24px; font-size: 1.1em">&#128064;{{ _(' Have a look at our ')}}<a href="https://specter-desktop-docs.netlify.app/" target="_blank" style="color: #fff; font-weight: bolder;">{{ _('documentation')}}</a>{{ _(' and the ')}}<a href="https://specter-desktop-docs.netlify.app/faq/" target="_blank" style="color: #fff; font-weight: bolder;">{{ _('FAQ')}}</a></p><br>
<span style="position: relative; z-index: 1;">&#128583;{{ _(" Still having issues?") }}</span><br>
<div style="display: flex; justify-content: center;">
<div style="max-width: 80%;">
<p style="position: relative; z-index: 1;">{{ _('Feel free to ')}}<a href="https://github.com/cryptoadvance/specter-desktop/issues/new" target="_blank" style="color: #fff;">{{ _('open an issue on GitHub')}}</a>{{ _(' or ask in the ')}}<a href="https://t.me/spectersupport" target="_blank" style="color: #fff;">{{ _('Specter Community telegram chat')}}</a></p>
</div>
</div>
<br>
<a href="https://github.com/cryptoadvance/specter-desktop#help-wanted-do-you-like-specter" target="_blank" style="position: relative; z-index: 1; color: #fff; display: inline-block;"><h2 id="help-wanted-text" style="display: inline-block;">{{ _('Help wanted: Do you like Specter?') }}</h2><img src="{{ url_for('static', filename='img/love.png') }}" width="20px" style="vertical-align: sub; margin-left: 10px;"/></a>
<div class="footer">
<span style="position: relative; z-index: 1;">{{ _("Supported and maintained by") }}:</span><br>
<a href="https://specter.solutions/" target="_blank" style="color: #fff; position: relative; z-index: 1;">
Specter.Solutions
</a>
<div class="row">
<a href="https://github.com/cryptoadvance/" target="_blank"><img src="{{ url_for('static', filename='img/github.svg') }}" style="width: 30px; margin: 10px; position: relative; z-index: 1;"></a>
<a href="https://twitter.com/specterwallet/" target="_blank"><img src="{{ url_for('static', filename='img/twitter.svg') }}" style="width: 30px; margin: 10px; position: relative; z-index: 1;"></a>
</div>
<span style="font-size: 0.8em;">uid: {{ specter.config_manager.data["uid"] }}</span>
<br><br>{{ _("Here should be some content but unfortunately, none here!") }}<br><br>
</div>
{% endif %}
{% endblock %}

View file

@ -1,85 +1,4 @@
<div id="bitcoin_core_info" class="hidden" style="text-align: left; max-width: 500px;">
<h1>{{ _("Bitcoin Core Node Info") }}:</h1>
{% if specter.chain %}
{% if specter.info['pruned'] %}
<p class="warning"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("You are using a pruned node.")}}<br>{{ _("Pruned nodes may not be able to scan for old balances when importing an existing wallet.")}}<br>{{ _("It is recommended to use Specter with an unpruned node.") }}</p>
<br>
{% elif not specter.info['blockfilterindex'] %}
<p class="warning"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("Your node does not have ")}}<code>blockfilterindex</code>{{ _(" enabled.")}}<br>{{ _("Setting ")}}<code>blockfilterindex=1</code>{{ _(" in your ")}}<code>bitcoin.conf</code>{{ _(" file is recommended as it takes just a few GB of storage")}}<br>{{ _("and helps to speed-up blockchain rescanning.") }}</p>
<br>
{% endif %}
<table>
<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; width: 35%;">{{ _("Bitcoin Core Version") }}:</td> <td style="text-align: right;">v{{specter.bitcoin_core_version}} <span class="note">({{specter.network_info['version']}})</span></td> </tr>
<tr> <td style="text-align: left;">{{ _("Connections count") }}:</td> <td style="text-align: right;">{{specter.network_info['connections']}}</td> </tr>
<tr> <td style="text-align: left;">{{ _("Difficulty") }}:</td> <td style="text-align: right;">{{specter.info.get('difficulty', 0) | int}}</td> </tr>
<tr> <td style="text-align: left;">{{ _("Size on disk") }}:</td> <td style="text-align: right;">{{specter.info['size_on_disk']|bytessize }}</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;">{{ _("Mempool Size") }}:</td> <td style="text-align: right;">{{specter.info['mempool_info'].size}} transactions</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 Bitcoin Core's") }} <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 and so, you're effectively a precoiner! 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 Bitcoin Core RPC Connection") }}
</a>
</div>
{% endif %}
{% endif %}
{% include specter.node.node_info_template() %}
{# {{ specter.node.node_info_template() | subrender }} #}
</div>

View file

@ -63,7 +63,7 @@
</style>
<div id="side-content">
<nav class="side">
{% if specter.info["initialblockdownload"] %}
{% if specter.info.get("initialblockdownload") %}
<p class="warning" style="margin-top: 30px;"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("Bitcoin Core is still syncing...")}}<br>{{ _('(data might be outdated)')}}</p>
{% endif %}
{% if specter.network_info.version < 200000 %}

View file

@ -0,0 +1,84 @@
<h1>{{ _("Bitcoin Core Node Info") }}:</h1>
{% if specter.chain %}
{% if specter.info['pruned'] %}
<p class="warning"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("You are using a pruned node.")}}<br>{{ _("Pruned nodes may not be able to scan for old balances when importing an existing wallet.")}}<br>{{ _("It is recommended to use Specter with an unpruned node.") }}</p>
<br>
{% elif not specter.info['blockfilterindex'] %}
<p class="warning"><img src="{{ url_for('static', filename='img/info_sign.svg') }}" style="width: 20px;"/><br>{{ _("Your node does not have ")}}<code>blockfilterindex</code>{{ _(" enabled.")}}<br>{{ _("Setting ")}}<code>blockfilterindex=1</code>{{ _(" in your ")}}<code>bitcoin.conf</code>{{ _(" file is recommended as it takes just a few GB of storage")}}<br>{{ _("and helps to speed-up blockchain rescanning.") }}</p>
<br>
{% endif %}
<table>
<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; width: 35%;">{{ _("Bitcoin Core Version") }}:</td> <td style="text-align: right;">v{{specter.bitcoin_core_version}} <span class="note">({{specter.network_info['version']}})</span></td> </tr>
<tr> <td style="text-align: left;">{{ _("Connections count") }}:</td> <td style="text-align: right;">{{specter.network_info['connections']}}</td> </tr>
<tr> <td style="text-align: left;">{{ _("Difficulty") }}:</td> <td style="text-align: right;">{{specter.info.get('difficulty', 0) | int}}</td> </tr>
<tr> <td style="text-align: left;">{{ _("Size on disk") }}:</td> <td style="text-align: right;">{{specter.info['size_on_disk']|bytessize }}</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;">{{ _("Mempool Size") }}:</td> <td style="text-align: right;">{{specter.info['mempool_info'].size}} transactions</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 Bitcoin Core's") }} <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 and so, you're effectively a precoiner! 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 Bitcoin Core RPC Connection") }}
</a>
</div>
{% endif %}
{% endif %}

View file

@ -1,5 +1,6 @@
{{ plugin_card('Device Blinder', 'Privacy in Collobarative Multisig.', url_for('static', filename='img/ghost_3d.png'), "https://github.com/mflaxman/blind-xpub") }}
{{ plugin_card('Joinmarket Frontend', 'Joinmarket Frontend', "https://raw.githubusercontent.com/cryptoadvance/specterext-jmf/e245825e909dd1d567cb4b427670d4e7018ca2c2/images/logo.svg" , "https://github.com/cryptoadvance/specterext-jmf") }}
{{ plugin_card('Cashu', 'Chaumian Ecash wallet', url_for('static', filename='img/ghost.png') , "https://github.com/callebtc/cashu") }}
{{ plugin_card('GoogleDrive', 'Backup and Restore via Google Drive', url_for('static', filename='img/ghost.png') , "https://github.com/cryptoadvance/specter-desktop/pull/1376") }}
{{ plugin_card('Something missing?', 'Add to the wishlist', url_for('static', filename='img/ghost.png'), "https://github.com/cryptoadvance/specter-desktop/edit/master/src/cryptoadvance/specter/templates/services/components/wishlist.jinja") }}

View file

@ -0,0 +1,17 @@
{% from 'wallet/history/components/total_wallet_balances.jinja' import total_wallet_balances %}
{% set amount_total = specter.wallet_manager.wallets.values() | sum(attribute='amount_total') %}
{% set amount_confirmed = specter.wallet_manager.wallets.values() | sum(attribute='amount_confirmed') %}
{% set amount_unconfirmed = specter.wallet_manager.wallets.values() | sum(attribute='amount_unconfirmed') %}
{% set amount_immature = specter.wallet_manager.wallets.values() | sum(attribute='amount_immature') %}
{% set balance = specter.wallet_manager.joined_balance() %}
{% set rescan_progress = specter.wallet_manager.wallets.values() | average_of_attribute(attribute='rescan_progress') %}
{{ total_wallet_balances(
_("Combined Wallet Balances"),
amount_total,
amount_confirmed,
amount_unconfirmed,
amount_immature,
balance,
rescan_progress,
"",
specter) }}

View file

@ -0,0 +1,2 @@
<h1 id="title">{{ _("Wallets Overview") }}</h1>
<p style="width: 80%; text-align: center; margin-left: auto; margin-right: auto;">{{ _("Here you can see the combined balance and transactions history of all your Specter wallets.") }}</p>

View file

@ -0,0 +1,18 @@
<div class="table-holder">
{% include "includes/tx-row.html" %}
{% include "includes/tx-data.html" %}
{% include "includes/explorer-link.html" %}
{% include "includes/tx-table.html" %}
<tx-table
{% if specter.price_check and (specter.alt_rate and specter.alt_symbol) %}
price="{{ specter.alt_rate }}"
symbol="{{ specter.alt_symbol }}"
{% endif %}
btc-unit="{{ specter.unit }}"
hide-sensitive-info="{{ specter.hide_sensitive_info | lower }}"
type="txlist"
scrollbar-default="on"
table-border-radius="0px">
</tx-table>
</div>
<div id="tx-popup" class="hidden"></div>

View file

@ -0,0 +1,26 @@
{% extends "base.jinja" %}
{% block main %}
<style>
.arrow {
text-decoration: none;
display: inline-block;
margin: 0px 16px 16px;
border-radius: 50%;
color: #fff;
font-size: 1.5em;
}
.disabled {
visibility: hidden;
}
</style>
{% include "includes/services-data.html" %}
{% include "includes/address-label.html" %}
{% include wallets_overview_vm.header_and_summary_include %}
{% include wallets_overview_vm.balance_overview_include %}
{% include wallets_overview_vm.tx_table_include %}
{% endblock %}

View file

@ -1,56 +0,0 @@
{% extends "base.jinja" %}
{% block main %}
<style>
.arrow {
text-decoration: none;
display: inline-block;
margin: 0px 16px 16px;
border-radius: 50%;
color: #fff;
font-size: 1.5em;
}
.disabled {
visibility: hidden;
}
</style>
{% include "includes/services-data.html" %}
{% include "includes/address-label.html" %}
<h1 id="title">{{ _("Wallets Overview") }}</h1>
<p style="width: 80%; text-align: center; margin-left: auto; margin-right: auto;">{{ _("Here you can see the combined balance and transactions history of all your Specter wallets.") }}</p>
{% from 'wallet/history/components/total_wallet_balances.jinja' import total_wallet_balances %}
{% set amount_total = specter.wallet_manager.wallets.values() | sum(attribute='amount_total') %}
{% set amount_confirmed = specter.wallet_manager.wallets.values() | sum(attribute='amount_confirmed') %}
{% set amount_unconfirmed = specter.wallet_manager.wallets.values() | sum(attribute='amount_unconfirmed') %}
{% set amount_immature = specter.wallet_manager.wallets.values() | sum(attribute='amount_immature') %}
{% set balance = specter.wallet_manager.joined_balance() %}
{% set rescan_progress = specter.wallet_manager.wallets.values() | average_of_attribute(attribute='rescan_progress') %}
{{ total_wallet_balances(
_("Combined Wallet Balances"),
amount_total,
amount_confirmed,
amount_unconfirmed,
amount_immature,
balance,
rescan_progress,
"",
specter) }}
<div class="table-holder">
{% include "includes/tx-row.html" %}
{% include "includes/tx-data.html" %}
{% include "includes/explorer-link.html" %}
{% include "includes/tx-table.html" %}
<tx-table
{% if specter.price_check and (specter.alt_rate and specter.alt_symbol) %}
price="{{ specter.alt_rate }}"
symbol="{{ specter.alt_symbol }}"
{% endif %}
btc-unit="{{ specter.unit }}"
hide-sensitive-info="{{ specter.hide_sensitive_info | lower }}"
type="txlist"
scrollbar-default="on"
table-border-radius="0px">
</tx-table>
</div>
<div id="tx-popup" class="hidden"></div>
{% endblock %}

View file

@ -0,0 +1,2 @@
<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>
<br><br>

View file

@ -0,0 +1,20 @@
<p style="position: relative; z-index: 1; line-height: 24px; font-size: 1.1em">&#128064;{{ _(' Have a look at our ')}}<a href="https://specter-desktop-docs.netlify.app/" target="_blank" style="color: #fff; font-weight: bolder;">{{ _('documentation')}}</a>{{ _(' and the ')}}<a href="https://specter-desktop-docs.netlify.app/faq/" target="_blank" style="color: #fff; font-weight: bolder;">{{ _('FAQ')}}</a></p><br>
<span style="position: relative; z-index: 1;">&#128583;{{ _(" Still having issues?") }}</span><br>
<div style="display: flex; justify-content: center;">
<div style="max-width: 80%;">
<p style="position: relative; z-index: 1;">{{ _('Feel free to ')}}<a href="https://github.com/cryptoadvance/specter-desktop/issues/new" target="_blank" style="color: #fff;">{{ _('open an issue on GitHub')}}</a>{{ _(' or ask in the ')}}<a href="https://t.me/spectersupport" target="_blank" style="color: #fff;">{{ _('Specter Community telegram chat')}}</a></p>
</div>
</div>
<br>
<a href="https://github.com/cryptoadvance/specter-desktop#help-wanted-do-you-like-specter" target="_blank" style="position: relative; z-index: 1; color: #fff; display: inline-block;"><h2 id="help-wanted-text" style="display: inline-block;">{{ _('Help wanted: Do you like Specter?') }}</h2><img src="{{ url_for('static', filename='img/love.png') }}" width="20px" style="vertical-align: sub; margin-left: 10px;"/></a>
<div class="footer">
<span style="position: relative; z-index: 1;">{{ _("Supported and maintained by") }}:</span><br>
<a href="https://specter.solutions/" target="_blank" style="color: #fff; position: relative; z-index: 1;">
Specter.Solutions
</a>
<div class="row">
<a href="https://github.com/cryptoadvance/" target="_blank"><img src="{{ url_for('static', filename='img/github.svg') }}" style="width: 30px; margin: 10px; position: relative; z-index: 1;"></a>
<a href="https://twitter.com/specterwallet/" target="_blank"><img src="{{ url_for('static', filename='img/twitter.svg') }}" style="width: 30px; margin: 10px; position: relative; z-index: 1;"></a>
</div>
<span style="font-size: 0.8em;">uid: {{ specter.config_manager.data["uid"] }}</span>
</div>

View file

@ -0,0 +1,13 @@
<div style="margin: auto; display: block;">
<div class="card" style="margin: auto;padding: 5px 10px 15px; width: 90%;">
<div class="row">
<img src="{{ url_for('static', filename='img/warning_sign.svg') }}" style="width: 20px;"/>
<p style="color: #FF9A00; font-size: 1.1em; margin-left: 10px;">{{ _('Using Specter from a remote machine?') }}</p>
</div>
<p style="margin-left: 30px; margin-top: -10px; margin-right: 20px; text-align: left;">{{ _('If you run Specter on a remote machine, like Umbrel or a myNode, you need to update settings to connect hardware wallets via USB.') }}</p>
<a href="{{ url_for('settings_endpoint.hwi') }}" style="color: #fff; text-decoration: underline; cursor: pointer;">
<h4>{{ _("Update your settings") }}</h4>
</a>
</div>
<br>
</div>

View file

@ -0,0 +1,105 @@
<h2 style="font-size: 1.6em; line-height: .5em; margin-top: 1em;">{{ _('Getting Started') }}</h2><br>
<table style="width: 90%; margin: auto;">
<tbody>
<tr class="tr-hover">
<td>
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not specter.chain else '') + 'tick.svg') }}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('setup_endpoint.node_type') }}">
<h2 style="position: relative; z-index: 1;">{{ _("Connect Specter with Bitcoin Core node.") }}</h2>
</a>
</td>
<td style="text-align: left;">
{% if not specter.chain %}
<button type="button" class="btn" onclick="showPageOverlay('connect-node-help-popup')">Need help?</button>
<div id="connect-node-help-popup" class="hidden">
<h1>{{ _("Connecting Specter to your Bitcoin Core node") }}</h1>
<div class="overflow">
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('no-node-help-popup')">{{ _("I don't have a Bitcoin Core node yet.") }}</button>
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('local-node-help-popup')">{{ _("I have Bitcoin Core on this computer.") }}</button>
<button style="padding: 10px; width: 80%; font-size: 1.2em; margin: 20px auto;" type="button" class="btn" onclick="hidePageOverlay();showPageOverlay('remote-node-help-popup')">{{ _("I have a remote Bitcoin Core node.") }}</button>
</div>
</div>
<div id="no-node-help-popup" class="hidden">
<h1>{{ _("Installing and Setting Up Bitcoin Core") }}</h1>
{{ _("Check out these video tutorials on downloading and setting up Bitcoin Core with Specter Desktop") }}:<br><br>
<ul style="text-align: left; margin: auto; line-height: 2;">
<li><a href="https://www.keepitsimplebitcoin.com/setup-bitcoin-core/" target="_blank" style="color: #fff;">KIS: Setting up Bitcoin Core</a></li>
<li><a href="https://www.keepitsimplebitcoin.com/how-to-use-specter-wallet-1/" target="_blank" style="color: #fff;">KIS: Setting up Specter Desktop</a></li>
<li><a href="https://www.youtube.com/watch?v=4koKF2MDXtk&t=2s" target="_blank" style="color: #fff;">Ministry of Nodes: Setting up Bitcoin Core and Specter on Windows</a></li>
</ul>
</div>
<div id="local-node-help-popup" class="hidden">
<h1>{{ _('Connecting Specter to your local Bitcoin Core node') }}</h1>
<p>{{ _("If you're running a local Bitcoin Core node, Specter should be able to auto-detect it if its data folder is located at the default location.") }}<br>
{{ _("In case it isn't, you might need to specify the Bitcoin Core data folder path in Specter's settings page.") }}<br>
{{ _("Alternatively, you can switch off auto-detect from the settings and provide the Bitcoin Core RPC credentials and host URL manually.") }}<br><br>
<b>{{ _("Please note:")}}</b><br>{{ _('Your node must be configured with ')}}<code>server=1</code>{{ _(' line in the ')}}<code>bitcoin.conf</code>{{ _(' file.')}}<br>
{{ _("If you don't have that, add it and restart Bitcoin Core.") }}"
</p>
<br><br>
{{ _("In addition, you can check out these video tutorials for setting up Bitcoin Core with Specter Desktop") }}:<br><br>
<ul style="text-align: left; margin: auto; line-height: 2;">
<li><a href="https://www.keepitsimplebitcoin.com/setup-bitcoin-core/" target="_blank" style="color: #fff;">KIS: Setting up Bitcoin Core</a></li>
<li><a href="https://www.keepitsimplebitcoin.com/how-to-use-specter-wallet-1/" target="_blank" style="color: #fff;">KIS: Setting up Specter Desktop</a></li>
<li><a href="https://www.youtube.com/watch?v=4koKF2MDXtk&t=2s" target="_blank" style="color: #fff;">Ministry of Nodes: Setting up Bitcoin Core and Specter on Windows</a></li>
</ul>
</div>
<div id="remote-node-help-popup" class="hidden">
<h1>{{ _("Connecting Specter to your remote Bitcoin Core node") }}</h1>
<h3>{{ _('Connecting to a "node in a box"') }}</h3>
<p style="font-size: 1.05em;">
{{ _("Specter can be used with a remote Bitcoin Core node running on a machine like RaspiBlitz, myNode, Nodl, Umbrel, and Embassy.") }}<br>
{{ _("RaspiBlitz and myNode currently allow running Specter in the remote machine directly, which means it's possible to use it and skip setting up connection manually.") }}<br><br>
{{ _("If you use a different node or would still like to connect run Specter locally but connect remotely, you can do so by going to Settings, Bitcoin Core tab, and provide the Bitcoin Core RPC credentials and host URL manually.") }}"
</p>
<h3>{{ _("Connecting over Tor") }}</h3>
<p style="font-size: 1.05em;">{{ _("Connecting over Tor requires that you first configure Tor on your local machine.") }}<br>
You can checkout <a href="https://www.keepitsimplebitcoin.com/how-to-install-tor/" style="color: #fff;" target="_blank">this video tutorial</a>, or read the documentation for <a href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/tor.md" target="_blank" style="color: #fff;">Mac and Linux</a> or <a href="https://github.com/Fonta1n3/FullyNoded/blob/master/Docs/Tor/Tor.md#windows-10" target="_blank" style="color: #fff;">Window</a><br>
</p>
</div>
{% endif %}
</td>
</tr>
<tr class="tr-hover">
<td>
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not (specter.device_manager.devices | length) else '') + 'tick.svg') }}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('devices_endpoint.new_device_type') }}">
<h2 style="position: relative; z-index: 1;">{{ _("Add signing devices you wish to use.") }}</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
<tr class="tr-hover">
<td>
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not (specter.wallet_manager.wallets | length) else '') + 'tick.svg') }}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('wallets_endpoint.new_wallet_type') }}">
<h2 style="position: relative; z-index: 1;">{{ _("Create your first wallet!") }}</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
{% if specter.chain == 'main' %}
<tr class="tr-hover">
<td>
<img src="{{ url_for('static', filename='img/ghost_3d.png')}}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('welcome_endpoint.get_whitepaper') }}">
<h2>{{ _("And when you are all set: Get the whitepaper from the timechain.") }}</h2>
</a>
</td>
<td style="text-align: left;">
</td>
</tr>
{% endif %}
</tbody>
</table>
<br>

View file

@ -0,0 +1,28 @@
{% extends "base.jinja" %}
{% block main %}
<br>
<div class="main-page-container">
<div style="max-width: 15em; text-align: center; margin-bottom: 2em; margin-top: .7em; margin-left: auto; margin-right: auto;">
{% include "includes/language/language_select.jinja" %}
</div>
<h1 style="font-size: 1.8em; line-height: 1em;">
{{ _('Welcome to Specter Desktop') }}
</h1>
{% if ('://localhost:' not in url_for('index', _external=True) and '://127.0.0.1:' not in url_for('index', _external=True)) and specter.hwi_bridge_url != "http://127.0.0.1:25441/hwi/api/" %}
{% include welcome_vm.specter_remote_include %}
{% endif %}
<div style="display: flex; justify-content: center; align-items: center;">
<div class="main-description-box">
<p style="font-size: 1.1em; margin: 1em auto 1em auto; text-align: left;">{{ _("Specter Desktop is a convenient wallet interface to better use your Bitcoin Core node. Its good for singlesig wallets, but comes with a special focus on multisignature setups for different hardware wallets and air-gapped signing devices.") }} <br><br>
&#128217; Learn <a style="color: #fff" href="https://specter-desktop-docs.netlify.app/multisig-guide/" target="_blank">{{ _("here") }}</a> more about multisig.
</p>
</div>
</div>
{% if not specter.chain %}
{% include welcome_vm.get_started_include %}
{% else %}
{% include welcome_vm.tick_checkboxes_include %}
{% endif %}
{% include welcome_vm.remaining_remarks_include %}
</div>
{% endblock %}

View file

@ -267,12 +267,14 @@ class User(UserMixin):
def wallet_manager(self):
if self._wallet_manager is None:
self.check_wallet_manager()
assert self._wallet_manager is not None
return self._wallet_manager
@property
def device_manager(self):
if self._device_manager is None:
self.check_device_manager()
assert self._device_manager is not None
return self._device_manager
def check_wallet_manager(self):

View file

@ -0,0 +1,21 @@
import logging
from flask import Flask
from flask import current_app as app
from threading import Thread
logger = logging.getLogger(__name__)
class FlaskThread(Thread):
"""A FlaskThread passes the applicationcontext to the new thread in order to make stuff working seamlessly in new threadsS
copied from https://stackoverflow.com/questions/39476889/use-flask-current-app-logger-inside-threading"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.app = app._get_current_object()
self.daemon = True
def run(self):
logger.debug(f"New thread started {self._target.__name__}")
with self.app.app_context():
super().run()

View file

@ -0,0 +1,97 @@
import logging
import os
import shutil
from urllib3.exceptions import NewConnectionError
from requests.exceptions import ConnectionError
from cryptoadvance.specter.specter_error import SpecterError
from ...config import BaseConfig
from ..specter_migrator import SpecterMigration
from ...helpers import load_jsons
from ...persistence import write_json_file
from ...managers.node_manager import NodeManager
import requests
logger = logging.getLogger(__name__)
class SpecterMigration_0002(SpecterMigration):
version = "v1.12.0" # the version this migration has been rolled out
# irrelevant though because we'll execute this script in any case
# as we can't have yet a say on when specter has been started first
def should_execute(self):
# This Migration cannot rely on the default-mechanism as the migration_framework was not
# in place when the functionality has been implemented
return True
@property
def description(self) -> str:
return """Node-class migration:
We introduced the Spectrum Node on an extension. With that, we made the choice of
the Node to be instantiated much more flexible. The node.json get a member called
python_class which is the fully qualified package name of the class the NodeManager
should instantiate.
This migrates all the node.json files to the new format.
Effectively it will:
* Iterate over all nodes in ~/.specter/nodes/*.json
* load each node.json and adds the correct python_class
* stores it again
In order to reverse this migration, you do need to reverese the addition of the
python_class like this:
cd ~/.specter/nodes
for file in `ls *.json`; do jq 'del(.python_class)' $file | sponge $file ; done
cd ..
jq 'del(.migration_executions[] | select(.migration_id == 2))' migration_data.json | sponge migration_data.json
"""
def execute(self):
node_folder = os.path.join(self.data_folder, "nodes")
if not os.path.isdir(node_folder):
logger.info("No node_folder found in {self.data_folder}. Nothing to do")
return
nodes = {}
nodes_files = load_jsons(node_folder, key="name")
for node_alias in nodes_files:
fullpath = os.path.join(self.data_folder, "%s.json" % node_alias)
if nodes_files[node_alias].get("external_node"):
logger.info(f"Migrating node {node_alias} to Node class.")
nodes_files[node_alias][
"python_class"
] = "cryptoadvance.specter.node.Node"
else:
logger.info(f"Migrating node {node_alias} to InternalNode class.")
nodes_files[node_alias][
"python_class"
] = "cryptoadvance.specter.internal_node.InternalNode"
if nodes_files[node_alias].get("external_node"):
logger.info(f"Deleting external_node key of {node_alias} in node.json")
del nodes_files[node_alias]["external_node"]
write_json_file(
nodes_files[node_alias], nodes_files[node_alias]["fullpath"]
)
# And you get something like this:
# {
# "name": "Specter Bitcoin",
# "python_class": "cryptoadvance.specter.node.Node",
# "alias": "specter_bitcoin",
# "autodetect": false,
# "datadir": "/home/someuser/.specter/nodes/specter_bitcoin/.bitcoin-main",
# "user": "bitcoin",
# "password": "3ah0yc-2dDEwUSqHuuZi-w",
# "port": 8332,
# "host": "localhost",
# "protocol": "http",
# "fullpath": "/home/someuser/.specter/nodes/specter_bitcoin.json",
# "bitcoind_path": "/home/someuser/.specter/bitcoin-binaries/bin/bitcoind",
# "bitcoind_network": "main",
# "version": "0.21.1"
# }

View file

@ -8,7 +8,7 @@ from pkgutil import iter_modules
import sys
from typing import List
from .common import camelcase2snake_case
from ..specter_error import SpecterError
from ..specter_error import SpecterError, SpecterInternalException
from .shell import grep
from .reflection_fs import detect_extension_style_in_cwd, search_dirs_in_path
@ -20,6 +20,19 @@ def _get_module_from_class(clazz):
return import_module(clazz.__module__)
def get_class(fqcn: str):
"""Returns a class by a fully qualified class name like e.g. cryptoadvance.specter.node.Node"""
module_name = ".".join(fqcn.split(".")[:-1])
class_name = fqcn.split(".")[-1]
try:
module = import_module(module_name)
my_class = getattr(module, class_name)
except (AttributeError, ModuleNotFoundError) as e:
raise SpecterInternalException(f"Could not find {fqcn}: {e}")
return my_class
def get_template_static_folder(foldername):
"""convenience-method to return static/template methods taking pyinstaller into account
foldername can be anything but probably most reasonable template or static
@ -98,9 +111,6 @@ def get_classlist_of_type_clazz_from_modulelist(clazz, modulelist):
):
# Unfortunately the superclass gets imported if you inherit from it and counts as an attribute as well
if str(attribute.__module__).startswith(fq_module_name):
logger.debug(
f" {attribute.__module__} <<<<-------------------------------------"
)
logger.debug(f"Adding {attribute} to {class_list}")
class_list.append(attribute)
logger.info(f" Found class {attribute.__name__}")

View file

@ -248,9 +248,13 @@ class MigDataManager(GenericDataManager):
self._save()
def _find_exec_log(self, id):
"""returns the latest migration_execution with that id"""
latest_migration = None
for migration in self.migration_executions:
if migration.get("migration_id") == id:
return migration
latest_migration = migration
if latest_migration != None:
return latest_migration
raise SpecterError(f"Can't find migration_execution with id {id}")
@property
@ -261,6 +265,7 @@ class MigDataManager(GenericDataManager):
executed_list = [
migration_execution.get("migration_id")
for migration_execution in self.migration_executions
if migration_execution.get("status") == "completed"
]
logger.debug(f"Executed migration_classes ids: {executed_list}")
return migration_id in executed_list

View file

@ -198,6 +198,7 @@ class WalletImporter:
**kwargs,
)
except Exception as e:
logger.exception(e)
raise SpecterError(f"Failed to create wallet: {e}")
logger.info(f"Created Wallet {self.wallet}")
self.wallet.keypoolrefill(0, self.wallet.IMPORT_KEYPOOL, change=False)

View file

@ -218,7 +218,9 @@ def elements_elreg(request):
@pytest.fixture
def empty_data_folder():
# Make sure that this folder never ever gets a reasonable non-testing use-case
with tempfile.TemporaryDirectory(prefix="specter_home_tmp_") as data_folder:
with tempfile.TemporaryDirectory(
prefix="specter_home_tmp_", ignore_cleanup_errors=True
) as data_folder:
yield data_folder

View file

@ -23,8 +23,8 @@ def test_fees(caplog, client):
logout(client)
@patch("cryptoadvance.specter.server_endpoints.wallets_api.get_price_at")
@patch("cryptoadvance.specter.server_endpoints.wallets_api._")
@patch("cryptoadvance.specter.server_endpoints.wallets.wallets_api.get_price_at")
@patch("cryptoadvance.specter.server_endpoints.wallets.wallets_api._")
def test_txlist_to_csv(
mock_babel: MagicMock,
mock_get_price_at: MagicMock,
@ -43,7 +43,9 @@ def test_txlist_to_csv(
assert mock_get_price_at() == (1000000, "$")
with app.test_request_context():
from cryptoadvance.specter.server_endpoints.wallets_api import txlist_to_csv
from cryptoadvance.specter.server_endpoints.wallets.wallets_api import (
txlist_to_csv,
)
curr_date = datetime.now()
for i, tx in enumerate(
@ -88,8 +90,8 @@ def test_txlist_to_csv(
# assert False
@patch("cryptoadvance.specter.server_endpoints.wallets_api.get_price_at")
@patch("cryptoadvance.specter.server_endpoints.wallets_api._")
@patch("cryptoadvance.specter.server_endpoints.wallets.wallets_api.get_price_at")
@patch("cryptoadvance.specter.server_endpoints.wallets.wallets_api._")
def test_addresses_list_to_csv(
mock_babel: MagicMock,
mock_get_price_at: MagicMock,
@ -109,7 +111,7 @@ def test_addresses_list_to_csv(
assert mock_get_price_at() == (1000000, "$")
with app.test_request_context():
from cryptoadvance.specter.server_endpoints.wallets_api import (
from cryptoadvance.specter.server_endpoints.wallets.wallets_api import (
addresses_list_to_csv,
)

View file

@ -3,6 +3,7 @@ import tempfile
import time
import tarfile
import os
from unittest.mock import MagicMock
import pytest
from cryptoadvance.specter.managers.node_manager import NodeManager
@ -18,10 +19,12 @@ from cryptoadvance.specter.process_controller.elementsd_controller import (
def test_NodeManager(
bitcoin_regtest: BitcoindPlainController, elements_elreg: ElementsPlainController
):
with tempfile.TemporaryDirectory("_some_datafolder_tmp") as data_folder:
with tempfile.TemporaryDirectory(
prefix="pytest_NodeManager_datafolder"
) as data_folder:
print(f"data_folder={data_folder}")
nm = NodeManager(data_folder=data_folder)
nm.add_node(
nm.add_external_node(
"BTC",
"bitcoin_regtest",
False,
@ -31,12 +34,11 @@ def test_NodeManager(
bitcoin_regtest.rpcconn.rpcport,
bitcoin_regtest.rpcconn._ipaddress,
"http",
external_node=True,
)
assert nm.nodes_names == ["Bitcoin Core", "bitcoin_regtest"]
nm.switch_node("bitcoin_regtest")
assert nm.active_node.rpc.getblockchaininfo()["chain"] == "regtest"
nm.add_node(
nm.add_external_node(
"ELM",
"elements_elreg",
False,
@ -46,12 +48,11 @@ def test_NodeManager(
elements_elreg.rpcconn.rpcport,
elements_elreg.rpcconn._ipaddress,
"http",
external_node=True,
)
assert nm.nodes_names == ["Bitcoin Core", "bitcoin_regtest", "elements_elreg"]
nm.switch_node("elements_elreg")
assert nm.active_node.rpc.getblockchaininfo()["chain"] == "elreg"
time.sleep(20)
nm.delete_node(nm.nodes["Bitcoin Core"], MagicMock())
""" For some reason this breaks other tests"""

View file

@ -139,11 +139,6 @@ def test_WalletManager_2_nodes(
wm.update(chain="regtest2", rpc=bitcoin_regtest2.get_rpc(), use_threading=True)
assert wm.wallets_names == ["a_test_wallet"]
with pytest.raises(Exception, match="can only be changed with one another") as e:
wm.update(chain="regtest3")
with pytest.raises(Exception, match="can only be changed with one another") as e:
wm.update(rpc=bitcoin_regtest2.get_rpc())
def test_WalletManager_check_duplicate_keys(empty_data_folder):
wm = WalletManager(

View file

@ -3,6 +3,7 @@ import pytest
import tempfile
from cryptoadvance.specter.node import Node
from cryptoadvance.specter.managers.node_manager import NodeManager
from cryptoadvance.specter.helpers import is_liquid
from mock import MagicMock, call, patch
@ -19,7 +20,7 @@ def test_Node_btc(bitcoin_regtest):
"host": bitcoin_regtest.rpcconn.ipaddress,
"protocol": "http",
},
manager=MagicMock(),
manager=NodeManager(data_folder=data_folder),
default_fullpath=os.path.join(data_folder, "a_testfile.json"),
)
result = node.test_rpc()
@ -33,6 +34,7 @@ def test_Node_btc(bitcoin_regtest):
del node_json["fullpath"] # This is very different because of the tempfile
assert node_json == {
"name": "",
"python_class": "cryptoadvance.specter.node.Node",
"alias": "",
"autodetect": False,
"datadir": "",
@ -42,7 +44,6 @@ def test_Node_btc(bitcoin_regtest):
"host": "localhost",
"protocol": "http",
"node_type": "BTC",
"external_node": True, # 'fullpath': ''
}
rpc = node._get_rpc()
@ -95,6 +96,7 @@ def test_Node_elm(elements_elreg):
print(f"node.json = {node.json}")
assert node_json == {
"name": "",
"python_class": "cryptoadvance.specter.node.Node",
"alias": "",
"autodetect": False,
"datadir": "",
@ -104,7 +106,6 @@ def test_Node_elm(elements_elreg):
"host": "localhost",
"protocol": "http",
"node_type": "BTC",
"external_node": True, # 'fullpath': ''
}
rpc = node._get_rpc()

View file

@ -1,5 +1,11 @@
import os
from cryptoadvance.specter.persistence import write_devices, write_device, write_wallet
from unittest.mock import MagicMock, Mock
from cryptoadvance.specter.persistence import (
write_devices,
write_device,
write_wallet,
PersistentObject,
)
from cryptoadvance.specter.key import Key
import json
@ -66,3 +72,10 @@ def test_write_device(app, a_key, a_tpub_only_key):
"/tmp/delete_me_test_file.json",
)
os.remove("/tmp/delete_me_test_file.json")
def test_PersistentObject():
some_node = PersistentObject.from_json(
{"python_class": "cryptoadvance.specter.node.Node"}, MagicMock()
)
assert some_node.__class__.__name__ == "Node"

View file

@ -123,13 +123,15 @@ def test_SpecterMigrator(empty_data_folder, caplog):
mylist = mm.plan_migration()
# This assertion will break every time you create a new migration-script
assert len(mylist) == 1
assert len(mylist) == 2
mm.execute_migrations(mylist)
assert len(mm.mig.migration_executions) == 1
assert len(mm.mig.migration_executions) == 2
assert "Setting execution log status of 1 to completed" in caplog.text
assert "Setting execution log status of 2 to completed" in caplog.text
assert mm.mig.migration_executions[0]["migration_id"] == 1
print("Content of nodes/specter_bitcoin/.bitcoin-main")
print(
os.listdir(
os.path.join(
@ -149,6 +151,8 @@ def test_SpecterMigrator(empty_data_folder, caplog):
"chainstate",
)
)
print("Content of nodes")
print(os.listdir(os.path.join(empty_data_folder, "nodes")))
specter_bitcoin_json = os.path.join(
empty_data_folder, "nodes", "specter_bitcoin.json"
)
@ -163,5 +167,4 @@ def test_SpecterMigrator(empty_data_folder, caplog):
assert config["password"]
assert config["port"] == 8332
assert config["host"] == "localhost"
assert config["external_node"] == False
# yeah, some more but should be ok

View file

@ -1,9 +1,11 @@
import logging
import pytest
from pathlib import Path
from typing import List
from cryptoadvance.specter.device import Device
from cryptoadvance.specter.devices.bitbox02 import BitBox02
from cryptoadvance.specter.util.reflection import (
get_class,
get_subclasses_for_clazz,
get_subclasses_for_clazz_in_cwd,
get_classlist_of_type_clazz_from_modulelist,
@ -13,9 +15,11 @@ from cryptoadvance.specter.util.reflection import (
)
from cryptoadvance.specter.util.specter_migrator import SpecterMigration
from cryptoadvance.specter.util.migrations.migration_0000 import SpecterMigration_0000
from cryptoadvance.specter.specter_error import SpecterInternalException
from cryptoadvance.specter.managers.service_manager import Service
from cryptoadvance.specter.services.swan.service import SwanService
from cryptoadvance.specter.services.bitcoinreserve.service import BitcoinReserveService
from cryptoadvance.specter.specter_error import SpecterInternalException
def test_get_module_from_class():
@ -29,6 +33,23 @@ def test_get_module_from_class():
)
def test_get_class():
assert type(get_class("cryptoadvance.specter.device.Device")) == type(Device)
assert get_class("cryptoadvance.specter.node.Node").__name__ == "Node"
# It doesn't make sense to raise SpecterErrors as the error messages aren't meaningful to the user
with pytest.raises(
SpecterInternalException,
match="Could not find cryptoadvance.specter.node.notExisting",
):
get_class("cryptoadvance.specter.node.notExisting")
with pytest.raises(
SpecterInternalException,
match="Could not find cryptoadvance.notExisting.notExisting",
):
get_class("cryptoadvance.notExisting.notExisting")
def test_get_package_dir_for_subclasses_of():
assert get_package_dir_for_subclasses_of(SpecterMigration).endswith(
"cryptoadvance/specter/util/migrations"