# 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 `
` 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. BUGFIX: doppia invalidazione al posticipo dell'eredita (v0.3.1) ### Sintomo segnalato dall'utente Posticipando il delivery time, il plugin chiedeva di firmare **due volte** la transazione di invalidazione ("Invalidate your old will"), e solo dopo faceva firmare la nuova eredita ("Prepare new will"). ### Causa Il percorso di posticipo era: 1. `task_phase1` rileva il posticipo -> `WillPostponedException` -> costruisce la tx di invalidazione -> 1ª firma. 2. Dopo il broadcast, `on_success_invalidate` **riavvia** `task_phase1` per ricostruire la nuova eredita. 3. **Ma** le will item vecchie erano ancora marcate `COMPLETE`/`PUSHED` con la stessa `tx.locktime` di prima (l'invalidazione on-chain non aggiornava lo stato in memoria), quindi la condizione del posticipo scattava di **nuovo** -> `WillPostponedException` -> **2ª** firma di invalidazione. 4. Solo al terzo giro la will veniva finalmente ricostruita. ### Correzione - **`core/will.py`**: nuovo metodo statico `Will.mark_invalidated_by_tx(will, tx)` che marca come `INVALIDATED` ogni will valida che spende almeno uno dei prevout consumati dalla tx di invalidazione appena trasmessa. Settare `INVALIDATED` azzera automaticamente il flag `VALID` (logica gia esistente in `WillItem.set_status`), cosi quelle will escono da `only_valid_list`. - **`gui/qt/dialogs.py`** (`loop_broadcast_invalidating`): dopo il broadcast **riuscito** (quando si ottiene il `txid`), si chiama `mark_invalidated_by_tx` e si salva. Al riavvio di `task_phase1` le vecchie will non sono piu `VALID`, quindi `WillPostponedException` non viene piu sollevata e la will viene ricostruita direttamente. Risultato: **una sola** firma di invalidazione, poi la new will. ### Test - Aggiunti 2 test in `tests/test_core_will.py` (`test_will_mark_invalidated_by_tx`, `test_will_mark_invalidated_by_tx_no_match`) e l'assert di gerarchia per `WillPostponedException`. - 184 test ufficiali passati; smoke test ed external-zip test OK; `ruff` senza nuove segnalazioni. Confermato dall'utente: **"confermo che funziona"**.