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:
- Willexecutors — elenco, aggiunta, modifica, selezione, eliminazione, import/export, ping, download lista.
- Heirs — elenco, aggiunta, modifica, eliminazione, import/export.
- Impostazioni — lettura e modifica (
settings set chiave=valore), reset a default. - 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)inelectrum/commands.py:2317. Un comando plugin:- è sempre un
async def; - viene registrato come
bal_<nome_funzione>suCommands(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).
- è sempre un
- Pre-parse CLI (
run_electrumriga 425):Plugins(tmp_config, cmd_only=True)importa solo l'__init__.pydi ogni plugin abilitato per registrare i comandi nel parser. In modalitàcmd_onlyil filtroavailable_forviene saltato (plugin.py:128), ma serveconfig['plugins.bal.enabled'] is True(plugin.py:117). - Daemon (
daemon.py:626):Plugins(self.config, 'cmdline'). Qui il filtroavailable_forvale: il plugin deve dichiarare"cmdline". - Caricamento entry-point (
plugin.py:622): il daemon importaelectrum.plugins.bal.<gui_name>congui_name='cmdline', quindi serve un modulobal/cmdline.pycon una classePlugin. - Iniezione wallet: il decorator
@command(righe 170-194) gestisce i flag:'w'→ risolve e iniettawalletdadaemon.get_wallet(wallet_path)(il wallet deve essere già caricato conelectrum 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 erroriUserFacingExceptionvengono 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(configbal_willexecutors, chiave perchainname),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) eWillItem(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 leBalConfig: chiavibal_*),BalTimestamp,get_version, registrazione dei dictheirs/will/will_settingsnel 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—BalWindowcontiene 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) ebuild_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 subal/(esclude__pycache__,.pyc), quindi includerà automaticamente i nuovi file dibal/cli/ebal/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 dibal/core/). Può importare solobal.core,electrum.*e stdlib.commands.py= livello di trasporto: firmaasync 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 diBalWindow, ma con errori espressi come eccezioni (i messaggi GUIshow_message/show_errordiventano raise/ritorni), e persiste esplicitamente su wallet DB.plugin.pyè quasi vuoto: ereditaBalPlugin.__init__e basta (serve solo perché Electrum istanzimodule.Plugin(self, config, name)).- Nessuna dipendenza nuova richiesta:
aiohttp,dnse il resto sono già usati dabal/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:
(oppure
# 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)from .cli import commandsesplicito). - Accortezza:
bal/cli/commands.pydeve 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/ecmdline.py. - Opzionale (consigliato): aggiungere una stampa di avviso quando l'archivio
contiene sia
cmdline.pycheqt.py, e verificare chemanifest.jsonabbia entrambi i valori inavailable_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.py — BalController
Facciata per-wallet che incapsula lo stato e i flussi. Attributi (speculari a
BalWindow):
plugin(ilBalPlugin/Plugininiettato),wallet(iniettato da Electrum),will_settings(daplugin.WILL_SETTINGS.get()+Util.fix_will_settings_tx_fees),heirs(Heirs(wallet)validati),willexecutors(Willexecutors.get_willexecutors(plugin)),willitems(dawallet.db.get_dict("will")→WillItem(w, wallet=wallet)),date_to_check(viaresolve_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__/popgià 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 daplugin_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/dictstampati 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 dabal/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 stampatomsgcon exit 1. - Errori di dominio BAL (
WillExpiredException,WillPostponedException,HeirNotFoundException,NoWillExecutorNotPresent,CheckAliveError,AmountException, ...): il controller le converte inUserFacingExceptioncon testo in chiaro (riuso dei messaggi già presenti, senza HTML/Qt). - Convenzione consigliata per comandi che producono più di un risultato:
ritornare un
dictcon chiave"result"/"warnings"quando servono avvisi (es. dopopreparecon heirs scartati per dust).
9. Compatibilità Electrum 4.7.2 / 4.8.0
plugin_command, il wrapper@commandedaemon._plugins.get_pluginesistono in entrambe le versioni (verificati su 4.8.0; usati identici daswapserver).- Il
BalPlugingià gestisce il cambio API di registrazione dict (json_db.register_dictvsstored_dict.register_name): nessun intervento. available_for: ["cmdline"]è lo stesso meccanismo ditrustedcoin(che ha giàcmdline.pyin 4.8.0).- Nessun nuovo import Qt in
bal/cli/: verificabile in CI con un check statico subal/cli/*.pyebal/cmdline.py.
10. Build / release
python3 build_zip.pyproducebal-electrum-plugin.zipconcli/,cmdline.pye il manifest aggiornato. Lo zip serve sia per la GUI che per il daemon.- Il test
external_zip_test.pyandrà esteso (vedi §11) per verificare che il zip, caricato da Electrum, registri anche i comandibal_*. - 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.balconPlugins(config, cmd_only=True); - asserisce che
known_commandscontenga tutti i nomibal_*della tabella; - asserisce che ogni funzione sia coroutine e abbia il flag
n.
- importa
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.
- wallet "fake"/temporaneo (pattern di
tests/test_cli_zip.py(o estensione diexternal_zip_test.py):- costruisce lo zip, lo carica come
electrum_external_plugins.balconPlugins(config, 'cmdline'), asserisceavailable_forinclude"cmdline"e cheget_plugin('bal')restituisca ilPlugindibal.cli.plugin(nessun import Qt eseguito).
- costruisce lo zip, lo carica come
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 "$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.baldeve continuare a passare (prova chebal/__init__+ Qt convivono con il nuovo import dibal.cli.commands).- Eseguire i
test_core_*.pyesistenti (nessuna logica core toccata). - Ruff: evitare nuove violazioni in
bal/cli/.
12. Rischi e decisioni aperte
- Daemon obbligatorio (non
--offline): imposto daplugin_command. → Accettato; documentato al §3.1. - Wallet pre-caricato: i comandi
wfalliscono con "wallet not loaded" se non si lancia primaelectrum load_wallet. → Documentare. bal/__init__.pyche importabal.cli.commands: viene eseguito anche all'avvio della GUI.commands.pydeve restare leggero (solo definizioni + import dielectrum.commandsebal.core). Da verificare consmoke_test.py.- Doppio caricamento: se un install è contemporaneamente interno E zip
esterno, la seconda importazione di
commands.pypotrebbe sollevare "Command name bal_... already exists". Pratica corrente: un solo install; si può mitigare con un guardif not getattr(module, '_registered'). - OP_RETURN heirs in CLI: gestiti come in GUI (
validate_op_return_hex, colonne quantità"0"). Da testare. - Persistenza
will_settings: oggi letta dalla config globale (bal_will_settings) inBalWindow.__init__, non dal wallet DB. Il controller deve replicare esattamente questo (config), non introdurre una seconda sorgente. - 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)
bal/cli/__init__.py,bal/cli/plugin.py,bal/cmdline.py, updatebal/manifest.json+bal/__init__.py.tests/test_cli_commands_registered.py+ verificasmoke_test.py.bal/cli/controller.py(read-only: status/list/show) →commands.pyper willexecutors/heirs/settings (senza rete).- Comandi will:
prepare,sign,export,import_merge,invalidate. - Comandi di rete:
ping,download,broadcast,check_executor. - Test zip (
test_cli_zip.py), estensioneexternal_zip_test.py, prova manuale col daemon, aggiornamento README/HANDOFF.