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:
@@ -88,6 +88,8 @@ from .dialogs import (
|
||||
BalWizardDialog,
|
||||
WillDetailDialog,
|
||||
WillExecutorDialog,
|
||||
WillQrExportDialog,
|
||||
WillQrImportDialog,
|
||||
)
|
||||
from .lists import HeirListWidget, PreviewList
|
||||
from .widgets import LockTimeWidget, PercAmountEdit
|
||||
@@ -980,6 +982,13 @@ class BalWindow:
|
||||
return self.show_transaction_real(tx, parent=parent)
|
||||
|
||||
def invalidate_will(self, will=None):
|
||||
# The reference timestamp is normally set by init_class_variables();
|
||||
# fall back to "now" so a first-action invalidation always has it.
|
||||
if not hasattr(self, "date_to_check") or self.date_to_check is None:
|
||||
self.date_to_check = resolve_date_to_check(
|
||||
self.bal_plugin.is_basic_mode(), self.will_settings
|
||||
)
|
||||
|
||||
def on_success(result):
|
||||
if result:
|
||||
self.show_message(
|
||||
@@ -1015,75 +1024,93 @@ class BalWindow:
|
||||
self.waiting_dialog.exe()
|
||||
|
||||
def sign_transactions(self, password, will=None, txids=None):
|
||||
try:
|
||||
willitems = will if will is not None else self.willitems
|
||||
txs = {}
|
||||
signed = None
|
||||
tosign = None
|
||||
try:
|
||||
willitems = will if will is not None else self.willitems
|
||||
txs = {}
|
||||
signed = None
|
||||
tosign = None
|
||||
|
||||
def get_message():
|
||||
msg = ""
|
||||
if signed:
|
||||
msg = _(f"signed: {signed}\n")
|
||||
return msg + _(f"signing: {tosign}")
|
||||
def get_message():
|
||||
msg = ""
|
||||
if signed:
|
||||
msg = _(f"signed: {signed}\n")
|
||||
return msg + _(f"signing: {tosign}")
|
||||
|
||||
if txids is not None:
|
||||
targets = [
|
||||
t for t in txids
|
||||
if t in willitems and willitems[t].get_status("VALID")
|
||||
]
|
||||
else:
|
||||
targets = Will.only_valid(willitems)
|
||||
for txid in targets:
|
||||
wi = willitems[txid]
|
||||
# Do NOT deepcopy: the stored tx carries wallet-derived objects
|
||||
# (utxo / script_descriptor) that hold a threading.RLock, and
|
||||
# copy.deepcopy raises "cannot pickle '_thread.RLock'". Re-parse
|
||||
# from the serialized form instead, which is exactly how the will
|
||||
# is persisted/loaded (WillItem.to_dict -> serialize -> tx_from_any).
|
||||
tx = Will.get_tx_from_any(str(wi.tx))
|
||||
if wi.get_status("COMPLETE"):
|
||||
if txids is not None:
|
||||
targets = [
|
||||
t for t in txids
|
||||
if t in willitems and willitems[t].get_status("VALID")
|
||||
]
|
||||
else:
|
||||
targets = Will.only_valid(willitems)
|
||||
for txid in targets:
|
||||
wi = willitems[txid]
|
||||
if wi.get_status("COMPLETE"):
|
||||
# Already signed and complete: keep as-is (the single-tx
|
||||
# helper short-circuits without touching the wallet).
|
||||
tx, _ = self._prepare_and_sign_tx(willitems, txid, password)
|
||||
txs[txid] = tx
|
||||
continue
|
||||
tosign = txid
|
||||
try:
|
||||
self.waiting_dialog.update(get_message())
|
||||
except Exception:
|
||||
pass
|
||||
tx, _signed = self._prepare_and_sign_tx(willitems, txid, password)
|
||||
signed = tosign
|
||||
txs[txid] = tx
|
||||
continue
|
||||
tosign = txid
|
||||
except Exception:
|
||||
return None
|
||||
return txs
|
||||
|
||||
def _prepare_and_sign_tx(self, willitems, txid, password):
|
||||
"""Prepare one will transaction and sign it.
|
||||
|
||||
Shared by the batch signer (:meth:`sign_transactions`) and the
|
||||
per-transaction review wizard of the QR import flow
|
||||
(:class:`WillTxReviewSignDialog`).
|
||||
|
||||
Returns ``(tx, newly_signed)``: ``newly_signed`` is False when the
|
||||
transaction was already COMPLETE (nothing was signed).
|
||||
"""
|
||||
wi = willitems[txid]
|
||||
# Do NOT deepcopy: the stored tx carries wallet-derived objects
|
||||
# (utxo / script_descriptor) that hold a threading.RLock, and
|
||||
# copy.deepcopy raises "cannot pickle '_thread.RLock'". Re-parse
|
||||
# from the serialized form instead, which is exactly how the will
|
||||
# is persisted/loaded (WillItem.to_dict -> serialize -> tx_from_any).
|
||||
tx = Will.get_tx_from_any(str(wi.tx))
|
||||
if wi.get_status("COMPLETE"):
|
||||
return tx, False
|
||||
for txin in tx.inputs():
|
||||
prevout = txin.prevout.to_json()
|
||||
if prevout[0] in willitems:
|
||||
change = willitems[prevout[0]].tx.outputs()[prevout[1]]
|
||||
txin._trusted_value_sats = change.value
|
||||
try:
|
||||
self.waiting_dialog.update(get_message())
|
||||
txin.script_descriptor = change.script_descriptor
|
||||
except Exception:
|
||||
pass
|
||||
for txin in tx.inputs():
|
||||
prevout = txin.prevout.to_json()
|
||||
if prevout[0] in willitems:
|
||||
change = willitems[prevout[0]].tx.outputs()[prevout[1]]
|
||||
txin._trusted_value_sats = change.value
|
||||
try:
|
||||
txin.script_descriptor = change.script_descriptor
|
||||
except Exception:
|
||||
pass
|
||||
txin.is_mine = True
|
||||
txin._TxInput__address = change.address
|
||||
txin._TxInput__scriptpubkey = change.scriptpubkey
|
||||
txin._TxInput__value_sats = change.value
|
||||
txin.is_mine = True
|
||||
txin._TxInput__address = change.address
|
||||
txin._TxInput__scriptpubkey = change.scriptpubkey
|
||||
txin._TxInput__value_sats = change.value
|
||||
txin._trusted_value_sats = change.value
|
||||
|
||||
self.wallet.sign_transaction(tx, password, ignore_warnings=True)
|
||||
signed = tosign
|
||||
# is_complete = False
|
||||
if tx.is_complete():
|
||||
# is_complete = True
|
||||
wi.set_status("COMPLETE", True)
|
||||
# Refresh the per-item signature counts from the freshly signed
|
||||
# partial tx: at this point the signatures are still present
|
||||
# (before any finalization), so the will list can show the real
|
||||
# "added/required" count (e.g. "1/2" for a multisig).
|
||||
try:
|
||||
have, required = tx.signature_count()
|
||||
wi.sigs_have = int(have)
|
||||
wi.sigs_required = int(required)
|
||||
except Exception as e:
|
||||
_logger.debug(f"signature_count after signing failed: {e}")
|
||||
txs[txid] = tx
|
||||
except Exception:
|
||||
return None
|
||||
return txs
|
||||
self.wallet.sign_transaction(tx, password, ignore_warnings=True)
|
||||
if tx.is_complete():
|
||||
wi.set_status("COMPLETE", True)
|
||||
# Refresh the per-item signature counts from the freshly signed
|
||||
# partial tx: at this point the signatures are still present
|
||||
# (before any finalization), so the will list can show the real
|
||||
# "added/required" count (e.g. "1/2" for a multisig).
|
||||
try:
|
||||
have, required = tx.signature_count()
|
||||
wi.sigs_have = int(have)
|
||||
wi.sigs_required = int(required)
|
||||
except Exception as e:
|
||||
_logger.debug(f"signature_count after signing failed: {e}")
|
||||
return tx, True
|
||||
|
||||
def get_wallet_password(self, message=None, parent=None):
|
||||
parent = self.window if not parent else parent
|
||||
@@ -1620,6 +1647,52 @@ class BalWindow:
|
||||
self.show_error(str(e))
|
||||
raise e
|
||||
|
||||
def export_will_via_qr(self, will=None):
|
||||
"""Export the will (default: the live one) as QR codes on screen.
|
||||
|
||||
The selected transactions are serialized with the wire format of
|
||||
:mod:`bal.core.qrtransfer` and shown, one frame at a time, in a
|
||||
:class:`WillQrExportDialog`. When Electrum's ``audio_modem`` plugin
|
||||
is available (:meth:`get_audio_modem_plugin`) the dialog also offers
|
||||
an "Audio" send button.
|
||||
"""
|
||||
try:
|
||||
willitems = will if will is not None else self.willitems
|
||||
d = WillQrExportDialog(self, will=willitems, bal_plugin=self.bal_plugin)
|
||||
show_on_top(d)
|
||||
except Exception as e:
|
||||
self.show_error(str(e))
|
||||
raise e
|
||||
|
||||
def get_audio_modem_plugin(self):
|
||||
"""Return Electrum's ``audio_modem`` plugin instance, or None.
|
||||
|
||||
The plugin is only usable when Electrum exposes it (the ``Plugins``
|
||||
manager knows the name) and its optional runtime dependency
|
||||
``amodem`` is installed (:meth:`is_available`). Every other case
|
||||
returns None so callers can simply hide the audio buttons.
|
||||
"""
|
||||
try:
|
||||
p = self.window.gui_object.plugins.get("audio_modem")
|
||||
except Exception:
|
||||
return None
|
||||
if not p or not getattr(p, "is_available", lambda: False)():
|
||||
return None
|
||||
return p
|
||||
|
||||
def _audio_send_payload(self, payload):
|
||||
"""Send a transfer payload through the audio_modem plugin.
|
||||
|
||||
Wraps the plugin's own ``_send`` with a proper parent widget. The
|
||||
audio channel zlib-compresses internally, so the payload is passed
|
||||
uncompressed (no BAL ``Z`` flag needed on that transport).
|
||||
"""
|
||||
plugin = self.get_audio_modem_plugin()
|
||||
if plugin is None:
|
||||
self.show_error(_("Audio MODEM plugin is not available."))
|
||||
return
|
||||
plugin._send(parent=self.window, blob=payload)
|
||||
|
||||
def merge_will(self, imported):
|
||||
"""Merge imported will items into the live will.
|
||||
|
||||
@@ -1762,6 +1835,18 @@ class BalWindow:
|
||||
|
||||
import_meta_gui(self.window, _("will"), on_file, on_success)
|
||||
|
||||
def import_will_via_qr(self):
|
||||
"""Import a will through QR codes (or audio) and review/sign it.
|
||||
|
||||
Opens a :class:`WillQrImportDialog`. The captured transactions are
|
||||
parsed into fresh :class:`WillItem` objects (never touching the
|
||||
live will), run through the same local validity pass the merge flow
|
||||
uses, and are then presented in the per-transaction review wizard
|
||||
(:class:`WillTxReviewSignDialog`).
|
||||
"""
|
||||
d = WillQrImportDialog(self, bal_plugin=self.bal_plugin)
|
||||
show_on_top(d)
|
||||
|
||||
def _load_will_file(self, path):
|
||||
data = read_json_file(path)
|
||||
willitems = {}
|
||||
|
||||
Reference in New Issue
Block a user