From b9c00446c4d65b0e39f26b3008a35d1f466c64f4 Mon Sep 17 00:00:00 2001 From: bot Date: Sat, 20 Jun 2026 09:48:50 -0400 Subject: [PATCH] add bal root files --- bal/LICENSE | 21 +++++++++++ bal/README.md | 23 ++++++++++++ bal/VERSION | 1 + bal/__init__.py | 37 ++++++++++++++++++++ bal/bal_resources.py | 14 ++++++++ bal/manifest.json | 10 ++++++ bal/qt.py | 83 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 189 insertions(+) create mode 100644 bal/LICENSE create mode 100644 bal/README.md create mode 100644 bal/VERSION create mode 100644 bal/__init__.py create mode 100644 bal/bal_resources.py create mode 100644 bal/manifest.json create mode 100644 bal/qt.py diff --git a/bal/LICENSE b/bal/LICENSE new file mode 100644 index 0000000..c9bc88f --- /dev/null +++ b/bal/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 copronista + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bal/README.md b/bal/README.md new file mode 100644 index 0000000..9cb3127 --- /dev/null +++ b/bal/README.md @@ -0,0 +1,23 @@ +# BalPlugin +Bitcoin After Life Electrum Plugin + +Free and decentralized Bitcoin inheritance support for Electrum: build +time-locked "will" transactions that transfer your funds to your heirs if you +stop refreshing them (dead-man's switch), optionally relayed by will-executor +servers. + +## Key behaviours + +- **Anticipate / postpone safety**: changing the delivery time of an + already-signed will is handled safely. Postponing a signed/sent will first + asks you to invalidate the old transaction on-chain (so a will-executor can + never broadcast the earlier-locktime transaction and execute the inheritance + too early), then lets you rebuild and re-send the new one via + **Tools → Prepare**. +- **"Server" column**: the will transaction list shows whether each transaction + is actually stored on the will-executor servers + (`Confirmed on server`, `Sent (not checked)`, `Send failed`, + `Not on server`, `Signed (not sent)`, `Not sent`), with a tooltip showing the + will-executor URL. + +See the top-level [`README.md`](../README.md) for installation and testing. diff --git a/bal/VERSION b/bal/VERSION new file mode 100644 index 0000000..87a0871 --- /dev/null +++ b/bal/VERSION @@ -0,0 +1 @@ +0.3.3 \ No newline at end of file diff --git a/bal/__init__.py b/bal/__init__.py new file mode 100644 index 0000000..a814720 --- /dev/null +++ b/bal/__init__.py @@ -0,0 +1,37 @@ +"""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 + qt.py Thin loader shim re-exporting `Plugin` for Electrum + +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 plugin targets Electrum 4.7.2 (the last stable release exposing +``json_db.register_dict``) and PyQt6. +""" + +__version__ = "0.3.3" diff --git a/bal/bal_resources.py b/bal/bal_resources.py new file mode 100644 index 0000000..dc2924c --- /dev/null +++ b/bal/bal_resources.py @@ -0,0 +1,14 @@ +import os + +PLUGIN_DIR = os.path.split(os.path.realpath(__file__))[0] +DEFAULT_ICON = "bal32x32.png" +DEFAULT_ICON_PATH = "icons" + + +def icon_path(icon_basename: str = DEFAULT_ICON): + path = resource_path(DEFAULT_ICON_PATH, icon_basename) + return path + + +def resource_path(*parts): + return os.path.join(PLUGIN_DIR, *parts) diff --git a/bal/manifest.json b/bal/manifest.json new file mode 100644 index 0000000..fa0fe4b --- /dev/null +++ b/bal/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "bal", + "fullname": "Bitcoin After Life", + "version": "0.3.3", + "description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.", + "author": "Svatantrya", + "licence": "MIT", + "available_for": ["qt"], + "icon": "icons/bal32x32.png" +} diff --git a/bal/qt.py b/bal/qt.py new file mode 100644 index 0000000..6bd488f --- /dev/null +++ b/bal/qt.py @@ -0,0 +1,83 @@ +""" +bal.qt +====== + +Compatibility shim for Electrum's plugin loader. + +Electrum loads a Qt plugin by importing the ``qt`` module of the plugin package +and looking for a ``Plugin`` class. The real implementation lives in the +well-separated ``bal.gui.qt`` sub-package, so this module re-exports the +``Plugin`` class from ``bal.gui.qt.plugin``. + +Why this file is not a one-line relative import +----------------------------------------------- +A plain ``from .gui.qt.plugin import Plugin`` works fine when the plugin is +installed as an *internal* plugin (under ``electrum/plugins/bal``). However, +when the very same code is loaded as an *external* plugin from a ``.zip``, +Electrum 4.7.x imports the package under the synthetic top-level name +``electrum_external_plugins.bal`` and only executes the package ``__init__`` and +this ``qt`` module. It never registers the intermediate parent packages +(``electrum_external_plugins`` itself, ``...bal.gui``, ``...bal.gui.qt``). As a +result, a relative import that has to walk up to those parents fails with:: + + ModuleNotFoundError: No module named 'electrum_external_plugins' + +To make the plugin work *both* as an internal package and as an external zip, +this shim resolves and imports ``Plugin`` defensively: + +1. It works out the name of the package this module lives in + (``__package__``), whatever Electrum decided to call it. +2. It makes sure every parent package in that chain exists in + ``sys.modules`` so Python's import machinery can resolve sub-modules. +3. It imports the ``.gui.qt.plugin`` sub-module via :func:`importlib.import_module` + using the resolved absolute name. + +This keeps the clean ``core`` / ``gui`` layout while staying robust to how the +plugin is loaded. +""" + +import importlib +import sys + + +def _ensure_parent_packages(pkg_name: str) -> None: + """Make sure every ancestor package of *pkg_name* is in ``sys.modules``. + + When loaded from a zip as an external plugin, Electrum only executes the + plugin package ``__init__`` and the ``qt`` module. The synthetic root + package (e.g. ``electrum_external_plugins``) and any intermediate packages + may be missing from ``sys.modules``, which breaks relative/absolute + sub-module imports. We backfill them here using this module's own loader + so that ``importlib`` can find sibling sub-packages. + """ + parts = pkg_name.split(".") + # Walk from the top-most ancestor down to (but not including) pkg_name. + for i in range(1, len(parts)): + ancestor = ".".join(parts[:i]) + if ancestor in sys.modules: + continue + try: + importlib.import_module(ancestor) + except Exception: + # The synthetic root (e.g. 'electrum_external_plugins') often has no + # real spec. Create a minimal namespace package stub so that the + # import machinery can still resolve its children. + import types + + module = types.ModuleType(ancestor) + module.__path__ = [] # mark as a (namespace) package + sys.modules[ancestor] = module + + +# The package this module belongs to. Could be 'electrum.plugins.bal' (internal) +# or 'electrum_external_plugins.bal' (external zip), depending on how Electrum +# loaded us. +_PKG = __package__ or "bal" + +_ensure_parent_packages(_PKG) + +# Import the real implementation using the fully-qualified, run-time package +# name so it works regardless of the synthetic prefix Electrum assigned. +_plugin_module = importlib.import_module(_PKG + ".gui.qt.plugin") + +Plugin = _plugin_module.Plugin # noqa: F401 (re-exported for Electrum)