diff --git a/contrib/deterministic-build/find_restricted_dependencies.py b/contrib/deterministic-build/find_restricted_dependencies.py index 5cc2c45f3663..55bda1a426db 100755 --- a/contrib/deterministic-build/find_restricted_dependencies.py +++ b/contrib/deterministic-build/find_restricted_dependencies.py @@ -1,10 +1,6 @@ #!/usr/bin/env python3 import sys - -try: - import requests -except ImportError as e: - sys.exit(f"Error: {str(e)}. Try 'python3 -m pip install '") +from importlib.metadata import requires, PackageNotFoundError def is_dependency_edge_blacklisted(*, parent_pkg: str, dep: str) -> bool: """Sometimes a package declares a hard dependency @@ -38,26 +34,24 @@ def main(): if not p: continue assert "==" in p, "This script expects a list of packages with pinned version, e.g. package==1.2.3, not {}".format(p) - p, v = p.rsplit("==", 1) - try: - data = requests.get("https://pypi.org/pypi/{}/{}/json".format(p, v)).json()["info"] - except ValueError: - raise Exception("Package could not be found: {}=={}".format(p, v)) + pkg_name, _ = p.rsplit("==", 1) try: - for r in data["requires_dist"]: # type: str - if ";" not in r: - continue - # example value for "r" at this point: "pefile (>=2017.8.1) ; sys_platform == \"win32\"" - dep, restricted = r.split(";", 1) - dep = dep.strip() - restricted = restricted.strip() - dep_basename = dep.split(" ")[0] - if check_restriction(dep=dep, restricted=restricted, parent_pkg=p): - print(dep_basename, sep=" ") - print("Installing {} from {} although it is only needed for {}".format(dep, p, restricted), file=sys.stderr) - except TypeError: - # Has no dependencies at all + reqs = requires(pkg_name) + except PackageNotFoundError: + raise Exception("Package not found in this environment: {}. Install it first.".format(p)) + if reqs is None: continue + for r in reqs: + if ";" not in r: + continue + # example value for "r": "pefile (>=2017.8.1) ; sys_platform == \"win32\"" + dep, restricted = r.split(";", 1) + dep = dep.strip() + restricted = restricted.strip() + dep_basename = dep.split(" ")[0] + if check_restriction(dep=dep, restricted=restricted, parent_pkg=pkg_name): + print(dep_basename, sep=" ") + print("Installing {} from {} although it is only needed for {}".format(dep, pkg_name, restricted), file=sys.stderr) if __name__ == "__main__": main() diff --git a/contrib/freeze_packages.sh b/contrib/freeze_packages.sh index 1a5dcc6767cc..4849795ecfe2 100755 --- a/contrib/freeze_packages.sh +++ b/contrib/freeze_packages.sh @@ -24,7 +24,7 @@ for suffix in '' '-hw' '-binaries' '-binaries-mac' '-build-wine' '-build-mac' '- reqfile="requirements${suffix}.txt" rm -rf "$venv_dir" - virtualenv -p "${SYSTEM_PYTHON}" "$venv_dir" + "${SYSTEM_PYTHON}" -m venv "$venv_dir" source "$venv_dir/bin/activate" @@ -36,16 +36,20 @@ for suffix in '' '-hw' '-binaries' '-binaries-mac' '-build-wine' '-build-mac' '- # that we should explicitly install them now, so that we pin latest versions if possible. python -m pip install --upgrade pip setuptools wheel + # install ghost packages to satisfy dependency resolvers + for package in $(cat "$contrib/requirements/ghost.txt"); do + python $contrib/install_ghost.py "$package" + done + python -m pip install -r "$contrib/requirements/${reqfile}" --upgrade echo "OK." requirements=$(pip freeze --all) - restricted=$(echo $requirements | ${SYSTEM_PYTHON} "$contrib/deterministic-build/find_restricted_dependencies.py") + restricted=$(echo $requirements | python "$contrib/deterministic-build/find_restricted_dependencies.py") if [ ! -z "$restricted" ]; then python -m pip install $restricted - requirements=$(pip freeze --all) fi echo "Generating package hashes... (${reqfile})" @@ -67,6 +71,12 @@ for suffix in '' '-hw' '-binaries' '-binaries-mac' '-build-wine' '-build-mac' '- HASHIN_FLAGS="--python-version source" fi + # remove ghost packages before hashin step, so that they aren't hashed + for package in $(cat "$contrib/requirements/ghost.txt"); do + python -m pip uninstall -y $package + done + requirements=$(pip freeze --all) + echo -e "\r Hashing requirements for $reqfile..." ${SYSTEM_PYTHON} -m hashin $HASHIN_FLAGS -r "$contrib/deterministic-build/${reqfile}" $requirements diff --git a/contrib/install_ghost.py b/contrib/install_ghost.py new file mode 100644 index 000000000000..f6c978cfa10d --- /dev/null +++ b/contrib/install_ghost.py @@ -0,0 +1,32 @@ +import sys, tempfile, subprocess +from pathlib import Path +from importlib.metadata import distribution + + +PYPROJECT_TOML = """ +[project] +name = "{name}" +version = "{version}" +description = "Ghost package to satisfy dependencies" +""" + + +def install_ghost(name: str, version: str) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + pyproject_toml = PYPROJECT_TOML.format(name=name, version=version) + (Path(tmpdir) / "pyproject.toml").write_text(pyproject_toml) + subprocess.check_call([sys.executable, "-m", "pip", "install", tmpdir]) + + dist = distribution(name) + for file in dist.files: + path = file.locate() + if path.name == "direct_url.json": + path.unlink() + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python install_ghost.py ==") + sys.exit(1) + name, version = sys.argv[1].split("==") + install_ghost(name, version) diff --git a/contrib/requirements/ghost.txt b/contrib/requirements/ghost.txt new file mode 100644 index 000000000000..f506ac04724d --- /dev/null +++ b/contrib/requirements/ghost.txt @@ -0,0 +1,5 @@ +click==8.3.1 +construct==2.10.70 +construct-classes==0.2.2 +platformdirs==4.9.4 +keyring==25.7.0 diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 2a6f7d9806b1..9df3a59f3683 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -1,7 +1,7 @@ hidapi # device plugin: trezor -trezor[hidapi]>=0.13.0,<0.14 +trezor[hidapi]>=0.20.0,<0.21 # device plugin: safe_t safet>=0.1.5 diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index 1863afc4d30e..389b134a191b 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -11,9 +11,9 @@ from electrum.plugin import runs_in_hwd_thread from electrum.hw_wallet.plugin import OutdatedHwFirmwareException, HardwareClientBase -from trezorlib.client import TrezorClient, PASSPHRASE_ON_DEVICE +from trezorlib.client import TrezorClient, PassphraseSetting, get_default_client from trezorlib.exceptions import TrezorFailure, Cancelled, OutdatedFirmwareError -from trezorlib.messages import WordRequestType, FailureType, ButtonRequestType +from trezorlib.messages import WordRequestType, FailureType, ButtonRequestType, Capability import trezorlib.btc import trezorlib.device @@ -53,7 +53,14 @@ def __init__(self, transport, handler, plugin): HardwareClientBase.__init__(self, plugin=plugin) if plugin.is_outdated_fw_ignored(): TrezorClient.is_outdated = lambda *args, **kwargs: False - self.client = TrezorClient(transport, ui=self) + + self.client = get_default_client( + app_name="Electrum", + path_or_transport=transport, + button_callback=self.button_request, + pin_callback=self.get_pin, + ) + self._session = None self.device = plugin.device self.handler = handler Logger.__init__(self) @@ -65,6 +72,30 @@ def __init__(self, transport, handler, plugin): self.used() + @property + def session(self): + if self._session is None: + assert self.handler is not None + + # If needed, unlock the device (triggering PIN entry dialog for legacy model). + with self.client.get_session(passphrase=PassphraseSetting.STANDARD_WALLET) as session: + session.ensure_unlocked() + + passphrase = PassphraseSetting.STANDARD_WALLET # (empty passphrase) + if self.client.features.passphrase_protection: + passphrase = self.get_passphrase(Capability.PassphraseEntry in self.client.features.capabilities) + + # Then, derive a session for this wallet (possibly with a passphrase) + if passphrase == PassphraseSetting.STANDARD_WALLET: + self._session = session # reuse the session above to avoid re-derivation + self.logger.info("Opened standard %s", self._session) + else: + self._session = self.client.get_session(passphrase) + self.logger.info("Re-opened passphrase %s", self._session) + + return self._session + + def run_flow(self, message=None, creating_wallet=False): if self.in_flow: raise RuntimeError("Overlapping call to run_flow") @@ -123,8 +154,9 @@ def has_usable_connection_with_device(self): return True try: - self.client.init_device() + self.client.ping(message="") except BaseException: + self.logger.exception("Ping failed") return False return True @@ -148,7 +180,7 @@ def i4b(self, x): def get_xpub(self, bip32_path, xtype, creating=False): address_n = parse_path(bip32_path) with self.run_flow(creating_wallet=creating): - node = trezorlib.btc.get_public_node(self.client, address_n).node + node = trezorlib.btc.get_public_node(self.session, address_n).node return BIP32Node(xtype=xtype, eckey=ecc.ECPubkey(node.public_key), chaincode=node.chain_code, @@ -164,17 +196,17 @@ def toggle_passphrase(self): msg = _("Confirm on your {} device to enable passphrases") enabled = not self.features.passphrase_protection with self.run_flow(msg): - trezorlib.device.apply_settings(self.client, use_passphrase=enabled) + trezorlib.device.apply_settings(self.session, use_passphrase=enabled) @runs_in_hwd_thread def change_label(self, label): with self.run_flow(_("Confirm the new label on your {} device")): - trezorlib.device.apply_settings(self.client, label=label) + trezorlib.device.apply_settings(self.session, label=label) @runs_in_hwd_thread def change_homescreen(self, homescreen): with self.run_flow(_("Confirm on your {} device to change your home screen")): - trezorlib.device.apply_settings(self.client, homescreen=homescreen) + trezorlib.device.apply_settings(self.session, homescreen=homescreen) @runs_in_hwd_thread def set_pin(self, remove): @@ -185,7 +217,7 @@ def set_pin(self, remove): else: msg = _("Confirm on your {} device to set a PIN") with self.run_flow(msg): - trezorlib.device.change_pin(self.client, remove) + trezorlib.device.change_pin(self.session, remove) @runs_in_hwd_thread def clear_session(self): @@ -194,7 +226,7 @@ def clear_session(self): self.logger.info(f"clear session: {self}") self.prevent_timeouts() try: - self.client.clear_session() + self.close() except BaseException as e: # If the device was removed it has the same effect... self.logger.info(f"clear_session: ignoring error {e}") @@ -202,8 +234,11 @@ def clear_session(self): @runs_in_hwd_thread def close(self): '''Called when Our wallet was closed or the device removed.''' - self.logger.info("closing client") - self.clear_session() + self.logger.info("locking: %s", self.client) + self.client.lock() + self.logger.info("closing: %s", self._session) + if self._session is not None: + self._session.close() @runs_in_hwd_thread def is_uptodate(self): @@ -233,7 +268,7 @@ def show_address(self, address_str, script_type, multisig=None): address_n = parse_path(address_str) with self.run_flow(): return trezorlib.btc.get_address( - self.client, + self.session, coin_name, address_n, show_display=True, @@ -246,7 +281,7 @@ def sign_message(self, address_str, message, *, script_type): address_n = parse_path(address_str) with self.run_flow(): return trezorlib.btc.sign_message( - self.client, + self.session, coin_name, address_n, message, @@ -256,9 +291,9 @@ def sign_message(self, address_str, message, *, script_type): @runs_in_hwd_thread def recover_device(self, recovery_type, *args, **kwargs): input_callback = self.mnemonic_callback(recovery_type) - with self.run_flow(): + with self.run_flow(), self.client.get_session(None) as seedless_session: return trezorlib.device.recover( - self.client, + seedless_session, *args, input_callback=input_callback, type=recovery_type, @@ -269,27 +304,27 @@ def recover_device(self, recovery_type, *args, **kwargs): @runs_in_hwd_thread def sign_tx(self, *args, **kwargs): with self.run_flow(): - return trezorlib.btc.sign_tx(self.client, *args, **kwargs) + return trezorlib.btc.sign_tx(self.session, *args, **kwargs) @runs_in_hwd_thread def get_ownership_id(self, *args, **kwargs): with self.run_flow(): - return trezorlib.btc.get_ownership_id(self.client, *args, **kwargs) + return trezorlib.btc.get_ownership_id(self.session, *args, **kwargs) @runs_in_hwd_thread def get_ownership_proof(self, *args, **kwargs): with self.run_flow(): - return trezorlib.btc.get_ownership_proof(self.client, *args, **kwargs) + return trezorlib.btc.get_ownership_proof(self.session, *args, **kwargs) @runs_in_hwd_thread def reset_device(self, *args, **kwargs): - with self.run_flow(): - return trezorlib.device.reset(self.client, *args, **kwargs) + with self.run_flow(), self.client.get_session(None) as seedless_session: + return trezorlib.device.reset(seedless_session, *args, **kwargs) @runs_in_hwd_thread def wipe_device(self, *args, **kwargs): - with self.run_flow(): - return trezorlib.device.wipe(self.client, *args, **kwargs) + with self.run_flow(), self.client.get_session(None) as seedless_session: + return trezorlib.device.wipe(seedless_session, *args, **kwargs) # ========= UI methods ========== @@ -335,7 +370,7 @@ def get_passphrase(self, available_on_device): self.handler.passphrase_on_device = available_on_device passphrase = self.handler.get_passphrase(msg, self.creating_wallet) - if passphrase is PASSPHRASE_ON_DEVICE: + if passphrase is PassphraseSetting.ON_DEVICE: return passphrase if passphrase is None: raise Cancelled diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 9487707443fb..16c3e794eb76 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -33,10 +33,12 @@ InputScriptType, OutputScriptType, MultisigRedeemScriptType, TxInputType, TxOutputType, TxOutputBinType, TransactionType, AmountUnit) - from trezorlib.client import PASSPHRASE_ON_DEVICE + from trezorlib.client import PassphraseSetting import trezorlib.log #trezorlib.log.enable_debug_output() + PASSPHRASE_ON_DEVICE = PassphraseSetting.ON_DEVICE + TREZORLIB = True except Exception as e: if not (isinstance(e, ModuleNotFoundError) and e.name == 'trezorlib'): @@ -153,8 +155,8 @@ class TrezorPlugin(HW_PluginBase): libraries_URL = 'https://pypi.org/project/trezor/' minimum_firmware = (1, 5, 2) keystore_class = TrezorKeyStore - minimum_library = (0, 13, 0) - maximum_library = (0, 14) + minimum_library = (0, 20, 0) + maximum_library = (0, 21) SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') DEVICE_IDS = (TREZOR_PRODUCT_KEY,)