Source code for btclib_wallet.hwi

# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.

"""A `PsbtSigner` over Bitcoin Core HWI's JSON command line.

https://github.com/bitcoin-core/HWI

HWI speaks to Trezor, Ledger, KeepKey, Digital Bitbox, Coldcard, BitBox02
and Jade over HID, USB, serial and emulator transports, and publishes a
JSON command line so that other software need not. This module is that
other software: it runs `hwi` as a subprocess. Five of its commands
answer the contract `btclib_wallet.psbt_signer` defines; a sixth,
`registerdescriptor`, is wrapped beside them with no protocol of its own
to answer -- registering a policy is not a question `psbt_signer` asks.

**Nothing is imported from hwilib, and nothing has to be installed for
btclib to work.** HWI declares `hidapi`, `libusb1`, `cbor2`, `pyserial`,
`noiseprotocol`, `protobuf` and vendor libraries, and a Python range
narrower than btclib's; a mandatory dependency on that is the thing issue
btclib-org/btclib#381 rules out. What this module needs at runtime is an
executable, named by the caller and absent until a device is actually being
used, so the import costs nothing outside the standard library and the tests run
with no HWI at all.

An optional extra importing `hwilib` beside this was weighed and refused
(btclib-org/btclib#469), and the reason is that Python range: HWI declares
`^3.9,<3.13`, where this library's `requires-python` is `>=3.11`, so an extra
nobody can install on 3.13 or later is a second and narrower support matrix
rather than an option. A subprocess has no such problem, the executable living
in an environment of its own. What a caller who does hold an open `hwilib`
device writes instead is a `psbt_signer.PsbtSigner` of their own: that contract
names an in-process driver as one of its shapes, and it is met in the caller's
environment rather than in this one.

`enumerate_devices` is the one call that names no device; everything else
is `HwiSigner`, which is selected by fingerprint and passes `--fingerprint`
to every command it runs. That is issue btclib-org/btclib#381's own rule, and
the reason selection is not optional: HWI's `--device-type` connects to "the
first device of this type enumerated", so two devices of one vendor make which
one signs a question of enumeration order.

Both take a `network: str`, and both put it through
`network.validated_network_name`, which normalizes it with the
`strip().lower()` tolerance issue btclib-org/btclib#216 decided to keep: a name
the rest of the library resolves is resolved here, and one no network answers to
is refused in the same words whichever of the two took it.

The subprocess is bounded twice. `timeout` is how long a command may run
-- a device waiting for a button press is the ordinary case, so the
default is generous and a caller signing unattended should lower it --
and `max_output` is how much of its answer is accepted: HWI answers with
one json object, and a backend that sends megabytes is one whose output
is not read to the end. It bounds stdout and stderr separately, and it
bounds what is written rather than what is parsed: both streams go to
temporary files whose size is watched while the child runs, so a backend
past the limit is killed where it stands rather than read to EOF into
memory and measured afterwards.

Failures come back as `exceptions.SignerError`, carrying HWI's own error
code where there is one: -14 is the user pressing the button that says
no, -3 is a cable, -9 is a model that will never do it. The one failure
that is not about a device -- the executable is not installed -- is the
`SignerNotFoundError` subclass, so that a caller which also offers
signers of other kinds can tell "no hardware here" from "the hardware
could not be reached" without matching on the text of a message.
`is_available` is that one question asked *before* anything is run, which is
when a caller deciding what to offer at all has to have the answer: a
refusal is the right answer to a question that was asked, and the wrong
way to find out there was nothing to ask.

## Wallet policies

A Ledger will not display or sign a multisig it has not been shown
first. BIP388 wallet policies are how it is shown, and registration is a
one-time exchange ending in an HMAC the host keeps and replays on every
later call. `hwilib/_cli.py` exposes `registerdescriptor` for that
exchange, and `HwiSigner.register_descriptor` wraps it the way `getxpub`
and `signmessage` are wrapped: one request, one opaque answer, nothing
here to check it against.

No HWI release through 3.2.0 carries that subcommand -- it is on
`master` -- and `.github/workflows/integration-hwi.yml` installs the
release its `HWI_VERSION` names, one of those, so the weekly
`integration-hwi` job runs a command line `register_descriptor` cannot
reach. Raising that value to the first release whose `hwilib/_cli.py`
adds `registerdescriptor` is what ends the wait, and what makes this
paragraph removable.

`displayaddress`'s BIP388 policy mode -- `--registration`, `--index`,
`--multipath-index` -- is `HwiSigner.display_policy_address`, on the same
unreleased footing as `register_descriptor`: no release through 3.2.0
carries it either, so it runs against `master` and against the stand-in
`tests/hwi_test.py` writes, never against the pinned release.
`psbt_signer.WalletPolicyAddressDisplay`/`display_policy_address` are the
protocol and the check beside `AddressDisplay`/`display_address`:
`descriptors.wallet_policy_address` computes the address a policy
describes at an index and a multipath index, from the template and
key-information vector `descriptors.wallet_policy` writes when the
policy is built, and `display_policy_address` compares it with what the
device shows the way `display_address` compares a device's screen with a
plain `Descriptor` (issue btclib-org/btclib#1588).

Staying aligned with a project this does not import is two things, and
neither is a copy of it. `tests/hwi_test.py` writes out the surface used
-- the commands, the flags, the answer keys, the error codes -- and
`tests/_data/README.md` pins `hwilib/_cli.py` and `hwilib/errors.py` to
the revisions it was read from, so the weekly upstream re-check reports
a command line that moved. What that already caught: `signtx` answers
`signed` beside the psbt, which `sign_psbt` now holds the two strings to.
"""

