From 30508936d30d9ef45259d0b1ed9ae4ef92a693a3 Mon Sep 17 00:00:00 2001 From: k9ert Date: Wed, 19 Oct 2022 14:46:29 +0200 Subject: [PATCH] Feature: Extensions can add new devices (#1887) --- docs/extensions.md | 345 ------------------ docs/extensions/callbacks.md | 11 + docs/extensions/data-storage.md | 55 +++ docs/extensions/devices.md | 41 +++ docs/extensions/frontend-aspects.md | 148 ++++++++ docs/extensions/intro.md | 160 ++++++++ docs/extensions/service-configuration.md | 12 + .../extensions_file_layout.png | Bin mkdocs.yml | 9 +- src/cryptoadvance/specter/cli/cli_ext.py | 22 +- src/cryptoadvance/specter/config.py | 1 + src/cryptoadvance/specter/device.py | 15 +- src/cryptoadvance/specter/devices/__init__.py | 2 - src/cryptoadvance/specter/devices/bitbox02.py | 2 +- .../specter/devices/bitcoin_core.py | 2 +- src/cryptoadvance/specter/devices/cobo.py | 2 +- src/cryptoadvance/specter/devices/coldcard.py | 2 +- .../specter/devices/device_types.py | 16 +- src/cryptoadvance/specter/devices/electrum.py | 4 +- .../specter/devices/elements_core.py | 2 +- src/cryptoadvance/specter/devices/jade.py | 2 +- src/cryptoadvance/specter/devices/keepkey.py | 2 +- src/cryptoadvance/specter/devices/keystone.py | 2 +- src/cryptoadvance/specter/devices/ledger.py | 2 +- src/cryptoadvance/specter/devices/passport.py | 2 +- .../specter/devices/seedsigner.py | 2 +- src/cryptoadvance/specter/devices/specter.py | 2 +- src/cryptoadvance/specter/devices/trezor.py | 2 +- .../specter/managers/service_manager.py | 39 +- src/cryptoadvance/specter/server.py | 4 +- .../specter/services/extension_gen.py | 16 +- .../device/new_device/new_device_type.jinja | 2 +- .../components/sidebar_device_list_item.jinja | 2 +- src/cryptoadvance/specter/util/reflection.py | 15 +- .../devhelp/devices/devhelpdevice.py | 12 + .../specterext/devhelp/service.py | 1 + .../static/devhelp/img/mydevice-logo.svg | 122 +++++++ .../specterext/electrum/__init__.py | 0 .../specterext/electrum/controller.py | 33 ++ .../specterext/electrum/devices/electrum.py | 22 ++ .../specterext/electrum/service.py | 28 ++ .../electrum}/img/devices/electrum_icon.svg | 0 .../electrum/img/electrum_lightblue.svg | 193 ++++++++++ .../electrum}/img/electrum_manual/1.png | Bin .../electrum}/img/electrum_manual/2.png | Bin .../device}/new_device_keys_electrum.jinja | 4 +- .../electrum/templates/electrum/index.jinja | 17 + tests/test_devices_specter.py | 13 + tests/test_services_extension_gen.py | 1 + tests/test_util_reflection.py | 7 + 50 files changed, 1016 insertions(+), 382 deletions(-) delete mode 100644 docs/extensions.md create mode 100644 docs/extensions/callbacks.md create mode 100644 docs/extensions/data-storage.md create mode 100644 docs/extensions/devices.md create mode 100644 docs/extensions/frontend-aspects.md create mode 100644 docs/extensions/intro.md create mode 100644 docs/extensions/service-configuration.md rename docs/images/{ => extensions}/extensions_file_layout.png (100%) create mode 100644 src/cryptoadvance/specterext/devhelp/devices/devhelpdevice.py create mode 100644 src/cryptoadvance/specterext/devhelp/static/devhelp/img/mydevice-logo.svg create mode 100644 src/cryptoadvance/specterext/electrum/__init__.py create mode 100644 src/cryptoadvance/specterext/electrum/controller.py create mode 100644 src/cryptoadvance/specterext/electrum/devices/electrum.py create mode 100644 src/cryptoadvance/specterext/electrum/service.py rename src/cryptoadvance/{specter/static => specterext/electrum/static/electrum}/img/devices/electrum_icon.svg (100%) create mode 100644 src/cryptoadvance/specterext/electrum/static/electrum/img/electrum_lightblue.svg rename src/cryptoadvance/{specter/static => specterext/electrum/static/electrum}/img/electrum_manual/1.png (100%) rename src/cryptoadvance/{specter/static => specterext/electrum/static/electrum}/img/electrum_manual/2.png (100%) rename src/cryptoadvance/{specter/templates/device/new_device => specterext/electrum/templates/electrum/device}/new_device_keys_electrum.jinja (81%) create mode 100644 src/cryptoadvance/specterext/electrum/templates/electrum/index.jinja diff --git a/docs/extensions.md b/docs/extensions.md deleted file mode 100644 index 17e9b0a073..0000000000 --- a/docs/extensions.md +++ /dev/null @@ -1,345 +0,0 @@ -# Extensions - -A developer's guide for the Specter Desktop `Extension` framework. - -We currently rework the naming of extensions/plugins/services. If not otherwise stated, for now, they are used interchangeably. - -## TL;DR - -You can create an extension with an up to date Specter Desktop instance as simple as this: -``` -$ pip3 install cryptoadvance.specter --upgrade -$ mkdir /tmp/rubberduck && cd /tmp/rubberduck -$ python3 -m cryptoadvance.specter ext gen - - We need an id and a prefix for your extension. - The id should be a short string. - The prefix is usually your GitHub username - or GitHub organisation name. - Both will be used to to create a directory structure like this: - ./src/mycorpname/specterext/myextension - They will also be used when publishing this extension to pypi. - -Enter the id of your extension (lowercase only): rubberduck -Enter the prefix: mynym - - Note: Isolated client mode means that the extensions won't share the session cookie with - Specter Desktop and the integration only happens on the server side. - -Should the extension work in isolated client mode (y/n)?: n - --> Created requirements.txt - --> Created .gitignore - --> Created src/mynym/specterext/rubberduck/service.py - --> Created src/mynym/specterext/rubberduck/controller.py - --> Created src/mynym/specterext/rubberduck/config.py - --> Created src/mynym/specterext/rubberduck/__init__.py - --> Created src/mynym/specterext/rubberduck/__main__.py - --> Created src/mynym/specterext/rubberduck/templates/rubberduck/index.jinja - --> Created src/mynym/specterext/rubberduck/static/rubberduck/css/styles.css - --> Created src/mynym/specterext/rubberduck/static/rubberduck/img/ghost.png (via Github) - --> Created src/mynym/specterext/rubberduck/static/rubberduck/img/logo.jpeg (via Github) - --> Created src/mynym/specterext/rubberduck/templates/rubberduck/base.jinja - --> Created src/mynym/specterext/rubberduck/templates/rubberduck/transactions.jinja - --> Created src/mynym/specterext/rubberduck/templates/rubberduck/settings.jinja - --> Created src/mynym/specterext/rubberduck/templates/rubberduck/components/rubberduck_menu.jinja - --> Created src/mynym/specterext/rubberduck/templates/rubberduck/components/rubberduck_tab.jinja - --> Created pytest.ini - --> Created tests/conftest.py - --> Created tests/fix_ghost_machine.py - --> Created tests/fix_devices_and_wallets.py - --> Created tests/fix_testnet.py - --> Created tests/fix_keys_and_seeds.py - --> Created pyproject.toml - --> Created setup.py - --> Created setup.cfg - --> Created MANIFEST.in - - Congratulations, you've created a new extension! - - Here is how to get it to run in your development environment: - pip3 install -e . - python3 -m cryptoadvance.specter server --config DevelopmentConfig --debug - # Point your browser to http://localhost:25441 - # Click "Choose plugins" --> rubberduck - - 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/mynym_rubberduck-0.0.1-py3-none-any.whl - - If you want to bring your extension to production, please refer to - the readme in the dummy-extension repo: - https://github.com/cryptoadvance/specterext-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 this information again via: - python3 -m cryptoadvance.specter ext gen --help -$ -``` -The created file structure looks like this and you will feel right at home if you have some knowledge about how Flask works: - - -![](./images/extensions_file_layout.png) - - - - -## Concept -As much as possible, each `extension` should be entirely self-contained with little or no custom code altering core Specter functionality. There is a name for that: Extension framework. -The term `extension` will be used for all sorts of extensions whereas `plugin` will be used as a component which can be de-/activated by a user. - -All extensions are completely seperated 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 as Specter itself. Extensions can be located in their own repository even if they are incorporated into the official Specter release. - -Independent of 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 ultimately a 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 above. - -All the attributes of an extension are currently (json support is planned) 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`. -```python -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 -``` -This defines the base `Service` class (to be renamed to "Extension") that all extensions must inherit from. This also enables `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 useful methods explained later. - -The `id` needs to be unique within a specific Specter instance where this extension is part of. The `name` is the display name 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. - -## Frontend aspects - -As stated, you can have your own frontend with a blueprint. If you only have one, it needs to have a `/` route in order to be linkable from the `choose your plugin` page. -If you create your extension with a blueprint, it'll also create a controller for you which, simplified, looks like this: -```python -rubberduck_endpoint = ScratchpadService.blueprint - -def ext() -> ScratchpadService: - ''' convenience for getting the extension-object''' - return app.specter.ext["rubberduck"] - -def specter() -> Specter: - ''' convenience for getting the specter-object''' - return app.specter - - -@rubberduck.route("/") -@login_required -@user_secret_decrypted_required -def index(): - return render_template( - "rubberduck/index.jinja", - ) -[...] -``` - But you can also have more than one blueprint. Define them like this in your service class: -```python - blueprint_modules = { - "default" : "mynym.specterext.rubberduck.controller", - "ui" : "mynym.specterext.rubberduck.controller_ui" - } -``` -You have to have a default blueprint which has the above mentioned index page. -In your controller, the endpoint needs to be specified like this: -```python -ui = RubberduckService.blueprints["ui"] -``` - -You might have an extension which wants to inject e.g. JavaScript code into each and every page of Specter Desktop. The extension needs to be activated for the user, though. You can do that via overwriting one of the `inject_in_basejinja_*` methods in your service class: -```python - @classmethod - def inject_in_basejinja_head(cls): - ''' e.g. rendering some snippet ''' - return render_template("devhelp/html_inject_in_basejinja_head.jinja") - - @classmethod - def inject_in_basejinja_body_top(cls): - ''' or directly returning text ''' - return "