Files
bal-electrum-plugin/PLAN_CMDLINE_PLUGIN.md

416 lines
24 KiB
Markdown

# Piano: supporto da riga di comando (CLI) per il plugin BAL
> **Stato**: solo piano. Nessun codice viene modificato finché il piano non viene approvato.
>
> **Versione di riferimento**: commit `2221389` (`core: anchor relative locktime/threshold recipes...`), working tree pulito.
---
## 1. Obiettivo
Rendere il plugin **Bitcoin After Life** utilizzabile da riga di comando / daemon
di Electrum, senza GUI Qt, esponendo comandi per:
1. **Willexecutors** — elenco, aggiunta, modifica, selezione, eliminazione, import/export, ping, download lista.
2. **Heirs** — elenco, aggiunta, modifica, eliminazione, import/export.
3. **Impostazioni** — lettura e modifica (`settings set chiave=valore`), reset a default.
4. **Will** — ciclo di vita completo: visualizza stato, check di coerenza, prepara/ricostruisci, firma, import/merge, esporta, invalida, trasmette ai will-executor, verifica lato will-executor (searchtx).
Il tutto riusando **esclusivamente la logica già presente in `bal/core/`** (che è
già GUI-free) e senza importare mai PyQt.
---
## 2. Stato attuale (verificato sul codice)
### 2.1 Meccanica di Electrum (4.8.0, checkout `electrum/`)
Ho verificato sul codice reale (`electrum/commands.py`, `electrum/plugin.py`,
`electrum/daemon.py`, `run_electrum`) i punti che governano i comandi dei plugin:
- **Registrazione comandi**: `@plugin_command(s, plugin_name)` in
`electrum/commands.py:2317`. Un comando plugin:
- è **sempre** un `async def`;
- viene registrato come `bal_<nome_funzione>` su `Commands` (quindi anche nel parser CLI);
- **forza il flag `'n'`** (richiede rete/daemon): *tutti* i comandi plugin richiedono un daemon in esecuzione e NON funzionano con `--offline`;
- alla chiamata inietta `plugin = daemon._plugins.get_plugin('bal')` (riga 2337).
- **Pre-parse CLI** (`run_electrum` riga 425): `Plugins(tmp_config, cmd_only=True)` importa solo l'`__init__.py` di ogni plugin abilitato per registrare i comandi nel parser. In modalità `cmd_only` il filtro `available_for` viene **saltato** (`plugin.py:128`), ma serve `config['plugins.bal.enabled'] is True` (`plugin.py:117`).
- **Daemon** (`daemon.py:626`): `Plugins(self.config, 'cmdline')`. Qui il filtro `available_for` **vale**: il plugin deve dichiarare `"cmdline"`.
- **Caricamento entry-point** (`plugin.py:622`): il daemon importa `electrum.plugins.bal.<gui_name>` con `gui_name='cmdline'`, quindi serve un modulo `bal/cmdline.py` con una classe `Plugin`.
- **Iniezione wallet**: il decorator `@command` (righe 170-194) gestisce i flag:
- `'w'` → risolve e inietta `wallet` da `daemon.get_wallet(wallet_path)` (il wallet deve essere già caricato con `electrum load_wallet`);
- `'p'` → richiede `--password` (o wallet già sbloccato) per le operazioni di firma.
- **Output**: il valore di ritorno del comando viene stampato come JSON da `run_electrum` (righe 626-630); in modalità daemon gli errori `UserFacingException` vengono stampati con exit code 1.
### 2.2 Il plugin (bal v0.6.1)
- `bal/core/` è già GUI-free e contiene tutta la logica riutilizzabile:
- `heirs.py``Heirs` (dict persistito in wallet DB, chiave `"heirs"`), validazione (`validate_heir`, `_validate`), `import_file`/`export_file`, `get_transactions`/`buildTransactions`.
- `willexecutors.py``Willexecutors` (config `bal_willexecutors`, chiave per `chainname`), `get_willexecutors`, `save`, `initialize_willexecutor`, `is_selected`, `is_valid`, `ping_servers_parallel`, `push_transactions_parallel`, `check_transactions_parallel`, `check_transaction`, `download_list`, `get_willexecutors_list_from_json`.
- `will.py``Will` (statiche) e `WillItem` (stato per-tx: `VALID/COMPLETE/PUSHED/CHECKED/...`), `is_will_valid`, `check_will`, `check_willexecutors_and_heirs`, `invalidate_will`, `normalize_will`, `get_min_locktime`, `get_tx_from_any`, `set_check_willexecutor`, `save_valid_transactions_to_history`.
- `plugin_base.py``BalPlugin` (tutte le `BalConfig`: chiavi `bal_*`), `BalTimestamp`, `get_version`, registrazione dei dict `heirs`/`will`/`will_settings` nel wallet DB.
- `checkalive.py``resolve_date_to_check`, `check_alive_expired` (riferimento temporale unico per ogni check).
- `util.py``Util` (locktime, quantità, confronto tx/heirs, `get_available_utxos`, `fix_will_settings_tx_fees`).
- `bal/gui/qt/window.py``BalWindow` contiene i flussi da **replicare in headless** (non riusabile direttamente perché legato a Qt):
- `init_will` (riga 151), `load_willitems`/`save_willitems` (120/129),
- `init_class_variables` (618) e `build_will` (397),
- `build_inheritance_transaction` (678) → il flusso completo "prepara will",
- `sign_transactions` (952), `ask_password_and_sign_transactions` (1084),
- `push_transactions_to_willexecutors` (1164), `broadcast_transactions` (1127),
- `check_transactions_task`/`check_transactions` (1414/1464),
- `export_json_file` (1246), `merge_will` (1264), `merge_will_from_file` (1348), `_load_will_file` (1406),
- `invalidate_will` (917).
- `bal/manifest.json`: `"available_for": ["qt"]`, `"version": "0.6.1"`.
- `build_zip.py`: cammina ricorsivamente su `bal/` (esclude `__pycache__`, `.pyc`), quindi **includerà automaticamente** i nuovi file di `bal/cli/` e `bal/cmdline.py`.
---
## 3. Architettura proposta
```
bal/
__init__.py # MODIFICATO: importa ``from .cli import commands`` (registra i comandi)
cmdline.py # NUOVO: shim zip-safe (come qt.py) che ri-espone Plugin da bal.cli.plugin
cli/
__init__.py # NUOVO
commands.py # NUOVO: tutti i @plugin_command (async), sottili, delegano al controller
controller.py # NUOVO: BalController — facciata headless per-wallet (replica di BalWindow senza Qt)
plugin.py # NUOVO: class Plugin(BalPlugin) — entry-point per il daemon (gui_name='cmdline')
manifest.json # MODIFICATO: available_for = ["qt", "cmdline"]
```
Principi:
- **`bal/cli/` non importa mai Qt** (stessa regola di `bal/core/`). Può importare solo `bal.core`, `electrum.*` e stdlib.
- **`commands.py` = livello di trasporto**: firma `async def bal_x(self, wallet=None, plugin=None, ...)`, valida/parsa argomenti, chiama il controller, ritorna strutture JSON-serializzabili. Zero logica di business.
- **`controller.py` = il cuore**: replica i passi GUI-free di `BalWindow`, ma con errori espressi come eccezioni (i messaggi GUI `show_message`/`show_error` diventano raise/ritorni), e persiste esplicitamente su wallet DB.
- **`plugin.py`** è quasi vuoto: eredita `BalPlugin.__init__` e basta (serve solo perché Electrum istanzi `module.Plugin(self, config, name)`).
- **Nessuna dipendenza nuova** richiesta: `aiohttp`, `dns` e il resto sono già usati da `bal/core`.
### 3.1 Perché i comandi richiedono il daemon
`plugin_command` forza il flag `'n'` in `commands.py:2321-2322`. Conseguenza
architetturale da documentare chiaramente:
```
electrum daemon -d # avvia il daemon (rete + plugin cmdline)
electrum load_wallet # carica/sblocca il wallet
electrum bal_heirs_list # i comandi BAL girano contro il daemon
```
Questa è la stessa limitazione di tutti gli altri plugin con comandi CLI
(es. `swapserver`, `nwc`). Non è aggirabile senza hackare `plugin_command`, che
escludiamo dal piano.
---
## 4. Modifiche ai file esistenti
### 4.1 `bal/manifest.json`
- `"available_for": ["qt", "cmdline"]`.
Nessun cambio di versione necessario per lo sviluppo; la versione si alzerà in
`make-release.sh` come già avviene.
### 4.2 `bal/__init__.py`
- Aggiungere in fondo:
```python
# Registra i comandi CLI (bal_*) appena Electrum importa il pacchetto,
# sia in modalità cmd_only (pre-parse) sia nel daemon.
from . import cli # noqa: F401 (importa bal.cli.commands, che registra i @plugin_command)
```
(oppure `from .cli import commands` esplicito).
- Accortezza: `bal/cli/commands.py` deve essere importabile **senza Qt** e senza
effetti collaterali pesanti, perché viene importato anche nel pre-parse CLI e
all'avvio della GUI.
### 4.3 `build_zip.py`
- Nessuna modifica obbligatoria: il walker include già `cli/` e `cmdline.py`.
- **Opzionale (consigliato)**: aggiungere una stampa di avviso quando l'archivio
contiene sia `cmdline.py` che `qt.py`, e verificare che `manifest.json` abbia
entrambi i valori in `available_for`.
---
## 5. Nuovi file
### 5.1 `bal/cmdline.py` (shim, ~stesso schema di `qt.py`)
Riproduce il pattern zip-safe di `qt.py` (creazione dei package intermedi in
`sys.modules`, import via `importlib.import_module`), ma punta a
`bal.cli.plugin`:
```python
Plugin = _plugin_module.Plugin
```
### 5.2 `bal/cli/plugin.py`
```python
class Plugin(BalPlugin):
def __init__(self, parent, config, name):
BalPlugin.__init__(self, parent, config, name)
```
Niente hook Qt, niente `bal_windows`. Il daemon lo istanzia quando
`get_plugin('bal')` viene chiamato dal wrapper di `plugin_command`.
### 5.3 `bal/cli/controller.py` — `BalController`
Facciata per-wallet che incapsula lo stato e i flussi. Attributi (speculari a
`BalWindow`):
- `plugin` (il `BalPlugin`/`Plugin` iniettato),
- `wallet` (iniettato da Electrum),
- `will_settings` (da `plugin.WILL_SETTINGS.get()` + `Util.fix_will_settings_tx_fees`),
- `heirs` (`Heirs(wallet)` validati),
- `willexecutors` (`Willexecutors.get_willexecutors(plugin)`),
- `willitems` (da `wallet.db.get_dict("will")` → `WillItem(w, wallet=wallet)`),
- `date_to_check` (via `resolve_date_to_check`).
Metodi principali (replicano le funzioni Qt, senza dialoghi):
| Metodo | Replica di (`window.py`) | Note |
|---|---|---|
| `load_willitems()` | 120 | Costruisce i `WillItem` dal dict `will` del wallet DB. |
| `save_willitems()` | 129 | `to_dict()` con `tx` serializzato a stringa, `json.dumps` di prova, scrittura su `wallet.db` + `wallet.save_db()`. |
| `init_class_variables()` | 618 | `date_to_check`, `no_willexecutor`, `willexecutors`, check `check_alive_expired`. |
| `check_will()` | 473 | `Will.is_will_valid(...)`; le eccezioni di dominio vengono propagate al comando. |
| `build_inheritance_transaction()` | 678 | Flusso 1/7→2/7 replicato: `Will.check_amounts`, guardie locktime/willexecutor, `check_will()` e rebuild su `NotCompleteWillException`. Le `show_message/show_error` diventano raise (`UserFacingException` con testo chiaro) oppure ritorni `{"status": "postponed", "invalidation": tx}`. |
| `sign_transactions(password)` | 952 | Firma i `VALID` non completi: fixup input dai willitems padre, `wallet.sign_transaction(tx, password, ignore_warnings=True)`, `set_status("COMPLETE")`, `check_signatures`. |
| `push_transactions_to_willexecutors(force)` | 1164 | `get_willexecutor_transactions` + `push_transactions_parallel` + gestione "already present" con `check_transaction`. Aggiorna `PUSHED/PUSH_FAIL`. |
| `check_transactions()` | 1414 | `check_transactions_parallel` + `set_check_willexecutor(res)` per item. |
| `export_json_file(path)` | 1246 | `write_json_file(path, {wid: wi.to_dict()...})` con `tx` come stringa (formato identico a `_load_will_file`). |
| `merge_will_from_file(path)` | 1348 | `_load_will_file` + `merge_will` (stessa semantica di `window.py:1264`). |
| `_load_will_file(path)` | 1406 | `read_json_file` + `tx_from_any` + `WillItem`. |
| `invalidate_will()` | 917 | `Will.invalidate_will(...)` con `history_label` e `will_locktime`. |
| `fetch_will_executors_list()` / `ping()` | 1491/1771 | `download_list(old, welist_server)` + `ping_servers_parallel`, poi `Willexecutors.save(plugin, ...)`. |
| `apply_settings(cfg_name, value)` | — | Mappa il nome chiave all'attributo `BalConfig` del plugin e fa `set(...)`. |
Regole di persistenza (fondamentali):
- **heirs** → `heirs.save()` (via `__setitem__`/`pop` già implementati) + `wallet.save_db()`.
- **will** → `save_willitems()` + `wallet.save_db()`.
- **willexecutors** → `Willexecutors.save(plugin, willexecutors)` (config, non wallet DB).
- **settings** → `BalConfig.set(...)` (config).
### 5.4 `bal/cli/commands.py` — comandi (tutti `async def` + `@plugin_command`)
Firma standard: `async def bal_x(self, wallet=None, plugin=None, ...)`. Flag:
- `'n'` — imposto automaticamente da `plugin_command` (rete/daemon).
- `'w'` — wallet richiesto e iniettato da Electrum.
- `'p'` — solo per i comandi che firmano (richiede `--password`).
Tutti i comandi costruiscono `controller = BalController(plugin, wallet)` e
ritornano strutture JSON-serializzabili. Elenco completo al §6.
---
## 6. Tabella comandi
Convenzioni:
- `<WALLET>`: wallet caricato nel daemon (non serve passarlo; Electrum usa quello
configurato o `--wallet`).
- Output: `list`/`dict` stampati come JSON; exit 0 su successo, 1 su errore.
- `*` = richiede password (`--password`) se il wallet è cifrato.
### 6.1 Willexecutors
| Comando | Flag | Argomenti | Descrizione / output |
|---|---|---|---|
| `bal_willexecutors_list` | `nw` | — | Elenco `{url: {address, base_fee, status, info, selected, last_update, sort}}` per la chain corrente. |
| `bal_willexecutors_show` | `nw` | `url` | Dettaglio di un singolo will-executor. |
| `bal_willexecutors_add` | `nw` | `url` `address` `base_fee` | Aggiunge/aggiorna un will-executor (via `initialize_willexecutor`), `selected=false` di default. Ritorna il record. |
| `bal_willexecutors_update` | `nw` | `url` `[address]` `[base_fee]` `[info]` `[promo_code]` | Modifica i campi indicati e salva. |
| `bal_willexecutors_select` | `nw` | `url` `value` | `is_selected(we, eval_bool(value))` + salva. |
| `bal_willexecutors_delete` | `nw` | `url` | Rimuove dalla lista e salva. |
| `bal_willexecutors_ping` | `nw` | `[url]` | `ping_servers_parallel` (tutti o uno); aggiorna `status/base_fee/address`; salva. Output: risultati per url. |
| `bal_willexecutors_download` | `nw` | — | `download_list(old, plugin.WELIST_SERVER.get())`; unisce e salva. Output: n. record. |
| `bal_willexecutors_import` | `nw` | `path` | Legge un JSON `{url: record}` (stesso formato di export), `initialize_willexecutor` per record, salva. |
| `bal_willexecutors_export` | `nw` | `path` | Scrive `{url: record}` su file JSON. |
### 6.2 Heirs
| Comando | Flag | Argomenti | Descrizione / output |
|---|---|---|---|
| `bal_heirs_list` | `nw` | — | `{name: [address, amount, locktime, ...]}` (tutte le colonne `HEIR_*`). |
| `bal_heirs_show` | `nw` | `name` | Dettaglio di un singolo heir. |
| `bal_heirs_add` | `nw` | `name` `address` `amount` `locktime` | Valida con `Heirs.validate_heir` (OP_RETURN incluso) e salva. `amount` può essere satoshi o `"50%"`. `locktime` può essere timestamp assoluto o relativo `"30d"`/`"1y"`. |
| `bal_heirs_update` | `nw` | `name` `[address]` `[amount]` `[locktime]` | Modifica i campi indicati (ri-validazione) e salva. |
| `bal_heirs_delete` | `nw` | `name` | `heirs.pop(name)` + `save_db()`. |
| `bal_heirs_import` | `nw` | `path` | `Heirs.import_file(path)` (validazione + merge). |
| `bal_heirs_export` | `nw` | `path` | `Heirs.export_file(path)`. |
### 6.3 Impostazioni
| Comando | Flag | Argomenti | Descrizione / output |
|---|---|---|---|
| `bal_settings_list` | `n` | — | Elenco di tutte le `BalConfig` del plugin: `{chiave: {value, default, name}}` (nome leggibile). |
| `bal_settings_get` | `n` | `key` | Valore corrente di una chiave (`bal_*`). |
| `bal_settings_set` | `n` | `key=value` | Scrive il valore (conversione di tipo: bool/int/str/JSON) via `BalConfig.set(...)`. `bal_will_settings` accetta JSON. |
| `bal_settings_reset` | `n` | `key` | `BalConfig.set(cfg.default)`. |
### 6.4 Will
| Comando | Flag | Argomenti | Descrizione / output |
|---|---|---|---|
| `bal_will_status` | `nw` | — | Per ogni `wid` (txid): locktime, `heirsvalue`, executor, flag di stato (`VALID/COMPLETE/PUSHED/CHECKED/CHECK_FAIL/...`), `sigs_have/sigs_required`, `tx_fees`, executor URL. |
| `bal_will_check` | `nw` | — | `check_will()` (coerenza heirs+executor+fees+locktime, in locale). Ritorna `{"valid": true}` o un errore esplicito (es. `HeirNotFound`, `WillPostponed`, `WillExpired`, `NoHeirs`). |
| `bal_will_prepare` | `nw` | — | Flusso completo `build_inheritance_transaction`: check → rebuild se non coerente → persiste. Output: riepilogo tx nuova/aggiornata per wid. |
| `bal_will_sign` | `nwp` | `[txid]` | Firma i `VALID` non completi (o solo `txid`). Aggiorna `COMPLETE` e `sigs_*`; persiste. Output per txid. |
| `bal_will_broadcast` | `nw` | `[txid]` `force` | `push_transactions_to_willexecutors(force, txids)` parallelo; aggiorna `PUSHED/PUSH_FAIL`. Output: `{url: status}`. |
| `bal_will_export` | `nw` | `path` | `export_json_file(path)`. |
| `bal_will_import_merge` | `nw` | `path` | `merge_will_from_file(path)` (stessa semantica GUI: merge psbt/stati, mai perdere una tx viva). |
| `bal_will_invalidate` | `nw` | — | `Will.invalidate_will(...)`; ritorna la tx di invalidazione (da firmare+trasmettere con i comandi sopra). |
| `bal_will_check_executor` | `nw` | `[txid]` | Verifica lato will-executor: `check_transactions_parallel` (searchtx) per i `VALID+PUSHED` non `CHECKED`; applica `set_check_willexecutor`. Output: `{wid: {url, checked, ok}}`. |
---
## 7. Flusso dati e persistenza
```
CLI (electrum bal_*) Daemon (Electrum 4.8.0)
┌───────────────────────┐ ┌──────────────────────────────────────┐
│ run_electrum │ RPC │ Daemon.run_cmdline │
│ pre-parse cmd_only │ ─────────────► │ plugin_command wrapper │
│ -> importa bal │ jsonrpc │ inietta plugin + wallet │
│ (registra bal_*) │ │ bal/cli/commands.py │
└───────────────────────┘ │ -> BalController(plugin, wallet) │
│ -> bal.core.* │
│ -> wallet.db / config (persist) │
└──────────────────────────────────────┘
```
- **Lettura**: `wallet.db.get_dict("will")` (wills), `Heirs(wallet)` (heirs),
`plugin.WILLEXECUTORS.get()`/`plugin.WILL_SETTINGS.get()` (config).
- **Scrittura**: `save_willitems()` → `wallet.db` + `wallet.save_db()`;
`heirs.save()`; `Willexecutors.save(...)`; `BalConfig.set(...)`.
- **Firma**: `wallet.sign_transaction(tx, password, ignore_warnings=True)` —
idem GUI, quindi compatibile con multisig e wallet cifrati (password via `--password`).
- **Rete**: `Network.get_instance()` già usato da `bal/core/willexecutors.py`
(i comandi `'n'` garantiscono rete attiva).
---
## 8. Errori, exit code, output
- Ritorno `None` → nessun output; `str` → stampato; `dict`/`list` → `json_encode`.
- Errori utente: sollevare `electrum.util.UserFacingException(msg)` → in modalità
daemon viene stampato `msg` con exit 1.
- Errori di dominio BAL (`WillExpiredException`, `WillPostponedException`,
`HeirNotFoundException`, `NoWillExecutorNotPresent`, `CheckAliveError`,
`AmountException`, ...): il controller le converte in `UserFacingException`
con testo in chiaro (riuso dei messaggi già presenti, senza HTML/Qt).
- Convenzione consigliata per comandi che producono più di un risultato:
ritornare un `dict` con chiave `"result"`/`"warnings"` quando servono avvisi
(es. dopo `prepare` con heirs scartati per dust).
---
## 9. Compatibilità Electrum 4.7.2 / 4.8.0
- `plugin_command`, il wrapper `@command` e `daemon._plugins.get_plugin` esistono
in entrambe le versioni (verificati su 4.8.0; usati identici da `swapserver`).
- Il `BalPlugin` già gestisce il cambio API di registrazione dict
(`json_db.register_dict` vs `stored_dict.register_name`): nessun intervento.
- `available_for: ["cmdline"]` è lo stesso meccanismo di `trustedcoin`
(che ha già `cmdline.py` in 4.8.0).
- **Nessun nuovo import Qt** in `bal/cli/`: verificabile in CI con un check
statico su `bal/cli/*.py` e `bal/cmdline.py`.
---
## 10. Build / release
- `python3 build_zip.py` produce `bal-electrum-plugin.zip` con `cli/`, `cmdline.py`
e il manifest aggiornato. Lo zip serve sia per la GUI che per il daemon.
- Il test `external_zip_test.py` andrà esteso (vedi §11) per verificare che il
zip, caricato da Electrum, registri anche i comandi `bal_*`.
- Nessun cambiamento a `make-release.sh` (la versione resta nel manifest).
---
## 11. Piano di test e verifica
### 11.1 Nuovi test standalone (stile repo: `tests/test_*.py` con `if __name__ == "__main__"`)
- `tests/test_cli_commands_registered.py` (runtime env):
- importa `electrum.plugins.bal` con `Plugins(config, cmd_only=True)`;
- asserisce che `known_commands` contenga tutti i nomi `bal_*` della tabella;
- asserisce che ogni funzione sia coroutine e abbia il flag `n`.
- `tests/test_cli_controller.py` (runtime env, offline, senza rete):
- wallet "fake"/temporaneo (pattern di `test_core_heirs.py`);
- CRUD heirs e willexecutors, settings get/set/reset, export/import will
(merge), build will con fixtures note.
- `tests/test_cli_zip.py` (o estensione di `external_zip_test.py`):
- costruisce lo zip, lo carica come `electrum_external_plugins.bal` con
`Plugins(config, 'cmdline')`, asserisce `available_for` include `"cmdline"`
e che `get_plugin('bal')` restituisca il `Plugin` di `bal.cli.plugin`
(nessun import Qt eseguito).
- `tests/test_cli_will_flows.py` (offline, dove possibile):
- prepare → sign → export → merge su un wallet di test con heirs fissi;
- verifica che `wallet.db.get_dict("will")` rifletta COMPLETE/PUSHED dopo
le operazioni che non toccano rete.
### 11.2 Verifica manuale (da documentare nel README/HANDOFF)
```bash
source "$BAL_HOME/electrum/env/bin/activate"
electrum daemon -d
electrum load_wallet
electrum bal_heirs_list
electrum bal_settings_list
electrum bal_will_status
electrum bal_will_prepare
electrum bal_will_sign --password '...' # se wallet cifrato
electrum bal_will_broadcast
electrum bal_will_check_executor
electrum bal_willexecutors_ping
electrum stop
```
### 11.3 Regressione
- `QT_QPA_PLATFORM=offscreen python3 tests/smoke_test.py electrum.plugins.bal`
deve continuare a passare (prova che `bal/__init__` + Qt convivono con il
nuovo import di `bal.cli.commands`).
- Eseguire i `test_core_*.py` esistenti (nessuna logica core toccata).
- Ruff: evitare nuove violazioni in `bal/cli/`.
---
## 12. Rischi e decisioni aperte
1. **Daemon obbligatorio** (non `--offline`): imposto da `plugin_command`.
→ Accettato; documentato al §3.1.
2. **Wallet pre-caricato**: i comandi `w` falliscono con "wallet not loaded" se
non si lancia prima `electrum load_wallet`. → Documentare.
3. **`bal/__init__.py` che importa `bal.cli.commands`**: viene eseguito anche
all'avvio della GUI. `commands.py` deve restare leggero (solo definizioni +
import di `electrum.commands` e `bal.core`). Da verificare con `smoke_test.py`.
4. **Doppio caricamento**: se un install è contemporaneamente interno E zip
esterno, la seconda importazione di `commands.py` potrebbe sollevare
"Command name bal_... already exists". Pratica corrente: un solo install;
si può mitigare con un guard `if not getattr(module, '_registered')`.
5. **OP_RETURN heirs** in CLI: gestiti come in GUI (`validate_op_return_hex`,
colonne quantità `"0"`). Da testare.
6. **Persistenza `will_settings`**: oggi letta dalla config globale
(`bal_will_settings`) in `BalWindow.__init__`, non dal wallet DB. Il
controller deve replicare esattamente questo (config), non introdurre una
seconda sorgente.
7. **Multisig**: la firma usa `wallet.sign_transaction` → supportata; il flusso
"merge PSBT" copre la firma parziale. Test dedicato con wallet multisig in
fase di implementazione.
---
## 13. Fasi di implementazione (ordine proposto)
1. `bal/cli/__init__.py`, `bal/cli/plugin.py`, `bal/cmdline.py`, update
`bal/manifest.json` + `bal/__init__.py`.
2. `tests/test_cli_commands_registered.py` + verifica `smoke_test.py`.
3. `bal/cli/controller.py` (read-only: status/list/show) → `commands.py` per
willexecutors/heirs/settings (senza rete).
4. Comandi will: `prepare`, `sign`, `export`, `import_merge`, `invalidate`.
5. Comandi di rete: `ping`, `download`, `broadcast`, `check_executor`.
6. Test zip (`test_cli_zip.py`), estensione `external_zip_test.py`, prova
manuale col daemon, aggiornamento README/HANDOFF.