feat(v0.4.7): report area 500px, heirs one-per-line, wizard line breaks, ALL-DUST guard
Owner-approved changes after testing v0.4.6, plus accumulated v0.4.x work, task-tracking notes, and an updated project HANDOFF document. The four v0.4.7 changes: 1. Report area (BalBuildWillDialog) opens 500px tall (min) up to 700px (max), then the scrollbar takes over. Previously it opened ~140px (too short). 2. Heirs are listed ONE per line again (green, bold) in _build_success_report, reverting the v0.4.6 single-line form. Heir names can be long and the report now scrolls, so compression is no longer needed. 3. Two wizard texts get an explicit line break: after "(or backup)" in the date hint and after "miner fees" in the fee note (widgets.py). 4. ALL-DUST guard: when EVERY heir's share is below the Bitcoin dust limit, the inheritance would pay nobody. Heirs.prepare_lists now raises HeirAmountIsDustException at the end (where all heirs across all locktimes are known with their final dust state), and dialogs.task_phase1 shows a clear RED message and stops without building/signing/checking. A mix of dust + valid heirs keeps building normally. The guard is intentionally in prepare_lists, NOT prepare_transactions (which only sees the lowest locktime and would false-positive). HeirAmountIsDustException is imported in common.py. Tests: 3 new tests in test_core_heirs_extra.py pin the dust behaviour (all-dust raises; mixed continues; multi-locktime continues). Full suite: 258 passed. ruff: no new errors. Version bumped 0.4.6 -> 0.4.7 (4 files). CHANGELOG #23. Also adds/updates HANDOFF.md so any future AI (Claude or another model) can resume the project with full context (rules, layout, build/test/lint, dust logic, git flow), and records the task-tracking notes in .agent_memory_tasks.md.
This commit is contained in:
@@ -554,6 +554,43 @@ class Heirs(dict, Logger):
|
||||
locktimes[locktime] = {key: value}
|
||||
else:
|
||||
locktimes[locktime][key] = value
|
||||
|
||||
# ALL-DUST GUARD (owner request, see CHANGELOG / log analysis).
|
||||
#
|
||||
# WHY HERE: ``locktimes`` now contains EVERY heir across EVERY locktime,
|
||||
# with their final resolved amount already computed and dust-marked
|
||||
# ("DUST: <n>" in HEIR_REAL_AMOUNT) by fixed_percent_lists_amount /
|
||||
# normalize_perc above. This is the only place where we can reliably
|
||||
# tell whether *all* heirs are dust, for BOTH fixed and percentage
|
||||
# heirs and across all dates. ``prepare_transactions`` only sees the
|
||||
# single lowest locktime, so checking there would wrongly block a will
|
||||
# whose later locktimes still have valid heirs (false positive).
|
||||
#
|
||||
# WHAT: count the REAL heirs (excluding the internal will-executor
|
||||
# pseudo-heirs, whose names start with the reserved ``w!ll3x3c"``
|
||||
# marker) and how many of them have a valid, non-dust amount. If there
|
||||
# are real heirs but NONE of them is payable, the inheritance would pay
|
||||
# nobody (only the change + the will-executor fee). Previously such an
|
||||
# "empty" will was still built, signed, checked and listed; we now
|
||||
# refuse it and raise HeirAmountIsDustException so the GUI can show a
|
||||
# clear message and stop. A mix of dust + valid heirs keeps building
|
||||
# normally with the valid ones (unchanged behaviour).
|
||||
real_heirs = 0
|
||||
valid_real_heirs = 0
|
||||
for heirs_at_locktime in locktimes.values():
|
||||
for name, heir in heirs_at_locktime.items():
|
||||
if str(name).startswith('w!ll3x3c"'):
|
||||
continue
|
||||
real_heirs += 1
|
||||
if len(heir) > HEIR_REAL_AMOUNT and "DUST" not in str(
|
||||
heir[HEIR_REAL_AMOUNT]
|
||||
):
|
||||
valid_real_heirs += 1
|
||||
if real_heirs > 0 and valid_real_heirs == 0:
|
||||
raise HeirAmountIsDustException(
|
||||
"All heirs' shares are below the dust limit"
|
||||
)
|
||||
|
||||
return locktimes, onlyfixed
|
||||
|
||||
def is_perc(self, key):
|
||||
|
||||
@@ -91,7 +91,7 @@ class BalPlugin(BasePlugin):
|
||||
"""
|
||||
|
||||
_version = None
|
||||
__version__ = "0.3.9" # AUTOMATICALLY GENERATED DO NOT EDIT
|
||||
__version__ = "0.4.7" # AUTOMATICALLY GENERATED DO NOT EDIT
|
||||
|
||||
# Command used to open an .ics calendar file, per operating system.
|
||||
default_app = {
|
||||
@@ -190,11 +190,31 @@ class BalPlugin(BasePlugin):
|
||||
# most one event per available day.
|
||||
self.NUM_REMINDERS = BalConfig(config, "bal_num_reminders", 3)
|
||||
|
||||
self.NO_WILLEXECUTOR = BalConfig(config, "bal_no_willexecutor", True)
|
||||
# "Add transaction without will-executor" backup tx. Default OFF
|
||||
# (False): a fresh wallet does NOT create the extra no-will-executor
|
||||
# backup transaction (the "azure" tx), so a plain inheritance has no
|
||||
# backup tx unless the user explicitly enables it from the wizard. The
|
||||
# chosen value is persisted per wallet, so reopening the plugin always
|
||||
# follows what is saved in that wallet (the default only applies when no
|
||||
# value has been stored yet, i.e. new wallets).
|
||||
self.NO_WILLEXECUTOR = BalConfig(config, "bal_no_willexecutor", False)
|
||||
self.HIDE_REPLACED = BalConfig(config, "bal_hide_replaced", True)
|
||||
self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True)
|
||||
self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True)
|
||||
self.FIRST_EXECUTION = BalConfig(config, "bal_first_execution", True)
|
||||
# SIMPLE / ADVANCED mode (global, plugin-wide).
|
||||
#
|
||||
# "basic" -> SIMPLE mode (DEFAULT): hides advanced controls (the
|
||||
# Raw/Date selector and the "Check Alive" field/icon) and
|
||||
# disables the "check alive" postpone behaviour, so the
|
||||
# plugin is easier for non-technical users.
|
||||
# "advanced" -> shows every control (the original behaviour).
|
||||
#
|
||||
# This is stored in Electrum's GLOBAL config (not in the wallet file),
|
||||
# so it never affects compatibility with existing wallets: an old wallet
|
||||
# simply opens with whatever global value is set, and its owner can
|
||||
# switch to "advanced" from the plugin settings whenever they want.
|
||||
self.USER_TYPE = BalConfig(config, "bal_user_type", "basic")
|
||||
self.WELIST_SERVER = BalConfig(
|
||||
config, "bal_welist_server", "https://welist.bitcoin-after.life/"
|
||||
)
|
||||
@@ -315,6 +335,16 @@ class BalPlugin(BasePlugin):
|
||||
will_settings["locktime"] = defaults['locktime']
|
||||
return will_settings
|
||||
|
||||
def is_basic_mode(self):
|
||||
"""Return True when the plugin runs in SIMPLE ("basic") mode.
|
||||
|
||||
Centralises the USER_TYPE check so the GUI never compares the raw
|
||||
string in many places. Anything other than the explicit "advanced"
|
||||
value is treated as basic, so the safe/simple behaviour is the default
|
||||
even if the stored value is missing or unexpected.
|
||||
"""
|
||||
return str(self.USER_TYPE.get()).lower() != "advanced"
|
||||
|
||||
@staticmethod
|
||||
def default_will_settings():
|
||||
"""Default will settings: a fee rate plus absolute threshold/locktime."""
|
||||
|
||||
@@ -349,6 +349,47 @@ class Will:
|
||||
|
||||
Will.search_anticipate_rec(will, old_inputs)
|
||||
|
||||
@staticmethod
|
||||
def _same_heirs(old_heirs, new_heirs):
|
||||
"""Return True if two heir maps describe the SAME inheritance.
|
||||
|
||||
Used by update_will (Option A) to decide whether a rebuilt transaction
|
||||
that kept the same txid can safely reuse the old (possibly already
|
||||
signed) WillItem, or whether the heirs changed and the item must be
|
||||
rebuilt as unsigned.
|
||||
|
||||
Two heir maps are considered equal when they have exactly the same heir
|
||||
names (keys) and, for each heir, the same destination ADDRESS, the same
|
||||
requested AMOUNT and the same LOCKTIME. Internal will-executor
|
||||
pseudo-heirs (keys starting with the reserved ``w!ll3x3c"`` prefix) are
|
||||
ignored, exactly as in check_willexecutors_and_heirs, because they are
|
||||
bookkeeping entries and not real heirs.
|
||||
|
||||
Args:
|
||||
old_heirs: heirs dict stored in the old (existing) WillItem.
|
||||
new_heirs: heirs dict of the freshly rebuilt WillItem.
|
||||
|
||||
Returns:
|
||||
bool: True if the real heirs are identical, False otherwise.
|
||||
"""
|
||||
def _real_heirs(heirs):
|
||||
# Keep only the real heirs and only the fields that define the
|
||||
# inheritance (address/amount/locktime), so cosmetic or derived
|
||||
# fields can never trigger a spurious "heirs changed" rebuild.
|
||||
out = {}
|
||||
for name, entry in (heirs or {}).items():
|
||||
if str(name)[:9] == 'w!ll3x3c"':
|
||||
continue
|
||||
# Heir entry layout (see heirs.py): [0]=address, [1]=amount,
|
||||
# [2]=locktime. We compare exactly the same fields that
|
||||
# check_willexecutors_and_heirs uses (their[0], their[1],
|
||||
# their[2]); index literals are used here to avoid importing the
|
||||
# heirs module (which would create a circular import).
|
||||
out[name] = (entry[0], entry[1], entry[2])
|
||||
return out
|
||||
|
||||
return _real_heirs(old_heirs) == _real_heirs(new_heirs)
|
||||
|
||||
@staticmethod
|
||||
def update_will(old_will, new_will):
|
||||
all_old_inputs = Will.get_all_inputs(old_will, only_valid=True)
|
||||
@@ -368,9 +409,32 @@ class Will:
|
||||
new_heirs = new_will[oid].heirs
|
||||
new_we = new_will[oid].we
|
||||
|
||||
new_will[oid] = old_will[oid]
|
||||
new_will[oid].heirs = new_heirs
|
||||
new_will[oid].we = new_we
|
||||
# OPTION A (heir-change full rebuild, user-approved):
|
||||
#
|
||||
# Historically, whenever a rebuilt transaction kept the SAME
|
||||
# txid as an old one, we REUSED the old WillItem object (which
|
||||
# may already be signed/COMPLETE/PUSHED) and only copied the new
|
||||
# heirs/will-executor onto it. That silently preserved the
|
||||
# "already signed" status even when the HEIRS had actually
|
||||
# changed (e.g. an heir was deleted, so amounts must be
|
||||
# recomputed and the whole wallet re-swept). The downstream
|
||||
# have_to_sign check then saw the item as COMPLETE and reported
|
||||
# "Nothing to do", so the new will was never signed/broadcast
|
||||
# (bugs E/F/K).
|
||||
#
|
||||
# We now reuse the old item ONLY when the heirs are IDENTICAL.
|
||||
# If the heir set/values changed, we keep the freshly built
|
||||
# item (status "New", not COMPLETE) so it is correctly detected
|
||||
# as needing a new signature and broadcast. The will-executor is
|
||||
# still refreshed in both cases.
|
||||
if Will._same_heirs(old_will[oid].heirs, new_heirs):
|
||||
new_will[oid] = old_will[oid]
|
||||
new_will[oid].heirs = new_heirs
|
||||
new_will[oid].we = new_we
|
||||
else:
|
||||
# Heirs changed: keep the new (unsigned) item but make sure
|
||||
# it carries the up-to-date will-executor.
|
||||
new_will[oid].we = new_we
|
||||
|
||||
continue
|
||||
else:
|
||||
|
||||
@@ -30,6 +30,14 @@ from .plugin_base import BalPlugin
|
||||
# block the UI.
|
||||
DEFAULT_TIMEOUT = 5
|
||||
|
||||
# Single, shared wall-clock deadline (seconds) for ALL network waits the user
|
||||
# can watch in the GUI: the parallel broadcast (pushtxs), the parallel check
|
||||
# (searchtx), the will-executor ping and the will-executor list download.
|
||||
# Having ONE constant (instead of several scattered 30s/45s values) keeps the
|
||||
# experience consistent and makes it trivial to tune. Requested by the user
|
||||
# (reduced from 30s/45s to 20s, unified into one variable).
|
||||
NETWORK_DEADLINE = 20
|
||||
|
||||
# Broadcast (pushtxs) timeouts. Broadcasting a will is important, so we keep a
|
||||
# couple of quick retries to survive a transient hiccup -- but far from the old
|
||||
# 10s x 10 retries + 30s sleeps (~140s) that froze the wizard on a dead server.
|
||||
@@ -43,7 +51,8 @@ PUSH_RETRY_SLEEP = 1
|
||||
# Global wall-clock deadline (seconds) for the whole parallel broadcast. Once
|
||||
# it elapses we stop waiting for the still-pending servers, mark them as
|
||||
# "Timeout" and let the wizard proceed instead of appearing stuck.
|
||||
PUSH_GLOBAL_DEADLINE = 30
|
||||
# Derived from the single shared NETWORK_DEADLINE constant above.
|
||||
PUSH_GLOBAL_DEADLINE = NETWORK_DEADLINE
|
||||
|
||||
# Check (searchtx) timeouts. Used when the user presses "Check" to verify that
|
||||
# each will-executor still holds the transaction. Like the broadcast path, the
|
||||
@@ -53,7 +62,8 @@ PUSH_GLOBAL_DEADLINE = 30
|
||||
CHECK_TIMEOUT = 8
|
||||
CHECK_MAX_RETRIES = 1
|
||||
CHECK_RETRY_SLEEP = 1
|
||||
CHECK_GLOBAL_DEADLINE = 30
|
||||
# Derived from the single shared NETWORK_DEADLINE constant above.
|
||||
CHECK_GLOBAL_DEADLINE = NETWORK_DEADLINE
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
@@ -68,6 +78,7 @@ class Willexecutors:
|
||||
# importing module-level names. Single source of truth: the module
|
||||
# constants defined above.
|
||||
DEFAULT_TIMEOUT = DEFAULT_TIMEOUT
|
||||
NETWORK_DEADLINE = NETWORK_DEADLINE
|
||||
PUSH_TIMEOUT = PUSH_TIMEOUT
|
||||
PUSH_MAX_RETRIES = PUSH_MAX_RETRIES
|
||||
PUSH_RETRY_SLEEP = PUSH_RETRY_SLEEP
|
||||
|
||||
Reference in New Issue
Block a user