Files
bal-electrum-plugin/CHANGELOG_REFACTOR.md
2026-06-20 09:50:43 -04:00

692 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BAL — Resoconto del refactoring (per l'autore originale)
Questo documento elenca **tutte** le modifiche apportate al plugin BAL
(Bitcoin After Life) rispetto alla versione originale `0.2.8`.
**Principio guida:** refactoring **conservativo e a comportamento invariato**
(Approccio A). La logica di business è stata mantenuta **byte-identica** dove
possibile; sono cambiati soprattutto la **disposizione dei file** e gli
**import**. Nessuna riscrittura algoritmica.
Ambiente di verifica: **Electrum 4.7.2** + **PyQt6** (l'ultima release stabile
che espone `json_db.register_dict`).
---
## 1. Riorganizzazione della struttura (separazione logica / GUI)
Il problema principale segnalato era che la logica e la grafica erano
mescolate, in particolare in un unico file `qt.py` da **4131 righe**.
### Struttura PRIMA (flat, 7 file)
```
BAL/
├── __init__.py (vuoto, 0 righe)
├── bal.py (243) logica + plugin base
├── util.py (533) helper
├── heirs.py (791) modello eredi + costruzione tx
├── will.py (927) modello will/WillItem
├── willexecutors.py (374) networking will-executor
├── qt.py (4131) TUTTA la GUI + il Plugin in un solo file
└── bal_resources.py (14)
```
### Struttura DOPO (core/ vs gui/)
```
bal/
├── manifest.json metadati conformi allo standard
├── qt.py shim di caricamento (re-export di Plugin)
├── __init__.py docstring di architettura + __version__
├── core/ LOGICA senza dipendenze Qt
│ ├── util.py (ex util.py)
│ ├── plugin_base.py (ex bal.py)
│ ├── heirs.py (ex heirs.py)
│ ├── will.py (ex will.py)
│ └── willexecutors.py (ex willexecutors.py)
└── gui/qt/ PRESENTAZIONE PyQt6
├── theme.py (59) mappatura stato → colore
├── common.py (155) import condivisi + helper GUI
├── widgets.py (782) widget "foglia"
├── calendar.py (80) BalCalendar
├── dialogs.py (1127) finestre di dialogo
├── lists.py (957) viste ad albero (eredi/preview/executor)
├── window.py (952) controller GUI per-wallet (BalWindow)
└── plugin.py (273) classe Plugin (@hook Electrum → GUI)
```
Il file `qt.py` da 4131 righe è stato suddiviso per **responsabilità**. I
**corpi delle classi sono stati copiati verbatim** (riga per riga) per non
toccare la logica delicata delle transazioni di eredità.
### Mappa: dove sono finite le 40 classi/funzioni di `qt.py`
| Classe/funzione (riga orig.) | Nuovo modulo |
|-------------------------------------|-------------------------|
| `Plugin` (67) | `gui/qt/plugin.py` |
| `shown_cv` (317) | `gui/qt/common.py` |
| `BalWindow` (330) | `gui/qt/window.py` |
| `add_widget` (1257) | `gui/qt/common.py` |
| `ClickableLabel` (1263) | `gui/qt/widgets.py` |
| `BalTxFeesWidget` (1271) | `gui/qt/widgets.py` |
| `_LockTimeEditor` (1340) | `gui/qt/widgets.py` |
| `BalTimeEditWidget` (1374) | `gui/qt/widgets.py` |
| `TimeRawEditWidget` (1508) | `gui/qt/widgets.py` |
| `LockTimeRawEdit` (1527) | `gui/qt/widgets.py` |
| `LockTimeDateEdit` (1605) | `gui/qt/widgets.py` |
| `ThresholdTimeWidget` (1644) | `gui/qt/widgets.py` |
| `LockTimeWidget` (1664) | `gui/qt/widgets.py` |
| `WillSettingsWidget` (1683) | `gui/qt/widgets.py` |
| `PercAmountEdit` (1818) | `gui/qt/widgets.py` |
| `BalDialog` (1883) | `gui/qt/dialogs.py` |
| `BalWizardDialog` (1913) | `gui/qt/dialogs.py` |
| `BalWizardWidget` (2002) | `gui/qt/dialogs.py` |
| `BalWizardHeirsWidget` (2068) | `gui/qt/dialogs.py` |
| `BalWizardWEDownloadWidget` (2103) | `gui/qt/dialogs.py` |
| `BalWizardWEWidget` (2190) | `gui/qt/dialogs.py` |
| `BalWizardLocktimeAndFeeWidget`(2207)| `gui/qt/dialogs.py` |
| `BalWaitingDialog` (2224) | `gui/qt/dialogs.py` |
| `BalBlockingWaitingDialog` (2285) | `gui/qt/dialogs.py` |
| `BalLineEdit` (2304) | `gui/qt/widgets.py` |
| `BalTextEdit` (2312) | `gui/qt/widgets.py` |
| `BalCheckBox` (2320) | `gui/qt/widgets.py` |
| `BalBuildWillDialog` (2335) | `gui/qt/dialogs.py` |
| `HeirListWidget` (2858) | `gui/qt/lists.py` |
| `PreviewList` (3059) | `gui/qt/lists.py` |
| `WillDetailDialog` (3445) | `gui/qt/dialogs.py` |
| `WillWidget` (3545) | `gui/qt/widgets.py` |
| `WillExecutorListWidget` (3637) | `gui/qt/lists.py` |
| `WillExecutorWidget` (3873) | `gui/qt/lists.py` |
| `WillExecutorDialog` (3982) | `gui/qt/dialogs.py` |
| `CheckAliveError` (4018) | `gui/qt/common.py` |
| `log_error` (4028) | `gui/qt/common.py` |
| `export_meta_gui` (4043) | `gui/qt/common.py` |
| `BalCalendar` (4066) | `gui/qt/calendar.py` |
---
## 2. Rimozioni (codice morto / debug) — comportamento invariato
Tutte le rimozioni seguenti sono state verificate come **non utilizzate** o
**puramente di debug**, quindi non alterano il comportamento del plugin.
1. **`util.py``core/util.py`**: rimossi tre helper di debug usati solo per
stampe a console:
- `print_var()` (orig. riga 439)
- `print_utxo()` (orig. riga 474)
- `print_prevout()` (orig. riga 486)
2. **`bal.py``core/plugin_base.py`**: rimossa la funzione **stub vuota**
`get_will_settings(x)` (orig. righe 12-14):
```python
def get_will_settings(x):
# print(x)
pass
```
⚠️ Verificato: **non era riferita da nessun `register_dict`** — i tre
`register_dict` usano `tuple`, `dict`, `lambda x: x`. Quindi era codice
morto. La funzione **usata** `get_will(x)` è stata mantenuta identica.
3. **`will.py` (`WillItem`) → spostato in `gui/qt/theme.py`**: il metodo
`WillItem.get_color()` (orig. riga 852) restituiva colori esadecimali —
è logica di **presentazione**, non di dominio. È stato spostato fuori dal
modello e trasformato nella funzione `status_color(will_item)` in
`gui/qt/theme.py`. **Verificato byte-identico** su tutte le combinazioni di
stato (stessa catena di `get_status(...)`, stessi codici colore).
---
## 3. Cambi di import (necessari per la nuova struttura)
Gli import sono stati aggiornati da "flat" a "a package". Esempi:
| Prima | Dopo |
|------------------------------------|---------------------------------------|
| `from .bal import BalPlugin` | `from .plugin_base import BalPlugin` (in willexecutors) |
| `from .util import Util` | `from .util import Util` (invariato, ora dentro core/) |
| (in qt.py) `from .bal import ...` | i moduli GUI importano da `...core.X` |
- Aggiunti `from .common import _, _logger` nei moduli GUI, perché `import *`
**non** esporta i nomi che iniziano con underscore.
- Aggiunti 3 import "lazy" (dentro le funzioni) in `dialogs.py` per spezzare il
ciclo `dialogs ↔ lists` (lists importa `BalBuildWillDialog` da dialogs).
La **logica interna dei metodi** non è stata toccata: `prepare_transactions()`,
`buildTransactions()` ecc. sono verbatim.
---
## 4. Packaging conforme allo standard Electrum
`manifest.json` reso conforme a https://plugins.electrum.org/developers.html :
| Campo | Prima | Dopo |
|------------------|--------------------|-------------------------------|
| `name` | `"BAL"` | `"bal"` (minuscolo = nome dir) |
| `version` | (assente, era solo nella description) | `"0.2.8"` |
| `description` | con `<br>` HTML | testo pulito |
| `licence` | (assente) | `"MIT"` |
| `fullname`/`author`/`available_for`/`icon` | presenti | invariati |
- `__init__.py` (era **vuoto**): ora contiene la docstring di architettura e
`__version__ = "0.2.8"`.
- Portati nel package: `LICENSE`, `VERSION`, `README.md`, `bal_resources.py`,
e la cartella `wallet_util/` (invariata).
---
## 5. CORREZIONE BUG: caricamento come plugin esterno (.zip)
Durante i test su **Electrum 4.7.2 portable per Windows** sono emersi due
problemi reali nel caricare il plugin come **plugin esterno da .zip**:
### Bug 5a — `ModuleNotFoundError: No module named 'electrum_external_plugins'`
- **Causa:** Electrum carica i plugin esterni da zip sotto il package sintetico
`electrum_external_plugins.bal`, ed esegue **solo** l'`__init__` del package e
il modulo `qt`. Non registra il package radice sintetico né i sotto-package
annidati (`gui`, `gui.qt`). Un semplice `from .gui.qt.plugin import Plugin`
fallisce risalendo ai parent mancanti.
- **Fix:** `qt.py` ora è uno shim resiliente che (1) rileva a runtime il proprio
nome di package (`__package__`), (2) ricostruisce in `sys.modules` gli
eventuali package padre mancanti, (3) importa `Plugin` con
`importlib.import_module`. Funziona **sia** come plugin interno
(`electrum.plugins.bal`) **sia** esterno (`electrum_external_plugins.bal`).
### Bug 5b — `zlib.error: Error -5 ... incomplete or truncated stream`
- **Causa:** alcune build portable di Electrum su Windows non riescono a
decomprimere con `zipimport` archivi che contengono **voci di directory** o
compressione non standard.
- **Fix:** aggiunto `build_zip.py`, che genera un archivio "zipimport-friendly":
solo file (nessuna voce di directory), DEFLATE standard, ordine deterministico
(SHA-256 riproducibile), escludendo `__pycache__`/`*.pyc`. Stampa anche
l'hash SHA-256 per verificare l'integrità del download.
---
## 6. Test aggiunti
- `tests/smoke_test.py` — verifica import + comportamento di base
(`BalTimestamp`, helper di `Util`, costanti `HEIR_*`, stati di `WillItem`,
hook del `Plugin`).
- `tests/external_zip_test.py` — riproduce **fedelmente** la sequenza di
caricamento di un plugin esterno da zip di Electrum (regressione per il
Bug 5a/5b).
Tutti i test passano sotto Electrum 4.7.2 + PyQt6.
---
## 7. Riepilogo: cosa NON è cambiato
- La logica di costruzione delle transazioni (`heirs.py`, `will.py`).
- I valori e i tipi di `json_db.register_dict(...)`.
- I codici colore degli stati (solo spostati in `theme.py`).
- L'algoritmo di tutte le classi GUI (copiate verbatim).
- Il formato dei dati salvati nel wallet.
## 8. Note / raccomandazioni
- Il plugin **richiede Electrum 4.7.2**: `json_db.register_dict` è stato
**rimosso** nelle versioni successive (master), dove andrebbe sostituito con
`stored_dict.register_name`. Valutare un adeguamento se si vuole supportare
Electrum più recente.
- Prima del rilascio è consigliata una prova **end-to-end in una sessione
Electrum reale** (preferibilmente su testnet), oltre agli smoke test.
---
## 9. CORREZIONI GUI — finestre e ciclo di vita (B1-B10)
Dopo il refactoring di struttura sono stati corretti **dieci difetti grafici e
di ciclo di vita** delle finestre, già presenti nel codice originale. La logica
di business è rimasta **byte-identica** (nessuna modifica a `bal/core/*`): sono
cambiati solo **presentazione, parent, modalità, z-order, ciclo di vita e
cleanup** delle finestre Qt.
Sintomi segnalati dall'utente, ora risolti:
- **(S1)** le finestre del plugin sparivano dietro la finestra di Electrum;
- **(S2)** alcuni meccanismi funzionavano solo dopo aver chiuso e riavviato
Electrum.
| ID | Problema (presente nell'originale) | Correzione applicata |
|-----|----------------------------------------------------------------------|----------------------|
| B1 | `self.parent = parent` sovrascriveva il metodo `parent()` di Qt, rompendo la gerarchia delle finestre | rinominato in `self._bal_parent` (in `dialogs.py`, `lists.py`, `widgets.py`); il parent reale passa da `top_level_of(parent)` |
| B2 | dialoghi aperti con `.show()` non modale → finivano sotto la finestra principale | sostituiti con `show_on_top()` / `show_modal()` e parent corretto |
| B3 | messaggio "Please restart Electrum to activate the BAL plugin": il plugin si attivava solo dopo riavvio | inizializzazione **a caldo** con `_setup_window()` che replica `load_wallet` — niente più riavvio |
| B4 | chiave del dizionario finestre usava il **metodo** `winId` invece del valore | chiave stabile `_window_key()` basata su `id(window)` |
| B5 | `on_close` ingoiava tutti gli errori con `except: pass` | riscritto: niente `except:pass`, log per ogni passo, reset pulito dello stato |
| B6 | `BalBlockingWaitingDialog` bloccava il thread della GUI (`processEvents` commentato) | ripristinato `processEvents()` → GUI reattiva durante l'attesa |
| B7 | `closeEvent`/`hideEvent` con cleanup del thread commentato | gestione esplicita di `closeEvent`/`hideEvent` + chiamata a `super()` |
| B8 | `closeEvent` incompleto in alcuni dialog | gestione uniforme dello stato di chiusura |
| B9 | `show()+raise_()` senza `activateWindow()` né modalità → finestra non in primo piano | `bring_to_front()` = `raise_()` + `activateWindow()` |
| B10 | gestione multi-wallet / multi-finestra fragile; menu cercato per titolo `&Tools` | uso dell'API ufficiale `window.tools_menu` |
### Nuovo modulo: `gui/qt/window_utils.py` (119 righe)
Gli helper per la gestione delle finestre sono stati **centralizzati** in un
unico modulo, così la stessa logica non viene duplicata nei vari dialog:
- `top_level_of(widget)` — risale alla finestra di primo livello corretta da
usare come parent;
- `bring_to_front(window)` — `raise_()` + `activateWindow()` per portare in
primo piano;
- `stop_thread(thread)` — stop+wait sicuro di un `TaskThread`;
- `show_modal(dialog)` — apertura modale corretta (`exec()`);
- `show_on_top(window)` — apertura non modale ma sopra le altre finestre.
`gui/qt/common.py` importa questi helper e li rende disponibili al resto della
GUI.
---
## 10. CORREZIONE BUG: download lista will-executor
Dopo l'installazione del pacchetto con le correzioni GUI, l'utente ha
segnalato che il comando **"download list"** dei will-executor non scaricava
più la lista.
### Indagine
Il codice di rete (`core/willexecutors.py`: `send_request`, `handle_response`,
`download_list`, `initialize_willexecutor`) è stato confrontato riga per riga
con l'originale Gitea ed è risultato **byte-identico** (l'unica differenza è il
parametro aggiuntivo `welist_server` in `download_list`, retro-compatibile).
Durante l'indagine sono comunque emersi e stati corretti **due difetti reali**
introdotti dalle correzioni GUI, che potevano "perdere" il risultato del
download:
1. **`BalDialog.closeEvent`/`hideEvent` fermavano il `TaskThread`.** In Electrum
`TaskThread.on_done` esegue `cb_done` (cioè `self.accept`, che **chiude** il
dialog) **prima** di `cb_result` (cioè `on_success`, che **aggiorna** la
lista). Fermare il thread alla chiusura del dialog **scartava** quindi il
risultato appena scaricato. → I due metodi sono stati riportati a **non**
fermare il thread (con commento esplicativo nel codice).
2. **`BalWaitingDialog.exe()` usava una modalità sbagliata** (`show_modal` /
`WindowModal`). → Ripristinato l'originale `self.exec()`, aggiungendo prima
`bring_to_front(self)` per garantire il primo piano.
Inoltre i percorsi del **pulsante** e del **wizard** (che prima scaricavano in
modi diversi e con messaggi diversi) sono stati **unificati** in un unico
helper `fetch_will_executors_list`, eseguito dentro il worker del `TaskThread`.
### Causa vera del mancato download: ambientale, NON del plugin
Una probe di controllo con `urllib` che **bypassava completamente Electrum**
falliva ugualmente con `WinError 10054` ("connection forcibly closed by remote
host"): segno che la **rete/ISP dell'utente resettava la connessione HTTPS**
verso `welist.bitcoin-after.life`. La conferma definitiva: **attivando una VPN
il download è andato a buon fine.**
L'originale "sembrava" funzionare perché spedisce comunque un will-executor di
**default già incorporato** (`https://we.bitcoin-after.life`), quindi la lista
non risultava mai del tutto vuota anche senza un download riuscito.
### Pulizia finale (scelta dall'utente — "Opzione 1")
- **Finestra di attesa non bloccante** mantenuta (`BalWaitingDialog`), così la
GUI non si congela durante il download.
- **Fallback dell'URL**: prima l'URL configurato (`WELIST_SERVER`), poi quello
hardcoded `https://welist.bitcoin-after.life/`.
- **Diagnostica dettagliata spostata nei soli log** (rimossa la probe `urllib`
dall'interfaccia).
- **Messaggio d'errore semplice per l'utente, in inglese** (`DOWNLOAD_FAILED_MESSAGE`):
> *"Could not download the will-executors list. This is usually caused by
> your internet connection or a firewall, not by the plugin. Please check
> your connection (a VPN often helps) and try again."*
### File toccati (solo presentazione/GUI, logica invariata)
- `gui/qt/window.py` — helper condiviso `fetch_will_executors_list`,
`download_list` con `TaskThread` + `BalWaitingDialog`, costante
`DOWNLOAD_FAILED_MESSAGE`.
- `gui/qt/lists.py` — `WillExecutorWidget.download_list` instradato sul
percorso condiviso con `on_success` che aggiorna/salva la lista.
- `gui/qt/dialogs.py` — `BalDialog.closeEvent`/`hideEvent` **non** fermano più
il thread; `BalWaitingDialog.exe()` torna a `self.exec()` + `bring_to_front`.
- `tests/gui_fixes_test.py` — asserzione di **regressione**: verifica che
`closeEvent`/`hideEvent` **non** contengano `stop_thread` (per non
reintrodurre il bug che scartava il download).
---
## 11. Confronto strutturale finale (originale Gitea → refactor)
Conteggio file `.py` (escluse cartelle generate):
| Originale (Gitea) | righe | → | Refactor (`bal/`) | righe |
|------------------------------|------:|----|-------------------------------------------|------:|
| `__init__.py` | 1 | → | `__init__.py` | 37 |
| `bal.py` | 161 | → | `core/plugin_base.py` | 351 |
| `util.py` | 1051 | → | `core/util.py` | 614 |
| `heirs.py` | 792 | → | `core/heirs.py` | 806 |
| `will.py` | 903 | → | `core/will.py` | 938 |
| `willexecutors.py` | 547 | → | `core/willexecutors.py` | 390 |
| `qt.py` (monolite GUI) | 3777 | → | suddiviso in `gui/qt/*` (vedi sotto) | — |
| `bal_resources.py` | 14 | → | `bal_resources.py` | 14 |
| `wallet_util/*.py` | 275 | → | `wallet_util/*.py` (invariati) | 280 |
Suddivisione del vecchio `qt.py` (3777 righe) nei moduli GUI:
| Modulo refactor | righe | Contenuto |
|-----------------------------|------:|-----------|
| `gui/qt/plugin.py` | 303 | classe `Plugin` (`@hook` Electrum → GUI) |
| `gui/qt/window.py` | 1048 | `BalWindow` (controller per-wallet) |
| `gui/qt/dialogs.py` | 1155 | finestre di dialogo + wizard |
| `gui/qt/lists.py` | 964 | viste ad albero (eredi/preview/executor) |
| `gui/qt/widgets.py` | 782 | widget "foglia" |
| `gui/qt/common.py` | 157 | import condivisi + helper |
| `gui/qt/window_utils.py` | 119 | helper finestre (NUOVO — vedi §9) |
| `gui/qt/calendar.py` | 80 | `BalCalendar` |
| `gui/qt/theme.py` | 59 | mappatura stato → colore |
| `gui/qt/__init__.py` | 17 | init package GUI |
> Le differenze nei conteggi di righe rispetto all'originale derivano da:
> riformattazione/commenti, separazione degli import per modulo, e spostamento
> di funzioni tra `util.py`/`bal.py` e i nuovi moduli. **Gli algoritmi non sono
> stati modificati.**
---
## 12. Cronologia delle modifiche su GitHub
- **`4198a51`** — import iniziale del refactor strutturale (v0.2.8): separazione
`core/` (logica) vs `gui/qt/` (presentazione), packaging conforme, fix
caricamento zip esterno, smoke test (sezioni §1-§8).
- **`d56fa36`** — questo changelog del refactoring (in italiano).
- **`4806997`** — `DIAGNOSI_GUI.md`: diagnosi dei bug GUI di z-order e ciclo di
vita (Fase A).
- **`dd6f677`** (PR **#2**, squash) — correzioni GUI **B1-B10** + fix download
lista will-executor + `window_utils.py` + test di regressione (sezioni §9-§10).
- **PR #3** — fix **OverflowError su Windows (anno 2038)** che rompeva le schede
Will/Heirs e la voce di menu (sezione §13).
---
## 13. CORREZIONE BUG: OverflowError su Windows (limite anno 2038)
### Sintomo (Windows 11)
Dopo aver **riavviato Electrum** o **cambiato wallet**, le schede **Will** e
**Heirs** sparivano e compariva una **voce di menu condensata/illeggibile**
(icona + testo sovrapposti) sotto il logo di Electrum, accanto a *Portafogli*.
Su Linux il problema non si manifestava.
### Causa vera (dal log di Electrum dell'utente)
```
OverflowError: Python int too large to convert to C int
window.py __init__ -> create_heirs_tab -> WillSettingsWidget
-> on_locktime_change -> BalTimestamp.to_date
-> datetime.fromtimestamp(NLOCKTIME_MAX)
```
- `NLOCKTIME_MAX = 2**32 - 1 = 4294967295` viene usato come locktime di
**default/sentinella**.
- Su **Windows** `time_t` è a **32 bit**, quindi `datetime.fromtimestamp(ts)`
solleva **`OverflowError`** per qualsiasi timestamp oltre il **2038**.
- Su **Linux 64-bit** la stessa chiamata **funziona**: ecco perché il bug si
vedeva solo su Windows e i test su Linux non lo intercettavano.
- L'eccezione interrompeva `BalWindow.__init__` durante `init_menubar` /
`load_wallet`, lasciando le schede Will/Heirs e la voce di menu **a metà
costruzione** → l'elemento grafico condensato/illeggibile sotto il logo.
> Nota: i due primi tentativi di correzione (status-bar no-op e idempotenza di
> `init_menubar_tools`) **non** centravano la causa; sono stati comunque
> mantenuti perché innocui e leggermente migliorativi, ma il vero colpevole era
> questo crash a monte.
### Fix (comportamento invariato per tutti i valori normali)
- **`BalTimestamp._safe_fromtimestamp()`**: `datetime.fromtimestamp` con
**clamp a INT32_MAX** (anno 2038) in caso di `OverflowError`/`OSError`/
`ValueError`, **esattamente** come la funzione `get_max_allowed_timestamp()`
dell'originale (workaround per Electrum issue **#6170**).
- Usato in `to_date` / `to_timestamp` / `__str__` / `__repr__` di
`BalTimestamp`.
- `gui/qt/widgets.py` (`set_value`): usa il converter sicuro.
- `core/util.py` (`timestamp_minus`): stessa protezione inline con clamp a
INT32_MAX.
I valori entro il 2038 (date assolute normali, durate relative come `90d`/`5y`)
producono **lo stesso identico risultato** di prima.
### Test
- `tests/windows_overflow_test.py` riproduce il limite 32-bit di Windows
(monkeypatch di `datetime.fromtimestamp`) e dimostra che **senza** il fix si
ottiene lo **stesso** `OverflowError` del log, mentre **con** il fix passa.
Verificato anche che il test **fallisce** senza il fix.
Confermato dall'utente: **"si ora funziona"**.
## 14. NUOVA FUNZIONE: invalidazione automatica al posticipo dell'eredità
### Problema
Una transazione di eredità viene firmata con un **locktime fisso e immutabile**
e inviata ai will-executor, che sono economicamente incentivati a trasmetterla
(incassano le fee). Se l'utente, dopo aver firmato/inviato, **posticipa** la
data di consegna (es. di un anno), la **vecchia** transazione gia firmata resta
valida sui server dei will-executor. Poiche ha il locktime piu basso, un
will-executor potrebbe trasmetterla appena scade, eseguendo l'eredita **in
anticipo** rispetto alla nuova volonta dell'utente. La versione precedente
**non gestiva** questo caso: il posticipo non produceva alcuna azione.
### Soluzione (Strategia B — invalidazione esplicita on-chain)
Al posticipo di un'eredita **gia firmata e/o inviata** (stato `COMPLETE` o
`PUSHED`), il plugin chiede di **invalidare on-chain** i fondi prima di
ricostruire la nuova eredita. L'invalidazione spende gli stessi UTXO verso un
nuovo indirizzo di change con `locktime = altezza corrente` (RBF), quindi e
trasmettibile subito: una volta confermata, la vecchia transazione pre-firmata
diventa **definitivamente inutilizzabile**, vincendo la corsa contro qualunque
will-executor.
### Dettagli tecnici
- **`core/will.py`**:
- nuova eccezione `WillPostponedException` (sottoclasse di
`NotCompleteWillException`);
- `check_willexecutors_and_heirs`: il confronto del locktime non usa piu
l'entry dell'erede memorizzata (`their[2]`), che viene aggiornata in memoria
insieme al nuovo valore al momento del posticipo e quindi risulterebbe
sempre uguale. Ora confronta il locktime richiesto con **`w.tx.locktime`**,
cioe il locktime **congelato** nella transazione firmata (immutabile, e
quello che i will-executor possiedono). Tre casi: invariato → coerente;
nuovo > tx su will firmato/inviato → `WillPostponedException`; nuovo > tx su
will mai inviato → semplice ricostruzione (nessuna fee on-chain).
- **`gui/qt/dialogs.py`** (`BalBuildWillDialog.task_phase1`, il percorso reale
usato da **Tools → Prepare**): aggiunto il ramo `except WillPostponedException`
**prima** di `NotCompleteWillException`; si comporta come il caso "will
scaduto" e ritorna `(None, tx)` per innescare firma + broadcast
dell'invalidazione. L'utente preme di nuovo **Prepare** per ricostruire,
rifirmare e reinviare la nuova eredita (due passi espliciti, per maggior
controllo).
- **`gui/qt/window.py`** (`build_inheritance_transaction`): aggiunto lo stesso
ramo per completezza del percorso alternativo, con messaggio esplicativo.
- **`gui/qt/common.py`**: `WillPostponedException` esportato.
### NUOVA COLONNA "Server" nella lista transazioni
Per dare all'utente visibilita costante sullo stato online delle proprie
transazioni di eredita, e stata aggiunta una colonna dedicata **"Server"** in
`PreviewList` (`gui/qt/lists.py`), con etichetta sempre leggibile
(`Confirmed on server`, `Sent (not checked)`, `Send failed`, `Not on server`,
`Signed (not sent)`, `Not sent`) e **tooltip** con URL del will-executor e
stato. Le funzioni `server_status_text()` e `server_status_tooltip()` sono in
`gui/qt/theme.py` e riusano gli stessi flag di stato gia esistenti.
### Test
- I 182 test ufficiali continuano a passare; smoke test ed external-zip test
OK; `ruff` senza nuove segnalazioni reali.
- Verificato sui dati reali del log dell'utente: il posticipo di un'eredita
firmata ora rileva correttamente la condizione e avvia l'invalidazione.
Confermato dall'utente: **"mi pare che funziona"**.
## 15. TENTATIVO E REVERT: fix doppia invalidazione al posticipo (v0.3.1 -> v0.3.2)
### v0.3.1 (RITIRATA)
Per risolvere la doppia firma dell'invalidazione al posticipo, era stato
introdotto `Will.mark_invalidated_by_tx()`, chiamato in
`loop_broadcast_invalidating` dopo il broadcast dell'invalidazione, per marcare
`INVALIDATED` le will che spendevano gli stessi UTXO della tx di invalidazione
e persistere lo stato con `save_willitems`.
### Perche e stata ritirata
La modifica ha introdotto una regressione grave segnalata dall'utente:
**la lista eredita mostrava ancora le vecchie eredita e l'aggiornamento di
eredi/date risultava incoerente**.
Causa: `loop_broadcast_invalidating` e il punto di broadcast usato per **TUTTI**
i tipi di invalidazione (posticipo, CheckAlive, will scaduto/anticipato), non
solo per il posticipo. Inoltre il metodo marcava e **persisteva** lo stato
`INVALIDATED` su tutte le will item che condividevano gli UTXO del wallet
(tipicamente tutte). Queste will item invalidate restavano poi in memoria e su
disco, inquinando la ricostruzione di eredi/date e lasciando vecchie voci nella
lista.
### v0.3.2 (questa versione): REVERT completo
- Rimosso `Will.mark_invalidated_by_tx()` da `core/will.py`.
- Rimossa la chiamata in `gui/qt/dialogs.py` (`loop_broadcast_invalidating`):
il metodo torna **identico** alla v0.3.0.
- Rimossi i due test relativi; mantenuto solo l'assert di gerarchia su
`WillPostponedException` (corretto e indipendente).
- `core/will.py` e `gui/qt/dialogs.py` sono ora **byte-identici** alla v0.3.0
funzionante (verificato con `git diff a394cde`).
Il bug della doppia invalidazione al posticipo resta quindi **aperto** e andra
riaffrontato in modo piu mirato (senza toccare il percorso di broadcast comune e
senza persistere stati su will che condividono gli UTXO), previa conferma
dell'utente. La priorita era ripristinare il comportamento corretto di
lista/eredi/date.
## 16. Aggiornamenti mancati, Check/Close coerenti, e rifinitura UI (v0.3.2)
### FIX 1 - Rimozione di un erede rilevata su Check / chiusura Electrum
`core/will.py` (`check_willexecutors_and_heirs`): prima il plugin
rilevava solo l'**aggiunta** di un erede (raise `HeirNotFoundException` quando un
erede corrente non era piu nella will). Mancava il caso inverso: la
**rimozione** di un erede. Aggiunto il ramo `else` che lancia
`HeirNotFoundException` anche quando la will porta ancora un erede che non e piu
presente nel set di eredi corrente. Cosi la ricostruzione dell'eredita scatta su
**Check** e alla **chiusura di Electrum** (entrambi usano lo stesso percorso
`BalBuildWillDialog.build_will_task()`), come deciso dall'utente: nessun
aggiornamento automatico dopo la modifica, solo manuale con Check / alla
chiusura.
### FIX 2 - Check interroga i server anche per le will gia inviate
`core/will.py` (nuovo `Will.needs_server_check(w)`) e
`gui/qt/lists.py` (`PreviewList.check`): prima il Check interrogava i server solo
per le will in stato `PUSHED`. Le will gia inviate ma rimaste su "New / Not sent"
non venivano ricontrollate ("nothing to do"). Ora `needs_server_check` include
ogni will **VALID** con un will-executor e **non ancora CHECKED**, anche se non
in stato `PUSHED`. Stesso controllo usato sia dal pulsante Check sia da
`on_close`.
### FIX 3 - Hide invalidated/replaced da finestra Impostazioni aggiornava la lista
`core/plugin_base.py` (nuovo `sync_hide_filters()`) e
`gui/qt/window.py` (`update_all`): le checkbox "Hide Replaced" / "Hide
Invalidated" nella finestra Impostazioni scrivono direttamente la config
(`BalConfig.set`) senza toccare i flag in cache `_hide_invalidated` /
`_hide_replaced` usati dalla lista per filtrare. Risultato: la lista continuava
a filtrare col valore vecchio finche non si riavviava Electrum. Ora
`update_all()` chiama `sync_hide_filters()` che ri-legge i flag dalla config,
quindi qualunque sorgente del cambiamento (toolbar o finestra Impostazioni)
aggiorna subito la lista.
### Rifinitura UI - Risultati in grassetto nel dialog "Building Will"
`gui/qt/dialogs.py` (`BalBuildWillDialog`): i **risultati** mostrati a destra di
ogni riga di stato (es. `Ok`, `Ko`, `Nothing to do`, `Skipped`, `Wait`,
`Timeout`) sono ora resi in **grassetto**, mantenendo i loro colori
(verde/rosso/giallo). Le etichette di stato a sinistra restano in peso normale.
Modifica centralizzata negli helper `msg_ok`, `msg_error`, `msg_warning`,
`msg_set_status`, piu le righe dei will-executor (push e check) che ora mostrano
`Ok/Ko` e `True/False` in grassetto + colore (verde/rosso).
### Test
- 186 test ufficiali passano; smoke test, external-zip test e simulazione dei
flussi di aggiornamento (`tests/sim_update_flows.py`) OK; `ruff` senza nuove
segnalazioni reali (solo falsi positivi pre-esistenti da star-import).
- Aggiunti test in `tests/test_core_will.py`:
`test_check_heirs_unchanged_is_coherent`,
`test_check_heir_removed_triggers_rebuild`,
`test_check_heir_added_triggers_rebuild`, `test_needs_server_check`.
Confermato dall'utente sui dati reali: dopo Sign -> Broadcast -> Check le
transazioni gia inviate sono tornate verdi ("confirmed on server"); la lista
torna pulita; il grassetto e l'aggiornamento delle hide-flag funzionano.
## 17. UI polish and bug fix — signed-tx colour, wizard, Building Will dialog (v0.3.3)
This session groups several behaviour-invariant UI refinements plus one colour
bug fix. All comments and code remain in English; only the chat with the
author was in Italian.
### FIX — Signed-but-not-sent transaction shown RED instead of blue
`core/will.py` (`needs_server_check`): a previous-session change (section 16,
"FIX 2") had removed the `PUSHED` requirement from `needs_server_check`, so a
will that was *signed but never broadcast* was still server-queried. The query
returned CHECK_FAIL, and because `status_color()` checks CHECK_FAIL (red,
`#e83845`) before COMPLETE (blue, `#2bc8ed`), the row turned red. Restored the
original Gitea `check()` condition by adding back `and w.get_status("PUSHED")`,
so only already-broadcast wills are server-checked. A signed-but-not-sent will
now stays blue (COMPLETE) as in the original.
- `tests/test_core_will.py` (`test_needs_server_check`): a freshly-built item
(VALID, not PUSHED) now correctly expects `False`.
### Wizard "Will Settings" — equal-width, left-aligned rows
`gui/qt/widgets.py` (`WillSettingsWidget`, vertical layout): the calendar button
and the fee field used to stretch to the dialog's right edge, far wider than the
date rows. Now every row is capped to the widest date-row width (`row_w`) and
left-aligned, so they form a tidy column. The leading icons keep their original
`HelpButton` width (`icon_w` is used only as a spacer in front of the calendar,
never to widen the icons themselves).
### Wizard button — icon + text
`gui/qt/lists.py` (`create_toolbar`): the "build your will" toolbar button is now
more inviting: a 28×28 wizard icon plus a bold `"Create your will"` caption,
`setMinimumHeight(40)`. `gui/qt/common.py` gained `QSize` in the QtCore import.
### Building Will dialog — clearer final report + manual Close
`gui/qt/dialogs.py` (`BalBuildWillDialog`):
- The closing summary line is no longer a bare "Ok": it now has an explicit
left-side label, `"All done: Ok"`, like the other result rows.
- A blank separator row is inserted above "All done" so the overall outcome is
visually detached from the per-step rows.
- The four `"checking variables"` status strings are capitalised to
`"Checking variables"` to match the rows below; the redundant trailing colon
on the final one was dropped (`msg_set_status` already adds `":\t"`).
- The final auto-closing countdown (`self.wait(5)`) was replaced by an explicit
right-aligned **"Close"** button (`_add_close_button` / `_on_close_clicked`).
The dialog now stays open until the user dismisses it, so the full report can
be read at leisure. The intermediate technical pauses (`wait(10)`, `wait(5)`,
`wait(3)`) are kept. Closing still shows the persistent "next steps"
(Sign / Broadcast) popup when `self._next_steps_hint` is set.
### Preview helpers (dev-only, not shipped logic)
`tests/preview_wizard_settings_align.py`, `tests/preview_wizard_button.py`,
`tests/preview_building_will_close_btn.py`: small offscreen scripts used to
render before/after mock-ups for visual approval.
### Test
- 186 official tests pass; smoke test, external-zip test OK.
- `ruff` reports only the pre-existing baseline false positives (F401/F403/F405
star-import re-exports, one F841, one F541) — no new issues.
- Version bumped to **0.3.3** (`bal/VERSION`, `bal/__init__.py`,
`bal/manifest.json`).
## 18. Documentation site (docs/) — no behaviour change
Added a `docs/` tree that renders directly on GitHub and GitHub Pages (no PDF):
- `docs/inheritance-options.md` + `.html`: a codeaccurate **Inheritance Options
Guide** covering every change a user can make (date earlier/later, add/remove
heir, change percentages, fees, willexecutors), each transaction status flag
and its colour, and what happens on the willexecutor servers. Includes a
decision **flow chart** (GitHubnative Mermaid block in the `.md`, plus a
static SVG fallback `docs/images/inheritance-flow.svg`, plus a live Mermaid
render in the `.html`). Behaviour is derived directly from
`core/will.py::is_will_valid` / `check_willexecutors_and_heirs` and
`gui/qt/window.py::build_inheritance_transaction`.
- `docs/manual/README.md` + `manual.html` + `images/`: the official **BAL User
Manual (revB)** converted from the upstream Gitea PDF into GitHubfriendly
Markdown/HTML with the original screenshots rerendered at high resolution
(`docs/manual/images/fig*.png`, `logo.png`).
- `docs/README.md`: documentation index linking both documents.
No plugin code changed; 186 tests still pass.