Files
bal-electrum-plugin/bal/__init__.py

132 lines
6.0 KiB
Python

"""BAL - Bitcoin After Life Electrum plugin.
Free and decentralized Bitcoin inheritance support for the Electrum wallet.
This package was reorganized (Approach A: conservative, behavior-preserving)
to cleanly separate logic from presentation. The original monolithic plugin
mixed the business logic with the PyQt GUI; here the two concerns live in
distinct sub-packages:
bal/
core/ GUI-free business logic (importable without Qt)
util.py Generic helpers (encoding, validation, ...)
plugin_base.py BasePlugin subclass, config, timestamp handling
heirs.py Heir list model + transaction building
will.py Will / WillItem domain model
willexecutors.py Will-executor (dead-man's switch) networking
gui/
qt/ PyQt6 presentation layer
theme.py Colors / status -> color mapping (status_color)
common.py Shared imports and small GUI helpers
widgets.py Leaf widgets (editors, labels, checkboxes, ...)
calendar.py BalCalendar widget
dialogs.py Dialog windows (wizard, build-will, detail, ...)
lists.py Tree/list views (heirs, preview, will-executors)
window.py BalWindow controller (per-wallet GUI state)
plugin.py Plugin class wiring Electrum @hooks to the GUI
cli/ Headless command-line layer (no Qt)
commands.py The @plugin_command transport layer (registers
the ``bal_*`` commands)
controller.py Headless replica of the Qt flows (later phases)
plugin.py Plugin(BalPlugin) entry point for the daemon
qt.py Thin loader shim re-exporting `Plugin` for Electrum
cmdline.py Thin loader shim re-exporting `Plugin` for the daemon
Electrum discovers the plugin through ``manifest.json`` and loads the GUI
entry point from ``qt.py`` (the shim), which imports the real ``Plugin``
from ``gui.qt.plugin``; the command-line/daemon entry point is ``cmdline.py``
(the shim), which imports ``Plugin`` from ``cli.plugin``.
The plugin supports Electrum 4.7.2 and 4.8.0 with PyQt6. Electrum 4.8.0 removed
``json_db.register_dict`` and replaced it with the path-based
``electrum.stored_dict.register_name``; ``core.plugin_base`` detects which API is
available and adapts, so both releases keep working.
"""
# The plugin version is NOT defined here. It lives only in ``bal/manifest.json``
# (the single source of truth) and is read at runtime via ``get_version()`` in
# ``bal/core/plugin_base.py`` (exposed as the ``BalPlugin.version`` property).
# Keeping a hardcoded ``__version__`` here would just be a stale duplicate.
# --------------------------------------------------------------------------- #
# CLI command registration
# --------------------------------------------------------------------------- #
# Electrum's CLI pre-parse (run_electrum calls ``Plugins(config, cmd_only=True)``)
# only imports the plugin package ``__init__`` to discover its commands.
# Importing ``bal.cli.commands`` here registers every ``bal_*`` command with
# ``electrum.commands`` (``known_commands`` + the ``Commands`` class), so the
# commands become available on the command line and over JSON-RPC without any Qt.
#
# The import must be zip-safe: when the plugin is loaded as an external zip,
# Electrum registers the package under the synthetic name
# ``electrum_external_plugins.bal``, but the module's ``__package__`` is only
# ``bal`` (the zip-internal directory name), which is not present in
# ``sys.modules`` and cannot be used for sub-module imports. We therefore
# resolve the real package name and import through ``importlib`` (the same
# trick as ``qt.py``).
import importlib
import sys as _sys
def _resolve_package_name() -> str:
"""Return the name this package is registered under in ``sys.modules``.
Internal plugins are imported as ``electrum.plugins.bal`` (a normal import,
so ``__package__`` is already correct). External zip plugins are imported
under the synthetic name ``electrum_external_plugins.bal`` with
``__package__`` set to just the zip-internal directory name (``bal``); only
the synthetic name is present in ``sys.modules``.
"""
pkg = __package__ or "bal"
if pkg in _sys.modules:
return pkg
synthetic = "electrum_external_plugins." + __name__
if synthetic in _sys.modules:
return synthetic
return pkg
def _ensure_parent_packages(pkg_name: str) -> None:
"""Backfill missing ancestor packages in ``sys.modules``.
When loaded from a zip as an external plugin, Electrum only executes the
package ``__init__``; the synthetic root package (``electrum_external_plugins``)
may be missing, which would break sub-module imports. We stub it out as a
namespace package so ``importlib`` can still resolve its children (same
helper as ``qt.py``).
"""
parts = pkg_name.split(".")
for i in range(1, len(parts)):
ancestor = ".".join(parts[:i])
if ancestor in _sys.modules:
continue
try:
importlib.import_module(ancestor)
except Exception:
import types
module = types.ModuleType(ancestor)
module.__path__ = [] # mark as a (namespace) package
_sys.modules[ancestor] = module
def _register_cli_commands() -> None:
"""Import ``bal.cli.commands`` so Electrum registers the ``bal_*`` commands.
Guarded so a dual install (internal package AND external zip) cannot
register the same command names twice, which would make
``electrum.commands.plugin_command`` raise
"Command name bal_... already exists".
"""
from electrum import commands as _electrum_commands
if getattr(_electrum_commands, "_bal_cli_commands_registered", False):
return
pkg = _resolve_package_name()
_ensure_parent_packages(pkg)
importlib.import_module(pkg + ".cli.commands")
_electrum_commands._bal_cli_commands_registered = True
_register_cli_commands()