core+gui: chunked multi-QR will transfer (export/import + review/sign wizard, audio channel)

BALQR-framed chunk scheduler (bal/core/qrtransfer.py) with zlib compression,
four chunk presets and a QR_CHUNK_SIZE setting (row 16).
Export/import dialogs (WillQrExportDialog/WillQrImportDialog), per-tx
review-and-sign wizard, export filters (All/Valid/Valid-NC) and an Auto
slideshow with speed + loop for the QR codes; optional audio_modem channel
with a local receive mirror. _prepare_and_sign_tx refactor (no behaviour
change); WillItem.status defaults to '' instead of None (import crash fix);
invalidate_will guard for a missing date_to_check. Docs: PLAN_QR_TRANSFER,
QML_PLAN, README, HANDOFF, CHANGELOG entry 56, AUDIO_MODEM_DEBIAN.
This commit is contained in:
2026-08-28 16:28:37 -04:00
parent 3a9ee5adb9
commit 8647eee586
16 changed files with 3106 additions and 64 deletions

View File

@@ -2908,3 +2908,108 @@ renumber the grid rows.
- Full test suite: 438 passed (unchanged).
**Outcome:** DONE.
## 56. QR / audio will transfer (chunked multi-QR export/import + review-and-sign wizard)
**Date:** 2026-08-27
**Goal (owner request):** export a will to another device via QR codes (with
an optional audio channel on top), and import it there with a per-transaction
review-and-sign flow. QR codes are chunked because a full will usually exceeds
a single code's capacity.
**What changed:**
- `bal/core/qrtransfer.py` (new, GUI-free): the wire format and chunk
scheduler — `BALQR{version}|{total}|{index}|{flags}|{payload}` frames,
`encode_transfer` / `decode_transfer`, `split_frames` / `parse_frame` /
`assemble`, optional `Z` (zlib+base64) compression at export, four chunk
presets (150/400/900/1800 bytes/frame, EC level M), plus
`preset_index_for_chunk_size` and the `QrTransferError` exception family.
- `tests/test_core_qr_transfer.py` (new): round-trips (plain/compressed),
boundaries (exact-fit, size>payload, `|` in payload), min-size guard, bad
magic/version/numbers/flags, multi-frame reassembly, header consistency.
- `bal/core/plugin_base.py`: new `QR_CHUNK_SIZE` config (default 150).
- `bal/gui/qt/plugin.py`: settings-dialog row 16 "QR Code Size" combo
(4 presets) + reset button, visible in BASIC and ADVANCED.
- `bal/gui/qt/dialogs.py`:
- `BalQrImage`: QR widget with MEDIUM error correction (Electrum's
`QRCodeWidget` is EC-L), reusing `draw_qr`.
- `WillQrExportDialog`: walks the frames (Prev/Next, "i of N" progress),
export filters **All / Valid / Valid-NC**, live chunk-preset selector,
**Auto slideshow** (toggle button + "QR codes per second" spinbox, stops on
the last frame and on filter/chunk changes), optional audio-send button.
- `WillQrImportDialog`: camera scan (Electrum `scan_qrcode_from_camera`,
one code at a time), manual paste fallback, slot grid (1..N) with
green=stored, total-mismatch reset, optional audio-receive that mirrors
the audio_modem `_recv` sink with a callback instead of `setText`.
- `WillTxReviewSignDialog`: per-transaction review (outputs via
`get_ui_address_str`, total outputs, fee) with Sign / Skip / Cancel and a
single wallet password; final page offers "Save signed file…" and
"Show signed QR…". Runs on the imported local copy only.
- `bal/gui/qt/window.py`: `export_will_via_qr`, `import_will_via_qr`,
`get_audio_modem_plugin`, `_audio_send_payload`; `sign_transactions`
refactored into a byte-equivalent batch loop plus the reusable
`_prepare_and_sign_tx(…, txid, password)` single-transaction helper.
- `bal/gui/qt/lists.py`: will-list menu gains **Export → QR Codes** and
**Import via QR**.
- `tests/test_gui_qr_transfer.py` (new): export build/navigation/chunk
change, filters (Valid, Valid-NC, empty-revert), import frame flow,
assembly+decode, total-mismatch reset, manual entry.
- `PLAN_QR_TRANSFER.md`: the full spec (wire format, settings, export,
import, wizard checklist P0P6, findings log).
- Docs: `README.md` QR-transfer section; `QML_PLAN.md` updated (Phase 2
`BalQrTransferModel`, Phase 3 dedicated QML export/import pages, R6
mitigation rewritten, deferred-chunks note removed).
**Audio channel caveats:**
- The audio send/receive buttons only appear when Electrum's `audio_modem`
plugin is enabled *and* `amodem` + PortAudio are installed (not present in
the current dev runtime — verified F22). On that channel the transport
zlib-compresses internally, so no BAL framing/`Z` flag is used.
- `WaitingDialog` requires a real `QWidget` parent and the plugin's `_recv`
hard-wires `parent.setText`, so receive uses a local mirror with a
callback sink.
**Verification:**
- `python3 tests/test_core_qr_transfer.py`: all pass.
- `QT_QPA_PLATFORM=offscreen python3 tests/test_gui_qr_transfer.py`: all pass.
- Pytest batch `tests/test_core_*.py tests/test_gui_*.py`: 374 passed (only
the two pre-existing `test_bt_to_date_*` datetime compare failures remain).
- `QT_QPA_PLATFORM=offscreen python3 tests/smoke_test.py
electrum.plugins.bal`: passed (clean import under real Electrum).
- `ruff`: no new violations on changed files.
**Audio-environment notes (dev box, discovered while testing):**
- `amodem` 1.16.0 is old and uses `np.ndarray.tostring()`, removed in numpy 2.x;
the runtime venv (numpy 2.4.6) needs the one-line patch
`tostring()` → `tobytes()` in
`electrum/env/lib/python3.11/site-packages/amodem/common.py` (done locally,
not in the repo). Any machine with numpy>=2 and this amodem version needs
the same patch (or numpy<2).
- Electrum's `audio_modem` plugin hardcodes `libportaudio.so` (unversioned).
Debian/Ubuntu only ship `libportaudio.so.2`, so the load fails silently
inside the plugin's `_send` `WaitingDialog` (no `on_error` → no sound, no
message). Fix on the dev box:
`sudo ln -s /usr/lib/x86_64-linux-gnu/libportaudio.so.2 /usr/lib/x86_64-linux-gnu/libportaudio.so`
(created by the `libportaudio-dev` package; a `LD_LIBRARY_PATH` stub works
without root). The audio buttons stay hidden unless the plugin is enabled
and available.
- Verified on the dev box (no physical mic required) via a full
send→sink→monitor→recv round-trip: set the default PulseAudio source to
`<sink>.monitor` at receive time; payload returned byte-identical.
**Follow-up fixes (same session, reported during audio testing):**
- `WillItem.__init__` now defaults `status` to `""` instead of `None`. A
`WillItem` built from a bare `{"tx": ...}` (QR/audio import, clipboard
merge) crashed in `set_status` with
`unsupported operand type(s) for +=: 'NoneType' and 'str'` during the
validity pass / `IMPORTED` marking.
- `BalWindow.invalidate_will` guards a missing `date_to_check` (first-action
case) like `merge_will` already did, fixing
`AttributeError: 'BalWindow' object has no attribute 'date_to_check'` when
invalidating before the periodic check initialized it.
- Regression test `test_imported_item_status_not_none` added to
`tests/test_gui_qr_transfer.py`; QR GUI suite 12/12, batch 377 passed.
**Outcome:** DONE.