mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
Feature: CLI and a publishing model for extensions (#1566)
* introduction of url-prefixes and defaults * starting a cli for extensions * more verbose * extension-test-data * logger improvements * bugfixes and testing * adding clasification * rename spext -> specterext * distinguish isolated_client and refactoring * improved error-message * better error_management * fix tests * ext gen completion * fix endpoint issue * added .gitignore and txs * formatting * very basic callback-infra * beauty * fix tests, tidy up, follow-links * proper mocking for extgen * adding a test for plugins * fix tests + docs * error-handling for non existing extensions * extensiongen config and making config work for new style * avoid pollition * ups * kick * kick * proper renaming of spext * fix tests * ToDo remark * fix test finally * fix the fixing fixing test * documentation * revert mounting for all config see #1595 * fix tests * fix cypress-test * prevent pollution * revert address-data to fix test * added ID * bugfix for test-cypress
This commit is contained in:
parent
22042f4b12
commit
b73e398c36
70 changed files with 3659 additions and 154 deletions
|
|
@ -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"
|
||||
],
|
||||
|
|
|
|||
41
cypress/integration/spec_plugins.js
Normal file
41
cypress/integration/spec_plugins.js
Normal file
|
|
@ -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()
|
||||
})
|
||||
|
||||
})
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 `<username>_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: `<wallet_alias>_<service>.json`).
|
||||
|
||||
|
|
@ -101,7 +120,7 @@ cryptoadvance.specter.services.swan
|
|||
|
||||
Each implementation must have the following required components:
|
||||
```
|
||||
/static
|
||||
/static/<service_id>
|
||||
/templates/<service_id>
|
||||
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//<service_id>/img/blah.png") as if they were in the main `/static` files root dir.
|
||||
|
||||
### `/templates/<service_id>`
|
||||
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`)
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
173
src/cryptoadvance/specter/cli/cli_ext.py
Normal file
173
src/cryptoadvance/specter/cli/cli_ext.py
Normal file
|
|
@ -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
|
||||
|
||||
"""
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
from ..specter_error import SpecterError
|
||||
|
||||
|
||||
class ExtensionException(SpecterError):
|
||||
pass
|
||||
15
src/cryptoadvance/specter/services/callbacks.py
Normal file
15
src/cryptoadvance/specter/services/callbacks.py
Normal file
|
|
@ -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"
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
215
src/cryptoadvance/specter/services/extension_gen.py
Normal file
215
src/cryptoadvance/specter/services/extension_gen.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = "<br>"
|
||||
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 += `<img class="service-icon" src='{{ext_url_prefix}}/${this.serviceId}/static/${services[this.serviceId].icon}'>${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 += `<button type="button" class="btn" onclick="location.href='${associateServiceUrl}';">{{ _("Associate with a service") }}</button>`
|
||||
addressInfoHTML += `<button type="button" id="associate-btn" class="btn" onclick="location.href='${associateServiceUrl}';">{{ _("Associate with a service") }}</button>`
|
||||
}
|
||||
addressInfoHTML += `
|
||||
</td></tr>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<br>
|
||||
<br>
|
||||
<div class="row">
|
||||
<button type="submit" class="btn">{{ _("Login") }}</button>
|
||||
<button type="submit" id="login-btn" class="btn">{{ _("Login") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<button type="submit" class="btn" name="action" value="save">{{ _("Save") }}</button>
|
||||
<button type="submit" id="submit-btn" class="btn" name="action" value="save">{{ _("Save") }}</button>
|
||||
</div>
|
||||
{% if method == "usernamepassword" and current_user.is_admin %}
|
||||
<br>
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@
|
|||
<img style="width: 100px;" src="{{ url_for('static', filename='img/party.png') }}"/><br><br>
|
||||
<h1 style="font-size: 1.8em;">{{ _("Setup Completed Successfully!") }}</h1>
|
||||
<p>{{ _("Your new node is up and running!") }}</p><br>
|
||||
<a href="{{url_for('about')}}" class="btn wizard-btn action" id="finish-setup-btn">{{ _("Done") }}</a>
|
||||
<a href="{{url_for('welcome_endpoint.about')}}" class="btn wizard-btn action" id="finish-setup-btn">{{ _("Done") }}</a>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -17,3 +17,7 @@ def camelcase2snake_case(name):
|
|||
pattern = re.compile(r"(?<!^)(?=[A-Z])")
|
||||
name = pattern.sub("_", name).lower()
|
||||
return name
|
||||
|
||||
|
||||
def snake_case2camelcase(word):
|
||||
return "".join(x.capitalize() or "_" for x in word.split("_"))
|
||||
|
|
|
|||
|
|
@ -6,9 +6,12 @@ from pathlib import Path
|
|||
import pkgutil
|
||||
from pkgutil import iter_modules
|
||||
import sys
|
||||
from typing import List
|
||||
from .common import camelcase2snake_case
|
||||
from ..specter_error import SpecterError
|
||||
|
||||
from .reflection_fs import detect_extension_style_in_cwd, search_dirs_in_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -60,6 +63,9 @@ def get_package_dir_for_subclasses_of(clazz):
|
|||
raise SpecterError("Unknown Class: {clazz}")
|
||||
|
||||
|
||||
# --------------- static discovery ------------------------------
|
||||
|
||||
|
||||
def get_classlist_of_type_clazz_from_modulelist(clazz, modulelist):
|
||||
"""A helper method converting a List of modules as described in config.py
|
||||
into a List of classes. In order to make that more util-like, you
|
||||
|
|
@ -76,28 +82,47 @@ def get_classlist_of_type_clazz_from_modulelist(clazz, modulelist):
|
|||
issubclass(attribute, clazz)
|
||||
and not attribute.__name__ == clazz.__name__
|
||||
):
|
||||
logger.debug(f"Adding {attribute} to {class_list}")
|
||||
class_list.append(attribute)
|
||||
logger.info(f" Found class {attribute.__name__}")
|
||||
return class_list
|
||||
|
||||
|
||||
def get_subclasses_for_clazz_in_cwd(clazz):
|
||||
def get_subclasses_for_clazz_in_cwd(clazz, cwd=".") -> 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}"
|
||||
|
|
|
|||
70
src/cryptoadvance/specter/util/reflection_fs.py
Normal file
70
src/cryptoadvance/specter/util/reflection_fs.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
25
tests/test_services_extension_gen.py
Normal file
25
tests/test_services_extension_gen.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
62
tests/test_util_reflection_fs.py
Normal file
62
tests/test_util_reflection_fs.py
Normal file
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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"))
|
||||
|
|
@ -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
|
||||
|
|
@ -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) -%}
|
||||
<nav class="row collapse-on-mobile">
|
||||
{{ menu_item(service.id, 'index', 'Main', active_menuitem, isLeft=true) }}
|
||||
{{ menu_item(service.id, 'settings_get', 'Settings', active_menuitem, isRight=true) }}
|
||||
<a href="javascript:void(0);" class="mobile-nav-icon" onclick="toggleMobileNav(this, `{{ url_for('static', filename='img/expand-more.svg') }}`, `{{ url_for('static', filename='img/expand-less.svg') }}`)">
|
||||
<img style="width: 36px;" src="{{ url_for('static', filename='img/expand-more.svg') }}"/>
|
||||
</a>
|
||||
</nav>
|
||||
{%- endmacro %}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{% extends "base.jinja" %}
|
||||
{% block main %}
|
||||
<img src="{{ url_for(service.id +'_endpoint' + '.static', filename=service.logo) }}" width="300"/>
|
||||
{% from 'breznguat/components/breznguat_menu.jinja' import breznguat_menu with context %}
|
||||
{{ breznguat_menu(tab) }}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
{% extends "breznguat/components/breznguat_tab.jinja" %}
|
||||
{% block title %}Settings{% endblock %}
|
||||
{% set tab = 'index' %}
|
||||
{% block content %}
|
||||
<style>
|
||||
.tagline {
|
||||
font-style: italic;
|
||||
font-size: 1.1em;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
padding-left: 4em;
|
||||
padding-right: 4em;
|
||||
}
|
||||
|
||||
.button_wide {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
.button_wide:hover {
|
||||
color: white;
|
||||
}
|
||||
.big_option {
|
||||
display: inline-block;
|
||||
width: 12em;
|
||||
padding: 1em;
|
||||
vertical-align: middle;
|
||||
height: 3.5em;
|
||||
border: 4px solid var(--cmap-border);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--cmap-bg-lighter);
|
||||
font-size: 1.5em;
|
||||
margin: 1em;
|
||||
position: relative;
|
||||
}
|
||||
.big_option:hover {
|
||||
background-color: var(--cmap-bg-lightest);
|
||||
border: 4px solid var(--main-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.big_option_text {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.bonus {
|
||||
color: #999;
|
||||
font-size: 0.7em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlights {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.show_list_bullet {
|
||||
margin-top: 0.5em;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
.show_list_bullet li {
|
||||
text-align: left;
|
||||
list-style-type: disc;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
|
||||
.plus_sign {
|
||||
font-size: 3em;
|
||||
color: #ccc;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
</style>
|
||||
<br><br>
|
||||
<div class="tagline">“Breznguatservice 4thewin.”</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
{% extends "breznguat/components/breznguat_tab.jinja" %}
|
||||
{% block title %}Settings{% endblock %}
|
||||
{% set tab = 'settings_get' %}
|
||||
{% block content %}
|
||||
<br/>
|
||||
<style>
|
||||
.big_option {
|
||||
display: inline-block;
|
||||
width: 14em;
|
||||
padding: 1em;
|
||||
vertical-align: middle;
|
||||
height: 3.5em;
|
||||
border: 4px solid var(--cmap-border);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--cmap-bg-lighter);
|
||||
font-size: 1.5em;
|
||||
margin: 1em;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
.big_option:hover {
|
||||
background-color: var(--cmap-bg-lightest);
|
||||
border: 4px solid var(--main-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.big_option_text {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.instructions {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
{# TODO: End Remove #}
|
||||
|
||||
.css-1rbd7t8 {
|
||||
box-sizing: border-box;
|
||||
margin: 60px 0px 24px;
|
||||
min-width: 0px;
|
||||
}
|
||||
.css-yn7azs {
|
||||
box-sizing: border-box;
|
||||
margin: 0px;
|
||||
min-width: 0px;
|
||||
-moz-box-pack: justify;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
font-family: "Source Serif Pro", serif;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.02em;
|
||||
font-variant: common-ligatures;
|
||||
text-rendering: optimizelegibility;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<h1>{{ _("Configure your extension") }}</h1>
|
||||
<br>
|
||||
|
||||
<div class="note">
|
||||
{{ _("- Maybe you want to put some notes for the user") }}<br/>
|
||||
{{ _("- One important setting is whether your extension gets a menu-point on the left") }}<br/>
|
||||
{{ _("- Let us assume you want the user to use a specific wallet. That should go here. ") }}<br/>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<form action="{{ url_for(service.get_blueprint_name() + '.settings_post') }}" method="POST" role="form">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div>Show Menu Item:</div>
|
||||
<select name="show_menu">
|
||||
<option value="yes" {% if show_menu == 'yes' %}selected{% endif %}>Yes</option>
|
||||
<option value="no" {% if show_menu == 'no' %}selected{% endif %}>No</option>
|
||||
</select>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
{{ _("Choose which wallet should be used:") }}:<br>
|
||||
<select name="used_wallet">
|
||||
{% for wallet in wallets %}
|
||||
<option value="{{ wallet.alias }}" {% if associated_wallet == wallet %}selected{% endif %}>{{ wallet.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<button type="submit" class="btn">{{ _("Save") }}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1 @@
|
|||
cryptoadvance.specter==1.8.1
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
[build-system]
|
||||
requires = [
|
||||
"cryptoadvance.specter==1.8.1"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
|
@ -0,0 +1 @@
|
|||
cryptoadvance.specter==1.8.1
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
|
|
@ -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"))
|
||||
|
|
@ -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
|
||||
|
|
@ -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) -%}
|
||||
<nav class="row collapse-on-mobile">
|
||||
{{ menu_item(service.id, 'index', 'Main', active_menuitem, isLeft=true) }}
|
||||
{{ menu_item(service.id, 'settings_get', 'Settings', active_menuitem, isRight=true) }}
|
||||
<a href="javascript:void(0);" class="mobile-nav-icon" onclick="toggleMobileNav(this, `{{ url_for('static', filename='img/expand-more.svg') }}`, `{{ url_for('static', filename='img/expand-less.svg') }}`)">
|
||||
<img style="width: 36px;" src="{{ url_for('static', filename='img/expand-more.svg') }}"/>
|
||||
</a>
|
||||
</nav>
|
||||
{%- endmacro %}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{% extends "base.jinja" %}
|
||||
{% block main %}
|
||||
<img src="{{ url_for(service.id +'_endpoint' + '.static', filename=service.logo) }}" width="300"/>
|
||||
{% from 'tretboot/components/tretboot_menu.jinja' import tretboot_menu with context %}
|
||||
{{ tretboot_menu(tab) }}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
{% extends "tretboot/components/tretboot_tab.jinja" %}
|
||||
{% block title %}Settings{% endblock %}
|
||||
{% set tab = 'index' %}
|
||||
{% block content %}
|
||||
<style>
|
||||
.tagline {
|
||||
font-style: italic;
|
||||
font-size: 1.1em;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
padding-left: 4em;
|
||||
padding-right: 4em;
|
||||
}
|
||||
|
||||
.button_wide {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
.button_wide:hover {
|
||||
color: white;
|
||||
}
|
||||
.big_option {
|
||||
display: inline-block;
|
||||
width: 12em;
|
||||
padding: 1em;
|
||||
vertical-align: middle;
|
||||
height: 3.5em;
|
||||
border: 4px solid var(--cmap-border);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--cmap-bg-lighter);
|
||||
font-size: 1.5em;
|
||||
margin: 1em;
|
||||
position: relative;
|
||||
}
|
||||
.big_option:hover {
|
||||
background-color: var(--cmap-bg-lightest);
|
||||
border: 4px solid var(--main-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.big_option_text {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.bonus {
|
||||
color: #999;
|
||||
font-size: 0.7em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlights {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.show_list_bullet {
|
||||
margin-top: 0.5em;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
.show_list_bullet li {
|
||||
text-align: left;
|
||||
list-style-type: disc;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
|
||||
.plus_sign {
|
||||
font-size: 3em;
|
||||
color: #ccc;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
</style>
|
||||
<br><br>
|
||||
<div class="tagline">“Tretbootservice 4thewin.”</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
{% extends "tretboot/components/tretboot_tab.jinja" %}
|
||||
{% block title %}Settings{% endblock %}
|
||||
{% set tab = 'settings_get' %}
|
||||
{% block content %}
|
||||
<br/>
|
||||
<style>
|
||||
.big_option {
|
||||
display: inline-block;
|
||||
width: 14em;
|
||||
padding: 1em;
|
||||
vertical-align: middle;
|
||||
height: 3.5em;
|
||||
border: 4px solid var(--cmap-border);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--cmap-bg-lighter);
|
||||
font-size: 1.5em;
|
||||
margin: 1em;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
.big_option:hover {
|
||||
background-color: var(--cmap-bg-lightest);
|
||||
border: 4px solid var(--main-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.big_option_text {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.instructions {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
{# TODO: End Remove #}
|
||||
|
||||
.css-1rbd7t8 {
|
||||
box-sizing: border-box;
|
||||
margin: 60px 0px 24px;
|
||||
min-width: 0px;
|
||||
}
|
||||
.css-yn7azs {
|
||||
box-sizing: border-box;
|
||||
margin: 0px;
|
||||
min-width: 0px;
|
||||
-moz-box-pack: justify;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
font-family: "Source Serif Pro", serif;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.02em;
|
||||
font-variant: common-ligatures;
|
||||
text-rendering: optimizelegibility;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<h1>{{ _("Configure your extension") }}</h1>
|
||||
<br>
|
||||
|
||||
<div class="note">
|
||||
{{ _("- Maybe you want to put some notes for the user") }}<br/>
|
||||
{{ _("- One important setting is whether your extension gets a menu-point on the left") }}<br/>
|
||||
{{ _("- Let us assume you want the user to use a specific wallet. That should go here. ") }}<br/>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<form action="{{ url_for(service.get_blueprint_name() + '.settings_post') }}" method="POST" role="form">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div>Show Menu Item:</div>
|
||||
<select name="show_menu">
|
||||
<option value="yes" {% if show_menu == 'yes' %}selected{% endif %}>Yes</option>
|
||||
<option value="no" {% if show_menu == 'no' %}selected{% endif %}>No</option>
|
||||
</select>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
{{ _("Choose which wallet should be used:") }}:<br>
|
||||
<select name="used_wallet">
|
||||
{% for wallet in wallets %}
|
||||
<option value="{{ wallet.alias }}" {% if associated_wallet == wallet %}selected{% endif %}>{{ wallet.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<button type="submit" class="btn">{{ _("Save") }}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
[build-system]
|
||||
requires = [
|
||||
"cryptoadvance.specter==1.8.1"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
|
@ -0,0 +1 @@
|
|||
cryptoadvance.specter==1.8.1
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
|
|
@ -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"))
|
||||
|
|
@ -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
|
||||
|
|
@ -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) -%}
|
||||
<nav class="row collapse-on-mobile">
|
||||
{{ menu_item(service.id, 'index', 'Main', active_menuitem, isLeft=true) }}
|
||||
{{ menu_item(service.id, 'settings_get', 'Settings', active_menuitem, isRight=true) }}
|
||||
<a href="javascript:void(0);" class="mobile-nav-icon" onclick="toggleMobileNav(this, `{{ url_for('static', filename='img/expand-more.svg') }}`, `{{ url_for('static', filename='img/expand-less.svg') }}`)">
|
||||
<img style="width: 36px;" src="{{ url_for('static', filename='img/expand-more.svg') }}"/>
|
||||
</a>
|
||||
</nav>
|
||||
{%- endmacro %}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{% extends "base.jinja" %}
|
||||
{% block main %}
|
||||
<img src="{{ url_for(service.id +'_endpoint' + '.static', filename=service.logo) }}" width="300"/>
|
||||
{% from 'ruderboot/components/ruderboot_menu.jinja' import ruderboot_menu with context %}
|
||||
{{ ruderboot_menu(tab) }}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
{% extends "ruderboot/components/ruderboot_tab.jinja" %}
|
||||
{% block title %}Settings{% endblock %}
|
||||
{% set tab = 'index' %}
|
||||
{% block content %}
|
||||
<style>
|
||||
.tagline {
|
||||
font-style: italic;
|
||||
font-size: 1.1em;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
padding-left: 4em;
|
||||
padding-right: 4em;
|
||||
}
|
||||
|
||||
.button_wide {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
.button_wide:hover {
|
||||
color: white;
|
||||
}
|
||||
.big_option {
|
||||
display: inline-block;
|
||||
width: 12em;
|
||||
padding: 1em;
|
||||
vertical-align: middle;
|
||||
height: 3.5em;
|
||||
border: 4px solid var(--cmap-border);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--cmap-bg-lighter);
|
||||
font-size: 1.5em;
|
||||
margin: 1em;
|
||||
position: relative;
|
||||
}
|
||||
.big_option:hover {
|
||||
background-color: var(--cmap-bg-lightest);
|
||||
border: 4px solid var(--main-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.big_option_text {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.bonus {
|
||||
color: #999;
|
||||
font-size: 0.7em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlights {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.show_list_bullet {
|
||||
margin-top: 0.5em;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
.show_list_bullet li {
|
||||
text-align: left;
|
||||
list-style-type: disc;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
|
||||
.plus_sign {
|
||||
font-size: 3em;
|
||||
color: #ccc;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
</style>
|
||||
<br><br>
|
||||
<div class="tagline">“Ruderbootservice 4thewin.”</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
{% extends "ruderboot/components/ruderboot_tab.jinja" %}
|
||||
{% block title %}Settings{% endblock %}
|
||||
{% set tab = 'settings_get' %}
|
||||
{% block content %}
|
||||
<br/>
|
||||
<style>
|
||||
.big_option {
|
||||
display: inline-block;
|
||||
width: 14em;
|
||||
padding: 1em;
|
||||
vertical-align: middle;
|
||||
height: 3.5em;
|
||||
border: 4px solid var(--cmap-border);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--cmap-bg-lighter);
|
||||
font-size: 1.5em;
|
||||
margin: 1em;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
.big_option:hover {
|
||||
background-color: var(--cmap-bg-lightest);
|
||||
border: 4px solid var(--main-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.big_option_text {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.instructions {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
{# TODO: End Remove #}
|
||||
|
||||
.css-1rbd7t8 {
|
||||
box-sizing: border-box;
|
||||
margin: 60px 0px 24px;
|
||||
min-width: 0px;
|
||||
}
|
||||
.css-yn7azs {
|
||||
box-sizing: border-box;
|
||||
margin: 0px;
|
||||
min-width: 0px;
|
||||
-moz-box-pack: justify;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
font-family: "Source Serif Pro", serif;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.02em;
|
||||
font-variant: common-ligatures;
|
||||
text-rendering: optimizelegibility;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<h1>{{ _("Configure your extension") }}</h1>
|
||||
<br>
|
||||
|
||||
<div class="note">
|
||||
{{ _("- Maybe you want to put some notes for the user") }}<br/>
|
||||
{{ _("- One important setting is whether your extension gets a menu-point on the left") }}<br/>
|
||||
{{ _("- Let us assume you want the user to use a specific wallet. That should go here. ") }}<br/>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<form action="{{ url_for(service.get_blueprint_name() + '.settings_post') }}" method="POST" role="form">
|
||||
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div>Show Menu Item:</div>
|
||||
<select name="show_menu">
|
||||
<option value="yes" {% if show_menu == 'yes' %}selected{% endif %}>Yes</option>
|
||||
<option value="no" {% if show_menu == 'no' %}selected{% endif %}>No</option>
|
||||
</select>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
{{ _("Choose which wallet should be used:") }}:<br>
|
||||
<select name="used_wallet">
|
||||
{% for wallet in wallets %}
|
||||
<option value="{{ wallet.alias }}" {% if associated_wallet == wallet %}selected{% endif %}>{{ wallet.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<button type="submit" class="btn">{{ _("Save") }}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue