docs: QR/audio will transfer notes
This commit is contained in:
170
CHANGELOG.md
170
CHANGELOG.md
@@ -3094,4 +3094,174 @@ misleading.
|
||||
- Manually tested by the owner in Electrum 4.8.1: `NO_FUTURE_DATE`,
|
||||
`WILLEXECUTOR_FEE` and `No Heirs` were all confirmed on screen.
|
||||
|
||||
## 57. Remove all `copy.deepcopy` (ad-hoc copy helpers; `WillItem` copies serialize/deserialize)
|
||||
|
||||
**Date:** 2026-08-28
|
||||
|
||||
**Goal (owner request):** eliminate every `copy.deepcopy` from the codebase
|
||||
and replace it with ad-hoc copy methods; `WillItem` copies must be produced by
|
||||
serializing and deserializing the item rather than by deep-copying live
|
||||
runtime objects (which can hold a `threading.RLock` and cannot be pickled).
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/core/util.py`: new `copy_structure(value, _path="copy")` — the single
|
||||
JSON-safe, deepcopy-free recursive cloner (dict / list / tuple cloned
|
||||
structurally, JSON scalars kept as-is, any accidental runtime object coerced
|
||||
to `str` + logged). It replaces the old `heirs._json_safe` implementation.
|
||||
- `bal/core/heirs.py`: `_json_safe` is now a thin backward-compatible alias of
|
||||
`bal.core.util.copy_structure`; `Heirs.save` behaviour is unchanged.
|
||||
- `bal/core/will.py`:
|
||||
- `WillItem.__init__` on a `WillItem` argument no longer does
|
||||
`self.__dict__ = w.__dict__.copy()` + `copy.deepcopy`; instead it
|
||||
serializes (`to_dict()`) and deserializes: the tx is re-parsed into a fresh
|
||||
object, `STATUS` is rebuilt from a clone, and heirs / will-executors are
|
||||
cloned recursively, so the copy shares no mutable state with the source.
|
||||
- New `WillItem.copy(wallet=None)` (serialize/deserialize round trip; re-adds
|
||||
wallet tx info when a wallet is passed) and the static
|
||||
`WillItem.copy_status_table(table)` used for the `STATUS` tables.
|
||||
- `to_dict()` now also emits `Father` / `Children` so the round trip is
|
||||
faithful.
|
||||
- `normalize_will` routes copies through the constructor / `copy()`.
|
||||
- `bal/gui/qt/window.py` and `bal/cli/controller.py`: the Build-will flow now
|
||||
uses `copy_structure(...)` instead of `copy.deepcopy(...)` for heirs and
|
||||
will-executors.
|
||||
- Dropped now-unused `import copy` (`will.py`, `controller.py`, `qt/common.py`,
|
||||
`qt/window.py`).
|
||||
- Tests updated to the same helpers: STATUS tables via
|
||||
`WillItem.copy_status_table`, heirs / built dicts via `copy_structure`
|
||||
(`test_core_will.py`, `test_core_will_invalidate.py`,
|
||||
`test_heir_relative_anchor.py`, `test_anticipate_manual_locktime.py`,
|
||||
`test_no_willexecutor_karen7.py`, `test_reproduce_none_type.py`,
|
||||
`test_group_e_mock_karen7.py`, `test_group_e_karen7_invalidate.py`,
|
||||
`sim_update_flows.py`).
|
||||
|
||||
**Verification:**
|
||||
- Full offline batch `tests/test_core_*.py tests/test_gui_*.py`: 377 passed,
|
||||
only the two pre-existing `test_bt_to_date_*` datetime compare failures
|
||||
remain (identical to HEAD — no regression; `test_heir_relative_anchor`
|
||||
isolated-file failure is pre-existing test-pollution at HEAD too).
|
||||
- Ad-hoc semantics check: `copy()`/ctor copy share no mutable state with the
|
||||
source (mutating source heirs/STATUS does not leak into the copy and vice
|
||||
versa), `copy_status_table` returns fresh lists, `normalize_will` runs.
|
||||
- `ruff` on all touched files: no new violations (4 findings, all pre-existing
|
||||
at HEAD).
|
||||
- `tests/smoke_test.py electrum.plugins.bal`: passed.
|
||||
- `python3 build_zip.py`: 45 files, 343591 bytes, sha256 `aa8f8154…`;
|
||||
`tests/external_zip_test.py bal-electrum-plugin.zip`: passed (Plugin class
|
||||
loads via the zipimport shim).
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
---
|
||||
|
||||
## Next. Animated-QR interop (BC-UR v1/v2, BBQR)
|
||||
|
||||
**Date:** 2026-09-08
|
||||
|
||||
**Goal:** Let BAL export/import a will not only as its own BAL QR frame format
|
||||
but also as BC-UR v1 (`ur:bytes`, BC32 + SHA-256), BC-UR v2 (`ur:bytes`, CBOR
|
||||
bytewords-minimal fountain codes) and BBQR (Coinkite `B$…`) animated-QR
|
||||
sequences, so transfers interoperate with Blockchain Commons / Coldcard-style
|
||||
tools and BitKit. Codecs must be stdlib-only and the export must keep BAL QR
|
||||
as the default.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/core/animated_qr.py` (new): stdlib-only codec module.
|
||||
- BC32 (bech32_bis checksum, XOR `0x3FFFFFFF`) encode/decode matching the
|
||||
BCR-2020-004/005 reference vectors.
|
||||
- bytewords-minimal encode/decode (BCR-2020-012) with CRC-32 rejection;
|
||||
the word list was transcribed verbatim from the reference C++.
|
||||
- BC-UR v2: CBOR part writer/reader, CRC-32, `choose_fragments`
|
||||
(xoshiro256** + alias + ary-threshold sampler) and XOR-based fountain
|
||||
mixing/solving; emits a redundant mixed wave for loss tolerance.
|
||||
- BC-UR v1: multipart with SHA-256 digest and single-part digest-less
|
||||
frames; `1of1` handling.
|
||||
- BBQR: base32 (encoding `2`), hex (uppercase, `H`) and zlib (lowercase,
|
||||
`Z`, automatic compression fallback) frames; out-of-order reconstruction.
|
||||
- One `AnimatedQrSession` + `detect_format` + `parse_for_detection` for
|
||||
auto-detecting the incoming format and keying the GUI debounce.
|
||||
- Safety caps: `_MAX_SESSION_PARTS = 20000`, `_MAX_MESSAGE_BYTES = 32 MB`,
|
||||
zlib-bomb guard, `TransferConflictError`/`SessionLimitError`.
|
||||
- `bal/gui/qt/dialogs.py`:
|
||||
- Export page (`BalQrExportWidget`) gained a **Format** selector
|
||||
(BAL QR default, BC-UR v1, BC-UR v2, BBQR) reusing the QR-size presets,
|
||||
with per-format intro/format-hint text.
|
||||
- Import page (`BalQrImportWidget`) now routes every frame through
|
||||
`parse_for_detection` + `AnimatedQrSession.add_part`, auto-detecting the
|
||||
format and resetting when the transfer's session key changes; the
|
||||
review/sign step resolves the session and decodes parts uniformly.
|
||||
- `qr_import_accept_frame` generalised to
|
||||
`(state, fmt, session_key, frame_total, index, payload, stable_reads=2)`.
|
||||
- `tests/test_core_animated_qr.py` (new, 32 tests): BC32 spec vectors,
|
||||
bytewords round-trip/CRC, C++ reference-frame decode+re-encode parity
|
||||
(single-part 12B, seq_len=2, seq_len=7), fountain solve with missing pure
|
||||
part, out-of-order/duplicate handling, single/multipart UR v1, BBQR
|
||||
Z/2/H round-trips, runt last part, zlib-bomb guard, detection positive/
|
||||
negative.
|
||||
|
||||
**Verification:**
|
||||
|
||||
- `tests/test_core_animated_qr.py`: 32/32 pass.
|
||||
- `tests/test_gui_qr_transfer.py` (now 36 tests) + `test_gui_export_dialogs.py`: pass.
|
||||
- `ruff` clean on `animated_qr.py`, `dialogs.py` and both test files;
|
||||
`pyright` 0 errors on the touched modules.
|
||||
- `tests/smoke_test.py electrum.plugins.bal`, `python3 build_zip.py` and
|
||||
`external_zip_test.py` all pass.
|
||||
- Full regression: 462 passed; only pre-existing failures remain
|
||||
(`test_bt_to_date_*`, will-invalidate fee, unrelated `sign_transactions`
|
||||
stub test).
|
||||
|
||||
**Notes / caveats:**
|
||||
|
||||
- A real bug was found & fixed during this work: `_ur2_part_cost` used
|
||||
`2 * body_len` but `bytewords_minimal_encode` appends a 4-byte CRC, so every
|
||||
UR v2 frame was undercounted by 8 characters and could overflow the QR
|
||||
budget for large transfers.
|
||||
- UR v1 multipart emits the digest-carrying `1of1/<digest>/<frag>` form for a
|
||||
single part (both headered and headerless single parts are accepted on
|
||||
import); this keeps deterministic digest verification.
|
||||
- Imported payloads are UTF-8 text; the codec sessions do not decode raw
|
||||
binary transfer blobs.
|
||||
|
||||
**Outcome:** DONE (uncommitted).
|
||||
|
||||
---
|
||||
|
||||
## Animated-QR bugfix: QVideoSink signal wiring + will-export JSON crash
|
||||
|
||||
**Date:** 2026-09-08
|
||||
|
||||
**Goal:** Fix two runtime crashes found by manual testing of the QR paths.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/gui/qt/dialogs.py`:
|
||||
- `_start_scan`/`_stop_scan` used `QVideoSink.videoFrame.connect/.disconnect`,
|
||||
but on PyQt6 `videoFrame` is the frame **getter method**, not a signal —
|
||||
this raised ``AttributeError: 'builtin_function_or_method' object has no
|
||||
attribute 'connect'`` on camera scan. Switched to the `videoFrameChanged`
|
||||
signal (same wiring Electrum's `QrReaderVideoSurface` uses).
|
||||
- `_stop_scan` now tolerates `AttributeError` when disconnecting the sink
|
||||
and guards the `errorOccurred` disconnect too, so a mid-init failure can
|
||||
never cascade into a second uncaught exception.
|
||||
- `_whole_will_json` (whole-will QR export) serialized ``WillItem.to_dict()``
|
||||
with plain `json.dumps`, crashing with ``TypeError: Object of type
|
||||
Transaction is not JSON serializable`` (the ``tx`` field holds a real
|
||||
``Transaction``). Now uses Electrum's `MyEncoder`, matching `write_json_file`.
|
||||
- `tests/test_gui_qr_transfer.py`: new `test_import_start_stop_scan_signal_wiring`
|
||||
drives the real `QVideoSink` life-cycle with a mocked camera and fails if
|
||||
the signal name regresses to `videoFrame`.
|
||||
- `tests/test_gui_export_dialogs.py`: new
|
||||
`test_qr_whole_will_json_serializes_transaction` covers the JSON export.
|
||||
|
||||
**Verification:**
|
||||
|
||||
- `pytest tests/test_gui_qr_transfer.py tests/test_gui_export_dialogs.py -q`: pass.
|
||||
- Full offline batch `tests/test_core_*.py tests/test_gui_*.py`: 444 passed.
|
||||
- Regression test flips correctly (fails when reverted to the buggy call).
|
||||
- `ruff` clean on touched files; `tests/smoke_test.py`, `build_zip.py`,
|
||||
`external_zip_test.py` all pass.
|
||||
|
||||
**Outcome:** DONE (uncommitted).
|
||||
|
||||
@@ -23,6 +23,25 @@ is updated to mark them as supported.
|
||||
See [`README.md`](README.md) for supported Electrum versions (currently 4.7.2
|
||||
and 4.8.0).
|
||||
|
||||
## QR wire-format compatibility
|
||||
|
||||
BAL exports/imports wills as QR codes. **BAL QR** (the default) is the plugin's
|
||||
own frame format and is only understood by BAL itself. The export page also
|
||||
supports **BC-UR v1**, **BC-UR v2** and **BBQR**:
|
||||
|
||||
| Format | Wire appearance | Interop target |
|
||||
|-----------|----------------------------|------------------------------------------------------|
|
||||
| BAL QR | `BALQR1\|total\|index\|…` | Past/other BAL versions (default, always exported) |
|
||||
| BC-UR v1 | `ur:bytes/<bc32>` | Blockchain Commons / Coldcard-style UR (BC32, SHA-256 digest, part counts per part) |
|
||||
| BC-UR v2 | `ur:bytes/<seq>-<seqlen>/<bytewords>` | BC-UR 2.x fountain codes (CBOR parts, CRC-32, bytewords-minimal) |
|
||||
| BBQR | `B$<enc><type><N><n>…` | Coinkite BitKit / Coldcard's BBQR animated-QR mode |
|
||||
|
||||
Import auto-detects the format of each scanned code; out-of-order, duplicate
|
||||
and (for UR v2) partially-lost fountain frames are handled. Interop is
|
||||
validation-tested against the reference C++ bc-ur encoder output and the
|
||||
BCR-2020-004/005 BC32 test vectors; it has not yet been cross-verified against
|
||||
third-party libraries (`ur`, `bbqr`, Coldcard firmwares).
|
||||
|
||||
## Reporting compatibility issues
|
||||
|
||||
If you find a compatibility problem not listed here, please open an issue on
|
||||
|
||||
81
HANDOFF.md
81
HANDOFF.md
@@ -395,19 +395,74 @@ See Section 5 for details.
|
||||
|
||||
### In progress: QR / audio will transfer (branch `feature/bal-qr-transfer`)
|
||||
|
||||
- P0–P4 of `PLAN_QR_TRANSFER.md` are implemented on
|
||||
`feature/bal-qr-transfer` and un-committed: core scheduler
|
||||
(`bal/core/qrtransfer.py`), settings row, export/import dialogs +
|
||||
review/sign wizard + lists/window wiring, all with tests
|
||||
(`tests/test_core_qr_transfer.py`, `tests/test_gui_qr_transfer.py`) and
|
||||
docs (README, CHANGELOG entry 56, QML_PLAN, this file).
|
||||
- Remaining: **P6** release hygiene — `python3 build_zip.py`, external-zip
|
||||
test, full ruff check (no NEW violations; baseline is intentionally dirty),
|
||||
and a manual on-device walkthrough of the QR path. The audio path is
|
||||
optional and hidden until the `audio_modem` plugin + `amodem` are
|
||||
installed.
|
||||
- Commit policy unchanged: nothing is committed until the owner explicitly
|
||||
confirms.
|
||||
- The full QR-transfer feature (P0–P6) is implemented, tested and committed on
|
||||
`feature/bal-qr-transfer` (commits `d288b55`, `ce3e36d`, pushed to
|
||||
`origin`). PR creation URL:
|
||||
`https://bitcoin-after.life/gitea/bitcoinafterlife/bal-electrum-plugin/pulls/new/feature/bal-qr-transfer`
|
||||
- Included: core scheduler (`bal/core/qrtransfer.py`), `QR_CHUNK_SIZE`
|
||||
setting (4 export presets), export/import dialogs + review/sign wizard +
|
||||
lists/window wiring, export filters, auto slideshow with per-second rate +
|
||||
loop option, audio send/receive buttons, and the crash fixes
|
||||
(`status` default, `invalidate_will` guard). Docs: README, CHANGELOG entry
|
||||
56, QML_PLAN, `AUDIO_MODEM_DEBIAN.md`.
|
||||
- Follow-up refactor (CHANGELOG entry 57): all `copy.deepcopy` removed —
|
||||
`copy_structure()` in `bal/core/util.py`, `WillItem.copy()` / ctor
|
||||
serialize/deserialize, `copy_status_table()`. Working tree clean after the
|
||||
branch's three commits.
|
||||
- Verification: batch 377 passed / 2 pre-existing `test_bt_to_date_*`
|
||||
failures; ruff no new violations; smoke + `build_zip.py` +
|
||||
external-zip OK; pyright clean. The isolated
|
||||
`test_heir_relative_anchor.py::test_karen7_frozen_delivery_not_expired`
|
||||
failure is pre-existing test pollution (fails identically on clean HEAD,
|
||||
passes inside the full batch) — not caused by entry 57.
|
||||
- Remaining: manual on-device walkthrough of the QR path (and, if wanted,
|
||||
the audio path — buttons only appear when the `audio_modem` plugin +
|
||||
`amodem` are installed; see the prerequisites below).
|
||||
|
||||
### In progress: animated-QR interop (BC-UR v1/v2, BBQR)
|
||||
|
||||
- `bal/core/animated_qr.py` implements stdlib-only codecs for **BC-UR v1**
|
||||
(BC32 + SHA-256 digest; the bech32_bis checksum variant per
|
||||
BCR-2020-004/005), **BC-UR v2** (CBOR part structure, bytewords-minimal,
|
||||
CRC-32, xoshiro256-based fountain with alias-sampled mixing) and **BBQR**
|
||||
(Coinkite `B$…` base32/hex/zlib frames), plus one shared
|
||||
`AnimatedQrSession` with `detect_format` auto-detection and
|
||||
`parse_for_detection` frame identity for the GUI debounce.
|
||||
- Current status as of this session: reference parity, GUI, and tests done;
|
||||
not yet committed.
|
||||
- **BC32/bytewords/codec parity:** BC32 reproduces the BCR-2020-004/005
|
||||
test vectors (`Hello, world`, `Hello world`, the long seed vector);
|
||||
bytewords-minimal round-trips with CRC rejection; UR v2 part encode +
|
||||
decode is byte-exact against the reference C++ bc-ur encoder for a
|
||||
single part, seq_len=2 (12 frames) and seq_len=7 (3 sampled mixes),
|
||||
validating CBOR framing, bytewords, alias+ary-threshold sampling,
|
||||
xoshiro256** and the XOR mix.
|
||||
- **Sessions:** UR v2 single-part (no seq header), out-of-order frames,
|
||||
duplicate drops, solve with a missing pure fragment (a second redundant
|
||||
mixed wave is emitted by `ur2_frames`), UR v1 single-part
|
||||
(digest-less `ur:bytes/<bc32>` accepted) and multipart, BBQR full-frame
|
||||
decode in any order for Z/2/H encodings.
|
||||
- **Safety:** `_MAX_SESSION_PARTS = 20000`, `_MAX_MESSAGE_BYTES = 32 MB`,
|
||||
`TransferConflictError` on a frame from a different transfer,
|
||||
`SessionLimitError`, BBQR zlib-bomb guard, UTF-8 payloads only.
|
||||
- **GUI:** `BalQrExportWidget` gained a Format selector (BAL QR default,
|
||||
BC-UR v1, BC-UR v2, BBQR) reusing the QR-size presets; the importer now
|
||||
routes every frame through `detect_format` +
|
||||
`AnimatedQrSession.add_part` with the shared
|
||||
`qr_import_accept_frame(state, fmt, session_key, frame_total, index,
|
||||
payload, stable_reads=2)` debounce (reset on session-key change).
|
||||
`_review_and_sign` resolves the session to the transfer text and decodes
|
||||
parts uniformly across formats.
|
||||
- **Verification:** `tests/test_core_animated_qr.py` (32 tests incl. the
|
||||
C++-reference parity vectors and BC32 spec vectors) and the extended
|
||||
`tests/test_gui_qr_transfer.py` pass; ruff clean on the new/changed
|
||||
files; pyright 0 errors; smoke test, `build_zip.py` and
|
||||
`external_zip_test.py` green. Only the pre-existing failures remain
|
||||
(`test_bt_to_date_*`, fee-exceeds-balance, karen7 pollution).
|
||||
- **Any remaining work:** manual on-device walkthrough of the QR path with
|
||||
the new formats; optionally validate against third-party libraries
|
||||
(`ur`, `bbqr`) once available; add the docstrings/branch notes already
|
||||
captured in `ag1.md`/`ag2.md` context where needed.
|
||||
|
||||
**Dev-box audio prerequisites (audio_modem channel):**
|
||||
See `AUDIO_MODEM_DEBIAN.md` — the full Debian setup + verification, with the
|
||||
|
||||
17
README.md
17
README.md
@@ -26,7 +26,8 @@ bal/ the installable Electrum plugin package
|
||||
│ ├── willexecutors.py
|
||||
│ ├── checkalive.py
|
||||
│ ├── reminders.py
|
||||
│ ├── qrtransfer.py QR will-transfer wire format / chunk scheduler
|
||||
│ ├── qrtransfer.py BAL QR will-transfer wire format / chunk scheduler
|
||||
│ ├── animated_qr.py BC-UR v1/v2 + BBQR codecs (stdlib-only)
|
||||
│ └── input_rules.py
|
||||
├── cli/ headless command-line layer (no Qt)
|
||||
│ ├── commands.py bal_* daemon commands (@plugin_command)
|
||||
@@ -97,7 +98,19 @@ export offers All / Valid / Valid-NC filters plus a QR size preset
|
||||
one at a time, then proposes exporting the signed transactions. When
|
||||
Electrum's `audio_modem` plugin is enabled (optional, requires `amodem` +
|
||||
PortAudio) Send/Receive audio buttons complement the QR channel. See
|
||||
[`PLAN_QR_TRANSFER.md`](PLAN_QR_TRANSFER.md) for the full wire-format spec.
|
||||
[`PLAN_QR_TRANSFER.md`](PLAN_QR_TRANSFER.md) for the BAL QR wire-format spec.
|
||||
|
||||
### Animated-QR formats (interop)
|
||||
|
||||
BAL QR is the default export format, but the export page's **Format** selector
|
||||
also emits **BC-UR v1** (`ur:bytes`, BC32 + SHA-256), **BC-UR v2**
|
||||
(`ur:bytes`, CBOR fountain codes) and **BBQR** (`B$…`, Coinkite, used by
|
||||
BitKit) animated-QR sequences. The importer auto-detects the format of each
|
||||
code it sees, so any of the four formats can be imported on a BAL device, and
|
||||
a BAL export can be imported by any tool that understands these standards.
|
||||
UR v2 imports tolerate out-of-order and duplicate frames (fountain decoding);
|
||||
BBQR frames may arrive in any order. Rotation/redundancy caps and the
|
||||
32 MB message limit (zlib-bomb guard) bound untrusted scanner input.
|
||||
|
||||
## Command-line / headless usage
|
||||
|
||||
|
||||
Reference in New Issue
Block a user