# 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_` 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.` 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 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.