mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
140 lines
5.1 KiB
Python
140 lines
5.1 KiB
Python
"""Opt-in tests requiring a physical Blockstream Jade attached + operator.
|
|
|
|
Skipped by default. Enable with::
|
|
|
|
pytest --run-jade-hardware tests/test_jade_hardware.py -s
|
|
|
|
The ``-s`` is required so operator prompts reach your terminal.
|
|
|
|
Operator setup
|
|
--------------
|
|
For ``test_jade_enumerate_via_specter`` and
|
|
``test_jade_extract_xpub_via_specter``: any Jade with any seed (your
|
|
real one is fine — those tests only read public material).
|
|
|
|
For ``test_jade_sign_psbt_via_specter``: the Jade must be in
|
|
**Temporary Signer mode** with the public **BIP-39 abandon vector** seed
|
|
(``abandon abandon ... about``) loaded. The simplest path:
|
|
|
|
1. Power-cycle the Jade so it's at the boot menu.
|
|
2. Choose ``Temporary Signer`` -> ``Scan SeedQR``.
|
|
3. Scan ``tests/fixtures/jade_seedqr_abandon.png`` (or display
|
|
``tests/fixtures/jade_seedqr_abandon.txt`` and scan from screen).
|
|
4. Confirm the **testnet** network on Jade.
|
|
5. The PSBT at ``tests/fixtures/jade_hardware.psbt`` is fabricated by
|
|
Coldcard's psbt_faker against this exact seed.
|
|
|
|
Temporary Signer state lives in RAM only; it's wiped on power-cycle. Your
|
|
real seed is not affected.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from cryptoadvance.specter.hwi_rpc import HWIBridge
|
|
|
|
|
|
FIXTURE_DIR = Path(__file__).parent / "fixtures"
|
|
PSBT_FIXTURE = FIXTURE_DIR / "jade_hardware.psbt"
|
|
SEEDQR_PNG = FIXTURE_DIR / "jade_seedqr_abandon.png"
|
|
SEEDQR_TXT = FIXTURE_DIR / "jade_seedqr_abandon.txt"
|
|
|
|
ABANDON_FINGERPRINT = "73c5da0a"
|
|
|
|
|
|
def _enumerate_jade(bridge: HWIBridge, chain: str = "main"):
|
|
# HWIBridge.enumerate defaults chain="" which Chain.argparse passes
|
|
# through unchanged; Jade's enumerate then fails with
|
|
# "Unhandled network: ". Pass an explicit chain.
|
|
devs = bridge.enumerate(chain=chain)
|
|
return [d for d in devs if d.get("type") == "jade"]
|
|
|
|
|
|
def _prompt(msg: str) -> None:
|
|
print(f"\n>>> {msg}")
|
|
try:
|
|
input(">>> Press Enter when ready... ")
|
|
except EOFError:
|
|
pass
|
|
|
|
|
|
@pytest.mark.jade_hardware
|
|
def test_jade_enumerate_via_specter():
|
|
"""Jade is detected by Specter's HWIBridge and reports a fingerprint."""
|
|
_prompt("Connect and unlock the Jade.")
|
|
bridge = HWIBridge(skip_hwi_initialisation=True)
|
|
jades = _enumerate_jade(bridge)
|
|
assert jades, "no Jade detected — connect, unlock, and rerun"
|
|
jade = jades[0]
|
|
assert jade.get("fingerprint"), f"Jade enumerated without fingerprint: {jade}"
|
|
assert jade.get("path"), f"Jade enumerated without path: {jade}"
|
|
|
|
|
|
@pytest.mark.jade_hardware
|
|
def test_jade_extract_xpub_via_specter():
|
|
"""Specter can pull an xpub at a known derivation from Jade."""
|
|
_prompt("Unlock the Jade. You may be asked to confirm the xpub export.")
|
|
bridge = HWIBridge(skip_hwi_initialisation=True)
|
|
jades = _enumerate_jade(bridge)
|
|
assert jades, "no Jade detected"
|
|
fingerprint = jades[0].get("fingerprint")
|
|
assert fingerprint, f"Jade enumerated without fingerprint: {jades[0]}"
|
|
|
|
# chain must be passed explicitly: HWIBridge.extract_xpub default is
|
|
# chain="" which Specter's JadeClient.__init__ rejects via _network()
|
|
# before extract_xpub's post-init override can apply.
|
|
xpub_line = bridge.extract_xpub(
|
|
derivation="m/84h/0h/0h",
|
|
device_type="jade",
|
|
fingerprint=fingerprint,
|
|
chain="main",
|
|
)
|
|
assert xpub_line, "extract_xpub returned empty"
|
|
assert xpub_line.startswith("["), f"unexpected format: {xpub_line!r}"
|
|
assert "]" in xpub_line, f"unexpected format: {xpub_line!r}"
|
|
body = xpub_line.split("]", 1)[1].strip()
|
|
assert body.startswith(("xpub", "zpub", "ypub")), f"unexpected xpub: {body[:8]}"
|
|
|
|
|
|
@pytest.mark.jade_hardware
|
|
def test_jade_sign_psbt_via_specter():
|
|
"""End-to-end: Specter signs the canned abandon-vector PSBT through Jade.
|
|
|
|
Requires Jade in Temporary Signer mode with the abandon-vector seed
|
|
(see module docstring). The fixture PSBT was generated by Coldcard's
|
|
psbt_faker against m/84'/1'/0' on testnet; xfp is 73c5da0a.
|
|
"""
|
|
assert PSBT_FIXTURE.exists(), f"missing fixture: {PSBT_FIXTURE}"
|
|
|
|
seedqr_hint = (
|
|
f"\n PNG: {SEEDQR_PNG}\n"
|
|
f" ASCII: cat {SEEDQR_TXT}"
|
|
)
|
|
_prompt(
|
|
"Put Jade in Temporary Signer mode -> Scan SeedQR -> select TESTNET."
|
|
f"\n SeedQR for the BIP-39 abandon-vector lives at:{seedqr_hint}\n"
|
|
" Then confirm the transaction on device when prompted."
|
|
)
|
|
|
|
psbt_b64 = PSBT_FIXTURE.read_text().strip()
|
|
|
|
bridge = HWIBridge(skip_hwi_initialisation=True)
|
|
jades = _enumerate_jade(bridge, chain="test")
|
|
assert jades, "no Jade detected"
|
|
fingerprint = jades[0].get("fingerprint")
|
|
assert fingerprint, f"Jade enumerated without fingerprint: {jades[0]}"
|
|
assert fingerprint.lower() == ABANDON_FINGERPRINT, (
|
|
f"connected Jade fingerprint is {fingerprint}; "
|
|
f"expected {ABANDON_FINGERPRINT} (abandon-vector). "
|
|
"Are you in Temporary Signer mode with the right SeedQR?"
|
|
)
|
|
|
|
signed = bridge.sign_tx(
|
|
psbt=psbt_b64,
|
|
device_type="jade",
|
|
fingerprint=fingerprint,
|
|
chain="test",
|
|
)
|
|
assert signed, "sign_tx returned empty"
|
|
assert signed != psbt_b64, "PSBT was returned unsigned"
|