core+gui+cli: animated QR will transfer (balqr/UR1/UR2/BBQR codecs, audio channel, export/import wizard)
This commit is contained in:
1178
bal/core/animated_qr.py
Normal file
1178
bal/core/animated_qr.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -59,7 +59,7 @@ from electrum.util import (
|
||||
write_json_file,
|
||||
)
|
||||
|
||||
from .util import Util
|
||||
from .util import Util, copy_structure
|
||||
from .willexecutors import Willexecutors
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -321,40 +321,14 @@ def get_change_output(wallet, in_amount, out_amount, fee):
|
||||
return out
|
||||
|
||||
|
||||
def _json_safe(value, _path="heirs", _depth=0):
|
||||
"""Return a JSON-serializable deep copy of *value*.
|
||||
def _json_safe(value, _path="heirs"):
|
||||
"""Backward-compatible alias of :func:`bal.core.util.copy_structure`.
|
||||
|
||||
The wallet DB persists the heirs dict via ``json_db.put``, which calls
|
||||
``copy.deepcopy`` on the value. If any nested element is a live runtime
|
||||
object (e.g. one holding a ``threading.RLock``), deepcopy raises
|
||||
``TypeError: cannot pickle '_thread.RLock' object`` and the whole
|
||||
"Build will" task fails.
|
||||
|
||||
To make persistence robust we coerce the structure to plain
|
||||
JSON-compatible types (dict / list / str / int / float / bool / None).
|
||||
Anything else is converted to ``str(value)`` and logged with its path so
|
||||
the offending field can be identified, instead of crashing the task.
|
||||
Kept so call sites that imported ``_json_safe`` directly keep working; the
|
||||
actual implementation (a JSON-safe, deepcopy-free clone) lives in
|
||||
``bal.core.util`` so every copy path shares one code base.
|
||||
"""
|
||||
# Primitive JSON scalars are kept as-is.
|
||||
if value is None or isinstance(value, (bool, int, float, str)):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
str(k): _json_safe(v, "{}[{!r}]".format(_path, k), _depth + 1)
|
||||
for k, v in value.items()
|
||||
}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [
|
||||
_json_safe(v, "{}[{}]".format(_path, i), _depth + 1)
|
||||
for i, v in enumerate(value)
|
||||
]
|
||||
# Unexpected runtime object: do not let it reach deepcopy. Log where it
|
||||
# was found so the real source can be fixed, then store a safe string.
|
||||
_logger.error(
|
||||
"heirs.save: non-serializable value at {} (type={}); coercing to str. "
|
||||
"value={!r}".format(_path, type(value).__name__, value)
|
||||
)
|
||||
return str(value)
|
||||
return copy_structure(value, _path=_path)
|
||||
|
||||
|
||||
class Heirs(dict, Logger):
|
||||
|
||||
@@ -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, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from electrum import constants, json_db
|
||||
from electrum.logging import get_logger
|
||||
@@ -109,7 +109,9 @@ def get_will(x):
|
||||
|
||||
try:
|
||||
# Electrum >= 4.8.0
|
||||
from electrum.stored_dict import register_name as _electrum_register_name # pyright: ignore[reportMissingImports]
|
||||
from electrum.stored_dict import (
|
||||
register_name as _electrum_register_name, # pyright: ignore[reportMissingImports]
|
||||
)
|
||||
|
||||
def _register_will_dict(name, method, _type=None):
|
||||
"""Register a plugin dict in the wallet DB (Electrum >= 4.8.0 API)."""
|
||||
|
||||
@@ -209,4 +209,4 @@ def __compute_total(transfer_len, chunk_size, flags):
|
||||
)
|
||||
if transfer_len <= budget * total:
|
||||
return total
|
||||
total += 1
|
||||
total += 1
|
||||
|
||||
@@ -21,8 +21,11 @@ import bisect
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from electrum.address_synchronizer import TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL
|
||||
from electrum.logging import get_logger
|
||||
from electrum.transaction import PartialTxOutput
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
# Bitcoin consensus rule: an nLockTime value strictly below this threshold is
|
||||
# interpreted as a *block height*, otherwise it is interpreted as a *UNIX
|
||||
# timestamp*.
|
||||
@@ -35,6 +38,41 @@ from electrum.transaction import PartialTxOutput
|
||||
LOCKTIME_THRESHOLD = 500000000
|
||||
|
||||
|
||||
def copy_structure(value, _path="copy"):
|
||||
"""Return a JSON-serializable deep copy of *value*.
|
||||
|
||||
This is the ad-hoc, deepcopy-free stand-in used every time the plugin needs
|
||||
an independent copy of a plain-data structure (heirs dicts, will-executor
|
||||
dicts, status tables). It recursively clones dict / list / tuple values
|
||||
while leaving JSON scalars (str / int / float / bool / None) as-is.
|
||||
|
||||
If any nested element is a live runtime object (e.g. one holding a
|
||||
``threading.RLock``), ``copy.deepcopy`` would raise
|
||||
``TypeError: cannot pickle '_thread.RLock' object``; instead we coerce the
|
||||
offending value to ``str(value)`` and log it with its path so the source
|
||||
field can be identified, without crashing the caller.
|
||||
"""
|
||||
# Primitive JSON scalars are kept as-is.
|
||||
if value is None or isinstance(value, (bool, int, float, str)):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
str(k): copy_structure(v, "{}[{!r}]".format(_path, k))
|
||||
for k, v in value.items()
|
||||
}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [
|
||||
copy_structure(v, "{}[{}]".format(_path, i)) for i, v in enumerate(value)
|
||||
]
|
||||
# Unexpected runtime object: do not let it reach deepcopy. Log where it
|
||||
# was found so the real source can be fixed, then store a safe string.
|
||||
_logger.error(
|
||||
"copy_structure: non-serializable value at {} (type={}); coercing to "
|
||||
"str. value={!r}".format(_path, type(value).__name__, value)
|
||||
)
|
||||
return str(value)
|
||||
|
||||
|
||||
class Util:
|
||||
"""Namespace of static helpers (kept as a class to preserve the original
|
||||
``Util.method(...)`` call sites used throughout the plugin)."""
|
||||
|
||||
112
bal/core/will.py
112
bal/core/will.py
@@ -26,7 +26,6 @@ The status flags themselves (the source of truth) stay here; only the mapping
|
||||
"status -> colour" now lives in the GUI layer. No behaviour changed.
|
||||
"""
|
||||
|
||||
import copy
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from electrum.i18n import _
|
||||
@@ -45,7 +44,7 @@ from electrum.util import (
|
||||
)
|
||||
|
||||
from .heirs import WillExecutorFeeTooHighException
|
||||
from .util import Util
|
||||
from .util import Util, copy_structure
|
||||
from .willexecutors import Willexecutors
|
||||
|
||||
MIN_LOCKTIME = 1
|
||||
@@ -143,7 +142,7 @@ class Will:
|
||||
willitems = {}
|
||||
for wid in will:
|
||||
Will.add_info_from_will(will, wid, wallet)
|
||||
willitems[wid] = WillItem(will[wid])
|
||||
willitems[wid] = WillItem(will[wid], wallet=wallet)
|
||||
will = willitems
|
||||
errors = {}
|
||||
for wid in will:
|
||||
@@ -165,7 +164,7 @@ class Will:
|
||||
outputs = will[wid].tx.outputs()
|
||||
ow = will[wid]
|
||||
ow.normalize_locktime(others_input)
|
||||
will[wid] = WillItem(ow.to_dict())
|
||||
will[wid] = ow.copy()
|
||||
|
||||
for i in range(0, len(outputs)):
|
||||
Will.change_input(
|
||||
@@ -465,7 +464,7 @@ class Will:
|
||||
continue
|
||||
utxo_str = utxo.prevout.to_str()
|
||||
if utxo_str in prevout_to_spend:
|
||||
balance += inputs[utxo_str][0][2].value_sats()
|
||||
balance += utxo.value_sats()
|
||||
utxo_to_spend.append(utxo)
|
||||
_logger.debug("utxo to spend: {}".format(utxo_to_spend))
|
||||
if len(utxo_to_spend) > 0:
|
||||
@@ -1327,49 +1326,76 @@ class WillItem(Logger):
|
||||
return self.STATUS[status][1]
|
||||
|
||||
def __init__(self, w, _id=None, wallet=None):
|
||||
if isinstance(
|
||||
w,
|
||||
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)
|
||||
self.we = w.get("willexecutor", None)
|
||||
self.status = w.get("status") or ""
|
||||
self.description = w.get("description", None)
|
||||
self.time = w.get("time", None)
|
||||
self.change = w.get("change", None)
|
||||
self.tx_fees = w.get("baltx_fees", 0)
|
||||
self.sigs_required = int(w.get("sigs_required", 0))
|
||||
self.sigs_have = int(w.get("sigs_have", 0))
|
||||
self.father = w.get("Father", None)
|
||||
self.children = w.get("Children", None)
|
||||
self.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
|
||||
for s in self.STATUS:
|
||||
self.STATUS[s][1] = w.get(s, WillItem.STATUS_DEFAULT[s][1])
|
||||
# Backward-compatibility migration (A2): the "PENDING" status was
|
||||
# renamed to "MEMPOOL". Wills saved by older versions of the plugin
|
||||
# store the flag under the legacy "PENDING" key, so if that key is
|
||||
# present and set, carry it over to "MEMPOOL". This way no state is
|
||||
# lost when loading an older will. The new key always wins if both
|
||||
# happen to be present.
|
||||
if "MEMPOOL" not in w and w.get("PENDING"):
|
||||
self.STATUS["MEMPOOL"][1] = True
|
||||
if isinstance(w, WillItem):
|
||||
# Copy a WillItem WITHOUT deepcopy. Serialize it to its plain-dict
|
||||
# form and deserialize from there: the tx is re-parsed into a fresh
|
||||
# object, STATUS is rebuilt from the clones below and heirs /
|
||||
# will-executors are cloned recursively, so the copy shares no
|
||||
# mutable state with the source. See also copy().
|
||||
data = w.to_dict()
|
||||
data["heirs"] = copy_structure(w.heirs) if w.heirs is not None else None
|
||||
data["willexecutor"] = (
|
||||
copy_structure(w.we) if w.we is not None else None
|
||||
)
|
||||
if not _id:
|
||||
self._id = self.tx.txid()
|
||||
else:
|
||||
self._id = _id
|
||||
_id = w._id
|
||||
w = data
|
||||
self.tx = Will.get_tx_from_any(w["tx"])
|
||||
self.heirs = w.get("heirs", None)
|
||||
self.we = w.get("willexecutor", None)
|
||||
self.status = w.get("status") or ""
|
||||
self.description = w.get("description", None)
|
||||
self.time = w.get("time", None)
|
||||
self.change = w.get("change", None)
|
||||
self.tx_fees = w.get("baltx_fees", 0)
|
||||
self.sigs_required = int(w.get("sigs_required", 0))
|
||||
self.sigs_have = int(w.get("sigs_have", 0))
|
||||
self.father = w.get("Father", None)
|
||||
self.children = w.get("Children", None)
|
||||
self.STATUS = WillItem.copy_status_table(WillItem.STATUS_DEFAULT)
|
||||
for s in self.STATUS:
|
||||
self.STATUS[s][1] = w.get(s, WillItem.STATUS_DEFAULT[s][1])
|
||||
# Backward-compatibility migration (A2): the "PENDING" status was
|
||||
# renamed to "MEMPOOL". Wills saved by older versions of the plugin
|
||||
# store the flag under the legacy "PENDING" key, so if that key is
|
||||
# present and set, carry it over to "MEMPOOL". This way no state is
|
||||
# lost when loading an older will. The new key always wins if both
|
||||
# happen to be present.
|
||||
if "MEMPOOL" not in w and w.get("PENDING"):
|
||||
self.STATUS["MEMPOOL"][1] = True
|
||||
if not _id:
|
||||
self._id = self.tx.txid()
|
||||
else:
|
||||
self._id = _id
|
||||
|
||||
if not self._id:
|
||||
self.status += "ERROR!!!"
|
||||
self.valid = False
|
||||
if not self._id:
|
||||
self.status += "ERROR!!!"
|
||||
self.valid = False
|
||||
|
||||
if wallet:
|
||||
self.tx.add_info_from_wallet(wallet)
|
||||
|
||||
def copy(self, wallet=None):
|
||||
"""Return an independent copy of this WillItem (no deepcopy).
|
||||
|
||||
The copy is produced by serializing this item and deserializing it:
|
||||
the transaction is re-parsed, the STATUS table is rebuilt and
|
||||
heirs / will-executors are cloned recursively, so the result shares no
|
||||
mutable state with ``self``. Pass a ``wallet`` when the copy's tx
|
||||
needs its address/value information restored
|
||||
(``tx.add_info_from_wallet``).
|
||||
"""
|
||||
return WillItem(self, _id=self._id, wallet=wallet)
|
||||
|
||||
@staticmethod
|
||||
def copy_status_table(status_table):
|
||||
"""Clone a STATUS table (``{flag: [label, bool]}``) without deepcopy.
|
||||
|
||||
Both the outer dict and every inner ``[label, bool]`` list are new
|
||||
objects, so mutating the returned table never affects the source.
|
||||
"""
|
||||
return {k: [label, value] for k, (label, value) in status_table.items()}
|
||||
|
||||
def to_dict(self):
|
||||
out = {
|
||||
"_id": self._id,
|
||||
@@ -1383,6 +1409,8 @@ class WillItem(Logger):
|
||||
"baltx_fees": self.tx_fees,
|
||||
"sigs_required": self.sigs_required,
|
||||
"sigs_have": self.sigs_have,
|
||||
"Father": self.father,
|
||||
"Children": self.children,
|
||||
}
|
||||
for key in self.STATUS:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user