From 568ec576d90c540b253ce5e3266618a2aa137d57 Mon Sep 17 00:00:00 2001 From: Andreas Griffin <116060138+andreasgriffin@users.noreply.github.com> Date: Fri, 14 Jun 2024 16:48:01 +0200 Subject: [PATCH] Prepare release 0.7.0a0 (#3) - [x] Update nostr_sdk https://github.com/rust-nostr/nostr/issues/453 , which is possible https://github.com/rust-nostr/nostr/commit/0c4e345c0c9d29943981c272a0c8b8b0c7ba7cf9 with the nest release --- .pre-commit-config.yaml | 3 +- README.md | 36 +-- bitcoin_safe/__init__.py | 2 +- bitcoin_safe/__main__.py | 4 +- bitcoin_safe/config.py | 22 +- bitcoin_safe/descriptors.py | 2 +- bitcoin_safe/dynamic_lib_load.py | 14 +- bitcoin_safe/fx.py | 4 +- bitcoin_safe/gui/qt/address_dialog.py | 8 +- bitcoin_safe/gui/qt/address_list.py | 50 +++-- bitcoin_safe/gui/qt/bitcoin_quick_receive.py | 4 +- bitcoin_safe/gui/qt/block_buttons.py | 54 ++--- bitcoin_safe/gui/qt/block_change_signals.py | 6 +- bitcoin_safe/gui/qt/buttonedit.py | 79 ++++--- bitcoin_safe/gui/qt/category_list.py | 20 +- bitcoin_safe/gui/qt/controlled_groupbox.py | 4 +- bitcoin_safe/gui/qt/custom_edits.py | 101 ++------- bitcoin_safe/gui/qt/data_tab_widget.py | 28 +-- bitcoin_safe/gui/qt/debug_widget.py | 10 +- bitcoin_safe/gui/qt/descriptor_edit.py | 128 +++++++++++ bitcoin_safe/gui/qt/descriptor_ui.py | 37 ++-- bitcoin_safe/gui/qt/dialog_import.py | 22 +- bitcoin_safe/gui/qt/dialogs.py | 23 +- bitcoin_safe/gui/qt/downloader.py | 14 +- bitcoin_safe/gui/qt/expandable_widget.py | 10 +- bitcoin_safe/gui/qt/export_data.py | 122 +++++++--- bitcoin_safe/gui/qt/extended_tabwidget.py | 16 +- bitcoin_safe/gui/qt/fee_group.py | 30 +-- bitcoin_safe/gui/qt/hist_list.py | 72 +++--- bitcoin_safe/gui/qt/html_delegate.py | 4 +- bitcoin_safe/gui/qt/invisible_scroll_area.py | 2 +- bitcoin_safe/gui/qt/keystore_ui.py | 80 +++---- bitcoin_safe/gui/qt/keystore_uis.py | 10 +- bitcoin_safe/gui/qt/label_syncer.py | 18 +- bitcoin_safe/gui/qt/language_chooser.py | 24 +- bitcoin_safe/gui/qt/main.py | 208 ++++++++++-------- bitcoin_safe/gui/qt/my_treeview.py | 104 ++++----- bitcoin_safe/gui/qt/nLockTimePicker.py | 10 +- .../gui/qt/new_wallet_welcome_screen.py | 8 +- bitcoin_safe/gui/qt/notification_bar.py | 10 +- .../gui/qt/notification_bar_regtest.py | 4 +- bitcoin_safe/gui/qt/plot.py | 18 +- bitcoin_safe/gui/qt/qr_components/__main__.py | 2 +- .../gui/qt/qr_components/quick_receive.py | 19 +- bitcoin_safe/gui/qt/qt_wallet.py | 160 ++++++++------ bitcoin_safe/gui/qt/recipients.py | 48 ++-- bitcoin_safe/gui/qt/search_tree_view.py | 45 ++-- bitcoin_safe/gui/qt/spinbox.py | 6 +- bitcoin_safe/gui/qt/spinning_button.py | 22 +- bitcoin_safe/gui/qt/step_progress_bar.py | 116 +++++----- bitcoin_safe/gui/qt/sync_tab.py | 59 ++--- bitcoin_safe/gui/qt/taglist/main.py | 4 +- bitcoin_safe/gui/qt/tutorial.py | 124 ++++++----- bitcoin_safe/gui/qt/tutorial_screenshots.py | 37 ++-- bitcoin_safe/gui/qt/tx_signing_steps.py | 8 +- bitcoin_safe/gui/qt/ui_tx.py | 60 ++--- .../gui/qt/update_notification_bar.py | 20 +- bitcoin_safe/gui/qt/util.py | 6 +- .../screenshots/coldcard-generate-seed.png | Bin 0 -> 17195 bytes .../coldcard-register-multisig-decriptor.png | Bin 0 -> 14288 bytes .../gui/screenshots/coldcard-view-seed.png | Bin 0 -> 14316 bytes .../screenshots/coldcard-wallet-export.png | Bin 0 -> 14353 bytes .../gui/screenshots/q-generate-seed.png | Bin 0 -> 105363 bytes .../q-register-multisig-decriptor.png | Bin 0 -> 170412 bytes bitcoin_safe/gui/screenshots/q-view-seed.png | Bin 0 -> 123949 bytes .../gui/screenshots/q-wallet-export.png | Bin 0 -> 153638 bytes bitcoin_safe/html.py | 2 +- bitcoin_safe/keystore.py | 22 +- bitcoin_safe/labels.py | 66 ++++-- bitcoin_safe/logging_handlers.py | 10 +- bitcoin_safe/logging_setup.py | 4 +- bitcoin_safe/mempool.py | 22 +- bitcoin_safe/network_config.py | 12 +- bitcoin_safe/pdfrecovery.py | 30 +-- bitcoin_safe/pythonbdk_types.py | 63 +++--- bitcoin_safe/rpc.py | 2 +- bitcoin_safe/signals.py | 18 +- bitcoin_safe/signature_manager.py | 12 +- bitcoin_safe/signer.py | 20 +- bitcoin_safe/simple_mailer.py | 4 +- bitcoin_safe/storage.py | 10 +- bitcoin_safe/util.py | 12 +- bitcoin_safe/wallet.py | 161 ++++++++------ poetry.lock | 106 ++++++--- pyproject.toml | 17 +- tests/gui/qt/test_gui_setup_wallet.py | 26 +-- tests/gui/qt/test_gui_setup_wallet_custom.py | 20 +- tests/gui/qt/test_helpers.py | 28 +-- tests/test_labels.py | 40 +++- tests/test_signature_manager.py | 2 +- tests/test_signers.py | 10 +- 91 files changed, 1604 insertions(+), 1276 deletions(-) create mode 100644 bitcoin_safe/gui/qt/descriptor_edit.py create mode 100644 bitcoin_safe/gui/screenshots/coldcard-generate-seed.png create mode 100644 bitcoin_safe/gui/screenshots/coldcard-register-multisig-decriptor.png create mode 100644 bitcoin_safe/gui/screenshots/coldcard-view-seed.png create mode 100644 bitcoin_safe/gui/screenshots/coldcard-wallet-export.png create mode 100644 bitcoin_safe/gui/screenshots/q-generate-seed.png create mode 100644 bitcoin_safe/gui/screenshots/q-register-multisig-decriptor.png create mode 100644 bitcoin_safe/gui/screenshots/q-view-seed.png create mode 100644 bitcoin_safe/gui/screenshots/q-wallet-export.png diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2825ff..7333bf9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,10 +38,11 @@ repos: args: - --check-untyped-defs # - --disallow-untyped-defs - - --strict-optional + # - --strict-optional # - --no-implicit-optional # - --warn-return-any - --warn-redundant-casts + # - --warn-unreachable additional_dependencies: - types-requests - types-PyYAML diff --git a/README.md b/README.md index b184f85..8b49f8c 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,23 @@ #### Features - **Easy** Bitcoin wallet for long-term cold storage - - **Best practices** built into the wallet setup - - **Easier** address labels by using categories (e.g. "KYC", "Non-KYC", "Work", "Friends", ...) + - **Easy** Multisig-Wallet Setup + - Step-by-Step instructions + - including transactions to test every hardware signer + - **Simpler** address labels by using categories (e.g. "KYC", "Non-KYC", "Work", "Friends", ...) - Automatic coin selection within categories - - **Easier** fee selection for non-technical users - - Automatic UTXO management as much as possible to prevent uneconomical UTXOs in the future - - Opportunistic merging of small utxos when fees are low + - **Sending** for non-technical users + - 1-click fee selection + - Automatic merging of small utxos when fees are low - **Collaborative**: - Wallet chat and sharing of PSBTs (via nostr) - Label synchronization between trusted devices (via nostr) - **Multi-Language**: - 🇺🇸 English, 🇨🇳 Chinese - 简体中文, 🇪🇸 Spanish - español de España, 🇯🇵 Japanese - 日本語, 🇷🇺 Russian - русский, 🇵🇹 Portuguese - português europeu, 🇮🇳 Hindi - हिन्दी, Arabic - العربية, (more upon request) -- **Fast**: Electrum server and upgrade to **Compact Block Filters** for the Bitcoin Safe 2.0 release +- **Fast**: Electrum server connectivity and planned upgrade to **Compact Block Filters** for the Bitcoin Safe 2.0 release - **Secure**: No seed generation or storage (on mainnet). - A hardware signer/signing device for safe seed storage is needed (storing seeds on a computer is reckless) - - Powered by **[BDK](https://github.com/bitcoindevkit/bdk)** + - Powered by **[BDK](https://github.com/bitcoindevkit/bdk)** ## Installation from Git repository @@ -79,11 +81,18 @@ #### More features -* Import export - * csv export of every list - * Label import and export in [BIP329](https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki) +* Many Import and Export options + * CSV export of every list + * Label import and export in [BIP329](https://bip329.org/) * Label import of Electrum wallet - +* Animated [Coldcard Q - QR code](https://bbqr.org/) and Legacy QR codes +* Connectivity to Electrum Servers, Esplora Server, RPC Bitcoin Node (like on [Umbrel](https://umbrel.com/)) + + +#### TODOs for beta release + +- [ ] Add more pytests + #### Goals (for the 2.0 Release) @@ -91,11 +100,6 @@ - Compact Block Filters are **fast** and **private** - Compact Block Filters (bdk) are being [worked on](https://github.com/bitcoindevkit/bdk/issues/679), and will be included in bdk 1.1. For now RPC, Electrum and Esplora are available, but will be replaced completely with Compact Block Filters. -#### TODOs for beta release - -- [ ] Add more pytests -- [ ] [bbqr code](https://bbqr.org/) - ## Development * Run the precommit manually for debugging diff --git a/bitcoin_safe/__init__.py b/bitcoin_safe/__init__.py index 035d5ae..ae80345 100644 --- a/bitcoin_safe/__init__.py +++ b/bitcoin_safe/__init__.py @@ -1 +1 @@ -__version__ = "0.6.2a0" +__version__ = "0.7.0a0" diff --git a/bitcoin_safe/__main__.py b/bitcoin_safe/__main__.py index 2731aa3..77a8f9a 100644 --- a/bitcoin_safe/__main__.py +++ b/bitcoin_safe/__main__.py @@ -16,7 +16,7 @@ from .gui.qt.util import custom_exception_handler -def parse_args(): +def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Bitcoin Safe") parser.add_argument("--network", help="Choose the network: bitcoin, regtest, testnet, signet ") @@ -27,7 +27,7 @@ def parse_args(): return parser.parse_args() -def main(): +def main() -> None: args = parse_args() sys.excepthook = custom_exception_handler diff --git a/bitcoin_safe/config.py b/bitcoin_safe/config.py index 9c66753..c38559d 100644 --- a/bitcoin_safe/config.py +++ b/bitcoin_safe/config.py @@ -38,7 +38,7 @@ logger = logging.getLogger(__name__) import os -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional import appdirs import bdkpython as bdk @@ -72,19 +72,19 @@ class UserConfig(BaseSaveableClass): bdk.Network.TESTNET: [0, 1000], } - def __init__(self): + def __init__(self) -> None: self.network_configs = NetworkConfigs() self.network = bdk.Network.BITCOIN if DEFAULT_MAINNET else bdk.Network.TESTNET self.last_wallet_files: Dict[str, List[str]] = {} # network:[file_path0] self.opened_txlike: Dict[str, List[str]] = {} # network:[serializedtx, serialized psbt] self.data_dir = appdirs.user_data_dir(self.app_name) self.is_maximized = False - self.recently_open_wallets: Dict[bdk.Network, deque] = { + self.recently_open_wallets: Dict[bdk.Network, deque[str]] = { network: deque(maxlen=5) for network in bdk.Network } - self.language_code = None + self.language_code: Optional[str] = None - def add_recently_open_wallet(self, file_path: str): + def add_recently_open_wallet(self, file_path: str) -> None: # ensure that the newest open file moves to the top of the queue, but isn't added multiple times recent_wallets = self.recently_open_wallets[self.network] if file_path in recent_wallets: @@ -96,17 +96,17 @@ def network_config(self) -> NetworkConfig: return self.network_configs.configs[self.network.name] @property - def wallet_dir(self): + def wallet_dir(self) -> str: return os.path.join(self.config_dir, self.network.name) - def get(self, key, default=None): + def get(self, key: str, default=None) -> Any: "For legacy reasons" if hasattr(self, key): return getattr(self, key) else: return default - def dump(self): + def dump(self) -> Dict[str, Any]: d = super().dump() d.update(self.__dict__.copy()) @@ -172,13 +172,13 @@ def from_dump_migration(cls, dct: Dict[str, Any]) -> Dict[str, Any]: return dct @classmethod - def exists(cls, password=None, file_path=None): + def exists(cls, password=None, file_path=None) -> bool: if file_path is None: file_path = cls.config_file return os.path.isfile(file_path) @classmethod - def from_file(cls, password=None, file_path=None): + def from_file(cls, password=None, file_path=None) -> "UserConfig": if file_path is None: file_path = cls.config_file if os.path.isfile(file_path): @@ -186,5 +186,5 @@ def from_file(cls, password=None, file_path=None): else: return UserConfig() - def save(self): + def save(self) -> None: # type: ignore super().save(self.config_file) diff --git a/bitcoin_safe/descriptors.py b/bitcoin_safe/descriptors.py index c451b0a..1ce4cf5 100644 --- a/bitcoin_safe/descriptors.py +++ b/bitcoin_safe/descriptors.py @@ -34,7 +34,7 @@ from typing import Sequence import bdkpython as bdk -from bitcoin_qrreader.multipath_descriptor import ( +from bitcoin_qr_tools.multipath_descriptor import ( MultipathDescriptor as BitcoinQRMultipathDescriptor, ) from bitcoin_usb.address_types import ( diff --git a/bitcoin_safe/dynamic_lib_load.py b/bitcoin_safe/dynamic_lib_load.py index 3e6b0e5..ecb97fc 100644 --- a/bitcoin_safe/dynamic_lib_load.py +++ b/bitcoin_safe/dynamic_lib_load.py @@ -31,6 +31,8 @@ import os import platform import sys +from importlib.metadata import PackageMetadata +from typing import Optional import bitcoin_usb import bitcointx @@ -46,7 +48,7 @@ # Function to show the warning dialog before starting the QApplication -def show_message_before_quit(msg: str): +def show_message_before_quit(msg: str) -> None: # Initialize QApplication first app = QApplication(sys.argv) # Without an application instance, some features might not work as expected @@ -54,7 +56,7 @@ def show_message_before_quit(msg: str): sys.exit(app.exec()) -def _get_binary_lib_path(): +def _get_binary_lib_path() -> str: # Get the platform-specific path to the binary library if platform.system() == "Windows": # On Windows, construct the path to the DLL @@ -66,7 +68,7 @@ def _get_binary_lib_path(): return lib_path -def setup_libsecp256k1(): +def setup_libsecp256k1() -> None: """The operating system might, or might not provide libsecp256k1 needed for bitcointx Therefore we require https://pypi.org/project/electrumsv-secp256k1/ in the build process as additional_requires @@ -92,7 +94,7 @@ def setup_libsecp256k1(): print(f"use libsecp256k1 from os") -def ensure_pyzbar_works(): +def ensure_pyzbar_works() -> None: "Ensure Visual C++ Redistributable Packages for Visual Studio 2013" # Get the platform-specific path to the binary library logger.info(f"Platform: {platform.system()}") @@ -120,14 +122,14 @@ def ensure_pyzbar_works(): pass -def get_briefcase_meta_data(): +def get_briefcase_meta_data() -> Optional[PackageMetadata]: import sys from importlib import metadata as importlib_metadata # Find the name of the module that was used to start the app app_module = sys.modules["__main__"].__package__ if not app_module: - return + return None # Retrieve the app's metadata metadata = importlib_metadata.metadata(app_module) diff --git a/bitcoin_safe/fx.py b/bitcoin_safe/fx.py index 2f830d1..85850a8 100644 --- a/bitcoin_safe/fx.py +++ b/bitcoin_safe/fx.py @@ -51,8 +51,8 @@ def __init__(self, signals_min: SignalsMin) -> None: self.update() logger.debug(f"initialized {self}") - def update(self): - def on_success(data): + def update(self) -> None: + def on_success(data) -> None: if not data: logger.debug(f"empty result of https://api.coingecko.com/api/v3/exchange_rates") return diff --git a/bitcoin_safe/gui/qt/address_dialog.py b/bitcoin_safe/gui/qt/address_dialog.py index 317675f..9aef1f6 100644 --- a/bitcoin_safe/gui/qt/address_dialog.py +++ b/bitcoin_safe/gui/qt/address_dialog.py @@ -29,13 +29,13 @@ import logging +from bitcoin_qr_tools.qr_widgets import QRCodeWidgetSVG + from bitcoin_safe.config import UserConfig from bitcoin_safe.gui.qt.buttonedit import ButtonEdit from bitcoin_safe.mempool import MempoolData from bitcoin_safe.util import serialized_to_hex -from .qr_components.image_widget import QRCodeWidgetSVG - logger = logging.getLogger(__name__) import bdkpython as bdk @@ -66,7 +66,7 @@ def __init__( address: str, mempool_data: MempoolData, parent=None, - ): + ) -> None: super().__init__(parent, Qt.WindowType.Window) self.setWindowTitle(self.tr("Address")) self.mempool_data = mempool_data @@ -160,6 +160,6 @@ def __init__( vbox.addLayout(Buttons(CloseButton(self))) self.setupUi() - def setupUi(self): + def setupUi(self) -> None: self.tabs.setTabText(self.tabs.indexOf(self.tab_details), self.tr("Details")) self.tabs.setTabText(self.tabs.indexOf(self.tab_advanced), self.tr("Advanced")) diff --git a/bitcoin_safe/gui/qt/address_list.py b/bitcoin_safe/gui/qt/address_list.py index f767a2f..9c6ff8b 100644 --- a/bitcoin_safe/gui/qt/address_list.py +++ b/bitcoin_safe/gui/qt/address_list.py @@ -53,6 +53,7 @@ # SOFTWARE. import logging +from typing import Any, Dict, Tuple from ...config import UserConfig from ...network_config import BlockchainType @@ -118,7 +119,7 @@ def __init__(self, upper_menu: QMenu, wallet: Wallet, signals: Signals) -> None: ) self.updateUi() - def updateUi(self): + def updateUi(self) -> None: self.import_label_menu.setTitle(translate("menu", "Import Labels")) self.action_bip329.setText(translate("menu", "Import Labels (BIP329 / Sparrow)")) self.action_electrum.setText(translate("menu", "Import Labels (Electrum Wallet)")) @@ -189,7 +190,7 @@ class Columns(MyTreeView.BaseColumnsEnum): key_column = Columns.ADDRESS column_widths = {Columns.ADDRESS: 150, Columns.COIN_BALANCE: 100} - def __init__(self, fx, config: UserConfig, wallet: Wallet, signals: Signals): + def __init__(self, fx, config: UserConfig, wallet: Wallet, signals: Signals) -> None: super().__init__( config=config, stretch_column=self.stretch_column, @@ -217,7 +218,10 @@ def __init__(self, fx, config: UserConfig, wallet: Wallet, signals: Signals): self.signals.category_updated.connect(self.update_with_filter) self.signals.language_switch.connect(self.updateUi) - def dragEnterEvent(self, event: QDragEnterEvent): + def updateUi(self) -> None: + self.update() + + def dragEnterEvent(self, event: QDragEnterEvent) -> None: # handle dropped files super().dragEnterEvent(event) if event.isAccepted(): @@ -232,10 +236,10 @@ def dragEnterEvent(self, event: QDragEnterEvent): else: event.ignore() - def dragMoveEvent(self, event: QDragMoveEvent): + def dragMoveEvent(self, event: QDragMoveEvent) -> None: return self.dragEnterEvent(event) - def dropEvent(self, event: QDropEvent): + def dropEvent(self, event: QDropEvent) -> None: # handle dropped files super().dropEvent(event) if event.isAccepted(): @@ -264,7 +268,7 @@ def dropEvent(self, event: QDropEvent): event.ignore() - def on_double_click(self, idx: QModelIndex): + def on_double_click(self, idx: QModelIndex) -> None: addr = self.get_role_data_for_current_item(col=self.key_column, role=self.ROLE_KEY) self.signals.show_address.emit(addr) @@ -282,19 +286,19 @@ def get_address(self, force_new=False, category: str = None) -> bdk.AddressInfo: self.select_row(address, self.Columns.ADDRESS) return address_info - def toggle_change(self, state: int): + def toggle_change(self, state: int) -> None: if state == self.show_change: return self.show_change = AddressTypeFilter(state) self.update() - def toggle_used(self, state: int): + def toggle_used(self, state: int) -> None: if state == self.show_used: return self.show_used = AddressUsageStateFilter(state) self.update() - def update_with_filter(self, update_filter: UpdateFilter): + def update_with_filter(self, update_filter: UpdateFilter) -> None: if update_filter.refresh_all: return self.update() logger.debug(f"{self.__class__.__name__} update_with_filter {update_filter}") @@ -334,7 +338,7 @@ def update_with_filter(self, update_filter: UpdateFilter): # manually sort, after the data is filled self.sortByColumn(self.Columns.TYPE, Qt.SortOrder.AscendingOrder) - def get_headers(self): + def get_headers(self) -> Dict: return { self.Columns.NUM_TXS: self.tr("Tx"), self.Columns.TYPE: self.tr("Type"), @@ -346,7 +350,7 @@ def get_headers(self): self.Columns.FIAT_BALANCE: self.tr("Fiat Balance"), } - def update(self): + def update(self) -> None: if self.maybe_defer_update(): return logger.debug(f"{self.__class__.__name__} update") @@ -381,7 +385,7 @@ def update(self): self.sortByColumn(self.Columns.TYPE, Qt.SortOrder.AscendingOrder) super().update() - def append_address(self, address: str): + def append_address(self, address: str) -> None: balance = self.wallet.get_addr_balance(address).total is_used_and_empty = self.wallet.address_is_used(address) and balance == 0 if self.show_used == AddressUsageStateFilter.UNUSED and (balance or is_used_and_empty): @@ -428,7 +432,7 @@ def append_address(self, address: str): self.std_model.insertRow(count, item) self.refresh_row(address, count) - def refresh_row(self, key: str, row: int): + def refresh_row(self, key: str, row: int) -> None: assert row is not None address = key label = self.wallet.get_label_for_address(address) @@ -475,7 +479,7 @@ def refresh_row(self, key: str, row: int): # if calculated_width > current_width: # self.header().resizeSection(self.Columns.ADDRESS, calculated_width) - def create_menu(self, position: QPoint): + def create_menu(self, position: QPoint) -> None: # is_multisig = isinstance(self.wallet, Multisig_Wallet) selected = self.selected_in_column(self.Columns.ADDRESS) if not selected: @@ -535,12 +539,12 @@ def create_menu(self, position: QPoint): # raise # super().place_text_on_clipboard(text, title=title) - def get_edit_key_from_coordinate(self, row, col): + def get_edit_key_from_coordinate(self, row, col) -> Any: if col != self.Columns.LABEL: return None return self.get_role_data_from_coordinate(row, self.key_column, role=self.ROLE_KEY) - def on_edited(self, idx, edit_key, *, text): + def on_edited(self, idx, edit_key, *, text) -> None: self.wallet.labels.set_addr_label(edit_key, text, timestamp="now") self.signals.labels_updated.emit( UpdateFilter( @@ -568,7 +572,7 @@ def __init__(self, address_list: AddressList, config: UserConfig, parent: QWidge self.address_list.signals.language_switch.connect(self.updateUi) self.address_list.signals.utxos_updated.connect(self.updateUi) - def updateUi(self): + def updateUi(self) -> None: super().updateUi() self.action_show_filter.setText(self.tr("Show Filter")) @@ -580,7 +584,7 @@ def updateUi(self): self.balance_label.setText(balance.format_short(self.address_list.wallet.network)) self.balance_label.setToolTip(balance.format_long(self.address_list.wallet.network)) - def create_toolbar_with_menu(self, title): + def create_toolbar_with_menu(self, title) -> None: super().create_toolbar_with_menu(title=title) font = QFont() @@ -603,7 +607,7 @@ def create_toolbar_with_menu(self, title): and self.config.network != bdk.Network.BITCOIN ): - def mine_to_selected_addresses(): + def mine_to_selected_addresses() -> None: selected = self.address_list.selected_in_column(self.address_list.Columns.ADDRESS) if not selected: return @@ -629,8 +633,8 @@ def mine_to_selected_addresses(): hbox = self.create_toolbar_buttons() self.toolbar.insertLayout(self.toolbar.count() - 1, hbox) - def create_toolbar_buttons(self): - def get_toolbar_buttons(): + def create_toolbar_buttons(self) -> QHBoxLayout: + def get_toolbar_buttons() -> Tuple[QComboBox, QComboBox]: return self.change_button, self.used_button hbox = QHBoxLayout() @@ -641,10 +645,10 @@ def get_toolbar_buttons(): self.toolbar_buttons = buttons return hbox - def on_hide_toolbar(self): + def on_hide_toolbar(self) -> None: self.update() - def show_toolbar(self, is_visible: bool, config=None): + def show_toolbar(self, is_visible: bool, config=None) -> None: super().show_toolbar(is_visible=is_visible, config=config) for b in self.toolbar_buttons: b.setVisible(is_visible) diff --git a/bitcoin_safe/gui/qt/bitcoin_quick_receive.py b/bitcoin_safe/gui/qt/bitcoin_quick_receive.py index 553ca4d..3b79fe9 100644 --- a/bitcoin_safe/gui/qt/bitcoin_quick_receive.py +++ b/bitcoin_safe/gui/qt/bitcoin_quick_receive.py @@ -44,7 +44,7 @@ def __init__( wallet: Wallet, title="", limit_to_categories=None, - ): + ) -> None: super().__init__(title) self.signals = signals self.wallet = wallet @@ -54,7 +54,7 @@ def __init__( self.signals.category_updated.connect(self.update) self.signals.language_switch.connect(self.update) - def update(self): + def update(self) -> None: super().update() self.clear_boxes() self.label_title.setText(self.tr("Quick Receive")) diff --git a/bitcoin_safe/gui/qt/block_buttons.py b/bitcoin_safe/gui/qt/block_buttons.py index 2d48a4d..efbd200 100644 --- a/bitcoin_safe/gui/qt/block_buttons.py +++ b/bitcoin_safe/gui/qt/block_buttons.py @@ -32,7 +32,7 @@ logger = logging.getLogger(__name__) import enum -from typing import List +from typing import Callable, List import bdkpython as bdk from PyQt6.QtCore import QLocale, QObject, Qt, QTimer, pyqtSignal @@ -50,7 +50,7 @@ locale = QLocale() # This initializes a QLocale object with the user's default locale -def format_block_number(block_number): +def format_block_number(block_number) -> str: return locale.toString(int(block_number)) @@ -75,47 +75,47 @@ def setText(self, arg__1: str) -> None: class LabelTitle(BaseBlockLabel): - def set(self, text: str, block_type: BlockType): + def set(self, text: str, block_type: BlockType) -> None: self.setText(html_f(text, color="white" if block_type else "black", size="16px")) class LabelApproximateMedianFee(BaseBlockLabel): - def set(self, median_fee: float, block_type: BlockType): + def set(self, median_fee: float, block_type: BlockType) -> None: s = f"~{int(median_fee)} Sat/vB" self.setText(html_f(s, color="white" if block_type else "black", size="12px")) class LabelExactMedianFee(BaseBlockLabel): - def set(self, median_fee: float, block_type: BlockType): + def set(self, median_fee: float, block_type: BlockType) -> None: s = f"{round(median_fee, 1)} Sat/vB" self.setText(html_f(s, color="white" if block_type else "black", size="12px")) class LabelNumberConfirmations(BaseBlockLabel): - def set(self, i: int, block_type: BlockType): + def set(self, i: int, block_type: BlockType) -> None: s = f"{i} Confirmation{'s' if i>1 else ''}" self.setText(html_f(s, color="white" if block_type else "black", size="12px")) class LabelBlockHeight(BaseBlockLabel): - def set(self, i: int, block_type: BlockType): + def set(self, i: int, block_type: BlockType) -> None: s = f"{round(i)}. Block" self.setText(html_f(s, color="white" if block_type else "black", size="12px")) class LabelFeeRange(BaseBlockLabel): - def set(self, min_fee: float, max_fee: float): + def set(self, min_fee: float, max_fee: float) -> None: s = f"{int(min_fee)} - {int(max_fee)} Sat/vB" self.setText(html_f(s, color="#eee002", size="10px")) class LabelTimeEstimation(BaseBlockLabel): - def set(self, block_number: int, block_type: BlockType): + def set(self, block_number: int, block_type: BlockType) -> None: if block_number < 6: s = self.tr("~in {t} min").format(t=(block_number) * 10) else: @@ -125,13 +125,13 @@ def set(self, block_number: int, block_type: BlockType): class LabelExplorer(BaseBlockLabel): - def set(self, block_type: BlockType): + def set(self, block_type: BlockType) -> None: s = "visit
mempool.space" self.setText(html_f(s, color="white" if block_type else "black", size="10px")) class BlockButton(QPushButton): - def __init__(self, size=100, parent=None): + def __init__(self, size=100, parent=None) -> None: super().__init__(parent=parent) # Create labels for each text line @@ -164,11 +164,11 @@ def __init__(self, size=100, parent=None): self.setMinimumHeight(size) self.setMinimumWidth(size) - def clear_labels(self): + def clear_labels(self) -> None: for label in self.labels: label.setText("") - def _set_background_gradient(self, color_top: QColor, color_bottom: QColor): + def _set_background_gradient(self, color_top: QColor, color_bottom: QColor) -> None: # Set the stylesheet for the QPushButton self.setStyleSheet( f""" @@ -181,7 +181,7 @@ def _set_background_gradient(self, color_top: QColor, color_bottom: QColor): """ ) - def set_background_gradient(self, min_fee: float, max_fee: float, block_type: BlockType): + def set_background_gradient(self, min_fee: float, max_fee: float, block_type: BlockType) -> None: self.block_type = block_type if self.block_type == BlockType.confirmed: self._set_background_gradient("#115fb0", "#9239f3") @@ -195,7 +195,7 @@ def set_background_gradient(self, min_fee: float, max_fee: float, block_type: Bl class VerticalButtonGroup(InvisibleScrollArea): signal_button_click = pyqtSignal(int) - def __init__(self, button_count=3, parent=None, size=100): + def __init__(self, button_count=3, parent=None, size=100) -> None: super().__init__(parent) layout = QVBoxLayout(self.content_widget) self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) @@ -210,8 +210,8 @@ def __init__(self, button_count=3, parent=None, size=100): for i in range(button_count): button = BlockButton(size=size) - def create_signal_handler(index): - def send_signal(): + def create_signal_handler(index) -> Callable: + def send_signal() -> None: return self.signal_button_click.emit(index) return send_signal @@ -233,7 +233,7 @@ def __init__(self, mempool_data: MempoolData, parent=None) -> None: self.timer.timeout.connect(self.mempool_data.set_data_from_mempoolspace) self.timer.start(10 * 60 * 1000) # 10 minutes in milliseconds - def set_mempool_block_unknown_fee_rate(self, i, confirmation_time: bdk.BlockTime = None): + def set_mempool_block_unknown_fee_rate(self, i, confirmation_time: bdk.BlockTime = None) -> None: logger.error("This should not be called") @@ -261,7 +261,7 @@ def __init__(self, mempool_data: MempoolData, max_button_count=3, parent=None) - self.refresh() self.mempool_data.signal_data_updated.connect(self.refresh) - def refresh(self, **kwargs): + def refresh(self, **kwargs) -> None: if self.mempool_data is None: return @@ -281,7 +281,7 @@ def refresh(self, **kwargs): button.label_fee_range.set(*self.mempool_data.fee_rates_min_max(i)) button.set_background_gradient(*self.mempool_data.fee_rates_min_max(i), BlockType.projected) - def _on_button_click(self, i: int): + def _on_button_click(self, i: int) -> None: logger.debug(f"Clicked button {i}: {self.mempool_data.median_block_fee_rate(i)}") self.signal_click.emit(self.mempool_data.median_block_fee_rate(i)) @@ -311,17 +311,17 @@ def __init__( self.button_group.signal_button_click.connect(self._on_button_click) self.mempool_data.signal_data_updated.connect(self.refresh) - def set_url(self, url: str): + def set_url(self, url: str) -> None: self.url = url - def set_unknown_fee_rate(self): + def set_unknown_fee_rate(self) -> None: for button in self.button_group.buttons: button.clear_labels() button.label_title.set(self.tr("Unconfirmed"), BlockType.projected) button.label_explorer.set(BlockType.projected) button.set_background_gradient(0, 1, BlockType.projected) - def refresh(self, fee_rate=None, **kwargs): + def refresh(self, fee_rate=None, **kwargs) -> None: self.fee_rate = fee_rate if fee_rate else self.fee_rate if self.mempool_data is None: return @@ -343,7 +343,7 @@ def refresh(self, fee_rate=None, **kwargs): button.label_time_estimation.set(block_index + 1, BlockType.projected) button.set_background_gradient(*self.mempool_data.fee_rates_min_max(i), BlockType.projected) - def _on_button_click(self, i: int): + def _on_button_click(self, i: int) -> None: block_index = self.mempool_data.fee_rate_to_projected_block_index(self.fee_rate) url = ( self.url @@ -377,10 +377,10 @@ def __init__( self.button_group.signal_button_click.connect(self._on_button_click) - def set_url(self, url: str): + def set_url(self, url: str) -> None: self.url = url - def refresh(self, fee_rate=None, confirmation_time=None, chain_height=None, **kwargs): + def refresh(self, fee_rate=None, confirmation_time=None, chain_height=None, **kwargs) -> None: self.fee_rate = fee_rate if fee_rate else self.fee_rate self.confirmation_time = confirmation_time if confirmation_time else self.confirmation_time if not self.confirmation_time: @@ -409,7 +409,7 @@ def refresh(self, fee_rate=None, confirmation_time=None, chain_height=None, **kw button.set_background_gradient(0, 1, BlockType.confirmed) button.label_exact_median_fee.setText("") - def _on_button_click(self, i: int): + def _on_button_click(self, i: int) -> None: if self.url: open_website(self.url) self.signal_click.emit(self.fee_rate) diff --git a/bitcoin_safe/gui/qt/block_change_signals.py b/bitcoin_safe/gui/qt/block_change_signals.py index 82a73e1..2fba82c 100644 --- a/bitcoin_safe/gui/qt/block_change_signals.py +++ b/bitcoin_safe/gui/qt/block_change_signals.py @@ -56,7 +56,7 @@ def _collect_sub_widget(self, widget: QWidget) -> List[QWidget]: widgets += self._collect_sub_widget(child) return widgets - def _collect_widgets_in_tab(self, tab_widget: QTabWidget): + def _collect_widgets_in_tab(self, tab_widget: QTabWidget) -> List[QWidget]: """Recursively collect all widgets in a QTabWidget.""" widgets = [] for index in range(tab_widget.count()): @@ -73,7 +73,7 @@ def collect_all_widgets(self) -> Set[QWidget]: l += self._collect_widgets_in_tab(widget) return set(l) - def __enter__(self): + def __enter__(self) -> None: if self.all_widgets is None: self.all_widgets = self.fill_widget_list() @@ -83,7 +83,7 @@ def __enter__(self): for widget in self.all_widgets: widget.blockSignals(True) - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type, exc_value, traceback) -> None: if self.all_widgets is None: self.all_widgets = self.fill_widget_list() diff --git a/bitcoin_safe/gui/qt/buttonedit.py b/bitcoin_safe/gui/qt/buttonedit.py index c49d952..c8ffb55 100644 --- a/bitcoin_safe/gui/qt/buttonedit.py +++ b/bitcoin_safe/gui/qt/buttonedit.py @@ -31,6 +31,8 @@ from typing import Callable, List, Optional, Union from bdkpython import bdk +from bitcoin_qr_tools.bitcoin_video_widget import BitcoinVideoWidget +from bitcoin_qr_tools.data import Data, DecodingException from PyQt6.QtCore import QSize, Qt, pyqtSignal from PyQt6.QtGui import QIcon, QResizeEvent from PyQt6.QtWidgets import ( @@ -47,7 +49,7 @@ QWidget, ) -from bitcoin_safe.gui.qt.util import do_copy, icon_path +from bitcoin_safe.gui.qt.util import Message, do_copy, icon_path from bitcoin_safe.i18n import translate logger = logging.getLogger(__name__) @@ -60,14 +62,14 @@ def __init__(self, qicon: QIcon, parent) -> None: class ButtonsField(QWidget): - def __init__(self, vertical_align: Qt = Qt.AlignmentFlag.AlignBottom, parent=None): + def __init__(self, vertical_align: Qt = Qt.AlignmentFlag.AlignBottom, parent=None) -> None: super().__init__(parent) self.grid_layout = QGridLayout(self) self.grid_layout.setContentsMargins(0, 0, 0, 0) self.grid_layout.setSpacing(0) self.vertical_align = vertical_align - def minimumSizeHint(self): + def minimumSizeHint(self) -> QSize: # Initialize minimum width and height width = 0 height = 0 @@ -83,11 +85,11 @@ def minimumSizeHint(self): # If there are no buttons, fall back to the default minimum size hint return super().minimumSizeHint() - def resizeEvent(self, event: QResizeEvent): + def resizeEvent(self, event: QResizeEvent) -> None: super().resizeEvent(event) self.rearrange_buttons() - def rearrange_buttons(self): + def rearrange_buttons(self) -> None: # Get the current size of the widget current_height = self.size().height() @@ -146,7 +148,7 @@ def __init__( parent=None, input_field=None, signal_update: pyqtSignal = None, - ): + ) -> None: super().__init__(parent=parent) self.callback_is_valid: Optional[Callable[[], bool]] = None self.buttons: List[QPushButton] = [] # Store button references @@ -185,7 +187,7 @@ def __init__( signal_update.connect(self.updateUi) self.updateUi() - def updateUi(self): + def updateUi(self) -> None: if self.button_camera: self.button_camera.setToolTip(translate("d", "Read QR code from camera")) if self.copy_button: @@ -197,7 +199,9 @@ def updateUi(self): if self.open_file_button: self.open_file_button.setToolTip(translate("d", "Open file")) - def add_button(self, button_path: Optional[str], button_callback: Callable, tooltip: str = ""): + def add_button( + self, button_path: Optional[str], button_callback: Callable, tooltip: str = "" + ) -> SquareButton: button = SquareButton(QIcon(button_path), parent=self) # Create the button with the icon if tooltip: button.setToolTip(tooltip) @@ -211,15 +215,16 @@ def add_button(self, button_path: Optional[str], button_callback: Callable, tool def add_copy_button( self, - ): - def on_copy(): + ) -> SquareButton: + def on_copy() -> None: do_copy(self.text()) self.copy_button = self.add_button( icon_path("copy.png"), on_copy, tooltip=translate("d", "Copy to clipboard") ) + return self.copy_button - def set_input_field(self, input_widget: QWidget): + def set_input_field(self, input_widget: QWidget) -> None: # Remove the current input field from the layout and delete it self.main_layout.removeWidget(self.input_field) self.input_field.deleteLater() @@ -228,24 +233,24 @@ def set_input_field(self, input_widget: QWidget): self.input_field = input_widget self.main_layout.insertWidget(0, self.input_field) # Insert at the beginning - def setText(self, value: str): + def setText(self, value: str) -> None: self.input_field.setText(value) - def setPlainText(self, value: str): + def setPlainText(self, value: str) -> None: self.input_field.setText(value) - def setStyleSheet(self, value: str): + def setStyleSheet(self, value: str) -> None: self.input_field.setStyleSheet(value) - def text(self): + def text(self) -> str: if hasattr(self.input_field, "toPlainText"): return getattr(self.input_field, "toPlainText")() return self.input_field.text() - def setPlaceholderText(self, value: str): + def setPlaceholderText(self, value: str) -> None: self.input_field.setPlaceholderText(value) - def setReadOnly(self, value: bool): + def setReadOnly(self, value: bool) -> None: self.input_field.setReadOnly(value) def add_qr_input_from_camera_button( @@ -253,18 +258,24 @@ def add_qr_input_from_camera_button( network: bdk.Network, *, custom_handle_input=None, - ): - def input_qr_from_camera(): - from bitcoin_qrreader import bitcoin_qr, bitcoin_qr_gui + ) -> SquareButton: + def input_qr_from_camera() -> None: + def exception_callback(e: Exception) -> None: + if isinstance(e, DecodingException): + Message("Could not recognize the input.") + else: + Message(str(e)) - def result_callback(data: bitcoin_qr.Data): + def result_callback(data: Data) -> None: if custom_handle_input: custom_handle_input(data, self) else: if hasattr(self, "setText"): self.setText(str(data.data_as_string())) - window = bitcoin_qr_gui.BitcoinVideoWidget(result_callback=result_callback, network=network) + window = BitcoinVideoWidget( + result_callback=result_callback, network=network, exception_callback=exception_callback + ) window.show() self.button_camera = self.add_button( @@ -279,17 +290,18 @@ def result_callback(data: bitcoin_qr.Data): def add_pdf_buttton( self, on_click: Callable, - ): + ) -> SquareButton: self.pdf_button = self.add_button( icon_path("pdf-file.svg"), on_click, tooltip=translate("d", "Create PDF") ) + return self.pdf_button def add_random_mnemonic_button( self, callback_seed=None, - ): - def on_click(): + ) -> SquareButton: + def on_click() -> None: seed = bdk.Mnemonic(bdk.WordCount.WORDS12).as_string() self.setText(seed) if callback_seed: @@ -298,12 +310,13 @@ def on_click(): self.mnemonic_button = self.add_button( icon_path("dice.svg"), on_click, tooltip=translate("d", "Create random mnemonic") ) + return self.mnemonic_button - def addResetButton(self, get_reset_text): - def on_click(): + def addResetButton(self, get_reset_text) -> SquareButton: + def on_click() -> None: self.setText(get_reset_text()) - self.add_button("reset-update.svg", on_click, "Reset") + return self.add_button("reset-update.svg", on_click, "Reset") # button.setStyleSheet("background-color: white;") def add_open_file_button( @@ -311,7 +324,7 @@ def add_open_file_button( callback_open_filepath, filter="All Files (*);;PSBT (*.psbt);;Transation (*.tx)", ) -> QPushButton: - def on_click(): + def on_click() -> None: file_path, _ = QFileDialog.getOpenFileName( self, translate("d", "Open Transaction/PSBT"), @@ -332,7 +345,7 @@ def on_click(): self.open_file_button = button return self.open_file_button - def format_as_error(self, value: bool): + def format_as_error(self, value: bool) -> None: if value: self.input_field.setStyleSheet( f"{self.input_field.__class__.__name__}" + " { background-color: #ff6c54; }" @@ -340,12 +353,12 @@ def format_as_error(self, value: bool): else: self.input_field.setStyleSheet("") - def format(self): + def format(self) -> None: if not self.callback_is_valid: return self.format_as_error(False) self.format_as_error(not self.callback_is_valid()) - def set_validator(self, callback_is_valid: Callable[[], bool]): + def set_validator(self, callback_is_valid: Callable[[], bool]) -> None: self.callback_is_valid = callback_is_valid @@ -353,7 +366,7 @@ def set_validator(self, callback_is_valid: Callable[[], bool]): if __name__ == "__main__": import sys - def example_callback(): + def example_callback() -> None: print("Button clicked!") app = QApplication(sys.argv) diff --git a/bitcoin_safe/gui/qt/category_list.py b/bitcoin_safe/gui/qt/category_list.py index 1cc46ff..e4003da 100644 --- a/bitcoin_safe/gui/qt/category_list.py +++ b/bitcoin_safe/gui/qt/category_list.py @@ -48,7 +48,7 @@ def __init__( parent=None, tag_name="category", immediate_release=True, - ): + ) -> None: super().__init__(parent, enable_drag=False, immediate_release=immediate_release) self.categories = categories @@ -59,11 +59,11 @@ def __init__( self.refresh(UpdateFilter(refresh_all=True)) self.signals.language_switch.connect(lambda: self.refresh(UpdateFilter(refresh_all=True))) - def refresh(self, update_filter: UpdateFilter): + def refresh(self, update_filter: UpdateFilter) -> None: self.recreate(self.categories, sub_texts=self.get_sub_texts()) @classmethod - def color(cls, category): + def color(cls, category) -> QColor: if not category: return QColor(255, 255, 255, 255) return hash_color(category) @@ -77,7 +77,7 @@ def __init__( get_sub_texts=None, parent=None, prevent_empty_categories=True, - ): + ) -> None: sub_texts = get_sub_texts() if get_sub_texts else None super().__init__(parent, categories, tag_name="", sub_texts=sub_texts) @@ -95,19 +95,19 @@ def __init__( self.list_widget.signal_tag_added.connect(self.on_added) self.signals.language_switch.connect(self.updateUi) - def updateUi(self): - super().updateUi() + def updateUi(self) -> None: self.tag_name = self.tr("category") + super().updateUi() self.refresh(UpdateFilter(refresh_all=True)) - def on_added(self, category): + def on_added(self, category) -> None: if not category or category in self.categories: return self.categories.append(category) self.signals.category_updated.emit(UpdateFilter(categories=[category])) - def on_delete(self, category): + def on_delete(self, category) -> None: if category not in self.categories: return idx = self.categories.index(category) @@ -118,11 +118,11 @@ def on_delete(self, category): self.list_widget.add("Default") self.signals.category_updated.emit(UpdateFilter(refresh_all=True)) - def refresh(self, update_filter: UpdateFilter): + def refresh(self, update_filter: UpdateFilter) -> None: self.list_widget.recreate(self.categories, sub_texts=self.get_sub_texts()) @classmethod - def color(cls, category): + def color(cls, category) -> QColor: if not category: return QColor(255, 255, 255, 255) return hash_color(category) diff --git a/bitcoin_safe/gui/qt/controlled_groupbox.py b/bitcoin_safe/gui/qt/controlled_groupbox.py index f79bfe6..ec22304 100644 --- a/bitcoin_safe/gui/qt/controlled_groupbox.py +++ b/bitcoin_safe/gui/qt/controlled_groupbox.py @@ -32,7 +32,7 @@ class ControlledGroupbox(QWidget): - def __init__(self, checkbox_text="Enable GroupBox", groupbox_text="", enabled=True): + def __init__(self, checkbox_text="Enable GroupBox", groupbox_text="", enabled=True) -> None: super().__init__() self.setLayout(QVBoxLayout()) @@ -52,7 +52,7 @@ def __init__(self, checkbox_text="Enable GroupBox", groupbox_text="", enabled=Tr self.groupbox.setEnabled(enabled) self.checkbox.stateChanged.connect(self.toggleGroupBox) - def toggleGroupBox(self, value): + def toggleGroupBox(self, value) -> None: """Toggle the enabled state of the groupbox based on the checkbox.""" self.groupbox.setEnabled(value == Qt.CheckState.Checked.value) diff --git a/bitcoin_safe/gui/qt/custom_edits.py b/bitcoin_safe/gui/qt/custom_edits.py index 959a277..0e788d4 100644 --- a/bitcoin_safe/gui/qt/custom_edits.py +++ b/bitcoin_safe/gui/qt/custom_edits.py @@ -28,105 +28,34 @@ import logging -from typing import Callable, Dict, List, Optional - -from bitcoin_safe.gui.qt.buttonedit import ButtonEdit -from bitcoin_safe.signals import SignalsMin -from bitcoin_safe.wallet import Wallet +from typing import Dict, List logger = logging.getLogger(__name__) import bdkpython as bdk -from bitcoin_qrreader.bitcoin_qr import MultipathDescriptor -from PyQt6.QtCore import QEvent, Qt, pyqtSignal +from PyQt6.QtCore import QSize, Qt, pyqtSignal from PyQt6.QtGui import QFocusEvent, QKeyEvent -from PyQt6.QtWidgets import QLineEdit, QTextEdit - -from ...pdfrecovery import make_and_open_pdf -from .util import Message, MessageType +from PyQt6.QtWidgets import QCompleter, QLineEdit, QTextEdit class MyTextEdit(QTextEdit): - def __init__(self, preferred_height=50): + def __init__(self, preferred_height=50) -> None: super().__init__() self.preferred_height = preferred_height - def sizeHint(self): + def sizeHint(self) -> QSize: size = super().sizeHint() size.setHeight(self.preferred_height) return size -class DescriptorEdit(ButtonEdit): - signal_change = pyqtSignal(str) - - def __init__( - self, - network: bdk.Network, - signals_min: SignalsMin, - get_wallet: Optional[Callable[[], Wallet]] = None, - signal_update: pyqtSignal = None, - ): - super().__init__( - input_field=MyTextEdit(preferred_height=50), - button_vertical_align=Qt.AlignmentFlag.AlignBottom, - signal_update=signal_update, - ) - self.network = network - - def do_pdf(): - if not get_wallet: - Message( - self.tr("Wallet setup not finished. Please finish before creating a Backup pdf."), - type=MessageType.Error, - ) - return - - make_and_open_pdf(get_wallet()) - - from bitcoin_qrreader import bitcoin_qr - - def custom_handle_camera_input(data: bitcoin_qr.Data, parent): - self.setText(str(data.data_as_string())) - self.signal_change.emit(str(data.data_as_string())) - - self.add_copy_button() - self.add_qr_input_from_camera_button( - network=self.network, - custom_handle_input=custom_handle_camera_input, - ) - if get_wallet is not None: - self.add_pdf_buttton(do_pdf) - self.set_validator(self._check_if_valid) - - def _check_if_valid(self): - if not self.text(): - return True - try: - MultipathDescriptor.from_descriptor_str(self.text(), self.network) - return True - except: - return False - - def keyReleaseEvent(self, e: QKeyEvent): - # print(e.type(), e.modifiers(), [key for key in Qt.Key if key.value == e.key() ] , e.matches(QKeySequence.StandardKey.Paste) ) - # If it's a regular key press - if e.type() == QEvent.Type.KeyRelease: - self.signal_change.emit(self.text()) - # If it's another type of shortcut, let the parent handle it - else: - super().keyReleaseEvent(e) - - -from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QCompleter, QLineEdit - - class QCompleterLineEdit(QLineEdit): signal_focus_out = pyqtSignal() - def __init__(self, network: bdk.Network, suggestions: Dict[bdk.Network, List[str]] = None, parent=None): + def __init__( + self, network: bdk.Network, suggestions: Dict[bdk.Network, List[str]] = None, parent=None + ) -> None: super().__init__(parent) # Dictionary to store suggestions for each network self.suggestions = suggestions if suggestions else {network: [] for network in bdk.Network} @@ -137,44 +66,44 @@ def __init__(self, network: bdk.Network, suggestions: Dict[bdk.Network, List[str self._completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) self.setCompleter(self._completer) - def set_network(self, network): + def set_network(self, network) -> None: """Set the network and update the completer.""" self.network = network if network not in self.suggestions: self.suggestions[network] = [] self._update_completer() - def reset_memory(self): + def reset_memory(self) -> None: """Clears the memory for the current network.""" if self.network: self.suggestions[self.network].clear() self._update_completer() - def add_current_to_memory(self): + def add_current_to_memory(self) -> None: """Adds the current text to the memory of the current network.""" current_text = self.text() if self.network and current_text and current_text not in self.suggestions[self.network]: self.suggestions[self.network].append(current_text) self._update_completer() - def add_to_memory(self, text): + def add_to_memory(self, text) -> None: """Adds a specific string to the memory of the current network.""" if self.network and text and text not in self.suggestions[self.network]: self.suggestions[self.network].append(text) self._update_completer() - def _update_completer(self): + def _update_completer(self) -> None: """Updates the completer with the current network's suggestions list.""" if self.network: self._completer.model().setStringList(self.suggestions[self.network]) - def keyPressEvent(self, event: QKeyEvent): + def keyPressEvent(self, event: QKeyEvent) -> None: if self.network and event.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down): if not self._completer.popup().isVisible(): self._completer.complete() super(QCompleterLineEdit, self).keyPressEvent(event) - def focusOutEvent(self, event: QFocusEvent): + def focusOutEvent(self, event: QFocusEvent) -> None: super().focusOutEvent(event) self.signal_focus_out.emit() diff --git a/bitcoin_safe/gui/qt/data_tab_widget.py b/bitcoin_safe/gui/qt/data_tab_widget.py index c585c3d..38eeef3 100644 --- a/bitcoin_safe/gui/qt/data_tab_widget.py +++ b/bitcoin_safe/gui/qt/data_tab_widget.py @@ -28,7 +28,7 @@ import logging -from typing import Any +from typing import Any, Dict logger = logging.getLogger(__name__) @@ -37,27 +37,27 @@ class DataTabWidget(QTabWidget): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) - self.tab_data = {} + self.tab_data: Dict[int, Any] = {} - def setTabData(self, index, data): + def setTabData(self, index, data) -> None: self.tab_data[index] = data - def tabData(self, index): + def tabData(self, index) -> Any: return self.tab_data.get(index) - def getCurrentTabData(self): + def getCurrentTabData(self) -> Any: current_index = self.currentIndex() return self.tabData(current_index) - def getAllTabData(self): + def getAllTabData(self) -> Dict[int, Any]: return self.tab_data - def clearTabData(self): + def clearTabData(self) -> None: self.tab_data = {} - def addTab(self, widget, icon=None, description="", data=None): + def addTab(self, widget, icon=None, description="", data=None) -> int: if icon: index = super().addTab(widget, QIcon(icon), description.replace("&", "").capitalize()) else: @@ -65,7 +65,7 @@ def addTab(self, widget, icon=None, description="", data=None): self.setTabData(index, data) return index - def insertTab(self, index, widget, icon=None, description="", data=None): + def insertTab(self, index, widget, icon=None, description="", data=None) -> int: if icon: new_index = super().insertTab( index, widget, QIcon(icon), description.replace("&", "").capitalize() @@ -75,11 +75,11 @@ def insertTab(self, index, widget, icon=None, description="", data=None): self._updateDataAfterInsert(new_index, data) return new_index - def removeTab(self, index): + def removeTab(self, index) -> None: super().removeTab(index) self._updateDataAfterRemove(index) - def _updateDataAfterInsert(self, new_index, data): + def _updateDataAfterInsert(self, new_index, data) -> None: new_data = {} for i, d in sorted(self.tab_data.items()): if i >= new_index: @@ -89,7 +89,7 @@ def _updateDataAfterInsert(self, new_index, data): new_data[new_index] = data self.tab_data = new_data - def _updateDataAfterRemove(self, removed_index): + def _updateDataAfterRemove(self, removed_index) -> None: new_data = {} for i, d in self.tab_data.items(): if i < removed_index: @@ -119,7 +119,7 @@ def get_data_for_tab(self, tab: QWidget) -> Any: tab_widget.addTab(tab2, description="Tab &2", data="Data for Tab 2") # No icon # Connect tab change signal to a function to display current tab data - def show_current_tab_data(index): + def show_current_tab_data(index) -> None: data = tab_widget.getCurrentTabData() QMessageBox.information(tab_widget, "Current Tab Data", f"Data for current tab: {data}") diff --git a/bitcoin_safe/gui/qt/debug_widget.py b/bitcoin_safe/gui/qt/debug_widget.py index 0f64839..33c87e6 100644 --- a/bitcoin_safe/gui/qt/debug_widget.py +++ b/bitcoin_safe/gui/qt/debug_widget.py @@ -41,14 +41,14 @@ class DebugWidget(QWidget): - def paintEvent(self, event: QPaintEvent): + def paintEvent(self, event: QPaintEvent) -> None: super().paintEvent(event) self.drawDebugInfo(self) - def _cleaned_size_policy(self, policy): + def _cleaned_size_policy(self, policy) -> str: return str(policy).split(".")[-1] - def _collect_debug_info(self, widget: QWidget, level=0): + def _collect_debug_info(self, widget: QWidget, level=0) -> str: indent = " " * level sizePolicy = widget.sizePolicy() sizePolicyText = f"{indent}SP: H-{self._cleaned_size_policy(sizePolicy.horizontalPolicy())}, V-{self._cleaned_size_policy(sizePolicy.verticalPolicy())}" @@ -78,7 +78,7 @@ def _collect_debug_info(self, widget: QWidget, level=0): return tooltipText - def drawDebugInfo(self, widget: QWidget): + def drawDebugInfo(self, widget: QWidget) -> None: widget_hash = hash(widget) random.seed(widget_hash) color = QColor(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) @@ -102,7 +102,7 @@ def drawDebugInfo(self, widget: QWidget): def generate_debug_class(BaseClass) -> QWidget: class DebugClass(BaseClass): - def paintEvent(self, event: QPaintEvent): + def paintEvent(self, event: QPaintEvent) -> None: super().paintEvent(event) DebugWidget().drawDebugInfo(self) diff --git a/bitcoin_safe/gui/qt/descriptor_edit.py b/bitcoin_safe/gui/qt/descriptor_edit.py new file mode 100644 index 0000000..af17e1f --- /dev/null +++ b/bitcoin_safe/gui/qt/descriptor_edit.py @@ -0,0 +1,128 @@ +# +# Bitcoin Safe +# Copyright (C) 2024 Andreas Griffin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of version 3 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import logging +from typing import Callable, Optional + +from bitcoin_qr_tools.data import Data, DataType + +from bitcoin_safe.gui.qt.buttonedit import ButtonEdit +from bitcoin_safe.gui.qt.custom_edits import MyTextEdit +from bitcoin_safe.gui.qt.export_data import ExportDataSimple +from bitcoin_safe.signals import SignalsMin +from bitcoin_safe.wallet import Wallet + +logger = logging.getLogger(__name__) + + +import bdkpython as bdk +from bitcoin_qr_tools.multipath_descriptor import MultipathDescriptor +from PyQt6.QtCore import QEvent, Qt, pyqtSignal +from PyQt6.QtGui import QKeyEvent + +from ...pdfrecovery import make_and_open_pdf +from .util import Message, MessageType, icon_path + + +class DescriptorEdit(ButtonEdit): + signal_change = pyqtSignal(str) + + def __init__( + self, + network: bdk.Network, + signals_min: SignalsMin, + get_wallet: Optional[Callable[[], Wallet]] = None, + signal_update: pyqtSignal = None, + ) -> None: + super().__init__( + input_field=MyTextEdit(preferred_height=50), + button_vertical_align=Qt.AlignmentFlag.AlignBottom, + signal_update=signal_update, + ) + self.signals_min = signals_min + self.network = network + + def do_pdf() -> None: + if not get_wallet: + Message( + self.tr("Wallet setup not finished. Please finish before creating a Backup pdf."), + type=MessageType.Error, + ) + return + + make_and_open_pdf(get_wallet()) + + from bitcoin_qr_tools.data import Data + + def custom_handle_camera_input(data: Data, parent) -> None: + self.setText(str(data.data_as_string())) + self.signal_change.emit(str(data.data_as_string())) + + self.add_copy_button() + self.add_button(icon_path("qr-code.svg"), self.show_export_widget, tooltip="Show QR code") + if get_wallet is not None: + self.add_pdf_buttton(do_pdf) + self.add_qr_input_from_camera_button( + network=self.network, + custom_handle_input=custom_handle_camera_input, + ) + self.set_validator(self._check_if_valid) + + def show_export_widget(self): + if not self._check_if_valid(): + Message(self.tr("Descriptor not valid")) + return + data = Data( + MultipathDescriptor.from_descriptor_str(self.text(), self.network), DataType.MultiPathDescriptor + ) + widget = ExportDataSimple( + data=data, + signals_min=self.signals_min, + enable_clipboard=False, + enable_usb=False, + ) + widget.show() + + def _check_if_valid(self) -> bool: + if not self.text(): + return True + try: + MultipathDescriptor.from_descriptor_str(self.text(), self.network) + return True + except: + return False + + def keyReleaseEvent(self, e: QKeyEvent) -> None: + # print(e.type(), e.modifiers(), [key for key in Qt.Key if key.value == e.key() ] , e.matches(QKeySequence.StandardKey.Paste) ) + # If it's a regular key press + if e.type() == QEvent.Type.KeyRelease: + self.signal_change.emit(self.text()) + # If it's another type of shortcut, let the parent handle it + else: + super().keyReleaseEvent(e) diff --git a/bitcoin_safe/gui/qt/descriptor_ui.py b/bitcoin_safe/gui/qt/descriptor_ui.py index b63d490..2a95225 100644 --- a/bitcoin_safe/gui/qt/descriptor_ui.py +++ b/bitcoin_safe/gui/qt/descriptor_ui.py @@ -29,6 +29,7 @@ import logging +from bitcoin_safe.gui.qt.descriptor_edit import DescriptorEdit from bitcoin_safe.gui.qt.keystore_uis import KeyStoreUIs from bitcoin_safe.i18n import translate @@ -55,7 +56,6 @@ from ...signals import SignalsMin, pyqtSignal from ...wallet import ProtoWallet, Wallet from .block_change_signals import BlockChangesSignals -from .custom_edits import DescriptorEdit class DescriptorUI(QObject): @@ -103,7 +103,7 @@ def __init__( self.updateUi() signals_min.language_switch.connect(self.updateUi) - def updateUi(self): + def updateUi(self) -> None: self.label_signers.setText(self.tr("Required Signers")) self.label_gap.setText(self.tr("Scan Address Limit")) self.edit_descriptor.input_field.setPlaceholderText( @@ -119,11 +119,11 @@ def updateUi(self): self.label_address_type.setText(translate("descriptor", "Address Type")) self.groupBox_wallet_descriptor.setTitle(translate("descriptor", "Wallet Descriptor")) - def set_protowallet(self, protowallet: ProtoWallet): + def set_protowallet(self, protowallet: ProtoWallet) -> None: self.protowallet = protowallet self.set_all_ui_from_protowallet() - def on_wallet_ui_changes(self): + def on_wallet_ui_changes(self) -> None: logger.debug("on_wallet_ui_changes") try: self.set_protowallet_from_ui() @@ -135,7 +135,7 @@ def on_wallet_ui_changes(self): logger.warning("on_wallet_ui_changes: Invalid input") self._set_keystore_tabs() - def on_descriptor_change(self, new_value: str): + def on_descriptor_change(self, new_value: str) -> None: new_value = new_value.strip().replace("\n", "") # self.set_protowallet_from_keystore_ui(cloned_protowallet) @@ -155,20 +155,20 @@ def on_descriptor_change(self, new_value: str): logger.info(f"Invalid descriptor {new_value}") return - def on_spin_threshold_changed(self, new_value: int): + def on_spin_threshold_changed(self, new_value: int) -> None: self.on_wallet_ui_changes() - def on_spin_signer_changed(self, new_value: int): + def on_spin_signer_changed(self, new_value: int) -> None: self.repopulate_comboBox_address_type(new_value > 1) self.on_wallet_ui_changes() - def set_protowallet_from_descriptor_str(self, descriptor: str): + def set_protowallet_from_descriptor_str(self, descriptor: str) -> None: self.protowallet = ProtoWallet.from_descriptor( self.protowallet.id, descriptor, self.protowallet.network ) - def _set_keystore_tabs(self): + def _set_keystore_tabs(self) -> None: self.keystore_uis._set_keystore_tabs() self.spin_req.setMinimum(1) @@ -176,7 +176,7 @@ def _set_keystore_tabs(self): self.spin_signers.setMinimum(self.spin_req.value()) self.spin_signers.setMaximum(10) - def set_wallet_ui_from_protowallet(self): + def set_wallet_ui_from_protowallet(self) -> None: with BlockChangesSignals([self.tab]): logger.debug(f"{self.__class__.__name__} set_wallet_ui_from_protowallet") self.repopulate_comboBox_address_type(self.protowallet.is_multisig()) @@ -207,7 +207,7 @@ def set_wallet_ui_from_protowallet(self): self.spin_gap.setValue(self.protowallet.gap) - def set_all_ui_from_protowallet(self): + def set_all_ui_from_protowallet(self) -> None: """Updates the 3 parts. - wallet ui (e.g. gap) @@ -219,7 +219,7 @@ def set_all_ui_from_protowallet(self): self.set_wallet_ui_from_protowallet() self.set_ui_descriptor() - def set_protowallet_from_ui(self): + def set_protowallet_from_ui(self) -> None: logger.debug("set_protowallet_from_keystore_ui") # these wallet settings must come first @@ -231,7 +231,7 @@ def set_protowallet_from_ui(self): self.keystore_uis.set_protowallet_from_keystore_ui() - def set_combo_box_address_type_default(self): + def set_combo_box_address_type_default(self) -> None: address_types = get_address_types(self.protowallet.is_multisig()) self.comboBox_address_type.setCurrentIndex( address_types.index(get_default_address_type(self.protowallet.is_multisig())) @@ -258,7 +258,7 @@ def get_m_of_n_from_ui(self) -> Tuple[int, int]: def get_gap_from_ui(self) -> int: return self.spin_gap.value() - def set_ui_descriptor(self): + def set_ui_descriptor(self) -> None: logger.debug(f"{self.__class__.__name__} set_ui_descriptor") # check if the descriptor actually CAN be calculated to a reasonable degree try: @@ -270,7 +270,7 @@ def set_ui_descriptor(self): except: self.edit_descriptor.setText("") - def disable_fields(self): + def disable_fields(self) -> None: self.comboBox_address_type.setHidden(self.no_edit_mode) self.label_address_type.setHidden(self.no_edit_mode) self.spin_signers.setHidden(self.no_edit_mode) @@ -289,7 +289,7 @@ def disable_fields(self): self.label_of.setDisabled(True) self.spin_signers.setDisabled(True) - def repopulate_comboBox_address_type(self, is_multisig: bool): + def repopulate_comboBox_address_type(self, is_multisig: bool) -> None: with BlockChangesSignals([self.tab]): # Fetch the new address types address_types = get_address_types(is_multisig) @@ -312,7 +312,7 @@ def repopulate_comboBox_address_type(self, is_multisig: bool): if default_address_type in address_type_names: self.comboBox_address_type.setCurrentIndex(address_type_names.index(default_address_type)) - def create_wallet_type_and_descriptor(self): + def create_wallet_type_and_descriptor(self) -> None: box_wallet_type_and_descriptor = QWidget(self.tab) box_wallet_type_and_descriptor.setLayout(QHBoxLayout(box_wallet_type_and_descriptor)) @@ -425,7 +425,7 @@ def create_wallet_type_and_descriptor(self): self.spin_signers.valueChanged.connect(self.on_spin_signer_changed) self.spin_req.valueChanged.connect(self.on_spin_threshold_changed) - def create_button_bar(self): + def create_button_bar(self) -> QDialogButtonBox: # Create buttons and layout self.button_box = QDialogButtonBox( @@ -442,3 +442,4 @@ def create_button_bar(self): ) self.tab.layout().addWidget(self.button_box, 0, Qt.AlignmentFlag.AlignRight) + return self.button_box diff --git a/bitcoin_safe/gui/qt/dialog_import.py b/bitcoin_safe/gui/qt/dialog_import.py index eba8c69..8d11836 100644 --- a/bitcoin_safe/gui/qt/dialog_import.py +++ b/bitcoin_safe/gui/qt/dialog_import.py @@ -48,7 +48,7 @@ logger = logging.getLogger(__name__) -def is_binary(file_path): +def is_binary(file_path) -> bool: """Check if a file is binary or text. Returns True if binary, False if text. @@ -64,7 +64,7 @@ def is_binary(file_path): return False -def file_to_str(file_path): +def file_to_str(file_path) -> str: if is_binary(file_path): with open(file_path, "rb") as f: return bytes(f.read()).hex() @@ -80,13 +80,13 @@ def __init__( callback_enter=None, callback_esc=None, process_filepath: Optional[Callable[[str], None]] = None, - ): + ) -> None: super().__init__(parent) self.process_filepath = process_filepath self.callback_enter = callback_enter self.callback_esc = callback_esc - def keyPressEvent(self, event: QKeyEvent): + def keyPressEvent(self, event: QKeyEvent) -> None: if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter: if self.callback_enter: self.callback_enter(self.toPlainText()) @@ -95,13 +95,13 @@ def keyPressEvent(self, event: QKeyEvent): self.callback_esc() super().keyPressEvent(event) - def dragEnterEvent(self, event: QDragEnterEvent): + def dragEnterEvent(self, event: QDragEnterEvent) -> None: if event.mimeData().hasUrls(): event.accept() else: event.ignore() - def dropEvent(self, event: QDropEvent): + def dropEvent(self, event: QDropEvent) -> None: file_path = event.mimeData().urls()[0].toLocalFile() if self.process_filepath: self.process_filepath(file_path) @@ -117,7 +117,7 @@ def __init__( callback_enter=None, callback_esc=None, file_filter="All Files (*);;PSBT (*.psbt);;Transation (*.tx)", - ): + ) -> None: super().__init__( parent, input_field=DragAndDropTextEdit( @@ -134,7 +134,7 @@ def __init__( ) self.button_open_file = self.add_open_file_button(self.process_filepath, filter=file_filter) - def process_filepath(self, file_path: str): + def process_filepath(self, file_path: str) -> None: s = file_to_str(file_path) self.setText(s) self.signal_drop_file.emit(s) @@ -151,7 +151,7 @@ def __init__( text_instruction_label="Please paste your Bitcoin Transaction or PSBT in here, or drop a file", instruction_widget: Optional[QWidget] = None, text_placeholder="Paste your Bitcoin Transaction or PSBT in here or drop a file", - ): + ) -> None: super().__init__(parent) self.on_open = on_open @@ -191,11 +191,11 @@ def __init__( shortcut = QShortcut(QKeySequence("Return"), self) shortcut.activated.connect(self.process_input) - def keyPressEvent(self, event: QKeyEvent): + def keyPressEvent(self, event: QKeyEvent) -> None: if event.key() == Qt.Key.Key_Escape: self.close() - def process_input(self, s: str): + def process_input(self, s: str) -> None: if self.on_open: self.on_open(s) diff --git a/bitcoin_safe/gui/qt/dialogs.py b/bitcoin_safe/gui/qt/dialogs.py index e09572a..3dbea63 100644 --- a/bitcoin_safe/gui/qt/dialogs.py +++ b/bitcoin_safe/gui/qt/dialogs.py @@ -29,6 +29,7 @@ import logging import os +from typing import Optional from .util import create_button_box, read_QIcon @@ -49,7 +50,7 @@ def question_dialog( text="", title="", buttons=QMessageBox.StandardButton.Cancel | QMessageBox.StandardButton.Yes -): +) -> bool: msg_box = QMessageBox() msg_box.setWindowTitle(title) msg_box.setText(text) @@ -72,7 +73,7 @@ def question_dialog( class PasswordQuestion(QDialog): - def __init__(self, parent=None, label_text=None): + def __init__(self, parent=None, label_text=None) -> None: super(PasswordQuestion, self).__init__(parent) self.setWindowTitle(self.tr("Password Input")) @@ -92,7 +93,7 @@ def __init__(self, parent=None, label_text=None): self.submit_button.clicked.connect(self.accept) self.layout.addWidget(self.submit_button) - def ask_for_password(self): + def ask_for_password(self) -> Optional[str]: if self.exec() == QDialog.DialogCode.Accepted: return self.password_input.text() else: @@ -103,7 +104,7 @@ def ask_for_password(self): from PyQt6.QtGui import QFont, QIcon, QPainter, QPixmap -def create_icon_from_unicode(unicode_char, font_name="Arial", size=18): +def create_icon_from_unicode(unicode_char, font_name="Arial", size=18) -> QIcon: # Create a QPixmap object and set its size pixmap = QPixmap(32, 32) pixmap.fill(Qt.GlobalColor.transparent) @@ -119,7 +120,7 @@ def create_icon_from_unicode(unicode_char, font_name="Arial", size=18): class PasswordCreation(QDialog): - def __init__(self, parent=None, window_title=None, label_text=None): + def __init__(self, parent=None, window_title=None, label_text=None) -> None: super(PasswordCreation, self).__init__(parent) window_title = window_title if window_title else self.tr("Create Password") @@ -171,13 +172,13 @@ def __init__(self, parent=None, window_title=None, label_text=None): self.submit_button.clicked.connect(self.verify_password) self.layout.addWidget(self.submit_button) - def toggle_password_visibility(self): + def toggle_password_visibility(self) -> None: new_visibility = self.password_input1.echoMode() == QLineEdit.EchoMode.Password self._set_password_visibility(self.password_input1, self.show_password_action1, new_visibility) self._set_password_visibility(self.password_input2, self.show_password_action1, new_visibility) - def _set_password_visibility(self, password_input, show_password_action, visibility): + def _set_password_visibility(self, password_input, show_password_action, visibility) -> None: if visibility: password_input.setEchoMode(QLineEdit.EchoMode.Normal) show_password_action.setIcon(self.icon_hide) @@ -187,7 +188,7 @@ def _set_password_visibility(self, password_input, show_password_action, visibil show_password_action.setIcon(self.icon_show) show_password_action.setToolTip(self.tr("Show Password")) # Set tooltip to "Show Password" - def verify_password(self): + def verify_password(self) -> None: # Check if passwords are identical password1 = self.password_input1.text() password2 = self.password_input2.text() @@ -203,7 +204,7 @@ def verify_password(self): msg_box.setStandardButtons(QMessageBox.StandardButton.Ok) msg_box.exec() - def get_password(self): + def get_password(self) -> Optional[str]: if self.exec() == QDialog.DialogCode.Accepted: return self.password_input1.text() else: @@ -211,7 +212,7 @@ def get_password(self): class WalletIdDialog(QDialog): - def __init__(self, wallet_dir, parent=None, window_title=None, label_text=None, prefilled=None): + def __init__(self, wallet_dir, parent=None, window_title=None, label_text=None, prefilled=None) -> None: super().__init__(parent) self.wallet_dir = wallet_dir window_title = window_title if window_title else self.tr("Choose wallet name") @@ -235,7 +236,7 @@ def __init__(self, wallet_dir, parent=None, window_title=None, label_text=None, self.setLayout(layout) self.name_input.setFocus() - def check_wallet_existence(self): + def check_wallet_existence(self) -> None: chosen_wallet_id = self.name_input.text() wallet_file = os.path.join(self.wallet_dir, filename_clean(chosen_wallet_id)) diff --git a/bitcoin_safe/gui/qt/downloader.py b/bitcoin_safe/gui/qt/downloader.py index 5e5ad9a..09869f4 100644 --- a/bitcoin_safe/gui/qt/downloader.py +++ b/bitcoin_safe/gui/qt/downloader.py @@ -49,13 +49,13 @@ class DownloadThread(QThread): progress = pyqtSignal(int) finished = pyqtSignal() - def __init__(self, url, destination_dir): + def __init__(self, url, destination_dir) -> None: super().__init__() self.url = url self.destination_dir = Path(destination_dir) self.filename: Path = self.destination_dir / Path(url).name - def run(self): + def run(self) -> None: response = requests.get(self.url, stream=True, timeout=2) content_length = response.headers.get("content-length") @@ -75,14 +75,14 @@ def run(self): class Downloader(QWidget): finished = pyqtSignal(DownloadThread) - def __init__(self, url, destination_dir): + def __init__(self, url, destination_dir) -> None: super().__init__() self.url = url self.destination_dir = Path(destination_dir) self.filename = Path(url).name # Extract filename from URL self.initUI() - def initUI(self): + def initUI(self) -> None: self.setWindowTitle(self.tr("Download Progress")) self.layout = QVBoxLayout() @@ -109,7 +109,7 @@ def initUI(self): self.setLayout(self.layout) self.setGeometry(400, 400, 300, 100) - def startDownload(self): + def startDownload(self) -> None: self.startButton.hide() self.progress.show() self.thread = DownloadThread(self.url, str(self.destination_dir)) @@ -117,12 +117,12 @@ def startDownload(self): self.thread.finished.connect(self.downloadFinished) self.thread.start() - def downloadFinished(self): + def downloadFinished(self) -> None: self.progress.hide() self.showFileButton.show() self.finished.emit(self.thread) - def showFile(self): + def showFile(self) -> None: filename = self.thread.filename try: if platform.system() == "Windows": diff --git a/bitcoin_safe/gui/qt/expandable_widget.py b/bitcoin_safe/gui/qt/expandable_widget.py index ddd85a6..3269ce5 100644 --- a/bitcoin_safe/gui/qt/expandable_widget.py +++ b/bitcoin_safe/gui/qt/expandable_widget.py @@ -43,7 +43,7 @@ class CustomHeader(QWidget): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.setLayout(QHBoxLayout()) current_margins = self.layout().contentsMargins() @@ -60,7 +60,7 @@ def __init__(self, parent=None): class ExpandableWidget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setLayout(QVBoxLayout()) @@ -102,14 +102,14 @@ def __init__(self): self.layout().setSpacing(0) self.layout().setContentsMargins(0, 0, 0, 0) - def toggle(self): + def toggle(self) -> None: is_visible = self.expandableWidget.isVisible() self.expandableWidget.setVisible(not is_visible) # Change the arrow direction based on visibility arrow_type = Qt.ArrowType.LeftArrow if is_visible else Qt.ArrowType.DownArrow self.toggleButton.setArrowType(arrow_type) - def add_header_widget(self, widget: QWidget): + def add_header_widget(self, widget: QWidget) -> None: """Add custom widget to the header.""" # Clear any existing widgets in the layout, except the toggle button while self.header.layout().count() > 1: # Leave the toggle button @@ -120,7 +120,7 @@ def add_header_widget(self, widget: QWidget): # Add the new widget before the toggle button self.header.layout().insertWidget(0, widget, 1) - def add_content_widget(self, widget: QWidget): + def add_content_widget(self, widget: QWidget) -> None: """Add custom widget to the content area.""" # Clear any existing widgets in the layout (optional) while self.expandableWidget.layout().count(): diff --git a/bitcoin_safe/gui/qt/export_data.py b/bitcoin_safe/gui/qt/export_data.py index 6b8d5ed..148774a 100644 --- a/bitcoin_safe/gui/qt/export_data.py +++ b/bitcoin_safe/gui/qt/export_data.py @@ -28,17 +28,17 @@ import logging -from typing import Dict, Optional +from typing import Any, Callable, Dict, Literal, Optional -from bitcoin_qrreader.bitcoin_qr import Data, DataType +from bitcoin_nostr_chat.connected_devices.connected_devices import short_key +from bitcoin_nostr_chat.nostr import BitcoinDM, ChatLabel +from bitcoin_qr_tools.data import Data, DataType +from bitcoin_qr_tools.qr_widgets import QRCodeWidgetSVG from bitcoin_safe.gui.qt.keystore_ui import SignerUI -from bitcoin_safe.gui.qt.nostr_sync.connected_devices.connected_devices import short_key -from bitcoin_safe.gui.qt.nostr_sync.nostr import BitcoinDM, ChatLabel from bitcoin_safe.threading_manager import TaskThread from bitcoin_safe.tx import short_tx_id, transaction_to_dict -from .qr_components.image_widget import QRCodeWidgetSVG from .sync_tab import SyncTab logger = logging.getLogger(__name__) @@ -47,10 +47,12 @@ import os import bdkpython as bdk +from bitcoin_qr_tools.qr_generator import QRGenerator from nostr_sdk import PublicKey from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QAction, QIcon from PyQt6.QtWidgets import ( + QBoxLayout, QGroupBox, QHBoxLayout, QMenu, @@ -63,7 +65,6 @@ ) from ...signals import SignalsMin, pyqtSignal -from .qr_components.qr import create_qr_svg from .util import Message, MessageType, do_copy, read_QIcon, save_file_dialog @@ -72,7 +73,7 @@ def __init__(self, title: str = None, parent=None, data=None) -> None: super().__init__(title=title, parent=parent) self.data = data - def setData(self, data): + def setData(self, data) -> None: self.data = data @@ -81,17 +82,28 @@ class HorizontalImportExportGroups(QWidget): def __init__( self, + layout: QBoxLayout = None, + enable_qr=True, + enable_file=True, + enable_usb=True, + enable_clipboard=True, ) -> None: super().__init__() self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) - self.setLayout(QHBoxLayout()) + self.setLayout(layout if layout is not None else QHBoxLayout()) self.layout().setAlignment(Qt.AlignmentFlag.AlignVCenter) # qr self.group_qr = DataGroupBox("QR Code") - self.group_qr.setLayout(QVBoxLayout()) + self.group_qr.setLayout(QHBoxLayout()) self.group_qr.layout().setAlignment(Qt.AlignmentFlag.AlignCenter) - self.layout().addWidget(self.group_qr) + if enable_qr: + self.layout().addWidget(self.group_qr) + + self.group_qr_buttons = QWidget() + self.group_qr_buttons.setLayout(QVBoxLayout()) + self.group_qr_buttons.layout().setAlignment(Qt.AlignmentFlag.AlignCenter) + self.group_qr.layout().addWidget(self.group_qr_buttons) # one of the groupboxes i have to make expanding, otherwise nothing is expanding self.group_qr.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding) @@ -100,19 +112,22 @@ def __init__( self.group_file = DataGroupBox("File") self.group_file.setLayout(QVBoxLayout()) self.group_file.layout().setAlignment(Qt.AlignmentFlag.AlignCenter) - self.layout().addWidget(self.group_file) + if enable_file: + self.layout().addWidget(self.group_file) # usb self.group_usb = DataGroupBox("USB") self.group_usb.setLayout(QVBoxLayout()) self.group_usb.layout().setAlignment(Qt.AlignmentFlag.AlignCenter) - self.layout().addWidget(self.group_usb) + if enable_usb: + self.layout().addWidget(self.group_usb) # clipboard self.group_share = DataGroupBox("Share") self.group_share.setLayout(QVBoxLayout()) self.group_share.layout().setAlignment(Qt.AlignmentFlag.AlignCenter) - self.layout().addWidget(self.group_share) + if enable_clipboard: + self.layout().addWidget(self.group_share) # seed self.group_seed = DataGroupBox("Seed") @@ -132,35 +147,53 @@ def __init__( signals_min: SignalsMin, sync_tabs: dict[str, SyncTab] = None, usb_signer_ui: SignerUI = None, + layout: QBoxLayout = None, + enable_qr=True, + enable_file=True, + enable_usb=True, + enable_clipboard=True, ) -> None: - super().__init__() + super().__init__( + layout=layout, + enable_qr=enable_qr, + enable_file=enable_file, + enable_usb=enable_usb, + enable_clipboard=enable_clipboard, + ) self.sync_tabs = sync_tabs if sync_tabs else {} self.signals_min = signals_min self.txid = None self.json_data = None self.serialized = None + self.qr_type: Literal["ur", "bbqr"] = "bbqr" self.set_data(data) self.signal_export_to_file.connect(self.export_to_file) # qr self.qr_label = QRCodeWidgetSVG() + self.qr_label.setMinimumSize(20, 20) self.qr_label.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding) - self.group_qr.layout().addWidget(self.qr_label) + group_qr_layout: QHBoxLayout = self.group_qr.layout() + group_qr_layout.insertWidget(0, self.qr_label) self.lazy_load_qr(data) self.button_enlarge_qr = QPushButton() self.button_enlarge_qr.setIcon(read_QIcon("zoom.png")) # self.button_enlarge_qr.setIconSize(QSize(30, 30)) # 24x24 pixels self.button_enlarge_qr.clicked.connect(self.qr_label.enlarge_image) - self.group_qr.layout().addWidget(self.button_enlarge_qr) + self.group_qr_buttons.layout().addWidget(self.button_enlarge_qr) self.button_save_qr = QPushButton() self.button_save_qr.setIcon(read_QIcon("download.png")) self.button_save_qr.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_DialogSaveButton)) self.button_save_qr.clicked.connect(self.export_qrcode) - self.group_qr.layout().addWidget(self.button_save_qr) + self.group_qr_buttons.layout().addWidget(self.button_save_qr) + + self.button_switch_qr_type = QPushButton() + self.button_switch_qr_type.clicked.connect(self.switch_qr_type) + self.group_qr_buttons.layout().addWidget(self.button_switch_qr_type) # file self.button_file = QPushButton() @@ -182,10 +215,15 @@ def __init__( self.updateUi() self.signals_min.language_switch.connect(self.updateUi) + self.signal_set_qr_images.connect(self.qr_label.set_images) - def updateUi(self): + def updateUi(self) -> None: self.button_enlarge_qr.setText(self.tr("Enlarge")) self.button_save_qr.setText(self.tr("Save as image")) + if self.qr_type == "bbqr": + self.button_switch_qr_type.setText(self.tr("Show Legacy QR Code")) + else: + self.button_switch_qr_type.setText(self.tr("Show BBQR Code")) self.button_file.setText(self.tr("Export file")) # copy button @@ -202,7 +240,13 @@ def updateUi(self): for wallet_id, menu in self.menu_share_with_single_devices.items(): menu.setTitle(self.tr("Share with single device")) - def set_data(self, data: Data): + def switch_qr_type(self) -> None: + self.qr_type = "ur" if self.qr_type == "bbqr" else "bbqr" + self.clear_qr() + self.lazy_load_qr(self.data) + self.updateUi() + + def set_data(self, data: Data) -> None: self.data = data self.serialized = data.data_as_string() if data.data_type == DataType.PSBT: @@ -231,7 +275,7 @@ def create_copy_button(self) -> QWidget: self.copy_toolbutton.setIcon(QIcon(read_QIcon("clip.svg"))) # Create a menu for the button - def copy_if_available(s: Optional[str]): + def copy_if_available(s: Optional[str]) -> None: if s: do_copy(s) else: @@ -258,10 +302,10 @@ def create_sync_share_button(self) -> QWidget: self.button_sync_share.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.button_sync_share.setIcon(QIcon(read_QIcon("cloud-sync.svg"))) - def factory(wallet_id: str, sync_tab: SyncTab, receiver_public_key_bech32: str = None): + def factory(wallet_id: str, sync_tab: SyncTab, receiver_public_key_bech32: str = None) -> Callable: def f( wallet_id=wallet_id, sync_tab=sync_tab, receiver_public_key_bech32=receiver_public_key_bech32 - ): + ) -> None: if not sync_tab.enabled(): Message(self.tr("Please enable the sync tab first")) return @@ -294,7 +338,9 @@ def f( outer_widget.layout().addWidget(self.button_sync_share) return outer_widget - def on_nostr_share_with_member(self, receiver_public_key: PublicKey, wallet_id: str, sync_tab: SyncTab): + def on_nostr_share_with_member( + self, receiver_public_key: PublicKey, wallet_id: str, sync_tab: SyncTab + ) -> None: if not sync_tab.enabled(): Message( self.tr("Please enable syncing in the wallet {wallet_id} first").format(wallet_id=wallet_id) @@ -305,7 +351,7 @@ def on_nostr_share_with_member(self, receiver_public_key: PublicKey, wallet_id: receiver_public_key, ) - def on_nostr_share_in_group(self, wallet_id: str, sync_tab: SyncTab): + def on_nostr_share_in_group(self, wallet_id: str, sync_tab: SyncTab) -> None: if not sync_tab.enabled(): Message( self.tr("Please enable syncing in the wallet {wallet_id} first").format(wallet_id=wallet_id) @@ -317,35 +363,38 @@ def on_nostr_share_in_group(self, wallet_id: str, sync_tab: SyncTab): send_also_to_me=False, ) - def export_qrcode(self): + def export_qrcode(self) -> Optional[str]: filename = save_file_dialog( name_filters=["Image (*.png)", "All Files (*.*)"], default_suffix="png", default_filename=f"{short_tx_id( self.txid)}.png" if self.txid else None, ) if not filename: - return + return None # Ensure the file has the .png extension if not filename.lower().endswith(".png"): filename += ".png" self.qr_label.save_file(filename) + return filename - def lazy_load_qr(self, data: Data, max_length=200): - def do(): - self.signal_set_qr_images.connect(self.qr_label.set_images) - fragments = data.generate_fragments_for_qr(max_qr_size=max_length) - images = [create_qr_svg(fragment) for fragment in fragments] + def clear_qr(self) -> None: + self.qr_label.set_images([]) + + def lazy_load_qr(self, data: Data, max_length=200) -> None: + def do() -> Any: + fragments = data.generate_fragments_for_qr(max_qr_size=max_length, qr_type=self.qr_type) + images = [QRGenerator.create_qr_svg(fragment) for fragment in fragments] return images - def on_done(result): + def on_done(result) -> None: pass - def on_error(packed_error_info): + def on_error(packed_error_info) -> None: Message(packed_error_info, type=MessageType.Error) - def on_success(result): + def on_success(result) -> None: if result: # here i must use a signal, and not set the image directly, because # self.qr_label can reference a destroyed c++ object @@ -353,7 +402,7 @@ def on_success(result): TaskThread(self, signals_min=self.signals_min).add_and_start(do, on_success, on_done, on_error) - def export_to_file(self): + def export_to_file(self) -> Optional[str]: default_suffix = "txt" if self.data.data_type == DataType.Tx: default_suffix = "tx" @@ -369,9 +418,10 @@ def export_to_file(self): default_filename=f"{short_tx_id( self.txid)}.{default_suffix}" if self.txid else None, ) if not filename: - return + return None # create a file descriptor fd = os.open(filename, os.O_CREAT | os.O_WRONLY) self.data.write_to_filedescriptor(fd) + return filename diff --git a/bitcoin_safe/gui/qt/extended_tabwidget.py b/bitcoin_safe/gui/qt/extended_tabwidget.py index 562d883..35dbca7 100644 --- a/bitcoin_safe/gui/qt/extended_tabwidget.py +++ b/bitcoin_safe/gui/qt/extended_tabwidget.py @@ -47,7 +47,7 @@ class ExtendedTabWidget(DataTabWidget): signal_tab_bar_visibility = pyqtSignal(bool) - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.set_top_right_widget() @@ -56,7 +56,7 @@ def __init__(self, parent=None): self.tabCloseRequested.connect(self.updateLineEditPosition) self.currentChanged.connect(self.updateLineEditPosition) - def eventFilter(self, obj, event): + def eventFilter(self, obj, event) -> bool: if obj == self.tabBar() and event.type() == event.Type.Show: if self.top_right_widget: self.signal_tab_bar_visibility.emit(True) @@ -65,7 +65,7 @@ def eventFilter(self, obj, event): self.signal_tab_bar_visibility.emit(False) return super().eventFilter(obj, event) - def set_top_right_widget(self, top_right_widget: QWidget = None, target_width=150): + def set_top_right_widget(self, top_right_widget: QWidget = None, target_width=150) -> None: self.top_right_widget = top_right_widget self.target_width = target_width @@ -78,7 +78,7 @@ def tabInserted(self, index: int) -> None: super().tabInserted(index) self.updateLineEditPosition() - def updateLineEditPosition(self): + def updateLineEditPosition(self) -> None: tabBarRect = self.tabBar().geometry() availableWidth = self.width() @@ -95,13 +95,13 @@ def updateLineEditPosition(self): ) self.top_right_widget.setFixedWidth(line_width) # Ensure fixed width is maintained - def resizeEvent(self, event: QResizeEvent): + def resizeEvent(self, event: QResizeEvent) -> None: self.updateLineEditPosition() super().resizeEvent(event) class LoadingWalletTab(QWidget): - def __init__(self, tabs: QTabWidget, name: str, focus=True): + def __init__(self, tabs: QTabWidget, name: str, focus=True) -> None: super().__init__(tabs) self.tabs = tabs self.name = name @@ -124,7 +124,7 @@ def __init__(self, tabs: QTabWidget, name: str, focus=True): self.layout().addWidget(self.emptyLabel) self.layout().addItem(spacerBottom) - def __enter__(self): + def __enter__(self) -> None: add_tab_to_tabs( self.tabs, self, @@ -135,7 +135,7 @@ def __enter__(self): ) QApplication.processEvents() - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type, exc_value, traceback) -> None: remove_tab(self, self.tabs) diff --git a/bitcoin_safe/gui/qt/fee_group.py b/bitcoin_safe/gui/qt/fee_group.py index 4e6f8fb..dc1616a 100644 --- a/bitcoin_safe/gui/qt/fee_group.py +++ b/bitcoin_safe/gui/qt/fee_group.py @@ -161,15 +161,15 @@ def __init__( self.spin_label.setText(unit_fee_str(self.config.network)) self.widget_around_spin_box.layout().addWidget(self.spin_label) - self.label_block_number = QLabel() - self.label_block_number.setHidden(True) - self.groupBox_Fee.layout().addWidget(self.label_block_number, alignment=Qt.AlignmentFlag.AlignHCenter) - self.fiat_fee_label = QLabel() self.fiat_fee_label.setHidden(True) self.groupBox_Fee.layout().addWidget(self.fiat_fee_label, alignment=Qt.AlignmentFlag.AlignHCenter) self.fx.signal_data_updated.connect(self.updateUi) + self.label_block_number = QLabel() + self.label_block_number.setHidden(True) + self.groupBox_Fee.layout().addWidget(self.label_block_number, alignment=Qt.AlignmentFlag.AlignHCenter) + layout.addWidget(self.groupBox_Fee, alignment=Qt.AlignmentFlag.AlignHCenter) self.spin_fee_rate.valueChanged.connect(self.updateUi) @@ -177,7 +177,7 @@ def __init__( self.mempool.refresh() self.updateUi() - def updateUi(self): + def updateUi(self) -> None: self.groupBox_Fee.setTitle(self.tr("Fee")) self.rbf_fee_label.setText( html_f(self.tr("... is the minimum to replace the existing transactions."), bf=True) @@ -186,7 +186,8 @@ def updateUi(self): self.high_fee_warning_label.setText(html_f(self.tr("High fee"), color="red", bf=True)) self.approximate_fee_label.setText(html_f(self.tr("Approximate fee rate"), bf=True)) - self.label_block_number.setHidden(bool(self.mempool.confirmation_time)) + # only in editor mode + self.label_block_number.setHidden(self.spin_fee_rate.isReadOnly()) if self.spin_fee_rate.value(): self.label_block_number.setText( self.tr("in ~{n}. Block").format( @@ -197,12 +198,13 @@ def updateUi(self): self.set_fiat_fee_label() self.update_fee_rate_warning() + self.mempool.refresh() - def set_vsize(self, vsize): + def set_vsize(self, vsize) -> None: self.vsize = vsize self.updateUi() - def set_fiat_fee_label(self): + def set_fiat_fee_label(self) -> None: if not self.fx.rates.get("usd"): self.fiat_fee_label.setHidden(True) return @@ -213,7 +215,7 @@ def set_fiat_fee_label(self): self.fiat_fee_label.setText(format_dollar(self.fx.rates["usd"]["value"] / 1e8 * fee)) self.fiat_fee_label.setHidden(False) - def set_rbf_label(self, min_fee_rate: Optional[float]): + def set_rbf_label(self, min_fee_rate: Optional[float]) -> None: self.rbf_fee_label.setVisible(bool(min_fee_rate)) if min_fee_rate: self.rbf_fee_label.setText( @@ -229,7 +231,7 @@ def set_rbf_label(self, min_fee_rate: Optional[float]): def set_fee_to_send_ratio( self, fee: int, total_output_amount: int, network: bdk.Network, fee_is_exact=False - ): + ) -> None: if total_output_amount > 0: too_high = fee / total_output_amount > FEE_RATIO_HIGH_WARNING else: @@ -272,7 +274,7 @@ def set_fee_to_send_ratio( ) ) - def update_fee_rate_warning(self): + def update_fee_rate_warning(self) -> None: fee_rate = self.spin_fee_rate.value() too_high = fee_rate > self.mempool.mempool_data.max_reasonable_fee_rate() @@ -296,7 +298,7 @@ def set_fee_rate( url: str = None, confirmation_time: bdk.BlockTime = None, chain_height=None, - ): + ) -> None: self.spin_fee_rate.setHidden(fee_rate is None) self.spin_label.setHidden(fee_rate is None) @@ -313,11 +315,11 @@ def set_fee_rate( self.updateUi() self.signal_set_fee_rate.emit(fee_rate) - def _set_value(self, value: float): + def _set_value(self, value: float) -> None: self.update_spin_fee_range(value) self.spin_fee_rate.setValue(value) - def update_spin_fee_range(self, value: float = 0): + def update_spin_fee_range(self, value: float = 0) -> None: "Set the acceptable range" fee_range = self.config.fee_ranges[self.config.network].copy() fee_range[1] = max( diff --git a/bitcoin_safe/gui/qt/hist_list.py b/bitcoin_safe/gui/qt/hist_list.py index f7e9cc1..4d5147e 100644 --- a/bitcoin_safe/gui/qt/hist_list.py +++ b/bitcoin_safe/gui/qt/hist_list.py @@ -69,10 +69,10 @@ import enum import json from enum import IntEnum -from typing import Dict, Iterable, List, Optional, Set, Tuple +from typing import Any, Dict, Iterable, List, Optional, Set, Tuple import bdkpython as bdk -from bitcoin_qrreader.bitcoin_qr import Data, DataType +from bitcoin_qr_tools.data import Data, DataType from PyQt6.QtCore import ( QModelIndex, QPersistentModelIndex, @@ -195,7 +195,7 @@ def __init__( hidden_columns=None, column_widths: Optional[Dict[int, int]] = None, address_domain: List[str] = None, - ): + ) -> None: super().__init__( config=config, stretch_column=HistList.Columns.LABEL, @@ -243,11 +243,12 @@ def __init__( self.signals.category_updated.connect(self.update_with_filter) self.signals.language_switch.connect(self.update) - def get_file_data(self, txid: str): + def get_file_data(self, txid: str) -> Optional[Data]: for wallet in get_wallets(self.signals): txdetails = wallet.get_tx(txid) if txdetails: return Data(txdetails.transaction, DataType.Tx) + return None def drag_keys_to_file_paths( self, drag_keys: Iterable[str], save_directory: Optional[str] = None @@ -278,7 +279,7 @@ def drag_keys_to_file_paths( return file_urls - def dragEnterEvent(self, event: QDragEnterEvent): + def dragEnterEvent(self, event: QDragEnterEvent) -> None: if event.mimeData().hasFormat("application/json"): logger.debug("accept drag enter") event.acceptProposedAction() @@ -289,10 +290,10 @@ def dragEnterEvent(self, event: QDragEnterEvent): else: event.ignore() - def dragMoveEvent(self, event: QDragMoveEvent): + def dragMoveEvent(self, event: QDragMoveEvent) -> None: return self.dragEnterEvent(event) - def dropEvent(self, event: QDropEvent): + def dropEvent(self, event: QDropEvent) -> None: # handle dropped files super().dropEvent(event) if event.isAccepted(): @@ -328,27 +329,39 @@ def dropEvent(self, event: QDropEvent): event.ignore() - def on_double_click(self, idx: QModelIndex): + def on_double_click(self, idx: QModelIndex) -> None: txid = self.get_role_data_for_current_item(col=self.key_column, role=self.ROLE_KEY) wallet, tx_details = self._tx_dict[txid] self.signals.open_tx_like.emit(tx_details) - def toggle_change(self, state: int): + def toggle_change(self, state: int) -> None: if state == self.show_change: return self.show_change = AddressTypeFilter(state) self.update() - def toggle_used(self, state: int): + def toggle_used(self, state: int) -> None: if state == self.show_used: return self.show_used = AddressUsageStateFilter(state) self.update() - def update_with_filter(self, update_filter: UpdateFilter): + def update_with_filter(self, update_filter: UpdateFilter) -> None: if update_filter.refresh_all: return self.update() + def categories_intersect(model: MyStandardItemModel, row) -> Set: + return set(model.data(model.index(row, self.Columns.CATEGORIES))).intersection( + set(update_filter.categories) + ) + + def tx_involves_address(txid) -> Set: + (wallet, tx) = self._tx_dict[txid] + fulltxdetail = wallet.get_dict_fulltxdetail().get(txid) + if not fulltxdetail: + return set() + return update_filter.addresses.intersection(fulltxdetail.involved_addresses()) + logger.debug(f"{self.__class__.__name__} update_with_filter {update_filter}") log_info = [] @@ -356,15 +369,16 @@ def update_with_filter(self, update_filter: UpdateFilter): # Select rows with an ID in id_list for row in range(model.rowCount()): txid = model.data(model.index(row, self.Columns.TXID)) - if txid in update_filter.txids or set( - model.data(model.index(row, self.Columns.CATEGORIES)) - ).intersection(set(update_filter.categories)): + + if any( + [txid in update_filter.txids, categories_intersect(model, row), tx_involves_address(txid)] + ): log_info.append((row, txid)) self.refresh_row(txid, row) logger.debug(f"Updated {log_info}") - def get_headers(self): + def get_headers(self) -> Dict: return { self.Columns.WALLET_ID: self.tr("Wallet"), self.Columns.STATUS: self.tr("Status"), @@ -375,7 +389,7 @@ def get_headers(self): self.Columns.TXID: self.tr("Txid"), } - def update(self): + def update(self) -> None: if self.maybe_defer_update(): return @@ -481,7 +495,7 @@ def update(self): self.sortByColumn(HistList.Columns.STATUS, Qt.SortOrder.DescendingOrder) super().update() - def refresh_row(self, key: str, row: int): + def refresh_row(self, key: str, row: int) -> None: assert row is not None wallet, tx = self._tx_dict[key] # STATUS = enum.auto() @@ -522,7 +536,7 @@ def refresh_row(self, key: str, row: int): item[self.Columns.CATEGORIES].setData(categories, self.ROLE_CLIPBOARD_DATA) item[self.Columns.CATEGORIES].setBackground(CategoryEditor.color(category)) - def create_menu(self, position: QPoint): + def create_menu(self, position: QPoint) -> None: # is_multisig = isinstance(self.wallet, Multisig_Wallet) selected = self.selected_in_column(self.Columns.TXID) if not selected: @@ -597,7 +611,7 @@ def create_menu(self, position: QPoint): # run_hook('receive_menu', menu, txids, self.wallet) menu.exec(self.viewport().mapToGlobal(position)) - def edit_tx(self, tx_details: bdk.TransactionDetails): + def edit_tx(self, tx_details: bdk.TransactionDetails) -> None: txinfos = ToolsTxUiInfo.from_tx( tx_details.transaction, FeeInfo.from_txdetails(tx_details), @@ -607,7 +621,7 @@ def edit_tx(self, tx_details: bdk.TransactionDetails): self.signals.open_tx_like.emit(txinfos) - def cancel_tx(self, tx_details: bdk.TransactionDetails): + def cancel_tx(self, tx_details: bdk.TransactionDetails) -> None: txinfos = ToolsTxUiInfo.from_tx( tx_details.transaction, FeeInfo.from_txdetails(tx_details), @@ -639,7 +653,7 @@ def cancel_tx(self, tx_details: bdk.TransactionDetails): self.signals.open_tx_like.emit(txinfos) - def export_raw_transactions(self, selected_items: List[QStandardItem], folder: str = None): + def export_raw_transactions(self, selected_items: List[QStandardItem], folder: str = None) -> None: if not folder: folder = QFileDialog.getExistingDirectory(None, "Select Folder") if not folder: @@ -652,12 +666,12 @@ def export_raw_transactions(self, selected_items: List[QStandardItem], folder: s logger.info(f"Saved {len(file_paths)} {self.std_model.drag_key} saved to {folder}") - def get_edit_key_from_coordinate(self, row: int, col: int): + def get_edit_key_from_coordinate(self, row: int, col: int) -> Any: if col != self.Columns.LABEL: return None return self.get_role_data_from_coordinate(row, self.key_column, role=self.ROLE_KEY) - def on_edited(self, idx: QModelIndex, edit_key: str, *, text: str): + def on_edited(self, idx: QModelIndex, edit_key: str, *, text: str) -> None: txid = edit_key wallet, tx = self._tx_dict[txid] @@ -675,18 +689,18 @@ def on_edited(self, idx: QModelIndex, edit_key: str, *, text: str): class RefreshButton(QPushButton): - def __init__(self, parent=None, height=20): + def __init__(self, parent=None, height=20) -> None: super().__init__(parent) self.setText("") # Use the standard pixmap for the button icon self.setIconSize(QSize(height, height)) # Icon size can be adjusted as needed self.set_icon_allow_refresh() - def set_icon_allow_refresh(self): + def set_icon_allow_refresh(self) -> None: icon = self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload) self.setIcon(icon) - def set_icon_is_syncing(self): + def set_icon_is_syncing(self) -> None: icon = read_QIcon("status_waiting.png") self.setIcon(icon) @@ -704,14 +718,14 @@ def __init__(self, hist_list: HistList, config: UserConfig, parent: QWidget = No self.hist_list.signals.language_switch.connect(self.updateUi) self.hist_list.signals.utxos_updated.connect(self.updateUi) - def updateUi(self): + def updateUi(self) -> None: super().updateUi() if self.balance_label: balance = Satoshis(self.hist_list.balance, self.config.network) self.balance_label.setText(balance.format_as_balance()) # self.balance_label.setToolTip(balance.format_long(wallets[0].network)) - def create_toolbar_with_menu(self, title): + def create_toolbar_with_menu(self, title) -> None: super().create_toolbar_with_menu(title=title) font = QFont() @@ -719,7 +733,7 @@ def create_toolbar_with_menu(self, title): if self.balance_label: self.balance_label.setFont(font) - def on_hide_toolbar(self): + def on_hide_toolbar(self) -> None: self.show_change = AddressTypeFilter.ALL # type: AddressTypeFilter self.show_used = AddressUsageStateFilter.ALL # type: AddressUsageStateFilter self.update() diff --git a/bitcoin_safe/gui/qt/html_delegate.py b/bitcoin_safe/gui/qt/html_delegate.py index 3dff1d0..08e383e 100644 --- a/bitcoin_safe/gui/qt/html_delegate.py +++ b/bitcoin_safe/gui/qt/html_delegate.py @@ -40,7 +40,7 @@ class HTMLDelegate: def __init__(self) -> None: pass - def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex): + def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex) -> None: logger.debug("HTMLDelegate.paint") text = index.model().data(index) @@ -75,7 +75,7 @@ def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIn painter.restore() - def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex): + def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize: text = index.model().data(index) doc = QTextDocument() diff --git a/bitcoin_safe/gui/qt/invisible_scroll_area.py b/bitcoin_safe/gui/qt/invisible_scroll_area.py index a4b16b3..c378dc3 100644 --- a/bitcoin_safe/gui/qt/invisible_scroll_area.py +++ b/bitcoin_safe/gui/qt/invisible_scroll_area.py @@ -33,7 +33,7 @@ class InvisibleScrollArea(QScrollArea): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent=parent) self.unique_id = uuid.uuid4() diff --git a/bitcoin_safe/gui/qt/keystore_ui.py b/bitcoin_safe/gui/qt/keystore_ui.py index 13782cf..a9d7a6a 100644 --- a/bitcoin_safe/gui/qt/keystore_ui.py +++ b/bitcoin_safe/gui/qt/keystore_ui.py @@ -49,7 +49,13 @@ from typing import Callable, List import bdkpython as bdk -from bitcoin_qrreader import bitcoin_qr +from bitcoin_qr_tools.data import ( + Data, + DataType, + SignerInfo, + convert_slip132_to_bip32, + is_slip132, +) from bitcoin_usb.address_types import AddressType from bitcoin_usb.gui import USBGui from bitcoin_usb.software_signer import SoftwareSigner @@ -82,7 +88,7 @@ ) -def icon_for_label(label: str): +def icon_for_label(label: str) -> QIcon: return read_QIcon("key-gray.png") if label.startswith("Recovery") else read_QIcon("key.png") @@ -132,7 +138,7 @@ def __init__( custom_handle_input=self._on_handle_input, ) - def fingerprint_validator(): + def fingerprint_validator() -> bool: txt = self.edit_fingerprint.text() if not txt: return True @@ -164,7 +170,7 @@ def fingerprint_validator(): self.label_seed = QLabel() self.edit_seed = ButtonEdit() - def callback_seed(seed: str): + def callback_seed(seed: str) -> None: keystore = self.get_ui_values_as_keystore() self.edit_fingerprint.setText(keystore.fingerprint) self.edit_xpub.setText(keystore.xpub) @@ -172,7 +178,7 @@ def callback_seed(seed: str): self.edit_seed.add_random_mnemonic_button(callback_seed=callback_seed) - def seed_validator(): + def seed_validator() -> bool: if not self.edit_seed.text(): return True return KeyStore.is_seed_valid(self.edit_seed.text()) @@ -206,8 +212,8 @@ def seed_validator(): self.button_qr.clicked.connect(lambda: self.edit_xpub.buttons[0].click()) self.button_hwi.clicked.connect(lambda: self.on_hwi_click()) - def process_input(s: str): - res = bitcoin_qr.Data.from_str(s, self.network) + def process_input(s: str) -> None: + res = Data.from_str(s, self.network) self._on_handle_input(res) self.button_file.clicked.connect( @@ -259,20 +265,20 @@ def process_input(s: str): ) @property - def label(self): + def label(self) -> str: return self.keystore.label if self.keystore else self._label @label.setter - def label(self, value: str): + def label(self, value: str) -> None: if self.keystore: self.keystore.label = value else: self._label = value - def remove_tab(self): + def remove_tab(self) -> None: self.tabs.removeTab(self.tabs.indexOf(self.tab)) - def seed_visibility(self, visible=False): + def seed_visibility(self, visible=False) -> None: self.edit_seed.setHidden(not visible) self.label_seed.setHidden(not visible) @@ -282,7 +288,7 @@ def seed_visibility(self, visible=False): # self.label_xpub.setHidden(visible) # self.label_fingerprint.setHidden(visible) - def on_label_change(self): + def on_label_change(self) -> None: self.tabs.setTabText(self.tabs.indexOf(self.tab), self.edit_label.text()) @property @@ -295,10 +301,10 @@ def key_origin(self) -> str: return standardized @key_origin.setter - def key_origin(self, value: str): + def key_origin(self, value: str) -> None: self.edit_key_origin.setText(value if value else "") - def format_all_fields(self): + def format_all_fields(self) -> None: self.edit_fingerprint.format() expected_key_origin = self.get_expected_key_origin() @@ -324,7 +330,7 @@ def format_all_fields(self): self.edit_key_origin.input_field.reset_memory() self.edit_key_origin.input_field.add_to_memory(expected_key_origin) - def successful_import_signer_info(self): + def successful_import_signer_info(self) -> None: this_index = self.tabs.indexOf(self.tab) self.tabs_import_type.setCurrentWidget(self.tab_manual) @@ -336,8 +342,8 @@ def successful_import_signer_info(self): def get_expected_key_origin(self) -> str: return self.get_address_type().key_origin(self.network) - def set_using_signer_info(self, signer_info: bitcoin_qr.SignerInfo): - def check_key_origin(signer_info: bitcoin_qr.SignerInfo): + def set_using_signer_info(self, signer_info: SignerInfo) -> None: + def check_key_origin(signer_info: SignerInfo) -> bool: expected_key_origin = self.get_expected_key_origin() if signer_info.key_origin != expected_key_origin: Message( @@ -356,11 +362,11 @@ def check_key_origin(signer_info: bitcoin_qr.SignerInfo): self.edit_fingerprint.setText(signer_info.fingerprint) self.successful_import_signer_info() - def _on_handle_input(self, data: bitcoin_qr.Data, parent: QWidget = None): + def _on_handle_input(self, data: Data, parent: QWidget = None) -> None: - if data.data_type == bitcoin_qr.DataType.SignerInfo: + if data.data_type == DataType.SignerInfo: self.set_using_signer_info(data.data) - elif data.data_type == bitcoin_qr.DataType.SignerInfos: + elif data.data_type == DataType.SignerInfos: expected_key_origin = self.get_expected_key_origin() # pick the right signer data for signer_info in data.data: @@ -375,18 +381,18 @@ def _on_handle_input(self, data: bitcoin_qr.Data, parent: QWidget = None): ) ) - elif data.data_type == bitcoin_qr.DataType.Xpub: + elif data.data_type == DataType.Xpub: self.edit_xpub.setText(data.data) - elif data.data_type == bitcoin_qr.DataType.Fingerprint: + elif data.data_type == DataType.Fingerprint: self.edit_fingerprint.setText(data.data) elif data.data_type in [ - bitcoin_qr.DataType.Descriptor, - bitcoin_qr.DataType.MultiPathDescriptor, + DataType.Descriptor, + DataType.MultiPathDescriptor, ]: Message(self.tr("Please paste descriptors into the descriptor field in the top right.")) elif isinstance(data.data, str) and parent: parent.setText(data.data) - elif isinstance(data, bitcoin_qr.Data): + elif isinstance(data, Data): Message( self.tr("{data_type} cannot be used here.").format(data_type=data.data_type), type=MessageType.Error, @@ -394,22 +400,22 @@ def _on_handle_input(self, data: bitcoin_qr.Data, parent: QWidget = None): else: Exception("Could not recognize the QR Code") - def xpub_validator(self): + def xpub_validator(self) -> bool: xpub = self.edit_xpub.text() # automatically convert slip132 - if bitcoin_qr.is_slip132(xpub): + if is_slip132(xpub): Message( self.tr("The xpub is in SLIP132 format. Converting to standard format."), title="Converting format", ) try: - self.edit_xpub.setText(bitcoin_qr.convert_slip132_to_bip32(xpub)) + self.edit_xpub.setText(convert_slip132_to_bip32(xpub)) except: return False return KeyStore.is_xpub_valid(self.edit_xpub.text(), network=self.network) - def updateUi(self): + def updateUi(self) -> None: self.tabs.setTabText( self.tabs.indexOf(self.tab), self.label, @@ -434,7 +440,7 @@ def updateUi(self): self.button_qr.setText(self.tr("Scan")) self.button_hwi.setText(self.tr("Connect USB")) - def on_hwi_click(self): + def on_hwi_click(self) -> None: address_type = self.get_address_type() usb = USBGui(self.network) key_origin = address_type.key_origin(self.network) @@ -443,9 +449,7 @@ def on_hwi_click(self): return fingerprint, xpub = result - self.set_using_signer_info( - bitcoin_qr.SignerInfo(fingerprint=fingerprint, key_origin=key_origin, xpub=xpub) - ) + self.set_using_signer_info(SignerInfo(fingerprint=fingerprint, key_origin=key_origin, xpub=xpub)) def get_ui_values_as_keystore(self) -> KeyStore: seed_str = self.edit_seed.text().strip() @@ -482,7 +486,7 @@ def get_ui_values_as_keystore(self) -> KeyStore: network=self.network, ) - def set_ui_from_keystore(self, keystore: KeyStore): + def set_ui_from_keystore(self, keystore: KeyStore) -> None: with BlockChangesSignals([self.tab]): logger.debug(f"{self.__class__.__name__} set_ui_from_keystore") self.edit_xpub.setText(keystore.xpub if keystore.xpub else "") @@ -533,8 +537,8 @@ def __init__( for signer in self.signature_importers: - def callback_generator(signer: AbstractSignatureImporter): - def f(): + def callback_generator(signer: AbstractSignatureImporter) -> Callable: + def f() -> None: signer.sign(self.psbt) return f @@ -569,8 +573,8 @@ def __init__( for signer in self.signature_importers: - def callback_generator(signer: AbstractSignatureImporter): - def f(): + def callback_generator(signer: AbstractSignatureImporter) -> Callable: + def f() -> None: signer.sign(self.psbt) return f diff --git a/bitcoin_safe/gui/qt/keystore_uis.py b/bitcoin_safe/gui/qt/keystore_uis.py index 8bf87d1..1daf7f4 100644 --- a/bitcoin_safe/gui/qt/keystore_uis.py +++ b/bitcoin_safe/gui/qt/keystore_uis.py @@ -79,7 +79,7 @@ def __init__( self.signals_min.language_switch.connect(self.updateUi) - def updateUi(self): + def updateUi(self) -> None: # udpate the label for where the keystore exists for i, keystore in enumerate(self.protowallet.keystores): if not keystore: @@ -92,7 +92,7 @@ def updateUi(self): def protowallet(self) -> ProtoWallet: return self.get_editable_protowallet() - def ui_keystore_ui_change(self, *args): + def ui_keystore_ui_change(self, *args) -> None: logger.debug("ui_keystore_ui_change") try: self.set_protowallet_from_keystore_ui() @@ -100,7 +100,7 @@ def ui_keystore_ui_change(self, *args): except: logger.warning("ui_keystore_ui_change: Invalid input") - def set_protowallet_from_keystore_ui(self): + def set_protowallet_from_keystore_ui(self) -> None: # and last are the keystore uis, which can cause exceptions, because the UI is not filled correctly for i, keystore_ui in enumerate(self.keystore_uis): @@ -119,7 +119,7 @@ def set_protowallet_from_keystore_ui(self): continue keystore.label = self.protowallet.signer_names(self.protowallet.threshold, i) - def _set_keystore_tabs(self): + def _set_keystore_tabs(self) -> None: # add keystore_ui if necessary if len(self.keystore_uis) < len(self.protowallet.keystores): for i in range(len(self.keystore_uis), len(self.protowallet.keystores)): @@ -159,7 +159,7 @@ def _set_keystore_tabs(self): self.setTabText(index, keystore_ui.label) self.setTabIcon(index, icon_for_label(keystore_ui.label)) - def set_keystore_ui_from_protowallet(self): + def set_keystore_ui_from_protowallet(self) -> None: logger.debug(f"set_keystore_ui_from_protowallet") self._set_keystore_tabs() for keystore, keystore_ui in zip(self.protowallet.keystores, self.keystore_uis): diff --git a/bitcoin_safe/gui/qt/label_syncer.py b/bitcoin_safe/gui/qt/label_syncer.py index 3ed5a73..10469c4 100644 --- a/bitcoin_safe/gui/qt/label_syncer.py +++ b/bitcoin_safe/gui/qt/label_syncer.py @@ -31,20 +31,18 @@ from collections import deque from typing import List +from bitcoin_nostr_chat.connected_devices.connected_devices import TrustedDevice +from bitcoin_nostr_chat.nostr import BitcoinDM, ChatLabel from nostr_sdk import PublicKey -from bitcoin_safe.gui.qt.nostr_sync.connected_devices.connected_devices import ( - TrustedDevice, -) -from bitcoin_safe.gui.qt.nostr_sync.nostr import BitcoinDM, ChatLabel from bitcoin_safe.gui.qt.sync_tab import SyncTab logger = logging.getLogger(__name__) +from bitcoin_nostr_chat.nostr_sync import Data, DataType + from bitcoin_safe.labels import Labels, LabelType from bitcoin_safe.signals import Signals, UpdateFilter -from .nostr_sync.nostr_sync import Data, DataType - class LabelSyncer: def __init__(self, labels: Labels, sync_tab: SyncTab, signals: Signals) -> None: @@ -61,7 +59,7 @@ def __init__(self, labels: Labels, sync_tab: SyncTab, signals: Signals) -> None: # store sent UpdateFilters to prevent recursive behavior self.sent_update_filter: deque = deque(maxlen=1000) - def on_add_trusted_device(self, trusted_device: TrustedDevice): + def on_add_trusted_device(self, trusted_device: TrustedDevice) -> None: if not self.sync_tab.enabled(): return logger.debug(f"on_add_trusted_device") @@ -76,13 +74,15 @@ def on_add_trusted_device(self, trusted_device: TrustedDevice): ) logger.debug(f"sent all labels to {trusted_device.pub_key_bech32}") - def on_nostr_label_bip329_received(self, data: Data): + def on_nostr_label_bip329_received(self, data: Data) -> None: if not self.sync_tab.enabled(): return logger.info(f"on_nostr_label_bip329_received {data}") if data.data_type == DataType.LabelsBip329: changed_labels = self.labels.import_dumps_data(data.data) + if not changed_labels: + return logger.debug(f"on_nostr_label_bip329_received updated: {changed_labels} ") addresses: List[str] = [] @@ -109,7 +109,7 @@ def on_nostr_label_bip329_received(self, data: Data): # the category editor maybe also needs to add categories self.signals.category_updated.emit(update_filter) - def on_labels_updated(self, update_filter: UpdateFilter): + def on_labels_updated(self, update_filter: UpdateFilter) -> None: if not self.sync_tab.enabled(): return if update_filter in self.sent_update_filter: diff --git a/bitcoin_safe/gui/qt/language_chooser.py b/bitcoin_safe/gui/qt/language_chooser.py index aa2a0d3..9593cf5 100644 --- a/bitcoin_safe/gui/qt/language_chooser.py +++ b/bitcoin_safe/gui/qt/language_chooser.py @@ -52,7 +52,7 @@ class LanguageDialog(QDialog): - def __init__(self, languages: Dict[str, str], parent=None): + def __init__(self, languages: Dict[str, str], parent=None) -> None: super().__init__(parent) self.setWindowTitle("Select Language") self.setLayout(QVBoxLayout()) @@ -71,18 +71,18 @@ def __init__(self, languages: Dict[str, str], parent=None): self.setWindowIcon(read_QIcon("logo.svg")) self.centerOnScreen() - def centerOnScreen(self): + def centerOnScreen(self) -> None: screen = QApplication.primaryScreen().geometry() dialog_size = self.geometry() x = (screen.width() - dialog_size.width()) // 2 y = (screen.height() - dialog_size.height()) // 2 self.move(x, y) - def setupComboBox(self, languages: Dict[str, str]): + def setupComboBox(self, languages: Dict[str, str]) -> None: for lang, name in languages.items(): self.comboBox.addItem(name, lang) - def choose_language(self): + def choose_language(self) -> Optional[str]: if self.exec() == QDialog.DialogCode.Accepted: return self.comboBox.currentData() else: @@ -100,23 +100,23 @@ def __init__(self, parent: QWidget, config: UserConfig, signal_language_switch: self.availableLanguages = {"en_US": QLocale(QLocale.Language.English).nativeLanguageName()} logger.debug(f"initialized {self}") - def default_lang(self): + def default_lang(self) -> str: return list(self.availableLanguages.keys())[0] def dialog_choose_language(self, parent) -> str: logger.debug(f"dialog_choose_language") dialog = LanguageDialog(self.get_languages(), parent) lang = dialog.choose_language() - if not lang: - lang = self.default_lang() - return lang + if lang: + return lang + return self.default_lang() def get_languages(self) -> Dict[str, str]: # Scan for other languages and add them to the list self.availableLanguages.update(self.scanForLanguages()) return self.availableLanguages - def populate_language_menu(self, language_menu: QMenu): + def populate_language_menu(self, language_menu: QMenu) -> None: language_menu.clear() # Menu Bar for language selection @@ -146,13 +146,13 @@ def scanForLanguages(self) -> Dict[str, str]: languages[langCode] = langName return languages - def _install_translator(self, name: str, path: str): + def _install_translator(self, name: str, path: str) -> None: translator_qt = QTranslator() if translator_qt.load(name, path): QApplication.instance().installTranslator(translator_qt) self.installed_translators.append(translator_qt) - def set_language(self, langCode: Optional[str]): + def set_language(self, langCode: Optional[str]) -> None: # remove all installed translators while self.installed_translators: QApplication.instance().removeTranslator(self.installed_translators.pop()) @@ -174,7 +174,7 @@ def set_language(self, langCode: Optional[str]): self._install_translator(f"app_{langCode}", str(self.config.locales_path)) - def switchLanguage(self, langCode): + def switchLanguage(self, langCode) -> None: self.set_language(langCode) self.signal_language_switch.emit() # Emit the signal when the language is switched self.config.language_code = langCode diff --git a/bitcoin_safe/gui/qt/main.py b/bitcoin_safe/gui/qt/main.py index f939561..970cc6f 100644 --- a/bitcoin_safe/gui/qt/main.py +++ b/bitcoin_safe/gui/qt/main.py @@ -50,8 +50,8 @@ from typing import Deque, Dict, List, Literal, Optional, Tuple, Union import bdkpython as bdk -from bitcoin_qrreader import bitcoin_qr, bitcoin_qr_gui -from bitcoin_qrreader.bitcoin_qr import Data, DataType +from bitcoin_qr_tools.bitcoin_video_widget import BitcoinVideoWidget +from bitcoin_qr_tools.data import Data, DataType from PyQt6.QtCore import QCoreApplication, QProcess from PyQt6.QtGui import QAction, QCloseEvent, QIcon, QKeySequence, QShortcut from PyQt6.QtWidgets import ( @@ -106,7 +106,7 @@ def __init__( network: Literal["bitcoin", "regtest", "signet", "testnet"] = None, config: UserConfig = None, **kwargs, - ): + ) -> None: "If netowrk == None, then the network from the user config will be taken" super().__init__() config_present = UserConfig.exists() or config @@ -177,7 +177,7 @@ def __init__( delayed_execution(self.load_last_state, self) - def load_last_state(self): + def load_last_state(self) -> None: opened_qt_wallets = self.open_last_opened_wallets() if not opened_qt_wallets: @@ -185,7 +185,7 @@ def load_last_state(self): self.open_last_opened_tx() - def set_title(self): + def set_title(self) -> None: title = "Bitcoin Safe" if self.config.network != bdk.Network.BITCOIN: title += f" - {self.config.network.name}" @@ -193,7 +193,7 @@ def set_title(self): title += f" - {qt_wallet.wallet.id}" self.setWindowTitle(title) - def setupUi(self, MainWindow: QWidget): + def setupUi(self, MainWindow: QWidget) -> None: logger.debug(f"start setupUi") # sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) # sizePolicy.setHorizontalStretch(0) @@ -248,7 +248,7 @@ def setupUi(self, MainWindow: QWidget): self.set_title() logger.debug(f"done setupUi") - def init_menubar(self): + def init_menubar(self) -> None: self.menubar = QMenuBar() # menu wallet self.menu_wallet = self.menubar.addMenu("") @@ -256,7 +256,7 @@ def init_menubar(self): self.menu_action_open_wallet = self.menu_wallet.addAction("", self.open_wallet) self.menu_action_open_wallet.setShortcut(QKeySequence("CTRL+O")) self.menu_wallet_recent = self.menu_wallet.addMenu("") - self.menu_action_save_current_wallet = self.menu_wallet.addAction("", self.save_current_wallet) + self.menu_action_save_current_wallet = self.menu_wallet.addAction("", self.save_qt_wallet) self.menu_action_save_current_wallet.setShortcut(QKeySequence("CTRL+S")) self.menu_wallet.addSeparator() @@ -333,11 +333,11 @@ def init_menubar(self): # # Add QWidgetAction to the menu # self.search_menu.addAction(widgetAction) - def showEvent(self, event): + def showEvent(self, event) -> None: super().showEvent(event) # self.updateUI() - def updateUI(self): + def updateUI(self) -> None: # menu self.menu_wallet.setTitle(self.tr("&Wallet")) @@ -379,18 +379,20 @@ def updateUI(self): if qt_wallet.tabs.top_right_widget: qt_wallet.tabs.top_right_widget.setVisible(main_search_field_hidden) - def populate_recent_wallets_menu(self): + def populate_recent_wallets_menu(self) -> None: self.menu_wallet_recent.clear() for filepath in reversed(self.config.recently_open_wallets[self.config.network]): + if not Path(filepath).exists(): + continue self.menu_wallet_recent.addAction( os.path.basename(filepath), lambda filepath=filepath: self.open_wallet(file_path=filepath) ) - def change_wallet_id(self): + def change_wallet_id(self) -> Optional[str]: qt_wallet = self.get_qt_wallet() if not qt_wallet: Message(self.tr("Please select the wallet")) - return + return None old_id = qt_wallet.wallet.id @@ -418,11 +420,12 @@ def change_wallet_id(self): new_file_path = os.path.join(directory, filename_clean(new_wallet_id)) qt_wallet.move_wallet_file(new_file_path) - qt_wallet.save() + self.save_qt_wallet(qt_wallet) logger.info(f"Saved {old_filepath} under new name {qt_wallet.file_path}") self.set_title() + return new_wallet_id - def change_wallet_password(self): + def change_wallet_password(self) -> None: qt_wallet = self.get_qt_wallet() if not qt_wallet: Message(self.tr("Please select the wallet")) @@ -430,7 +433,7 @@ def change_wallet_password(self): qt_wallet.change_password() - def on_signal_broadcast_tx(self, transaction: bdk.Transaction): + def on_signal_broadcast_tx(self, transaction: bdk.Transaction) -> None: last_qt_wallet_involved: Optional[QTWallet] = None for qt_wallet in self.qt_wallets.values(): if qt_wallet.wallet.transaction_involves_wallet(transaction): @@ -441,12 +444,12 @@ def on_signal_broadcast_tx(self, transaction: bdk.Transaction): self.tab_wallets.setCurrentWidget(last_qt_wallet_involved.tab) last_qt_wallet_involved.tabs.setCurrentWidget(last_qt_wallet_involved.history_tab) - def on_tab_changed(self, index: int): + def on_tab_changed(self, index: int) -> None: qt_wallet = self.get_qt_wallet(self.tab_wallets.widget(index)) if qt_wallet: self.last_qtwallet = qt_wallet - def _init_tray(self): + def _init_tray(self) -> None: self.tray = QSystemTrayIcon(read_QIcon("logo.svg"), self) self.tray.setToolTip("Bitcoin Safe") @@ -460,23 +463,23 @@ def _init_tray(self): self.signals.notification.connect(self.show_message_as_tray_notification) self.tray.show() - def show_message_as_tray_notification(self, message: Message): + def show_message_as_tray_notification(self, message: Message) -> None: icon, _ = message.get_icon_and_title() title = message.title or "Bitcoin Safe" if message.msecs: return self.tray.showMessage( title, message.msg, Message.icon_to_q_system_tray_icon(icon), message.msecs ) - return self.tray.showMessage(title, message.msg, Message.icon_to_q_system_tray_icon(icon)) + self.tray.showMessage(title, message.msg, Message.icon_to_q_system_tray_icon(icon)) - def onTrayIconActivated(self, reason: QSystemTrayIcon.ActivationReason): + def onTrayIconActivated(self, reason: QSystemTrayIcon.ActivationReason) -> None: if reason == QSystemTrayIcon.ActivationReason.Trigger: Message(self.tr("test"), no_show=True).emit_with(self.signals.notification) - def open_network_settings(self): + def open_network_settings(self) -> None: self.network_settings_ui.exec() - def export_wallet_for_coldcard(self, wallet: Wallet = None): + def export_wallet_for_coldcard(self, wallet: Wallet = None) -> None: qt_wallet = self.get_qt_wallet(if_none_serve_last_active=True) if not qt_wallet or not qt_wallet.wallet: Message(self.tr("Please select the wallet first."), type=MessageType.Warning) @@ -484,7 +487,7 @@ def export_wallet_for_coldcard(self, wallet: Wallet = None): qt_wallet.export_wallet_for_coldcard() - def open_tx_file(self, file_path: Optional[str] = None): + def open_tx_file(self, file_path: Optional[str] = None) -> None: if not file_path: file_path, _ = QFileDialog.getOpenFileName( self, @@ -520,15 +523,17 @@ def open_tx_like_in_tab( bytes, str, ], - ): + ) -> None: logger.debug(f"Trying to open tx with type {type(txlike)}") # first do the bdk instance cases if isinstance(txlike, (bdk.TransactionDetails, bdk.Transaction)): - return self.open_tx_in_tab(txlike) + self.open_tx_in_tab(txlike) + return None if isinstance(txlike, (bdk.PartiallySignedTransaction, TxBuilderInfos)): - return self.open_psbt_in_tab(txlike) + self.open_psbt_in_tab(txlike) + return None if isinstance(txlike, TxUiInfos): wallet = ToolsTxUiInfo.get_likely_source_wallet(txlike, self.signals) @@ -541,18 +546,19 @@ def open_tx_like_in_tab( wallet = current_qt_wallet.wallet if current_qt_wallet else None if not wallet: Message(self.tr("No wallet open. Please open the sender wallet to edit this thransaction.")) - return + return None qt_wallet = self.qt_wallets.get(wallet.id) if not qt_wallet: Message(self.tr(" Please open the sender wallet to edit this thransaction.")) - return + return None self.tab_wallets.setCurrentWidget(qt_wallet.tab) qt_wallet.tabs.setCurrentWidget(qt_wallet.send_tab) ToolsTxUiInfo.pop_change_recipient(txlike, wallet) - return qt_wallet.uitx_creator.set_ui(txlike) + qt_wallet.uitx_creator.set_ui(txlike) + return None # try to convert a bytes like object to a string if isinstance(txlike, bytes): @@ -565,36 +571,39 @@ def open_tx_like_in_tab( txlike = str(txlike) if isinstance(txlike, str): - res = bitcoin_qr.Data.from_str(txlike, self.config.network) - if res.data_type == bitcoin_qr.DataType.Txid: + res = Data.from_str(txlike, self.config.network) + if res.data_type == DataType.Txid: txdetails = self.fetch_txdetails(res.data) if txdetails: - return self.open_tx_in_tab(txdetails) + self.open_tx_in_tab(txdetails) + return None if not txlike: raise Exception(f"txid {res.data} could not be found in wallets") - elif res.data_type == bitcoin_qr.DataType.PSBT: - return self.open_psbt_in_tab(res.data) - elif res.data_type == bitcoin_qr.DataType.Tx: - return self.open_tx_in_tab(res.data) + elif res.data_type == DataType.PSBT: + self.open_psbt_in_tab(res.data) + return None + elif res.data_type == DataType.Tx: + self.open_tx_in_tab(res.data) + return None else: logger.warning(f"DataType {res.data_type.name} was not handled.") + return None - def load_tx_like_from_qr(self): - def result_callback(data: bitcoin_qr.Data): + def load_tx_like_from_qr(self) -> None: + def result_callback(data: Data) -> None: if data.data_type in [ - bitcoin_qr.DataType.PSBT, - bitcoin_qr.DataType.Tx, - bitcoin_qr.DataType.Txid, + DataType.PSBT, + DataType.Tx, + DataType.Txid, ]: self.open_tx_like_in_tab(data.data) - window = bitcoin_qr_gui.BitcoinVideoWidget( - result_callback=result_callback, network=self.config.network - ) + window = BitcoinVideoWidget(result_callback=result_callback, network=self.config.network) window.show() + return None - def dialog_open_tx_from_str(self): - def process_input(s: str): + def dialog_open_tx_from_str(self) -> None: + def process_input(s: str) -> None: self.open_tx_like_in_tab(s) tx_dialog = ImportDialog( @@ -609,7 +618,9 @@ def process_input(s: str): ) tx_dialog.show() - def open_tx_in_tab(self, txlike: Union[bdk.Transaction, bdk.TransactionDetails]): + def open_tx_in_tab( + self, txlike: Union[bdk.Transaction, bdk.TransactionDetails] + ) -> Tuple[UITx_ViewerTab, UITx_Viewer]: tx: bdk.Transaction = None fee = None confirmation_time = None @@ -630,7 +641,7 @@ def open_tx_in_tab(self, txlike: Union[bdk.Transaction, bdk.TransactionDetails]) elif isinstance(txlike, bdk.Transaction): tx = txlike - def get_outpoints(): + def get_outpoints() -> List[OutPoint]: return [OutPoint.from_bdk(input.previous_output) for input in tx.input()] utxo_list = UTXOList( @@ -676,7 +687,7 @@ def open_psbt_in_tab( tx: Union[ bdk.PartiallySignedTransaction, TxBuilderInfos, bdk.TxBuilderResult, str, bdk.TransactionDetails ], - ): + ) -> Tuple[UITx_ViewerTab, UITx_Viewer]: psbt: bdk.PartiallySignedTransaction = None fee_info: Optional[FeeInfo] = None @@ -706,7 +717,7 @@ def open_psbt_in_tab( logger.debug("is bdk.TransactionDetails") raise Exception("cannot handle TransactionDetails") - def get_outpoints(): + def get_outpoints() -> List[OutPoint]: return [OutPoint.from_bdk(input.previous_output) for input in psbt.extract_tx().input()] utxo_list = UTXOList( @@ -756,7 +767,7 @@ def open_last_opened_wallets(self) -> List[QTWallet]: opened_wallets.append(qt_wallet) return opened_wallets - def open_last_opened_tx(self): + def open_last_opened_tx(self) -> None: for serialized in self.config.opened_txlike.get(str(self.config.network), []): self.open_tx_like_in_tab(serialized) @@ -775,7 +786,7 @@ def open_last_opened_tx(self): # TaskThread(self, signals_min=self.signals).add_and_start(do, on_success, on_done, on_error) - def open_wallet(self, file_path: Optional[str] = None): + def open_wallet(self, file_path: Optional[str] = None) -> Optional[QTWallet]: if not file_path: file_path, _ = QFileDialog.getOpenFileName( self, @@ -785,13 +796,13 @@ def open_wallet(self, file_path: Optional[str] = None): ) if not file_path: logger.debug("No file selected") - return + return None # make sure this wallet isn't open already by this instance opened_file_paths = [qt_wallet.file_path for qt_wallet in self.qt_wallets.values()] if file_path in opened_file_paths: Message(self.tr("The wallet {file_path} is already open.").format(file_path=file_path)) - return + return None if not QTWallet.get_wallet_lockfile(Path(file_path)): if not question_dialog( @@ -800,12 +811,15 @@ def open_wallet(self, file_path: Optional[str] = None): ).format(file_path=file_path), title=self.tr("Wallet already open"), ): - return + return None logger.debug(f"Selected file: {file_path}") if not os.path.isfile(file_path): - logger.debug(self.tr("There is no such file: {file_path}").format(file_path=file_path)) - return + Message( + self.tr("There is no such file: {file_path}").format(file_path=file_path), + type=MessageType.Error, + ) + return None password = None if Storage().has_password(file_path): direcory, filename = os.path.split(file_path) @@ -819,7 +833,7 @@ def open_wallet(self, file_path: Optional[str] = None): # the file could also be corrupted, but the "wrong password" is by far the likliest caught_exception_message(e, "Wrong password. Wallet could not be loaded.") QTWallet.remove_lockfile(Path(file_path)) - return + return None # self.advance_tips_in_background(wallet) qt_wallet = self.add_qt_wallet(wallet) @@ -832,7 +846,7 @@ def open_wallet(self, file_path: Optional[str] = None): self.signals.finished_open_wallet.emit(wallet.id) return qt_wallet - def export_bip329_labels(self, wallet_id: str): + def export_bip329_labels(self, wallet_id: str) -> None: qt_wallet = self.qt_wallets.get(wallet_id) if not qt_wallet: return @@ -850,7 +864,7 @@ def export_bip329_labels(self, wallet_id: str): with open(file_path, "w") as file: file.write(s) - def import_bip329_labels(self, wallet_id: str): + def import_bip329_labels(self, wallet_id: str) -> None: qt_wallet = self.qt_wallets.get(wallet_id) if not qt_wallet: return @@ -871,7 +885,7 @@ def import_bip329_labels(self, wallet_id: str): qt_wallet.wallet.labels.import_bip329_jsonlines(lines) self.signals.labels_updated.emit(UpdateFilter(refresh_all=True)) - def import_electrum_wallet_labels(self, wallet_id: str): + def import_electrum_wallet_labels(self, wallet_id: str) -> None: qt_wallet = self.qt_wallets.get(wallet_id) if not qt_wallet: return @@ -892,16 +906,17 @@ def import_electrum_wallet_labels(self, wallet_id: str): qt_wallet.wallet.labels.import_electrum_wallet_json(lines, network=self.config.network) self.signals.labels_updated.emit(UpdateFilter(refresh_all=True)) - def save_current_wallet(self): - qt_wallet = self.get_qt_wallet() + def save_qt_wallet(self, qt_wallet: QTWallet = None) -> None: + qt_wallet = qt_wallet if qt_wallet else self.get_qt_wallet() if qt_wallet: qt_wallet.save() + self.add_recently_open_wallet(qt_wallet.file_path) - def save_all_wallets(self): + def save_all_wallets(self) -> None: for qt_wallet in self.qt_wallets.values(): - qt_wallet.save() + self.save_qt_wallet(qt_wallet=qt_wallet) - def write_current_open_txs_to_config(self): + def write_current_open_txs_to_config(self) -> None: l = [] for index in range(self.tab_wallets.count()): @@ -912,20 +927,20 @@ def write_current_open_txs_to_config(self): self.config.opened_txlike[str(self.config.network)] = l - def click_create_single_signature_wallet(self): + def click_create_single_signature_wallet(self) -> None: qtprotowallet = self.create_qtprotowallet((1, 1), show_tutorial=True) if qtprotowallet: qtprotowallet.wallet_descriptor_ui.disable_fields() - def click_create_multisig_signature_wallet(self): + def click_create_multisig_signature_wallet(self) -> None: qtprotowallet = self.create_qtprotowallet((2, 3), show_tutorial=True) if qtprotowallet: qtprotowallet.wallet_descriptor_ui.disable_fields() - def click_custom_signature(self): + def click_custom_signature(self) -> None: qtprotowallet = self.create_qtprotowallet((3, 5), show_tutorial=False) - def new_wallet(self): + def new_wallet(self) -> None: self.welcome_screen.add_new_wallet_welcome_tab() def new_wallet_id(self) -> str: @@ -942,7 +957,7 @@ def create_qtwallet_from_protowallet(self, protowallet: ProtoWallet) -> QTWallet # adding these should only be done at wallet creation qt_wallet.address_list_tags.add(self.tr("Friends")) qt_wallet.address_list_tags.add(self.tr("KYC-Exchange")) - qt_wallet.save() + self.save_qt_wallet(qt_wallet) qt_wallet.sync() return qt_wallet @@ -952,7 +967,7 @@ def create_qtwallet_from_ui( protowallet: ProtoWallet, keystore_uis: KeyStoreUIs, wallet_steps: WalletSteps, - ): + ) -> None: if keystore_uis.ask_accept_unexpected_origins(): self.tab_wallets.removeTab(self.tab_wallets.indexOf(wallet_tab)) qt_wallet = self.create_qtwallet_from_protowallet(protowallet=protowallet) @@ -962,7 +977,7 @@ def create_qtwallet_from_ui( wallet_steps.set_current_index(wallet_steps.current_index() - 1) return - def create_qtwallet_from_qtprotowallet(self, qtprotowallet: QTProtoWallet): + def create_qtwallet_from_qtprotowallet(self, qtprotowallet: QTProtoWallet) -> None: self.create_qtwallet_from_ui( wallet_tab=qtprotowallet.tab, protowallet=qtprotowallet.protowallet, @@ -970,7 +985,9 @@ def create_qtwallet_from_qtprotowallet(self, qtprotowallet: QTProtoWallet): wallet_steps=qtprotowallet.wallet_steps, ) - def create_qtprotowallet(self, m_of_n: Tuple[int, int], show_tutorial=False) -> Optional[QTProtoWallet]: + def create_qtprotowallet( + self, m_of_n: Tuple[int, int], show_tutorial: bool = False + ) -> Optional[QTProtoWallet]: # ask for wallet name dialog = WalletIdDialog(self.config.wallet_dir) @@ -1031,7 +1048,7 @@ def create_qtprotowallet(self, m_of_n: Tuple[int, int], show_tutorial=False) -> return qtprotowallet def add_qt_wallet(self, wallet: Wallet) -> QTWallet: - def set_tab_widget_icon(tab: QWidget, icon: QIcon): + def set_tab_widget_icon(tab: QWidget, icon: QIcon) -> None: idx = self.tab_wallets.indexOf(tab) if idx != -1: self.tab_wallets.setTabIcon(idx, icon) @@ -1085,7 +1102,7 @@ def set_tab_widget_icon(tab: QWidget, icon: QIcon): self.last_qtwallet = qt_wallet return qt_wallet - def toggle_tutorial(self): + def toggle_tutorial(self) -> None: qt_wallet = self.get_qt_wallet() if not qt_wallet: Message(self.tr("Please complete the wallet setup.")) @@ -1113,17 +1130,20 @@ def _get_qt_base_wallet( return self.last_qtwallet return None - def get_qt_wallet(self, tab: QTabWidget = None, if_none_serve_last_active=False) -> Optional[QTWallet]: + def get_qt_wallet( + self, tab: QTabWidget = None, if_none_serve_last_active: bool = False + ) -> Optional[QTWallet]: return self._get_qt_base_wallet( self.qt_wallets, tab=tab, if_none_serve_last_active=if_none_serve_last_active ) - def get_blockchain_of_any_wallet(self) -> bdk.Blockchain: + def get_blockchain_of_any_wallet(self) -> Optional[bdk.Blockchain]: for qt_wallet in self.qt_wallets.values(): if qt_wallet.wallet.blockchain: return qt_wallet.wallet.blockchain + return None - def show_address(self, addr: str, parent=None): + def show_address(self, addr: str, parent: QWidget = None) -> None: qt_wallet = self.get_qt_wallet() if not qt_wallet: @@ -1141,14 +1161,14 @@ def show_address(self, addr: str, parent=None): self.address_dialogs.append(d) d.show() - def event_wallet_tab_closed(self): + def event_wallet_tab_closed(self) -> None: if not self.tab_wallets.count(): self.welcome_screen.add_new_wallet_welcome_tab() - def event_wallet_tab_added(self): + def event_wallet_tab_added(self) -> None: pass - def remove_qt_wallet(self, qt_wallet: QTWallet): + def remove_qt_wallet(self, qt_wallet: QTWallet) -> None: if not qt_wallet: return for i in range(self.tab_wallets.count()): @@ -1162,16 +1182,16 @@ def remove_qt_wallet(self, qt_wallet: QTWallet): del self.qt_wallets[qt_wallet.wallet.id] self.event_wallet_tab_closed() - def add_recently_open_wallet(self, file_path: str): + def add_recently_open_wallet(self, file_path: str) -> None: self.config.add_recently_open_wallet(file_path) self.populate_recent_wallets_menu() - def remove_all_qt_wallet(self): + def remove_all_qt_wallet(self) -> None: for qt_wallet in self.qt_wallets.copy().values(): self.remove_qt_wallet(qt_wallet) - def close_tab(self, index: int): + def close_tab(self, index: int) -> None: # qt_wallet qt_wallet = self.get_qt_wallet(tab=self.tab_wallets.widget(index)) if qt_wallet: @@ -1180,7 +1200,7 @@ def close_tab(self, index: int): ): return logger.debug(self.tr("Closing wallet {id}").format(id=qt_wallet.wallet.id)) - qt_wallet.save() + self.save_qt_wallet(qt_wallet) else: logger.debug(self.tr("Closing tab {name}").format(name=self.tab_wallets.tabText(index))) self.tab_wallets.removeTab(index) @@ -1190,12 +1210,12 @@ def close_tab(self, index: int): # other events self.event_wallet_tab_closed() - def sync(self): + def sync(self) -> None: qt_wallet = self.get_qt_wallet() if qt_wallet: qt_wallet.sync() - def closeEvent(self, event: QCloseEvent): + def closeEvent(self, event: QCloseEvent) -> None: self.threading_manager.stop_and_wait_all() self.config.last_wallet_files[str(self.config.network)] = [ @@ -1207,15 +1227,15 @@ def closeEvent(self, event: QCloseEvent): logger.info(f"Finished close handling") super().closeEvent(event) - def save_config(self): + def save_config(self) -> None: self.write_current_open_txs_to_config() self.config.save() - def save_and_restart(self, params: str): + def save_and_restart(self, params: str) -> None: self.save_config() self.restart(params=params) - def restart(self, params: str): + def restart(self, params: str) -> None: # Use shlex.split to properly handle spaces and special characters in arguments params_list = shlex.split(params) @@ -1241,14 +1261,14 @@ def restart(self, params: str): # The close event was not accepted, so the application will not quit. pass - def signal_handler(self, signum, frame): + def signal_handler(self, signum, frame) -> None: logger.debug(f"Handling signal: {signum}") close_event = QCloseEvent() self.closeEvent(close_event) logger.debug(f"Received signal {signum}, exiting.") QCoreApplication.quit() - def setup_signal_handlers(self): + def setup_signal_handlers(self) -> None: for sig in [ getattr(syssignal, attr) for attr in ["SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT"] diff --git a/bitcoin_safe/gui/qt/my_treeview.py b/bitcoin_safe/gui/qt/my_treeview.py index 122568d..5966477 100644 --- a/bitcoin_safe/gui/qt/my_treeview.py +++ b/bitcoin_safe/gui/qt/my_treeview.py @@ -70,7 +70,7 @@ from decimal import Decimal from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Type, Union -from bitcoin_qrreader.bitcoin_qr import Data +from bitcoin_qr_tools.data import Data from PyQt6 import QtCore from PyQt6.QtCore import ( QAbstractItemModel, @@ -88,6 +88,7 @@ pyqtSignal, ) from PyQt6.QtGui import ( + QAction, QCursor, QDrag, QDragEnterEvent, @@ -124,12 +125,12 @@ class MyMenu(QMenu): - def __init__(self, config: UserConfig): + def __init__(self, config: UserConfig) -> None: QMenu.__init__(self) self.setToolTipsVisible(True) self.config = config - def addToggle(self, text: str, callback: Callable, *, tooltip=""): + def addToggle(self, text: str, callback: Callable, *, tooltip="") -> QAction: m = self.addAction(text, callback) m.setCheckable(True) m.setToolTip(tooltip) @@ -142,7 +143,7 @@ def __init__( parent, drag_key: str = "item", drag_keys_to_file_paths=None, - ): + ) -> None: super().__init__(parent) self.mytreeview: MyTreeView = parent self.drag_key = drag_key @@ -157,7 +158,7 @@ def csv_drag_keys_to_file_paths( file_path = os.path.join(save_directory, f"export.csv") if save_directory else None return [self.mytreeview.csv_drag_keys_to_file_path(drag_keys=drag_keys, file_path=file_path)] - def flags(self, index: QtCore.QModelIndex): + def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlag: if index.column() == self.mytreeview.key_column: # only enable dragging for column 1 return super().flags(index) | Qt.ItemFlag.ItemIsDragEnabled else: @@ -194,11 +195,11 @@ def mimeData(self, indexes: List[QtCore.QModelIndex]) -> QMimeData: class MySortModel(QSortFilterProxyModel): - def __init__(self, parent, *, sort_role: int): + def __init__(self, parent, *, sort_role: int) -> None: super().__init__(parent) self._sort_role = sort_role - def lessThan(self, source_left: QModelIndex, source_right: QModelIndex): + def lessThan(self, source_left: QModelIndex, source_right: QModelIndex) -> bool: item1 = self.sourceModel().itemFromIndex(source_left) item2 = self.sourceModel().itemFromIndex(source_right) data1 = item1.data(self._sort_role) @@ -214,19 +215,19 @@ def lessThan(self, source_left: QModelIndex, source_right: QModelIndex): class ElectrumItemDelegate(QStyledItemDelegate): - def __init__(self, tv: "MyTreeView"): + def __init__(self, tv: "MyTreeView") -> None: super().__init__(tv) self.icon_shift_right = 30 self.tv = tv self.opened = None - def on_closeEditor(editor: QLineEdit, hint): + def on_closeEditor(editor: QLineEdit, hint) -> None: self.opened = None self.tv.is_editor_open = False if self.tv._pending_update: self.tv.update() - def on_commitData(editor: QLineEdit): + def on_commitData(editor: QLineEdit) -> None: new_text = editor.text() idx = QModelIndex(self.opened) row, col = idx.row(), idx.column() @@ -237,11 +238,11 @@ def on_commitData(editor: QLineEdit): self.closeEditor.connect(on_closeEditor) self.commitData.connect(on_commitData) - def initStyleOption(self, option: QStyleOptionViewItem, index: QModelIndex): + def initStyleOption(self, option: QStyleOptionViewItem, index: QModelIndex) -> None: super().initStyleOption(option, index) option.displayAlignment = self.tv.column_alignments.get(index.column(), Qt.AlignmentFlag.AlignLeft) - def createEditor(self, parent, option: QStyleOptionViewItem, idx: QModelIndex): + def createEditor(self, parent, option: QStyleOptionViewItem, idx: QModelIndex) -> QWidget: self.opened = QPersistentModelIndex(idx) self.tv.is_editor_open = True return super().createEditor(parent, option, idx) @@ -296,7 +297,7 @@ class MyTreeView(QTreeView): class BaseColumnsEnum(enum.IntEnum): @staticmethod - def _generate_next_value_(name: str, start: int, count: int, last_values): + def _generate_next_value_(name: str, start: int, count: int, last_values) -> int: # this is overridden to get a 0-based counter return count @@ -310,7 +311,7 @@ def __init__( stretch_column: Optional[int] = None, column_widths: Optional[Dict[int, int]] = None, editable_columns: Optional[Sequence[int]] = None, - ): + ) -> None: parent = parent super().__init__(parent) self.std_model = MyStandardItemModel(parent) @@ -354,10 +355,10 @@ def __init__( self.setDefaultDropAction(Qt.DropAction.CopyAction) self.setDragEnabled(True) # this must be after the other drag toggles - def updateUi(self): + def updateUi(self) -> None: pass - def startDrag(self, action: Qt.DropAction): + def startDrag(self, action: Qt.DropAction) -> None: indexes = self.selectedIndexes() if indexes: drag = QDrag(self) @@ -406,21 +407,22 @@ def create_menu(self, position: QPoint) -> None: # run_hook('receive_menu', menu, addrs, self.wallet) menu.exec(self.viewport().mapToGlobal(position)) - def set_editability(self, items: List[QStandardItem]): + def set_editability(self, items: List[QStandardItem]) -> None: for idx, i in enumerate(items): i.setEditable(idx in self.editable_columns) - def selected_in_column(self, column: int): + def selected_in_column(self, column: int) -> List[QModelIndex]: items = self.selectionModel().selectedIndexes() return list(x for x in items if x.column() == column) - def current_row_in_column(self, column: int): + def current_row_in_column(self, column: int) -> Optional[QModelIndex]: idx = self.selectionModel().currentIndex() if idx.isValid(): # Retrieve data for a specific role from the current index # Replace 'YourSpecificRole' with the role you are interested in # For example, QtCore.Qt.DisplayRole for the display text return idx.sibling(idx.row(), column) + return None def get_role_data_for_current_item(self, *, col, role) -> Any: idx = self.selectionModel().currentIndex() @@ -450,7 +452,7 @@ def original_model(self) -> QAbstractItemModel: else: return model - def set_current_idx(self, set_current: QPersistentModelIndex): + def set_current_idx(self, set_current: QPersistentModelIndex) -> None: if set_current: assert isinstance(set_current, QPersistentModelIndex) assert set_current.isValid() @@ -458,12 +460,12 @@ def set_current_idx(self, set_current: QPersistentModelIndex): QModelIndex(set_current), QItemSelectionModel.SelectionFlag.SelectCurrent ) - def select_row(self, content, column, role=Qt.ItemDataRole.DisplayRole): + def select_row(self, content, column, role=Qt.ItemDataRole.DisplayRole) -> None: return self.select_rows([content], column, role) def select_rows( self, content_list, column, role=Qt.ItemDataRole.DisplayRole, clear_previous_selection=True - ): + ) -> None: last_selected_index = None model = self.model() selection_model = self.selectionModel() @@ -486,7 +488,7 @@ def select_rows( def update_headers( self, headers: Union[Dict[Any, str], Iterable[str]], - ): + ) -> None: # headers is either a list of column names, or a dict: (col_idx->col_name) if not isinstance(headers, dict): # convert to dict headers = dict(enumerate(headers)) @@ -509,7 +511,7 @@ def selectionChanged(self, selected: QItemSelection, deselected: QItemSelection) super().selectionChanged(selected, deselected) self.on_selection_changed.emit() - def keyPressEvent(self, event: QKeyEvent): + def keyPressEvent(self, event: QKeyEvent) -> None: if self.itemDelegate().opened: return if event.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter]: @@ -530,8 +532,8 @@ def keyPressEvent(self, event: QKeyEvent): else: super().keyPressEvent(event) - def copyKeyRoleToClipboard(self, row_numbers): - def get_data(row, col): + def copyKeyRoleToClipboard(self, row_numbers) -> None: + def get_data(row, col) -> Any: model = self.model() index = model.index(row, self.key_column) @@ -551,8 +553,8 @@ def get_data(row, col): stream.write(str(get_data(row, self.ROLE_KEY)) + "\n") # append newline character after each row do_copy(stream.getvalue(), title=f"{len(row_numbers)} rows have been copied as text") - def get_rows_as_list(self, row_numbers): - def get_data(row, col): + def get_rows_as_list(self, row_numbers) -> Any: + def get_data(row, col) -> Any: model = self.model() # assuming this is a QAbstractItemModel or subclass index = model.index(row, col) @@ -580,7 +582,7 @@ def get_data(row, col): return table - def copyRowsToClipboardAsCSV(self, row_numbers): + def copyRowsToClipboardAsCSV(self, row_numbers) -> None: table = self.get_rows_as_list(row_numbers) stream = io.StringIO() @@ -588,7 +590,7 @@ def copyRowsToClipboardAsCSV(self, row_numbers): writer.writerows(table) do_copy(stream.getvalue(), title=f"{len(row_numbers)} rows have ben copied as csv") - def mouseDoubleClickEvent(self, event: QMouseEvent): + def mouseDoubleClickEvent(self, event: QMouseEvent) -> None: idx: QModelIndex = self.indexAt(event.pos()) if self.proxy: idx = self.proxy.mapToSource(idx) @@ -601,16 +603,16 @@ def mouseDoubleClickEvent(self, event: QMouseEvent): else: self.on_double_click(idx) - def on_double_click(self, idx: QModelIndex): + def on_double_click(self, idx: QModelIndex) -> None: pass - def on_activated(self, idx: QModelIndex): + def on_activated(self, idx: QModelIndex) -> None: # on 'enter' we show the menu pt = self.visualRect(idx).bottomLeft() pt.setX(50) self.customContextMenuRequested.emit(pt) - def edit(self, idx, trigger=QAbstractItemView.EditTrigger.AllEditTriggers, event=None): + def edit(self, idx, trigger=QAbstractItemView.EditTrigger.AllEditTriggers, event=None) -> bool: """ this is to prevent: edit: editing failed @@ -621,7 +623,7 @@ def edit(self, idx, trigger=QAbstractItemView.EditTrigger.AllEditTriggers, event def on_edited(self, idx: QModelIndex, edit_key, *, text: str) -> None: raise NotImplementedError() - def should_hide(self, row: int): + def should_hide(self, row: int) -> bool: """row_num is for self.model(). So if there is a proxy, it is the row number in that! @@ -635,7 +637,7 @@ def get_text_from_coordinate(self, row: int, col: int) -> str: return "" return item.text() - def get_role_data_from_coordinate(self, row: int, col: int, *, role): + def get_role_data_from_coordinate(self, row: int, col: int, *, role) -> Any: idx = self.model().index(row, col) item = self.item_from_index(idx) if not item: @@ -643,7 +645,7 @@ def get_role_data_from_coordinate(self, row: int, col: int, *, role): role_data = item.data(role) return role_data - def get_edit_key_from_coordinate(self, row: int, col: int): + def get_edit_key_from_coordinate(self, row: int, col: int) -> Any: # overriding this might allow avoiding storing duplicate data return self.get_role_data_from_coordinate(row, col, role=self.ROLE_EDIT_KEY) @@ -693,7 +695,7 @@ def hide_rows(self) -> List[bool]: "Returns a [row0_is_now_hidden, row1_is_now_hidden, ...]" return [self.hide_row(row) for row in range(self.model().rowCount())] - def as_csv_string(self, row_numbers: Optional[List[int]] = None, export_all=False): + def as_csv_string(self, row_numbers: Optional[List[int]] = None, export_all=False) -> str: table = self.get_rows_as_list( row_numbers=list(range(self.model().rowCount())) if export_all else row_numbers ) @@ -704,7 +706,7 @@ def as_csv_string(self, row_numbers: Optional[List[int]] = None, export_all=Fals return stream.getvalue() - def export_as_csv(self, file_path=None): + def export_as_csv(self, file_path=None) -> None: if not file_path: file_path, _ = QFileDialog.getSaveFileName( self, "Export csv", "", "All Files (*);;Text Files (*.csv)" @@ -768,7 +770,7 @@ def add_copy_menu(self, menu: QMenu, idx: QModelIndex, force_columns=None) -> QM def place_text_on_clipboard(self, text: str, *, title: str = None) -> None: do_copy(text, title=title) - def showEvent(self, e: QShowEvent): + def showEvent(self, e: QShowEvent) -> None: super().showEvent(e) if e.isAccepted() and self._pending_update: self._forced_update = True @@ -789,7 +791,7 @@ def find_row_by_key(self, key: str) -> Optional[int]: return row return None - def refresh_all(self): + def refresh_all(self) -> None: if self.maybe_defer_update(): return for row in range(0, self.std_model.rowCount()): @@ -800,18 +802,18 @@ def refresh_all(self): def refresh_row(self, key: str, row: int) -> None: pass - def refresh_item(self, key: str): + def refresh_item(self, key: str) -> None: row = self.find_row_by_key(key) if row is not None: self.refresh_row(key, row) - def delete_item(self, key: str): + def delete_item(self, key: str) -> None: row = self.find_row_by_key(key) if row is not None: self.std_model.takeRow(row) self.hide_if_empty() - def dragEnterEvent(self, event: QDragEnterEvent): + def dragEnterEvent(self, event: QDragEnterEvent) -> None: if event.mimeData().hasUrls(): # Iterate through the list of dropped file URLs for url in event.mimeData().urls(): @@ -830,10 +832,10 @@ def dragEnterEvent(self, event: QDragEnterEvent): if not event.isAccepted(): event.ignore() - def dragMoveEvent(self, event: QDragMoveEvent): + def dragMoveEvent(self, event: QDragMoveEvent) -> None: return self.dragEnterEvent(event) - def dropEvent(self, event: QDropEvent): + def dropEvent(self, event: QDropEvent) -> None: if event.mimeData().hasUrls(): # Iterate through the list of dropped file URLs for url in event.mimeData().urls(): @@ -855,7 +857,7 @@ def dropEvent(self, event: QDropEvent): if not event.isAccepted(): event.ignore() - def update(self): + def update(self) -> None: super().update() self.signal_update.emit() @@ -881,7 +883,7 @@ def __init__(self, searchable_list: MyTreeView, config: UserConfig, parent: QWid # which is done in updateUi self.searchable_list.signal_update.connect(self.updateUi) - def create_layout(self): + def create_layout(self) -> None: self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) # Left, Top, Right, Bottom margins @@ -913,19 +915,19 @@ def create_toolbar_with_menu(self, title): self.toolbar.addWidget(toolbar_button) return self.toolbar, self.menu, self.balance_label, self.search_edit, self.action_export_as_csv - def show_toolbar(self, is_visible: bool, config=None): + def show_toolbar(self, is_visible: bool, config=None) -> None: if is_visible == self.toolbar_is_visible: return self.toolbar_is_visible = is_visible if not is_visible: self.on_hide_toolbar() - def on_hide_toolbar(self): + def on_hide_toolbar(self) -> None: pass - def toggle_toolbar(self, config=None): + def toggle_toolbar(self, config=None) -> None: self.show_toolbar(not self.toolbar_is_visible, config) - def updateUi(self): + def updateUi(self) -> None: self.search_edit.setPlaceholderText(translate("mytreeview", "Type to filter")) self.action_export_as_csv.setText(translate("mytreeview", "Export as CSV")) diff --git a/bitcoin_safe/gui/qt/nLockTimePicker.py b/bitcoin_safe/gui/qt/nLockTimePicker.py index 1603677..8bea84a 100644 --- a/bitcoin_safe/gui/qt/nLockTimePicker.py +++ b/bitcoin_safe/gui/qt/nLockTimePicker.py @@ -47,11 +47,11 @@ class DateTimePicker(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.initUI() - def initUI(self): + def initUI(self) -> None: self.dateTimeEdit = QDateTimeEdit(self) self.dateTimeEdit.setCalendarPopup(True) @@ -62,7 +62,7 @@ def initUI(self): layout.addWidget(self.dateTimeEdit) self.setLayout(layout) - def print_time(self): + def print_time(self) -> None: # Convert QDateTime to Python datetime object local_datetime = self.get_datetime() @@ -76,7 +76,7 @@ def get_datetime(self) -> datetime: class CheckBoxGroupBox(QWidget): - def __init__(self, enabled=True): + def __init__(self, enabled=True) -> None: super().__init__() # Create the checkbox self.checkbox = QCheckBox() @@ -96,7 +96,7 @@ def __init__(self, enabled=True): self.setLayout(layout) self.toggleGroupBox(self.checkbox.checkState()) - def toggleGroupBox(self, state: Qt.CheckState): + def toggleGroupBox(self, state: Qt.CheckState) -> None: # Enable or disable the group box based on the checkbox state self.groupBox.setEnabled(state == Qt.CheckState.Checked) diff --git a/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py b/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py index c781a23..d87239a 100644 --- a/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py +++ b/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py @@ -78,7 +78,7 @@ def __init__(self, main_tabs: DataTabWidget, network: Network, signals: Signals) ) logger.debug(f"initialized welcome_screen = {self}") - def add_new_wallet_welcome_tab(self): + def add_new_wallet_welcome_tab(self) -> None: add_tab_to_tabs( self.main_tabs, self.tab, @@ -89,7 +89,7 @@ def add_new_wallet_welcome_tab(self): data=self, ) - def create_ui(self): + def create_ui(self) -> None: self.tab = QWidget() self.horizontalLayout_2 = QHBoxLayout(self.tab) self.groupBox_singlesig = QGroupBox(self.tab) @@ -177,7 +177,7 @@ def create_ui(self): self.updateUi() self.signals.language_switch.connect(self.updateUi) - def updateUi(self): + def updateUi(self) -> None: self.label_singlesig.setText( f"""

{self.tr('Single Signature Wallet')}