from __future__ import annotations

import json
import os
import shutil
import subprocess
import tempfile
import time
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import IO, Any

from btclib.alias import Octets
from btclib.exceptions import BTClibValueError, SignerError, SignerNotFoundError
from btclib.network import validated_network_name
from btclib.utils import assert_type, bytes_from_octets, is_integer

from btclib_wallet.bip32.der_path import DerPath, str_from_der_path
from btclib_wallet.descriptors import Descriptor, add_checksum, at_index
from btclib_wallet.psbt.psbt import Psbt
from btclib_wallet.psbt_signer import SignerCapabilities

__all__ = [
    "DEFAULT_EXECUTABLE",
    "DEFAULT_MAX_OUTPUT",
    "DEFAULT_TIMEOUT",
    "NO_CAPABILITIES",
    "HwiDevice",
    "HwiSigner",
    "enumerate_devices",
    "is_available",
]

# the command HWI publishes, and the one name for it here: what
# `is_available` looks for on the PATH and what every command runs are the
# same string, so that "it is not installed" and "it is installed" cannot
# be answers about two different executables
DEFAULT_EXECUTABLE = "hwi"

# a button press is what a signing command waits for, so the bound is on
# a person and not on a computation: two minutes is long enough for a
# device that asks to confirm every output of a large transaction, and
# short enough that an unattended caller is not hung for an afternoon
DEFAULT_TIMEOUT = 120.0

# one json object, and the largest is a signed psbt: a megabyte is past
# any transaction standardness relays, and a backend that sends more is
# not one whose answer should be parsed to find out
DEFAULT_MAX_OUTPUT = 1 << 20

# the slice a wait is cut into, and so how far past `max_output` a stream
# may grow before the child is stopped: short enough that a flood is
# caught while it is still small, long enough that watching a device wait
# for a button press is not a busy loop
_POLL_INTERVAL = 0.05

# what a caller that has not said gets: nothing supported, which is the
# honest default when the answer is not in any json HWI prints. A module
# global rather than a call in the signature, which is a mutable default
# in every language that has them and a ruff finding in this one
NO_CAPABILITIES = SignerCapabilities()

