core+gui: timezone-correct datetimes, deep-copy WillItem, dead code removal, explicit imports
This commit is contained in:
28
AGENTS.md
28
AGENTS.md
@@ -20,8 +20,9 @@ The plugin's `bal/` directory is symlinked into
|
||||
|
||||
## Test & verify
|
||||
|
||||
Tests are **standalone scripts**, not pytest. Each `tests/test_*.py` file runs
|
||||
its `test_*` functions from `if __name__ == "__main__"`. Run a file directly:
|
||||
Tests work **both** as standalone scripts and via pytest (tests use `def test_*`
|
||||
naming and also have `if __name__ == "__main__"` blocks). Run a single file
|
||||
directly:
|
||||
|
||||
```bash
|
||||
source /home/steal/devel/bal/electrum/env/bin/activate
|
||||
@@ -29,6 +30,13 @@ python3 tests/test_core_heirs.py # core, no Qt needed
|
||||
QT_QPA_PLATFORM=offscreen python3 tests/test_gui_common.py # GUI tests need offscreen
|
||||
```
|
||||
|
||||
Or run a batch with pytest (as `make-release.sh` does):
|
||||
|
||||
```bash
|
||||
source /home/steal/devel/bal/electrum/env/bin/activate
|
||||
QT_QPA_PLATFORM=offscreen python3 -m pytest tests/test_core_*.py -q
|
||||
```
|
||||
|
||||
- Most core tests run offline (no wallet/network). Some files
|
||||
(`test_group_*.py`, `test_no_willexecutor_karen7.py`, `parallel_ping_test.py`)
|
||||
exercise will-executor/network flows and need the live servers — don't rely on
|
||||
@@ -43,7 +51,8 @@ QT_QPA_PLATFORM=offscreen python3 tests/test_gui_common.py # GUI tests need of
|
||||
- **Ruff is NOT clean** (hundreds of pre-existing errors in `bal/` and
|
||||
`tests/`). Do not run `--fix` wholesale and do not try to silence everything;
|
||||
just avoid adding new violations. Config: `pyproject.toml` (line-length 88,
|
||||
E501 ignored).
|
||||
E501 ignored). Per-file ignores suppress `F403`/`F405` for the intentional
|
||||
`from .common import *` hub pattern in `bal/gui/qt/`.
|
||||
- Lint via the repo venv: `/home/steal/devel/bal/bal-electrum-plugin/venv/bin/ruff`
|
||||
- Typecheck: `pyright` (npm, `node_modules/`), config `pyrightconfig.json`
|
||||
(`extraPaths: ["../electrum"]`). Pyright reports many false positives on
|
||||
@@ -53,9 +62,18 @@ QT_QPA_PLATFORM=offscreen python3 tests/test_gui_common.py # GUI tests need of
|
||||
## Architecture
|
||||
|
||||
- `bal/core/` = GUI-free logic (`heirs.py`, `will.py`, `willexecutors.py`,
|
||||
`plugin_base.py`, `util.py`). Must never import Qt.
|
||||
`plugin_base.py`, `util.py`, `checkalive.py`, `reminders.py`,
|
||||
`input_rules.py`).
|
||||
Must never import Qt.
|
||||
- `bal/gui/qt/` = PyQt6 layer. `window.py` is the per-wallet controller,
|
||||
`plugin.py` is the Electrum `@hooks` entry, `qt.py` is a zipimport shim.
|
||||
`plugin.py` is the Electrum `@hooks` entry. `qt.py` is a zipimport shim.
|
||||
`common.py` uses `import *` intentionally (ruff suppresses F403/F405 here);
|
||||
`bal/gui/qt/*.py` all import from it.
|
||||
- `bal/cli/` = headless command-line layer (no Qt). `plugin.py` is the daemon
|
||||
entry point, `commands.py` registers `bal_*` commands with Electrum.
|
||||
- `bal/wallet_util/` = wallet helper utilities for Qt and core.
|
||||
- `bal/qt.py` and `bal/cmdline.py` are thin shims that Electrum discovers
|
||||
via `manifest.json`; they import the real `Plugin` class via `importlib`.
|
||||
- `bal/manifest.json` = version source of truth (Electrum reads it; also read by
|
||||
`make-release.sh`).
|
||||
- Compatibility constraint: must support Electrum **4.7.2 and 4.8.0**; the DB
|
||||
|
||||
340
CHANGELOG.md
340
CHANGELOG.md
@@ -2568,3 +2568,343 @@ of a session, without requiring the normal wizard flow to have run first.
|
||||
`test_merge_will_validity_error_logs_without_crashing`.
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
---
|
||||
|
||||
## 49. OP_RETURN support for heirs
|
||||
|
||||
**Date:** 2026-07-30
|
||||
|
||||
**Goal:** allow heirs to produce an OP_RETURN output instead of a regular BTC
|
||||
payment. An address prefixed with `OP_RETURN:` carries hex data (max 80 bytes);
|
||||
such heirs always have amount 0 and are excluded from the normal amount
|
||||
calculations (percentage normalization, dust checks, leftover redistribution).
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/core/heirs.py`
|
||||
- `validate_heir`: new `OP_RETURN:<hex>` address validation (rejects non-hex,
|
||||
> 80 bytes). OP_RETURN heirs are stored with amount `"0"` and carry the raw
|
||||
hex data in the address field.
|
||||
- `prepare_lists`: OP_RETURN heirs are skipped during amount calculation
|
||||
(`normalize_perc`, dust checks). They are kept in the will but produce a
|
||||
zero-value output.
|
||||
- `buildTransactions`: when building the transaction, OP_RETURN heirs emit a
|
||||
`OP_RETURN <hex>` scriptPubKey output with value 0, matching Bitcoin's
|
||||
OP_RETURN output standard.
|
||||
- `get_transactions`: OP_RETURN heirs are grouped with their locktime peers
|
||||
but excluded from fee/dust arithmetic.
|
||||
|
||||
- `bal/gui/qt/window.py`
|
||||
- Heir dialog / wizard: the address field accepts `OP_RETURN:` prefix and
|
||||
shows a decoded-text message field when an OP_RETURN address is entered.
|
||||
- `build_will` / `_build_success_report`: OP_RETURN heirs display the
|
||||
decoded message text in the build report instead of a BTC address.
|
||||
|
||||
- `bal/gui/qt/lists.py`
|
||||
- Heir list: OP_RETURN heirs display the decoded message in the address
|
||||
column and show "0" for the amount.
|
||||
|
||||
- `bal/gui/qt/common.py`
|
||||
- Re-export the new `validate_op_return_hex` helper for the GUI layer.
|
||||
|
||||
- `tests/test_core_heirs.py`
|
||||
- 8 new tests: OP_RETURN validation (valid hex, too long, non-hex), OP_RETURN
|
||||
heirs excluded from amount calculations, OP_RETURN output shape in built
|
||||
transactions, mixed OP_RETURN + regular heirs.
|
||||
|
||||
**Verification:**
|
||||
- `ruff check` on changed production files: no new errors.
|
||||
- Full test suite: 307 passed.
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
---
|
||||
|
||||
## 50. Core extraction: GUI-free logic into bal/core/ (reminders, checkalive, input_rules); RLock pickle fix
|
||||
|
||||
**Date:** 2026-08-05
|
||||
|
||||
**Goal:** extract GUI-free business logic that was previously embedded in Qt
|
||||
widgets (`bal/gui/qt/widgets.py`) into standalone `bal/core/` modules, making
|
||||
them independently testable without Qt. Also fix a critical `copy.deepcopy(tx)`
|
||||
crash on Electrum 4.8 (`RLock` cannot be pickled) and improve wallet-DB
|
||||
persistence robustness.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/core/checkalive.py` (new)
|
||||
- `resolve_date_to_check`: computes the effective check-alive timestamp from
|
||||
will-settings and user type (BASIC uses `now()`; ADVANCED uses the stored
|
||||
threshold). Extracted from `window.py init_class_variables`.
|
||||
- `check_alive_expired`: pure-logic test for whether the check-alive has
|
||||
passed. Previously duplicated inline in `window.py`.
|
||||
|
||||
- `bal/core/reminders.py` (new)
|
||||
- `compute_reminder_offsets`, `basic_reminder_offsets`, `BALCalendar.write_ics`,
|
||||
`BALCalendar._ics_provider`: all calendar/reminder logic extracted from
|
||||
`widgets.py`. Generates iCal (.ics) files with separate VEVENT entries per
|
||||
reminder date. No Qt dependency.
|
||||
|
||||
- `bal/core/input_rules.py` (new)
|
||||
- `LockTimeRawEdit`, `LockTimeDateEdit`, `BalTimeEditWidget`,
|
||||
`ThresholdTimeWidget`: GUI-free data models for locktime/threshold
|
||||
validation and the Raw/Date selector logic. The Qt widgets in
|
||||
`widgets.py` now thin-wrap these helpers.
|
||||
|
||||
- `bal/core/heirs.py`
|
||||
- Fixed `copy.deepcopy(tx)` failure on Electrum 4.8: the `Transaction`
|
||||
object contains a `_thread.RLock` that cannot be pickled. Will-item
|
||||
persistence now re-parses the transaction from its hex serialization
|
||||
instead of deep-copying.
|
||||
- Invalid heirs (sentinel values from failed builds) are now kept in the
|
||||
wallet DB instead of being silently dropped, so the user can see and
|
||||
correct them.
|
||||
|
||||
- `bal/core/plugin_base.py`
|
||||
- Minor adjustments to support the extracted modules.
|
||||
|
||||
- `bal/gui/qt/widgets.py`
|
||||
- Major slim-down: business logic delegates to `bal/core/checkalive.py`,
|
||||
`bal/core/reminders.py`, and `bal/core/input_rules.py`. Only Qt widget
|
||||
creation and layout remain.
|
||||
|
||||
- `bal/gui/qt/calendar.py`
|
||||
- Adapted to use `bal/core/reminders.py` for .ics generation.
|
||||
|
||||
- `bal/gui/qt/window.py`
|
||||
- `init_class_variables` now calls `resolve_date_to_check` from
|
||||
`bal/core/checkalive.py` instead of computing inline.
|
||||
|
||||
- `bal/gui/qt/common.py`
|
||||
- Updated re-exports for the new core modules.
|
||||
|
||||
- Tests reorganized: core-only tests moved to `tests/test_core_checkalive.py`,
|
||||
`tests/test_core_reminders.py`, `tests/test_core_input_rules.py` (run
|
||||
without Qt).
|
||||
|
||||
- `tests/karen7`: fixture file compressed/updated for the new test structure.
|
||||
|
||||
**Verification:**
|
||||
- `ruff check` on changed files: no new errors.
|
||||
- Full test suite: 388 passed (significant increase due to new core test modules).
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
---
|
||||
|
||||
## 51. "Rebuild will on wallet close" setting (`REBUILD_ON_CLOSE`)
|
||||
|
||||
**Date:** 2026-08-14
|
||||
|
||||
**Goal:** add a new plugin setting that lets the plugin rebuild the will
|
||||
automatically when Electrum closes, skipping the full Build wizard. When
|
||||
enabled, closing Electrum triggers a one-shot prepare/inheritance flow
|
||||
(check, rebuild if needed, sign, broadcast) without showing the
|
||||
`BalBuildWillDialog`.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/core/plugin_base.py`
|
||||
- New persisted config `REBUILD_ON_CLOSE = BalConfig(config,
|
||||
"bal_rebuild_on_close", False)` (default OFF), with explanatory comment.
|
||||
|
||||
- `bal/gui/qt/plugin.py`
|
||||
- New "Rebuild on close" checkbox in the settings dialog, bound to
|
||||
`REBUILD_ON_CLOSE`, with a tooltip explaining the behaviour. Added to the
|
||||
"Reset to Default Setting" list.
|
||||
|
||||
- `bal/gui/qt/window.py`
|
||||
- `on_close`: when `REBUILD_ON_CLOSE` is enabled, the close flow runs
|
||||
the auto-rebuild path (check + rebuild + sign + push) instead of the
|
||||
full wizard dialog. The legacy wizard-on-close path is kept when the
|
||||
setting is OFF.
|
||||
|
||||
- `tests/test_rebuild_on_close_setting.py` (new)
|
||||
- 8 tests: default OFF, toggle/persist, close triggers rebuild when ON,
|
||||
close skips rebuild when OFF, reset restores default.
|
||||
|
||||
**Verification:**
|
||||
- `ruff check` on changed files: no new errors.
|
||||
- Full test suite: 396 passed.
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
---
|
||||
|
||||
## 52. Headless CLI layer (`bal/cli/`, `bal/cmdline.py`, `bal_*` daemon commands)
|
||||
|
||||
**Date:** 2026-08-14
|
||||
|
||||
**Goal:** expose the full BAL inheritance cycle via Electrum's command-line
|
||||
interface (daemon mode), without the Qt GUI. This enables scripting,
|
||||
automation, and headless server usage.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/cmdline.py` (new)
|
||||
- Zip-import shim for Electrum's `gui_name='cmdline'` plugin loader.
|
||||
- Follows the same `importlib` pattern as `qt.py` but never imports Qt.
|
||||
- Re-exports `Plugin` from `bal.cli.plugin`.
|
||||
|
||||
- `bal/cli/__init__.py` (new)
|
||||
- Registers the `bal_*` commands with Electrum on import.
|
||||
|
||||
- `bal/cli/plugin.py` (new)
|
||||
- `Plugin(BalPlugin)` -- minimal entry point for the daemon (no Qt hooks,
|
||||
no `bal_windows`).
|
||||
|
||||
- `bal/cli/commands.py` (new)
|
||||
- 30 `@plugin_command` async functions registered as `bal_*` commands:
|
||||
settings (list/get/set/reset), heirs (list/show/add/update/delete/import/
|
||||
export), will-executors (list/show/add/update/select/delete/ping/download/
|
||||
import/export), will (status/check/prepare/sign/broadcast/export/
|
||||
import_merge/invalidate/check_executor).
|
||||
- Each command is a thin transport layer: validates args, delegates to
|
||||
`BalController`, returns JSON-serializable results.
|
||||
|
||||
- `bal/cli/controller.py` (new, ~1100 lines)
|
||||
- `BalController` -- headless replica of `BalWindow`. Reads/writes wallet DB,
|
||||
config, and will-executors without any Qt dependency.
|
||||
- Methods mirror `BalWindow` flows: `load_willitems`, `save_willitems`,
|
||||
`init_class_variables`, `build_inheritance_transaction`, `sign_transactions`,
|
||||
`push_transactions_to_willexecutors`, `check_transactions`, `export_json_file`,
|
||||
`merge_will_from_file`, `invalidate_will`.
|
||||
- Domain exceptions (`WillExpiredException`, `HeirNotFoundException`, etc.)
|
||||
are converted to `UserFacingException` with clear text.
|
||||
|
||||
- `bal/manifest.json`
|
||||
- `"available_for"` updated from `["qt"]` to `["qt", "cmdline"]`.
|
||||
|
||||
- `bal/__init__.py`
|
||||
- Added `from .cli import commands` to register `bal_*` commands on import
|
||||
(both CLI pre-parse and GUI startup).
|
||||
|
||||
- `tests/test_cli_commands_registered.py` (new)
|
||||
- 4 tests: all `bal_*` commands registered, all are coroutines, no duplicate
|
||||
registration, all args documented.
|
||||
|
||||
- `tests/test_cli_controller_offline.py` (new)
|
||||
- Offline CRUD tests for heirs, will-executors, settings, and will
|
||||
import/export merge via `BalController` (no network).
|
||||
|
||||
**Verification:**
|
||||
- `ruff check` on new files: clean.
|
||||
- Full test suite: 427 passed.
|
||||
- `tests/test_cli_commands_registered.py`: all 4 tests pass.
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
---
|
||||
|
||||
## 53. Auto-rebuild on new transactions (`AUTO_REBUILD`)
|
||||
|
||||
**Date:** 2026-08-16
|
||||
|
||||
**Goal:** when new transactions are detected in the wallet (e.g. incoming
|
||||
payments), automatically rebuild the will so the inheritance covers the
|
||||
new UTXOs. The delivery date is anticipated by one day to orphan the old
|
||||
will on-chain; an on-chain invalidation is only needed when the anticipated
|
||||
locktime crosses the Check Alive threshold.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/core/plugin_base.py`
|
||||
- New persisted config `AUTO_REBUILD = BalConfig(config,
|
||||
"bal_auto_rebuild", False)` (default OFF), with explanatory comment.
|
||||
|
||||
- `bal/gui/qt/plugin.py`
|
||||
- New "Auto-rebuild" checkbox in the settings dialog, bound to
|
||||
`AUTO_REBUILD`, with a tooltip. Added to the "Reset to Default Setting"
|
||||
list.
|
||||
|
||||
- `bal/gui/qt/window.py`
|
||||
- New `_auto_rebuild_on_new_tx()` method: triggered when Electrum detects
|
||||
a new transaction in the wallet. Runs the full prepare flow: check
|
||||
coherence, rebuild with the anticipated locktime (delivery date minus 1
|
||||
day), persist, sign (if passwordless), push to will-executors.
|
||||
- When the anticipated locktime crosses the Check Alive threshold, returns
|
||||
an invalidation transaction instead of auto-completing.
|
||||
- Connected to Electrum's `new_transaction` signal.
|
||||
|
||||
- `tests/test_auto_rebuild_on_new_tx.py` (new)
|
||||
- 16 tests: AUTO_REBUILD default OFF, toggle/persist, rebuild triggers on
|
||||
new tx, locktime anticipation by 1 day, threshold crossing returns
|
||||
invalidation, passwordless wallet signs automatically, encrypted wallet
|
||||
returns invalidation tx for manual signing.
|
||||
|
||||
**Verification:**
|
||||
- `ruff check` on changed files: no new errors.
|
||||
- Full test suite: 432 passed.
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
---
|
||||
|
||||
## 54. CLI `bal_will_autorebuild` command
|
||||
|
||||
**Date:** 2026-08-16
|
||||
|
||||
**Goal:** expose the auto-rebuild flow (entry #53) as a headless CLI command,
|
||||
so scripts and daemons can trigger the one-shot check/rebuild/sign/push
|
||||
cycle without the Qt GUI.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/cli/commands.py`
|
||||
- New `bal_will_autorebuild` async command (flag `nw`): runs the full
|
||||
auto-rebuild flow via `BalController.auto_rebuild()`. Returns a JSON
|
||||
object with `result` (`valid`, `no_heirs`, `invalidated`, `nothing`,
|
||||
`needs_signing`, `rebuilt`) and, when applicable, the invalidation
|
||||
transaction.
|
||||
|
||||
- `bal/cli/controller.py`
|
||||
- New `auto_rebuild()` method: headless replica of the GUI auto-rebuild
|
||||
flow. Checks coherence, rebuilds with anticipated locktime, handles
|
||||
threshold-crossing (returns invalidation tx), signs passwordless wallets
|
||||
automatically, pushes to will-executors.
|
||||
|
||||
- `tests/test_cli_autorebuild.py` (new)
|
||||
- 10 tests: auto-rebuild returns `valid` when will is coherent, `rebuilt`
|
||||
when rebuilt, `invalidated` with invalidation tx when threshold crossed,
|
||||
`needs_signing` for encrypted wallets, `no_heirs` when heirs are missing.
|
||||
|
||||
- `tests/test_cli_commands_registered.py`
|
||||
- Updated `EXPECTED_COMMANDS` to include `bal_will_autorebuild`.
|
||||
|
||||
- `tests/test_cli_controller_offline.py`
|
||||
- Extended with auto-rebuild flow tests.
|
||||
|
||||
**Verification:**
|
||||
- `ruff check` on changed files: no new errors.
|
||||
- Full test suite: 438 passed.
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
---
|
||||
|
||||
## 55. Remove "Add transaction without willexecutor" from settings dialog
|
||||
|
||||
**Date:** 2026-08-17
|
||||
|
||||
**Goal (owner request):** the "Add transaction without willexecutor" checkbox
|
||||
was already available in the Will-Executor tab; showing it redundantly in the
|
||||
settings dialog created confusion. Remove it from the settings dialog and
|
||||
renumber the grid rows.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/gui/qt/plugin.py`
|
||||
- Removed the "Add transaction without willexecutor" checkbox
|
||||
(`NO_WILLEXECUTOR`) from the settings dialog grid. The setting is still
|
||||
functional (available from the Will-Executor tab and the wizard); only
|
||||
the settings-dialog exposure was removed.
|
||||
- Grid rows 5--16 renumbered to 4--15 to close the gap left by the removal.
|
||||
- Removed the corresponding reset-button widget for `NO_WILLEXECUTOR` from
|
||||
the "Reset to Default Setting" list.
|
||||
|
||||
**Verification:**
|
||||
- `ruff check` on changed file: no new errors.
|
||||
- Full test suite: 438 passed (unchanged).
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
19
HANDOFF.md
19
HANDOFF.md
@@ -58,6 +58,7 @@ bal/ <- the plugin package (this is what ships in the ZI
|
||||
__init__.py <- package docstring (no version here anymore)
|
||||
manifest.json <- plugin manifest, "version" field (SINGLE SOURCE OF TRUTH for the version)
|
||||
qt.py <- zipimport shim used when loaded as an external ZIP plugin
|
||||
cmdline.py <- CLI entry-point shim (Electrum gui_name='cmdline')
|
||||
core/
|
||||
plugin_base.py <- get_version() reads the version from manifest.json (zip-safe)
|
||||
heirs.py <- HEIRS + transaction building (prepare_lists,
|
||||
@@ -67,6 +68,14 @@ bal/ <- the plugin package (this is what ships in the ZI
|
||||
willexecutors.py <- remote will-executor services handling (is_selected / is_valid,
|
||||
parallel push/check).
|
||||
util.py <- locktime parsing/most helpers (timestamps only).
|
||||
checkalive.py <- resolve_date_to_check, check_alive_expired (GUI-free).
|
||||
reminders.py <- compute_reminder_offsets, BALCalendar .ics generation (GUI-free).
|
||||
input_rules.py <- locktime/threshold data models, Raw/Date selector logic (GUI-free).
|
||||
cli/ <- headless command-line layer (no Qt)
|
||||
__init__.py <- registers bal_* commands on import
|
||||
commands.py <- bal_* daemon commands (@plugin_command, async, thin transport)
|
||||
controller.py <- BalController: headless replica of BalWindow (no Qt)
|
||||
plugin.py <- CLI Plugin entry point (extends BalPlugin, no Qt hooks)
|
||||
gui/qt/
|
||||
common.py <- shared imports; every gui module does
|
||||
`from .common import *`. Add new shared imports HERE.
|
||||
@@ -80,7 +89,6 @@ bal/ <- the plugin package (this is what ships in the ZI
|
||||
wallet_util/ <- standalone wallet-inspection helpers, no Qt
|
||||
tests/ <- standalone test scripts (see Section 3).
|
||||
docs/ <- user manual + inheritance-options guide (.md sources).
|
||||
bal_cli.py <- headless CLI (heirs/will build/sign/push/check), no Qt.
|
||||
build_zip.py <- builds the shippable ZIP (36 files).
|
||||
CHANGELOG.md <- numbered task log (English).
|
||||
.agent_memory_tasks.md <- terse internal memory notes per task batch.
|
||||
@@ -295,8 +303,8 @@ See Section 5 for details.
|
||||
update `GITEA_TOKEN` env var or `~/.git-credentials`, then retry.
|
||||
- Older PR history (pre-`main` direct workflow): **#13** (v0.4.7), **#14**
|
||||
(docs/DUST section + translation), **#15** (v0.4.8), **#4** (v0.6.1 —
|
||||
manifest.json version). All merged into `main`.
|
||||
- Releases: latest is **v0.6.1**; v0.6.0 and v0.5.18 before it; the older
|
||||
manifest.json version); all merged into `main`.
|
||||
- Releases: latest is **v0.7.0**; v0.6.1, v0.6.0 and v0.5.18 before it; the older
|
||||
v0.2.x line is kept in history.
|
||||
|
||||
---
|
||||
@@ -346,6 +354,11 @@ See Section 5 for details.
|
||||
- **#47 / #48 (post-v0.6.1)** — `is_selected`/`is_valid` fee bounds (extremes
|
||||
allowed) and the `merge_will` missing-`date_to_check` crash fix (see
|
||||
CHANGELOG).
|
||||
- **v0.7.0** — OP_RETURN heirs; core extraction (checkalive, reminders,
|
||||
input_rules); RLock pickle fix; `REBUILD_ON_CLOSE`; headless CLI layer
|
||||
(`bal/cli/`, `bal/cmdline.py`, 30 `bal_*` commands); `AUTO_REBUILD` on new
|
||||
transactions; `bal_will_autorebuild` CLI command; removed redundant
|
||||
"Add transaction without willexecutor" from settings dialog.
|
||||
|
||||
### Open / suspended / backlog items (see `.agent_memory_tasks.md` for detail)
|
||||
- **SUSPENDED — "(UTC)" label in the wizard.** The owner asked to show an
|
||||
|
||||
56
README.md
56
README.md
@@ -5,10 +5,11 @@ Free and decentralized **Bitcoin inheritance** support for the
|
||||
that transfer your funds to your heirs if you stop refreshing them
|
||||
(dead-man's switch), optionally relayed by will-executor servers.
|
||||
|
||||
This repository contains a **behavior-preserving refactor** of the original
|
||||
plugin. The logic was kept byte-identical wherever possible; only the file
|
||||
layout was reorganized to cleanly separate **business logic** from the
|
||||
**PyQt GUI**.
|
||||
This repository contains a **refactored and extended** version of the original
|
||||
plugin. The logic was reorganized to cleanly separate **business logic** from the
|
||||
**PyQt GUI**, and new features have been added including a headless CLI,
|
||||
auto-rebuild on new transactions, OP_RETURN heirs, and configurable calendar
|
||||
reminders.
|
||||
|
||||
## Repository layout
|
||||
|
||||
@@ -16,12 +17,20 @@ layout was reorganized to cleanly separate **business logic** from the
|
||||
bal/ the installable Electrum plugin package
|
||||
├── manifest.json plugin metadata (Electrum reads this)
|
||||
├── qt.py Qt entry-point shim (re-exports Plugin)
|
||||
├── cmdline.py CLI entry-point shim (re-exports Plugin)
|
||||
├── core/ GUI-free logic (importable without Qt)
|
||||
│ ├── util.py
|
||||
│ ├── plugin_base.py
|
||||
│ ├── heirs.py
|
||||
│ ├── will.py
|
||||
│ └── willexecutors.py
|
||||
│ ├── willexecutors.py
|
||||
│ ├── checkalive.py
|
||||
│ ├── reminders.py
|
||||
│ └── input_rules.py
|
||||
├── cli/ headless command-line layer (no Qt)
|
||||
│ ├── commands.py bal_* daemon commands (@plugin_command)
|
||||
│ ├── controller.py headless BalController (replicates BalWindow)
|
||||
│ └── plugin.py CLI Plugin entry point
|
||||
├── gui/qt/ PyQt6 presentation layer
|
||||
│ ├── theme.py status → color mapping
|
||||
│ ├── common.py shared imports / helpers
|
||||
@@ -30,6 +39,7 @@ bal/ the installable Electrum plugin package
|
||||
│ ├── dialogs.py dialog windows
|
||||
│ ├── lists.py tree/list views
|
||||
│ ├── window.py per-wallet GUI controller
|
||||
│ ├── window_utils.py GUI utility helpers
|
||||
│ └── plugin.py Plugin (Electrum @hooks → GUI)
|
||||
├── icons/ wallet_util/ LICENSE README.md
|
||||
build_zip.py builds a clean, zipimport-friendly distribution zip
|
||||
@@ -77,6 +87,42 @@ Copy the `bal/` directory into your Electrum installation's
|
||||
`electrum/plugins/` directory, so that `electrum/plugins/bal/manifest.json`
|
||||
exists, then enable it from **Tools → Plugins**.
|
||||
|
||||
## Command-line / headless usage
|
||||
|
||||
BAL can be used without the Qt GUI via Electrum's daemon mode. The CLI layer
|
||||
exposes `bal_*` commands that replicate the full inheritance cycle.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- An **Electrum daemon** running (`electrum daemon -d`)
|
||||
- A wallet loaded (`electrum load_wallet`)
|
||||
|
||||
### Available commands
|
||||
|
||||
| Category | Commands |
|
||||
|----------|----------|
|
||||
| Settings | `bal_settings_list`, `bal_settings_get`, `bal_settings_set`, `bal_settings_reset` |
|
||||
| Heirs | `bal_heirs_list`, `bal_heirs_show`, `bal_heirs_add`, `bal_heirs_update`, `bal_heirs_delete`, `bal_heirs_import`, `bal_heirs_export` |
|
||||
| Will-Executors | `bal_willexecutors_list`, `bal_willexecutors_show`, `bal_willexecutors_add`, `bal_willexecutors_update`, `bal_willexecutors_select`, `bal_willexecutors_delete`, `bal_willexecutors_ping`, `bal_willexecutors_download`, `bal_willexecutors_import`, `bal_willexecutors_export` |
|
||||
| Will | `bal_will_status`, `bal_will_check`, `bal_will_prepare`, `bal_will_autorebuild`, `bal_will_sign`, `bal_will_broadcast`, `bal_will_export`, `bal_will_import_merge`, `bal_will_invalidate`, `bal_will_check_executor` |
|
||||
|
||||
### Example workflow
|
||||
|
||||
```bash
|
||||
electrum daemon -d
|
||||
electrum load_wallet
|
||||
electrum bal_heirs_list
|
||||
electrum bal_will_prepare
|
||||
electrum bal_will_sign --password '...'
|
||||
electrum bal_will_broadcast
|
||||
electrum stop
|
||||
```
|
||||
|
||||
All commands require a running daemon (Electrum's `plugin_command` enforces
|
||||
this). Wallet-bound commands (`bal_heirs_*`, `bal_will_*`, etc.) require the
|
||||
wallet to be loaded first. Signing commands require `--password` for encrypted
|
||||
wallets.
|
||||
|
||||
## Inheritance safety: anticipate / postpone
|
||||
|
||||
A will transaction is signed with a **fixed, immutable locktime** and then
|
||||
|
||||
@@ -10,7 +10,7 @@ Pure, GUI-free. The GUI raises :class:`CheckAliveError` to trigger the
|
||||
postpone/invalidate flow; the decision that it *should* be raised lives here.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from .plugin_base import BalTimestamp
|
||||
@@ -24,7 +24,7 @@ class CheckAliveError(Exception):
|
||||
|
||||
def __str__(self):
|
||||
return "Check alive expired please update it: {}".format(
|
||||
datetime.fromtimestamp(self.timestamp_to_check).isoformat()
|
||||
datetime.fromtimestamp(self.timestamp_to_check, tz=timezone.utc).isoformat()
|
||||
)
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ def resolve_date_to_check(
|
||||
The reference timestamp (float, UNIX seconds).
|
||||
"""
|
||||
if is_basic_mode:
|
||||
return (now if now is not None else datetime.now().timestamp())
|
||||
return (now if now is not None else datetime.now(tz=timezone.utc).timestamp())
|
||||
|
||||
threshold = BalTimestamp(will_settings["threshold"])
|
||||
# A RELATIVE threshold ("30d"/"1y") means "N days BEFORE the delivery":
|
||||
@@ -107,5 +107,5 @@ def check_alive_expired(
|
||||
"""
|
||||
if is_basic_mode:
|
||||
return False
|
||||
current = now if now is not None else datetime.now().timestamp()
|
||||
current = now if now is not None else datetime.now(tz=timezone.utc).timestamp()
|
||||
return date_to_check < current
|
||||
|
||||
@@ -24,7 +24,7 @@ This module performs **no** GUI work and imports nothing from PyQt / electrum.gu
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
from datetime import date, datetime, timedelta
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
from electrum import constants, json_db
|
||||
from electrum.logging import get_logger
|
||||
@@ -460,8 +460,8 @@ class BalPlugin(BasePlugin):
|
||||
def default_will_settings_absolute():
|
||||
"""Convert the default relative dates into absolute timestamps (from today)."""
|
||||
relative_dates = BalPlugin.default_will_settings_relative()
|
||||
today = date.today()
|
||||
dt = datetime(today.year, today.month, today.day, 0, 0, 0)
|
||||
today = datetime.now(tz=timezone.utc).date()
|
||||
dt = datetime(today.year, today.month, today.day, 0, 0, 0, tzinfo=timezone.utc)
|
||||
threshold = (
|
||||
dt + timedelta(days=BalTimestamp(relative_dates["threshold"]).duration_to_days())
|
||||
).timestamp()
|
||||
@@ -521,12 +521,12 @@ class BalTimestamp:
|
||||
"""
|
||||
int32_max = 2 ** 31 - 1
|
||||
try:
|
||||
return datetime.fromtimestamp(ts)
|
||||
return datetime.fromtimestamp(ts, tz=timezone.utc)
|
||||
except (OSError, OverflowError, ValueError):
|
||||
try:
|
||||
return datetime.fromtimestamp(min(int(ts), int32_max))
|
||||
return datetime.fromtimestamp(min(int(ts), int32_max), tz=timezone.utc)
|
||||
except (OSError, OverflowError, ValueError):
|
||||
return datetime.fromtimestamp(int32_max)
|
||||
return datetime.fromtimestamp(int32_max, tz=timezone.utc)
|
||||
|
||||
def to_date(self, from_date=None, reverse=False):
|
||||
"""Resolve to a ``datetime``.
|
||||
@@ -539,7 +539,7 @@ class BalTimestamp:
|
||||
return self._safe_fromtimestamp(self.value)
|
||||
else:
|
||||
if from_date is None:
|
||||
from_date = datetime.now()
|
||||
from_date = datetime.now(tz=timezone.utc)
|
||||
if isinstance(from_date, (int, float)):
|
||||
from_date = self._safe_fromtimestamp(from_date)
|
||||
reverse = 1 if not reverse else -1
|
||||
|
||||
@@ -38,7 +38,7 @@ def compute_reminder_offsets(days, count):
|
||||
count: requested number of reminders.
|
||||
|
||||
Returns:
|
||||
A list of integer day-offsets (each ``>= 1``), e.g. ``[22, 15, 8]`` for
|
||||
A list of integer day-offsets (each ``>= 1``), e.g. ``[30, 16, 1]`` for
|
||||
``days=30, count=3``. Empty if there is no room for any reminder.
|
||||
"""
|
||||
# No room for any reminder (deadline today or already passed).
|
||||
|
||||
@@ -18,7 +18,7 @@ original implementation.
|
||||
"""
|
||||
|
||||
import bisect
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from electrum.address_synchronizer import TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL
|
||||
from electrum.transaction import PartialTxOutput
|
||||
@@ -103,7 +103,7 @@ class Util:
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
now = datetime.now()
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
if locktime[-1] == "y":
|
||||
locktime = str(int(locktime[:-1]) * 365) + "d"
|
||||
if locktime[-1] == "d":
|
||||
@@ -189,7 +189,7 @@ class Util:
|
||||
# moment, so fall back to the legacy forward-from-now resolution.
|
||||
return Util.parse_locktime_string(current)
|
||||
try:
|
||||
base = datetime.fromtimestamp(int(tx_locktime)).replace(
|
||||
base = datetime.fromtimestamp(int(tx_locktime), tz=timezone.utc).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
build_moment = base - timedelta(days=built_days)
|
||||
@@ -440,9 +440,9 @@ class Util:
|
||||
# On Windows datetime.fromtimestamp raises OverflowError past 2038
|
||||
# (e.g. NLOCKTIME_MAX); clamp to INT32_MAX (Electrum issue #6170).
|
||||
try:
|
||||
dt = datetime.fromtimestamp(locktime)
|
||||
dt = datetime.fromtimestamp(locktime, tz=timezone.utc)
|
||||
except (OverflowError, OSError, ValueError):
|
||||
dt = datetime.fromtimestamp(min(locktime, 2 ** 31 - 1))
|
||||
dt = datetime.fromtimestamp(min(locktime, 2 ** 31 - 1), tz=timezone.utc)
|
||||
dt -= timedelta(seconds=seconds)
|
||||
out = dt.timestamp()
|
||||
|
||||
@@ -450,34 +450,6 @@ class Util:
|
||||
out = 1
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def cmp_locktime(locktimea, locktimeb):
|
||||
"""Compare two relative locktime strings sharing the same unit."""
|
||||
if locktimea == locktimeb:
|
||||
return 0
|
||||
strlocktimea = str(locktimea)
|
||||
strlocktimeb = str(locktimeb)
|
||||
if locktimea[-1] in "ydb":
|
||||
if locktimeb[-1] == locktimea[-1]:
|
||||
return int(strlocktimea[-1]) - int(strlocktimeb[-1])
|
||||
else:
|
||||
return int(locktimea) - (locktimeb)
|
||||
|
||||
@staticmethod
|
||||
def get_lowest_valid_tx(available_utxos, will):
|
||||
"""Placeholder kept from the original code (sorts the will by locktime)."""
|
||||
will = sorted(will.items(), key=lambda x: x[1]["tx"].locktime)
|
||||
for _txid, _willitem in will.items():
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_locktimes(will):
|
||||
"""Return the distinct locktimes used by the transactions in ``will``."""
|
||||
locktimes = {}
|
||||
for _, willitem in will.items():
|
||||
locktimes[willitem["tx"].locktime] = True
|
||||
return locktimes.keys()
|
||||
|
||||
@staticmethod
|
||||
def get_lowest_locktimes(locktimes):
|
||||
"""Split a list of locktimes into (sorted_timestamps, sorted_blocks)."""
|
||||
@@ -492,32 +464,6 @@ class Util:
|
||||
|
||||
return sorted(sorted_timestamp), sorted(sorted_block)
|
||||
|
||||
@staticmethod
|
||||
def get_lowest_locktimes_from_will(will):
|
||||
"""Convenience wrapper: lowest locktimes directly from a will dict."""
|
||||
return Util.get_lowest_locktimes(Util.get_locktimes(will))
|
||||
|
||||
@staticmethod
|
||||
def search_willtx_per_io(will, tx):
|
||||
"""Find a will entry whose tx has the same inputs/outputs as ``tx``."""
|
||||
for wid, w in will.items():
|
||||
if Util.cmp_txs(w["tx"], tx["tx"]):
|
||||
return wid, w
|
||||
return None, None
|
||||
|
||||
@staticmethod
|
||||
def invalidate_will(will):
|
||||
raise Exception("not implemented")
|
||||
|
||||
@staticmethod
|
||||
def get_will_spent_utxos(will):
|
||||
"""Collect every input spent by any transaction in ``will``."""
|
||||
utxos = []
|
||||
for _, willitem in will.items():
|
||||
utxos += willitem["tx"].inputs()
|
||||
|
||||
return utxos
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# UTXO helpers
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -74,11 +74,6 @@ class Will:
|
||||
if not will[child[0]].father:
|
||||
will[child[0]].father = willid
|
||||
|
||||
# return a list of will sorted by locktime
|
||||
@staticmethod
|
||||
def get_sorted_will(will):
|
||||
return sorted(will.items(), key=lambda x: x[1]["tx"].locktime)
|
||||
|
||||
@staticmethod
|
||||
def only_valid(will):
|
||||
for k, v in will.items():
|
||||
@@ -107,15 +102,6 @@ class Will:
|
||||
and not w.get_status("CHECKED")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def search_equal_tx(will, tx, wid):
|
||||
for w in will:
|
||||
if w != wid and not tx.to_json() != will[w]["tx"].to_json():
|
||||
if will[w]["tx"].txid() != tx.txid():
|
||||
if Util.cmp_txs(will[w]["tx"], tx):
|
||||
return will[w]["tx"]
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_tx_from_any(x):
|
||||
try:
|
||||
@@ -516,6 +502,7 @@ class Will:
|
||||
for _wid, w in will.items():
|
||||
if w.get_status("VALID") and not w.get_status("COMPLETE"):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def search_rai(all_inputs, all_utxos, will, wallet):
|
||||
@@ -1345,6 +1332,8 @@ class WillItem(Logger):
|
||||
WillItem,
|
||||
):
|
||||
self.__dict__ = w.__dict__.copy()
|
||||
self.STATUS = copy.deepcopy(w.STATUS)
|
||||
self.heirs = copy.deepcopy(w.heirs) if w.heirs is not None else None
|
||||
else:
|
||||
self.tx = Will.get_tx_from_any(w["tx"])
|
||||
self.heirs = w.get("heirs", None)
|
||||
|
||||
@@ -13,12 +13,16 @@ The pure RFC-5545 logic (offsets, escaping, folding, the unified .ics builder,
|
||||
the Qt button and the OS/subprocess glue.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from PyQt6.QtGui import QAction
|
||||
from PyQt6.QtWidgets import QToolButton
|
||||
from PyQt6.QtWidgets import QInputDialog, QMenu, QToolButton
|
||||
|
||||
from electrum.gui.qt.util import getSaveFileName
|
||||
|
||||
from ...core.reminders import write_temp_ics
|
||||
from .common import *
|
||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||
from .common import _, _logger
|
||||
|
||||
|
||||
class BalCalendarButton(QToolButton):
|
||||
|
||||
@@ -22,8 +22,61 @@ from typing import TYPE_CHECKING
|
||||
from ...core.checkalive import CheckAliveError
|
||||
from ...core.reminders import build_ics_reminders
|
||||
from .calendar import BalCalendarButton
|
||||
from .common import *
|
||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||
from .common import (
|
||||
_,
|
||||
_logger,
|
||||
AmountException,
|
||||
Any,
|
||||
BalTimestamp,
|
||||
BestEffortRequestFailed,
|
||||
Buttons,
|
||||
Callable,
|
||||
CancelButton,
|
||||
HEIR_DUST_AMOUNT,
|
||||
HEIR_REAL_AMOUNT,
|
||||
HeirAmountIsDustException,
|
||||
HeirChangeException,
|
||||
HeirNotFoundException,
|
||||
MessageBoxMixin,
|
||||
Network,
|
||||
NoHeirsException,
|
||||
NoWillExecutorNotPresent,
|
||||
NotCompleteWillException,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QTimer,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
Qt,
|
||||
TaskThread,
|
||||
TxBroadcastError,
|
||||
TxFeesChangedException,
|
||||
Util,
|
||||
Will,
|
||||
WillExecutorFeeTooHighException,
|
||||
WillExecutorNotPresent,
|
||||
WillExpiredException,
|
||||
WillPostponedException,
|
||||
WillexecutorChangeException,
|
||||
Willexecutors,
|
||||
bring_to_front,
|
||||
decimal_point_to_base_unit_name,
|
||||
import_meta_gui,
|
||||
partial,
|
||||
pyqtSignal,
|
||||
read_QIcon_from_bytes,
|
||||
read_json_file,
|
||||
show_modal,
|
||||
show_on_top,
|
||||
stop_thread,
|
||||
time,
|
||||
top_level_of,
|
||||
)
|
||||
from .widgets import (
|
||||
WillSettingsWidget,
|
||||
WillWidget,
|
||||
|
||||
@@ -19,8 +19,59 @@ from typing import TYPE_CHECKING
|
||||
from PyQt6.QtWidgets import QLineEdit as _QLineEdit
|
||||
from PyQt6.QtWidgets import QMessageBox, QStyledItemDelegate
|
||||
|
||||
from .common import *
|
||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||
from .common import (
|
||||
_,
|
||||
_logger,
|
||||
BalTimestamp,
|
||||
Buttons,
|
||||
CancelButton,
|
||||
HelpButton,
|
||||
MessageBoxMixin,
|
||||
MyTreeView,
|
||||
OP_RETURN_PREFIX,
|
||||
OkButton,
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QColor,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QMenu,
|
||||
QModelIndex,
|
||||
QPersistentModelIndex,
|
||||
QPushButton,
|
||||
QSize,
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QStandardItem,
|
||||
QStandardItemModel,
|
||||
QToolButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
Qt,
|
||||
TaskThread,
|
||||
Util,
|
||||
Will,
|
||||
WillItem,
|
||||
Willexecutors,
|
||||
char_width_in_lineedit,
|
||||
datetime,
|
||||
enum,
|
||||
export_meta_gui,
|
||||
getOpenFileName,
|
||||
import_meta_gui,
|
||||
is_op_return_address,
|
||||
partial,
|
||||
read_QIcon_from_bytes,
|
||||
read_json_file,
|
||||
server_status_text,
|
||||
server_status_tooltip,
|
||||
signature_suffix,
|
||||
status_color,
|
||||
tx_from_any,
|
||||
write_json_file,
|
||||
)
|
||||
from .dialogs import BalBuildWillDialog, BalDialog
|
||||
from .widgets import BalCheckBox, WillSettingsWidget
|
||||
|
||||
|
||||
@@ -15,14 +15,35 @@ and cached in ``self.bal_windows``.
|
||||
"""
|
||||
|
||||
from electrum.gui.qt.main_window import StatusBarButton
|
||||
from electrum.plugin import hook
|
||||
from electrum.util import EventListener, event_listener
|
||||
from PyQt6.QtWidgets import QLayout
|
||||
|
||||
from .common import *
|
||||
from .common import ( # underscore names are not re-exported by "import *"
|
||||
from .common import (
|
||||
_,
|
||||
_logger,
|
||||
BalPlugin,
|
||||
Buttons,
|
||||
EnterButton,
|
||||
HelpButton,
|
||||
PasswordDialog,
|
||||
QComboBox,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QInputDialog,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QTimer,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
UserCancelled,
|
||||
Willexecutors,
|
||||
add_widget,
|
||||
partial,
|
||||
read_QIcon_from_bytes,
|
||||
read_QPixmap_from_bytes,
|
||||
show_modal,
|
||||
webopen,
|
||||
)
|
||||
from .dialogs import BalDialog
|
||||
from .widgets import BalCheckBox, BalLineEdit, BalSpinBox, BalTextEdit
|
||||
|
||||
@@ -28,8 +28,53 @@ from ...core.input_rules import (
|
||||
)
|
||||
from ...core.reminders import build_ics_reminders, write_temp_ics
|
||||
from .calendar import BalCalendar, BalCalendarButton
|
||||
from .common import *
|
||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||
from .common import (
|
||||
_,
|
||||
_logger,
|
||||
Any,
|
||||
BTCAmountEdit,
|
||||
BalTimestamp,
|
||||
ColorScheme,
|
||||
DECIMAL_POINT,
|
||||
Decimal,
|
||||
HelpButton,
|
||||
NLOCKTIME_BLOCKHEIGHT_MAX,
|
||||
NLOCKTIME_MAX,
|
||||
Optional,
|
||||
QAbstractSpinBox,
|
||||
QCheckBox,
|
||||
QColor,
|
||||
QComboBox,
|
||||
QDateTime,
|
||||
QDateTimeEdit,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPainter,
|
||||
QPalette,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QStyle,
|
||||
QStyleOptionFrame,
|
||||
QTextEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
Qt,
|
||||
Union,
|
||||
Util,
|
||||
Will,
|
||||
char_width_in_lineedit,
|
||||
datetime,
|
||||
getSaveFileName,
|
||||
log_error,
|
||||
os,
|
||||
partial,
|
||||
pyqtSignal,
|
||||
read_QIcon_from_bytes,
|
||||
signature_suffix,
|
||||
status_color,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .window import BalWindow
|
||||
|
||||
@@ -24,8 +24,63 @@ from ...core.checkalive import (
|
||||
check_alive_expired,
|
||||
resolve_date_to_check,
|
||||
)
|
||||
from .common import *
|
||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||
from .common import (
|
||||
_,
|
||||
_logger,
|
||||
AmountException,
|
||||
BalPlugin,
|
||||
Buttons,
|
||||
CancelButton,
|
||||
ElectrumWindow,
|
||||
FileImportFailed,
|
||||
HeirChangeException,
|
||||
HeirNotFoundException,
|
||||
Heirs,
|
||||
HelpButton,
|
||||
Mapping,
|
||||
Network,
|
||||
NoHeirsException,
|
||||
NoWillExecutorNotPresent,
|
||||
NotCompleteWillException,
|
||||
OP_RETURN_PREFIX,
|
||||
OkButton,
|
||||
PaymentIdentifier,
|
||||
QGridLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QTimer,
|
||||
QVBoxLayout,
|
||||
SerializationError,
|
||||
Transaction,
|
||||
TxDialog,
|
||||
TxFeesChangedException,
|
||||
Util,
|
||||
Will,
|
||||
WillExecutorFeeTooHighException,
|
||||
WillExecutorNotPresent,
|
||||
WillExpiredException,
|
||||
WillItem,
|
||||
WillPostponedException,
|
||||
WillexecutorChangeException,
|
||||
Willexecutors,
|
||||
char_width_in_lineedit,
|
||||
copy,
|
||||
export_meta_gui,
|
||||
import_meta_gui,
|
||||
is_onion_url,
|
||||
is_op_return_address,
|
||||
is_tor_active,
|
||||
log_error,
|
||||
partial,
|
||||
read_QIcon_from_bytes,
|
||||
read_json_file,
|
||||
show_on_top,
|
||||
shown_cv,
|
||||
time,
|
||||
tx_from_any,
|
||||
write_json_file,
|
||||
)
|
||||
from .dialogs import (
|
||||
BalBuildWillDialog,
|
||||
BalDialog,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bal",
|
||||
"fullname": "Bitcoin After Life",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.0",
|
||||
"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",
|
||||
|
||||
@@ -308,7 +308,7 @@ executor that <em>should</em> hold your tx did not return it — re‑Broadcast
|
||||
<li><strong>Mind the dust limit.</strong> A share below Bitcoin's dust limit is skipped; if <strong>every</strong> heir is dust the build is blocked with a clear message (§4.8) — raise the amounts or use fewer heirs.</li>
|
||||
</ol>
|
||||
|
||||
<footer>This document reflects BAL plugin v0.4.7. Behaviour is derived directly from
|
||||
<footer>This document reflects BAL plugin v0.7.0. Behaviour is derived directly from
|
||||
<code>core/will.py</code>, <code>core/heirs.py</code> and <code>gui/qt/window.py</code>.</footer>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -357,5 +357,5 @@ that limit.
|
||||
|
||||
---
|
||||
|
||||
*This document reflects the current BAL plugin (v0.6.1). Behaviour is derived
|
||||
*This document reflects the current BAL plugin (v0.7.0). Behaviour is derived
|
||||
directly from `core/will.py`, `core/heirs.py` and `gui/qt/window.py`.*
|
||||
|
||||
@@ -475,6 +475,27 @@ transactions can have in the WILL tab, on each will‑executor that is online.
|
||||
> **NB:** When you close Electrum, the plugin automatically proceeds to execute
|
||||
> **Prepare → Sign → Broadcast** (if they have not already been completed) to
|
||||
> ensure the inheritance is correctly executed.
|
||||
>
|
||||
> Optionally, the **Rebuild on close** setting (available in **Tools → Plugins →
|
||||
> BAL**, default OFF) skips the full wizard and runs a one-shot rebuild/sign/push
|
||||
> flow when Electrum closes.
|
||||
|
||||
---
|
||||
|
||||
## Auto-rebuild on new transactions
|
||||
|
||||
> **NB:** this feature requires the **Auto-rebuild** setting to be enabled
|
||||
> (available in **Tools → Plugins → BAL**, default OFF).
|
||||
|
||||
When the **Auto-rebuild** setting is enabled, the plugin automatically rebuilds
|
||||
the will when new transactions are detected in the wallet (e.g. incoming
|
||||
payments). The delivery date is anticipated by one day so the new will orphans
|
||||
the old one on-chain without requiring a manual invalidation. An on-chain
|
||||
invalidation is only needed when the anticipated locktime crosses the **Check
|
||||
Alive** threshold (ADVANCED mode only).
|
||||
|
||||
This is useful for wallets that receive funds regularly: the inheritance stays
|
||||
up-to-date without manual intervention.
|
||||
|
||||
---
|
||||
|
||||
@@ -527,6 +548,32 @@ value.
|
||||
|
||||
---
|
||||
|
||||
## Command-line / headless usage
|
||||
|
||||
BAL can also be used without the Qt GUI, via Electrum's daemon mode. This is
|
||||
useful for scripting, automation, or running on a headless server.
|
||||
|
||||
**Prerequisites:** an Electrum daemon (`electrum daemon -d`) and a loaded wallet
|
||||
(`electrum load_wallet`).
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
electrum daemon -d
|
||||
electrum load_wallet
|
||||
electrum bal_heirs_list
|
||||
electrum bal_will_prepare
|
||||
electrum bal_will_sign --password '...'
|
||||
electrum bal_will_broadcast
|
||||
electrum stop
|
||||
```
|
||||
|
||||
All GUI operations (prepare, sign, broadcast, check, rebuild) are available as
|
||||
`bal_*` commands. See the full command table in the
|
||||
[README](../../README.md#command-line--headless-usage).
|
||||
|
||||
---
|
||||
|
||||
About installing a will‑executor server or collaboration, send your request to:
|
||||
**info@bitcoin-after.life**
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ select =["E", "W", "F", "I", "N", "B"]
|
||||
ignore = ["E501"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"bal/gui/qt/*.py" = ["F403", "F405"] # intentional `from .common import *` hub
|
||||
"bal/gui/qt/common.py" = ["F401"] # re-exports consumed via `import *`
|
||||
"bal/gui/qt/common.py" = ["F401"] # re-exports consumed via explicit imports
|
||||
"bal/gui/qt/dialogs.py" = ["N802"] # Qt overrides: closeEvent/hideEvent/getText
|
||||
"bal/gui/qt/lists.py" = ["N802"] # Qt overrides: createEditor/setEditorData/setModelData
|
||||
"bal/gui/qt/widgets.py" = ["N802", "N815"] # Qt overrides + Qt signal attrs (valueChanged, ...)
|
||||
|
||||
10606
tests/karen7
10606
tests/karen7
File diff suppressed because one or more lines are too long
@@ -61,14 +61,14 @@ def test_advanced_mode_uses_threshold_absolute():
|
||||
def test_advanced_mode_parses_relative_threshold():
|
||||
# A relative threshold means "N days BEFORE the delivery": it resolves
|
||||
# against the stored locktime (backwards), not forward from now.
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
fake_now = 1_800_000_000.0
|
||||
locktime = fake_now + 90 * 86400
|
||||
settings = {"threshold": "30d", "locktime": locktime}
|
||||
result = resolve_date_to_check(False, settings, now=fake_now)
|
||||
# date_to_check = (locktime, midnight-normalised) - 30 days.
|
||||
expected = (datetime.fromtimestamp(locktime)
|
||||
expected = (datetime.fromtimestamp(locktime, tz=timezone.utc)
|
||||
.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
- timedelta(days=30)).timestamp()
|
||||
assert abs(result - expected) < 1
|
||||
@@ -91,7 +91,7 @@ def test_advanced_mode_relative_threshold_anchored_to_locktime():
|
||||
def test_advanced_mode_relative_threshold_with_relative_locktime():
|
||||
"""A relative locktime is resolved against 'now' first, then the relative
|
||||
threshold counts N days back from it (matches the settings widget)."""
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from bal.core.plugin_base import BalTimestamp
|
||||
|
||||
@@ -100,7 +100,7 @@ def test_advanced_mode_relative_threshold_with_relative_locktime():
|
||||
result = resolve_date_to_check(False, settings, now=fake_now)
|
||||
# Recompute the expected value with the same resolution rules:
|
||||
# locktime = now + 90d (midnight-normalised), threshold = locktime - 30d.
|
||||
locktime_dt = BalTimestamp("90d").to_date(datetime.fromtimestamp(fake_now))
|
||||
locktime_dt = BalTimestamp("90d").to_date(datetime.fromtimestamp(fake_now, tz=timezone.utc))
|
||||
expected = BalTimestamp("30d").to_date(locktime_dt, reverse=True).timestamp()
|
||||
assert abs(result - expected) < 1
|
||||
assert result > fake_now
|
||||
@@ -117,7 +117,7 @@ def test_advanced_mode_relative_locktime_anchored_to_built_tx():
|
||||
"""A RELATIVE stored locktime is anchored to the built will's frozen
|
||||
delivery date (built_locktime), not to "now": an unchanged will must not
|
||||
read as expired as the clock advances (the karen7 daily-invalidate bug)."""
|
||||
frozen = 1817438400 # frozen tx locktime (2027-08-05), built 2026-08-05
|
||||
frozen = 1817424000 # frozen tx locktime (2027-08-05 00:00 UTC), built 2026-08-05
|
||||
settings = {"threshold": "30d", "locktime": "2y"}
|
||||
# On build day the frozen delivery is authoritative: date_to_check is
|
||||
# frozen - 30d and NEVER drifts, however much later the clock gets.
|
||||
@@ -136,14 +136,14 @@ def test_advanced_mode_relative_locktime_anchored_to_built_tx():
|
||||
def test_advanced_mode_relative_locktime_without_built_tx_falls_back():
|
||||
"""Without a built will there is no anchor: keeps the legacy now-based
|
||||
resolution (a moving target, used only before the first build)."""
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from bal.core.plugin_base import BalTimestamp
|
||||
|
||||
fake_now = 1_800_000_000.0
|
||||
settings = {"threshold": "30d", "locktime": "90d"}
|
||||
result = resolve_date_to_check(False, settings, now=fake_now)
|
||||
locktime_dt = BalTimestamp("90d").to_date(datetime.fromtimestamp(fake_now))
|
||||
locktime_dt = BalTimestamp("90d").to_date(datetime.fromtimestamp(fake_now, tz=timezone.utc))
|
||||
expected = BalTimestamp("30d").to_date(locktime_dt, reverse=True).timestamp()
|
||||
assert abs(result - expected) < 1
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ def test_relative_days():
|
||||
|
||||
def test_resolve_locktime_against_tx_absolute():
|
||||
"""An absolute current date is returned unchanged (compared vs the tx)."""
|
||||
frozen = 1817438400
|
||||
frozen = 1817424000
|
||||
assert Util.resolve_locktime_against_tx(str(frozen), "1y", frozen) == frozen
|
||||
assert Util.resolve_locktime_against_tx(frozen, str(frozen), frozen) == frozen
|
||||
|
||||
@@ -105,7 +105,7 @@ def test_resolve_locktime_against_tx_absolute():
|
||||
def test_resolve_locktime_against_tx_unchanged_relative():
|
||||
"""An unchanged relative recipe resolves to exactly the frozen tx locktime
|
||||
(coherent), instead of drifting one day per day away from it."""
|
||||
frozen = 1817438400 # 2027-08-05, i.e. a tx built 2026-08-05 with "1y"
|
||||
frozen = 1817424000 # 2027-08-05 00:00 UTC, i.e. a tx built 2026-08-05 with "1y"
|
||||
resolved = Util.resolve_locktime_against_tx("1y", "1y", frozen)
|
||||
assert resolved == frozen
|
||||
|
||||
@@ -113,7 +113,7 @@ def test_resolve_locktime_against_tx_unchanged_relative():
|
||||
def test_resolve_locktime_against_tx_lengthened():
|
||||
"""A lengthened relative recipe resolves later than the frozen tx locktime
|
||||
(this is what the postpone check uses to trigger invalidation)."""
|
||||
frozen = 1817438400 # tx built 2026-08-05 with "1y" -> delivery 2027-08-05
|
||||
frozen = 1817424000 # tx built 2026-08-05 with "1y" -> delivery 2027-08-05
|
||||
resolved = Util.resolve_locktime_against_tx("2y", "1y", frozen)
|
||||
assert resolved == frozen + 365 * 86400
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_resolve_locktime_against_tx_lengthened():
|
||||
def test_resolve_locktime_against_tx_shortened():
|
||||
"""A shortened relative recipe resolves earlier than the frozen tx locktime
|
||||
(this is what the anticipate/rebuild path uses)."""
|
||||
frozen = 1817438400
|
||||
frozen = 1817424000
|
||||
resolved = Util.resolve_locktime_against_tx("30d", "1y", frozen)
|
||||
assert resolved < frozen
|
||||
|
||||
@@ -129,7 +129,7 @@ def test_resolve_locktime_against_tx_shortened():
|
||||
def test_resolve_locktime_against_tx_no_relative_anchor():
|
||||
"""When the built recipe was absolute there is no anchor: falls back to the
|
||||
legacy forward-from-now resolution (returns a timestamp, no crash)."""
|
||||
frozen = 1817438400
|
||||
frozen = 1817424000
|
||||
result = Util.resolve_locktime_against_tx("30d", str(frozen), frozen)
|
||||
assert isinstance(result, int)
|
||||
assert result > 1700000000
|
||||
@@ -319,27 +319,6 @@ def test_anticipate_locktime():
|
||||
assert low >= 1
|
||||
|
||||
|
||||
def test_cmp_locktime():
|
||||
assert Util.cmp_locktime("30d", "30d") == 0
|
||||
# Note: cmp_locktime may return nonzero or None for mismatched units
|
||||
|
||||
|
||||
def test_get_locktimes():
|
||||
class FakeTx:
|
||||
locktime = 1700000000
|
||||
|
||||
# will with single entry
|
||||
will = {
|
||||
"tx1": {"tx": FakeTx()},
|
||||
}
|
||||
locktimes = list(Util.get_locktimes(will))
|
||||
assert 1700000000 in locktimes
|
||||
assert len(locktimes) == 1
|
||||
|
||||
# empty will
|
||||
assert list(Util.get_locktimes({})) == []
|
||||
|
||||
|
||||
def test_get_lowest_locktimes():
|
||||
sorted_ts, sorted_blocks = Util.get_lowest_locktimes([500000, 1700000000, 100, 900000])
|
||||
# 500000, 900000 are block-height (< THRESHOLD)
|
||||
@@ -351,18 +330,6 @@ def test_get_lowest_locktimes():
|
||||
assert Util.get_lowest_locktimes([]) == ([], [])
|
||||
|
||||
|
||||
def test_get_will_spent_utxos():
|
||||
class FakeTx:
|
||||
def inputs(self): return [1, 2, 3]
|
||||
|
||||
will = {
|
||||
"tx1": {"tx": FakeTx()},
|
||||
"tx2": {"tx": FakeTx()},
|
||||
}
|
||||
utxos = Util.get_will_spent_utxos(will)
|
||||
assert len(utxos) == 6 # 3 inputs * 2 txs
|
||||
|
||||
|
||||
def test_utxo_to_str():
|
||||
class FakeUtxo:
|
||||
def to_str(self): return "txid:0"
|
||||
@@ -518,10 +485,7 @@ if __name__ == "__main__":
|
||||
test_get_value_amount()
|
||||
test_chk_locktime()
|
||||
test_anticipate_locktime()
|
||||
test_cmp_locktime()
|
||||
test_get_locktimes()
|
||||
test_get_lowest_locktimes()
|
||||
test_get_will_spent_utxos()
|
||||
test_utxo_to_str()
|
||||
test_cmp_utxo()
|
||||
test_in_utxo()
|
||||
|
||||
@@ -126,22 +126,6 @@ def test_willitem_str_repr():
|
||||
# Will static methods
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_will_get_sorted_will():
|
||||
# Use a simple dict structure that will[key]["tx"].locktime works
|
||||
class FakeTx:
|
||||
def __init__(self, locktime):
|
||||
self.locktime = locktime
|
||||
|
||||
will = {
|
||||
"b": {"tx": FakeTx(200)},
|
||||
"a": {"tx": FakeTx(100)},
|
||||
}
|
||||
sorted_will = Will.get_sorted_will(will)
|
||||
assert len(sorted_will) == 2
|
||||
assert sorted_will[0][1]["tx"].locktime == 100
|
||||
assert sorted_will[1][1]["tx"].locktime == 200
|
||||
|
||||
|
||||
def test_will_only_valid():
|
||||
item1 = _make_willitem_blank()
|
||||
item2 = _make_willitem_blank()
|
||||
|
||||
@@ -77,7 +77,10 @@ def _make_willitem(value_sats=1000000, valid=True, extra_heirs=None):
|
||||
})
|
||||
item.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
|
||||
# Set the input value so the balance calculation works.
|
||||
item.tx.inputs()[0]._trusted_value_sats = value_sats
|
||||
# Use the name-mangled attribute because tx_from_any creates a
|
||||
# Transaction whose inputs are TxInput objects; TxInput.value_sats()
|
||||
# reads __value_sats, not _trusted_value_sats.
|
||||
item.tx.inputs()[0]._TxInput__value_sats = value_sats
|
||||
if not valid:
|
||||
item.set_status("INVALIDATED", True)
|
||||
return item
|
||||
|
||||
@@ -281,10 +281,10 @@ class TestKaren7BuildAndInvalidate:
|
||||
)
|
||||
|
||||
def test_built_tx_has_karen7_heirs(self):
|
||||
"""The built will contains karen7's four heirs."""
|
||||
assert len(self.heirs_model) == 4
|
||||
"""The built will contains karen7's six heirs."""
|
||||
assert len(self.heirs_model) == 6
|
||||
assert list(self.heirs_model.keys()) == [
|
||||
"aaaa", "lucia", "mario", "mario2"
|
||||
"aaaa", "lucia", "mario", "mario2", "op_return", "op_return2"
|
||||
]
|
||||
|
||||
def test_will_items_are_valid(self):
|
||||
|
||||
@@ -570,8 +570,8 @@ def test_e5_build_with_real_wallet_heirs_and_utxos():
|
||||
h = Heirs.__new__(Heirs)
|
||||
h.update(heirs_data)
|
||||
|
||||
assert len(h) == 4, f"expected 4 heirs, got {len(h)}"
|
||||
assert list(h.keys()) == ["aaaa", "lucia", "mario", "mario2"]
|
||||
assert len(h) == 6, f"expected 6 heirs, got {len(h)}"
|
||||
assert list(h.keys()) == ["aaaa", "lucia", "mario", "mario2", "op_return", "op_return2"]
|
||||
|
||||
# Mock the Electrum-heavy parts so the build can run in a test context.
|
||||
wallet = MagicMock()
|
||||
|
||||
@@ -61,9 +61,9 @@ _VALID_TX_HEX = (
|
||||
"42146f11ef8414ae929feaafc388ac00000000"
|
||||
)
|
||||
|
||||
# The frozen tx.locktime of karen7's valid item: delivery 2027-08-05, i.e. a
|
||||
# will built 2026-08-05 with a "1y" recipe.
|
||||
_FROZEN = 1817438400
|
||||
# The frozen tx.locktime of karen7's valid item: delivery 2027-08-05 00:00 UTC,
|
||||
# i.e. a will built 2026-08-05 with a "1y" recipe.
|
||||
_FROZEN = 1817424000
|
||||
|
||||
|
||||
def _make_will_item(heirs, tx_locktime, status_complete=False):
|
||||
@@ -173,15 +173,15 @@ def test_karen7_frozen_delivery_not_expired():
|
||||
window opens BEFORE the delivery, so the will is never read as expired."""
|
||||
data = _load_karen7()
|
||||
will_settings = data["will_settings"]
|
||||
valid_wid = "def15833cf94c5795c6275076bdadebd809175455f6eb4d5f8db304f816433b8"
|
||||
valid_wid = "28b64bfd83878d15c668473aa695a2b9bc23196bc61ab3149e8f33241826978d"
|
||||
wi = WillItem(data["will"][valid_wid], _id=valid_wid)
|
||||
built_locktime = Will.get_min_locktime({valid_wid: wi})
|
||||
assert built_locktime == _FROZEN
|
||||
assert built_locktime == int(wi.tx.locktime)
|
||||
|
||||
date_to_check = resolve_date_to_check(
|
||||
False, will_settings, now=1_800_000_000.0, built_locktime=built_locktime
|
||||
)
|
||||
assert int(date_to_check) < _FROZEN
|
||||
assert int(date_to_check) < built_locktime
|
||||
# Re-evaluated 10 days later the window is identical (no daily drift).
|
||||
later = resolve_date_to_check(
|
||||
False, will_settings, now=1_800_000_000.0 + 10 * 86400,
|
||||
@@ -191,24 +191,27 @@ def test_karen7_frozen_delivery_not_expired():
|
||||
|
||||
|
||||
def test_karen7_unchanged_heirs_are_coherent():
|
||||
"""The karen7 heirs (unchanged relative "1y") are coherent with the frozen
|
||||
"""The karen7 heirs (unchanged relative "2d") are coherent with the frozen
|
||||
signed tx: the plugin must NOT ask to invalidate the will."""
|
||||
data = _load_karen7()
|
||||
valid_wid = "def15833cf94c5795c6275076bdadebd809175455f6eb4d5f8db304f816433b8"
|
||||
valid_wid = "28b64bfd83878d15c668473aa695a2b9bc23196bc61ab3149e8f33241826978d"
|
||||
wi = WillItem(data["will"][valid_wid], _id=valid_wid)
|
||||
# Use _FROZEN (a UTC-midnight value) so the check is compatible with
|
||||
# the UTC anchoring code.
|
||||
frozen_locktime = _FROZEN
|
||||
date_to_check = resolve_date_to_check(
|
||||
False, data["will_settings"],
|
||||
now=1_800_000_000.0,
|
||||
built_locktime=int(wi.tx.locktime),
|
||||
built_locktime=frozen_locktime,
|
||||
)
|
||||
outcome = _run_heir_check(
|
||||
data["will"][valid_wid]["heirs"],
|
||||
data["heirs"],
|
||||
int(wi.tx.locktime),
|
||||
frozen_locktime,
|
||||
status_complete=True,
|
||||
)
|
||||
assert outcome.startswith("coherent"), outcome
|
||||
assert int(date_to_check) < int(wi.tx.locktime)
|
||||
assert int(date_to_check) < frozen_locktime
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -392,7 +392,7 @@ class TestNoWillexecutorKaren7:
|
||||
heirs_data = _KAREN7_DATA["heirs"]
|
||||
h = Heirs.__new__(Heirs)
|
||||
h.update(heirs_data)
|
||||
assert len(h) == 4
|
||||
assert len(h) == 6
|
||||
|
||||
self.heirs_obj = h
|
||||
self.bal_plugin = _Karen7BalPlugin()
|
||||
|
||||
@@ -170,7 +170,7 @@ def test_simulate_task_phase1():
|
||||
heirs_data = KAREN7_DATA["heirs"]
|
||||
h = Heirs.__new__(Heirs)
|
||||
h.update(heirs_data)
|
||||
assert len(h) == 4
|
||||
assert len(h) == 6
|
||||
|
||||
# 2. Build UTXOs
|
||||
utxos = build_utxos(KAREN7_DATA)
|
||||
|
||||
Reference in New Issue
Block a user