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:
829
.agent_memory_tasks.md
Normal file
829
.agent_memory_tasks.md
Normal file
@@ -0,0 +1,829 @@
|
||||
|
||||
## TASK A (proposto, NON ancora avviato) — Migliorare il messaggio "WILL EXPIRED"
|
||||
|
||||
**Origine:** osservato dall'utente negli screenshot del 23/06/2026 (will scaduto -> percorso invalidate + re-sign). La LOGICA è corretta; si migliora solo la UX/chiarezza del messaggio.
|
||||
|
||||
**3 miglioramenti approvati dall'utente (da implementare poi, in inglese, regole R1-R4 + zip-first):**
|
||||
1. Tradurre il timestamp Unix grezzo (es. 1782118800) in data leggibile.
|
||||
Esempio: invece di "Will Expired 9f1b0a75...: 1782118800"
|
||||
mostrare "Will expired (locktime 2026-06-22 11:00 UTC) - too late to anticipate, will invalidate and re-sign".
|
||||
2. Rendere il messaggio INFORMATIVO e non un ERRORE: il rosso sembra un errore,
|
||||
ma e' un flusso normale. Usare colore di avviso (arancione) o aggiungere frase
|
||||
tipo "This is expected: the will is past its locktime, switching to invalidate + re-sign."
|
||||
3. Accorciare l'hash del will per leggibilita' (es. primi 8 + ultimi 4 caratteri).
|
||||
|
||||
**Note tecniche da fare in DISCOVER quando si avvia:**
|
||||
- Trovare il punto del codice che genera la stringa "Will Expired ... <timestamp>" (probabilmente nel wizard "Building Will").
|
||||
- Verificare se il colore rosso e' impostato li' (rich text / stylesheet).
|
||||
- Seguire METHOD: DISCOVER -> PLAN (attendere OK) -> EXECUTE -> VERIFY -> ZIP-first -> commit/PR/release solo dopo conferma.
|
||||
|
||||
## TASK B (proposto, NON ancora avviato) — Etichette descrizione transazioni in Cronologia
|
||||
|
||||
**Origine:** osservato dall'utente nello screenshot Cronologia del 23/06/2026.
|
||||
Nella tab "Cronologia" di Electrum (transazioni on-chain), il plugin BAL scrive
|
||||
una descrizione colorata nella colonna "Descrizione".
|
||||
|
||||
**Stato attuale:**
|
||||
- La transazione di EREDITA' e' etichettata "BAL Transaction" (colore rosso).
|
||||
- La transazione di INVALIDATE NON ha alcuna descrizione.
|
||||
|
||||
**Modifiche richieste (approvate dall'utente):**
|
||||
1. RINOMINARE l'etichetta dell'eredita': "BAL Transaction" -> "BAL Inheritance transaction".
|
||||
2. CAMBIARE il colore dell'etichetta eredita' da ROSSO a VERDE.
|
||||
3. AGGIUNGERE una nuova etichetta per le transazioni di invalidate:
|
||||
"BAL Invalidate transaction" in colore ARANCIONE (oggi compaiono senza descrizione).
|
||||
|
||||
**Riepilogo finale desiderato:**
|
||||
| Transazione | Etichetta desiderata | Colore |
|
||||
|-------------|-----------------------------|-----------|
|
||||
| Eredita' | BAL Inheritance transaction | VERDE |
|
||||
| Invalidate | BAL Invalidate transaction | ARANCIONE |
|
||||
|
||||
**Note tecniche da fare in DISCOVER quando si avvia:**
|
||||
- Trovare nel codice dove viene impostata l'etichetta "BAL Transaction"
|
||||
(probabilmente wallet.set_label(txid, ...) o simile) e dove/come e' impostato il colore.
|
||||
- Capire come il plugin distingue una tx di eredita' da una di invalidate, per
|
||||
applicare l'etichetta corretta a ciascuna. La tx di invalidate oggi NON riceve
|
||||
label -> trovare il punto dove viene creata/broadcastata e aggiungere lì set_label.
|
||||
- Verificare se il colore (rosso) e' gestito da Electrum o dal plugin, e come
|
||||
impostare il verde per l'eredita'.
|
||||
- Seguire METHOD: DISCOVER -> PLAN (attendere OK) -> EXECUTE -> VERIFY -> ZIP-first -> commit/PR/release solo dopo conferma.
|
||||
|
||||
## TASK C (proposto, NON ancora avviato) — Checkbox "no will-executor" anche in Plugin Settings
|
||||
|
||||
**Origine:** richiesta utore del 23/06/2026.
|
||||
Nel wizard "Create your WILL", finestra di download will-executor, esiste il checkbox
|
||||
"Add transactions without willexecutor". L'utente vuole lo STESSO checkbox anche nella
|
||||
finestra "Plugin settings" (stesso stile delle altre righe), default ON, con un
|
||||
HelpButton che spiega la funzione.
|
||||
|
||||
**Testo del tastino di aiuto (fornito dall'utente, da usare verbatim):**
|
||||
"Create a will that does not require a Will-executor; it can be saved, for example,
|
||||
on a USB stick, and a copy can be given to the heirs."
|
||||
|
||||
**SCOPERTA IMPORTANTE (DISCOVER gia' fatto):**
|
||||
- La config ESISTE GIA': `self.NO_WILLEXECUTOR = BalConfig(config, "bal_no_willexecutor", True)`
|
||||
in `bal/core/plugin_base.py:193` (default True = ON). Quindi NON va creata.
|
||||
- Il checkbox del wizard e' in `bal/gui/qt/lists.py:916-918`:
|
||||
hbox.addWidget(QLabel(_("Add transactions without willexecutor")))
|
||||
heir_no_willexecutor = BalCheckBox(self.bal_plugin.NO_WILLEXECUTOR)
|
||||
-> usa BalCheckBox legato alla stessa config. Aggiungendo lo stesso checkbox nelle
|
||||
settings, i due rimangono sincronizzati automaticamente (stessa BalConfig).
|
||||
- La finestra Settings e' `settings_dialog()` in `bal/gui/qt/plugin.py:372`.
|
||||
Usa una griglia con la helper `add_widget(grid, label, widget, row, help_)`
|
||||
(definita in `bal/gui/qt/common.py:98`) che mette: QLabel(col0), widget(col1),
|
||||
HelpButton(col2). Le righe attuali vanno 1..8 (8 = bottone Rebroadcast).
|
||||
- Esiste gia' codice COMMENTATO che faceva esattamente questo (plugin.py:382 e
|
||||
536-542): `# heir_no_willexecutor = BalCheckBox(self.NO_WILLEXECUTOR)` e un
|
||||
add_widget "Backup Transaction" -> si puo' riattivare/adattare.
|
||||
- C'e' anche un blocco "Reset setting" (on_reset_defaults, plugin.py:560-596) con
|
||||
una lista `resets = [...]`: per coerenza la nuova checkbox va AGGIUNTA a quella
|
||||
lista cosi' il Reset la riporta al default (ON).
|
||||
|
||||
**PLAN bozza (da rifinire e far approvare quando si avvia):**
|
||||
1. In `settings_dialog()`: creare `heir_no_willexecutor = BalCheckBox(self.NO_WILLEXECUTOR)`.
|
||||
2. Aggiungere una riga con `add_widget(grid, "<label>", heir_no_willexecutor, <row>, "<help text utente>")`.
|
||||
- Decidere label breve (es. "No will-executor" / "Backup will (no will-executor)") -> CHIEDERE ALL'UTENTE quale label preferisce nella colonna sinistra.
|
||||
- Decidere la riga: inserire prima del Rebroadcast (riga 8), eventualmente rinumerando le righe successive.
|
||||
3. Aggiungere `(self.NO_WILLEXECUTOR, heir_no_willexecutor, "check")` alla lista `resets`
|
||||
in on_reset_defaults, cosi' il "Reset setting" la riporta a ON.
|
||||
4. Mantenere lo stile esistente (HelpButton, griglia). Default gia' ON via config.
|
||||
|
||||
**DOMANDA DA PORRE PRIMA DI EXECUTE:** quale etichetta breve mostrare nella colonna
|
||||
sinistra delle settings? (l'utente ha fornito solo il testo del tastino di aiuto).
|
||||
|
||||
- Seguire METHOD: DISCOVER(fatto) -> PLAN (attendere OK) -> EXECUTE -> VERIFY -> ZIP-first -> commit/PR/release solo dopo conferma.
|
||||
|
||||
## TASK D (proposto, NON ancora avviato) — Rinominare il tastone del wizard
|
||||
|
||||
**Origine:** richiesta utente del 23/06/2026.
|
||||
Il tastone grande che apre il wizard mostra "Create your will" e l'utente vuole
|
||||
cambiarlo in "Build your will" (aveva scritto "BUIL YOUR WILL" -> refuso per BUILD).
|
||||
|
||||
**SCOPERTA (DISCOVER gia' fatto):**
|
||||
- Il bottone e' in `bal/gui/qt/lists.py:473`:
|
||||
wizard = QPushButton(" " + _("Create your will"))
|
||||
- INCOERENZA esistente: il tooltip dello STESSO bottone (lists.py:483) dice gia'
|
||||
wizard.setToolTip(_("Wizard - Build your will"))
|
||||
e tutti i commenti del codice (8 occorrenze) chiamano il wizard "Build your will".
|
||||
Quindi cambiare il testo del bottone in "Build your will" UNIFORMA tutto.
|
||||
|
||||
**PARERE AGENTE (concordato con l'utente):** "Build your will" e' la scelta migliore
|
||||
(coerenza con tooltip/commenti/config; "Build" comunica meglio il processo guidato a step).
|
||||
|
||||
**FORMA SCELTA DALL'UTENTE:** "Build Your Will" (title case).
|
||||
|
||||
**PLAN bozza:** sostituire la sola stringa "Create your will" -> "Build Your Will"
|
||||
(title case) in `lists.py:473`. Verificare che non ci siano altre occorrenze del
|
||||
testo del bottone da allineare. Lasciare invariato il tooltip (gia' "Build your will").
|
||||
|
||||
- Seguire METHOD: DISCOVER(fatto) -> PLAN (attendere OK) -> EXECUTE -> VERIFY -> ZIP-first -> commit/PR/release solo dopo conferma.
|
||||
|
||||
================================================================
|
||||
## PENDING TASK (post-v0.3.9) — UNIFY INVALIDATE PROCEDURE — NOT STARTED
|
||||
## Status: WAITING for user to provide MORE input before planning/acting.
|
||||
## User said: "OPZIONE A ... ma aspetta prima di agire, che ho altro da darti in pasto"
|
||||
## then refined the requirement (see below), then: "pero attendi altro, intanto salva questo punto"
|
||||
================================================================
|
||||
|
||||
### GOAL: make the "invalidate" procedure IDENTICAL for both CHECK button and WIZARD.
|
||||
|
||||
Currently there are TWO different invalidate procedures (user noticed the inconsistency):
|
||||
- PROCEDURE 1 "classic/manual" = window.py::invalidate_will (line 695):
|
||||
waiting dialog + "please sign and broadcast" popup + CLASSIC Electrum tx window
|
||||
(Sign/Broadcast buttons) + SETS history label "BAL Invalidate transaction" (line 704).
|
||||
Used by: Tools->Invalidate menu (lists.py:468->571), dialog button (dialogs.py:1356),
|
||||
on-close/postpone paths (window.py:539,576,614), and the wizard-add-heir popup (my v0.3.9).
|
||||
- PROCEDURE 2 "automatic" = dialogs.py::invalidate_task (line 902):
|
||||
password prompt inside wizard -> sign + auto broadcast (loop_broadcast_invalidating, line 729)
|
||||
-> DOES NOT set the history label. Used by CHECK button (lists.py:545 -> BalBuildWillDialog.build_will_task)
|
||||
and the FIRST WillExpiredException handler in task_phase1 (dialogs.py:594 -> return None, Will.invalidate_will at 598).
|
||||
|
||||
### CHECK button flow: lists.py:545 check() -> BalBuildWillDialog(...).build_will_task()
|
||||
-> task_phase1 -> on_success_phase1. SAME engine as wizard.
|
||||
- If expired immediately -> FIRST handler (dialogs.py:594) -> return None, Will.invalidate_will -> on_success_phase1 have_to_sign is None -> password prompt "Invalidate your old will" -> invalidate_task (PROCEDURE 2, NO label).
|
||||
- If heir added -> HeirNotFoundException -> build_will -> inner check expired -> "invalidate_classic" signal -> my v0.3.9 popup (Tools->Invalidate).
|
||||
|
||||
### FINAL REQUIREMENT (user, latest):
|
||||
1. CHECK and WIZARD must behave IDENTICALLY.
|
||||
2. First show a WARNING popup (no more "use the top-right menu Tools -> Invalidate" text).
|
||||
3. Then AUTOMATICALLY open the CLASSIC Electrum sign window (PROCEDURE 1 / window.py::invalidate_will),
|
||||
which sets the "BAL Invalidate transaction" label and lets the user Sign + Broadcast.
|
||||
4. So: warning popup -> user clicks OK -> classic sign window opens BY ITSELF, IN FRONT.
|
||||
|
||||
### APPROACH agreed-in-principle (still need final OK + user has MORE input coming):
|
||||
- Close the CHECK/wizard dialog FIRST, show warning, then open classic window LAST so it stays in front
|
||||
(the focus problem before came from the wizard closing AFTER the tx window opened).
|
||||
- Unify: route the expired cases (FIRST handler + invalidate_classic + CHECK) to the SAME helper that
|
||||
shows the warning then calls window.py::invalidate_will (PROCEDURE 1). Drop PROCEDURE 2 (invalidate_task) usage for expired.
|
||||
- KNOWN RISK: auto-opening the classic window while a dialog closes previously put it BEHIND the wallet on the user's PC.
|
||||
Mitigation: ensure the classic window is the LAST thing opened (nothing closes after it). Tools->Invalidate works
|
||||
perfectly precisely because nothing else is closing.
|
||||
|
||||
### WARNING POPUP TEXT (verbatim, user-approved, English per R1):
|
||||
"Your will has expired and must be invalidated before it can be rebuilt.
|
||||
A transaction window will now open:
|
||||
please SIGN and then BROADCAST it to invalidate your old will.
|
||||
After the invalidation is confirmed, press the Check button to finish the will."
|
||||
(NOTE: keep the exact wording incl. double space "and then BROADCAST" as user wrote it? -> ASK / normalize to single space.)
|
||||
|
||||
### DO NOT ACT YET. Wait for user's additional input. Then: full PLAN -> wait OK (R4) -> zip-first.
|
||||
|
||||
================================================================
|
||||
## FUTURE TASK (analysis only, NOT started) — SIMPLE / ADVANCED mode
|
||||
## Status: DISCOVER done. No PLAN yet, no code. User said "intanto fa l'analisi e tieni tutto qui".
|
||||
================================================================
|
||||
|
||||
### USER REQUEST
|
||||
Add a SIMPLE / ADVANCED switch in the plugin settings panel.
|
||||
- SIMPLE (DEFAULT): hide RAW mode and the CHECK ALIVE parameter from the UI.
|
||||
- ADVANCED: everything visible as today.
|
||||
- A wallet that already used RAW / has advanced wills must open in ADVANCED mode.
|
||||
- MUST keep compatibility with existing wallets that have OLD inheritances.
|
||||
- User asked whether it is feasible without rewriting the engine.
|
||||
|
||||
### USER CLARIFICATIONS (verbatim meaning)
|
||||
1. CHECK ALIVE = proof the user is still alive: opening Electrum proves you are alive;
|
||||
if you open the plugin AFTER the check-alive date, it proposes creating a postponed
|
||||
(anticipated/rescheduled) inheritance.
|
||||
2. In SIMPLE mode, locktime entry uses normal DATA mode (calendar), not RAW.
|
||||
3. A wallet that had RAW should open the plugin in ADVANCED mode.
|
||||
|
||||
### TECHNICAL FINDINGS (DISCOVER)
|
||||
- CHECK ALIVE = `will_settings["threshold"]` (a timestamp). Read in
|
||||
window.py::init_class_variables (~459): `date_to_check = BalTimestamp(threshold).to_timestamp()`;
|
||||
if `date_to_check < now` -> raises CheckAliveError (~469). Default relative dates:
|
||||
threshold "30d", locktime "1y" (plugin_base.py:341-342; defaults built at ~320-337).
|
||||
`threshold` is NOT used inside will.py (the tx-core); it lives in will_settings and
|
||||
drives the "are you still alive / postpone" prompt.
|
||||
- DELIVERY TIME = `will_settings["locktime"]` = the actual tx locktime. ESSENTIAL, untouched.
|
||||
- RAW / DATA = only an INPUT MODE in the UI (widgets.py): RAW = type "30d"/"1y"
|
||||
(TimeRawEditWidget / LockTimeRawEdit ~378/411); DATA = pick a calendar date.
|
||||
Combo defined ~253 options ["Raw","Date"]. The SAVED value is always a date/timestamp,
|
||||
so RAW vs DATA does NOT change what is stored in the will -> NO compatibility impact.
|
||||
- ThresholdTimeWidget (widgets.py:559) = the CHECK ALIVE editor (base_field="threshold",
|
||||
label "🚨"). LockTimeWidget (591) = DELIVERY TIME (base_field="locktime", label "🚛").
|
||||
- Help text for CHECK ALIVE already documents DATA vs RAW behaviour (widgets.py:562-575).
|
||||
|
||||
### FEASIBILITY VERDICT
|
||||
- Feasible: YES.
|
||||
- Rewrite engine from scratch: NOT NEEDED and NOT RECOMMENDED. CHECK ALIVE and RAW sit
|
||||
ON TOP of the engine. SIMPLE mode = UI hiding + sensible defaults, NOT an engine rewrite.
|
||||
- Compatibility with old wallets: PRESERVED, because the engine keeps reading the same
|
||||
threshold/locktime timestamps.
|
||||
|
||||
### PROPOSED APPROACH (to be turned into a PLAN later, then wait OK)
|
||||
- New config flag e.g. BalConfig "bal_ui_mode" / SIMPLE default (or a boolean ADVANCED=False).
|
||||
- SIMPLE: force locktime editor to DATA mode and HIDE the Raw/Date combo; HIDE the CHECK
|
||||
ALIVE (threshold) field; set threshold to a safe default automatically so
|
||||
init_class_variables still works (DECISION NEEDED: what default? e.g. threshold = locktime,
|
||||
or threshold = locktime minus a small delta, or disable the check-alive prompt entirely
|
||||
in SIMPLE). ASK THE USER what CHECK ALIVE should default to in SIMPLE before coding.
|
||||
- ADVANCED: current behaviour, all fields visible.
|
||||
- AUTO-DETECT advanced wallets: if an existing wallet/will was created with RAW or has a
|
||||
non-default threshold, open in ADVANCED automatically (per user rule #3). DECISION NEEDED:
|
||||
exact detection criterion (e.g. threshold != default, or a stored marker).
|
||||
- Engine (will.py, heirs.py) stays UNCHANGED -> compatibility preserved.
|
||||
|
||||
### OPEN QUESTIONS TO ASK BEFORE PLAN
|
||||
1. In SIMPLE mode, what should CHECK ALIVE do by default? (a) disabled/no prompt,
|
||||
(b) auto-set to a value (which?), (c) something else.
|
||||
2. Exact rule to auto-detect "advanced" wallets to force ADVANCED mode on open.
|
||||
3. Should the SIMPLE/ADVANCED switch be global (plugin-wide) or per-wallet?
|
||||
|
||||
### DO NOT ACT. Analysis stored. Wait for user to resume + answer open questions.
|
||||
|
||||
----------------------------------------------------------------
|
||||
## SIMPLE/ADVANCED — USER DECISIONS (round 2) — still analysis only, DO NOT code
|
||||
----------------------------------------------------------------
|
||||
|
||||
### DECISION 1 — CHECK ALIVE in SIMPLE mode
|
||||
In SIMPLE mode, CHECK ALIVE must behave AS IF IT DID NOT EXIST as a parameter that
|
||||
influences rewriting the inheritance. I.e. it must NOT trigger the "you are alive ->
|
||||
postpone / rewrite the will" behaviour. So in SIMPLE mode the check-alive prompt is
|
||||
effectively NEUTRALIZED (no postpone proposal driven by threshold).
|
||||
TECHNICAL IMPLICATION: window.py::init_class_variables (~459-470) computes
|
||||
date_to_check from will_settings["threshold"] and raises CheckAliveError if it is in
|
||||
the past. In SIMPLE mode we must avoid that path influencing rewrites -> e.g. set
|
||||
threshold so it never triggers (or skip the check-alive logic entirely when mode==SIMPLE).
|
||||
Exact mechanism to be decided in PLAN, but the INTENT is: SIMPLE = no check-alive effect.
|
||||
|
||||
### DECISION 2 — persistence of the mode
|
||||
- NEW wallets/wills: store the mode (SIMPLE or ADVANCED) as a piece of information saved
|
||||
WITH the wallet (per-wallet), via wallet.db (same place as "will": window.py uses
|
||||
self.wallet.db.get_dict("will"); plugin_base.py registers dicts e.g.
|
||||
json_db.register_dict("will_settings", ...) at line 55). So add a stored marker, e.g.
|
||||
in will_settings or a dedicated db key (to be decided in PLAN).
|
||||
- OLD wallets that do NOT have this info: open by DEFAULT as ADVANCED.
|
||||
(This also satisfies rule #3 from round 1: a wallet that used RAW opens in ADVANCED,
|
||||
because old wallets default to ADVANCED.)
|
||||
|
||||
### DECISION 3 — the SIMPLE/ADVANCED switch is GLOBAL (plugin-wide)
|
||||
- The toggle itself lives in the plugin settings panel as a GLOBAL config
|
||||
(a BalConfig, like the others in plugin_base.py ~146-211), default SIMPLE.
|
||||
- BUT each wallet also remembers the mode it was created/saved with (decision 2).
|
||||
-> PLAN must reconcile: global switch default = SIMPLE, yet an existing/old wallet
|
||||
opens ADVANCED, and new wallets persist their mode. Need to define precedence:
|
||||
likely the per-wallet stored mode wins when present; the global switch sets the
|
||||
default for NEW wallets and the global UI default. CONFIRM precedence with user in PLAN.
|
||||
|
||||
### PERSISTENCE MECHANISMS CONFIRMED (DISCOVER)
|
||||
- Per-wallet data: self.wallet.db.get_dict("will") (window.py:133). will_settings dict
|
||||
registered via json_db.register_dict("will_settings", ...) (plugin_base.py:55).
|
||||
- Global plugin settings: BalConfig wrappers over Electrum config (plugin_base.py:58, 146+).
|
||||
- => GLOBAL switch = new BalConfig; PER-WALLET mode marker = new key in wallet.db /
|
||||
will_settings. Both feasible, engine untouched.
|
||||
|
||||
### STILL OPEN FOR PLAN (ask/confirm before coding)
|
||||
- Precedence rule when global switch and per-wallet stored mode disagree.
|
||||
- Exact SIMPLE-mode mechanism to neutralize check-alive (set threshold vs skip logic).
|
||||
- Where exactly to store the per-wallet mode marker (will_settings key name).
|
||||
- What other ADVANCED-only UI elements to hide in SIMPLE besides RAW combo + CHECK ALIVE
|
||||
(e.g. multiverse, editable dates, num reminders?) -> ASK user for the full SIMPLE list.
|
||||
|
||||
### DO NOT ACT. Wait for user to resume. Then full PLAN -> wait OK (R4) -> zip-first.
|
||||
|
||||
================================================================
|
||||
## NEW TASKS (added by user, latest message) — TO-DO LIST ONLY, NOT STARTED
|
||||
## User: "ti aggiungo altri punti, da mettere in lista delle cose da fare"
|
||||
## NO coding yet. Each needs DISCOVER(refine) -> PLAN -> wait OK (R4) -> zip-first.
|
||||
================================================================
|
||||
|
||||
### TASK #01 — Fix misleading "heir not found" message after wizard when date was only anticipated
|
||||
**User (verbatim, IT):** "dopo aver fatto una rendita e il wizard, il plugin dice 'heir not found',
|
||||
ma in realtà è stato solo anticipata la data, il messaggio informativo è sbagliato, ma il resto funziona bene."
|
||||
**Problem:** the FUNCTIONAL behaviour is correct (the will is rebuilt with the anticipated/postponed
|
||||
date). Only the INFORMATIONAL message is wrong/misleading: it says "Heir not found" when actually the
|
||||
date was simply anticipated.
|
||||
**TECHNICAL CONTEXT (already discovered):**
|
||||
- The message comes from dialogs.py::task_phase1, in the `except NotCompleteWillException as e:` block
|
||||
(~625). At lines ~641-642:
|
||||
elif isinstance(e, HeirNotFoundException):
|
||||
message = _("Heir not found")
|
||||
- HeirNotFoundException is a subclass of NotCompleteWillException and is raised by will.py::search_rai
|
||||
(check_will order at will.py:561: check_invalidated -> check_will_expired -> search_rai).
|
||||
- So when an heir is added / date anticipated, the rebuild path raises HeirNotFoundException and the
|
||||
user sees "Heir not found", which is misleading.
|
||||
**DISCOVER to refine when starting:** confirm in which exact scenario(s) HeirNotFoundException is raised
|
||||
during a normal anticipate/postpone flow; decide the correct, non-alarming wording (English, R1).
|
||||
Possibly distinguish "genuine heir-not-found error" vs "date anticipated, rebuilding" so the message is
|
||||
accurate in both cases. ASK user for preferred wording if ambiguous (R3).
|
||||
|
||||
### TASK #02 — Will-executor server list: green-check ONLY servers that responded + green ping; re-evaluate each download
|
||||
**User (verbatim, IT):** "quando scarico dal wizard la lista dei will executor, il plugin deve spuntare
|
||||
di verde solo i server che hanno risposto correttamente, e che hanno il pallino del ping verde.
|
||||
altrimenti poi si impalla tutto il plugin e continua a fare broadcast a quelli che non rispondono bene.
|
||||
meglio scartare alla fonte i server che non rispondono subito bene. questo deve valere ogni volta che
|
||||
scarico la lista server; se la seconda volta scarico e un server che prima non rispondeva ora risponde,
|
||||
il plugin lo aggiunge alla lista server."
|
||||
**Goal:**
|
||||
1. When downloading the will-executor list from the wizard, auto-select (green check) ONLY servers that
|
||||
(a) responded correctly AND (b) have a GREEN ping dot.
|
||||
2. Discard non-responsive servers AT THE SOURCE (do not select / do not broadcast to them) so the plugin
|
||||
does not get stuck continuously broadcasting to dead/slow servers.
|
||||
3. This must apply EVERY time the list is downloaded. On a later download, if a previously non-responsive
|
||||
server now responds correctly, it gets added/selected again.
|
||||
**TECHNICAL CONTEXT (already discovered, to refine in DISCOVER):**
|
||||
- Download/selection logic: bal/core/willexecutors.py (get_willexecutors, get_willexecutor_transactions,
|
||||
is_selected, push logic). Willexecutors.is_selected(...) decides the green checkmark.
|
||||
- Wizard download window/checkbox UI is in lists.py (download list ~917 area).
|
||||
- The push/broadcast loop is dialogs.py::loop_push (~750), which contacts SELECTED servers.
|
||||
- NEED: filter out servers that don't respond / lack green ping BEFORE selecting them, and re-run this
|
||||
evaluation on every download (so the selected set reflects current health each time).
|
||||
**DISCOVER to refine when starting:** find exactly where the ping/green-dot status is computed and where
|
||||
is_selected is set after a download; decide the criterion ("responded correctly" + "green ping") and how
|
||||
to re-evaluate on each download without losing manual user choices (ASK if conflict). Engine of will.py
|
||||
untouched.
|
||||
|
||||
### TASK #03 — Invalidate tx missing "BAL Invalidate transaction" history label when invalidate done from the AUTO-opened window
|
||||
**User (verbatim, IT):** "come già detto prima con te, la transazione invalidate non ha scritto
|
||||
'BAL invalidate transaction' in cronologia del wallet, se fatto invalidate dalla finestra che si apre
|
||||
in automatico. mentre lo scrive solo quando faccio invalidate dal menu TOOLS, Invalidate manualmente."
|
||||
**Problem:** the history label "BAL Invalidate transaction" is written ONLY when invalidating manually via
|
||||
Tools -> Invalidate (PROCEDURE 1), NOT when invalidating from the window that opens automatically
|
||||
(PROCEDURE 2).
|
||||
**TECHNICAL CONTEXT (already discovered):**
|
||||
- PROCEDURE 1 (manual, GOOD): window.py::invalidate_will (~695) sets the label at ~704:
|
||||
self.wallet.set_label(result.txid(), "BAL Invalidate transaction")
|
||||
- PROCEDURE 2 (automatic, MISSING label): dialogs.py::invalidate_task (~902) +
|
||||
loop_broadcast_invalidating (~729). Neither calls set_label. The FIRST WillExpiredException handler
|
||||
in task_phase1 (dialogs.py ~594-607 -> return None, Will.invalidate_will) routes the CHECK button to
|
||||
PROCEDURE 2, which is why the label is missing.
|
||||
**OVERLAP:** this is the SAME root cause described in the saved "PENDING TASK — UNIFY INVALIDATE
|
||||
PROCEDURE" above. Fixing the unify task (route all expired-invalidate cases to PROCEDURE 1, which sets
|
||||
the label) would also fix TASK #03. Keep them linked: solving "unify invalidate" with auto-open of the
|
||||
classic window (PROCEDURE 1) resolves #03 automatically. If implemented separately, the minimal fix is to
|
||||
add set_label("BAL Invalidate transaction") in the PROCEDURE 2 broadcast path.
|
||||
|
||||
### DO NOT ACT on #01/#02/#03. Saved to to-do list only. Wait for user to add more or to choose one;
|
||||
### then DISCOVER -> PLAN -> wait OK (R4) -> zip-first.
|
||||
|
||||
---
|
||||
|
||||
## PLUGIN STATES TABLE (requested in TASK #01 — for .md documentation)
|
||||
**Source of truth:** bal/core/will.py `STATUS_DEFAULT` (lines 898-917) + side-effect rules in
|
||||
`set_status` (lines 919-970); "Server" column derived in bal/gui/qt/theme.py `server_status_text`
|
||||
(lines 63-83). Built by READING the code (R3 — not invented). The visible "Stato" column is a
|
||||
COMPOSITE string built by appending each flag as it is set: `status += "." + name`, with "NOT "
|
||||
prepended when a flag is cleared (will.py:952). That is why screenshots show chains like
|
||||
"New.Firmato.Pushed.Checked.Confirmed".
|
||||
|
||||
### A. Individual status flags (STATUS_DEFAULT) — 18 flags
|
||||
Each flag is `[label, default_value]`. Default `value` is the initial boolean.
|
||||
|
||||
| Key (code) | Label shown | Default | Meaning (WHY it exists) |
|
||||
|--------------|---------------|:-------:|--------------------------|
|
||||
| ANTICIPATED | Anticipated | False | Locktime moved earlier by 1 day. KEEPS VALID on purpose (still deliverable). |
|
||||
| BROADCASTED | Broadcasted | False | The transaction was broadcast to the Bitcoin network. |
|
||||
| CHECKED | Checked | False | Will-executor server confirmed it holds the tx. Setting it also sets PUSHED and clears PUSH_FAIL. |
|
||||
| CHECK_FAIL | Check Failed | False | The post-push verification on the server failed. |
|
||||
| COMPLETE | Signed | False | The will transaction has been fully signed. |
|
||||
| CONFIRMED | Confirmed | False | Tx confirmed on-chain. Clears VALID and clears INVALIDATED. |
|
||||
| ERROR | Error | False | A generic error state for the item. |
|
||||
| EXPIRED | Expired | False | The will is past its locktime (delivery time reached). |
|
||||
| EXPORTED | Exported | False | The will item was exported (backup/share). |
|
||||
| IMPORTED | Imported | False | The will item was imported from external data. |
|
||||
| INVALIDATED | Invalidated | False | A spend invalidated this will. Clears VALID. Cleared again if CONFIRMED/MEMPOOL. |
|
||||
| MEMPOOL | Mempool | False | Tx seen in the mempool. Clears VALID and clears INVALIDATED. |
|
||||
| PUSH_FAIL | Push failed | False | Sending the tx to the will-executor server failed. |
|
||||
| PUSHED | Pushed | False | Tx was sent (pushed) to the will-executor server. Setting it clears PUSH_FAIL and CHECK_FAIL. |
|
||||
| REPLACED | Replaced | False | Superseded by another will tx. Clears VALID. |
|
||||
| RESTORED | Restored | False | Item restored (e.g. from a backup). |
|
||||
| UPDATED | Updated | False | Replaced by a new tx with SAME locktime + SAME heirs. KEEPS VALID on purpose. |
|
||||
| VALID | Valid | True | The only flag that starts True. The will is currently a valid, deliverable tx. |
|
||||
|
||||
### B. Side-effect rules (state machine) — will.py set_status (lines 955-969)
|
||||
- INVALIDATED, REPLACED, CONFIRMED, MEMPOOL -> clear VALID.
|
||||
- CONFIRMED, MEMPOOL -> also clear INVALIDATED (tx is on-chain/in mempool).
|
||||
- PUSHED -> clear PUSH_FAIL and CHECK_FAIL.
|
||||
- CHECKED -> set PUSHED, clear PUSH_FAIL.
|
||||
- ANTICIPATED and UPDATED -> intentionally KEEP VALID (not in the clear list).
|
||||
|
||||
### C. "Server" column (online will-executor state) — theme.py server_status_text, priority order
|
||||
Evaluated top-to-bottom; first match wins.
|
||||
| Condition (flags) | Text shown |
|
||||
|-------------------------------------------|-----------------------|
|
||||
| CHECK_FAIL AND NOT CHECKED | Not on server |
|
||||
| CHECKED | Confirmed on server |
|
||||
| PUSH_FAIL | Send failed |
|
||||
| PUSHED | Sent (not checked) |
|
||||
| COMPLETE | Signed (not sent) |
|
||||
| (none of the above) | Not sent |
|
||||
|
||||
### D. Example composite "Stato" chains (from screenshots) and how they form
|
||||
- "New" -> fresh item, only VALID true (label may show "New").
|
||||
- "New.Firmato" -> + COMPLETE (Signed; "Firmato" = old/IT locale).
|
||||
- "New.Firmato.Pushed" -> + PUSHED.
|
||||
- "New.Firmato.Pushed.Checked" -> + CHECKED (also implies PUSHED).
|
||||
- "New.Firmato.Pushed.Checked.Confirmed" -> + CONFIRMED (clears VALID under the hood).
|
||||
- "New.Firmato.Pushed.Check Failed.NOT Pushed" -> CHECK_FAIL then a later clear of PUSHED -> "NOT Pushed".
|
||||
NOTE: the composite string is APPEND-ONLY history of flag changes, so it can contain both a flag and
|
||||
its later "NOT <flag>" negation. The boolean truth is in STATUS[...][1], not in the visible chain.
|
||||
|
||||
### NOTE for #01 fix: the misleading "Heir not found" message is a SEPARATE UI string in
|
||||
dialogs.py task_phase1 (~641-642), NOT a status flag. The states table above documents the engine
|
||||
flags; #01 is about correcting the human message when the real cause is a date anticipation.
|
||||
|
||||
---
|
||||
|
||||
## CHECK-WINDOW MESSAGES LIST (requested by user — outcomes shown in the info window after CHECK)
|
||||
**Source of truth:** bal/gui/qt/dialogs.py `BalBuildWillDialog` — task_phase1 (~542-707), the message
|
||||
helpers (msg_set_checking/building/signing/pushing/invalidating ~1204-1230), and the color/result
|
||||
helpers msg_ok/msg_warning/msg_error (~1234-1245). Colors: OK=green #05ad05, WARNING=orange #cfa808,
|
||||
ERROR=red #ff0000. Built by READING the code (R3 — not invented).
|
||||
|
||||
NOTE: this is DIFFERENT from the "PLUGIN STATES TABLE" above. The states table lists the will-item
|
||||
flags (the "Stato" column). THIS list is the human-readable progress/outcome lines printed in the
|
||||
"Building Will" info window when the user presses CHECK (or runs the wizard). The window has fixed
|
||||
ROW LABELS and, for each, a variable STATUS text.
|
||||
|
||||
### Row labels (left side, fixed) in the CHECK / build window
|
||||
1. "Checking variables"
|
||||
2. "Checking your will"
|
||||
3. "Building your will"
|
||||
4. "Invalidating old will" (only when an invalidation is triggered)
|
||||
5. "Signing your will" (phase 2)
|
||||
6. "Broadcasting your will to executors" (phase 2)
|
||||
|
||||
### ROW 1 — "Checking variables" outcomes
|
||||
| Status text shown | Color | When (code) |
|
||||
|--------------------------------------------------------------------------------|--------|-------------|
|
||||
| Check Alive Threshold Passed: you have to Invalidate your old Will | RED | CheckAliveError + an invalidate tx exists (~559) |
|
||||
| No Heirs | RED | NoHeirsException (~564) |
|
||||
| Ok | GREEN | check_amounts passed (~579) |
|
||||
| In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts | (warning popup) | AmountException (~583) |
|
||||
|
||||
### ROW 2 — "Checking your will" outcomes (this is where "Heir not found" / "New" appear)
|
||||
| Status text shown | Color | When (code) | Triggers rebuild? |
|
||||
|----------------------------------|---------|-------------|:-----------------:|
|
||||
| Ok | GREEN | check_will passed, nothing to change (~593) | no |
|
||||
| Expired | (plain) | WillExpiredException (~596) -> invalidate path | invalidate |
|
||||
| Postponed: invalidating old will | (plain) | WillPostponedException (~609) -> invalidate path | invalidate |
|
||||
| No Heirs | (plain) | NoHeirsException (~614) | no |
|
||||
| Heirs changed: | (plain) | HeirChangeException (~621) | YES |
|
||||
| Will-Executor not present | (plain) | WillExecutorNotPresent (~623) | YES |
|
||||
| Will-Executor changed | (plain) | WillexecutorChangeException (~625) | YES |
|
||||
| Txfees are changed | (plain) | TxFeesChangedException (~627) | YES |
|
||||
| Heir not found | (plain) | HeirNotFoundException (~629) [#01: MISLEADING — often it is just an anticipated date] | YES |
|
||||
| New | (plain) | NotCompleteWillException with no specific subtype (~634) [a brand-new will item, "New heir" case] | YES |
|
||||
|
||||
### ROW 3 — "Building your will" outcomes
|
||||
| Status text shown | Color | When (code) |
|
||||
|----------------------------------------------------------------|---------|-------------|
|
||||
| Ok | GREEN | build_will succeeded (~652) |
|
||||
| Balance is too low, or CheckAlive is in the past.Skipped | RED | build_will returned nothing (~643) |
|
||||
| Will-Executor excluded -> Skipped | RED | WillExecutorNotPresent during build (~656) |
|
||||
| <expired notice from exception> | ORANGE | WillExpiredException after rebuild -> returns "invalidate_classic" (~683) |
|
||||
| <error text> | RED | any other Exception during build (~688) |
|
||||
| <heir>,<amount> is DUST -> Excluded from will <wid> | ORANGE | a heir's amount is DUST (~695-699) |
|
||||
|
||||
### ROW 4 — "Invalidating old will" outcomes (loop_broadcast_invalidating ~729)
|
||||
| Status text shown | Color | When |
|
||||
|------------------------|-------|------|
|
||||
| Broadcasting | plain | start of broadcast |
|
||||
| Ok | GREEN | broadcast succeeded |
|
||||
| <broadcast error msg> | RED | TxBroadcastError / BestEffortRequestFailed |
|
||||
|
||||
### ROW 5 — "Signing your will" (phase 2) — set via msg_set_signing (~1220)
|
||||
Shows progress while signing; final result via msg_ok / msg_error.
|
||||
|
||||
### ROW 6 — "Broadcasting your will to executors" (phase 2, loop_push ~750)
|
||||
| Status text shown | Color | When |
|
||||
|------------------------------------------------|-------|------|
|
||||
| Broadcasting N/M (Xs / Ys) | plain | live progress per selected will-executor |
|
||||
| <url> : Ok | GREEN | that server accepted the tx (-> PUSHED) |
|
||||
| <url> : Ko | RED | that server rejected (-> PUSH_FAIL) |
|
||||
| <url> : Timeout - no answer | RED | server did not answer in time (-> PUSH_FAIL) [related to #02] |
|
||||
| checking <url> - <wid> : Waiting | plain | verifying an "already present" server |
|
||||
| checked <url> - <wid> : <True/False> | GREEN/RED | post-push verification result (-> CHECKED) |
|
||||
|
||||
### LINK TO #01: the "Heir not found" line (ROW 2) is exactly the misleading message. The fix is to
|
||||
distinguish the real cause: if the will item only had its date anticipated (locktime moved earlier),
|
||||
show a correct message instead of "Heir not found". DISCOVER will confirm the precise condition.
|
||||
|
||||
---
|
||||
|
||||
## TASK #01b — Replace misleading "Heir not found" / "New" messages (NOT STARTED — analysis only)
|
||||
**User decision (Opzione 2):** change BOTH the CHECK window (dialogs.py) AND window.py for consistency.
|
||||
**User said:** "ma metti tutto in lista non fare modifiche per ora" (just add to list, do NOT modify yet).
|
||||
|
||||
### GOAL
|
||||
Replace the two MISLEADING outcome texts shown in ROW 2 "Checking your will" with a single clear text:
|
||||
**NEW TEXT (verbatim, FINAL/approved — TWO LINES):**
|
||||
```
|
||||
Found CHANGES to the DATE or the HEIRS,
|
||||
a NEW WILL must be prepared.
|
||||
```
|
||||
In code this is a single string with a newline: `"Found CHANGES to the DATE or the HEIRS,\na NEW WILL must be prepared."`
|
||||
(No trailing quote character; the message ends at "prepared." Note: "CHANGES" is UPPERCASE.)
|
||||
|
||||
### SCOPE — exactly 3 string changes (Opzione 2: A + B for consistency)
|
||||
| # | File:line | Current text | Flow | Action |
|
||||
|---|-----------|-------------------------|----------------------------------------|--------|
|
||||
| 1 | bal/gui/qt/dialogs.py:631 | `_("Heir not found")` | CHECK window / wizard (task_phase1) | -> new text |
|
||||
| 2 | bal/gui/qt/dialogs.py:636 | `"New"` (msg_set_checking) | CHECK window / wizard (task_phase1) | -> new text |
|
||||
| 3 | bal/gui/qt/window.py:594 | `"Heir not found"` | build_inheritance_transaction (popup via show_message) | -> new text |
|
||||
|
||||
### IMPORTANT — what to LEAVE UNCHANGED (do NOT touch)
|
||||
In the SAME NotCompleteWillException block there are 4 OTHER specific messages that stay AS-IS,
|
||||
because they are still correct/useful:
|
||||
- "Heirs changed:" (HeirChangeException)
|
||||
- "Will-Executor not present" (WillExecutorNotPresent) [window.py uses "Will-Executor not present:"]
|
||||
- "Will-Executor changed" (WillexecutorChangeException)
|
||||
- "Txfees are changed" (TxFeesChangedException)
|
||||
ONLY the HeirNotFoundException branch and the no-subtype/"New" fallback are replaced.
|
||||
|
||||
### TECHNICAL NOTES (DISCOVER done)
|
||||
- dialogs.py block: lines 618-636, inside task_phase1, `except NotCompleteWillException as e:`.
|
||||
- line 631: `elif isinstance(e, HeirNotFoundException): message = _("Heir not found")`
|
||||
- line 635-636: `else: self.msg_set_checking("New")` (the no-specific-subtype fallback = "New").
|
||||
- window.py block: lines 578-599, inside build_inheritance_transaction, same exception ladder.
|
||||
- line 594: `elif isinstance(e, HeirNotFoundException): message = "Heir not found"`
|
||||
- NOTE: window.py has NO "New" fallback (if message stays False, it shows nothing) -> only the
|
||||
HeirNotFoundException line is changed here. The other branches stay.
|
||||
- Both blocks then call build/rebuild; behaviour (the rebuild) is UNCHANGED — only the human text changes.
|
||||
- "New" string is also the basis for the composite "Stato" column? NO — that "New" is a separate UI
|
||||
label; this task only touches the CHECK-window message fallback, not the will-item status flags.
|
||||
|
||||
### OPEN QUESTION to confirm at PLAN time (before coding)
|
||||
- dialogs.py:636 currently passes the bare string `"New"` (NOT wrapped in `_()`), so it is not
|
||||
translatable. When replacing, wrap the new text in `_( ... )` for both files for consistency? (default: YES.)
|
||||
|
||||
### METHOD when starting (R4): DISCOVER (done) -> PLAN (wait OK) -> EXECUTE -> ruff + full tests ->
|
||||
### ZIP for user to test -> commit ONLY after explicit confirmation. Add a numbered CHANGELOG entry.
|
||||
|
||||
---
|
||||
|
||||
## TASK BATCH #17 - v0.4.0 USER-TEST FEEDBACK (A-K) - DISCOVER done, PLAN pending OK
|
||||
|
||||
User tested ZIP v0.4.0 and reported 11 points. R4: analyze + propose, DO NOT code yet.
|
||||
|
||||
### UI text / label fixes (low risk)
|
||||
- (A) widgets.py LockTimeWidget.help_text (~607): insert a line "(ONLY IN ADVANCED MODE)<br>"
|
||||
right BEFORE the "if you choose Raw, you can insert various options based on suffix:" line.
|
||||
- (B) plugin.py: red warning QLabel (lbl_warning ~465, shown at top via outer.addWidget ~687).
|
||||
Add a blank vertical space below it, above the grid (User Type row). Use addSpacing.
|
||||
- (C) plugin.py:478 add_widget label "USER TYPE" -> "User Type".
|
||||
- (D) plugin.py:519 add_widget label "Editable dates" -> "Panel editable Date and Fee".
|
||||
Verify it fits before the checkbox (label is column 0 of the grid; should be fine).
|
||||
- (G) plugin.py: move the "No will-executor TX" checkbox (currently row 8, ~576) to between
|
||||
"Editable dates"(row4) and "Number of reminders"(row5). Requires renumbering rows 5..8 (+1).
|
||||
|
||||
### Layout (medium risk)
|
||||
- (H) widgets.py WillSettingsWidget vertical layout (~674-722, the WIZARD).
|
||||
Requirements: calendar/date box narrower; left-aligned & tidy; fee box width fit to ~5 chars;
|
||||
ADVANCED mode must stay aligned when the check-alive (threshold) row also appears.
|
||||
|
||||
### Wait-time (low risk)
|
||||
- (J) Reduce 30s waits to 20s. Locations found:
|
||||
willexecutors.py:46 PUSH_GLOBAL_DEADLINE = 30 -> 20
|
||||
willexecutors.py:56 CHECK_GLOBAL_DEADLINE = 30 -> 20
|
||||
(window.py:1226 ping_deadline derives from PUSH_GLOBAL_DEADLINE; window.py:1166
|
||||
download_deadline=45 is a separate "45s" value - user said only the 30s ones; leave 45 unless asked.)
|
||||
Also comments at willexecutors.py:35,50,294 mention "30s sleeps" (text only, no behaviour).
|
||||
|
||||
### BUGS (high risk - need careful logic work)
|
||||
- (E) "eredus" shown in RED on "Building your will:" line.
|
||||
ROOT CAUSE: dialogs.py:734-735 `except Exception as e: self.msg_set_building(self.msg_error(e))`.
|
||||
build_will() (window.py:309) raises an exception whose str(e) is the heir name ("eredus"),
|
||||
shown as red error. Likely HeirNotFoundException or similar raised inside get_transactions.
|
||||
QUESTION FOR USER ALREADY ASKED: with 10 heirs does it list all 10 here? -> Answer: the
|
||||
"Building your will:" line shows ONE status only; the per-heir lines that CAN appear are the
|
||||
DUST-exclusion warnings (dialogs.py:739-748), one line per heir that is DUST. So normally NOT
|
||||
all 10 are listed; only DUST heirs get an extra line. Need to confirm which exception carries
|
||||
the heir name to decide the fix (turn into green Ok, or a clear message instead of raw name).
|
||||
- (F) After DELETING one of two heirs: window says "Found CHANGES... a NEW WILL must be prepared"
|
||||
(good) but then "Signing: Nothing to do" / "Broadcasting: Nothing to do", and on exit the WILL
|
||||
tab gets the inheritance tx list but they are NOT signed. Pressing CHECK re-opens same window,
|
||||
still "Nothing to do".
|
||||
RELEVANT CODE: dialogs.py have_to_sign loop (750-754): have_to_sign=True only if some valid
|
||||
willitem is NOT COMPLETE. on_success_phase1 (1077): else branch -> msg_set_signing("Nothing to
|
||||
do") when have_to_sign is False. task_phase2 only signs if have_to_sign.
|
||||
SUSPECT: will.py update_will (366-375): when a txid is unchanged between old/new will, it REUSES
|
||||
the OLD willitem object (which is COMPLETE), so freshly-rebuilt items inherit COMPLETE status
|
||||
and have_to_sign stays False. After deleting an heir the remaining tx may keep same txid ->
|
||||
reused as COMPLETE -> "Nothing to do". NEEDS deeper confirm; possibly the rebuilt tx should be
|
||||
re-flagged "New"/not-COMPLETE when heirs set changed. HIGH RISK - confirm with logs before edit.
|
||||
- (K) BASIC, add heir from WIZARD keeping SAME date: it correctly rebuilds + signs + creates new tx
|
||||
in WILL list, but does NOT auto-broadcast. User asks if it should.
|
||||
RELEVANT CODE: task_phase2 (1142-1153): have_to_push True only if willitem has `.we` (a
|
||||
will-executor) AND COMPLETE AND not PUSHED. If no will-executor is attached (e.g. "No
|
||||
will-executor TX" mode or none selected), have_to_push stays False -> "Nothing to do" (correct,
|
||||
nothing to broadcast). Need to confirm user's wallet had a will-executor selected. If yes, the
|
||||
push path should fire; if it didn't, investigate selection state after wizard add-heir.
|
||||
DESIGN QUESTION FOR USER: in BASIC, when AUTO_SIGN is on and a will-executor is selected,
|
||||
should broadcast always be automatic? (current logic only pushes when a will-executor exists.)
|
||||
|
||||
### Logic verification (no code, just run)
|
||||
- (I) Run full test suite to confirm BASIC mode (only DELIVERY TIME, no check-alive) works.
|
||||
|
||||
### PROPOSED GROUPING (for credit efficiency, one ZIP cycle)
|
||||
- Group A = pure text/label/spacing/move: A, B, C, D, G (very low risk)
|
||||
- Group B = layout H (calendar narrower, left-align, fee ~5 chars, ADVANCED alignment)
|
||||
- Group C = wait-time J (30->20 in willexecutors.py x2)
|
||||
- Group D = bug E (red heir name -> green/clear message)
|
||||
- Group E = bugs F and K (sign/broadcast logic) - HIGHEST RISK, may need user logs
|
||||
- Then: (I) run full tests, ruff, build ONE ZIP v0.4.1, user tests, commit only after OK.
|
||||
|
||||
---
|
||||
|
||||
## TASK BATCH #17 - UPDATE after LOG analysis (log.txt 183 lines, 2026-06-24)
|
||||
|
||||
### ROOT CAUSE CONFIRMED for E + F (they are the SAME bug)
|
||||
Log sequence on CHECK after heir change:
|
||||
1. check_willexecutors_and_heirs -> "heir: erede002new not found" -> HeirNotFoundException
|
||||
-> "not complete erede002new true" -> message "Found CHANGES..." -> have_to_build=True. OK
|
||||
2. build_will() -> "txs built: {3 tx}" (new tx created). OK
|
||||
3. dialogs.py:697 self.bal_window.check_will() -> is_will_valid -> check_willexecutors_and_heirs
|
||||
AGAIN -> "heir: erede001 not found" -> raises HeirNotFoundException("erede001").
|
||||
4. That exception is caught by the GENERIC `except Exception as e:` (dialogs.py:734) ->
|
||||
msg_set_building(self.msg_error(e)) => shows the HEIR NAME in RED == BUG E
|
||||
then `return False, None` => have_to_sign=False => "Nothing to do" / no sign / no push == BUG F
|
||||
Log proof: lines 96-98 "check willexecutors heirs / heir: erede001 not found / have to sign False".
|
||||
|
||||
WHY does check_will() still see the OLD heir after rebuild?
|
||||
-> will.py update_will (366-375): for txid unchanged between old/new will it REUSES the OLD
|
||||
WillItem object (old heirs/old we/COMPLETE). So after build_will the willitems still carry
|
||||
stale heir entries -> check_willexecutors_and_heirs raises HeirNotFoundException again.
|
||||
-> Confirmed by user: deleting/adding an heir must FULLY rebuild (values recomputed: single heir
|
||||
auto-scaled to 100%, whole wallet always emptied). So reusing old items is wrong here.
|
||||
|
||||
USER WORKAROUND that worked: deleting ALL tx in WILL list, then CHECK -> correctly rebuilds & asks
|
||||
to sign. This confirms: the stale reused items are the problem; with an empty will there is nothing
|
||||
stale to reuse.
|
||||
|
||||
### FIX DIRECTION (E+F) - to confirm at PLAN:
|
||||
- The post-build verification at dialogs.py:697 should NOT re-raise HeirNotFound for the freshly
|
||||
rebuilt will. Options:
|
||||
(a) After build_will(), the new willitems must reflect the NEW heirs (not reuse old COMPLETE
|
||||
items whose heirs no longer match). i.e. update_will should NOT copy old heirs onto a tx
|
||||
whose heir SET changed; only reuse when heirs are identical.
|
||||
(b) Or: in dialogs.py wrap/handle the second check_will() so a HeirNotFound on the just-rebuilt
|
||||
will is treated as "needs signing" (have_to_sign=True) instead of red error + return False.
|
||||
- Preferred: (a) at the source (update_will) so status is correct (new tx => not COMPLETE =>
|
||||
have_to_sign True => sign => push). HIGH RISK: update_will is shared by many paths; must run
|
||||
full test suite. Need to be 100% sure before editing (R3).
|
||||
|
||||
### E - user clarified: show ALL heirs in GREEN.
|
||||
Today the "Building your will:" line shows ONE status. To list all heirs in green we add, after a
|
||||
successful build, one green line per heir (name) - source: willitems[wid].heirs keys (skip the
|
||||
internal 'w!ll3x3c"' executor pseudo-heirs). Place where msg_set_building(msg_ok()) succeeds
|
||||
(dialogs.py:701). Need a per-heir green row helper (msg_set_status with COLOR_OK).
|
||||
|
||||
### K - user clarified:
|
||||
- "No will-executor TX" = the CELESTE/backup tx (the one NOT needing a will-executor). Checkbox
|
||||
ON => also create that backup tx; OFF => only create tx that go to will-executors.
|
||||
- RENAME setting label to "Add transaction without willexecutor"; if it doesn't fit before the
|
||||
checkbox, use "Add TX without willexecutor". (plugin.py row, currently "No will-executor TX".)
|
||||
- K bug: after the E+F fix the rebuilt tx will be not-COMPLETE => sign => push. User states 2
|
||||
will-executors were selected and tx already have we associated, so once have_to_sign becomes
|
||||
True the existing task_phase2 push path (have_to_push when w.we & COMPLETE & not PUSHED) should
|
||||
fire automatically. So K is very likely RESOLVED by the same E+F fix. CONFIRM after fix with a
|
||||
test that simulates add-heir + selected will-executors -> loop_push called.
|
||||
- User decision: "when coming from the WIZARD, broadcast must ALWAYS be automatic for selected
|
||||
servers". Current logic already auto-pushes when have_to_push; the missing piece was have_to_sign
|
||||
being wrongly False. Keep auto-push as is.
|
||||
|
||||
### J - user UPDATE: also reduce download_deadline (window.py:1166, =45) to 20, and unify ALL into a
|
||||
SINGLE variable if possible. PLAN: define one constant (e.g. willexecutors.NETWORK_DEADLINE = 20)
|
||||
and use it for PUSH_GLOBAL_DEADLINE, CHECK_GLOBAL_DEADLINE, ping_deadline, download_deadline.
|
||||
|
||||
### REVISED GROUPING (one ZIP v0.4.1):
|
||||
G1 text/labels/spacing/move: A, B, C, D(+ rename K-label), G (low risk)
|
||||
G2 layout H
|
||||
G3 wait-time J (single shared deadline constant = 20)
|
||||
G4 BUG E+F+K core fix (update_will heir-set reuse OR post-build handling) + show all heirs green
|
||||
Then I (full tests) + ruff -> ZIP v0.4.1 -> user test -> commit after OK.
|
||||
|
||||
---
|
||||
|
||||
## TASK BATCH #17 - DONE (v0.4.1, delivered as test ZIP, NOT committed)
|
||||
Implemented G1(A,B,C,D,G + rename K-label) + G2(H layout) + G3(J unified NETWORK_DEADLINE=20)
|
||||
+ G4(E+F+K core fix via Will._same_heirs Option A + all heirs green).
|
||||
- New helper Will._same_heirs (will.py ~352); update_will reuses old item only if heirs identical.
|
||||
- dialogs.py build-success: lists every heir in green (COLOR_OK), skips w!ll3x3c" pseudo-heirs.
|
||||
- widgets.py: help_text "(ONLY IN ADVANCED MODE)"; compact left-aligned wizard date/fee layout
|
||||
(date rows fixed 16-char + icon, fee field 5-char, calendar left-aligned with stretch).
|
||||
- plugin.py: "User Type", "Panel editable Date and Fee", checkbox moved to row5 & renamed
|
||||
"Add transaction without willexecutor", addSpacing(12) under red warning; rows renumbered.
|
||||
- willexecutors.py: NETWORK_DEADLINE=20; PUSH/CHECK_GLOBAL_DEADLINE derive from it; class attr added.
|
||||
- window.py: download_deadline = Willexecutors.NETWORK_DEADLINE.
|
||||
- Version 0.4.0->0.4.1 (4 files). CHANGELOG entry #17.
|
||||
- New tests tests/test_group_f_heir_change_rebuild.py (9). Full suite: 248 passed. ruff: no new errors.
|
||||
- ZIP: bal-electrum-plugin-v0.4.1.zip, 37 files, 263683 bytes,
|
||||
sha256 914954306e8e88c1ecad7be1d459cb9617a6b36fa9bf91fe2d1d8912a22621b2,
|
||||
url https://www.genspark.ai/api/files/s/GLFdDxWj. NOT committed (zip-first; awaiting user OK).
|
||||
|
||||
---
|
||||
|
||||
## TASK BATCH #18 - v0.4.2 (real E/F/K fix + layout H + tooltip)
|
||||
|
||||
CRITICAL CORRECTION: v0.4.1's `_same_heirs`/`update_will` fix did NOT fix E/F/K.
|
||||
Confirmed via electrum_log_20260624T152516Z_24316.log: rebuilt txids
|
||||
(117c9f/05753d/fbb49f) are ALL NEW vs old (129ef8), so update_will's "reuse old
|
||||
item if txid matches" branch never runs.
|
||||
|
||||
REAL ROOT CAUSE: in dialogs.py task_phase1, after build_will() the second
|
||||
check_will() raises HeirNotFoundException (subclass of NotCompleteWillException),
|
||||
caught by generic `except Exception` -> heir name RED + return False,None ->
|
||||
"Nothing to do".
|
||||
|
||||
REAL FIX (v0.4.2): added `except NotCompleteWillException as e:` BEFORE generic
|
||||
Exception (and after WillExecutorNotPresent/WillExpiredException). It calls
|
||||
_build_success_report() (green Ok + heir list) and falls through to have_to_sign
|
||||
loop -> sign -> auto-push. Extracted _build_success_report() helper used on both
|
||||
clean and rebuilt paths.
|
||||
|
||||
Layout H (widgets.py): kept composites INTACT (they hold 2 editors + Raw/Date
|
||||
combo in ADVANCED), forced all leading icons to same fixed width + left-align
|
||||
stacking so icons align and fields start at same x; widened fee field to 8 chars.
|
||||
|
||||
Tooltip: "Delivery Time, click for more information".
|
||||
|
||||
STATUS: 248 tests pass, ruff clean (no new errors), version bumped to 0.4.2,
|
||||
CHANGELOG entry #18 added (and #17 outcome corrected to PARTIAL). ZIP v0.4.2
|
||||
built (sha256 8e40d6c3...). Delivered for testing. NO COMMIT until user confirms.
|
||||
|
||||
---
|
||||
|
||||
## TASK BATCH #19 - v0.4.3 (date-sync after auto-anticipate + BASIC calendar + sign reason)
|
||||
|
||||
BUG (owner, after v0.4.2 test): after a rebuild the engine auto-anticipates tx
|
||||
locktime by 1 day (Will.check_anticipate), but WILL_SETTINGS["locktime"] stayed
|
||||
at the original date -> next Check compared stored(orig) vs tx(orig-1d) and
|
||||
mistook it for a POSTPONE -> wrongly asked to invalidate.
|
||||
|
||||
FIX 1 (dialogs.py): _sync_locktime_to_built_txs() in _build_success_report sets
|
||||
stored locktime = Will.get_min_locktime(willitems) (min of valid built txs),
|
||||
ONLY if min < current (anticipate only, never overwrite a postpone). Routed via
|
||||
BalWindow.update_setting_widgets(update_all=True) so it persists AND refreshes
|
||||
the date widgets in all panels/wizard; the .ics calendar reads the same alarm so
|
||||
it gets the anticipated date too. Owner confirmed: min is fine when multiple txs.
|
||||
|
||||
FIX 2 (dialogs.py): _date_was_anticipated flag -> before the sign prompt show an
|
||||
orange note explaining WHY (date moved 1 day earlier so the new will replaces the
|
||||
old one; please sign+broadcast).
|
||||
|
||||
FIX 3 (widgets.py): BASIC calendar. check-alive is hidden in BASIC so spreading
|
||||
reminders over it is wrong. New pure helper basic_reminder_offsets(days) +
|
||||
BASIC_REMINDER_OFFSETS=(30,10,1): fixed reminders 30/10/1 days before delivery,
|
||||
dropping any in the past. ADVANCED unchanged. The .ics now uses the (anticipated)
|
||||
locktime alarm. New tests test_group_g_basic_calendar.py (7).
|
||||
|
||||
STATUS: 255 tests pass, ruff clean (no new errors), version -> 0.4.3, CHANGELOG
|
||||
#19 added. ZIP v0.4.3 to be built and delivered. NO COMMIT until user confirms.
|
||||
|
||||
---
|
||||
|
||||
## TASK BATCH #20 - v0.4.4 (UI wording + check-alive visibility + backup-tx default OFF)
|
||||
|
||||
P1 (plugin.py:556): reminders help text -> BASIC (30/10/1 days before) + ADVANCED
|
||||
(spread, range 1-5 default 3).
|
||||
P2 (widgets.py wizard else-branch only): QLabel above date "Enter the date on
|
||||
which you want the inheritance (or backup)..."; QLabel below fee "Please note: Do
|
||||
not reduce the miner fees unless you know what you're doing". Wizard only.
|
||||
P3 (lists.py:495, PreviewList=WILL tab): refresh tooltip "Check" -> "Check
|
||||
Inheritance".
|
||||
P4 BUG FIX: check-alive (threshold) visibility was set only in
|
||||
WillSettingsWidget.__init__; WILL/HEIR toolbars persist across USER TYPE change so
|
||||
ADVANCED didn't re-show it (only wizard, which is recreated). Added
|
||||
apply_user_type_visibility() (shows threshold when not basic) called from
|
||||
window.update_all() for both heir_list_widget & will_list_widget.
|
||||
P5 (plugin_base.py:193): NO_WILLEXECUTOR default True -> False. Confirmed Option A
|
||||
(stored in Electrum GLOBAL config via config.set_key, NOT per-wallet). Default
|
||||
applies only to new installs; saved choice is respected. Reset uses cfg.default so
|
||||
it's OFF too.
|
||||
|
||||
STATUS: 255 tests pass, ruff clean (no new errors), version -> 0.4.4, CHANGELOG
|
||||
#20 added. ZIP v0.4.4 to be built/delivered. NO COMMIT until user confirms.
|
||||
|
||||
## TASK BATCH #21 — v0.4.5 (invalidate loop + wizard truncation)
|
||||
- Issue 1: wizard QLabels truncated -> added setMinimumWidth(30*char_width_in_lineedit()) to date_hint + fee_note (widgets.py wizard else-branch ~771-810).
|
||||
- Issue 2a (loop): invalidate_task wait(5)->wait(10) + set self._invalidation_broadcast=True after broadcast; on_success_phase1 (have_to_sign is None) now STOPS with clear message + _add_close_button when _invalidation_broadcast already set (no re-prompt loop). Flag initialised in __init__ ~line 546.
|
||||
- Issue 2b (label): loop_broadcast_invalidating (dialogs.py:903) now sets wallet.set_label(txid, "BAL Invalidate transaction") on successful broadcast.
|
||||
- Version bumped 0.4.4 -> 0.4.5 (plugin_base.py, __init__.py, VERSION, manifest.json).
|
||||
- Verified: py_compile OK; 255 passed; ruff no new errors; headless label check -> 3 & 2 lines (not truncated).
|
||||
- ZIP v0.4.5 delivered for testing. NO COMMIT until user confirms.
|
||||
|
||||
## TASK BATCH #22 — v0.4.6 (6 fixes from allegato 13-18)
|
||||
- #1 DUST (dialogs.py task_phase1 ~767): dust_heirs dict de-dup -> one row PER HEIR, no willexecutor ref. Was N_exec x N_heirs rows.
|
||||
- #2 Heirs (dialogs.py _build_success_report ~815): single green/bold line "Heirs: a, b, c" via shown_heirs list + one msg_set_status(_("Heirs"),...COLOR_OK).
|
||||
- #3 Scroll (dialogs.py __init__ ~524 + msg_update ~1565): message_label wrapped in QScrollArea (setWidgetResizable, setMaximumHeight 400), wordWrap on label, auto-scroll to bottom (verticalScrollBar.setValue(max)). Close button stays in self.vbox below scroll = always visible.
|
||||
- #4 Wizard final check (dialogs.py on_next_we ~127, case B): added same block as lists.py check() -> loop Will.needs_server_check + self.bal_window.check_transactions(will) BEFORE self.close(). Root cause: wizard only called build_will_task(), never check_transactions().
|
||||
- #5 Wizard truncation (widgets.py ~771): REMOVED alignment=AlignLeft flag from date_hint+fee_note (alignment flag blocks horizontal stretch -> wordwrap on narrow sizeHint -> truncated). Added setSizePolicy(Expanding,Minimum) + self.setMinimumWidth(44*cw). Headless verified at 780px dialog -> labels expand to 740px, full text fits.
|
||||
- #6 Notice (dialogs.py on_success_phase1 ~1336): yellow msg_warning -> "<b>...</b>" black bold + "\n" split after "previous one." (msg_update converts \n->br).
|
||||
- Version 0.4.5 -> 0.4.6. CHANGELOG #22. 255 tests pass, ruff no new errors.
|
||||
- ZIP v0.4.6 delivered for testing. NO COMMIT until user confirms.
|
||||
|
||||
## TASK BATCH #23 — v0.4.7 (4 fixes from testing v0.4.6)
|
||||
- #1 (allegato1) Scroll height (dialogs.py __init__ ~562-563): scroll_area.setMinimumHeight(500) + setMaximumHeight(700). Was ~140px too short.
|
||||
- #2 Heirs revert (dialogs.py _build_success_report ~905): REVERTED v0.4.6 "one line" -> ONE heir per line again, green/bold, de-dup via shown_heirs set, skip w!ll3x3c" pseudo-heirs. Reason: long heir names + report now scrolls.
|
||||
- #3 (allegato2) Wizard line breaks (widgets.py): explicit \n after "(or backup)" in date_hint (~801) and after "miner fees" in fee_note (~831). setWordWrap honours \n.
|
||||
- #4 ALL-DUST guard. KEY DECISION: guard placed at END of prepare_lists (heirs.py ~577-591), NOT in prepare_transactions. WHY: prepare_transactions only sees the single lowest locktime -> false positive if a later locktime has valid heirs. prepare_lists sees ALL heirs/locktimes with final dust marking (fixed AND percentage). Counts real_heirs (excl. w!ll3x3c") + valid_real_heirs (no "DUST" in HEIR_REAL_AMOUNT); raise HeirAmountIsDustException if real_heirs>0 and valid_real_heirs==0.
|
||||
- Propagation: HeirAmountIsDustException is NOT WillExecutorFeeException -> skips that except in buildTransactions (heirs.py:646) -> reaches GUI clean, no misleading "error preparing transactions" log. (prepare_transactions guard was REMOVED/reverted to original.)
|
||||
- GUI: dialogs.py task_phase1 ~803 added `except HeirAmountIsDustException` BEFORE generic `except Exception`: red msg_error "All heirs' shares are below the dust limit: the inheritance cannot be created. Increase the amounts or reduce the number of heirs." + return False,None (no sign/check, no empty will in list).
|
||||
- Import: common.py ~65 now imports HeirAmountIsDustException from ...core.heirs (re-exported via `from .common import *`; no __all__).
|
||||
- CRITICAL LEARNING: with FIXED amounts + LARGE balance, leftover funds are REDISTRIBUTED (normalize_perc real=True) so small fixed amounts get a VALID HEIR_REAL_AMOUNT (NOT dust). Real all-dust case = SMALL balance + PERCENTAGE heirs (matches user log 214/316/3 sat). heir list shape when redistributed: [addr, amt, lt, REAL_VALID, "DUST: x", dust_raw].
|
||||
- 3 new tests (tests/test_core_heirs_extra.py): test_prepare_lists_all_dust_raises (800 sat, 40%/60% -> raises), test_prepare_lists_mixed_dust_continues (5000 fixed + 1% -> no raise), test_prepare_lists_multi_locktime_continues (1% @30d + 5000 @60d -> no raise).
|
||||
- Version 0.4.6 -> 0.4.7. CHANGELOG #23. 258 tests pass (255+3), ruff no new errors.
|
||||
- ZIP v0.4.7 delivered for testing. NO COMMIT until user confirms.
|
||||
571
CHANGELOG.md
571
CHANGELOG.md
@@ -853,3 +853,574 @@ commit).
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.3.9, confirmed OK by the user before
|
||||
commit).
|
||||
|
||||
---
|
||||
|
||||
## 16. v0.4.0 - Unified invalidate, clearer CHECK message, will-executor auto-select, SIMPLE/ADVANCED mode
|
||||
|
||||
**Date:** 2026-06-24
|
||||
|
||||
This release bundles five related improvements (Groups 1-3 of the user's
|
||||
to-do list). Version bumped 0.3.9 -> 0.4.0.
|
||||
|
||||
### Group 1 - Invalidate / messages
|
||||
|
||||
**#01b - Clearer "Checking your will" message.**
|
||||
The two misleading outcomes "Heir not found" and "New" were both replaced with a
|
||||
single, accurate two-line message, because in practice that branch is most often
|
||||
reached when the delivery date was anticipated, not when an heir is missing:
|
||||
|
||||
Found CHANGES to the DATE or the HEIRS,
|
||||
a NEW WILL must be prepared.
|
||||
|
||||
- `bal/gui/qt/dialogs.py`: `task_phase1` - HeirNotFoundException branch and the
|
||||
no-subtype fallback (previously "New").
|
||||
- `bal/gui/qt/window.py`: `build_inheritance_transaction` - HeirNotFoundException
|
||||
branch (kept consistent with the CHECK window).
|
||||
- The other specific messages (Heirs changed / Will-Executor not present /
|
||||
Will-Executor changed / Txfees changed) are unchanged.
|
||||
|
||||
**UNIFY invalidate procedure + #03 - Same behaviour for CHECK and WIZARD, and the
|
||||
history label is now always written.**
|
||||
When a will is expired (CHECK on an expired will, or an heir added to an expired
|
||||
will), the plugin now ALWAYS:
|
||||
1. shows a warning popup (no more "use Tools -> Invalidate" wording);
|
||||
2. automatically opens Electrum's classic transaction window via
|
||||
`BalWalletWindow.invalidate_will()` so the user signs and broadcasts the
|
||||
invalidation - this path already sets the "BAL Invalidate transaction" history
|
||||
label, which fixes #03 (the label was missing when invalidating from the
|
||||
automatically-opened window).
|
||||
- `bal/gui/qt/dialogs.py`: the first `WillExpiredException` handler in
|
||||
`task_phase1` now returns the `"invalidate_classic"` signal (instead of routing
|
||||
to the label-less automatic path); `on_success_phase1` shows the approved popup
|
||||
text and opens the classic window with `QTimer.singleShot(0, ...)` AFTER closing
|
||||
the wizard, so the transaction window stays in front.
|
||||
|
||||
### Group 2 - Will-executor auto-select (#02)
|
||||
|
||||
In the wizard's "Automatically download and select willexecutors" flow, the green
|
||||
SELECTED tick now follows the green ping dot: only servers that actually answered
|
||||
the ping (status == 200) are selected, and every non-responding server is
|
||||
explicitly DESELECTED. This discards dead/slow servers at the source (no more
|
||||
getting stuck broadcasting to them) and is re-evaluated on every download, so a
|
||||
server that failed before but now answers is selected again.
|
||||
- `bal/gui/qt/dialogs.py`: `ping_on_done` in `BalWizardWEDownloadWidget._on_next`
|
||||
(robust `str(status) == "200"` comparison + safe `.get`).
|
||||
|
||||
### Group 3 - SIMPLE / ADVANCED mode (global)
|
||||
|
||||
A new global "USER TYPE" setting lets the user pick a simpler interface.
|
||||
- New global config `USER_TYPE` (default `"basic"`) and helper
|
||||
`BalPlugin.is_basic_mode()` in `bal/core/plugin_base.py`. Stored in Electrum's
|
||||
global config, so it does not affect existing wallet files; an old wallet opens
|
||||
with the global value and its owner can switch to ADVANCED at will.
|
||||
- `bal/gui/qt/plugin.py`: new "USER TYPE" row in the settings dialog - a two-choice
|
||||
combo [BASIC, ADVANCED] with a HelpButton, added at the top (row 0) and to the
|
||||
"Reset setting" list (reset -> BASIC).
|
||||
- In BASIC mode:
|
||||
- the Raw/Date selector is hidden and every date field is forced to the calendar
|
||||
("Date") editor, so the user never sees or uses RAW
|
||||
(`bal/gui/qt/widgets.py`, `BalTimeEditWidget`);
|
||||
- the whole "Check Alive" (threshold) row and its icon are hidden, while the
|
||||
"Delivery time" (locktime) row stays visible
|
||||
(`bal/gui/qt/widgets.py`, `WillSettingsWidget`);
|
||||
- the "check alive" postpone behaviour is disabled: `init_class_variables`
|
||||
skips raising `CheckAliveError`, so a passed check-alive date never forces a
|
||||
postpone/rewrite (`bal/gui/qt/window.py`). The delivery time is unaffected.
|
||||
|
||||
### Verification
|
||||
- `ruff`: no new errors in the changed lines (only the usual pre-existing
|
||||
F401/F403/F405 star-import noise and one pre-existing F841).
|
||||
- Full test suite: `239 passed`.
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.4.0; commit only after the user
|
||||
confirms the ZIP works).
|
||||
|
||||
---
|
||||
|
||||
## 17. v0.4.1 - Heir-change rebuild fix, all heirs in green, UI tidy-up, unified 20s deadline
|
||||
|
||||
**Date:** 2026-06-24
|
||||
|
||||
**Goal:** Address the user's v0.4.0 test feedback (points A-K), with the most
|
||||
important items being two functional bugs surfaced via a log:
|
||||
|
||||
- **(E + F + K) Heir-change full rebuild (the core fix):** After deleting or
|
||||
changing an heir, pressing CHECK said "Found CHANGES... a NEW WILL must be
|
||||
prepared" but then "Signing: Nothing to do" / "Broadcasting: Nothing to do",
|
||||
and the rebuilt transactions were never signed or broadcast. The log showed
|
||||
that, right after `build_will()`, a second coherence check raised
|
||||
`HeirNotFoundException(<heir name>)`, which was (1) shown in RED with the heir
|
||||
name (bug E) and (2) made the dialog return `have_to_sign=False` (bugs F/K).
|
||||
Root cause: `Will.update_will` reused the OLD (already signed/COMPLETE)
|
||||
`WillItem` whenever a rebuilt transaction kept the same txid, copying only the
|
||||
new heirs onto it - so a stale heir set survived and the item stayed COMPLETE.
|
||||
Fix (user-approved "Option A"): a new helper `Will._same_heirs` compares the
|
||||
real heirs (address/amount/locktime, ignoring the reserved `w!ll3x3c"`
|
||||
will-executor pseudo-heirs); the old item is reused ONLY when the heirs are
|
||||
identical, otherwise the freshly built (unsigned) item is kept so it is
|
||||
correctly detected as needing signing and broadcasting. This also fixes K:
|
||||
once the rebuilt tx is not COMPLETE, the existing auto-sign + auto-push path
|
||||
(have_to_push when a will-executor is attached) fires automatically.
|
||||
- **(E) Show all heirs in GREEN:** On a successful build, every heir is now
|
||||
listed on its own line in green ("Ok" colour) in the Building Will window,
|
||||
instead of a heir name ever appearing in red as exception text.
|
||||
- **(A)** Delivery-time help popup: added a "(ONLY IN ADVANCED MODE)" line
|
||||
before the Raw-suffix explanation.
|
||||
- **(B)** Plugin settings: added a blank gap below the red warning, above the
|
||||
first setting row.
|
||||
- **(C)** Renamed the "USER TYPE" settings label to "User Type".
|
||||
- **(D)** Renamed "Editable dates" to "Panel editable Date and Fee".
|
||||
- **(G + K-label)** Moved the will-executor backup-tx checkbox up (now right
|
||||
below "Panel editable Date and Fee", above "Number of reminders") and renamed
|
||||
it from "No will-executor TX" to "Add transaction without willexecutor".
|
||||
- **(H)** Wizard date-field layout: date rows are now a compact fixed width
|
||||
(no longer the inflated 40-char minimum), the calendar button is left-aligned
|
||||
and no longer stretches, and the mining-fee field is sized to ~5 characters.
|
||||
ADVANCED mode (with the extra Check-Alive row) stays aligned.
|
||||
- **(J)** Network waits unified into a single shared
|
||||
`Willexecutors.NETWORK_DEADLINE = 20` constant (was 30s push/check and 45s
|
||||
download); push/check/ping/download all derive from it.
|
||||
|
||||
**Files changed:**
|
||||
- `bal/core/will.py` - new `_same_heirs` helper; `update_will` reuses old item
|
||||
only when heirs are identical (Option A).
|
||||
- `bal/gui/qt/dialogs.py` - list all heirs in green on successful build.
|
||||
- `bal/gui/qt/widgets.py` - help-text line (A); compact, left-aligned wizard
|
||||
date/fee layout (H).
|
||||
- `bal/gui/qt/plugin.py` - settings labels/spacing/row order (B, C, D, G, K-label).
|
||||
- `bal/core/willexecutors.py` - single `NETWORK_DEADLINE = 20` (J).
|
||||
- `bal/gui/qt/window.py` - download deadline derives from `NETWORK_DEADLINE` (J).
|
||||
- `tests/test_group_f_heir_change_rebuild.py` - new unit tests for `_same_heirs`.
|
||||
- Version bumped 0.4.0 -> 0.4.1 (`plugin_base.py`, `__init__.py`, `VERSION`,
|
||||
`manifest.json`).
|
||||
|
||||
**Verification:**
|
||||
- `ruff`: no new errors (only the pre-existing F401/F403/F405 star-import noise
|
||||
and two pre-existing F841 at `will.py:151` and `dialogs.py:645`).
|
||||
- Full test suite: `248 passed` (239 existing + 9 new Group F tests).
|
||||
|
||||
**Outcome:** PARTIAL (delivered as test ZIP v0.4.1). User testing showed that
|
||||
the CORE bug (E/F/K) was STILL present: the `_same_heirs`/`update_will` fix was
|
||||
misdirected - on a real heir change the rebuilt transactions get COMPLETELY NEW
|
||||
txids, so `update_will`'s "reuse the old item when the txid matches" branch never
|
||||
runs. The real cause was found and fixed in entry 18. The `_same_heirs` helper is
|
||||
kept as a correct safety improvement. Items A, B, C, D, G, J, I were confirmed OK.
|
||||
|
||||
---
|
||||
|
||||
## 18. v0.4.2 - Real fix for the heir-change rebuild bug (E/F/K), wizard layout (H), tooltip
|
||||
|
||||
**Date:** 2026-06-24
|
||||
|
||||
**Goal:** Fix the bugs the user found while testing v0.4.1: after adding or
|
||||
deleting an heir (or building from the wizard), the heir name was shown in RED
|
||||
and the will was reported as "Nothing to do" (never signed/broadcast); the
|
||||
green heir list disappeared; the wizard icons/fields were still misaligned and
|
||||
the fee box was too small; and a tooltip needed rewording.
|
||||
|
||||
**Root cause of E/F/K (confirmed from the user's Electrum log):** In
|
||||
`task_phase1` (dialogs.py), after `build_will()` rebuilds the whole will, a
|
||||
SECOND `check_will()` re-validates it. When the heirs (or date) changed, this
|
||||
re-validation legitimately raises a `NotCompleteWillException` subclass
|
||||
(`HeirNotFoundException`) - the freshly rebuilt, still-unsigned transactions do
|
||||
not yet "cover" the new heir set. That exception was caught by the GENERIC
|
||||
`except Exception`, which printed the heir name in RED and returned
|
||||
`have_to_sign=False`, so the rebuilt will was never signed ("Nothing to do").
|
||||
|
||||
**What changed:**
|
||||
|
||||
- **(E/F/K)** `bal/gui/qt/dialogs.py`: added a dedicated
|
||||
`except NotCompleteWillException` handler BEFORE the generic `except Exception`
|
||||
(and after the existing `WillExecutorNotPresent` / `WillExpiredException`
|
||||
handlers, whose order matters). It treats the post-build re-validation failure
|
||||
as what it really is - "the will was rebuilt and now needs signing" - instead
|
||||
of an error. It shows the green "Ok" result and the full heir list, then falls
|
||||
through to the existing `have_to_sign` detection, so the new (status "New")
|
||||
transactions are correctly signed and (with a will-executor attached)
|
||||
auto-broadcast. The heir-green-listing was extracted into a new
|
||||
`_build_success_report()` helper, used on BOTH the clean and the rebuilt
|
||||
paths, so the green heir list is always shown and the heir name never appears
|
||||
in red again.
|
||||
- **(H)** `bal/gui/qt/widgets.py`: reworked the wizard's vertical layout. Every
|
||||
leading icon (delivery time, check-alive, calendar, fee) is forced to the same
|
||||
fixed width and the composites are stacked left-aligned, so the icons line up
|
||||
one under the other and every field starts at the same x just to their right.
|
||||
The fee field is WIDENED (~8 chars) so the spin-box arrows no longer cover the
|
||||
digits. The composites are kept INTACT (not split apart) because the
|
||||
delivery-time / check-alive widgets hold two editors plus a runtime Raw/Date
|
||||
selector in ADVANCED mode; splitting them would break that mode.
|
||||
- **Tooltip** `bal/gui/qt/widgets.py`: the delivery-time icon tooltip now reads
|
||||
exactly "Delivery Time, click for more information".
|
||||
|
||||
**Files changed:**
|
||||
- `bal/gui/qt/dialogs.py` - `NotCompleteWillException` handler + `_build_success_report`.
|
||||
- `bal/gui/qt/widgets.py` - wizard layout (H) and tooltip wording.
|
||||
- Version bumped 0.4.1 -> 0.4.2 (`plugin_base.py`, `__init__.py`, `VERSION`,
|
||||
`manifest.json`).
|
||||
|
||||
**Verification:**
|
||||
- `ruff`: no new errors (only pre-existing star-import noise and the two
|
||||
pre-existing F841 at `widgets.py:563` and `dialogs.py:645`).
|
||||
- Full test suite: `248 passed`.
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.4.2; commit only after the user
|
||||
confirms the ZIP works).
|
||||
|
||||
---
|
||||
|
||||
## 19. v0.4.3 - Sync delivery date after auto-anticipation; BASIC calendar reminders; sign-reason message
|
||||
|
||||
**Date:** 2026-06-24
|
||||
|
||||
**Goal:** Fix a follow-up bug reported by the owner after testing v0.4.2, add a
|
||||
clearer message when signing is requested, and make the calendar export work in
|
||||
BASIC mode.
|
||||
|
||||
**(1) Delivery date out of sync after an automatic anticipation (main bug).**
|
||||
|
||||
Root cause (confirmed from the code + the owner's report): when the will is
|
||||
rebuilt while it still spends the same coins as a previous one (e.g. after
|
||||
deleting an heir WITHOUT changing the date), the core engine AUTOMATICALLY
|
||||
anticipates the transaction locktime by one day (Will.check_anticipate /
|
||||
Util.anticipate_locktime) so the new transaction can be mined before the old
|
||||
one. However the plugin's stored delivery date (WILL_SETTINGS["locktime"]) was
|
||||
NOT updated, so on the next Check the plugin compared the stored date (original)
|
||||
with the transaction locktime (original minus one day), mistook the automatic
|
||||
anticipation for a user POSTPONE, and wrongly asked to invalidate the will.
|
||||
|
||||
Fix: after a (re)build, `BalBuildWillDialog._sync_locktime_to_built_txs` sets the
|
||||
stored delivery date to the MINIMUM locktime among the valid built transactions
|
||||
(via `Will.get_min_locktime`). The date is only ever moved EARLIER
|
||||
(anticipation); a genuine user postpone is never overwritten. The update is
|
||||
routed through `BalWindow.update_setting_widgets`, which stores the value,
|
||||
persists it and refreshes the date widgets in every panel/wizard, so the visible
|
||||
delivery date reflects the anticipated date and the calendar (.ics) export uses
|
||||
it too (owner-confirmed behaviour; the minimum is used when several
|
||||
transactions carry different locktimes).
|
||||
|
||||
**(2) Explain WHY signing is requested after an anticipation.**
|
||||
|
||||
When the date was auto-anticipated, the wizard now shows an orange note on the
|
||||
"Building your will" row before the sign prompt: "The delivery date was
|
||||
automatically moved one day earlier so the updated will can correctly replace
|
||||
the previous one. Please sign (and broadcast) to confirm the change." (A
|
||||
`_date_was_anticipated` flag set by the sync step drives this.)
|
||||
|
||||
**(3) Calendar reminders in BASIC mode.**
|
||||
|
||||
In BASIC mode the check-alive parameter is hidden and not managed by the user,
|
||||
so spreading reminders over the check-alive period (the ADVANCED behaviour) is
|
||||
meaningless and produced wrong/garbage dates. The calendar now uses three FIXED
|
||||
reminders in BASIC - 30, 10 and 1 day before the inheritance delivery date -
|
||||
dropping any offset that would fall in the past. ADVANCED mode is unchanged. The
|
||||
logic is a pure helper `basic_reminder_offsets()` with unit tests.
|
||||
|
||||
**Files changed:**
|
||||
- `bal/gui/qt/dialogs.py` - `_sync_locktime_to_built_txs` + `_date_was_anticipated`
|
||||
flag + sign-reason note.
|
||||
- `bal/gui/qt/widgets.py` - BASIC calendar reminders (`basic_reminder_offsets`,
|
||||
`BASIC_REMINDER_OFFSETS`); the .ics export now uses the anticipated delivery
|
||||
date.
|
||||
- `tests/test_group_g_basic_calendar.py` - new unit tests for the BASIC
|
||||
reminder offsets.
|
||||
- Version bumped 0.4.2 -> 0.4.3 (`plugin_base.py`, `__init__.py`, `VERSION`,
|
||||
`manifest.json`).
|
||||
|
||||
**Verification:**
|
||||
- `ruff`: no new errors (only pre-existing star-import noise and the two
|
||||
pre-existing F841 at `widgets.py:563` and `dialogs.py:645`).
|
||||
- Full test suite: `255 passed` (248 + 7 new BASIC-calendar tests).
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.4.3; commit only after the user
|
||||
confirms the ZIP works).
|
||||
|
||||
---
|
||||
|
||||
## 20. v0.4.4 - Wizard hints, BASIC/ADVANCED help text, Check tooltip, ADVANCED check-alive visibility, backup-tx default OFF
|
||||
|
||||
**Date:** 2026-06-24
|
||||
|
||||
**Goal:** Apply a batch of owner-requested UI/wording fixes and fix a visibility
|
||||
bug for the Check-Alive field.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- **(P1)** Plugin settings, "Number of reminders" help text: rewritten to
|
||||
document BASIC and ADVANCED separately. BASIC: "Calendar reminder 30, 10 and
|
||||
1 days before."; ADVANCED: the previous "spread across the check-alive period"
|
||||
explanation (range 1 to 5, default 3).
|
||||
- **(P2)** Build-your-will WIZARD only: added an explanatory label ABOVE the
|
||||
date field - "Enter the date on which you want the inheritance (or backup) of
|
||||
your Electrum wallet to take effect." - and a cautionary label BELOW the
|
||||
miner-fee field - "Please note: Do not reduce the miner fees unless you know
|
||||
what you're doing". These appear only in the wizard (vertical layout), not on
|
||||
the WILL/HEIR toolbars.
|
||||
- **(P3)** WILL tab: the Check button tooltip changed from "Check" to
|
||||
"Check Inheritance" so the icon's purpose is clear.
|
||||
- **(P4 - bug fix)** Check-Alive visibility on USER TYPE change: the WILL/HEIR
|
||||
toolbar settings widgets are created once and reused for the whole session, so
|
||||
switching from BASIC to ADVANCED previously did NOT re-show the Check-Alive
|
||||
field there (it reappeared only in the freshly-created wizard). Added
|
||||
`WillSettingsWidget.apply_user_type_visibility()`, called from
|
||||
`BalWindow.update_all()` (which the USER TYPE combo triggers), so the
|
||||
Check-Alive field is shown/hidden immediately on the existing WILL and HEIR
|
||||
tabs too, without restarting Electrum.
|
||||
- **(P5)** "Add transaction without willexecutor" now defaults to OFF
|
||||
(`NO_WILLEXECUTOR` default False). A fresh wallet therefore does NOT create the
|
||||
extra no-will-executor backup transaction unless the user enables it from the
|
||||
wizard. The value is persisted in Electrum's configuration (as before), so the
|
||||
plugin always follows the saved choice; the new default only applies when no
|
||||
value has been stored yet.
|
||||
|
||||
**Files changed:**
|
||||
- `bal/gui/qt/plugin.py` - reminders help text (P1).
|
||||
- `bal/gui/qt/widgets.py` - wizard date hint + miner-fee note (P2);
|
||||
`apply_user_type_visibility` (P4).
|
||||
- `bal/gui/qt/lists.py` - Check tooltip -> "Check Inheritance" (P3).
|
||||
- `bal/gui/qt/window.py` - call `apply_user_type_visibility` from `update_all` (P4).
|
||||
- `bal/core/plugin_base.py` - `NO_WILLEXECUTOR` default -> False (P5).
|
||||
- Version bumped 0.4.3 -> 0.4.4 (`plugin_base.py`, `__init__.py`, `VERSION`,
|
||||
`manifest.json`).
|
||||
|
||||
**Verification:**
|
||||
- `ruff`: no new errors (only pre-existing star-import noise and pre-existing
|
||||
F841 at `widgets.py:591`, `lists.py:185` and `lists.py:272`).
|
||||
- Full test suite: `255 passed`.
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.4.4; commit only after the user
|
||||
confirms the ZIP works).
|
||||
|
||||
---
|
||||
|
||||
## 21. v0.4.5 - Fix invalidation loop (10s pause + stop-on-persistent-postpone), add "BAL Invalidate transaction" label on the automatic path, fix wizard text truncation
|
||||
|
||||
**Date:** 2026-06-24
|
||||
|
||||
**Reported issues (after testing v0.4.4):**
|
||||
|
||||
- **Issue 1 (wizard text truncated):** the explanatory hint above the delivery
|
||||
date and the miner-fee note below the fee field were displayed cut in half.
|
||||
- **Issue 2 (invalidation loop):** when the user postpones the delivery date,
|
||||
the "Invalidate your old will" window appears; after signing, an IDENTICAL
|
||||
invalidation window immediately appears again (endless loop) while Electrum
|
||||
is still broadcasting the transaction. In addition, the
|
||||
"BAL Invalidate transaction" label did not appear in Electrum's on-chain
|
||||
history for this automatic ("postpone") path.
|
||||
|
||||
**Root causes:**
|
||||
|
||||
- Issue 1: the wizard QLabels had `setWordWrap(True)` but were added to the
|
||||
vertical layout with `AlignLeft`, so each label took its narrow `sizeHint`
|
||||
width. Word-wrap then computed line breaks against an almost-zero width and
|
||||
the text appeared truncated.
|
||||
- Issue 2a (loop): after broadcasting the automatic invalidation,
|
||||
`on_success_invalidate` re-ran phase 1 immediately. Electrum had not yet seen
|
||||
the invalidation transaction, so phase 1 still detected a postpone and the
|
||||
wizard re-prompted to invalidate - forever.
|
||||
- Issue 2b (missing label): the automatic broadcast in
|
||||
`loop_broadcast_invalidating` did not set any history label (unlike the
|
||||
Tools -> Invalidate menu path, which does).
|
||||
|
||||
**What changed (user-approved solution):**
|
||||
|
||||
- `bal/gui/qt/dialogs.py`
|
||||
- `loop_broadcast_invalidating`: set the `"BAL Invalidate transaction"`
|
||||
history label (fixes Issue 2b), matching the Tools -> Invalidate menu.
|
||||
IMPORTANT follow-up fix: the first attempt did not work because it took the
|
||||
txid from `Network.broadcast_transaction()`'s return value - but that method
|
||||
is declared `-> None` and ALWAYS returns None, so the `set_label()` call
|
||||
sat in an `else` branch that was never reached. The label is now taken from
|
||||
`tx.txid()` (the transaction is already signed and complete here, so this is
|
||||
the stable, correct id - exactly what the working Tools -> Invalidate path
|
||||
uses) and is set BEFORE broadcasting (set_label is local-only, no network).
|
||||
- `invalidate_task`: the post-broadcast pause is now 10 seconds (was 5) so
|
||||
Electrum has time to register the new transaction before phase 1 re-runs;
|
||||
a new `self._invalidation_broadcast` flag is set after the broadcast.
|
||||
- `on_success_phase1` (the `have_to_sign is None` branch): if
|
||||
`_invalidation_broadcast` is set and a postpone is STILL detected, STOP with
|
||||
a clear message ("Your old will has been invalidated and the transaction was
|
||||
broadcast... please wait until it is confirmed, then press Check again")
|
||||
instead of re-prompting to invalidate (fixes Issue 2a - no more loop).
|
||||
- `__init__`: initialise `self._invalidation_broadcast = False`.
|
||||
|
||||
- `bal/gui/qt/widgets.py`
|
||||
- Wizard branch: give the two explanatory QLabels (delivery-date hint and
|
||||
miner-fee note) a `setMinimumWidth(30 * char_width_in_lineedit())` so
|
||||
word-wrap uses the full dialog width and the whole text is visible
|
||||
(fixes Issue 1).
|
||||
|
||||
- Version bumped 0.4.4 -> 0.4.5 (`plugin_base.py`, `__init__.py`, `VERSION`,
|
||||
`manifest.json`).
|
||||
|
||||
**Verification:**
|
||||
|
||||
- `py_compile`: `dialogs.py` and `widgets.py` compile OK.
|
||||
- Full test suite: `255 passed`.
|
||||
- `ruff`: no new errors (only pre-existing star-import F401/F403/F405 noise and
|
||||
the pre-existing F841 `e` warnings).
|
||||
- Headless wizard label check: with the 270px minimum width, both labels wrap
|
||||
to multiple lines (3 lines and 2 lines) instead of being truncated.
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.4.5; commit only after the user
|
||||
confirms the ZIP works).
|
||||
|
||||
---
|
||||
|
||||
## 22. v0.4.6 - DUST one-line-per-heir, heirs on one line, scrollable report, wizard final check, wizard text truncation fix, anticipated-date notice styling
|
||||
|
||||
**Date:** 2026-06-24
|
||||
|
||||
**Reported issues (after testing v0.4.5):**
|
||||
|
||||
- **Issue 1 (allegato13 - DUST):** when the wallet balance is below the dust
|
||||
limit the inheritance is not feasible. The dialog printed the "is DUST"
|
||||
exclusion ONCE PER (will-executor x heir): with 20 will-executors and 10
|
||||
heirs that is 200 identical rows. There should be one row PER HEIR.
|
||||
- **Issue 2 (allegato18 - heirs):** heirs were listed one per line, wasting
|
||||
vertical space. They should be on a single line: "Heirs: a, b, c".
|
||||
- **Issue 3 (allegato14 - overflow):** with many will-executors the "Building
|
||||
Will" window kept growing taller for every line until it ran off-screen and
|
||||
the bottom buttons became unreachable. It needs a scrollable area.
|
||||
- **Issue 4 (allegato15 - wizard final check, case B):** the wizard did not run
|
||||
the final will-executor verification, so the user always had to press "Check"
|
||||
manually afterwards.
|
||||
- **Issue 5 (allegato16 - wizard text truncated):** the wizard hint and fee
|
||||
note were still cut in half.
|
||||
- **Issue 6 (allegato17 - notice styling):** the yellow "delivery date was
|
||||
moved" notice should be black bold and split onto two lines.
|
||||
|
||||
**Root causes:**
|
||||
|
||||
- Issue 1: a double loop over every valid will AND every heir printed the dust
|
||||
row N_executors x N_heirs times.
|
||||
- Issue 4: the wizard's `on_next_we` only called `build_will_task()` and, unlike
|
||||
the "Check" button (`lists.py:check()`), never called
|
||||
`check_transactions()`.
|
||||
- Issue 5: the two QLabels were added to the vertical layout WITH an
|
||||
`alignment=AlignLeft` flag. A widget added with an alignment flag is NOT
|
||||
stretched to the layout width, so the word-wrapped label used a narrow
|
||||
sizeHint width and the reserved height was too small, cutting the text.
|
||||
`setMinimumWidth` alone could not fix it because the alignment flag still
|
||||
blocked horizontal stretch.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/gui/qt/dialogs.py`
|
||||
- `task_phase1` (DUST report): collect dust heirs in a de-duplicated dict and
|
||||
print ONE row per heir, without the will-executor reference (Issue 1).
|
||||
- `_build_success_report`: list all heirs on a SINGLE green/bold line
|
||||
("Heirs: a, b, c") instead of one row each (Issue 2).
|
||||
- `__init__` + `msg_update`: wrap the report label in a `QScrollArea` with a
|
||||
capped maximum height (400px) and auto-scroll to the bottom, so the dialog
|
||||
no longer grows off-screen and the buttons stay reachable (Issue 3).
|
||||
- `BalWizardDialog.on_next_we`: after building, run the SAME final
|
||||
`check_transactions()` as the Check button (`Will.needs_server_check`),
|
||||
so the wizard performs the will-executor verification automatically
|
||||
(Issue 4).
|
||||
- `on_success_phase1`: the anticipated-date notice is now black bold and split
|
||||
onto two lines after "...previous one." (Issue 6).
|
||||
|
||||
- `bal/gui/qt/widgets.py`
|
||||
- Wizard branch: add the two explanatory QLabels WITHOUT an alignment flag and
|
||||
with an Expanding/Minimum size policy (plus a minimum width on the widget),
|
||||
so they stretch to the full width and word-wrap correctly instead of being
|
||||
truncated (Issue 5).
|
||||
|
||||
- Version bumped 0.4.5 -> 0.4.6 (`plugin_base.py`, `__init__.py`, `VERSION`,
|
||||
`manifest.json`).
|
||||
|
||||
**Verification:**
|
||||
|
||||
- `py_compile`: `dialogs.py` and `widgets.py` compile OK.
|
||||
- Full test suite: `255 passed`.
|
||||
- `ruff`: no new errors (only pre-existing star-import noise and the
|
||||
pre-existing F841 `e` warnings).
|
||||
- Headless wizard label check (real wizard layout, 780px dialog): both labels
|
||||
expand to the full width (740px) and the full text fits - NOT truncated.
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.4.6; commit only after the user
|
||||
confirms the ZIP works).
|
||||
|
||||
---
|
||||
|
||||
## 23. v0.4.7 - Report area opens 500px tall, heirs back to one-per-line, explicit wizard line breaks, ALL-DUST guard (block only when EVERY heir is dust)
|
||||
|
||||
**Date:** 2026-06-24
|
||||
|
||||
**Goal (4 owner-approved changes from testing v0.4.6):**
|
||||
|
||||
1. (allegato1) The scrollable "Building Will" report opened far too short
|
||||
(~140px). Open it already 500px tall, growing up to 700px before the
|
||||
scrollbar takes over.
|
||||
2. Revert the v0.4.6 "all heirs on one line" form back to ONE heir per line
|
||||
(green, bold). Heir names can be long and, now that the report scrolls,
|
||||
there is no need to compress them.
|
||||
3. (allegato2) Add an explicit line break in two wizard texts at the exact
|
||||
spots the owner marked: after "(or backup)" in the date hint and after
|
||||
"miner fees" in the fee note.
|
||||
4. (LOG analysis) When EVERY heir's share is below the dust limit the will was
|
||||
still built, signed, checked and listed - an "empty" inheritance that pays
|
||||
nobody. Block it with a clear message, but ONLY when ALL heirs are dust; a
|
||||
mix of dust + valid heirs must keep building normally.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/gui/qt/dialogs.py`
|
||||
- `BalBuildWillDialog.__init__`: report `QScrollArea` now
|
||||
`setMinimumHeight(500)` / `setMaximumHeight(700)` (change 1).
|
||||
- `_build_success_report`: list each heir on its own line again, green +
|
||||
bold, de-duplicated and skipping the internal will-executor pseudo-heirs
|
||||
(change 2, revert of v0.4.6).
|
||||
- `task_phase1`: add a dedicated `except HeirAmountIsDustException` handler
|
||||
BEFORE the generic `except Exception`, showing a clear RED message
|
||||
("All heirs' shares are below the dust limit: the inheritance cannot be
|
||||
created. Increase the amounts or reduce the number of heirs.") and stopping
|
||||
without signing/checking, so no empty will is created (change 4).
|
||||
|
||||
- `bal/gui/qt/widgets.py`
|
||||
- Wizard date hint: explicit `\n` after "(or backup)".
|
||||
- Wizard fee note: explicit `\n` after "miner fees" (change 3).
|
||||
|
||||
- `bal/core/heirs.py`
|
||||
- `prepare_lists`: NEW all-dust guard added at the END of the function, where
|
||||
the `locktimes` dict already contains EVERY heir of EVERY locktime with the
|
||||
final dust marking. It counts the real heirs (excluding the `w!ll3x3c"`
|
||||
will-executor pseudo-heirs) and how many have a valid, non-dust amount; if
|
||||
there are real heirs but none is payable it raises
|
||||
`HeirAmountIsDustException` (change 4).
|
||||
- WHY here and NOT in `prepare_transactions`: `prepare_transactions` only
|
||||
ever processes the single lowest locktime, so a guard there would wrongly
|
||||
block a will whose later locktimes still have valid heirs (false positive).
|
||||
`prepare_lists` is the only place that sees all heirs/locktimes AND the
|
||||
final dust state of both fixed and percentage heirs.
|
||||
- The `HeirAmountIsDustException` raised here propagates cleanly: it is not a
|
||||
`WillExecutorFeeException`, so it skips that handler in `buildTransactions`
|
||||
and reaches the GUI without the misleading "error preparing transactions"
|
||||
log.
|
||||
|
||||
- `bal/gui/qt/common.py`
|
||||
- Import `HeirAmountIsDustException` from `...core.heirs` so it is available
|
||||
to `dialogs.py` via `from .common import *`.
|
||||
|
||||
- `tests/test_core_heirs_extra.py`
|
||||
- Add 3 tests pinning the dust logic:
|
||||
`test_prepare_lists_all_dust_raises` (tiny balance + percentages -> raises),
|
||||
`test_prepare_lists_mixed_dust_continues` (dust + valid -> no raise),
|
||||
`test_prepare_lists_multi_locktime_continues` (dust on early date, valid on
|
||||
later date -> no raise; guards against the false positive).
|
||||
|
||||
- Version bumped 0.4.6 -> 0.4.7 (`plugin_base.py`, `__init__.py`, `VERSION`,
|
||||
`manifest.json`).
|
||||
|
||||
**Verification:**
|
||||
|
||||
- `py_compile`: `heirs.py`, `common.py`, `dialogs.py` and the test file compile OK.
|
||||
- Full test suite: `258 passed` (255 previous + 3 new dust tests).
|
||||
- `ruff`: no new errors (only pre-existing star-import noise and the
|
||||
pre-existing F841 `e` warnings).
|
||||
- Manual dust trace (real `prepare_lists`, mocked wallet): all-dust raises;
|
||||
mixed and multi-locktime continue and keep the valid heir.
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.4.7; commit only after the user
|
||||
confirms the ZIP works).
|
||||
|
||||
377
HANDOFF.md
377
HANDOFF.md
@@ -1,221 +1,232 @@
|
||||
# HANDOFF — BAL Electrum Plugin (Bitcoin After Life)
|
||||
# HANDOFF — BAL (Bitcoin After Life) Electrum plugin
|
||||
|
||||
> **Purpose of this file:** allow ANY new chat / AI model to resume this project
|
||||
> WITHOUT losing context. If you are a new assistant, READ THIS FILE FIRST,
|
||||
> then read `CHANGELOG.md` and `.agent_memory_tasks.md`.
|
||||
>
|
||||
> **Chat language is Italian, but ALL output (code, comments, UI text, docs,
|
||||
> CHANGELOG, commit messages) MUST be in ENGLISH.** The user is NOT a programmer.
|
||||
> Purpose: let ANY future AI assistant (Claude or another model, more advanced
|
||||
> or cheaper) resume work on this project with full context, without having to
|
||||
> re-discover the codebase. Read this file FIRST, then `CHANGELOG.md` and
|
||||
> `.agent_memory_tasks.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. MANDATORY STANDING RULES (apply to EVERY task, never skip)
|
||||
## 0. TL;DR — what this project is
|
||||
|
||||
- **R1 — LANGUAGE:** Italian is ONLY for chatting. ALL deliverables in ENGLISH
|
||||
(code, docstrings, comments, UI strings, docs, CHANGELOG, commit messages).
|
||||
- **R2 — DOCUMENTED CODE:** every method/class needs a docstring + explanatory
|
||||
comments. When a non-obvious design choice is made, explain WHY in a comment.
|
||||
- **R3 — NEVER INVENT:** if anything is missing or unclear, STOP and ask the user
|
||||
clear, simple questions (he is not a programmer). Be "100% sure" before acting.
|
||||
- **R4 — HUMAN CHECKPOINT:** before writing/modifying code, show the PLAN and WAIT
|
||||
for the user's explicit "OK".
|
||||
- **METHOD per task:** DISCOVER → PLAN (wait OK) → EXECUTE → VERIFY → ITERATE
|
||||
(max 8 attempts, then declare "UNRESOLVED").
|
||||
- **LOG:** a single `CHANGELOG.md` in English, one numbered entry per task.
|
||||
- **ZIP-FIRST:** always deliver a test ZIP for the user to try BEFORE committing
|
||||
plugin code. Commit ONLY after explicit user confirmation.
|
||||
- Always run `ruff` + the official test suite before committing/reporting/zipping.
|
||||
- **Product:** BAL ("Bitcoin After Life") — an inheritance plugin for the
|
||||
**Electrum 4.7.2** Bitcoin wallet (Qt / **PyQt6**).
|
||||
- **Form:** external **ZIP plugin** (not bundled in Electrum). The user
|
||||
installs the ZIP from Electrum's plugin manager.
|
||||
- **What it does:** lets a wallet owner pre-build, sign and (later) broadcast
|
||||
Bitcoin transactions that pay one or more **heirs** after a chosen **date**
|
||||
(a future UNIX-timestamp `nLockTime`). Optional **will-executors** (remote
|
||||
services) can be paid a fee to broadcast the inheritance when due. The owner
|
||||
periodically proves they are alive ("check-alive"); if the deadline passes,
|
||||
the inheritance becomes spendable.
|
||||
- **Current version:** see `bal/VERSION` (last shipped: **0.4.7**).
|
||||
|
||||
---
|
||||
|
||||
## 2. PROJECT OVERVIEW
|
||||
## 1. MANDATORY working rules (the owner set these — always follow them)
|
||||
|
||||
- Electrum **4.7.2** Qt (PyQt6) inheritance plugin.
|
||||
- External zip plugin id: `electrum_external_plugins.bal`.
|
||||
- **zipimport caches the plugin → the user MUST fully restart Electrum after
|
||||
installing a new ZIP** (always remind him).
|
||||
- Repo: `Bitcoin-after-life/test` (GitHub). Working branch: `genspark_ai_developer`.
|
||||
- Main code lives under `bal/`. Tests under `tests/`. Electrum source vendored in
|
||||
`electrum-src/` (read-only reference).
|
||||
These are non-negotiable. They come from the owner directly.
|
||||
|
||||
### Version files (keep ALL FOUR in sync on every release)
|
||||
- `bal/manifest.json` → `"version"`
|
||||
- `bal/__init__.py` → `__version__`
|
||||
- `bal/core/plugin_base.py` → `__version__ = "..." # AUTOMATICALLY GENERATED DO NOT EDIT`
|
||||
- `bal/VERSION`
|
||||
- **Current released version: 0.3.9**
|
||||
- **R1 — LANGUAGE.** The CHAT language with the owner is **Italian**. But ALL
|
||||
*output* — source code, comments, docstrings, UI strings, docs, `CHANGELOG.md`,
|
||||
commit messages, this handoff — must be in **ENGLISH**.
|
||||
- **R2 — DOCUMENTED CODE.** Every method/class gets a docstring + explanatory
|
||||
comments. Always explain *WHY* for any non-obvious decision.
|
||||
- **R3 — NEVER INVENT.** If something is missing or unclear, STOP and ask the
|
||||
owner clear, simple questions. **The owner is NOT a programmer** — explain in
|
||||
plain language, avoid jargon. Be "100% sure" before acting.
|
||||
- **R4 — HUMAN CHECKPOINT.** Before writing/modifying code, present the PLAN
|
||||
and WAIT for an explicit "OK" from the owner.
|
||||
- **METHOD:** DISCOVER → PLAN (wait for OK) → EXECUTE → VERIFY → ITERATE
|
||||
(max ~8 attempts per problem, then step back and ask).
|
||||
- **LOG:** keep a single `CHANGELOG.md`, in English, **one numbered entry per
|
||||
task** (newest entry appended at the END of the file).
|
||||
- **ZIP-FIRST.** Deliver a test ZIP and let the owner test it BEFORE committing.
|
||||
**Commit ONLY after the owner explicitly confirms the ZIP works.**
|
||||
- **ALWAYS** run `ruff` + the official test suite before committing / reporting
|
||||
/ zipping.
|
||||
- **CREDIT-SAVING (important).** The owner is low on funds. Minimize token /
|
||||
credit usage: report brief summaries (do NOT paste whole modified code
|
||||
blocks back), and batch work into a single ZIP/test cycle where possible.
|
||||
|
||||
---
|
||||
|
||||
## 3. BUILD / TEST / RELEASE COMMANDS
|
||||
## 2. Repository layout (what lives where)
|
||||
|
||||
### Build the ZIP (clear caches first)
|
||||
```bash
|
||||
cd /home/user/webapp
|
||||
find bal -name "__pycache__" -type d -exec rm -rf {} + ; find bal -name "*.pyc" -delete
|
||||
python3 build_zip.py bal-electrum-plugin-vX.Y.Z.zip # builds 37 files
|
||||
```
|
||||
bal/ <- the plugin package (this is what ships in the ZIP)
|
||||
__init__.py <- __version__ (one of 4 version files)
|
||||
VERSION <- plain-text version (one of 4 version files)
|
||||
manifest.json <- plugin manifest, "version" field (one of 4)
|
||||
core/
|
||||
plugin_base.py <- __version__ "AUTOMATICALLY GENERATED" (one of 4)
|
||||
heirs.py <- HEIRS + transaction building (prepare_lists,
|
||||
prepare_transactions, buildTransactions). CORE LOGIC.
|
||||
will.py <- Will/WillItem, validation (check_amounts, check_will),
|
||||
exceptions (AmountException, WillExpiredException, ...).
|
||||
willexecutors.py <- remote will-executor services handling.
|
||||
util.py <- locktime parsing/most helpers (timestamps only).
|
||||
gui/qt/
|
||||
common.py <- shared imports; every gui module does
|
||||
`from .common import *`. Add new shared imports HERE.
|
||||
dialogs.py <- the big build/sign/broadcast dialog
|
||||
(BalBuildWillDialog, task_phase1/2), wizard glue.
|
||||
widgets.py <- WillSettingsWidget + wizard widgets/labels.
|
||||
window.py <- BalWalletWindow (build_will, check_will, get_transactions).
|
||||
lists.py, calendar.py, theme.py, window_utils.py, ...
|
||||
tests/ <- pytest suite (see run command below).
|
||||
electrum-src/ <- a copy of Electrum source, used ONLY for tests
|
||||
(PYTHONPATH=electrum-src). NOT shipped in the ZIP.
|
||||
build_zip.py <- builds the shippable ZIP (37 files).
|
||||
CHANGELOG.md <- numbered task log (English).
|
||||
.agent_memory_tasks.md <- terse internal memory notes per task batch.
|
||||
HANDOFF.md <- this file.
|
||||
```
|
||||
|
||||
### Run the full test suite (must stay GREEN: 239 passed)
|
||||
---
|
||||
|
||||
## 3. How to build, test and lint
|
||||
|
||||
Run everything from `/home/user/webapp`.
|
||||
|
||||
**Full test suite (expected: 258 passed as of v0.4.7):**
|
||||
```bash
|
||||
cd /home/user/webapp
|
||||
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 -m pytest \
|
||||
tests/test_core_*.py tests/test_gui_*.py \
|
||||
tests/test_anticipate_past_locktime.py tests/test_anticipate_manual_locktime.py \
|
||||
tests/test_group_b_auto_sign.py tests/test_group_c_settings.py \
|
||||
tests/test_group_d_alarms.py tests/test_group_e_mock_giovanna7.py -q
|
||||
tests/test_group_d_alarms.py tests/test_group_e_mock_giovanna7.py \
|
||||
tests/test_group_f_heir_change_rebuild.py tests/test_group_g_basic_calendar.py -q
|
||||
```
|
||||
|
||||
### Ruff (only PRE-EXISTING noise is acceptable)
|
||||
**Lint (only NEW errors matter; ignore pre-existing noise):**
|
||||
```bash
|
||||
cd /home/user/webapp && ruff check bal/<changed files>
|
||||
ruff check <files> | grep -oE "^[^ ]+\.py:[0-9]+:[0-9]+: [A-Z][0-9]+" \
|
||||
| grep -vE "F401|F403|F405|F841"
|
||||
```
|
||||
Pre-existing warnings that are NOT your fault and can be ignored:
|
||||
- `F401/F403/F405` star-import noise (`from .common import *`).
|
||||
- `F841` at: `will.py:151`, `dialogs.py:615` (`except NoHeirsException as e`),
|
||||
`lists.py:185`, `lists.py:272`.
|
||||
- `E501` long lines in several pre-existing spots.
|
||||
Pre-existing, KNOWN-OK ruff noise: `F401/F403/F405` (star-imports via
|
||||
`from .common import *`) and 2× `F841` (an unused `e` in two `except` blocks).
|
||||
Do NOT "fix" these unless asked — they are intentional / out of scope.
|
||||
|
||||
### Git / PR / Release workflow
|
||||
- `setup_github_environment` first. If `git push` fails with
|
||||
"Invalid username or token", CALL `setup_github_environment` AGAIN, then retry.
|
||||
- Token for API calls:
|
||||
`TOKEN=$(sed -n 's#https://\([^:]*\):\([^@]*\)@.*#\2#p' ~/.git-credentials | head -1)`
|
||||
- Repo for API: `Bitcoin-after-life/test`.
|
||||
- Releases published so far: v0.3.6, v0.3.7, v0.3.8, **v0.3.9 (latest)**.
|
||||
- Attach the ZIP as a release asset via the uploads API.
|
||||
|
||||
---
|
||||
|
||||
## 4. CURRENT STATE (as of v0.3.9, COMMITTED + RELEASED)
|
||||
|
||||
v0.3.9 is merged to `main` (PR #11) and released with the ZIP attached.
|
||||
sha256 of the released ZIP: `cd52b6f5e6276fb707ea4bf477a00bd0469dfd88960282fb74c219ee0f5f4292`.
|
||||
|
||||
What shipped in v0.3.9 (TASK A/B/C/D + regression fix A3):
|
||||
- **A** clearer "Will expired" message: shortened will id (8+8 chars via
|
||||
`Will._short_will_id`) + readable UTC date (`Will._format_locktime`) instead of
|
||||
raw UNIX timestamp; shown ORANGE (warning) not RED (error) in the wizard.
|
||||
- **A2** message split on two lines via `<br>` (rendered as HTML by `msg_warning`).
|
||||
- **B** History labels (text only): inheritance tx → `BAL Inheritance transaction`;
|
||||
invalidate tx → `BAL Invalidate transaction`. Colours = Electrum defaults
|
||||
(Electrum colours outgoing tx descriptions red by itself, history_list.py:193-196).
|
||||
- **C** "No will-executor TX" checkbox in plugin Settings (`plugin.py`), bound to
|
||||
existing `NO_WILLEXECUTOR` config (default ON, line plugin_base.py:193), with help
|
||||
text "Create a will that does not require a Will-executor; it can be saved, for
|
||||
example, on a USB stick, and a copy can be given to the heirs." Included in reset.
|
||||
- **D** wizard button "Create your will" → "Build Your Will" (lists.py:473).
|
||||
- **A3 regression fix:** adding an heir to an expired will via the wizard no longer
|
||||
failed to invalidate. Implemented a `"invalidate_classic"` signal returned from
|
||||
`task_phase1` and handled in `on_success_phase1` (dialogs.py) that closes the
|
||||
wizard and shows a popup telling the user to use `Tools → Invalidate`.
|
||||
**NOTE: this popup text is about to be CHANGED — see the pending task below.**
|
||||
|
||||
---
|
||||
|
||||
## 5. PENDING TASK (NOT STARTED) — UNIFY THE INVALIDATE PROCEDURE
|
||||
|
||||
### Problem the user reported
|
||||
There are currently TWO different "invalidate" procedures, and the user wants ONE
|
||||
identical behaviour for BOTH the **CHECK button** and the **WIZARD**.
|
||||
|
||||
- **PROCEDURE 1 "classic/manual"** = `window.py::invalidate_will` (line ~695):
|
||||
waiting dialog → "please sign and broadcast" popup → CLASSIC Electrum tx window
|
||||
(Sign/Broadcast buttons) → **SETS** history label "BAL Invalidate transaction"
|
||||
(line ~704). Used by: Tools→Invalidate menu (lists.py:468→571), a dialog button
|
||||
(dialogs.py:1356), the on-close/postpone paths (window.py:539,576,614).
|
||||
**This window opens correctly IN FRONT** (user confirmed) because nothing else
|
||||
is closing at the same time.
|
||||
- **PROCEDURE 2 "automatic"** = `dialogs.py::invalidate_task` (line ~902):
|
||||
password prompt inside the wizard → sign + auto-broadcast
|
||||
(`loop_broadcast_invalidating`, line ~729) → **does NOT set the history label**.
|
||||
Used by the CHECK button and the FIRST `WillExpiredException` handler in
|
||||
`task_phase1` (dialogs.py:594 → `return None, Will.invalidate_will` at ~598,
|
||||
which makes `on_success_phase1` see `have_to_sign is None` → password prompt).
|
||||
|
||||
### CHECK button flow (important)
|
||||
`lists.py:545 check()` → `BalBuildWillDialog(...).build_will_task()` →
|
||||
`task_phase1` → `on_success_phase1`. **It is the SAME engine as the wizard.**
|
||||
|
||||
### FINAL REQUIREMENT (user-approved, OPTION A refined)
|
||||
Make CHECK and WIZARD behave IDENTICALLY for an expired will:
|
||||
1. First show a **WARNING popup** (REMOVE the old "use the top-right menu
|
||||
Tools → Invalidate" wording).
|
||||
2. Then **AUTOMATICALLY open the CLASSIC Electrum sign window** (PROCEDURE 1,
|
||||
`window.py::invalidate_will`) so the label is set and the user can Sign + Broadcast.
|
||||
3. Sequence: warning popup → user clicks OK → classic sign window opens BY ITSELF,
|
||||
IN FRONT.
|
||||
|
||||
### APPROVED WARNING POPUP TEXT (verbatim, English per R1)
|
||||
**Build the ZIP (always clear caches first so zipimport doesn't ship stale .pyc):**
|
||||
```bash
|
||||
find bal -name "__pycache__" -type d -exec rm -rf {} + ; find bal -name "*.pyc" -delete
|
||||
python3 build_zip.py bal-electrum-plugin-vX.Y.Z.zip # produces 37 files
|
||||
```
|
||||
Your will has expired and must be invalidated before it can be rebuilt.
|
||||
A transaction window will now open:
|
||||
please SIGN and then BROADCAST it to invalidate your old will.
|
||||
After the invalidation is confirmed, press the Check button to finish the will.
|
||||
|
||||
**Bump version — there are FOUR files, keep them in sync:**
|
||||
```
|
||||
bal/core/plugin_base.py -> __version__ = "X.Y.Z" # AUTOMATICALLY GENERATED DO NOT EDIT
|
||||
bal/__init__.py -> __version__ = "X.Y.Z"
|
||||
bal/VERSION -> X.Y.Z
|
||||
bal/manifest.json -> "version": "X.Y.Z",
|
||||
```
|
||||
(The user wrote "SIGN and then BROADCAST" with a double space — normalize to a
|
||||
single space unless he objects.)
|
||||
|
||||
### Implementation approach (agreed in principle; still needs final PLAN + OK)
|
||||
- Route the expired cases (FIRST handler at ~594, the `invalidate_classic` block,
|
||||
and therefore the CHECK button) through ONE shared helper that:
|
||||
(a) closes the CHECK/wizard dialog FIRST,
|
||||
(b) shows the warning popup,
|
||||
(c) then calls `self.bal_window.invalidate_will()` (PROCEDURE 1) LAST, so the
|
||||
classic window is the last thing opened and stays in front.
|
||||
- Drop the use of PROCEDURE 2 (`invalidate_task`) for the expired case.
|
||||
- **KNOWN RISK / why this is delicate:** earlier attempts to auto-open the classic
|
||||
window *while the wizard was closing* put it BEHIND the main wallet window on the
|
||||
user's machine (Windows focus/stacking). The fix is to make sure NOTHING closes
|
||||
AFTER the classic window opens (close the dialog first, open the tx window last).
|
||||
`Tools → Invalidate` works perfectly precisely because no other window is closing.
|
||||
- The user has hinted he may add MORE requirements before this is implemented, so
|
||||
CONFIRM the full scope before coding.
|
||||
|
||||
### Status: WAITING. Do NOT code yet. Build full PLAN → wait OK (R4) → zip-first.
|
||||
**IMPORTANT for the owner when testing:** after installing a ZIP, the owner
|
||||
must **fully restart Electrum** (not just reload the plugin) — Electrum's
|
||||
`zipimport` caches modules, so a partial reload runs stale code.
|
||||
|
||||
---
|
||||
|
||||
## 6. KEY FILE / LINE REFERENCES (verify line numbers, they drift)
|
||||
## 4. Key technical knowledge (hard-won — saves you hours)
|
||||
|
||||
- `bal/core/will.py`
|
||||
- `check_will()` order (line ~561): `check_invalidated` → `check_will_expired`
|
||||
(raises `WillExpiredException`) → `search_rai` (raises `HeirNotFoundException`).
|
||||
- `invalidate_will()` static (line ~394) builds the invalidation PartialTransaction.
|
||||
- `_short_will_id` / `_format_locktime` helpers + the expired message (with `<br>`).
|
||||
- `bal/gui/qt/dialogs.py`
|
||||
- `BalBuildWillDialog` is the CHECK + wizard engine.
|
||||
- `build_will_task()` (~530) starts `task_phase1`.
|
||||
- `task_phase1()` (~542): first `check_will()`; first `WillExpiredException`
|
||||
handler (~594) → `return None, Will.invalidate_will(...)`; `NotCompleteWill`/
|
||||
`HeirNotFound` → `have_to_build`; inner `check_will()`; inner `WillExpiredException`
|
||||
(~659) → currently returns `"invalidate_classic", None`.
|
||||
- `on_success_phase1()` (~924): unpacks `(have_to_sign, tx)`. If
|
||||
`have_to_sign == "invalidate_classic"` → shows popup. If `have_to_sign is None`
|
||||
→ password prompt "Invalidate your old will" → `invalidate_task` (PROCEDURE 2).
|
||||
- `invalidate_task()` (~902) + `loop_broadcast_invalidating()` (~729): PROCEDURE 2.
|
||||
- `QTimer` is available via `from .common import *` (defined in common.py:53).
|
||||
- `bal/gui/qt/window.py`
|
||||
- `invalidate_will()` (~695): PROCEDURE 1 (the "good" one). Sets label at ~704.
|
||||
- `show_transaction_real()` (~656) uses `show_on_top(d, modal_to_window=False)`.
|
||||
- on-close/postpone expired handling at ~539, ~576, ~614.
|
||||
- `bal/gui/qt/lists.py`
|
||||
- `check()` (~545): the CHECK button. `invalidate_will()` (~571). Menu actions
|
||||
"Check"/"Invalidate" at ~467/468. Wizard button "Build Your Will" at ~473.
|
||||
- `bal/gui/qt/plugin.py`: settings dialog; "No will-executor TX" checkbox + reset.
|
||||
- `bal/gui/qt/window_utils.py`: `show_on_top` (~100), `bring_to_front` (~52),
|
||||
`show_modal` (~86).
|
||||
- `bal/gui/qt/common.py`: `add_widget` helper (~98); imports `QTimer`, `Qt`, etc.
|
||||
- **Locktimes are UNIX timestamps only.** Block-height locktimes were removed
|
||||
(CHANGELOG #1). Ordering/expiry compare timestamps.
|
||||
- **`heirs.py` data shape.** An heir is a list indexed by constants
|
||||
(`heirs.py` top): `HEIR_ADDRESS=0`, `HEIR_AMOUNT=1` (sats or `"<n>%"`),
|
||||
`HEIR_LOCKTIME=2`, `HEIR_REAL_AMOUNT=3` (resolved sats, or the string
|
||||
`"DUST: <n>"` when below the dust limit), `HEIR_DUST_AMOUNT=4` (raw dust sats).
|
||||
- **Will-executor pseudo-heirs.** Internally, each selected will-executor is
|
||||
injected as a fake "heir" whose NAME starts with the reserved marker
|
||||
`w!ll3x3c"` (i.e. `'w!ll3x3c"' + url + '"' + str(locktime)`). Its amount is
|
||||
the executor `base_fee` (always non-dust). When you count/iterate "real"
|
||||
heirs you MUST skip names starting with `w!ll3x3c"`.
|
||||
- **Transaction-building pipeline:**
|
||||
`window.build_will()` → `Heirs.get_transactions()` (recursive over locktimes)
|
||||
→ `Heirs.buildTransactions()` → `Heirs.prepare_lists()` (builds the
|
||||
`locktimes` dict for ALL future locktimes, resolves amounts, marks dust)
|
||||
and `prepare_transactions()` (builds ONE tx for the LOWEST locktime only;
|
||||
the recursion handles the others via leftover `available_utxos`).
|
||||
- **DUST logic (v0.4.7 — verify before touching):**
|
||||
- The "all heirs are dust" guard lives at the END of `prepare_lists`
|
||||
(NOT in `prepare_transactions`). Reason: `prepare_transactions` only sees
|
||||
the single lowest locktime, so a guard there would FALSE-POSITIVE block a
|
||||
will whose later locktimes still have valid heirs. `prepare_lists` is the
|
||||
only place that sees ALL heirs across ALL locktimes with their final dust
|
||||
state (fixed AND percentage).
|
||||
- Guard: count real heirs (skip `w!ll3x3c"`); if there are real heirs but
|
||||
NONE has a valid (non-`"DUST"`) `HEIR_REAL_AMOUNT`, raise
|
||||
`HeirAmountIsDustException` (defined in `heirs.py`). A mix of dust + valid
|
||||
heirs keeps building normally.
|
||||
- **Critical nuance:** with FIXED amounts and a LARGE balance, leftover funds
|
||||
are REDISTRIBUTED (`normalize_perc(..., real=True)`), so small fixed
|
||||
amounts end up with a VALID `HEIR_REAL_AMOUNT` (not dust). The real
|
||||
all-dust case is **small balance + percentage heirs** (matches the owner's
|
||||
log: shares of 214 / 316 / 3 sat). Tests reproduce this with
|
||||
`prepare_lists(800, 100, wallet)` and `"40%"/"60%"` heirs.
|
||||
- The exception is NOT a `WillExecutorFeeException`, so it skips that handler
|
||||
in `buildTransactions` and propagates cleanly to the GUI.
|
||||
- GUI: `dialogs.py task_phase1` has a dedicated `except
|
||||
HeirAmountIsDustException` BEFORE the generic `except Exception`. It shows a
|
||||
RED message and stops (`return False, None`) — no signing/checking, no
|
||||
empty will in the list. `HeirAmountIsDustException` is imported in
|
||||
`common.py` and re-exported via `from .common import *`.
|
||||
- **`broadcast_transaction` returns `None`** (Electrum `network.py`). To get a
|
||||
txid, use `tx.txid()` — do NOT rely on the broadcast return value
|
||||
(this was the root cause of the missing "BAL Invalidate transaction" label,
|
||||
CHANGELOG #21 / v0.4.5).
|
||||
- **Qt label truncation gotcha (CHANGELOG #22).** A `QLabel` added with
|
||||
`alignment=Qt.AlignmentFlag.AlignLeft` is NOT stretched by Qt, so word-wrap
|
||||
computes on a narrow sizeHint and the text gets truncated. Fix: drop the
|
||||
alignment flag, add `setSizePolicy(Expanding, Minimum)` + `setMinimumWidth`.
|
||||
With `setWordWrap(True)`, an explicit `\n` in the text forces a line break.
|
||||
- **`BalBuildWillDialog` report area.** Messages are accumulated as HTML in
|
||||
`self.labels` and joined by `msg_update` (`"<br><br>".join(...)`, `\n`→`<br>`).
|
||||
The report is inside a `QScrollArea` (v0.4.7: `setMinimumHeight(500)`,
|
||||
`setMaximumHeight(700)`); the Close button sits BELOW the scroll area so it
|
||||
stays reachable.
|
||||
|
||||
---
|
||||
|
||||
## 7. HOW TO RESUME IN A NEW CHAT (any model)
|
||||
## 5. Git / delivery workflow
|
||||
|
||||
Tell the new assistant:
|
||||
> "Read `/home/user/webapp/HANDOFF.md`, then `CHANGELOG.md` and
|
||||
> `.agent_memory_tasks.md`. Follow rules R1–R4 and zip-first. The next task is the
|
||||
> 'unify invalidate procedure' task in HANDOFF.md section 5 — present the PLAN and
|
||||
> wait for my OK before coding."
|
||||
- **Branch:** work on `genspark_ai_developer`. Open PRs into `main`.
|
||||
- **Commit policy:** ZIP-FIRST — build a test ZIP, let the owner confirm it
|
||||
works, THEN commit. (This differs from "commit after every change"; the owner
|
||||
explicitly prefers ZIP-first because they manually test each build.)
|
||||
- Before opening/updating a PR: `git fetch origin main`, rebase, resolve
|
||||
conflicts preferring remote `main` unless a local change is essential,
|
||||
squash local commits into ONE comprehensive commit, push (force if needed),
|
||||
then create/update the PR and SHARE the PR URL with the owner.
|
||||
- The previous PR for this line of work is **PR #13** on the repo.
|
||||
- Deliverable ZIPs are uploaded with the file-wrapper tool and the URL is given
|
||||
to the owner. (Latest: v0.4.7.)
|
||||
|
||||
Everything needed (rules, state, pending task, build/test commands, file map) is in
|
||||
this file. The real work is safe in Git (PR #11, release v0.3.9) and in CHANGELOG.md.
|
||||
---
|
||||
|
||||
## 6. Version history (short — full detail in CHANGELOG.md)
|
||||
|
||||
- **v0.4.5** — fix invalidation loop; add "BAL Invalidate transaction" label on
|
||||
the automatic path (root cause: `broadcast_transaction` returns None → use
|
||||
`tx.txid()`); fix wizard text truncation.
|
||||
- **v0.4.6** — DUST one-line-per-heir report; heirs on one line; scrollable
|
||||
report area; wizard final check (`on_next_we` now calls
|
||||
`check_transactions`); wizard truncation fix (remove AlignLeft); anticipated-
|
||||
date notice styling.
|
||||
- **v0.4.7** — report area opens 500px tall (max 700); heirs reverted to ONE
|
||||
per line (green/bold); explicit `\n` line breaks in two wizard texts
|
||||
(after "(or backup)" and after "miner fees"); **ALL-DUST guard** in
|
||||
`prepare_lists` that blocks (clear RED message) only when EVERY heir is dust;
|
||||
3 new tests pinning the dust behaviour. 258 tests pass.
|
||||
|
||||
---
|
||||
|
||||
## 7. How to resume (checklist for the next AI)
|
||||
|
||||
1. Read this file, then `CHANGELOG.md` (last entries) and `.agent_memory_tasks.md`.
|
||||
2. Confirm the environment: `git status`, current branch, `bal/VERSION`.
|
||||
3. Run the full test suite (Section 3) — expect all green (258 as of v0.4.7).
|
||||
4. Talk to the owner in **Italian**, write everything else in **English**.
|
||||
5. For any change: present a PLAN, wait for "OK" (R4), then implement, test,
|
||||
build a ZIP, let the owner test, and only commit after explicit confirmation.
|
||||
6. Keep credit usage low: summarize, don't paste big code blocks; batch work.
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.3.9
|
||||
0.4.7
|
||||
|
||||
@@ -34,4 +34,4 @@ The plugin targets Electrum 4.7.2 (the last stable release exposing
|
||||
``json_db.register_dict``) and PyQt6.
|
||||
"""
|
||||
|
||||
__version__ = "0.3.9"
|
||||
__version__ = "0.4.7"
|
||||
|
||||
@@ -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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -62,7 +62,8 @@ from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox,
|
||||
|
||||
# --- Core (GUI-free) logic layer ---
|
||||
from ...core.plugin_base import BalPlugin, BalTimestamp
|
||||
from ...core.heirs import HEIR_DUST_AMOUNT, HEIR_REAL_AMOUNT, Heirs
|
||||
from ...core.heirs import (HEIR_DUST_AMOUNT, HEIR_REAL_AMOUNT,
|
||||
HeirAmountIsDustException, Heirs)
|
||||
from ...core.util import Util
|
||||
from ...core.will import (AmountException, HeirChangeException,
|
||||
HeirNotFoundException, NoHeirsException,
|
||||
|
||||
@@ -127,6 +127,21 @@ class BalWizardDialog(BalDialog):
|
||||
def on_next_we(self):
|
||||
close_window = BalBuildWillDialog(self.bal_window)
|
||||
close_window.build_will_task()
|
||||
|
||||
# Run the SAME final server check as the "Check" button (allegato15,
|
||||
# case B): previously the wizard only ran build_will_task() and skipped
|
||||
# the will-executor verification, so the user always had to press
|
||||
# "Check" manually after finishing the wizard. We now replicate exactly
|
||||
# the lists.py check() logic: after building, query every will that
|
||||
# needs a server check (Will.needs_server_check) and run
|
||||
# check_transactions(), which shows the "Checking transactions" dialog.
|
||||
will = {}
|
||||
for wid, w in self.bal_window.willitems.items():
|
||||
if Will.needs_server_check(w):
|
||||
will[wid] = w
|
||||
if will:
|
||||
self.bal_window.check_transactions(will)
|
||||
|
||||
self.close()
|
||||
# self.next_widget(BalWizardLocktimeAndFeeWidget(self.bal_window,self,self.on_next_locktimeandfee,self.on_next_wedonwload,self.on_next_wedonwload.on_cancel_heir))
|
||||
|
||||
@@ -321,10 +336,30 @@ class BalWizardWEDownloadWidget(BalWizardWidget):
|
||||
ping_on_done()
|
||||
|
||||
def ping_on_done():
|
||||
# Task #02 - "Automatically download and select"
|
||||
# (index 0): the green SELECTED tick must follow the
|
||||
# green ping dot, i.e. select ONLY servers that actually
|
||||
# answered the ping (status == 200) and DESELECT every
|
||||
# server that did not (timeout / error / never pinged).
|
||||
#
|
||||
# Why the explicit deselect matters: previously a server
|
||||
# that had been selected on an earlier download but is
|
||||
# now unreachable stayed selected, so the plugin kept
|
||||
# broadcasting to a dead server and got stuck. Forcing
|
||||
# selected=False for non-200 servers discards them at the
|
||||
# source. Re-running this (each "Automatically download"
|
||||
# action) re-evaluates every server: one that failed
|
||||
# before but now answers is selected again.
|
||||
#
|
||||
# We compare the status as a string ("200") to stay
|
||||
# consistent with the will-executor list view
|
||||
# (lists.py uses str(status) == "200"), and use .get()
|
||||
# so a missing "status" key never raises.
|
||||
if index < 1:
|
||||
for we in self.bal_window.willexecutors:
|
||||
if self.bal_window.willexecutors[we]["status"] == 200:
|
||||
self.bal_window.willexecutors[we]["selected"] = True
|
||||
wedict = self.bal_window.willexecutors[we]
|
||||
responded = str(wedict.get("status", "")) == "200"
|
||||
wedict["selected"] = responded
|
||||
Willexecutors.save(
|
||||
self.bal_window.bal_plugin, self.bal_window.willexecutors
|
||||
)
|
||||
@@ -502,10 +537,35 @@ class BalBuildWillDialog(BalDialog):
|
||||
self.bal_window = bal_window
|
||||
self.bal_plugin = bal_window.bal_plugin
|
||||
self.message_label = QLabel(_("Building Will:"))
|
||||
# Allow the long report text to wrap instead of forcing the dialog ever
|
||||
# wider, and let it grow downward inside the scroll area below.
|
||||
self.message_label.setWordWrap(True)
|
||||
self.message_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
|
||||
)
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.vbox.addWidget(self.message_label, 0)
|
||||
|
||||
# SCROLLABLE message area (allegato14): with many will-executors the
|
||||
# report can reach dozens of lines. Previously the dialog kept resizing
|
||||
# itself taller for every new line (see msg_update's resize), so with
|
||||
# e.g. 50 will-executors the window grew past the screen and the bottom
|
||||
# buttons (Close) became unreachable. We now put the message label in a
|
||||
# QScrollArea with a capped maximum height: once the text exceeds that
|
||||
# height a vertical scrollbar appears and the buttons stay visible.
|
||||
self.scroll_area = QScrollArea(self)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setWidget(self.message_label)
|
||||
# Open the report area already ~500px tall (owner request, allegato1:
|
||||
# the previous ~140px area was far too short). The dialog may still grow
|
||||
# up to 700px to fit a few more lines; beyond that the vertical
|
||||
# scrollbar takes over and the bottom buttons stay reachable.
|
||||
self.scroll_area.setMinimumHeight(500)
|
||||
self.scroll_area.setMaximumHeight(700)
|
||||
self.vbox.addWidget(self.scroll_area, 1)
|
||||
|
||||
# Kept for backward compatibility (referenced by old/commented code);
|
||||
# no longer used to lay out the messages.
|
||||
self.qwidget = QWidget(self)
|
||||
self.vbox.addWidget(self.qwidget, 1)
|
||||
self.labelsbox = QVBoxLayout(self.qwidget)
|
||||
self.setMinimumWidth(600)
|
||||
self.setMinimumHeight(100)
|
||||
@@ -519,6 +579,18 @@ class BalBuildWillDialog(BalDialog):
|
||||
# Manual next-steps hint (Sign / Broadcast) shown to the user after the
|
||||
# dialog finishes; None when nothing is left to do.
|
||||
self._next_steps_hint = None
|
||||
# Set to True by _sync_locktime_to_built_txs when the delivery date was
|
||||
# automatically anticipated during a rebuild. Used to explain to the
|
||||
# user WHY signing is being requested (otherwise the sign prompt appears
|
||||
# without any reason, as the owner reported).
|
||||
self._date_was_anticipated = False
|
||||
# Set to True right after we broadcast an automatic invalidation
|
||||
# transaction (the "postpone" path). On the very next phase-1 re-check
|
||||
# Electrum may not have seen the invalidation tx yet, so it would still
|
||||
# report a postpone and the wizard would re-prompt to invalidate over
|
||||
# and over (the reported loop). When this flag is set and a postpone is
|
||||
# STILL detected, we STOP with a clear message instead of re-prompting.
|
||||
self._invalidation_broadcast = False
|
||||
self.network = Network.get_instance()
|
||||
self._stopping = False
|
||||
self.thread = TaskThread(self)
|
||||
@@ -592,12 +664,22 @@ class BalBuildWillDialog(BalDialog):
|
||||
self.bal_window.check_will()
|
||||
self.msg_set_checking(self.msg_ok())
|
||||
except WillExpiredException:
|
||||
# UNIFY INVALIDATE PROCEDURE (+ task #03):
|
||||
#
|
||||
# The will is already expired (e.g. the CHECK button is pressed on an
|
||||
# expired will). Previously this returned (None, invalidate_tx),
|
||||
# which routed to the automatic invalidate path (password prompt +
|
||||
# auto-broadcast) that did NOT set the "BAL Invalidate transaction"
|
||||
# history label.
|
||||
#
|
||||
# We now return the SAME "invalidate_classic" signal used elsewhere,
|
||||
# so on_success_phase1 shows the warning popup and auto-opens
|
||||
# Electrum's classic transaction window (which sets the label). This
|
||||
# makes the CHECK button and the WIZARD behave identically and fixes
|
||||
# the missing-label bug (#03).
|
||||
_logger.debug("expired")
|
||||
self.msg_set_checking("Expired")
|
||||
fee_per_byte = self.bal_window.will_settings.get("baltx_fees", 1)
|
||||
return None, Will.invalidate_will(
|
||||
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
|
||||
)
|
||||
return "invalidate_classic", None
|
||||
except WillPostponedException as e:
|
||||
# An already signed/sent will is being postponed. Like an expired
|
||||
# will, the previously committed coins must be invalidated on-chain
|
||||
@@ -628,12 +710,30 @@ class BalBuildWillDialog(BalDialog):
|
||||
elif isinstance(e, TxFeesChangedException):
|
||||
message = _("Txfees are changed")
|
||||
elif isinstance(e, HeirNotFoundException):
|
||||
message = _("Heir not found")
|
||||
# Task #01b: the old text "Heir not found" was misleading.
|
||||
# In practice this branch is reached whenever the will is no
|
||||
# longer coherent and must be rebuilt - very often simply
|
||||
# because the delivery date was anticipated, NOT because an heir
|
||||
# is genuinely missing. We therefore show a clear, accurate
|
||||
# message that covers both the DATE and the HEIRS cases.
|
||||
message = _(
|
||||
"Found CHANGES to the DATE or the HEIRS,\n"
|
||||
"a NEW WILL must be prepared."
|
||||
)
|
||||
if message:
|
||||
_logger.debug(f"message: {message}")
|
||||
self.msg_set_checking(message)
|
||||
else:
|
||||
self.msg_set_checking("New")
|
||||
# Task #01b: the old fallback text "New" was unclear. When the
|
||||
# will is incomplete without a more specific reason, it still
|
||||
# means the will has to be rebuilt, so we use the same clear
|
||||
# message as the HeirNotFoundException branch above.
|
||||
self.msg_set_checking(
|
||||
_(
|
||||
"Found CHANGES to the DATE or the HEIRS,\n"
|
||||
"a NEW WILL must be prepared."
|
||||
)
|
||||
)
|
||||
|
||||
if have_to_build:
|
||||
self.msg_set_building()
|
||||
@@ -647,10 +747,7 @@ class BalBuildWillDialog(BalDialog):
|
||||
return False, None
|
||||
|
||||
self.bal_window.check_will()
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
# Label shown in Electrum's History tab for inheritance txs.
|
||||
self.bal_window.wallet.set_label(wid, "BAL Inheritance transaction")
|
||||
self.msg_set_building(self.msg_ok())
|
||||
self._build_success_report()
|
||||
except WillExecutorNotPresent:
|
||||
self.msg_set_status(
|
||||
_("Will-Executor excluded"), None, _("Skipped"), self.COLOR_ERROR
|
||||
@@ -683,19 +780,75 @@ class BalBuildWillDialog(BalDialog):
|
||||
self.msg_set_building(self.msg_warning(e))
|
||||
return "invalidate_classic", None
|
||||
|
||||
except NotCompleteWillException as e:
|
||||
# IMPORTANT (bugs E/F/K): build_will() above has just REBUILT the
|
||||
# whole will because the heirs (or the date) changed. The
|
||||
# post-build re-validation (check_will) then legitimately reports
|
||||
# that the new will differs from the previous one - e.g. it
|
||||
# raises HeirNotFoundException for a heir that is not yet covered
|
||||
# by an already-signed transaction. That is NOT an error: it is
|
||||
# exactly the signal that the freshly built transactions still
|
||||
# need to be SIGNED (and then broadcast).
|
||||
#
|
||||
# Previously this fell through to the generic "except Exception"
|
||||
# below, which (1) printed the heir name in RED and (2) returned
|
||||
# have_to_sign=False, so the rebuilt will was never signed
|
||||
# ("Nothing to do"). We now treat it as a successful rebuild:
|
||||
# show the green "Ok" + the heir list and FALL THROUGH to the
|
||||
# have_to_sign detection, so the new (status "New", not COMPLETE)
|
||||
# transactions are correctly detected and signed/broadcast.
|
||||
_logger.debug(f"will rebuilt, needs signing: {e}")
|
||||
self._build_success_report()
|
||||
|
||||
except HeirAmountIsDustException:
|
||||
# ALL-DUST CASE (owner request): every heir's share is below the
|
||||
# Bitcoin dust limit, so the inheritance would pay nobody. We do
|
||||
# NOT build, sign or check anything - we show a clear message in
|
||||
# red and stop, so no "empty" will ends up in the list. This is
|
||||
# raised by Heirs.prepare_lists only when ALL real heirs are
|
||||
# dust; a mix of dust + valid heirs never reaches here.
|
||||
self.msg_set_building(
|
||||
self.msg_error(
|
||||
_(
|
||||
"All heirs' shares are below the dust limit: "
|
||||
"the inheritance cannot be created. "
|
||||
"Increase the amounts or reduce the number of heirs."
|
||||
)
|
||||
)
|
||||
)
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
self.msg_set_building(self.msg_error(e))
|
||||
return False, None
|
||||
|
||||
# excluded_heirs = []
|
||||
# DUST report (one line PER HEIR, not per will-executor).
|
||||
#
|
||||
# WHY: the wallet balance can be so small that each heir's share falls
|
||||
# below Bitcoin's dust limit, so the inheritance is not feasible. The
|
||||
# "is DUST" condition depends ONLY on the heir's amount, NOT on which
|
||||
# will-executor transaction we are looking at. The previous code looped
|
||||
# over every valid will (one per will-executor) AND every heir, so with
|
||||
# e.g. 20 will-executors and 10 heirs it printed 20x10 = 200 identical
|
||||
# "is DUST ... Excluded from will <wid>" rows (owner report, allegato13).
|
||||
#
|
||||
# We now collect the dust heirs in a de-duplicated dict (heir id ->
|
||||
# dust amount) across all valid wills and print ONE row per heir,
|
||||
# without the will-executor reference. So N heirs => at most N rows,
|
||||
# regardless of how many will-executors exist.
|
||||
dust_heirs = {}
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
heirs = self.bal_window.willitems[wid].heirs
|
||||
for hid, heir in heirs.items():
|
||||
if "DUST" in str(heir[HEIR_REAL_AMOUNT]):
|
||||
# Keep the first dust amount seen for this heir; it is the
|
||||
# same share in every will-executor copy of the will.
|
||||
dust_heirs.setdefault(hid, heir[HEIR_DUST_AMOUNT])
|
||||
for hid, dust_amount in dust_heirs.items():
|
||||
self.msg_set_status(
|
||||
f"{hid},{heir[HEIR_DUST_AMOUNT]} is DUST",
|
||||
f"{_('Heir')} {hid}",
|
||||
None,
|
||||
f"Excluded from will {wid}",
|
||||
f"{dust_amount} is DUST - excluded (amount below dust limit)",
|
||||
self.COLOR_WARNING,
|
||||
)
|
||||
|
||||
@@ -706,6 +859,116 @@ class BalBuildWillDialog(BalDialog):
|
||||
break
|
||||
return have_to_sign, txs
|
||||
|
||||
def _build_success_report(self):
|
||||
"""Mark the build step as done and list every heir in green.
|
||||
|
||||
Called after a successful (re)build of the will. It:
|
||||
1. labels each inheritance transaction in Electrum's history;
|
||||
2. shows the green "Ok" result on the "Building your will" row;
|
||||
3. lists EVERY heir of the freshly built will, one per line, in green
|
||||
(the "Ok" colour), so the user can confirm at a glance who will
|
||||
inherit.
|
||||
|
||||
Previously a heir name could appear in RED (it was the text of a rebuild
|
||||
exception) and the heir list was only shown on the "all clean" path,
|
||||
which is why after changing/deleting an heir the user saw a single red
|
||||
heir name and no green list. This helper is now used both on the clean
|
||||
path and on the "will was rebuilt and needs signing" path, so the green
|
||||
heir list is always shown.
|
||||
|
||||
Internal will-executor pseudo-heirs (reserved ``w!ll3x3c"`` prefix) are
|
||||
skipped, exactly as the core coherence check does. A set keeps the list
|
||||
unique even when an heir appears in several transactions.
|
||||
"""
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
# Label shown in Electrum's History tab for inheritance txs.
|
||||
self.bal_window.wallet.set_label(wid, "BAL Inheritance transaction")
|
||||
# Keep the plugin's stored delivery date in sync with the (possibly
|
||||
# auto-anticipated) transactions, otherwise the next Check would wrongly
|
||||
# ask to invalidate. See _sync_locktime_to_built_txs for the full why.
|
||||
self._sync_locktime_to_built_txs()
|
||||
self.msg_set_building(self.msg_ok())
|
||||
# List EACH heir on its OWN line, green + bold (owner request: revert the
|
||||
# one-line "Heirs: a, b, c" form of v0.4.6). Heir names can be long, and
|
||||
# now that the report area scrolls (allegato1) there is no need to cram
|
||||
# them onto a single line. We de-duplicate (an heir can appear in several
|
||||
# will-executor transactions) and skip the internal will-executor
|
||||
# pseudo-heirs (reserved ``w!ll3x3c"`` prefix), exactly as before.
|
||||
shown_heirs = set()
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
for hname in self.bal_window.willitems[wid].heirs:
|
||||
if str(hname)[:9] == 'w!ll3x3c"':
|
||||
continue
|
||||
if hname in shown_heirs:
|
||||
continue
|
||||
shown_heirs.add(hname)
|
||||
self.msg_set_status(_("Heir"), None, str(hname), self.COLOR_OK)
|
||||
|
||||
def _sync_locktime_to_built_txs(self):
|
||||
"""Align the plugin's stored delivery date with the built transactions.
|
||||
|
||||
WHY this is needed (bug reported by the owner):
|
||||
When the will is rebuilt while it still spends the same coins as a
|
||||
previous one (e.g. after deleting an heir WITHOUT changing the date),
|
||||
the core engine AUTOMATICALLY anticipates the transaction locktime by
|
||||
one day (see Will.check_anticipate / Util.anticipate_locktime). This is
|
||||
correct and required so the new transaction can be mined BEFORE the old
|
||||
one it replaces.
|
||||
|
||||
However the plugin's own stored delivery date
|
||||
(WILL_SETTINGS["locktime"]) was NOT updated and stayed at the original
|
||||
date. On the next Check the plugin compared the stored date (original)
|
||||
with the transaction locktime (original minus one day) and, since
|
||||
stored > tx, mistook the automatic anticipation for a user POSTPONE,
|
||||
wrongly asking to invalidate the will.
|
||||
|
||||
Fix: after a (re)build, set the stored delivery date to the MINIMUM
|
||||
locktime among the valid built transactions. We only ever move the date
|
||||
EARLIER (anticipation): if the minimum is not strictly below the current
|
||||
stored date we leave it untouched, so a genuine user-chosen postpone is
|
||||
never silently overwritten. The owner confirmed that, when several
|
||||
transactions carry different locktimes, taking the minimum is the
|
||||
desired behaviour, and that the date shown in the panel/wizard must
|
||||
reflect this anticipated date (so the calendar .ics also uses it).
|
||||
|
||||
We route the update through BalWindow.update_setting_widgets, which is
|
||||
the single place that (1) stores the value in WILL_SETTINGS, (2)
|
||||
persists it to Electrum's database and (3) refreshes the date widgets in
|
||||
every panel/wizard, so the visible date and the .ics export stay
|
||||
consistent.
|
||||
"""
|
||||
# Minimum locktime across the valid (just built) inheritance txs.
|
||||
# Will.get_min_locktime returns None when there is no valid tx.
|
||||
min_locktime = Will.get_min_locktime(self.bal_window.willitems, None)
|
||||
if min_locktime is None:
|
||||
return
|
||||
min_locktime = int(min_locktime)
|
||||
# Current stored delivery date, as a comparable UNIX timestamp.
|
||||
try:
|
||||
current = int(
|
||||
Util.parse_locktime_string(
|
||||
self.bal_window.will_settings["locktime"]
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
# If the stored value cannot be parsed, fall back to syncing.
|
||||
current = None
|
||||
# Only anticipate (move the date EARLIER); never overwrite a postpone.
|
||||
if current is not None and min_locktime >= current:
|
||||
return
|
||||
_logger.debug(
|
||||
f"sync delivery date to anticipated tx locktime: "
|
||||
f"{current} -> {min_locktime}"
|
||||
)
|
||||
# Remember that we anticipated the date, so the later sign prompt can
|
||||
# explain WHY signing is needed (see on_success_phase1).
|
||||
self._date_was_anticipated = True
|
||||
# update_setting_widgets stores the value, persists it and refreshes the
|
||||
# date widgets in all panels/wizard (so the .ics calendar uses it too).
|
||||
self.bal_window.update_setting_widgets(
|
||||
min_locktime, "locktime", update_all=True
|
||||
)
|
||||
|
||||
def on_accept(self):
|
||||
self.bal_window.update_all()
|
||||
pass
|
||||
@@ -733,12 +996,34 @@ class BalBuildWillDialog(BalDialog):
|
||||
try:
|
||||
tx.add_info_from_wallet(self.bal_window.wallet)
|
||||
self.network.run_from_another_thread(tx.add_info_from_network(self.network))
|
||||
txid = self.network.run_from_another_thread(
|
||||
|
||||
# IMPORTANT (task #21 fix): get the txid from the transaction
|
||||
# object, NOT from broadcast_transaction()'s return value.
|
||||
# Network.broadcast_transaction is declared "-> None" and ALWAYS
|
||||
# returns None (it only raises on failure). The previous code stored
|
||||
# that None into `txid` and put set_label() in the `else: # txid`
|
||||
# branch, which was therefore NEVER reached - that is why the
|
||||
# "BAL Invalidate transaction" history label kept missing on this
|
||||
# automatic ("postpone") path. The transaction is already signed and
|
||||
# complete here, so tx.txid() is the correct, stable id - exactly
|
||||
# what the working Tools -> Invalidate path uses
|
||||
# (BalWalletWindow.invalidate_will -> result.txid()).
|
||||
txid = tx.txid()
|
||||
|
||||
# Set the history label BEFORE broadcasting. set_label only writes to
|
||||
# the local wallet metadata (no network needed), so doing it first
|
||||
# guarantees the label exists the moment the transaction shows up in
|
||||
# the History tab, regardless of how fast the broadcast/notification
|
||||
# arrives.
|
||||
if txid:
|
||||
self.bal_window.wallet.set_label(txid, "BAL Invalidate transaction")
|
||||
else:
|
||||
_logger.debug(f"invalidate tx has no txid: {tx}")
|
||||
|
||||
self.network.run_from_another_thread(
|
||||
self.network.broadcast_transaction(tx, timeout=120), timeout=120
|
||||
)
|
||||
self.msg_set_invalidating(self.msg_ok())
|
||||
if not txid:
|
||||
_logger.debug(f"should not be none txid: {txid}")
|
||||
|
||||
except TxBroadcastError as e:
|
||||
_logger.error(f"fail to broadcast transaction:{e}")
|
||||
@@ -909,7 +1194,19 @@ class BalBuildWillDialog(BalDialog):
|
||||
if tx:
|
||||
if tx.is_complete():
|
||||
self.loop_broadcast_invalidating(tx)
|
||||
self.wait(5)
|
||||
# Wait 10 seconds (was 5) AFTER broadcasting the
|
||||
# invalidation so Electrum's wallet/network has time to see
|
||||
# the new transaction before we re-run phase 1. Without this
|
||||
# pause the immediate re-check still detected the old
|
||||
# (not-yet-invalidated) will and re-prompted to invalidate,
|
||||
# producing the reported loop. This runs in the worker
|
||||
# thread, so the GUI is not frozen by the sleep.
|
||||
self.wait(10)
|
||||
# Remember that we just broadcast an invalidation. If the
|
||||
# next phase-1 re-check STILL reports a postpone (because
|
||||
# Electrum has not registered the tx yet), we stop with a
|
||||
# clear message instead of looping (see on_success_phase1).
|
||||
self._invalidation_broadcast = True
|
||||
else:
|
||||
raise Exception("tx not complete")
|
||||
else:
|
||||
@@ -946,46 +1243,74 @@ class BalBuildWillDialog(BalDialog):
|
||||
# is safe. We then stop and close the wizard.
|
||||
if self.have_to_sign == "invalidate_classic":
|
||||
self.thread.stop()
|
||||
# Design decision (window stacking + user clarity):
|
||||
# UNIFY INVALIDATE PROCEDURE (+ task #03):
|
||||
#
|
||||
# When an heir is added to an already-expired will, the rebuilt will
|
||||
# is itself expired and the old will must be invalidated on-chain
|
||||
# before the new one can be used. We previously tried to open the
|
||||
# invalidation transaction window AUTOMATICALLY from here, but doing
|
||||
# so from within the closing wizard proved fragile: depending on the
|
||||
# OS window manager and Qt's event ordering, the transaction window
|
||||
# kept ending up BEHIND the main wallet window (it lost focus when
|
||||
# the wizard closed). Neither closing-before-opening nor a deferred
|
||||
# QTimer close() fixed it reliably on every machine.
|
||||
# before the new one can be used.
|
||||
#
|
||||
# The robust solution is to NOT auto-open any window here. Instead we
|
||||
# close the wizard and show a clear instruction telling the user to
|
||||
# run "Tools -> Invalidate" themselves. That menu path is already
|
||||
# known to work perfectly (its transaction window always stays in
|
||||
# front, because no other window is closing at the same time), and
|
||||
# it also makes the user consciously aware that they are performing a
|
||||
# deliberate, important action (invalidating their old will).
|
||||
# We make the CHECK button and the WIZARD behave IDENTICALLY:
|
||||
# 1. show a WARNING popup (no "Tools -> Invalidate" wording);
|
||||
# 2. AUTOMATICALLY open Electrum's classic transaction dialog via
|
||||
# BalWalletWindow.invalidate_will() (the same code used by the
|
||||
# Tools -> Invalidate menu). That path already sets the
|
||||
# "BAL Invalidate transaction" history label - which fixes
|
||||
# task #03 (the label was previously missing on the automatic
|
||||
# path).
|
||||
#
|
||||
# Close the wizard first so the instruction popup is the only window
|
||||
# left, then show the guidance message.
|
||||
# Window-stacking note (history): opening the transaction window
|
||||
# straight from within the closing wizard used to leave it BEHIND
|
||||
# the main window on some window managers. The robust fix is to
|
||||
# close the wizard FIRST and defer the call with QTimer.singleShot
|
||||
# so it runs on the next event-loop iteration, when the wizard is
|
||||
# already gone and the transaction window becomes the front-most,
|
||||
# focused window.
|
||||
self.close()
|
||||
self.bal_window.show_message(
|
||||
_(
|
||||
"Your will has expired and must be invalidated before it "
|
||||
"can be rebuilt.\n\n"
|
||||
"Please use the top-right menu Tools -> Invalidate to "
|
||||
"invalidate your old will: a transaction window will open "
|
||||
"where you can sign and broadcast the invalidation.\n\n"
|
||||
"can be rebuilt.\n"
|
||||
"A transaction window will now open:\n"
|
||||
"please SIGN and then BROADCAST it to invalidate your old "
|
||||
"will.\n"
|
||||
"After the invalidation is confirmed, press the Check "
|
||||
"button near Tools, to finish the will."
|
||||
"button to finish the will."
|
||||
)
|
||||
)
|
||||
# Deferred so the wizard is fully closed before the classic
|
||||
# invalidate window opens (keeps it in front, fixes the old
|
||||
# "window behind" problem).
|
||||
QTimer.singleShot(0, self.bal_window.invalidate_will)
|
||||
return
|
||||
|
||||
_logger.debug("have to sign {}".format(self.have_to_sign))
|
||||
password = None
|
||||
if self.have_to_sign is None:
|
||||
_logger.debug("have to invalidate")
|
||||
|
||||
# LOOP GUARD (task #21): if we already broadcast an invalidation on
|
||||
# the previous pass and phase 1 STILL reports a postpone, Electrum
|
||||
# has simply not seen the invalidation transaction yet. Re-prompting
|
||||
# to invalidate here is exactly what produced the reported endless
|
||||
# loop ("Invalidate your old will" reappearing right after signing).
|
||||
# Instead of re-prompting, STOP cleanly with a clear message and
|
||||
# tell the user to retry the Check once the invalidation confirms.
|
||||
if self._invalidation_broadcast:
|
||||
self.thread.stop()
|
||||
self.msg_set_invalidating(self.msg_ok())
|
||||
self.bal_window.show_message(
|
||||
_(
|
||||
"Your old will has been invalidated and the "
|
||||
"transaction was broadcast.\n"
|
||||
"Electrum may need a little time to register it.\n"
|
||||
"Please wait until the invalidation transaction is "
|
||||
"confirmed, then press the Check button again to "
|
||||
"finish updating your will."
|
||||
)
|
||||
)
|
||||
self._add_close_button()
|
||||
return
|
||||
|
||||
self.msg_set_invalidating()
|
||||
# need to sign invalidate and restart phase 1
|
||||
|
||||
@@ -1017,6 +1342,26 @@ class BalBuildWillDialog(BalDialog):
|
||||
return
|
||||
|
||||
elif self.have_to_sign:
|
||||
# If the will was rebuilt with an automatically anticipated delivery
|
||||
# date, explain WHY we are now asking to sign: otherwise the sign
|
||||
# prompt appears with no reason (owner feedback). The note is shown
|
||||
# on the "Building your will" row (orange warning) before the modal
|
||||
# password prompt opens, so the user can read it.
|
||||
if self._date_was_anticipated:
|
||||
# Render this notice in BLACK BOLD (not the yellow warning
|
||||
# colour) and split it onto TWO lines after "...previous one."
|
||||
# for readability (allegato17). The "\n" is converted to a line
|
||||
# break by msg_update (it replaces "\n" with "<br>").
|
||||
self.msg_set_building(
|
||||
"<b>{}</b>".format(
|
||||
_(
|
||||
"The delivery date was automatically moved one day "
|
||||
"earlier so the updated will can correctly replace "
|
||||
"the previous one.\n"
|
||||
"Please sign (and broadcast) to confirm the change."
|
||||
)
|
||||
)
|
||||
)
|
||||
password = self.bal_window.get_wallet_password(
|
||||
_("Sign your will"), parent=self
|
||||
)
|
||||
@@ -1307,8 +1652,18 @@ class BalBuildWillDialog(BalDialog):
|
||||
full_text = "<br><br>".join(self.labels).replace("\n", "<br>")
|
||||
self.message_label.setText(full_text)
|
||||
self.message_label.adjustSize()
|
||||
# self.setMinimumHeight(len(self.labels)*40)
|
||||
# Auto-scroll the report to the BOTTOM so the newest line is always
|
||||
# visible (allegato14). The QScrollArea caps the height, so instead of
|
||||
# resizing the whole dialog taller we move its vertical scrollbar to the
|
||||
# maximum. ensureWidgetVisible is deferred via the scrollbar range so it
|
||||
# reflects the just-added text.
|
||||
scrollbar = self.scroll_area.verticalScrollBar()
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
# Let the dialog grow only up to the scroll area's capped height; beyond
|
||||
# that the scrollbar handles overflow and the buttons stay reachable.
|
||||
self.resize(self.sizeHint())
|
||||
# Re-assert the bottom position after the resize/relayout settled.
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
|
||||
def get_text(self):
|
||||
return self.message_label.text()
|
||||
|
||||
@@ -491,8 +491,10 @@ class PreviewList(MyTreeView, MessageBoxMixin):
|
||||
self.bal_window.bal_plugin.read_file("icons/reload.png")
|
||||
)
|
||||
)
|
||||
# Tooltip so the icon is self-explanatory when hovered.
|
||||
refresh.setToolTip(_("Check"))
|
||||
# Tooltip so the icon is self-explanatory when hovered. "Check
|
||||
# Inheritance" makes it clear the button re-checks the inheritance/will
|
||||
# state (not a generic refresh).
|
||||
refresh.setToolTip(_("Check Inheritance"))
|
||||
refresh.clicked.connect(self.check)
|
||||
|
||||
widget = QWidget(self)
|
||||
|
||||
@@ -417,6 +417,32 @@ class Plugin(BalPlugin):
|
||||
# be saved on a USB stick and a copy given to the heirs).
|
||||
heir_no_willexecutor = BalCheckBox(self.NO_WILLEXECUTOR)
|
||||
|
||||
# USER TYPE selector (SIMPLE / ADVANCED, global). A two-choice combo
|
||||
# (not a free-text field) bound to the USER_TYPE config:
|
||||
# index 0 -> "BASIC" -> stored value "basic" (DEFAULT)
|
||||
# index 1 -> "ADVANCED" -> stored value "advanced"
|
||||
#
|
||||
# BASIC hides the advanced controls (Raw/Date selector and the
|
||||
# "Check Alive" field) and disables the check-alive postpone behaviour.
|
||||
# Changing it refreshes the open windows so the controls appear/disappear
|
||||
# immediately. It is kept in a named variable so the "Reset" button can
|
||||
# restore its displayed value after a reset.
|
||||
user_type_combo = QComboBox()
|
||||
user_type_combo.addItems([_("BASIC"), _("ADVANCED")])
|
||||
# Map the stored string to the combo index (anything but "advanced"
|
||||
# falls back to BASIC, matching BalPlugin.is_basic_mode()).
|
||||
user_type_combo.setCurrentIndex(
|
||||
0 if str(self.USER_TYPE.get()).lower() != "advanced" else 1
|
||||
)
|
||||
|
||||
def on_user_type_change(idx):
|
||||
# Persist "basic"/"advanced" and refresh the open windows so the
|
||||
# advanced controls appear/disappear right away.
|
||||
self.USER_TYPE.set("advanced" if idx == 1 else "basic")
|
||||
self.update_all()
|
||||
|
||||
user_type_combo.currentIndexChanged.connect(on_user_type_change)
|
||||
|
||||
# Editable line/text widgets are created once and kept in named
|
||||
# variables so the "Reset" button (Group C / C4b) can refresh the
|
||||
# displayed values after resetting the underlying config.
|
||||
@@ -447,6 +473,21 @@ class Plugin(BalPlugin):
|
||||
# Reset/support button row (bottom). Assigning ``QGridLayout(d)`` would
|
||||
# have made the grid the dialog's only layout, leaving no room for them.
|
||||
grid = QGridLayout()
|
||||
add_widget(
|
||||
grid,
|
||||
"User Type",
|
||||
user_type_combo,
|
||||
0,
|
||||
(
|
||||
"Choose how much detail the plugin shows.\n\n"
|
||||
"BASIC (default): a simpler interface. It hides the advanced "
|
||||
"controls (the Raw/Date selector and the 'Check Alive' field) "
|
||||
"and turns off the 'check alive' postpone behaviour, so you "
|
||||
"only set the delivery date.\n\n"
|
||||
"ADVANCED: shows every control, including the 'Check Alive' "
|
||||
"field and the Raw/Date selector."
|
||||
),
|
||||
)
|
||||
add_widget(
|
||||
grid,
|
||||
"Hide Replaced",
|
||||
@@ -475,7 +516,7 @@ class Plugin(BalPlugin):
|
||||
)
|
||||
add_widget(
|
||||
grid,
|
||||
"Editable dates",
|
||||
"Panel editable Date and Fee",
|
||||
heir_editable_dates,
|
||||
4,
|
||||
(
|
||||
@@ -485,14 +526,38 @@ class Plugin(BalPlugin):
|
||||
"When disabled, those dates are display-only outside the wizard."
|
||||
),
|
||||
)
|
||||
# "Add transaction without will-executor" setting (formerly labelled
|
||||
# "No will-executor TX"). When ON the plugin ALSO builds the backup
|
||||
# inheritance transaction that does NOT require a will-executor (the
|
||||
# "celeste"/light-blue one shown in the will list): it can be saved on a
|
||||
# USB stick and a copy handed to the heirs. When OFF only the
|
||||
# transactions destined to the selected will-executors are built.
|
||||
#
|
||||
# Placed here (row 5, right below "Panel editable Date and Fee" and above
|
||||
# "Number of reminders") at the user's request so related options sit
|
||||
# together. The remaining grid rows below were renumbered accordingly.
|
||||
add_widget(
|
||||
grid,
|
||||
"Add transaction without willexecutor",
|
||||
heir_no_willexecutor,
|
||||
5,
|
||||
(
|
||||
"Create a will that does not require a Will-executor; it can be "
|
||||
"saved, for example, on a USB stick, and a copy can be given to "
|
||||
"the heirs."
|
||||
),
|
||||
)
|
||||
add_widget(
|
||||
grid,
|
||||
"Number of reminders",
|
||||
heir_num_reminders,
|
||||
5,
|
||||
6,
|
||||
(
|
||||
"How many reminder alarms the exported calendar (.ics) event "
|
||||
"contains.\n"
|
||||
"contains.\n\n"
|
||||
"BASIC MODE:\n"
|
||||
"Calendar reminder 30, 10 and 1 days before.\n\n\n"
|
||||
"ADVANCED MODE:\n"
|
||||
"The reminders are spread across the check-alive period and "
|
||||
"always fall before the delivery deadline.\n"
|
||||
"If the period is shorter than the requested number, at most "
|
||||
@@ -503,7 +568,7 @@ class Plugin(BalPlugin):
|
||||
grid,
|
||||
"Event summary",
|
||||
edit_event_summary,
|
||||
6,
|
||||
7,
|
||||
(
|
||||
"Default message to be used in event summary\n"
|
||||
"Variables:\n"
|
||||
@@ -516,7 +581,7 @@ class Plugin(BalPlugin):
|
||||
grid,
|
||||
"Event description",
|
||||
edit_event_description,
|
||||
7,
|
||||
8,
|
||||
(
|
||||
"Default message to be used in event description\n"
|
||||
"Variables:\n"
|
||||
@@ -527,21 +592,6 @@ class Plugin(BalPlugin):
|
||||
)
|
||||
#add_widget(grid, "Bal Mode", bal_mode, 4, "choose bal mode")
|
||||
|
||||
# "No will-executor TX" setting. Mirrors the checkbox shown in the
|
||||
# wizard's will-executor download window (both bound to NO_WILLEXECUTOR),
|
||||
# so it can also be toggled from the plugin settings. Default ON.
|
||||
add_widget(
|
||||
grid,
|
||||
"No will-executor TX",
|
||||
heir_no_willexecutor,
|
||||
8,
|
||||
(
|
||||
"Create a will that does not require a Will-executor; it can be "
|
||||
"saved, for example, on a USB stick, and a copy can be given to "
|
||||
"the heirs."
|
||||
),
|
||||
)
|
||||
|
||||
# add_widget(
|
||||
# grid,
|
||||
# "Ping Willexecutors",
|
||||
@@ -584,6 +634,7 @@ class Plugin(BalPlugin):
|
||||
# Map each config object to the widget that displays it, so we can
|
||||
# both reset the stored value and update what the user sees.
|
||||
resets = [
|
||||
(self.USER_TYPE, user_type_combo, "user_type"),
|
||||
(self.HIDE_REPLACED, heir_hide_replaced, "check"),
|
||||
(self.HIDE_INVALIDATED, heir_hide_invalidated, "check"),
|
||||
(self.AUTO_SIGN, heir_auto_sign, "check"),
|
||||
@@ -607,6 +658,11 @@ class Plugin(BalPlugin):
|
||||
widget.setText(cfg.default)
|
||||
elif kind == "text":
|
||||
widget.setPlainText(cfg.default)
|
||||
elif kind == "user_type":
|
||||
# Default is "basic" -> combo index 0; "advanced" -> index 1.
|
||||
widget.setCurrentIndex(
|
||||
1 if str(cfg.default).lower() == "advanced" else 0
|
||||
)
|
||||
# Refresh the open BAL windows so any dependent view (e.g. the
|
||||
# editable-dates state is not in this list, but hide filters are)
|
||||
# reflects the reset values.
|
||||
@@ -638,6 +694,9 @@ class Plugin(BalPlugin):
|
||||
# Outer layout: warning (top) -> settings grid -> bottom button row.
|
||||
outer = QVBoxLayout(d)
|
||||
outer.addWidget(lbl_warning)
|
||||
# Blank vertical gap below the red warning so it is not glued to the
|
||||
# first setting row ("User Type"); requested by the user for readability.
|
||||
outer.addSpacing(12)
|
||||
outer.addLayout(grid)
|
||||
outer.addLayout(bottom_row)
|
||||
|
||||
|
||||
@@ -75,6 +75,34 @@ def compute_reminder_offsets(days, count):
|
||||
return sorted(offsets, reverse=True)
|
||||
|
||||
|
||||
# Fixed reminder offsets (in days BEFORE the delivery date) used in BASIC mode.
|
||||
# In BASIC the check-alive parameter is hidden/unmanaged, so reminders cannot be
|
||||
# spread over it; instead the owner asked for three fixed reminders: 30, 10 and
|
||||
# 1 day before the inheritance delivery date.
|
||||
BASIC_REMINDER_OFFSETS = (30, 10, 1)
|
||||
|
||||
|
||||
def basic_reminder_offsets(days_to_deadline):
|
||||
"""Return the BASIC-mode reminder offsets that still fall in the future.
|
||||
|
||||
BASIC mode uses the fixed offsets in ``BASIC_REMINDER_OFFSETS`` (30, 10 and
|
||||
1 day before the delivery date). Any offset that would land in the past is
|
||||
dropped, because a reminder before "today" is useless: if the delivery date
|
||||
is only ``days_to_deadline`` days away, only the offsets that are ``<=
|
||||
days_to_deadline`` are kept.
|
||||
|
||||
Args:
|
||||
days_to_deadline: whole days from now until the delivery date.
|
||||
|
||||
Returns:
|
||||
A list of integer day-offsets (each ``>= 1``), sorted as in
|
||||
``BASIC_REMINDER_OFFSETS`` (descending: earliest reminder first). Empty
|
||||
when the delivery date is less than one day away.
|
||||
"""
|
||||
horizon = max(int(days_to_deadline), 0)
|
||||
return [off for off in BASIC_REMINDER_OFFSETS if 1 <= off <= horizon]
|
||||
|
||||
|
||||
class ClickableLabel(QLabel):
|
||||
doubleClicked = pyqtSignal()
|
||||
|
||||
@@ -256,6 +284,13 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
|
||||
1: self.locktime_date_e,
|
||||
}
|
||||
self.combo.addItems(options)
|
||||
# SIMPLE / ADVANCED (task: hide the Raw/Date selector in BASIC mode).
|
||||
#
|
||||
# In BASIC mode the user must not see or use the Raw/Date selector:
|
||||
# every date field is forced to the calendar ("Date") editor and the
|
||||
# combo is hidden. We keep a flag so the rest of __init__ can force the
|
||||
# Date editor regardless of the stored value's format.
|
||||
self._basic_mode = self.bal_window.bal_plugin.is_basic_mode()
|
||||
default_index = 0
|
||||
if not default_locktime:
|
||||
default_locktime = self.bal_window.bal_plugin.WILL_SETTINGS.get()[self.base_field]
|
||||
@@ -264,6 +299,10 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
|
||||
default_index = 1
|
||||
except Exception:
|
||||
default_index = 0
|
||||
# Force the calendar ("Date") editor in BASIC mode so the user always
|
||||
# picks a date and never sees the RAW input ("30d"/"1y" style).
|
||||
if self._basic_mode:
|
||||
default_index = 1
|
||||
#hbox.addWidget(QLabel(self.label_text))
|
||||
help_button=HelpButton(self.help_text)
|
||||
help_button.setText(self.label_text)
|
||||
@@ -290,6 +329,11 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
|
||||
self.set_value(default_locktime)
|
||||
self.current_value=default_locktime
|
||||
hbox.addWidget(self.combo)
|
||||
# In BASIC mode hide the Raw/Date selector entirely: the field stays
|
||||
# locked on the calendar editor chosen above, so the user only ever
|
||||
# picks a Date and cannot switch to RAW.
|
||||
if self._basic_mode:
|
||||
self.combo.setVisible(False)
|
||||
for w in self.editors:
|
||||
hbox.addWidget(w)
|
||||
|
||||
@@ -594,13 +638,19 @@ class LockTimeWidget(BalTimeEditWidget):
|
||||
"<b>DELIVERY TIME</b><br><br>"
|
||||
"Set Locktime for transactions.<br>"
|
||||
"Any time is needed transaction will be anticipated by 1day<br><br>"
|
||||
# The Raw locktime syntax below is only available in ADVANCED mode
|
||||
# (in BASIC mode the Raw/Date selector is hidden and only the Date
|
||||
# picker is shown), so we say so explicitly to avoid confusing users.
|
||||
"(ONLY IN ADVANCED MODE)<br>"
|
||||
"if you choose Raw, you can insert various options based on suffix:<br>"
|
||||
" - d: number of days after current day(ex: 1d means tomorrow)<br>"
|
||||
" - y: number of years after currrent day(ex: 1y means one year from today)<br>"
|
||||
)
|
||||
label_text = "🚛"
|
||||
#label_text = "Locktime"
|
||||
tooltip_text = "Delivery time"
|
||||
# Hover tooltip for the delivery-time icon; mirrors the style of the fee
|
||||
# icon tooltip ("..., click for more information") so the two are consistent.
|
||||
tooltip_text = "Delivery Time, click for more information"
|
||||
base_field = "locktime"
|
||||
|
||||
def __init__(self, bal_window, parent, init_value=None):
|
||||
@@ -641,6 +691,15 @@ class WillSettingsWidget(QWidget):
|
||||
self.widgets["threshold"] = ThresholdTimeWidget(bal_window, self)
|
||||
self.widgets["locktime"].valueEdited.connect(self.on_locktime_change)
|
||||
self.widgets["threshold"].valueEdited.connect(self.on_locktime_change)
|
||||
# SIMPLE / ADVANCED: in BASIC mode hide the whole "Check Alive"
|
||||
# (threshold) row, including its leading icon. The widget is still
|
||||
# created and kept in self.widgets so the rest of the code (and the
|
||||
# saved settings) keep working; it is only hidden from view. The
|
||||
# Delivery time (locktime) row stays visible. We hide it after creation
|
||||
# so both the horizontal (toolbar/Heirs) and vertical (wizard) layouts
|
||||
# below add an already-hidden widget.
|
||||
if bal_window.bal_plugin.is_basic_mode():
|
||||
self.widgets["threshold"].setVisible(False)
|
||||
# self.widgets['baltx_fees'].valueChange.connect(self.bal_window.update_setting_widgets)
|
||||
self.on_locktime_change()
|
||||
self.widgets["baltx_fees"] = BalTxFeesWidget(bal_window, self)
|
||||
@@ -656,53 +715,135 @@ class WillSettingsWidget(QWidget):
|
||||
box.addWidget(self.calendar_button)
|
||||
box.addWidget(self.widgets["baltx_fees"])
|
||||
else:
|
||||
# Vertical layout (the "Build your will" wizard): make every row the
|
||||
# same width and left aligned so they all fit in one tidy block,
|
||||
# instead of letting the calendar button and the fee field stretch to
|
||||
# the dialog's right edge (which made them far wider than the date
|
||||
# rows above).
|
||||
#
|
||||
# IMPORTANT: the leading icons keep their ORIGINAL size. The icons
|
||||
# are HelpButtons, which already pin themselves to a fixed width
|
||||
# (2.2 * char_width_in_lineedit()); we must NOT widen them, otherwise
|
||||
# they look oversized compared with the original toolbar layout. We
|
||||
# only need to (1) align the calendar row's left edge with the icons'
|
||||
# original width and (2) cap every row to the date-row width.
|
||||
# ----------------------------------------------------------------- #
|
||||
# Vertical layout (the "Build your will" wizard) - Layout H. #
|
||||
# #
|
||||
# User requirements (allegato4): #
|
||||
# * the leading ICONS (delivery time, calendar, fee and - in #
|
||||
# ADVANCED mode - check-alive) must be aligned one under the #
|
||||
# other on the left; #
|
||||
# * the editable FIELDS must all start from the SAME x position, #
|
||||
# just to the right of their icon; #
|
||||
# * the fee field must be WIDER (its up/down arrows were covering #
|
||||
# the digits) - not a tiny box. #
|
||||
# #
|
||||
# DESIGN NOTE - why we do NOT pull the icon/field out of each #
|
||||
# composite into a shared grid: the LockTime / Check-Alive composites #
|
||||
# hold TWO editors (Raw and Date) plus a Raw/Date combo that the user #
|
||||
# can switch at runtime in ADVANCED mode; ``self.editor`` is swapped #
|
||||
# live (see on_current_index_changed). Reparenting only the currently #
|
||||
# active editor would orphan the other editor and the combo and break #
|
||||
# ADVANCED mode. So we keep every composite INTACT and instead align #
|
||||
# them by: #
|
||||
# 1. forcing every leading icon (prefix_widget) to the SAME fixed #
|
||||
# width, so each composite's field starts at the same x; and #
|
||||
# 2. stacking the whole composites left-aligned in the VBox. #
|
||||
# This is robust to the Raw/Date switching and keeps all internal #
|
||||
# logic working untouched. #
|
||||
# ----------------------------------------------------------------- #
|
||||
locktime_w = self.widgets["locktime"]
|
||||
threshold_w = self.widgets["threshold"]
|
||||
fees_w = self.widgets["baltx_fees"]
|
||||
|
||||
# Original icon width (HelpButton's own fixed width); used only to
|
||||
# offset the calendar button so its field starts under the others.
|
||||
icon_w = locktime_w.prefix_widget.sizeHint().width()
|
||||
|
||||
# Common row width = natural width of the date rows (the reference).
|
||||
row_w = max(
|
||||
locktime_w.sizeHint().width(),
|
||||
threshold_w.sizeHint().width(),
|
||||
# Common icon width = the widest leading icon (incl. the calendar
|
||||
# button). Forcing every icon to this width makes the icons line up
|
||||
# one under the other and, because each field sits immediately to the
|
||||
# right of its icon, makes every field start at the same x too.
|
||||
icon_w = max(
|
||||
locktime_w.prefix_widget.sizeHint().width(),
|
||||
threshold_w.prefix_widget.sizeHint().width(),
|
||||
fees_w.prefix_widget.sizeHint().width(),
|
||||
self.calendar_button.sizeHint().width(),
|
||||
)
|
||||
for w in (locktime_w, threshold_w, fees_w):
|
||||
w.setFixedWidth(row_w)
|
||||
for icon in (
|
||||
locktime_w.prefix_widget,
|
||||
threshold_w.prefix_widget,
|
||||
fees_w.prefix_widget,
|
||||
self.calendar_button,
|
||||
):
|
||||
icon.setFixedWidth(icon_w)
|
||||
|
||||
# The calendar row has no prefix icon: wrap it so it starts with an
|
||||
# empty spacer of the icon width (calendar field aligned with the
|
||||
# date/fee fields) and cap it to the same total width as the rows
|
||||
# above, so it no longer stretches to the dialog's right edge.
|
||||
calendar_row = QWidget(self)
|
||||
calendar_box = QHBoxLayout(calendar_row)
|
||||
calendar_box.setContentsMargins(0, 0, 0, 0)
|
||||
calendar_box.setSpacing(0)
|
||||
calendar_spacer = QWidget()
|
||||
calendar_spacer.setFixedWidth(icon_w)
|
||||
calendar_box.addWidget(calendar_spacer)
|
||||
calendar_box.addWidget(self.calendar_button)
|
||||
calendar_row.setFixedWidth(row_w)
|
||||
# WIDEN the fee field (user feedback: the spin-box up/down arrows
|
||||
# were covering the digits). ~8 chars leaves room for the value and
|
||||
# the arrows.
|
||||
fees_w.field_widget.setFixedWidth(8 * char_width_in_lineedit())
|
||||
|
||||
# WHY THE TEXT WAS TRUNCATED (allegato16, fixed here):
|
||||
# A QLabel added to a box layout WITH an alignment flag (the old
|
||||
# ``alignment=Qt.AlignmentFlag.AlignLeft``) is NOT stretched to the
|
||||
# layout width - Qt gives it only its sizeHint. For a word-wrapped
|
||||
# label that sizeHint width is ambiguous/narrow, so the text wrapped
|
||||
# against an almost-minimum width and the reserved height was too
|
||||
# small, cutting the sentence in half. setMinimumWidth alone did not
|
||||
# help because the alignment flag still prevented horizontal stretch.
|
||||
#
|
||||
# TARGETED FIX:
|
||||
# 1. give the whole vertical WillSettingsWidget a sensible minimum
|
||||
# width, so the box (and thus the labels) has real width to work
|
||||
# with even before the parent dialog stretches it;
|
||||
# 2. add the two explanatory labels WITHOUT an alignment flag, so
|
||||
# they expand to the full box width and word-wrap correctly;
|
||||
# 3. set an Expanding/Minimum size policy so the label takes the
|
||||
# available width and computes its height from that width
|
||||
# (heightForWidth), guaranteeing the full text is shown.
|
||||
hint_min_width = 44 * char_width_in_lineedit()
|
||||
self.setMinimumWidth(hint_min_width)
|
||||
|
||||
# Explanatory hint ABOVE the date field (wizard only): tell the user
|
||||
# what the delivery date means. Wrapped so it fits the dialog width.
|
||||
# The explicit "\n" forces the line break exactly where the owner
|
||||
# asked (allegato2): after "(or backup)". With setWordWrap(True) the
|
||||
# QLabel honours the newline, so the sentence always shows on two
|
||||
# tidy lines instead of wrapping at an arbitrary point.
|
||||
date_hint = QLabel(
|
||||
_(
|
||||
"Enter the date on which you want the inheritance (or "
|
||||
"backup)\nof your Electrum wallet to take effect."
|
||||
)
|
||||
)
|
||||
date_hint.setWordWrap(True)
|
||||
date_hint.setMinimumWidth(hint_min_width)
|
||||
date_hint.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum
|
||||
)
|
||||
# NOTE: added WITHOUT an alignment flag on purpose (see above), so
|
||||
# the label is stretched to the full width and wraps correctly.
|
||||
box.addWidget(date_hint)
|
||||
|
||||
# Stack the composites one under the other, all left-aligned so the
|
||||
# fixed-width icons share a common left edge.
|
||||
box.addWidget(locktime_w, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
box.addWidget(threshold_w, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
box.addWidget(calendar_row, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
# In BASIC mode the Check-Alive (threshold) row is hidden entirely
|
||||
# (it was already set invisible above); in ADVANCED mode it shows
|
||||
# with its icon aligned under the delivery-time icon.
|
||||
box.addWidget(
|
||||
self.calendar_button, alignment=Qt.AlignmentFlag.AlignLeft
|
||||
)
|
||||
box.addWidget(fees_w, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
# Cautionary note BELOW the miner-fee field (wizard only): warn the
|
||||
# user not to lower the miner fee unless they know what they do.
|
||||
# Explicit "\n" break after "miner fees" (allegato2), same rationale
|
||||
# as date_hint above.
|
||||
fee_note = QLabel(
|
||||
_(
|
||||
"Please note: Do not reduce the miner fees\nunless you "
|
||||
"know what you\u2019re doing"
|
||||
)
|
||||
)
|
||||
fee_note.setWordWrap(True)
|
||||
fee_note.setMinimumWidth(hint_min_width)
|
||||
fee_note.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum
|
||||
)
|
||||
# Added WITHOUT an alignment flag (see date_hint above) so it
|
||||
# stretches to full width and wraps correctly instead of truncating.
|
||||
box.addWidget(fee_note)
|
||||
|
||||
self.apply_editable_dates()
|
||||
return
|
||||
|
||||
# Group C / C2: apply the current "Editable dates" setting to the date
|
||||
# fields. Done once at creation here, and re-applied later by
|
||||
# apply_editable_dates() whenever the setting changes (called from
|
||||
@@ -744,6 +885,39 @@ class WillSettingsWidget(QWidget):
|
||||
# editable outside the wizard only when the setting is ticked.
|
||||
self.widgets["baltx_fees"].set_read_only(not editable_dates)
|
||||
|
||||
def apply_user_type_visibility(self):
|
||||
"""Re-apply the BASIC/ADVANCED visibility of the Check-Alive field.
|
||||
|
||||
WHY this is needed (bug reported by the owner): the Check-Alive
|
||||
(threshold) field is hidden in BASIC mode and shown in ADVANCED mode.
|
||||
That visibility used to be decided ONLY in __init__. The toolbar
|
||||
settings widgets of the WILL and HEIR tabs are created once and then
|
||||
REUSED across the session (they are not rebuilt when the user switches
|
||||
USER TYPE), so after switching from BASIC to ADVANCED the Check-Alive
|
||||
field stayed hidden there. The wizard worked only because it is recreated
|
||||
every time it is opened.
|
||||
|
||||
This method re-reads is_basic_mode() and shows/hides the Check-Alive
|
||||
field accordingly. It is called from BalWindow.update_all() (which the
|
||||
USER TYPE combo triggers when changed), exactly like apply_editable_dates
|
||||
is, so toggling BASIC/ADVANCED takes effect immediately on the already
|
||||
existing WILL/HEIR toolbars without restarting Electrum.
|
||||
|
||||
It is safe to call repeatedly and on either layout (horizontal toolbar
|
||||
or vertical wizard): it only flips the visibility of the threshold
|
||||
widget.
|
||||
"""
|
||||
try:
|
||||
basic = self.bal_window.bal_plugin.is_basic_mode()
|
||||
except Exception:
|
||||
# If the mode cannot be read, keep the field visible (the safe,
|
||||
# information-preserving default).
|
||||
basic = False
|
||||
threshold = self.widgets.get("threshold")
|
||||
if threshold is not None:
|
||||
# Hidden in BASIC, visible in ADVANCED.
|
||||
threshold.setVisible(not basic)
|
||||
|
||||
def open_or_save_calendar(self):
|
||||
"""Build and save an .ics calendar file with SEPARATE reminder events.
|
||||
|
||||
@@ -774,20 +948,40 @@ class WillSettingsWidget(QWidget):
|
||||
"""
|
||||
now = BalCalendar.format_time(datetime.now())
|
||||
|
||||
# locktime = delivery deadline; threshold = start of the check-alive
|
||||
# period. Both are datetimes exposed by the date widgets as ``.alarm``.
|
||||
# locktime = delivery deadline. It is exposed by the date widget as
|
||||
# ``.alarm`` and already reflects the (possibly auto-anticipated) minimum
|
||||
# transaction locktime, so the calendar uses the correct delivery date.
|
||||
locktime = self.widgets["locktime"].alarm
|
||||
threshold = self.widgets["threshold"].alarm
|
||||
|
||||
# BASIC vs ADVANCED reminder strategy.
|
||||
#
|
||||
# In ADVANCED mode the reminders are spread uniformly across the
|
||||
# check-alive (threshold) period, ending one day before the deadline.
|
||||
#
|
||||
# In BASIC mode the check-alive parameter is NOT shown nor managed by the
|
||||
# user (it stays at an arbitrary default), so spreading reminders over it
|
||||
# is meaningless. The owner asked that, in BASIC, the calendar simply
|
||||
# saves the inheritance delivery date with three fixed reminders: 30 days
|
||||
# before, 10 days before and 1 day before. We also drop any fixed offset
|
||||
# that would fall in the past (a reminder before "today" is useless), so
|
||||
# a short-dated will still gets the reminders that are still in the
|
||||
# future.
|
||||
if self.bal_window.bal_plugin.is_basic_mode():
|
||||
# Whole days from now until the delivery date. Fixed offsets (30, 10,
|
||||
# 1 day before) are applied by basic_reminder_offsets, which also
|
||||
# drops any offset that would fall in the past.
|
||||
days_to_deadline = (locktime - datetime.now()).days
|
||||
offsets = basic_reminder_offsets(days_to_deadline)
|
||||
else:
|
||||
# ADVANCED: spread reminders over the check-alive period as before.
|
||||
threshold = self.widgets["threshold"].alarm
|
||||
# Whole days available between check-alive and the deadline.
|
||||
days = (locktime - threshold).days
|
||||
|
||||
# How many reminder events the user asked for (default 3 if unreadable).
|
||||
# How many reminders the user asked for (default 3 if unreadable).
|
||||
try:
|
||||
count = int(self.bal_window.bal_plugin.NUM_REMINDERS.get())
|
||||
except Exception:
|
||||
count = 3
|
||||
|
||||
# Day-offsets BEFORE the deadline, e.g. [30, 16, 1]. The list always
|
||||
# ends with 1 (one day before the locktime) when >= 2 reminders fit.
|
||||
offsets = compute_reminder_offsets(days, count)
|
||||
|
||||
@@ -466,7 +466,17 @@ class BalWindow:
|
||||
self.willexecutors = Willexecutors.get_willexecutors(
|
||||
self.bal_plugin, update=True, bal_window=self, task=False
|
||||
)
|
||||
if self.date_to_check < datetime.now().timestamp():
|
||||
# SIMPLE / ADVANCED: in BASIC mode the "Check Alive" parameter must
|
||||
# behave AS IF IT DID NOT EXIST. The check-alive threshold drives
|
||||
# the "you are alive -> postpone the inheritance" prompt; raising
|
||||
# CheckAliveError here is what triggers that postpone/invalidate
|
||||
# flow. In BASIC we therefore SKIP this check entirely, so a passed
|
||||
# check-alive date never forces a postpone/rewrite of the will. The
|
||||
# delivery time (locktime) is unaffected and still fully enforced.
|
||||
if (
|
||||
not self.bal_plugin.is_basic_mode()
|
||||
and self.date_to_check < datetime.now().timestamp()
|
||||
):
|
||||
raise CheckAliveError(self.date_to_check)
|
||||
|
||||
self.init_heirs_to_locktime(self.bal_plugin.ENABLE_MULTIVERSE.get())
|
||||
@@ -591,7 +601,15 @@ class BalWindow:
|
||||
elif isinstance(e, TxFeesChangedException):
|
||||
message = "Txfees are changed"
|
||||
elif isinstance(e, HeirNotFoundException):
|
||||
message = "Heir not found"
|
||||
# Task #01b: replace the misleading "Heir not found" text.
|
||||
# This branch is most often hit because the delivery date
|
||||
# was anticipated, not because an heir is missing, so we use
|
||||
# a clear message that covers both the DATE and the HEIRS
|
||||
# cases (kept consistent with dialogs.py / the CHECK window).
|
||||
message = (
|
||||
"Found CHANGES to the DATE or the HEIRS,\n"
|
||||
"a NEW WILL must be prepared."
|
||||
)
|
||||
|
||||
if message:
|
||||
self.show_message(
|
||||
@@ -1141,11 +1159,12 @@ class BalWindow:
|
||||
|
||||
base_msg = _("Downloading will-executors list...")
|
||||
download_start = time.time()
|
||||
# Upper bound shown to the user. fetch_will_executors_list tries up to
|
||||
# two endpoints, each with timeout=10 and one retry (~21s worst case),
|
||||
# so ~45s is a realistic maximum. Showing "Xs / 45s" tells the user how
|
||||
# long they may have to wait instead of an open-ended counter.
|
||||
download_deadline = 45
|
||||
# Upper bound shown to the user. Unified with every other network wait
|
||||
# via the single shared Willexecutors.NETWORK_DEADLINE constant (the
|
||||
# user asked for one consistent 20s value everywhere instead of the old
|
||||
# scattered 30s/45s numbers). Showing "Xs / NETWORK_DEADLINEs" tells the
|
||||
# user how long they may have to wait instead of an open-ended counter.
|
||||
download_deadline = Willexecutors.NETWORK_DEADLINE
|
||||
|
||||
def task():
|
||||
# Heartbeat: show an elapsed-seconds counter (with the max wait made
|
||||
@@ -1316,6 +1335,16 @@ class BalWindow:
|
||||
_settings_widget.apply_editable_dates()
|
||||
except Exception as _edit_err:
|
||||
_logger.debug(f"apply_editable_dates error: {_edit_err}")
|
||||
# Re-apply BASIC/ADVANCED visibility of the Check-Alive
|
||||
# field on the existing WILL/HEIR toolbars, so switching
|
||||
# USER TYPE shows/hides it immediately (bug fix: it used to
|
||||
# reappear only in the wizard, not on these tabs).
|
||||
try:
|
||||
_settings_widget.apply_user_type_visibility()
|
||||
except Exception as _vis_err:
|
||||
_logger.debug(
|
||||
f"apply_user_type_visibility error: {_vis_err}"
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(f"error while updating window: {e}")
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bal",
|
||||
"fullname": "Bitcoin After Life",
|
||||
"version": "0.3.9",
|
||||
"version": "0.4.7",
|
||||
"description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.",
|
||||
"author": "Svatantrya",
|
||||
"licence": "MIT",
|
||||
|
||||
@@ -81,6 +81,98 @@ def test_heirs_prepare_lists():
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# ALL-DUST GUARD (prepare_lists) - owner request, v0.4.7
|
||||
#
|
||||
# The plugin must REFUSE to build an inheritance when EVERY heir's share is
|
||||
# below the Bitcoin dust limit (HeirAmountIsDustException), but must keep
|
||||
# building normally when at least one heir is valid - including the tricky
|
||||
# case where the valid heir lives on a DIFFERENT locktime than the dust one.
|
||||
# These three tests pin that behaviour down so it cannot silently regress.
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_prepare_lists_all_dust_raises():
|
||||
"""All heirs below the dust limit -> HeirAmountIsDustException.
|
||||
|
||||
Reproduces the owner's log: a very small wallet balance split between
|
||||
percentage heirs gives each one a tiny share (like 214/316/3 sat), all
|
||||
below the dust threshold. prepare_lists must then REFUSE to build the
|
||||
"empty" inheritance and raise the exception.
|
||||
|
||||
NOTE: we deliberately use *percentage* heirs with a *small* balance. With
|
||||
fixed amounts and a large balance the leftover funds are redistributed to
|
||||
the heirs (so they would no longer be dust) - that is a different, valid
|
||||
case and is covered by the other prepare_lists tests.
|
||||
"""
|
||||
from bal.core.heirs import HeirAmountIsDustException
|
||||
|
||||
wallet = MagicMock()
|
||||
wallet.dust_threshold.return_value = 500
|
||||
h = Heirs.__new__(Heirs)
|
||||
# Tiny balance (800 sat) split 40%/60% -> each share is far below 500.
|
||||
h.update({
|
||||
"a": ["bcrt1q087zm5m3jrhfg78zflqefhcr9heh4c98kzmvhp", "40%", "30d"],
|
||||
"b": ["bcrt1q08z5t4x74u2883sx2qwsmzk2hj8e5n7z83e4vy", "60%", "30d"],
|
||||
})
|
||||
raised = False
|
||||
try:
|
||||
h.prepare_lists(800, 100, wallet)
|
||||
except HeirAmountIsDustException:
|
||||
raised = True
|
||||
assert raised, "all-dust will must raise HeirAmountIsDustException"
|
||||
|
||||
|
||||
def test_prepare_lists_mixed_dust_continues():
|
||||
"""Some dust + at least one valid heir -> build continues normally.
|
||||
|
||||
The guard must NOT fire here: one heir's share is dust (a 1% slice of a
|
||||
small balance), the other is a valid fixed amount, so the inheritance is
|
||||
still feasible (unchanged behaviour).
|
||||
"""
|
||||
from bal.core.heirs import HeirAmountIsDustException
|
||||
|
||||
wallet = MagicMock()
|
||||
wallet.dust_threshold.return_value = 500
|
||||
h = Heirs.__new__(Heirs)
|
||||
h.update({
|
||||
"ok": ["bcrt1q087zm5m3jrhfg78zflqefhcr9heh4c98kzmvhp", 5000, "30d"],
|
||||
"dust": ["bcrt1q08z5t4x74u2883sx2qwsmzk2hj8e5n7z83e4vy", "1%", "30d"],
|
||||
})
|
||||
raised = False
|
||||
try:
|
||||
result, _onlyfixed = h.prepare_lists(5200, 100, wallet)
|
||||
except HeirAmountIsDustException:
|
||||
raised = True
|
||||
assert not raised, "a mix of dust + valid heirs must NOT be blocked"
|
||||
assert isinstance(result, dict) and len(result) > 0
|
||||
|
||||
|
||||
def test_prepare_lists_multi_locktime_continues():
|
||||
"""Dust heir and valid heir on DIFFERENT locktimes -> build continues.
|
||||
|
||||
This pins down the false-positive fix: prepare_transactions only ever sees
|
||||
the lowest locktime, so the dust check MUST live in prepare_lists (which
|
||||
sees ALL locktimes). A dust heir at the earlier date must not block a valid
|
||||
heir at the later date.
|
||||
"""
|
||||
from bal.core.heirs import HeirAmountIsDustException
|
||||
|
||||
wallet = MagicMock()
|
||||
wallet.dust_threshold.return_value = 500
|
||||
h = Heirs.__new__(Heirs)
|
||||
h.update({
|
||||
"early_dust": ["bcrt1q087zm5m3jrhfg78zflqefhcr9heh4c98kzmvhp", "1%", "30d"],
|
||||
"late_valid": ["bcrt1q08z5t4x74u2883sx2qwsmzk2hj8e5n7z83e4vy", 5000, "60d"],
|
||||
})
|
||||
raised = False
|
||||
try:
|
||||
result, _onlyfixed = h.prepare_lists(5200, 100, wallet)
|
||||
except HeirAmountIsDustException:
|
||||
raised = True
|
||||
assert not raised, "valid heir on a later locktime must NOT be blocked"
|
||||
assert isinstance(result, dict) and len(result) > 0
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Heirs static methods (pure but use Electrum constants)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
116
tests/test_group_f_heir_change_rebuild.py
Normal file
116
tests/test_group_f_heir_change_rebuild.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Tests for Group F (heir-change full rebuild, "Option A").
|
||||
|
||||
Context (bugs E/F/K): when an heir was deleted/changed and the rebuilt
|
||||
inheritance transaction happened to keep the SAME txid, ``Will.update_will``
|
||||
used to REUSE the old (already signed/COMPLETE) WillItem, copying only the new
|
||||
heirs onto it. The downstream ``have_to_sign`` check then saw the item as
|
||||
COMPLETE and reported "Nothing to do", so the new will was never signed or
|
||||
broadcast.
|
||||
|
||||
The fix reuses the old item ONLY when the real heirs are identical. These tests
|
||||
pin the behaviour of the helper ``Will._same_heirs`` that drives that decision.
|
||||
|
||||
Run:
|
||||
source electrum/env/bin/activate
|
||||
python3 tests/test_group_f_heir_change_rebuild.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
from bal.core.will import Will
|
||||
|
||||
|
||||
# Heir entry layout (see heirs.py): [0]=address, [1]=amount, [2]=locktime.
|
||||
# Extra trailing fields (e.g. real/dust amount) must NOT affect equality.
|
||||
def _heir(address, amount, locktime, *extra):
|
||||
return [address, amount, locktime, *extra]
|
||||
|
||||
|
||||
def test_same_heirs_identical():
|
||||
"""Identical real heirs -> True (old signed item may be reused)."""
|
||||
a = {"alice": _heir("bc1qalice", "50%", "2026-12-01")}
|
||||
b = {"alice": _heir("bc1qalice", "50%", "2026-12-01")}
|
||||
assert Will._same_heirs(a, b) is True
|
||||
|
||||
|
||||
def test_same_heirs_ignores_extra_fields():
|
||||
"""Derived/extra fields (real amount, dust flag) do not break equality."""
|
||||
a = {"alice": _heir("bc1qalice", "50%", "2026-12-01", 12345, "DUST")}
|
||||
b = {"alice": _heir("bc1qalice", "50%", "2026-12-01")}
|
||||
assert Will._same_heirs(a, b) is True
|
||||
|
||||
|
||||
def test_same_heirs_deleted_heir():
|
||||
"""Deleting one of two heirs -> changed -> False (must rebuild/re-sign)."""
|
||||
old = {
|
||||
"alice": _heir("bc1qalice", "50%", "2026-12-01"),
|
||||
"bob": _heir("bc1qbob", "50%", "2026-12-01"),
|
||||
}
|
||||
new = {
|
||||
# bob removed; alice is auto-scaled to 100% by the plugin.
|
||||
"alice": _heir("bc1qalice", "100%", "2026-12-01"),
|
||||
}
|
||||
assert Will._same_heirs(old, new) is False
|
||||
|
||||
|
||||
def test_same_heirs_added_heir():
|
||||
"""Adding a heir -> changed -> False."""
|
||||
old = {"alice": _heir("bc1qalice", "100%", "2026-12-01")}
|
||||
new = {
|
||||
"alice": _heir("bc1qalice", "50%", "2026-12-01"),
|
||||
"carol": _heir("bc1qcarol", "50%", "2026-12-01"),
|
||||
}
|
||||
assert Will._same_heirs(old, new) is False
|
||||
|
||||
|
||||
def test_same_heirs_changed_amount():
|
||||
"""Same heir name but different amount -> changed -> False."""
|
||||
old = {"alice": _heir("bc1qalice", "50%", "2026-12-01")}
|
||||
new = {"alice": _heir("bc1qalice", "70%", "2026-12-01")}
|
||||
assert Will._same_heirs(old, new) is False
|
||||
|
||||
|
||||
def test_same_heirs_changed_address():
|
||||
"""Same heir name but different destination address -> False."""
|
||||
old = {"alice": _heir("bc1qalice", "50%", "2026-12-01")}
|
||||
new = {"alice": _heir("bc1qOTHER", "50%", "2026-12-01")}
|
||||
assert Will._same_heirs(old, new) is False
|
||||
|
||||
|
||||
def test_same_heirs_changed_locktime():
|
||||
"""Same heir but different delivery locktime -> False."""
|
||||
old = {"alice": _heir("bc1qalice", "50%", "2026-12-01")}
|
||||
new = {"alice": _heir("bc1qalice", "50%", "2027-06-01")}
|
||||
assert Will._same_heirs(old, new) is False
|
||||
|
||||
|
||||
def test_same_heirs_ignores_willexecutor_pseudo_heirs():
|
||||
"""Reserved 'w!ll3x3c\"' pseudo-heirs are bookkeeping, not real heirs.
|
||||
|
||||
A difference only in the pseudo-heir entries must NOT be reported as a heir
|
||||
change (the will-executor is refreshed separately in update_will).
|
||||
"""
|
||||
old = {
|
||||
"alice": _heir("bc1qalice", "100%", "2026-12-01"),
|
||||
'w!ll3x3c"server1': _heir("bc1qwe1", "0", "0"),
|
||||
}
|
||||
new = {
|
||||
"alice": _heir("bc1qalice", "100%", "2026-12-01"),
|
||||
'w!ll3x3c"server2': _heir("bc1qwe2", "0", "0"),
|
||||
}
|
||||
assert Will._same_heirs(old, new) is True
|
||||
|
||||
|
||||
def test_same_heirs_empty():
|
||||
"""Two empty heir maps are equal; None is treated as empty."""
|
||||
assert Will._same_heirs({}, {}) is True
|
||||
assert Will._same_heirs(None, {}) is True
|
||||
assert Will._same_heirs(None, None) is True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import pytest
|
||||
raise SystemExit(pytest.main([__file__, "-v"]))
|
||||
79
tests/test_group_g_basic_calendar.py
Normal file
79
tests/test_group_g_basic_calendar.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Tests for the BASIC-mode calendar reminders.
|
||||
|
||||
Context: in BASIC mode the check-alive parameter is hidden and not managed by
|
||||
the user, so calendar reminders cannot be spread over the check-alive period as
|
||||
they are in ADVANCED mode. Instead the calendar uses three fixed reminders -
|
||||
30, 10 and 1 day before the inheritance delivery date - and drops any reminder
|
||||
that would fall in the past.
|
||||
|
||||
These tests pin the behaviour of the pure helper ``basic_reminder_offsets`` that
|
||||
drives that decision.
|
||||
|
||||
``basic_reminder_offsets`` lives in ``bal.gui.qt.widgets`` (which imports
|
||||
PyQt6), so these tests are run headless with ``QT_QPA_PLATFORM=offscreen`` like
|
||||
the other GUI tests.
|
||||
|
||||
Run:
|
||||
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src \
|
||||
python3 -m pytest tests/test_group_g_basic_calendar.py -q
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
from bal.gui.qt.widgets import (BASIC_REMINDER_OFFSETS,
|
||||
basic_reminder_offsets)
|
||||
|
||||
|
||||
def test_basic_offsets_all_future():
|
||||
"""A far delivery date keeps all three fixed reminders (30, 10, 1)."""
|
||||
assert basic_reminder_offsets(365) == [30, 10, 1]
|
||||
|
||||
|
||||
def test_basic_offsets_exactly_30_days():
|
||||
"""Exactly 30 days away: the 30-day reminder is still valid (<=)."""
|
||||
assert basic_reminder_offsets(30) == [30, 10, 1]
|
||||
|
||||
|
||||
def test_basic_offsets_drops_30_when_too_close():
|
||||
"""20 days away: the 30-day reminder is in the past and is dropped."""
|
||||
assert basic_reminder_offsets(20) == [10, 1]
|
||||
|
||||
|
||||
def test_basic_offsets_only_one_left():
|
||||
"""5 days away: only the 1-day reminder remains."""
|
||||
assert basic_reminder_offsets(5) == [1]
|
||||
|
||||
|
||||
def test_basic_offsets_empty_when_deadline_today():
|
||||
"""Delivery date less than a day away: no reminder fits."""
|
||||
assert basic_reminder_offsets(0) == []
|
||||
|
||||
|
||||
def test_basic_offsets_empty_when_negative():
|
||||
"""A past delivery date (negative days) yields no reminders."""
|
||||
assert basic_reminder_offsets(-10) == []
|
||||
|
||||
|
||||
def test_basic_offsets_are_a_subset_of_the_fixed_set():
|
||||
"""Whatever the horizon, results are always a subset of the fixed offsets."""
|
||||
for horizon in (-1, 0, 1, 9, 10, 11, 29, 30, 100):
|
||||
result = basic_reminder_offsets(horizon)
|
||||
assert set(result).issubset(set(BASIC_REMINDER_OFFSETS))
|
||||
# Always sorted descending (earliest reminder first) and every offset >= 1.
|
||||
assert result == sorted(result, reverse=True)
|
||||
assert all(off >= 1 for off in result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_basic_offsets_all_future()
|
||||
test_basic_offsets_exactly_30_days()
|
||||
test_basic_offsets_drops_30_when_too_close()
|
||||
test_basic_offsets_only_one_left()
|
||||
test_basic_offsets_empty_when_deadline_today()
|
||||
test_basic_offsets_empty_when_negative()
|
||||
test_basic_offsets_are_a_subset_of_the_fixed_set()
|
||||
print("all BASIC calendar reminder tests passed")
|
||||
Reference in New Issue
Block a user