BAL - Bitcoin After Life Electrum plugin (v0.2.8)

Behavior-preserving refactor of the original BAL plugin with clean separation
of business logic from the PyQt GUI.

Layout:
  bal/core/   GUI-free logic (util, plugin_base, heirs, will, willexecutors)
  bal/gui/qt/ PyQt6 presentation (theme, common, widgets, calendar, dialogs,
              lists, window, plugin)
  bal/qt.py   Qt entry-point shim (works as internal and external zip plugin)
  bal/manifest.json  standard-conforming metadata

Tooling:
  build_zip.py            deterministic, zipimport-friendly archive builder
  tests/smoke_test.py     imports + behavior regression test
  tests/external_zip_test.py  reproduces Electrum's external-zip loading

Targets Electrum 4.7.2 + PyQt6. Logic kept byte-identical where possible.
This commit is contained in:
GenSpark AI Developer
2026-06-07 09:22:34 +00:00
commit 4198a5145b
41 changed files with 8345 additions and 0 deletions

73
build_zip.py Normal file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Build a clean, zipimport-friendly distribution archive of the BAL plugin.
Electrum loads external plugins from a ``.zip`` using Python's ``zipimport``.
``zipimport`` is picky about the archive layout, so this builder deliberately:
* writes **only files** (no explicit directory entries) — some Electrum
portable builds choke on directory records inside the archive;
* uses standard DEFLATE compression (well supported by ``zipimport``);
* emits entries in a deterministic, sorted order so the archive is
reproducible (stable SHA-256);
* skips ``__pycache__`` directories and compiled ``*.pyc``/``*.pyo`` files.
The archive keeps the top-level ``bal/`` directory so that the package is
importable as ``bal`` (and Electrum derives ``dirname='bal'`` from the path of
``bal/manifest.json``).
Usage::
python3 build_zip.py [output.zip]
Prints the resulting size and SHA-256 so the download can be integrity-checked.
"""
import hashlib
import os
import sys
import zipfile
SRC_ROOT = "bal"
DEFAULT_OUT = "bal-electrum-plugin.zip"
def build(out_path: str) -> None:
if os.path.exists(out_path):
os.remove(out_path)
files = []
for dirpath, dirnames, filenames in os.walk(SRC_ROOT):
# prune cache dirs in place so os.walk does not descend into them
dirnames[:] = [d for d in dirnames if d != "__pycache__"]
for fn in filenames:
if fn.endswith((".pyc", ".pyo")):
continue
files.append(os.path.join(dirpath, fn))
files.sort()
with zipfile.ZipFile(
out_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6
) as z:
for f in files:
arc = f.replace(os.sep, "/") # forward slashes inside the archive
z.write(f, arc)
# Integrity + summary
with zipfile.ZipFile(out_path) as z:
bad = z.testzip()
if bad is not None:
raise SystemExit(f"ERROR: corrupt entry in archive: {bad}")
names = z.namelist()
if not any(n.endswith("manifest.json") for n in names):
raise SystemExit("ERROR: manifest.json missing from archive")
data = open(out_path, "rb").read()
print(f"built : {out_path}")
print(f"files : {len(files)}")
print(f"size : {len(data)} bytes")
print(f"sha256: {hashlib.sha256(data).hexdigest()}")
if __name__ == "__main__":
out = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_OUT
build(out)