mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
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:
parent
9a3f92d618
commit
de8f5af619
56 changed files with 1101 additions and 474 deletions
|
|
@ -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-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-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).
|
||||
|
|
@ -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:
|
|||
|
||||

|
||||
|
||||
## 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
30
docs/extensions/nodes.md
Normal 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(...
|
||||
```
|
||||
|
|
@ -44,6 +44,7 @@ nav:
|
|||
- extensions/data-storage.md
|
||||
- extensions/callbacks.md
|
||||
- extensions/devices.md
|
||||
- extensions/nodes.md
|
||||
|
||||
theme:
|
||||
name: readthedocs
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>"
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from .wallets import *
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -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__)
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1 @@
|
|||
from .welcome import *
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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-----------------------------------------------------------------"
|
||||
|
|
|
|||
|
|
@ -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. It’s good for singlesig wallets, but comes with a special focus on multisignature setups for different hardware wallets and air-gapped signing devices.") }} <br><br>
|
||||
📙 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">👀{{ _(' 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;">🙇{{ _(" 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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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") }}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<p style="position: relative; z-index: 1; line-height: 24px; font-size: 1.1em">👀{{ _(' 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;">🙇{{ _(" 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
28
src/cryptoadvance/specter/templates/welcome/welcome.jinja
Normal file
28
src/cryptoadvance/specter/templates/welcome/welcome.jinja
Normal 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. It’s good for singlesig wallets, but comes with a special focus on multisignature setups for different hardware wallets and air-gapped signing devices.") }} <br><br>
|
||||
📙 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 %}
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
21
src/cryptoadvance/specter/util/flask.py
Normal file
21
src/cryptoadvance/specter/util/flask.py
Normal 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()
|
||||
97
src/cryptoadvance/specter/util/migrations/migration_0002.py
Normal file
97
src/cryptoadvance/specter/util/migrations/migration_0002.py
Normal 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"
|
||||
# }
|
||||
|
|
@ -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__}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue