Files
bal-electrum-plugin/PLAN_CMDLINE_PLUGIN.md

24 KiB

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.pyHeirs (dict persistito in wallet DB, chiave "heirs"), validazione (validate_heir, _validate), import_file/export_file, get_transactions/buildTransactions.
    • willexecutors.pyWillexecutors (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.pyWill (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.pyBalPlugin (tutte le BalConfig: chiavi bal_*), BalTimestamp, get_version, registrazione dei dict heirs/will/will_settings nel wallet DB.
    • checkalive.pyresolve_date_to_check, check_alive_expired (riferimento temporale unico per ogni check).
    • util.pyUtil (locktime, quantità, confronto tx/heirs, get_available_utxos, fix_will_settings_tx_fees).
  • bal/gui/qt/window.pyBalWindow 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:
    # 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:

Plugin = _plugin_module.Plugin

5.2 bal/cli/plugin.py

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.pyBalController

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):

  • heirsheirs.save() (via __setitem__/pop già implementati) + wallet.save_db().
  • willsave_willitems() + wallet.save_db().
  • willexecutorsWillexecutors.save(plugin, willexecutors) (config, non wallet DB).
  • settingsBalConfig.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/listjson_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)

source /home/steal/devel/bal/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.