diff --git a/cypress.json b/cypress.json index 3d468c56e..a00047a19 100644 --- a/cypress.json +++ b/cypress.json @@ -7,6 +7,7 @@ "spec_node_configured.js", "spec_wallet_send.js", "spec_wallet_utxo.js", + "spec_plugins.js", "spec_elm_single_segwit_wallet.js", "spec_elm_multi_segwit_wallet.js" ], diff --git a/cypress/integration/spec_plugins.js b/cypress/integration/spec_plugins.js new file mode 100644 index 000000000..e9b457028 --- /dev/null +++ b/cypress/integration/spec_plugins.js @@ -0,0 +1,41 @@ +describe('plugins are working', () => { + + it('can create the associate button', () => { + cy.viewport(1200,660) + cy.visit('/') + + // choose address + cy.get('#test_hot_wallet_1-sidebar-list-item').click() + cy.get('[href="/wallets/wallet/test_hot_wallet_1/addresses/"]').click() + cy.get('addresses-table').shadow().find('address-row').eq(0).shadow().find('.explorer-link').click() + cy.get('address-data').shadow().find('#associate-btn').click() + + cy.contains("Service integration requires an authentication method that includes a password") + // ok, let's set a password + cy.get('select').select("passwordonly") + cy.get('#specter_password_only').type("mySecretPassword") + cy.get('#submit-btn').click() + cy.contains("Admin password successfully updated") + + // choose address again + cy.get('#test_hot_wallet_1-sidebar-list-item').click() + cy.get('[href="/wallets/wallet/test_hot_wallet_1/addresses/"]').click() + cy.get('addresses-table').shadow().find('address-row').eq(0).shadow().find('.explorer-link').click() + cy.get('address-data').shadow().find('#associate-btn').click() + cy.contains("Associating an address with a service will") + + + }) + + it('deactivates the password protection again', () => { + cy.viewport(1200,660) + cy.visit('/') + cy.get('#password').type("mySecretPassword") + cy.get('#login-btn').click() + cy.get('[href="/settings/"] > .svg-white').click() + cy.get('[href="/settings/auth"]').click() + cy.get('select').select("none") + cy.get('#submit-btn').click() + }) + +}) \ No newline at end of file diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index eb66104ed..67ad78e62 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -38,7 +38,7 @@ We're no longer using travis-ci due to the abuse-detection-system going wild on # Cirrus-CI -[Cirrus-CI](https://cirrus-ci.org) is used by Bitcoin-Core and HWI and is a quite good replacement for travis. We're using it only for PRs so far. The [../.cirrus.yml] file defines the build. We have two task, one for pytest and one for the cypress-tests. +[Cirrus-CI](https://cirrus-ci.org) is used by Bitcoin-Core and HWI and is a quite good replacement for travis. We're using it only for PRs so far. The [../.cirrus.yml] file defines the build. We have two task, one for pytest and one for the [cypress-tests](./cypress-testing.md). # Releasing diff --git a/docs/services/services.md b/docs/services/services.md index cd1af1de9..b06ff0a1f 100644 --- a/docs/services/services.md +++ b/docs/services/services.md @@ -1,11 +1,47 @@ # Third-Party Service Integrations -A developer's guide for Specter Desktop `Service` integrations. +A developer's guide for the Specter Desktop `Extension` framework. + +We currently rework the naming of extensions/plugins/services. If not otherwise stated, you can see those three terms as the same, for now. -## Basic Code Philosophy -As much as possible, each `Service` implementation should be entirely self-contained with little or no custom code altering existing/core Specter functionality. +## Concept +As much as possible, each `Service` implementation should be entirely self-contained with little or no custom code altering existing/core Specter functionality. There is a name for that: Extension-/Pluginframework. +The term `extension` will be used for all sorts extensions whereas `plugin` will be used as a component which can be de-/activated by a user. +All extensions are completely sperated in a specific folder-structure. There are internal extensions which `SHOULD` be located in `cryptoadvance.specterext.id_of_extension` but at least 2 extensions are still at the deprecated location of `cryptoadvance.specter.services`. However that does not mean that an extension needs to be located in the same repository than specter itself. There can and will be extensions which are located in their own repositories. + +Independent whether an extension is shipped with the official specter-release-binaries and whether it's an internal (which is shipped) or external extension (which might be shipped), the creation of extensions is already heavily supported and encouraged. +Whether an extension is shipped with the official binary is entirely the choice of the Specter Team. However, you can simply develop extensions and use them on production (only for technical personel) as described in `specterext-dummy` (see below). + +A description of how to create your own extension can be found at the [dummy-extension](https://github.com/cryptoadvance/specterext-dummy/). You will need to choose an organisation or username if you create one. This is used for package-structure. + +All the attributes of an extension are currently (json-support is planned sooner or later) defined as attributes of a class which is derived from the class `Service` (should be renamed). That class has attributes which are essential. So let's discuss them briefly. + +## Extension Attributes +Here is an Example. This class definition MUST be stored in a file called "service.py" within a package with the name `org-id.specterext.extions-id`. +``` +class DiceService(Service): + id = "dice" + name = "Specter Dice" + icon = "dice/dice_logo.png" + logo = "dice/dice_logo.png" + desc = "Send your bet!" + has_blueprint = True + blueprint_module = "k9ert.specterext.dice.controller" + isolated_client = False + devstatus = devstatus_alpha +``` +So this defines the base `Service` class (to be renamed to "Extension") that all extensions must inherit from. This is wired to enable `Extension` auto-discovery. Any feature that is common to most or all `Service` integrations should be implemented here. +With inheriting from `Service` you get some usefull methods explained later. + + +The `id` needs to be unique within a specific specter-instance where this extension is part of. The `name` is the displayname as shown to the user in the plugin-area (currently there is not yet a technical difference between extensions and plugins). The `icon` will be used where labels are used to be diplayed if this extension is reserving addresses. The `logo` and the `desc` (ription) is also used in the plugin-area ("choose plugins"). +If the extension has a UI (currently all of them have one), `has_blueprint` is True. `The blueprint_module` is referencing the controller-module where endpoints are defined. It's recommended to follow the format `org.specterext.extions-id.controller`. +`isolated_client` Should not be used yet. It is determining where in the url-path-tree the blueprint will be mounted. This might have an impact on whether the extension's frontend-client has access to the cookie used in specter. Check `config.py` for details. +`devstatus` is one of `devstatus_alpha`, `devstatus_beta` or `devstatus_prod` defined in `cryptoadvance.specter.services.service`. Each specter-instance will have a config-variable called `SERVICES_DEVSTATUS_THRESHOLD` (prod in Production and alpha in Development) and depending on that, the plugin will be available to the user. + +## Data-Storage Effort 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, `Address.service_id` field). @@ -19,18 +55,6 @@ Users can also manually associate an existing `Address` with a `Service` (this i _Note: TODO: manually un-reserve an `Address` from a `Service`._ -## Basic Code Structure -All `Service`-related code should be contained within `cryptoadvance.specter.services`. The base components are: - - -### `Service` Base Class -Defines the base `Service` class that all service integrations must inherit from. This is wired to enable `Service` auto-discovery. Any feature that is common to most or all `Service` integrations should be implemented here. - -Each `Service` must specify a unique `Service.id` that is just a short string (e.g. "swan"). This is the main identifier throughout the code. - -Includes methods to "reserve" addresses for the `Service` to basically make those not-yet-used addresses somewhat off-limits to the rest of the UI (can still be manually overridden though). - - ### `Service` Configuration In order to separate the service-configuration from the main-configuration, you can specify your config in a file called `config.py`. It's structure is similiar to the specter-wide `config.py`, e.g.: ``` @@ -42,11 +66,6 @@ class ProductionConfig(BaseConfig): ``` In your code, you can access the correct value as in any other flask-code, like `api_url = app.config.get("SWAN_API_URL")`. If the instance is running a config (e.g. `DevelopmentConfig`) which is not available in your service-specific config (as above), the inheritance-hirarchy from the mainconfig will get traversed and the first hit will get get configured. In this example, it would be `BaseConfig`. - -### `ServiceManager` -Simple manager that contains all `Service`s. Performs the `Service` auto-discovery at startup and filters availability by each `Service`'s release level (i.e. alpha, beta, etc). - - ### `ServiceEncryptedStorage` Most `Service`s will require user secrets (e.g. API key and secret). Each Specter `User` will have their own on-disk encrypted `ServiceEncryptedStorage` with filename `_services.json`. Note that the user's secrets for all `Service`s will be stored in this one file. @@ -82,7 +101,7 @@ Whenever possible, external code should not directly access these `Service`-rela ### `ServiceAnnotationsStorage` -Annotations are any address-specific or transaction-specific data from a `Service` that we might want to present to the user. Example: a `Service` that integrates with a onchain storefront would have product/order data associated with a utxo. That additional data could be imported by the `Service` and stored as an annotation. This annotation data could then be displayed to the user when viewing the details for that particular address or tx. +Annotations are any address-specific or transaction-specific data from a `Service` that we might want to present to the user (not yet implemented). Example: a `Service` that integrates with a onchain storefront would have product/order data associated with a utxo. That additional data could be imported by the `Service` and stored as an annotation. This annotation data could then be displayed to the user when viewing the details for that particular address or tx. Annotations are stored on a per-wallet and per-`Service` basis as _unencrypted_ on-disk data (filename: `_.json`). @@ -101,7 +120,7 @@ cryptoadvance.specter.services.swan Each implementation must have the following required components: ``` -/static +/static/ /templates/ controller.py service.py @@ -110,7 +129,7 @@ service.py This makes each implementation its own Flask `Blueprint`. ### `/static` -Because of Flask `Blueprint` imports, you can just add static files here and reference them (e.g. "static/img/blah.png") as if they were in the main `/static` files root dir. +Because of Flask `Blueprint` imports, you can just add static files here and reference them (e.g. "static///img/blah.png") as if they were in the main `/static` files root dir. ### `/templates/` Again, Flask `Blueprint`s import the `/templates` directory as-is, but to avoid namespace collisions on the template files (e.g. `/templates/index.html`) they should be contained within a subdirectory named with the `Service.id` (e.g. `/templates/swan/index.html`) diff --git a/pytest.ini b/pytest.ini index 20b61fd98..b98916213 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] -norecursedirs = tests/bitcoin* tests/elements +norecursedirs = tests/bitcoin* tests/elements tests/xtestdata_testextensions log_format = [%(levelname)8s] %(message)s %(name)s (%(filename)s:%(lineno)s) addopts = --bitcoind-version v22.0.0 --elementsd-version v0.21.0.1 markers = diff --git a/src/cryptoadvance/specter/cli/__init__.py b/src/cryptoadvance/specter/cli/__init__.py index c1e87ffa1..144a6dc8e 100644 --- a/src/cryptoadvance/specter/cli/__init__.py +++ b/src/cryptoadvance/specter/cli/__init__.py @@ -5,6 +5,7 @@ from http.client import HTTPConnection import click from .cli_noded import bitcoind, elementsd +from .cli_ext import ext from .cli_server import server logger = logging.getLogger(__name__) @@ -58,5 +59,6 @@ def entry_point(config_home, debug=False, tracerpc=False, tracerequests=False): entry_point.add_command(server) +entry_point.add_command(ext) entry_point.add_command(bitcoind) entry_point.add_command(elementsd) diff --git a/src/cryptoadvance/specter/cli/cli_ext.py b/src/cryptoadvance/specter/cli/cli_ext.py new file mode 100644 index 000000000..512d1c6a8 --- /dev/null +++ b/src/cryptoadvance/specter/cli/cli_ext.py @@ -0,0 +1,173 @@ +import logging +import os +import shutil +import signal +import sys +import time +from os import path +from pathlib import Path +from socket import gethostname +from urllib.parse import urlparse + +import click +import requests + +from ..server import create_app, init_app +from ..services.extension_gen import ExtGen +from ..specter_error import SpecterError +from ..util.common import snake_case2camelcase +from ..util.reflection_fs import search_dirs_in_path +from ..util.shell import run_shell +from ..util.tor import start_hidden_service, stop_hidden_services + +logger = logging.getLogger(__name__) + +ext_mark = "specterext" +dummy_ext_url = ( + "https://raw.githubusercontent.com/cryptoadvance/specterext-dummy/master" +) + + +@click.group() +def ext(): + pass + + +@ext.command() +@click.option("--org", "org", default=None, help="Use a specific organsiation") +@click.option("--ext-id", "ext_id", default=None, help="Use a specific Extension ID") +@click.option( + "--isolated-client/--no-isolated-client", + default=None, + help="Whether the extension should be isolated on the client", +) +@click.option( + "--tmpl-fs-source", + "tmpl_fs_source", + help="Use a Filesystem source for the templates e.g. ~/src/specterext-dummy", +) +@click.option( + "--dryrun/--no-dryrun", + default=False, + help="Output content on stdout instead of creating files", +) +def gen(org, ext_id, isolated_client, tmpl_fs_source, dryrun): + # fmt: off + """Will generate a new extension in a more or less empty directory. + \b + It'll ask you for the missing information if you don't pass the + necessary details (see below). + + After creation, you can get the extension to run like this in your Development Environment: + + \b + pip3 install -e . + python3 -m cryptoadvance.specter server --config DevelopmentConfig --debug + # point your browser to http://localhost:25441 + # "choose Services" --> YourService + + If you want to package it, you can build it like this: + + \b + python3 -m pip install --upgrade build + python3 -m build + # install it like this: + pip3 install dist/{org}_{ext_id}-0.0.1-py3-none-any.whl + + In order to use your extension in production, please refer to the Readme.md in the + https://github.com/cryptoadvance/{ext_mark}-dummy#how-to-get-this-to-production + + To publish your package: + + \b + python3 -m pip install --upgrade twine + python3 -m twine upload --repository testpypi dist/* + + """ + # fmt: on + if ext_id == None: + print( + """ + We need an ID and a prefix for your extension. It'll reflect in the package-layout. + The id should be a short string. + The prefix is usually something like your github-username or github organisation-name. + Both will be used to to create the directory structure ( ./src/mycorpname/specterext/myextension ) + and it will be used to prepare the files in order to publish this extension to pypi. + + """ + ) + ext_id = click.prompt( + "What should be the ID of your extension (lowercase only)", type=str + ) + if org == None: + org = click.prompt( + "what should be the prefix?", + type=str, + ) + if isolated_client == None: + print( + """ + Should the extension be working in isolated_client-mode? In that case it's won't share the + session-cookie with specter and the integration can only happen on server-side? + """ + ) + isolated_client = click.prompt( + "Should the extension work in isolated client mode (y/n)?", + type=bool, + ) + result = run_shell(["git", "config", "--get", "user.name"]) + if result["code"] == 0: + author = result["out"].decode("ascii").strip() + else: + author = click.prompt("Please type in your Name: ", type=str) + result = run_shell(["git", "config", "--get", "user.email"]) + if result["code"] == 0: + email = result["out"].decode("ascii").strip() + else: + email = click.prompt("Please type in your E-Mail: ", type=str) + + extgen = ExtGen( + ".", + org, + ext_id, + isolated_client, + author, + email, + dry_run=dryrun, + tmpl_fs_source=tmpl_fs_source, + ) + extgen.generate() + # piggyback + # replace(f"./{dir}/service.py", "piggyback = False", f"piggyback = {piggyback}") + # if piggyback: + # replace(f"./{dir}/controller.py", "@login_required", "") + + print( + f""" + Congratulations, you've created a new extension + + Here is how to get it tor run on your Development Environment: + pip3 install -e . + python3 -m cryptoadvance.specter server --config DevelopmentConfig --debug + # point your browser to http://localhost:25441 + # "choose Services" --> {ext_id} + + If you want to package it, you can build it like this: + python3 -m pip install --upgrade build + python3 -m build + # install it like this: + pip3 install dist/{org}_{ext_id}-0.0.1-py3-none-any.whl + + In order to use your extension in production, please refer to the Readme.md in the + https://github.com/cryptoadvance/{ext_mark}-dummy#how-to-get-this-to-production + + To publish your package + + python3 -m pip install --upgrade twine + python3 -m twine upload --repository testpypi dist/* + + You can get all these information again via: + python3 -m cryptoadvance.specter ext gen --help + +""" + ) diff --git a/src/cryptoadvance/specter/config.py b/src/cryptoadvance/specter/config.py index bffa535e0..eff7b3d94 100644 --- a/src/cryptoadvance/specter/config.py +++ b/src/cryptoadvance/specter/config.py @@ -179,6 +179,12 @@ class BaseConfig(object): os.getenv("REQUEST_TIME_WARNING_THRESHOLD", "20") ) + # As described in https://github.com/cryptoadvance/specter-desktop/pull/1579#issuecomment-1049895972 + # This should get removed after v1.9.0 is out. + SPECTER_URL_PREFIX = "" + EXT_URL_PREFIX = "/svc" + SESSION_COOKIE_PATH = SPECTER_URL_PREFIX + class DevelopmentConfig(BaseConfig): # https://stackoverflow.com/questions/22463939/demystify-flask-app-secret-key @@ -250,13 +256,7 @@ class ProductionConfig(BaseConfig): # Repeating it here as it's SECURITY CRITICAL. Check comments in BaseConfig SERVICES_LOAD_FROM_CWD = False - SPECTER_URL_PREFIX = "/spc" - EXT_URL_PREFIX = "/spc/ext" - EXTERNAT_EXT_URL_PREFIX = "/ext" - SESSION_COOKIE_PATH = SPECTER_URL_PREFIX - - # As described in https://github.com/cryptoadvance/specter-desktop/pull/1579#issuecomment-1049895972 - # This should get removed after v1.9.0 is out. - SPECTER_URL_PREFIX = "" - EXT_URL_PREFIX = "/svc" - SESSION_COOKIE_PATH = SPECTER_URL_PREFIX + # SPECTER_URL_PREFIX = "/spc" + # EXT_URL_PREFIX = "/spc/ext" + # EXTERNAT_EXT_URL_PREFIX = "/ext" + # SESSION_COOKIE_PATH = SPECTER_URL_PREFIX diff --git a/src/cryptoadvance/specter/managers/service_manager.py b/src/cryptoadvance/specter/managers/service_manager.py index cb6d5a2e2..145575e7a 100644 --- a/src/cryptoadvance/specter/managers/service_manager.py +++ b/src/cryptoadvance/specter/managers/service_manager.py @@ -9,6 +9,7 @@ from typing import Dict, List from cryptoadvance.specter.config import ProductionConfig from cryptoadvance.specter.managers.singleton import ConfigurableSingletonException +from cryptoadvance.specter.specter_error import SpecterError from cryptoadvance.specter.user import User from cryptoadvance.specter.util.reflection import get_template_static_folder from flask import current_app as app @@ -16,6 +17,7 @@ from flask import url_for from flask.blueprints import Blueprint from ..services.service import Service +from ..services import callbacks, ExtensionException from ..services.service_encrypted_storage import ServiceEncryptedStorageManager from ..util.reflection import ( _get_module_from_class, @@ -37,7 +39,7 @@ class ServiceManager: # Each Service class is stored here, keyed on its Service.id str self._services: Dict[str, Service] = {} - logger.info("----> starting service discovery <----") + logger.info("----> starting service discovery Static") # How do we discover services? Two configs are relevant: # * SERVICES_LOAD_FROM_CWD (boolean, CWD is current working directory) # * EXTENSION_LIST (array of Fully Qualified module strings like ["cryptoadvance.specter.services.swan.service"]) @@ -47,14 +49,16 @@ class ServiceManager: class_list = get_classlist_of_type_clazz_from_modulelist( Service, app.config.get("EXTENSION_LIST", []) ) + logger.info("----> starting service discovery Dynamic") if app.config.get("SERVICES_LOAD_FROM_CWD", False): class_list.extend(get_subclasses_for_clazz_in_cwd(Service)) + logger.info("----> starting service loading") class_list = set(class_list) # remove duplicates (shouldn't happen but ...) for clazz in class_list: compare_map = {"alpha": 1, "beta": 2, "prod": 3} if compare_map[self.devstatus_threshold] <= compare_map[clazz.devstatus]: # First configure the service - self.configure_service_for_module(clazz.id) + self.configure_service_for_module(clazz) # Now activate it self._services[clazz.id] = clazz( active=clazz.id in self.specter.config.get("services", []), @@ -76,7 +80,8 @@ class ServiceManager: except ConfigurableSingletonException as e: # Test suite triggers multiple calls; ignore for now. pass - logger.info("----> finished service discovery <----") + logger.info("----> finished service processing") + self.execute_ext_callbacks("afterServiceManagerInit") @classmethod def register_blueprint_for_ext(cls, clazz, ext): @@ -112,37 +117,53 @@ class ServiceManager: ext_prefix = app.config["ISOLATED_CLIENT_EXT_URL_PREFIX"] else: ext_prefix = app.config["EXT_URL_PREFIX"] - app.register_blueprint(clazz.blueprint, url_prefix=f"{ext_prefix}/{clazz.id}") - if ( - app.testing - and len([vf for vf in app.view_functions if vf.startswith(clazz.id)]) <= 1 - ): # the swan-static one - # Yet again that nasty workaround which has been described in the archblog. - # The easy variant can be found in server.py - # The good news is, that we'll only do that for testing - import importlib - - logger.info("Reloading Extension controller") - importlib.reload(controller_module) + try: app.register_blueprint( clazz.blueprint, url_prefix=f"{ext_prefix}/{clazz.id}" ) + logger.info(f" Mounted {clazz.id} to {ext_prefix}/{clazz.id}") + if ( + app.testing + and len([vf for vf in app.view_functions if vf.startswith(clazz.id)]) + <= 1 + ): # the swan-static one + # Yet again that nasty workaround which has been described in the archblog. + # The easy variant can be found in server.py + # The good news is, that we'll only do that for testing + import importlib - logger.info(f" Mounting {clazz.id} to {ext_prefix}/{clazz.id}") + logger.info("Reloading Extension controller") + importlib.reload(controller_module) + app.register_blueprint( + clazz.blueprint, url_prefix=f"{ext_prefix}/{clazz.id}" + ) + except AssertionError as e: + if str(e).startswith("A name collision"): + raise SpecterError( + f""" + There is a name collision for the {clazz.blueprint.name}. \n + This is probably because you're running in DevelopementConfig and configured + the extension at the same time in the EXTENSION_LIST which currently loks like this: + {app.config['EXTENSION_LIST']}) + """ + ) @classmethod - def configure_service_for_module(cls, service_id): + def configure_service_for_module(cls, clazz): """searches for ConfigClasses in the module-Directory and merges its config in the global config""" try: - module = import_module( - f"cryptoadvance.specter.services.{service_id}.config" - ) + module = import_module(f"cryptoadvance.specter.services.{clazz.id}.config") except ModuleNotFoundError: - logger.warning( - f"Service {service_id} does not have a service Configuration! Skipping!" - ) - return + # maybe the other style: + org = clazz.__module__.split(".")[0] + try: + module = import_module(f"{org}.specterext.{clazz.id}.config") + except ModuleNotFoundError: + logger.warning( + f"Service {clazz.id} does not have a service Configuration! Skipping!" + ) + return main_config_clazz_name = app.config.get("SPECTER_CONFIGURATION_CLASS_FULLNAME") main_config_clazz_slug = main_config_clazz_name.split(".")[-1] potential_config_classes = [] @@ -183,6 +204,18 @@ class ServiceManager: app.config[key] = getattr(clazz, key) logger.debug(f"setting {key} = {app.config[key]}") + def execute_ext_callbacks(self, callback_id, *args, **kwargs): + """will execute the callback function for each extension which has defined that method + the callback_id needs to be passed and specify why the callback has been called. + It needs to be one of the constants defined in cryptoadvance.specter.services.callbacks + """ + if callback_id not in dir(callbacks): + raise Exception(f"Non existing callback_id: {callback_id}") + logger.debug(f"Executing callback {callback_id}") + for ext in self.services.values(): + if hasattr(ext, "callback"): + ext.callback(callback_id, *args, **kwargs) + @property def services(self) -> Dict[str, Service]: return self._services or {} @@ -213,11 +246,11 @@ class ServiceManager: ) ext.active = ext.id in service_names_active - def get_service(self, service_id: str) -> Service: - if service_id not in self._services: - # TODO: better error handling? - raise Exception(f"No such Service: '{service_id}'") - return self._services[service_id] + def get_service(self, plugin_id: str) -> Service: + """get an extension-instance by ID. Raises an ExtensionException if it doesn't find it.""" + if plugin_id not in self._services: + raise ExtensionException(f"No such plugin: '{plugin_id}'") + return self._services[plugin_id] def remove_all_services_from_user(self, user: User): """ diff --git a/src/cryptoadvance/specter/server.py b/src/cryptoadvance/specter/server.py index cb8f07500..a7979862f 100644 --- a/src/cryptoadvance/specter/server.py +++ b/src/cryptoadvance/specter/server.py @@ -7,6 +7,7 @@ from cryptoadvance.specter.liquid.rpc import LiquidRPC from cryptoadvance.specter.managers.service_manager import ServiceManager from cryptoadvance.specter.rpc import BitcoinRPC from cryptoadvance.specter.util.reflection import get_template_static_folder +from .services.callbacks import after_serverpy_init_app from dotenv import load_dotenv from flask import Flask, jsonify, redirect, request, session, url_for from flask_babel import Babel @@ -114,8 +115,8 @@ def init_app(app, hwibridge=False, specter=None): Response("Not Found", status=404), {app.config["APP_URL_PREFIX"]: app.wsgi_app}, ) - # First: Migrations + print(f"-----------{app.config['SPECTER_DATA_FOLDER']}") mm = SpecterMigrator(app.config["SPECTER_DATA_FOLDER"]) mm.execute_migrations() @@ -127,7 +128,9 @@ def init_app(app, hwibridge=False, specter=None): if specter is None: # the default. If not None, then it got injected for testing - app.logger.info("Initializing Specter") + app.logger.info( + f"Initializing Specter with data-folder {app.config['SPECTER_DATA_FOLDER']}" + ) specter = Specter( data_folder=app.config["SPECTER_DATA_FOLDER"], config=app.config["DEFAULT_SPECTER_CONFIG"], @@ -229,7 +232,7 @@ def init_app(app, hwibridge=False, specter=None): return jsonify(success=False) # --------------------- Babel integration --------------------- - + specter.service_manager.execute_ext_callbacks(after_serverpy_init_app) return app diff --git a/src/cryptoadvance/specter/server_endpoints/auth.py b/src/cryptoadvance/specter/server_endpoints/auth.py index 3bdf06be2..1b5c9147d 100644 --- a/src/cryptoadvance/specter/server_endpoints/auth.py +++ b/src/cryptoadvance/specter/server_endpoints/auth.py @@ -1,22 +1,15 @@ import random import time -from flask import ( - Flask, - Blueprint, - render_template, - request, - redirect, - url_for, - jsonify, - flash, -) -from flask_babel import lazy_gettext as _ -from flask_login import login_required, current_user, logout_user +from flask import Blueprint, Flask from flask import current_app as app -from ..helpers import alias -from ..user import User, hash_password, verify_password +from flask import flash, jsonify, redirect, render_template, request, url_for +from flask_babel import lazy_gettext as _ +from flask_login import current_user, login_required, logout_user +from ..helpers import alias +from ..services import ExtensionException +from ..user import User, hash_password, verify_password rand = random.randint(0, 1e32) # to force style refresh last_sensitive_request = 0 # to rate limit sensitive requests @@ -222,6 +215,9 @@ def redirect_login(request): try: service_cls = app.specter.service_manager.get_service(service_id) service_cls.on_user_login() + except ExtensionException as ee: + if not str(ee).startswith("No such plugin"): + raise ee except Exception as e: app.logger.exception(e) diff --git a/src/cryptoadvance/specter/services/__init__.py b/src/cryptoadvance/specter/services/__init__.py index e69de29bb..46b589d5e 100644 --- a/src/cryptoadvance/specter/services/__init__.py +++ b/src/cryptoadvance/specter/services/__init__.py @@ -0,0 +1,5 @@ +from ..specter_error import SpecterError + + +class ExtensionException(SpecterError): + pass diff --git a/src/cryptoadvance/specter/services/callbacks.py b/src/cryptoadvance/specter/services/callbacks.py new file mode 100644 index 000000000..1d891786d --- /dev/null +++ b/src/cryptoadvance/specter/services/callbacks.py @@ -0,0 +1,15 @@ +""" Here we have some constants getting an id for extension-points/callbacks. As camelcase + is used, we don't use CAPITAL letters to not loose the meaning of the camelcase. + + These constants are expected as parameter to the ServiceManager.callback function + and it'll throw an exception if the constant does not exist. + + There are some weak naming conventions: + 1. after/before + 2. class or file + 3. method or function + +""" + +afterServiceManagerInit = "afterServiceManagerInit" +after_serverpy_init_app = "after_serverpy_init_app" diff --git a/src/cryptoadvance/specter/services/controller.py b/src/cryptoadvance/specter/services/controller.py index 50c92c99a..569d91e3f 100644 --- a/src/cryptoadvance/specter/services/controller.py +++ b/src/cryptoadvance/specter/services/controller.py @@ -1,10 +1,13 @@ import logging +from functools import wraps -from flask import Blueprint, render_template, redirect, url_for, flash -from flask import current_app as app, request +from flask import Blueprint +from flask import current_app as app +from flask import flash, redirect, render_template, request, url_for from flask_babel import lazy_gettext as _ from flask_login import current_user, login_required -from functools import wraps + +from ..services import ExtensionException logger = logging.getLogger(__name__) @@ -73,7 +76,10 @@ def associate_addr(wallet_alias, address): # Inject the User's active Services services = [] for service_id in current_user.services: - services.append(app.specter.service_manager.get_service(service_id=service_id)) + try: + services.append(app.specter.service_manager.get_service(service_id)) + except ExtensionException: + pass return render_template( "services/associate_addr.jinja", diff --git a/src/cryptoadvance/specter/services/extension_gen.py b/src/cryptoadvance/specter/services/extension_gen.py new file mode 100644 index 000000000..621c95af6 --- /dev/null +++ b/src/cryptoadvance/specter/services/extension_gen.py @@ -0,0 +1,215 @@ +import logging +import os +import shutil +import signal +import sys +import time +from datetime import datetime +from os import path +from os.path import exists, getmtime, join +from pathlib import Path +from socket import gethostname +from sre_constants import BRANCH +from urllib.parse import urlparse + +import click +import requests +from jinja2 import BaseLoader, Environment, FileSystemLoader, TemplateNotFound + +from ..server import create_app, init_app +from ..specter_error import SpecterError +from ..util.common import camelcase2snake_case, snake_case2camelcase +from ..util.reflection_fs import search_dirs_in_path +from ..util.shell import run_shell +from ..util.tor import start_hidden_service, stop_hidden_services + +ext_mark = "specterext" +logger = logging.getLogger(__name__) + + +class ExtGen: + def __init__( + self, + base_path, + ext_org, + ext_id, + isolated_client, + author, + email, + dry_run=False, + branch="master", + tmpl_fs_source=None, + ): + self.base_path = base_path + self.org = ext_org + self.id = ext_id + self.isolated_client = isolated_client + self.author = author + self.author_email = email + self.version = "1.8.1" # relevant if tmpl-sources specify a dependency (requirements.txt) #ToDo improve + self.branch = branch + self.tmpl_fs_source = tmpl_fs_source + + self.dry_run = dry_run + self.create_envs() + + def create_envs(self): + if self.tmpl_fs_source == None: + loader = GithubUrlLoader(branch=self.branch) + else: + loader = FileSystemLoader(self.tmpl_fs_source) + self.jinja_env = Environment( + loader=loader, + trim_blocks=True, + block_start_string="<<", + block_end_string=">>", + variable_start_string="<=", + variable_end_string="=>", + comment_start_string="<#", + comment_end_string="#>", + ) + self.jinja_env.filters["camelcase"] = snake_case2camelcase + self.env = Environment(loader=loader, trim_blocks=True) + self.env.filters["camelcase"] = snake_case2camelcase + self.sd_env = Environment( + loader=GithubUrlLoader( + base_url="https://raw.githubusercontent.com/cryptoadvance/specter-desktop/", + branch=self.branch, + ) + ) + + def env_for_template(self, template): + """chooses the right env for the template""" + if Path(template).name in ["conftest.py", "ghost_machine.py"]: + return self.sd_env + if Path(template).suffix.endswith("jinja"): + return self.jinja_env + return self.env + + def generate(self): + self.generate_basics() + self.generate_preppub() + + def generate_basics(self): + self.render("requirements.txt", version=self.version) + self.render(".gitignore") + package_path = f"src/dummyorg/specterext/dummy" + self.render(f"{package_path}/service.py") + self.render(f"{package_path}/controller.py") + self.render(f"{package_path}/config.py") + self.render(f"{package_path}/__init__.py") + self.render(f"{package_path}/__main__.py") + self.render(f"{package_path}/templates/dummy/index.jinja") + if not self.isolated_client: + self.render(f"{package_path}/static/dummy/css/styles.css") + self.create_binary_file(f"{package_path}/static/dummy/img/ghost.png") + self.create_binary_file(f"{package_path}/static/dummy/img/logo.jpeg") + self.render(f"{package_path}/templates/dummy/base.jinja") + self.render(f"{package_path}/templates/dummy/transactions.jinja") + self.render(f"{package_path}/templates/dummy/settings.jinja") + self.render(f"{package_path}/templates/dummy/components/dummy_menu.jinja") + self.render(f"{package_path}/templates/dummy/components/dummy_tab.jinja") + + self.render(f"tests/conftest.py", env=self.sd_env) + self.render(f"tests/ghost_machine.py", env=self.sd_env) + # after #1591 is merged + # self.render(f"tests/devices_and_wallets.py", env=self.sd_env) + + def create_binary_file(self, sourcepath): + """textfiles can all be rendered. Binaries must be wgettet or copied""" + targetpath = Path( + sourcepath.replace("dummyorg", self.org).replace("dummy", self.id) + ) + if self.dry_run: + print(f"-------------------------------------") + print(" Creation of targetpath skipped because dryrun") + print(f"-------------------------------------") + return + if targetpath.is_file(): + return + targetpath.parents[0].mkdir(parents=True, exist_ok=True) + if self.tmpl_fs_source != None: + sourcepath = Path(self.tmpl_fs_source, sourcepath) + shutil.copy(sourcepath, targetpath) + print(f" --> Created {targetpath} (copied)") + else: + r = requests.get(self.env.loader.url_for_template(sourcepath)) + open(targetpath, "wb").write(r.content) + print(f" --> Created {targetpath} (via Github)") + + def generate_preppub(self): + self.render("pyproject.toml", version=self.version) + self.render("setup.py") + # Author and Email + if not self.author: + result = run_shell(["git", "config", "--get", "user.name"]) + if result["code"] == 0: + author = result["out"].decode("ascii").strip() + else: + author = click.prompt("Please type in your Name: ", type=str) + if not self.author_email: + result = run_shell(["git", "config", "--get", "user.email"]) + if result["code"] == 0: + email = result["out"].decode("ascii").strip() + else: + email = click.prompt("Please type in your E-Mail: ", type=str) + self.render("setup.cfg") + self.render("MANIFEST.in") + + def render(self, template, env=None, **kargv): + # The template path is the same that we want to store on disk + file_name = Path( + template.replace("dummyorg", self.org).replace("dummy", self.id) + ) + if env == None: + env = self.env_for_template(template) + template = env.get_template(template) + + if not self.dry_run: + file_name.parents[0].mkdir(parents=True, exist_ok=True) + rendered_text = template.render(ext=self, **kargv) + if self.dry_run: + print() + print( + f"File: {file_name} ({env.block_start_string} {env.block_end_string})" + ) + # print(f"Url: ") + print("-------------------------------------------------") + print(rendered_text) + print("-------------------------------------------------") + print() + else: + fq_fname = Path(self.base_path, file_name) + if not fq_fname.is_file(): + with open(fq_fname, "w") as file: + file.write(rendered_text) + print(f" --> Created {fq_fname}") + + +class GithubUrlLoader(BaseLoader): + """A Jinja2 TemplateLoader which is loading templates directly from github. + If you don't specify it, it'll be https://github.com/cryptoadvance/specterext-dummy + """ + + dummy_base_url = "https://raw.githubusercontent.com/cryptoadvance/specterext-dummy" + + def __init__(self, base_url=None, branch=None): + if base_url == None: + base_url = self.dummy_base_url + if branch == None: + branch = "master" + self.base_url = base_url + "/" + branch + + def url_for_template(self, template): + if template[0] == "/": + template = template[1:] + url = self.base_url + "/" + template + return url + + def get_source(self, environment, template): + url = self.url_for_template(template) + r = requests.get(url) + if r.status_code != 200: + raise TemplateNotFound(f"{url} results in {r.status_code}") + source = r.text + return source, url, None diff --git a/src/cryptoadvance/specter/services/service.py b/src/cryptoadvance/specter/services/service.py index d33014a13..4e3b7c9d2 100644 --- a/src/cryptoadvance/specter/services/service.py +++ b/src/cryptoadvance/specter/services/service.py @@ -34,7 +34,7 @@ class Service: logo = None desc = None # TODO: rename to "description" to be explicit has_blueprint = True # the default - # If the blueprint gets a "/svc" prefix (isolated_client = True), the login cookie won't work for all specter core functionality + # If the blueprint gets a "/ext" prefix (isolated_client = True), the login cookie won't work for all specter core functionality isolated_client = True devstatus = devstatus_alpha diff --git a/src/cryptoadvance/specter/templates/includes/address-data.html b/src/cryptoadvance/specter/templates/includes/address-data.html index 05c723947..a7888d9ea 100644 --- a/src/cryptoadvance/specter/templates/includes/address-data.html +++ b/src/cryptoadvance/specter/templates/includes/address-data.html @@ -40,15 +40,6 @@ this.note = clone.querySelector(".note"); this.info = clone.querySelector(".address-data-info"); - - - this.note.innerText = `{{ _("Loading address") }}: ${this.address} {{ _("details") }}...`; - - - // Attach the created element to the shadow dom - shadow.appendChild(clone); - } - connectedCallback() { // Read input "data-*" attributes this.isVerifyQR = this.getAttribute('data-verify-qr') == 'True'; this.isVerifyHwi = this.getAttribute('data-verify-hwi') == 'True'; @@ -60,21 +51,12 @@ this.label = this.getAttribute('data-label'); this.serviceId = this.getAttribute('data-service-id'); this.wallet = this.getAttribute('data-address-wallet'); - this.fetchAddressData(); - } - static get observedAttributes() {return ['data-address-wallet', 'data-address']; } - - attributeChangedCallback(name, oldValue, newValue) { - if (name=="data-address") { - this.address = newValue - } - if (name=="data-address-wallet") { - this.wallet = newValue - } - this.info.innerHTML = "
" this.note.innerText = `{{ _("Loading address") }}: ${this.address} {{ _("details") }}...`; this.fetchAddressData(); + + // Attach the created element to the shadow dom + shadow.appendChild(clone); } async fetchAddressData() { @@ -82,7 +64,6 @@ var formData = new FormData(); formData.append('address', this.address); formData.append('csrf_token', '{{ csrf_token() }}'); - console.log( this.address) try { const response = await fetch( url, @@ -122,7 +103,7 @@ addressInfoHTML += `${services[this.serviceId].name}`; } else { let associateServiceUrl = `{{ url_for('services_endpoint.associate_addr', wallet_alias='WALLET_ALIAS', address='ADDRESS') }}`.replace('WALLET_ALIAS', this.wallet).replace('ADDRESS', jsonResponse.address); - addressInfoHTML += `` + addressInfoHTML += `` } addressInfoHTML += ` diff --git a/src/cryptoadvance/specter/templates/login.jinja b/src/cryptoadvance/specter/templates/login.jinja index 901edec64..13e2680d3 100644 --- a/src/cryptoadvance/specter/templates/login.jinja +++ b/src/cryptoadvance/specter/templates/login.jinja @@ -16,7 +16,7 @@

- +
diff --git a/src/cryptoadvance/specter/templates/settings/auth_settings.jinja b/src/cryptoadvance/specter/templates/settings/auth_settings.jinja index 0e14421a8..0da58255e 100644 --- a/src/cryptoadvance/specter/templates/settings/auth_settings.jinja +++ b/src/cryptoadvance/specter/templates/settings/auth_settings.jinja @@ -64,7 +64,7 @@ {% endif %}
- +
{% if method == "usernamepassword" and current_user.is_admin %}
diff --git a/src/cryptoadvance/specter/templates/setup/end.jinja b/src/cryptoadvance/specter/templates/setup/end.jinja index e280b5bd1..192edb9ae 100644 --- a/src/cryptoadvance/specter/templates/setup/end.jinja +++ b/src/cryptoadvance/specter/templates/setup/end.jinja @@ -4,5 +4,5 @@

{{ _("Setup Completed Successfully!") }}

{{ _("Your new node is up and running!") }}


- {{ _("Done") }} + {{ _("Done") }} {% endblock %} diff --git a/src/cryptoadvance/specter/util/common.py b/src/cryptoadvance/specter/util/common.py index cc4f05771..65d15f45a 100644 --- a/src/cryptoadvance/specter/util/common.py +++ b/src/cryptoadvance/specter/util/common.py @@ -17,3 +17,7 @@ def camelcase2snake_case(name): pattern = re.compile(r"(? List[type]: """Returns all subclasses of class clazz located in the CWD if the cwd - is not a specter-desktop dev-env-kind-of-dir + is not a specter-desktop dev-env-kind-of-dir or contains any .py-file + So """ package_dirs = [] - if Path("./src/cryptoadvance").is_dir() or getattr(sys, "frozen", False): - # No discovery in specter-desktop-dev-env (doesn't make sense) - # or appimage-mode (technically difficult on Linux and security-risk for - # appimage-users even on --config DevelopmentConfig) + # security first! No dynamic loading in app-images + if getattr(sys, "frozen", False): return [] - else: - package_dirs.append(".") - logger.info("Running in non-specter-src-folder. Added CWD to Service-Discovery") - return get_subclasses_for_clazz(clazz, package_dirs) + + # if not testing but in a folder which looks like specter-desktop/src --> No dynamic extensions + if "PYTEST_CURRENT_TEST" not in os.environ: + if Path("./src/cryptoadvance").is_dir(): + return [] + + # Depending on the style we either add "." or "./src" to the searchpath + + extension_style = detect_extension_style_in_cwd() + if extension_style == "adhoc": + package_dirs.append(Path(".")) + elif extension_style == "publish-ready": + package_dirs.extend(search_dirs_in_path(Path("./src"))) + elif extension_style == "specter-desktop": + if "PYTEST_CURRENT_TEST" in os.environ: + # I admit, ugly hack + logger.info("We're in testing mode. Adding CWD to searchpath") + package_dirs.append(Path("./src")) + else: + raise Exception(f"This should not happen") + logger.info(f"Detected Extension-style: {extension_style}") + logger.info(f"We'll search in those package_dirs {package_dirs}") + return get_subclasses_for_clazz(clazz, package_dirs) -def get_subclasses_for_clazz(clazz, package_dirs=None): +def get_subclasses_for_clazz(clazz, package_dirs: List[str] = None): """Returns all subclasses of class clazz located in the CWD potentially add additional_packagedirs which is usefull for calculating pyinstaller hiddenimports @@ -105,11 +130,15 @@ def get_subclasses_for_clazz(clazz, package_dirs=None): if package_dirs == None: package_dirs = [get_package_dir_for_subclasses_of(clazz)] class_list = [] - logger.info(f"Collecting subclasses of {clazz.__name__} in {package_dirs}...") - for (_, module_name, _) in iter_modules( - package_dirs + logger.info( + f"Collecting subclasses of {clazz.__name__} in {' '.join([ str(dir) for dir in package_dirs]) }..." + ) + for (importer, module_name, is_pkg) in iter_modules( + [str(dir) for dir in package_dirs] ): # import the module and iterate through its attributes - logger.debug(f"Iterating on {module_name} ") + logger.debug( + f"Iterating on importer={importer} , module_name={module_name} is_pkg={is_pkg}" + ) if clazz.__name__ == "Service": try: @@ -117,20 +146,41 @@ def get_subclasses_for_clazz(clazz, package_dirs=None): f"cryptoadvance.specter.services.{module_name}.service" ) logger.debug( - f"Imported cryptoadvance.specter.services.{module_name}.service" + f" Imported cryptoadvance.specter.services.{module_name}.service" ) except ModuleNotFoundError: - logger.debug( - f"No Service Impl found in cryptoadvance.specter.services.{module_name}." - ) + # Ignore the stuff lying around in cryptoadvance/specter/services + if importer.path.endswith("cryptoadvance/specter/services"): + continue try: module = import_module(f"{module_name}.service") - logger.debug(f"Imported {module_name}.service") + logger.debug(f" Imported {module_name}.service") except ModuleNotFoundError as e: - logger.debug( - f"No Service Impl found in {module_name}.service. Skipping!" - ) - continue + try: + # Another style is orgname.specterext.extensionid, for that we have to guess the orgname: + orgname = str(importer).split("/")[-2] + logger.debug(f"guessing orgname: {orgname}") + module = import_module( + f"{orgname}.specterext.{module_name}.service" + ) + logger.debug( + f" Imported {orgname}.specterext.{module_name}.service" + ) + except ModuleNotFoundError as e: + if module_name in str(e.name) or orgname in str(e.name): + raise Exception( + f""" + While iterating over {importer} for module {module_name}, + a Service implementation could not be found in this places: + * cryptoadvance.specter.services.{module_name}.service + * {module_name}.service + * {orgname}.specterext.{module_name}.service + Maybe you did forget to do this: + $ pip3 install -e . + """ + ) + else: + raise e elif clazz.__name__ == "SpecterMigration": module = import_module( f"cryptoadvance.specter.util.migrations.{module_name}" diff --git a/src/cryptoadvance/specter/util/reflection_fs.py b/src/cryptoadvance/specter/util/reflection_fs.py new file mode 100644 index 000000000..db21d8778 --- /dev/null +++ b/src/cryptoadvance/specter/util/reflection_fs.py @@ -0,0 +1,70 @@ +""" util stuff for searching the filesystem mainly used by the reflection.py """ + +import logging +import os +import sys +from pathlib import Path +from typing import List + +logger = logging.getLogger(__name__) + + +def detect_extension_style_in_cwd(cwd=".") -> str: + """don't override the cwd other than in testing!! + if you do, you probably have to mess with sys.path at the same time + """ + if Path(cwd, "src/cryptoadvance/specter").is_dir() or getattr(sys, "frozen", False): + return "specter-desktop" + if Path(cwd, "src").is_dir(): + if Path(cwd, "setup.py").is_file(): + return "publish-ready" + else: + raise Exception( + f""" + You have an inconsistent project file-layout in folder + {Path(cwd).resolve()} + Either you: + * Have a src-folder and you can have .py-files in your projectroot OR + * you don't have a src-folder but ./extensionid/service.py (+ __init__.py) + But not having a ./src-folder AND some .py-file in the project-root is not allowed. + """ + ) + else: + # Ad-Hoc style is: ./extensionid/service.py but no .py-files in cwd + # as this screws up the discovery + for pth in Path(cwd).iterdir(): + if pth.suffix == ".py": + raise Exception( + f""" + You have an inconsistent project file-layout in folder + {Path(cwd).resolve()} + Either you: + * Have a src-folder and you can have .py-files in your projectroot OR + * you don't have a src-folder but ./extensionid/service.py (+ __init__.py) + But not having a ./src-folder AND some .py-file in the project-root is not allowed. + """ + ) + return "adhoc" + + +def search_dirs_in_path( + path: Path, search_dirname="specterext", return_without_extid=True +) -> List[Path]: + """recursively walks the filesystem collecting directories which are called "specterext" + returns a list of PATH all ending with specterext. + If return_without_extid is False, it'll return one level deeper which might be a different length + """ + if isinstance(path, str): + path = Path(path) + plist: List[Path] = [] + if not path.is_dir(): + raise Exception(f"Search path does not exist: {path}") + for root, dirs, _ in os.walk(path, followlinks=True): + for dirname in dirs: + if dirname == search_dirname: + if return_without_extid: + plist.append(Path(root, dirname)) + else: + subdirs = [x for x in Path(root, dirname).iterdir() if x.is_dir()] + plist.extend(subdirs) + return plist diff --git a/tests/conftest.py b/tests/conftest.py index 636d88a9d..8bd8b8a40 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -473,8 +473,6 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): def specter_app_with_config(config={}, specter=None): """helper-function to create SpecterFlasks""" - if specter == None: - specter = Specter() if isinstance(config, dict): tempClass = type("tempClass", (TestConfig,), {}) for key, value in config.items(): diff --git a/tests/test_ep_controller.py b/tests/test_ep_controller.py index 88f2c1cb8..e520d461b 100644 --- a/tests/test_ep_controller.py +++ b/tests/test_ep_controller.py @@ -1,4 +1,5 @@ import logging +import os import pytest import sys from flask import Blueprint @@ -100,7 +101,12 @@ def test_APP_URL_PREFIX(caplog): caplog.set_level(logging.INFO) caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter") myapp = specter_app_with_config( - config={"APP_URL_PREFIX": "/someprefix", "SPECTER_URL_PREFIX": ""} + config={ + "APP_URL_PREFIX": "/someprefix", + "SPECTER_URL_PREFIX": "", + "EXT_URL_PREFIX": "/spc/ext", + "SPECTER_DATA_FOLDER": os.path.expanduser("~/.specter_testing"), + } ) client = myapp.test_client() login(client, "secret") @@ -132,6 +138,7 @@ def test_SPECTER_URL_PREFIX(caplog): "APP_URL_PREFIX": "", "SPECTER_URL_PREFIX": "/someprefix", "EXT_URL_PREFIX": "/someprefix/extensions", + "SPECTER_DATA_FOLDER": os.path.expanduser("~/.specter_testing"), } ) client = myapp.test_client() diff --git a/tests/test_services_extension_gen.py b/tests/test_services_extension_gen.py new file mode 100644 index 000000000..7f2b36a10 --- /dev/null +++ b/tests/test_services_extension_gen.py @@ -0,0 +1,25 @@ +from cryptoadvance.specter.services.extension_gen import ExtGen, GithubUrlLoader +from jinja2 import FileSystemLoader +from mock import patch + + +def test_GithubUrlLoader(): + gh_url_loader: ExtGen = GithubUrlLoader() + assert gh_url_loader.get_source(None, "/Readme.md") + + +def test_ExtGen(caplog): + + with patch("cryptoadvance.specter.services.extension_gen.GithubUrlLoader") as mock: + extgen = ExtGen( + ".", + "testorg", + "testext", + False, + "Some Author", + "some@mail", + # uncomment the below line to see a real generation + # tmpl_fs_source="../specterext-dummy", + dry_run=True, + ) + extgen.generate() diff --git a/tests/test_services_swan_client.py b/tests/test_services_swan_client.py index f2a542ba9..2b7a3b833 100644 --- a/tests/test_services_swan_client.py +++ b/tests/test_services_swan_client.py @@ -54,9 +54,7 @@ def test_SwanClient(app): sc = SwanClient("a_hostname", "a_access_token", 123123, "a_refresh_token") with app.app_context(): assert not sc.is_access_token_valid() - assert ( - sc.calc_callback_url() == "http://a_hostname/spc/ext/swan/oauth2/callback" - ) + assert sc.calc_callback_url() == "http://a_hostname/svc/swan/oauth2/callback" start_url = sc.get_oauth2_start_url("a_hostname") start_url.startswith( diff --git a/tests/test_util_common.py b/tests/test_util_common.py index 3e3516e26..03bfd15d0 100644 --- a/tests/test_util_common.py +++ b/tests/test_util_common.py @@ -1,4 +1,8 @@ -from cryptoadvance.specter.util.common import camelcase2snake_case, str2bool +from cryptoadvance.specter.util.common import ( + camelcase2snake_case, + snake_case2camelcase, + str2bool, +) def test_str2bool(): @@ -20,3 +24,8 @@ def test_str2bool(): def test_camelcase2snake_case(): assert camelcase2snake_case("Service") == "service" assert camelcase2snake_case("DeviceType") == "device_type" + + +def test_snake_case2camelcase(): + assert snake_case2camelcase("service") == "Service" + assert snake_case2camelcase("device_Type") == "DeviceType" diff --git a/tests/test_util_priceprovider.py b/tests/test_util_priceprovider.py index be25e4da8..b87f98171 100644 --- a/tests/test_util_priceprovider.py +++ b/tests/test_util_priceprovider.py @@ -12,11 +12,11 @@ from cryptoadvance.specter.util.price_providers import ( from mock import MagicMock -def test_underlying_requests(): +def test_underlying_requests(empty_data_folder): """This test might fail on MacOS see #1512 """ - specter_mock = Specter() + specter_mock = Specter(data_folder=empty_data_folder) requests_session = specter_mock.requests_session() currency = "eur" price = requests_session.get( @@ -26,8 +26,8 @@ def test_underlying_requests(): assert float(price) -def test_failsafe_request_get(): - specter_mock = Specter() +def test_failsafe_request_get(empty_data_folder): + specter_mock = Specter(data_folder=empty_data_folder) requests_session = specter_mock.requests_session() currency = "notExisting" url = "https://www.bitstamp.net/api/v2/ticker/btc{}".format(currency) diff --git a/tests/test_util_reflection.py b/tests/test_util_reflection.py index f83c9f975..f5bedf949 100644 --- a/tests/test_util_reflection.py +++ b/tests/test_util_reflection.py @@ -1,10 +1,13 @@ import logging from pathlib import Path +from typing import List from cryptoadvance.specter.util.reflection import ( get_subclasses_for_clazz, + get_subclasses_for_clazz_in_cwd, get_classlist_of_type_clazz_from_modulelist, _get_module_from_class, get_package_dir_for_subclasses_of, + search_dirs_in_path, ) from cryptoadvance.specter.util.specter_migrator import SpecterMigration from cryptoadvance.specter.util.migrations.migration_0000 import SpecterMigration_0000 @@ -45,6 +48,15 @@ def test_get_classlist_from_importlist(caplog): assert BitcoinReserveService in classlist +def test_get_subclasses_for_clazz_in_cwd(caplog): + caplog.set_level(logging.DEBUG) + classlist: List[type] = get_subclasses_for_clazz_in_cwd( + Service, cwd="./tests/xtestdata_testextensions" + ) + # damn, this is difficult to test + # assert len(classlist) == 3 + + def test_get_subclasses_for_class(caplog): caplog.set_level(logging.INFO) classlist = get_subclasses_for_clazz(SpecterMigration) diff --git a/tests/test_util_reflection_fs.py b/tests/test_util_reflection_fs.py new file mode 100644 index 000000000..3e424e881 --- /dev/null +++ b/tests/test_util_reflection_fs.py @@ -0,0 +1,62 @@ +import os +from pathlib import Path +from cryptoadvance.specter.util.reflection_fs import ( + search_dirs_in_path, + detect_extension_style_in_cwd, +) +from typing import List + + +def test_search_dir_in_cwd(): + plist: List[Path] = search_dirs_in_path(Path("./tests/xtestdata_testextensions")) + assert len(plist) == 2 + assert isinstance(plist[0], Path) + assert ( + Path( + "tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext" + ) + in plist + ) + assert ( + Path( + "tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext" + ) + in plist + ) + + plist = search_dirs_in_path( + Path("./tests/xtestdata_testextensions"), return_without_extid=False + ) + assert len(plist) == 2 + assert isinstance(plist[0], Path) + assert ( + Path( + "tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot" + ) + in plist + ) + assert ( + Path( + "tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot" + ) + in plist + ) + + +def test_detect_extension_style_in_cwd(): + assert ( + detect_extension_style_in_cwd( + "tests/xtestdata_testextensions/ext_root_fully_qualified_1" + ) + == "publish-ready" + ) # + assert ( + detect_extension_style_in_cwd( + "tests/xtestdata_testextensions/ext_root_fully_qualified_2" + ) + == "publish-ready" + ) + assert ( + detect_extension_style_in_cwd("tests/xtestdata_testextensions/ext_root_adhoc_1") + == "adhoc" + ) diff --git a/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/__init__.py b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/controller.py b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/controller.py new file mode 100644 index 000000000..41faba2da --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/controller.py @@ -0,0 +1,57 @@ +import logging +from flask import redirect, render_template, request, url_for, flash +from flask import current_app as app +from flask_login import login_required, current_user + +from cryptoadvance.specter.services.controller import user_secret_decrypted_required +from cryptoadvance.specter.user import User +from cryptoadvance.specter.wallet import Wallet +from .service import BreznguatService + + +logger = logging.getLogger(__name__) + +breznguat_endpoint = BreznguatService.blueprint + + +@breznguat_endpoint.route("/") +@login_required +@user_secret_decrypted_required +def index(): + return render_template( + "breznguat/index.jinja", + ) + + +@breznguat_endpoint.route("/settings", methods=["GET"]) +@login_required +@user_secret_decrypted_required +def settings_get(): + associated_wallet: Wallet = BreznguatService.get_associated_wallet() + + # Get the user's Wallet objs, sorted by Wallet.name + wallet_names = sorted(current_user.wallet_manager.wallets.keys()) + wallets = [current_user.wallet_manager.wallets[name] for name in wallet_names] + + return render_template( + "breznguat/settings.jinja", + associated_wallet=associated_wallet, + wallets=wallets, + cookies=request.cookies, + ) + + +@breznguat_endpoint.route("/settings", methods=["POST"]) +@login_required +@user_secret_decrypted_required +def settings_post(): + show_menu = request.form["show_menu"] + user = app.specter.user_manager.get_user() + if show_menu == "yes": + user.add_service(BreznguatService.id) + else: + user.remove_service(BreznguatService.id) + used_wallet_alias = request.form.get("used_wallet") + if used_wallet_alias != None: + wallet = current_user.wallet_manager.get_by_alias(used_wallet_alias) + return redirect(url_for(f"{BreznguatService.get_blueprint_name()}.settings_get")) diff --git a/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/service.py b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/service.py new file mode 100644 index 000000000..213e02d89 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/service.py @@ -0,0 +1,50 @@ +import logging + +from cryptoadvance.specter.services.service import ( + Service, + devstatus_alpha, + devstatus_prod, +) + +# A SpecterError can be thrown and will be shown to the user as a red banner +from cryptoadvance.specter.specter_error import SpecterError +from flask import current_app as app +from cryptoadvance.specter.wallet import Wallet + +logger = logging.getLogger(__name__) + + +class BreznguatService(Service): + id = "breznguat" + name = "Breznguat Service" + icon = "breznguat/img/ghost.png" + logo = "breznguat/img/breznguat_logo.jpeg" + desc = "Where a Breznguat grows bigger." + has_blueprint = True + blueprint_module = "breznguat.controller" + devstatus = devstatus_alpha + piggyback = False + + # TODO: As more Services are integrated, we'll want more robust categorization and sorting logic + sort_priority = 2 + + # ServiceEncryptedStorage field names for this service + # Those will end up as keys in a json-file + SPECTER_WALLET_ALIAS = "wallet" + + @classmethod + def get_associated_wallet(cls) -> Wallet: + """Get the Specter `Wallet` that is currently associated with this service""" + service_data = cls.get_current_user_service_data() + if not service_data or cls.SPECTER_WALLET_ALIAS not in service_data: + # Service is not initialized; nothing to do + return + try: + return app.specter.wallet_manager.get_by_alias( + service_data[cls.SPECTER_WALLET_ALIAS] + ) + except SpecterError as e: + logger.debug(e) + # Referenced an unknown wallet + # TODO: keep ignoring or remove the unknown wallet from service_data? + return diff --git a/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/components/breznguat_menu.jinja b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/components/breznguat_menu.jinja new file mode 100644 index 000000000..7f3606492 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/components/breznguat_menu.jinja @@ -0,0 +1,16 @@ +{% from 'components/menu_item.jinja' import menu_item %} + +{# + vaultoro_menu - Tabs menu to navigate between the vaultoro screens. + Parameters: + - active_menuitem: Current active tab. Options: 'general', 'bitcoin_core', 'auth'. + #} +{% macro breznguat_menu(active_menuitem) -%} + +{%- endmacro %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/components/breznguat_tab.jinja b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/components/breznguat_tab.jinja new file mode 100644 index 000000000..7767e4368 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/components/breznguat_tab.jinja @@ -0,0 +1,8 @@ +{% extends "base.jinja" %} +{% block main %} + + {% from 'breznguat/components/breznguat_menu.jinja' import breznguat_menu with context %} + {{ breznguat_menu(tab) }} + {% block content %} + {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/index.jinja b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/index.jinja new file mode 100644 index 000000000..af080bbd4 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/index.jinja @@ -0,0 +1,87 @@ +{% extends "breznguat/components/breznguat_tab.jinja" %} +{% block title %}Settings{% endblock %} +{% set tab = 'index' %} +{% block content %} + +

+
“Breznguatservice 4thewin.”
+{% endblock %} + + + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/settings.jinja b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/settings.jinja new file mode 100644 index 000000000..748e83ba5 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_adhoc_1/breznguat/templates/breznguat/settings.jinja @@ -0,0 +1,109 @@ +{% extends "breznguat/components/breznguat_tab.jinja" %} +{% block title %}Settings{% endblock %} +{% set tab = 'settings_get' %} +{% block content %} +
+ + +
+

{{ _("Configure your extension") }}

+
+ +
+ {{ _("- Maybe you want to put some notes for the user") }}
+ {{ _("- One important setting is whether your extension gets a menu-point on the left") }}
+ {{ _("- Let us assume you want the user to use a specific wallet. That should go here. ") }}
+
+
+ +
+ + +
Show Menu Item:
+ +
+
+
+ + {{ _("Choose which wallet should be used:") }}:
+ +
+
+
+ +
+ +
+ +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_adhoc_1/requirements.txt b/tests/xtestdata_testextensions/ext_root_adhoc_1/requirements.txt new file mode 100644 index 000000000..38c6d5f37 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_adhoc_1/requirements.txt @@ -0,0 +1 @@ +cryptoadvance.specter==1.8.1 \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_adhoc_1/tests/conftest.py b/tests/xtestdata_testextensions/ext_root_adhoc_1/tests/conftest.py new file mode 100644 index 000000000..3b444495c --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_adhoc_1/tests/conftest.py @@ -0,0 +1,507 @@ +import atexit +import json +import logging +import os +import shutil +import subprocess +import tempfile +import time + +import docker +import pytest +from cryptoadvance.specter.managers.device_manager import DeviceManager +from cryptoadvance.specter.managers.user_manager import UserManager +from cryptoadvance.specter.process_controller.bitcoind_controller import ( + BitcoindPlainController, +) +from cryptoadvance.specter.process_controller.bitcoind_docker_controller import ( + BitcoindDockerController, +) +from cryptoadvance.specter.process_controller.elementsd_controller import ( + ElementsPlainController, +) +from cryptoadvance.specter.rpc import BitcoinRPC +from cryptoadvance.specter.server import SpecterFlask, create_app, init_app +from cryptoadvance.specter.specter import Specter +from cryptoadvance.specter.specter_error import SpecterError +from cryptoadvance.specter.user import User, hash_password +from cryptoadvance.specter.util.wallet_importer import WalletImporter +from cryptoadvance.specter.util.common import str2bool +from cryptoadvance.specter.util.shell import which +import code, traceback, signal + +logger = logging.getLogger(__name__) + +pytest_plugins = ["ghost_machine"] + +# This is from https://stackoverflow.com/questions/132058/showing-the-stack-trace-from-a-running-python-application +# it enables stopping a hanging test via sending the pytest-process a SIGUSR2 (12) +# kill 12 pid-of-pytest +# In the article they claim to open a debug-console which didn't work for me but at least +# you get a stacktrace in the output. +def debug(sig, frame): + """Interrupt running process, and provide a python prompt for + interactive debugging.""" + d = {"_frame": frame} # Allow access to frame object. + d.update(frame.f_globals) # Unless shadowed by global + d.update(frame.f_locals) + + i = code.InteractiveConsole(d) + message = "Signal received : entering python shell.\nTraceback:\n" + message += "".join(traceback.format_stack(frame)) + i.interact(message) + + +def listen(): + signal.signal(signal.SIGUSR2, debug) # Register handler + + +def pytest_addoption(parser): + """Internally called to add options to pytest + see pytest_generate_tests(metafunc) on how to check that + Also used to register the SIGUSR2 (12) as decribed in conftest.py + """ + parser.addoption("--docker", action="store_true", help="run bitcoind in docker") + parser.addoption( + "--bitcoind-version", + action="store", + default="v0.20.1", + help="Version of bitcoind (something which works with git checkout ...)", + ) + parser.addoption( + "--bitcoind-log-stdout", + action="store", + default=False, + help="Whether bitcoind should log to stdout (default:False)", + ) + parser.addoption( + "--elementsd-version", + action="store", + default="master", + help="Version of elementsd (something which works with git checkout ...)", + ) + listen() + + +def pytest_generate_tests(metafunc): + # ToDo: use custom compiled version of bitcoind + # E.g. test again bitcoind version [currentRelease] + master-branch + if "docker" in metafunc.fixturenames: + if metafunc.config.getoption("docker"): + # That's a list because we could do both (see above) but currently that doesn't make sense in that context + metafunc.parametrize("docker", [True], scope="session") + else: + metafunc.parametrize("docker", [False], scope="session") + + +def instantiate_bitcoind_controller(docker, request, rpcport=18543, extra_args=[]): + # logging.getLogger().setLevel(logging.DEBUG) + requested_version = request.config.getoption("--bitcoind-version") + log_stdout = str2bool(request.config.getoption("--bitcoind-log-stdout")) + if docker: + from cryptoadvance.specter.process_controller.bitcoind_docker_controller import ( + BitcoindDockerController, + ) + + bitcoind_controller = BitcoindDockerController( + rpcport=rpcport, docker_tag=requested_version + ) + else: + if os.path.isfile("tests/bitcoin/src/bitcoind"): + bitcoind_controller = BitcoindPlainController( + bitcoind_path="tests/bitcoin/src/bitcoind", rpcport=rpcport + ) # always prefer the self-compiled bitcoind if existing + elif os.path.isfile("tests/bitcoin/bin/bitcoind"): + bitcoind_controller = BitcoindPlainController( + bitcoind_path="tests/bitcoin/bin/bitcoind", rpcport=rpcport + ) # next take the self-installed binary if existing + else: + bitcoind_controller = BitcoindPlainController( + rpcport=rpcport + ) # Alternatively take the one on the path for now + bitcoind_controller.start_bitcoind( + cleanup_at_exit=True, + cleanup_hard=True, + extra_args=extra_args, + log_stdout=log_stdout, + ) + assert not bitcoind_controller.datadir is None + running_version = bitcoind_controller.version() + requested_version = request.config.getoption("--bitcoind-version") + assert running_version == requested_version, ( + "Please make sure that the Bitcoind-version (%s) matches with the version in pytest.ini (%s)" + % (running_version, requested_version) + ) + return bitcoind_controller + + +def instantiate_elementsd_controller(request, rpcport=18643, extra_args=[]): + if os.path.isfile("tests/elements/src/elementsd"): + elementsd_controller = ElementsPlainController( + elementsd_path="tests/elements/src/elementsd", rpcport=rpcport + ) # always prefer the self-compiled bitcoind if existing + elif os.path.isfile("tests/elements/bin/elementsd"): + elementsd_controller = ElementsPlainController( + elementsd_path="tests/elements/bin/elementsd", rpcport=rpcport + ) # next take the self-installed binary if existing + else: + elementsd_controller = ElementsPlainController( + rpcport=rpcport + ) # Alternatively take the one on the path for now + elementsd_controller.start_elementsd( + cleanup_at_exit=True, cleanup_hard=True, extra_args=extra_args + ) + assert not elementsd_controller.datadir is None + running_version = elementsd_controller.version() + requested_version = request.config.getoption("--elementsd-version") + assert running_version == requested_version, ( + "Please make sure that the elementsd-version (%s) matches with the version in pytest.ini (%s)" + % (running_version, requested_version) + ) + return elementsd_controller + + +# Below this point are fixtures. Fixtures have a scope. Check about scopes here: +# https://docs.pytest.org/en/6.2.x/fixture.html#scope-sharing-fixtures-across-classes-modules-packages-or-session +# possible values: function, class, module, package or session. +# The nodes are of scope session. All else is the default (function) + + +@pytest.fixture(scope="session") +def bitcoind_path(): + if os.path.isfile("tests/bitcoin/src/bitcoind"): + return "tests/bitcoin/src/bitcoind" + elif os.path.isfile("tests/bitcoin/bin/bitcoind"): + return "tests/bitcoin/bin/bitcoind" + else: + return which("bitcoind") + + +@pytest.fixture(scope="session") +def bitcoin_regtest(docker, request): + bitcoind_regtest = instantiate_bitcoind_controller(docker, request, extra_args=None) + try: + assert bitcoind_regtest.get_rpc().test_connection() + assert not bitcoind_regtest.datadir is None + yield bitcoind_regtest + finally: + bitcoind_regtest.stop_bitcoind() + + +@pytest.fixture(scope="session") +def elements_elreg(request): + elements_elreg = instantiate_elementsd_controller(request, extra_args=None) + try: + yield elements_elreg + assert not elements_elreg.datadir is None + finally: + elements_elreg.stop_elementsd() + + +@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: + yield data_folder + + +@pytest.fixture +def devices_filled_data_folder(empty_data_folder): + os.makedirs(empty_data_folder + "/devices") + with open(empty_data_folder + "/devices/trezor.json", "w") as text_file: + text_file.write( + """ +{ + "name": "Trezor", + "type": "trezor", + "keys": [ + { + "derivation": "m/49h/0h/0h", + "original": "ypub6XFn7hfb676MLm6ZsAviuQKXeRDNgT9Bs32KpRDPnkKgKDjKcrhYCXJ88aBfy8co2k9eujugJX5nwq7RPG4sj6yncDEPWN9dQGmFWPy4kFB", + "fingerprint": "1ef4e492", + "type": "sh-wpkh", + "xpub": "xpub6CRWp2zfwRYsVTuT2p96hKE2UT4vjq9gwvW732KWQjwoG7v6NCXyaTdz7NE5yDxsd72rAGK7qrjF4YVrfZervsJBjsXxvTL98Yhc7poBk7K" + }, + { + "derivation": "m/84h/0h/0h", + "original": "zpub6rGoJTXEhKw7hUFkjMqNctTzojkzRPa3VFuUWAirqpuj13mRweRmnYpGD1aQVFpxNfp17zVU9r7F6oR3c4zL3DjXHdewVvA7kjugHSqz5au", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "xpub6CcGh8BQPxr9zssX4eG8CiGzToU6Y9b3f2s2wNw65p9xtr8ySL6eYRVzAbfEVSX7ZPaPd3JMEXQ9LEBvAgAJSkNKYxG6L6X9DHnPWNQud4H" + }, + { + "derivation": "m/48h/0h/0h/1h", + "original": "Ypub6jtWQ1r2D7EwqNoxERU28MWZH4WdL3pWdN8guFJRBTmGwstJGzMXJe1VaNZEuAAVsZwpKPhs5GzNPEZR77mmX1mjwzEiouxmQYsrxFBNVNN", + "fingerprint": "1ef4e492", + "type": "sh-wsh", + "xpub": "xpub6EA9y7SfVU96ZWTTTQDR6C5FPJKvB59RPyxoCb8zRgYzGbWAFvogbTVRkTeBLpHgETm2hL7BjQFKNnL66CCoaHyUFBRtpbgHF6YLyi7fr6m" + }, + { + "derivation": "m/48h/0h/0h/2h", + "original": "Zpub74imhgWwMnnRkSPkiNavCQtSBu1fGo8RP96h9eT2GHCgN5eFU9mZVPhGphvGnG26A1cwJxtkmbHR6nLeTw4okpCDjZCEj2HRLJoVHAEsch9", + "fingerprint": "1ef4e492", + "type": "wsh", + "xpub": "xpub6EA9y7SfVU96dGr96zYgxAMd8AgWBCTqEeQafbPi8VcWdhStCS4AA9X4yb3dE1VM7GKLwRhWy4BpD3VkjK5q1riMAQgz9oBSu8QKv5S7KzD" + }, + { + "derivation": "m/49h/1h/0h", + "original": "upub5EKoQv21nQNkhdt4yuLyRnWitA3EGhW1ru1Y8VTG8gdys2JZhqiYkhn4LHp2heHnH41kz95bXPvrYVRuFUrdUMik6YdjFV4uL4EubnesttQ", + "fingerprint": "1ef4e492", + "type": "sh-wpkh", + "xpub": "tpubDDCDr9rSwixeXKeGwAgwFy8bjBaE5wya9sAVqEC4ccXWmcQxY34KmLRJdwmaDsCnHsu5r9P9SUpYtXmCoRwukWDqmAUJgkBbjC2FXUzicn6" + }, + { + "derivation": "m/84h/1h/0h", + "original": "vpub5Y35MNUT8sUR2SnRCU9A9S6z1JDACMTuNnM8WHXvuS7hCwuVuoRAWJGpi66Yo8evGPiecN26oLqx19xf57mqVQjiYb9hbb4QzbNmFfsS9ko", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc" + }, + { + "derivation": "m/48h/1h/0h/1h", + "original": "Upub5Tk9tZtdzVaTGWtygRTKDDmaN5vfB59pn2L5MQyH6BkVpg2Y5J95rtpQndjmXNs3LNFiy8zxpHCTtvxxeePjgipF7moTHQZhe3E5uPzDXh8", + "fingerprint": "1ef4e492", + "type": "sh-wsh", + "xpub": "tpubDFiVCZzdarbyfdVoh2LJDL3eVKRPmxwnkiqN8tSYCLod75a2966anQbjHajqVAZ97j54xZJPr9hf7ogVuNL4pPCfwvXdKGDQ9SjZF7vXQu1" + }, + { + "derivation": "m/48h/1h/0h/2h", + "original": "Vpub5naRCEZZ9B7wCKLWuqoNdg6ddWEx8ruztUygXFZDJtW5LRMqUP5HV2TsNw1nc74Ba3QPDSH7qzauZ8LdfNmnmofpfmztCGPgP7vaaYSmpgN", + "fingerprint": "1ef4e492", + "type": "wsh", + "xpub": "tpubDFiVCZzdarbyk8kE65tjRhHCambEo8iTx4xkXL8b33BKZj66HWsDnUb3rg4GZz6Mwm6vTNyzRCjYtiScCQJ77ENedb2deDDtcoNQXiUouJQ" + } + ] +} +""" + ) + with open(empty_data_folder + "/devices/specter.json", "w") as text_file: + text_file.write( + """ +{ + "name": "Specter", + "type": "specter", + "keys": [ + { + "derivation": "m/48h/1h/0h/2h", + "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", + "fingerprint": "08686ac6", + "type": "wsh", + "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL" + }, + { + "derivation": "m/84h/1h/0h", + "original": "vpub5ZSem3mLXiSJzgDX6pJb2N9L6sJ8m6ejaksLPLSuB53LBzCi2mMsBg19eEUSDkHtyYp75GATjLgt5p3S43WjaVCXAWU9q9H5GhkwJBrMiAb", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUiy4ncDirveTfhmvggdj8nxcW5JgHpGzYz3UVscJY5aEzFvgUPk4YyajadBnsTBmE2YZmAtJC14Q21xncJgVaHQ7UdqMRVRbU" + }, + { + "derivation": "m/84h/1h/1h", + "original": "vpub5ZSem3mLXiSK55jPzfLVhbHbTEwGzEFZv3xrGFCw1vGHSNw7WcVuJXysJLWcgENQd3iXSNQaeSXUBW55Hy4GAjSTjrWP4vpKKkUN9jiU1Tc", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj3UJV7ZtqKgoy8JKprrjdHubbBb3r7qmwHsEH69g7h6xyanWaCYdVEEV3Yu7a6s4ceFnp8DjXeeFxY8eXvH7XTAC4gxfDNEW" + }, + { + "derivation": "m/84h/1h/2h", + "original": "vpub5ZSem3mLXiSK64v64deytnDCoYqbUSYHvmVurUGVMEnXMyEybtF3FEnNuiFDDC6J18a81fv5ptQXaQaaRiYx8MRxahipgxPLdxubpYt1dkD", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj4TVBBYDKWsjaUcE9M52MJd8emp7QTAJBDTY9BRRFdomVCAFAjWMNcKLe8Cd5HJwg3AJKFyEDcGFTNyryYJgYmNdJMhwB2RG" + }, + { + "derivation": "m/84h/1h/3h", + "original": "vpub5ZSem3mLXiSK8cKzh4sHxTvN7mgYQA29HfoAZeCDtX1M2zdejN5XVAtVyqhk8eui18JTtZ9M3VD3AiWCz8VwrybhBUh3HxzS8js3mLVybDT", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj6zu5oyRdaZSjnq56GnWCfXRuUz38zSWztUvpJuFjsjscGHhheyAncK4z15rLVukBdUDwpPBDLtRBykqC9KHeG9akJWRipKK" + } + ] +} +""" + ) + return empty_data_folder # no longer empty, though + + +@pytest.fixture +def wallets_filled_data_folder(devices_filled_data_folder): + os.makedirs(os.path.join(devices_filled_data_folder, "wallets", "regtest")) + with open( + os.path.join(devices_filled_data_folder, "wallets", "regtest", "simple.json"), + "w", + ) as json_file: + json_file.write( + """ +{ + "alias": "simple", + "fullpath": "/home/kim/.specter/wallets/regtest/simple.json", + "name": "Simple", + "address_index": 0, + "keypool": 5, + "address": "bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej", + "change_index": 0, + "change_address": "bcrt1qt28v03278lmmxllys89acddp2p5y4zds94944n", + "change_keypool": 5, + "type": "simple", + "description": "Single (Segwit)", + "keys": [{ + "derivation": "m/84h/1h/0h", + "original": "vpub5Y35MNUT8sUR2SnRCU9A9S6z1JDACMTuNnM8WHXvuS7hCwuVuoRAWJGpi66Yo8evGPiecN26oLqx19xf57mqVQjiYb9hbb4QzbNmFfsS9ko", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc" + }], + "recv_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", + "change_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/1/*)#h4z73prm", + "device": "Trezor", + "device_type": "trezor", + "address_type": "bech32" +} + +""" + ) + return devices_filled_data_folder # and with wallets obviously + + +@pytest.fixture +def device_manager(devices_filled_data_folder): + return DeviceManager(os.path.join(devices_filled_data_folder, "devices")) + + +# @pytest.fixture +# def user_manager(empty_data_folder) -> UserManager: +# """A UserManager having users alice, bob and eve""" +# specter = Specter(data_folder=empty_data_folder) +# user_manager = UserManager(specter=specter) +# config = {} +# user_manager.get_user("admin").decrypt_user_secret("admin") +# user_manager.create_user( +# user_id="alice", +# username="alice", +# plaintext_password="plain_pass_alice", +# config=config, +# ) +# user_manager.create_user( +# user_id="bob", +# username="bob", +# plaintext_password="plain_pass_bob", +# config=config, +# ) +# user_manager.create_user( +# user_id="eve", +# username="eve", +# plaintext_password="plain_pass_eve", +# config=config, +# ) +# return user_manager + + +@pytest.fixture +def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): + assert bitcoin_regtest.get_rpc().test_connection() + config = { + "rpc": { + "autodetect": False, + "datadir": "", + "user": bitcoin_regtest.rpcconn.rpcuser, + "password": bitcoin_regtest.rpcconn.rpcpassword, + "port": bitcoin_regtest.rpcconn.rpcport, + "host": bitcoin_regtest.rpcconn.ipaddress, + "protocol": "http", + }, + "auth": { + "method": "rpcpasswordaspin", + }, + } + specter = Specter(data_folder=devices_filled_data_folder, config=config) + assert specter.chain == "regtest" + # Create a User + someuser = specter.user_manager.add_user( + User.from_json( + user_dict={ + "id": "someuser", + "username": "someuser", + "password": hash_password("somepassword"), + "config": {}, + "is_admin": False, + "services": None, + }, + specter=specter, + ) + ) + specter.user_manager.save() + specter.check() + + assert not someuser.wallet_manager.working_folder is None + + # Create a Wallet + wallet_json = '{"label": "a_simple_wallet", "blockheight": 0, "descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", "devices": [{"type": "trezor", "label": "trezor"}]} ' + wallet_importer = WalletImporter( + wallet_json, specter, device_manager=someuser.device_manager + ) + wallet_importer.create_nonexisting_signers( + someuser.device_manager, + {"unknown_cosigner_0_name": "trezor", "unknown_cosigner_0_type": "trezor"}, + ) + dm: DeviceManager = someuser.device_manager + wallet = wallet_importer.create_wallet(someuser.wallet_manager) + try: + # fund it with some coins + bitcoin_regtest.testcoin_faucet(address=wallet.getnewaddress()) + # make sure it's confirmed + bitcoin_regtest.mine() + # Realize that the wallet has funds: + wallet.update() + except SpecterError as se: + if str(se).startswith("Timeout"): + pytest.fail( + "We got a Bitcoin-RPC timeout while setting up the test, minting some coins. Test Error! Check cpu/mem utilastion and btc/elem logs!" + ) + return + else: + raise se + + assert wallet.fullbalance >= 20 + assert not specter.wallet_manager.working_folder is None + try: + yield specter + finally: + # Deleting all Wallets (this will also purge them on core) + for user in specter.user_manager.users: + for wallet in list(user.wallet_manager.wallets.values()): + user.wallet_manager.delete_wallet( + wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest" + ) + + +@pytest.fixture +def app(specter_regtest_configured) -> SpecterFlask: + """the Flask-App, but uninitialized""" + app = create_app(config="cryptoadvance.specter.config.TestConfig") + app.app_context().push() + app.config["TESTING"] = True + app.testing = True + app.tor_service_id = None + app.tor_enabled = False + init_app(app, specter=specter_regtest_configured) + return app + + +@pytest.fixture +def app_no_node(empty_data_folder) -> SpecterFlask: + specter = Specter(data_folder=empty_data_folder) + app = create_app(config="cryptoadvance.specter.config.TestConfig") + app.app_context().push() + app.config["TESTING"] = True + app.testing = True + app.tor_service_id = None + app.tor_enabled = False + init_app(app, specter=specter) + return app + + +@pytest.fixture +def client(app): + """a test_client from an initialized Flask-App""" + return app.test_client() diff --git a/tests/xtestdata_testextensions/ext_root_adhoc_1/tests/ghost_machine.py b/tests/xtestdata_testextensions/ext_root_adhoc_1/tests/ghost_machine.py new file mode 100644 index 000000000..b1d3b142d --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_adhoc_1/tests/ghost_machine.py @@ -0,0 +1,67 @@ +import pytest + +# Using https://iancoleman.io/bip39/ and https://jlopp.github.io/xpub-converter/ +# mnemonic = "ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost machine" + + +# m/44'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_44(): + xpub = "xpub6CGap5qbgNCEsvXg2gAjEho17zECMA9PbZa7QkrEWTPnPRaubE6qKots5pNwhyFtuYSPa9gQu4jTTZi8WPaXJhtCHrvHQaFRqayN1saQoWv" + return xpub + + +# m/49'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_49(): + xpub = "xpub6BtcNhqbaFaoC3oEfKky3Sm22pF48U2jmAf78cB3wdAkkGyAgmsVrgyt1ooSt3bHWgzsdUQh2pTJ867yTeUAMmFDKNSBp8J7WPmp7Df7zjv" + return xpub + + +@pytest.fixture +def ghost_machine_ypub(): + ypub = "ypub6WisgNWWiw8H3LzMVgYbFXrXCnPW562EgHBKv14wKdYdoNnPwS34Uke231m2sxFCvL7gNx1FVUor1NjYBLtB9zvpBi8cQ37bn7qTVqo3fjR" + return ypub + + +@pytest.fixture +def ghost_machine_tpub_49(): + tpub = "tpubDC5CZBbVc15fpTeqkyUBKgHqYCqkeaUtPjvGz7RJEttndfcN29psPcxTSj5RNJaWYaRQq8kqovLBrZA2tju3ThSAP9fY1eiSvorchnseFZu" + return tpub + + +@pytest.fixture +def ghost_machine_upub(): + upub = "upub5DCn7wm4SgVmzmtdoi8DVVfxhBJkqL1L6mmKHNgVky1Fj5VyBxV6NzKD957sr5fWXkY5y8THtqSVWWpjLnomBYw4iXpxaPbkXg5Gn6s5tQf" + return upub + + +# m/84'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_84(): + xpub = "xpub6CjsHfiuBnHMPBkxThQ4DDjTw2Qq3VMEVcPBoMBGejZGkj3WQR15LeJLmymPpSzYHX21C8SdFWHgMw2RUBdAQ2Aj4MMS93a68mxPQeS8oHr" + return xpub + + +@pytest.fixture +def ghost_machine_zpub(): + zpub = "zpub6rQPu14jV9NK5n9C8QyJdPvUGxhivjLEKqRdN8y3QkK2rvfxujLCamccpPgZpGJP6oFch5dkApzn8WFYuaTBzVXvo2kHJsD4gE5gBnCBYj1" + return zpub + + +@pytest.fixture +def ghost_machine_tpub_84(): + tpub = "tpubDC4DsqH5rqHqipMNqUbDFtQT3AkKkUrvLsN6miySvortU3s1LGaNVAb7wX2No2VsuxQV82T8s3HJLv3kdx1CPjsJ3onC1Zo5mWCQzRVaWVX" + return tpub + + +@pytest.fixture +def ghost_machine_vpub(): + vpub = "vpub5Y24kG7ZrCFRkRnHia2sdnt5N7MmsrNry1jMrP8XptMEcZZqkjQA6bc1f52RGiEoJmdy1Vk9Qck9tAL1ohKvuq3oFXe3ADVse6UiTHzuyKx" + return vpub diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/MANIFEST.in b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/MANIFEST.in new file mode 100644 index 000000000..fdd68dd3d --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/MANIFEST.in @@ -0,0 +1,5 @@ +recursive-include src/boatacccorp/specterext/tretboot/templates * +recursive-include src/boatacccorp/specterext/tretboot/static * +recursive-include src/boatacccorp/specterext/tretboot/*/LC_MESSAGES *.mo +recursive-include src/boatacccorp/specterext/tretboot/translations/*/LC_MESSAGES *.po +include requirements.txt diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/pyproject.toml b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/pyproject.toml new file mode 100644 index 000000000..bacb2fa6e --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +requires = [ + "cryptoadvance.specter==1.8.1" +] +build-backend = "setuptools.build_meta" diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/requirements.txt b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/requirements.txt new file mode 100644 index 000000000..38c6d5f37 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/requirements.txt @@ -0,0 +1 @@ +cryptoadvance.specter==1.8.1 \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/setup.cfg b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/setup.cfg new file mode 100644 index 000000000..5e618d574 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/setup.cfg @@ -0,0 +1,24 @@ +[metadata] +name = boatacccorp_tretboot +version = 0.0.1 +author = Your Name +author_email = some_mail@mail.com +description = A small example package +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/boatacccorp/spex-tretboot +project_urls = + Bug Tracker = https://github.com/pypa/sampleproject/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + +[options] +package_dir = + = src +packages = find_namespace: +python_requires = >=3.6 + +[options.packages.find] +where = src diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/setup.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/setup.py new file mode 100644 index 000000000..606849326 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/__init__.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/controller.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/controller.py new file mode 100644 index 000000000..0058506e9 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/controller.py @@ -0,0 +1,57 @@ +import logging +from flask import redirect, render_template, request, url_for, flash +from flask import current_app as app +from flask_login import login_required, current_user + +from cryptoadvance.specter.services.controller import user_secret_decrypted_required +from cryptoadvance.specter.user import User +from cryptoadvance.specter.wallet import Wallet +from .service import TretbootService + + +logger = logging.getLogger(__name__) + +tretboot_endpoint = TretbootService.blueprint + + +@tretboot_endpoint.route("/") +@login_required +@user_secret_decrypted_required +def index(): + return render_template( + "tretboot/index.jinja", + ) + + +@tretboot_endpoint.route("/settings", methods=["GET"]) +@login_required +@user_secret_decrypted_required +def settings_get(): + associated_wallet: Wallet = TretbootService.get_associated_wallet() + + # Get the user's Wallet objs, sorted by Wallet.name + wallet_names = sorted(current_user.wallet_manager.wallets.keys()) + wallets = [current_user.wallet_manager.wallets[name] for name in wallet_names] + + return render_template( + "tretboot/settings.jinja", + associated_wallet=associated_wallet, + wallets=wallets, + cookies=request.cookies, + ) + + +@tretboot_endpoint.route("/settings", methods=["POST"]) +@login_required +@user_secret_decrypted_required +def settings_post(): + show_menu = request.form["show_menu"] + user = app.specter.user_manager.get_user() + if show_menu == "yes": + user.add_service(TretbootService.id) + else: + user.remove_service(TretbootService.id) + used_wallet_alias = request.form.get("used_wallet") + if used_wallet_alias != None: + wallet = current_user.wallet_manager.get_by_alias(used_wallet_alias) + return redirect(url_for(f"{TretbootService.get_blueprint_name()}.settings_get")) diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/service.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/service.py new file mode 100644 index 000000000..6d63a65a5 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/service.py @@ -0,0 +1,50 @@ +import logging + +from cryptoadvance.specter.services.service import ( + Service, + devstatus_alpha, + devstatus_prod, +) + +# A SpecterError can be thrown and will be shown to the user as a red banner +from cryptoadvance.specter.specter_error import SpecterError +from flask import current_app as app +from cryptoadvance.specter.wallet import Wallet + +logger = logging.getLogger(__name__) + + +class TretbootService(Service): + id = "tretboot" + name = "Tretboot Service" + icon = "tretboot/img/ghost.png" + logo = "tretboot/img/tretboot_logo.jpeg" + desc = "Where a Tretboot grows bigger." + has_blueprint = True + blueprint_module = "boatacccorp.specterext.tretboot.controller" + devstatus = devstatus_alpha + piggyback = False + + # TODO: As more Services are integrated, we'll want more robust categorization and sorting logic + sort_priority = 2 + + # ServiceEncryptedStorage field names for this service + # Those will end up as keys in a json-file + SPECTER_WALLET_ALIAS = "wallet" + + @classmethod + def get_associated_wallet(cls) -> Wallet: + """Get the Specter `Wallet` that is currently associated with this service""" + service_data = cls.get_current_user_service_data() + if not service_data or cls.SPECTER_WALLET_ALIAS not in service_data: + # Service is not initialized; nothing to do + return + try: + return app.specter.wallet_manager.get_by_alias( + service_data[cls.SPECTER_WALLET_ALIAS] + ) + except SpecterError as e: + logger.debug(e) + # Referenced an unknown wallet + # TODO: keep ignoring or remove the unknown wallet from service_data? + return diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/components/tretboot_menu.jinja b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/components/tretboot_menu.jinja new file mode 100644 index 000000000..8e93cf424 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/components/tretboot_menu.jinja @@ -0,0 +1,16 @@ +{% from 'components/menu_item.jinja' import menu_item %} + +{# + vaultoro_menu - Tabs menu to navigate between the vaultoro screens. + Parameters: + - active_menuitem: Current active tab. Options: 'general', 'bitcoin_core', 'auth'. + #} +{% macro tretboot_menu(active_menuitem) -%} + +{%- endmacro %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/components/tretboot_tab.jinja b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/components/tretboot_tab.jinja new file mode 100644 index 000000000..caa04e223 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/components/tretboot_tab.jinja @@ -0,0 +1,8 @@ +{% extends "base.jinja" %} +{% block main %} + + {% from 'tretboot/components/tretboot_menu.jinja' import tretboot_menu with context %} + {{ tretboot_menu(tab) }} + {% block content %} + {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/index.jinja b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/index.jinja new file mode 100644 index 000000000..659644777 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/index.jinja @@ -0,0 +1,87 @@ +{% extends "tretboot/components/tretboot_tab.jinja" %} +{% block title %}Settings{% endblock %} +{% set tab = 'index' %} +{% block content %} + +

+
“Tretbootservice 4thewin.”
+{% endblock %} + + + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/settings.jinja b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/settings.jinja new file mode 100644 index 000000000..ce62ef3a8 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/src/boatacccorp/specterext/tretboot/tretboot/templates/tretboot/settings.jinja @@ -0,0 +1,109 @@ +{% extends "tretboot/components/tretboot_tab.jinja" %} +{% block title %}Settings{% endblock %} +{% set tab = 'settings_get' %} +{% block content %} +
+ + +
+

{{ _("Configure your extension") }}

+
+ +
+ {{ _("- Maybe you want to put some notes for the user") }}
+ {{ _("- One important setting is whether your extension gets a menu-point on the left") }}
+ {{ _("- Let us assume you want the user to use a specific wallet. That should go here. ") }}
+
+
+ +
+ + +
Show Menu Item:
+ +
+
+
+ + {{ _("Choose which wallet should be used:") }}:
+ +
+
+
+ +
+ +
+ +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/tests/conftest.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/tests/conftest.py new file mode 100644 index 000000000..3b444495c --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/tests/conftest.py @@ -0,0 +1,507 @@ +import atexit +import json +import logging +import os +import shutil +import subprocess +import tempfile +import time + +import docker +import pytest +from cryptoadvance.specter.managers.device_manager import DeviceManager +from cryptoadvance.specter.managers.user_manager import UserManager +from cryptoadvance.specter.process_controller.bitcoind_controller import ( + BitcoindPlainController, +) +from cryptoadvance.specter.process_controller.bitcoind_docker_controller import ( + BitcoindDockerController, +) +from cryptoadvance.specter.process_controller.elementsd_controller import ( + ElementsPlainController, +) +from cryptoadvance.specter.rpc import BitcoinRPC +from cryptoadvance.specter.server import SpecterFlask, create_app, init_app +from cryptoadvance.specter.specter import Specter +from cryptoadvance.specter.specter_error import SpecterError +from cryptoadvance.specter.user import User, hash_password +from cryptoadvance.specter.util.wallet_importer import WalletImporter +from cryptoadvance.specter.util.common import str2bool +from cryptoadvance.specter.util.shell import which +import code, traceback, signal + +logger = logging.getLogger(__name__) + +pytest_plugins = ["ghost_machine"] + +# This is from https://stackoverflow.com/questions/132058/showing-the-stack-trace-from-a-running-python-application +# it enables stopping a hanging test via sending the pytest-process a SIGUSR2 (12) +# kill 12 pid-of-pytest +# In the article they claim to open a debug-console which didn't work for me but at least +# you get a stacktrace in the output. +def debug(sig, frame): + """Interrupt running process, and provide a python prompt for + interactive debugging.""" + d = {"_frame": frame} # Allow access to frame object. + d.update(frame.f_globals) # Unless shadowed by global + d.update(frame.f_locals) + + i = code.InteractiveConsole(d) + message = "Signal received : entering python shell.\nTraceback:\n" + message += "".join(traceback.format_stack(frame)) + i.interact(message) + + +def listen(): + signal.signal(signal.SIGUSR2, debug) # Register handler + + +def pytest_addoption(parser): + """Internally called to add options to pytest + see pytest_generate_tests(metafunc) on how to check that + Also used to register the SIGUSR2 (12) as decribed in conftest.py + """ + parser.addoption("--docker", action="store_true", help="run bitcoind in docker") + parser.addoption( + "--bitcoind-version", + action="store", + default="v0.20.1", + help="Version of bitcoind (something which works with git checkout ...)", + ) + parser.addoption( + "--bitcoind-log-stdout", + action="store", + default=False, + help="Whether bitcoind should log to stdout (default:False)", + ) + parser.addoption( + "--elementsd-version", + action="store", + default="master", + help="Version of elementsd (something which works with git checkout ...)", + ) + listen() + + +def pytest_generate_tests(metafunc): + # ToDo: use custom compiled version of bitcoind + # E.g. test again bitcoind version [currentRelease] + master-branch + if "docker" in metafunc.fixturenames: + if metafunc.config.getoption("docker"): + # That's a list because we could do both (see above) but currently that doesn't make sense in that context + metafunc.parametrize("docker", [True], scope="session") + else: + metafunc.parametrize("docker", [False], scope="session") + + +def instantiate_bitcoind_controller(docker, request, rpcport=18543, extra_args=[]): + # logging.getLogger().setLevel(logging.DEBUG) + requested_version = request.config.getoption("--bitcoind-version") + log_stdout = str2bool(request.config.getoption("--bitcoind-log-stdout")) + if docker: + from cryptoadvance.specter.process_controller.bitcoind_docker_controller import ( + BitcoindDockerController, + ) + + bitcoind_controller = BitcoindDockerController( + rpcport=rpcport, docker_tag=requested_version + ) + else: + if os.path.isfile("tests/bitcoin/src/bitcoind"): + bitcoind_controller = BitcoindPlainController( + bitcoind_path="tests/bitcoin/src/bitcoind", rpcport=rpcport + ) # always prefer the self-compiled bitcoind if existing + elif os.path.isfile("tests/bitcoin/bin/bitcoind"): + bitcoind_controller = BitcoindPlainController( + bitcoind_path="tests/bitcoin/bin/bitcoind", rpcport=rpcport + ) # next take the self-installed binary if existing + else: + bitcoind_controller = BitcoindPlainController( + rpcport=rpcport + ) # Alternatively take the one on the path for now + bitcoind_controller.start_bitcoind( + cleanup_at_exit=True, + cleanup_hard=True, + extra_args=extra_args, + log_stdout=log_stdout, + ) + assert not bitcoind_controller.datadir is None + running_version = bitcoind_controller.version() + requested_version = request.config.getoption("--bitcoind-version") + assert running_version == requested_version, ( + "Please make sure that the Bitcoind-version (%s) matches with the version in pytest.ini (%s)" + % (running_version, requested_version) + ) + return bitcoind_controller + + +def instantiate_elementsd_controller(request, rpcport=18643, extra_args=[]): + if os.path.isfile("tests/elements/src/elementsd"): + elementsd_controller = ElementsPlainController( + elementsd_path="tests/elements/src/elementsd", rpcport=rpcport + ) # always prefer the self-compiled bitcoind if existing + elif os.path.isfile("tests/elements/bin/elementsd"): + elementsd_controller = ElementsPlainController( + elementsd_path="tests/elements/bin/elementsd", rpcport=rpcport + ) # next take the self-installed binary if existing + else: + elementsd_controller = ElementsPlainController( + rpcport=rpcport + ) # Alternatively take the one on the path for now + elementsd_controller.start_elementsd( + cleanup_at_exit=True, cleanup_hard=True, extra_args=extra_args + ) + assert not elementsd_controller.datadir is None + running_version = elementsd_controller.version() + requested_version = request.config.getoption("--elementsd-version") + assert running_version == requested_version, ( + "Please make sure that the elementsd-version (%s) matches with the version in pytest.ini (%s)" + % (running_version, requested_version) + ) + return elementsd_controller + + +# Below this point are fixtures. Fixtures have a scope. Check about scopes here: +# https://docs.pytest.org/en/6.2.x/fixture.html#scope-sharing-fixtures-across-classes-modules-packages-or-session +# possible values: function, class, module, package or session. +# The nodes are of scope session. All else is the default (function) + + +@pytest.fixture(scope="session") +def bitcoind_path(): + if os.path.isfile("tests/bitcoin/src/bitcoind"): + return "tests/bitcoin/src/bitcoind" + elif os.path.isfile("tests/bitcoin/bin/bitcoind"): + return "tests/bitcoin/bin/bitcoind" + else: + return which("bitcoind") + + +@pytest.fixture(scope="session") +def bitcoin_regtest(docker, request): + bitcoind_regtest = instantiate_bitcoind_controller(docker, request, extra_args=None) + try: + assert bitcoind_regtest.get_rpc().test_connection() + assert not bitcoind_regtest.datadir is None + yield bitcoind_regtest + finally: + bitcoind_regtest.stop_bitcoind() + + +@pytest.fixture(scope="session") +def elements_elreg(request): + elements_elreg = instantiate_elementsd_controller(request, extra_args=None) + try: + yield elements_elreg + assert not elements_elreg.datadir is None + finally: + elements_elreg.stop_elementsd() + + +@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: + yield data_folder + + +@pytest.fixture +def devices_filled_data_folder(empty_data_folder): + os.makedirs(empty_data_folder + "/devices") + with open(empty_data_folder + "/devices/trezor.json", "w") as text_file: + text_file.write( + """ +{ + "name": "Trezor", + "type": "trezor", + "keys": [ + { + "derivation": "m/49h/0h/0h", + "original": "ypub6XFn7hfb676MLm6ZsAviuQKXeRDNgT9Bs32KpRDPnkKgKDjKcrhYCXJ88aBfy8co2k9eujugJX5nwq7RPG4sj6yncDEPWN9dQGmFWPy4kFB", + "fingerprint": "1ef4e492", + "type": "sh-wpkh", + "xpub": "xpub6CRWp2zfwRYsVTuT2p96hKE2UT4vjq9gwvW732KWQjwoG7v6NCXyaTdz7NE5yDxsd72rAGK7qrjF4YVrfZervsJBjsXxvTL98Yhc7poBk7K" + }, + { + "derivation": "m/84h/0h/0h", + "original": "zpub6rGoJTXEhKw7hUFkjMqNctTzojkzRPa3VFuUWAirqpuj13mRweRmnYpGD1aQVFpxNfp17zVU9r7F6oR3c4zL3DjXHdewVvA7kjugHSqz5au", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "xpub6CcGh8BQPxr9zssX4eG8CiGzToU6Y9b3f2s2wNw65p9xtr8ySL6eYRVzAbfEVSX7ZPaPd3JMEXQ9LEBvAgAJSkNKYxG6L6X9DHnPWNQud4H" + }, + { + "derivation": "m/48h/0h/0h/1h", + "original": "Ypub6jtWQ1r2D7EwqNoxERU28MWZH4WdL3pWdN8guFJRBTmGwstJGzMXJe1VaNZEuAAVsZwpKPhs5GzNPEZR77mmX1mjwzEiouxmQYsrxFBNVNN", + "fingerprint": "1ef4e492", + "type": "sh-wsh", + "xpub": "xpub6EA9y7SfVU96ZWTTTQDR6C5FPJKvB59RPyxoCb8zRgYzGbWAFvogbTVRkTeBLpHgETm2hL7BjQFKNnL66CCoaHyUFBRtpbgHF6YLyi7fr6m" + }, + { + "derivation": "m/48h/0h/0h/2h", + "original": "Zpub74imhgWwMnnRkSPkiNavCQtSBu1fGo8RP96h9eT2GHCgN5eFU9mZVPhGphvGnG26A1cwJxtkmbHR6nLeTw4okpCDjZCEj2HRLJoVHAEsch9", + "fingerprint": "1ef4e492", + "type": "wsh", + "xpub": "xpub6EA9y7SfVU96dGr96zYgxAMd8AgWBCTqEeQafbPi8VcWdhStCS4AA9X4yb3dE1VM7GKLwRhWy4BpD3VkjK5q1riMAQgz9oBSu8QKv5S7KzD" + }, + { + "derivation": "m/49h/1h/0h", + "original": "upub5EKoQv21nQNkhdt4yuLyRnWitA3EGhW1ru1Y8VTG8gdys2JZhqiYkhn4LHp2heHnH41kz95bXPvrYVRuFUrdUMik6YdjFV4uL4EubnesttQ", + "fingerprint": "1ef4e492", + "type": "sh-wpkh", + "xpub": "tpubDDCDr9rSwixeXKeGwAgwFy8bjBaE5wya9sAVqEC4ccXWmcQxY34KmLRJdwmaDsCnHsu5r9P9SUpYtXmCoRwukWDqmAUJgkBbjC2FXUzicn6" + }, + { + "derivation": "m/84h/1h/0h", + "original": "vpub5Y35MNUT8sUR2SnRCU9A9S6z1JDACMTuNnM8WHXvuS7hCwuVuoRAWJGpi66Yo8evGPiecN26oLqx19xf57mqVQjiYb9hbb4QzbNmFfsS9ko", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc" + }, + { + "derivation": "m/48h/1h/0h/1h", + "original": "Upub5Tk9tZtdzVaTGWtygRTKDDmaN5vfB59pn2L5MQyH6BkVpg2Y5J95rtpQndjmXNs3LNFiy8zxpHCTtvxxeePjgipF7moTHQZhe3E5uPzDXh8", + "fingerprint": "1ef4e492", + "type": "sh-wsh", + "xpub": "tpubDFiVCZzdarbyfdVoh2LJDL3eVKRPmxwnkiqN8tSYCLod75a2966anQbjHajqVAZ97j54xZJPr9hf7ogVuNL4pPCfwvXdKGDQ9SjZF7vXQu1" + }, + { + "derivation": "m/48h/1h/0h/2h", + "original": "Vpub5naRCEZZ9B7wCKLWuqoNdg6ddWEx8ruztUygXFZDJtW5LRMqUP5HV2TsNw1nc74Ba3QPDSH7qzauZ8LdfNmnmofpfmztCGPgP7vaaYSmpgN", + "fingerprint": "1ef4e492", + "type": "wsh", + "xpub": "tpubDFiVCZzdarbyk8kE65tjRhHCambEo8iTx4xkXL8b33BKZj66HWsDnUb3rg4GZz6Mwm6vTNyzRCjYtiScCQJ77ENedb2deDDtcoNQXiUouJQ" + } + ] +} +""" + ) + with open(empty_data_folder + "/devices/specter.json", "w") as text_file: + text_file.write( + """ +{ + "name": "Specter", + "type": "specter", + "keys": [ + { + "derivation": "m/48h/1h/0h/2h", + "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", + "fingerprint": "08686ac6", + "type": "wsh", + "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL" + }, + { + "derivation": "m/84h/1h/0h", + "original": "vpub5ZSem3mLXiSJzgDX6pJb2N9L6sJ8m6ejaksLPLSuB53LBzCi2mMsBg19eEUSDkHtyYp75GATjLgt5p3S43WjaVCXAWU9q9H5GhkwJBrMiAb", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUiy4ncDirveTfhmvggdj8nxcW5JgHpGzYz3UVscJY5aEzFvgUPk4YyajadBnsTBmE2YZmAtJC14Q21xncJgVaHQ7UdqMRVRbU" + }, + { + "derivation": "m/84h/1h/1h", + "original": "vpub5ZSem3mLXiSK55jPzfLVhbHbTEwGzEFZv3xrGFCw1vGHSNw7WcVuJXysJLWcgENQd3iXSNQaeSXUBW55Hy4GAjSTjrWP4vpKKkUN9jiU1Tc", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj3UJV7ZtqKgoy8JKprrjdHubbBb3r7qmwHsEH69g7h6xyanWaCYdVEEV3Yu7a6s4ceFnp8DjXeeFxY8eXvH7XTAC4gxfDNEW" + }, + { + "derivation": "m/84h/1h/2h", + "original": "vpub5ZSem3mLXiSK64v64deytnDCoYqbUSYHvmVurUGVMEnXMyEybtF3FEnNuiFDDC6J18a81fv5ptQXaQaaRiYx8MRxahipgxPLdxubpYt1dkD", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj4TVBBYDKWsjaUcE9M52MJd8emp7QTAJBDTY9BRRFdomVCAFAjWMNcKLe8Cd5HJwg3AJKFyEDcGFTNyryYJgYmNdJMhwB2RG" + }, + { + "derivation": "m/84h/1h/3h", + "original": "vpub5ZSem3mLXiSK8cKzh4sHxTvN7mgYQA29HfoAZeCDtX1M2zdejN5XVAtVyqhk8eui18JTtZ9M3VD3AiWCz8VwrybhBUh3HxzS8js3mLVybDT", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj6zu5oyRdaZSjnq56GnWCfXRuUz38zSWztUvpJuFjsjscGHhheyAncK4z15rLVukBdUDwpPBDLtRBykqC9KHeG9akJWRipKK" + } + ] +} +""" + ) + return empty_data_folder # no longer empty, though + + +@pytest.fixture +def wallets_filled_data_folder(devices_filled_data_folder): + os.makedirs(os.path.join(devices_filled_data_folder, "wallets", "regtest")) + with open( + os.path.join(devices_filled_data_folder, "wallets", "regtest", "simple.json"), + "w", + ) as json_file: + json_file.write( + """ +{ + "alias": "simple", + "fullpath": "/home/kim/.specter/wallets/regtest/simple.json", + "name": "Simple", + "address_index": 0, + "keypool": 5, + "address": "bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej", + "change_index": 0, + "change_address": "bcrt1qt28v03278lmmxllys89acddp2p5y4zds94944n", + "change_keypool": 5, + "type": "simple", + "description": "Single (Segwit)", + "keys": [{ + "derivation": "m/84h/1h/0h", + "original": "vpub5Y35MNUT8sUR2SnRCU9A9S6z1JDACMTuNnM8WHXvuS7hCwuVuoRAWJGpi66Yo8evGPiecN26oLqx19xf57mqVQjiYb9hbb4QzbNmFfsS9ko", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc" + }], + "recv_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", + "change_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/1/*)#h4z73prm", + "device": "Trezor", + "device_type": "trezor", + "address_type": "bech32" +} + +""" + ) + return devices_filled_data_folder # and with wallets obviously + + +@pytest.fixture +def device_manager(devices_filled_data_folder): + return DeviceManager(os.path.join(devices_filled_data_folder, "devices")) + + +# @pytest.fixture +# def user_manager(empty_data_folder) -> UserManager: +# """A UserManager having users alice, bob and eve""" +# specter = Specter(data_folder=empty_data_folder) +# user_manager = UserManager(specter=specter) +# config = {} +# user_manager.get_user("admin").decrypt_user_secret("admin") +# user_manager.create_user( +# user_id="alice", +# username="alice", +# plaintext_password="plain_pass_alice", +# config=config, +# ) +# user_manager.create_user( +# user_id="bob", +# username="bob", +# plaintext_password="plain_pass_bob", +# config=config, +# ) +# user_manager.create_user( +# user_id="eve", +# username="eve", +# plaintext_password="plain_pass_eve", +# config=config, +# ) +# return user_manager + + +@pytest.fixture +def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): + assert bitcoin_regtest.get_rpc().test_connection() + config = { + "rpc": { + "autodetect": False, + "datadir": "", + "user": bitcoin_regtest.rpcconn.rpcuser, + "password": bitcoin_regtest.rpcconn.rpcpassword, + "port": bitcoin_regtest.rpcconn.rpcport, + "host": bitcoin_regtest.rpcconn.ipaddress, + "protocol": "http", + }, + "auth": { + "method": "rpcpasswordaspin", + }, + } + specter = Specter(data_folder=devices_filled_data_folder, config=config) + assert specter.chain == "regtest" + # Create a User + someuser = specter.user_manager.add_user( + User.from_json( + user_dict={ + "id": "someuser", + "username": "someuser", + "password": hash_password("somepassword"), + "config": {}, + "is_admin": False, + "services": None, + }, + specter=specter, + ) + ) + specter.user_manager.save() + specter.check() + + assert not someuser.wallet_manager.working_folder is None + + # Create a Wallet + wallet_json = '{"label": "a_simple_wallet", "blockheight": 0, "descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", "devices": [{"type": "trezor", "label": "trezor"}]} ' + wallet_importer = WalletImporter( + wallet_json, specter, device_manager=someuser.device_manager + ) + wallet_importer.create_nonexisting_signers( + someuser.device_manager, + {"unknown_cosigner_0_name": "trezor", "unknown_cosigner_0_type": "trezor"}, + ) + dm: DeviceManager = someuser.device_manager + wallet = wallet_importer.create_wallet(someuser.wallet_manager) + try: + # fund it with some coins + bitcoin_regtest.testcoin_faucet(address=wallet.getnewaddress()) + # make sure it's confirmed + bitcoin_regtest.mine() + # Realize that the wallet has funds: + wallet.update() + except SpecterError as se: + if str(se).startswith("Timeout"): + pytest.fail( + "We got a Bitcoin-RPC timeout while setting up the test, minting some coins. Test Error! Check cpu/mem utilastion and btc/elem logs!" + ) + return + else: + raise se + + assert wallet.fullbalance >= 20 + assert not specter.wallet_manager.working_folder is None + try: + yield specter + finally: + # Deleting all Wallets (this will also purge them on core) + for user in specter.user_manager.users: + for wallet in list(user.wallet_manager.wallets.values()): + user.wallet_manager.delete_wallet( + wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest" + ) + + +@pytest.fixture +def app(specter_regtest_configured) -> SpecterFlask: + """the Flask-App, but uninitialized""" + app = create_app(config="cryptoadvance.specter.config.TestConfig") + app.app_context().push() + app.config["TESTING"] = True + app.testing = True + app.tor_service_id = None + app.tor_enabled = False + init_app(app, specter=specter_regtest_configured) + return app + + +@pytest.fixture +def app_no_node(empty_data_folder) -> SpecterFlask: + specter = Specter(data_folder=empty_data_folder) + app = create_app(config="cryptoadvance.specter.config.TestConfig") + app.app_context().push() + app.config["TESTING"] = True + app.testing = True + app.tor_service_id = None + app.tor_enabled = False + init_app(app, specter=specter) + return app + + +@pytest.fixture +def client(app): + """a test_client from an initialized Flask-App""" + return app.test_client() diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_1/tests/ghost_machine.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/tests/ghost_machine.py new file mode 100644 index 000000000..b1d3b142d --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_1/tests/ghost_machine.py @@ -0,0 +1,67 @@ +import pytest + +# Using https://iancoleman.io/bip39/ and https://jlopp.github.io/xpub-converter/ +# mnemonic = "ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost machine" + + +# m/44'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_44(): + xpub = "xpub6CGap5qbgNCEsvXg2gAjEho17zECMA9PbZa7QkrEWTPnPRaubE6qKots5pNwhyFtuYSPa9gQu4jTTZi8WPaXJhtCHrvHQaFRqayN1saQoWv" + return xpub + + +# m/49'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_49(): + xpub = "xpub6BtcNhqbaFaoC3oEfKky3Sm22pF48U2jmAf78cB3wdAkkGyAgmsVrgyt1ooSt3bHWgzsdUQh2pTJ867yTeUAMmFDKNSBp8J7WPmp7Df7zjv" + return xpub + + +@pytest.fixture +def ghost_machine_ypub(): + ypub = "ypub6WisgNWWiw8H3LzMVgYbFXrXCnPW562EgHBKv14wKdYdoNnPwS34Uke231m2sxFCvL7gNx1FVUor1NjYBLtB9zvpBi8cQ37bn7qTVqo3fjR" + return ypub + + +@pytest.fixture +def ghost_machine_tpub_49(): + tpub = "tpubDC5CZBbVc15fpTeqkyUBKgHqYCqkeaUtPjvGz7RJEttndfcN29psPcxTSj5RNJaWYaRQq8kqovLBrZA2tju3ThSAP9fY1eiSvorchnseFZu" + return tpub + + +@pytest.fixture +def ghost_machine_upub(): + upub = "upub5DCn7wm4SgVmzmtdoi8DVVfxhBJkqL1L6mmKHNgVky1Fj5VyBxV6NzKD957sr5fWXkY5y8THtqSVWWpjLnomBYw4iXpxaPbkXg5Gn6s5tQf" + return upub + + +# m/84'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_84(): + xpub = "xpub6CjsHfiuBnHMPBkxThQ4DDjTw2Qq3VMEVcPBoMBGejZGkj3WQR15LeJLmymPpSzYHX21C8SdFWHgMw2RUBdAQ2Aj4MMS93a68mxPQeS8oHr" + return xpub + + +@pytest.fixture +def ghost_machine_zpub(): + zpub = "zpub6rQPu14jV9NK5n9C8QyJdPvUGxhivjLEKqRdN8y3QkK2rvfxujLCamccpPgZpGJP6oFch5dkApzn8WFYuaTBzVXvo2kHJsD4gE5gBnCBYj1" + return zpub + + +@pytest.fixture +def ghost_machine_tpub_84(): + tpub = "tpubDC4DsqH5rqHqipMNqUbDFtQT3AkKkUrvLsN6miySvortU3s1LGaNVAb7wX2No2VsuxQV82T8s3HJLv3kdx1CPjsJ3onC1Zo5mWCQzRVaWVX" + return tpub + + +@pytest.fixture +def ghost_machine_vpub(): + vpub = "vpub5Y24kG7ZrCFRkRnHia2sdnt5N7MmsrNry1jMrP8XptMEcZZqkjQA6bc1f52RGiEoJmdy1Vk9Qck9tAL1ohKvuq3oFXe3ADVse6UiTHzuyKx" + return vpub diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/MANIFEST.in b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/MANIFEST.in new file mode 100644 index 000000000..6ae80b778 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/MANIFEST.in @@ -0,0 +1,5 @@ +recursive-include src/boatacccorp/specterext/ruderboot/templates * +recursive-include src/boatacccorp/specterext/ruderboot/static * +recursive-include src/boatacccorp/specterext/ruderboot/*/LC_MESSAGES *.mo +recursive-include src/boatacccorp/specterext/ruderboot/translations/*/LC_MESSAGES *.po +include requirements.txt diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/pyproject.toml b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/pyproject.toml new file mode 100644 index 000000000..bacb2fa6e --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +requires = [ + "cryptoadvance.specter==1.8.1" +] +build-backend = "setuptools.build_meta" diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/requirements.txt b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/requirements.txt new file mode 100644 index 000000000..38c6d5f37 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/requirements.txt @@ -0,0 +1 @@ +cryptoadvance.specter==1.8.1 \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/setup.cfg b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/setup.cfg new file mode 100644 index 000000000..8eba638b1 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/setup.cfg @@ -0,0 +1,24 @@ +[metadata] +name = boatacccorp_ruderboot +version = 0.0.1 +author = Your Name +author_email = some_mail@mail.com +description = A small example package +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/boatacccorp/spex-ruderboot +project_urls = + Bug Tracker = https://github.com/pypa/sampleproject/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + +[options] +package_dir = + = src +packages = find_namespace: +python_requires = >=3.6 + +[options.packages.find] +where = src diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/setup.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/setup.py new file mode 100644 index 000000000..606849326 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/__init__.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/controller.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/controller.py new file mode 100644 index 000000000..eef751a87 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/controller.py @@ -0,0 +1,57 @@ +import logging +from flask import redirect, render_template, request, url_for, flash +from flask import current_app as app +from flask_login import login_required, current_user + +from cryptoadvance.specter.services.controller import user_secret_decrypted_required +from cryptoadvance.specter.user import User +from cryptoadvance.specter.wallet import Wallet +from .service import RuderbootService + + +logger = logging.getLogger(__name__) + +ruderboot_endpoint = RuderbootService.blueprint + + +@ruderboot_endpoint.route("/") +@login_required +@user_secret_decrypted_required +def index(): + return render_template( + "ruderboot/index.jinja", + ) + + +@ruderboot_endpoint.route("/settings", methods=["GET"]) +@login_required +@user_secret_decrypted_required +def settings_get(): + associated_wallet: Wallet = RuderbootService.get_associated_wallet() + + # Get the user's Wallet objs, sorted by Wallet.name + wallet_names = sorted(current_user.wallet_manager.wallets.keys()) + wallets = [current_user.wallet_manager.wallets[name] for name in wallet_names] + + return render_template( + "ruderboot/settings.jinja", + associated_wallet=associated_wallet, + wallets=wallets, + cookies=request.cookies, + ) + + +@ruderboot_endpoint.route("/settings", methods=["POST"]) +@login_required +@user_secret_decrypted_required +def settings_post(): + show_menu = request.form["show_menu"] + user = app.specter.user_manager.get_user() + if show_menu == "yes": + user.add_service(RuderbootService.id) + else: + user.remove_service(RuderbootService.id) + used_wallet_alias = request.form.get("used_wallet") + if used_wallet_alias != None: + wallet = current_user.wallet_manager.get_by_alias(used_wallet_alias) + return redirect(url_for(f"{RuderbootService.get_blueprint_name()}.settings_get")) diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/service.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/service.py new file mode 100644 index 000000000..d0e118628 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/service.py @@ -0,0 +1,50 @@ +import logging + +from cryptoadvance.specter.services.service import ( + Service, + devstatus_alpha, + devstatus_prod, +) + +# A SpecterError can be thrown and will be shown to the user as a red banner +from cryptoadvance.specter.specter_error import SpecterError +from flask import current_app as app +from cryptoadvance.specter.wallet import Wallet + +logger = logging.getLogger(__name__) + + +class RuderbootService(Service): + id = "ruderboot" + name = "Ruderboot Service" + icon = "ruderboot/img/ghost.png" + logo = "ruderboot/img/ruderboot_logo.jpeg" + desc = "Where a Ruderboot grows bigger." + has_blueprint = True + blueprint_module = "boatacccorp.specterext.ruderboot.controller" + devstatus = devstatus_alpha + piggyback = False + + # TODO: As more Services are integrated, we'll want more robust categorization and sorting logic + sort_priority = 2 + + # ServiceEncryptedStorage field names for this service + # Those will end up as keys in a json-file + SPECTER_WALLET_ALIAS = "wallet" + + @classmethod + def get_associated_wallet(cls) -> Wallet: + """Get the Specter `Wallet` that is currently associated with this service""" + service_data = cls.get_current_user_service_data() + if not service_data or cls.SPECTER_WALLET_ALIAS not in service_data: + # Service is not initialized; nothing to do + return + try: + return app.specter.wallet_manager.get_by_alias( + service_data[cls.SPECTER_WALLET_ALIAS] + ) + except SpecterError as e: + logger.debug(e) + # Referenced an unknown wallet + # TODO: keep ignoring or remove the unknown wallet from service_data? + return diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/components/ruderboot_menu.jinja b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/components/ruderboot_menu.jinja new file mode 100644 index 000000000..7c228dd19 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/components/ruderboot_menu.jinja @@ -0,0 +1,16 @@ +{% from 'components/menu_item.jinja' import menu_item %} + +{# + vaultoro_menu - Tabs menu to navigate between the vaultoro screens. + Parameters: + - active_menuitem: Current active tab. Options: 'general', 'bitcoin_core', 'auth'. + #} +{% macro ruderboot_menu(active_menuitem) -%} + +{%- endmacro %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/components/ruderboot_tab.jinja b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/components/ruderboot_tab.jinja new file mode 100644 index 000000000..1597909ce --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/components/ruderboot_tab.jinja @@ -0,0 +1,8 @@ +{% extends "base.jinja" %} +{% block main %} + + {% from 'ruderboot/components/ruderboot_menu.jinja' import ruderboot_menu with context %} + {{ ruderboot_menu(tab) }} + {% block content %} + {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/index.jinja b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/index.jinja new file mode 100644 index 000000000..148da9106 --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/index.jinja @@ -0,0 +1,87 @@ +{% extends "ruderboot/components/ruderboot_tab.jinja" %} +{% block title %}Settings{% endblock %} +{% set tab = 'index' %} +{% block content %} + +

+
“Ruderbootservice 4thewin.”
+{% endblock %} + + + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/settings.jinja b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/settings.jinja new file mode 100644 index 000000000..345af826d --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/src/boatacccorp/specterext/ruderboot/templates/ruderboot/settings.jinja @@ -0,0 +1,109 @@ +{% extends "ruderboot/components/ruderboot_tab.jinja" %} +{% block title %}Settings{% endblock %} +{% set tab = 'settings_get' %} +{% block content %} +
+ + +
+

{{ _("Configure your extension") }}

+
+ +
+ {{ _("- Maybe you want to put some notes for the user") }}
+ {{ _("- One important setting is whether your extension gets a menu-point on the left") }}
+ {{ _("- Let us assume you want the user to use a specific wallet. That should go here. ") }}
+
+
+ +
+ + +
Show Menu Item:
+ +
+
+
+ + {{ _("Choose which wallet should be used:") }}:
+ +
+
+
+ +
+ +
+ +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/tests/conftest.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/tests/conftest.py new file mode 100644 index 000000000..3b444495c --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/tests/conftest.py @@ -0,0 +1,507 @@ +import atexit +import json +import logging +import os +import shutil +import subprocess +import tempfile +import time + +import docker +import pytest +from cryptoadvance.specter.managers.device_manager import DeviceManager +from cryptoadvance.specter.managers.user_manager import UserManager +from cryptoadvance.specter.process_controller.bitcoind_controller import ( + BitcoindPlainController, +) +from cryptoadvance.specter.process_controller.bitcoind_docker_controller import ( + BitcoindDockerController, +) +from cryptoadvance.specter.process_controller.elementsd_controller import ( + ElementsPlainController, +) +from cryptoadvance.specter.rpc import BitcoinRPC +from cryptoadvance.specter.server import SpecterFlask, create_app, init_app +from cryptoadvance.specter.specter import Specter +from cryptoadvance.specter.specter_error import SpecterError +from cryptoadvance.specter.user import User, hash_password +from cryptoadvance.specter.util.wallet_importer import WalletImporter +from cryptoadvance.specter.util.common import str2bool +from cryptoadvance.specter.util.shell import which +import code, traceback, signal + +logger = logging.getLogger(__name__) + +pytest_plugins = ["ghost_machine"] + +# This is from https://stackoverflow.com/questions/132058/showing-the-stack-trace-from-a-running-python-application +# it enables stopping a hanging test via sending the pytest-process a SIGUSR2 (12) +# kill 12 pid-of-pytest +# In the article they claim to open a debug-console which didn't work for me but at least +# you get a stacktrace in the output. +def debug(sig, frame): + """Interrupt running process, and provide a python prompt for + interactive debugging.""" + d = {"_frame": frame} # Allow access to frame object. + d.update(frame.f_globals) # Unless shadowed by global + d.update(frame.f_locals) + + i = code.InteractiveConsole(d) + message = "Signal received : entering python shell.\nTraceback:\n" + message += "".join(traceback.format_stack(frame)) + i.interact(message) + + +def listen(): + signal.signal(signal.SIGUSR2, debug) # Register handler + + +def pytest_addoption(parser): + """Internally called to add options to pytest + see pytest_generate_tests(metafunc) on how to check that + Also used to register the SIGUSR2 (12) as decribed in conftest.py + """ + parser.addoption("--docker", action="store_true", help="run bitcoind in docker") + parser.addoption( + "--bitcoind-version", + action="store", + default="v0.20.1", + help="Version of bitcoind (something which works with git checkout ...)", + ) + parser.addoption( + "--bitcoind-log-stdout", + action="store", + default=False, + help="Whether bitcoind should log to stdout (default:False)", + ) + parser.addoption( + "--elementsd-version", + action="store", + default="master", + help="Version of elementsd (something which works with git checkout ...)", + ) + listen() + + +def pytest_generate_tests(metafunc): + # ToDo: use custom compiled version of bitcoind + # E.g. test again bitcoind version [currentRelease] + master-branch + if "docker" in metafunc.fixturenames: + if metafunc.config.getoption("docker"): + # That's a list because we could do both (see above) but currently that doesn't make sense in that context + metafunc.parametrize("docker", [True], scope="session") + else: + metafunc.parametrize("docker", [False], scope="session") + + +def instantiate_bitcoind_controller(docker, request, rpcport=18543, extra_args=[]): + # logging.getLogger().setLevel(logging.DEBUG) + requested_version = request.config.getoption("--bitcoind-version") + log_stdout = str2bool(request.config.getoption("--bitcoind-log-stdout")) + if docker: + from cryptoadvance.specter.process_controller.bitcoind_docker_controller import ( + BitcoindDockerController, + ) + + bitcoind_controller = BitcoindDockerController( + rpcport=rpcport, docker_tag=requested_version + ) + else: + if os.path.isfile("tests/bitcoin/src/bitcoind"): + bitcoind_controller = BitcoindPlainController( + bitcoind_path="tests/bitcoin/src/bitcoind", rpcport=rpcport + ) # always prefer the self-compiled bitcoind if existing + elif os.path.isfile("tests/bitcoin/bin/bitcoind"): + bitcoind_controller = BitcoindPlainController( + bitcoind_path="tests/bitcoin/bin/bitcoind", rpcport=rpcport + ) # next take the self-installed binary if existing + else: + bitcoind_controller = BitcoindPlainController( + rpcport=rpcport + ) # Alternatively take the one on the path for now + bitcoind_controller.start_bitcoind( + cleanup_at_exit=True, + cleanup_hard=True, + extra_args=extra_args, + log_stdout=log_stdout, + ) + assert not bitcoind_controller.datadir is None + running_version = bitcoind_controller.version() + requested_version = request.config.getoption("--bitcoind-version") + assert running_version == requested_version, ( + "Please make sure that the Bitcoind-version (%s) matches with the version in pytest.ini (%s)" + % (running_version, requested_version) + ) + return bitcoind_controller + + +def instantiate_elementsd_controller(request, rpcport=18643, extra_args=[]): + if os.path.isfile("tests/elements/src/elementsd"): + elementsd_controller = ElementsPlainController( + elementsd_path="tests/elements/src/elementsd", rpcport=rpcport + ) # always prefer the self-compiled bitcoind if existing + elif os.path.isfile("tests/elements/bin/elementsd"): + elementsd_controller = ElementsPlainController( + elementsd_path="tests/elements/bin/elementsd", rpcport=rpcport + ) # next take the self-installed binary if existing + else: + elementsd_controller = ElementsPlainController( + rpcport=rpcport + ) # Alternatively take the one on the path for now + elementsd_controller.start_elementsd( + cleanup_at_exit=True, cleanup_hard=True, extra_args=extra_args + ) + assert not elementsd_controller.datadir is None + running_version = elementsd_controller.version() + requested_version = request.config.getoption("--elementsd-version") + assert running_version == requested_version, ( + "Please make sure that the elementsd-version (%s) matches with the version in pytest.ini (%s)" + % (running_version, requested_version) + ) + return elementsd_controller + + +# Below this point are fixtures. Fixtures have a scope. Check about scopes here: +# https://docs.pytest.org/en/6.2.x/fixture.html#scope-sharing-fixtures-across-classes-modules-packages-or-session +# possible values: function, class, module, package or session. +# The nodes are of scope session. All else is the default (function) + + +@pytest.fixture(scope="session") +def bitcoind_path(): + if os.path.isfile("tests/bitcoin/src/bitcoind"): + return "tests/bitcoin/src/bitcoind" + elif os.path.isfile("tests/bitcoin/bin/bitcoind"): + return "tests/bitcoin/bin/bitcoind" + else: + return which("bitcoind") + + +@pytest.fixture(scope="session") +def bitcoin_regtest(docker, request): + bitcoind_regtest = instantiate_bitcoind_controller(docker, request, extra_args=None) + try: + assert bitcoind_regtest.get_rpc().test_connection() + assert not bitcoind_regtest.datadir is None + yield bitcoind_regtest + finally: + bitcoind_regtest.stop_bitcoind() + + +@pytest.fixture(scope="session") +def elements_elreg(request): + elements_elreg = instantiate_elementsd_controller(request, extra_args=None) + try: + yield elements_elreg + assert not elements_elreg.datadir is None + finally: + elements_elreg.stop_elementsd() + + +@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: + yield data_folder + + +@pytest.fixture +def devices_filled_data_folder(empty_data_folder): + os.makedirs(empty_data_folder + "/devices") + with open(empty_data_folder + "/devices/trezor.json", "w") as text_file: + text_file.write( + """ +{ + "name": "Trezor", + "type": "trezor", + "keys": [ + { + "derivation": "m/49h/0h/0h", + "original": "ypub6XFn7hfb676MLm6ZsAviuQKXeRDNgT9Bs32KpRDPnkKgKDjKcrhYCXJ88aBfy8co2k9eujugJX5nwq7RPG4sj6yncDEPWN9dQGmFWPy4kFB", + "fingerprint": "1ef4e492", + "type": "sh-wpkh", + "xpub": "xpub6CRWp2zfwRYsVTuT2p96hKE2UT4vjq9gwvW732KWQjwoG7v6NCXyaTdz7NE5yDxsd72rAGK7qrjF4YVrfZervsJBjsXxvTL98Yhc7poBk7K" + }, + { + "derivation": "m/84h/0h/0h", + "original": "zpub6rGoJTXEhKw7hUFkjMqNctTzojkzRPa3VFuUWAirqpuj13mRweRmnYpGD1aQVFpxNfp17zVU9r7F6oR3c4zL3DjXHdewVvA7kjugHSqz5au", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "xpub6CcGh8BQPxr9zssX4eG8CiGzToU6Y9b3f2s2wNw65p9xtr8ySL6eYRVzAbfEVSX7ZPaPd3JMEXQ9LEBvAgAJSkNKYxG6L6X9DHnPWNQud4H" + }, + { + "derivation": "m/48h/0h/0h/1h", + "original": "Ypub6jtWQ1r2D7EwqNoxERU28MWZH4WdL3pWdN8guFJRBTmGwstJGzMXJe1VaNZEuAAVsZwpKPhs5GzNPEZR77mmX1mjwzEiouxmQYsrxFBNVNN", + "fingerprint": "1ef4e492", + "type": "sh-wsh", + "xpub": "xpub6EA9y7SfVU96ZWTTTQDR6C5FPJKvB59RPyxoCb8zRgYzGbWAFvogbTVRkTeBLpHgETm2hL7BjQFKNnL66CCoaHyUFBRtpbgHF6YLyi7fr6m" + }, + { + "derivation": "m/48h/0h/0h/2h", + "original": "Zpub74imhgWwMnnRkSPkiNavCQtSBu1fGo8RP96h9eT2GHCgN5eFU9mZVPhGphvGnG26A1cwJxtkmbHR6nLeTw4okpCDjZCEj2HRLJoVHAEsch9", + "fingerprint": "1ef4e492", + "type": "wsh", + "xpub": "xpub6EA9y7SfVU96dGr96zYgxAMd8AgWBCTqEeQafbPi8VcWdhStCS4AA9X4yb3dE1VM7GKLwRhWy4BpD3VkjK5q1riMAQgz9oBSu8QKv5S7KzD" + }, + { + "derivation": "m/49h/1h/0h", + "original": "upub5EKoQv21nQNkhdt4yuLyRnWitA3EGhW1ru1Y8VTG8gdys2JZhqiYkhn4LHp2heHnH41kz95bXPvrYVRuFUrdUMik6YdjFV4uL4EubnesttQ", + "fingerprint": "1ef4e492", + "type": "sh-wpkh", + "xpub": "tpubDDCDr9rSwixeXKeGwAgwFy8bjBaE5wya9sAVqEC4ccXWmcQxY34KmLRJdwmaDsCnHsu5r9P9SUpYtXmCoRwukWDqmAUJgkBbjC2FXUzicn6" + }, + { + "derivation": "m/84h/1h/0h", + "original": "vpub5Y35MNUT8sUR2SnRCU9A9S6z1JDACMTuNnM8WHXvuS7hCwuVuoRAWJGpi66Yo8evGPiecN26oLqx19xf57mqVQjiYb9hbb4QzbNmFfsS9ko", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc" + }, + { + "derivation": "m/48h/1h/0h/1h", + "original": "Upub5Tk9tZtdzVaTGWtygRTKDDmaN5vfB59pn2L5MQyH6BkVpg2Y5J95rtpQndjmXNs3LNFiy8zxpHCTtvxxeePjgipF7moTHQZhe3E5uPzDXh8", + "fingerprint": "1ef4e492", + "type": "sh-wsh", + "xpub": "tpubDFiVCZzdarbyfdVoh2LJDL3eVKRPmxwnkiqN8tSYCLod75a2966anQbjHajqVAZ97j54xZJPr9hf7ogVuNL4pPCfwvXdKGDQ9SjZF7vXQu1" + }, + { + "derivation": "m/48h/1h/0h/2h", + "original": "Vpub5naRCEZZ9B7wCKLWuqoNdg6ddWEx8ruztUygXFZDJtW5LRMqUP5HV2TsNw1nc74Ba3QPDSH7qzauZ8LdfNmnmofpfmztCGPgP7vaaYSmpgN", + "fingerprint": "1ef4e492", + "type": "wsh", + "xpub": "tpubDFiVCZzdarbyk8kE65tjRhHCambEo8iTx4xkXL8b33BKZj66HWsDnUb3rg4GZz6Mwm6vTNyzRCjYtiScCQJ77ENedb2deDDtcoNQXiUouJQ" + } + ] +} +""" + ) + with open(empty_data_folder + "/devices/specter.json", "w") as text_file: + text_file.write( + """ +{ + "name": "Specter", + "type": "specter", + "keys": [ + { + "derivation": "m/48h/1h/0h/2h", + "original": "Vpub5n9kKePTPPGtw3RddeJWJe29epEyBBcoHbbPi5HhpoG2kTVsSCUzsad33RJUt3LktEUUPPofcZczuudnwR7ZgkAkT6N2K2Z7wdyjYrVAkXM", + "fingerprint": "08686ac6", + "type": "wsh", + "xpub": "tpubDFHpKypXq4kwUrqLotPs6fCic5bFqTRGMBaTi9s5YwwGymE8FLGwB2kDXALxqvNwFxB1dLWYBmmeFVjmUSdt2AsaQuPmkyPLBKRZW8BGCiL" + }, + { + "derivation": "m/84h/1h/0h", + "original": "vpub5ZSem3mLXiSJzgDX6pJb2N9L6sJ8m6ejaksLPLSuB53LBzCi2mMsBg19eEUSDkHtyYp75GATjLgt5p3S43WjaVCXAWU9q9H5GhkwJBrMiAb", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUiy4ncDirveTfhmvggdj8nxcW5JgHpGzYz3UVscJY5aEzFvgUPk4YyajadBnsTBmE2YZmAtJC14Q21xncJgVaHQ7UdqMRVRbU" + }, + { + "derivation": "m/84h/1h/1h", + "original": "vpub5ZSem3mLXiSK55jPzfLVhbHbTEwGzEFZv3xrGFCw1vGHSNw7WcVuJXysJLWcgENQd3iXSNQaeSXUBW55Hy4GAjSTjrWP4vpKKkUN9jiU1Tc", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj3UJV7ZtqKgoy8JKprrjdHubbBb3r7qmwHsEH69g7h6xyanWaCYdVEEV3Yu7a6s4ceFnp8DjXeeFxY8eXvH7XTAC4gxfDNEW" + }, + { + "derivation": "m/84h/1h/2h", + "original": "vpub5ZSem3mLXiSK64v64deytnDCoYqbUSYHvmVurUGVMEnXMyEybtF3FEnNuiFDDC6J18a81fv5ptQXaQaaRiYx8MRxahipgxPLdxubpYt1dkD", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj4TVBBYDKWsjaUcE9M52MJd8emp7QTAJBDTY9BRRFdomVCAFAjWMNcKLe8Cd5HJwg3AJKFyEDcGFTNyryYJgYmNdJMhwB2RG" + }, + { + "derivation": "m/84h/1h/3h", + "original": "vpub5ZSem3mLXiSK8cKzh4sHxTvN7mgYQA29HfoAZeCDtX1M2zdejN5XVAtVyqhk8eui18JTtZ9M3VD3AiWCz8VwrybhBUh3HxzS8js3mLVybDT", + "fingerprint": "08686ac6", + "type": "wpkh", + "xpub": "tpubDDUotcvrYMUj6zu5oyRdaZSjnq56GnWCfXRuUz38zSWztUvpJuFjsjscGHhheyAncK4z15rLVukBdUDwpPBDLtRBykqC9KHeG9akJWRipKK" + } + ] +} +""" + ) + return empty_data_folder # no longer empty, though + + +@pytest.fixture +def wallets_filled_data_folder(devices_filled_data_folder): + os.makedirs(os.path.join(devices_filled_data_folder, "wallets", "regtest")) + with open( + os.path.join(devices_filled_data_folder, "wallets", "regtest", "simple.json"), + "w", + ) as json_file: + json_file.write( + """ +{ + "alias": "simple", + "fullpath": "/home/kim/.specter/wallets/regtest/simple.json", + "name": "Simple", + "address_index": 0, + "keypool": 5, + "address": "bcrt1qcatuhg0gll3h7py4cmn53rjjn9xlsqfwj3zcej", + "change_index": 0, + "change_address": "bcrt1qt28v03278lmmxllys89acddp2p5y4zds94944n", + "change_keypool": 5, + "type": "simple", + "description": "Single (Segwit)", + "keys": [{ + "derivation": "m/84h/1h/0h", + "original": "vpub5Y35MNUT8sUR2SnRCU9A9S6z1JDACMTuNnM8WHXvuS7hCwuVuoRAWJGpi66Yo8evGPiecN26oLqx19xf57mqVQjiYb9hbb4QzbNmFfsS9ko", + "fingerprint": "1ef4e492", + "type": "wpkh", + "xpub": "tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc" + }], + "recv_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", + "change_descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/1/*)#h4z73prm", + "device": "Trezor", + "device_type": "trezor", + "address_type": "bech32" +} + +""" + ) + return devices_filled_data_folder # and with wallets obviously + + +@pytest.fixture +def device_manager(devices_filled_data_folder): + return DeviceManager(os.path.join(devices_filled_data_folder, "devices")) + + +# @pytest.fixture +# def user_manager(empty_data_folder) -> UserManager: +# """A UserManager having users alice, bob and eve""" +# specter = Specter(data_folder=empty_data_folder) +# user_manager = UserManager(specter=specter) +# config = {} +# user_manager.get_user("admin").decrypt_user_secret("admin") +# user_manager.create_user( +# user_id="alice", +# username="alice", +# plaintext_password="plain_pass_alice", +# config=config, +# ) +# user_manager.create_user( +# user_id="bob", +# username="bob", +# plaintext_password="plain_pass_bob", +# config=config, +# ) +# user_manager.create_user( +# user_id="eve", +# username="eve", +# plaintext_password="plain_pass_eve", +# config=config, +# ) +# return user_manager + + +@pytest.fixture +def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): + assert bitcoin_regtest.get_rpc().test_connection() + config = { + "rpc": { + "autodetect": False, + "datadir": "", + "user": bitcoin_regtest.rpcconn.rpcuser, + "password": bitcoin_regtest.rpcconn.rpcpassword, + "port": bitcoin_regtest.rpcconn.rpcport, + "host": bitcoin_regtest.rpcconn.ipaddress, + "protocol": "http", + }, + "auth": { + "method": "rpcpasswordaspin", + }, + } + specter = Specter(data_folder=devices_filled_data_folder, config=config) + assert specter.chain == "regtest" + # Create a User + someuser = specter.user_manager.add_user( + User.from_json( + user_dict={ + "id": "someuser", + "username": "someuser", + "password": hash_password("somepassword"), + "config": {}, + "is_admin": False, + "services": None, + }, + specter=specter, + ) + ) + specter.user_manager.save() + specter.check() + + assert not someuser.wallet_manager.working_folder is None + + # Create a Wallet + wallet_json = '{"label": "a_simple_wallet", "blockheight": 0, "descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", "devices": [{"type": "trezor", "label": "trezor"}]} ' + wallet_importer = WalletImporter( + wallet_json, specter, device_manager=someuser.device_manager + ) + wallet_importer.create_nonexisting_signers( + someuser.device_manager, + {"unknown_cosigner_0_name": "trezor", "unknown_cosigner_0_type": "trezor"}, + ) + dm: DeviceManager = someuser.device_manager + wallet = wallet_importer.create_wallet(someuser.wallet_manager) + try: + # fund it with some coins + bitcoin_regtest.testcoin_faucet(address=wallet.getnewaddress()) + # make sure it's confirmed + bitcoin_regtest.mine() + # Realize that the wallet has funds: + wallet.update() + except SpecterError as se: + if str(se).startswith("Timeout"): + pytest.fail( + "We got a Bitcoin-RPC timeout while setting up the test, minting some coins. Test Error! Check cpu/mem utilastion and btc/elem logs!" + ) + return + else: + raise se + + assert wallet.fullbalance >= 20 + assert not specter.wallet_manager.working_folder is None + try: + yield specter + finally: + # Deleting all Wallets (this will also purge them on core) + for user in specter.user_manager.users: + for wallet in list(user.wallet_manager.wallets.values()): + user.wallet_manager.delete_wallet( + wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest" + ) + + +@pytest.fixture +def app(specter_regtest_configured) -> SpecterFlask: + """the Flask-App, but uninitialized""" + app = create_app(config="cryptoadvance.specter.config.TestConfig") + app.app_context().push() + app.config["TESTING"] = True + app.testing = True + app.tor_service_id = None + app.tor_enabled = False + init_app(app, specter=specter_regtest_configured) + return app + + +@pytest.fixture +def app_no_node(empty_data_folder) -> SpecterFlask: + specter = Specter(data_folder=empty_data_folder) + app = create_app(config="cryptoadvance.specter.config.TestConfig") + app.app_context().push() + app.config["TESTING"] = True + app.testing = True + app.tor_service_id = None + app.tor_enabled = False + init_app(app, specter=specter) + return app + + +@pytest.fixture +def client(app): + """a test_client from an initialized Flask-App""" + return app.test_client() diff --git a/tests/xtestdata_testextensions/ext_root_fully_qualified_2/tests/ghost_machine.py b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/tests/ghost_machine.py new file mode 100644 index 000000000..b1d3b142d --- /dev/null +++ b/tests/xtestdata_testextensions/ext_root_fully_qualified_2/tests/ghost_machine.py @@ -0,0 +1,67 @@ +import pytest + +# Using https://iancoleman.io/bip39/ and https://jlopp.github.io/xpub-converter/ +# mnemonic = "ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost machine" + + +# m/44'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_44(): + xpub = "xpub6CGap5qbgNCEsvXg2gAjEho17zECMA9PbZa7QkrEWTPnPRaubE6qKots5pNwhyFtuYSPa9gQu4jTTZi8WPaXJhtCHrvHQaFRqayN1saQoWv" + return xpub + + +# m/49'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_49(): + xpub = "xpub6BtcNhqbaFaoC3oEfKky3Sm22pF48U2jmAf78cB3wdAkkGyAgmsVrgyt1ooSt3bHWgzsdUQh2pTJ867yTeUAMmFDKNSBp8J7WPmp7Df7zjv" + return xpub + + +@pytest.fixture +def ghost_machine_ypub(): + ypub = "ypub6WisgNWWiw8H3LzMVgYbFXrXCnPW562EgHBKv14wKdYdoNnPwS34Uke231m2sxFCvL7gNx1FVUor1NjYBLtB9zvpBi8cQ37bn7qTVqo3fjR" + return ypub + + +@pytest.fixture +def ghost_machine_tpub_49(): + tpub = "tpubDC5CZBbVc15fpTeqkyUBKgHqYCqkeaUtPjvGz7RJEttndfcN29psPcxTSj5RNJaWYaRQq8kqovLBrZA2tju3ThSAP9fY1eiSvorchnseFZu" + return tpub + + +@pytest.fixture +def ghost_machine_upub(): + upub = "upub5DCn7wm4SgVmzmtdoi8DVVfxhBJkqL1L6mmKHNgVky1Fj5VyBxV6NzKD957sr5fWXkY5y8THtqSVWWpjLnomBYw4iXpxaPbkXg5Gn6s5tQf" + return upub + + +# m/84'/0'/0' + + +@pytest.fixture +def ghost_machine_xpub_84(): + xpub = "xpub6CjsHfiuBnHMPBkxThQ4DDjTw2Qq3VMEVcPBoMBGejZGkj3WQR15LeJLmymPpSzYHX21C8SdFWHgMw2RUBdAQ2Aj4MMS93a68mxPQeS8oHr" + return xpub + + +@pytest.fixture +def ghost_machine_zpub(): + zpub = "zpub6rQPu14jV9NK5n9C8QyJdPvUGxhivjLEKqRdN8y3QkK2rvfxujLCamccpPgZpGJP6oFch5dkApzn8WFYuaTBzVXvo2kHJsD4gE5gBnCBYj1" + return zpub + + +@pytest.fixture +def ghost_machine_tpub_84(): + tpub = "tpubDC4DsqH5rqHqipMNqUbDFtQT3AkKkUrvLsN6miySvortU3s1LGaNVAb7wX2No2VsuxQV82T8s3HJLv3kdx1CPjsJ3onC1Zo5mWCQzRVaWVX" + return tpub + + +@pytest.fixture +def ghost_machine_vpub(): + vpub = "vpub5Y24kG7ZrCFRkRnHia2sdnt5N7MmsrNry1jMrP8XptMEcZZqkjQA6bc1f52RGiEoJmdy1Vk9Qck9tAL1ohKvuq3oFXe3ADVse6UiTHzuyKx" + return vpub diff --git a/utils/test-cypress.sh b/utils/test-cypress.sh index a6659362d..5602a167d 100755 --- a/utils/test-cypress.sh +++ b/utils/test-cypress.sh @@ -263,15 +263,15 @@ function restore_snapshot { rm -rf ${BTCD_REGTEST_DATA_DIR} mkdir ${BTCD_REGTEST_DATA_DIR} - echo "--> Unpacking ./cypress/fixtures/${spec_file}_btcdir.tar.gz ... " + echo "--> Unpacking ./cypress/fixtures/${spec_file}_btcdir.tar.gz ... into ${BTCD_REGTEST_DATA_DIR}" tar -xzf ./cypress/fixtures/${spec_file}_btcdir.tar.gz -C ${BTCD_REGTEST_DATA_DIR} --strip-components=1 rm -rf ${ELMD_REGTEST_DATA_DIR} mkdir ${ELMD_REGTEST_DATA_DIR} - echo "--> Unpacking ./cypress/fixtures/${spec_file}_elmdir.tar.gz ... " + echo "--> Unpacking ./cypress/fixtures/${spec_file}_elmdir.tar.gz into ${ELMD_REGTEST_DATA_DIR} ..." tar -xzf ./cypress/fixtures/${spec_file}_elmdir.tar.gz -C ${ELMD_REGTEST_DATA_DIR} --strip-components=1 - echo "--> Unpacking ./cypress/fixtures/${spec_file}_specterdir.tar.gz ... " + echo "--> Unpacking ./cypress/fixtures/${spec_file}_specterdir.tar.gz ... into ${SPECTER_DATA_FOLDER} ..." rm -rf $SPECTER_DATA_FOLDER mkdir $SPECTER_DATA_FOLDER tar -xzf ./cypress/fixtures/${spec_file}_specterdir.tar.gz -C $SPECTER_DATA_FOLDER --strip-components=1 @@ -305,8 +305,8 @@ function sub_open { spec_file=$1 if [ -n "${spec_file}" ]; then restore_snapshot ${spec_file} - start_bitcoind --cleanuphard --reset - start_elementsd --cleanuphard --reset + start_bitcoind + start_elementsd start_specter else start_bitcoind --reset @@ -321,8 +321,8 @@ function sub_run { spec_file=$1 if [ -f ./cypress/integration/${spec_file} ]; then restore_snapshot ${spec_file} - start_bitcoind --cleanuphard --reset - start_elementsd --cleanuphard --reset + start_bitcoind + start_elementsd start_specter # Run $spec_file and all of the others coming later! #$(npm bin)/cypress run --spec $(./utils/calc_cypress_test_spec.py --run $spec_file)