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
|
**Outcome:** DONE (delivered as test ZIP v0.3.9, confirmed OK by the user before
|
||||||
commit).
|
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
|
> Purpose: let ANY future AI assistant (Claude or another model, more advanced
|
||||||
> WITHOUT losing context. If you are a new assistant, READ THIS FILE FIRST,
|
> or cheaper) resume work on this project with full context, without having to
|
||||||
> then read `CHANGELOG.md` and `.agent_memory_tasks.md`.
|
> re-discover the codebase. Read this file FIRST, then `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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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
|
- **Product:** BAL ("Bitcoin After Life") — an inheritance plugin for the
|
||||||
(code, docstrings, comments, UI strings, docs, CHANGELOG, commit messages).
|
**Electrum 4.7.2** Bitcoin wallet (Qt / **PyQt6**).
|
||||||
- **R2 — DOCUMENTED CODE:** every method/class needs a docstring + explanatory
|
- **Form:** external **ZIP plugin** (not bundled in Electrum). The user
|
||||||
comments. When a non-obvious design choice is made, explain WHY in a comment.
|
installs the ZIP from Electrum's plugin manager.
|
||||||
- **R3 — NEVER INVENT:** if anything is missing or unclear, STOP and ask the user
|
- **What it does:** lets a wallet owner pre-build, sign and (later) broadcast
|
||||||
clear, simple questions (he is not a programmer). Be "100% sure" before acting.
|
Bitcoin transactions that pay one or more **heirs** after a chosen **date**
|
||||||
- **R4 — HUMAN CHECKPOINT:** before writing/modifying code, show the PLAN and WAIT
|
(a future UNIX-timestamp `nLockTime`). Optional **will-executors** (remote
|
||||||
for the user's explicit "OK".
|
services) can be paid a fee to broadcast the inheritance when due. The owner
|
||||||
- **METHOD per task:** DISCOVER → PLAN (wait OK) → EXECUTE → VERIFY → ITERATE
|
periodically proves they are alive ("check-alive"); if the deadline passes,
|
||||||
(max 8 attempts, then declare "UNRESOLVED").
|
the inheritance becomes spendable.
|
||||||
- **LOG:** a single `CHANGELOG.md` in English, one numbered entry per task.
|
- **Current version:** see `bal/VERSION` (last shipped: **0.4.7**).
|
||||||
- **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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. PROJECT OVERVIEW
|
## 1. MANDATORY working rules (the owner set these — always follow them)
|
||||||
|
|
||||||
- Electrum **4.7.2** Qt (PyQt6) inheritance plugin.
|
These are non-negotiable. They come from the owner directly.
|
||||||
- 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).
|
|
||||||
|
|
||||||
### Version files (keep ALL FOUR in sync on every release)
|
- **R1 — LANGUAGE.** The CHAT language with the owner is **Italian**. But ALL
|
||||||
- `bal/manifest.json` → `"version"`
|
*output* — source code, comments, docstrings, UI strings, docs, `CHANGELOG.md`,
|
||||||
- `bal/__init__.py` → `__version__`
|
commit messages, this handoff — must be in **ENGLISH**.
|
||||||
- `bal/core/plugin_base.py` → `__version__ = "..." # AUTOMATICALLY GENERATED DO NOT EDIT`
|
- **R2 — DOCUMENTED CODE.** Every method/class gets a docstring + explanatory
|
||||||
- `bal/VERSION`
|
comments. Always explain *WHY* for any non-obvious decision.
|
||||||
- **Current released version: 0.3.9**
|
- **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
|
bal/ <- the plugin package (this is what ships in the ZIP)
|
||||||
cd /home/user/webapp
|
__init__.py <- __version__ (one of 4 version files)
|
||||||
find bal -name "__pycache__" -type d -exec rm -rf {} + ; find bal -name "*.pyc" -delete
|
VERSION <- plain-text version (one of 4 version files)
|
||||||
python3 build_zip.py bal-electrum-plugin-vX.Y.Z.zip # builds 37 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
|
```bash
|
||||||
cd /home/user/webapp
|
|
||||||
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 -m pytest \
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 -m pytest \
|
||||||
tests/test_core_*.py tests/test_gui_*.py \
|
tests/test_core_*.py tests/test_gui_*.py \
|
||||||
tests/test_anticipate_past_locktime.py tests/test_anticipate_manual_locktime.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_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
|
```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:
|
Pre-existing, KNOWN-OK ruff noise: `F401/F403/F405` (star-imports via
|
||||||
- `F401/F403/F405` star-import noise (`from .common import *`).
|
`from .common import *`) and 2× `F841` (an unused `e` in two `except` blocks).
|
||||||
- `F841` at: `will.py:151`, `dialogs.py:615` (`except NoHeirsException as e`),
|
Do NOT "fix" these unless asked — they are intentional / out of scope.
|
||||||
`lists.py:185`, `lists.py:272`.
|
|
||||||
- `E501` long lines in several pre-existing spots.
|
|
||||||
|
|
||||||
### Git / PR / Release workflow
|
**Build the ZIP (always clear caches first so zipimport doesn't ship stale .pyc):**
|
||||||
- `setup_github_environment` first. If `git push` fails with
|
```bash
|
||||||
"Invalid username or token", CALL `setup_github_environment` AGAIN, then retry.
|
find bal -name "__pycache__" -type d -exec rm -rf {} + ; find bal -name "*.pyc" -delete
|
||||||
- Token for API calls:
|
python3 build_zip.py bal-electrum-plugin-vX.Y.Z.zip # produces 37 files
|
||||||
`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)
|
|
||||||
```
|
```
|
||||||
Your will has expired and must be invalidated before it can be rebuilt.
|
|
||||||
A transaction window will now open:
|
**Bump version — there are FOUR files, keep them in sync:**
|
||||||
please SIGN and then BROADCAST it to invalidate your old will.
|
```
|
||||||
After the invalidation is confirmed, press the Check button to finish the will.
|
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)
|
**IMPORTANT for the owner when testing:** after installing a ZIP, the owner
|
||||||
- Route the expired cases (FIRST handler at ~594, the `invalidate_classic` block,
|
must **fully restart Electrum** (not just reload the plugin) — Electrum's
|
||||||
and therefore the CHECK button) through ONE shared helper that:
|
`zipimport` caches modules, so a partial reload runs stale code.
|
||||||
(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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. KEY FILE / LINE REFERENCES (verify line numbers, they drift)
|
## 4. Key technical knowledge (hard-won — saves you hours)
|
||||||
|
|
||||||
- `bal/core/will.py`
|
- **Locktimes are UNIX timestamps only.** Block-height locktimes were removed
|
||||||
- `check_will()` order (line ~561): `check_invalidated` → `check_will_expired`
|
(CHANGELOG #1). Ordering/expiry compare timestamps.
|
||||||
(raises `WillExpiredException`) → `search_rai` (raises `HeirNotFoundException`).
|
- **`heirs.py` data shape.** An heir is a list indexed by constants
|
||||||
- `invalidate_will()` static (line ~394) builds the invalidation PartialTransaction.
|
(`heirs.py` top): `HEIR_ADDRESS=0`, `HEIR_AMOUNT=1` (sats or `"<n>%"`),
|
||||||
- `_short_will_id` / `_format_locktime` helpers + the expired message (with `<br>`).
|
`HEIR_LOCKTIME=2`, `HEIR_REAL_AMOUNT=3` (resolved sats, or the string
|
||||||
- `bal/gui/qt/dialogs.py`
|
`"DUST: <n>"` when below the dust limit), `HEIR_DUST_AMOUNT=4` (raw dust sats).
|
||||||
- `BalBuildWillDialog` is the CHECK + wizard engine.
|
- **Will-executor pseudo-heirs.** Internally, each selected will-executor is
|
||||||
- `build_will_task()` (~530) starts `task_phase1`.
|
injected as a fake "heir" whose NAME starts with the reserved marker
|
||||||
- `task_phase1()` (~542): first `check_will()`; first `WillExpiredException`
|
`w!ll3x3c"` (i.e. `'w!ll3x3c"' + url + '"' + str(locktime)`). Its amount is
|
||||||
handler (~594) → `return None, Will.invalidate_will(...)`; `NotCompleteWill`/
|
the executor `base_fee` (always non-dust). When you count/iterate "real"
|
||||||
`HeirNotFound` → `have_to_build`; inner `check_will()`; inner `WillExpiredException`
|
heirs you MUST skip names starting with `w!ll3x3c"`.
|
||||||
(~659) → currently returns `"invalidate_classic", None`.
|
- **Transaction-building pipeline:**
|
||||||
- `on_success_phase1()` (~924): unpacks `(have_to_sign, tx)`. If
|
`window.build_will()` → `Heirs.get_transactions()` (recursive over locktimes)
|
||||||
`have_to_sign == "invalidate_classic"` → shows popup. If `have_to_sign is None`
|
→ `Heirs.buildTransactions()` → `Heirs.prepare_lists()` (builds the
|
||||||
→ password prompt "Invalidate your old will" → `invalidate_task` (PROCEDURE 2).
|
`locktimes` dict for ALL future locktimes, resolves amounts, marks dust)
|
||||||
- `invalidate_task()` (~902) + `loop_broadcast_invalidating()` (~729): PROCEDURE 2.
|
and `prepare_transactions()` (builds ONE tx for the LOWEST locktime only;
|
||||||
- `QTimer` is available via `from .common import *` (defined in common.py:53).
|
the recursion handles the others via leftover `available_utxos`).
|
||||||
- `bal/gui/qt/window.py`
|
- **DUST logic (v0.4.7 — verify before touching):**
|
||||||
- `invalidate_will()` (~695): PROCEDURE 1 (the "good" one). Sets label at ~704.
|
- The "all heirs are dust" guard lives at the END of `prepare_lists`
|
||||||
- `show_transaction_real()` (~656) uses `show_on_top(d, modal_to_window=False)`.
|
(NOT in `prepare_transactions`). Reason: `prepare_transactions` only sees
|
||||||
- on-close/postpone expired handling at ~539, ~576, ~614.
|
the single lowest locktime, so a guard there would FALSE-POSITIVE block a
|
||||||
- `bal/gui/qt/lists.py`
|
will whose later locktimes still have valid heirs. `prepare_lists` is the
|
||||||
- `check()` (~545): the CHECK button. `invalidate_will()` (~571). Menu actions
|
only place that sees ALL heirs across ALL locktimes with their final dust
|
||||||
"Check"/"Invalidate" at ~467/468. Wizard button "Build Your Will" at ~473.
|
state (fixed AND percentage).
|
||||||
- `bal/gui/qt/plugin.py`: settings dialog; "No will-executor TX" checkbox + reset.
|
- Guard: count real heirs (skip `w!ll3x3c"`); if there are real heirs but
|
||||||
- `bal/gui/qt/window_utils.py`: `show_on_top` (~100), `bring_to_front` (~52),
|
NONE has a valid (non-`"DUST"`) `HEIR_REAL_AMOUNT`, raise
|
||||||
`show_modal` (~86).
|
`HeirAmountIsDustException` (defined in `heirs.py`). A mix of dust + valid
|
||||||
- `bal/gui/qt/common.py`: `add_widget` helper (~98); imports `QTimer`, `Qt`, etc.
|
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:
|
- **Branch:** work on `genspark_ai_developer`. Open PRs into `main`.
|
||||||
> "Read `/home/user/webapp/HANDOFF.md`, then `CHANGELOG.md` and
|
- **Commit policy:** ZIP-FIRST — build a test ZIP, let the owner confirm it
|
||||||
> `.agent_memory_tasks.md`. Follow rules R1–R4 and zip-first. The next task is the
|
works, THEN commit. (This differs from "commit after every change"; the owner
|
||||||
> 'unify invalidate procedure' task in HANDOFF.md section 5 — present the PLAN and
|
explicitly prefers ZIP-first because they manually test each build.)
|
||||||
> wait for my OK before coding."
|
- 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.
|
``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}
|
locktimes[locktime] = {key: value}
|
||||||
else:
|
else:
|
||||||
locktimes[locktime][key] = value
|
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
|
return locktimes, onlyfixed
|
||||||
|
|
||||||
def is_perc(self, key):
|
def is_perc(self, key):
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class BalPlugin(BasePlugin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_version = None
|
_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.
|
# Command used to open an .ics calendar file, per operating system.
|
||||||
default_app = {
|
default_app = {
|
||||||
@@ -190,11 +190,31 @@ class BalPlugin(BasePlugin):
|
|||||||
# most one event per available day.
|
# most one event per available day.
|
||||||
self.NUM_REMINDERS = BalConfig(config, "bal_num_reminders", 3)
|
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_REPLACED = BalConfig(config, "bal_hide_replaced", True)
|
||||||
self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True)
|
self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True)
|
||||||
self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True)
|
self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True)
|
||||||
self.FIRST_EXECUTION = BalConfig(config, "bal_first_execution", 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(
|
self.WELIST_SERVER = BalConfig(
|
||||||
config, "bal_welist_server", "https://welist.bitcoin-after.life/"
|
config, "bal_welist_server", "https://welist.bitcoin-after.life/"
|
||||||
)
|
)
|
||||||
@@ -315,6 +335,16 @@ class BalPlugin(BasePlugin):
|
|||||||
will_settings["locktime"] = defaults['locktime']
|
will_settings["locktime"] = defaults['locktime']
|
||||||
return will_settings
|
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
|
@staticmethod
|
||||||
def default_will_settings():
|
def default_will_settings():
|
||||||
"""Default will settings: a fee rate plus absolute threshold/locktime."""
|
"""Default will settings: a fee rate plus absolute threshold/locktime."""
|
||||||
|
|||||||
@@ -349,6 +349,47 @@ class Will:
|
|||||||
|
|
||||||
Will.search_anticipate_rec(will, old_inputs)
|
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
|
@staticmethod
|
||||||
def update_will(old_will, new_will):
|
def update_will(old_will, new_will):
|
||||||
all_old_inputs = Will.get_all_inputs(old_will, only_valid=True)
|
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_heirs = new_will[oid].heirs
|
||||||
new_we = new_will[oid].we
|
new_we = new_will[oid].we
|
||||||
|
|
||||||
new_will[oid] = old_will[oid]
|
# OPTION A (heir-change full rebuild, user-approved):
|
||||||
new_will[oid].heirs = new_heirs
|
#
|
||||||
new_will[oid].we = new_we
|
# 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
|
continue
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ from .plugin_base import BalPlugin
|
|||||||
# block the UI.
|
# block the UI.
|
||||||
DEFAULT_TIMEOUT = 5
|
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
|
# 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
|
# 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.
|
# 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
|
# Global wall-clock deadline (seconds) for the whole parallel broadcast. Once
|
||||||
# it elapses we stop waiting for the still-pending servers, mark them as
|
# it elapses we stop waiting for the still-pending servers, mark them as
|
||||||
# "Timeout" and let the wizard proceed instead of appearing stuck.
|
# "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
|
# Check (searchtx) timeouts. Used when the user presses "Check" to verify that
|
||||||
# each will-executor still holds the transaction. Like the broadcast path, the
|
# each will-executor still holds the transaction. Like the broadcast path, the
|
||||||
@@ -53,7 +62,8 @@ PUSH_GLOBAL_DEADLINE = 30
|
|||||||
CHECK_TIMEOUT = 8
|
CHECK_TIMEOUT = 8
|
||||||
CHECK_MAX_RETRIES = 1
|
CHECK_MAX_RETRIES = 1
|
||||||
CHECK_RETRY_SLEEP = 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__)
|
_logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -68,6 +78,7 @@ class Willexecutors:
|
|||||||
# importing module-level names. Single source of truth: the module
|
# importing module-level names. Single source of truth: the module
|
||||||
# constants defined above.
|
# constants defined above.
|
||||||
DEFAULT_TIMEOUT = DEFAULT_TIMEOUT
|
DEFAULT_TIMEOUT = DEFAULT_TIMEOUT
|
||||||
|
NETWORK_DEADLINE = NETWORK_DEADLINE
|
||||||
PUSH_TIMEOUT = PUSH_TIMEOUT
|
PUSH_TIMEOUT = PUSH_TIMEOUT
|
||||||
PUSH_MAX_RETRIES = PUSH_MAX_RETRIES
|
PUSH_MAX_RETRIES = PUSH_MAX_RETRIES
|
||||||
PUSH_RETRY_SLEEP = PUSH_RETRY_SLEEP
|
PUSH_RETRY_SLEEP = PUSH_RETRY_SLEEP
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox,
|
|||||||
|
|
||||||
# --- Core (GUI-free) logic layer ---
|
# --- Core (GUI-free) logic layer ---
|
||||||
from ...core.plugin_base import BalPlugin, BalTimestamp
|
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.util import Util
|
||||||
from ...core.will import (AmountException, HeirChangeException,
|
from ...core.will import (AmountException, HeirChangeException,
|
||||||
HeirNotFoundException, NoHeirsException,
|
HeirNotFoundException, NoHeirsException,
|
||||||
|
|||||||
@@ -127,6 +127,21 @@ class BalWizardDialog(BalDialog):
|
|||||||
def on_next_we(self):
|
def on_next_we(self):
|
||||||
close_window = BalBuildWillDialog(self.bal_window)
|
close_window = BalBuildWillDialog(self.bal_window)
|
||||||
close_window.build_will_task()
|
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.close()
|
||||||
# self.next_widget(BalWizardLocktimeAndFeeWidget(self.bal_window,self,self.on_next_locktimeandfee,self.on_next_wedonwload,self.on_next_wedonwload.on_cancel_heir))
|
# 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()
|
ping_on_done()
|
||||||
|
|
||||||
def 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:
|
if index < 1:
|
||||||
for we in self.bal_window.willexecutors:
|
for we in self.bal_window.willexecutors:
|
||||||
if self.bal_window.willexecutors[we]["status"] == 200:
|
wedict = self.bal_window.willexecutors[we]
|
||||||
self.bal_window.willexecutors[we]["selected"] = True
|
responded = str(wedict.get("status", "")) == "200"
|
||||||
|
wedict["selected"] = responded
|
||||||
Willexecutors.save(
|
Willexecutors.save(
|
||||||
self.bal_window.bal_plugin, self.bal_window.willexecutors
|
self.bal_window.bal_plugin, self.bal_window.willexecutors
|
||||||
)
|
)
|
||||||
@@ -502,10 +537,35 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
self.bal_window = bal_window
|
self.bal_window = bal_window
|
||||||
self.bal_plugin = bal_window.bal_plugin
|
self.bal_plugin = bal_window.bal_plugin
|
||||||
self.message_label = QLabel(_("Building Will:"))
|
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 = 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.qwidget = QWidget(self)
|
||||||
self.vbox.addWidget(self.qwidget, 1)
|
|
||||||
self.labelsbox = QVBoxLayout(self.qwidget)
|
self.labelsbox = QVBoxLayout(self.qwidget)
|
||||||
self.setMinimumWidth(600)
|
self.setMinimumWidth(600)
|
||||||
self.setMinimumHeight(100)
|
self.setMinimumHeight(100)
|
||||||
@@ -519,6 +579,18 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
# Manual next-steps hint (Sign / Broadcast) shown to the user after the
|
# Manual next-steps hint (Sign / Broadcast) shown to the user after the
|
||||||
# dialog finishes; None when nothing is left to do.
|
# dialog finishes; None when nothing is left to do.
|
||||||
self._next_steps_hint = None
|
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.network = Network.get_instance()
|
||||||
self._stopping = False
|
self._stopping = False
|
||||||
self.thread = TaskThread(self)
|
self.thread = TaskThread(self)
|
||||||
@@ -592,12 +664,22 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
self.bal_window.check_will()
|
self.bal_window.check_will()
|
||||||
self.msg_set_checking(self.msg_ok())
|
self.msg_set_checking(self.msg_ok())
|
||||||
except WillExpiredException:
|
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")
|
_logger.debug("expired")
|
||||||
self.msg_set_checking("Expired")
|
self.msg_set_checking("Expired")
|
||||||
fee_per_byte = self.bal_window.will_settings.get("baltx_fees", 1)
|
return "invalidate_classic", None
|
||||||
return None, Will.invalidate_will(
|
|
||||||
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
|
|
||||||
)
|
|
||||||
except WillPostponedException as e:
|
except WillPostponedException as e:
|
||||||
# An already signed/sent will is being postponed. Like an expired
|
# An already signed/sent will is being postponed. Like an expired
|
||||||
# will, the previously committed coins must be invalidated on-chain
|
# will, the previously committed coins must be invalidated on-chain
|
||||||
@@ -628,12 +710,30 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
elif isinstance(e, TxFeesChangedException):
|
elif isinstance(e, TxFeesChangedException):
|
||||||
message = _("Txfees are changed")
|
message = _("Txfees are changed")
|
||||||
elif isinstance(e, HeirNotFoundException):
|
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:
|
if message:
|
||||||
_logger.debug(f"message: {message}")
|
_logger.debug(f"message: {message}")
|
||||||
self.msg_set_checking(message)
|
self.msg_set_checking(message)
|
||||||
else:
|
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:
|
if have_to_build:
|
||||||
self.msg_set_building()
|
self.msg_set_building()
|
||||||
@@ -647,10 +747,7 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
self.bal_window.check_will()
|
self.bal_window.check_will()
|
||||||
for wid in Will.only_valid(self.bal_window.willitems):
|
self._build_success_report()
|
||||||
# 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())
|
|
||||||
except WillExecutorNotPresent:
|
except WillExecutorNotPresent:
|
||||||
self.msg_set_status(
|
self.msg_set_status(
|
||||||
_("Will-Executor excluded"), None, _("Skipped"), self.COLOR_ERROR
|
_("Will-Executor excluded"), None, _("Skipped"), self.COLOR_ERROR
|
||||||
@@ -683,21 +780,77 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
self.msg_set_building(self.msg_warning(e))
|
self.msg_set_building(self.msg_warning(e))
|
||||||
return "invalidate_classic", None
|
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:
|
except Exception as e:
|
||||||
self.msg_set_building(self.msg_error(e))
|
self.msg_set_building(self.msg_error(e))
|
||||||
return False, None
|
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):
|
for wid in Will.only_valid(self.bal_window.willitems):
|
||||||
heirs = self.bal_window.willitems[wid].heirs
|
heirs = self.bal_window.willitems[wid].heirs
|
||||||
for hid, heir in heirs.items():
|
for hid, heir in heirs.items():
|
||||||
if "DUST" in str(heir[HEIR_REAL_AMOUNT]):
|
if "DUST" in str(heir[HEIR_REAL_AMOUNT]):
|
||||||
self.msg_set_status(
|
# Keep the first dust amount seen for this heir; it is the
|
||||||
f"{hid},{heir[HEIR_DUST_AMOUNT]} is DUST",
|
# same share in every will-executor copy of the will.
|
||||||
None,
|
dust_heirs.setdefault(hid, heir[HEIR_DUST_AMOUNT])
|
||||||
f"Excluded from will {wid}",
|
for hid, dust_amount in dust_heirs.items():
|
||||||
self.COLOR_WARNING,
|
self.msg_set_status(
|
||||||
)
|
f"{_('Heir')} {hid}",
|
||||||
|
None,
|
||||||
|
f"{dust_amount} is DUST - excluded (amount below dust limit)",
|
||||||
|
self.COLOR_WARNING,
|
||||||
|
)
|
||||||
|
|
||||||
have_to_sign = False
|
have_to_sign = False
|
||||||
for wid in Will.only_valid(self.bal_window.willitems):
|
for wid in Will.only_valid(self.bal_window.willitems):
|
||||||
@@ -706,6 +859,116 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
break
|
break
|
||||||
return have_to_sign, txs
|
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):
|
def on_accept(self):
|
||||||
self.bal_window.update_all()
|
self.bal_window.update_all()
|
||||||
pass
|
pass
|
||||||
@@ -733,12 +996,34 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
try:
|
try:
|
||||||
tx.add_info_from_wallet(self.bal_window.wallet)
|
tx.add_info_from_wallet(self.bal_window.wallet)
|
||||||
self.network.run_from_another_thread(tx.add_info_from_network(self.network))
|
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.network.broadcast_transaction(tx, timeout=120), timeout=120
|
||||||
)
|
)
|
||||||
self.msg_set_invalidating(self.msg_ok())
|
self.msg_set_invalidating(self.msg_ok())
|
||||||
if not txid:
|
|
||||||
_logger.debug(f"should not be none txid: {txid}")
|
|
||||||
|
|
||||||
except TxBroadcastError as e:
|
except TxBroadcastError as e:
|
||||||
_logger.error(f"fail to broadcast transaction:{e}")
|
_logger.error(f"fail to broadcast transaction:{e}")
|
||||||
@@ -909,7 +1194,19 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
if tx:
|
if tx:
|
||||||
if tx.is_complete():
|
if tx.is_complete():
|
||||||
self.loop_broadcast_invalidating(tx)
|
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:
|
else:
|
||||||
raise Exception("tx not complete")
|
raise Exception("tx not complete")
|
||||||
else:
|
else:
|
||||||
@@ -946,46 +1243,74 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
# is safe. We then stop and close the wizard.
|
# is safe. We then stop and close the wizard.
|
||||||
if self.have_to_sign == "invalidate_classic":
|
if self.have_to_sign == "invalidate_classic":
|
||||||
self.thread.stop()
|
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
|
# 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
|
# 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
|
# before the new one can be used.
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
# The robust solution is to NOT auto-open any window here. Instead we
|
# We make the CHECK button and the WIZARD behave IDENTICALLY:
|
||||||
# close the wizard and show a clear instruction telling the user to
|
# 1. show a WARNING popup (no "Tools -> Invalidate" wording);
|
||||||
# run "Tools -> Invalidate" themselves. That menu path is already
|
# 2. AUTOMATICALLY open Electrum's classic transaction dialog via
|
||||||
# known to work perfectly (its transaction window always stays in
|
# BalWalletWindow.invalidate_will() (the same code used by the
|
||||||
# front, because no other window is closing at the same time), and
|
# Tools -> Invalidate menu). That path already sets the
|
||||||
# it also makes the user consciously aware that they are performing a
|
# "BAL Invalidate transaction" history label - which fixes
|
||||||
# deliberate, important action (invalidating their old will).
|
# task #03 (the label was previously missing on the automatic
|
||||||
|
# path).
|
||||||
#
|
#
|
||||||
# Close the wizard first so the instruction popup is the only window
|
# Window-stacking note (history): opening the transaction window
|
||||||
# left, then show the guidance message.
|
# 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.close()
|
||||||
self.bal_window.show_message(
|
self.bal_window.show_message(
|
||||||
_(
|
_(
|
||||||
"Your will has expired and must be invalidated before it "
|
"Your will has expired and must be invalidated before it "
|
||||||
"can be rebuilt.\n\n"
|
"can be rebuilt.\n"
|
||||||
"Please use the top-right menu Tools -> Invalidate to "
|
"A transaction window will now open:\n"
|
||||||
"invalidate your old will: a transaction window will open "
|
"please SIGN and then BROADCAST it to invalidate your old "
|
||||||
"where you can sign and broadcast the invalidation.\n\n"
|
"will.\n"
|
||||||
"After the invalidation is confirmed, press the Check "
|
"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
|
return
|
||||||
|
|
||||||
_logger.debug("have to sign {}".format(self.have_to_sign))
|
_logger.debug("have to sign {}".format(self.have_to_sign))
|
||||||
password = None
|
password = None
|
||||||
if self.have_to_sign is None:
|
if self.have_to_sign is None:
|
||||||
_logger.debug("have to invalidate")
|
_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()
|
self.msg_set_invalidating()
|
||||||
# need to sign invalidate and restart phase 1
|
# need to sign invalidate and restart phase 1
|
||||||
|
|
||||||
@@ -1017,6 +1342,26 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
elif self.have_to_sign:
|
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(
|
password = self.bal_window.get_wallet_password(
|
||||||
_("Sign your will"), parent=self
|
_("Sign your will"), parent=self
|
||||||
)
|
)
|
||||||
@@ -1307,8 +1652,18 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
full_text = "<br><br>".join(self.labels).replace("\n", "<br>")
|
full_text = "<br><br>".join(self.labels).replace("\n", "<br>")
|
||||||
self.message_label.setText(full_text)
|
self.message_label.setText(full_text)
|
||||||
self.message_label.adjustSize()
|
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())
|
self.resize(self.sizeHint())
|
||||||
|
# Re-assert the bottom position after the resize/relayout settled.
|
||||||
|
scrollbar.setValue(scrollbar.maximum())
|
||||||
|
|
||||||
def get_text(self):
|
def get_text(self):
|
||||||
return self.message_label.text()
|
return self.message_label.text()
|
||||||
|
|||||||
@@ -491,8 +491,10 @@ class PreviewList(MyTreeView, MessageBoxMixin):
|
|||||||
self.bal_window.bal_plugin.read_file("icons/reload.png")
|
self.bal_window.bal_plugin.read_file("icons/reload.png")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Tooltip so the icon is self-explanatory when hovered.
|
# Tooltip so the icon is self-explanatory when hovered. "Check
|
||||||
refresh.setToolTip(_("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)
|
refresh.clicked.connect(self.check)
|
||||||
|
|
||||||
widget = QWidget(self)
|
widget = QWidget(self)
|
||||||
|
|||||||
@@ -417,6 +417,32 @@ class Plugin(BalPlugin):
|
|||||||
# be saved on a USB stick and a copy given to the heirs).
|
# be saved on a USB stick and a copy given to the heirs).
|
||||||
heir_no_willexecutor = BalCheckBox(self.NO_WILLEXECUTOR)
|
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
|
# Editable line/text widgets are created once and kept in named
|
||||||
# variables so the "Reset" button (Group C / C4b) can refresh the
|
# variables so the "Reset" button (Group C / C4b) can refresh the
|
||||||
# displayed values after resetting the underlying config.
|
# displayed values after resetting the underlying config.
|
||||||
@@ -447,6 +473,21 @@ class Plugin(BalPlugin):
|
|||||||
# Reset/support button row (bottom). Assigning ``QGridLayout(d)`` would
|
# Reset/support button row (bottom). Assigning ``QGridLayout(d)`` would
|
||||||
# have made the grid the dialog's only layout, leaving no room for them.
|
# have made the grid the dialog's only layout, leaving no room for them.
|
||||||
grid = QGridLayout()
|
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(
|
add_widget(
|
||||||
grid,
|
grid,
|
||||||
"Hide Replaced",
|
"Hide Replaced",
|
||||||
@@ -475,7 +516,7 @@ class Plugin(BalPlugin):
|
|||||||
)
|
)
|
||||||
add_widget(
|
add_widget(
|
||||||
grid,
|
grid,
|
||||||
"Editable dates",
|
"Panel editable Date and Fee",
|
||||||
heir_editable_dates,
|
heir_editable_dates,
|
||||||
4,
|
4,
|
||||||
(
|
(
|
||||||
@@ -485,14 +526,38 @@ class Plugin(BalPlugin):
|
|||||||
"When disabled, those dates are display-only outside the wizard."
|
"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(
|
add_widget(
|
||||||
grid,
|
grid,
|
||||||
"Number of reminders",
|
"Number of reminders",
|
||||||
heir_num_reminders,
|
heir_num_reminders,
|
||||||
5,
|
6,
|
||||||
(
|
(
|
||||||
"How many reminder alarms the exported calendar (.ics) event "
|
"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 "
|
"The reminders are spread across the check-alive period and "
|
||||||
"always fall before the delivery deadline.\n"
|
"always fall before the delivery deadline.\n"
|
||||||
"If the period is shorter than the requested number, at most "
|
"If the period is shorter than the requested number, at most "
|
||||||
@@ -503,7 +568,7 @@ class Plugin(BalPlugin):
|
|||||||
grid,
|
grid,
|
||||||
"Event summary",
|
"Event summary",
|
||||||
edit_event_summary,
|
edit_event_summary,
|
||||||
6,
|
7,
|
||||||
(
|
(
|
||||||
"Default message to be used in event summary\n"
|
"Default message to be used in event summary\n"
|
||||||
"Variables:\n"
|
"Variables:\n"
|
||||||
@@ -516,7 +581,7 @@ class Plugin(BalPlugin):
|
|||||||
grid,
|
grid,
|
||||||
"Event description",
|
"Event description",
|
||||||
edit_event_description,
|
edit_event_description,
|
||||||
7,
|
8,
|
||||||
(
|
(
|
||||||
"Default message to be used in event description\n"
|
"Default message to be used in event description\n"
|
||||||
"Variables:\n"
|
"Variables:\n"
|
||||||
@@ -527,21 +592,6 @@ class Plugin(BalPlugin):
|
|||||||
)
|
)
|
||||||
#add_widget(grid, "Bal Mode", bal_mode, 4, "choose bal mode")
|
#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(
|
# add_widget(
|
||||||
# grid,
|
# grid,
|
||||||
# "Ping Willexecutors",
|
# "Ping Willexecutors",
|
||||||
@@ -584,6 +634,7 @@ class Plugin(BalPlugin):
|
|||||||
# Map each config object to the widget that displays it, so we can
|
# Map each config object to the widget that displays it, so we can
|
||||||
# both reset the stored value and update what the user sees.
|
# both reset the stored value and update what the user sees.
|
||||||
resets = [
|
resets = [
|
||||||
|
(self.USER_TYPE, user_type_combo, "user_type"),
|
||||||
(self.HIDE_REPLACED, heir_hide_replaced, "check"),
|
(self.HIDE_REPLACED, heir_hide_replaced, "check"),
|
||||||
(self.HIDE_INVALIDATED, heir_hide_invalidated, "check"),
|
(self.HIDE_INVALIDATED, heir_hide_invalidated, "check"),
|
||||||
(self.AUTO_SIGN, heir_auto_sign, "check"),
|
(self.AUTO_SIGN, heir_auto_sign, "check"),
|
||||||
@@ -607,6 +658,11 @@ class Plugin(BalPlugin):
|
|||||||
widget.setText(cfg.default)
|
widget.setText(cfg.default)
|
||||||
elif kind == "text":
|
elif kind == "text":
|
||||||
widget.setPlainText(cfg.default)
|
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
|
# Refresh the open BAL windows so any dependent view (e.g. the
|
||||||
# editable-dates state is not in this list, but hide filters are)
|
# editable-dates state is not in this list, but hide filters are)
|
||||||
# reflects the reset values.
|
# reflects the reset values.
|
||||||
@@ -638,6 +694,9 @@ class Plugin(BalPlugin):
|
|||||||
# Outer layout: warning (top) -> settings grid -> bottom button row.
|
# Outer layout: warning (top) -> settings grid -> bottom button row.
|
||||||
outer = QVBoxLayout(d)
|
outer = QVBoxLayout(d)
|
||||||
outer.addWidget(lbl_warning)
|
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(grid)
|
||||||
outer.addLayout(bottom_row)
|
outer.addLayout(bottom_row)
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,34 @@ def compute_reminder_offsets(days, count):
|
|||||||
return sorted(offsets, reverse=True)
|
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):
|
class ClickableLabel(QLabel):
|
||||||
doubleClicked = pyqtSignal()
|
doubleClicked = pyqtSignal()
|
||||||
|
|
||||||
@@ -256,6 +284,13 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
|
|||||||
1: self.locktime_date_e,
|
1: self.locktime_date_e,
|
||||||
}
|
}
|
||||||
self.combo.addItems(options)
|
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
|
default_index = 0
|
||||||
if not default_locktime:
|
if not default_locktime:
|
||||||
default_locktime = self.bal_window.bal_plugin.WILL_SETTINGS.get()[self.base_field]
|
default_locktime = self.bal_window.bal_plugin.WILL_SETTINGS.get()[self.base_field]
|
||||||
@@ -264,6 +299,10 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
|
|||||||
default_index = 1
|
default_index = 1
|
||||||
except Exception:
|
except Exception:
|
||||||
default_index = 0
|
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))
|
#hbox.addWidget(QLabel(self.label_text))
|
||||||
help_button=HelpButton(self.help_text)
|
help_button=HelpButton(self.help_text)
|
||||||
help_button.setText(self.label_text)
|
help_button.setText(self.label_text)
|
||||||
@@ -290,6 +329,11 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
|
|||||||
self.set_value(default_locktime)
|
self.set_value(default_locktime)
|
||||||
self.current_value=default_locktime
|
self.current_value=default_locktime
|
||||||
hbox.addWidget(self.combo)
|
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:
|
for w in self.editors:
|
||||||
hbox.addWidget(w)
|
hbox.addWidget(w)
|
||||||
|
|
||||||
@@ -594,13 +638,19 @@ class LockTimeWidget(BalTimeEditWidget):
|
|||||||
"<b>DELIVERY TIME</b><br><br>"
|
"<b>DELIVERY TIME</b><br><br>"
|
||||||
"Set Locktime for transactions.<br>"
|
"Set Locktime for transactions.<br>"
|
||||||
"Any time is needed transaction will be anticipated by 1day<br><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>"
|
"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>"
|
" - 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>"
|
" - y: number of years after currrent day(ex: 1y means one year from today)<br>"
|
||||||
)
|
)
|
||||||
label_text = "🚛"
|
label_text = "🚛"
|
||||||
#label_text = "Locktime"
|
#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"
|
base_field = "locktime"
|
||||||
|
|
||||||
def __init__(self, bal_window, parent, init_value=None):
|
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["threshold"] = ThresholdTimeWidget(bal_window, self)
|
||||||
self.widgets["locktime"].valueEdited.connect(self.on_locktime_change)
|
self.widgets["locktime"].valueEdited.connect(self.on_locktime_change)
|
||||||
self.widgets["threshold"].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.widgets['baltx_fees'].valueChange.connect(self.bal_window.update_setting_widgets)
|
||||||
self.on_locktime_change()
|
self.on_locktime_change()
|
||||||
self.widgets["baltx_fees"] = BalTxFeesWidget(bal_window, self)
|
self.widgets["baltx_fees"] = BalTxFeesWidget(bal_window, self)
|
||||||
@@ -656,53 +715,135 @@ class WillSettingsWidget(QWidget):
|
|||||||
box.addWidget(self.calendar_button)
|
box.addWidget(self.calendar_button)
|
||||||
box.addWidget(self.widgets["baltx_fees"])
|
box.addWidget(self.widgets["baltx_fees"])
|
||||||
else:
|
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,
|
# Vertical layout (the "Build your will" wizard) - Layout H. #
|
||||||
# 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
|
# User requirements (allegato4): #
|
||||||
# rows above).
|
# * the leading ICONS (delivery time, calendar, fee and - in #
|
||||||
#
|
# ADVANCED mode - check-alive) must be aligned one under the #
|
||||||
# IMPORTANT: the leading icons keep their ORIGINAL size. The icons
|
# other on the left; #
|
||||||
# are HelpButtons, which already pin themselves to a fixed width
|
# * the editable FIELDS must all start from the SAME x position, #
|
||||||
# (2.2 * char_width_in_lineedit()); we must NOT widen them, otherwise
|
# just to the right of their icon; #
|
||||||
# they look oversized compared with the original toolbar layout. We
|
# * the fee field must be WIDER (its up/down arrows were covering #
|
||||||
# only need to (1) align the calendar row's left edge with the icons'
|
# the digits) - not a tiny box. #
|
||||||
# original width and (2) cap every row to the date-row width.
|
# #
|
||||||
|
# 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"]
|
locktime_w = self.widgets["locktime"]
|
||||||
threshold_w = self.widgets["threshold"]
|
threshold_w = self.widgets["threshold"]
|
||||||
fees_w = self.widgets["baltx_fees"]
|
fees_w = self.widgets["baltx_fees"]
|
||||||
|
|
||||||
# Original icon width (HelpButton's own fixed width); used only to
|
# Common icon width = the widest leading icon (incl. the calendar
|
||||||
# offset the calendar button so its field starts under the others.
|
# button). Forcing every icon to this width makes the icons line up
|
||||||
icon_w = locktime_w.prefix_widget.sizeHint().width()
|
# 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.
|
||||||
# Common row width = natural width of the date rows (the reference).
|
icon_w = max(
|
||||||
row_w = max(
|
locktime_w.prefix_widget.sizeHint().width(),
|
||||||
locktime_w.sizeHint().width(),
|
threshold_w.prefix_widget.sizeHint().width(),
|
||||||
threshold_w.sizeHint().width(),
|
fees_w.prefix_widget.sizeHint().width(),
|
||||||
|
self.calendar_button.sizeHint().width(),
|
||||||
)
|
)
|
||||||
for w in (locktime_w, threshold_w, fees_w):
|
for icon in (
|
||||||
w.setFixedWidth(row_w)
|
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
|
# WIDEN the fee field (user feedback: the spin-box up/down arrows
|
||||||
# empty spacer of the icon width (calendar field aligned with the
|
# were covering the digits). ~8 chars leaves room for the value and
|
||||||
# date/fee fields) and cap it to the same total width as the rows
|
# the arrows.
|
||||||
# above, so it no longer stretches to the dialog's right edge.
|
fees_w.field_widget.setFixedWidth(8 * char_width_in_lineedit())
|
||||||
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)
|
|
||||||
|
|
||||||
|
# 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(locktime_w, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||||
box.addWidget(threshold_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)
|
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
|
# Group C / C2: apply the current "Editable dates" setting to the date
|
||||||
# fields. Done once at creation here, and re-applied later by
|
# fields. Done once at creation here, and re-applied later by
|
||||||
# apply_editable_dates() whenever the setting changes (called from
|
# 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.
|
# editable outside the wizard only when the setting is ticked.
|
||||||
self.widgets["baltx_fees"].set_read_only(not editable_dates)
|
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):
|
def open_or_save_calendar(self):
|
||||||
"""Build and save an .ics calendar file with SEPARATE reminder events.
|
"""Build and save an .ics calendar file with SEPARATE reminder events.
|
||||||
|
|
||||||
@@ -774,23 +948,43 @@ class WillSettingsWidget(QWidget):
|
|||||||
"""
|
"""
|
||||||
now = BalCalendar.format_time(datetime.now())
|
now = BalCalendar.format_time(datetime.now())
|
||||||
|
|
||||||
# locktime = delivery deadline; threshold = start of the check-alive
|
# locktime = delivery deadline. It is exposed by the date widget as
|
||||||
# period. Both are datetimes exposed by the date widgets as ``.alarm``.
|
# ``.alarm`` and already reflects the (possibly auto-anticipated) minimum
|
||||||
|
# transaction locktime, so the calendar uses the correct delivery date.
|
||||||
locktime = self.widgets["locktime"].alarm
|
locktime = self.widgets["locktime"].alarm
|
||||||
threshold = self.widgets["threshold"].alarm
|
|
||||||
|
|
||||||
# Whole days available between check-alive and the deadline.
|
# BASIC vs ADVANCED reminder strategy.
|
||||||
days = (locktime - threshold).days
|
#
|
||||||
|
# In ADVANCED mode the reminders are spread uniformly across the
|
||||||
# How many reminder events the user asked for (default 3 if unreadable).
|
# check-alive (threshold) period, ending one day before the deadline.
|
||||||
try:
|
#
|
||||||
count = int(self.bal_window.bal_plugin.NUM_REMINDERS.get())
|
# In BASIC mode the check-alive parameter is NOT shown nor managed by the
|
||||||
except Exception:
|
# user (it stays at an arbitrary default), so spreading reminders over it
|
||||||
count = 3
|
# is meaningless. The owner asked that, in BASIC, the calendar simply
|
||||||
|
# saves the inheritance delivery date with three fixed reminders: 30 days
|
||||||
# Day-offsets BEFORE the deadline, e.g. [30, 16, 1]. The list always
|
# before, 10 days before and 1 day before. We also drop any fixed offset
|
||||||
# ends with 1 (one day before the locktime) when >= 2 reminders fit.
|
# that would fall in the past (a reminder before "today" is useless), so
|
||||||
offsets = compute_reminder_offsets(days, count)
|
# 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 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)
|
||||||
|
|
||||||
# Per-event heir details and the shared description/summary templates.
|
# Per-event heir details and the shared description/summary templates.
|
||||||
heirs_details = "\r\n".join(
|
heirs_details = "\r\n".join(
|
||||||
|
|||||||
@@ -466,7 +466,17 @@ class BalWindow:
|
|||||||
self.willexecutors = Willexecutors.get_willexecutors(
|
self.willexecutors = Willexecutors.get_willexecutors(
|
||||||
self.bal_plugin, update=True, bal_window=self, task=False
|
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)
|
raise CheckAliveError(self.date_to_check)
|
||||||
|
|
||||||
self.init_heirs_to_locktime(self.bal_plugin.ENABLE_MULTIVERSE.get())
|
self.init_heirs_to_locktime(self.bal_plugin.ENABLE_MULTIVERSE.get())
|
||||||
@@ -591,7 +601,15 @@ class BalWindow:
|
|||||||
elif isinstance(e, TxFeesChangedException):
|
elif isinstance(e, TxFeesChangedException):
|
||||||
message = "Txfees are changed"
|
message = "Txfees are changed"
|
||||||
elif isinstance(e, HeirNotFoundException):
|
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:
|
if message:
|
||||||
self.show_message(
|
self.show_message(
|
||||||
@@ -1141,11 +1159,12 @@ class BalWindow:
|
|||||||
|
|
||||||
base_msg = _("Downloading will-executors list...")
|
base_msg = _("Downloading will-executors list...")
|
||||||
download_start = time.time()
|
download_start = time.time()
|
||||||
# Upper bound shown to the user. fetch_will_executors_list tries up to
|
# Upper bound shown to the user. Unified with every other network wait
|
||||||
# two endpoints, each with timeout=10 and one retry (~21s worst case),
|
# via the single shared Willexecutors.NETWORK_DEADLINE constant (the
|
||||||
# so ~45s is a realistic maximum. Showing "Xs / 45s" tells the user how
|
# user asked for one consistent 20s value everywhere instead of the old
|
||||||
# long they may have to wait instead of an open-ended counter.
|
# scattered 30s/45s numbers). Showing "Xs / NETWORK_DEADLINEs" tells the
|
||||||
download_deadline = 45
|
# user how long they may have to wait instead of an open-ended counter.
|
||||||
|
download_deadline = Willexecutors.NETWORK_DEADLINE
|
||||||
|
|
||||||
def task():
|
def task():
|
||||||
# Heartbeat: show an elapsed-seconds counter (with the max wait made
|
# Heartbeat: show an elapsed-seconds counter (with the max wait made
|
||||||
@@ -1316,6 +1335,16 @@ class BalWindow:
|
|||||||
_settings_widget.apply_editable_dates()
|
_settings_widget.apply_editable_dates()
|
||||||
except Exception as _edit_err:
|
except Exception as _edit_err:
|
||||||
_logger.debug(f"apply_editable_dates error: {_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:
|
except Exception as e:
|
||||||
_logger.error(f"error while updating window: {e}")
|
_logger.error(f"error while updating window: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bal",
|
"name": "bal",
|
||||||
"fullname": "Bitcoin After Life",
|
"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.",
|
"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",
|
"author": "Svatantrya",
|
||||||
"licence": "MIT",
|
"licence": "MIT",
|
||||||
|
|||||||
@@ -81,6 +81,98 @@ def test_heirs_prepare_lists():
|
|||||||
assert isinstance(result, dict)
|
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)
|
# 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