forked from bitcoinafterlife/bal-electrum-plugin
add remaining root files
This commit is contained in:
691
CHANGELOG_REFACTOR.md
Normal file
691
CHANGELOG_REFACTOR.md
Normal file
@@ -0,0 +1,691 @@
|
|||||||
|
# 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 code‑accurate **Inheritance Options
|
||||||
|
Guide** covering every change a user can make (date earlier/later, add/remove
|
||||||
|
heir, change percentages, fees, will‑executors), each transaction status flag
|
||||||
|
and its colour, and what happens on the will‑executor servers. Includes a
|
||||||
|
decision **flow chart** (GitHub‑native 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 GitHub‑friendly
|
||||||
|
Markdown/HTML with the original screenshots re‑rendered 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.
|
||||||
319
DIAGNOSI_GUI.md
Normal file
319
DIAGNOSI_GUI.md
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
# BAL — Diagnosi dei problemi GUI (Fase A) → ✅ RISOLTI (Fase B)
|
||||||
|
|
||||||
|
> **STATO: tutti i bug B1-B10 sono stati CORRETTI** e mergiati in `main`
|
||||||
|
> (PR #2, squash `dd6f677`). La logica di business resta **byte-identica**
|
||||||
|
> (nessuna modifica a `bal/core/*`): sono cambiati solo presentazione, parent,
|
||||||
|
> modalità, ciclo di vita e cleanup delle finestre.
|
||||||
|
>
|
||||||
|
> | ID | Stato | Fix applicato |
|
||||||
|
> |----|-------|---------------|
|
||||||
|
> | B1 | ✅ FIXED | `self.parent` → `self._bal_parent` (dialogs/lists/widgets); parent = `top_level_of(parent)` |
|
||||||
|
> | B2 | ✅ FIXED | `.show()` → `show_on_top()` / `show_modal()` con parent corretto |
|
||||||
|
> | B3 | ✅ FIXED | init a caldo: `_setup_window()` replica `load_wallet`, niente "restart Electrum" |
|
||||||
|
> | B4 | ✅ FIXED | chiave finestra stabile `_window_key()` = `id(window)` |
|
||||||
|
> | B5 | ✅ FIXED | `on_close` riscritto: niente `except:pass`, log per-step, reset stato |
|
||||||
|
> | B6 | ✅ FIXED | `BalBlockingWaitingDialog`: `processEvents()` ripristinato |
|
||||||
|
> | B7 | ✅ FIXED | `closeEvent/hideEvent`: `stop_thread()` + `super()` |
|
||||||
|
> | B8 | ✅ FIXED | `closeEvent`: `stop_thread()` (stop+wait) + `super()` |
|
||||||
|
> | B9 | ✅ FIXED | `bring_to_front()` = `raise_()` + `activateWindow()` |
|
||||||
|
> | B10| ✅ FIXED | uso di `window.tools_menu` (API ufficiale), niente ricerca per titolo `&Tools` |
|
||||||
|
>
|
||||||
|
> Helper centralizzati in `bal/gui/qt/window_utils.py`:
|
||||||
|
> `top_level_of`, `bring_to_front`, `stop_thread`, `show_modal`, `show_on_top`.
|
||||||
|
> Test di regressione: `tests/gui_fixes_test.py` (oltre a smoke + external_zip).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## (Storico) Diagnosi originale
|
||||||
|
|
||||||
|
Documento di sola **diagnosi**: nessuna riga di codice funzionale era stata
|
||||||
|
modificata in Fase A. Elenca i problemi grafici/di ciclo di vita riscontrati nel
|
||||||
|
codice, la loro **causa tecnica** e il **fix proposto**, con riferimenti riga.
|
||||||
|
|
||||||
|
I due sintomi che hai segnalato:
|
||||||
|
- **(S1)** Le finestre del plugin spariscono dietro la finestra di Electrum.
|
||||||
|
- **(S2)** Alcuni meccanismi funzionano solo dopo aver chiuso e "ripulito"
|
||||||
|
Electrum.
|
||||||
|
|
||||||
|
Sono entrambi spiegati dai bug qui sotto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Riepilogo (tabella)
|
||||||
|
|
||||||
|
| ID | Gravità | Sintomo | File:riga | Causa breve |
|
||||||
|
|----|---------|---------|-----------|-------------|
|
||||||
|
| B1 | 🔴 Alta | S1 | `dialogs.py:40,69,475` | `self.parent = parent` sovrascrive il metodo `QWidget.parent()` |
|
||||||
|
| B2 | 🔴 Alta | S1 | `window.py:148,936`, `window.py:566` | dialoghi aperti con `.show()` (non-modali, senza stare in primo piano) |
|
||||||
|
| B3 | 🔴 Alta | S2 | `plugin.py:38-42` | messaggio "Please restart Electrum" = init a caldo non gestito |
|
||||||
|
| B4 | 🔴 Alta | S2 | `plugin.py:45,111` | chiave dizionario `winId` (metodo) invece di `winId()` (valore) |
|
||||||
|
| B5 | 🟠 Media | S2 | `window.py:664-677` | `on_close` con `except: pass` che nasconde errori di cleanup |
|
||||||
|
| B6 | 🟠 Media | S1/S2 | `dialogs.py:445-462` | `BalBlockingWaitingDialog` blocca il thread GUI, `processEvents` commentato |
|
||||||
|
| B7 | 🟠 Media | S2 | `dialogs.py:48-58` | `closeEvent/hideEvent` con cleanup thread commentato |
|
||||||
|
| B8 | 🟠 Media | S2 | `dialogs.py:828-830` | `closeEvent` chiama `thread.stop()` ma non `thread.wait()` né `super()` |
|
||||||
|
| B9 | 🟡 Bassa | S1 | `dialogs.py:1121-1122` | `show()+raise_()` senza `activateWindow()` né modalità |
|
||||||
|
| B10| 🟡 Bassa | — | `plugin.py:36` (init), vari | gestione finestre multiple/ multi-wallet fragile |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dettaglio dei problemi
|
||||||
|
|
||||||
|
### B1 — `self.parent = parent` rompe il sistema di finestre di Qt 🔴
|
||||||
|
**Dove:** `dialogs.py:40` (in `BalDialog.__init__`), ripetuto a `:69` e `:475`;
|
||||||
|
analoghi in altri dialoghi.
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.parent = parent # <-- PROBLEMA
|
||||||
|
super().__init__(parent)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causa:** in Qt, `parent()` è un **metodo** di `QWidget` che restituisce il
|
||||||
|
widget genitore. Assegnando un **attributo** `self.parent`, lo si maschera: da
|
||||||
|
quel punto `self.parent` non è più il metodo ma il valore salvato. Qualunque
|
||||||
|
codice (anche interno a Qt o di Electrum) che si aspetta `widget.parent()` come
|
||||||
|
metodo può comportarsi in modo imprevisto. Inoltre il `parent` passato non è
|
||||||
|
sempre la **top-level window** corretta, quindi il dialogo non viene agganciato
|
||||||
|
gerarchicamente alla finestra di Electrum e finisce **dietro** (S1).
|
||||||
|
|
||||||
|
**Fix proposto:**
|
||||||
|
- Non sovrascrivere `parent`: rinominare l'attributo (es. `self._bal_parent`).
|
||||||
|
- Passare sempre come `parent` la **top-level window** di Electrum
|
||||||
|
(`window.top_level_window()`), così il dialogo resta in primo piano rispetto
|
||||||
|
ad essa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B2 — Dialoghi aperti con `.show()` invece che modali 🔴
|
||||||
|
**Dove:**
|
||||||
|
- `window.py:148` `show_willexecutor_dialog` → `self.willexecutor_dialog.show()`
|
||||||
|
- `window.py:936` `preview_modal_dialog` → `self.dw.show()` (il nome dice
|
||||||
|
"modal" ma usa `show()`!)
|
||||||
|
- `window.py:566` `show_transaction_real` → `d.show()`
|
||||||
|
|
||||||
|
**Causa:** `show()` apre una finestra **non-modale e indipendente**: se il
|
||||||
|
`parent` non è impostato correttamente (vedi B1), la finestra non resta sopra
|
||||||
|
Electrum e ci "sparisce dietro" (S1). Si nota l'incoerenza: altrove si usa
|
||||||
|
correttamente `.exec()` (es. `init_wizard` a `window.py:144`, `settings_dialog`
|
||||||
|
a `plugin.py:254`), che è modale e resta in primo piano.
|
||||||
|
|
||||||
|
**Fix proposto:**
|
||||||
|
- Per i dialoghi che devono restare in primo piano: usare `exec()` (modale) **o**
|
||||||
|
`show()` + parent corretto + `setWindowModality(Qt.WindowModal)` +
|
||||||
|
`raise_()` + `activateWindow()`.
|
||||||
|
- Mantenere la stessa logica di "cosa fa il dialogo" (nessun cambio di
|
||||||
|
comportamento funzionale, solo z-order/modalità).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B3 — "Please restart Electrum to activate the BAL plugin" 🔴
|
||||||
|
**Dove:** `plugin.py:38-42` (hook `init_qt`).
|
||||||
|
|
||||||
|
```python
|
||||||
|
if wallet:
|
||||||
|
window.show_warning(_("Please restart Electrum to activate the BAL plugin"), ...)
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causa:** quando il plugin viene **abilitato a caldo** (wallet già aperto),
|
||||||
|
l'hook `init_qt` si arrende e chiede il riavvio invece di inizializzare le tab
|
||||||
|
e i menu sul wallet già caricato. È **la causa diretta del sintomo S2**: "devi
|
||||||
|
chiudere/riavviare Electrum perché funzioni".
|
||||||
|
|
||||||
|
**Fix proposto:**
|
||||||
|
- In `init_qt`, se c'è già un wallet aperto, eseguire la stessa inizializzazione
|
||||||
|
che normalmente avviene in `load_wallet` (creare `BalWindow`, tab, menu,
|
||||||
|
caricare il will) **senza** richiedere il riavvio.
|
||||||
|
- Simmetricamente, gestire bene `close_wallet` per smontare tab/menu, così
|
||||||
|
ri-abilitare/ricaricare non lascia stato sporco.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B4 — Chiave del dizionario `winId` (metodo) invece di `winId()` 🔴
|
||||||
|
**Dove:** `plugin.py:45` (scrittura) e `plugin.py:111` (lettura).
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.bal_windows[top_level_window.winId] = w # scrive con la *funzione* winId
|
||||||
|
...
|
||||||
|
w = self.bal_windows.get(window.winId, None) # legge con la *funzione* winId
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causa:** `winId` senza parentesi è il **metodo legato** (bound method), non
|
||||||
|
l'identificatore della finestra. Usato come chiave "funziona per caso" perché
|
||||||
|
lo stesso oggetto-finestra produce lo stesso bound method; ma è fragile e
|
||||||
|
semanticamente errato: con più finestre/wallet o dopo riaperture la
|
||||||
|
corrispondenza può saltare, creando `BalWindow` duplicati o non trovando quello
|
||||||
|
giusto → stato incoerente (contribuisce a S2).
|
||||||
|
|
||||||
|
**Fix proposto:**
|
||||||
|
- Usare una chiave stabile e corretta, es. `int(window.winId())` oppure
|
||||||
|
`id(window)`, in modo **coerente** sia in scrittura sia in lettura.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B5 — `on_close` ingoia tutti gli errori 🟠
|
||||||
|
**Dove:** `window.py:664-677`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def on_close(self):
|
||||||
|
try:
|
||||||
|
if not self.disable_plugin:
|
||||||
|
close_window = BalBuildWillDialog(self)
|
||||||
|
close_window.build_will_task()
|
||||||
|
self.save_willitems()
|
||||||
|
self.heirs_tab.close()
|
||||||
|
...
|
||||||
|
except Exception:
|
||||||
|
pass # <-- nasconde qualsiasi errore di cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causa:** se una qualsiasi di queste operazioni fallisce, l'eccezione viene
|
||||||
|
silenziata: tab/menu non vengono rimossi, lo stato (`willitems`, `heirs`, tab)
|
||||||
|
resta in memoria e "sporco" finché non si riavvia Electrum (S2).
|
||||||
|
|
||||||
|
**Fix proposto:**
|
||||||
|
- Non silenziare: loggare l'errore con `_logger`.
|
||||||
|
- Rendere il cleanup **robusto e idempotente** (ogni passo in un try/except
|
||||||
|
separato con log), così un fallimento parziale non blocca gli altri passi.
|
||||||
|
- Azzerare esplicitamente lo stato (`willitems={}`, riferimenti a tab/menu a
|
||||||
|
`None`) a fine `on_close`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B6 — `BalBlockingWaitingDialog` blocca il thread della GUI 🟠
|
||||||
|
**Dove:** `dialogs.py:445-462`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.show()
|
||||||
|
# QCoreApplication.processEvents() # <-- commentato
|
||||||
|
# QCoreApplication.processEvents()
|
||||||
|
try:
|
||||||
|
task() # esegue il task SUL thread GUI -> finestra "congelata"
|
||||||
|
finally:
|
||||||
|
self.accept()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causa:** dopo `show()` non si dà alla GUI il tempo di disegnarsi
|
||||||
|
(`processEvents` è commentato) e poi si esegue `task()` **bloccando** il thread
|
||||||
|
dell'interfaccia. Risultato: la finestra "Please wait" può apparire vuota,
|
||||||
|
non ridisegnarsi, e l'app sembra bloccata (contribuisce a S1/percezione di
|
||||||
|
freeze).
|
||||||
|
|
||||||
|
**Fix proposto:**
|
||||||
|
- O eseguire il task in un `TaskThread` (come fa già `BalWaitingDialog`),
|
||||||
|
- oppure, se deve restare bloccante, ripristinare un `processEvents()` dopo
|
||||||
|
`show()` per far disegnare la finestra prima del task.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B7 — `closeEvent`/`hideEvent` con cleanup thread commentato 🟠
|
||||||
|
**Dove:** `dialogs.py:48-58` (`BalDialog`).
|
||||||
|
|
||||||
|
```python
|
||||||
|
def closeEvent(self, event):
|
||||||
|
self._stopping = True
|
||||||
|
#if self.thread:
|
||||||
|
# self.thread.stop() # <-- disattivato
|
||||||
|
super().closeEvent(event)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causa:** alla chiusura del dialogo i thread eventualmente attivi **non**
|
||||||
|
vengono fermati. Restano in esecuzione in background, possono scrivere su widget
|
||||||
|
già distrutti o tenere risorse/connessioni → comportamenti erratici finché non
|
||||||
|
si riavvia (S2).
|
||||||
|
|
||||||
|
**Fix proposto:**
|
||||||
|
- Ripristinare in modo sicuro lo stop dei thread: `if self.thread:
|
||||||
|
self.thread.stop(); self.thread.wait()` con guardia su `None`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B8 — `BalBuildWillDialog.closeEvent` incompleto 🟠
|
||||||
|
**Dove:** `dialogs.py:828-830`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def closeEvent(self, event):
|
||||||
|
self._stopping = True
|
||||||
|
self.thread.stop()
|
||||||
|
# manca self.thread.wait() e manca super().closeEvent(event)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causa:** `stop()` segnala lo stop ma non attende la fine del thread
|
||||||
|
(`wait()`), e non viene chiamato `super().closeEvent(event)`: l'evento di
|
||||||
|
chiusura non è propagato correttamente. Possibili thread orfani e finestre che
|
||||||
|
non si chiudono pulite.
|
||||||
|
|
||||||
|
**Fix proposto:**
|
||||||
|
- `self.thread.stop(); self.thread.wait(); super().closeEvent(event)` con
|
||||||
|
guardia su `self.thread is None`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B9 — `show()+raise_()` senza `activateWindow()`/modalità 🟡
|
||||||
|
**Dove:** `dialogs.py:1121-1122` (es. `WillExecutorDialog`/dettaglio).
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.show()
|
||||||
|
self.raise_()
|
||||||
|
# manca self.activateWindow(); nessuna modalità impostata
|
||||||
|
```
|
||||||
|
|
||||||
|
**Causa:** `raise_()` alza la finestra nello stack ma su alcuni window manager
|
||||||
|
(incluso Windows) senza `activateWindow()` non riceve il focus e può comunque
|
||||||
|
finire dietro. Senza modalità, l'utente può tornare alla finestra principale
|
||||||
|
lasciando il dialogo nascosto.
|
||||||
|
|
||||||
|
**Fix proposto:**
|
||||||
|
- Aggiungere `self.activateWindow()` dopo `raise_()`, e valutare
|
||||||
|
`setWindowModality(Qt.WindowModal)` dove ha senso.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B10 — Gestione finestre multiple / multi-wallet fragile 🟡
|
||||||
|
**Dove:** `plugin.py:30-62` (`init_qt`), `get_window` (`plugin.py:109-115`).
|
||||||
|
|
||||||
|
**Causa:** la mappa `bal_windows` e l'aggancio ai menu si basano su assunzioni
|
||||||
|
(B4) e sull'iterazione dei figli del menubar per nome (`"&Tools"`), che è
|
||||||
|
sensibile alla **localizzazione** (tu usi `Locale: Italian_Italy`!). Se il menu
|
||||||
|
non si chiama esattamente `&Tools` nella lingua corrente, l'aggancio può
|
||||||
|
fallire silenziosamente.
|
||||||
|
|
||||||
|
**Fix proposto:**
|
||||||
|
- Usare l'API ufficiale `window.tools_menu` (già usata in `init_menubar`,
|
||||||
|
`plugin.py:79`) invece di cercare il menu per titolo tradotto.
|
||||||
|
- Unificare la creazione/lookup di `BalWindow` su una chiave stabile (B4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Strategia di correzione proposta (per la Fase B/C)
|
||||||
|
|
||||||
|
Per **non cambiare la logica di funzionamento** e ridurre i rischi, propongo di
|
||||||
|
introdurre un **unico punto centralizzato** di gestione finestre (un piccolo
|
||||||
|
helper, es. `gui/qt/window_utils.py`) con funzioni tipo:
|
||||||
|
|
||||||
|
- `show_modal(dialog)` → imposta parent corretto, modalità, `exec()`.
|
||||||
|
- `show_on_top(dialog)` → `show()` + `raise_()` + `activateWindow()` per i
|
||||||
|
pochi casi che devono restare non-modali.
|
||||||
|
|
||||||
|
E poi sostituire i `.show()`/`.exec()` sparsi con queste funzioni. Vantaggi:
|
||||||
|
- la **logica di business resta intatta** (cosa fa il dialogo non cambia);
|
||||||
|
- si tocca **solo** il "come" viene mostrato/chiuso;
|
||||||
|
- più facile da testare e da revisionare (diff piccolo e localizzato).
|
||||||
|
|
||||||
|
### Ordine consigliato
|
||||||
|
1. **B3 + B4** (init a caldo + chiave finestre): risolvono la radice di S2.
|
||||||
|
2. **B1 + B2 + B9** (parent/modalità/z-order): risolvono S1.
|
||||||
|
3. **B5 + B7 + B8** (cleanup robusto + thread): chiudono i residui di S2.
|
||||||
|
4. **B6 + B10** (waiting dialog + menu localizzati): rifiniture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cosa serve da te per la Fase B/C
|
||||||
|
- Conferma che posso modificare il **comportamento della GUI** (parent,
|
||||||
|
modalità, cleanup, init a caldo) mantenendo invariata la logica di business.
|
||||||
|
- Test su **Electrum portable Windows** dopo ogni gruppo di fix, con descrizione
|
||||||
|
/screenshot di cosa succede (apertura dialoghi, abilitazione a caldo,
|
||||||
|
chiusura wallet).
|
||||||
|
|
||||||
|
> Nota: i bug B1–B10 esistono **identici nell'originale** — questo refactor li
|
||||||
|
> ha preservati fedelmente (era l'obiettivo della fase precedente). La Fase B/C
|
||||||
|
> li corregge.
|
||||||
134
README.md
134
README.md
@@ -1 +1,133 @@
|
|||||||
clean slate
|
# BAL — Bitcoin After Life (Electrum plugin)
|
||||||
|
|
||||||
|
Free and decentralized **Bitcoin inheritance** support for the
|
||||||
|
[Electrum](https://electrum.org) wallet. Build time-locked "will" transactions
|
||||||
|
that transfer your funds to your heirs if you stop refreshing them
|
||||||
|
(dead-man's switch), optionally relayed by will-executor servers.
|
||||||
|
|
||||||
|
This repository contains a **behavior-preserving refactor** of the original
|
||||||
|
plugin. The logic was kept byte-identical wherever possible; only the file
|
||||||
|
layout was reorganized to cleanly separate **business logic** from the
|
||||||
|
**PyQt GUI**.
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
```
|
||||||
|
bal/ the installable Electrum plugin package
|
||||||
|
├── manifest.json plugin metadata (Electrum reads this)
|
||||||
|
├── qt.py Qt entry-point shim (re-exports Plugin)
|
||||||
|
├── core/ GUI-free logic (importable without Qt)
|
||||||
|
│ ├── util.py
|
||||||
|
│ ├── plugin_base.py
|
||||||
|
│ ├── heirs.py
|
||||||
|
│ ├── will.py
|
||||||
|
│ └── willexecutors.py
|
||||||
|
├── gui/qt/ PyQt6 presentation layer
|
||||||
|
│ ├── theme.py status → color mapping
|
||||||
|
│ ├── common.py shared imports / helpers
|
||||||
|
│ ├── widgets.py leaf widgets
|
||||||
|
│ ├── calendar.py calendar widget
|
||||||
|
│ ├── dialogs.py dialog windows
|
||||||
|
│ ├── lists.py tree/list views
|
||||||
|
│ ├── window.py per-wallet GUI controller
|
||||||
|
│ └── plugin.py Plugin (Electrum @hooks → GUI)
|
||||||
|
├── icons/ wallet_util/ LICENSE VERSION README.md
|
||||||
|
build_zip.py builds a clean, zipimport-friendly distribution zip
|
||||||
|
tests/ smoke + external-zip regression tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Electrum 4.7.2** — the last stable release exposing `json_db.register_dict`,
|
||||||
|
which this plugin relies on. Newer versions removed it.
|
||||||
|
- **PyQt6** (bundled with the Electrum desktop GUI).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Build the distribution archive
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 build_zip.py
|
||||||
|
# -> bal-electrum-plugin.zip (prints size + SHA-256 for integrity checks)
|
||||||
|
```
|
||||||
|
|
||||||
|
The builder writes a `zipimport`-friendly archive (files only, standard
|
||||||
|
DEFLATE, deterministic order) to avoid loader errors seen on some Electrum
|
||||||
|
portable builds.
|
||||||
|
|
||||||
|
### Install as an external plugin (zip)
|
||||||
|
|
||||||
|
1. Electrum → **Tools → Plugins** → install from file → pick the built zip.
|
||||||
|
2. Enable **Bitcoin After Life** and restart Electrum.
|
||||||
|
3. (Recommended) verify the downloaded zip's SHA-256 matches the value printed
|
||||||
|
by `build_zip.py`.
|
||||||
|
|
||||||
|
### Install as an internal plugin
|
||||||
|
|
||||||
|
Copy the `bal/` directory into your Electrum installation's
|
||||||
|
`electrum/plugins/` directory, so that `electrum/plugins/bal/manifest.json`
|
||||||
|
exists, then enable it from **Tools → Plugins**.
|
||||||
|
|
||||||
|
## Inheritance safety: anticipate / postpone
|
||||||
|
|
||||||
|
A will transaction is signed with a **fixed, immutable locktime** and then
|
||||||
|
optionally sent to will-executor servers, which are economically incentivised
|
||||||
|
to broadcast it (they collect fees). Because the locktime is baked into the
|
||||||
|
signed transaction, simply changing the delivery time later is **not enough**:
|
||||||
|
the old, already-signed transaction keeps living on the will-executors.
|
||||||
|
|
||||||
|
The plugin handles the two cases as follows (triggered when you press
|
||||||
|
**Tools → Prepare**):
|
||||||
|
|
||||||
|
* **Anticipate** (new delivery time *earlier* than the signed locktime): the
|
||||||
|
will is treated as expired and you are asked to **invalidate** the old
|
||||||
|
transaction on-chain, then rebuild.
|
||||||
|
* **Postpone** (new delivery time *later* than the signed locktime) on a will
|
||||||
|
that was already **signed and/or pushed**: the previously committed coins
|
||||||
|
must be invalidated on-chain **first**, otherwise a will-executor could
|
||||||
|
broadcast the old (earlier-locktime) transaction and execute the inheritance
|
||||||
|
*too early*. The plugin detects this by comparing the requested locktime with
|
||||||
|
the locktime **frozen inside the signed transaction** (`tx.locktime`), and
|
||||||
|
asks you to sign and broadcast an invalidation transaction. After it is
|
||||||
|
broadcast, press **Prepare** again to rebuild, re-sign and re-send the new
|
||||||
|
(postponed) inheritance. Postponing a will that was *never* signed/sent just
|
||||||
|
rebuilds it (no on-chain fee).
|
||||||
|
|
||||||
|
## Transaction list: the "Server" column
|
||||||
|
|
||||||
|
The will transaction list shows a dedicated **Server** column so you always
|
||||||
|
know whether each inheritance transaction is actually stored on the
|
||||||
|
will-executor servers, independently of the row colour:
|
||||||
|
|
||||||
|
| Label | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `Confirmed on server` | the will-executor confirmed it stored the transaction |
|
||||||
|
| `Sent (not checked)` | pushed to the will-executor, not yet re-checked |
|
||||||
|
| `Send failed` / `Not on server` | push failed or the server no longer has it |
|
||||||
|
| `Signed (not sent)` | signed locally, not sent to any will-executor |
|
||||||
|
| `Not sent` | not signed/sent yet |
|
||||||
|
|
||||||
|
Hovering the cell shows a tooltip with the will-executor URL and the current
|
||||||
|
state.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# imports + behavior
|
||||||
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
|
||||||
|
python3 tests/smoke_test.py electrum.plugins.bal
|
||||||
|
|
||||||
|
# external-zip loading regression
|
||||||
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
|
||||||
|
python3 tests/external_zip_test.py bal-electrum-plugin.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Safety
|
||||||
|
|
||||||
|
This plugin builds real Bitcoin inheritance transactions with time-locks. Test
|
||||||
|
on **testnet** or a fund-less wallet first, and review the generated
|
||||||
|
transactions before broadcasting.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [`bal/LICENSE`](bal/LICENSE).
|
||||||
|
|||||||
253
REPORT_NETWORKING_PARALLELO.md
Normal file
253
REPORT_NETWORKING_PARALLELO.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# Technical report — Parallel networking (Will-Executor anti-freeze) + UI feedback
|
||||||
|
|
||||||
|
**Audience:** external programmer / plugin maintainer
|
||||||
|
**Author:** AI refactoring work (on GitHub `Bitcoin-after-life/test`)
|
||||||
|
**Date:** 2026-06-15
|
||||||
|
**Branch:** `feature/networking-parallelo` (Pull Request #4)
|
||||||
|
**Private Gitea repo `kaibot/bal-plugin-ai`: NOT modified** (per explicit request; cloned read-only only to run the official tests).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Problem
|
||||||
|
|
||||||
|
When the plugin contacts the Will-Executor servers (pushing transactions,
|
||||||
|
pinging/refreshing the inheritance, downloading the list, and **checking**
|
||||||
|
transactions), it used to do it **sequentially**. If a server did not answer,
|
||||||
|
the thread stayed blocked on the connection timeouts and, worse, on the
|
||||||
|
**retries**:
|
||||||
|
|
||||||
|
- `send_request` retried up to **10 times** with `time.sleep(3)` on every
|
||||||
|
timeout → roughly **~140 seconds per unreachable server**, summed one after
|
||||||
|
another.
|
||||||
|
|
||||||
|
Consequences:
|
||||||
|
- Noticeable already with a few servers; with **20 servers** it became
|
||||||
|
unusable.
|
||||||
|
- The user saw "Stay waiting — Not responding" with no idea what was happening.
|
||||||
|
- A single dead server blocked the whole operation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Solution (overview)
|
||||||
|
|
||||||
|
1. **Parallelism** with `ThreadPoolExecutor`: servers are contacted
|
||||||
|
concurrently. Total time ≈ the **slowest** server, not the **sum**.
|
||||||
|
2. **Fast-fail** for interactive operations (ping/info/download): no retry
|
||||||
|
storm, a single short timeout and the server is marked "KO".
|
||||||
|
3. **Aggressive timeouts + global deadline** for push/check: a short per-server
|
||||||
|
retry budget is kept (a real transaction must survive a transient hiccup),
|
||||||
|
but a wall-clock **global deadline** caps the whole batch so a dialog never
|
||||||
|
freezes behind one unresponsive server.
|
||||||
|
4. **Live feedback + reliable elapsed-time counter**: `on_each(...)` updates the
|
||||||
|
dialog as results arrive, and `on_tick()` refreshes an elapsed-time counter
|
||||||
|
(`Xs / DEADLINEs`) so the user always knows progress and the maximum wait.
|
||||||
|
|
||||||
|
### Why it is thread-safe
|
||||||
|
`Network.send_http_on_proxy()` uses `asyncio.run_coroutine_threadsafe(coro,
|
||||||
|
loop)` and then `coro.result()`: every call schedules its own coroutine on
|
||||||
|
Electrum's shared asyncio loop and blocks **only its own worker thread**.
|
||||||
|
Multiple concurrent calls are therefore safe → `ThreadPoolExecutor` gives true
|
||||||
|
parallelism.
|
||||||
|
|
||||||
|
UI updates go through `BalWaitingDialog.update()` / the dialog's `pyqtSignal`,
|
||||||
|
which marshals to the GUI thread automatically. Callbacks from worker threads
|
||||||
|
can therefore update the dialog safely.
|
||||||
|
|
||||||
|
### Why the counter is driven from the calling thread (important)
|
||||||
|
An earlier attempt refreshed the elapsed-time counter from a separate raw
|
||||||
|
`threading.Thread` heartbeat that emitted the `pyqtSignal`. That proved
|
||||||
|
**unreliable**: a `pyqtSignal` emitted from a raw (non-Qt) Python thread inside
|
||||||
|
the wizard's `TaskThread` was not reliably marshalled and the dialog never
|
||||||
|
repainted — the counter was invisible.
|
||||||
|
|
||||||
|
The fix: the parallel helpers accept an **`on_tick` callback that is invoked
|
||||||
|
periodically from the CALLING thread** (the same thread that already drives
|
||||||
|
`on_each` and successfully repaints). The helpers poll the futures in short
|
||||||
|
slices (`concurrent.futures.wait(..., timeout=tick_interval)`) and call
|
||||||
|
`on_tick()` between waits. No heartbeat thread is used anymore.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Modified files (all on GitHub `Bitcoin-after-life/test`, branch `feature/networking-parallelo`)
|
||||||
|
|
||||||
|
### 3.1 `bal/core/willexecutors.py`
|
||||||
|
|
||||||
|
**Networking constants** (module level, also exposed as `Willexecutors` class
|
||||||
|
attributes for a single source of truth in the GUI):
|
||||||
|
```python
|
||||||
|
DEFAULT_TIMEOUT = 5 # interactive ops (ping/info/list)
|
||||||
|
|
||||||
|
PUSH_TIMEOUT = 8 # broadcast (pushtxs)
|
||||||
|
PUSH_MAX_RETRIES = 2
|
||||||
|
PUSH_RETRY_SLEEP = 1
|
||||||
|
PUSH_GLOBAL_DEADLINE = 30 # wall-clock cap for the whole parallel push
|
||||||
|
|
||||||
|
CHECK_TIMEOUT = 8 # check (searchtx)
|
||||||
|
CHECK_MAX_RETRIES = 1
|
||||||
|
CHECK_RETRY_SLEEP = 1
|
||||||
|
CHECK_GLOBAL_DEADLINE = 30 # wall-clock cap for the whole parallel check
|
||||||
|
```
|
||||||
|
Worst case per server is now ~26s (push) / ~17s (check) instead of ~140s, and
|
||||||
|
the global deadline guarantees the dialog proceeds within 30s regardless.
|
||||||
|
|
||||||
|
**`send_request(...)`** — keyword-only retry controls:
|
||||||
|
```python
|
||||||
|
def send_request(method, url, data=None, *, timeout=10, handle_response=None,
|
||||||
|
count_reply=0, max_retries=10, retry_sleep=3):
|
||||||
|
```
|
||||||
|
- Defaults unchanged → callers that need the historical behaviour are
|
||||||
|
unaffected.
|
||||||
|
- Interactive callers pass `max_retries=0` → fast-fail.
|
||||||
|
|
||||||
|
**`get_info_task(...)`** — fast-fail by default (`max_retries=0`); a
|
||||||
|
timeout/empty response yields `status="KO"`.
|
||||||
|
|
||||||
|
**`check_transaction(...)`** — now accepts `timeout`/`max_retries`/`retry_sleep`
|
||||||
|
(defaults from the `CHECK_*` constants) and forwards them to `send_request`,
|
||||||
|
replacing the old ~140s default storm.
|
||||||
|
|
||||||
|
**NEW `ping_servers_parallel(willexecutors, *, on_each=None, max_workers=8,
|
||||||
|
timeout=DEFAULT_TIMEOUT, on_tick=None, tick_interval=1.0)`**
|
||||||
|
- `ThreadPoolExecutor`; polls futures in slices and calls `on_tick()` from the
|
||||||
|
calling thread; mutates `willexecutors` in place; invokes
|
||||||
|
`on_each(url, we, ok)` as results arrive; a worker exception never blocks the
|
||||||
|
others (defensive try/except).
|
||||||
|
|
||||||
|
**NEW `push_transactions_parallel(willexecutors, *, on_each=None, max_workers=8,
|
||||||
|
deadline=PUSH_GLOBAL_DEADLINE, on_timeout=None, on_tick=None,
|
||||||
|
tick_interval=1.0)`**
|
||||||
|
- Parallel push only to entries that have a `"txs"` key; each server keeps its
|
||||||
|
short retry budget.
|
||||||
|
- `on_each(url, we, ok, exc)` per server; `on_timeout(url, we)` for servers
|
||||||
|
still pending when the global deadline elapses; `on_tick()` for the counter.
|
||||||
|
- Manual pool (no `with`) so `shutdown(wait=False, cancel_futures=True)` does
|
||||||
|
not block on a hung worker once the deadline is reached.
|
||||||
|
- Returns `{url: (ok, exc)}` for the servers that answered in time.
|
||||||
|
|
||||||
|
**NEW `check_transactions_parallel(items, *, on_each=None, max_workers=8,
|
||||||
|
deadline=CHECK_GLOBAL_DEADLINE, on_timeout=None, on_tick=None,
|
||||||
|
tick_interval=1.0)`**
|
||||||
|
- Same design as the push helper but for the **Check** (searchtx) operation.
|
||||||
|
- `items` is an iterable of `(wid, url)` pairs; `_check_one` calls
|
||||||
|
`check_transaction`.
|
||||||
|
- `on_each(wid, url, result_or_None, exc)`, `on_timeout(wid, url)`, `on_tick()`.
|
||||||
|
- Returns `{wid: (result_or_None, exc)}`.
|
||||||
|
|
||||||
|
### 3.2 `bal/gui/qt/window.py`
|
||||||
|
|
||||||
|
- **`ping_willexecutors_task(self, wes)`** rewritten on `ping_servers_parallel`
|
||||||
|
with live feedback and a counter `Ping Will-Executors: 2/3 (3s / 30s)` driven
|
||||||
|
by `on_tick` from the calling thread.
|
||||||
|
- **`push_transactions_to_willexecutors(self, force=False)`** rewritten on
|
||||||
|
`push_transactions_parallel`; `on_each` does thread-safe book-keeping + UI
|
||||||
|
update; "already present" servers are verified afterwards (original check
|
||||||
|
logic intact).
|
||||||
|
- **`check_transactions_task(self, will)`** rewritten on
|
||||||
|
`check_transactions_parallel`; shows `Checking transactions: 2/5 (4s / 30s)`,
|
||||||
|
reusing the original `set_check_willexecutor(...)` per-item logic inside
|
||||||
|
`on_each` (and `set_check_willexecutor(None)` on `on_timeout`).
|
||||||
|
- **`fetch_will_executors_list(...)`** fast-fail download
|
||||||
|
(`timeout=10, max_retries=1, retry_sleep=1`); the download dialog shows
|
||||||
|
`Downloading will-executors list... (Xs / 45s)`.
|
||||||
|
|
||||||
|
### 3.3 `bal/gui/qt/dialogs.py`
|
||||||
|
|
||||||
|
- **`BalBuildWillDialog.loop_push`** (the "Building Will" wizard broadcast step)
|
||||||
|
rewritten on `push_transactions_parallel` with the `on_tick` counter
|
||||||
|
`Broadcasting 2/3 (5s / 30s)`. The previous raw heartbeat thread was removed.
|
||||||
|
|
||||||
|
### 3.4 `bal/gui/qt/plugin.py` — status-bar icon (restored)
|
||||||
|
|
||||||
|
`create_status_bar` re-adds the BAL `StatusBarButton` (bottom-right of the
|
||||||
|
Electrum status bar). It shows that the plugin is installed and opens the plugin
|
||||||
|
settings on click; it also de-duplicates the button per window. (Comments in
|
||||||
|
English.)
|
||||||
|
|
||||||
|
### 3.5 `bal/gui/qt/lists.py` and `bal/gui/qt/widgets.py` — GUI usability
|
||||||
|
|
||||||
|
- **Tooltips** (hover) on the Will toolbar icons, all in English:
|
||||||
|
Wizard (`Wizard - Build your will`), Delivery time (truck), Check Alive
|
||||||
|
(siren), Calendar, Check (refresh).
|
||||||
|
- **Toolbar order** changed to:
|
||||||
|
`Wizard | Delivery time | Check Alive | Calendar | Check`; layout margins
|
||||||
|
tightened so everything fits the Will window.
|
||||||
|
|
||||||
|
### 3.6 `bal/core/util.py` — BUGFIX (pre-existing regression)
|
||||||
|
|
||||||
|
In `get_value_amount` (line 324) `Util.in_output(...)` (returns `bool`) had been
|
||||||
|
used instead of `Util.din_output(...)` (returns the tuple
|
||||||
|
`(same_amount, same_address)`), causing:
|
||||||
|
```
|
||||||
|
TypeError: cannot unpack non-iterable bool object
|
||||||
|
```
|
||||||
|
**Fixed** by restoring `din_output`. Found by running the official Gitea tests
|
||||||
|
(`tests/test_core_util.py::test_get_value_amount`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Verification (ruff + official tests)
|
||||||
|
|
||||||
|
### 4.1 ruff (lint / PEP8)
|
||||||
|
- `ruff check` on the new code: **no new issues** introduced. The `F403/F405/
|
||||||
|
F401` warnings come from the original `from .common import *` pattern;
|
||||||
|
per-file counts are identical between HEAD and the working tree.
|
||||||
|
- The new parallel functions add **0 `E501`** (line-length) issues; in
|
||||||
|
`window.py` the count actually decreased after the rewrite.
|
||||||
|
- `ruff check tests/parallel_ping_test.py` → no new issues.
|
||||||
|
|
||||||
|
### 4.2 Official tests from the Gitea repo `kaibot/bal-plugin-ai/tests`
|
||||||
|
Run against the refactored code (with all the networking + UI changes):
|
||||||
|
|
||||||
|
| Suite | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| `test_core_*` + `test_gui_*` (pytest) | **182 passed** |
|
||||||
|
| `smoke_test.py` | OK |
|
||||||
|
| `external_zip_test.py` | OK |
|
||||||
|
| `windows_overflow_test.py` | OK |
|
||||||
|
| `gui_fixes_test.py` | OK |
|
||||||
|
| `parallel_ping_test.py` (new) | OK — parallel ping/push/check ~`0.50s` for 8 servers (sequential would be ~`4.00s`); global deadline enforced; `on_tick` fired from the calling thread; static checks that the dialogs use the parallel helpers + the `Xs / Ns` counter |
|
||||||
|
|
||||||
|
Commands (as per README):
|
||||||
|
```bash
|
||||||
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
|
||||||
|
python3 -m pytest tests/ -q
|
||||||
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
|
||||||
|
python3 tests/smoke_test.py electrum.plugins.bal
|
||||||
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
|
||||||
|
python3 tests/external_zip_test.py bal-electrum-plugin.zip
|
||||||
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
|
||||||
|
python3 tests/parallel_ping_test.py bal
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Integration notes / risks
|
||||||
|
|
||||||
|
- **No change to the server protocol**: only the *how* (parallel) and the
|
||||||
|
*when* (retries/deadline) of the calls changed, not the payloads.
|
||||||
|
- **Push transactions**: per-server retries are intentionally kept so a real
|
||||||
|
transaction is not lost to a transient hiccup; only ping/info/download use
|
||||||
|
fast-fail. The global deadline marks unanswered servers as failed (`on_timeout`)
|
||||||
|
so the user can retry later.
|
||||||
|
- **`max_workers=8`** is conservative; with many servers (e.g. 20) it can be
|
||||||
|
raised, but 8 workers already collapse the total time to the slowest server.
|
||||||
|
- **Thread/UI**: all UI updates from workers go through `pyqtSignal`-based
|
||||||
|
dialog updates; the periodic counter is driven by `on_tick` from the calling
|
||||||
|
thread. Do **not** reintroduce a raw heartbeat thread emitting signals — it
|
||||||
|
does not repaint reliably.
|
||||||
|
- **Compatibility**: signatures are backward compatible (new parameters are
|
||||||
|
keyword-only with defaults that preserve the old behaviour).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. How to test
|
||||||
|
|
||||||
|
1. Install `bal-electrum-plugin.zip` (Tools → Plugins → install from file).
|
||||||
|
Fully close and reopen Electrum to avoid the cached zip import.
|
||||||
|
2. Configure several Will-Executors, including **at least one unreachable**.
|
||||||
|
3. Run push / ping / Check: each dialog shows per-server status plus a counter
|
||||||
|
`N/total (Xs / 30s)` and **no longer freezes** on the dead server — within
|
||||||
|
the global deadline the operation reports the dead server and proceeds.
|
||||||
|
|
||||||
|
The SHA-256 of the zip is printed by `build_zip.py` at the end of the build
|
||||||
|
(use it to verify integrity).
|
||||||
73
build_zip.py
Normal file
73
build_zip.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build a clean, zipimport-friendly distribution archive of the BAL plugin.
|
||||||
|
|
||||||
|
Electrum loads external plugins from a ``.zip`` using Python's ``zipimport``.
|
||||||
|
``zipimport`` is picky about the archive layout, so this builder deliberately:
|
||||||
|
|
||||||
|
* writes **only files** (no explicit directory entries) — some Electrum
|
||||||
|
portable builds choke on directory records inside the archive;
|
||||||
|
* uses standard DEFLATE compression (well supported by ``zipimport``);
|
||||||
|
* emits entries in a deterministic, sorted order so the archive is
|
||||||
|
reproducible (stable SHA-256);
|
||||||
|
* skips ``__pycache__`` directories and compiled ``*.pyc``/``*.pyo`` files.
|
||||||
|
|
||||||
|
The archive keeps the top-level ``bal/`` directory so that the package is
|
||||||
|
importable as ``bal`` (and Electrum derives ``dirname='bal'`` from the path of
|
||||||
|
``bal/manifest.json``).
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
python3 build_zip.py [output.zip]
|
||||||
|
|
||||||
|
Prints the resulting size and SHA-256 so the download can be integrity-checked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
SRC_ROOT = "bal"
|
||||||
|
DEFAULT_OUT = "bal-electrum-plugin.zip"
|
||||||
|
|
||||||
|
|
||||||
|
def build(out_path: str) -> None:
|
||||||
|
if os.path.exists(out_path):
|
||||||
|
os.remove(out_path)
|
||||||
|
|
||||||
|
files = []
|
||||||
|
for dirpath, dirnames, filenames in os.walk(SRC_ROOT):
|
||||||
|
# prune cache dirs in place so os.walk does not descend into them
|
||||||
|
dirnames[:] = [d for d in dirnames if d != "__pycache__"]
|
||||||
|
for fn in filenames:
|
||||||
|
if fn.endswith((".pyc", ".pyo")):
|
||||||
|
continue
|
||||||
|
files.append(os.path.join(dirpath, fn))
|
||||||
|
files.sort()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(
|
||||||
|
out_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6
|
||||||
|
) as z:
|
||||||
|
for f in files:
|
||||||
|
arc = f.replace(os.sep, "/") # forward slashes inside the archive
|
||||||
|
z.write(f, arc)
|
||||||
|
|
||||||
|
# Integrity + summary
|
||||||
|
with zipfile.ZipFile(out_path) as z:
|
||||||
|
bad = z.testzip()
|
||||||
|
if bad is not None:
|
||||||
|
raise SystemExit(f"ERROR: corrupt entry in archive: {bad}")
|
||||||
|
names = z.namelist()
|
||||||
|
if not any(n.endswith("manifest.json") for n in names):
|
||||||
|
raise SystemExit("ERROR: manifest.json missing from archive")
|
||||||
|
|
||||||
|
data = open(out_path, "rb").read()
|
||||||
|
print(f"built : {out_path}")
|
||||||
|
print(f"files : {len(files)}")
|
||||||
|
print(f"size : {len(data)} bytes")
|
||||||
|
print(f"sha256: {hashlib.sha256(data).hexdigest()}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
out = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_OUT
|
||||||
|
build(out)
|
||||||
10140
giovanna7
Executable file
10140
giovanna7
Executable file
File diff suppressed because one or more lines are too long
47
promptai
Normal file
47
promptai
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
traduci tutti i testi, commenti, docstring, e nomi variabile in inglese.
|
||||||
|
scrivi sempre tutto il codice in inglese.
|
||||||
|
scrivi sempre commenti e docstring su tutti i metodi e classi per spiegarne il funzionamento.
|
||||||
|
|
||||||
|
quando non sei sicuro non inventare, chiedi conferma tenendo pero' conto che non sono un programmatore.
|
||||||
|
siccome ordinare block-height e timestamp e' impossibile, elimina block-height dalla codebase, considera solo i timestamp.
|
||||||
|
|
||||||
|
assicurati che i seguenti stati della transazione vengano assegnati correttamente:
|
||||||
|
ANTICIPATED — La transazione è stata generata con un locktime anticipato di 1 giorno rispetto a una transazione pre-esistente con cui condivide gli stessi eredi. Rimane valida (VALID non viene pulito).
|
||||||
|
REPLACED — La transazione è stata sostituita perché almeno uno dei suoi input viene speso da una nuova transazione con locktime inferiore. La vecchia tx perde lo status VALID e viene marcata REPLACED. Si propaga a cascata ai figli.
|
||||||
|
INVALIDATED — La transazione non è più spendibile perché almeno uno dei suoi input è stato speso da una transazione in mempool o già confermata, e la tx precedente non esiste più nella will. Perde lo status VALID.
|
||||||
|
UPDATED — la transazione e' spendibile e valida, ma una nuova transazione la rimpiazza mantenendo lo stesso locktime e gli stessi eredi
|
||||||
|
MEMPOOL — la transazione e' stata vista nella mempool di electrum
|
||||||
|
CONFIRMED — la transazione e' confermata nella blockchain
|
||||||
|
|
||||||
|
|
||||||
|
aggiungi una opzione nella configurazione del plugin con una checkbox che chiede se firmare automaticamente le transazioni default: True
|
||||||
|
ripristina che il pulsante check firmi le transazioni, e se e' impostata una password quando la chiede se si clicca su annulla, o se la nuova checkbox nelle impostazioni del plugin e' settata per non firmare automaticamente le transazioni deve saltare la fase della firma e come ora dire all'utente di firmare
|
||||||
|
|
||||||
|
modifica gli allarmi dell'evento nel calendario:
|
||||||
|
aggiungi una opzione nelle impostazioni del plugin che chiede il numero di allarmi si vogliono impostare default 3
|
||||||
|
quando si crea l'evento, si sceglie quale e' maggiore tra la data attuale ed il check-alive, si divide il tempo per il numero di allarmi piu' uno, e si impostano gli allarmi sulle prime divisioni.
|
||||||
|
|
||||||
|
crea dei test mock per testare le funzionalità:
|
||||||
|
usa il wallet giovanna7 per prendere dati plausibili ed indirizzi bitcoin validi
|
||||||
|
usa l'environment di electrum in ../electrum/env in sola lettura.
|
||||||
|
|
||||||
|
|
||||||
|
scrivi test per verificare l'output dell'evento per il calendario.
|
||||||
|
testa che l'evento abbia la data e l'ora del valore inferiore tra will-settings['locktime'] o della tx valida con locktime inferiore
|
||||||
|
testa che gli allarmi vengano creati correttamente secondo lo standard ics compatibile con i vari calendari online
|
||||||
|
|
||||||
|
crea almeno 3 eredi con indirizzi, locktime, ed amount validi
|
||||||
|
crea almeno 2 willexecutors con base amount ed indirizzi validi
|
||||||
|
esegui una eredità e confronta gli ammontare degli eredi con gli output della transazione per verificare che siano corretti
|
||||||
|
modifica l'indirizzo di un erede, aggiorna, e verifica che la nuova eredità sia anticipata di un giorno, abbia stato anticipated ed aggiornato l'output dell'erede.
|
||||||
|
modifica l'ammontare di un erede, aggiorna, e verifica che la nuova eredita sia anticipata di un ulteriore giorno, abbia stato anticipated ed aggiornato l'output dell'erede.
|
||||||
|
modifica la base_fee ad un willexecutor, aggiorna l'eredità, ed assicurati che la nuova eredità abbia lo stesso locktime di quella attuale e l'output per il willexecutor aggiornato con la nuova base_fee. la vecchia transazione deve avere lo stato UPDATED e conservare lo stato valido
|
||||||
|
testa che tutti gli stati vengano assegnati correttamente secondo la loro descrizione
|
||||||
|
|
||||||
|
scrivi dei test per testare la connettività
|
||||||
|
simula tutti i tipi di errori di rete e verifica che vengano gestiti correttamente in tutte le varie richieste ai willexecutors ed alla welist
|
||||||
|
|
||||||
|
scrivi test per testare i willexecutors e le loro risposte alle varie interrogazioni.
|
||||||
|
usa il willexecutor di default we.bitcoin-after.life/bitcoin per eseguire le interrogazioni e verificare le risposte.
|
||||||
|
usa la welist all'indirizzo https://welist.bitcoin-after.life/data/bitcoin
|
||||||
|
|
||||||
47
promptai.txt
Normal file
47
promptai.txt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
traduci tutti i testi, commenti, docstring, e nomi variabile in inglese.
|
||||||
|
scrivi sempre tutto il codice in inglese.
|
||||||
|
scrivi sempre commenti e docstring su tutti i metodi e classi per spiegarne il funzionamento.
|
||||||
|
|
||||||
|
quando non sei sicuro non inventare, chiedi conferma tenendo pero' conto che non sono un programmatore.
|
||||||
|
siccome ordinare block-height e timestamp e' impossibile, elimina block-height dalla codebase, considera solo i timestamp.
|
||||||
|
|
||||||
|
assicurati che i seguenti stati della transazione vengano assegnati correttamente:
|
||||||
|
ANTICIPATED — La transazione è stata generata con un locktime anticipato di 1 giorno rispetto a una transazione pre-esistente con cui condivide gli stessi eredi. Rimane valida (VALID non viene pulito).
|
||||||
|
REPLACED — La transazione è stata sostituita perché almeno uno dei suoi input viene speso da una nuova transazione con locktime inferiore. La vecchia tx perde lo status VALID e viene marcata REPLACED. Si propaga a cascata ai figli.
|
||||||
|
INVALIDATED — La transazione non è più spendibile perché almeno uno dei suoi input è stato speso da una transazione in mempool o già confermata, e la tx precedente non esiste più nella will. Perde lo status VALID.
|
||||||
|
UPDATED — la transazione e' spendibile e valida, ma una nuova transazione la rimpiazza mantenendo lo stesso locktime e gli stessi eredi
|
||||||
|
MEMPOOL — la transazione e' stata vista nella mempool di electrum
|
||||||
|
CONFIRMED — la transazione e' confermata nella blockchain
|
||||||
|
|
||||||
|
|
||||||
|
aggiungi una opzione nella configurazione del plugin con una checkbox che chiede se firmare automaticamente le transazioni default: True
|
||||||
|
ripristina che il pulsante check firmi le transazioni, e se e' impostata una password quando la chiede se si clicca su annulla, o se la nuova checkbox nelle impostazioni del plugin e' settata per non firmare automaticamente le transazioni deve saltare la fase della firma e come ora dire all'utente di firmare
|
||||||
|
|
||||||
|
modifica gli allarmi dell'evento nel calendario:
|
||||||
|
aggiungi una opzione nelle impostazioni del plugin che chiede il numero di allarmi si vogliono impostare default 3
|
||||||
|
quando si crea l'evento, si sceglie quale e' maggiore tra la data attuale ed il check-alive, si divide il tempo per il numero di allarmi piu' uno, e si impostano gli allarmi sulle prime divisioni.
|
||||||
|
|
||||||
|
crea dei test mock per testare le funzionalità:
|
||||||
|
usa il wallet giovanna7 per prendere dati plausibili ed indirizzi bitcoin validi
|
||||||
|
usa l'environment di electrum in ../electrum/env in sola lettura.
|
||||||
|
|
||||||
|
|
||||||
|
scrivi test per verificare l'output dell'evento per il calendario.
|
||||||
|
testa che l'evento abbia la data e l'ora del valore inferiore tra will-settings['locktime'] o della tx valida con locktime inferiore
|
||||||
|
testa che gli allarmi vengano creati correttamente secondo lo standard ics compatibile con i vari calendari online
|
||||||
|
|
||||||
|
crea almeno 3 eredi con indirizzi, locktime, ed amount validi
|
||||||
|
crea almeno 2 willexecutors con base amount ed indirizzi validi
|
||||||
|
esegui una eredità e confronta gli ammontare degli eredi con gli output della transazione per verificare che siano corretti
|
||||||
|
modifica l'indirizzo di un erede, aggiorna, e verifica che la nuova eredità sia anticipata di un giorno, abbia stato anticipated ed aggiornato l'output dell'erede.
|
||||||
|
modifica l'ammontare di un erede, aggiorna, e verifica che la nuova eredita sia anticipata di un ulteriore giorno, abbia stato anticipated ed aggiornato l'output dell'erede.
|
||||||
|
modifica la base_fee ad un willexecutor, aggiorna l'eredità, ed assicurati che la nuova eredità abbia lo stesso locktime di quella attuale e l'output per il willexecutor aggiornato con la nuova base_fee. la vecchia transazione deve avere lo stato UPDATED e conservare lo stato valido
|
||||||
|
testa che tutti gli stati vengano assegnati correttamente secondo la loro descrizione
|
||||||
|
|
||||||
|
scrivi dei test per testare la connettività
|
||||||
|
simula tutti i tipi di errori di rete e verifica che vengano gestiti correttamente in tutte le varie richieste ai willexecutors ed alla welist
|
||||||
|
|
||||||
|
scrivi test per testare i willexecutors e le loro risposte alle varie interrogazioni.
|
||||||
|
usa il willexecutor di default we.bitcoin-after.life/bitcoin per eseguire le interrogazioni e verificare le risposte.
|
||||||
|
usa la welist all'indirizzo https://welist.bitcoin-after.life/data/bitcoin
|
||||||
|
|
||||||
Reference in New Issue
Block a user