Files
bal-electrum-plugin/bal/cli/commands.py

408 lines
13 KiB
Python

"""
bal.cli.commands
================
CLI commands (``bal_*``) for the Bitcoin After Life plugin.
This module is the *transport layer* of the command-line front-end: every
function is a coroutine decorated with ``@plugin_command`` so Electrum exposes
it as ``bal_<name>`` both on the command line and over JSON-RPC. The functions
validate their arguments and delegate the real work to
:mod:`bal.cli.controller` (a headless replica of the Qt flows); this module
never imports Qt.
It must stay lightweight: Electrum imports it during the CLI pre-parse
(``run_electrum`` calls ``Plugins(config, cmd_only=True)``) and on every
GUI/daemon startup, before any wallet or network object exists. The heavy
imports (``bal.core``, the controller) happen lazily inside each command.
Flags (see ``electrum.commands.plugin_command``):
* ``n`` -> requires a running daemon/network (always set for plugins);
* ``w`` -> resolves and injects the wallet from the daemon;
* ``p`` -> requires the wallet password (for signing).
"""
from electrum.commands import plugin_command
from electrum.util import UserFacingException
from .controller import BalController, _user_facing
plugin_name = "bal"
def _controller(plugin, wallet):
"""Build the headless controller, or fail with a clear message."""
if plugin is None:
raise UserFacingException("the bal plugin is not enabled in this daemon")
if wallet is None:
raise UserFacingException("wallet not loaded")
return BalController(plugin, wallet)
def _call(plugin, wallet, method, *args, **kwargs):
controller = _controller(plugin, wallet)
try:
return getattr(controller, method)(*args, **kwargs)
except Exception as e:
raise _user_facing(e) from e
# --------------------------------------------------------------------------- #
# Settings
# --------------------------------------------------------------------------- #
@plugin_command("n", plugin_name)
async def settings_list(self, plugin=None):
"""List all BAL plugin configuration options (key, name and value).
Returns a JSON object mapping every BAL configuration option (``bal_*``)
to an object with ``value``, ``default`` and ``name``.
"""
return _call(plugin, None, "settings_list")
@plugin_command("n", plugin_name)
async def settings_get(self, key, plugin=None):
"""Show the current value of one BAL configuration option.
arg:str:key:The configuration key (e.g. ``bal_tx_fees``).
"""
return _call(plugin, None, "settings_get", key)
@plugin_command("n", plugin_name)
async def settings_set(self, key, value, plugin=None):
"""Set a BAL configuration option (booleans, integers, strings, JSON).
arg:str:key:The configuration key (e.g. ``bal_user_type``).
arg:str:value:The new value; JSON for object-typed keys such as ``bal_will_settings``.
"""
return _call(plugin, None, "settings_set", key, value)
@plugin_command("n", plugin_name)
async def settings_reset(self, key, plugin=None):
"""Reset a BAL configuration option to its default value.
arg:str:key:The configuration key (e.g. ``bal_tx_fees``).
"""
return _call(plugin, None, "settings_reset", key)
# --------------------------------------------------------------------------- #
# Heirs
# --------------------------------------------------------------------------- #
@plugin_command("nw", plugin_name)
async def heirs_list(self, wallet=None, plugin=None):
"""List the heirs of the current wallet.
Returns a JSON object mapping heir names to their ``[address, amount,
locktime]`` values.
"""
return _call(plugin, wallet, "heirs_list")
@plugin_command("nw", plugin_name)
async def heirs_show(self, name, wallet=None, plugin=None):
"""Show the details of a single heir.
arg:str:name:The heir name.
"""
return _call(plugin, wallet, "heirs_show", name)
@plugin_command("nw", plugin_name)
async def heirs_add(self, name, address, amount, locktime=None, wallet=None, plugin=None):
"""Add (or replace) an heir in the current wallet.
arg:str:name:The heir name.
arg:str:address:The destination address (or ``OP_RETURN:<hex>`` for an OP_RETURN heir).
arg:str:amount:The amount in satoshis or a percentage like ``50%%``.
arg:str:locktime:The delivery locktime (absolute timestamp or ``30d``/``1y``); defaults to the will locktime.
"""
return _call(plugin, wallet, "heirs_add", name, address, amount, locktime)
@plugin_command("nw", plugin_name)
async def heirs_update(
self,
name,
address=None,
amount=None,
locktime=None,
wallet=None,
plugin=None,
):
"""Update an existing heir (only the given fields).
arg:str:name:The heir name.
arg:str:address:The new destination address.
arg:str:amount:The new amount in satoshis or a percentage.
arg:str:locktime:The new delivery locktime.
"""
return _call(plugin, wallet, "heirs_update", name, address, amount, locktime)
@plugin_command("nw", plugin_name)
async def heirs_delete(self, names, wallet=None, plugin=None):
"""Delete one or more heirs.
arg:json:names:A JSON array of heir names (e.g. ``["Alice","Bob"]``).
"""
return _call(plugin, wallet, "heirs_delete", names)
@plugin_command("nw", plugin_name)
async def heirs_import(self, path, wallet=None, plugin=None):
"""Import heirs from a JSON file (validated, merged).
arg:str:path:Path to the JSON file.
"""
return _call(plugin, wallet, "heirs_import", path)
@plugin_command("nw", plugin_name)
async def heirs_export(self, path, wallet=None, plugin=None):
"""Export the heirs to a JSON file.
arg:str:path:Destination file path.
"""
return _call(plugin, wallet, "heirs_export", path)
# --------------------------------------------------------------------------- #
# Will-Executors
# --------------------------------------------------------------------------- #
@plugin_command("nw", plugin_name)
async def willexecutors_list(self, wallet=None, plugin=None):
"""List the will-executors for the current network.
Returns a JSON object mapping executor URLs to their records (address,
base_fee, status, info, selected, ...).
"""
return _call(plugin, wallet, "willexecutors_list")
@plugin_command("nw", plugin_name)
async def willexecutors_show(self, url, wallet=None, plugin=None):
"""Show the details of a single will-executor.
arg:str:url:The will-executor URL.
"""
return _call(plugin, wallet, "willexecutors_show", url)
@plugin_command("nw", plugin_name)
async def willexecutors_add(
self,
url,
address="",
base_fee=0,
info=None,
wallet=None,
plugin=None,
):
"""Add a new will-executor (not selected by default).
arg:str:url:The will-executor base URL.
arg:str:address:The executor fee address for this network.
arg:int:base_fee:The executor base fee in satoshis.
arg:str:info:A human-readable description.
"""
return _call(plugin, wallet, "willexecutors_add", url, address, base_fee, info)
@plugin_command("nw", plugin_name)
async def willexecutors_update(
self,
url,
address=None,
base_fee=None,
info=None,
promo_code=None,
rename_to=None,
wallet=None,
plugin=None,
):
"""Update an existing will-executor (only the given fields).
arg:str:url:The will-executor URL to update.
arg:str:address:The new fee address.
arg:int:base_fee:The new base fee in satoshis.
arg:str:info:The new description.
arg:str:promo_code:The new promo code.
arg:str:rename_to:Optionally move the record to a new URL.
"""
return _call(
plugin,
wallet,
"willexecutors_update",
url,
address,
base_fee,
info,
promo_code,
rename_to,
)
@plugin_command("nw", plugin_name)
async def willexecutors_select(
self, url, value=True, wallet=None, plugin=None
):
"""Select (or deselect) a will-executor.
arg:str:url:The will-executor URL.
arg:bool:value:True to select, False to deselect.
"""
return _call(plugin, wallet, "willexecutors_select", [url], value)
@plugin_command("nw", plugin_name)
async def willexecutors_delete(self, urls, wallet=None, plugin=None):
"""Delete one or more will-executors.
arg:json:urls:A JSON array of executor URLs (e.g. ``["https://we.example.com"]``).
"""
return _call(plugin, wallet, "willexecutors_delete", urls)
@plugin_command("nw", plugin_name)
async def willexecutors_ping(self, urls=None, wallet=None, plugin=None):
"""Ping the selected (or the given) will-executor servers.
Updates status/base_fee/address from each server and saves. Returns
``{url: {status, ok}}``.
arg:json:urls:Optional JSON array of URLs to ping; defaults to the selected executors.
"""
return _call(plugin, wallet, "willexecutors_ping", urls)
@plugin_command("nw", plugin_name)
async def willexecutors_download(self, wallet=None, plugin=None):
"""Download the will-executor list from the welist server and merge it.
Returns the number of records downloaded and the new total.
"""
return _call(plugin, wallet, "willexecutors_download")
@plugin_command("nw", plugin_name)
async def willexecutors_import(self, path, wallet=None, plugin=None):
"""Import will-executors from a JSON file (``{url: record}``).
arg:str:path:Path to the JSON file.
"""
return _call(plugin, wallet, "willexecutors_import", path)
@plugin_command("nw", plugin_name)
async def willexecutors_export(self, path, wallet=None, plugin=None):
"""Export the will-executors to a JSON file.
arg:str:path:Destination file path.
"""
return _call(plugin, wallet, "willexecutors_export", path)
# --------------------------------------------------------------------------- #
# Will
# --------------------------------------------------------------------------- #
@plugin_command("nw", plugin_name)
async def will_status(self, wallet=None, plugin=None):
"""Show the current will: per-transaction status, locktime and executors.
Returns a JSON object with a per-txid detail list and global status counts.
"""
return _call(plugin, wallet, "will_status")
@plugin_command("nw", plugin_name)
async def will_check(self, wallet=None, plugin=None):
"""Check the local coherence of the will (heirs, executors, fees, locktime).
Returns ``{"valid": true}`` when coherent, or raises a descriptive error.
"""
return _call(plugin, wallet, "will_check")
@plugin_command("nw", plugin_name)
async def will_prepare(self, wallet=None, plugin=None):
"""Run the full prepare/inheritance flow (check, rebuild, persist).
Returns a JSON object with ``result`` (``coherent``, ``rebuilt``,
``expired``, ``postponed``) and, when needed, the invalidation
transaction to sign and broadcast.
"""
return _call(plugin, wallet, "prepare_will")
@plugin_command("nwp", plugin_name)
async def will_sign(self, txid=None, password=None, wallet=None, plugin=None):
"""Sign the valid, not-yet-complete will transactions (or just one).
Updates the COMPLETE status and the signature counters and persists.
arg:str:txid:Optional transaction id to sign; signs all valid ones when omitted.
"""
txids = [txid] if txid is not None else None
txs = _call(plugin, wallet, "sign_transactions", password, txids)
return {wid: str(tx) for wid, tx in txs.items()}
@plugin_command("nw", plugin_name)
async def will_broadcast(
self, txid=None, force=False, wallet=None, plugin=None
):
"""Send the signed will transactions to their will-executors (in parallel).
Updates the PUSHED/PUSH_FAIL statuses and persists. Returns ``{url: status}``.
arg:str:txid:Optional transaction id to broadcast; all valid+signed ones when omitted.
arg:bool:force:Force re-pushing transactions already marked as PUSHED.
"""
txids = [txid] if txid is not None else None
return _call(plugin, wallet, "push_transactions_to_willexecutors", force, txids)
@plugin_command("nw", plugin_name)
async def will_export(self, path, wallet=None, plugin=None):
"""Export the whole will to a JSON file.
arg:str:path:Destination file path.
"""
return _call(plugin, wallet, "export_will", path)
@plugin_command("nw", plugin_name)
async def will_import_merge(self, path, wallet=None, plugin=None):
"""Merge a will file into the current will (PSBTs and statuses are merged).
arg:str:path:Path to the will JSON file.
"""
return _call(plugin, wallet, "merge_will_from_file", path)
@plugin_command("nw", plugin_name)
async def will_invalidate(self, wallet=None, plugin=None):
"""Build the on-chain invalidation transaction for the current will.
Returns ``{txid, tx}`` (or nulls when there is nothing to invalidate); the
transaction still needs to be signed and broadcast.
"""
return _call(plugin, wallet, "invalidate_will_command")
@plugin_command("nw", plugin_name)
async def will_check_executor(self, txid=None, wallet=None, plugin=None):
"""Ask the will-executors whether they hold our pushed transactions.
Runs the searchtx check in parallel, applies the per-item status and
persists. Returns ``{txid: {url, pushed, checked, check_fail}}``.
arg:str:txid:Optional transaction id to check; checks all pending ones when omitted.
"""
txids = [txid] if txid is not None else None
return _call(plugin, wallet, "check_transactions", txids)