Compare commits
7 Commits
bitcoinaft
...
4b0af6f4bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
4b0af6f4bf
|
|||
| f72ed33ea0 | |||
| 6c41a28541 | |||
| 00eefe0525 | |||
|
|
b610bea5a8 | ||
|
|
4ae019cba5 | ||
| 372f3952ca |
181
CHANGELOG.md
181
CHANGELOG.md
@@ -2329,3 +2329,184 @@ The request was to make the failure message clearer (no timeout change).
|
|||||||
- `ruff`: no new errors.
|
- `ruff`: no new errors.
|
||||||
|
|
||||||
**Outcome:** DONE (delivered as test ZIP v0.5.18; commit only after confirmation).
|
**Outcome:** DONE (delivered as test ZIP v0.5.18; commit only after confirmation).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 43. v0.6.0 - Version bump for the official repository release
|
||||||
|
|
||||||
|
**Date:** 2026-07-17
|
||||||
|
|
||||||
|
**Context:** the plugin content of this version is IDENTICAL to v0.5.18 - no code
|
||||||
|
changes. A release was published on the official repository
|
||||||
|
(bitcoinafterlife/bal-electrum-plugin) tagged "v0.6.0", but the internal version
|
||||||
|
files still read "0.5.18" (a human oversight: the release tag was not matched by
|
||||||
|
a version bump in the code), so Electrum displayed "0.5.18" after installing it.
|
||||||
|
This entry aligns the internal version with the intended "0.6.0" release tag by
|
||||||
|
bumping `bal/VERSION`, `bal/manifest.json`, `bal/__init__.py` and
|
||||||
|
`bal/core/plugin_base.py` from 0.5.18 to 0.6.0. No functional changes.
|
||||||
|
|
||||||
|
**Verification:** full test suite against Electrum 4.7.2 and 4.8.0 (same 266
|
||||||
|
passed / 2 pre-existing unrelated failures as v0.5.18); `ruff` clean.
|
||||||
|
|
||||||
|
**Outcome:** DONE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 44. Repo: inheritance-logic test suite + first BAL CLI (no plugin version change)
|
||||||
|
|
||||||
|
**Date:** 2026-07-20
|
||||||
|
|
||||||
|
**Scope note:** this entry adds REPOSITORY files only (`tests/`, `bal_cli.py`).
|
||||||
|
The plugin package `bal/` is byte-for-byte unchanged, so the plugin version
|
||||||
|
stays **0.6.0** on purpose (no zip rebuild, no version bump - avoiding any new
|
||||||
|
tag/code mismatch).
|
||||||
|
|
||||||
|
**1) New logic test suite - `tests/test_inheritance_scenarios.py` (23 tests):**
|
||||||
|
- Multi-heir distribution via `Heirs.prepare_lists`: 50/50 percent split of
|
||||||
|
(balance - fees); percentages normalized to their SUM (30+30 behaves like
|
||||||
|
50/50); fixed heirs paid first with percents sharing the remainder; fixed
|
||||||
|
amounts exceeding the balance scaled down proportionally (`onlyfixed`).
|
||||||
|
- Dust rules: below-dust heirs marked `DUST:` while valid heirs keep building;
|
||||||
|
the leftover redistribution can LIFT dust fixed heirs above the threshold
|
||||||
|
(pinned: 100:200 on 1M -> 333333/666666); all-dust wills refused
|
||||||
|
(`HeirAmountIsDustException`); `BalanceTooLowException` when balance < fees.
|
||||||
|
- Will-executor fees: one pseudo-heir per distinct locktime; fees larger than
|
||||||
|
the balance raise `WillExecutorFeeException`.
|
||||||
|
- Expired-heir exclusion vs `from_locktime`; grouping of heirs by locktime.
|
||||||
|
- Heir add/remove/locktime-change all flag the will as changed
|
||||||
|
(`Will._same_heirs`); the `Heirs` mapping persists on add/remove.
|
||||||
|
- Will expiry (`Will.check_will_expired`): past locktime raises
|
||||||
|
`WillExpiredException`, future/boundary (== now) does not, non-VALID items
|
||||||
|
are ignored.
|
||||||
|
- Locktime parsing: int passthrough, `<n>d` -> future midnight, `1y` == `365d`.
|
||||||
|
|
||||||
|
**2) First BAL CLI - `bal_cli.py` (offline iteration):**
|
||||||
|
- Reuses `bal.core` directly with Electrum as a library (no Qt, headless).
|
||||||
|
- Commands: `heirs list/add/remove/export/import`, `status`.
|
||||||
|
- Safety: **testnet by default**, mainnet only with an explicit `--mainnet`
|
||||||
|
flag (active network always printed); encrypted wallets via `--password` or
|
||||||
|
`BAL_WALLET_PASSWORD` env var; address/locktime validation (rejects wrong
|
||||||
|
network and past locktimes); guaranteed process termination (electrum leaves
|
||||||
|
non-daemon threads even offline - the CLI stops the wallet and event loop,
|
||||||
|
then hard-exits with the proper exit code, so machine callers never hang).
|
||||||
|
- Planned next iteration: `will build/sign/push/check` with an explicit
|
||||||
|
`--yes` confirmation flag for automation.
|
||||||
|
- End-to-end smoke test `tests/test_cli_smoke.py`: creates a REAL testnet
|
||||||
|
wallet via electrum-as-library in a subprocess, then drives
|
||||||
|
add/list/reject-invalid-address/reject-past-locktime/export/remove/status.
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- Full suite against **Electrum 4.7.2**: `290 passed`; against **4.8.0**:
|
||||||
|
`290 passed` (same 2 pre-existing, unrelated `baltx_fees` failures in both).
|
||||||
|
- `ruff`: no new errors on the three new files.
|
||||||
|
|
||||||
|
**Outcome:** DONE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 45. Repo: CLI will commands (build / sign / push / check) - no plugin version change
|
||||||
|
|
||||||
|
**Date:** 2026-07-21
|
||||||
|
|
||||||
|
**Goal:** complete the headless workflow started in entry #44, so a machine can
|
||||||
|
run the whole inheritance cycle without the Qt GUI.
|
||||||
|
|
||||||
|
**What was added (`bal_cli.py`):**
|
||||||
|
- `will build` - builds the will transactions offline through the same
|
||||||
|
`bal.core` pipeline the GUI uses (`Heirs.get_transactions` -> `WillItem` ->
|
||||||
|
`Will.update_will` / `normalize_will`), storing them as "New".
|
||||||
|
- `will sign` - signs the valid, incomplete transactions with the wallet
|
||||||
|
password (`--password` / `BAL_WALLET_PASSWORD`). Prints a summary and asks
|
||||||
|
for confirmation; `--yes` skips it for automation. Chained will inputs
|
||||||
|
(spending a previous will's change) are patched exactly as the GUI does.
|
||||||
|
Watching-only wallets and hardware keystores are rejected with a clear
|
||||||
|
message (hardware devices need physical confirmation).
|
||||||
|
- `will push` - sends signed transactions to the selected will-executors via
|
||||||
|
`Willexecutors.push_transactions_parallel`; `--yes` and `--force` supported;
|
||||||
|
updates PUSHED / PUSH_FAIL statuses.
|
||||||
|
- `will check` - read-only report with **exit codes for scripts**: 0 valid,
|
||||||
|
complete and pushed; 2 no will stored; 3 EXPIRED; 4 not fully signed;
|
||||||
|
5 signed but not pushed; 6 heirs changed since the will was built.
|
||||||
|
- `_CliBalPlugin`: minimal stand-in exposing only what `bal.core` needs
|
||||||
|
headless (`WILLEXECUTORS`, `NO_WILLEXECUTOR`, `get_decimal_point`).
|
||||||
|
|
||||||
|
**Three real bugs found and fixed while implementing this:**
|
||||||
|
1. `Will.only_valid()` returns a GENERATOR - `len()` on it raised
|
||||||
|
`TypeError`; now wrapped in `list()`.
|
||||||
|
2. `copy.deepcopy(tx)` fails on Electrum 4.8 (`cannot pickle '_thread.RLock'`);
|
||||||
|
signing now re-parses the transaction from its serialization instead.
|
||||||
|
3. **Silent will loss on save**: values loaded from the wallet DB are
|
||||||
|
`StoredDict`s carrying the DB lock, and `JsonDB.put` deep-copies its value
|
||||||
|
and returns False *silently* when that fails - the will appeared saved but
|
||||||
|
was never written. Persistence now normalizes through a
|
||||||
|
`json.dumps`/`loads` round-trip (which also validates serializability) and
|
||||||
|
exits with an explicit error if anything is non-serializable.
|
||||||
|
|
||||||
|
Also added: pre-build validation of the selected executor addresses for the
|
||||||
|
active network (refreshed from the server when possible), and a guard that
|
||||||
|
catches exception sentinels embedded by `heirs.py` in a heir entry when an
|
||||||
|
output cannot be built.
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- New `test_cli_will_cycle` in `tests/test_cli_smoke.py`: creates a REAL
|
||||||
|
testnet wallet, funds it offline with a handmade transaction, then drives
|
||||||
|
`build -> check(4) -> sign --yes -> check(5) -> push (aborted at the
|
||||||
|
confirmation prompt) -> status`. No network access.
|
||||||
|
- Full suite: **291 passed** against **Electrum 4.7.2** and **4.8.0** (same 2
|
||||||
|
pre-existing, unrelated `baltx_fees` failures in both).
|
||||||
|
- `ruff`: all checks passed.
|
||||||
|
|
||||||
|
**Note:** repository-only change (CLI + tests). The plugin version is
|
||||||
|
unchanged; the Qt plugin code is untouched.
|
||||||
|
|
||||||
|
**Outcome:** DONE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 46. v0.6.1 - Version read from manifest.json (single source of truth) - PR #4
|
||||||
|
|
||||||
|
**Date:** 2026-07-22
|
||||||
|
|
||||||
|
**Goal (Truman):** stop keeping the version in four places (`bal/VERSION`,
|
||||||
|
`bal/manifest.json`, `bal/__init__.py`, `bal/core/plugin_base.py`) synced by a
|
||||||
|
pre-commit hook. The version must be read at runtime from `manifest.json` only.
|
||||||
|
|
||||||
|
**What changed:**
|
||||||
|
- `bal/core/plugin_base.py`: new module function `get_version()` reads the
|
||||||
|
`"version"` field from `bal/manifest.json` using `importlib.resources`
|
||||||
|
(zip-safe: works both extracted and from inside a zip, which is how Electrum
|
||||||
|
loads external plugins via `zipimport`; no hand-built paths, so no Windows
|
||||||
|
backslash-in-zip issue). The value is cached. `BalPlugin` now exposes a
|
||||||
|
`version` **property** returning `get_version()`; the hardcoded
|
||||||
|
`__version__` class attribute and the old `version()` method that read the
|
||||||
|
`VERSION` file are removed.
|
||||||
|
- The three call sites that printed the version now use the property
|
||||||
|
(`self.bal_window.bal_plugin.version` in `bal/gui/qt/widgets.py` and
|
||||||
|
`bal/gui/qt/dialogs.py`) or `get_version()` where no plugin instance is
|
||||||
|
available (`bal/core/willexecutors.py`, the HTTP user-agent, a static
|
||||||
|
method).
|
||||||
|
- **Removed the `bal/VERSION` file** (the plugin package now ships 36 files
|
||||||
|
instead of 37).
|
||||||
|
- `HANDOFF.md`: updated the four references that told maintainers to edit
|
||||||
|
`bal/VERSION` / keep four files in sync - now they point to
|
||||||
|
`manifest.json` as the single source of truth. (Historical version
|
||||||
|
references in `CHANGELOG.md` and `.agent_memory_tasks.md` are left intact on
|
||||||
|
purpose - they describe past events.)
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
- New `tests/test_version_source.py` (7 tests): version equals the manifest
|
||||||
|
field; the `version` property matches `get_version()`; no hardcoded
|
||||||
|
`__version__` remains; no `bal/VERSION` file remains; the value is cached;
|
||||||
|
and - the key one - the version is read correctly when `bal` is imported
|
||||||
|
FROM INSIDE A ZIP in a child interpreter (the real Electrum/Windows
|
||||||
|
scenario).
|
||||||
|
- Full suite: **298 passed** against **Electrum 4.7.2** and **4.8.0** (same 2
|
||||||
|
pre-existing, unrelated `baltx_fees` failures in both).
|
||||||
|
- `ruff`: clean.
|
||||||
|
- Also verified the built distribution zip reports the manifest version via
|
||||||
|
`get_version()` (bumped to 0.6.1 in this change).
|
||||||
|
|
||||||
|
**Note:** repository change targeting PR #4 (branch
|
||||||
|
`bitcoinafterlife-patch-5`). No functional change to inheritance behavior.
|
||||||
|
|
||||||
|
**Outcome:** DONE.
|
||||||
|
|||||||
30
COMPATIBILITY.md
Normal file
30
COMPATIBILITY.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Wallet Compatibility
|
||||||
|
|
||||||
|
BAL (Bitcoin After Life) builds and signs Electrum transactions using
|
||||||
|
Electrum's own wallet and signing infrastructure. Its compatibility therefore
|
||||||
|
depends on the wallet type in use.
|
||||||
|
|
||||||
|
| Wallet type | Status | Notes |
|
||||||
|
|-------------------------------------------------|-----------------------------|-------|
|
||||||
|
| Standard wallet (single-signature, seed-based) | ✅ Supported | Primary, fully tested target |
|
||||||
|
| Hardware wallets (Ledger, Trezor, Coldcard, BitBox02, Jade, KeepKey, etc.) | ✅ Supported | Any hardware wallet supported by Electrum itself |
|
||||||
|
| Multisig wallets | ❌ Not yet supported | Known limitation identified 2026-07-18. Support is planned for a future plugin release. |
|
||||||
|
| Electrum TrustedCoin (2FA) wallets | ❓ Unknown / unsupported | Known limitation identified 2026-07-18. It has not yet been determined whether or when this will be addressed. |
|
||||||
|
|
||||||
|
## What "not supported" means in practice
|
||||||
|
|
||||||
|
For multisig and TrustedCoin (2FA) wallets, BAL's behavior has not been
|
||||||
|
verified and should be considered **unreliable**. Do not rely on BAL to
|
||||||
|
protect an inheritance set up on one of these wallet types until this document
|
||||||
|
is updated to mark them as supported.
|
||||||
|
|
||||||
|
## Electrum version compatibility
|
||||||
|
|
||||||
|
See [`README.md`](README.md) for supported Electrum versions (currently 4.7.2
|
||||||
|
and 4.8.0).
|
||||||
|
|
||||||
|
## Reporting compatibility issues
|
||||||
|
|
||||||
|
If you find a compatibility problem not listed here, please open an issue on
|
||||||
|
this repository describing the wallet type, Electrum version, and the exact
|
||||||
|
error or unexpected behavior observed.
|
||||||
99
COMPATIBILITY_ROADMAP.md
Normal file
99
COMPATIBILITY_ROADMAP.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Compatibility Roadmap: Multisig and TrustedCoin (2FA) Wallets
|
||||||
|
|
||||||
|
Status document, 2026-07-19. Companion to [`COMPATIBILITY.md`](COMPATIBILITY.md):
|
||||||
|
that file states *what* is supported today; this one explains *why* multisig and
|
||||||
|
TrustedCoin (2FA) wallets are currently unsupported and *how* support can be
|
||||||
|
added.
|
||||||
|
|
||||||
|
## Root cause (common to both)
|
||||||
|
|
||||||
|
BAL currently does two things that are only valid for standard
|
||||||
|
(single-signature) wallets:
|
||||||
|
|
||||||
|
1. **It builds transactions itself** with `PartialTransaction.from_io(...)`
|
||||||
|
(`bal/core/will.py`), bypassing `wallet.make_unsigned_transaction`.
|
||||||
|
2. **It signs with a single call** — `wallet.sign_transaction(tx, password)`
|
||||||
|
(`bal/gui/qt/window.py`, `sign_transactions`) — and considers the will ready
|
||||||
|
when `tx.is_complete()` is true.
|
||||||
|
|
||||||
|
On a standard wallet one signature completes the transaction. On multisig and
|
||||||
|
2FA wallets **one signature is not enough**: the transaction stays incomplete,
|
||||||
|
is never marked `COMPLETE`, and can never be pushed to will-executors.
|
||||||
|
|
||||||
|
A secondary single-sig assumption: `plugin.py` uses `wallet.get_keystore()`
|
||||||
|
(singular); multisig wallets expose `get_keystores()` (plural).
|
||||||
|
|
||||||
|
## Multisig wallets — solvable, medium/large effort
|
||||||
|
|
||||||
|
A 2-of-3 multisig wallet typically holds **only one** of the required private
|
||||||
|
keys locally; the other cosigners hold theirs. `wallet.sign_transaction` adds
|
||||||
|
the local signature only, and BAL has no flow to collect the missing ones.
|
||||||
|
|
||||||
|
**Proposed solution: the standard PSBT coordination round** (the same flow
|
||||||
|
Electrum itself uses for multisig spending):
|
||||||
|
|
||||||
|
1. Build and sign locally as today.
|
||||||
|
2. If the transaction is not complete, **export the partially-signed
|
||||||
|
transaction(s)** (file and/or QR) and mark the will with a new status such
|
||||||
|
as `WAITING_COSIGNERS`.
|
||||||
|
3. Each cosigner signs in their own Electrum (native feature — no new
|
||||||
|
software needed on their side).
|
||||||
|
4. BAL **re-imports and merges the signatures**; once complete, the will is
|
||||||
|
pushed to will-executors as today.
|
||||||
|
|
||||||
|
Notes and caveats:
|
||||||
|
|
||||||
|
- **Chained will transactions** (a will tx spending the change of a previous
|
||||||
|
will tx) remain workable: with segwit, the txid of an unsigned/partially
|
||||||
|
signed transaction is already stable, so the whole chain can be exported as
|
||||||
|
a batch of PSBTs in one round.
|
||||||
|
- **Every rebuild requires a new cosigner round.** Check Alive postponements
|
||||||
|
and balance-change rebuilds re-sign the will, so each of them needs the
|
||||||
|
cosigners again. This is inherent to multisig and must be clearly
|
||||||
|
communicated in the UI.
|
||||||
|
- Implementation surface: export/import/merge pipeline, GUI for it, the new
|
||||||
|
status in the transaction list, and tests.
|
||||||
|
|
||||||
|
Target: **next plugin release**, as announced.
|
||||||
|
|
||||||
|
## TrustedCoin (2FA) wallets — harder, with one blocking unknown
|
||||||
|
|
||||||
|
An Electrum 2FA wallet (`Wallet_2fa`, defined in Electrum's `trustedcoin`
|
||||||
|
plugin) is technically a **2-of-3 multisig whose second signer is the
|
||||||
|
TrustedCoin server**:
|
||||||
|
|
||||||
|
- signing requires a **one-time password (OTP) per transaction**
|
||||||
|
(`server.sign(short_id, raw_tx, otp)`);
|
||||||
|
- the server co-signs only transactions that include **its billing fee**,
|
||||||
|
which Electrum adds inside `Wallet_2fa.make_unsigned_transaction` — a code
|
||||||
|
path BAL currently bypasses (see root cause #1).
|
||||||
|
|
||||||
|
So today: no billing output, no OTP prompt, local signature only → incomplete
|
||||||
|
transaction.
|
||||||
|
|
||||||
|
Even with full integration (building via the wallet's
|
||||||
|
`make_unsigned_transaction`, adding the OTP prompt flow), one **decisive
|
||||||
|
unknown** remains: will transactions carry a **locktime years in the future**.
|
||||||
|
Whether the TrustedCoin server agrees to co-sign a transaction with such a
|
||||||
|
far-future `nLockTime` is an undocumented server-side policy. If it refuses,
|
||||||
|
2FA support is **not achievable** without TrustedCoin's cooperation. This is
|
||||||
|
why `COMPATIBILITY.md` marks 2FA as *unknown*.
|
||||||
|
|
||||||
|
**Proposed plan:**
|
||||||
|
|
||||||
|
1. **Empirical test on testnet** (cheap, decisive): create a test 2FA wallet,
|
||||||
|
build a far-future-locktime transaction through the proper 2FA path, and
|
||||||
|
check whether the server signs it.
|
||||||
|
2. If it signs → implement support: build via `make_unsigned_transaction`
|
||||||
|
(billing output included), integrate the OTP prompt, and document that
|
||||||
|
every rebuild costs one OTP round and TrustedCoin fees.
|
||||||
|
3. If it refuses → document 2FA as unsupported, with the practical
|
||||||
|
workaround: Electrum allows disabling 2FA by restoring the wallet from the
|
||||||
|
full seed, which turns it into a standard wallet — fully supported by BAL.
|
||||||
|
|
||||||
|
## Recommended order of work
|
||||||
|
|
||||||
|
1. **Multisig first**: deterministic path, standard Electrum tooling, already
|
||||||
|
announced for the next release.
|
||||||
|
2. **TrustedCoin empirical test in parallel**: low cost, and its outcome
|
||||||
|
decides whether 2FA support is feasible at all.
|
||||||
17
HANDOFF.md
17
HANDOFF.md
@@ -19,7 +19,7 @@
|
|||||||
services) can be paid a fee to broadcast the inheritance when due. The owner
|
services) can be paid a fee to broadcast the inheritance when due. The owner
|
||||||
periodically proves they are alive ("check-alive"); if the deadline passes,
|
periodically proves they are alive ("check-alive"); if the deadline passes,
|
||||||
the inheritance becomes spendable.
|
the inheritance becomes spendable.
|
||||||
- **Current version:** see `bal/VERSION` (last shipped: **0.4.8**).
|
- **Current version:** see the `"version"` field of `bal/manifest.json` (the single source of truth; read at runtime via `get_version()` in `bal/core/plugin_base.py`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -55,11 +55,10 @@ These are non-negotiable. They come from the owner directly.
|
|||||||
|
|
||||||
```
|
```
|
||||||
bal/ <- the plugin package (this is what ships in the ZIP)
|
bal/ <- the plugin package (this is what ships in the ZIP)
|
||||||
__init__.py <- __version__ (one of 4 version files)
|
__init__.py <- package docstring (no version here anymore)
|
||||||
VERSION <- plain-text version (one of 4 version files)
|
manifest.json <- plugin manifest, "version" field (SINGLE SOURCE OF TRUTH for the version)
|
||||||
manifest.json <- plugin manifest, "version" field (one of 4)
|
|
||||||
core/
|
core/
|
||||||
plugin_base.py <- __version__ "AUTOMATICALLY GENERATED" (one of 4)
|
plugin_base.py <- get_version() reads the version from manifest.json (zip-safe)
|
||||||
heirs.py <- HEIRS + transaction building (prepare_lists,
|
heirs.py <- HEIRS + transaction building (prepare_lists,
|
||||||
prepare_transactions, buildTransactions). CORE LOGIC.
|
prepare_transactions, buildTransactions). CORE LOGIC.
|
||||||
will.py <- Will/WillItem, validation (check_amounts, check_will),
|
will.py <- Will/WillItem, validation (check_amounts, check_will),
|
||||||
@@ -115,13 +114,11 @@ find bal -name "__pycache__" -type d -exec rm -rf {} + ; find bal -name "*.pyc"
|
|||||||
python3 build_zip.py bal-electrum-plugin-vX.Y.Z.zip # produces 37 files
|
python3 build_zip.py bal-electrum-plugin-vX.Y.Z.zip # produces 37 files
|
||||||
```
|
```
|
||||||
|
|
||||||
**Bump version — there are FOUR files, keep them in sync:**
|
**Bump version — ONE file only (single source of truth):**
|
||||||
```
|
```
|
||||||
bal/core/plugin_base.py -> __version__ = "X.Y.Z" # AUTOMATICALLY GENERATED DO NOT EDIT
|
|
||||||
bal/__init__.py -> __version__ = "X.Y.Z"
|
|
||||||
bal/VERSION -> X.Y.Z
|
|
||||||
bal/manifest.json -> "version": "X.Y.Z",
|
bal/manifest.json -> "version": "X.Y.Z",
|
||||||
```
|
```
|
||||||
|
The code reads this at runtime via `get_version()` in `bal/core/plugin_base.py` (exposed as the `BalPlugin.version` property), so there is nothing else to keep in sync. There is no longer a `bal/VERSION` file nor a hardcoded `__version__`.
|
||||||
|
|
||||||
**IMPORTANT for the owner when testing:** after installing a ZIP, the owner
|
**IMPORTANT for the owner when testing:** after installing a ZIP, the owner
|
||||||
must **fully restart Electrum** (not just reload the plugin) — Electrum's
|
must **fully restart Electrum** (not just reload the plugin) — Electrum's
|
||||||
@@ -292,7 +289,7 @@ must **fully restart Electrum** (not just reload the plugin) — Electrum's
|
|||||||
## 7. How to resume (checklist for the next AI)
|
## 7. How to resume (checklist for the next AI)
|
||||||
|
|
||||||
1. Read this file, then `CHANGELOG.md` (last entries) and `.agent_memory_tasks.md`.
|
1. Read this file, then `CHANGELOG.md` (last entries) and `.agent_memory_tasks.md`.
|
||||||
2. Confirm the environment: `git status`, current branch, `bal/VERSION`.
|
2. Confirm the environment: `git status`, current branch, and the `"version"` field of `bal/manifest.json`.
|
||||||
3. Run the full test suite (Section 3) — expect all green (266 as of v0.4.8).
|
3. Run the full test suite (Section 3) — expect all green (266 as of v0.4.8).
|
||||||
4. Talk to the owner in **Italian**, write everything else in **English**.
|
4. Talk to the owner in **Italian**, write everything else in **English**.
|
||||||
5. For any change: present a PLAN, wait for "OK" (R4), then implement, test,
|
5. For any change: present a PLAN, wait for "OK" (R4), then implement, test,
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -38,10 +38,19 @@ tests/ smoke + external-zip regression tests
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- **Electrum 4.7.2** — the last stable release exposing `json_db.register_dict`,
|
- **Electrum 4.7.2 or 4.8.0** — the plugin detects which wallet-DB
|
||||||
which this plugin relies on. Newer versions removed it.
|
registration API is available (`json_db.register_dict` on 4.7.2,
|
||||||
|
`stored_dict.register_name` on 4.8.0) and adapts automatically.
|
||||||
- **PyQt6** (bundled with the Electrum desktop GUI).
|
- **PyQt6** (bundled with the Electrum desktop GUI).
|
||||||
|
|
||||||
|
## Wallet compatibility
|
||||||
|
|
||||||
|
BAL currently supports **standard (single-signature) wallets** and
|
||||||
|
**hardware wallets** supported by Electrum. **Multisig wallets** and
|
||||||
|
**Electrum TrustedCoin (2FA) wallets** are **not yet supported** — see
|
||||||
|
[`COMPATIBILITY.md`](COMPATIBILITY.md) for the full compatibility matrix and
|
||||||
|
current status.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Build the distribution archive
|
### Build the distribution archive
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
0.6.0
|
|
||||||
@@ -36,4 +36,7 @@ The plugin supports Electrum 4.7.2 and 4.8.0 with PyQt6. Electrum 4.8.0 removed
|
|||||||
available and adapts, so both releases keep working.
|
available and adapts, so both releases keep working.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.5.18"
|
# 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.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ serialised together with the wallet file.
|
|||||||
This module performs **no** GUI work and imports nothing from PyQt / electrum.gui.
|
This module performs **no** GUI work and imports nothing from PyQt / electrum.gui.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
@@ -33,6 +34,45 @@ from electrum.transaction import tx_from_any
|
|||||||
_logger = get_logger(__name__)
|
_logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Plugin version - single source of truth
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# The version lives ONLY in bal/manifest.json (the file Electrum itself reads).
|
||||||
|
# We used to hardcode it in four files and keep them in sync with a pre-commit
|
||||||
|
# hook; reading it from the manifest removes that duplication.
|
||||||
|
#
|
||||||
|
# importlib.resources is used on purpose: it reads a data file bundled inside
|
||||||
|
# the ``bal`` package and works identically whether the plugin runs from an
|
||||||
|
# extracted directory or from INSIDE a zip (Electrum loads external plugins via
|
||||||
|
# zipimport). It never builds a path by hand, so there is no os.path.join
|
||||||
|
# backslash issue on Windows inside a zip.
|
||||||
|
_VERSION_CACHE = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_version():
|
||||||
|
"""Return the plugin version from ``bal/manifest.json`` (cached).
|
||||||
|
|
||||||
|
Zip-safe and independent of the current working directory. Falls back to
|
||||||
|
``"unknown"`` if the manifest cannot be read, so importing the plugin never
|
||||||
|
fails just because of version lookup.
|
||||||
|
"""
|
||||||
|
global _VERSION_CACHE
|
||||||
|
if _VERSION_CACHE is None:
|
||||||
|
try:
|
||||||
|
import importlib.resources
|
||||||
|
|
||||||
|
data = (
|
||||||
|
importlib.resources.files("bal")
|
||||||
|
.joinpath("manifest.json")
|
||||||
|
.read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
_VERSION_CACHE = json.loads(data)["version"]
|
||||||
|
except Exception as e: # noqa: BLE001 - never break import over version
|
||||||
|
_logger.error(f"failed to read version from manifest.json: {e}")
|
||||||
|
_VERSION_CACHE = "unknown"
|
||||||
|
return _VERSION_CACHE
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Wallet-DB registration
|
# Wallet-DB registration
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -120,9 +160,6 @@ class BalPlugin(BasePlugin):
|
|||||||
layer (or unit tests) can use the plugin logic without importing Qt.
|
layer (or unit tests) can use the plugin logic without importing Qt.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_version = None
|
|
||||||
__version__ = "0.5.18" # AUTOMATICALLY GENERATED DO NOT EDIT
|
|
||||||
|
|
||||||
# Command used to open an .ics calendar file, per operating system.
|
# Command used to open an .ics calendar file, per operating system.
|
||||||
default_app = {
|
default_app = {
|
||||||
"Linux": "xdg-open",
|
"Linux": "xdg-open",
|
||||||
@@ -138,18 +175,11 @@ class BalPlugin(BasePlugin):
|
|||||||
# Default geometry hint for some dialogs (kept from the original code).
|
# Default geometry hint for some dialogs (kept from the original code).
|
||||||
SIZE = (159, 97)
|
SIZE = (159, 97)
|
||||||
|
|
||||||
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
"""Return the plugin version, read once from the ``VERSION`` file."""
|
"""Plugin version, read from ``bal/manifest.json`` (single source of
|
||||||
if not self._version:
|
truth). See :func:`get_version`."""
|
||||||
try:
|
return get_version()
|
||||||
f = ""
|
|
||||||
with open("{}/VERSION".format(self.plugin_dir), "r") as fi:
|
|
||||||
f = str(fi.read())
|
|
||||||
self._version = f.strip()
|
|
||||||
except Exception as e:
|
|
||||||
_logger.error(f"failed to get version: {e}")
|
|
||||||
self._version = "unknown"
|
|
||||||
return self._version
|
|
||||||
|
|
||||||
def __init__(self, parent, config, name):
|
def __init__(self, parent, config, name):
|
||||||
self.logger = get_logger(__name__)
|
self.logger = get_logger(__name__)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from electrum.i18n import _
|
|||||||
from electrum.logging import get_logger
|
from electrum.logging import get_logger
|
||||||
from electrum.network import Network
|
from electrum.network import Network
|
||||||
|
|
||||||
from .plugin_base import BalPlugin
|
from .plugin_base import BalPlugin, get_version
|
||||||
|
|
||||||
# Per-request timeout (seconds) for interactive operations (ping / info /
|
# Per-request timeout (seconds) for interactive operations (ping / info /
|
||||||
# list download). These fail fast (no retries) so a dead server does not
|
# list download). These fail fast (no retries) so a dead server does not
|
||||||
@@ -272,7 +272,7 @@ class Willexecutors:
|
|||||||
raise Exception("You are offline.")
|
raise Exception("You are offline.")
|
||||||
_logger.debug(f"<-- {method} {url} {data}")
|
_logger.debug(f"<-- {method} {url} {data}")
|
||||||
headers = {}
|
headers = {}
|
||||||
headers["user-agent"] = f"BalPlugin v:{BalPlugin.__version__}"
|
headers["user-agent"] = f"BalPlugin v:{get_version()}"
|
||||||
headers["Content-Type"] = "text/plain"
|
headers["Content-Type"] = "text/plain"
|
||||||
if not handle_response:
|
if not handle_response:
|
||||||
handle_response = Willexecutors.handle_response
|
handle_response = Willexecutors.handle_response
|
||||||
|
|||||||
@@ -1533,7 +1533,7 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
"BEGIN:VCALENDAR",
|
"BEGIN:VCALENDAR",
|
||||||
"VERSION:2.0",
|
"VERSION:2.0",
|
||||||
"PRODID:-//Bitcoin After Life//Electrum Plugin/"
|
"PRODID:-//Bitcoin After Life//Electrum Plugin/"
|
||||||
f"{BalPlugin.__version__}",
|
f"{self.bal_window.bal_plugin.version}",
|
||||||
]
|
]
|
||||||
|
|
||||||
total = len(offsets)
|
total = len(offsets)
|
||||||
|
|||||||
@@ -1153,7 +1153,7 @@ class WillSettingsWidget(QWidget):
|
|||||||
lines = [
|
lines = [
|
||||||
"BEGIN:VCALENDAR",
|
"BEGIN:VCALENDAR",
|
||||||
"VERSION:2.0",
|
"VERSION:2.0",
|
||||||
f"PRODID:-//Bitcoin After Life//Electrum Plugin/{BalPlugin.__version__}",
|
f"PRODID:-//Bitcoin After Life//Electrum Plugin/{self.bal_window.bal_plugin.version}",
|
||||||
]
|
]
|
||||||
|
|
||||||
# One separate VEVENT per reminder offset (its own date in the calendar).
|
# One separate VEVENT per reminder offset (its own date in the calendar).
|
||||||
@@ -1284,7 +1284,7 @@ class WillSettingsWidget(QWidget):
|
|||||||
"BEGIN:VCALENDAR",
|
"BEGIN:VCALENDAR",
|
||||||
"VERSION:2.0",
|
"VERSION:2.0",
|
||||||
"PRODID:-//Bitcoin After Life//Electrum Plugin/"
|
"PRODID:-//Bitcoin After Life//Electrum Plugin/"
|
||||||
f"{BalPlugin.__version__}",
|
f"{self.bal_window.bal_plugin.version}",
|
||||||
]
|
]
|
||||||
|
|
||||||
total = len(offsets)
|
total = len(offsets)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bal",
|
"name": "bal",
|
||||||
"fullname": "Bitcoin After Life",
|
"fullname": "Bitcoin After Life",
|
||||||
"version": "0.5.18",
|
"version": "0.6.1",
|
||||||
"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.",
|
"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",
|
"author": "Svatantrya",
|
||||||
"licence": "MIT",
|
"licence": "MIT",
|
||||||
|
|||||||
116
tests/test_version_source.py
Normal file
116
tests/test_version_source.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""Tests for the single-source-of-truth plugin version.
|
||||||
|
|
||||||
|
The plugin version must come ONLY from ``bal/manifest.json`` (no hardcoded
|
||||||
|
copies, no ``bal/VERSION`` file), and it must be readable both from an extracted
|
||||||
|
package and from INSIDE a zip (the way Electrum loads external plugins via
|
||||||
|
zipimport). These tests lock that behavior.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
|
||||||
|
python3 -m pytest tests/test_version_source.py -q
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
import bal
|
||||||
|
from bal.core.plugin_base import BalPlugin, get_version
|
||||||
|
|
||||||
|
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
_MANIFEST = os.path.join(_REPO, "bal", "manifest.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _manifest_version():
|
||||||
|
with open(_MANIFEST, encoding="utf-8") as f:
|
||||||
|
return json.load(f)["version"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_version_matches_manifest():
|
||||||
|
"""get_version() returns exactly the manifest 'version' field."""
|
||||||
|
assert get_version() == _manifest_version()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_version_is_not_unknown():
|
||||||
|
"""The manifest must be readable - never the 'unknown' fallback here."""
|
||||||
|
assert get_version() != "unknown"
|
||||||
|
# a plausible semantic version, e.g. 0.6.0
|
||||||
|
assert get_version()[0].isdigit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_property_matches_get_version():
|
||||||
|
"""BalPlugin.version is a property returning the same value."""
|
||||||
|
assert isinstance(BalPlugin.version, property)
|
||||||
|
# A bare object works as ``self`` because the property ignores instance
|
||||||
|
# state and simply delegates to get_version().
|
||||||
|
class _Dummy:
|
||||||
|
version = BalPlugin.version
|
||||||
|
assert _Dummy().version == get_version()
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_hardcoded_class_version():
|
||||||
|
"""The old hardcoded BalPlugin.__version__ constant is gone."""
|
||||||
|
assert "__version__" not in vars(BalPlugin)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_version_file_in_package():
|
||||||
|
"""The bal/VERSION file has been removed (manifest is the only source)."""
|
||||||
|
assert not os.path.exists(os.path.join(_REPO, "bal", "VERSION"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_version_cached():
|
||||||
|
"""Second call returns the cached value (same object)."""
|
||||||
|
v1 = get_version()
|
||||||
|
v2 = get_version()
|
||||||
|
assert v1 == v2
|
||||||
|
assert v1 is v2 # cached string, identical object
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_readable_from_inside_zip(tmp_path):
|
||||||
|
"""The version must be readable when 'bal' is imported from a zip.
|
||||||
|
|
||||||
|
Electrum loads external plugins via zipimport, so this is the real
|
||||||
|
production scenario (and the one that used to break on Windows with
|
||||||
|
os.path.join). We build a zip of the bal package and read the version in a
|
||||||
|
fresh interpreter whose only path to 'bal' is that zip.
|
||||||
|
"""
|
||||||
|
bal_dir = os.path.dirname(os.path.abspath(bal.__file__))
|
||||||
|
zip_path = tmp_path / "bal_plugin.zip"
|
||||||
|
with zipfile.ZipFile(zip_path, "w") as z:
|
||||||
|
for root, _dirs, files in os.walk(bal_dir):
|
||||||
|
for fn in files:
|
||||||
|
if fn.endswith(".pyc"):
|
||||||
|
continue
|
||||||
|
full = os.path.join(root, fn)
|
||||||
|
# arcname keeps the leading 'bal/' package prefix
|
||||||
|
arc = os.path.join(
|
||||||
|
"bal", os.path.relpath(full, bal_dir)
|
||||||
|
)
|
||||||
|
z.write(full, arc)
|
||||||
|
|
||||||
|
# electrum must stay importable in the child, but 'bal' must resolve ONLY
|
||||||
|
# from the zip. So: drop the on-disk repo (and this package's dir) from the
|
||||||
|
# child's path, prepend the zip, and run from a neutral working directory.
|
||||||
|
repo_real = os.path.realpath(_REPO)
|
||||||
|
bal_parent_real = os.path.realpath(os.path.dirname(bal_dir))
|
||||||
|
filtered = [
|
||||||
|
p for p in sys.path
|
||||||
|
if p and os.path.realpath(p) not in (repo_real, bal_parent_real)
|
||||||
|
]
|
||||||
|
child_path = os.pathsep.join([str(zip_path)] + filtered)
|
||||||
|
env = dict(os.environ, PYTHONPATH=child_path, QT_QPA_PLATFORM="offscreen")
|
||||||
|
code = (
|
||||||
|
"import bal, os;"
|
||||||
|
"assert 'bal_plugin.zip' in bal.__file__.replace(os.sep, '/'),"
|
||||||
|
" 'bal not loaded from zip: ' + bal.__file__;"
|
||||||
|
"from bal.core.plugin_base import get_version;"
|
||||||
|
"print(get_version())"
|
||||||
|
)
|
||||||
|
out = subprocess.run(
|
||||||
|
[sys.executable, "-c", code], env=env, capture_output=True, text=True,
|
||||||
|
cwd=str(tmp_path),
|
||||||
|
)
|
||||||
|
assert out.returncode == 0, f"child failed: {out.stderr}"
|
||||||
|
assert out.stdout.strip() == _manifest_version()
|
||||||
Reference in New Issue
Block a user