firmware/testing/electrum.py
2026-06-22 12:46:50 -04:00

97 lines
3.5 KiB
Python

# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# Lightweight pytest wrapper around the `electrum` CLI in --regtest --offline mode.
# No backend (electrs/ElectrumX) needed: UTXOs are fed via `addtransaction`,
# with raw tx hex coming from the `bitcoind` fixture. Targets Electrum 4.7+
import os, time, shutil, pytest, tempfile, subprocess
class Electrum:
def __init__(self, path):
self.electrum_path = path
self.datadir = tempfile.mkdtemp(prefix="electrum-test-")
self.daemon_started = False
def _cli(self, *args, offline=False):
# `--offline` is required for commands run *before* the daemon starts
# (setconfig, daemon -d) and rejected for commands that talk *to* the
# running daemon (restore, load_wallet, addtransaction, payto).
cmd = [self.electrum_path, "--regtest"]
if offline:
cmd.append("--offline")
cmd += ["-D", self.datadir, *args]
return subprocess.run(cmd, capture_output=True, text=True, check=True)
def start(self):
# Pre-daemon commands run --offline.
self._cli("setconfig", "log_to_file", "false", offline=True)
self._cli("daemon", "-d", offline=True)
self.daemon_started = True
time.sleep(1.5) # let RPC bind
def stop(self):
if self.daemon_started:
try:
self._cli("daemon", "stop")
except subprocess.CalledProcessError:
pass
self.daemon_started = False
if os.path.exists(self.datadir):
shutil.rmtree(self.datadir, ignore_errors=True)
def cleanup(self, *args, **kwargs):
self.stop()
def imported_addr_wallet(self, addr, name="paper"):
# Create and load a watch-only imported-address wallet. Returns the
# name; Electrum picks the actual on-disk location based on --regtest.
self._cli("restore", addr, "-w", name)
self._cli("load_wallet", "-w", name)
return name
def addtransaction(self, wallet, tx_hex):
# Feed a raw transaction so the wallet sees its UTXOs without a server.
self._cli("addtransaction", tx_hex, "-w", wallet)
def payto_unsigned_psbt(self, wallet, dest, amount, feerate=5):
# Build an unsigned PSBT spending to `dest`. Returns base64 PSBT.
# Offline daemon has no fee oracle, so we pass an explicit feerate
# (sat/byte). RBF is on by default in Electrum 4.7+.
r = self._cli("payto", dest, str(amount),
"--unsigned", "--feerate", str(feerate),
"-w", wallet)
# Electrum CLI wraps strings in quotes; strip them.
return r.stdout.strip().strip('"')
@staticmethod
def create(*args, **kwargs):
e = Electrum(*args, **kwargs)
e.start()
return e
def _find_electrum():
# Resolve the `electrum` binary, in order:
# 1. ELECTRUM_BIN env var — for users with a venv install
# (e.g. ELECTRUM_BIN=/home/me/electrum/ENV/bin/electrum)
# 2. `electrum` on PATH
path = os.environ.get("ELECTRUM_BIN") or shutil.which("electrum")
if path and os.path.isfile(path) and os.access(path, os.X_OK):
return path
return None
@pytest.fixture
def electrum():
# Electrum 4.7+ daemon in --regtest --offline mode.
# Skips if no usable binary — set ELECTRUM_BIN to point at one.
path = _find_electrum()
if not path:
pytest.skip("electrum not found — set $ELECTRUM_BIN or put it on PATH")
e = Electrum.create(path)
yield e
e.stop()
# EOF