97 lines
3.5 KiB
Python
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
|