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"""
>4v+MsGUC94j(?J|w9`rn4R&2zteZRe|_TWtw<)i`6zkeS( zKPJD_AUVnFx&i>`{l5>WRAzLNmr4XTAV>ybABq%}ir6}4?-c+*%mkE{(DYh9Hq@`A z9n$?(-sZcz>cMAZicap^|Hz8|vXuw^F?3f3cI zzv{WpHr?TtQ{4jH?6<>wHy?&Vh-x%2#wHfsej+GwIgMq?UQ7DMFl@d3!NDNl>H6-t zVm-Y3;bLOz>GYZEi|U*GVXVhqu89rLi080Vf4;u6QD*0hW!rQ7Uo5N6L#(1t2UuU! z 0(GK_uygcoX(5S_yTV$)v-_H7y4YEuaAaQUM-|ts zLUg-rfhgJ_%7G;TLH{cT(d#m~&QqgbTLkAnoI;Hpv*}7J@V{@zZ_n$$_1xT_$2StV zeVywSOe;gcP{c?H9r*k(EO+L0Ggc^9N7V0iJ`nH`iCP_cyYqA)ultGNq~ns+@6?x@ z!jQ7ufjUb*{hJ$3weJqqdx1-staouZLhi^E{*TTbXJKp`gLz_682)GRPS Qr9|KT6K3|c7;IW)eNV{BLj?3@KcMlO1gS{Fmn}fz)1aYMI7#-*EAPjG8>70Ti z3O~$K-{T3VpH~6X=9sZFF>G6{>BixhNQ{W;*hFnx$g6%o4Y_S?mAYMaQGYodfmdC| zW^TrC=+E*oI#ay@hoggY$ow8wR1)p>i;@->#W4VTy^AW{Hya^$6%Wr)#!rVr*1Piw z)xX9q&zigaQ4tVMKQA>{I^X=z6+SCw+x$TjFFu+ciO|MCxpqk|e0EjXvX|TZas6qP zUMy-QSnljRzFWLLAI97jTP|*+B;Fsl`~1Xn+g`YTSLk$LPxO|J-*wmD?=ssaJ!WrW zdv4Q~Kj4>pRx3|o-s9rX*#gBKK>5mtf0K$;zT;|I Te#h}d z6jzup2Hf#yppb?(Nk}kJ3Lte=%6L{omM^tse_JUS4#P-^7!s;FqyWWUGPEBjW4u6? zb3cSWc%C|IIT-AQdMM}X&lu+E|IkcMF|$45-v1+GnhM~vD!EQSE23Cdb1 jV zqasyeX1Q@7Fz)YS_xfPuipzO#u|1 aock+dOu$Okh zOwHBew%!c#uJ~cm7h=Yim! o-jV19J|29-Kh^c9+!=Q_L-$LZ|!I0?EnF z9tYQrz&hX=x?0Q|6%~p$Wlp|h(%xtLnu-=%`1w&{lq5U!d=a=*+w4i2Zz+N8o?H}a zPUIL|I+Jn{v>W^M^k(p8X+@~Z4ccC9h|gK9VR!k*%vFoj4q;TC1XdLrBs2P4JOZ|8 zTNcNC(MJ9=S{mQry~Psj5ff8Lf!VmfrD!u%vj9wuxyZ>e+3lxlB=fO z(RBL6q1uNBf)4xKOK$rrEYL1m!HIeXsE%-x`xtI?#>C#biK#CwwD;X4^mZAXZj-e| zl8G_GQA2iD==hoJ^_hI?`o-%V;1J#pz3X_RS&TtV^?EsRq I8_1n- zhEnUq4>4WbS3kc#D)a|e+V*|(IU*)(VH|Ym`&nsvURCFaq(5WhRoErK 7|sia|0#f+qHAe>jgfE_*R8%3{MAQP9}-$4g2YW)Zb9Y1XIOh;NqJO*Yi)&{4< zc4E4-d*RH}hYduZ4{|H|ed` $KiQmfya)%F>Fr-TH=>S#49MDDk>MUne5)%PwYC z>0xGSa_O@ka?$HClo)kPUBxCw+gRK_&7tY-Sl#@1&dm(!ZanY!3gpRxPG!)Vu_0C* z-Tgcg-^gmgkS u|-I4mca{&MjQpM}*)_k9?y6Ks(LD0PGwz((Tg^Viz!=5QT3a!J{NALXB5S z3&Vt^q~=_1M&OoCtbP>fKm{X9?~u~ATZ5sl7*0AVBqYYN?=>?gMIk|KKswA~TM`p% zwIIg$S@JM*v81_%_V(>aXy6$!*+<)&C8hhGJ+y-A5%|vBP^I*#>>(LFgy9X=S>Zhq zJ`3%p3+*SS(=`0ecxvQQ767m>JL|iAZDZTW#h|Ig 8*BGnmE9P0+W{S;JzD)34Yd24?3hXUsW=jeZ zp-i^mv3eHa6|E8F<{AjJc_E91Q6Ih6%HS1WPi{`slFqrO>%Rn;Q;HxijGK8q?7cb$ z9MW -H5Y Jr>?w$Xd+KGg0t0J sVdV)YJcx|n zvno6rFlCdeZ@3ZQ4sytZ@GMMSuda6k ckF7di ja z1OBT(AN%y4>JWkifDwuc+Y(1UNyibX2-jqsbVUmHx`!|V6WUo=k`#l380~s}_Y$#i z3h;RoP&k$UD L{XLqDHDR}a~Gled_#RQ!7;?|h>r8IjsK1~FlSv|odLi~yLy zQlprGp_tH;w2|BVc>6Y_`CZx{YCeK(2zv **kA%?GsDp0ko(C+l?s^iA! zw7NQ;^6Rj*DM6F22x)H4YPESe1HuuRZj?GE-F=Rm6alnxWGl+vX7fJSGe2`mya`uU z((@ON3Kv%T2M6(ac|X Sv2xl(7(a}3?g!% zR08ZF8mF&Run1<*>YOUTrS2nw?;xgRRml{5l%qIkef$A8ZD_a5y}@cFdbKcu=y7XZ zQC+&wwSsNOZGnl)H4w@AqN7s2WP1lk{AAWHm>r*AyW;|S^-E}+fr?~ULMVZUdY*mH z_`A$vHBI >sN#yN2ty`N#zZNkF>YvY5 -7Rikb5Z fa2A&o@1=YZJxG_n=vPK@$RF%d z?vBa5?wdKQ&2E!#U@sQLGsHw9L{Uv8$U21Rz;)?rsrU%$23dF)j@W{5+RpB#Y0IC4 zgMi{IlZj~b=iK+>txBoy+KJdC81de;ns<_akK=h3s$fWwo2m@9y7-22H1BW}484NK zSrK2h*ih-_H>W(;W)vxi>2tJoTaaH)=VNgw_z``}+^diSXP|Ct6Uj0AtjP*cUZhn7 zG}lP85k_K^qIaa4=?Fz8VN~g1fOjX`G+=YSz~{{ar>Nw<=W9g|XjtG-D6@7fh%zeX zUM+HA`X(Lpg}2>(uFxehd-Osw)4SrczAbcqGpiAs;8(gnC-+ 4 qz5Qj zq3}v$iMDuLQ?6_~3{!8COQp93dGyxpT@vCnE$?1F&ZxpEF2ueTg0voP=+K&H5riMq zpBw7brr?dP)mwku@~d*nn=VTQ&{&UKsTn-K7ke1(w44zu;|SZ0{fov))D2d|(*;Qz z$N)(y3SRk+r+Es~NN;LKaiXPAwvJPCsdt?2WfZF4^xea8 (bwxfUwE?FzqpAy z;s$&jIDOU4Iea7QBzH#AOXigdTsn(uTHaP_!hLVcuw!}xVgwLyW5)4;eR!bdbdg{s zps5OQ5k|wu!pYgeMvSMN%wd&kpMOp{GeX&&4r{|=hmohnvvxTn4pOs+TE!4VrllMC zUSk7-%p8}2-e$hY$UlTrwdCR+L*Z{$`gV7Df6w2ot+D rfYPtXYkUy9Hh2V%v$qJ;qQM)1v;6$t_^ll65e9FX_E= zaJx&$M{9nG4bBSv3VqgE+;HN&5_JkYJln$)7p8d_ zMS4t$(=d~DCsttX=l~Ev6-=p6m-Q}q|8}ko+Zzz@4kA=5Q |jOVk87pw@Si!}-47$@bE>E(m&iZ{w`@NXzAZTFBHgVv?5mjz54wY+ z;0L03NfuIE!8=e%fJB(zYH0JZcccc?7w=M%6-|w7<9TBK(eNDxB)(bPgRsODzD?fm znJxwdZ$`z#rKePD#PZ{~%=F>lHb=u-aTe@-k;mRISRvcwpFdm+M_xo7QQsPt^aQ=6 zZntObhn}_)ci^eZv^$;Q3SD>zccocq-Tex0Su4 4c8uZ^4V9mZ8ACkum#XD zGyBdtVHhMOlsIBC*5{
M@;nic%f<^M+t)fzk+Pd&1z|Q8QF>~{TOqkF7(?u zC_sBSG_$K$A!Ep3m0i6;pl?7tO-);0T5&7#oAg#&`IemqQ6n7niO^UgpdIycg%-nh zPssh~@cZVjM`itb3BDO{@pNiY!eO3D>T0qMkCD!-UzHfLov35u!Fe2tdl4o9W6h|Y zl)+XNB0c2BK-Yi)p6YeRdZRO9zA!G;V~OA#?7kyDj)Iq$iaVIEX^txeiLVLAqbwRa z`^lbox#R(xFGz4jlfPU+rw}pHAm2BmFtOThW~=^;AioEn=i2g8Hg$Kd63!v48t*5* zQHa2|31N+JCQ@*YSMc(US_Ztx{*&GQkaXbevqPyCBRoJJ+tUy!1`ASK68>4D&8=(+ z*DjJM8DpnzeC>0nQ}TO7`4so4ih>?|nU&WU55lDlm%`)TY7%3}tJJq2m~=iRrtN ?7F>zoMF1Jv>X&{np%|&caf?lBscgj-w-y>2Ze8Ws@g^d zOzV%NP}xacU?sL#bhElC6R*C4r4*_QFc}x~dM&W2Z{|Ba7Ai6Ob_ss8E&0o&)X>5c zqN7nCGdwx4^DBv<7BZwUj%%hgOUCrB8O$gpraP1C#A!KgmVZ@b-cBiJ^(QyVpoNXr zSu`wy$Ob_KI;K*B8#ny@xw35hQ95%Y?KqdhE2Ndt#l03mgIvSA2=!ka<}0wQuamNs zJU<(hi1nfIEDg4S=}>vfJ7GVutZ9%fprYWVx$8|_>Ku(JXa$`_4@P@>HaF6%#S%oP zi6jM=M%-q-!FAyJrd>FPTC&i?DkCUO8nJAGm8n42Z8xWdt-$#14FRJ{f2G2Zy3(uL zfR$Uo#|bKr`7h){@#==(24hsSbyxbcT<%C|y|o_ByA1n5SbiaibE)I0lk_t93wye@$zHx&D(%akrM{(k zMOR5??@M0~a`Y$f)uhrTbPTyRza(-9F-Yh&tyg$iK2*Ji{umu4K(;;_sNRYZ#ecKO zr<5`B2Hp9%mK5~;Q;9-2gD@S@_TrkZU8yx*O~a2|g(;iLZ{Ju-CHbzY8+%`-PsN%p zAFROsg>F#=Z!=5!_c70O=i5_-=`RoUJ-;97`8>I?f26_R#ke?UzpiX1)mNA`VOiSN zk-a}_OkQTd5gRh8NYT9gRHOQilPDIRUs$-Ow^yd~i7+Ap`CN-J8mu>EF8s> @ssf@4X1ZpPisft175f{ig21 z66jrSS07pBz}JM5y~!$S-X|{cK@wyWrQt3TqC_Giats6jO;e>wD=Y+yrGIdKw7Brg zII{si$?WM!&7@2d1<9x2I4p;c0Ok93D5p |B^LFM99B*R_G> zsj)S*b_AaW{M4hRB@wIflJ!owQKdR5lk`@o5&JYbN?!rtWDBX_L!^%FTWpOR>DRN= zsmVQXDs*miWK`3UFq+5|%5m`Aw4^LT@s-VmBR%e7g~p=dYKS(&lZ(s`s3mm`=*3#A zlIGzDKfio4mIZ@}UZZa!7BgF};PjbYvb(#?mG*5FZ6H5+Fh@9T6bC7J!NQU6occ`d h2kil0r-Nsx-RNWAi%$B@myIL $`tut$Y6sG_0pj?X%B0yG|8dMU=X#91bQWCISKij)J_jCISLdEc{s)9Tomt z2BW_L0RbPyM_b=r6Y2$Uc6G9{wTA)RUpd17FmGEc1O)H-q6`DirvmZPkNza>D5?kp zoA}hDyENZV_q&(1+-0>NEbCK}gzuZ(5U{OK``jLWeZIV=>v%bH0jaK(;yIa;rGmLm zOm5qCe0F
lEWij$Y zp@2t=_eat^y2N%1%fWqK?+r!iHjWU^YAXs07G5;2Y@c1;K0L#|6FwOZ{5f>FD)#W1 zVq&3NxIRV8P35lgS8EWZ*x|?In2{q*->9 1>ZJp}x43z>(XlzA}?6WhxwkDD& z*w2Q<9s(YAu`Xp j)Gb3as9YUYq(x)k&|(K0e#2ZTI#3JUCayo+wr M4V=dD8^8bDeZ$j+?CB|hz%S}tI~n#LV`z+_Y7j`@L~Vt@fw zJMitLj;qvDPM#|L06L&%)4ekt7bB5oJ!Lb6M^gDT%ANkcnT=v9GsAOclW^sVd2Q_} zh`fto{l(k2nFfu}+L@&m_rqylI{$dC=Y_0N5pkk@Z<7U{F{&-)2y>K>RV^9Q>hT?k z`hiwz=aY(tbzfL~HSt_L?w@vOJ*wUwL}SVJ+}Np}9OFMjsyE7As5EZ(yruDa7@Fs+ zcs#kst4aH8z~)rECUKj}^E2}v`%Q={ldEj^&?FZ{qdZ~|=uPc+H@l}?WCBVB>Li-_ zh0>#*@v(2ZJYC x_08el-_%QZar{21*X^2aMdlv`j>LN z&U`+FM&i({;!afKVfX^8ySk;HkZsI>5PV&2dIZ1>?XsTuDRY1N?YY-QZ=eoY>Xa<3 zB5ljC&8nc#n>pJ$w!U}KsZmNb`xEJmdqeeujL(HaDj;2OT*ykuR|t^8>!xurND5rM z_!MNfUf6BcyD+XrnclV_$1Gbb*qXRr9&Mv@HRHgPA$_p((v) }R(Y URDwGeq3~G zLL_&-5PLb}U}}y>z3f)s^T1f_haeI2lVioD_fomZI1 z5ZLDR0_}T6YKTRWIO8*o8HMk|Ma(@38!fT6sCOah4My}>wMeIG?@S8KWq&b8nltTx z!j6%6XtkVkWmew`Q)wn0=NS^v_!au3zjM-g?ZbVJ+mZJu?c`H{%WxDDo1i@I!YJmv z5^X2`9< v26rWe zcRw9UrnP|l6O^akbpoBVyL3H)atHUMDIcyA_hgas0sBOSGc5P^nyu~i#BNpOEu*b? z^_9t4QU@%Vh<@0#DTAFH>}`5H(>Lzy;tAxf=gA+RJs~ecUYcseYJj9y&Optd9^uw% zeLMR8-XEEWcX5u=T}K8LsmW>|B{`FU%a?aLS2j-l>G-RsbbS?WbQ{Dx?T7+M;{dSu zgLhuvOItE}Fe)2koPr4e P)q3?Zc` zGU^p2GG~zK4`y%Ea^iWmFc%WClf-GIxn+rNta-WfNNmE@W2ed5W6>0H;h4(EY#nD% zoG5uBtq)({fzSl?_KzwynGN3}A%IURX;H9hbr|8WxR0g oLG? z6})2J>)0&>RFta|(CAJ%wzKEXa%fx~?R3;cHm1V)uSKdP!XYKyZx)xa5--J+`tUts zZnB7M#XOY4#_bID1$&xjd8KuQb*?Ne)zK&ur|sUN-)Y#>ZFJV6hD6}JUuu*R*m0w` zCF+;icjnU<-^Z;{OoZZ2$K%?gxLTg3Pmu7lk{`Zy*QSM )VH*f?@hq2skLYdKDE`4a1P z1j;k*Ygq%j@{v;d7z@h0=YFrVv$8ijd#or(Bdj~Ak_nvdzFO(n2a0BcTnczYj$>3W z& -yOH`UKx3KpYxYzdP{jC zf*9|gm6{vsi+`{t{N5s@dS!xq;a%W|qGJPH)=DIyrl6b2c(K&Yrg@v!`_$~^ukON_ zy?#_oM{*)jXzVt2=1%bX92T$`t q(`qlAJu{phd`OMmA7GC;6LY}L;c{$^oDEd`0>z; z6VoJ=J+zpqlwvyC>f=-^9fFEDz5YCYl)G*n(Ls?s!Ff;`FblA#B+q_9$>xoiOV8~Z zmWqqKdV{Zk5R->FyhO%4TCnX}?oD%Lcv_43R87}69~zXYB>6KL*qlt0ziw-pB?cHi zS4ClI!WoF)n&szkM^e?M8F(fta!$u|#%v1Rs3*ZKqzY2Iv>Bs`Z 8h?2Z=x!%Ds zVLlDr?|&~XwQ-#X5V^?K9VnTRWn2KyzRPQSYN$1zvCusd?`x@0{R#RGl-!4ujxg4F zj#sMx$zZcv+kg#*RIwEwlLVoH$osQ5zJ1rxclU)YDA@kcIUH&h^$_xlA(wXkiHmd} z>!2^AK@b`rwTBosz^p(fvQe3Gm@;Ky95FLjHMxWKiGjo!0=4#K 3Hs{_zuo*gg8~Wtw>&G$xu{tMaw7eC)L!sEUa19Li5LG3wRco+tG(sRVS6r zY(+R;r}z|YIXRrGp&dW@1QiI2gCYsCC&W`
&4akjM zTnD6u>jMs>Wi_)+%fKj}3N`{1kvo*AbO~F*qNI+BpIIMkJqh3#>OY?OjSUM&HB8%u z;cb;lvK%BQ u}fsDh7Q?xz_-srx80R^%yTus}B*S>O(wPrzso5 z7}U#;1;&fjn2wv=gi&~uhUjZ2nDL|6dkH=GoY~@`c)|MdEct;Py5|rVvodF -aMam|Ea$FidP)BThoR1x;?UnvKcFs}ZtAd1}%;RkF z51)=m<@W2icc6!ZKSM4OPv<6@zg*b3@QwGL@dNG0S<_HHbKBg1%In!;C}yD+uq+~7 z7?L0w3y<0fdVU`Mp<~Hmy*C=n&KzDx#LVBhJ q4U0Z -;)wS~Ok? zw1eCb9z)s`Hw+-jH `0b5o78LC%RfHUK?0a@mLK){Ewt)g4dzw*3155A}+Rwy+UjDQ@ z`1K=A`N|i!C~fDrdn1fD Ek5h)-?hiLWuJgGf|hg-S(S7|bzbJDeIZ z^GjZH1wQ_3nrrGvk2qbN;|n$E1SP?g-Gxp}!BW3-#BfOf^Xn<|k zsrxD?UJBJTx~^wp`nJmm1;=CZ20 &!Vc@#@(9%8+(Qgxk^=arKj^ z($TS6VTskobaL(cGSxVBC8sG?z(<}fta4Tei@e6YH2C3=#e5?-jzlx-#R0p%OOIF` zL8;JEg$(x+v}%@pJ(mx UvrwmNXE{v#M@#c z X8B`6!7_`yrCChM`!S^A|I114*1uKTm$JW(*D{JLtA!Pvc zhai3-e%2QGsLQ8eq@1SSZ3$;T1=_smR-AM*US7l^mReC{Xlo8^>V>Va`9;!)Qb<3w z+H=c)8*c5tsKqyB^4j{<<-NhZVwYu*8XyPO0wr+5S935F-1c8XXH&vF5jgoELB Vbrk6U XZI4VgL!+{U4Dt%f zduGDLB!@YEN*2?3OX06YOWyY6X8K2S{g}%Qf~BqQF4f8kJ~^w#MN5CH)S}yMV~NOT zy8IN^7X8BUboj*$r7iq|Mqfo)(89?91hsTBhk?8uoZ%NZ2nfPr-p)`9JD5Ab9A<6n zD8g{m`k4V>YbnB@$D_ib;w%NTv6c66g=zVyYFqf&SqNA%h>2nfdkew|9ANHHfVYFa zqnn_&2*V# *PK3e6-Q8Ib4EFN!0(o(RoLsHJoB{#@U=A)Y7Z(tY0J^<$bccEa9o-mzQ~bpt z4Rf<_wRLv4b#es!=7gF%dAN%(Fu=zF|H#k5Sw-ca^p0+Ss{q#n> A~S@r0B7ozVZ)!%Z80&jr?mxjA{bTEJvHVUF&M|4LzL@lSte4_Es? Nk}ooXUdN0s`T)-~*alTJixQ{JeZXem*`P zpama4Cm#nVluv*M_J_*SLQvMp)d32hPFn}4H4N IxzZT%f>bS{PBR-A}HkwgStDpYCAdEi!l670Px%MPkRG||C$teTQ@l3 z)$fk~yXUoFE`PoIYYEuf{uu%Q{ e`}KL;m}i(i|QLy&`CkO#uX!70eW!2tfdVDRr% z{f~-;!T(DW;Xi Czuom;x&9*s{v+^zyX*g%T$ulJ;DI^9KLvTgk29a*8CT#(AyjiEIcbE) z-{0@sOOoLh3}<-*Hv|OSp5H%+scg7Za3h+#f{F~<4k9%MIxof2bRPl&d76T>gtqtm zp-EslO|Qp;)w2JXSWfPQ)JGBy4Yr7+A%d4miIP| Xk=I{%t#Ix9;5 z BclBjth8c}`f P)-vq^g0h7kv6pYAp+f^i zS-zKxO`mRwf9juD9Eko*zFL3{J1=^a9d9J3uQqS9(7kXu05-0?E6K&R_kG)aOdjYz z`1HzoGQ`{c#g68Ir`Jly?X(t0jyP^WpI&)l)d+Qx*v&*WS3q3uML=I=yRPG`^Urao z&w>3kr-3cpuIqiZtc+@+YX`NPm!AWN;3JKm6?ex2R xlX zjp+(KP3Df%m9?17su!&aAVX<`t1F7dFZ|pgpg5Z&=kxBcruz#Isn6{mmy5H0I~>f+ z%&dSHRtftv7IQUFzP0Y_vy-LDud_{WGP{)64MXiJKhq{GUekHMvI;*vCbtF|g{wVI zxY{o?;Eag{g44ia!ICHF!sn6*2oFbgFTULTa=q#g!{T&0KsOD%|I7+dn`pi=zuS+( zsy6Rcyt`;jiAdnTURdt?rkA$bg+^Dv;n(+_IDWC2NKN#XVTs?HSXKC%@Q2^o*zvej zi59P@012wt`f&PidNb2kJV1$FRa&d(BCDD}SWQrb(j;Hy3-Ak3XK~L~-%W7fHQ04m zbEvm*EDP+f;5c8Wtg0em{!+2h?@R@xaK?x@Ft+4VcDL($G{`7tnp&o-qRDIuQXoP> zp!TA!6xpUOH*iD76TIS&xtppw9*0jNTjOFl&%y(l(&M=-Jf}h8ST+7#rt*$E$Pe#z z6JbQ9*cGzj^mVK<-PP-#C;Vq4+${Kb<9X Ji1aSPRsi>R1G)&~m_UKdCG9mq%Oq_my( z3O*AI#_qk_EcK-8+;-C|x!YnvN??DVi~grd$8dF%sE^5GNZC8fsvPq{$LKE%+Q=m% z=f=9juJ$ety*E?K06qMp{1(W~D?(|YUTact-_}>Ek5s~4$9d^GWg86{Ql}5MVw^Rr zC*T}!$$;-70J^|Ch$Di-s`c*P)M*?aZOY{jH?_ywizVMp5u=6yyDAz+G{4#Jeyd@w zhm-por8>_p0&l-K`AI!&@$n5OtHRT`Np{@{Gd^B-8>wg _9KhqB7SM(9^|zll)ST~!a#vqt@tWUW zVE7$(5jE}P2OjMso(7SlG_Rm7-7g*;U$m*MtPRt4ZHs-T5b{DYYAEj$7@om#(4nWB z$C{|6{(FyPAiyq@`fGdr-#h7(JZ>*@0wyK^B0Wlwjh01Sef6bEI2G*|lLW$`pwLkK z_9{jr`rc!`xiIM~+=*7cwr2?Ahs%r;I7g`_;^^oo)ivi!Li#N`Vzpruv)L2@Wai~B z!jIPu&Wa9jMFKA#eK(2PPqC*Q>Xh#p5ZYb6e1A#nTeJEeQ77uEeOj$rP2DjF!XA#8 z6AT;MEgmqqn735#lLAxS+o;HM!H-x3K(Bg%j7&;(IO&8?zMTerfg-j}B^Ij` z65hiCnPox{L6__f@uP1nq1Z-HP!G0M(Rf+z!9C(yP2&Bq#;)}+VOe?#a&N39@CJue z!TghB{!9vV4izKw2Scp~=8ZJ<-tm24!6*PfHjcj2SR5Na(-=TD@MNqR|GZE*!4E)p zSeRMBHj)f#X^5t&u3vpz`?592s=t;Ern&R@a?*d)F?C4E>1trF@t`An&d zQytpdx$1q5=%Z?v?hpu3i5SX+?u~rdlW!}esEcvs%}dVkNsX>d9^P(s9Bc3;KJ_LS zSef=svgsK_K)(HK&s^Y;YySn@j50M?iF-JsYnu7PX#=WEi9OFNe#JH0yP(nlSyiJQ zEq$o`8vX`IHqV}1{2f@9^=9IO78yMP1%j*W%g;h834J(IZTHU~D!Xp&Z~e|k>U?IW z)h &VHKh}JLJdvSW&`gEtc?e|N3U$n<1CE_`76gQPSg40zsz$2Loy+U!L<)c$P zdfb;y`I=KN8s8+PyjQp%872&;az2DYGF F6$3MLE6Ff%`!PjWYbYOvy94<0=tCpSNvf1pY97F zZ%ga3qcWiiEUpaTV~xReTDxIX7!}Oo{3xYMCBRLt(>+LN{OuQ{DD5)yFes`1;LWc^ zPcx3x7)JxyfaIUpvdImDly~ml`DLLhZPcX_Q-4w_ebBl>U|Uq9H9qAZODc#P9awL# zSuJ; 3!%?ABdhP&HabA}E4irg3t!5KS#Fm@4^N`!N)K+dVn5J^KE! zwv=7)UP8y3D*Fw|6abW7=qo7W@w40@Vn`8eu2ub$+vDP~iM>|Az zDNw%69FH14HG@Q9@gAl0 z_WZbA-mFlN1@&IaFNPUeNfLTB?c1_w{zQ$RL2Qb?P8qd(4A^B1OUd<@#Xq#xyIbx& zw)`oKIp+FxWa&5;k0@TeCYBf >*hFIM|A%wc%2)_)L|LB+#~WeKvFnDWeG zh!sgjeh#BZZ;3F7=1R;yfYxcaHejmP&((c+YCE+x&4jE`R9J(Gv*22~VII#S(CjX` zQ{KR8 -~b>8|?p=Dt6 SLhn~dtY^gqp|+BYi|2-!Zvx2eIXZ7 z` (`G|wrXbqbGkED%7*jVALB_ejD=(1Qrw6ei!xZ|O~iOasVvo|e<8 z4eca+F^>F()ZjalbVyH{d#;s>7dkUQ(_~~Vrc4zpnf+Z(g-ibS=-|+jlmlzjNcZAR zi|Yq;JavtawFga!PMr>=T?P?Nv8BmZC!gJD0+>d_3`9rtOW4`-)pm*{M$SZ&x97eu zGWFW%$l4Rq!A~vqT!ecz^3yPkF9O}#xJ5vP#yGN9{BJy7s`D)t)IM!M0qvmh>um~F z46V2l%(f2QXoKVg?3m=gYJIEh59gW$WF9Z3imFfLd0JEyPRJjR$IQr%m2)MUZNyr2 z$xtd2fv%dQ^}N8gtGgqGC`^RciJmM>`y2`;mb#a&(;iuXJJ}KzSJ*3I_cX4gB&+?F z1COd^%pQ9q(go`lZhjxu+(H)B3H=7!De9qhlEF3o?6&XEL2oyrzjB$b*C?ER+F@@k z`>A9^Fu+C!Z$R%=^BObC75?EJsx>4Ar`qwYcDs=2dI`N0>zayi>nuF@`OKQ8uZUsT z&{2y5pLr&FNU%WbcdW4c!P#<~t5*wkinz@`vEl?UGCITBWG#~N(n-FH*#(-;`-5<5 z>%88jXz6$vt%HCf9~G`^7c@hSGGXewo~gy&4GTxXWGeEUYTv(_K@^Y+ZyTAzBlcC_ zX^*zx9)pG ePoAgRBERf$4} zJYthwAtDjCBtfS{nd{XHBJX@dS-0w*;xA!>S+pPvGyAg~-gUoz!k~i-$ Cv;tV MYeG MUTPN)POygIn_Y(Yo#U z&p77$INUszS32JtGN`wYP2XA=k}hn%Wk@T81uC_QXE!HW3yc&ly0=p{1pBC5*rmaC zcAO==5 DCRT3ckfQxKRR7RhWeGB5AW{!ZAAYq1}ge~(2 zq&YzogLFZNL>d9z78E2D+rLs>g55i3wf;0yAY2P=l-7TftXZ7NCGT@~eX-v_t3Rt= zkZ;80)T=c}H@rA^79>hA0)V17*O+`oa&CB48mD=j1nQcAve@Z)mW=1ld5v?@ja5hN z$AgVvc9ermz8@=Ph8ys3;D{Mz9Mnmz40>;POyPS;Ep8ovwu5wfOwIllGjQhXcuL*! zEMy^RPjrEUH)M<_c+cM;ipqW1V7q8H5-*v^ZB{@y{KOA>ENzjou@yTFyopvu!M`Uq zyPlV7j5}>8I=rvs*VbMvTk|#r^|+z)amrQgNq3YwGGFqjfW_mj9yE6EyQIQgELaaA zes^Arx2Sk+shp%Am(q2&o(~!D)1g;QOk^|{dBoN&yC3oRqiTdAf)MHPv87LDM0PU^ zJ55qzj9K)o290+NMGI&{vi47B`=lKVqX+0g=h&eLh^IM8o;-tPWp~lzVIG^vtKipd zG_B=b*|O3a@^~2 aUECzwTUcns=j<6 67`snF1uMnb*D))!SW-UP)J3{sxSR z#;WvAJ2!gcyD+3oF=N<19_J0fT9u%J&G2?SJqtcwGW$+(zK$xHCV@ZpGoP|tTUNpF znKIU_eNrPNA^6AnoHIYMJc9W~N?i;_XoV}+rljt>y`^o%iPo)jhvjdZKBQ#!`&p1` zrOb^Yio;UaO5N@0#x%&>pxV|47KxRnl-SbJ!g8OtF!~BFx9~*c**gOR+ZX$tX!%8` z1(g$TAq7>gvgvwnoIh$Z_ZgOT+pxc*dX>`f^7OlLv$XL*Ai82dzTMCg?*gx5G&HgA z3G0LsS;lf}p@n?r&~!aP=}>IJ>C0yVPqi~XPztHSw~tA|vX{ m)I925mliN>s4aV|Al|b5gv!@NVwIU%Gu7hi@hfnzy*0=IZ6AAHa z>OEArDa*1BrhT&jezIoffF>r-Hzzya%)PERSjQtm@g?&5%xN>oc46u#(%T*7BB44b zz+FHrkjxl2+Q64HhEmvLp2xvp!mAzd`u(?u7Y*E9jhA{ARFPJB7Wu|HO2)QBqK7Td zL&LCSD%@&jHg7Y-4Fu5T=VZt=pQvu+`+n*oAv>-6xZeQI|JmR_?5*GviN)m=sC2qg zc%SB3^>xK?i;1%=OJ#nUa$6D{!V%iPn&u~$g%`Sf%*+E2LABjo5$)73`SLv|=Y%N+ z3s#7_;p*;B;DVTl|8`jW%?5IxAPu`dhXm9+R`=$u0D4BZ!-2?Ys4-x^>xmCp6m(Ji z3Y$|zM1|Bla-g-=YYfp{a2 ja^yRjBYw6c`3#37$& zTQ8B_`vf6`=&r%!tFNUS8<|ux67gs#V{^X1OMtIwqDvo6c`t0u3vNH+F68u6Q0?J2 z+=#kNyEJ2)5YzDrsdwiQH6Q{qrt&NCI`|)7KiPimE{^z2-y)N+yZp 3=ZW-ON50t~~F uXhSkI3kH2sStueK zKs(I_AV}dAjM_8B+1t)&bKe%eACT$ipRAA)&dGAPPs&+H%dj=fuCatM;!$01XM5}) za{)!o4g4y7-VaVj=r|ZQQ0p9Vt-g*LRo?`TvZ?uJj2kndcXyjs6R2D1}f<;eapvb*=Z-#xB+{`;M#_mr M}t1$Xl}JruXmrzVrGhV z5p%3JS ~a}F)am5LxowF-L1M*Hf0H1KMIj)A8O0A^m7Sl6I+|4HZR}YOMxNe z{J$3?);UuO-z_Vh3ZJeuS~ >(fPYseP(t;>LH4 z*^Dq3R#?9Rl)VxkH**(3-*}~Kf_7dQx5W$l+ET*K2(|erp@@z$P}{ZWsa6`Fbv7`o z(@Ij2Scbj2mWic9ZsdU-Y|-6bcVDS+-|0c8;X;274EIxC)SAJU_F&OV{=(gR4QH;? z4GIa>JdEOOR`U`cBkJod&MLLu{+NrknTy7SnFY 8+0LQK&?W@vefZWnxQ(1rnS0^E2G3vSJ7I~`wSk65;9UYqG zuWR!%eiG&H$ylQM*bAFG3{8Mm6Le0eP`ri-?Afia(>Te=ZnN)c3#16HY%Q^K1CkSK zu+Ut7%sFl#O_=@cZN{&Z-aG4ph+1k?dpfLjW(rRh;xgZTptAm>5YxG!>N_MhJjv+| zVoMD)!2GZM Tm;?Nu76=M5s?wE`X0QJbLf(># literal 0 HcmV?d00001 diff --git a/bitcoin_safe/gui/screenshots/coldcard-view-seed.png b/bitcoin_safe/gui/screenshots/coldcard-view-seed.png new file mode 100644 index 0000000000000000000000000000000000000000..306a39041f88983cea37c3aef7e698e04529b72d GIT binary patch literal 14316 zcmeIYbyQnl^Dd5Sky0Fjwm2cd3GPtbT>=DxTW~1urMR|GihFS_P>Q!mfda*?MT$eQ zVmIyQ)!*-~b=P~>{r>kRIVWfDnR)iivu97v*( WTz-)OaW;Cz(3T0gutU&oJHyTIUcFS+GbrCJwUsMWdcw?%;?vEexpsGL!E-|t?7%Ma#kO#KnK}4J z6(3I*)$!K&ubW(}+nd|nrkkKMp1096-4r(F9D&@D_p~dsQ*UR3m$t73xVs#K+b4uq z==ZwsEN2dH2VI4JNtD6mjb8@4@^=eyTy0_R1caY8RDa#7m2`!!ynefKai11Cp5J}` z)?ndcedW$9;fLRb$dfG-?bsW$r?*B@6Lti!pf008LZE6Om&C#r*>z1s*o|54-6fyV z#FX~#iuuo3m)l ^Nb+eVu1Ic+qh)J6k$< z(NM5b@pk+o@ZEUcTz^Hb_QhezGw0(|z^pUf8^y%;lm7e?G%dB`gWs${J<(c6zU!MC zMcKUj{^cwhoSg_s3k?WY-6l=_vDz4G=6HOeVcr$#+1nq?olY=vo^W8^>*maxLF37- zI=;LkzxmbSyjFfzmki%el6N@wKM4+T@z~2V{DK_%+dcH_jG`c=-EGfb6kTvTT$5hT z4ju-7nU%qQS-};fiq{eBQfKmu=uw(RAwGp**PSIOJJ?k!))!$7&VR$wjzy=fG2zji z#`Y~=I=&b^|J{z11Gi}W$dN&*5JRluDY?pP70*xaXng^^g^#L#TK%-mkZ*8s9!Z?; zc~Gry>|A|j>)be}&(nE66VL+GHr21fa-11;`r0V)v8*sb!s$79FvpKvsbN&Y`Gv@q zv6!d(R(}TtMOD)&u;YN-d!>0WF90yG^<_$3(t9A=&hLEvDB4GeULt4-K~-$x+HpG% z@L4{iY%=&z`L^?JFF|PKSE1ud^WE&A tPOr6yZ1b@6z<@VE>k8=Y~na%)8Ga@vAQw?D;+hT?SFy}?S@boWp0dkayD zFYUo!+wKn*R=h_0d$y+P=t(Qe>Ov%btU1lumXBuxR+hI^^HxTKu&AZV1?|-` z-N%^X%VWrW>#O$n6z22dX$ddr1a_+gC&4>)XD{t&RxOu=_QaPTPLAnBCSE^Rhi>=6 zD4y*5Z&=oEbo3vpJ01!r1np%SpUJIkIruc|`izOwMrCGWPz$7|o-`rWAzq0TbM)d8 z#HQHn@S}uc?}YQNUfp9O5 l(L zk%+4mC1wUI5w))A0f;XIvMc6&eT?w$1*8LL%#Me`Hm%ghpu0j%>+gj}T1=KHc+yCu zYEV2J)E$24w!eQYKCVQs!Oc1CH&rOLM4?wSwN)=~X#p-&)mVsx_OgZa2&$ZV riyI>YlB?guZJYV;3882NDkeJz3K_-pR__Gr4t@`#Vee zf?{@x`dy$y;4=QF`if )w*QlDQz3^y|dFc2ILObC@nR6 ztI*Uoy!+y@xzSX2kLZhF`!Wru1k@j2fQ(<-zRwt}*!VKQy*TM8`KZ^Mm5@`-!F@!< z0i6TIy;MGUCYcw^`g%74`{31%b2fRYQ^C(xc47y&hc8hRHNLJ66R)vI&jhG&-6_EC zqQJ$XGa=Q%gz;VR!sc*d 9-Ksyy0mBQ#qn6;F>J^ABN_|n314%HinR!Q z=?i`u(QD=td{_h=etVj@oySkH$)Q@YP>Fx{%-wPIdaO(Kob#~l$EVmH*w!jO^{G#Z z;g;zP9A5Uu`&nxd6UKYWj0pBq`?QrQ!W>reXk%vZPp6XJZoKzu;p|J*GG3j@alc+} zUzwX^RPq_>A&lh8op@ Gq6th=7nn0ocZbot#C}XTJ!5x>PHE`byOPz6f2_-C+4vWers;BrDR*u zq6CI_Sn32g&S!nfoM@c4C-~Zja@@M W1x~PNcRjZ0RMpo*!ae}tPOG2n6LFC2PD;&ICZAw+_$2B^AD0Zr4edo6+6E*wX zIaEXSw8uc06S{(5V4J6bf7btt!^R=)Xg!2R*PD&;mYOOy#E?wmvjiHxv6uDI12X@R zJ+!0m%~WA-!?T|`U_)ZWbaXtq87~4!MTaSxnHJZ3dZ }33V@k? fYERIcjy1IG6f372)|>A^x;Rid8G zPu{(30^zEGim4!!W+^?(ceZu`leg}o$wtyuMQ&e6`a%IoFYKd7z7obn9ex^MwRC}_ z+#bDGLAix16Ms7(+fuYIY6L)fLMGg6>%jCejTi;0VgPcKgO!03>Smt^f;Qa;`tyb? zi>zo|HtTHAkh)-Ude?xui-em;1+- chSS=6e%#@k0tIUS5Y7lsZwZ-tGJuE|t6a{I( zZll *~+cjh7^{VaKh)27&Z8p~8dgZ37GsXXl#P&}a&9?ON5L{Okhg z?3s#?il+@A&J#cH^ii3Vp%<2~j!DGum}I-Dl&J>fT35yGOrl$o?ErQY??de6;|o-~ z3zLL{`bb)a*M}!^G2diw>BP%<&9Fur^ tTa zz{=l3Oo-i{kXKQBr> *@G1z~ zs))8*-G|uf#-h*5Q$#J!gtZ@48dj*ECx>2S$xV4WbSzSOVQaZPt KEYtdk E;D>mUMB#0aX4Y}ax zcK2Tc-VC-Va7y`#qay4GPN#%qhcf!M#$mtY2o&tcb;Fq1Wp1Ftugw7s0=(xRbMf8+ zao^{Dnz r%B1Fs{B6^P5OB=GAV8)ngOMga%x>^=Rk ;rucVk3 u`t^r+zY5(^PuE?&PO?<)_wmpsZBcaVrSOsh!co&*upQNd^gvp zR!h(u-|H;|Lh1DVvcnRlenre#fVZb0!Wb16N4rP3i((NO)Kq+v+!1)5Iv9=ps67C? zB8C-#E~0_0ubf9?S`z{OL0L`c%{rI5VAbsL2?a}5Cg1TV4L+XO&)s+>Rxgy}o22CH zu$8OwZPHR>%c1C`YIbc0Hiw8^BSJ2jG_lWw^7GFg=R!0YO_SmSx+~W`YkqXu2Q$jR zmx(!Z3F#fM&%uKr; mwjmz}_kb z^+Wb~=g6Y`7`esn!a*S{Aj;KQ)YM`$X<&Yjv|OkZnl$=~Z{z#xCGz-e-mW@vLu+q< z#0LUcWpWyWR@kx@dv6&ZIY}lZBCw*DZ2dl8G+RvLu{-RgpMhA?s4r)*>i|M3#uT@g z$_Q&gr;Z`39wd5<{VpCroX@E;M+$nh7f_i}llr|y_}fPG(bnTMpR;S}5~2^~r5dX>s#ZsKpS6%=B)`8<8^s_7dVAhe@0{K%Ub)5b=lOmNDbT=*)k- zY5)LItvE@v-o?*9NvizuF5akqoN57`o>~w{N*=w3Ba6Ywi>)fz6cCj;px>-K vVWo0G LRX`tZ zXwsn0{bp0c0i4^p9TN7CvxxO|l0lCzfHfuq5=$(%Y$_T3xqi9;(TCfN)%w-Hijx$N zz7X!KGDjY_6Mb|q(jd1_>E;M6ev-t{u=DT*Km56}Vtr|f4NGx_3t`WjV+JhE#FCZx z10nZ?ve!1xt>W)CNu)G4 =fu8>A2 Yq*Y5tpDa ze388FZ>3P7dqkeI)ewZ5Ru%hf_dUxlKYEA2Dt-?A=}dO(`^~l0VMD;z4;9c78j{je zy7e^_p(G%8!HQb9JQ~lheOz@IEzF{k5dFbmCTrE3868$>Lc05gW_^QM*Wti(GsKS) z-2!H{2TZ6qwjr+xuZk55o=& RME|mNxn^WWAn~UjI|+C616??T#v}H6TuHG z=A;MJ3~_m +GTgqXQqMhjc4yVE?NVy zajH3{M!GQacQTTdpY)qP(|hnvMv7EA0?f$QAi=6^@B89yz~!Ys#g@_X)*UUh$&&Xy zD+#ONQf-V-jGbULWGiSLhw~H1vB+_&G{xg*-`dwtXtq*%(Z-)s&W(t =r|9RUXUhReCg4o*H)_G^H(!HX);I+J=23k7G1HOJV96#!OYDS~1C9 zD-+$vY-A1 ;yukdDFYSG^8hq4Y(jY9b!xEVybsz5>{2VRREPttS|Im8DPRA&lFbDu%lc;S92Cj z?C490q%zu{uApE?zc-AnpA}yg|Gco!Z3nr%MM=ByXe9m)m_ FWlyc7!1qtYCKbF5*mw9o
<_kdrIKvQ72480<7f*< 6|$5ePR3508(J5737X=;~p| z0~Qex;Q{gT@bYpaHMl+fTo6!SZWmAH-w=Oc$iY0VJ?z~O_O31rzcHa!u3iXnCMKku z;qU%AyQ!)D1K!2+Z!94B;PHjJ@qmFK9%pBsf3@&L$a^C}{^rpC*1}U4xqr%|4fAyM z^00= N2uWBm_%H!lyTKjGL|^T3>7&PY{H 66tv~D0)x0=yf8s-ervEbw+Js3d9t?V=Y#TE3)%?T{tJ|{izfo=Vh#HZ zg#-uMBXI=4yuAE;*4ErYAQ31xKhlO9DlBBpZ7mG81zGW0gJB}V{{o@mVUNs8sMEiC z^&83t2_-BnEC>>^73Ss#gGIRc1#CgwBK#l$ZV@4{h&2>!#V>5j`v=O#8lvFp;S5Di zr@b@O4#wl=V)sYzn{bG zKNu{)$14IB5I~0XPbGbrhbJ- j0sdGZV}Zzez@P|M z4_#MRCvm3VE;0PJ{D)W#xtwgE2&fzs0Yid xBOwT`DF|hraDE5EX_JPBG&jOM#ZV;IJH(?+M zNEiYV`5R$8|MMXV35&pNd66MP!2-x^7eW>>VLoJ@f`p+~*4Ba`Fs~5bzdQYZA0qFc z5dX@F|4)cwJip80-zQa!=l?hDe<}P+9Ys>}S0A!IBg;L{KkEJ8IQv~${|8@xOSk{Q z5s=XT9ppda_rG-gm#+Vaf&WPPzuEO)y8a^u{v+l8X4n5Sy72z_#0Yai-st%tp9de- z>mmO_f`VnGq9})Q|NEWaS(=LM!F5wI@ _WeFk(>V!gke%2FWi@&1Z8TauB0zV4 z&=3j=8I`h}w65>`zF9yO?R3!H*uma>7f&=zZH{GA`a((^TTs#6hPhiI|BsIasE;er zinWrfjBym@)1oiYu^moQn8VA;hBjp4-DOm?%h|ymoQ)2-N44DZILY&J@(vEEWx>LA zjNeacgAViVzzrP@0bA~yBO(oao?RNoBTIAHBB!D&uTF2rHZC5aJjMxQKz^7$=i~ml z%Gdne#0irL{qv27`A5t~`hOO>sk7busBT%`efus99a} 2sfbI)%L^Je^nckK1O zh MP2Z{>lCmCI>oa1bLnusugz09_)hJWL=ff0az~^$qnlbw{3BMEH;k{y@LmdS zjMD{?UIcCOeZ4a2j@4KB3;{yD$%DHZBLKU1G=qeI4?2Sy>z!%_8|Mr6 o{DFkHnlGfCaj(NQfOBCr%N+kcpD477zF~JdJ-c z%qqz?n0TzSqH+2{)TSk1f5@l%WQh5sv{TRT+mQGCP;%neE;EVquOEhdG6BL2D`tLL z2lLbU38vrsy=I5)o*e ^9z%qp2}a4@ozJWAClww^oUz|bD{j`>jdxxiC)yKH)SbPFcKbg3 z^0?X+PyjY{#)hLY5oYdv4UH7a>pN4CEC$ntf9(PU9KL%L{LYz1;vE$8rE3R(r?Zj% zZP(L<)(EebrKsp z2#oc|!8~o9ZNj%-pT@M$c z#}8H#i|>C~m|k^UY^YewKkaReNPI?0bhsFGeRxmv?u>l2=OJboWv_t6EeA08PNR0W zi;q7lUf2^$5`sK_Enm28mzN$Lc#f~l69qqLXt?iD?|r!Edn8>I@cpfOKUvFa9A`)# zackqWOvf$i-f|G%vy;@r)uXire$q&^HKv+p>$rixD2P@|%p~){IIB&wZZ|*eZzcxx z;xM9X&t&O8j7eUy?k$Quv*GDFZz7`=@xY5F^08;bPICV7I|*BNts9LKjssLwF5}Bf z-KNyWiMki-F@2V=9KVJ1e&y#2H?RnL;T$S^x%WL&76*JYGP>5KQ|fl|%>`%T1FOWu z6AEOe?#>nx k8F2B;=4i<^@aj`*(-nW$Wq;-zIq@C^ZJ*l@WX_99 z-20jO+@4*YMMV=VoUd_OTuEeP(Df1pZ{OQo?HRrETcZ#TIKPLt<*a3P9bw+w9xQ#G zbg}-V)My^7Q+mFB&~5Sr+f=GDvYfj+JmBC~Xz!~#MyYz!@tElSa~jt`#Am`JY&ptr zGPjJYisnpzmb)RkOX8pUz(l^>eqKx*YMzu7f)PR7v(kG{c$abqijX8{M8#23!g+Yp zW*cI~)t0s={h>P=XY6d;9wmHr0*yrc-gFK<0_P(oP{nP (nJS4XdnKCb35I$}nhb(2Xu4@u8qQinVK z3}&58gSpk^tA7xrYnvN7d^u`s8JuY9_6k4zR|aR&c8{aeV{F=QA;fAp6IKCIz}5AS zynVmUVbbg}12|+e_d0|e7B$t&tDDvhlxQKO%;_t^x*`gc6WxmUhWF6x0ZOnok@hbl z>N}sKB-G? B4_bOBs_Rs4rcg?1ID_? !~{#}+Dy;W8gwDTgql+iKs*Oejq#VTYMgu7zz;sMJOxNS6*oOgn|K*&h#K zdaV7Z6l7ZLQ&Y=r2{gz)z3?cKR=K0VC9qvQWVUk}#`Nf62l59}gMz9mw-K9+)T5s@ z$=g C70`j#&)ATVR6T>VYW^vuWAOph-Eo zSNUb@4dzY4j;j0>Ck5Nhy-}^zg;S}6p-4ZNukugK9s*t`1XPHrXbFOX)ZR+g%C*+e z(lAggk}KqniABIEA0@!N>EeI}WPoyEpspIFhIalJ%)Do*`CCTYw7cClYa7aVT4!QY z->ZOO+)``%0$4PPfa}5Oa_+Y?a?HA~2L_+R_ZGsL6ZFXQk4-^67s{PHq`1^F(Aw8u zNY;7*QDb#%@-FLH2_E_cy=$BA13s_H Hr7?w^=yHh$PJg{-s)ZKS(=h9jfTnk~x z$3%U&_uaHf&>_FFpm$I=IMaJXL1cHmf7?U4#U*9WqOns?w7T6>`s1`Ia)iEY;wYnK zJDpv2UD_Ca)JH;TZyot`##BitPU0e4eTJhbl}xf|FU&HmS>_`e?GoGVjEz+1rc$@< zizDOsRB4q{@p8RKGA@dA#(oi+U{)^TKYf^Cb6-!$J|OVfi cq$S4yiEKCB+pxsth2E{x+#uLl|1qW#Fv=lO!a&6d1VaG&im42H)Ad9 z;`ZZ|clV|uLK5E4<$BE2Y3)QGH;zVDIkY|STBW0i5SV~3ydY(Ac<}-k`@<%V q(JN?+uZX*DSbv1mPo~)=wp#* zkt7&FSaan|?O$ahO4A3yIgPDq9t^tjVbzq(kSVri(VHr `4qVP=s=VyzRF@AzFa)f?L?qnBz}avorl@=ElGW7c{Z6N&vp^{ zS=PDkNk&6eZ^4~u5q5rTl_{G}(MO1_Cl~dteOVV=%ztl-KTbA}8Gyr7VRjaRRg{#i zQ56urr@h6*h|10==M79t{^B*fDU8d=!N3sFEw3%)m>;)2cI{GOv-4>kgLJM&H#9SM zED(djKl=*ap(gNfUYz@9bLFkfWwtoGK@&Abfkf`7^>r>3RF6 Yt%+yMKXWMyR3z
kr(nAU2YixH&na2485h!@sS zJ>m8GXwOmX`3laT3kD_1LVHg&D$u8c4(yq eKx z#2ad-UfstEkLo(~dX*QR(`C;BIkvnywrX2J1UR^6z^_LfPG9weOp0-9D4e^+%xzx5 zRUhHQK24S`Bv;t(;M1$%w-&zS)+*&HNg51Q!oRfsaQrFfb~YVxEVz1wvHjhp)=4M{ zlHBPOwZe@*_dH^Yv{x7pVmkDrZEo|q%a;zWLMFjoTkYN{WQ}DAh%L~P=X#!)m9^P% zfXE}b)~Hx&Fe86N7_t9zPaen9r5m6serYC07A099L%tBNRbTe~Bp{2a57oXY&};OA zeZ5w8k=dl2b%yZ0k(h|T-xX%+aXGduq1=~7qlwro#4KA@&SWJoB-hW^3iRkn^}~4< zMwW+#jIoI32Ge=pj7oC5Rn}ner%2vzOO|(PkCbDbG@jUuQwq~?QlGxSnif~Fpo%Cr z5+&b~N;Bfu=0eG%CH_#QRd_e!Y?xY)HlO3Kq+*$Ro?GF3k~SsR+_&cX1ksxBcwf-q zmb0E$LHs4COU!$iQ(KV_D{h1ey|P`Q7vVj@v R!Irf;6lGT6Ya`M;;uUITlpb} z(BmK;UM^}28yq`3;wPlrQp88aOng}L{u32wv2++Y3>5XVKDqkx7wEt }Bxqz({+ z7{>xI5rxxSH`m$r
|AkQQSH|eI#AgYAeWcVQ<7Tv^{HTL|D(~D zeZ?{Md2adosiq*kQsrbGy|>xX-7vLz>(Th<#0jaSJc1CKrOwk5v{tfo1&K`f12o{= zYM9HfD`9Zn?28gbtRHCrYZWt6ddi?Bws5;N-dqZ;!MxkBla0i%!X dbE)JxxqMkJ4r9U$lFV?2=|Mik9qO z@MQTD#}WkUhI&QFej)ZSCgq)t4tHV=#>JwNy|>+xnVBFq_|cft(_%XySID)uT=PNq z1SA^BnTOwg&OShQ%nZ;Qc~OqXQ*rXC%puMt #V07(m`vDP8Mk>Di$k{^KVMV8C zuPM=SLET5MDvw+DahFN+ nXeRQoa+e;i`UxIR zs(4~9?T?lNjFJ?SDc`oA7ko9x=m$tZ`{EV+kNaIiU+Q+GZAyJ@ynpQTD9h`8#q{js z^jV8S6zGU$d&0Z&(+{x34uzHg;PJC|fZ|Mj#NorOwy$~qM+WU0J>zHjgOS40$)&xC z9d_EeInU|}eGB|~OujcRDFJ~aa&ZG3J_kl^nf&*r5t;&C}Yuh5|lTrABqn@vG^ z7afF!%38#`#;4k74;FTlqT5)U4owcFWAuqXw7oLTpS9qk2wY #zl^Xyz7)iv5_srkG{ 1G7+v|t3Mxa2(GKx9N6r5qiZ@WVqv)=ja<*au){_~bPa=Idnh Hb&j`#s{OEj0O1rboY+@urZEy_JT@`Vxvo%w@z# zF@!n9!hq4X+KRr0MqxUnk~}LBs!5z3oz4O?<&eV9?H&3OzUsyoi6+l14J1at^pk^m zCx1>%BYTlm%nZ^^d`LcBK-}9Subhm$t-two_|}8Mcoru(?g&LM*t$SWy-7=?)v{OJ z&wG7t(}rv%nd=CJufrz(oukqBesKy zJILon+KvlO;b5yiNjcB2U_n>L!}^wU@#xuRf4}_$rdln2;dn-hh^AduKR=E^RV?N> zM~jm_XEy8eZe9p+yJ_u&`L0`!{`pwFZSKz-`?5Wo+Fat^C?*vXm9>HkK3`iZUxF~E z9vF{@R1_K@M%m^|L%Le6x(-n@EzTp 1CC6R4ylShNVV4V9F-DA{FB-p+1sE0P zqF$R);63YJJxm+>gjoO#{7jLcH(WLtiM)4b_y{_oLFU?(Oht-U{Zw`|E4D+OFHtZF zN1GD{SM(N$j2|b3x(E8R_dEw#H^#~`(=h9^zU(6y;^4iz3 ;`4s;KV*$PXNP8*vGO!!j7_oLh XT4&qyIlOfe)lM}*0qr0Jn zo_#}0uJ+7_7r~3rrGK_D7%yKxMj082o0qUj$$5N(sDH+PNd994Gprz)fg)z_D5C75 zkv}ptZfg~L?%^s4=2lxR2s4%r#U<5?j^rEK8h#``#R>+>yJM_`HKt}4GI&!7Rxsak m$_6nKAs=1(x!c-3Dj& bdvJGmNpKG?AwZBoaJwY$ zdu)C8&spoc|IW;GuPs&2u6lM=ckjJ=LX{PzG0~r+0{{R_Ss4jc0091XSpyaM>0E)= zaRLC~rFp1pIjI`E0__}Z&7sy1pp&~D1PF11ngalCO9g4#arAuFl8?>kY<{#dtigyO zsK;HSm!dB9uZC}8R|+aNoIB+4nwk*g_U`9>JZ`Wbel@(?H)UO cyE_=a zex1DBIo@elDqQ)nbI!fJ>pvnFS$Q}geXx6edV9XccXO?SwH7J7V!1aiyxjTFdn9~m z=+inm9-vdkIA<=*w ?tET2A&V^C@ass;{M{MR(e3&9?tSM-*BU|A(Xr;>c)!nW zvr>sgz;2z6Av4QabA1+4-PKn$K~J?Sy_a5|$(@>Wm+lW`AN=psG9IpKwPOcWZ-oth z&D-2R9NDgv2-C~>9BWhxC)afnYCl}vL{}DZJ~T5M?mhn6?(ps?_3_ddx~(L q>iOjT?T *yAe%1J1O2pN`ZX_l*4C*fv n8 zR_B*8Q(L$_AaYH3H1@yXX|dl}Kd2;qHSKL966*HLh@)T ;sQT4RUIZ2F~7*pU? z23$!pRsu&!_0W %#QCBEMVlsZExVe)FH#C0rhl{1gF~Z&Fj;;w(*e{Nt z2>!W+Etz;$afw32mzj&^N{;fWjup+j+OTt;q_XC@rGp sn*Ms!AFyixETX_XXD_)x-+eY8E3M~~|utLV> z1nshS)TlI7o7$lt@}~!f{mWkVx7U0nlNyE=Zfj*nEgcU>a}-*RHk-_vy0%Cre>|SF zVHQ06a+vMmgrsGcxF(P=dMDEs=3-bh{*W%oxIF1~Sl9qZGP>+qd6Svn;9ylacS)73 zMts$? AO}fXHn`lD=Jr>&S-%-*wOlR4!5HiW)?nQ zR#|peSHERklqfYj8>2pxIh>ez1tnDwavGaUlA=Fb|Aeg1&pzs{JO8p8^WmU&VOikR z2I)r~GO*kx7;YLNd}$k`6K1QX&u+3>2sGHO=1_S7x1V7^QTWCp0)531!X`HW*q_EH zd_ jppdVmngtVK Mlx4 z>;Th2xw@5l8oEplenBQA+bp`;sd1Jiu5TP9H!xi&<~9tDJ_tHOy?zwf>sIG 7KXNNVme{gimkX>z`>M~p<5OzH<`RO$p zvD7RqF~0`(`uAez9i6(hXSRJdR@2(*N|eyvkKNNH_3`*Ao}MpiT{iZ*f1pHrem4mw zpsME7=M}&YcJR{=sbHXhg(2OZ(0Fu7)LC%>`;;)tnJ`#8KKHP4qVjB#@fSYdNI~qo zG-{pk$J^b4t+Nq0pL0t4<&97APFi+kFX<;>ZnxJ0r)bT5>qU4zQ{1THxm7TDl_f9A zw0FgWdO@0QUKyln8f$Hp cDL8Av%PL;|yk`}jLJkf#Ds@?iS(-i}d^YmBopaN=_RStP zRSTmqEqwFqfg&Mdy!?gOg9x1XDEm(39*nG7XQGn}UQdz8Yn4k;$&FU~&!{MGtC2qU znXkRz;SkKq{B}g)g8co=*oah0jY+0cBXAE1srw$Nzm54lp&C&_TNyK0G>+ y9D$wm~>NrbD($3m=31H$Td77?D;jR8lw3 zo_k{k;aH-AN$T?guNcO4kI+nmPd{sO&q{MqOUP^bj0( Jd5fttyDU(e%vt$6-K zNQ3UUtZyACim;RiAa5~4lnJl}35ok@7%MgPpwNDGcXzh?JgpcHa-IOyioBaj6~(EG zH7<*)tgX`MmPWiUE2d(=#P=ljSGhCzfm^F}L`w1w`-6r{Cbf^t2~Pw>{hNfbk(L-k z%T)t-U`|e`uAcB2T`j(GtxY~_G+9*ovZuK}B`b|z(sPmCL5}{VwLnTLu2%^myn3gz ztXF&yj6qGHE&0H)js1bz8T=k`W@h?)P9Y6}V13eZhutV1D?|T59^{t&?sAj5g|{&& zm~&*Keb@05UC#+U?Kly@Zg>r@2?bY0Q#q>0Dd9-nef?ptBCvjdyOizVlD3Wc@v5q( zUkbWp qk4XV}2I)FWa|Jz}gPVsOj58yX_#O!z5u@>1g)2TXB;b~L}1 zO{%_)iywV4N@I~~kIuH7Sf?&d{K>( 8WtVyvr%+)8;^ zlA`-bY>SF1QE=#r%6DQHWO`YmUHK2h*jrubzz*Hwos(yjceSxYJ+E8itKNME4@c~C z1Z-2!U}H CgD)etmA)YRZ*sxlM-;I3VfxUuL$3RDF9tg~i<{O*-CjN8y z-d_#<5T`K4Lp;PNHsixV+J{I*y9QsucQ~H9MRBMTS6r+UA#@{6k%>dQW|D=@_!Gv~ z{cGloaqhknIfCdqY^qVoo ^tKoO?MAt7qtHZ$h?l7mhFuPxdKf zM!M)}f}X1(vBXrJte`Y>*Kdj7R1Y%d-Y;Vv5EMp;8Z#q)A>I{8%DcjkvsnY=AQ~Ak z^W#2f0QqCnR|1@rlu8Gogv-J3NE3_~q7I+>#PEPs)5QK~uX_yg*$^ox#3^XhsTDg6 zuNoxcUbs@rA|>XRRiKude)kBcmA=BY?c=3(FQa#eBK@FFYv=7y(>&aVS=c{725;5G z>p+l+&w)CML&HVuSDl9EmK%sqtGh>4o*CF20Sn5<*Uht9B~Uz678(&tKl*XdjbJn- z0A-FiU@9ZWQZ));h@G#-anoC!Lq(7}5v)0Z(Bd=*A(#A|L%^d~J$QgO>jiSlki)-p zI>__cut;>F3E2DkGS>qn`3tX1{0^m7X +f6d#MwperhNgk;2_I*yl0 zOE6USj4gL!KX5xY9CRcvF#PHnt+c|r*5?`8=RAc!G|-6n-EPhZ v?Ny9OI!c0KZ0=jtnAll^_b5ht7I`bD zDOG~x3%yE=T!lXbR*xl-H%5zPcoq3yl)3iZi?_xAz6X>Q{g6HCFX{D4d4?8elDk0W zF$4ekY(03P7ylLT0m0{>BWO??}u2Vk1cz$`^-=OCSnJ(IBwa zt{}8pnV(YpBeuHzB3uv*am%!KI9~qe57MrPlR%JT2Tdo2u4$|akk$h}0wD7tE>8A2 zh8&h=AeD*H)h7v|^cQ0^fB*yhMI7u+r+4ut1AFwf5jEB*hGv pYYLNicDL_