# `--chain` decides the version bytes of the xpubs HWI answers with, so
# this mapping is what makes `account_descriptors` agree with the device
# about which chain the account is on. Every btclib network has a chain
# here, which is what `bitcoin_core_rpc.chain_from_network` is for Core's
# vocabulary -- and unlike that one this is not a bijection: testnet4
# goes out as `test`, HWI's own `testnet4`, which its parser takes, being
# the rejected alternative.
#
# Both decide the same bytes. In HWI 3.2.0 a chain is compared with
# `Chain.MAIN` and with no other, `JadeClient.NETWORKS` excepted, so an
# xpub comes back under the testnet version prefix and BIP44 coin type 1
# for either name -- and that exception is what decides: it holds `test`
# and answers `BadArgumentError: Unhandled network` for `testnet4`. So
# sending `testnet4` would change no answer read here and would cost the
# Jade. btclib's own testnet4 and testnet share every version prefix,
# differing in the genesis block and the consensus parameters, which no
# encoding reads
_HWI_CHAIN = {
    "mainnet": "main",
    "testnet": "test",
    "regtest": "regtest",
    "signet": "signet",
    "testnet4": "test",
}


[docs] @dataclass(frozen=True) class HwiDevice: """One entry of what `hwi enumerate` answers. `fingerprint` is None for a device that cannot be asked for one yet -- a locked Trezor, a Ledger with no app open -- and `error` says why, with HWI's own `code` beside it. Such a device is enumerated on purpose: what a caller does about a locked device is unlock it, and a list that left it out would say it is not there. """ type: str model: str path: str fingerprint: bytes | None = None needs_pin_sent: bool = False needs_passphrase_sent: bool = False error: str = "" code: int | None = None @property def is_usable(self) -> bool: """Answer whether the device answered a fingerprint and no error.""" return self.fingerprint is not None and not self.error
def _executable(executable: str | Sequence[str]) -> list[str]: """Return the argv prefix that runs HWI, from a name or a whole argv. A sequence is what a caller passes for anything that is not a bare executable on the PATH -- `python -m hwilib`, an interpreter and a script, a wrapper with flags of its own -- and it is what the tests use to run a stand-in with no HWI installed. """ return [executable] if isinstance(executable, str) else list(executable)
[docs] def is_available(executable: str | Sequence[str] = DEFAULT_EXECUTABLE) -> bool: """Whether the command line this module runs is there to be run. Asked before running it, rather than read off a failure afterwards. A caller that offers signers of several kinds decides which to offer at all -- what devices to enumerate, whether to fall back to a software signer, what to put in front of a user -- and that decision comes before there is a device to ask about, so a refusal is not the shape of the answer: `enumerate_devices` on a host with no HWI raises `SignerNotFoundError`, which is the right answer to a question that was asked and the wrong way to find out that there was nothing to ask. What is looked for is argv[0] of what would be run, which is why this belongs here and not in the caller: an `executable` is a name on the PATH or a whole argv -- `["python", "-m", "hwilib"]`, a wrapper with flags of its own -- and which part of it has to be on the PATH is this module's own convention, `_executable`'s. A caller writing `shutil.which("hwi")` beside it writes the default name a second time and takes that convention as read. True is not a promise that a device will answer, or that what is on the PATH is HWI at all: it is that there is something to run, which is the half of it a caller cannot find out without running one. """ return shutil.which(_executable(executable)[0]) is not None
def _watch( process: subprocess.Popen[bytes], streams: tuple[IO[bytes], IO[bytes]], *, timeout: float, max_output: int, ) -> bool: """Wait for the process, answering whether it ran out of time. It returns as soon as the child has exited, as soon as either stream is past the limit, or when the deadline is reached. Killing it is the caller's to do, that being what all three ways out have in common. """ deadline = time.monotonic() + timeout while (left := deadline - time.monotonic()) > 0: try: process.wait(timeout=min(_POLL_INTERVAL, left)) except subprocess.TimeoutExpired: if any( os.fstat(stream.fileno()).st_size > max_output for stream in streams ): return False else: return False return True def _kill(process: subprocess.Popen[bytes]) -> None: """Kill the child and reap it; a child already reaped is left alone. `send_signal` is what does the leaving alone, doing nothing once the return code is in, so this needs no state of its own and can run on the way out of every path. """ process.kill() process.wait() def _read(stream: IO[bytes], limit: int) -> bytes: """Return at most `limit` bytes of what the child wrote to a stream.""" stream.seek(0) return stream.read(limit) def _run( argv: list[str], *, timeout: float, max_output: int, ) -> Any: """Run HWI and return the json it answered, or raise what it failed with. Every failure is a `SignerError`: a backend that cannot be started, an answer that is not json, an answer past the limit, and the error object HWI itself returns. The last is the one carrying a code, which is why the others explicitly carry None -- "no number" is a fact about the exchange rather than a device that answered zero. The two streams are temporary files rather than pipes, which is what lets the limit be enforced while the child is still running: a pipe has to be drained by whoever is also timing the child, and draining it is the buffering the limit is there to prevent. A file is written by the child alone, so the size is a question this process can ask between two waits, and one byte past the limit is read back whatever the child did afterwards. """ with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err: try: process = subprocess.Popen(argv, stdout=out, stderr=err) # noqa: S603 except FileNotFoundError as e: # the one failure that is not about a device: the command line # is not installed, which a caller offering signers of several # kinds reports differently from one it could not reach raise SignerNotFoundError(f"cannot run {argv[0]}: {e}") from e except OSError as e: raise SignerError(f"cannot run {argv[0]}: {e}") from e try: timed_out = _watch( process, (out, err), timeout=timeout, max_output=max_output ) finally: _kill(process) if timed_out: raise SignerError(f"{argv[0]} timed out after {timeout} s") # one byte past the limit is all that has to be read to know the # limit was passed, and all that is read of a child that flooded stdout = _read(out, max_output + 1) stderr = _read(err, max_output + 1) if len(stdout) > max_output: err_msg = f"{argv[0]} answered more than {max_output} bytes" raise SignerError(err_msg) if len(stderr) > max_output: # the same limit and a separate one: stderr is diagnostics, so a # backend flooding it says nothing about the answer, and a backend # flooding it is still one this process will not hold in memory err_msg = f"{argv[0]} wrote more than {max_output} bytes to stderr" raise SignerError(err_msg) if not stdout.strip(): # a command that printed nothing has failed in its own way, and # stderr is where it said so: HWI prints a traceback there when it # cannot even build the parser message = stderr.decode("utf-8", "replace").strip() raise SignerError(f"{argv[0]} answered nothing: {message or 'no output'}") try: answer = json.loads(stdout) except json.JSONDecodeError as e: raise SignerError(f"{argv[0]} did not answer json: {e}") from e if isinstance(answer, dict) and "error" in answer: raise SignerError(str(answer["error"]), answer.get("code")) return answer def _fingerprint(entry: Mapping[str, Any]) -> bytes | None: """Return the four octets of an entry's fingerprint, or None for none. None is a device that cannot be asked for one yet, which `HwiDevice` documents; anything that is not four octets of hex is a device described by a backend that is not speaking HWI's protocol, and is the caller's `SignerError` rather than the `ValueError` `bytes.fromhex` raises from underneath the library. """ fingerprint = entry.get("fingerprint") if fingerprint is None: return None try: return bytes_from_octets(fingerprint, 4) except (TypeError, ValueError) as e: err_msg = f"hwi enumerate answered an invalid fingerprint {fingerprint!r}: {e}" raise SignerError(err_msg) from e def _code(entry: Mapping[str, Any]) -> int | None: """Return an entry's error code, refusing one that is not a number. `HwiDevice.code` is what a caller branches on -- -14 is the button that says no -- so a code is a number or there is none. `is_integer` and not `isinstance`, `True` being an `int` in this language and not an error code in any other. """ code = entry.get("code") if code is None or is_integer(code): return code raise SignerError(f"hwi enumerate answered a code that is not a number: {code!r}") def _device(entry: Any) -> HwiDevice: """Return one enumerate entry as an `HwiDevice`, its fields narrowed. `enumerate` is the one command whose answer is a list of objects rather than one object with a known field in it, so this is where a json nobody validated is read. What the entry can be wrong about is a `SignerError` like everything else the exchange can be wrong about: a caller catches one class, and an `AttributeError` from a string that stood where an object should have is not that class. The strings and the two flags are coerced rather than refused, which is the line drawn here: `str` and `bool` accept every object there is, and a model name that arrived as a number describes the device it describes. A fingerprint and a code are the two fields a caller acts on, and neither has a reading that is merely odd. """ if not isinstance(entry, Mapping): raise SignerError(f"hwi enumerate did not answer a device: {entry!r}") return HwiDevice( type=str(entry.get("type", "")), model=str(entry.get("model", "")), path=str(entry.get("path", "")), fingerprint=_fingerprint(entry), needs_pin_sent=bool(entry.get("needs_pin_sent")), needs_passphrase_sent=bool(entry.get("needs_passphrase_sent")), error=str(entry.get("error", "")), code=_code(entry), )
[docs] def enumerate_devices( *, executable: str | Sequence[str] = DEFAULT_EXECUTABLE, network: str = "mainnet", timeout: float = DEFAULT_TIMEOUT, max_output: int = DEFAULT_MAX_OUTPUT, emulators: bool = False, ) -> list[HwiDevice]: """Return the devices HWI can see, the ones it cannot talk to included. `emulators` is HWI's own `--emulators`, off by default: an emulator is a device with no secure element and no owner, and enumerating one without being asked would make a test fixture look like a signer. """ assert_type(emulators, bool, "emulators") network = validated_network_name(network) argv = [*_executable(executable), *_chain_args(network), "enumerate"] if emulators: argv.insert(-1, "--emulators") answer = _run(argv, timeout=timeout, max_output=max_output) if not isinstance(answer, list): raise SignerError(f"hwi enumerate did not answer a list: {answer!r}") return [_device(entry) for entry in answer]
def _chain_args(network: str) -> list[str]: """Return HWI's `--chain` flag for a btclib network name. The name is one `validated_network_name` has already returned, so it is a key of `NETWORKS` and of `_HWI_CHAIN` with it: refusing belongs to the entry point that took the caller's spelling, and what is left here is the translation. A `HwiSigner.network` assigned after construction goes round that entry point, and reaches this lookup as a `KeyError`. """ return ["--chain", _HWI_CHAIN[network]]
[docs] class HwiSigner: """One device, selected by fingerprint, answering the signer contract. `btclib_wallet.psbt_signer`'s three protocols over five HWI commands: `getxpub`, `signtx`, `signmessage`, `displayaddress`, and `enumerate` for the selection. Everything a caller should check about the answers is checked by the functions of that module -- `request_signatures`, `display_address`, `sign_message` -- and not here: this is the transport, and a transport that also decided what to trust would be two things. `register_descriptor` is a sixth command with no protocol of its own; the module docstring's "Wallet policies" says why. The fingerprint is what a device is named by. Passing one selects it; passing none enumerates and refuses unless exactly one device is usable, because "the first one enumerated" is not a choice a library makes for a caller holding two. `capabilities` is the caller's word: HWI's JSON CLI does not report what a model supports, and the matrix that does is maintained per vendor and per firmware in HWI's own documentation. A default of nothing supported is the honest one, and a caller that knows its device says so. """ def __init__( self, fingerprint: Octets | None = None, *, executable: str | Sequence[str] = DEFAULT_EXECUTABLE, network: str = "mainnet", timeout: float = DEFAULT_TIMEOUT, max_output: int = DEFAULT_MAX_OUTPUT, emulators: bool = False, capabilities: SignerCapabilities = NO_CAPABILITIES, ) -> None: assert_type(emulators, bool, "emulators") self.executable = _executable(executable) self.network = validated_network_name(network) self.timeout = timeout self.max_output = max_output self.emulators = emulators self._capabilities = capabilities self._fingerprint = ( self._select() if fingerprint is None else bytes_from_octets(fingerprint, 4) ) self._closed = False def _select(self) -> bytes: """Return the fingerprint of the one usable device, refusing two. A device that could not be asked for a fingerprint is not a candidate and is named in the refusal: "no device" and "one device, locked" are different things to be told, and the second is fixed by unlocking it rather than by plugging something in. """ devices = enumerate_devices( executable=self.executable, network=self.network, timeout=self.timeout, max_output=self.max_output, emulators=self.emulators, ) usable = [device for device in devices if device.is_usable] if len(usable) == 1: assert usable[0].fingerprint is not None # noqa: S101 return usable[0].fingerprint if not usable: unusable = ", ".join( f"{device.type} ({device.error or 'no fingerprint'})" for device in devices ) err_msg = f"no usable device: {unusable}" if unusable else "no device" raise SignerError(err_msg) fingerprints = ", ".join( device.fingerprint.hex() for device in usable if device.fingerprint ) err_msg = f"{len(usable)} usable devices ({fingerprints}):" err_msg += " name the one to use by its fingerprint" raise SignerError(err_msg) def _hwi(self, *args: str) -> Any: """Run one command against this device, by fingerprint.""" if self._closed: raise SignerError("the signer is closed") argv = [ *self.executable, *_chain_args(self.network), "--fingerprint", self._fingerprint.hex(), *args, ] return _run(argv, timeout=self.timeout, max_output=self.max_output) def _answer(self, command: list[str], field: str) -> str: """Return one string field of an answer, refusing one without it.""" answer = self._hwi(*command) if not isinstance(answer, dict) or field not in answer: err_msg = f"hwi {command[0]} did not answer a {field}: {answer!r}" raise SignerError(err_msg) return str(answer[field]) @property def master_fingerprint(self) -> bytes: """Return the fingerprint this signer was selected by. Not asked of the device again: it is what every command carries as `--fingerprint`, so HWI has refused to talk to a device answering anything else before any of them ran. """ return self._fingerprint
[docs] def xpub(self, der_path: DerPath) -> str: """Return the extended public key at a path: HWI's `getxpub`.""" return self._answer(["getxpub", str_from_der_path(der_path)], "xpub")
[docs] def sign_psbt(self, psbt: Psbt) -> Psbt: """Return what `hwi signtx` answered, parsed and otherwise untouched. Untouched deliberately: what the answer *contains* is checked against the psbt that was sent by `psbt_signer.request_signatures`, which is the caller of this and the one place that comparison belongs. What is checked here is the other thing, and only this layer can: `signtx` answers `signed` beside the psbt -- HWI computes it as "the base64 I return is not the base64 I was given" -- so the flag and the two strings have to agree. A device claiming it signed while handing back what it was sent, or denying it while handing back something else, has answered inconsistently, and the psbt is not the place that shows it: the comparison is over the very strings that crossed the boundary. A device that signed nothing is not an error and does not raise. One signer of an m-of-n answers for its own key and for no other, which is the same answer `psbt.sign` gives by adding nothing; what a caller compares is the psbt it gets back. """ sent = psbt.b64encode() answer = self._hwi("signtx", sent) if not isinstance(answer, dict) or "psbt" not in answer: raise SignerError(f"hwi signtx did not answer a psbt: {answer!r}") returned = str(answer["psbt"]) # absent from HWI before the flag existed, and a caller may be # running one of those: what is not answered is not checked signed = answer.get("signed") if signed is not None and bool(signed) != (returned != sent): err_msg = f"hwi signtx answered signed={signed!r} and a psbt that" err_msg += " was" if returned == sent else " was not" err_msg += " the one it was given" raise SignerError(err_msg) return Psbt.b64decode(returned)
[docs] def sign_message(self, message: Octets, der_path: DerPath) -> str: """Return the compact signature of a message: HWI's `signmessage`. `Octets` as everywhere else in btclib, so a `str` is the hex of the message and bytes are the message: `ecc.bms` reads it that way and the two have to agree, a signature being verified here against what was signed there. What goes on the command line is text, HWI's `signmessage` taking a string and passing it to its own signer as one. The bytes are decoded as utf-8 for that, and a message that is not utf-8 is one this backend cannot be asked for -- which is a limit of the command line rather than of the device, and is said as such. """ octets = bytes_from_octets(message) try: text = octets.decode("utf-8") except UnicodeDecodeError as e: err_msg = "hwi signmessage takes text on a command line:" err_msg += " this message is not utf-8" raise BTClibValueError(err_msg) from e return self._answer( ["signmessage", text, str_from_der_path(der_path)], "signature" )
[docs] def display_address(self, descriptor: Descriptor, index: int = 0) -> str: """Return the address the device shows: HWI's `displayaddress`. The descriptor is sent with the index written into it rather than as the ranged one it may be: HWI derives a ranged descriptor at index 0 whatever was meant, so a caller asking for index 5 would be shown index 0 and told it was 5. `descriptors.at_index` is what names the one script, and `psbt_signer.display_address` is what then compares the answer with the address that descriptor describes. Checksummed, which is what HWI's `--desc` parser requires of anything it is given. """ text = add_checksum(str(at_index(descriptor, index))) return self._answer(["displayaddress", "--desc", text], "address")
[docs] def register_descriptor(self, name: str, descriptor: Descriptor) -> str: """Register a wallet policy with the device: HWI's `registerdescriptor`. A Ledger will not display or sign a multisig it has not been shown first (module docstring, "Wallet policies"); this is that showing. What comes back is opaque -- an HMAC on Ledger, nothing at all on a device that needs none -- and is the caller's to persist and pass back as `registration` to `display_policy_address`. The descriptor goes out ranged, whole rather than at one index: registration is of the policy, and `display_policy_address`'s own index is what later asks for one address of it. Checksummed, which is what HWI's parser requires of anything it is given, `--desc` included. No HWI release through 3.2.0 has `registerdescriptor` at all, so this needs a build of `master`; the module docstring's *Wallet policies* says what ends that. """ assert_type(name, str, "name") text = add_checksum(str(descriptor)) return self._answer(["registerdescriptor", name, text], "registration")
[docs] def display_policy_address( self, registration: str, index: int = 0, multipath_index: int = 0 ) -> str: """Show one address of a registered policy: HWI's `displayaddress`. The registered-policy half of `displayaddress`, mutually exclusive with the plain `--desc` `display_address` sends: `registration` is what `register_descriptor` returned, and `index`/`multipath_index` are what `psbt_signer.display_policy_address` also resolves `descriptors.wallet_policy_address` at, on the host side, to check this answer against. No HWI release through 3.2.0 has this command's policy group either, so it needs the same `master` build `register_descriptor` does; the module docstring's *Wallet policies* says what ends that. """ assert_type(registration, str, "registration") return self._answer( [ "displayaddress", "--registration", registration, "--index", str(index), "--multipath-index", str(multipath_index), ], "address", )
@property def capabilities(self) -> SignerCapabilities: """Return what the caller said this device can be asked to sign.""" return self._capabilities
[docs] def close(self) -> None: """Refuse further commands; there is no connection to release. A subprocess per command is what a command line is, so nothing is held open between two of them and closing is a decision rather than a release. Making it refuse afterwards is what gives a caller the same shape as a signer that does hold something -- and `contextlib.closing` then works over either. """ self._closed = True