From 43fd53190c5d4f21bb23b4a0745fe3505f92abce Mon Sep 17 00:00:00 2001 From: Andreas Griffin <116060138+andreasgriffin@users.noreply.github.com> Date: Sun, 2 Feb 2025 07:55:00 +0100 Subject: [PATCH] Refactor (#74) - Refactor - Added restore from seed for bitbox02 without needing vendor software - Fixes for small UI bugs and CSV Drag&Drop in lists - Stricter tests for clean wallet shutdowns --- .gitignore | 6 +- bitcoin_safe/__init__.py | 2 +- bitcoin_safe/__main__.py | 12 +- ...al_carrying_object.py => category_info.py} | 40 +- bitcoin_safe/config.py | 15 +- bitcoin_safe/descriptor_export_tools.py | 13 +- bitcoin_safe/descriptors.py | 5 +- bitcoin_safe/dynamic_lib_load.py | 18 +- bitcoin_safe/execute_config.py | 7 +- bitcoin_safe/fx.py | 10 +- .../icons/distribute-multisigsig-export.svgz | Bin 293552 -> 38806 bytes bitcoin_safe/gui/locales/app_ar_AE.qm | Bin 97842 -> 98929 bytes bitcoin_safe/gui/locales/app_ar_AE.ts | 135 +++-- bitcoin_safe/gui/locales/app_de_DE.qm | Bin 112398 -> 113625 bytes bitcoin_safe/gui/locales/app_de_DE.ts | 131 +++-- bitcoin_safe/gui/locales/app_es_ES.qm | Bin 109742 -> 111067 bytes bitcoin_safe/gui/locales/app_es_ES.ts | 135 +++-- bitcoin_safe/gui/locales/app_fr_FR.qm | Bin 113642 -> 114867 bytes bitcoin_safe/gui/locales/app_fr_FR.ts | 135 +++-- bitcoin_safe/gui/locales/app_hi_IN.qm | Bin 102858 -> 104065 bytes bitcoin_safe/gui/locales/app_hi_IN.ts | 135 +++-- bitcoin_safe/gui/locales/app_it_IT.qm | Bin 108622 -> 109791 bytes bitcoin_safe/gui/locales/app_it_IT.ts | 135 +++-- bitcoin_safe/gui/locales/app_ja_JP.qm | Bin 83824 -> 84773 bytes bitcoin_safe/gui/locales/app_ja_JP.ts | 135 +++-- bitcoin_safe/gui/locales/app_pt_PT.qm | Bin 108716 -> 109941 bytes bitcoin_safe/gui/locales/app_pt_PT.ts | 135 +++-- bitcoin_safe/gui/locales/app_ru_RU.qm | Bin 109164 -> 110169 bytes bitcoin_safe/gui/locales/app_ru_RU.ts | 135 +++-- bitcoin_safe/gui/locales/app_zh_CN.qm | Bin 75210 -> 76017 bytes bitcoin_safe/gui/locales/app_zh_CN.ts | 135 +++-- bitcoin_safe/gui/qt/address_dialog.py | 26 +- bitcoin_safe/gui/qt/address_edit.py | 52 +- bitcoin_safe/gui/qt/address_list.py | 122 +++-- bitcoin_safe/gui/qt/analyzer_indicator.py | 15 +- bitcoin_safe/gui/qt/analyzers.py | 29 +- bitcoin_safe/gui/qt/attached_widgets.py | 5 +- bitcoin_safe/gui/qt/bitcoin_quick_receive.py | 11 +- bitcoin_safe/gui/qt/block_buttons.py | 88 ++-- bitcoin_safe/gui/qt/buttonedit.py | 121 +++-- bitcoin_safe/gui/qt/category_list.py | 38 +- bitcoin_safe/gui/qt/custom_edits.py | 2 +- bitcoin_safe/gui/qt/data_tab_widget.py | 29 +- bitcoin_safe/gui/qt/descriptor_edit.py | 66 +-- bitcoin_safe/gui/qt/descriptor_ui.py | 85 ++-- bitcoin_safe/gui/qt/dialog_import.py | 39 +- bitcoin_safe/gui/qt/dialogs.py | 2 +- bitcoin_safe/gui/qt/downloader.py | 1 + bitcoin_safe/gui/qt/expandable_widget.py | 4 +- bitcoin_safe/gui/qt/export_data.py | 167 ++++--- bitcoin_safe/gui/qt/extended_tabwidget.py | 15 +- bitcoin_safe/gui/qt/fee_group.py | 37 +- bitcoin_safe/gui/qt/hist_list.py | 71 +-- bitcoin_safe/gui/qt/histtabwidget.py | 31 +- bitcoin_safe/gui/qt/html_delegate.py | 4 +- bitcoin_safe/gui/qt/keystore_ui.py | 123 +++-- bitcoin_safe/gui/qt/keystore_uis.py | 12 +- bitcoin_safe/gui/qt/labeledit.py | 7 +- bitcoin_safe/gui/qt/language_chooser.py | 30 +- bitcoin_safe/gui/qt/main.py | 426 +++++++++------- bitcoin_safe/gui/qt/my_treeview.py | 410 ++++++++++------ bitcoin_safe/gui/qt/nLockTimePicker.py | 5 +- bitcoin_safe/gui/qt/network_settings/main.py | 11 +- .../gui/qt/new_wallet_welcome_screen.py | 91 ++-- bitcoin_safe/gui/qt/notification_bar.py | 2 +- .../gui/qt/qr_components/quick_receive.py | 14 +- bitcoin_safe/gui/qt/qt_wallet.py | 453 ++++++++--------- bitcoin_safe/gui/qt/qt_wallet_base.py | 46 +- bitcoin_safe/gui/qt/recipients.py | 72 +-- bitcoin_safe/gui/qt/register_multisig.py | 15 +- bitcoin_safe/gui/qt/sankey_widget.py | 2 +- bitcoin_safe/gui/qt/search_tree_view.py | 30 +- bitcoin_safe/gui/qt/spinning_button.py | 7 +- bitcoin_safe/gui/qt/step_progress_bar.py | 45 +- bitcoin_safe/gui/qt/sync_tab.py | 6 +- bitcoin_safe/gui/qt/taglist/__init__.py | 2 +- bitcoin_safe/gui/qt/taglist/__main__.py | 2 +- .../{main.py => custom_list_widget.py} | 377 ++++---------- bitcoin_safe/gui/qt/taglist/tag_editor.py | 230 +++++++++ bitcoin_safe/gui/qt/tx_export.py | 3 +- bitcoin_safe/gui/qt/ui_tx.py | 151 +++--- bitcoin_safe/gui/qt/unique_deque.py | 3 +- .../gui/qt/update_notification_bar.py | 4 +- bitcoin_safe/gui/qt/usb_register_multisig.py | 1 + bitcoin_safe/gui/qt/util.py | 47 +- bitcoin_safe/gui/qt/utxo_list.py | 57 ++- bitcoin_safe/gui/qt/wallet_balance_chart.py | 16 +- bitcoin_safe/gui/qt/wallet_list.py | 3 +- bitcoin_safe/gui/qt/wizard.py | 387 ++++++++------- bitcoin_safe/gui/qt/wizard_base.py | 44 ++ bitcoin_safe/gui/qt/wrappers.py | 7 +- bitcoin_safe/hardware_signers.py | 7 +- bitcoin_safe/keystore.py | 13 +- bitcoin_safe/logging_setup.py | 3 - bitcoin_safe/mempool.py | 16 +- bitcoin_safe/network_config.py | 10 +- bitcoin_safe/pdf_statement.py | 2 +- bitcoin_safe/pdfrecovery.py | 2 +- bitcoin_safe/psbt_util.py | 6 - bitcoin_safe/pythonbdk_types.py | 6 +- bitcoin_safe/signal_tracker.py | 164 +++++++ bitcoin_safe/signals.py | 18 +- bitcoin_safe/signature_manager.py | 6 +- bitcoin_safe/signer.py | 24 +- bitcoin_safe/simple_mailer.py | 1 + bitcoin_safe/storage.py | 4 +- bitcoin_safe/threading_manager.py | 9 +- bitcoin_safe/tx.py | 7 +- bitcoin_safe/util.py | 9 +- bitcoin_safe/wallet.py | 71 ++- poetry.lock | 320 ++++++------ pyproject.toml | 7 +- tests/gui/qt/taglist/test_main.py | 59 +-- tests/gui/qt/test_data_tab_widget.py | 12 +- tests/gui/qt/test_default_network_config.py | 37 +- tests/gui/qt/test_helpers.py | 122 ++++- tests/gui/qt/test_setup_wallet.py | 301 ++++++------ tests/gui/qt/test_setup_wallet_custom.py | 142 +++--- tests/gui/qt/test_wallet_features.py | 461 +++++++++--------- tests/gui/qt/test_wallet_open.py | 22 +- tests/gui/qt/test_wallet_send.py | 242 ++++----- tests/non_gui/test_mail_handler.py | 1 + tests/non_gui/test_wallet_coin_select.py | 1 - tests/test_util.py | 15 +- tools/build.py | 5 + tools/release.py | 9 +- 126 files changed, 4553 insertions(+), 3256 deletions(-) rename bitcoin_safe/{gui/qt/signal_carrying_object.py => category_info.py} (56%) rename bitcoin_safe/gui/qt/taglist/{main.py => custom_list_widget.py} (66%) create mode 100644 bitcoin_safe/gui/qt/taglist/tag_editor.py create mode 100644 bitcoin_safe/signal_tracker.py diff --git a/.gitignore b/.gitignore index 3b799af..27f600b 100644 --- a/.gitignore +++ b/.gitignore @@ -205,7 +205,7 @@ screenshots*/ profile.html docs/org - +tests/output # build results @@ -220,4 +220,6 @@ bitcoin_safe *.so.1 version tools/libusb/ -*.dll \ No newline at end of file +*.dll + + diff --git a/bitcoin_safe/__init__.py b/bitcoin_safe/__init__.py index d342006..8b3f64b 100644 --- a/bitcoin_safe/__init__.py +++ b/bitcoin_safe/__init__.py @@ -1,2 +1,2 @@ # this is the source of the version information -__version__ = "1.0.4" +__version__ = "1.0.5" diff --git a/bitcoin_safe/__main__.py b/bitcoin_safe/__main__.py index f5d6779..ab1c5c9 100644 --- a/bitcoin_safe/__main__.py +++ b/bitcoin_safe/__main__.py @@ -1,15 +1,17 @@ +import argparse import sys # all import must be absolute, because this is the entry script for pyinstaller -from bitcoin_safe.dynamic_lib_load import ensure_pyzbar_works +from bitcoin_safe.logging_setup import setup_logging + +setup_logging() +from bitcoin_safe.dynamic_lib_load import setup_libsecp256k1 -# setup the logging -from bitcoin_safe.logging_setup import setup_logging # type: ignore +setup_libsecp256k1() +from bitcoin_safe.dynamic_lib_load import ensure_pyzbar_works ensure_pyzbar_works() -import argparse -import sys from PyQt6.QtWidgets import QApplication diff --git a/bitcoin_safe/gui/qt/signal_carrying_object.py b/bitcoin_safe/category_info.py similarity index 56% rename from bitcoin_safe/gui/qt/signal_carrying_object.py rename to bitcoin_safe/category_info.py index 67dc35e..6300fa4 100644 --- a/bitcoin_safe/gui/qt/signal_carrying_object.py +++ b/bitcoin_safe/category_info.py @@ -26,36 +26,18 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import enum +from dataclasses import dataclass -import logging -from typing import Callable, List, Tuple -from PyQt6.QtCore import QObject, pyqtBoundSignal +class SubtextType(enum.Enum): + hide = enum.auto() + click_new_address = enum.auto() + balance = enum.auto() -from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo -from ...signals import SignalFunction - -logger = logging.getLogger(__name__) - - -class SignalCarryingObject(QObject): - def __init__(self, parent=None, **kwargs) -> None: - super().__init__(parent, **kwargs) - self._connected_signals: List[ - Tuple[SignalFunction | pyqtBoundSignal | TypedPyQtSignalNo | TypedPyQtSignal, Callable] - ] = [] - - def connect_signal( - self, - signal: SignalFunction | pyqtBoundSignal | TypedPyQtSignalNo | TypedPyQtSignal, - f: Callable, - **kwargs - ) -> None: - signal.connect(f, **kwargs) - self._connected_signals.append((signal, f)) - - def disconnect_signals(self) -> None: - while self._connected_signals: - signal, f = self._connected_signals.pop() - signal.disconnect(f) +@dataclass +class CategoryInfo: + category: str + text_click_new_address: str = "" + text_balance: str = "" diff --git a/bitcoin_safe/config.py b/bitcoin_safe/config.py index 504f75b..1526ad6 100644 --- a/bitcoin_safe/config.py +++ b/bitcoin_safe/config.py @@ -28,26 +28,23 @@ import logging +import os from pathlib import Path +from typing import Any, Dict, List, Optional +import appdirs +import bdkpython as bdk from packaging import version from bitcoin_safe.gui.qt.unique_deque import UniqueDeque from .execute_config import DEFAULT_MAINNET - -logger = logging.getLogger(__name__) - -import os -from typing import Any, Dict, List, Optional - -import appdirs -import bdkpython as bdk - from .network_config import NetworkConfig, NetworkConfigs from .storage import BaseSaveableClass from .util import current_project_dir, path_to_rel_home_path, rel_home_path_to_abs_path +logger = logging.getLogger(__name__) + MIN_RELAY_FEE = 1 FEE_RATIO_HIGH_WARNING = 0.05 # warn user if fee/amount for on-chain tx is higher than this NO_FEE_WARNING_BELOW = 10 # sat/vB diff --git a/bitcoin_safe/descriptor_export_tools.py b/bitcoin_safe/descriptor_export_tools.py index e414ad2..ef0bd68 100644 --- a/bitcoin_safe/descriptor_export_tools.py +++ b/bitcoin_safe/descriptor_export_tools.py @@ -28,13 +28,6 @@ import logging - -from bitcoin_safe.gui.qt.util import save_file_dialog -from bitcoin_safe.hardware_signers import DescriptorExportType, DescriptorExportTypes -from bitcoin_safe.wallet import filename_clean - -logger = logging.getLogger(__name__) - from typing import Optional import bdkpython as bdk @@ -42,8 +35,14 @@ from bitcoin_qr_tools.signer_info import SignerInfo from bitcoin_usb.address_types import DescriptorInfo +from bitcoin_safe.gui.qt.util import save_file_dialog +from bitcoin_safe.hardware_signers import DescriptorExportType, DescriptorExportTypes +from bitcoin_safe.wallet import filename_clean + from .descriptors import MultipathDescriptor +logger = logging.getLogger(__name__) + class DescriptorExportTools: diff --git a/bitcoin_safe/descriptors.py b/bitcoin_safe/descriptors.py index f3ebbd5..59f969e 100644 --- a/bitcoin_safe/descriptors.py +++ b/bitcoin_safe/descriptors.py @@ -28,9 +28,6 @@ import logging - -logger = logging.getLogger(__name__) - from typing import Sequence import bdkpython as bdk @@ -45,6 +42,8 @@ SimplePubKeyProvider, ) +logger = logging.getLogger(__name__) + def get_default_address_type(is_multisig) -> AddressType: return AddressTypes.p2wsh if is_multisig else AddressTypes.p2wpkh diff --git a/bitcoin_safe/dynamic_lib_load.py b/bitcoin_safe/dynamic_lib_load.py index 9841627..e986e06 100644 --- a/bitcoin_safe/dynamic_lib_load.py +++ b/bitcoin_safe/dynamic_lib_load.py @@ -120,7 +120,7 @@ def setup_libsecp256k1() -> None: bitcoin_usb.set_custom_secp256k1_path(lib_path) bitcointx.set_custom_secp256k1_path(lib_path) elif get_libsecp256k1_os_path(): - logger.info(f"libsecp256k1 was found in the OS") + logger.info(translate("setup_libsecp256k1", f"libsecp256k1 was found in the OS")) else: msg = translate( "dynamic_lib_load", "libsecp256k1 could not be found. Please install libsecp256k1 in your OS." @@ -135,14 +135,22 @@ def ensure_pyzbar_works() -> None: # Get the platform-specific path to the binary library logger.info(f"Platform: {platform.system()}") if platform.system() == "Windows": - logger.info("Trying to import pyzbar to see if Visual C++ Redistributable is installed. ") + logger.info( + translate( + "ensure_pyzbar_works", + "Trying to import pyzbar to see if Visual C++ Redistributable is installed. ", + ) + ) try: from pyzbar import pyzbar pyzbar.__name__ - logger.info(f"pyzbar successfully loaded ") - except: # Do not restrict it to FileNotFoundError, because it can cause other exceptions - logger.info(f"pyzbar not loaded ") + logger.info(translate("ensure_pyzbar_works", f"pyzbar successfully loaded ")) + except ( + Exception + ) as e: # Do not restrict it to FileNotFoundError, because it can cause other exceptions + logger.debug(str(e)) + logger.info(translate("ensure_pyzbar_works", f"pyzbar not loaded ")) show_warning_before_failiure( translate("lib_load", """You are missing the {link}\nPlease install it.""").format( link=link( diff --git a/bitcoin_safe/execute_config.py b/bitcoin_safe/execute_config.py index 87fab96..be8255c 100644 --- a/bitcoin_safe/execute_config.py +++ b/bitcoin_safe/execute_config.py @@ -27,5 +27,10 @@ # SOFTWARE. -DEFAULT_MAINNET = True +IS_PRODUCTION = True # change this for testing + +DEFAULT_MAINNET = IS_PRODUCTION ENABLE_THREADING = True +ENABLE_TIMERS = True +DEFAULT_LANG_CODE = "en_US" +MEMPOOL_SCHEDULE_TIMER = 10 * 60 * 1000 if IS_PRODUCTION else 1 * 60 * 1000 diff --git a/bitcoin_safe/fx.py b/bitcoin_safe/fx.py index cf64a3f..ea6a8fa 100644 --- a/bitcoin_safe/fx.py +++ b/bitcoin_safe/fx.py @@ -30,15 +30,15 @@ import logging from typing import Dict -from bitcoin_safe.threading_manager import ThreadingManager - -logger = logging.getLogger(__name__) - from PyQt6.QtCore import QLocale, QObject, pyqtSignal +from bitcoin_safe.threading_manager import ThreadingManager + from .mempool import threaded_fetch from .signals import TypedPyQtSignalNo +logger = logging.getLogger(__name__) + class FX(QObject, ThreadingManager): signal_data_updated: TypedPyQtSignalNo = pyqtSignal() # type: ignore @@ -48,7 +48,7 @@ def __init__(self, proxies: Dict | None, threading_parent: ThreadingManager | No self.proxies = proxies self.rates: Dict[str, Dict] = {} self.update() - logger.debug(f"initialized {self}") + logger.debug(f"initialized {self.__class__.__name__}") def update_if_needed(self) -> None: if self.rates: diff --git a/bitcoin_safe/gui/icons/distribute-multisigsig-export.svgz b/bitcoin_safe/gui/icons/distribute-multisigsig-export.svgz index d5d084fc223f4c80c1e38fabaf23ed953f2028af..7d9ceb06b0c35f8c64100f53a99be3fa81867565 100644 GIT binary patch literal 38806 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 literal 293552 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 diff --git a/bitcoin_safe/gui/locales/app_ar_AE.qm b/bitcoin_safe/gui/locales/app_ar_AE.qm index 517d37fc81f79e69cada0b32dfccc057f61a491a..bea802edbac740e96c75e9c3641905d6441df3ef 100644 GIT binary patch delta 7331 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 P(w(K) Please back up this descriptor to be able to recover the funds! يحتوي "الوصف" هذا على جميع المعلومات لإعادة بناء المحفظة. يرجى عمل نسخة احتياطية من هذا الوصف لتكون قادرًا على استعادة الأموال! + + Descriptor unchanged + الوصف غير متغير + New descriptor entered تم إدخال وصف جديد @@ -648,8 +652,8 @@ the sending value {sent} إنشاء معاملة - Prefill Transaction again - املأ حقول المعاملة مسبقًا مرة أخرى + Retry + إعادة المحاولة Yes, I see the transaction in the history @@ -1162,13 +1166,17 @@ Location of signing device: ..... جميع الملفات (*);;PSBT (*.psbt);;صفقة (*.tx) - Selected file: {file_path} - الملف المحدد: {مسار_الملف} + No file selected + لم يتم اختيار ملف &New Wallet &محفظة جديدة + + Selected file: {file_path} + الملف المحدد: {مسار_الملف} + No wallet open. Please open the sender wallet to edit this thransaction. لا توجد محفظة مفتوحة. يرجى فتح محفظة الإرسال لتعديل هذه الصفقة. @@ -1205,14 +1213,14 @@ Location of signing device: ..... PSBT {txid} PSBT {txid} - - Open Wallet - فتح المحفظة - &Open Wallet &فتح المحفظة + + Open Wallet + فتح المحفظة + Wallet Files (*.wallet);;All Files (*) ملفات المحفظة (*.wallet);;كل الملفات (*) @@ -1249,14 +1257,14 @@ Location of signing device: ..... A wallet with id {name} is already open. هناك محفظة برقم {اسم} مفتوحة بالفعل. - - Please complete the wallet setup. - يرجى استكمال إعداد المحفظة. - Open &Recent فتح &الأخيرة + + Please complete the wallet setup. + يرجى استكمال إعداد المحفظة. + Close wallet {id}? هل تريد إغلاق المحفظة {id}؟ @@ -1293,14 +1301,14 @@ Location of signing device: ..... Refresh تحديث - - Set Passphrase - تعيين عبارة المرور - &Save Current Wallet &حفظ المحفظة الحالية + + Set Passphrase + تعيين عبارة المرور + Get an xpub احصل على xpub @@ -1337,14 +1345,14 @@ Location of signing device: ..... Actions الإجراءات - - Keypool - Keypool - &Search &بحث + + Keypool + Keypool + Descriptors الموصوفات @@ -1401,6 +1409,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.csv) كل الملفات (*);;ملفات نصية (*.csv) + + No file selected + لم يتم اختيار ملف + NetworkSettingsUI @@ -1692,6 +1704,10 @@ Location of signing device: ..... Delete wallet حذف المحفظة + + No file selected + لم يتم اختيار ملف + Password incorrect كلمة المرور غير صحيحة @@ -1708,14 +1724,14 @@ Location of signing device: ..... Wallet saved تم حفظ المحفظة - - {amount} in {shortid} - {amount} في {shortid} - Descriptor وصف + + {amount} in {shortid} + {amount} في {shortid} + The transactions {txs} @@ -1756,13 +1772,25 @@ Location of signing device: ..... Disconnected from {server} منفصل عن {server} + + Sync && Chat + المزامنة && الدردشة + Click for new address انقر للحصول على عنوان جديد - Sync && Chat - المزامنة && الدردشة + {num_inputs} Inputs: {inputs} + {num_inputs} المدخلات: {inputs} + + + start updating lists + بدء تحديث القوائم + + + finished updating lists + انتهاء تحديث القوائم Export labels @@ -1788,14 +1816,14 @@ Location of signing device: ..... Import Electrum Wallet labels استيراد تسميات محفظة إلكتروم - - All Files (*);;JSON Files (*.json) - جميع الملفات (*);;ملفات JSON (*.json) - History التاريخ + + All Files (*);;JSON Files (*.json) + جميع الملفات (*);;ملفات JSON (*.json) + Receive استلام @@ -1903,6 +1931,10 @@ Location of signing device: ..... Address عنوان + + No rows recognized + لم يتم التعرف على أي صفوف + {address} is not a valid address! {address} ليس عنوانًا صالحًا! @@ -1951,6 +1983,10 @@ Location of signing device: ..... All Files (*);;Wallet Files (*.csv) كل الملفات (*);;ملفات المحفظة (*.csv) + + No file selected + لم يتم اختيار ملف + Open CSV فتح CSV @@ -1963,10 +1999,6 @@ Location of signing device: ..... Please use the CSV template and include the header row. يرجى استخدام نموذج CSV وتضمين صف العنوان. - - No rows recognized - لم يتم التعرف على أي صفوف - RegisterMultisig @@ -2030,6 +2062,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.svg) جميع الملفات (*);;ملفات نصية (*.svg) + + No file selected + لم يتم اختيار ملف + ScreenshotsExportXpub @@ -2441,10 +2477,6 @@ You can restore your labels at a later time with 'Import Sync Key'.Select a category that fits the recipient best حدد الفئة التي تناسب المستلم بشكل أفضل - - {num_inputs} Inputs: {inputs} - {num_inputs} المدخلات: {inputs} - Adding outpoints {outpoints} إضافة نقاط خارجية {outpoints} @@ -3077,6 +3109,13 @@ below {rate} لم يتم العثور على libsecp256k1. يرجى تثبيت libsecp256k1 في نظام التشغيل الخاص بك. + + ensure_pyzbar_works + + Trying to import pyzbar to see if Visual C++ Redistributable is installed. + جارٍ محاولة استيراد pyzbar لمعرفة ما إذا كان Visual C++ قابل لإعادة التوزيع مثبتًا. + + export @@ -3286,14 +3325,14 @@ It is best to use your own server, such as {link}. open_file - - All Files (*);;PSBT (*.psbt);;Transation (*.tx) - جميع الملفات (*);;PSBT (*.psbt);;صفقة (*.tx) - Open Transaction/PSBT فتح المعاملة/PSBT + + All Files (*);;PSBT (*.psbt);;Transation (*.tx) + جميع الملفات (*);;PSBT (*.psbt);;صفقة (*.tx) + pdf @@ -3305,6 +3344,10 @@ It is best to use your own server, such as {link}. {keystore_label}: Fingerprint: {keystore_fingerprint}, Key origin: {keystore_key_origin}, {keystore_xpub} {keystore_label}: بصمة: {keystore_fingerprint}, أصل المفتاح: {keystore_key_origin}, {keystore_xpub} + + File not found! + لم يتم العثور على الملف! + {i}. Seed backup of a {threshold} of {m} Multi-Sig Wallet: "{id}" {i}. نسخة احتياطية لبذور محفظة متعددة التواقيع من {threshold} من {m}: "{id}" @@ -3337,10 +3380,6 @@ It is best to use your own server, such as {link}. On rescanning this wallet, scan to at least address index {max_tip} to discover all funded addresses. عند إعادة فحص هذه المحفظة، افحص على الأقل حتى مؤشر العنوان {max_tip} لاكتشاف جميع العناوين الممولة. - - Label syncronization backup key: {label_sync_nsec} - مفتاح النسخ الاحتياطي لمزامنة العلامات: {label_sync_nsec} - 1. Glue or tape the 'Recovery sheet' ({number} words) over the table below<br/> 2. Fold this paper at the line below <br/> @@ -3349,6 +3388,10 @@ It is best to use your own server, such as {link}. 1. الصق أو ثبّت 'ورقة الاسترداد' ({number} كلمات) فوق الجدول أدناه<br/>2. طوِّ هذه الورقة عند الخط أدناه<br/>3. ضع هذه الورقة في مكان آمن حيث يمكنك الوصول إليها فقط<br/>4. يمكنك وضع الموقع الجهاز إما أ) مع النسخة الاحتياطية لبذور الورق، أو ب) في موقع آمن آخر (إذا كان متوفرًا) + + Label syncronization backup key: {label_sync_nsec} + مفتاح النسخ الاحتياطي لمزامنة العلامات: {label_sync_nsec} + Balance Statement of {id} كشف حساب الرصيد لـ{id} diff --git a/bitcoin_safe/gui/locales/app_de_DE.qm b/bitcoin_safe/gui/locales/app_de_DE.qm index 75f7b5dc46ec2695c67b30f7d17387ffe7fc6fe4..21744fae2ca2015a1418a491d8379749af07b08a 100644 GIT binary patch delta 7331 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 O7a7!Y&&o6w)8;>!XSk~X delta 6575 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 Please back up this descriptor to be able to recover the funds! Dieser "Deskriptor" enthält alle Informationen, um das Wallet neu aufzubauen. Bitte sichern Sie diesen Deskriptor, um die Mittel wiederherstellen zu können! + + Descriptor unchanged + Deskriptor unverändert + New descriptor entered Neuer Deskriptor eingegeben @@ -648,8 +652,8 @@ the sending value {sent} Transaktion erstellen - Prefill Transaction again - Transaktion erneut vorfüllen + Retry + Erneut versuchen Yes, I see the transaction in the history @@ -1162,13 +1166,17 @@ Location of signing device: ..... Alle Dateien (*);;PSBT (*.psbt);;Transaktion (*.tx) - Selected file: {file_path} - Ausgewählte Datei: {file_path} + No file selected + Keine Datei ausgewählt &New Wallet Neues Wallet + + Selected file: {file_path} + Ausgewählte Datei: {file_path} + No wallet open. Please open the sender wallet to edit this thransaction. Kein Wallet geöffnet. Bitte öffnen Sie das Sender-Wallet, um diese Transaktion zu bearbeiten. @@ -1206,11 +1214,11 @@ Location of signing device: ..... PSBT {txid} - Open Wallet + &Open Wallet Wallet öffnen - &Open Wallet + Open Wallet Wallet öffnen @@ -1249,14 +1257,14 @@ Location of signing device: ..... A wallet with id {name} is already open. Ein Wallet mit der ID {name} ist bereits geöffnet. - - Please complete the wallet setup. - Bitte vervollständigen Sie die Einrichtung des Wallets. - Open &Recent Kürzlich öffnen + + Please complete the wallet setup. + Bitte vervollständigen Sie die Einrichtung des Wallets. + Close wallet {id}? Wallet {id} schließen? @@ -1293,14 +1301,14 @@ Location of signing device: ..... Refresh Aktualisieren - - Set Passphrase - Passphrase festlegen - &Save Current Wallet Aktuelles Wallet speichern + + Set Passphrase + Passphrase festlegen + Get an xpub Einen xpub erhalten @@ -1337,14 +1345,14 @@ Location of signing device: ..... Actions Aktionen - - Keypool - Keypool - &Search Suchen + + Keypool + Keypool + Descriptors Deskriptoren @@ -1401,6 +1409,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.csv) Alle Dateien (*);;Textdateien (*.csv) + + No file selected + Keine Datei ausgewählt + NetworkSettingsUI @@ -1689,6 +1701,10 @@ Location of signing device: ..... Delete wallet Wallet löschen + + No file selected + Keine Datei ausgewählt + Password incorrect Passwort falsch @@ -1705,14 +1721,14 @@ Location of signing device: ..... Wallet saved Wallet gespeichert - - {amount} in {shortid} - {amount} in {shortid} - Descriptor Deskriptor + + {amount} in {shortid} + {amount} in {shortid} + The transactions {txs} @@ -1753,13 +1769,25 @@ Location of signing device: ..... Disconnected from {server} Getrennt von {server} + + Sync && Chat + Synchronisieren && Chatten + Click for new address Für neue Adresse klicken - Sync && Chat - Synchronisieren && Chatten + {num_inputs} Inputs: {inputs} + {num_inputs} Inputs: {inputs} + + + start updating lists + Start der Listenaktualisierung + + + finished updating lists + Listenaktualisierung abgeschlossen Export labels @@ -1785,14 +1813,14 @@ Location of signing device: ..... Import Electrum Wallet labels Electrum Wallet-Etiketten importieren - - All Files (*);;JSON Files (*.json) - Alle Dateien (*);;JSON-Dateien (*.json) - History Historie + + All Files (*);;JSON Files (*.json) + Alle Dateien (*);;JSON-Dateien (*.json) + Receive Empfangen @@ -1900,6 +1928,10 @@ Location of signing device: ..... Address Adresse + + No rows recognized + Keine Zeilen erkannt + {address} is not a valid address! {address} ist keine gültige Adresse! @@ -1948,6 +1980,10 @@ Location of signing device: ..... All Files (*);;Wallet Files (*.csv) Alle Dateien (*);;Wallet-Dateien (*.csv) + + No file selected + Keine Datei ausgewählt + Open CSV CSV öffnen @@ -1960,10 +1996,6 @@ Location of signing device: ..... Please use the CSV template and include the header row. Bitte verwenden Sie die CSV-Vorlage und schließen Sie die Kopfzeile ein. - - No rows recognized - Keine Zeilen erkannt - RegisterMultisig @@ -2027,6 +2059,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.svg) Alle Dateien (*);;Textdateien (*.svg) + + No file selected + Keine Datei ausgewählt + ScreenshotsExportXpub @@ -2438,10 +2474,6 @@ You can restore your labels at a later time with 'Import Sync Key'.Select a category that fits the recipient best Wählen Sie eine Kategorie, die am besten zum Empfänger passt - - {num_inputs} Inputs: {inputs} - {num_inputs} Inputs: {inputs} - Adding outpoints {outpoints} Outpoints {outpoints} hinzufügen @@ -3074,6 +3106,13 @@ below {rate} libsecp256k1 konnte nicht gefunden werden. Bitte installieren Sie libsecp256k1 in Ihrem Betriebssystem. + + ensure_pyzbar_works + + Trying to import pyzbar to see if Visual C++ Redistributable is installed. + Versuch, pyzbar zu importieren, um zu sehen, ob Visual C++ Redistributable installiert ist. + + export @@ -3283,14 +3322,14 @@ It is best to use your own server, such as {link}. open_file - - All Files (*);;PSBT (*.psbt);;Transation (*.tx) - Alle Dateien (*);;PSBT (*.psbt);;Transaktion (*.tx) - Open Transaction/PSBT Transaktion/PSBT öffnen + + All Files (*);;PSBT (*.psbt);;Transation (*.tx) + Alle Dateien (*);;PSBT (*.psbt);;Transaktion (*.tx) + pdf @@ -3302,6 +3341,10 @@ It is best to use your own server, such as {link}. {keystore_label}: Fingerprint: {keystore_fingerprint}, Key origin: {keystore_key_origin}, {keystore_xpub} {keystore_label}: Fingerabdruck: {keystore_fingerprint}, Schlüsselursprung: {keystore_key_origin}, {keystore_xpub} + + File not found! + Datei nicht gefunden! + {i}. Seed backup of a {threshold} of {m} Multi-Sig Wallet: "{id}" {i}. Seed-Backup eines {threshold} von {m} Multi-Sig-Wallet: "{id}" @@ -3334,10 +3377,6 @@ It is best to use your own server, such as {link}. On rescanning this wallet, scan to at least address index {max_tip} to discover all funded addresses. Beim erneuten Scannen dieses Wallets scannen Sie mindestens bis zum Adressindex {max_tip}, um alle finanzierten Adressen zu entdecken. - - Label syncronization backup key: {label_sync_nsec} - Label-Synchronisations-Backup-Schlüssel: {label_sync_nsec} - 1. Glue or tape the 'Recovery sheet' ({number} words) over the table below<br/> 2. Fold this paper at the line below <br/> @@ -3346,6 +3385,10 @@ It is best to use your own server, such as {link}. 1. Kleben oder kleben Sie das 'Recovery sheet' ({number} Wörter) über die Tabelle unten<br/> 2. Falten Sie dieses Papier an der Linie unten <br/> 3. Legen Sie dieses Papier an einen sicheren Ort, wo nur Sie Zugang haben<br/> 4. Sie können den Hardware-Signer entweder a) zusammen mit dem Papier-Seed-Backup oder b) an einem anderen sicheren Ort (falls verfügbar) aufbewahren + + Label syncronization backup key: {label_sync_nsec} + Label-Synchronisations-Backup-Schlüssel: {label_sync_nsec} + Balance Statement of {id} Kontostandsauszug von {id} diff --git a/bitcoin_safe/gui/locales/app_es_ES.qm b/bitcoin_safe/gui/locales/app_es_ES.qm index 7e8e9092d96f3b89b6af177177d1c848197a6cce..3463589770061229e4bd7c7272f21a825aba1233 100644 GIT binary patch delta 7506 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 delta 6594 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 diff --git a/bitcoin_safe/gui/locales/app_es_ES.ts b/bitcoin_safe/gui/locales/app_es_ES.ts index f82fbd5..a7332cc 100644 --- a/bitcoin_safe/gui/locales/app_es_ES.ts +++ b/bitcoin_safe/gui/locales/app_es_ES.ts @@ -430,6 +430,10 @@ shown on your BitBox02. Please back up this descriptor to be able to recover the funds! Este "descriptor" contiene toda la información para reconstruir la cartera. ¡Por favor, respalda este descriptor para poder recuperar los fondos! + + Descriptor unchanged + Descriptor sin cambios + New descriptor entered Nuevo descriptor introducido @@ -648,8 +652,8 @@ the sending value {sent} Crear Transacción - Prefill Transaction again - Precargar transacción nuevamente + Retry + Reintentar Yes, I see the transaction in the history @@ -1162,13 +1166,17 @@ Location of signing device: ..... Todos los archivos (*);;PSBT (*.psbt);;Transacción (*.tx) - Selected file: {file_path} - Archivo seleccionado: {file_path} + No file selected + No se ha seleccionado ningún archivo &New Wallet &Cartera Nueva + + Selected file: {file_path} + Archivo seleccionado: {file_path} + No wallet open. Please open the sender wallet to edit this thransaction. No hay cartera abierta. Por favor, abre la cartera emisora para editar esta transacción. @@ -1205,14 +1213,14 @@ Location of signing device: ..... PSBT {txid} PSBT {txid} - - Open Wallet - Abrir Cartera - &Open Wallet &Abrir Cartera + + Open Wallet + Abrir Cartera + Wallet Files (*.wallet);;All Files (*) Archivos de Cartera (*.wallet);;Todos los Archivos (*) @@ -1249,14 +1257,14 @@ Location of signing device: ..... A wallet with id {name} is already open. Una cartera con id {name} ya está abierta. - - Please complete the wallet setup. - Por favor, completa la configuración de la cartera. - Open &Recent Abrir &Reciente + + Please complete the wallet setup. + Por favor, completa la configuración de la cartera. + Close wallet {id}? ¿Cerrar cartera {id}? @@ -1293,14 +1301,14 @@ Location of signing device: ..... Refresh Refrescar - - Set Passphrase - Establecer frase de contraseña - &Save Current Wallet &Guardar Cartera Actual + + Set Passphrase + Establecer frase de contraseña + Get an xpub Obtener un xpub @@ -1337,14 +1345,14 @@ Location of signing device: ..... Actions Acciones - - Keypool - Keypool - &Search &Buscar + + Keypool + Keypool + Descriptors Descriptores @@ -1401,6 +1409,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.csv) Todos los Archivos (*);;Archivos de Texto (*.csv) + + No file selected + No se ha seleccionado ningún archivo + NetworkSettingsUI @@ -1692,6 +1704,10 @@ Location of signing device: ..... Delete wallet Eliminar cartera + + No file selected + No se ha seleccionado ningún archivo + Password incorrect Contraseña incorrecta @@ -1708,14 +1724,14 @@ Location of signing device: ..... Wallet saved Cartera guardada - - {amount} in {shortid} - {amount} en {shortid} - Descriptor Descriptor + + {amount} in {shortid} + {amount} en {shortid} + The transactions {txs} @@ -1756,13 +1772,25 @@ Location of signing device: ..... Disconnected from {server} Desconectado de {server} + + Sync && Chat + Sincronizar && Chatear + Click for new address Haz clic para una nueva dirección - Sync && Chat - Sincronizar && Chatear + {num_inputs} Inputs: {inputs} + {num_inputs} Entradas: {inputs} + + + start updating lists + comenzando la actualización de listas + + + finished updating lists + listas actualizadas Export labels @@ -1788,14 +1816,14 @@ Location of signing device: ..... Import Electrum Wallet labels Importar etiquetas de la cartera Electrum - - All Files (*);;JSON Files (*.json) - Todos los archivos (*);;Archivos JSON (*.json) - History Historial + + All Files (*);;JSON Files (*.json) + Todos los archivos (*);;Archivos JSON (*.json) + Receive Recibir @@ -1903,6 +1931,10 @@ Location of signing device: ..... Address Dirección + + No rows recognized + No se reconocieron filas + {address} is not a valid address! ¡{address} no es una dirección válida! @@ -1951,6 +1983,10 @@ Location of signing device: ..... All Files (*);;Wallet Files (*.csv) Todos los Archivos (*);;Archivos de Cartera (*.csv) + + No file selected + No se ha seleccionado ningún archivo + Open CSV Abrir CSV @@ -1963,10 +1999,6 @@ Location of signing device: ..... Please use the CSV template and include the header row. Por favor, usa la plantilla CSV e incluye la fila de encabezado. - - No rows recognized - No se reconocieron filas - RegisterMultisig @@ -2030,6 +2062,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.svg) Todos los archivos (*);;Archivos de texto (*.svg) + + No file selected + No se ha seleccionado ningún archivo + ScreenshotsExportXpub @@ -2441,10 +2477,6 @@ You can restore your labels at a later time with 'Import Sync Key'.Select a category that fits the recipient best Selecciona una categoría que se ajuste mejor al destinatario - - {num_inputs} Inputs: {inputs} - {num_inputs} Entradas: {inputs} - Adding outpoints {outpoints} Agregando puntos de salida {outpoints} @@ -3077,6 +3109,13 @@ below {rate} No se pudo encontrar libsecp256k1. Por favor, instale libsecp256k1 en su sistema operativo. + + ensure_pyzbar_works + + Trying to import pyzbar to see if Visual C++ Redistributable is installed. + Intentando importar pyzbar para ver si está instalado el Visual C++ Redistributable. + + export @@ -3286,14 +3325,14 @@ It is best to use your own server, such as {link}. open_file - - All Files (*);;PSBT (*.psbt);;Transation (*.tx) - Todos los archivos (*);;PSBT (*.psbt);;Transacción (*.tx) - Open Transaction/PSBT Abrir Transacción/PSBT + + All Files (*);;PSBT (*.psbt);;Transation (*.tx) + Todos los archivos (*);;PSBT (*.psbt);;Transacción (*.tx) + pdf @@ -3305,6 +3344,10 @@ It is best to use your own server, such as {link}. {keystore_label}: Fingerprint: {keystore_fingerprint}, Key origin: {keystore_key_origin}, {keystore_xpub} {keystore_label}: Huella digital: {keystore_fingerprint}, Origen de la clave: {keystore_key_origin}, {keystore_xpub} + + File not found! + ¡Archivo no encontrado! + {i}. Seed backup of a {threshold} of {m} Multi-Sig Wallet: "{id}" {i}. Copia de seguridad de semilla de una Cartera Multi-Firma de {threshold} de {m}: "{id}" @@ -3337,10 +3380,6 @@ It is best to use your own server, such as {link}. On rescanning this wallet, scan to at least address index {max_tip} to discover all funded addresses. Al volver a escanear esta cartera, escanee al menos hasta el índice de dirección {max_tip} para descubrir todas las direcciones financiadas. - - Label syncronization backup key: {label_sync_nsec} - Clave de respaldo de sincronización de etiquetas: {label_sync_nsec} - 1. Glue or tape the 'Recovery sheet' ({number} words) over the table below<br/> 2. Fold this paper at the line below <br/> @@ -3349,6 +3388,10 @@ It is best to use your own server, such as {link}. 1. Pegue o cinta la 'Hoja de recuperación' ({number} palabras) sobre la tabla a continuación<br/>2. Doble este papel en la línea de abajo<br/>3. Coloque este papel en un lugar seguro, donde solo usted tenga acceso<br/>4. Puede poner el firmante de hardware ya sea a) junto con el respaldo de semilla de papel, o b) en otro lugar seguro (si está disponible) + + Label syncronization backup key: {label_sync_nsec} + Clave de respaldo de sincronización de etiquetas: {label_sync_nsec} + Balance Statement of {id} Estado de cuenta de {id} diff --git a/bitcoin_safe/gui/locales/app_fr_FR.qm b/bitcoin_safe/gui/locales/app_fr_FR.qm index 4aaf410108fae92c5fa4b3ff6552f8ab5c28fef6..c75168ec698067f2e0979ee9e8af8e19af0428ac 100644 GIT binary patch delta 7327 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 diff --git a/bitcoin_safe/gui/locales/app_fr_FR.ts b/bitcoin_safe/gui/locales/app_fr_FR.ts index dd51731..6bb4040 100644 --- a/bitcoin_safe/gui/locales/app_fr_FR.ts +++ b/bitcoin_safe/gui/locales/app_fr_FR.ts @@ -430,6 +430,10 @@ shown on your BitBox02. Please back up this descriptor to be able to recover the funds! Exporter le descripteur + + Descriptor unchanged + Descripteur inchangé + New descriptor entered Signataires requis @@ -648,8 +652,8 @@ the sending value {sent} Créer une transaction - Prefill Transaction again - Pré-remplir à nouveau la transaction + Retry + Réessayer Yes, I see the transaction in the history @@ -1162,13 +1166,17 @@ Location of signing device: ..... Tous les fichiers (*);;PSBT (*.psbt);;Transaction (*.tx) - Selected file: {file_path} - Fichier sélectionné : {file_path} + No file selected + Aucun fichier sélectionné &New Wallet &Nouveau Portefeuille + + Selected file: {file_path} + Fichier sélectionné : {file_path} + No wallet open. Please open the sender wallet to edit this thransaction. Aucun portefeuille ouvert. Veuillez ouvrir le portefeuille expéditeur pour modifier cette transaction. @@ -1205,14 +1213,14 @@ Location of signing device: ..... PSBT {txid} PSBT {txid} - - Open Wallet - Ouvrir le portefeuille - &Open Wallet &Ouvrir le portefeuille + + Open Wallet + Ouvrir le portefeuille + Wallet Files (*.wallet);;All Files (*) Fichiers de portefeuille (*.wallet);;Tous les fichiers (*) @@ -1249,14 +1257,14 @@ Location of signing device: ..... A wallet with id {name} is already open. Un portefeuille avec l'identifiant {name} est déjà ouvert. - - Please complete the wallet setup. - Veuillez terminer la configuration du portefeuille. - Open &Recent Ouvrir &Récent + + Please complete the wallet setup. + Veuillez terminer la configuration du portefeuille. + Close wallet {id}? Fermer le portefeuille {id} ? @@ -1293,14 +1301,14 @@ Location of signing device: ..... Refresh Rafraîchir - - Set Passphrase - Définir la phrase secrète - &Save Current Wallet &Enregistrer le Portefeuille Actuel + + Set Passphrase + Définir la phrase secrète + Get an xpub Obtenir un xpub @@ -1337,14 +1345,14 @@ Location of signing device: ..... Actions Actions - - Keypool - Keypool - &Search &Recherche + + Keypool + Keypool + Descriptors Descripteurs @@ -1401,6 +1409,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.csv) Tous les fichiers (*);;Fichiers texte (*.csv) + + No file selected + Aucun fichier sélectionné + NetworkSettingsUI @@ -1692,6 +1704,10 @@ Voulez-vous continuer malgré tout ? Delete wallet Supprimer le portefeuille + + No file selected + Aucun fichier sélectionné + Password incorrect Mot de passe incorrect @@ -1708,14 +1724,14 @@ Voulez-vous continuer malgré tout ? Wallet saved Portefeuille sauvegardé - - {amount} in {shortid} - {amount} dans {shortid} - Descriptor Descripteur + + {amount} in {shortid} + {amount} dans {shortid} + The transactions {txs} @@ -1756,13 +1772,25 @@ Voulez-vous continuer malgré tout ? Disconnected from {server} Déconnecté de {server} + + Sync && Chat + Synchroniser && Discuter + Click for new address Cliquez pour une nouvelle adresse - Sync && Chat - Synchroniser && Discuter + {num_inputs} Inputs: {inputs} + {num_inputs} Entrées : {inputs} + + + start updating lists + début de la mise à jour des listes + + + finished updating lists + mise à jour des listes terminée Export labels @@ -1788,14 +1816,14 @@ Voulez-vous continuer malgré tout ? Import Electrum Wallet labels Importer les étiquettes de portefeuille Electrum - - All Files (*);;JSON Files (*.json) - Tous les fichiers (*);;Fichiers JSON (*.json) - History Historique + + All Files (*);;JSON Files (*.json) + Tous les fichiers (*);;Fichiers JSON (*.json) + Receive Recevoir @@ -1903,6 +1931,10 @@ Voulez-vous continuer malgré tout ? Address Adresse + + No rows recognized + Aucune ligne reconnue + {address} is not a valid address! {address} n'est pas une adresse valide ! @@ -1951,6 +1983,10 @@ Voulez-vous continuer malgré tout ? All Files (*);;Wallet Files (*.csv) Tous les fichiers (*);;Fichiers de portefeuille (*.csv) + + No file selected + Aucun fichier sélectionné + Open CSV Ouvrir CSV @@ -1963,10 +1999,6 @@ Voulez-vous continuer malgré tout ? Please use the CSV template and include the header row. Veuillez utiliser le modèle CSV et inclure la ligne d'en-tête. - - No rows recognized - Aucune ligne reconnue - RegisterMultisig @@ -2030,6 +2062,10 @@ Voulez-vous continuer malgré tout ? All Files (*);;Text Files (*.svg) Tous les fichiers (*);;Fichiers texte (*.svg) + + No file selected + Aucun fichier sélectionné + ScreenshotsExportXpub @@ -2441,10 +2477,6 @@ You can restore your labels at a later time with 'Import Sync Key'.Select a category that fits the recipient best Sélectionnez une catégorie qui correspond le mieux au destinataire - - {num_inputs} Inputs: {inputs} - {num_inputs} Entrées : {inputs} - Adding outpoints {outpoints} Ajout des points de sortie {outpoints} @@ -3077,6 +3109,13 @@ below {rate} libsecp256k1 est introuvable. Veuillez installer libsecp256k1 sur votre système d'exploitation. + + ensure_pyzbar_works + + Trying to import pyzbar to see if Visual C++ Redistributable is installed. + Tentative d'importation de pyzbar pour voir si le Visual C++ Redistributable est installé. + + export @@ -3286,14 +3325,14 @@ It is best to use your own server, such as {link}. open_file - - All Files (*);;PSBT (*.psbt);;Transation (*.tx) - Tous les fichiers (*);;PSBT (*.psbt);;Transaction (*.tx) - Open Transaction/PSBT Ouvrir Transaction/PSBT + + All Files (*);;PSBT (*.psbt);;Transation (*.tx) + Tous les fichiers (*);;PSBT (*.psbt);;Transaction (*.tx) + pdf @@ -3305,6 +3344,10 @@ It is best to use your own server, such as {link}. {keystore_label}: Fingerprint: {keystore_fingerprint}, Key origin: {keystore_key_origin}, {keystore_xpub} {keystore_label} : Empreinte : {keystore_fingerprint}, Origine de la clé : {keystore_key_origin}, {keystore_xpub} + + File not found! + Fichier non trouvé ! + {i}. Seed backup of a {threshold} of {m} Multi-Sig Wallet: "{id}" {i}. Sauvegarde de graine d'un portefeuille Multi-Signature de {threshold} sur {m} : "{id}" @@ -3337,10 +3380,6 @@ It is best to use your own server, such as {link}. On rescanning this wallet, scan to at least address index {max_tip} to discover all funded addresses. Lors du re-scannage de ce portefeuille, scannez au moins jusqu'à l'indice d'adresse {max_tip} pour découvrir toutes les adresses financées. - - Label syncronization backup key: {label_sync_nsec} - Clé de sauvegarde de synchronisation des étiquettes : {label_sync_nsec} - 1. Glue or tape the 'Recovery sheet' ({number} words) over the table below<br/> 2. Fold this paper at the line below <br/> @@ -3349,6 +3388,10 @@ It is best to use your own server, such as {link}. 1. Collez ou scotchez la 'Feuille de récupération' ({number} mots) sur le tableau ci-dessous<br/> 2. Pliez ce papier sur la ligne ci-dessous <br/> 3. Placez ce papier dans un endroit sécurisé, où seulement vous avez accès<br/> 4. Vous pouvez placer le signataire matériel soit a) avec la sauvegarde de graine sur papier, soit b) dans un autre lieu sécurisé (si disponible) + + Label syncronization backup key: {label_sync_nsec} + Clé de sauvegarde de synchronisation des étiquettes : {label_sync_nsec} + Balance Statement of {id} Relevé de solde de {id} diff --git a/bitcoin_safe/gui/locales/app_hi_IN.qm b/bitcoin_safe/gui/locales/app_hi_IN.qm index 8c26a348c8ea6f136740a108499d786f0cf1c01d..a7dcabd4da1b995d1d2868c37208224eae2f8046 100644 GIT binary patch delta 7488 zcmbW6cT^Nvl!x!@uF%y{1w|00Ra8I)6vT`HBPxP|aZEsqAW3LcFtmy}D<}rc3G)az zVnPhe7{Q!H1#`}07;%4e?K!*i&z{{q9?w_RR@J-t-uFDaNtn7@SZr$>vdN|1(8z=L+QszxhFb{tPqMn_I+Fk(n6M5d1`Bw?ipai0hLx{|O5luTp z+}i0xtL_rFHJNDo9pbXK5b5%)WEQ;_afdmge=ZRB3Ln?+w2~Rh7U2Z+yvK=ub^?)fD1S0CcH;<@?TiO>H;)TuM^1tuuq zP5hZ$vM@)Z^@24&NJ85}qAIZ@-pD2j$tLk{u>E(^He5rjn~Ch&4vfA z%E~n0+)APr88m237*Ww;8kCg|R|L~wJD4;@PlKcIyecO5*jg5 zO>9CWg_~f>5RoDSPeS2W6lDppAv&>-qAK{S5+#lQO{p@IG#wTLxR+PI6F)%@(1GVyrg`cK8DH4J{p+bh=N0rh! z<5FVAEINPHgP3_H6*^VxhGSWUf2IMm%>X`gVs!WvwM@bC|`Q z7>N5tZ0y_xME$+lq}NS}Y^tR#P}a9py~8pV>WRJ|VwsL>q32VUM_I(`A7*)cb+8*a zj99&E>~M%Lk;hebEPo|2pP%gb(;OJM2Rq@rkC>@BJ24g^`|32iI97`UoDDXDgO;#M zui&WWPV8C}h*_*=Hzs8hvmeCX;(ZH6&0+7G*CAs26qkS? zS}VGKEF)&~UD3VnNn$ar6+QPnB3j>1(QEuFmbxT zBt>{mH-zRknZ9!rBd0wi=3YgSlrofP{CCCpV~dH(!xZCR=!lISpx8L56*RuC*jyQ9 z$uUN;ZTTrUR#5DCz=(eDuh`qv1(x(v93JUK6cwyEa@K-{YjKL>WsKOkG)3W`E<{hy zD9)A*C)(dlalRC$>yV}>{&0Zk&vGl7^1R||QGQ`5k^2y3RcZ!T>6DHZHB8)SfYSIa99gbc zx-i7HdVzBAxIU;;;a0N7{xX}tlsT@kGAsqA-TP5#dI>f?t~5{mge2{*Od1Ry$IVfu z9(Y8oR*`bH(^aB^DCN9#m|QnOx%hV_QTlP^hI}*8zBWqB&ifn+XPNSk0p +bHwu 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 C4wmTv delta 6572 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 diff --git a/bitcoin_safe/gui/locales/app_hi_IN.ts b/bitcoin_safe/gui/locales/app_hi_IN.ts index 6abe581..805a5bc 100644 --- a/bitcoin_safe/gui/locales/app_hi_IN.ts +++ b/bitcoin_safe/gui/locales/app_hi_IN.ts @@ -430,6 +430,10 @@ shown on your BitBox02. Please back up this descriptor to be able to recover the funds! यह "वर्णनकर्ता" वॉलेट को पुनर्निर्माण करने के लिए सभी जानकारी रखता है। कृपया धन की पुनर्प्राप्ति के लिए इस वर्णनकर्ता का बैकअप लें! + + Descriptor unchanged + विवरणक अपरिवर्तित + New descriptor entered नया विवरणक दर्ज किया गया @@ -648,8 +652,8 @@ the sending value {sent} लेन-देन बनाएं - Prefill Transaction again - फिर से लेन-देन पूर्व भरें + Retry + पुन: प्रयास करें Yes, I see the transaction in the history @@ -1162,13 +1166,17 @@ Location of signing device: ..... सभी फाइलें (*);;PSBT (*.psbt);;लेन-देन (*.tx) - Selected file: {file_path} - चयनित फ़ाइल: {file_path} + No file selected + कोई फ़ाइल नहीं चुनी गई &New Wallet &नया वॉलेट + + Selected file: {file_path} + चयनित फ़ाइल: {file_path} + No wallet open. Please open the sender wallet to edit this thransaction. कोई वॉलेट खुला नहीं है। कृपया इस लेन-देन को संपादित करने के लिए प्रेषक वॉलेट खोलें। @@ -1205,14 +1213,14 @@ Location of signing device: ..... PSBT {txid} PSBT {txid} - - Open Wallet - वॉलेट खोलें - &Open Wallet &वॉलेट खोलें + + Open Wallet + वॉलेट खोलें + Wallet Files (*.wallet);;All Files (*) वॉलेट फ़ाइलें (*.wallet);;सभी फ़ाइलें (*) @@ -1249,14 +1257,14 @@ Location of signing device: ..... A wallet with id {name} is already open. वॉलेट के साथ id {name} पहले से खुला है। - - Please complete the wallet setup. - कृपया वॉलेट सेटअप पूरा करें। - Open &Recent हाल का खोलें + + Please complete the wallet setup. + कृपया वॉलेट सेटअप पूरा करें। + Close wallet {id}? वॉलेट {id} बंद करें? @@ -1293,14 +1301,14 @@ Location of signing device: ..... Refresh ताज़ा करें - - Set Passphrase - पासफ़्रेज़ सेट करें - &Save Current Wallet &मौजूदा वॉलेट सहेजें + + Set Passphrase + पासफ़्रेज़ सेट करें + Get an xpub एक एक्सपब प्राप्त करें @@ -1337,14 +1345,14 @@ Location of signing device: ..... Actions क्रियाएँ - - Keypool - Keypool - &Search &खोजें + + Keypool + Keypool + Descriptors विवरणक @@ -1401,6 +1409,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.csv) सभी फ़ाइलें (*);;टेक्स्ट फ़ाइलें (*.csv) + + No file selected + कोई फ़ाइल नहीं चुनी गई + NetworkSettingsUI @@ -1692,6 +1704,10 @@ Location of signing device: ..... Delete wallet वॉलेट हटाएं + + No file selected + कोई फ़ाइल नहीं चुनी गई + Password incorrect पासवर्ड गलत है @@ -1708,14 +1724,14 @@ Location of signing device: ..... Wallet saved वॉलेट सहेजा गया - - {amount} in {shortid} - {amount} में {shortid} - Descriptor वर्णनकर्ता + + {amount} in {shortid} + {amount} में {shortid} + The transactions {txs} @@ -1756,13 +1772,25 @@ Location of signing device: ..... Disconnected from {server} {server} से डिस्कनेक्टेड + + Sync && Chat + सिंक && चैट + Click for new address नया पता के लिए क्लिक करें - Sync && Chat - सिंक && चैट + {num_inputs} Inputs: {inputs} + {num_inputs} इनपुट: {inputs} + + + start updating lists + सूचियाँ अपडेट करना शुरू + + + finished updating lists + सूचियों का अपडेट समाप्त Export labels @@ -1788,14 +1816,14 @@ Location of signing device: ..... Import Electrum Wallet labels इलेक्ट्रम वॉलेट लेबल आयात करें - - All Files (*);;JSON Files (*.json) - सभी फ़ाइलें (*);;JSON फ़ाइलें (*.json) - History इतिहास + + All Files (*);;JSON Files (*.json) + सभी फ़ाइलें (*);;JSON फ़ाइलें (*.json) + Receive प्राप्त करें @@ -1903,6 +1931,10 @@ Location of signing device: ..... Address पता + + No rows recognized + कोई पंक्तियाँ पहचानी नहीं गईं + {address} is not a valid address! {address} एक मान्य पता नहीं है! @@ -1951,6 +1983,10 @@ Location of signing device: ..... All Files (*);;Wallet Files (*.csv) सभी फ़ाइलें (*);;वॉलेट फ़ाइलें (*.csv) + + No file selected + कोई फ़ाइल नहीं चुनी गई + Open CSV CSV खोलें @@ -1963,10 +1999,6 @@ Location of signing device: ..... Please use the CSV template and include the header row. कृपया CSV टेम्पलेट का उपयोग करें और हैडर रो शामिल करें। - - No rows recognized - कोई पंक्तियाँ पहचानी नहीं गईं - RegisterMultisig @@ -2030,6 +2062,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.svg) सभी फाइलें (*);;टेक्स्ट फाइलें (*.svg) + + No file selected + कोई फ़ाइल नहीं चुनी गई + ScreenshotsExportXpub @@ -2441,10 +2477,6 @@ You can restore your labels at a later time with 'Import Sync Key'.Select a category that fits the recipient best प्राप्तकर्ता के लिए सबसे उपयुक्त श्रेणी चुनें - - {num_inputs} Inputs: {inputs} - {num_inputs} इनपुट: {inputs} - Adding outpoints {outpoints} आउटपॉइंट्स {outpoints} जोड़ना @@ -3077,6 +3109,13 @@ below {rate} libsecp256k1 नहीं मिला। कृपया अपने ओएस में libsecp256k1 स्थापित करें। + + ensure_pyzbar_works + + Trying to import pyzbar to see if Visual C++ Redistributable is installed. + pyzbar को आयात करने का प्रयास कर रहा हूँ ताकि देख सकूं कि Visual C++ Redistributable स्थापित है या नहीं। + + export @@ -3286,14 +3325,14 @@ It is best to use your own server, such as {link}. open_file - - All Files (*);;PSBT (*.psbt);;Transation (*.tx) - सभी फाइलें (*);;PSBT (*.psbt);;लेन-देन (*.tx) - Open Transaction/PSBT लेन-देन/PSBT खोलें + + All Files (*);;PSBT (*.psbt);;Transation (*.tx) + सभी फाइलें (*);;PSBT (*.psbt);;लेन-देन (*.tx) + pdf @@ -3305,6 +3344,10 @@ It is best to use your own server, such as {link}. {keystore_label}: Fingerprint: {keystore_fingerprint}, Key origin: {keystore_key_origin}, {keystore_xpub} {keystore_label}: फिंगरप्रिंट: {keystore_fingerprint}, की ओरिजिन: {keystore_key_origin}, {keystore_xpub} + + File not found! + फ़ाइल नहीं मिली! + {i}. Seed backup of a {threshold} of {m} Multi-Sig Wallet: "{id}" {i}. {threshold} का {m} मल्टी-सिग वॉलेट का सीड बैकअप: "{id}" @@ -3337,10 +3380,6 @@ It is best to use your own server, such as {link}. On rescanning this wallet, scan to at least address index {max_tip} to discover all funded addresses. इस वॉलेट को फिर से स्कैन करते समय, कम से कम पते के सूचकांक {max_tip} तक स्कैन करें ताकि सभी वित्तपोषित पते पता चल सकें। - - Label syncronization backup key: {label_sync_nsec} - लेबल सिंक्रोनाइजेशन बैकअप कुंजी: {label_sync_nsec} - 1. Glue or tape the 'Recovery sheet' ({number} words) over the table below<br/> 2. Fold this paper at the line below <br/> @@ -3349,6 +3388,10 @@ It is best to use your own server, such as {link}. 1. 'रिकवरी शीट' ({number} शब्दों) को नीचे दी गई तालिका पर गोंद या टेप करें<br/>2. नीचे दी गई लाइन पर इस कागज को मोड़ें<br/>3. इस कागज को केवल आपके पास पहुँचने वाली सुरक्षित जगह में रखें<br/>4. आप हार्डवेयर साइनर को या तो a) कागज़ के बीज बैकअप के साथ रख सकते हैं, या b) दूसरी सुरक्षित जगह में (अगर उपलब्ध है) + + Label syncronization backup key: {label_sync_nsec} + लेबल सिंक्रोनाइजेशन बैकअप कुंजी: {label_sync_nsec} + Balance Statement of {id} {id} का बैलेंस स्टेटमेंट diff --git a/bitcoin_safe/gui/locales/app_it_IT.qm b/bitcoin_safe/gui/locales/app_it_IT.qm index 7ac65fd75166d0d8889b52cf8008b54b6a647259..946e831d5959570783d8a53b24eb160951e2ef0e 100644 GIT binary patch delta 7484 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 UiA^+y$0o&uIuwc~*30?706ja!ZU6uP delta 6557 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 diff --git a/bitcoin_safe/gui/locales/app_it_IT.ts b/bitcoin_safe/gui/locales/app_it_IT.ts index ad866cf..94479c2 100644 --- a/bitcoin_safe/gui/locales/app_it_IT.ts +++ b/bitcoin_safe/gui/locales/app_it_IT.ts @@ -430,6 +430,10 @@ shown on your BitBox02. Please back up this descriptor to be able to recover the funds! Questo "descrittore" contiene tutte le informazioni per ricostruire il portafoglio. Si prega di fare il backup di questo descrittore per poter recuperare i fondi! + + Descriptor unchanged + Descrittore invariato + New descriptor entered Nuovo descrittore inserito @@ -648,8 +652,8 @@ the sending value {sent} Crea Transazione - Prefill Transaction again - Riprecompila i campi della transazione + Retry + Riprova Yes, I see the transaction in the history @@ -1162,13 +1166,17 @@ Location of signing device: ..... Tutti i File (*);;PSBT (*.psbt);;Transazione (*.tx) - Selected file: {file_path} - File selezionato: {file_path} + No file selected + Nessun file selezionato &New Wallet &Nuovo Portafoglio + + Selected file: {file_path} + File selezionato: {file_path} + No wallet open. Please open the sender wallet to edit this thransaction. Nessun portafoglio aperto. Si prega di aprire prima il portafoglio mittente per modificare questa transazione. @@ -1205,14 +1213,14 @@ Location of signing device: ..... PSBT {txid} PSBT {txid} - - Open Wallet - Apri Portafoglio - &Open Wallet &Apri Portafoglio + + Open Wallet + Apri Portafoglio + Wallet Files (*.wallet);;All Files (*) File di Portafoglio (*.wallet);;Tutti i File (*) @@ -1249,14 +1257,14 @@ Location of signing device: ..... A wallet with id {name} is already open. Un portafoglio con id {name} è già aperto. - - Please complete the wallet setup. - Si prega di completare la configurazione del portafoglio. - Open &Recent Apri &Recente + + Please complete the wallet setup. + Si prega di completare la configurazione del portafoglio. + Close wallet {id}? Chiudere il portafoglio {id}? @@ -1293,14 +1301,14 @@ Location of signing device: ..... Refresh Aggiorna - - Set Passphrase - Imposta Passphrase - &Save Current Wallet &Salva Portafoglio Corrente + + Set Passphrase + Imposta Passphrase + Get an xpub Ottieni un xpub @@ -1337,14 +1345,14 @@ Location of signing device: ..... Actions Azioni - - Keypool - Keypool - &Search &Ricerca + + Keypool + Keypool + Descriptors Descrittori @@ -1401,6 +1409,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.csv) Tutti i File (*);;File di Testo (*.csv) + + No file selected + Nessun file selezionato + NetworkSettingsUI @@ -1692,6 +1704,10 @@ Vuoi procedere comunque? Delete wallet Elimina portafoglio + + No file selected + Nessun file selezionato + Password incorrect Password errata @@ -1708,14 +1724,14 @@ Vuoi procedere comunque? Wallet saved Portafoglio salvato - - {amount} in {shortid} - {amount} in {shortid} - Descriptor Descrittore + + {amount} in {shortid} + {amount} in {shortid} + The transactions {txs} @@ -1756,13 +1772,25 @@ Vuoi procedere comunque? Disconnected from {server} Disconnesso da {server} + + Sync && Chat + Sincronizza && Chatta + Click for new address Clicca per un nuovo indirizzo - Sync && Chat - Sincronizza && Chatta + {num_inputs} Inputs: {inputs} + {num_inputs} Input: {inputs} + + + start updating lists + inizio aggiornamento liste + + + finished updating lists + aggiornamento liste completato Export labels @@ -1788,14 +1816,14 @@ Vuoi procedere comunque? Import Electrum Wallet labels Importa etichette del portafoglio Electrum - - All Files (*);;JSON Files (*.json) - Tutti i File (*);;File JSON (*.json) - History Cronologia + + All Files (*);;JSON Files (*.json) + Tutti i File (*);;File JSON (*.json) + Receive Ricevi @@ -1903,6 +1931,10 @@ Vuoi procedere comunque? Address Indirizzo + + No rows recognized + Nessuna riga riconosciuta + {address} is not a valid address! {address} non è un indirizzo valido! @@ -1951,6 +1983,10 @@ Vuoi procedere comunque? All Files (*);;Wallet Files (*.csv) Tutti i File (*);;File di Portafoglio (*.csv) + + No file selected + Nessun file selezionato + Open CSV Apri CSV @@ -1963,10 +1999,6 @@ Vuoi procedere comunque? Please use the CSV template and include the header row. Si prega di utilizzare il modello CSV e includere la riga di intestazione. - - No rows recognized - Nessuna riga riconosciuta - RegisterMultisig @@ -2030,6 +2062,10 @@ Vuoi procedere comunque? All Files (*);;Text Files (*.svg) Tutti i File (*);;File di Testo (*.svg) + + No file selected + Nessun file selezionato + ScreenshotsExportXpub @@ -2441,10 +2477,6 @@ You can restore your labels at a later time with 'Import Sync Key'.Select a category that fits the recipient best Seleziona una categoria che si adatti meglio al destinatario - - {num_inputs} Inputs: {inputs} - {num_inputs} Input: {inputs} - Adding outpoints {outpoints} Aggiungendo outpoints {outpoints} @@ -3077,6 +3109,13 @@ below {rate} libsecp256k1 non è stato trovato. Si prega di installare libsecp256k1 nel proprio sistema operativo. + + ensure_pyzbar_works + + Trying to import pyzbar to see if Visual C++ Redistributable is installed. + Tentativo di importare pyzbar per vedere se il Visual C++ Redistributable è installato. + + export @@ -3286,14 +3325,14 @@ It is best to use your own server, such as {link}. open_file - - All Files (*);;PSBT (*.psbt);;Transation (*.tx) - Tutti i File (*);;PSBT (*.psbt);;Transazione (*.tx) - Open Transaction/PSBT Apri Transazione/PSBT + + All Files (*);;PSBT (*.psbt);;Transation (*.tx) + Tutti i File (*);;PSBT (*.psbt);;Transazione (*.tx) + pdf @@ -3305,6 +3344,10 @@ It is best to use your own server, such as {link}. {keystore_label}: Fingerprint: {keystore_fingerprint}, Key origin: {keystore_key_origin}, {keystore_xpub} {keystore_label}: Impronta: {keystore_fingerprint}, Origine chiave: {keystore_key_origin}, {keystore_xpub} + + File not found! + File non trovato! + {i}. Seed backup of a {threshold} of {m} Multi-Sig Wallet: "{id}" {i}. Backup del seme di un Portafoglio Multi-Sig di {threshold} di {m}: "{id}" @@ -3337,10 +3380,6 @@ It is best to use your own server, such as {link}. On rescanning this wallet, scan to at least address index {max_tip} to discover all funded addresses. Riscansionando questo portafoglio, scansionare almeno fino all'indice di indirizzo {max_tip} per scoprire tutti gli indirizzi finanziati. - - Label syncronization backup key: {label_sync_nsec} - Chiave di backup per la sincronizzazione delle etichette: {label_sync_nsec} - 1. Glue or tape the 'Recovery sheet' ({number} words) over the table below<br/> 2. Fold this paper at the line below <br/> @@ -3349,6 +3388,10 @@ It is best to use your own server, such as {link}. 1. Incolla o fissa il 'Foglio di recupero' ({number} parole) sulla tabella sotto<br/>2. Piega questo foglio sulla linea sotto<br/>3. Metti questo foglio in un luogo sicuro, dove solo tu hai accesso<br/>4. Puoi mettere il firmatario hardware sia a) insieme al backup del seme di carta, o b) in un altro luogo sicuro (se disponibile) + + Label syncronization backup key: {label_sync_nsec} + Chiave di backup per la sincronizzazione delle etichette: {label_sync_nsec} + Balance Statement of {id} Estratto conto di {id} diff --git a/bitcoin_safe/gui/locales/app_ja_JP.qm b/bitcoin_safe/gui/locales/app_ja_JP.qm index 62ae1bc472e40fea10bf6c5f12bc56d928bc50ae..af078b41e5040c0785f2f4360701af20e5fd5333 100644 GIT binary patch delta 7224 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 Please back up this descriptor to be able to recover the funds! この「ディスクリプター」にはウォレットを再構築するためのすべての情報が含まれています。資金を回復するためにこのディスクリプターのバックアップを取ってください! + + Descriptor unchanged + 記述子は変更されていません + New descriptor entered 新しいデスクリプターが入力されました @@ -648,8 +652,8 @@ the sending value {sent} トランザクションを作成 - Prefill Transaction again - 再度トランザクションフィールドを事前に入力する + Retry + 再試行 Yes, I see the transaction in the history @@ -1162,13 +1166,17 @@ Location of signing device: ..... 選択されたファイル:{file_path} - Selected file: {file_path} - ウォレットが開かれていません。このトランザクションを編集するために送信者のウォレットを開いてください。 + No file selected + ファイルが選択されていません &New Wallet &ウォレットを開く + + Selected file: {file_path} + ウォレットが開かれていません。このトランザクションを編集するために送信者のウォレットを開いてください。 + No wallet open. Please open the sender wallet to edit this thransaction. トランザクションまたはPSBTを開く @@ -1205,14 +1213,14 @@ Location of signing device: ..... PSBT {txid} ウォレットファイル (.wallet) - - Open Wallet - ウォレット {file_path} はすでに開いています。 - &Open Wallet 最近開いた&ウォレット + + Open Wallet + ウォレット {file_path} はすでに開いています。 + Wallet Files (*.wallet);;All Files (*) ウォレットファイル (*.wallet);;すべてのファイル (*) @@ -1249,14 +1257,14 @@ Location of signing device: ..... A wallet with id {name} is already open. ウォレット {id} を閉じますか? - - Please complete the wallet setup. - ウォレットを閉じる - Open &Recent 現在のウォレットを&保存 + + Please complete the wallet setup. + ウォレットを閉じる + Close wallet {id}? ウォレット {id} を閉じる @@ -1293,14 +1301,14 @@ Location of signing device: ..... Refresh 更新 - - Set Passphrase - パスフレーズを設定する - &Save Current Wallet &変更/エクスポート + + Set Passphrase + パスフレーズを設定する + Get an xpub xpubを取得する @@ -1337,14 +1345,14 @@ Location of signing device: ..... Actions アクション - - Keypool - Keypool - &Search &検索 + + Keypool + Keypool + Descriptors ディスクリプタ @@ -1401,6 +1409,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.csv) すべてのファイル (*);;テキストファイル (*.csv) + + No file selected + ファイルが選択されていません + NetworkSettingsUI @@ -1692,6 +1704,10 @@ Location of signing device: ..... Delete wallet ウォレットを削除 + + No file selected + ファイルが選択されていません + Password incorrect 新しいパスワード: @@ -1708,14 +1724,14 @@ Location of signing device: ..... Wallet saved {amount}を受け取りました - - {amount} in {shortid} - {amount} が {shortid} に - Descriptor 履歴 + + {amount} in {shortid} + {amount} が {shortid} に + The transactions {txs} @@ -1756,13 +1772,25 @@ Location of signing device: ..... Disconnected from {server} {server} から切断しました + + Sync && Chat + 同期 && チャット + Click for new address ウォレットの設定がまだありません - Sync && Chat - 同期 && チャット + {num_inputs} Inputs: {inputs} + {num_inputs}入力:{inputs} + + + start updating lists + リストの更新を開始 + + + finished updating lists + リストの更新完了 Export labels @@ -1788,14 +1816,14 @@ Location of signing device: ..... Import Electrum Wallet labels Electrum ウォレットラベルのインポート - - All Files (*);;JSON Files (*.json) - すべてのファイル (*);;JSON ファイル (*.json) - History 変更が適用されるものはありません。 + + All Files (*);;JSON Files (*.json) + すべてのファイル (*);;JSON ファイル (*.json) + Receive {filename}にバックアップを保存しました @@ -1903,6 +1931,10 @@ Location of signing device: ..... Address アドレス + + No rows recognized + 行が認識されませんでした + {address} is not a valid address! {address} は有効なアドレスではありません! @@ -1951,6 +1983,10 @@ Location of signing device: ..... All Files (*);;Wallet Files (*.csv) すべてのファイル (*);;ウォレットファイル (*.csv) + + No file selected + ファイルが選択されていません + Open CSV CSVを開く @@ -1963,10 +1999,6 @@ Location of signing device: ..... Please use the CSV template and include the header row. CSVテンプレートを使用し、ヘッダ行を含めてください。 - - No rows recognized - 行が認識されませんでした - RegisterMultisig @@ -2030,6 +2062,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.svg) すべてのファイル (*);;テキストファイル (*.svg) + + No file selected + ファイルが選択されていません + ScreenshotsExportXpub @@ -2441,10 +2477,6 @@ You can restore your labels at a later time with 'Import Sync Key'.Select a category that fits the recipient best 受取人に最適なカテゴリを選択 - - {num_inputs} Inputs: {inputs} - {num_inputs}入力:{inputs} - Adding outpoints {outpoints} アウトポイント{outpoints}を追加 @@ -3077,6 +3109,13 @@ below {rate} libsecp256k1が見つかりませんでした。OSにlibsecp256k1をインストールしてください。 + + ensure_pyzbar_works + + Trying to import pyzbar to see if Visual C++ Redistributable is installed. + pyzbar をインポートして Visual C++ 再頒布可能パッケージがインストールされているか確認しようとしています。 + + export @@ -3286,14 +3325,14 @@ It is best to use your own server, such as {link}. open_file - - All Files (*);;PSBT (*.psbt);;Transation (*.tx) - 選択されたファイル:{file_path} - Open Transaction/PSBT トランザクション/PSBTを開く + + All Files (*);;PSBT (*.psbt);;Transation (*.tx) + 選択されたファイル:{file_path} + pdf @@ -3305,6 +3344,10 @@ It is best to use your own server, such as {link}. {keystore_label}: Fingerprint: {keystore_fingerprint}, Key origin: {keystore_key_origin}, {keystore_xpub} {keystore_label}:フィンガープリント:{keystore_fingerprint}, キー起源:{keystore_key_origin}, {keystore_xpub} + + File not found! + ファイルが見つかりません! + {i}. Seed backup of a {threshold} of {m} Multi-Sig Wallet: "{id}" {i}。{threshold}の{m}マルチシグウォレットのシードバックアップ:"{id}" @@ -3337,10 +3380,6 @@ It is best to use your own server, such as {link}. On rescanning this wallet, scan to at least address index {max_tip} to discover all funded addresses. このウォレットを再スキャンする場合、少なくともアドレスインデックス{max_tip}までスキャンして、すべての資金提供されたアドレスを発見してください。 - - Label syncronization backup key: {label_sync_nsec} - ラベル同期バックアップキー:{label_sync_nsec} - 1. Glue or tape the 'Recovery sheet' ({number} words) over the table below<br/> 2. Fold this paper at the line below <br/> @@ -3349,6 +3388,10 @@ It is best to use your own server, such as {link}. 1. 回復シート({number}語)を以下の表に貼り付けるかテープで固定する<br/>2. 以下の線でこの紙を折りたたむ<br/>3. この紙をあなただけがアクセスできる安全な場所に保管する<br/>4. ハードウェア署名者をa) 紙のシードバックアップと一緒に、またはb) 利用可能な場合は別の安全な場所に置くことができます + + Label syncronization backup key: {label_sync_nsec} + ラベル同期バックアップキー:{label_sync_nsec} + Balance Statement of {id} {id}の残高明細書 diff --git a/bitcoin_safe/gui/locales/app_pt_PT.qm b/bitcoin_safe/gui/locales/app_pt_PT.qm index 75f7c2240a301fa1bc68ad5fe031cde7182efe61..0f4f5d04fe017d2b14ae905d206400d5104b29e9 100644 GIT binary patch delta 7376 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 GIQ}0)$iSZf delta 6572 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 Uj%R%emycy~^TMAgEZ+YA0P*VdasU7T diff --git a/bitcoin_safe/gui/locales/app_pt_PT.ts b/bitcoin_safe/gui/locales/app_pt_PT.ts index 6a68ed2..1712402 100644 --- a/bitcoin_safe/gui/locales/app_pt_PT.ts +++ b/bitcoin_safe/gui/locales/app_pt_PT.ts @@ -430,6 +430,10 @@ shown on your BitBox02. Please back up this descriptor to be able to recover the funds! Este "descritor" contém todas as informações para reconstruir a carteira. Por favor, faça backup deste descritor para poder recuperar os fundos! + + Descriptor unchanged + Descritor inalterado + New descriptor entered Novo descritor introduzido @@ -648,8 +652,8 @@ the sending value {sent} Criar Transação - Prefill Transaction again - Preencher novamente a transação + Retry + Tentar novamente Yes, I see the transaction in the history @@ -1162,13 +1166,17 @@ Location of signing device: ..... Todos os Arquivos (*);;PSBT (*.psbt);;Transação (*.tx) - Selected file: {file_path} - Arquivo selecionado: {file_path} + No file selected + Nenhum arquivo selecionado &New Wallet &Nova Carteira + + Selected file: {file_path} + Arquivo selecionado: {file_path} + No wallet open. Please open the sender wallet to edit this thransaction. Nenhuma carteira aberta. Por favor, abra a carteira do remetente para editar esta transação. @@ -1205,14 +1213,14 @@ Location of signing device: ..... PSBT {txid} PSBT {txid} - - Open Wallet - Abrir Carteira - &Open Wallet &Abrir Carteira + + Open Wallet + Abrir Carteira + Wallet Files (*.wallet);;All Files (*) Ficheiros de Carteira (*.wallet);;Todos os Ficheiros (*) @@ -1249,14 +1257,14 @@ Location of signing device: ..... A wallet with id {name} is already open. Uma carteira com id {name} já está aberta. - - Please complete the wallet setup. - Por favor, complete a configuração da carteira. - Open &Recent Abrir &Recente + + Please complete the wallet setup. + Por favor, complete a configuração da carteira. + Close wallet {id}? Fechar carteira {id}? @@ -1293,14 +1301,14 @@ Location of signing device: ..... Refresh Atualizar - - Set Passphrase - Definir Passphrase - &Save Current Wallet &Salvar Carteira Atual + + Set Passphrase + Definir Passphrase + Get an xpub Obter um xpub @@ -1337,14 +1345,14 @@ Location of signing device: ..... Actions Ações - - Keypool - Keypool - &Search &Pesquisar + + Keypool + Keypool + Descriptors Descritores @@ -1401,6 +1409,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.csv) Todos os Ficheiros (*);;Ficheiros de Texto (*.csv) + + No file selected + Nenhum arquivo selecionado + NetworkSettingsUI @@ -1692,6 +1704,10 @@ Deseja prosseguir mesmo assim? Delete wallet Eliminar carteira + + No file selected + Nenhum arquivo selecionado + Password incorrect Senha incorreta @@ -1708,14 +1724,14 @@ Deseja prosseguir mesmo assim? Wallet saved Carteira salva - - {amount} in {shortid} - {amount} em {shortid} - Descriptor Descritor + + {amount} in {shortid} + {amount} em {shortid} + The transactions {txs} @@ -1756,13 +1772,25 @@ Deseja prosseguir mesmo assim? Disconnected from {server} Desconectado de {server} + + Sync && Chat + Sincronizar && Conversar + Click for new address Clique para novo endereço - Sync && Chat - Sincronizar && Conversar + {num_inputs} Inputs: {inputs} + {num_inputs} Entradas: {inputs} + + + start updating lists + iniciar atualização de listas + + + finished updating lists + listas atualizadas Export labels @@ -1788,14 +1816,14 @@ Deseja prosseguir mesmo assim? Import Electrum Wallet labels Importar etiquetas da carteira Electrum - - All Files (*);;JSON Files (*.json) - Todos os Ficheiros (*);;Ficheiros JSON (*.json) - History Histórico + + All Files (*);;JSON Files (*.json) + Todos os Ficheiros (*);;Ficheiros JSON (*.json) + Receive Receber @@ -1903,6 +1931,10 @@ Deseja prosseguir mesmo assim? Address Endereço + + No rows recognized + Nenhuma linha reconhecida + {address} is not a valid address! {address} não é um endereço válido! @@ -1951,6 +1983,10 @@ Deseja prosseguir mesmo assim? All Files (*);;Wallet Files (*.csv) Todos os Ficheiros (*);;Ficheiros de Carteira (*.csv) + + No file selected + Nenhum arquivo selecionado + Open CSV Abrir CSV @@ -1963,10 +1999,6 @@ Deseja prosseguir mesmo assim? Please use the CSV template and include the header row. Por favor, use o modelo CSV e inclua a linha de cabeçalho. - - No rows recognized - Nenhuma linha reconhecida - RegisterMultisig @@ -2030,6 +2062,10 @@ Deseja prosseguir mesmo assim? All Files (*);;Text Files (*.svg) Todos os Arquivos (*);;Arquivos de Texto (*.svg) + + No file selected + Nenhum arquivo selecionado + ScreenshotsExportXpub @@ -2441,10 +2477,6 @@ You can restore your labels at a later time with 'Import Sync Key'.Select a category that fits the recipient best Selecione uma categoria que melhor se ajuste ao destinatário - - {num_inputs} Inputs: {inputs} - {num_inputs} Entradas: {inputs} - Adding outpoints {outpoints} Adicionando pontos de saída {outpoints} @@ -3077,6 +3109,13 @@ below {rate} libsecp256k1 não foi encontrada. Por favor, instale libsecp256k1 no seu sistema operacional. + + ensure_pyzbar_works + + Trying to import pyzbar to see if Visual C++ Redistributable is installed. + Tentando importar pyzbar para verificar se o Visual C++ Redistributable está instalado. + + export @@ -3286,14 +3325,14 @@ It is best to use your own server, such as {link}. open_file - - All Files (*);;PSBT (*.psbt);;Transation (*.tx) - Todos os Arquivos (*);;PSBT (*.psbt);;Transação (*.tx) - Open Transaction/PSBT Abrir Transação/PSBT + + All Files (*);;PSBT (*.psbt);;Transation (*.tx) + Todos os Arquivos (*);;PSBT (*.psbt);;Transação (*.tx) + pdf @@ -3305,6 +3344,10 @@ It is best to use your own server, such as {link}. {keystore_label}: Fingerprint: {keystore_fingerprint}, Key origin: {keystore_key_origin}, {keystore_xpub} {keystore_label}: Impressão digital: {keystore_fingerprint}, Origem da chave: {keystore_key_origin}, {keystore_xpub} + + File not found! + Ficheiro não encontrado! + {i}. Seed backup of a {threshold} of {m} Multi-Sig Wallet: "{id}" {i}. Cópia de segurança da semente de uma Carteira Multi-Assinatura de {threshold} de {m}: "{id}" @@ -3337,10 +3380,6 @@ It is best to use your own server, such as {link}. On rescanning this wallet, scan to at least address index {max_tip} to discover all funded addresses. Ao rescannear esta carteira, escaneie pelo menos até o índice de endereço {max_tip} para descobrir todos os endereços financiados. - - Label syncronization backup key: {label_sync_nsec} - Chave de backup de sincronização de etiquetas: {label_sync_nsec} - 1. Glue or tape the 'Recovery sheet' ({number} words) over the table below<br/> 2. Fold this paper at the line below <br/> @@ -3349,6 +3388,10 @@ It is best to use your own server, such as {link}. 1. Cole ou prenda a 'Folha de Recuperação' ({number} palavras) sobre a tabela abaixo<br/>2. Dobre este papel na linha abaixo<br/>3. Coloque este papel num local seguro, onde só você tem acesso<br/>4. Você pode colocar o assinante de hardware ou a) junto com o backup de semente de papel, ou b) em outro local seguro (se disponível) + + Label syncronization backup key: {label_sync_nsec} + Chave de backup de sincronização de etiquetas: {label_sync_nsec} + Balance Statement of {id} Extrato de saldo de {id} diff --git a/bitcoin_safe/gui/locales/app_ru_RU.qm b/bitcoin_safe/gui/locales/app_ru_RU.qm index 2a8bfc8a274196ae56805deb8ec22e5f0d5cce6f..cdb5d3ee57d97f8f8a4671f08c8f7f952a2cce56 100644 GIT binary patch delta 7473 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 Please back up this descriptor to be able to recover the funds! Этот "дескриптор" содержит всю информацию для воссоздания кошелька. Пожалуйста, сделайте резервную копию этого дескриптора, чтобы иметь возможность восстановить средства! + + Descriptor unchanged + Дескриптор не изменен + New descriptor entered Введен новый дескриптор @@ -648,8 +652,8 @@ the sending value {sent} Создать транзакцию - Prefill Transaction again - Предварительно заполнить поля транзакции снова + Retry + Повторить Yes, I see the transaction in the history @@ -1162,13 +1166,17 @@ Location of signing device: ..... Все файлы (*);;PSBT (*.psbt);;Транзакция (*.tx) - Selected file: {file_path} - Выбранный файл: {file_path} + No file selected + Файл не выбран &New Wallet &Новый кошелек + + Selected file: {file_path} + Выбранный файл: {file_path} + No wallet open. Please open the sender wallet to edit this thransaction. Кошелек не открыт. Пожалуйста, откройте отправляющий кошелек для редактирования этой транзакции. @@ -1205,14 +1213,14 @@ Location of signing device: ..... PSBT {txid} PSBT {txid} - - Open Wallet - Открыть кошелек - &Open Wallet &Открыть кошелек + + Open Wallet + Открыть кошелек + Wallet Files (*.wallet);;All Files (*) Файлы кошельков (*.wallet);;Все файлы (*) @@ -1249,14 +1257,14 @@ Location of signing device: ..... A wallet with id {name} is already open. Кошелек с идентификатором {name} уже открыт. - - Please complete the wallet setup. - Пожалуйста, завершите настройку кошелька. - Open &Recent Открыть &Недавние + + Please complete the wallet setup. + Пожалуйста, завершите настройку кошелька. + Close wallet {id}? Закрыть кошелек {id}? @@ -1293,14 +1301,14 @@ Location of signing device: ..... Refresh Обновить - - Set Passphrase - Установить пароль - &Save Current Wallet &Сохранить текущий кошелек + + Set Passphrase + Установить пароль + Get an xpub Получить xpub @@ -1337,14 +1345,14 @@ Location of signing device: ..... Actions Действия - - Keypool - Keypool - &Search &Поиск + + Keypool + Keypool + Descriptors Дескрипторы @@ -1401,6 +1409,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.csv) Все файлы (*);;Текстовые файлы (*.csv) + + No file selected + Файл не выбран + NetworkSettingsUI @@ -1692,6 +1704,10 @@ Location of signing device: ..... Delete wallet Удалить кошелек + + No file selected + Файл не выбран + Password incorrect Неверный пароль @@ -1708,14 +1724,14 @@ Location of signing device: ..... Wallet saved Кошелек сохранен - - {amount} in {shortid} - {amount} в {shortid} - Descriptor Дескриптор + + {amount} in {shortid} + {amount} в {shortid} + The transactions {txs} @@ -1756,13 +1772,25 @@ Location of signing device: ..... Disconnected from {server} Отключено от {server} + + Sync && Chat + Синхронизация && Чат + Click for new address Нажмите для нового адреса - Sync && Chat - Синхронизация && Чат + {num_inputs} Inputs: {inputs} + {num_inputs} Входы: {inputs} + + + start updating lists + начать обновление списков + + + finished updating lists + списки обновлены Export labels @@ -1788,14 +1816,14 @@ Location of signing device: ..... Import Electrum Wallet labels Импортировать метки кошелька Electrum - - All Files (*);;JSON Files (*.json) - Все файлы (*);;Файлы JSON (*.json) - History История + + All Files (*);;JSON Files (*.json) + Все файлы (*);;Файлы JSON (*.json) + Receive Получить @@ -1903,6 +1931,10 @@ Location of signing device: ..... Address Адрес + + No rows recognized + Строки не распознаны + {address} is not a valid address! {address} не является действительным адресом! @@ -1951,6 +1983,10 @@ Location of signing device: ..... All Files (*);;Wallet Files (*.csv) Все файлы (*);;Файлы кошельков (*.csv) + + No file selected + Файл не выбран + Open CSV Открыть CSV @@ -1963,10 +1999,6 @@ Location of signing device: ..... Please use the CSV template and include the header row. Пожалуйста, используйте шаблон CSV и включите строку заголовка. - - No rows recognized - Строки не распознаны - RegisterMultisig @@ -2030,6 +2062,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.svg) Все файлы (*);;Текстовые файлы (*.svg) + + No file selected + Файл не выбран + ScreenshotsExportXpub @@ -2441,10 +2477,6 @@ You can restore your labels at a later time with 'Import Sync Key'.Select a category that fits the recipient best Выберите категорию, которая лучше всего подходит получателю - - {num_inputs} Inputs: {inputs} - {num_inputs} Входы: {inputs} - Adding outpoints {outpoints} Добавление точек выхода {outpoints} @@ -3077,6 +3109,13 @@ below {rate} libsecp256k1 не найдена. Пожалуйста, установите libsecp256k1 в вашей операционной системе. + + ensure_pyzbar_works + + Trying to import pyzbar to see if Visual C++ Redistributable is installed. + Попытка импорта pyzbar для проверки установки Visual C++ Redistributable. + + export @@ -3286,14 +3325,14 @@ It is best to use your own server, such as {link}. open_file - - All Files (*);;PSBT (*.psbt);;Transation (*.tx) - Все файлы (*);;PSBT (*.psbt);;Транзакция (*.tx) - Open Transaction/PSBT Открыть транзакцию/PSBT + + All Files (*);;PSBT (*.psbt);;Transation (*.tx) + Все файлы (*);;PSBT (*.psbt);;Транзакция (*.tx) + pdf @@ -3305,6 +3344,10 @@ It is best to use your own server, such as {link}. {keystore_label}: Fingerprint: {keystore_fingerprint}, Key origin: {keystore_key_origin}, {keystore_xpub} {keystore_label}: Отпечаток: {keystore_fingerprint}, Происхождение ключа: {keystore_key_origin}, {keystore_xpub} + + File not found! + Файл не найден! + {i}. Seed backup of a {threshold} of {m} Multi-Sig Wallet: "{id}" {i}. Резервная копия семени для мультиподписного кошелька {threshold} из {m}: "{id}" @@ -3337,10 +3380,6 @@ It is best to use your own server, such as {link}. On rescanning this wallet, scan to at least address index {max_tip} to discover all funded addresses. При повторном сканировании этого кошелька сканируйте как минимум до индекса адреса {max_tip}, чтобы обнаружить все финансируемые адреса. - - Label syncronization backup key: {label_sync_nsec} - Ключ резервного копирования синхронизации меток: {label_sync_nsec} - 1. Glue or tape the 'Recovery sheet' ({number} words) over the table below<br/> 2. Fold this paper at the line below <br/> @@ -3349,6 +3388,10 @@ It is best to use your own server, such as {link}. 1. Наклейте или приклейте 'Лист восстановления' ({number} слов) на таблицу ниже<br/>2. Сложите эту бумагу по линии ниже<br/>3. Поместите эту бумагу в безопасное место, доступное только вам<br/>4. Вы можете поместить аппаратного подписанта либо a) вместе с бумажным резервным копированием семян, либо b) в другом безопасном месте (если таковое имеется) + + Label syncronization backup key: {label_sync_nsec} + Ключ резервного копирования синхронизации меток: {label_sync_nsec} + Balance Statement of {id} Выписка по балансу {id} diff --git a/bitcoin_safe/gui/locales/app_zh_CN.qm b/bitcoin_safe/gui/locales/app_zh_CN.qm index 5a5e3ab3c9739791a61d06d84b23f7a64f783911..f2f2a8ed7b5c51d8088ed3c011d9b3a8ccde865b 100644 GIT binary patch delta 7199 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 Y?>285Y4*(|E&XFo>+{DnvT^PH2fT9yHUIzs diff --git a/bitcoin_safe/gui/locales/app_zh_CN.ts b/bitcoin_safe/gui/locales/app_zh_CN.ts index df93f83..d2729e9 100644 --- a/bitcoin_safe/gui/locales/app_zh_CN.ts +++ b/bitcoin_safe/gui/locales/app_zh_CN.ts @@ -430,6 +430,10 @@ shown on your BitBox02. Please back up this descriptor to be able to recover the funds! 这个“描述”包含重建钱包所需的所有信息。请备份此描述以便能够恢复资金! + + Descriptor unchanged + 描述符未更改 + New descriptor entered 输入了新的描述符 @@ -648,8 +652,8 @@ the sending value {sent} 创建交易 - Prefill Transaction again - 再次预填交易 + Retry + 重试 Yes, I see the transaction in the history @@ -1163,13 +1167,17 @@ Location of signing device: ..... 所有文件 (*);;PSBT (*.psbt);;交易 (*.tx) - Selected file: {file_path} - 选中的文件:{file_path} + No file selected + 未选择文件 &New Wallet &新建钱包 + + Selected file: {file_path} + 选中的文件:{file_path} + No wallet open. Please open the sender wallet to edit this thransaction. 没有打开钱包。请打开发送者钱包以编辑此交易。 @@ -1206,14 +1214,14 @@ Location of signing device: ..... PSBT {txid} PSBT {txid} - - Open Wallet - 打开钱包 - &Open Wallet &打开钱包 + + Open Wallet + 打开钱包 + Wallet Files (*.wallet);;All Files (*) 钱包文件 (*.wallet);;所有文件 (*) @@ -1250,14 +1258,14 @@ Location of signing device: ..... A wallet with id {name} is already open. 一个ID为 {name} 的钱包已经打开。 - - Please complete the wallet setup. - 请完成钱包设置。 - Open &Recent 打开&最近 + + Please complete the wallet setup. + 请完成钱包设置。 + Close wallet {id}? 关闭钱包 {id} 吗? @@ -1294,14 +1302,14 @@ Location of signing device: ..... Refresh 刷新 - - Set Passphrase - 设置密码短语 - &Save Current Wallet &保存当前钱包 + + Set Passphrase + 设置密码短语 + Get an xpub 获取一个xpub @@ -1338,14 +1346,14 @@ Location of signing device: ..... Actions 操作 - - Keypool - 密钥池 - &Search &搜索 + + Keypool + 密钥池 + Descriptors 描述符 @@ -1402,6 +1410,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.csv) 所有文件 (*);;文本文件 (*.csv) + + No file selected + 未选择文件 + NetworkSettingsUI @@ -1695,6 +1707,10 @@ Location of signing device: ..... Delete wallet 删除钱包 + + No file selected + 未选择文件 + Password incorrect 密码错误 @@ -1711,14 +1727,14 @@ Location of signing device: ..... Wallet saved 钱包已保存 - - {amount} in {shortid} - {amount} 在 {shortid} - Descriptor 描述 + + {amount} in {shortid} + {amount} 在 {shortid} + The transactions {txs} @@ -1759,13 +1775,25 @@ Location of signing device: ..... Disconnected from {server} 已从 {server} 断开 + + Sync && Chat + 同步 && 聊天 + Click for new address 点击获取新地址 - Sync && Chat - 同步 && 聊天 + {num_inputs} Inputs: {inputs} + {num_inputs} 输入:{inputs} + + + start updating lists + 开始更新列表 + + + finished updating lists + 完成列表更新 Export labels @@ -1791,14 +1819,14 @@ Location of signing device: ..... Import Electrum Wallet labels 导入 Electrum 钱包标签 - - All Files (*);;JSON Files (*.json) - 所有文件 (*);;JSON 文件 (*.json) - History 历史记录 + + All Files (*);;JSON Files (*.json) + 所有文件 (*);;JSON 文件 (*.json) + Receive 接收 @@ -1906,6 +1934,10 @@ Location of signing device: ..... Address 地址 + + No rows recognized + 未识别到行 + {address} is not a valid address! {address} 不是一个有效的地址! @@ -1954,6 +1986,10 @@ Location of signing device: ..... All Files (*);;Wallet Files (*.csv) 所有文件 (*);;钱包文件 (*.csv) + + No file selected + 未选择文件 + Open CSV 打开 CSV @@ -1966,10 +2002,6 @@ Location of signing device: ..... Please use the CSV template and include the header row. 请使用 CSV 模板,并包含标题行。 - - No rows recognized - 未识别到行 - RegisterMultisig @@ -2033,6 +2065,10 @@ Location of signing device: ..... All Files (*);;Text Files (*.svg) 所有文件 (*);;文本文件 (*.svg) + + No file selected + 未选择文件 + ScreenshotsExportXpub @@ -2444,10 +2480,6 @@ You can restore your labels at a later time with 'Import Sync Key'.Select a category that fits the recipient best 选择最适合接收者的类别 - - {num_inputs} Inputs: {inputs} - {num_inputs} 输入:{inputs} - Adding outpoints {outpoints} 添加输出点{outpoints} @@ -3083,6 +3115,13 @@ below {rate} 找不到 libsecp256k1。请在您的操作系统中安装 libsecp256k1。 + + ensure_pyzbar_works + + Trying to import pyzbar to see if Visual C++ Redistributable is installed. + 尝试导入 pyzbar 以查看是否安装了 Visual C++ 可再发行组件。 + + export @@ -3293,14 +3332,14 @@ It is best to use your own server, such as {link}. open_file - - All Files (*);;PSBT (*.psbt);;Transation (*.tx) - 所有文件 (*);;PSBT (*.psbt);;交易 (*.tx) - Open Transaction/PSBT 打开交易/PSBT + + All Files (*);;PSBT (*.psbt);;Transation (*.tx) + 所有文件 (*);;PSBT (*.psbt);;交易 (*.tx) + pdf @@ -3312,6 +3351,10 @@ It is best to use your own server, such as {link}. {keystore_label}: Fingerprint: {keystore_fingerprint}, Key origin: {keystore_key_origin}, {keystore_xpub} {keystore_label}:指纹:{keystore_fingerprint},密钥起源:{keystore_key_origin},{keystore_xpub} + + File not found! + 文件未找到! + {i}. Seed backup of a {threshold} of {m} Multi-Sig Wallet: "{id}" {i}。 {threshold}的{m}多重签名钱包的种子备份:“{id}” @@ -3344,10 +3387,6 @@ It is best to use your own server, such as {link}. On rescanning this wallet, scan to at least address index {max_tip} to discover all funded addresses. 重新扫描此钱包时,请至少扫描到地址索引 {max_tip},以发现所有资金地址。 - - Label syncronization backup key: {label_sync_nsec} - 标签同步备份密钥:{label_sync_nsec} - 1. Glue or tape the 'Recovery sheet' ({number} words) over the table below<br/> 2. Fold this paper at the line below <br/> @@ -3356,6 +3395,10 @@ It is best to use your own server, such as {link}. 1. 将'恢复表'({number}词)粘贴或胶带在下面的表格上<br/>2. 在下面的线处折叠这张纸<br/>3. 将这张纸放在只有您能访问的安全位置<br/>4. 您可以将硬件签名器放在与纸质助记词种子备份一起的地方,或者b) 在另一个安全位置(如果有的话) + + Label syncronization backup key: {label_sync_nsec} + 标签同步备份密钥:{label_sync_nsec} + Balance Statement of {id} {id} 的余额报表 diff --git a/bitcoin_safe/gui/qt/address_dialog.py b/bitcoin_safe/gui/qt/address_dialog.py index de98c22..2b52ddb 100644 --- a/bitcoin_safe/gui/qt/address_dialog.py +++ b/bitcoin_safe/gui/qt/address_dialog.py @@ -30,7 +30,11 @@ import logging import typing +import bdkpython as bdk from bitcoin_qr_tools.gui.qr_widgets import QRCodeWidgetSVG +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QCloseEvent, QKeyEvent, QKeySequence, QShortcut +from PyQt6.QtWidgets import QFormLayout, QHBoxLayout, QSizePolicy, QVBoxLayout, QWidget from bitcoin_safe.config import UserConfig from bitcoin_safe.descriptors import MultipathDescriptor @@ -39,21 +43,16 @@ from bitcoin_safe.gui.qt.usb_register_multisig import USBValidateAddressWidget from bitcoin_safe.keystore import KeyStoreImporterTypes from bitcoin_safe.mempool import MempoolData -from bitcoin_safe.typestubs import TypedPyQtSignalNo +from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo from bitcoin_safe.util import serialized_to_hex -logger = logging.getLogger(__name__) - -import bdkpython as bdk -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QKeyEvent -from PyQt6.QtWidgets import QFormLayout, QHBoxLayout, QSizePolicy, QVBoxLayout, QWidget - from ...signals import Signals from ...wallet import Wallet from .hist_list import HistList from .util import Buttons, CloseButton, read_QIcon +logger = logging.getLogger(__name__) + class AddressDetailsAdvanced(QWidget): def __init__( @@ -130,6 +129,8 @@ def set_address(self, bdk_address: bdk.Address): class AddressDialog(QWidget): + aboutToClose: TypedPyQtSignal[QWidget] = pyqtSignal(QWidget) # type: ignore + def __init__( self, fx, @@ -223,6 +224,11 @@ def __init__( vbox.addLayout(Buttons(CloseButton(self))) self.setupUi() + self.shortcut_close = QShortcut(QKeySequence("Ctrl+W"), self) + self.shortcut_close.activated.connect(self.close) + self.shortcut_close2 = QShortcut(QKeySequence("ESC"), self) + self.shortcut_close2.activated.connect(self.close) + # Override keyPressEvent method def keyPressEvent(self, event: QKeyEvent) -> None: # type: ignore[override] # Check if the pressed key is 'Esc' @@ -234,3 +240,7 @@ def setupUi(self) -> None: self.recipient_tabs.updateUi() self.recipient_tabs.setTabText(self.recipient_tabs.indexOf(self.tab_advanced), self.tr("Advanced")) self.recipient_tabs.setTabText(self.recipient_tabs.indexOf(self.tab_validate), self.tr("Validate")) + + def closeEvent(self, event: QCloseEvent | None): + self.aboutToClose.emit(self) # Emit the signal when the window is about to close + super().closeEvent(event) diff --git a/bitcoin_safe/gui/qt/address_edit.py b/bitcoin_safe/gui/qt/address_edit.py index 39d53d7..1177549 100644 --- a/bitcoin_safe/gui/qt/address_edit.py +++ b/bitcoin_safe/gui/qt/address_edit.py @@ -28,20 +28,17 @@ import logging - -from bitcoin_safe.gui.qt.analyzers import AddressAnalyzer -from bitcoin_safe.gui.qt.buttonedit import ButtonEdit, SquareButton -from bitcoin_safe.util import block_explorer_URL - -logger = logging.getLogger(__name__) - from typing import Optional import bdkpython as bdk from bitcoin_qr_tools.data import Data, DataType from PyQt6 import QtCore, QtGui from PyQt6.QtCore import pyqtSignal -from PyQt6.QtWidgets import QMessageBox, QSizePolicy, QStyle +from PyQt6.QtWidgets import QMessageBox, QSizePolicy + +from bitcoin_safe.gui.qt.analyzers import AddressAnalyzer +from bitcoin_safe.gui.qt.buttonedit import ButtonEdit, SquareButton +from bitcoin_safe.util import block_explorer_URL from ...i18n import translate from ...signals import Signals, TypedPyQtSignal, UpdateFilter, UpdateFilterReason @@ -49,6 +46,8 @@ from .dialogs import question_dialog from .util import ColorScheme, icon_path, webopen +logger = logging.getLogger(__name__) + class AddressEdit(ButtonEdit): signal_text_change: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore @@ -76,18 +75,12 @@ def __init__( self.setPlaceholderText(self.tr("Enter address here")) - def on_handle_input(data: Data) -> None: - if data.data_type == DataType.Bip21: - if data.data.get("address"): - self.setText(data.data.get("address")) - self.signal_bip21_input.emit(data) - self.camera_button = self.add_qr_input_from_camera_button( network=network, ) - self.signal_data.connect(on_handle_input) + self.signal_data.connect(self._on_handle_input) self.copy_button = self.add_copy_button() - self.mempool_button = self._add_mempool_button(self.signals) if self.signals else None + self.mempool_button = self._add_mempool_button() self.input_field.setAnalyzer(AddressAnalyzer(self.network, parent=self)) @@ -100,6 +93,12 @@ def on_handle_input(data: Data) -> None: # signals self.input_field.textChanged.connect(self.on_text_changed) + def _on_handle_input(self, data: Data) -> None: + if data.data_type == DataType.Bip21: + if data.data.get("address"): + self.setText(data.data.get("address")) + self.signal_bip21_input.emit(data) + def set_allow_edit(self, allow_edit: bool): self.allow_edit = allow_edit @@ -112,17 +111,18 @@ def set_allow_edit(self, allow_edit: bool): if self.mempool_button: self.mempool_button.setHidden(allow_edit) - def _add_mempool_button(self, signals: Signals) -> SquareButton: - def on_click() -> None: - mempool_url = signals.get_mempool_url() - if mempool_url is None: - return - addr_URL = block_explorer_URL(mempool_url, "addr", self.address) - if addr_URL: - webopen(addr_URL) + def _on_click(self) -> None: + mempool_url = self.signals.get_mempool_url() + if mempool_url is None: + return + addr_URL = block_explorer_URL(mempool_url, "addr", self.address) + if addr_URL: + webopen(addr_URL) + + def _add_mempool_button(self) -> SquareButton: copy_button = self.add_button( - icon_path("block-explorer.svg"), on_click, tooltip=translate("d", "View on block explorer") + icon_path("block-explorer.svg"), self._on_click, tooltip=translate("d", "View on block explorer") ) return copy_button @@ -173,8 +173,6 @@ def format_address_field(self, wallet: Optional[Wallet]) -> None: if background_color: palette.setColor(QtGui.QPalette.ColorRole.Base, background_color) - else: - palette = (self.input_field.style() or QStyle()).standardPalette() self.input_field.setPalette(palette) self.input_field.update() diff --git a/bitcoin_safe/gui/qt/address_list.py b/bitcoin_safe/gui/qt/address_list.py index 80c27f4..7b54de1 100644 --- a/bitcoin_safe/gui/qt/address_list.py +++ b/bitcoin_safe/gui/qt/address_list.py @@ -53,7 +53,8 @@ # SOFTWARE. import logging -from typing import Any, Dict, Tuple +from functools import partial +from typing import Any, Dict from bitcoin_safe.fx import FX from bitcoin_safe.gui.qt.wrappers import Menu @@ -114,24 +115,23 @@ class ImportLabelMenu: - def __init__(self, upper_menu: Menu, wallet: Wallet, wallet_signals: WalletSignals) -> None: + def __init__(self, upper_menu: Menu, wallet_signals: WalletSignals) -> None: self.wallet_signals = wallet_signals - self.wallet = wallet self.import_label_menu = upper_menu.add_menu( "", ) self.action_import = self.import_label_menu.add_action( "", - lambda: self.wallet_signals.import_labels.emit(), + self.wallet_signals.import_labels.emit, ) self.action_bip329_import = self.import_label_menu.add_action( "", - lambda: self.wallet_signals.import_bip329_labels.emit(), + self.wallet_signals.import_bip329_labels.emit, ) self.action_electrum_import = self.import_label_menu.add_action( "", - lambda: self.wallet_signals.import_electrum_wallet_labels.emit(), + self.wallet_signals.import_electrum_wallet_labels.emit, ) self.action_nostr_import = self.import_label_menu.add_action( "", @@ -159,20 +159,19 @@ def updateUi(self) -> None: class ExportLabelMenu: - def __init__(self, upper_menu: Menu, wallet: Wallet, wallet_signals: WalletSignals) -> None: + def __init__(self, upper_menu: Menu, wallet_signals: WalletSignals) -> None: self.wallet_signals = wallet_signals - self.wallet = wallet self.export_label_menu = upper_menu.add_menu( "", ) self.action_export_full = self.export_label_menu.add_action( "", - lambda: self.wallet_signals.export_labels.emit(), + self.wallet_signals.export_labels.emit, ) self.action_bip329 = self.export_label_menu.add_action( "", - lambda: self.wallet_signals.export_bip329_labels.emit(), + self.wallet_signals.export_bip329_labels.emit, ) self.updateUi() @@ -272,9 +271,17 @@ def __init__( self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.show_change = AddressTypeFilter.ALL # type: AddressTypeFilter self.show_used = AddressUsageStateFilter.ALL # type: AddressUsageStateFilter - self._source_model = MyStandardItemModel(self, drag_key="addresses") + self._source_model = MyStandardItemModel( + key_column=self.key_column, + parent=self, + ) self.proxy = MySortModel( - self, source_model=self._source_model, sort_role=MyItemDataRole.ROLE_SORT_ORDER + key_column=self.key_column, + Columns=self.Columns, + drag_key="addresses", + parent=self, + source_model=self._source_model, + sort_role=MyItemDataRole.ROLE_SORT_ORDER, ) ColorScheme.update_from_widget(self) self.setModel(self.proxy) @@ -601,14 +608,14 @@ def create_menu(self, position: QPoint) -> Menu: return menu addr = addrs[0] menu.add_action( - self.tr("Details"), lambda: self.wallet_signals.show_address.emit(addr, self.wallet.id) + self.tr("Details"), partial(self.wallet_signals.show_address.emit, addr, self.wallet.id) ) addr_URL = block_explorer_URL(self.config.network_config.mempool_url, "addr", addr) if addr_URL: menu.add_action( self.tr("View on block explorer"), - lambda: webopen(addr_URL), + partial(webopen, addr_URL), icon=read_QIcon("block-explorer.svg"), ) @@ -620,12 +627,15 @@ def create_menu(self, position: QPoint) -> Menu: menu.add_action( self.tr("Copy as csv"), - lambda: self.copyRowsToClipboardAsCSV([r.row() for r in selected]), + partial( + self.copyRowsToClipboardAsCSV, + [item.data(MySortModel.role_drag_key) for item in selected_items if item], + ), icon=read_QIcon("csv-file.svg"), ) menu.addSeparator() - self.export_label_menu = ExportLabelMenu(menu, wallet=self.wallet, wallet_signals=self.wallet_signals) - self.import_label_menu = ImportLabelMenu(menu, wallet=self.wallet, wallet_signals=self.wallet_signals) + self.export_label_menu = ExportLabelMenu(menu, wallet_signals=self.wallet_signals) + self.import_label_menu = ImportLabelMenu(menu, wallet_signals=self.wallet_signals) # run_hook('receive_menu', menu, addrs, self.wallet) if viewport := self.viewport(): @@ -636,14 +646,6 @@ def create_menu(self, position: QPoint) -> Menu: def _add_category_menu(self, menu: Menu, idx: QModelIndex): copy_menu = menu.add_menu(self.tr("Set category")) - def factory(category, address): - def f(category=category, address=address): - drag_info = AddressDragInfo([category], [address]) - logger.debug(f"_add_category_menu set {drag_info}") - self.signal_tag_dropped.emit(drag_info) - - return f - for category in self.wallet.labels.categories: item = self.sourceModel().horizontalHeaderItem(self.Columns.ADDRESS) if not item: @@ -655,9 +657,10 @@ def f(category=category, address=address): if address is None: address = item_col.text().strip() + action = partial(self.signal_tag_dropped.emit, AddressDragInfo([category], [address])) copy_menu.add_action( category, - factory(category=category, address=address), + action, icon=create_color_square(CategoryEditor.color(category)), ) @@ -732,17 +735,38 @@ def updateUi(self) -> None: if self.balance_label: balance = self.address_list.wallet.get_balance() if self.signals: - display_balance = ( - self.signals.wallet_signals[self.address_list.wallet.id] - .get_display_balance.emit() - .get(self.address_list.wallet.id) - ) + display_balance = self.signals.wallet_signals[ + self.address_list.wallet.id + ].get_display_balance.emit() if display_balance: balance = display_balance 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 toggle_toolbar(self, config=None) -> None: + super().toggle_toolbar(config=config if config else self.config) + + def _mine_to_selected_addresses(self) -> None: + selected = self.address_list.selected_in_column(self.address_list.Columns.ADDRESS) + if not selected: + return + selected_items = [self.address_list.item_from_index(item) for item in selected] + addresses = [item.text() for item in selected_items if item] + + for address in addresses: + response = send_rpc_command( + self.config.network_config.rpc_ip, + str(self.config.network_config.rpc_port), + self.config.network_config.rpc_username, + self.config.network_config.rpc_password, + "generatetoaddress", + params=[1, address], + ) + logger.info(f"{response}") + if self.signals: + self.signals.chain_data_changed.emit(f"Mined to addresses {addresses}") + def create_toolbar_with_menu(self, title) -> None: super().create_toolbar_with_menu(title=title) @@ -750,13 +774,9 @@ def create_toolbar_with_menu(self, title) -> None: font.setPointSize(12) self.balance_label.setFont(font) - self.action_show_filter = self.menu.addToggle("", lambda: self.toggle_toolbar(self.config)) - self.menu_export_labels = ExportLabelMenu( - self.menu, wallet=self.address_list.wallet, wallet_signals=self.address_list.wallet_signals - ) - self.menu_import_labels = ImportLabelMenu( - self.menu, wallet=self.address_list.wallet, wallet_signals=self.address_list.wallet_signals - ) + self.action_show_filter = self.menu.addToggle("", self.toggle_toolbar) + self.menu_export_labels = ExportLabelMenu(self.menu, wallet_signals=self.address_list.wallet_signals) + self.menu_import_labels = ImportLabelMenu(self.menu, wallet_signals=self.address_list.wallet_signals) if ( self.config @@ -764,39 +784,17 @@ def create_toolbar_with_menu(self, title) -> None: and self.config.network != bdk.Network.BITCOIN ): - def mine_to_selected_addresses() -> None: - selected = self.address_list.selected_in_column(self.address_list.Columns.ADDRESS) - if not selected: - return - selected_items = [self.address_list.item_from_index(item) for item in selected] - addresses = [item.text() for item in selected_items if item] - - for address in addresses: - response = send_rpc_command( - self.config.network_config.rpc_ip, - str(self.config.network_config.rpc_port), - self.config.network_config.rpc_username, - self.config.network_config.rpc_password, - "generatetoaddress", - params=[1, address], - ) - logger.info(f"{response}") - if self.signals: - self.signals.chain_data_changed.emit(f"Mined to addresses {addresses}") - b = QPushButton(self.tr("Generate to selected adddresses")) - b.clicked.connect(mine_to_selected_addresses) + b.clicked.connect(self._mine_to_selected_addresses) self.toolbar.insertWidget(self.toolbar.count() - 2, b) hbox = self.create_toolbar_buttons() self.toolbar.insertLayout(self.toolbar.count() - 1, hbox) def create_toolbar_buttons(self) -> QHBoxLayout: - def get_toolbar_buttons() -> Tuple[QComboBox, QComboBox]: - return self.change_button, self.used_button hbox = QHBoxLayout() - buttons = get_toolbar_buttons() + buttons = [self.change_button, self.used_button] for b in buttons: b.setVisible(False) hbox.addWidget(b) diff --git a/bitcoin_safe/gui/qt/analyzer_indicator.py b/bitcoin_safe/gui/qt/analyzer_indicator.py index e662a39..c4428b1 100644 --- a/bitcoin_safe/gui/qt/analyzer_indicator.py +++ b/bitcoin_safe/gui/qt/analyzer_indicator.py @@ -28,12 +28,6 @@ import logging - -from bitcoin_safe.gui.qt.analyzers import AnalyzerMessage, AnalyzerState, BaseAnalyzer -from bitcoin_safe.gui.qt.custom_edits import AnalyzerLineEdit, AnalyzerTextEdit - -logger = logging.getLogger(__name__) - from typing import List, Optional, Union from PyQt6.QtCore import QSize, Qt @@ -49,6 +43,11 @@ QWidget, ) +from bitcoin_safe.gui.qt.analyzers import AnalyzerMessage, AnalyzerState, BaseAnalyzer +from bitcoin_safe.gui.qt.custom_edits import AnalyzerLineEdit, AnalyzerTextEdit + +logger = logging.getLogger(__name__) + class ElidedLabel(QLabel): def __init__(self, elide_mode: Qt.TextElideMode = Qt.TextElideMode.ElideRight): @@ -184,7 +183,8 @@ def update_label_text(self) -> None: class CustomIntAnalyzer(BaseAnalyzer): """Custom validator that allows any input but validates numeric input.""" - def analyze(self, input: str, pos: int = 0) -> AnalyzerMessage: + @staticmethod + def analyze(input: str, pos: int = 0) -> AnalyzerMessage: if input.isdigit(): return AnalyzerMessage("ok", AnalyzerState.Valid) elif not input: @@ -195,7 +195,6 @@ def setup_line_edit(line_edit: Union[AnalyzerLineEdit, AnalyzerTextEdit]): """Set up a QLineEdit with a custom validator that allows all inputs and styles the QLineEdit based on validity.""" analyzer = CustomIntAnalyzer() line_edit.setAnalyzer(analyzer) - # line_edit.textChanged.connect(lambda text, le=line_edit, val=validator: validate_input(le, val)) def validate_input(line_edit: Union[AnalyzerLineEdit, AnalyzerTextEdit], analyzer: CustomIntAnalyzer): """Update the line edit style based on validation.""" diff --git a/bitcoin_safe/gui/qt/analyzers.py b/bitcoin_safe/gui/qt/analyzers.py index f21a51d..831d136 100644 --- a/bitcoin_safe/gui/qt/analyzers.py +++ b/bitcoin_safe/gui/qt/analyzers.py @@ -28,17 +28,6 @@ import logging - -from bitcoin_usb.address_types import SimplePubKeyProvider - -from bitcoin_safe.gui.qt.custom_edits import ( - AnalyzerMessage, - AnalyzerState, - BaseAnalyzer, -) - -logger = logging.getLogger(__name__) - from typing import Callable, Tuple import bdkpython as bdk @@ -46,11 +35,20 @@ from bitcoin_qr_tools.multipath_descriptor import ( MultipathDescriptor as BitcoinQRMultipathDescriptor, ) +from bitcoin_usb.address_types import SimplePubKeyProvider from PyQt6.QtCore import QObject +from bitcoin_safe.gui.qt.custom_edits import ( + AnalyzerMessage, + AnalyzerState, + BaseAnalyzer, +) + from ...keystore import KeyStore from .util import Message +logger = logging.getLogger(__name__) + class KeyOriginAnalyzer(BaseAnalyzer, QObject): def __init__(self, get_expected_key_origin: Callable[[], str], parent: QObject | None) -> None: @@ -65,6 +63,7 @@ def analyze(self, input: str, pos: int = 0) -> AnalyzerMessage: try: input = SimplePubKeyProvider.format_key_origin(input) except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") return AnalyzerMessage(str(e), AnalyzerState.Invalid) if input == self.get_expected_key_origin(): @@ -81,6 +80,7 @@ def analyze(self, input: str, pos: int = 0) -> AnalyzerMessage: try: input = SimplePubKeyProvider.format_fingerprint(input) except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") return AnalyzerMessage(str(e), AnalyzerState.Invalid) if KeyStore.is_fingerprint_valid(input): @@ -104,8 +104,8 @@ def normalize(self, input: str, pos: int = 0) -> Tuple[str, int]: ) try: input = ConverterXpub.convert_slip132_to_bip32(input) - except: - pass + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") return input, pos def analyze(self, input: str, pos: int = 0) -> AnalyzerMessage: @@ -161,7 +161,8 @@ def analyze(self, input: str, pos: int = 0) -> AnalyzerMessage: try: bdk_address = bdk.Address(input, network=self.network) is_valid = bool(bdk_address) - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") is_valid = False if is_valid: diff --git a/bitcoin_safe/gui/qt/attached_widgets.py b/bitcoin_safe/gui/qt/attached_widgets.py index 074f8c3..8c8cf8c 100644 --- a/bitcoin_safe/gui/qt/attached_widgets.py +++ b/bitcoin_safe/gui/qt/attached_widgets.py @@ -29,13 +29,12 @@ import logging from collections import deque - -logger = logging.getLogger(__name__) - from typing import Type from PyQt6.QtWidgets import QWidget +logger = logging.getLogger(__name__) + class AttachedWidgets(deque): def remove_all_of_type(self, cls: Type[QWidget]) -> None: diff --git a/bitcoin_safe/gui/qt/bitcoin_quick_receive.py b/bitcoin_safe/gui/qt/bitcoin_quick_receive.py index e12ff0b..921a303 100644 --- a/bitcoin_safe/gui/qt/bitcoin_quick_receive.py +++ b/bitcoin_safe/gui/qt/bitcoin_quick_receive.py @@ -42,7 +42,9 @@ logger = logging.getLogger(__name__) -class BitcoinQuickReceive(QuickReceive): +class BitcoinQuickReceive( + QuickReceive, +): def __init__( self, wallet_signals: WalletSignals, @@ -62,9 +64,10 @@ def __init__( # signals self.wallet_signals.updated.connect(self.update_content) - self.wallet_signals.language_switch.connect( - lambda: self.update_content(UpdateFilter(refresh_all=True)) - ) + self.wallet_signals.language_switch.connect(self.refresh_all) + + def refresh_all(self): + self.update_content(UpdateFilter(refresh_all=True)) def set_address(self, category: str, address_info: bdk.AddressInfo): address = address_info.address.as_string() diff --git a/bitcoin_safe/gui/qt/block_buttons.py b/bitcoin_safe/gui/qt/block_buttons.py index 5411e05..e7b2911 100644 --- a/bitcoin_safe/gui/qt/block_buttons.py +++ b/bitcoin_safe/gui/qt/block_buttons.py @@ -29,7 +29,8 @@ import enum import logging -from typing import Callable, Dict, List +from functools import partial +from typing import Dict, List from urllib.parse import urlparse import bdkpython as bdk @@ -44,6 +45,7 @@ ) from bitcoin_safe.config import MIN_RELAY_FEE, UserConfig +from bitcoin_safe.execute_config import ENABLE_TIMERS, MEMPOOL_SCHEDULE_TIMER from bitcoin_safe.network_config import ProxyInfo from bitcoin_safe.util import block_explorer_URL_of_projected_block, unit_fee_str @@ -281,14 +283,7 @@ def __init__(self, network: bdk.Network, button_count=3, parent=None, size=100) # Create buttons for i in range(button_count): button = BlockButton(network=network, size=size) - - def create_signal_handler(index) -> Callable: - def send_signal() -> None: - return self.signal_button_click.emit(index) - - return send_signal - - button.clicked.connect(create_signal_handler(i)) + button.clicked.connect(partial(self.signal_button_click.emit, i)) self.buttons.append(button) layout.addWidget(button) @@ -300,7 +295,7 @@ def set_active(self, index: int, exclusive=True): button.set_active(i == index) -class ObjectRequiringMempool(QObject): +class MempoolScheduler(QObject): def __init__(self, proxies: Dict | None, mempool_data: MempoolData, parent=None) -> None: super().__init__(parent=parent) self.proxies = proxies @@ -309,7 +304,8 @@ def __init__(self, proxies: Dict | None, mempool_data: MempoolData, parent=None) self.timer = QTimer() self.timer.timeout.connect(self.set_data_from_mempoolspace) - self.timer.start(10 * 60 * 1000) # 10 minutes in milliseconds + if ENABLE_TIMERS: + self.timer.start(MEMPOOL_SCHEDULE_TIMER) def set_mempool_block_unknown_fee_rate(self, i, confirmation_time: bdk.BlockTime | None = None) -> None: logger.error("This should not be called") @@ -318,23 +314,24 @@ def set_data_from_mempoolspace(self): self.mempool_data.set_data_from_mempoolspace(proxies=self.proxies) -class BaseBlock(QObject): +class BaseBlock(VerticalButtonGroup): signal_click: TypedPyQtSignal[float] = pyqtSignal(float) # type: ignore def __init__( self, mempool_data: MempoolData, - button_group: VerticalButtonGroup, + network: bdk.Network, confirmation_time: bdk.BlockTime | None = None, + button_count=3, parent=None, + size=100, ) -> None: - QObject.__init__(self, parent=parent) + super().__init__(network=network, button_count=button_count, parent=parent, size=size) self.confirmation_time = confirmation_time self.mempool_data = mempool_data - self.button_group = button_group # signals - self.button_group.signal_button_click.connect(self._on_button_click) + self.signal_button_click.connect(self._on_button_click) self.mempool_data.signal_data_updated.connect(self.refresh) def refresh(self, **kwargs) -> None: @@ -347,7 +344,7 @@ def _on_button_click(self, i: int) -> None: pass -class MempoolButtons(BaseBlock, ObjectRequiringMempool): +class MempoolButtons(BaseBlock): "Showing multiple buttons of the next, the 2. and the 3. block templates according to the mempool" def __init__( @@ -358,13 +355,18 @@ def __init__( max_button_count=3, parent=None, ) -> None: - button_group = VerticalButtonGroup( - network=mempool_data.network_config.network, button_count=max_button_count, parent=parent + super().__init__( + mempool_data=mempool_data, + confirmation_time=None, + network=mempool_data.network_config.network, + button_count=max_button_count, + parent=parent, ) - BaseBlock.__init__( - self, mempool_data=mempool_data, button_group=button_group, confirmation_time=None, parent=parent + self.mempool_scheduler = MempoolScheduler( + proxies=proxies, + mempool_data=mempool_data, + parent=parent, ) - ObjectRequiringMempool.__init__(self, proxies=proxies, mempool_data=mempool_data, parent=parent) self.fee_rate = fee_rate self.refresh() @@ -377,7 +379,7 @@ def refresh(self, fee_rate=None, **kwargs) -> None: block_index = self.mempool_data.fee_rate_to_projected_block_index(self.fee_rate) - for i, button in enumerate(self.button_group.buttons): + for i, button in enumerate(self.buttons): block_number = i + 1 button.setVisible(i < max(1, self.mempool_data.num_mempool_blocks())) button.label_title.set( @@ -400,11 +402,11 @@ def refresh(self, fee_rate=None, **kwargs) -> None: def _on_button_click(self, i: int) -> None: logger.debug(f"Clicked button {i}: {self.mempool_data.median_block_fee_rate(i)}") - self.button_group.set_active(i) + self.set_active(i) self.signal_click.emit(self.mempool_data.median_block_fee_rate(i)) -class MempoolProjectedBlock(BaseBlock, ObjectRequiringMempool): +class MempoolProjectedBlock(BaseBlock): "The Button showing the block in which the fee_rate fits" def __init__( @@ -414,21 +416,21 @@ def __init__( fee_rate: float = 1, parent=None, ) -> None: - button_group = VerticalButtonGroup( - network=mempool_data.network_config.network, size=100, button_count=1, parent=parent - ) - - BaseBlock.__init__( - self, mempool_data=mempool_data, button_group=button_group, confirmation_time=None, parent=parent + super().__init__( + mempool_data=mempool_data, + confirmation_time=None, + network=mempool_data.network_config.network, + button_count=1, + parent=parent, + size=100, ) - ObjectRequiringMempool.__init__( - self, - proxies=( + self.mempool_scheduler = MempoolScheduler( + ( ProxyInfo.parse(config.network_config.proxy_url).get_requests_proxy_dict() if config.network_config.proxy_url else None ), - mempool_data=mempool_data, + mempool_data, parent=parent, ) @@ -442,7 +444,7 @@ def set_url(self, url: str) -> None: self.url = url def set_unknown_fee_rate(self) -> None: - for button in self.button_group.buttons: + for button in self.buttons: button.clear_labels() button.label_title.set(self.tr("Unconfirmed"), BlockType.projected) button.explorer_explorer_icon.setVisible(True) @@ -458,7 +460,7 @@ def refresh(self, fee_rate=None, **kwargs) -> None: block_index = self.mempool_data.fee_rate_to_projected_block_index(self.fee_rate) - for button in self.button_group.buttons: + for button in self.buttons: button.label_title.set( self.tr("~{n}. Block").format(n=format_block_number(block_index + 1)), BlockType.projected @@ -495,15 +497,13 @@ def __init__( fee_rate: float | None = None, parent=None, ) -> None: - button_group = VerticalButtonGroup( - network=mempool_data.network_config.network, button_count=1, parent=parent, size=120 - ) - super().__init__( - parent=parent, mempool_data=mempool_data, - button_group=button_group, confirmation_time=confirmation_time, + network=mempool_data.network_config.network, + button_count=1, + parent=parent, + size=120, ) self.fee_rate = fee_rate @@ -520,7 +520,7 @@ def refresh( if not self.confirmation_time: return - for i, button in enumerate(self.button_group.buttons): + for i, button in enumerate(self.buttons): button.label_title.set( self.tr("Block {n}").format(n=format_block_number(self.confirmation_time.height)), BlockType.confirmed, diff --git a/bitcoin_safe/gui/qt/buttonedit.py b/bitcoin_safe/gui/qt/buttonedit.py index 35132e3..26bed5b 100644 --- a/bitcoin_safe/gui/qt/buttonedit.py +++ b/bitcoin_safe/gui/qt/buttonedit.py @@ -28,6 +28,7 @@ import logging +from functools import partial from typing import Callable, List, Optional, Union from bdkpython import bdk @@ -55,6 +56,7 @@ ) from bitcoin_safe.gui.qt.util import Message, clear_layout, do_copy, icon_path from bitcoin_safe.i18n import translate +from bitcoin_safe.signal_tracker import SignalTools, SignalTracker from bitcoin_safe.typestubs import TypedPyQtSignalNo from ...signals import TypedPyQtSignal @@ -186,6 +188,7 @@ def __init__( **kwargs, ) -> None: super().__init__(parent=parent) + self.signal_tracker = SignalTracker() self.input_field: Union[AnalyzerTextEdit, AnalyzerLineEdit] = ( input_field if input_field else AnalyzerLineEdit(parent=self) ) @@ -260,21 +263,21 @@ def add_button( self.button_container.append_button(button) return button + def _on_copy(self) -> None: + do_copy(self.text()) + def add_copy_button( self, ) -> 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") + icon_path("copy.png"), self._on_copy, tooltip=translate("d", "Copy to clipboard") ) return self.copy_button def set_input_field(self, input_widget: Union[AnalyzerTextEdit, AnalyzerLineEdit]) -> None: # Remove the current input field from the layout and delete it self.input_field.setParent(None) # type: ignore[call-overload] - self.input_field.deleteLater() # Set the new input field and add it to the layout self.input_field = input_widget @@ -300,45 +303,45 @@ def setPlaceholderText(self, value: str | None) -> None: def setReadOnly(self, value: bool) -> None: self.input_field.setReadOnly(value) + def _exception_callback(self, e: Exception) -> None: + if isinstance(e, DecodingException): + Message("Could not recognize the input.") + else: + Message(str(e)) + + def _result_callback_input_qr_from_camera(self, data: Data) -> None: + if hasattr(self, "setText"): + self.setText(str(data.data_as_string())) + def input_qr_from_camera( self, network: bdk.Network, set_data_as_string=True, close_camera_on_result=True ) -> 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: Data) -> None: - if set_data_as_string and hasattr(self, "setText"): - self.setText(str(data.data_as_string())) self.close_all_video_widgets.emit() self._temp_bitcoin_video_widget = BitcoinVideoWidget( network=network, close_on_result=close_camera_on_result ) - self._temp_bitcoin_video_widget.signal_data.connect(result_callback) + if set_data_as_string: + self._temp_bitcoin_video_widget.signal_data.connect(self._result_callback_input_qr_from_camera) self._temp_bitcoin_video_widget.signal_data.connect(self.signal_data) - self._temp_bitcoin_video_widget.signal_recognize_exception.connect(exception_callback) + self._temp_bitcoin_video_widget.signal_recognize_exception.connect(self._exception_callback) self._temp_bitcoin_video_widget.show() def add_qr_input_from_camera_button( self, network: bdk.Network, set_data_as_string=False, close_camera_on_result=True ) -> SquareButton: - def input_qr_from_camera(): - self.input_qr_from_camera( + + self.button_camera = self.add_button( + icon_path("camera.svg"), + partial( + self.input_qr_from_camera, network=network, set_data_as_string=set_data_as_string, close_camera_on_result=close_camera_on_result, - ) - - self.button_camera = self.add_button( - icon_path("camera.svg"), input_qr_from_camera, translate("d", "Read QR code from camera") + ), + translate("d", "Read QR code from camera"), ) - # side-effect: we export these methods: - self.on_qr_from_camera_input_btn = input_qr_from_camera - return self.button_camera def add_pdf_buttton( @@ -351,48 +354,60 @@ def add_pdf_buttton( ) return self.pdf_button + def _on_click_add_random_mnemonic_button(self, callback_seed: Callable | None = None) -> None: + seed = bdk.Mnemonic(bdk.WordCount.WORDS12).as_string() + self.setText(seed) + if callback_seed: + callback_seed(seed) + def add_random_mnemonic_button( self, - callback_seed=None, + callback_seed: Callable | None = None, ) -> SquareButton: - def on_click() -> None: - seed = bdk.Mnemonic(bdk.WordCount.WORDS12).as_string() - self.setText(seed) - if callback_seed: - callback_seed(seed) self.mnemonic_button = self.add_button( - icon_path("dice.svg"), on_click, tooltip=translate("d", "Create random mnemonic") + icon_path("dice.svg"), + partial(self._on_click_add_random_mnemonic_button, callback_seed=callback_seed), + tooltip=translate("d", "Create random mnemonic"), ) return self.mnemonic_button def addResetButton(self, get_reset_text) -> SquareButton: - def on_click() -> None: - self.setText(get_reset_text()) - - return self.add_button("reset-update.svg", on_click, "Reset") + return self.add_button("reset-update.svg", partial(self.setText, get_reset_text()), "Reset") # button.setStyleSheet("background-color: white;") + def _on_click_add_open_file_button( + self, callback_open_filepath: Callable | None = None, filter=None + ) -> None: + file_path, _ = QFileDialog.getOpenFileName( + self, + translate("open_file", "Open Transaction/PSBT"), + "", + filter, + ) + if not file_path: + logger.debug("No file selected") + return + + logger.info(f"Selected file: {file_path}") + if callback_open_filepath: + callback_open_filepath(file_path) + def add_open_file_button( self, - callback_open_filepath, + callback_open_filepath: Callable | None = None, filter=translate("open_file", "All Files (*);;PSBT (*.psbt);;Transation (*.tx)"), ) -> QPushButton: - def on_click() -> None: - file_path, _ = QFileDialog.getOpenFileName( - self, - translate("open_file", "Open Transaction/PSBT"), - "", - filter, - ) - if not file_path: - logger.debug("No file selected") - return - - logger.info(f"Selected file: {file_path}") - callback_open_filepath(file_path) - button = self.add_button(None, on_click, translate("d", "Open file")) + button = self.add_button( + None, + partial( + self._on_click_add_open_file_button, + callback_open_filepath=callback_open_filepath, + filter=filter, + ), + translate("d", "Open file"), + ) icon = (self.style() or QStyle()).standardIcon(QStyle.StandardPixmap.SP_DirOpenIcon) button.setIcon(icon) @@ -419,6 +434,12 @@ def format_and_apply_validator(self) -> None: self.format_as_error(error) self.setToolTip(analysis.msg if error else "") + def close(self): + self.signal_tracker.disconnect_all() + SignalTools.disconnect_all_signals_from(self) + self.setParent(None) + super().close() + # Example usage if __name__ == "__main__": diff --git a/bitcoin_safe/gui/qt/category_list.py b/bitcoin_safe/gui/qt/category_list.py index 5ca13c5..05cae31 100644 --- a/bitcoin_safe/gui/qt/category_list.py +++ b/bitcoin_safe/gui/qt/category_list.py @@ -28,27 +28,25 @@ import logging - -from bitcoin_safe.typestubs import TypedPyQtSignal - -logger = logging.getLogger(__name__) - -from typing import Callable, List +from typing import List from PyQt6.QtCore import pyqtSignal from PyQt6.QtGui import QColor +from bitcoin_safe.category_info import SubtextType +from bitcoin_safe.typestubs import TypedPyQtSignal + from ...signals import UpdateFilter, UpdateFilterReason, WalletSignals from .taglist import CustomListWidget, TagEditor from .util import category_color +logger = logging.getLogger(__name__) + class CategoryList(CustomListWidget): def __init__( self, - categories: List[str], wallet_signals: WalletSignals, - get_sub_texts: Callable[[], List[str]], parent=None, immediate_release=True, ) -> None: @@ -60,8 +58,6 @@ def __init__( immediate_release=immediate_release, ) - self.categories = categories - self.get_sub_texts = get_sub_texts self.wallet_signals = wallet_signals self.wallet_signals.updated.connect(self.refresh) self.refresh(UpdateFilter(refresh_all=True)) @@ -98,7 +94,7 @@ def refresh(self, update_filter: UpdateFilter | None = None) -> None: return logger.debug(f"{self.__class__.__name__} update_with_filter {update_filter}") - self.recreate(self.categories, sub_texts=self.get_sub_texts()) + self.recreate(self.wallet_signals.get_category_infos() or list()) @classmethod def color(cls, category) -> QColor: @@ -112,16 +108,14 @@ class CategoryEditor(TagEditor): def __init__( self, - get_categories: Callable[[], List[str]], wallet_signals: WalletSignals, - get_sub_texts: Callable[[], List[str]], parent=None, prevent_empty_categories=True, + subtext_type: SubtextType = SubtextType.balance, ) -> None: - super().__init__(parent, get_categories(), sub_texts=get_sub_texts()) + category_infos = wallet_signals.get_category_infos.emit() or list() + super().__init__(parent=parent, category_infos=category_infos, subtext_type=subtext_type) - self.get_categories = get_categories - self.get_sub_texts = get_sub_texts self.wallet_signals = wallet_signals self.prevent_empty_categories = prevent_empty_categories @@ -147,7 +141,9 @@ def updateUi(self) -> None: self.refresh(UpdateFilter(refresh_all=True)) def on_added(self, category) -> None: - if not category or category in self.get_categories(): + category_infos = self.wallet_signals.get_category_infos.emit() or [] + categories = [category_info.category for category_info in category_infos] + if not category or category in categories: return self.signal_category_added.emit(category) @@ -156,13 +152,15 @@ def on_added(self, category) -> None: ) def on_delete(self, category: str) -> None: - if category not in self.get_categories(): + category_infos = self.wallet_signals.get_category_infos() or list() + categories = [category_info.category for category_info in category_infos] + if category not in categories: return self.wallet_signals.updated.emit( UpdateFilter(categories=[category], reason=UpdateFilterReason.CategoryDeleted) ) - if not self.get_categories() and self.prevent_empty_categories: + if not categories and self.prevent_empty_categories: self.list_widget.add("Default") self.wallet_signals.updated.emit( UpdateFilter(refresh_all=True, reason=UpdateFilterReason.CategoryDeleted) @@ -173,7 +171,7 @@ def refresh(self, update_filter: UpdateFilter) -> None: return logger.debug(f"{self.__class__.__name__} update_with_filter {update_filter}") - self.list_widget.recreate(self.get_categories(), sub_texts=self.get_sub_texts()) + self.list_widget.recreate(self.wallet_signals.get_category_infos() or list()) @classmethod def color(cls, category) -> QColor: diff --git a/bitcoin_safe/gui/qt/custom_edits.py b/bitcoin_safe/gui/qt/custom_edits.py index e3b18a8..b97028a 100644 --- a/bitcoin_safe/gui/qt/custom_edits.py +++ b/bitcoin_safe/gui/qt/custom_edits.py @@ -180,7 +180,7 @@ def __init__( self._completer.setFilterMode(Qt.MatchFlag.MatchContains) self._completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) self._model = QStringListModel() - self.setCompleter(self._completer) + # self.setCompleter(self._completer) def set_completer_list(self, words: Iterable[str]): self._model.setStringList(words) diff --git a/bitcoin_safe/gui/qt/data_tab_widget.py b/bitcoin_safe/gui/qt/data_tab_widget.py index 8ab62e5..a1e1224 100644 --- a/bitcoin_safe/gui/qt/data_tab_widget.py +++ b/bitcoin_safe/gui/qt/data_tab_widget.py @@ -28,25 +28,22 @@ import logging -from typing import Dict, Generic, Type, TypeVar +from typing import Dict, Generic, TypeVar + +from PyQt6.QtGui import QIcon +from PyQt6.QtWidgets import QApplication, QWidget from bitcoin_safe.gui.qt.histtabwidget import HistTabWidget logger = logging.getLogger(__name__) -from typing import Dict, Generic, Type, TypeVar - -from PyQt6.QtGui import QIcon -from PyQt6.QtWidgets import QApplication, QWidget - T = TypeVar("T") T2 = TypeVar("T2") -class DataTabWidget(Generic[T], HistTabWidget): - def __init__(self, data_class: Type[T], parent=None) -> None: - super().__init__(parent) - self._data_class = data_class +class DataTabWidget(HistTabWidget, Generic[T]): + def __init__(self, parent=None) -> None: + super().__init__(parent=parent) self._tab_data: Dict[QWidget, T] = {} def setTabData(self, widget: QWidget, data: T) -> None: @@ -56,7 +53,7 @@ def tabData(self, index: int) -> T | None: tab = self.widget(index) if not tab: return None - return self._tab_data[tab] + return self._tab_data.get(tab) def get_data_for_tab(self, tab: QWidget) -> T: return self._tab_data[tab] @@ -68,9 +65,7 @@ def getCurrentTabData(self) -> T | None: return self._tab_data[current_widget] def getAllTabData(self) -> Dict[QWidget, T]: - widgets_raw = [self.widget(i) for i in range(self.count())] - widgets = [w for w in widgets_raw if w] - return {widget: self.get_data_for_tab(widget) for widget in widgets} + return self._tab_data def clearTabData(self) -> None: self._tab_data.clear() @@ -122,9 +117,11 @@ def add_tab( def removeTab(self, index: int) -> None: widget = self.widget(index) - super().removeTab(index) if widget in self._tab_data: del self._tab_data[widget] + super().removeTab(index) + if widget: + widget.setParent(None) # Detach it from the parent if __name__ == "__main__": @@ -134,7 +131,7 @@ def removeTab(self, index: int) -> None: app = QApplication(sys.argv) - tab_widget = DataTabWidget(str) + tab_widget = DataTabWidget[str]() tab_widget.setMovable(True) tab1 = QWidget() tab2 = QWidget() diff --git a/bitcoin_safe/gui/qt/descriptor_edit.py b/bitcoin_safe/gui/qt/descriptor_edit.py index 2ec14e3..8a23604 100644 --- a/bitcoin_safe/gui/qt/descriptor_edit.py +++ b/bitcoin_safe/gui/qt/descriptor_edit.py @@ -28,12 +28,16 @@ import logging -from typing import Callable, Optional +from typing import Optional +import bdkpython as bdk from bitcoin_qr_tools.data import Data from bitcoin_qr_tools.multipath_descriptor import ( MultipathDescriptor as BitcoinQRMultipathDescriptor, ) +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QCloseEvent +from PyQt6.QtWidgets import QDialog, QVBoxLayout, QWidget from bitcoin_safe.descriptors import MultipathDescriptor from bitcoin_safe.gui.qt.analyzers import DescriptorAnalyzer @@ -45,18 +49,15 @@ from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo from bitcoin_safe.wallet import Wallet -logger = logging.getLogger(__name__) - - -import bdkpython as bdk -from PyQt6.QtCore import Qt, pyqtSignal -from PyQt6.QtWidgets import QDialog, QVBoxLayout - from ...pdfrecovery import make_and_open_pdf from .util import Message, MessageType, icon_path +logger = logging.getLogger(__name__) + class DescriptorExport(QDialog): + aboutToClose: TypedPyQtSignal[QWidget] = pyqtSignal(QWidget) # type: ignore + def __init__( self, descriptor: MultipathDescriptor, @@ -87,16 +88,19 @@ def __init__( self._layout = QVBoxLayout(self) self._layout.addWidget(self.export_widget) + def closeEvent(self, event: QCloseEvent | None): + self.aboutToClose.emit(self) # Emit the signal when the window is about to close + super().closeEvent(event) + -class DescriptorEdit(ButtonEdit): +class DescriptorEdit(ButtonEdit, ThreadingManager): signal_descriptor_change: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore def __init__( self, network: bdk.Network, signals_min: SignalsMin, - get_lang_code: Callable[[], str], - get_wallet: Optional[Callable[[], Wallet]] = None, + wallet: Optional[Wallet] = None, signal_update: TypedPyQtSignalNo | None = None, threading_parent: ThreadingManager | None = None, ) -> None: @@ -108,32 +112,33 @@ def __init__( threading_parent=threading_parent, close_all_video_widgets=signals_min.close_all_video_widgets, ) # type: ignore - self.threading_parent = threading_parent self.signals_min = signals_min self.network = network self.input_field - self.get_wallet = get_wallet - - 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(), lang_code=get_lang_code()) + self.wallet = wallet 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) + if wallet is not None: + self.add_pdf_buttton(self._do_pdf) self.add_qr_input_from_camera_button( network=self.network, ) - self.signal_data.connect(self._custom_handle_camera_input) self.input_field.setAnalyzer(DescriptorAnalyzer(self.network, parent=self)) - self.input_field.textChanged.connect(self.on_input_field_textChanged) + + # signals + self.signal_tracker.connect(self.signal_data, self._custom_handle_camera_input) + self.signal_tracker.connect(self.input_field.textChanged, self.on_input_field_textChanged) + + def _do_pdf(self) -> None: + if not self.wallet: + Message( + self.tr("Wallet setup not finished. Please finish before creating a Backup pdf."), + type=MessageType.Error, + ) + return + + make_and_open_pdf(self.wallet, lang_code=self.signals_min.get_current_lang_code.emit() or "en_US") def on_input_field_textChanged(self): self.signal_descriptor_change.emit(self.text_cleaned()) @@ -153,11 +158,12 @@ def show_export_widget(self): signals_min=self.signals_min, parent=self, network=self.network, - threading_parent=self.threading_parent, - wallet_id=self.get_wallet().id if self.get_wallet is not None else "Multisig", + threading_parent=self, + wallet_id=self.wallet.id if self.wallet is not None else "Multisig", ) dialog.show() - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") logger.error( f"Could not create a DescriptorExport for {self.__class__.__name__} with text {self.text()}" ) diff --git a/bitcoin_safe/gui/qt/descriptor_ui.py b/bitcoin_safe/gui/qt/descriptor_ui.py index 063a4a3..4c02770 100644 --- a/bitcoin_safe/gui/qt/descriptor_ui.py +++ b/bitcoin_safe/gui/qt/descriptor_ui.py @@ -28,24 +28,13 @@ import logging +from typing import Optional, Tuple from bitcoin_qr_tools.multipath_descriptor import ( MultipathDescriptor as BitcoinQRMultipathDescriptor, ) - -from bitcoin_safe.gui.qt.descriptor_edit import DescriptorEdit -from bitcoin_safe.gui.qt.dialogs import question_dialog -from bitcoin_safe.gui.qt.keystore_uis import KeyStoreUIs -from bitcoin_safe.gui.qt.util import Message, MessageType -from bitcoin_safe.i18n import translate -from bitcoin_safe.threading_manager import ThreadingManager - -logger = logging.getLogger(__name__) - -from typing import Callable, Optional, Tuple - from bitcoin_usb.address_types import get_address_types -from PyQt6.QtCore import QMargins, QObject, Qt, pyqtSignal +from PyQt6.QtCore import QMargins, Qt, pyqtSignal from PyQt6.QtWidgets import ( QComboBox, QDialogButtonBox, @@ -60,13 +49,23 @@ QWidget, ) +from bitcoin_safe.gui.qt.descriptor_edit import DescriptorEdit +from bitcoin_safe.gui.qt.dialogs import question_dialog +from bitcoin_safe.gui.qt.keystore_uis import KeyStoreUIs +from bitcoin_safe.gui.qt.util import Message, MessageType +from bitcoin_safe.i18n import translate +from bitcoin_safe.signal_tracker import SignalTools, SignalTracker +from bitcoin_safe.threading_manager import ThreadingManager + from ...descriptors import AddressType, get_default_address_type from ...signals import SignalsMin, TypedPyQtSignalNo from ...wallet import ProtoWallet, Wallet from .block_change_signals import BlockChangesSignals +logger = logging.getLogger(__name__) + -class DescriptorUI(QObject): +class DescriptorUI(QWidget): signal_qtwallet_apply_setting_changes: TypedPyQtSignalNo = pyqtSignal() # type: ignore signal_qtwallet_cancel_setting_changes: TypedPyQtSignalNo = pyqtSignal() # type: ignore signal_qtwallet_cancel_wallet_creation: TypedPyQtSignalNo = pyqtSignal() # type: ignore @@ -75,35 +74,31 @@ def __init__( self, protowallet: ProtoWallet, signals_min: SignalsMin, - get_lang_code: Callable[[], str], - get_wallet: Optional[Callable[[], Wallet]] = None, + wallet: Optional[Wallet] = None, threading_parent: ThreadingManager | None = None, ) -> None: super().__init__() + self.signal_tracker = SignalTracker() + self._layout = QVBoxLayout(self) # if we are in the wallet setp process, then wallet = None self.protowallet = protowallet - self.get_wallet = get_wallet + self.wallet = wallet self.signals_min = signals_min - self.get_lang_code = get_lang_code - self.threading_parent = threading_parent self.no_edit_mode = (self.protowallet.threshold, len(self.protowallet.keystores)) in [(1, 1), (2, 3)] - self.tab = QWidget() - self.tab_layout = QVBoxLayout(self.tab) - - self.create_wallet_type_and_descriptor() + self.create_wallet_type_and_descriptor(threading_parent=threading_parent) self.repopulate_comboBox_address_type(self.protowallet.is_multisig()) self.edit_descriptor.signal_descriptor_change.connect(self.on_descriptor_change) self.keystore_uis = KeyStoreUIs( - get_editable_protowallet=lambda: self.protowallet, + get_editable_protowallet=self.get_editable_protowallet, get_address_type=self.get_address_type_from_ui, signals_min=signals_min, ) - self.tab_layout.addWidget(self.keystore_uis) + self._layout.addWidget(self.keystore_uis) self.keystore_uis.setCurrentIndex(0) @@ -115,6 +110,9 @@ def __init__( self.updateUi() signals_min.language_switch.connect(self.updateUi) + def get_editable_protowallet(self): + return self.protowallet + def updateUi(self) -> None: self.label_signers.setText(self.tr("Required Signers")) self.label_gap.setText(self.tr("Scan Addresses ahead")) @@ -143,7 +141,8 @@ def on_wallet_ui_changes(self) -> None: self.set_ui_descriptor() self.keystore_uis.set_keystore_ui_from_protowallet() self.set_wallet_ui_from_protowallet() - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") logger.warning("on_wallet_ui_changes: Invalid input") self._set_keystore_tabs() @@ -155,7 +154,7 @@ def on_descriptor_change(self, new_value: str) -> None: old_descriptor = self.protowallet.to_multipath_descriptor() if old_descriptor and (new_value == old_descriptor.as_string()): - logger.info("Descriptor unchanged") + logger.info(self.tr("Descriptor unchanged")) return else: logger.info(f"Descriptor changed: {old_descriptor} --> {new_value}") @@ -176,6 +175,7 @@ def on_descriptor_change(self, new_value: str) -> None: self.keystore_uis.set_keystore_ui_from_protowallet() self.disable_fields() except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") Message(str(e), title="Error", type=MessageType.Error) return @@ -203,7 +203,7 @@ def _set_keystore_tabs(self) -> None: self.spin_signers.setMaximum(10) def set_wallet_ui_from_protowallet(self) -> None: - with BlockChangesSignals([self.tab]): + with BlockChangesSignals([self]): logger.debug(f"{self.__class__.__name__} set_wallet_ui_from_protowallet") self.repopulate_comboBox_address_type(self.protowallet.is_multisig()) self.comboBox_address_type.setCurrentText(self.protowallet.address_type.name) @@ -240,7 +240,7 @@ def set_all_ui_from_protowallet(self) -> None: - Keystore UI (e.g. xpubs) - descriptor ui """ - with BlockChangesSignals([self.tab]): + with BlockChangesSignals([self]): self.keystore_uis.set_keystore_ui_from_protowallet() self.set_wallet_ui_from_protowallet() self.set_ui_descriptor() @@ -293,7 +293,8 @@ def set_ui_descriptor(self) -> None: self.edit_descriptor.setText(multipath_descriptor.as_string_private()) else: self.edit_descriptor.setText("") - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") self.edit_descriptor.setText("") self.edit_descriptor.format_and_apply_validator() @@ -305,7 +306,7 @@ def disable_fields(self) -> None: self.label_signers.setHidden(self.no_edit_mode) self.label_of.setHidden(self.no_edit_mode) - with BlockChangesSignals([self.tab]): + with BlockChangesSignals([self]): self.set_combo_box_address_type_default() self.spin_signers.setValue(len(self.protowallet.keystores)) @@ -317,7 +318,7 @@ def disable_fields(self) -> None: self.spin_signers.setDisabled(True) def repopulate_comboBox_address_type(self, is_multisig: bool) -> None: - with BlockChangesSignals([self.tab]): + with BlockChangesSignals([self]): # Fetch the new address types address_types = get_address_types(is_multisig) address_type_names = [a.name for a in address_types] @@ -339,8 +340,8 @@ def repopulate_comboBox_address_type(self, is_multisig: bool) -> None: 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) -> None: - box_wallet_type_and_descriptor = QWidget(self.tab) + def create_wallet_type_and_descriptor(self, threading_parent: ThreadingManager | None = None) -> None: + box_wallet_type_and_descriptor = QWidget(self) box_wallet_type_and_descriptor_layout = QHBoxLayout(box_wallet_type_and_descriptor) current_margins = box_wallet_type_and_descriptor_layout.contentsMargins() @@ -427,10 +428,9 @@ def create_wallet_type_and_descriptor(self) -> None: self.edit_descriptor = DescriptorEdit( network=self.protowallet.network, signals_min=self.signals_min, - get_wallet=self.get_wallet, + wallet=self.wallet, signal_update=self.signals_min.language_switch, - threading_parent=self.threading_parent, - get_lang_code=self.get_lang_code, + threading_parent=threading_parent, ) self.edit_descriptor.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) @@ -438,7 +438,7 @@ def create_wallet_type_and_descriptor(self) -> None: box_wallet_type_and_descriptor_layout.addWidget(self.groupBox_wallet_descriptor) - self.tab_layout.addWidget(box_wallet_type_and_descriptor) + self._layout.addWidget(box_wallet_type_and_descriptor) self.spin_signers.valueChanged.connect(self.on_spin_signer_changed) self.spin_req.valueChanged.connect(self.on_spin_threshold_changed) @@ -456,5 +456,12 @@ def create_button_bar(self) -> QDialogButtonBox: if _button := self.button_box.button(QDialogButtonBox.StandardButton.Discard): _button.clicked.connect(self.signal_qtwallet_cancel_wallet_creation.emit) - self.tab_layout.addWidget(self.button_box, 0, Qt.AlignmentFlag.AlignRight) + self._layout.addWidget(self.button_box, 0, Qt.AlignmentFlag.AlignRight) return self.button_box + + def close(self): + self.signal_tracker.disconnect_all() + SignalTools.disconnect_all_signals_from(self) + self.edit_descriptor.close() + self.setParent(None) + super().close() diff --git a/bitcoin_safe/gui/qt/dialog_import.py b/bitcoin_safe/gui/qt/dialog_import.py index 213a190..7652dd4 100644 --- a/bitcoin_safe/gui/qt/dialog_import.py +++ b/bitcoin_safe/gui/qt/dialog_import.py @@ -28,11 +28,19 @@ import logging +from functools import partial from typing import Callable, Optional import bdkpython as bdk from PyQt6.QtCore import QObject, Qt, pyqtSignal -from PyQt6.QtGui import QDragEnterEvent, QDropEvent, QKeyEvent, QKeySequence, QShortcut +from PyQt6.QtGui import ( + QCloseEvent, + QDragEnterEvent, + QDropEvent, + QKeyEvent, + QKeySequence, + QShortcut, +) from PyQt6.QtWidgets import ( QApplication, QDialog, @@ -57,7 +65,7 @@ def is_binary(file_path) -> bool: """ try: with open(file_path, "r") as f: - for chunk in iter(lambda: f.read(1024), ""): + for chunk in iter(partial(f.read, 1024), ""): if "\0" in chunk: # found null byte return True except UnicodeDecodeError: @@ -124,6 +132,7 @@ def dropEvent(self, event: QDropEvent | None) -> None: file_path = mime_data.urls()[0].toLocalFile() if self.process_filepath: self.process_filepath(file_path) + return # prevent super().dropEvent(event) super().dropEvent(event) @@ -162,6 +171,8 @@ def process_filepath(self, file_path: str) -> None: class ImportDialog(QDialog): + aboutToClose: TypedPyQtSignal[QWidget] = pyqtSignal(QWidget) # type: ignore + def __init__( self, network: bdk.Network, @@ -183,7 +194,7 @@ def __init__( self.instruction_label = QLabel(text_instruction_label) self.text_edit = DragAndDropButtonEdit( network=network, - callback_enter=self.process_input, + callback_enter=self.on_ok_button, callback_esc=self.close, close_all_video_widgets=close_all_video_widgets, ) @@ -206,26 +217,32 @@ def __init__( if self.button_ok: self.button_ok.setDefault(True) self.button_ok.setText(text_button_ok) - self.button_ok.clicked.connect(lambda: self.process_input(self.text_edit.text())) + self.button_ok.clicked.connect(self.on_button_ok_clicked) layout.addWidget(self.buttonBox) - # connect signals - self.text_edit.signal_drop_file.connect(self.process_input) - shortcut = QShortcut(QKeySequence("Return"), self) - shortcut.activated.connect(self.process_input) + shortcut.activated.connect(self.on_ok_button) + self.shortcut_close = QShortcut(QKeySequence("Ctrl+W"), self) + self.shortcut_close.activated.connect(self.close) + self.shortcut_close = QShortcut(QKeySequence("ESC"), self) + self.shortcut_close.activated.connect(self.close) + + def on_button_ok_clicked(self): + self.on_ok_button(self.text_edit.text()) def keyPressEvent(self, event: QKeyEvent | None) -> None: if event and event.key() == Qt.Key.Key_Escape: self.close() - def process_input(self, s: str) -> None: + def on_ok_button(self, s: str) -> None: if self.on_open: + self.close() self.on_open(s) - # close lets the entire application crash - self.deleteLater() + def closeEvent(self, event: QCloseEvent | None): + self.aboutToClose.emit(self) # Emit the signal when the window is about to close + super().closeEvent(event) if __name__ == "__main__": diff --git a/bitcoin_safe/gui/qt/dialogs.py b/bitcoin_safe/gui/qt/dialogs.py index dc687d1..f5e0670 100644 --- a/bitcoin_safe/gui/qt/dialogs.py +++ b/bitcoin_safe/gui/qt/dialogs.py @@ -164,7 +164,7 @@ def __init__(self, parent=None, window_title=None, label_text=None) -> None: self.show_password_action1.setFont( QFont("Arial", 12) ) # Set the font to Arial to ensure Unicode support - self.show_password_action1.triggered.connect(lambda: self.toggle_password_visibility()) + self.show_password_action1.triggered.connect(self.toggle_password_visibility) self.password_input1.addAction(self.show_password_action1, QLineEdit.ActionPosition.TrailingPosition) # Second password input diff --git a/bitcoin_safe/gui/qt/downloader.py b/bitcoin_safe/gui/qt/downloader.py index 8a7a5b3..12df247 100644 --- a/bitcoin_safe/gui/qt/downloader.py +++ b/bitcoin_safe/gui/qt/downloader.py @@ -80,6 +80,7 @@ def run(self) -> None: self.progress.emit(int(100 * dl / int(content_length))) self.finished.emit() except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") self.aborted.emit() logger.warning(str(e)) diff --git a/bitcoin_safe/gui/qt/expandable_widget.py b/bitcoin_safe/gui/qt/expandable_widget.py index fb5b7eb..f46f299 100644 --- a/bitcoin_safe/gui/qt/expandable_widget.py +++ b/bitcoin_safe/gui/qt/expandable_widget.py @@ -120,7 +120,7 @@ def add_header_widget(self, widget: QWidget) -> None: if not child_widget: break if child_widget is not self.toggleButton: - child_widget.deleteLater() + child_widget.close() # Add the new widget before the toggle button self.header._layout.insertWidget(0, widget, 1) @@ -134,7 +134,7 @@ def add_content_widget(self, widget: QWidget) -> None: break child_widget = layout_item.widget() if child_widget: - child_widget.deleteLater() + child_widget.close() self.expandableWidget_layout.addWidget(widget) diff --git a/bitcoin_safe/gui/qt/export_data.py b/bitcoin_safe/gui/qt/export_data.py index f19cfcd..e081a44 100644 --- a/bitcoin_safe/gui/qt/export_data.py +++ b/bitcoin_safe/gui/qt/export_data.py @@ -27,36 +27,21 @@ # SOFTWARE. +import json import logging +import os from datetime import datetime +from functools import partial from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union +import bdkpython as bdk from bitcoin_nostr_chat.bitcoin_dm import BitcoinDM, ChatLabel from bitcoin_nostr_chat.ui.ui import short_key from bitcoin_qr_tools.data import Data, DataType from bitcoin_qr_tools.gui.qr_widgets import QRCodeWidgetSVG -from bitcoin_qr_tools.unified_encoder import QrExportType, QrExportTypes, UnifiedEncoder - -from bitcoin_safe.descriptor_export_tools import DescriptorExportTools -from bitcoin_safe.descriptors import MultipathDescriptor -from bitcoin_safe.gui.qt.keystore_ui import SignerUI -from bitcoin_safe.gui.qt.wrappers import Menu -from bitcoin_safe.i18n import translate -from bitcoin_safe.threading_manager import TaskThread, ThreadingManager -from bitcoin_safe.tx import short_tx_id, transaction_to_dict -from bitcoin_safe.typestubs import TypedPyQtSignal -from bitcoin_safe.wallet import filename_clean - -from .sync_tab import SyncTab - -logger = logging.getLogger(__name__) - -import json -import os - -import bdkpython as bdk from bitcoin_qr_tools.qr_generator import QRGenerator +from bitcoin_qr_tools.unified_encoder import QrExportType, QrExportTypes, UnifiedEncoder from nostr_sdk import PublicKey from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QAction, QIcon @@ -73,6 +58,16 @@ QWidget, ) +from bitcoin_safe.descriptor_export_tools import DescriptorExportTools +from bitcoin_safe.descriptors import MultipathDescriptor +from bitcoin_safe.gui.qt.keystore_ui import SignerUI +from bitcoin_safe.gui.qt.wrappers import Menu +from bitcoin_safe.i18n import translate +from bitcoin_safe.threading_manager import TaskThread, ThreadingManager +from bitcoin_safe.tx import short_tx_id, transaction_to_dict +from bitcoin_safe.typestubs import TypedPyQtSignal +from bitcoin_safe.wallet import filename_clean + from ...hardware_signers import ( DescriptorExportType, DescriptorExportTypes, @@ -81,8 +76,11 @@ HardwareSigners, ) from ...signals import SignalsMin +from .sync_tab import SyncTab from .util import Message, MessageType, do_copy, read_QIcon, save_file_dialog +logger = logging.getLogger(__name__) + class DataGroupBox(QGroupBox): def __init__(self, title: str | None = None, parent=None, data=None) -> None: @@ -178,20 +176,28 @@ def _fill_menu(self): self._menu.clear() self._menu.blockSignals(True) - # Create a menu for the button - def copy_if_available(s: Optional[str]) -> None: - if s: - do_copy(s) - else: - Message(self.tr("Not available")) - - self.action_copy_data = self._menu.add_action("", lambda: copy_if_available(self.serialized)) + self.action_copy_data = self._menu.add_action("", self.on_action_copy_data) self._menu.addSeparator() - self.action_copy_txid = self._menu.add_action("", lambda: copy_if_available(self.txid)) - self.action_json = self._menu.add_action("", lambda: copy_if_available(self.json_data)) + self.action_copy_txid = self._menu.add_action("", self.on_action_copy_txid) + self.action_json = self._menu.add_action("", self.on_action_json) self._menu.blockSignals(False) + def copy_if_available(self, s: Optional[str]) -> None: + if s: + do_copy(s) + else: + Message(self.tr("Not available")) + + def on_action_copy_data(self): + return self.copy_if_available(self.serialized) + + def on_action_copy_txid(self): + return self.copy_if_available(self.txid) + + def on_action_json(self): + return self.copy_if_available(self.json_data) + def _set_data(self, data: Data) -> None: self.data = data self.serialized = data.data_as_string() @@ -295,8 +301,24 @@ def set_data(self, data: Data): self._fill_menu() self.updateUi() - @staticmethod + @classmethod + def _save_file( + cls, + wallet_id: str, + multipath_descriptor: MultipathDescriptor, + network: bdk.Network, + descripor_type: DescriptorExportType, + ): + return DescriptorExportTools.export( + wallet_id=wallet_id, + multipath_descriptor=multipath_descriptor, + network=network, + descripor_type=descripor_type, + ) + + @classmethod def fill_file_menu_descriptor_export_actions( + cls, menu: Menu, wallet_id: str, multipath_descriptor: MultipathDescriptor, @@ -305,21 +327,16 @@ def fill_file_menu_descriptor_export_actions( menu.blockSignals(True) menu.clear() - def factory_save_file(descripor_type: DescriptorExportType): - def save_descriptor(descripor_type: DescriptorExportType = descripor_type): - return DescriptorExportTools.export( - wallet_id=wallet_id, - multipath_descriptor=multipath_descriptor, - network=network, - descripor_type=descripor_type, - ) - - return save_descriptor - for export_type in DescriptorExportTypes.as_list(): menu.add_action( get_export_display_name(export_type=export_type), - factory_save_file(export_type), + partial( + cls._save_file, + wallet_id=wallet_id, + multipath_descriptor=multipath_descriptor, + network=network, + descripor_type=export_type, + ), icon=get_export_icon(export_type=export_type), ) menu.blockSignals(False) @@ -364,6 +381,17 @@ def __init__( self._fill_menu() self.updateUi() + def _share_with_device( + self, wallet_id: str, sync_tab: SyncTab, receiver_public_key_bech32: str | None = None + ) -> None: + if not sync_tab.enabled(): + Message(self.tr("Please enable the sync tab first")) + return + if receiver_public_key_bech32: + self.on_nostr_share_with_member(PublicKey.parse(receiver_public_key_bech32), wallet_id, sync_tab) + else: + self.on_nostr_share_in_group(wallet_id, sync_tab) + def _fill_menu(self): menu = self._menu menu.clear() @@ -372,34 +400,28 @@ def _fill_menu(self): self._menu.blockSignals(True) - def factory( - wallet_id: str, sync_tab: SyncTab, receiver_public_key_bech32: str | None = 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 - if receiver_public_key_bech32: - self.on_nostr_share_with_member( - PublicKey.parse(receiver_public_key_bech32), wallet_id, sync_tab - ) - else: - self.on_nostr_share_in_group(wallet_id, sync_tab) - - return f - # Create a menu for the button self.action_share_with_all_devices.clear() self.menu_share_with_single_devices.clear() for wallet_id, sync_tab in self.sync_tabs.items(): - self.action_share_with_all_devices[wallet_id] = menu.add_action("", factory(wallet_id, sync_tab)) + action_alldevices = partial( + self._share_with_device, + wallet_id=wallet_id, + sync_tab=sync_tab, + receiver_public_key_bech32=None, + ) + self.action_share_with_all_devices[wallet_id] = menu.add_action("", action_alldevices) self.menu_share_with_single_devices[wallet_id] = menu.add_menu("") for member in sync_tab.nostr_sync.group_chat.members: + action = partial( + self._share_with_device, + wallet_id=wallet_id, + sync_tab=sync_tab, + receiver_public_key_bech32=member.to_bech32(), + ) self.menu_share_with_single_devices[wallet_id].add_action( - f"{short_key(member.to_bech32())}", factory(wallet_id, sync_tab, member.to_bech32()) + f"{short_key(member.to_bech32())}", action ) menu.addSeparator() @@ -831,23 +853,20 @@ def __init__( self._fill_menu() self.updateUi() + def _show_export_widget(self, export_type: QrExportType): + if not self.export_qr_widget: + return + self.export_qr_widget.combo_qr_type.setCurrentQrType(value=export_type) + self.export_qr_widget.show() + def _fill_menu(self): self._menu.clear() self._menu.blockSignals(True) - def factory_show_export_widget(export_type: QrExportType): - def show_export_widget(export_type: QrExportType = export_type): - if not self.export_qr_widget: - return - self.export_qr_widget.combo_qr_type.setCurrentQrType(value=export_type) - self.export_qr_widget.show() - - return show_export_widget - for qr_type in self.export_qr_widget.qr_types: self._menu.add_action( get_export_display_name(qr_type), - factory_show_export_widget(qr_type), + partial(self._show_export_widget, qr_type), icon=get_export_icon(qr_type), ) diff --git a/bitcoin_safe/gui/qt/extended_tabwidget.py b/bitcoin_safe/gui/qt/extended_tabwidget.py index c77d43b..2c7573d 100644 --- a/bitcoin_safe/gui/qt/extended_tabwidget.py +++ b/bitcoin_safe/gui/qt/extended_tabwidget.py @@ -26,7 +26,8 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from typing import Callable, Type +import logging +from typing import Callable from PyQt6.QtCore import QPoint, Qt, pyqtSignal from PyQt6.QtGui import QMouseEvent, QResizeEvent @@ -45,14 +46,14 @@ from bitcoin_safe.gui.qt.util import read_QIcon from bitcoin_safe.typestubs import TypedPyQtSignal +logger = logging.getLogger(__name__) -class ExtendedTabWidget(DataTabWidget): + +class ExtendedTabWidget(DataTabWidget[T]): signal_tab_bar_visibility: TypedPyQtSignal[bool] = pyqtSignal(bool) # type: ignore - def __init__( - self, data_class: Type[T], show_ContextMenu: Callable[[QPoint, int], None] | None = None, parent=None - ) -> None: - super().__init__(data_class, parent=parent) + def __init__(self, show_ContextMenu: Callable[[QPoint, int], None] | None = None, parent=None) -> None: + super().__init__(parent=parent) self.set_top_right_widget() self.show_ContextMenu = show_ContextMenu self.tabBar().installEventFilter(self) # type: ignore @@ -168,7 +169,7 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: app = QApplication(sys.argv) edit = QLineEdit(f"Ciiiiii") - tabWidget = ExtendedTabWidget(object) + tabWidget = ExtendedTabWidget[object]() # Add tabs with larger widgets for i in range(3): diff --git a/bitcoin_safe/gui/qt/fee_group.py b/bitcoin_safe/gui/qt/fee_group.py index 79e1af8..3c7bf0b 100644 --- a/bitcoin_safe/gui/qt/fee_group.py +++ b/bitcoin_safe/gui/qt/fee_group.py @@ -28,20 +28,6 @@ import logging - -from bitcoin_safe.fx import FX -from bitcoin_safe.gui.qt.notification_bar import NotificationBar -from bitcoin_safe.gui.qt.util import icon_path -from bitcoin_safe.html_utils import html_f, link -from bitcoin_safe.network_config import ProxyInfo -from bitcoin_safe.psbt_util import FeeInfo -from bitcoin_safe.typestubs import TypedPyQtSignal - -from ...config import FEE_RATIO_HIGH_WARNING, NO_FEE_WARNING_BELOW, UserConfig -from .util import adjust_bg_color_for_darkmode - -logger = logging.getLogger(__name__) - from typing import List, Optional import bdkpython as bdk @@ -56,6 +42,15 @@ QWidget, ) +from bitcoin_safe.fx import FX +from bitcoin_safe.gui.qt.notification_bar import NotificationBar +from bitcoin_safe.gui.qt.util import icon_path +from bitcoin_safe.html_utils import html_f, link +from bitcoin_safe.network_config import ProxyInfo +from bitcoin_safe.psbt_util import FeeInfo +from bitcoin_safe.typestubs import TypedPyQtSignal + +from ...config import FEE_RATIO_HIGH_WARNING, NO_FEE_WARNING_BELOW, UserConfig from ...mempool import MempoolData, TxPrio from ...util import Satoshis, format_fee_rate, unit_fee_str from .block_buttons import ( @@ -64,6 +59,9 @@ MempoolButtons, MempoolProjectedBlock, ) +from .util import adjust_bg_color_for_darkmode + +logger = logging.getLogger(__name__) class FeeWarningBar(NotificationBar): @@ -157,9 +155,7 @@ def __init__( self._mempool_buttons, ] for button_group in self._all_mempool_buttons: - self.groupBox_Fee_layout.addWidget( - button_group.button_group, alignment=Qt.AlignmentFlag.AlignHCenter - ) + self.groupBox_Fee_layout.addWidget(button_group, alignment=Qt.AlignmentFlag.AlignHCenter) self.set_mempool_visibility() self.approximate_fee_label = QLabel() @@ -215,13 +211,16 @@ def __init__( if allow_edit: self.visible_mempool_buttons.signal_click.connect(self.set_fee_rate) # self.spin_fee_rate.editingFinished.connect(lambda: self.set_fee_rate(self.spin_fee_rate.value())) - self.spin_fee_rate.valueChanged.connect(lambda: self.set_fee_rate(self.spin_fee_rate.value())) + self.spin_fee_rate.valueChanged.connect(self.on_spin_fee_rate_changed) self.visible_mempool_buttons.mempool_data.signal_data_updated.connect(self.updateUi) # refresh self.visible_mempool_buttons.refresh() self.updateUi() + def on_spin_fee_rate_changed(self): + self.set_fee_rate(self.spin_fee_rate.value()) + def set_confirmation_time(self, confirmation_time: bdk.BlockTime | None = None): self._confirmed_block.confirmation_time = confirmation_time self.set_mempool_visibility() @@ -237,7 +236,7 @@ def set_mempool_visibility(self): self.visible_mempool_buttons = self._mempool_buttons for mempool_buttons in self._all_mempool_buttons: - mempool_buttons.button_group.setVisible(self.visible_mempool_buttons == mempool_buttons) + mempool_buttons.setVisible(self.visible_mempool_buttons == mempool_buttons) def updateUi(self) -> None: self.groupBox_Fee.setTitle(self.tr("Fee")) diff --git a/bitcoin_safe/gui/qt/hist_list.py b/bitcoin_safe/gui/qt/hist_list.py index f88f7d3..7dc4c45 100644 --- a/bitcoin_safe/gui/qt/hist_list.py +++ b/bitcoin_safe/gui/qt/hist_list.py @@ -52,24 +52,13 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import datetime +import enum import logging import os import tempfile - -from PyQt6.QtGui import QFont, QFontMetrics - -from bitcoin_safe.config import MIN_RELAY_FEE, UserConfig -from bitcoin_safe.gui.qt.wrappers import Menu -from bitcoin_safe.mempool import MempoolData -from bitcoin_safe.psbt_util import FeeInfo -from bitcoin_safe.pythonbdk_types import Balance, Recipient -from bitcoin_safe.typestubs import TypedPyQtSignal - -logger = logging.getLogger(__name__) - -import datetime -import enum from enum import IntEnum +from functools import partial from typing import Any, Dict, Iterable, List, Optional, Set, Tuple import bdkpython as bdk @@ -82,10 +71,18 @@ QDragMoveEvent, QDropEvent, QFont, + QFontMetrics, QStandardItem, ) from PyQt6.QtWidgets import QAbstractItemView, QFileDialog, QPushButton, QStyle, QWidget +from bitcoin_safe.config import MIN_RELAY_FEE, UserConfig +from bitcoin_safe.gui.qt.wrappers import Menu +from bitcoin_safe.mempool import MempoolData +from bitcoin_safe.psbt_util import FeeInfo +from bitcoin_safe.pythonbdk_types import Balance, Recipient +from bitcoin_safe.typestubs import TypedPyQtSignal + from ...i18n import translate from ...signals import Signals, UpdateFilter, UpdateFilterReason from ...util import ( @@ -114,6 +111,8 @@ from .taglist import AddressDragInfo from .util import Message, MessageType, read_QIcon, sort_id_to_icon, webopen +logger = logging.getLogger(__name__) + class AddressUsageStateFilter(IntEnum): ALL = 0 @@ -225,12 +224,17 @@ def __init__( # ): # type: AddressUsageStateFilter # self.used_button.addItem(addr_usage_state.ui_text()) self._source_model = MyStandardItemModel( - self, - drag_key="txids", - drag_keys_to_file_paths=self.drag_keys_to_file_paths, + key_column=self.key_column, + parent=self, ) self.proxy = MySortModel( - self, source_model=self._source_model, sort_role=MyItemDataRole.ROLE_SORT_ORDER + drag_key="txids", + Columns=self.Columns, + key_column=self.key_column, + parent=self, + source_model=self._source_model, + sort_role=MyItemDataRole.ROLE_SORT_ORDER, + custom_drag_keys_to_file_paths=self.drag_keys_to_file_paths, ) self.setModel(self.proxy) self.update_content() @@ -572,13 +576,13 @@ def create_menu(self, position: QPoint) -> Menu: if not item: return menu txid = txids[0] - menu.add_action(translate("hist_list", "Details"), lambda: self.signals.open_tx_like.emit(txid)) + menu.add_action(translate("hist_list", "Details"), partial(self.signals.open_tx_like.emit, txid)) addr_URL = block_explorer_URL(self.config.network_config.mempool_url, "tx", txid) if addr_URL: menu.add_action( translate("hist_list", "View on block explorer"), - lambda: webopen(addr_URL), + partial(webopen, addr_URL), icon=read_QIcon("block-explorer.svg"), ) menu.addSeparator() @@ -600,13 +604,16 @@ def create_menu(self, position: QPoint) -> Menu: menu.add_action( translate("hist_list", "Copy as csv"), - lambda: self.copyRowsToClipboardAsCSV([r.row() for r in selected]), + partial( + self.copyRowsToClipboardAsCSV, + [item.data(MySortModel.role_drag_key) for item in selected_items if item], + ), icon=read_QIcon("csv-file.svg"), ) menu.add_action( translate("hist_list", "Save as file"), - lambda: self.export_raw_transactions(selected_items), + partial(self.export_raw_transactions, selected_items), icon=(self.style() or QStyle()).standardIcon(QStyle.StandardPixmap.SP_DialogSaveButton), ) @@ -624,11 +631,12 @@ def create_menu(self, position: QPoint) -> Menu: if tx_status and tx_status.can_rbf(): menu.addSeparator() menu.add_action( - translate("hist_list", "Edit with higher fee (RBF)"), lambda: self.edit_tx(tx_details) + translate("hist_list", "Edit with higher fee (RBF)"), partial(self.edit_tx, tx_details) ) menu.add_action( - translate("hist_list", "Try cancel transaction (RBF)"), lambda: self.cancel_tx(tx_details) + translate("hist_list", "Try cancel transaction (RBF)"), + partial(self.cancel_tx, tx_details), ) # run_hook('receive_menu', menu, txids, self.wallet) @@ -692,7 +700,7 @@ def export_raw_transactions( file_paths = self.drag_keys_to_file_paths(keys, save_directory=folder) - logger.info(f"Saved {len(file_paths)} {self._source_model.drag_key} saved to {folder}") + logger.info(f"Saved {len(file_paths)} {self.proxy.drag_key} saved to {folder}") def get_edit_key_from_coordinate(self, row: int, col: int) -> Any: if col != self.Columns.LABEL: @@ -718,6 +726,11 @@ def on_edited(self, idx: QModelIndex, edit_key: str, *, text: str) -> None: ) ) + def close(self): + self._tx_dict.clear() + self.setParent(None) + super().close() + class RefreshButton(QPushButton): def __init__(self, parent=None, height=20) -> None: @@ -761,11 +774,9 @@ def updateUi(self) -> None: if self.hist_list.signals and not self.hist_list.address_domain: if self.hist_list.signals: - display_balance = ( - self.hist_list.signals.wallet_signals[self.hist_list.wallet_id] - .get_display_balance.emit() - .get(self.hist_list.wallet_id) - ) + display_balance = self.hist_list.signals.wallet_signals[ + self.hist_list.wallet_id + ].get_display_balance.emit() if isinstance(display_balance, Balance): balance_total = Satoshis(display_balance.total, self.config.network) diff --git a/bitcoin_safe/gui/qt/histtabwidget.py b/bitcoin_safe/gui/qt/histtabwidget.py index a9d3586..3d07592 100644 --- a/bitcoin_safe/gui/qt/histtabwidget.py +++ b/bitcoin_safe/gui/qt/histtabwidget.py @@ -27,6 +27,8 @@ # SOFTWARE. +import logging +from functools import partial from typing import Optional from PyQt6.QtWidgets import ( @@ -40,16 +42,21 @@ from bitcoin_safe.gui.qt.unique_deque import UniqueDeque +logger = logging.getLogger(__name__) + class HistTabWidget(QTabWidget): """Stores the closing activation history of the tabs and upon close, activates the last active one. Args: QTabWidget: Inherits from QTabWidget. + """ def __init__(self, parent: Optional[QWidget] = None): - super().__init__(parent) + super().__init__( + parent, + ) self._tab_history: UniqueDeque[int] = UniqueDeque(maxlen=100000) # History of activated tab indices self.currentChanged.connect(self.on_current_changed) @@ -73,19 +80,35 @@ def remove_tab_from_history(self, index: int) -> None: self._tab_history = UniqueDeque([i for i in self._tab_history if i != index]) self._tab_history = UniqueDeque([i - 1 if i > index else i for i in self._tab_history]) - def get_last_active_tab(self) -> int: + def get_last_active_tab(self) -> Optional[QWidget]: + index = self.get_last_active_tab_index() + if index >= 0: + return self.widget(index) + return None + + def get_last_active_tab_index(self) -> int: if len(self._tab_history) >= 2: return self._tab_history[-2] elif len(self._tab_history) >= 1: return self._tab_history[-1] return self.currentIndex() + def jump_to_tab(self, widget: QWidget) -> None: + index = self.indexOf(widget) + if index >= 0: + self.setCurrentIndex(index) + def jump_to_last_active_tab(self) -> None: """Sets the current tab to the last active one from history or to the first tab if history is empty.""" - index = self.get_last_active_tab() + index = self.get_last_active_tab_index() if index >= 0: self.setCurrentIndex(index) + def remove_tab(self, widget: QWidget) -> None: + index = self.indexOf(widget) + if index >= 0: + self.removeTab(index) + def removeTab(self, index: int) -> None: self.remove_tab_from_history(index) return super().removeTab(index) @@ -105,7 +128,7 @@ def remove(index): self.tab_widget.tabCloseRequested.connect(remove) self.tab_widget.currentChanged.connect( - lambda: print(f"on_currentChanged = {self.tab_widget._tab_history}") + partial(print, f"on_currentChanged = {self.tab_widget._tab_history}") ) self.setCentralWidget(self.tab_widget) # Adding example tabs diff --git a/bitcoin_safe/gui/qt/html_delegate.py b/bitcoin_safe/gui/qt/html_delegate.py index cb2116b..a1d12a3 100644 --- a/bitcoin_safe/gui/qt/html_delegate.py +++ b/bitcoin_safe/gui/qt/html_delegate.py @@ -29,12 +29,12 @@ import logging -logger = logging.getLogger(__name__) - from PyQt6.QtCore import QModelIndex, QPoint, QSize from PyQt6.QtGui import QHelpEvent, QPainter, QTextDocument from PyQt6.QtWidgets import QStyle, QStyleOptionViewItem +logger = logging.getLogger(__name__) + class HTMLDelegate: def __init__(self) -> None: diff --git a/bitcoin_safe/gui/qt/keystore_ui.py b/bitcoin_safe/gui/qt/keystore_ui.py index 89d921a..93020ec 100644 --- a/bitcoin_safe/gui/qt/keystore_ui.py +++ b/bitcoin_safe/gui/qt/keystore_ui.py @@ -28,45 +28,17 @@ import logging -from typing import Iterable, Optional, Tuple, Union - -from bitcoin_safe.gui.qt.analyzer_indicator import AnalyzerIndicator -from bitcoin_safe.gui.qt.analyzers import ( - FingerprintAnalyzer, - KeyOriginAnalyzer, - SeedAnalyzer, - XpubAnalyzer, -) -from bitcoin_safe.gui.qt.data_tab_widget import DataTabWidget -from bitcoin_safe.gui.qt.spinning_button import SpinningButton -from bitcoin_safe.gui.qt.wrappers import Menu -from bitcoin_safe.i18n import translate -from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo - -from ...dynamic_lib_load import setup_libsecp256k1 - -setup_libsecp256k1() - -from bitcoin_usb.address_types import SimplePubKeyProvider - -from bitcoin_safe.gui.qt.buttonedit import ButtonEdit -from bitcoin_safe.gui.qt.custom_edits import AnalyzerTextEdit, QCompleterLineEdit -from bitcoin_safe.gui.qt.tutorial_screenshots import ScreenshotsExportXpub - -from .dialog_import import ImportDialog - -logger = logging.getLogger(__name__) - -from typing import Callable, List +from functools import partial +from typing import Callable, Iterable, List, Optional, Tuple, Union import bdkpython as bdk from bitcoin_qr_tools.data import ConverterXpub, Data, DataType, SignerInfo -from bitcoin_usb.address_types import AddressType +from bitcoin_usb.address_types import AddressType, SimplePubKeyProvider from bitcoin_usb.seed_tools import get_network_index from bitcoin_usb.software_signer import SoftwareSigner from bitcoin_usb.usb_gui import USBGui from PyQt6.QtCore import QObject, QSize, Qt, pyqtSignal -from PyQt6.QtGui import QIcon +from PyQt6.QtGui import QCloseEvent, QIcon from PyQt6.QtWidgets import ( QDialogButtonBox, QFormLayout, @@ -82,10 +54,27 @@ QWidget, ) +from bitcoin_safe.gui.qt.analyzer_indicator import AnalyzerIndicator +from bitcoin_safe.gui.qt.analyzers import ( + FingerprintAnalyzer, + KeyOriginAnalyzer, + SeedAnalyzer, + XpubAnalyzer, +) +from bitcoin_safe.gui.qt.buttonedit import ButtonEdit +from bitcoin_safe.gui.qt.custom_edits import AnalyzerTextEdit, QCompleterLineEdit +from bitcoin_safe.gui.qt.data_tab_widget import DataTabWidget +from bitcoin_safe.gui.qt.spinning_button import SpinningButton +from bitcoin_safe.gui.qt.tutorial_screenshots import ScreenshotsExportXpub +from bitcoin_safe.gui.qt.wrappers import Menu +from bitcoin_safe.i18n import translate +from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo + from ...keystore import KeyStore, KeyStoreImporterTypes from ...signals import SignalsMin from ...signer import AbstractSignatureImporter, SignatureImporterUSB from .block_change_signals import BlockChangesSignals +from .dialog_import import ImportDialog from .util import ( Message, MessageType, @@ -96,6 +85,8 @@ read_QIcon, ) +logger = logging.getLogger(__name__) + def icon_for_label(label: str) -> QIcon: return ( @@ -104,6 +95,8 @@ def icon_for_label(label: str) -> QIcon: class BaseHardwareSignerInteractionWidget(QWidget): + aboutToClose: TypedPyQtSignal[QWidget] = pyqtSignal(QWidget) # type: ignore + def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self.setWindowIcon(read_QIcon("logo.svg")) @@ -136,6 +129,10 @@ def updateUi(self) -> None: if self.help_button: self.help_button.setText(self.tr("Help")) + def closeEvent(self, event: QCloseEvent | None): + self.aboutToClose.emit(self) # Emit the signal when the window is about to close + super().closeEvent(event) + class HardwareSignerInteractionWidget(BaseHardwareSignerInteractionWidget): def __init__(self, parent: QWidget | None = None) -> None: @@ -304,31 +301,16 @@ def __init__( button_qr = self.hardware_signer_interaction.add_qr_import_buttonn() self.hardware_signer_interaction.add_help_button(ScreenshotsExportXpub()) - button_qr.clicked.connect(lambda: self.edit_xpub.button_container.buttons[0].click()) + button_qr.clicked.connect(self.edit_xpub.button_container.buttons[0].click) self.usb_gui = USBGui(self.network, initalization_label=self.hardware_signer_label) signal_end_hwi_blocker: TypedPyQtSignalNo = self.usb_gui.signal_end_hwi_blocker # type: ignore button_hwi = self.hardware_signer_interaction.add_hwi_button( signal_end_hwi_blocker=signal_end_hwi_blocker ) - button_hwi.clicked.connect(lambda: self.on_hwi_click()) + button_hwi.clicked.connect(self.on_hwi_click) - def process_input(s: str) -> None: - res = Data.from_str(s, network=self.network) - self._on_handle_input(res) - - def import_dialog(): - ImportDialog( - self.network, - on_open=process_input, - window_title=self.tr("Import fingerprint and xpub"), - text_button_ok=self.tr("OK"), - text_instruction_label=self.tr("Please paste the exported file (like sparrow-export.json):"), - text_placeholder=self.tr("Please paste the exported file (like sparrow-export.json)"), - close_all_video_widgets=self.signals_min.close_all_video_widgets, - ).exec() - - button_file.clicked.connect(import_dialog) + button_file.clicked.connect(self._import_dialog) # self.tab_import_layout.addItem(QSpacerItem(1, 1, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)) self.tab_import_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -369,10 +351,26 @@ def import_dialog(): self.edit_key_origin_input.textChanged.connect(self.format_all_fields) self.signals_min.language_switch.connect(self.updateUi) + def _process_input(self, s: str) -> None: + res = Data.from_str(s, network=self.network) + self._on_handle_input(res) + + def _import_dialog(self): + ImportDialog( + self.network, + on_open=self._process_input, + window_title=self.tr("Import fingerprint and xpub"), + text_button_ok=self.tr("OK"), + text_instruction_label=self.tr("Please paste the exported file (like sparrow-export.json):"), + text_placeholder=self.tr("Please paste the exported file (like sparrow-export.json)"), + close_all_video_widgets=self.signals_min.close_all_video_widgets, + ).exec() + def on_edit_seed_changed(self, text: str): try: keystore = self.get_ui_values_as_keystore() except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") Message(str(e), type=MessageType.Error) return self.edit_fingerprint.setText(keystore.fingerprint) @@ -393,7 +391,8 @@ def seed_visibility(self, visible=False) -> None: def key_origin(self) -> str: try: standardized = SimplePubKeyProvider.format_key_origin(self.edit_key_origin.text().strip()) - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") return "" return standardized @@ -524,7 +523,8 @@ def xpub_validator(self) -> bool: ) try: self.edit_xpub.setText(ConverterXpub.convert_slip132_to_bip32(xpub)) - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") return False return KeyStore.is_xpub_valid(self.edit_xpub.text(), network=self.network) @@ -565,6 +565,7 @@ def on_hwi_click(self) -> None: try: result = self.usb_gui.get_fingerprint_and_xpub(key_origin=key_origin) except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") Message( str(e) + "\n\n" @@ -677,12 +678,6 @@ def __init__( self.layout_keystore_buttons = QVBoxLayout(self) - def callback_generator(signer: AbstractSignatureImporter) -> Callable: - def f() -> None: - signer.sign(self.psbt) - - return f - self.buttons: List[QPushButton] = [] for signer in self.signature_importers: button: QPushButton @@ -700,7 +695,8 @@ def f() -> None: button.setIcon(QIcon(icon_path(signer.keystore_type.icon_filename))) self.buttons.append(button) button.setMinimumHeight(30) - button.clicked.connect(callback_generator(signer)) + callback = partial(signer.sign, self.psbt) + button.clicked.connect(callback) self.layout_keystore_buttons.addWidget(button) # forward the signal_signature_added from each signer to self.signal_signature_added @@ -727,16 +723,11 @@ def __init__( for signer in self.signature_importers: - def callback_generator(signer: AbstractSignatureImporter) -> Callable: - def f() -> None: - signer.sign(self.psbt) - - return f - button = QPushButton(signer.label) button.setMinimumHeight(30) button.setIcon(QIcon(icon_path(signer.keystore_type.icon_filename))) - button.clicked.connect(callback_generator(signer)) + action = partial(signer.sign, self.psbt) + button.clicked.connect(action) self.layout_keystore_buttons.addWidget(button) # forward the signal_signature_added from each signer to self.signal_signature_added diff --git a/bitcoin_safe/gui/qt/keystore_uis.py b/bitcoin_safe/gui/qt/keystore_uis.py index 3136cc4..113ac4f 100644 --- a/bitcoin_safe/gui/qt/keystore_uis.py +++ b/bitcoin_safe/gui/qt/keystore_uis.py @@ -28,6 +28,7 @@ import logging +from typing import Callable, List from bitcoin_qr_tools.data import SignerInfo from PyQt6.QtCore import pyqtSignal @@ -39,15 +40,13 @@ from bitcoin_safe.signals import SignalsMin from bitcoin_safe.typestubs import TypedPyQtSignal -logger = logging.getLogger(__name__) - -from typing import Callable, List - from ...descriptors import AddressType from ...wallet import ProtoWallet from .keystore_ui import KeyStoreUI, icon_for_label from .util import Message, MessageType +logger = logging.getLogger(__name__) + class OrderTrackingTabBar(QTabBar): signal_new_tab_order: TypedPyQtSignal[List[int]] = pyqtSignal(list) # type: ignore @@ -83,7 +82,7 @@ def __init__( get_address_type: Callable[[], AddressType], signals_min: SignalsMin, ) -> None: - super().__init__(KeyStoreUI) + super().__init__() self.tab_bar = OrderTrackingTabBar() self.setTabBar(self.tab_bar) self.setMovable(True) @@ -248,7 +247,8 @@ def ui_keystore_ui_change(self, *args) -> None: try: self.set_protowallet_from_keystore_ui() self.set_keystore_ui_from_protowallet() - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") logger.warning("ui_keystore_ui_change: Invalid input") def set_protowallet_from_keystore_ui(self) -> None: diff --git a/bitcoin_safe/gui/qt/labeledit.py b/bitcoin_safe/gui/qt/labeledit.py index 9254dc1..253a8a0 100644 --- a/bitcoin_safe/gui/qt/labeledit.py +++ b/bitcoin_safe/gui/qt/labeledit.py @@ -144,9 +144,10 @@ def __init__( # signals if dismiss_label_on_focus_loss: - self.label_edit.signal_textEditedAndFocusLost.connect( - lambda: self.label_edit.setText(self.label_edit.originalText) - ) + self.label_edit.signal_textEditedAndFocusLost.connect(self.on_signal_textEditedAndFocusLost) + + def on_signal_textEditedAndFocusLost(self): + return self.label_edit.setText(self.label_edit.originalText) def _format_category_edit(self) -> None: palette = QtGui.QPalette() diff --git a/bitcoin_safe/gui/qt/language_chooser.py b/bitcoin_safe/gui/qt/language_chooser.py index d6057f5..3fe36c2 100644 --- a/bitcoin_safe/gui/qt/language_chooser.py +++ b/bitcoin_safe/gui/qt/language_chooser.py @@ -28,14 +28,8 @@ import logging - -from bitcoin_safe.gui.qt.util import read_QIcon -from bitcoin_safe.gui.qt.wrappers import Menu -from bitcoin_safe.typestubs import TypedPyQtSignalNo - -logger = logging.getLogger(__name__) - import os +from functools import partial from typing import Dict, List, Optional from PyQt6.QtCore import QLibraryInfo, QLocale, QObject, Qt, QTranslator @@ -50,6 +44,12 @@ ) from bitcoin_safe.config import UserConfig +from bitcoin_safe.execute_config import DEFAULT_LANG_CODE +from bitcoin_safe.gui.qt.util import read_QIcon +from bitcoin_safe.gui.qt.wrappers import Menu +from bitcoin_safe.typestubs import TypedPyQtSignalNo + +logger = logging.getLogger(__name__) FLAGS = { "en_US": "🇺🇸", @@ -160,11 +160,11 @@ def __init__( self.config = config self.signals_language_switch = signals_language_switch self.installed_translators: List[QTranslator] = [] - self.current_language_code: str = "en_US" + self.current_language_code: str = DEFAULT_LANG_CODE # Start with default language (English) in the list self.availableLanguages = {"en_US": QLocale("en_US").nativeLanguageName()} - logger.debug(f"initialized {self}") + logger.debug(f"initialized {self.__class__.__name__}") @staticmethod def create_flag_icon(unicode_flag: str, size: int = 32) -> QIcon: @@ -207,16 +207,10 @@ def get_languages(self) -> Dict[str, str]: def populate_language_menu(self, language_menu: Menu) -> None: language_menu.clear() - # Menu Bar for language selection - def factory(lang): - def f(lang=lang): - self.switchLanguage(langCode=lang) - - return f - for lang, name in self.get_languages().items(): icon = self.create_flag_icon(FLAGS[lang]) if lang in FLAGS else QIcon() - language_menu.add_action(text=name, slot=factory(lang), icon=icon) + action = partial(self.switchLanguage, lang) + language_menu.add_action(text=name, slot=action, icon=icon) def scanForLanguages(self) -> Dict[str, str]: languages: Dict[str, str] = {} @@ -246,7 +240,7 @@ def _install_translator(self, name: str, path: str) -> None: self.installed_translators.append(translator_qt) def set_language(self, langCode: Optional[str]) -> None: - langCode = langCode if langCode else "en_US" + langCode = langCode if langCode else DEFAULT_LANG_CODE # remove all installed translators instance = QApplication.instance() while self.installed_translators and instance: diff --git a/bitcoin_safe/gui/qt/main.py b/bitcoin_safe/gui/qt/main.py index 1bd770e..687c4bf 100644 --- a/bitcoin_safe/gui/qt/main.py +++ b/bitcoin_safe/gui/qt/main.py @@ -27,11 +27,33 @@ # SOFTWARE. +import base64 +import gc import logging +import os import signal as syssignal +import sys +from functools import partial from pathlib import Path +from typing import Dict, Iterable, List, Literal, Optional, Tuple, Union +import bdkpython as bdk +from bitcoin_qr_tools.data import Data, DataType +from bitcoin_qr_tools.gui.bitcoin_video_widget import BitcoinVideoWidget from bitcoin_usb.tool_gui import ToolGui +from PyQt6.QtCore import QCoreApplication, QPoint, QProcess, Qt, QTimer, pyqtSignal +from PyQt6.QtGui import QCloseEvent, QKeySequence, QShortcut +from PyQt6.QtWidgets import ( + QDialog, + QFileDialog, + QMainWindow, + QSizePolicy, + QStyle, + QSystemTrayIcon, + QTabWidget, + QVBoxLayout, + QWidget, +) from bitcoin_safe import __version__ from bitcoin_safe.descriptors import MultipathDescriptor @@ -43,45 +65,21 @@ from bitcoin_safe.gui.qt.notification_bar_regtest import NotificationBarRegtest from bitcoin_safe.gui.qt.packaged_tx_like import PackagedTxLike from bitcoin_safe.gui.qt.register_multisig import RegisterMultisigInteractionWidget +from bitcoin_safe.gui.qt.search_tree_view import SearchWallets from bitcoin_safe.gui.qt.update_notification_bar import UpdateNotificationBar +from bitcoin_safe.gui.qt.wizard import ImportXpubs, TutorialStep, Wizard from bitcoin_safe.gui.qt.wrappers import Menu, MenuBar from bitcoin_safe.keystore import KeyStoreImporterTypes from bitcoin_safe.logging_setup import get_log_file from bitcoin_safe.network_config import ProxyInfo from bitcoin_safe.pdf_statement import make_and_open_pdf_statement from bitcoin_safe.pdfrecovery import make_and_open_pdf +from bitcoin_safe.signal_tracker import SignalTools from bitcoin_safe.threading_manager import ThreadingManager from bitcoin_safe.typestubs import TypedPyQtSignal from bitcoin_safe.util import rel_home_path_to_abs_path from bitcoin_safe.util_os import xdg_open_file -logger = logging.getLogger(__name__) - -import base64 -import os -import sys -from typing import Dict, Iterable, List, Literal, Optional, Tuple, Union - -import bdkpython as bdk -from bitcoin_qr_tools.data import Data, DataType -from bitcoin_qr_tools.gui.bitcoin_video_widget import BitcoinVideoWidget -from PyQt6.QtCore import QCoreApplication, QPoint, QProcess, Qt, QTimer, pyqtSignal -from PyQt6.QtGui import QCloseEvent, QIcon, QKeySequence, QShortcut -from PyQt6.QtWidgets import ( - QDialog, - QFileDialog, - QMainWindow, - QSizePolicy, - QStyle, - QSystemTrayIcon, - QTabWidget, - QVBoxLayout, - QWidget, -) - -from bitcoin_safe.gui.qt.search_tree_view import SearchWallets -from bitcoin_safe.gui.qt.wizard import ImportXpubs, TutorialStep, Wizard - from ...config import UserConfig from ...fx import FX from ...mempool import MempoolData @@ -110,9 +108,12 @@ ) from .utxo_list import UTXOList, UtxoListWithToolbar +logger = logging.getLogger(__name__) + class MainWindow(QMainWindow): signal_recently_open_wallet_changed: TypedPyQtSignal[List[str]] = pyqtSignal(list) # type: ignore + signal_remove_attached_widget: TypedPyQtSignal[QWidget] = pyqtSignal(QWidget) # type: ignore def __init__( self, @@ -121,18 +122,20 @@ def __init__( open_files_at_startup: List[str] | None = None, **kwargs, ) -> None: - "If netowrk == None, then the network from the user config will be taken" + "If network == None, then the network from the user config will be taken" super().__init__() self.open_files_at_startup = open_files_at_startup if open_files_at_startup else [] config_present = UserConfig.exists() or config self.config = config if config else UserConfig.from_file() self.config.network = bdk.Network[network.upper()] if network else self.config.network self.new_startup_network: bdk.Network | None = None - self.attached_widgets = AttachedWidgets(maxlen=1000) + # i need to keep references of open windows attached + # to the mainwindow to avoid memory issues + # however I need to clear them again with signal_remove_attached_widget + self.attached_widgets = AttachedWidgets(maxlen=10000) self.setMinimumSize(600, 600) self.signals = Signals() - self.qt_wallets: Dict[str, QTWallet] = {} self.threading_manager = ThreadingManager(threading_manager_name=self.__class__.__name__) self.fx = FX( @@ -178,11 +181,12 @@ def __init__( self.signals.show_network_settings.connect(self.open_network_settings) self.welcome_screen = NewWalletWelcomeScreen( - self.tab_wallets, network=self.config.network, signals=self.signals, signal_recently_open_wallet_changed=self.signal_recently_open_wallet_changed, + parent=self.tab_wallets, ) + self.welcome_screen.signal_remove_me.connect(self.tab_wallets.remove_tab) # signals self.welcome_screen.signal_onclick_single_signature.connect(self.click_create_single_signature_wallet) @@ -191,10 +195,9 @@ def __init__( ) self.welcome_screen.signal_onclick_custom_signature.connect(self.click_custom_signature) self.signals.add_qt_wallet.connect(self.add_qt_wallet) - self.signals.close_qt_wallet.connect( - lambda wallet_id: self.remove_qt_wallet(self.qt_wallets.get(wallet_id)) - ) + self.signals.close_qt_wallet.connect(self.remove_qt_wallet_by_id) + self.signals.get_current_lang_code.connect(self.language_chooser.get_current_lang_code) self.signals.event_wallet_tab_added.connect(self.event_wallet_tab_added) self.signals.event_wallet_tab_closed.connect(self.event_wallet_tab_closed) self.signals.chain_data_changed.connect(self.sync) @@ -204,6 +207,8 @@ def __init__( self.signals.language_switch.connect(self.updateUI) self.signal_recently_open_wallet_changed.connect(self.populate_recent_wallets_menu) self.signals.close_all_video_widgets.connect(self.close_video_widget) + self.signals.signal_set_tab_properties.connect(self.on_set_tab_properties) + self.signal_remove_attached_widget.connect(self.on_signal_remove_attached_widget) self._init_tray() # Populate recent wallets menu @@ -212,8 +217,7 @@ def __init__( ) self.search_box = SearchWallets( - get_qt_wallets=self.get_qt_wallets, - signal_min=self.signals, + signals=self.signals, parent=self.tab_wallets, ) self.tab_wallets.set_top_right_widget(self.search_box) @@ -223,12 +227,25 @@ def __init__( delayed_execution(self.load_last_state, self) + @property + def qt_wallets(self) -> Dict[str, QTWallet]: + res: Dict[str, QTWallet] = {} + for tab_data in self.tab_wallets.getAllTabData().values(): + if isinstance(tab_data, QTWallet): + res[tab_data.wallet.id] = tab_data + return res + + @property + def qt_protowallets(self) -> Dict[str, QTProtoWallet]: + res: Dict[str, QTProtoWallet] = {} + for tab_data in self.tab_wallets.getAllTabData().values(): + if isinstance(tab_data, QTProtoWallet): + res[tab_data.protowallet.id] = tab_data + return res + def close_video_widget(self): self.attached_widgets.remove_all_of_type(BitcoinVideoWidget) - def get_qt_wallets(self) -> List[QTWallet]: - return list(self.qt_wallets.values()) - def get_mempool_url(self) -> str: return self.config.network_config.mempool_url @@ -239,7 +256,7 @@ def load_last_state(self) -> None: opened_qt_wallets = self.open_last_opened_wallets() if not opened_qt_wallets: - self.welcome_screen.add_new_wallet_welcome_tab() + self.welcome_screen.add_new_wallet_welcome_tab(self.tab_wallets) self.open_last_opened_tx() for file_path in self.open_files_at_startup: @@ -273,9 +290,10 @@ def setupUi(self) -> None: self.setMinimumSize(w, h) ##### - self.tab_wallets = ExtendedTabWidget( - object, parent=self, show_ContextMenu=self.tab_wallets_show_context_menu + self.tab_wallets = ExtendedTabWidget[object]( + parent=self, show_ContextMenu=self.tab_wallets_show_context_menu ) + self.tab_wallets.setObjectName(f"member of {self.__class__.__name__}") self.tab_wallets.tabBar().setExpanding(True) # type: ignore[union-attr] # This will expand tabs to fill the tab widget width self.tab_wallets.setTabBarAutoHide(False) self.tab_wallets.setMovable(True) # Enable tab reordering @@ -328,16 +346,19 @@ def setupUi(self) -> None: def tab_wallets_show_context_menu(self, position: QPoint, index: int) -> None: menu = Menu() self.action_close_tab = menu.add_action(self.tr("Close Tab")) - self.action_close_all_tabs = menu.add_action(self.tr("Close all transactions")) + self.action_close_all_tx_tabs = menu.add_action(self.tr("Close all transactions")) # Connect actions - self.action_close_tab.triggered.connect(lambda: self.close_tab(index)) - self.action_close_all_tabs.triggered.connect(lambda: self.close_all_tabs_of_type(cls=UITx_Viewer)) + self.action_close_tab.triggered.connect(partial(self.close_tab, index)) + self.action_close_all_tx_tabs.triggered.connect(self.on_close_all_tx_tabs) # Add more actions as needed # Show the menu at the position menu.exec(position) + def on_close_all_tx_tabs(self): + self.close_all_tabs_of_type(cls=UITx_Viewer) + def close_all_tabs_of_type(self, cls): for index in reversed(range(self.tab_wallets.count())): # Get the widget for the current tab @@ -449,9 +470,7 @@ def init_menubar(self) -> None: # menu about self.menu_about = self.menubar.add_menu("") - self.menu_action_version = self.menu_about.add_action( - "", lambda: webopen("https://github.com/andreasgriffin/bitcoin-safe/releases") - ) + self.menu_action_version = self.menu_about.add_action("", self.on_menu_action_version) self.menu_action_check_update = self.menu_about.add_action( "", self.update_notification_bar.check_and_make_visible ) @@ -472,7 +491,13 @@ def menu_action_show_log(): # other shortcuts self.shortcut_close_tab = QShortcut(QKeySequence("Ctrl+W"), self) - self.shortcut_close_tab.activated.connect(lambda: self.close_tab(self.tab_wallets.currentIndex())) + self.shortcut_close_tab.activated.connect(self.close_current_tab) + + def close_current_tab(self): + self.close_tab(self.tab_wallets.currentIndex()) + + def on_menu_action_version(self): + webopen("https://github.com/andreasgriffin/bitcoin-safe/releases") def showEvent(self, event) -> None: super().showEvent(event) @@ -522,14 +547,10 @@ def updateUI(self) -> None: self.menu_show_logs.setText(self.tr("&Show Logs")) # the search fields - for qt_wallet in self.qt_wallets.values(): - if self.tab_wallets.top_right_widget: - main_search_field_hidden = ( - self.tab_wallets.count() <= 1 - ) and self.tab_wallets.tabBarAutoHide() - self.tab_wallets.top_right_widget.setVisible(not main_search_field_hidden) - if qt_wallet.tabs.top_right_widget: - qt_wallet.tabs.top_right_widget.setVisible(main_search_field_hidden) + # for qt_wallet in self.qt_wallets.values(): + if self.tab_wallets.top_right_widget: + main_search_field_hidden = (self.tab_wallets.count() <= 1) and self.tab_wallets.tabBarAutoHide() + self.tab_wallets.top_right_widget.setVisible(not main_search_field_hidden) def focus_search_box(self): self.search_box.search_field.setFocus(Qt.FocusReason.ShortcutFocusReason) @@ -537,16 +558,11 @@ def focus_search_box(self): def populate_recent_wallets_menu(self, recently_open_wallets: Iterable[str]) -> None: self.menu_wallet_recent.clear() - def factory(filepath): - def f(*args): - self.open_wallet(file_path=filepath) - - return f - for filepath in reversed(list(recently_open_wallets)): if not Path(filepath).exists(): continue - self.menu_wallet_recent.add_action(os.path.basename(filepath), factory(filepath=filepath)) + action = partial(self.signals.open_file_path.emit, filepath) + self.menu_wallet_recent.add_action(os.path.basename(filepath), action) def change_wallet_id(self) -> Optional[str]: qt_wallet = self.get_qt_wallet() @@ -567,12 +583,9 @@ def change_wallet_id(self) -> Optional[str]: # in the wallet qt_wallet.wallet.set_wallet_id(new_wallet_id) - # change dict key - self.qt_wallets[new_wallet_id] = qt_wallet - del self.qt_wallets[old_id] # tab text - self.tab_wallets.setTabText(self.tab_wallets.indexOf(qt_wallet.tab), new_wallet_id) + self.tab_wallets.setTabText(self.tab_wallets.indexOf(qt_wallet), new_wallet_id) # save under new filename old_filepath = qt_wallet.file_path @@ -595,23 +608,20 @@ def change_wallet_password(self) -> None: qt_wallet.change_password() def on_signal_broadcast_tx(self, transaction: bdk.Transaction) -> None: - def f_sync_all(qt_wallets: List[QTWallet]): - for qt_wallet in qt_wallets: - qt_wallet.sync() - - qt_wallets_to_sync: List[QTWallet] = [] - last_qt_wallet_involved: Optional[QTWallet] = None for qt_wallet in self.qt_wallets.values(): if qt_wallet.wallet.transaction_related_to_my_addresses(transaction): - qt_wallets_to_sync.append(qt_wallet) last_qt_wallet_involved = qt_wallet if last_qt_wallet_involved: - self.tab_wallets.setCurrentWidget(last_qt_wallet_involved.tab) + self.tab_wallets.setCurrentWidget(last_qt_wallet_involved) last_qt_wallet_involved.tabs.setCurrentWidget(last_qt_wallet_involved.history_tab) - QTimer.singleShot(500, lambda: f_sync_all(qt_wallets_to_sync)) + QTimer.singleShot(500, self.sync_all) + + def sync_all(self): + for qt_wallet in self.qt_wallets.values(): + qt_wallet.sync() def on_tab_changed(self, index: int) -> None: qt_wallet = self.get_qt_wallet(self.tab_wallets.widget(index)) @@ -665,6 +675,7 @@ def show_descriptor_export_window(self, wallet: Wallet | None = None) -> None: wallet_id=qt_wallet.wallet.id, ) self.attached_widgets.append(d) + d.aboutToClose.connect(self.signal_remove_attached_widget) d.show() def show_register_multisig(self, wallet: Wallet | None = None) -> None: @@ -679,10 +690,15 @@ def show_register_multisig(self, wallet: Wallet | None = None) -> None: hardware_signer_interaction = RegisterMultisigInteractionWidget( qt_wallet=qt_wallet, threading_parent=self.threading_manager ) + hardware_signer_interaction.aboutToClose.connect(self.signal_remove_attached_widget) hardware_signer_interaction.set_minimum_size_as_floating_window() hardware_signer_interaction.show() self.attached_widgets.append(hardware_signer_interaction) + def on_signal_remove_attached_widget(self, widget: QWidget): + if widget in self.attached_widgets: + self.attached_widgets.remove(widget) + def export_pdf_statement(self, wallet: Wallet | None = None) -> None: qt_wallet = self.get_qt_wallet(if_none_serve_last_active=True) if not qt_wallet or not qt_wallet.wallet: @@ -716,13 +732,11 @@ def open_tx_file(self, file_path: Optional[str] = None) -> None: self.tr("All Files (*);;PSBT (*.psbt);;Transation (*.tx)"), ) if not file_path: - logger.info("No file selected") + logger.info(self.tr("No file selected")) return logger.info(self.tr("Selected file: {file_path}").format(file_path=file_path)) - with open(file_path, "rb") as file: - string_content = file.read() - + string_content = file_to_str(file_path) self.signals.open_tx_like.emit(string_content) def fetch_txdetails(self, txid: str) -> Optional[bdk.TransactionDetails]: @@ -767,7 +781,9 @@ def open_tx_like_in_tab( if not wallet: logger.info( - f"Could not identify the wallet belonging to the transaction inputs. Trying to open anyway..." + self.tr( + f"Could not identify the wallet belonging to the transaction inputs. Trying to open anyway..." + ) ) current_qt_wallet = self.get_qt_wallet(if_none_serve_last_active=True) wallet = current_qt_wallet.wallet if current_qt_wallet else None @@ -779,7 +795,7 @@ def open_tx_like_in_tab( if not qt_wallet: Message(self.tr(" Please open the sender wallet to edit this thransaction.")) return None - self.tab_wallets.setCurrentWidget(qt_wallet.tab) + self.tab_wallets.setCurrentWidget(qt_wallet) qt_wallet.tabs.setCurrentWidget(qt_wallet.send_tab) ToolsTxUiInfo.pop_change_recipient(txlike, wallet) @@ -800,7 +816,8 @@ def open_tx_like_in_tab( if isinstance(txlike, str): try: res = Data.from_str(txlike, network=self.config.network) - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") Message(self.tr("Could not decode this string"), type=MessageType.Error) return None if res.data_type == DataType.Txid: @@ -820,19 +837,21 @@ def open_tx_like_in_tab( logger.warning(f"DataType {res.data_type.name} was not handled.") return None + def _result_callback_load_tx_like_from_qr(self, data: Data) -> None: + if data.data_type in [ + DataType.PSBT, + DataType.Tx, + DataType.Txid, + ]: + self.signals.open_tx_like.emit(data.data) + def load_tx_like_from_qr(self) -> None: - def result_callback(data: Data) -> None: - if data.data_type in [ - DataType.PSBT, - DataType.Tx, - DataType.Txid, - ]: - self.open_tx_like_in_tab(data.data) self.signals.close_all_video_widgets.emit() d = BitcoinVideoWidget() + d.aboutToClose.connect(self.signal_remove_attached_widget) self.attached_widgets.append(d) - d.signal_data.connect(result_callback) + d.signal_data.connect(self._result_callback_load_tx_like_from_qr) d.show() return None @@ -843,12 +862,10 @@ def dialog_open_qr_scanner(self) -> None: ) def dialog_open_tx_from_str(self) -> ImportDialog: - def process_input(s: str) -> None: - self.open_tx_like_in_tab(s) tx_dialog = ImportDialog( network=self.config.network, - on_open=process_input, + on_open=self.signals.open_tx_like.emit, window_title=self.tr("Open Transaction or PSBT"), text_button_ok=self.tr("OK"), text_instruction_label=self.tr( @@ -857,6 +874,7 @@ def process_input(s: str) -> None: text_placeholder=self.tr("Paste your Bitcoin Transaction or PSBT in here or drop a file"), close_all_video_widgets=self.signals.close_all_video_widgets, ) + tx_dialog.aboutToClose.connect(self.signal_remove_attached_widget) self.attached_widgets.append(tx_dialog) tx_dialog.show() return tx_dialog @@ -915,7 +933,7 @@ def open_tx_in_tab( utxo_list = UTXOList( self.config, self.signals, - get_outpoints=lambda: get_prev_outpoints(tx), + outpoints=get_prev_outpoints(tx), hidden_columns=[ UTXOList.Columns.OUTPOINT, UTXOList.Columns.PARENTS, @@ -1012,7 +1030,7 @@ def open_psbt_in_tab( utxo_list = UTXOList( self.config, self.signals, - get_outpoints=lambda: get_prev_outpoints(psbt.extract_tx()), + outpoints=get_prev_outpoints(psbt.extract_tx()), hidden_columns=[ UTXOList.Columns.OUTPOINT, UTXOList.Columns.PARENTS, @@ -1086,7 +1104,7 @@ def open_wallet(self, file_path: Optional[str] = None) -> Optional[QTWallet]: self.tr("Wallet Files (*.wallet);;All Files (*)"), ) if not file_path: - logger.info("No file selected") + logger.info(self.tr("No file selected")) return None # make sure this wallet isn't open already by this instance @@ -1126,11 +1144,10 @@ def open_wallet(self, file_path: Optional[str] = None) -> Optional[QTWallet]: signals=self.signals, mempool_data=self.mempool_data, fx=self.fx, - get_lang_code=self.language_chooser.get_current_lang_code, - set_tab_widget_icon=self.set_tab_widget_icon, threading_parent=self.threading_manager, ) except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") # 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.", log_traceback=True) QTWallet.remove_lockfile(Path(file_path)) @@ -1173,20 +1190,20 @@ def write_current_open_txs_to_config(self) -> None: self.config.opened_txlike[str(self.config.network)] = l 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() + qt_protowallet = self.create_qtprotowallet((1, 1), show_tutorial=True) + if qt_protowallet: + qt_protowallet.wallet_descriptor_ui.disable_fields() 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() + qt_protowallet = self.create_qtprotowallet((2, 3), show_tutorial=True) + if qt_protowallet: + qt_protowallet.wallet_descriptor_ui.disable_fields() def click_custom_signature(self) -> None: - qtprotowallet = self.create_qtprotowallet((3, 5), show_tutorial=False) + qt_protowallet = self.create_qtprotowallet((3, 5), show_tutorial=False) def new_wallet(self) -> None: - self.welcome_screen.add_new_wallet_welcome_tab() + self.welcome_screen.add_new_wallet_welcome_tab(self.tab_wallets) def new_wallet_id(self) -> str: return f'{self.tr("new")}{len(self.qt_wallets)}' @@ -1197,7 +1214,6 @@ def create_qtwallet_from_protowallet( wallet = Wallet.from_protowallet( protowallet, self.config, default_category=CategoryEditor.get_default_categories()[0] ) - file_path = None password = None qt_wallet = QTWallet( @@ -1206,17 +1222,16 @@ def create_qtwallet_from_protowallet( self.signals, self.mempool_data, self.fx, - set_tab_widget_icon=self.set_tab_widget_icon, file_path=file_path, password=password, threading_parent=self.threading_manager, - get_lang_code=self.language_chooser.get_current_lang_code, tutorial_index=tutorial_index, + parent=self, ) qt_wallet = self.add_qt_wallet(qt_wallet, file_path=file_path, password=password) # adding these should only be done at wallet creation - qt_wallet.address_list_tags.add_default_categories() + qt_wallet.address_tab_category_editor.add_default_categories() qt_wallet.uitx_creator.clear_ui() # after the categories are updtaed, this selected the default category in the send tab self.save_qt_wallet(qt_wallet) qt_wallet.sync() @@ -1240,17 +1255,18 @@ def create_qtwallet_from_ui( else: return except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") Message(str(e), type=MessageType.Error) - def create_qtwallet_from_qtprotowallet(self, qtprotowallet: QTProtoWallet) -> None: + def create_qtwallet_from_qtprotowallet(self, qt_protowallet: QTProtoWallet) -> None: self.create_qtwallet_from_ui( - wallet_tab=qtprotowallet.tab, - protowallet=qtprotowallet.protowallet, - keystore_uis=qtprotowallet.wallet_descriptor_ui.keystore_uis, + wallet_tab=qt_protowallet, + protowallet=qt_protowallet.protowallet, + keystore_uis=qt_protowallet.wallet_descriptor_ui.keystore_uis, tutorial_index=( - qtprotowallet.tutorial_index + 1 - if qtprotowallet.tutorial_index is not None - else qtprotowallet.tutorial_index + qt_protowallet.tutorial_index + 1 + if qt_protowallet.tutorial_index is not None + else qt_protowallet.tutorial_index ), ) @@ -1274,67 +1290,102 @@ def create_qtprotowallet( wallet_id=wallet_id, ) - qtprotowallet = QTProtoWallet( + qt_protowallet = QTProtoWallet( config=self.config, signals=self.signals, protowallet=protowallet, threading_parent=self.threading_manager, - get_lang_code=self.language_chooser.get_current_lang_code, + parent=self.tab_wallets, ) - qtprotowallet.signal_close_wallet.connect( - lambda: self.close_tab(self.tab_wallets.indexOf(qtprotowallet.tab)) - ) - qtprotowallet.signal_create_wallet.connect( - lambda: self.create_qtwallet_from_qtprotowallet(qtprotowallet) - ) + qt_protowallet.signal_close_wallet.connect(self.on_signal_close_qtprotowallet) + qt_protowallet.signal_create_wallet.connect(self.on_signal_create_qtprotowallet) # tutorial - qtprotowallet.wizard = Wizard( - wallet_tabs=qtprotowallet.tabs, - qtwalletbase=qtprotowallet, + qt_protowallet.wizard = Wizard( + wallet_tabs=qt_protowallet.tabs, + qtwalletbase=qt_protowallet, ) if show_tutorial: - qtprotowallet.wizard.set_current_index(0) - qtprotowallet.wizard.set_visibilities() - - tab_import_xpub = qtprotowallet.wizard.tab_generators[TutorialStep.import_xpub] - if not isinstance(tab_import_xpub, ImportXpubs): - logger.error(f"{tab_import_xpub} is not of type ImportXpubs") - return None - - def create_qtwallet_from_ui(): - if not isinstance(tab_import_xpub, ImportXpubs): - logger.error(f"{tab_import_xpub} is not of type ImportXpubs") # type: ignore[unreachable] - return None + qt_protowallet.wizard.set_current_index(0) + qt_protowallet.wizard.set_visibilities() - if not tab_import_xpub.keystore_uis: - Message("Cannot create wallet, because no keystores are available", type=MessageType.Error) - return - self.create_qtwallet_from_ui( - wallet_tab=qtprotowallet.tab, - protowallet=protowallet, - keystore_uis=tab_import_xpub.keystore_uis, - tutorial_index=qtprotowallet.tutorial_index, - ) - - qtprotowallet.wizard.signal_create_wallet.connect(create_qtwallet_from_ui) + qt_protowallet.wizard.signal_create_wallet.connect( + self.create_qtwallet_from_protowallet_from_wizard_keystore + ) # add to tabs self.tab_wallets.add_tab( - tab=qtprotowallet.tab, + tab=qt_protowallet, icon=read_QIcon("file.png"), - description=qtprotowallet.protowallet.id, + description=qt_protowallet.protowallet.id, focus=True, - data=qtprotowallet, + data=qt_protowallet, ) - return qtprotowallet + return qt_protowallet + + def create_qtwallet_from_protowallet_from_wizard_keystore(self, protowallet_id: str): + """The keystore from the wizard UI are the ones used for walle creation + + It is checked if qt_protowallet.protowallet is consitent with the UI of the wizard + + Args: + protowallet_id (str): _description_ + + Returns: + _type_: _description_ + """ + qt_protowallet = self.qt_protowallets.get(protowallet_id) + if not qt_protowallet or not qt_protowallet.wizard: + return + if not hasattr(qt_protowallet.wizard, "tab_generators"): + return + tab_generators = getattr(qt_protowallet.wizard, "tab_generators") + if not isinstance(tab_generators, dict): + return + + if not isinstance(tab_import_xpub := tab_generators.get(TutorialStep.import_xpub), ImportXpubs): + logger.error(f"{tab_import_xpub} is not of type ImportXpubs") # type: ignore[unreachable] + return None - def set_tab_widget_icon(self, tab: QWidget, icon: QIcon, tooltip: str | None) -> None: - idx = self.tab_wallets.indexOf(tab) + if not tab_import_xpub.keystore_uis: + Message("Cannot create wallet, because no keystores are available", type=MessageType.Error) + return + + org_protowallet = qt_protowallet.protowallet + tab_import_xpub.keystore_uis.set_protowallet_from_keystore_ui() + if not org_protowallet.is_essentially_equal(qt_protowallet.protowallet): + Message("QtProtowallet inconsitent. Cannot create wallet", type=MessageType.Error) + return + + self.create_qtwallet_from_ui( + wallet_tab=qt_protowallet, + protowallet=qt_protowallet.protowallet, + keystore_uis=tab_import_xpub.keystore_uis, + tutorial_index=qt_protowallet.tutorial_index, + ) + + def on_signal_close_qtprotowallet(self, wallet_id): + qt_protowallet = self.qt_protowallets.get(wallet_id) + if not qt_protowallet: + return + self.close_tab(self.tab_wallets.indexOf(qt_protowallet)) + + def on_signal_create_qtprotowallet(self, wallet_id): + qt_protowallet = self.qt_protowallets.get(wallet_id) + if not qt_protowallet: + return + self.create_qtwallet_from_qtprotowallet(qt_protowallet) + + def on_set_tab_properties(self, wallet_id: str, icon_name: str, tooltip: str) -> None: + qt_wallet = self.qt_wallets.get(wallet_id) + if not qt_wallet: + return + + idx = self.tab_wallets.indexOf(qt_wallet) if idx != -1: - self.tab_wallets.setTabIcon(idx, icon) + self.tab_wallets.setTabIcon(idx, read_QIcon(icon_name)) self.tab_wallets.setTabToolTip(idx, tooltip if tooltip else "") def add_qt_wallet( @@ -1350,7 +1401,7 @@ def add_qt_wallet( qt_wallet.file_path = file_path with LoadingWalletTab(self.tab_wallets, qt_wallet.wallet.id, focus=True): - self.welcome_screen.remove_tab() + self.welcome_screen.remove_me() # tutorial qt_wallet.wizard = Wizard( wallet_tabs=qt_wallet.tabs, @@ -1362,9 +1413,8 @@ def add_qt_wallet( qt_wallet.wizard.set_visibilities() # add to tabs - self.qt_wallets[qt_wallet.wallet.id] = qt_wallet self.tab_wallets.add_tab( - tab=qt_wallet.tab, + tab=qt_wallet, icon=read_QIcon("status_waiting.svg"), description=qt_wallet.wallet.id, focus=True, @@ -1406,7 +1456,7 @@ def _get_qt_base_wallet( ) -> Optional[QtWalletBase]: tab = self.tab_wallets.currentWidget() if tab is None else tab for qt_base_wallet in qt_base_wallets: - if tab == qt_base_wallet.tab: + if tab == qt_base_wallet: return qt_base_wallet if if_none_serve_last_active: return self.last_qtwallet @@ -1442,21 +1492,39 @@ def show_address(self, addr: str, wallet_id: str, parent: QWidget | None = None) self.mempool_data, parent=parent, ) + d.aboutToClose.connect(self.signal_remove_attached_widget) self.attached_widgets.append(d) d.show() def event_wallet_tab_closed(self) -> None: if not self.tab_wallets.count(): - self.welcome_screen.add_new_wallet_welcome_tab() + self.welcome_screen.add_new_wallet_welcome_tab(self.tab_wallets) + # necessary to remove old qt_wallets from memory def event_wallet_tab_added(self) -> None: pass + def remove_qt_wallet_by_id(self, wallet_id: str) -> None: + qt_wallet = self.qt_wallets.get(wallet_id) + if not qt_wallet: + return + self.remove_qt_wallet(qt_wallet=qt_wallet) + + def remove_qt_protowallet(self, qt_protowallet: Optional[QTProtoWallet]) -> None: + if not qt_protowallet: + return + for i in range(self.tab_wallets.count()): + if self.tab_wallets.widget(i) == qt_protowallet: + self.tab_wallets.removeTab(i) + + qt_protowallet.close() + self.event_wallet_tab_closed() + def remove_qt_wallet(self, qt_wallet: Optional[QTWallet]) -> None: if not qt_wallet: return for i in range(self.tab_wallets.count()): - if self.tab_wallets.widget(i) == qt_wallet.tab: + if self.tab_wallets.widget(i) == qt_wallet: self.tab_wallets.removeTab(i) self.add_recently_open_wallet(qt_wallet.file_path) @@ -1465,8 +1533,8 @@ def remove_qt_wallet(self, qt_wallet: Optional[QTWallet]) -> None: self.last_qtwallet = None qt_wallet.close() QTWallet.remove_lockfile(wallet_file_path=Path(qt_wallet.file_path)) - del self.qt_wallets[qt_wallet.wallet.id] self.event_wallet_tab_closed() + gc.collect() def add_recently_open_wallet(self, file_path: str) -> None: self.config.add_recently_open_wallet(file_path) @@ -1480,6 +1548,9 @@ def remove_all_qt_wallet(self) -> None: self.remove_qt_wallet(qt_wallet) def close_tab(self, index: int) -> None: + last_active_tab = self.tab_wallets.get_last_active_tab() + + tab = self.tab_wallets.widget(index) tab_data = self.tab_wallets.tabData(index) # qt_wallet qt_wallet = self.get_qt_wallet(tab=self.tab_wallets.widget(index)) @@ -1490,6 +1561,10 @@ def close_tab(self, index: int) -> None: return logger.info(self.tr("Closing wallet {id}").format(id=qt_wallet.wallet.id)) self.save_qt_wallet(qt_wallet) + self.remove_qt_wallet(qt_wallet) + elif isinstance(tab_data, QTProtoWallet): + tab_data.close() + self.remove_qt_protowallet(tab_data) elif isinstance(tab_data, UITx_Viewer): if isinstance(tab_data.data.data, bdk.PartiallySignedTransaction) and question_dialog( self.tr("Do you want to save the PSBT {id}?").format( @@ -1499,21 +1574,21 @@ def close_tab(self, index: int) -> None: ): tab_data.export_data_simple.button_export_file.export_to_file() logger.info(self.tr("Closing tab {name}").format(name=self.tab_wallets.tabText(index))) + tab_data.close() else: logger.info(self.tr("Closing tab {name}").format(name=self.tab_wallets.tabText(index))) - self.tab_wallets.jump_to_last_active_tab() + if last_active_tab: + self.tab_wallets.jump_to_tab(last_active_tab) + + if tab: + self.tab_wallets.remove_tab(tab) - # get the tabdata before removing the tab - self.tab_wallets.removeTab(index) if isinstance(tab_data, ThreadingManager): # this is necessary to ensure the closeevent # and with it the thread cleanup is called tab_data.end_threading_manager() - if qt_wallet: - self.remove_qt_wallet(qt_wallet) - # other events self.event_wallet_tab_closed() @@ -1536,15 +1611,16 @@ def closeEvent(self, event: Optional[QCloseEvent]) -> None: self.save_all_wallets() self.threading_manager.end_threading_manager() - self.remove_all_qt_wallet() if self.new_startup_network: self.config.network = self.new_startup_network self.config.save() + SignalTools.disconnect_all_signals_from(self.signals) + SignalTools.disconnect_all_signals_from(self) self.tray.hide() - logger.info(f"Finished close handling of {self}") + logger.info(f"Finished close handling of {self.__class__.__name__}") super().closeEvent(event) def restart(self, new_startup_network: bdk.Network | None = None) -> None: diff --git a/bitcoin_safe/gui/qt/my_treeview.py b/bitcoin_safe/gui/qt/my_treeview.py index 66314ab..c35e026 100644 --- a/bitcoin_safe/gui/qt/my_treeview.py +++ b/bitcoin_safe/gui/qt/my_treeview.py @@ -51,29 +51,16 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import logging - -from bitcoin_safe.gui.qt.dialog_import import file_to_str -from bitcoin_safe.gui.qt.html_delegate import HTMLDelegate -from bitcoin_safe.gui.qt.wrappers import Menu -from bitcoin_safe.signals import Signals -from bitcoin_safe.util import str_to_qbytearray -from bitcoin_safe.wallet import TxStatus - -from ...config import UserConfig -from ...i18n import translate -from ...signals import TypedPyQtSignalNo - -logger = logging.getLogger(__name__) - import csv import enum import io import json +import logging import os import os.path import tempfile from decimal import Decimal +from functools import partial from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Type, Union from bitcoin_qr_tools.data import Data @@ -127,8 +114,20 @@ QWidget, ) +from bitcoin_safe.gui.qt.dialog_import import file_to_str +from bitcoin_safe.gui.qt.html_delegate import HTMLDelegate +from bitcoin_safe.gui.qt.wrappers import Menu +from bitcoin_safe.signals import Signals +from bitcoin_safe.util import str_to_qbytearray, unique_elements +from bitcoin_safe.wallet import TxStatus + +from ...config import UserConfig +from ...i18n import translate +from ...signals import TypedPyQtSignalNo from .util import do_copy, read_QIcon +logger = logging.getLogger(__name__) + def needs_frequent_flag(status: TxStatus | None) -> bool: if not status: @@ -163,69 +162,48 @@ def addToggle(self, text: str, callback: Callable, *, tooltip="") -> QAction: class MyStandardItemModel(QStandardItemModel): + def __init__( self, - parent, - drag_key: str = "item", - drag_keys_to_file_paths=None, + key_column: int, + parent=None, ) -> None: super().__init__(parent) - self.mytreeview: MyTreeView = parent - self.drag_key = drag_key - self.drag_keys_to_file_paths = self.csv_drag_keys_to_file_paths - if drag_keys_to_file_paths: - self.drag_keys_to_file_paths = drag_keys_to_file_paths - - def csv_drag_keys_to_file_paths( - self, drag_keys: Iterable[str], save_directory: Optional[str] = None - ) -> List[str]: - """Writes the selected rows in a csv file (the directory is )""" - 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)] + self.key_column = key_column def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlag: - if index.column() == self.mytreeview.key_column: # only enable dragging for column 1 + if index.column() == self.key_column: # only enable dragging for column 1 return super().flags(index) | Qt.ItemFlag.ItemIsDragEnabled else: return super().flags(index) - def mimeData(self, indexes: Iterable[QtCore.QModelIndex]) -> QMimeData: - mime_data = QMimeData() - keys = set() - for index in indexes: - if index.isValid(): - item = self.item(index.row(), self.mytreeview.key_column) - if not item: - continue - key = item.data(role=MyItemDataRole.ROLE_KEY) - keys.add(key) - - # set the key data for internal drags - d = { - "type": f"drag_{self.drag_key}", - self.drag_key: list(keys), - } - - mime_data.setData("application/json", str_to_qbytearray(json.dumps(d))) - - # set the key data for files - file_urls = [] - for file_path in self.drag_keys_to_file_paths(keys): - # Add the file URL to the list - file_urls.append(QUrl.fromLocalFile(file_path)) - - # Set the URLs of the files in the mime data - mime_data.setUrls(file_urls) - - return mime_data +class MySortModel(QSortFilterProxyModel): + role_drag_key = MyItemDataRole.ROLE_CLIPBOARD_DATA + class CSVOrderTpye(enum.Enum): + proxy_order = enum.auto() + source_order = enum.auto() + selection_order = enum.auto() + sorted_drag_key = enum.auto() -class MySortModel(QSortFilterProxyModel): - def __init__(self, parent, source_model: MyStandardItemModel, sort_role: int) -> None: + def __init__( + self, + key_column: int, + parent, + source_model: MyStandardItemModel, + sort_role: int, + Columns: Iterable[int], + drag_key: str = "item", + custom_drag_keys_to_file_paths=None, + ) -> None: super().__init__(parent) + self.key_column = key_column self._sort_role = sort_role self._source_model = source_model + self.Columns = Columns + self.drag_key = drag_key + self.custom_drag_keys_to_file_paths = custom_drag_keys_to_file_paths self.setSourceModel(source_model) self.setSortRole(sort_role) @@ -234,9 +212,7 @@ def setSourceModel(self, sourceModel: MyStandardItemModel) -> None: # type: ign super().setSourceModel(sourceModel) def sourceModel(self) -> MyStandardItemModel: - if self._source_model: - return self._source_model - return MyStandardItemModel(parent=self) + return self._source_model def lessThan(self, source_left: QModelIndex, source_right: QModelIndex) -> bool: item1 = self.sourceModel().itemFromIndex(source_left) @@ -253,9 +229,151 @@ def lessThan(self, source_left: QModelIndex, source_right: QModelIndex) -> bool: v2 = item2.text() try: return Decimal(v1) < Decimal(v2) - except: + except Exception: return v1 < v2 + def close(self): + self._source_model.clear() + # super().close() + + def item_from_index(self, idx: QModelIndex) -> Optional[QStandardItem]: + return self.sourceModel().itemFromIndex(self.mapToSource(idx)) + + def get_rows_as_list( + self, drag_keys: List[str] | None, order: CSVOrderTpye = CSVOrderTpye.proxy_order + ) -> Any: + "if drag_keys is None, then all rows" + + def get_data( + row, col, role=MyItemDataRole.ROLE_CLIPBOARD_DATA, model: MyStandardItemModel | MySortModel = self + ) -> Any: + index = model.index(row, col) + item = self.item_from_index(index) + if item: + return item.data(role) + + # collect data + proxy_ordered_dict: Dict[str, List[str]] = {} + for row in range(self.rowCount()): + drag_key = get_data(row, self.key_column, role=self.role_drag_key) + if drag_keys is None or get_data(row, self.key_column, role=self.role_drag_key) in drag_keys: + row_data = [] + for column in self.Columns: + row_data.append(get_data(row, column)) + proxy_ordered_dict[drag_key] = row_data + + ordered_drag_keys: Iterable[str] = [] + if order == self.CSVOrderTpye.proxy_order: + ordered_drag_keys = proxy_ordered_dict.keys() + elif order == self.CSVOrderTpye.selection_order: + ordered_drag_keys = unique_elements(drag_keys) if drag_keys else proxy_ordered_dict.keys() + elif order == self.CSVOrderTpye.sorted_drag_key: + ordered_drag_keys = ( + sorted(unique_elements(drag_keys)) if drag_keys else sorted(proxy_ordered_dict.keys()) + ) + elif order == self.CSVOrderTpye.source_order: + ordered_drag_keys = [] + for row in range(self._source_model.rowCount()): + drag_key = get_data(row, self.key_column, role=self.role_drag_key, model=self._source_model) + if drag_key: + ordered_drag_keys.append(drag_key) + + # assemble the table + table = [] + headers = [ + self.headerData(i, QtCore.Qt.Orientation.Horizontal) for i in range(self.columnCount()) + ] # retrieve headers + table.append(headers) # write headers to table + for drag_key in ordered_drag_keys: + if _row_data := proxy_ordered_dict.get(drag_key): + table.append(_row_data) + return table + + def as_csv_string(self, drag_keys: List[str] | None) -> str: + table = self.get_rows_as_list(drag_keys=drag_keys) + + stream = io.StringIO() + writer = csv.writer(stream) + writer.writerows(table) + + return stream.getvalue() + + def csv_drag_keys_to_file_paths( + self, drag_keys: List[str], save_directory: Optional[str] = None + ) -> List[str]: + """Writes the selected rows in a csv file (the directory is )""" + file_path = os.path.join(save_directory, f"export.csv") if save_directory else None + return [self.csv_drag_keys_to_file_path(drag_keys=drag_keys, file_path=file_path)] + + def csv_drag_keys_to_file_path( + self, drag_keys: List[str] | None = None, file_path: str | None = None + ) -> str: + "if drag_keys is None, then export all" + + # Fetch the serialized data using the drag_keys + csv_string = self.as_csv_string(drag_keys=drag_keys) + + if file_path: + file_descriptor = os.open(file_path, os.O_CREAT | os.O_WRONLY) + else: + # Create a temporary file + file_descriptor, file_path = tempfile.mkstemp( + suffix=f".csv", + prefix=f"{self.drag_key} ", + ) + + with os.fdopen(file_descriptor, "w") as file: + file.write(csv_string) + + logger.debug(f"CSV Table saved to {file_path}") + return file_path + + def mimeData(self, indexes: Iterable[QtCore.QModelIndex]) -> QMimeData: + """_summary_ + + Args: + indexes (Iterable[QtCore.QModelIndex]): + these are in the order of how they were selected + + Returns: + QMimeData: _description_ + """ + mime_data = QMimeData() + keys = list() + for index in indexes: + if index.isValid(): + item = self.item_from_index(index.sibling(index.row(), self.key_column)) + if not item: + continue + key = item.data(role=self.role_drag_key) + keys.append(key) + keys = unique_elements(keys) + + # set the key data for internal drags + d = { + "type": f"drag_{self.drag_key}", + self.drag_key: list(keys), + } + + mime_data.setData("application/json", str_to_qbytearray(json.dumps(d))) + + # set the key data for files + + file_urls = [] + file_paths = ( + self.custom_drag_keys_to_file_paths(keys) + if self.custom_drag_keys_to_file_paths + else self.csv_drag_keys_to_file_paths(keys) + ) + for file_path in file_paths: + # Add the file URL to the list + file_urls.append(QUrl.fromLocalFile(file_path)) + + # Set the URLs of the files in the mime data + mime_data.setUrls(file_urls) + + return mime_data + class ElectrumItemDelegate(QStyledItemDelegate): def __init__(self, tv: "MyTreeView") -> None: @@ -358,10 +476,9 @@ def __init__( sort_column: int | None = None, sort_order: Qt.SortOrder = Qt.SortOrder.AscendingOrder, ) -> None: - parent = parent super().__init__(parent) self.signals = signals - self._source_model = MyStandardItemModel(parent) + self._source_model = MyStandardItemModel(key_column=self.key_column, parent=self) self.config = config self.stretch_column = stretch_column self.column_widths = column_widths if column_widths else {} @@ -392,7 +509,13 @@ def __init__( self._forced_update = False self._default_bg_brush = QStandardItem().background() - self.proxy = QSortFilterProxyModel() + self.proxy = MySortModel( + key_column=self.key_column, + Columns=self.Columns, + parent=self, + source_model=self._source_model, + sort_role=MyItemDataRole.ROLE_SORT_ORDER, + ) # Here's where we set the font globally for the view font = QFont("Arial", 10) @@ -455,7 +578,15 @@ def startDrag(self, action: Qt.DropAction) -> None: def create_menu(self, position: QPoint) -> Menu: menu = Menu() - selected: List[QModelIndex] = self.selected_in_column(self.key_column) + # is_multisig = isinstance(self.wallet, Multisig_Wallet) + selected = self.selected_in_column(self.key_column) + if not selected: + return menu + multi_select = len(selected) > 1 + + _selected_items = [self.item_from_index(item) for item in selected] + selected_items = [item for item in _selected_items if item] + if not selected: current_row = self.current_row_in_column(self.key_column) if current_row: @@ -474,7 +605,10 @@ def create_menu(self, position: QPoint) -> Menu: menu.add_action( self.tr("Copy as csv"), - lambda: self.copyRowsToClipboardAsCSV([r.row() for r in selected]), + partial( + self.copyRowsToClipboardAsCSV, + [item.data(MySortModel.role_drag_key) for item in selected_items if item], + ), icon=read_QIcon("csv-file.svg"), ) @@ -488,12 +622,6 @@ def add_copy_menu(self, menu: Menu, idx: QModelIndex, include_columns_even_if_hi copy_menu = menu.add_menu(self.tr("Copy")) copy_menu.setIcon(read_QIcon("copy.png")) - def factory(text, title): - def f(text=text, title=title): - self.place_text_on_clipboard(text=text, title=title) - - return f - for column in self.Columns: if self.isColumnHidden(column) and ( include_columns_even_if_hidden is None or column not in include_columns_even_if_hidden @@ -514,7 +642,8 @@ def f(text=text, title=title): if not clipboard_data: continue - copy_menu.add_action(column_title, factory(text=clipboard_data, title=column_title)) + action = partial(self.place_text_on_clipboard, text=clipboard_data, title=column_title) + copy_menu.add_action(column_title, action) return copy_menu def set_editability(self, items: List[QStandardItem]) -> None: @@ -705,42 +834,16 @@ def get_data(row, col) -> Any: ) # 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) -> Any: - def get_data(row, col) -> Any: - model = self.model() # assuming this is a QAbstractItemModel or subclass - index = model.index(row, col) - - if hasattr(model, "data"): - return model.data(index, MyItemDataRole.ROLE_CLIPBOARD_DATA) - else: - item = self.item_from_index(index) - if item: - return item.data(MyItemDataRole.ROLE_CLIPBOARD_DATA) - - row_numbers = sorted(row_numbers) - - table = [] - headers = [ - self.model().headerData(i, QtCore.Qt.Orientation.Horizontal) - for i in range(self.model().columnCount()) - ] # retrieve headers - table.append(headers) # write headers to table - - for row in row_numbers: - row_data = [] - for column in self.Columns: - row_data.append(get_data(row, column)) - table.append(row_data) - - return table - - def copyRowsToClipboardAsCSV(self, row_numbers) -> None: - table = self.get_rows_as_list(row_numbers) + def copyRowsToClipboardAsCSV(self, drag_keys: List[str] | None) -> None: + table = self.proxy.get_rows_as_list(drag_keys) stream = io.StringIO() writer = csv.writer(stream) writer.writerows(table) - do_copy(stream.getvalue(), title=f"{len(row_numbers)} rows have ben copied as csv") + do_copy( + stream.getvalue(), + title=f"{len(list(drag_keys) ) if drag_keys else self.model().rowCount() } rows have ben copied as csv", + ) def mouseDoubleClickEvent(self, event: QMouseEvent | None) -> None: if not event: @@ -831,55 +934,16 @@ 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) -> str: - table = self.get_rows_as_list( - row_numbers=list(range(self.model().rowCount())) if export_all else row_numbers - ) - - stream = io.StringIO() - writer = csv.writer(stream) - writer.writerows(table) - - return stream.getvalue() - def export_as_csv(self, file_path=None) -> None: if not file_path: file_path, _ = QFileDialog.getSaveFileName( self, self.tr("Export csv"), "", self.tr("All Files (*);;Text Files (*.csv)") ) if not file_path: - logger.info("No file selected") + logger.info(self.tr("No file selected")) return - self.csv_drag_keys_to_file_path(file_path=file_path, export_all=True) - - def csv_drag_keys_to_file_path( - self, drag_keys: Optional[Iterable[str]] = None, file_path: str | None = None, export_all=False - ) -> str: - row_numbers: List[int] = [] - if drag_keys and not export_all: - for row_number in range(0, self._source_model.rowCount()): - item = self._source_model.item(row_number, self.key_column) - if item and item.data(MyItemDataRole.ROLE_KEY) in drag_keys: - row_numbers.append(row_number) - - # Fetch the serialized data using the drag_keys - csv_string = self.as_csv_string(row_numbers=row_numbers, export_all=export_all) - - if file_path: - file_descriptor = os.open(file_path, os.O_CREAT | os.O_WRONLY) - else: - # Create a temporary file - file_descriptor, file_path = tempfile.mkstemp( - suffix=f".csv", - prefix=f"{self._source_model.drag_key} ", - ) - - with os.fdopen(file_descriptor, "w") as file: - file.write(csv_string) - - logger.debug(f"CSV Table saved to {file_path}") - return file_path + self.proxy.csv_drag_keys_to_file_path(file_path=file_path) def place_text_on_clipboard(self, text: str, *, title: str | None = None) -> None: do_copy(text, title=title) @@ -1059,8 +1123,8 @@ def update_content(self) -> None: # sort again just as before self.signal_update.emit() - @staticmethod - def get_json_mime_data(mime_data: QMimeData) -> Optional[Dict]: + @classmethod + def get_json_mime_data(cls, mime_data: QMimeData) -> Optional[Dict]: if mime_data.hasFormat("application/json"): data_bytes = mime_data.data("application/json") try: @@ -1068,18 +1132,32 @@ def get_json_mime_data(mime_data: QMimeData) -> Optional[Dict]: logger.debug(f"dragEnterEvent: {json_string}") d = json.loads(json_string) return d - except: + except Exception as e: + logger.debug(f"{cls.__name__}: {e}") return None return None + def close(self): + self.proxy.close() + self._source_model.clear() + self.setParent(None) + super().close() + class SearchableTab(QWidget): def __init__(self, parent=None, **kwargs) -> None: super().__init__(parent=parent) - self.searchable_list: MyTreeView + self.searchable_list: MyTreeView | None = None + + def close(self): + if self.searchable_list: + self.searchable_list.close() + self.searchable_list = None + self.setParent(None) + super().close() class TreeViewWithToolbar(SearchableTab): @@ -1096,7 +1174,6 @@ def __init__( # in searchable_list signal_update will be sent after the update. and since this # is relevant for the balance to show, i need to update also the balance label # which is done in updateUi - self.searchable_list.signal_update.connect(self.updateUi) def create_layout(self) -> None: layout = QVBoxLayout(self) @@ -1106,18 +1183,19 @@ def create_layout(self) -> None: layout.addLayout(self.toolbar) layout.addWidget(self.searchable_list) + def _searchable_list_export_as_csv(self): + if self.searchable_list: + self.searchable_list.export_as_csv() + def create_toolbar_with_menu(self, title): self.menu = MyMenu(self.config) self.action_export_as_csv = self.menu.add_action( - "", self.searchable_list.export_as_csv, icon=read_QIcon("csv-file.svg") + "", self._searchable_list_export_as_csv, icon=read_QIcon("csv-file.svg") ) toolbar_button = QToolButton() - def create_menu(): - self.menu.exec(QCursor.pos()) - - toolbar_button.clicked.connect(create_menu) + toolbar_button.clicked.connect(partial(self.menu.exec, QCursor.pos())) toolbar_button.setIcon(read_QIcon("preferences.svg")) toolbar_button.setMenu(self.menu) toolbar_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) @@ -1128,7 +1206,8 @@ def create_menu(): self.search_edit = QLineEdit() self.search_edit.setClearButtonEnabled(True) - self.search_edit.textChanged.connect(self.searchable_list.filter) + if self.searchable_list: + self.search_edit.textChanged.connect(self.searchable_list.filter) self.toolbar.addWidget(self.balance_label) self.toolbar.addStretch() @@ -1152,3 +1231,10 @@ def toggle_toolbar(self, config=None) -> None: def updateUi(self) -> None: self.search_edit.setPlaceholderText(translate("mytreeview", "Type to filter")) self.action_export_as_csv.setText(translate("mytreeview", "Export as CSV")) + + def close(self): + if self.searchable_list: + self.searchable_list.close() + self.searchable_list = None + self.setParent(None) + super().close() diff --git a/bitcoin_safe/gui/qt/nLockTimePicker.py b/bitcoin_safe/gui/qt/nLockTimePicker.py index c0077c7..5dd0df9 100644 --- a/bitcoin_safe/gui/qt/nLockTimePicker.py +++ b/bitcoin_safe/gui/qt/nLockTimePicker.py @@ -28,9 +28,6 @@ import logging - -logger = logging.getLogger(__name__) - import sys from datetime import datetime, timezone @@ -45,6 +42,8 @@ QWidget, ) +logger = logging.getLogger(__name__) + class DateTimePicker(QWidget): def __init__(self) -> None: diff --git a/bitcoin_safe/gui/qt/network_settings/main.py b/bitcoin_safe/gui/qt/network_settings/main.py index 257553a..651d8e6 100644 --- a/bitcoin_safe/gui/qt/network_settings/main.py +++ b/bitcoin_safe/gui/qt/network_settings/main.py @@ -373,7 +373,7 @@ def __init__( self.groupbox_blockexplorer_layout = QHBoxLayout(self.groupbox_blockexplorer) button_mempool = QPushButton(self) button_mempool.setIcon(read_QIcon("block-explorer.svg")) - button_mempool.clicked.connect(lambda: webopen(self.edit_mempool_url.text())) + button_mempool.clicked.connect(self.on_button_mempool_clicked) self.edit_mempool_url = QCompleterLineEdit( network=network, suggestions={network: list(get_mempool_url(network).values()) for network in bdk.Network}, @@ -428,6 +428,9 @@ def __init__( self.signals.language_switch.connect(self.updateUi) self.updateUi() + def on_button_mempool_clicked(self): + return webopen(self.edit_mempool_url.text()) + def updateUi(self): self.setWindowTitle(self.tr("Network Settings")) self.groupbox_connection.setTitle(self.tr("Blockchain data source")) @@ -660,7 +663,8 @@ def compactblockfilters_ip(self, ip): def compactblockfilters_port(self) -> int: try: return int(self.compactblockfilters_port_edit.text()) - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") return self.network_configs.configs[self.network.name].compactblockfilters_port @compactblockfilters_port.setter @@ -707,7 +711,8 @@ def rpc_ip(self, ip: str): def rpc_port(self) -> int: try: return int(self.rpc_port_edit.text()) - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") return self.network_configs.configs[self.network.name].rpc_port @rpc_port.setter diff --git a/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py b/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py index b8834e5..7bdff3d 100644 --- a/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py +++ b/bitcoin_safe/gui/qt/new_wallet_welcome_screen.py @@ -29,16 +29,8 @@ import logging -from bitcoin_safe.gui.qt.data_tab_widget import DataTabWidget -from bitcoin_safe.gui.qt.wallet_list import RecentlyOpenedWalletsGroup -from bitcoin_safe.html_utils import html_f -from bitcoin_safe.signals import Signals -from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo - -logger = logging.getLogger(__name__) - from bdkpython import Network -from PyQt6.QtCore import QObject, Qt, pyqtSignal +from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtWidgets import ( QGroupBox, QHBoxLayout, @@ -49,24 +41,32 @@ QWidget, ) -from ...util import call_call_functions +from bitcoin_safe.gui.qt.data_tab_widget import DataTabWidget +from bitcoin_safe.gui.qt.wallet_list import RecentlyOpenedWalletsGroup +from bitcoin_safe.html_utils import html_f +from bitcoin_safe.signals import Signals +from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo + from .util import read_QIcon, svg_widgets_hardware_signers +logger = logging.getLogger(__name__) + -class NewWalletWelcomeScreen(QObject): +class NewWalletWelcomeScreen(QWidget): signal_onclick_multisig_signature: TypedPyQtSignalNo = pyqtSignal() # type: ignore signal_onclick_single_signature: TypedPyQtSignalNo = pyqtSignal() # type: ignore signal_onclick_custom_signature: TypedPyQtSignalNo = pyqtSignal() # type: ignore + signal_remove_me: TypedPyQtSignal[QWidget] = pyqtSignal(QWidget) # type: ignore def __init__( self, - main_tabs: DataTabWidget[object], network: Network, signals: Signals, signal_recently_open_wallet_changed: TypedPyQtSignal, + parent=None, ) -> None: - super().__init__() - self.main_tabs = main_tabs + super().__init__(parent) + self.setVisible(False) self.signals = signals self.signal_recently_open_wallet_changed = signal_recently_open_wallet_changed @@ -75,20 +75,29 @@ def __init__( self.create_ui() - self.pushButton_multisig.clicked.connect( - lambda: call_call_functions([self.signal_onclick_multisig_signature.emit, self.remove_tab]) - ) - self.pushButton_singlesig.clicked.connect( - lambda: call_call_functions([self.signal_onclick_single_signature.emit, self.remove_tab]) - ) - self.pushButton_custom_wallet.clicked.connect( - lambda: call_call_functions([self.signal_onclick_custom_signature.emit, self.remove_tab]) - ) - logger.debug(f"initialized welcome_screen = {self}") + self.pushButton_multisig.clicked.connect(self.on_pushButton_multisig) + self.pushButton_singlesig.clicked.connect(self.on_pushButton_singlesig) + self.pushButton_custom_wallet.clicked.connect(self.on_pushButton_custom_wallet) + logger.debug(f"initialized welcome_screen = {self.__class__.__name__}") + + def remove_me(self): + self.signal_remove_me.emit(self) + + def on_pushButton_multisig(self): + self.signal_onclick_multisig_signature.emit() + self.signal_remove_me.emit(self) - def add_new_wallet_welcome_tab(self) -> None: - self.main_tabs.add_tab( - tab=self.tab, + def on_pushButton_singlesig(self): + self.signal_onclick_single_signature.emit() + self.signal_remove_me.emit(self) + + def on_pushButton_custom_wallet(self): + self.signal_onclick_custom_signature.emit() + self.signal_remove_me.emit(self) + + def add_new_wallet_welcome_tab(self, main_tabs: DataTabWidget[object]) -> None: + main_tabs.add_tab( + tab=self, icon=read_QIcon("file.png"), description=self.tr("Create new wallet"), focus=True, @@ -96,16 +105,17 @@ def add_new_wallet_welcome_tab(self) -> None: ) def create_ui(self) -> None: - self.tab = QWidget() - self.tab_layout = QHBoxLayout(self.tab) + svg_max_height = 70 + svg_max_width = 60 + self._layout = QHBoxLayout(self) self.groupbox_recently_opened_wallets = RecentlyOpenedWalletsGroup( signal_open_wallet=self.signals.open_wallet, signal_recently_open_wallet_changed=self.signal_recently_open_wallet_changed, ) - self.tab_layout.addWidget(self.groupbox_recently_opened_wallets) + self._layout.addWidget(self.groupbox_recently_opened_wallets) - self.groupBox_singlesig = QGroupBox(self.tab) + self.groupBox_singlesig = QGroupBox(self) self.verticalLayout = QVBoxLayout(self.groupBox_singlesig) self.label_singlesig = QLabel(self.groupBox_singlesig) # font = QFont() @@ -120,7 +130,7 @@ def create_ui(self) -> None: self.horizontalLayout_4 = QHBoxLayout(self.groupBox_1signingdevice) svg_widgets = svg_widgets_hardware_signers( - 1, parent=self.groupBox_1signingdevice, max_height=100, max_width=100 + 1, parent=self.groupBox_1signingdevice, max_height=svg_max_height, max_width=svg_max_width ) for svg_widget in svg_widgets: self.horizontalLayout_4.addWidget(svg_widget) @@ -134,9 +144,9 @@ def create_ui(self) -> None: self.verticalLayout.addWidget(self.pushButton_singlesig) - self.tab_layout.addWidget(self.groupBox_singlesig) + self._layout.addWidget(self.groupBox_singlesig) - self.groupBox_multisig = QGroupBox(self.tab) + self.groupBox_multisig = QGroupBox(self) self.verticalLayout_multisig = QVBoxLayout(self.groupBox_multisig) self.label_multisig = QLabel(self.groupBox_multisig) # font1 = QFont() @@ -155,7 +165,7 @@ def create_ui(self) -> None: self.groupBox_3signingdevices_layout = QHBoxLayout(self.groupBox_3signingdevices) svg_widgets = svg_widgets_hardware_signers( - 3, parent=self.groupBox_3signingdevices, max_height=100, max_width=100 + 3, parent=self.groupBox_3signingdevices, max_height=svg_max_height, max_width=svg_max_width ) for i, svg_widget in enumerate(svg_widgets): self.groupBox_3signingdevices_layout.addWidget(svg_widget) @@ -168,9 +178,9 @@ def create_ui(self) -> None: self.verticalLayout_multisig.addWidget(self.pushButton_multisig) - self.tab_layout.addWidget(self.groupBox_multisig) + self._layout.addWidget(self.groupBox_multisig) - self.groupBox_3 = QGroupBox(self.tab) + self.groupBox_3 = QGroupBox(self) self.verticalLayout_2 = QVBoxLayout(self.groupBox_3) self.label_custom = QLabel(self.groupBox_3) # self.label_custom.setFont(font) @@ -182,7 +192,7 @@ def create_ui(self) -> None: self.verticalLayout_2.addWidget(self.pushButton_custom_wallet) - self.tab_layout.addWidget(self.groupBox_3) + self._layout.addWidget(self.groupBox_3) self.label_singlesig.setAlignment(Qt.AlignmentFlag.AlignTop) self.label_multisig.setAlignment(Qt.AlignmentFlag.AlignTop) @@ -236,8 +246,3 @@ def updateUi(self) -> None: ) ) self.pushButton_custom_wallet.setText(self.tr("Create custom wallet")) - - def remove_tab(self) -> None: - index = self.main_tabs.indexOf(self.tab) - if index >= 0 and self.main_tabs.count() > 1: - self.main_tabs.removeTab(index) diff --git a/bitcoin_safe/gui/qt/notification_bar.py b/bitcoin_safe/gui/qt/notification_bar.py index 511eb1b..dfd503a 100644 --- a/bitcoin_safe/gui/qt/notification_bar.py +++ b/bitcoin_safe/gui/qt/notification_bar.py @@ -105,7 +105,7 @@ def __init__( if has_close_button: main_widget_layout.addWidget(self.closeButton) self.closeButton.setFixedSize(self.sizeHint().height(), self.sizeHint().height()) - logger.debug(f"initialized {self}") + logger.debug(f"initialized {self.__class__.__name__}") def set_background_color(self, color: QColor) -> None: self.setStyleSheet( diff --git a/bitcoin_safe/gui/qt/qr_components/quick_receive.py b/bitcoin_safe/gui/qt/qr_components/quick_receive.py index 9e222af..793cd5d 100644 --- a/bitcoin_safe/gui/qt/qr_components/quick_receive.py +++ b/bitcoin_safe/gui/qt/qr_components/quick_receive.py @@ -27,6 +27,7 @@ # SOFTWARE. +import logging from typing import List from bitcoin_qr_tools.gui.qr_widgets import QRCodeWidgetSVG @@ -44,8 +45,11 @@ from bitcoin_safe.gui.qt.buttonedit import ButtonEdit from bitcoin_safe.gui.qt.custom_edits import AnalyzerTextEdit +from bitcoin_safe.signal_tracker import SignalTools, SignalTracker from bitcoin_safe.typestubs import TypedPyQtSignalNo +logger = logging.getLogger(__name__) + class TitledComponent(QWidget): def __init__(self, title, hex_color, parent=None) -> None: @@ -146,6 +150,7 @@ def resizeEvent(self, event: QResizeEvent | None) -> None: class QuickReceive(QWidget): def __init__(self, title="Quick Receive", parent=None) -> None: super().__init__(parent) + self.signal_tracker = SignalTracker() self.setSizePolicy( QSizePolicy.Policy.Preferred, # Horizontal size policy @@ -204,9 +209,16 @@ def remove_box(self) -> None: if self.group_boxes: group_box = self.group_boxes.pop() group_box.setParent(None) # type: ignore[call-overload] - group_box.deleteLater() self.content_widget.adjustSize() def clear_boxes(self) -> None: while self.group_boxes: self.remove_box() + + def close(self): + self.signal_tracker.disconnect_all() + SignalTools.disconnect_all_signals_from(self) + + self.clear_boxes() + self.setParent(None) + super().close() diff --git a/bitcoin_safe/gui/qt/qt_wallet.py b/bitcoin_safe/gui/qt/qt_wallet.py index 3f890f4..e02491e 100644 --- a/bitcoin_safe/gui/qt/qt_wallet.py +++ b/bitcoin_safe/gui/qt/qt_wallet.py @@ -34,11 +34,11 @@ import shutil from datetime import timedelta from pathlib import Path -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union +from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union import bdkpython as bdk from bitcoin_qr_tools.data import Data -from PyQt6.QtCore import Qt, QTimer, pyqtBoundSignal, pyqtSignal +from PyQt6.QtCore import Qt, QTimer, pyqtSignal from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import ( QApplication, @@ -52,28 +52,23 @@ QWidget, ) +from bitcoin_safe.category_info import CategoryInfo, SubtextType from bitcoin_safe.fx import FX -from bitcoin_safe.gui.qt.extended_tabwidget import ExtendedTabWidget from bitcoin_safe.gui.qt.label_syncer import LabelSyncer from bitcoin_safe.gui.qt.my_treeview import SearchableTab, TreeViewWithToolbar from bitcoin_safe.gui.qt.qt_wallet_base import QtWalletBase, SyncStatus from bitcoin_safe.gui.qt.sync_tab import SyncTab -from bitcoin_safe.pythonbdk_types import Balance +from bitcoin_safe.pythonbdk_types import Balance, python_utxo_balance +from bitcoin_safe.signal_tracker import SignalTools from bitcoin_safe.storage import BaseSaveableClass, filtered_for_init from bitcoin_safe.threading_manager import TaskThread, ThreadingManager from bitcoin_safe.typestubs import TypedPyQtSignal from bitcoin_safe.util import Satoshis from ...config import UserConfig -from ...execute_config import ENABLE_THREADING +from ...execute_config import ENABLE_THREADING, ENABLE_TIMERS from ...mempool import MempoolData -from ...signals import ( - Signals, - TypedPyQtSignalNo, - UpdateFilter, - UpdateFilterReason, - WalletSignals, -) +from ...signals import Signals, UpdateFilter, UpdateFilterReason, WalletSignals from ...tx import TxBuilderInfos, TxUiInfos, short_tx_id from ...wallet import ( DeltaCacheListTransactions, @@ -104,24 +99,24 @@ class QTProtoWallet(QtWalletBase): - signal_create_wallet: TypedPyQtSignalNo = pyqtSignal() # type: ignore - signal_close_wallet: TypedPyQtSignalNo = pyqtSignal() # type: ignore + signal_create_wallet: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore + signal_close_wallet: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore def __init__( self, protowallet: ProtoWallet, config: UserConfig, signals: Signals, - get_lang_code: Callable[[], str], threading_parent: ThreadingManager | None = None, tutorial_index: int | None = None, + parent=None, ) -> None: super().__init__( config=config, signals=signals, threading_parent=threading_parent, - get_lang_code=get_lang_code, tutorial_index=tutorial_index, + parent=parent, ) ( @@ -151,31 +146,39 @@ def create_and_add_settings_tab(self, protowallet: ProtoWallet) -> Tuple[QWidget protowallet=protowallet, signals_min=self.signals, threading_parent=self, - get_lang_code=self.get_lang_code, ) - self.tabs.add_tab( - tab=wallet_descriptor_ui.tab, - icon=read_QIcon("preferences.svg"), - description=self.tr("Setup wallet"), - data=wallet_descriptor_ui, + self.tabs.addTab( + wallet_descriptor_ui, + read_QIcon("preferences.svg"), + self.tr("Setup wallet"), ) wallet_descriptor_ui.signal_qtwallet_apply_setting_changes.connect(self.on_apply_setting_changes) - wallet_descriptor_ui.signal_qtwallet_cancel_wallet_creation.connect(self.signal_close_wallet.emit) - return wallet_descriptor_ui.tab, wallet_descriptor_ui + wallet_descriptor_ui.signal_qtwallet_cancel_wallet_creation.connect(self.on_cancel_wallet_creation) + return wallet_descriptor_ui, wallet_descriptor_ui + + def on_cancel_wallet_creation(self): + self.signal_close_wallet.emit(self.protowallet.id) def on_apply_setting_changes(self) -> None: try: self.wallet_descriptor_ui.set_protowallet_from_ui() except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") Message(str(e), type=MessageType.Error) return - self.signal_create_wallet.emit() + self.signal_create_wallet.emit(self.protowallet.id) def get_editable_protowallet(self) -> ProtoWallet: return self.protowallet + def close(self): + self.signal_tracker.disconnect_all() + SignalTools.disconnect_all_signals_from(self) + self.setParent(None) + super().close() + class ProgressSignal: def __init__(self, signal_settext_balance_label: TypedPyQtSignal[str]) -> None: @@ -203,27 +206,25 @@ def __init__( signals: Signals, mempool_data: MempoolData, fx: FX, - get_lang_code: Callable[[], str], sync_tab: SyncTab | None = None, - set_tab_widget_icon: Optional[Callable[[QWidget, QIcon, str | None], None]] = None, password: str | None = None, file_path: str | None = None, threading_parent: ThreadingManager | None = None, display_balance: Balance | None = None, notified_tx_ids: Iterable[str] | None = None, tutorial_index: int | None = None, + parent=None, ) -> None: super().__init__( signals=signals, config=config, threading_parent=threading_parent, - get_lang_code=get_lang_code, tutorial_index=tutorial_index, + parent=parent, ) self.mempool_data = mempool_data self.wallet = self.set_wallet(wallet) self.password = password - self.set_tab_widget_icon = set_tab_widget_icon self.fx = fx self._file_path = file_path self.sync_status: SyncStatus = SyncStatus.unknown @@ -251,14 +252,15 @@ def __init__( ( self.history_tab, self.history_list, - self.balance_plot, + self.wallet_balance_chart, self.history_tab_with_toolbar, ) = self._create_hist_tab(self.tabs) ( - self.addresses_tab, + self.address_tab, self.address_list, - self.address_list_tags, + self.address_tab_category_editor, + self.address_list_with_toolbar, ) = self._create_addresses_tab(self.tabs) self.send_tab, self.uitx_creator = self._create_send_tab(self.tabs) @@ -270,34 +272,34 @@ def __init__( self.sync_tab, self.sync_tab_widget, self.label_syncer = self.add_sync_tab() - self.create_status_bar(self.tab, self.outer_layout) + self.create_status_bar(self, self.outer_layout) self.update_status_visualization(self.sync_status) self.tabs.setCurrentIndex(0) self.address_list.signal_tag_dropped.connect(self.set_category) - self.address_list_tags.list_widget.signal_addresses_dropped.connect(self.set_category) - self.address_list_tags.delete_button.signal_addresses_dropped.connect(self.set_category) - self.address_list_tags.list_widget.signal_tag_deleted.connect(self.delete_category) - self.address_list_tags.list_widget.signal_tag_renamed.connect( - lambda old, new: self.rename_category(old, new) - ) + self.address_tab_category_editor.list_widget.signal_addresses_dropped.connect(self.set_category) + self.address_tab_category_editor.delete_button.signal_addresses_dropped.connect(self.set_category) + self.address_tab_category_editor.list_widget.signal_tag_deleted.connect(self.delete_category) + self.address_tab_category_editor.list_widget.signal_tag_renamed.connect(self.rename_category) self.updateUi() self.quick_receive.update_content(UpdateFilter(refresh_all=True)) #### connect signals - self.connect_signal(self.signal_on_change_sync_status, self.update_status_visualization) - self.connect_signal(self.signals.language_switch, self.updateUi) - self.connect_signal(self.wallet_signals.updated, self.signals.any_wallet_updated.emit) - self.connect_signal(self.wallet_signals.export_labels, self.export_labels) - self.connect_signal(self.wallet_signals.export_bip329_labels, self.export_bip329_labels) - self.connect_signal(self.wallet_signals.import_labels, self.import_labels) - self.connect_signal(self.wallet_signals.import_bip329_labels, self.import_bip329_labels) - self.connect_signal( - self.wallet_signals.import_electrum_wallet_labels, self.import_electrum_wallet_labels + # only signals, not member of [wallet_signals, wallet_signals] have to be tracked, + # all others I can connect automatically + self.signal_tracker.connect(self.signal_on_change_sync_status, self.update_status_visualization) + self.signal_tracker.connect(self.signals.language_switch, self.updateUi) + self.signal_tracker.connect(self.signal_on_change_sync_status, self.update_display_balance) + self.wallet_signals.updated.connect(self.signals.any_wallet_updated.emit) + self.wallet_signals.export_labels.connect(self.export_labels) + self.wallet_signals.export_bip329_labels.connect(self.export_bip329_labels) + self.wallet_signals.import_labels.connect(self.import_labels) + self.wallet_signals.import_bip329_labels.connect(self.import_bip329_labels) + self.wallet_signals.import_electrum_wallet_labels.connect( + self.import_electrum_wallet_labels, ) - self.connect_signal(self.signal_on_change_sync_status, self.update_display_balance) self._start_sync_retry_timer() self._start_sync_regularly_timer() @@ -321,8 +323,6 @@ def from_file( signals: Signals, mempool_data: MempoolData, fx: FX, - get_lang_code: Callable[[], str], - set_tab_widget_icon: Optional[Callable[[QWidget, QIcon, str | None], None]] = None, password: str | None = None, threading_parent: ThreadingManager | None = None, ) -> "QTWallet": @@ -336,8 +336,6 @@ def from_file( "signals": signals, "mempool_data": mempool_data, "fx": fx, - "get_lang_code": get_lang_code, - "set_tab_widget_icon": set_tab_widget_icon, "file_path": file_path, "threading_parent": threading_parent, }, @@ -410,7 +408,7 @@ def updateUi(self) -> None: self.tabs.setTabText(self.tabs.indexOf(self.wallet_descriptor_tab), self.tr("Descriptor")) self.tabs.setTabText(self.tabs.indexOf(self.sync_tab_widget), self.tr("Sync && Chat")) self.tabs.setTabText(self.tabs.indexOf(self.history_tab), self.tr("History")) - self.tabs.setTabText(self.tabs.indexOf(self.addresses_tab), self.tr("Receive")) + self.tabs.setTabText(self.tabs.indexOf(self.address_tab), self.tr("Receive")) def set_display_balance(self, value: Balance): self.display_balance = value @@ -429,64 +427,37 @@ def stop_sync_timer(self) -> None: self.timer_sync_retry.stop() self.timer_sync_regularly.stop() - def disconnect_signals(self) -> None: - def discon_sig(signal: pyqtBoundSignal): - """ - Disconnect only breaks one connection at a time, - so loop to be safe. - """ - while True: - try: - signal.disconnect() - except TypeError: - break - return - - super().disconnect_signals() - signals = self.wallet_signals - for signal_name in dir(signals): - signal = getattr(signals, signal_name) - if not isinstance(signal, pyqtBoundSignal): - continue - discon_sig(signal) - - def close(self) -> None: - self.disconnect_signals() - self.label_syncer.send_all_labels_to_myself() - self.sync_tab.unsubscribe_all() - self.sync_tab.nostr_sync.stop() - self.stop_sync_timer() - self.end_threading_manager() - def _start_sync_regularly_timer(self, delay_retry_sync=60) -> None: if self.timer_sync_regularly.isActive(): return self.timer_sync_regularly.setInterval(delay_retry_sync * 1000) - def sync() -> None: - if self.sync_status not in [SyncStatus.synced]: - return + self.timer_sync_regularly.timeout.connect(self._regular_sync) + if ENABLE_TIMERS: + self.timer_sync_regularly.start() + + def _regular_sync(self): + if self.sync_status not in [SyncStatus.synced]: + return + + logger.info(f"Regular update: Sync wallet {self.wallet.id} again") + self.sync() - logger.info(f"Regular update: Sync wallet {self.wallet.id} again") - self.sync() + def _sync_if_needed(self) -> None: + if self.sync_status in [SyncStatus.syncing, SyncStatus.synced]: + return - self.timer_sync_regularly.timeout.connect(sync) - self.timer_sync_regularly.start() + logger.info(f"Retry timer: Try syncing wallet {self.wallet.id}") + self.sync() def _start_sync_retry_timer(self, delay_retry_sync=30) -> None: if self.timer_sync_retry.isActive(): return self.timer_sync_retry.setInterval(delay_retry_sync * 1000) - def sync_if_needed() -> None: - if self.sync_status in [SyncStatus.syncing, SyncStatus.synced]: - return - - logger.info(f"Retry timer: Try syncing wallet {self.wallet.id}") - self.sync() - - self.timer_sync_retry.timeout.connect(sync_if_needed) - self.timer_sync_retry.start() + self.timer_sync_retry.timeout.connect(self._sync_if_needed) + if ENABLE_TIMERS: + self.timer_sync_retry.start() def get_mn_tuple(self) -> Tuple[int, int]: return self.wallet.get_mn_tuple() @@ -507,21 +478,20 @@ def create_and_add_settings_tab(self) -> Tuple[QWidget, DescriptorUI]: wallet_descriptor_ui = DescriptorUI( protowallet=self.wallet.as_protowallet(), signals_min=self.signals, - get_wallet=lambda: self.wallet, - get_lang_code=self.get_lang_code, + wallet=self.wallet, + threading_parent=self, ) - self.tabs.add_tab( - tab=wallet_descriptor_ui.tab, - icon=read_QIcon("preferences.svg"), - description="", - data=wallet_descriptor_ui, + self.tabs.addTab( + wallet_descriptor_ui, + read_QIcon("preferences.svg"), + "", ) wallet_descriptor_ui.signal_qtwallet_apply_setting_changes.connect( self.on_qtwallet_apply_setting_changes ) wallet_descriptor_ui.signal_qtwallet_cancel_setting_changes.connect(self.cancel_setting_changes) - return wallet_descriptor_ui.tab, wallet_descriptor_ui + return wallet_descriptor_ui, wallet_descriptor_ui def on_qtwallet_apply_setting_changes(self): # save old status, such that the backup has all old data (inlcuding the "SyncTab" in the data_dump) @@ -558,11 +528,10 @@ def on_qtwallet_apply_setting_changes(self): self.signals, self.mempool_data, self.fx, - set_tab_widget_icon=self.set_tab_widget_icon, file_path=self.file_path, password=self.password, threading_parent=self.threading_parent, - get_lang_code=self.get_lang_code, + parent=self, ) self.signals.close_qt_wallet.emit(self.wallet.id) @@ -572,18 +541,13 @@ def add_sync_tab(self) -> Tuple[SyncTab, QWidget, LabelSyncer]: "Create a wallet settings tab, such that one can create a wallet (e.g. with xpub)" icon_path = SyncTab.get_icon_path(enabled=self.sync_tab.enabled()) - self.tabs.add_tab( - tab=self.sync_tab.main_widget, icon=QIcon(icon_path), description="", data=self.sync_tab - ) + self.tabs.addTab(self.sync_tab.main_widget, QIcon(icon_path), "") self.sync_tab.main_widget.checkbox.stateChanged.connect(self._set_sync_tab_icon) label_syncer = LabelSyncer(self.wallet.labels, self.sync_tab, self.wallet_signals) self.sync_tab.finish_init_after_signal_connection() return self.sync_tab, self.sync_tab.main_widget, label_syncer - def __repr__(self) -> str: - return f"QTWallet({self.__dict__})" - def _set_sync_tab_icon(self, enabled: bool): index = self.tabs.indexOf(self.sync_tab.main_widget) if index < 0: @@ -665,7 +629,7 @@ def save(self) -> Optional[str]: # type: ignore # opportunity to set a filename while not self._file_path: self._file_path, _ = QFileDialog.getSaveFileName( - self.tab, + self, self.tr("Save wallet"), f"{os.path.join(self.config.wallet_dir, filename_clean(self.wallet.id))}", self.tr("All Files (*);;Wallet Files (*.wallet)"), @@ -676,7 +640,7 @@ def save(self) -> Optional[str]: # type: ignore ), title=self.tr("Delete wallet"), ): - logger.info("No file selected") + logger.info(self.tr("No file selected")) return None # if it is the first time saving, then the user can set a password @@ -745,7 +709,7 @@ def hanlde_removed_txs(self, removed_txs: List[bdk.TransactionDetails]) -> None: message_content + "\n" + self.tr("Do you want to save a copy of these transactions?") ): folder_path = QFileDialog.getExistingDirectory( - self.tab, "Select Folder to save the removed transactions" + self, "Select Folder to save the removed transactions" ) if folder_path: @@ -845,7 +809,7 @@ def _create_send_tab(self, tabs: QTabWidget) -> Tuple[SearchableTab, UITx_Creato utxo_list = UTXOList( self.config, self.signals, - get_outpoints=lambda: [], # this is filled in uitx_creator + outpoints=[], hidden_columns=[ UTXOList.Columns.OUTPOINT, UTXOList.Columns.PARENTS, @@ -866,9 +830,9 @@ def _create_send_tab(self, tabs: QTabWidget) -> Tuple[SearchableTab, UITx_Creato utxo_list=utxo_list, config=self.config, signals=self.signals, - parent=self.tab, + parent=self, ) - self.tabs.add_tab(tab=uitx_creator, icon=read_QIcon("send.svg"), description="", data=uitx_creator) + self.tabs.addTab(uitx_creator, read_QIcon("send.svg"), "") uitx_creator.signal_create_tx.connect(self.create_psbt) @@ -880,6 +844,7 @@ def do() -> Union[TxBuilderInfos, Exception]: try: return self.wallet.create_psbt(txinfos) except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") return e def on_done(builder_infos: Union[TxBuilderInfos, Exception]) -> None: @@ -926,15 +891,27 @@ def on_error(packed_error_info) -> None: self.append_thread(TaskThread().add_and_start(do, on_success, on_done, on_error)) + def get_wallet(self) -> Wallet: + return self.wallet + + def get_qt_wallet(self) -> "QTWallet": + return self + def set_wallet(self, wallet: Wallet) -> Wallet: self.wallet = wallet - self.connect_signal(self.wallet_signals.updated, self.wallet.on_addresses_updated) - - self.connect_signal(self.signals.get_wallets, lambda: self.wallet, slot_name=self.wallet.id) - self.connect_signal(self.signals.get_qt_wallets, lambda: self, slot_name=self.wallet.id) - self.connect_signal( - self.wallet_signals.get_display_balance, self.get_display_balance, slot_name=self.wallet.id + self.wallet_signals.updated.connect(self.wallet.on_addresses_updated) + self.signal_tracker.connect(self.signals.get_wallets, self.get_wallet, slot_name=self.wallet.id) + self.signal_tracker.connect(self.signals.get_qt_wallets, self.get_qt_wallet, slot_name=self.wallet.id) + self.signal_tracker.connect( + self.wallet_signals.get_display_balance, + self.get_display_balance, + slot_name=self.wallet.id, + ) + self.signal_tracker.connect( + self.wallet_signals.get_category_infos, + self.get_category_infos, + slot_name=self.wallet.id, ) return wallet @@ -952,7 +929,7 @@ def rename_category(self, old_category: str, new_category: str) -> None: def delete_category(self, category: str) -> None: # Show dialog and get input text, ok = QInputDialog.getText( - self.tab, + self, self.tr("Rename or merge categories"), self.tr("Choose a new name, or an existing name for merging:"), text=category, @@ -998,35 +975,30 @@ def update_status_visualization(self, sync_status: SyncStatus) -> None: if not self.wallet: return - icon = None - tooltip = None + icon_text = "" + tooltip = "" if sync_status == SyncStatus.syncing: - icon = read_QIcon("status_waiting.svg") + icon_text = "status_waiting.svg" self.history_tab_with_toolbar.sync_button.set_icon_is_syncing() tooltip = self.tr("Syncing with {server}").format( server=self.config.network_config.description_short() ) elif self.wallet.get_height() and sync_status in [SyncStatus.synced]: using_proxy = self.config.network_config.proxy_url - icon = ( - read_QIcon("status_connected_proxy.svg") - if using_proxy - else read_QIcon("status_connected.svg") - ) + icon_text = ("status_connected_proxy.svg") if using_proxy else ("status_connected.svg") tooltip = self.config.network_config.description_short() tooltip = self.tr("Connected to {server}").format( server=self.config.network_config.description_short() ) self.history_tab_with_toolbar.sync_button.set_icon_allow_refresh() else: - icon = read_QIcon("status_disconnected.svg") + icon_text = "status_disconnected.svg" tooltip = self.tr("Disconnected from {server}").format( server=self.config.network_config.description_short() ) self.history_tab_with_toolbar.sync_button.set_icon_allow_refresh() - if self.set_tab_widget_icon: - self.set_tab_widget_icon(self.tab, icon, tooltip) + self.signals.signal_set_tab_properties.emit(self.wallet.id, icon_text, tooltip) def create_list_tab( self, @@ -1037,6 +1009,7 @@ def create_list_tab( ) -> SearchableTab: # create a horizontal widget and layout searchable_tab = SearchableTab(parent=tabs) + searchable_tab.setObjectName(f"created for {treeview_with_toolbar.__class__.__name__}") searchable_tab.searchable_list = treeview_with_toolbar.searchable_list searchable_tab_layout = QHBoxLayout(searchable_tab) searchable_tab.setLayout(searchable_tab_layout) @@ -1053,9 +1026,10 @@ def create_list_tab( return searchable_tab def _create_hist_tab( - self, tabs: ExtendedTabWidget + self, tabs: QTabWidget ) -> Tuple[SearchableTab, HistList, WalletBalanceChart, HistListWithToolbar]: tab = SearchableTab(parent=tabs) + tab.setObjectName(f"created as HistList tab containrer") tab_layout = QHBoxLayout(tab) tab_layout.setContentsMargins(0, 0, 0, 0) # Left, Top, Right, Bottom margins @@ -1089,38 +1063,44 @@ def _create_hist_tab( ) right_widget_layout.addWidget(self.quick_receive) - plot = WalletBalanceChart(self.wallet, wallet_signals=self.wallet_signals, parent=right_widget) - right_widget_layout.addWidget(plot) + wallet_balance_chart = WalletBalanceChart( + self.wallet, wallet_signals=self.wallet_signals, parent=right_widget + ) + right_widget_layout.addWidget(wallet_balance_chart) splitter1.addWidget(right_widget) - tabs.add_tab( - tab=tab, - icon=read_QIcon("history.svg"), - description="", - position=2, - data=[treeview_with_toolbar, plot], + tabs.insertTab( + 2, + tab, + read_QIcon("history.svg"), + "", ) splitter1.setSizes([1, 1]) - return tab, l, plot, treeview_with_toolbar + return tab, l, wallet_balance_chart, treeview_with_toolbar - def _subtexts_for_categories(self) -> List[str]: - return [self.tr("Click for new address")] * len(self.wallet.labels.categories) + def get_category_infos(self) -> List[CategoryInfo]: - # d = {} - # for address in self.wallet.get_addresses(): - # category = self.wallet.labels.get_category(address) - # if category not in d: - # d[category] = [] + category_python_utxo_dict = self.wallet.get_category_python_utxo_dict() - # d[category].append(address) - - # return [f"{len(d.get(category, []))} Addresses" for category in self.wallet.labels.categories] + return [ + CategoryInfo( + category=category, + text_click_new_address=self.tr("Click for new address"), + text_balance=self.tr("{num_inputs} Inputs: {inputs}").format( + num_inputs=len(category_python_utxo_dict.get(category, [])), + inputs=Satoshis( + python_utxo_balance(category_python_utxo_dict.get(category, [])), self.wallet.network + ).str_with_unit(), + ), + ) + for category in self.wallet.labels.categories + ] def _create_addresses_tab( - self, tabs: ExtendedTabWidget - ) -> Tuple[SearchableTab, AddressList, CategoryEditor]: + self, tabs: QTabWidget + ) -> Tuple[SearchableTab, AddressList, CategoryEditor, AddressListWithToolbar]: l = AddressList( fx=self.fx, config=self.config, @@ -1130,25 +1110,23 @@ def _create_addresses_tab( ) treeview_with_toolbar = AddressListWithToolbar(l, self.config, parent=tabs, signals=self.signals) - tags = CategoryEditor( - lambda: self.wallet.labels.categories, - self.wallet_signals, - get_sub_texts=self._subtexts_for_categories, + address_tab_category_editor = CategoryEditor( + self.wallet_signals, subtext_type=SubtextType.click_new_address ) - tags.signal_category_added.connect(self.on_add_category) + address_tab_category_editor.signal_category_added.connect(self.on_add_category) - def create_new_address(category) -> None: - self.address_list.get_address(force_new=True, category=category) + address_tab_category_editor.list_widget.signal_tag_clicked.connect(self.create_new_address) - tags.list_widget.signal_tag_clicked.connect(create_new_address) + address_tab_category_editor.setMaximumWidth(150) + tab = self.create_list_tab( + treeview_with_toolbar, tabs, horizontal_widgets_left=[address_tab_category_editor] + ) - tags.setMaximumWidth(150) - tab = self.create_list_tab(treeview_with_toolbar, tabs, horizontal_widgets_left=[tags]) + tabs.insertTab(1, tab, read_QIcon("receive.svg"), "") + return tab, l, address_tab_category_editor, treeview_with_toolbar - tabs.add_tab( - tab=tab, icon=read_QIcon("receive.svg"), description="", position=1, data=treeview_with_toolbar - ) - return tab, l, tags + def create_new_address(self, category) -> None: + self.address_list.get_address(force_new=True, category=category) def on_add_category(self, category: str) -> None: self.wallet.labels.add_category(category) @@ -1159,49 +1137,47 @@ def set_sync_status(self, new: SyncStatus) -> None: self.signal_on_change_sync_status.emit(new) QApplication.processEvents() - def sync(self) -> None: - if self.sync_status == SyncStatus.syncing: - logger.info(f"Syncing already in progress") - return - - def do() -> Any: - self.wallet.sync(progress=ProgressSignal(self.signal_settext_balance_label)) + def _sync(self) -> Any: + self.wallet.sync(progress=ProgressSignal(self.signal_settext_balance_label)) - def on_done(result) -> None: - self._syncing_delay = datetime.datetime.now() - self._last_syncing_start - interval_timer_sync_regularly = max(int(self._syncing_delay.total_seconds() * 200), 30) # in sec - self.timer_sync_regularly.setInterval(interval_timer_sync_regularly * 1000) - logger.info( - f"Syncing took {self._syncing_delay} --> set the interval_timer_sync_regularly to {interval_timer_sync_regularly}s" - ) + def _sync_on_done(self, result) -> None: + self._syncing_delay = datetime.datetime.now() - self._last_syncing_start + interval_timer_sync_regularly = max(int(self._syncing_delay.total_seconds() * 200), 30) # in sec + self.timer_sync_regularly.setInterval(interval_timer_sync_regularly * 1000) + logger.info( + f"Syncing took {self._syncing_delay} --> set the interval_timer_sync_regularly to {interval_timer_sync_regularly}s" + ) - def on_error(packed_error_info) -> None: - self.set_sync_status(SyncStatus.error) - logger.info( - f"Could not sync. SynStatus set to {SyncStatus.error.name} for wallet {self.wallet.id}" - ) - logger.error(str(packed_error_info)) - # custom_exception_handler(*packed_error_info) + def _sync_on_error(self, packed_error_info) -> None: + self.set_sync_status(SyncStatus.error) + logger.info(f"Could not sync. SynStatus set to {SyncStatus.error.name} for wallet {self.wallet.id}") + logger.error(str(packed_error_info)) + # custom_exception_handler(*packed_error_info) + + def _sync_on_success(self, result) -> None: + self.set_sync_status(SyncStatus.synced) + logger.info(f"success syncing wallet '{self.wallet.id}'") + + logger.info(self.tr("start updating lists")) + new_chain_height = self.wallet.get_height_no_cache() + # self.wallet.clear_cache() + self.refresh_caches_and_ui_lists( + force_ui_refresh=False, + chain_height_advanced=new_chain_height != self._last_sync_chain_height, + ) + # self.update_tabs() + logger.info(self.tr("finished updating lists")) + self._last_sync_chain_height = new_chain_height - def on_success(result) -> None: - self.set_sync_status(SyncStatus.synced) - logger.info(f"success syncing wallet '{self.wallet.id}'") - - logger.info("start updating lists") - new_chain_height = self.wallet.get_height_no_cache() - # self.wallet.clear_cache() - self.refresh_caches_and_ui_lists( - force_ui_refresh=False, - chain_height_advanced=new_chain_height != self._last_sync_chain_height, - ) - # self.update_tabs() - logger.info("finished updating lists") - self._last_sync_chain_height = new_chain_height + self.fx.update_if_needed() + self.signal_after_sync.emit(self.sync_status) - self.fx.update_if_needed() - self.signal_after_sync.emit(self.sync_status) + def sync(self) -> None: + if self.sync_status == SyncStatus.syncing: + logger.info(f"Syncing already in progress") + return - logger.info(f"Refresh all caches before syncing.") + logger.info(self.tr(f"Refresh all caches before syncing.")) # This takkles the following problem: # During the syncing process the cache and the bdk results # become inconsitent (since the bdk has newer info) @@ -1221,7 +1197,11 @@ def on_success(result) -> None: self.set_sync_status(SyncStatus.syncing) self._last_syncing_start = datetime.datetime.now() - self.append_thread(TaskThread().add_and_start(do, on_success, on_done, on_error)) + self.append_thread( + TaskThread().add_and_start( + self._sync, self._sync_on_success, self._sync_on_done, self._sync_on_error + ) + ) def get_editable_protowallet(self) -> ProtoWallet: return self.wallet.as_protowallet() @@ -1229,13 +1209,13 @@ def get_editable_protowallet(self) -> ProtoWallet: def export_bip329_labels(self) -> None: s = self.wallet.labels.export_bip329_jsonlines() file_path, _ = QFileDialog.getSaveFileName( - self.tab, + self, self.tr("Export labels"), f"{self.wallet.id}_labels.jsonl", self.tr("All Files (*);;JSON Files (*.jsonl);;JSON Files (*.json)"), ) if not file_path: - logger.info("No file selected") + logger.info(self.tr("No file selected")) return with open(file_path, "w") as file: @@ -1244,13 +1224,13 @@ def export_bip329_labels(self) -> None: def export_labels(self) -> None: s = self.wallet.labels.dumps_data_jsonlines() file_path, _ = QFileDialog.getSaveFileName( - self.tab, + self, self.tr("Export labels"), f"{self.wallet.id}_labels.jsonl", self.tr("All Files (*);;JSON Files (*.jsonl);;JSON Files (*.json)"), ) if not file_path: - logger.info("No file selected") + logger.info(self.tr("No file selected")) return with open(file_path, "w") as file: @@ -1258,13 +1238,13 @@ def export_labels(self) -> None: def import_bip329_labels(self) -> None: file_path, _ = QFileDialog.getOpenFileName( - self.tab, + self, self.tr("Import labels"), "", self.tr("All Files (*);;JSONL Files (*.jsonl);;JSON Files (*.json)"), ) if not file_path: - logger.info("No file selected") + logger.info(self.tr("No file selected")) return with open(file_path, "r") as file: @@ -1279,13 +1259,13 @@ def import_bip329_labels(self) -> None: def import_labels(self) -> None: file_path, _ = QFileDialog.getOpenFileName( - self.tab, + self, self.tr("Import labels"), "", self.tr("All Files (*);;JSONL Files (*.jsonl);;JSON Files (*.json)"), ) if not file_path: - logger.info("No file selected") + logger.info(self.tr("No file selected")) return with open(file_path, "r") as file: @@ -1308,13 +1288,13 @@ def import_labels(self) -> None: def import_electrum_wallet_labels(self) -> None: file_path, _ = QFileDialog.getOpenFileName( - self.tab, + self, self.tr("Import Electrum Wallet labels"), "", self.tr("All Files (*);;JSON Files (*.json)"), ) if not file_path: - logger.info("No file selected") + logger.info(self.tr("No file selected")) return with open(file_path, "r") as file: @@ -1326,3 +1306,28 @@ def import_electrum_wallet_labels(self) -> None: self.tr("Successfully updated {number} Labels").format(number=len(changed_data)), type=MessageType.Info, ) + + def close(self) -> bool: + # crucial is to explicitly close everything that has a wallet attached + self.stop_sync_timer() + self.address_tab_category_editor.close() + self.quick_receive.close() + self.address_tab.close() + self.address_list_with_toolbar.close() + self.history_tab.close() + self.history_tab.searchable_list = None + self.history_tab_with_toolbar.close() + self.wallet_descriptor_tab.close() + self.wallet_descriptor_ui.close() + self.send_tab.close() + self.uitx_creator.close() + self.wallet_balance_chart.close() + self.label_syncer.send_all_labels_to_myself() + self.sync_tab.unsubscribe_all() + self.sync_tab.nostr_sync.stop() + self.tabs.clear() + self.tabs.close() + self.wallet.close() + SignalTools.disconnect_all_signals_from(self.wallet_signals) + self.setParent(None) # THIS made it that the qt wallet is destroyed + return super().close() diff --git a/bitcoin_safe/gui/qt/qt_wallet_base.py b/bitcoin_safe/gui/qt/qt_wallet_base.py index 358700d..ace5268 100644 --- a/bitcoin_safe/gui/qt/qt_wallet_base.py +++ b/bitcoin_safe/gui/qt/qt_wallet_base.py @@ -30,14 +30,13 @@ import enum import logging from abc import abstractmethod -from typing import Callable, List, Tuple +from typing import List, Tuple from PyQt6.QtCore import pyqtSignal -from PyQt6.QtWidgets import QVBoxLayout, QWidget +from PyQt6.QtWidgets import QTabWidget, QVBoxLayout, QWidget -from bitcoin_safe.gui.qt.extended_tabwidget import ExtendedTabWidget -from bitcoin_safe.gui.qt.signal_carrying_object import SignalCarryingObject from bitcoin_safe.gui.qt.wizard_base import WizardBase +from bitcoin_safe.signal_tracker import SignalTools, SignalTracker from bitcoin_safe.threading_manager import ThreadingManager from bitcoin_safe.typestubs import TypedPyQtSignal @@ -56,38 +55,40 @@ class SyncStatus(enum.Enum): error = enum.auto() -class QtWalletBase(SignalCarryingObject, ThreadingManager): +class WrapperQWidget(QWidget): + def __init__(self, parent=None, **kwargs) -> None: + super().__init__(parent, **kwargs) + + +class QtWalletBase(WrapperQWidget, ThreadingManager): signal_after_sync: TypedPyQtSignal[SyncStatus] = pyqtSignal(SyncStatus) # type: ignore # SyncStatus - wizard: WizardBase + wizard: WizardBase | None = None wallet_descriptor_tab: QWidget def __init__( self, config: UserConfig, signals: Signals, - get_lang_code: Callable[[], str], threading_parent: ThreadingManager | None = None, tutorial_index: int | None = None, - **kwargs + parent=None, + **kwargs, ) -> None: - super().__init__(threading_parent=threading_parent, **kwargs) - self.get_lang_code = get_lang_code - if threading_parent: - self.threading_parent = threading_parent + super().__init__(threading_parent=threading_parent, parent=parent, **kwargs) + self.signal_tracker = SignalTracker() + self.config = config self.signals = signals self.tutorial_index = tutorial_index - self.tab = QWidget() - - self.outer_layout = QVBoxLayout(self.tab) + self.outer_layout = QVBoxLayout(self) current_margins = self.outer_layout.contentsMargins() self.outer_layout.setContentsMargins( 0, current_margins.top() // 2, 0, 0 ) # Left, Top, Right, Bottom margins # add the tab_widget for history, utx, send tabs - self.tabs = ExtendedTabWidget(object, parent=self.tab) + self.tabs = QTabWidget(self) self.outer_layout.addWidget(self.tabs) @@ -105,4 +106,15 @@ def get_editable_protowallet(self) -> ProtoWallet: def set_tutorial_index(self, value: int | None): self.tutorial_index = value - self.wizard.set_visibilities() + if self.wizard: + self.wizard.set_visibilities() + + def close(self) -> bool: + SignalTools.disconnect_all_signals_from(self) + self.signal_tracker.disconnect_all() + self.end_threading_manager() + if self.wizard: + self.wizard.close() + self.wizard = None + self.setParent(None) + return super().close() diff --git a/bitcoin_safe/gui/qt/recipients.py b/bitcoin_safe/gui/qt/recipients.py index 420e167..3726ed8 100644 --- a/bitcoin_safe/gui/qt/recipients.py +++ b/bitcoin_safe/gui/qt/recipients.py @@ -30,21 +30,6 @@ import csv import logging from pathlib import Path - -from bitcoin_safe.gui.qt.address_edit import AddressEdit -from bitcoin_safe.gui.qt.analyzers import AmountAnalyzer -from bitcoin_safe.gui.qt.labeledit import WalletLabelAndCategoryEdit -from bitcoin_safe.gui.qt.util import Message, MessageType, read_QIcon -from bitcoin_safe.gui.qt.wrappers import Menu -from bitcoin_safe.labels import LabelType -from bitcoin_safe.typestubs import TypedPyQtSignal -from bitcoin_safe.wallet import get_wallet_of_address - -from ...pythonbdk_types import Recipient, is_address -from .invisible_scroll_area import InvisibleScrollArea - -logger = logging.getLogger(__name__) - from typing import Any, List import bdkpython as bdk @@ -68,10 +53,23 @@ QWidget, ) +from bitcoin_safe.gui.qt.address_edit import AddressEdit +from bitcoin_safe.gui.qt.analyzers import AmountAnalyzer +from bitcoin_safe.gui.qt.labeledit import WalletLabelAndCategoryEdit +from bitcoin_safe.gui.qt.util import Message, MessageType, read_QIcon +from bitcoin_safe.gui.qt.wrappers import Menu +from bitcoin_safe.labels import LabelType +from bitcoin_safe.typestubs import TypedPyQtSignal +from bitcoin_safe.wallet import get_wallet_of_address + +from ...pythonbdk_types import Recipient, is_address from ...signals import Signals from ...util import is_int, unit_sat_str, unit_str +from .invisible_scroll_area import InvisibleScrollArea from .spinbox import BTCSpinBox +logger = logging.getLogger(__name__) + class CloseButton(QPushButton): def __init__(self, parent=None) -> None: @@ -261,6 +259,8 @@ def enabled(self, state: bool) -> None: class RecipientTabWidget(QTabWidget): signal_close: "TypedPyQtSignal[RecipientTabWidget]" = pyqtSignal(QTabWidget) # type: ignore + signal_clicked_send_max_button: "TypedPyQtSignal[RecipientWidget]" = pyqtSignal(RecipientWidget) # type: ignore + signal_amount_changed: "TypedPyQtSignal[RecipientWidget]" = pyqtSignal(RecipientWidget) # type: ignore def __init__( self, @@ -281,12 +281,25 @@ def __init__( allow_edit=allow_edit, parent=self, ) + self.addTab(self.recipient_widget, read_QIcon("person.svg"), title) - self.tabCloseRequested.connect(lambda: self.signal_close.emit(self)) + # connect signals + self.tabCloseRequested.connect(self.on_tabCloseRequested) + self.recipient_widget.amount_spin_box.valueChanged.connect(self.on_amount_spin_box_changed) + self.recipient_widget.send_max_button.clicked.connect(self.on_send_max_button) self.recipient_widget.address_edit.signal_text_change.connect(self.autofill_tab_text) + def on_tabCloseRequested(self): + self.signal_close.emit(self) + + def on_send_max_button(self): + self.signal_clicked_send_max_button.emit(self.recipient_widget) + + def on_amount_spin_box_changed(self): + self.signal_amount_changed.emit(self.recipient_widget) + def set_allow_edit(self, allow_edit: bool): self.recipient_widget.set_allow_edit(allow_edit) self.setTabsClosable(allow_edit) @@ -359,8 +372,8 @@ def autofill_tab_text(self, *args): class Recipients(QWidget): signal_added_recipient: TypedPyQtSignal[RecipientTabWidget] = pyqtSignal(RecipientTabWidget) # type: ignore signal_removed_recipient: TypedPyQtSignal[RecipientTabWidget] = pyqtSignal(RecipientTabWidget) # type: ignore - signal_clicked_send_max_button: TypedPyQtSignal[RecipientTabWidget] = pyqtSignal(RecipientTabWidget) # type: ignore - signal_amount_changed: TypedPyQtSignal[RecipientTabWidget] = pyqtSignal(RecipientTabWidget) # type: ignore + signal_clicked_send_max_button: TypedPyQtSignal[RecipientWidget] = pyqtSignal(RecipientWidget) # type: ignore + signal_amount_changed: TypedPyQtSignal[RecipientWidget] = pyqtSignal(RecipientWidget) # type: ignore def __init__(self, signals: Signals, network: bdk.Network, allow_edit=True) -> None: super().__init__() @@ -393,12 +406,12 @@ def __init__(self, signals: Signals, network: bdk.Network, allow_edit=True) -> N menu = Menu(self) self.action_export_csv_template = menu.add_action( - "", lambda: self.export_csv([]), icon=read_QIcon("csv-file.svg") + "", self.on_action_export_csv_template, icon=read_QIcon("csv-file.svg") ) self.action_import_csv = menu.add_action("", self.import_csv, icon=read_QIcon("csv-file.svg")) menu.addSeparator() self.action_export_csv = menu.add_action( - "", lambda: self.export_csv(self.recipients), icon=read_QIcon("csv-file.svg") + "", self.on_action_export_csv, icon=read_QIcon("csv-file.svg") ) self.toolbutton_csv.setMenu(menu) @@ -446,6 +459,12 @@ def _get_csv_header(self) -> List[str]: self.tr("Label"), ] + def on_action_export_csv_template(self): + self.export_csv([]) + + def on_action_export_csv(self): + self.export_csv(self.recipients) + def export_csv(self, recipients: List[Recipient], file_path: str | Path | None = None) -> Path | None: if not file_path: @@ -456,7 +475,7 @@ def export_csv(self, recipients: List[Recipient], file_path: str | Path | None = self.tr("All Files (*);;Wallet Files (*.csv)"), ) if not file_path: - logger.info("No file selected") + logger.info(self.tr("No file selected")) return None table = self.as_list(recipients) @@ -477,7 +496,7 @@ def import_csv(self, file_path: str | None = None): self.tr("All Files (*);;CSV (*.csv)"), ) if not file_path: - logger.info("No file selected") + logger.info(self.tr("No file selected")) return with open(file_path, "r") as file: @@ -538,10 +557,6 @@ def add_recipient(self, recipient: Recipient | None = None) -> RecipientTabWidge recipient_box.recipient_widget.set_max(recipient.checked_max_amount) if recipient.label is not None: recipient_box.label = recipient.label - recipient_box.signal_close.connect(self.ui_remove_recipient_widget) - recipient_box.recipient_widget.amount_spin_box.valueChanged.connect( - lambda *args: self.signal_amount_changed.emit(recipient_box) - ) # insert before the button position def insert_before_button(new_widget: QWidget) -> None: @@ -553,9 +568,8 @@ def insert_before_button(new_widget: QWidget) -> None: insert_before_button(recipient_box) - recipient_box.recipient_widget.send_max_button.clicked.connect( - lambda: self.signal_clicked_send_max_button.emit(recipient_box) - ) + recipient_box.signal_close.connect(self.ui_remove_recipient_widget) + recipient_box.signal_clicked_send_max_button.connect(self.signal_amount_changed.emit) self.signal_added_recipient.emit(recipient_box) return recipient_box diff --git a/bitcoin_safe/gui/qt/register_multisig.py b/bitcoin_safe/gui/qt/register_multisig.py index e8f7b48..70e6bb9 100644 --- a/bitcoin_safe/gui/qt/register_multisig.py +++ b/bitcoin_safe/gui/qt/register_multisig.py @@ -56,7 +56,6 @@ def __init__( super().__init__(parent=parent) self.setWindowTitle(self.tr("Register Multisig")) self.qt_wallet = qt_wallet - self.threading_parent = threading_parent # export widgets self.export_qr_widget = None @@ -72,7 +71,7 @@ def __init__( enable_file=False, enable_qr=True, network=self.qt_wallet.config.network, - threading_parent=self.threading_parent, + threading_parent=threading_parent, wallet_name=self.qt_wallet.wallet.id, ) self.export_qr_widget.set_minimum_size_as_floating_window() @@ -93,7 +92,7 @@ def __init__( data=data, signals_min=self.qt_wallet.signals, network=self.qt_wallet.wallet.network, - threading_parent=self.threading_parent, + threading_parent=threading_parent, parent=self, ) self.add_button(self.export_qr_button) @@ -112,19 +111,21 @@ def __init__( addresses = self.qt_wallet.wallet.get_addresses() index = 0 address = addresses[index] if len(addresses) > index else "" - usb_widget = USBRegisterMultisigWidget( + self.usb_widget = USBRegisterMultisigWidget( network=self.qt_wallet.wallet.network, signals=self.qt_wallet.signals, ) - usb_widget.set_descriptor( + self.usb_widget.set_descriptor( keystores=self.qt_wallet.wallet.keystores, descriptor=self.qt_wallet.wallet.multipath_descriptor, expected_address=address, kind=bdk.KeychainKind.EXTERNAL, address_index=index, ) - button_hwi = self.add_hwi_button(signal_end_hwi_blocker=usb_widget.usb_gui.signal_end_hwi_blocker) - button_hwi.clicked.connect(lambda: usb_widget.show()) + button_hwi = self.add_hwi_button( + signal_end_hwi_blocker=self.usb_widget.usb_gui.signal_end_hwi_blocker + ) + button_hwi.clicked.connect(self.usb_widget.show) self.updateUi() diff --git a/bitcoin_safe/gui/qt/sankey_widget.py b/bitcoin_safe/gui/qt/sankey_widget.py index e2deef7..4b3fd29 100644 --- a/bitcoin_safe/gui/qt/sankey_widget.py +++ b/bitcoin_safe/gui/qt/sankey_widget.py @@ -411,7 +411,7 @@ def export_to_svg(self) -> None: self, self.tr("Export svg"), "", self.tr("All Files (*);;Text Files (*.svg)") ) if not file_path: - logger.info("No file selected") + logger.info(self.tr("No file selected")) return self._export_to_svg(Path(file_path)) diff --git a/bitcoin_safe/gui/qt/search_tree_view.py b/bitcoin_safe/gui/qt/search_tree_view.py index 3c740a7..c757283 100644 --- a/bitcoin_safe/gui/qt/search_tree_view.py +++ b/bitcoin_safe/gui/qt/search_tree_view.py @@ -28,13 +28,6 @@ import logging - -from bitcoin_safe.html_utils import html_f -from bitcoin_safe.i18n import translate -from bitcoin_safe.signals import SignalsMin - -logger = logging.getLogger(__name__) - import sys from typing import Callable, List, Optional @@ -65,6 +58,11 @@ from bitcoin_safe.gui.qt.my_treeview import MyTreeView, SearchableTab from bitcoin_safe.gui.qt.qt_wallet import QTWallet from bitcoin_safe.gui.qt.ui_tx import UITx_Creator +from bitcoin_safe.html_utils import html_f +from bitcoin_safe.i18n import translate +from bitcoin_safe.signals import Signals + +logger = logging.getLogger(__name__) class SearchHTMLDelegate(QStyledItemDelegate): @@ -355,8 +353,7 @@ def eventFilter(self, obj: QObject, event: QEvent) -> bool: # type: ignore[over class SearchWallets(SearchTreeView): def __init__( self, - get_qt_wallets: Callable[[], List[QTWallet]], - signal_min: SignalsMin, + signals: Signals, parent=None, result_height=300, result_width=500, @@ -369,10 +366,8 @@ def __init__( result_height=result_height, result_width=result_width, ) - self.signal_min = signal_min - - self.get_qt_wallets = get_qt_wallets - self.signal_min.language_switch.connect(self.updateUi) + self.signals = signals + self.signals.language_switch.connect(self.updateUi) def search_result_on_click(self, result_item: ResultItem) -> None: # call the parent action first @@ -395,11 +390,11 @@ def search_result_on_click(self, result_item: ResultItem) -> None: these_tabs.setCurrentWidget(result_item.obj) result_item.obj.tabs_inputs.setCurrentWidget(result_item.obj.tab_inputs_utxos) elif isinstance(result_item.obj, QTWallet): - parent = result_item.obj.tab.parent() + parent = result_item.obj.parent() if parent: wallet_tabs = parent.parent() if isinstance(wallet_tabs, QTabWidget): - wallet_tabs.setCurrentWidget(result_item.obj.tab) + wallet_tabs.setCurrentWidget(result_item.obj) def do_search(self, search_text: str) -> ResultItem: def format_result_text(matching_string: str) -> str: @@ -409,10 +404,11 @@ def format_result_text(matching_string: str) -> str: search_text = search_text.strip() root = ResultItem("") - for qt_wallet in self.get_qt_wallets(): + qt_wallets: List[QTWallet] = list(self.signals.get_qt_wallets.emit().values()) + for qt_wallet in qt_wallets: wallet_item = ResultItem(html_f(qt_wallet.wallet.id, bf=True), obj=qt_wallet) - wallet_addresses = ResultItem(html_f(self.tr("Addresses"), bf=True), obj=qt_wallet.addresses_tab) + wallet_addresses = ResultItem(html_f(self.tr("Addresses"), bf=True), obj=qt_wallet.address_tab) for address in qt_wallet.wallet.get_addresses(): if search_text in address: ResultItem( diff --git a/bitcoin_safe/gui/qt/spinning_button.py b/bitcoin_safe/gui/qt/spinning_button.py index f5f10e8..51cc122 100644 --- a/bitcoin_safe/gui/qt/spinning_button.py +++ b/bitcoin_safe/gui/qt/spinning_button.py @@ -34,6 +34,7 @@ from PyQt6.QtSvg import QSvgRenderer from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget +from bitcoin_safe.execute_config import ENABLE_TIMERS from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo from .util import icon_path @@ -75,7 +76,8 @@ def on_clicked(self) -> None: self.setIcon(QIcon()) self.start_spin() self.setDisabled(True) - self.timeout_timer.start(self.timeout * 1000) + if ENABLE_TIMERS: + self.timeout_timer.start(self.timeout * 1000) def enable_button(self, *args, **kwargs) -> None: self.stop_spin() @@ -90,7 +92,8 @@ def set_enable_signal(self, enable_signal: TypedPyQtSignal | TypedPyQtSignalNo) def start_spin(self) -> None: # Timer to update rotation self.timer.timeout.connect(self.rotate_svg) - self.timer.start(100) # Update rotation every 100 ms + if ENABLE_TIMERS: + self.timer.start(100) # Update rotation every 100 ms def stop_spin(self) -> None: self.timer.stop() diff --git a/bitcoin_safe/gui/qt/step_progress_bar.py b/bitcoin_safe/gui/qt/step_progress_bar.py index 1ffd999..3fd29e8 100644 --- a/bitcoin_safe/gui/qt/step_progress_bar.py +++ b/bitcoin_safe/gui/qt/step_progress_bar.py @@ -28,16 +28,10 @@ import logging -from dataclasses import dataclass - -from bitcoin_safe.signals import SignalsMin -from bitcoin_safe.threading_manager import ThreadingManager -from bitcoin_safe.typestubs import TypedPyQtSignal - -logger = logging.getLogger(__name__) - import os import sys +from dataclasses import dataclass +from functools import partial from math import ceil from typing import Callable, List, Optional, Union @@ -69,6 +63,11 @@ ) from bitcoin_safe.gui.qt.util import create_button_box +from bitcoin_safe.signals import SignalsMin +from bitcoin_safe.threading_manager import ThreadingManager +from bitcoin_safe.typestubs import TypedPyQtSignal + +logger = logging.getLogger(__name__) def height_of_str(text, widget: QWidget, max_width: float) -> float: @@ -414,7 +413,8 @@ def insertWidget(self, index, widget: QWidget) -> None: def setCurrentIndex(self, index: int) -> None: if 0 <= index < len(self.widgets): if self._currentIndex != -1: - self.widgets[self._currentIndex].setVisible(False) + if 0 <= self._currentIndex < len(self.widgets): + self.widgets[self._currentIndex].setVisible(False) self.widgets[index].setVisible(True) self._currentIndex = index @@ -437,7 +437,6 @@ def removeWidget(self, widget: QWidget) -> None: if widget in self.widgets: self.widgets.remove(widget) widget.setParent(None) # type: ignore[call-overload] - widget.deleteLater() # This is important to fully remove the widget if self._currentIndex >= len(self.widgets): self.setCurrentIndex(len(self.widgets) - 1) @@ -473,10 +472,8 @@ def __init__( use_resizing_stacked_widget=True, threading_parent: ThreadingManager | None = None, ) -> None: - super().__init__(parent=parent, threading_parent=threading_parent) # type: ignore + super().__init__(parent=parent, threading_parent=threading_parent) self.signals_min = signals_min - if threading_parent: - self.threading_parent = threading_parent self.step_bar = StepProgressBar( len(step_labels), current_index=current_index, @@ -596,6 +593,13 @@ def set_current_index(self, index: int) -> None: # visiblities. So it is critical to do the set_current_widget at the end. self.signal_set_current_widget.emit(new_widget) + def clear_widgets(self): + while self.stacked_widget.count(): + widget = self.stacked_widget.widget(0) + if widget: + widget.setParent(None) + self.stacked_widget.removeWidget(widget) + def set_custom_widget(self, index: int, widget: QWidget) -> None: """Sets the custom widget for the specified step. @@ -622,6 +626,11 @@ def set_custom_widget(self, index: int, widget: QWidget) -> None: self.signal_set_current_widget.emit(widget) self.signal_widget_focus.emit(widget) + def close(self): + self.end_threading_manager() + self.clear_widgets() + super().close() + @dataclass class VisibilityOption: @@ -666,7 +675,6 @@ def set_widget(self, widget: QWidget) -> None: w = item.widget() if w is not None: # Check if the item is a widget w.setParent(None) # type: ignore[call-overload] - w.deleteLater() # Ensure the widget is deleted # Insert the new widget at position 0 in the layout self._layout.insertWidget(0, widget) @@ -814,17 +822,12 @@ def __init__(self) -> None: ] ) - def factory(i) -> Callable: - def f(i=i) -> None: - print(f"callback action for {i}") - - return f - for i in range(self.step_progress_container.count()): widget = self.step_progress_container.stacked_widget.widget(i) if not isinstance(widget, TutorialWidget): continue - widget.set_callback(factory(i)) + callback = partial(print, f"callback action for {i}") + widget.set_callback(callback) self.step_progress_container.set_custom_widget(i, QTextEdit(f"{i}")) self.init_ui() diff --git a/bitcoin_safe/gui/qt/sync_tab.py b/bitcoin_safe/gui/qt/sync_tab.py index 1e0c966..9bc5be7 100644 --- a/bitcoin_safe/gui/qt/sync_tab.py +++ b/bitcoin_safe/gui/qt/sync_tab.py @@ -29,7 +29,9 @@ import hashlib import logging +from typing import Any, Dict, List +import bdkpython as bdk import nostr_sdk from bitcoin_nostr_chat.bitcoin_dm import BitcoinDM from bitcoin_nostr_chat.nostr_sync import NostrSync @@ -48,10 +50,6 @@ logger = logging.getLogger(__name__) -from typing import Any, Dict, List - -import bdkpython as bdk - class SyncTab(QObject): def __init__( diff --git a/bitcoin_safe/gui/qt/taglist/__init__.py b/bitcoin_safe/gui/qt/taglist/__init__.py index 15b6a64..57de521 100644 --- a/bitcoin_safe/gui/qt/taglist/__init__.py +++ b/bitcoin_safe/gui/qt/taglist/__init__.py @@ -1 +1 @@ -from .main import * +from .tag_editor import * diff --git a/bitcoin_safe/gui/qt/taglist/__main__.py b/bitcoin_safe/gui/qt/taglist/__main__.py index 4b04d4d..91120e7 100644 --- a/bitcoin_safe/gui/qt/taglist/__main__.py +++ b/bitcoin_safe/gui/qt/taglist/__main__.py @@ -1,6 +1,6 @@ from PyQt6.QtWidgets import QApplication, QVBoxLayout, QWidget -from bitcoin_safe.gui.qt.taglist.main import CustomListWidget, TagEditor +from bitcoin_safe.gui.qt.taglist.tag_editor import CustomListWidget, TagEditor if __name__ == "__main__": import sys diff --git a/bitcoin_safe/gui/qt/taglist/main.py b/bitcoin_safe/gui/qt/taglist/custom_list_widget.py similarity index 66% rename from bitcoin_safe/gui/qt/taglist/main.py rename to bitcoin_safe/gui/qt/taglist/custom_list_widget.py index 94380d2..e892bd7 100644 --- a/bitcoin_safe/gui/qt/taglist/main.py +++ b/bitcoin_safe/gui/qt/taglist/custom_list_widget.py @@ -26,19 +26,10 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. - -import logging - -from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo - -from ....i18n import translate -from ....util import qbytearray_to_str, register_cache, str_to_qbytearray -from ..util import category_color - -logger = logging.getLogger(__name__) - import json -from typing import Dict, Generator, Iterable, List, Optional, Tuple +import logging +from functools import lru_cache, partial +from typing import Callable, Generator, Iterable, List, Optional, Tuple from PyQt6.QtCore import ( QMimeData, @@ -52,7 +43,6 @@ ) from PyQt6.QtGui import ( QColor, - QCursor, QDrag, QDragEnterEvent, QDragLeaveEvent, @@ -74,15 +64,21 @@ QListWidget, QListWidgetItem, QMenu, - QPushButton, QStyle, QStyledItemDelegate, QStyleOptionButton, QStyleOptionViewItem, - QVBoxLayout, QWidget, ) +from bitcoin_safe.category_info import CategoryInfo, SubtextType +from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo + +from ....util import qbytearray_to_str, register_cache, str_to_qbytearray +from ..util import category_color + +logger = logging.getLogger(__name__) + def clean_tag(tag: str) -> str: return tag.strip() @@ -132,26 +128,48 @@ def __init__(self, editable=True, parent=None) -> None: super().__init__(parent) self.editable = editable self.currentlyEditingIndex = QModelIndex() - self.imageCache: Dict[Tuple[QModelIndex, QStyle.StateFlag, str, str, int, int], QImage] = ( - {} - ) # Cache for storing pre-rendered images - self.cache_size = 100 - - def _remove_first_cache_item(self): - key = None - for key in self.imageCache.keys(): - break - if key in self.imageCache: - del self.imageCache[key] - - def add_to_cache(self, key, value): - # logger.debug(f"Cache size {self.__class__.__name__}: {len(self.imageCache)}") - if len(self.imageCache) + 1 > self.cache_size: - self._remove_first_cache_item() - - self.imageCache[key] = value - - def renderHtmlToImage(self, index: QModelIndex, option: QStyleOptionViewItem, text: str, subtext: str): + self.cached_renderHtmlToImage: Callable[ + [QModelIndex, QStyleOptionViewItem, str, str, Tuple], QImage + ] = lru_cache(maxsize=128)(self.renderHtmlToImage) + + @classmethod + def expand_qobject(cls, obj) -> Tuple: + d = {} + for key in dir(obj): + try: + d[key] = str(getattr(obj, key)) + except: + pass + return tuple(d.items()) + + def paint(self, painter: QPainter | None, option: QStyleOptionViewItem, index: QModelIndex): + if not painter: + super().paint(painter=painter, option=option, index=index) + return + + # Check if the editor is open for this index + if self.currentlyEditingIndex.isValid() and self.currentlyEditingIndex == index: + text = "" # Set text to empty if editor is open + subtext = "" + else: + text = index.data(Qt.ItemDataRole.DisplayRole) + subtext = index.data(Qt.ItemDataRole.UserRole + 2) # Assuming subtext is stored in UserRole + 2 + + image = self.cached_renderHtmlToImage(index, option, text, subtext, self.expand_qobject(option)) + + # Draw the cached image + # image = self.imageCache[key] + if image: + painter.drawImage(option.rect.topLeft(), image) + + def renderHtmlToImage( + self, + index: QModelIndex, + option: QStyleOptionViewItem, + text: str, + subtext: str, + additional_keys: Tuple, + ): """ Renders the item appearance to an image, including button-like background and HTML text. """ @@ -274,43 +292,6 @@ def draw_html_text(self, painter: QPainter, text: str, subtext: str, rect: QRect doc.drawContents(painter) painter.restore() - def paint(self, painter: QPainter | None, option: QStyleOptionViewItem, index: QModelIndex): - if not painter: - super().paint(painter=painter, option=option, index=index) - return - - # Check if the editor is open for this index - if self.currentlyEditingIndex.isValid() and self.currentlyEditingIndex == index: - text = "" # Set text to empty if editor is open - subtext = "" - else: - text = index.data(Qt.ItemDataRole.DisplayRole) - subtext = index.data(Qt.ItemDataRole.UserRole + 2) # Assuming subtext is stored in UserRole + 2 - - key = ( - index, - option.state, - text, - subtext, - option.rect.width(), - option.rect.height(), - ) - # Ensure there's an image rendered for this index - if key not in self.imageCache: - # Render and cache the item appearance - self.add_to_cache(key, self.renderHtmlToImage(index, option, text, subtext)) - - # Draw the cached image - image = self.imageCache[key] - if image: - painter.drawImage(option.rect.topLeft(), image) - - def clearCache(self): - """ - Clears the cached images. Call this method if you need to refresh the items. - """ - self.imageCache.clear() - def createEditor(self, parent, option: QStyleOptionViewItem, index: QModelIndex) -> Optional[QWidget]: if not self.editable: return None @@ -344,76 +325,27 @@ def setModelData(self, editor: QLineEdit, model, index: QModelIndex): # type: i self.signal_tag_renamed.emit(old_value, new_value) -class DeleteButton(QPushButton): - signal_delete_item: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore - signal_addresses_dropped: TypedPyQtSignal[AddressDragInfo] = pyqtSignal(AddressDragInfo) # type: ignore - - def __init__(self, *args, **kwargs): - super(DeleteButton, self).__init__(*args, **kwargs) - self.setAcceptDrops(True) - icon = (self.style() or QStyle()).standardIcon(QStyle.StandardPixmap.SP_TrashIcon) - self.setIcon(icon) - - def dragEnterEvent(self, event: QDragEnterEvent | None): - if not event: - super().dragEnterEvent(event) - return - - mime_data = event.mimeData() - if mime_data and mime_data.hasFormat("application/json"): - json_string = qbytearray_to_str(mime_data.data("application/json")) - - d = json.loads(json_string) - logger.debug(f"dragEnterEvent: Got {d}") - if d.get("type") == "drag_tag" or d.get("type") == "drag_addresses": - event.acceptProposedAction() - return - - event.ignore() - - def dragLeaveEvent(self, event: QDragLeaveEvent | None): - "this is just to hide/undide the button" - logger.debug("Drag has left the delete button") - - def dropEvent(self, event: QDropEvent | None): - super().dropEvent(event) - if not event or event.isAccepted(): - return - - mime_data = event.mimeData() - if mime_data and mime_data.hasFormat("application/json"): - json_string = qbytearray_to_str(mime_data.data("application/json")) - - d = json.loads(json_string) - logger.debug(f"dropEvent: Got {d}") - if d.get("type") == "drag_tag": - self.signal_delete_item.emit(d.get("tag")) - event.acceptProposedAction() - return - if d.get("type") == "drag_addresses": - drag_info = AddressDragInfo([None], d.get("addresses")) - logger.debug(f"dropEvent: {drag_info}") - self.signal_addresses_dropped.emit(drag_info) - event.accept() - return - - event.ignore() - - class CustomListWidget(QListWidget): signal_tag_added: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore signal_tag_clicked: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore signal_tag_deleted: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore - signal_tag_renamed: TypedPyQtSignal[str, str] = pyqtSignal(str, str) # type: ignore + signal_tag_renamed: TypedPyQtSignal[str, str] = pyqtSignal(str, str) # type: ignore # (old,new) signal_addresses_dropped: TypedPyQtSignal[AddressDragInfo] = pyqtSignal(AddressDragInfo) # type: ignore signal_start_drag: TypedPyQtSignalNo = pyqtSignal() # type: ignore signal_stop_drag: TypedPyQtSignalNo = pyqtSignal() # type: ignore def __init__( - self, parent=None, editable=True, allow_no_selection=False, enable_drag=True, immediate_release=True + self, + parent=None, + editable=True, + allow_no_selection=False, + enable_drag=True, + immediate_release=True, + subtext_type: SubtextType = SubtextType.balance, ): super().__init__(parent) self.editable = editable + self.subtext_type = subtext_type self.allow_no_selection = allow_no_selection self.immediate_release = immediate_release @@ -434,7 +366,7 @@ def __init__( self.itemChanged.connect(self.on_item_changed) # new self.setMouseTracking(True) - self._drag_start_position = None + self._drag_start_position: QPoint | None = None self.setStyleSheet( """ @@ -482,35 +414,6 @@ def rename_selected(self, new_text: str): item.setText(new_text) # item.setBackground() - def setAllSelection(self, selected=True): - for i in range(self.count()): - item = self.item(i) - if item: - item.setSelected(selected) - - def mousePressEvent(self, event: QMouseEvent | None): - if not event: - super().mousePressEvent(event) - return - - item = self.itemAt(event.pos()) - if item and event.button() == Qt.MouseButton.LeftButton: - self._drag_start_position = event.pos() - if self.allow_no_selection and not ( - QApplication.keyboardModifiers() & Qt.KeyboardModifier.ControlModifier - ): - if item.isSelected(): - self.setAllSelection(False) - return - - super().mousePressEvent(event) - - def mouseDoubleClickEvent(self, event: QMouseEvent | None): - if event and event.button() == Qt.MouseButton.LeftButton: - pass - - super().mouseDoubleClickEvent(event) - def mouseReleaseEvent(self, event: QMouseEvent | None): if event and event.button() == Qt.MouseButton.LeftButton: # Perform actions that should happen after the mouse button is released @@ -521,19 +424,19 @@ def mouseReleaseEvent(self, event: QMouseEvent | None): self.on_item_clicked(item) if self.immediate_release: if not (QApplication.keyboardModifiers() & Qt.KeyboardModifier.ControlModifier): - self.setAllSelection(False) + self.clearSelection() return if event and event.button() == Qt.MouseButton.RightButton: # Get the item at the mouse position item = self.itemAt(event.pos()) if item: self.show_context_menu(event.globalPosition(), item=item) - - super().mouseReleaseEvent(event) + else: + super().mouseReleaseEvent(event) def show_context_menu(self, position: QPointF, item: QListWidgetItem): context_menu = QMenu(self) - context_menu.addAction(self.tr("Delete Category"), lambda: self.delete_item(item.text())) + context_menu.addAction(self.tr("Delete Category"), partial(self.delete_item, item.text())) context_menu.exec(position.toPoint()) def mouseMoveEvent(self, event: QMouseEvent | None): @@ -550,7 +453,8 @@ def mouseMoveEvent(self, event: QMouseEvent | None): if self.dragEnabled(): self.startDrag(Qt.DropAction.MoveAction) - super().mouseMoveEvent(event) + else: + super().mouseMoveEvent(event) def startDrag(self, action: Qt.DropAction | None): item = self.currentItem() @@ -574,6 +478,7 @@ def startDrag(self, action: Qt.DropAction | None): drag.exec(action) self.signal_stop_drag.emit() + self.clearSelection() def dragEnterEvent(self, event: QDragEnterEvent | None): if not event: @@ -595,7 +500,6 @@ def dragEnterEvent(self, event: QDragEnterEvent | None): event.ignore() def dropEvent(self, event: QDropEvent | None): - logger.debug("drop") super().dropEvent(event) if not event or event.isAccepted(): return @@ -617,6 +521,10 @@ def dropEvent(self, event: QDropEvent | None): event.ignore() + def dragLeaveEvent(self, event: QDragLeaveEvent | None): + "do nothing" + super().dragLeaveEvent(event) + def delete_item(self, item_text: str): for i in range(self.count()): item = self.item(i) @@ -635,7 +543,7 @@ def get_item_texts(self) -> Generator[str, None, None]: for item in self.get_items(): yield item.text() - def recreate(self, tags: List[str], sub_texts: Iterable[str | None] | None = None): + def recreate(self, category_infos: list[CategoryInfo]): # Store the texts of selected items selected_texts = [item.text() for item in self.selectedItems()] @@ -644,134 +552,19 @@ def recreate(self, tags: List[str], sub_texts: Iterable[str | None] | None = Non self.takeItem(i) # Add all items back - cleaned_sub_texts = sub_texts if sub_texts else [None] * len(tags) - for sub_text, tag in zip(cleaned_sub_texts, tags): - self.add(tag, sub_text=sub_text) # Assuming `self.add` correctly adds items + for category_info in category_infos: + subtext: str | None = None + if self.subtext_type == SubtextType.hide: + subtext = None + if self.subtext_type == SubtextType.balance: + subtext = category_info.text_balance + elif self.subtext_type == SubtextType.click_new_address: + subtext = category_info.text_click_new_address + + self.add(category_info.category, sub_text=subtext) # Assuming `self.add` correctly adds items # Re-select items based on stored texts for i in range(self.count()): item = self.item(i) if item and item.text() in selected_texts: item.setSelected(True) - - -class TagEditor(QWidget): - def __init__( - self, - parent=None, - tags: List[str] | None = None, - sub_texts: Iterable[str | None] | None = None, - tag_name="tag", - ): - super(TagEditor, self).__init__(parent) - self.tag_name = tag_name - self._layout = QVBoxLayout(self) - - self.input_field = QLineEdit() - self.input_field.setClearButtonEnabled(True) - self.input_field.returnPressed.connect(self.add_new_tag_from_input_field) - - self.delete_button = DeleteButton() - self.delete_button.hide() - - self.list_widget = CustomListWidget(parent=self) - self._layout.addWidget(self.input_field) - self._layout.addWidget(self.delete_button) - self._layout.addWidget(self.list_widget) - self.list_widget.signal_start_drag.connect(self.show_delete_button) - self.list_widget.signal_stop_drag.connect(self.hide_delete_button) - self.list_widget.signal_addresses_dropped.connect(self.hide_delete_button) - self.delete_button.signal_delete_item.connect(self.list_widget.delete_item) - self.delete_button.signal_addresses_dropped.connect(self.hide_delete_button) - - self.setAcceptDrops(True) - # TagEditor.updateUi ensure that this is not overwritten in a child class - TagEditor.updateUi(self) - - if tags: - self.list_widget.recreate(tags, sub_texts=sub_texts) - - def updateUi(self): - self.input_field.setPlaceholderText(self.default_placeholder_text()) - self.delete_button.setText(translate("tageditor", "Delete {name}").format(name=self.tag_name)) - - def default_placeholder_text(self): - return translate("tageditor", "Add new {name}").format(name=self.tag_name) - - def dragEnterEvent(self, event: QDragEnterEvent | None): - if not event: - super().dragEnterEvent(event) - return - - mime_data = event.mimeData() - if mime_data and mime_data.hasFormat("application/json"): - # print('accept') - # tag = self.itemAt(event.pos()) - - json_string = qbytearray_to_str(mime_data.data("application/json")) - # dropped_addresses = json.loads(json_string) - # print(f'drag enter {dropped_addresses, tag.text()}') - logger.debug(f"dragEnterEvent: {json_string}") - - event.acceptProposedAction() - - logger.debug(f"show_delete_button") - self.show_delete_button() - else: - event.ignore() - - def dragLeaveEvent(self, event: QDragLeaveEvent | None): - "this is just to hide/undide the button" - if not event: - super().dragLeaveEvent(event) - return - - if not self.rect().contains(self.mapFromGlobal(QCursor.pos())): - logger.debug("Drag operation left the TagEditor") - self.hide_delete_button() - else: - event.ignore() - - def dropEvent(self, event: QDropEvent | None): - super().dropEvent(event) - - if not event or event.isAccepted(): - return - - mime_data = event.mimeData() - if mime_data and mime_data.hasFormat("application/json"): - self.hide_delete_button() - event.accept() - else: - event.ignore() - - def show_delete_button(self, *args): - self.input_field.hide() - self.delete_button.show() - - def hide_delete_button(self, *args): - self.input_field.show() - self.delete_button.hide() - - def add(self, new_tag: str, sub_text: str | None = None) -> Optional[CustomListWidgetItem]: - if not self.tag_exists(new_tag): - return self.list_widget.add(new_tag, sub_text=sub_text) - return None - - def add_new_tag_from_input_field(self): - new_tag = clean_tag(self.input_field.text()) - item = self.add(new_tag) - if item: - self.input_field.setPlaceholderText(self.default_placeholder_text()) - else: - self.input_field.setPlaceholderText( - translate("tageditor", "This {name} exists already.").format(name=self.tag_name) - ) - self.input_field.clear() - - def tag_exists(self, tag: str): - for i in range(self.list_widget.count()): - item = self.list_widget.item(i) - if item and item.text() == tag: - return True - return False diff --git a/bitcoin_safe/gui/qt/taglist/tag_editor.py b/bitcoin_safe/gui/qt/taglist/tag_editor.py new file mode 100644 index 0000000..7f98401 --- /dev/null +++ b/bitcoin_safe/gui/qt/taglist/tag_editor.py @@ -0,0 +1,230 @@ +# +# 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 json +import logging +from typing import Optional + +from PyQt6.QtCore import pyqtSignal +from PyQt6.QtGui import QCursor, QDragEnterEvent, QDragLeaveEvent, QDropEvent +from PyQt6.QtWidgets import QLineEdit, QPushButton, QStyle, QVBoxLayout, QWidget + +from bitcoin_safe.category_info import CategoryInfo, SubtextType +from bitcoin_safe.gui.qt.taglist.custom_list_widget import ( + AddressDragInfo, + CustomListWidget, + CustomListWidgetItem, + clean_tag, +) +from bitcoin_safe.typestubs import TypedPyQtSignal + +from ....i18n import translate +from ....util import qbytearray_to_str + +logger = logging.getLogger(__name__) + + +class DeleteButton(QPushButton): + signal_delete_item: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore + signal_addresses_dropped: TypedPyQtSignal[AddressDragInfo] = pyqtSignal(AddressDragInfo) # type: ignore + + def __init__(self, *args, **kwargs): + super(DeleteButton, self).__init__(*args, **kwargs) + self.setAcceptDrops(True) + icon = (self.style() or QStyle()).standardIcon(QStyle.StandardPixmap.SP_TrashIcon) + self.setIcon(icon) + + def dragEnterEvent(self, event: QDragEnterEvent | None): + if not event: + super().dragEnterEvent(event) + return + + mime_data = event.mimeData() + if mime_data and mime_data.hasFormat("application/json"): + json_string = qbytearray_to_str(mime_data.data("application/json")) + + d = json.loads(json_string) + logger.debug(f"dragEnterEvent: Got {d}") + if d.get("type") == "drag_tag" or d.get("type") == "drag_addresses": + event.acceptProposedAction() + return + + event.ignore() + + def dragLeaveEvent(self, event: QDragLeaveEvent | None): + "this is just to hide/undide the button" + logger.debug("Drag has left the delete button") + + def dropEvent(self, event: QDropEvent | None): + super().dropEvent(event) + if not event or event.isAccepted(): + return + + mime_data = event.mimeData() + if mime_data and mime_data.hasFormat("application/json"): + json_string = qbytearray_to_str(mime_data.data("application/json")) + + d = json.loads(json_string) + logger.debug(f"dropEvent: Got {d}") + if d.get("type") == "drag_tag": + self.signal_delete_item.emit(d.get("tag")) + event.acceptProposedAction() + return + if d.get("type") == "drag_addresses": + drag_info = AddressDragInfo([None], d.get("addresses")) + logger.debug(f"dropEvent: {drag_info}") + self.signal_addresses_dropped.emit(drag_info) + event.accept() + return + + event.ignore() + + +class TagEditor(QWidget): + def __init__( + self, + parent=None, + category_infos: list[CategoryInfo] | None = None, + tag_name="tag", + subtext_type: SubtextType = SubtextType.balance, + ): + super(TagEditor, self).__init__(parent) + self.tag_name = tag_name + self._layout = QVBoxLayout(self) + + self.input_field = QLineEdit() + self.input_field.setClearButtonEnabled(True) + self.input_field.returnPressed.connect(self.add_new_tag_from_input_field) + + self.delete_button = DeleteButton() + self.delete_button.hide() + + self.list_widget = CustomListWidget(parent=self, subtext_type=subtext_type) + self._layout.addWidget(self.input_field) + self._layout.addWidget(self.delete_button) + self._layout.addWidget(self.list_widget) + self.list_widget.signal_start_drag.connect(self.show_delete_button) + self.list_widget.signal_stop_drag.connect(self.hide_delete_button) + self.list_widget.signal_addresses_dropped.connect(self.hide_delete_button) + self.delete_button.signal_delete_item.connect(self.list_widget.delete_item) + self.delete_button.signal_addresses_dropped.connect(self.hide_delete_button) + + self.setAcceptDrops(True) + # TagEditor.updateUi ensure that this is not overwritten in a child class + TagEditor.updateUi(self) + + if category_infos: + self.list_widget.recreate(category_infos) + + def updateUi(self): + self.input_field.setPlaceholderText(self.default_placeholder_text()) + self.delete_button.setText(translate("tageditor", "Delete {name}").format(name=self.tag_name)) + + def default_placeholder_text(self): + return translate("tageditor", "Add new {name}").format(name=self.tag_name) + + def dragEnterEvent(self, event: QDragEnterEvent | None): + if not event: + super().dragEnterEvent(event) + return + + mime_data = event.mimeData() + if mime_data and mime_data.hasFormat("application/json"): + # print('accept') + # tag = self.itemAt(event.pos()) + + json_string = qbytearray_to_str(mime_data.data("application/json")) + # dropped_addresses = json.loads(json_string) + # print(f'drag enter {dropped_addresses, tag.text()}') + logger.debug(f"dragEnterEvent: {json_string}") + + event.acceptProposedAction() + + logger.debug(f"show_delete_button") + self.show_delete_button() + else: + event.ignore() + super().dragEnterEvent(event) + + def dragLeaveEvent(self, event: QDragLeaveEvent | None): + "this is just to hide/undide the button" + if not event: + super().dragLeaveEvent(event) + return + + if not self.rect().contains(self.mapFromGlobal(QCursor.pos())): + logger.debug("Drag operation left the TagEditor") + self.hide_delete_button() + else: + event.ignore() + super().dragLeaveEvent(event) + + def dropEvent(self, event: QDropEvent | None): + super().dropEvent(event) + + if not event or event.isAccepted(): + return + + mime_data = event.mimeData() + if mime_data and mime_data.hasFormat("application/json"): + self.hide_delete_button() + event.accept() + else: + event.ignore() + super().dropEvent(event) + + def show_delete_button(self, *args): + self.input_field.hide() + self.delete_button.show() + + def hide_delete_button(self, *args): + self.input_field.show() + self.delete_button.hide() + + def add(self, new_tag: str, sub_text: str | None = None) -> Optional[CustomListWidgetItem]: + if not self.tag_exists(new_tag): + return self.list_widget.add(new_tag, sub_text=sub_text) + return None + + def add_new_tag_from_input_field(self): + new_tag = clean_tag(self.input_field.text()) + item = self.add(new_tag) + if item: + self.input_field.setPlaceholderText(self.default_placeholder_text()) + else: + self.input_field.setPlaceholderText( + translate("tageditor", "This {name} exists already.").format(name=self.tag_name) + ) + self.input_field.clear() + + def tag_exists(self, tag: str): + for i in range(self.list_widget.count()): + item = self.list_widget.item(i) + if item and item.text() == tag: + return True + return False diff --git a/bitcoin_safe/gui/qt/tx_export.py b/bitcoin_safe/gui/qt/tx_export.py index c474961..6aca2e4 100644 --- a/bitcoin_safe/gui/qt/tx_export.py +++ b/bitcoin_safe/gui/qt/tx_export.py @@ -61,7 +61,6 @@ def __init__( super().__init__(parent=parent) self.setWindowTitle(self.tr("Export Transaction")) self.data = data - self.threading_parent = threading_parent if not self.data: return @@ -71,7 +70,7 @@ def __init__( data=self.data, signals_min=signals_min, network=network, - threading_parent=self.threading_parent, + threading_parent=threading_parent, parent=self, ) self.add_button(self.export_qr_button) diff --git a/bitcoin_safe/gui/qt/ui_tx.py b/bitcoin_safe/gui/qt/ui_tx.py index 39ac7c4..34bcb9c 100644 --- a/bitcoin_safe/gui/qt/ui_tx.py +++ b/bitcoin_safe/gui/qt/ui_tx.py @@ -29,9 +29,27 @@ import logging from collections import defaultdict +from typing import Any, Callable, Dict, List, Optional, Set, Tuple +import bdkpython as bdk from bitcoin_qr_tools.data import Data, DataType from bitcoin_usb.psbt_tools import PSBTTools +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QColor, QFont +from PyQt6.QtWidgets import ( + QCheckBox, + QDialogButtonBox, + QHBoxLayout, + QLabel, + QLayout, + QPushButton, + QSizePolicy, + QSplitter, + QStyle, + QTabWidget, + QVBoxLayout, + QWidget, +) from bitcoin_safe.fx import FX from bitcoin_safe.gui.qt.block_change_signals import BlockChangesSignals @@ -49,38 +67,11 @@ from bitcoin_safe.keystore import KeyStore from bitcoin_safe.labels import LabelType from bitcoin_safe.network_config import ProxyInfo +from bitcoin_safe.signal_tracker import SignalTools, SignalTracker from bitcoin_safe.threading_manager import TaskThread, ThreadingManager from bitcoin_safe.typestubs import TypedPyQtSignal from ...config import MIN_RELAY_FEE, UserConfig -from ...signals import TypedPyQtSignalNo -from .dialog_import import ImportDialog -from .my_treeview import MyItemDataRole, SearchableTab -from .nLockTimePicker import nLocktimePicker -from .util import adjust_bg_color_for_darkmode - -logger = logging.getLogger(__name__) - -from typing import Callable, Dict, List, Optional, Set, Tuple - -import bdkpython as bdk -from PyQt6.QtCore import Qt, pyqtSignal -from PyQt6.QtGui import QColor, QFont -from PyQt6.QtWidgets import ( - QCheckBox, - QDialogButtonBox, - QHBoxLayout, - QLabel, - QLayout, - QPushButton, - QSizePolicy, - QSplitter, - QStyle, - QTabWidget, - QVBoxLayout, - QWidget, -) - from ...mempool import MempoolData, TxPrio from ...psbt_util import FeeInfo, PubKeyInfo, SimpleInput, SimplePSBT from ...pythonbdk_types import ( @@ -92,7 +83,13 @@ python_utxo_balance, robust_address_str_from_script, ) -from ...signals import Signals, SignalsMin, UpdateFilter, UpdateFilterReason +from ...signals import ( + Signals, + SignalsMin, + TypedPyQtSignalNo, + UpdateFilter, + UpdateFilterReason, +) from ...signer import ( AbstractSignatureImporter, SignatureImporterClipboard, @@ -103,7 +100,6 @@ ) from ...tx import TxUiInfos, calc_minimum_rbf_fee_info from ...util import ( - Satoshis, block_explorer_URL, clean_list, format_fee_rate, @@ -118,17 +114,23 @@ get_wallets, ) from .category_list import CategoryList -from .recipients import Recipients, RecipientTabWidget +from .dialog_import import ImportDialog +from .my_treeview import MyItemDataRole, SearchableTab +from .nLockTimePicker import nLocktimePicker +from .recipients import Recipients, RecipientTabWidget, RecipientWidget from .util import ( Message, MessageType, add_to_buttonbox, + adjust_bg_color_for_darkmode, caught_exception_message, clear_layout, read_QIcon, ) from .utxo_list import UTXOList, UtxoListWithToolbar +logger = logging.getLogger(__name__) + class LinkingWarningBar(NotificationBar): def __init__(self, signals_min: SignalsMin) -> None: @@ -183,6 +185,7 @@ def updateUi(self) -> None: class UITx_Base: def __init__(self, config: UserConfig, signals: Signals, mempool_data: MempoolData, **kwargs) -> None: super().__init__(**kwargs) + self.signal_tracker = SignalTracker() self.signals = signals self.mempool_data = mempool_data self.config = config @@ -251,7 +254,7 @@ def __init__( focus_ui_element: UiElements = UiElements.none, ) -> None: super().__init__( - serialize=lambda: self.do_serialize(), + serialize=self.do_serialize, parent=parent, config=config, signals=signals, @@ -297,7 +300,8 @@ def __init__( self._layout.addWidget(self.upper_widget) # in out - self.tabs_inputs_outputs = ExtendedTabWidget(object) + self.tabs_inputs_outputs = ExtendedTabWidget[object](parent=self) + self.tabs_inputs_outputs.setObjectName(f"member of {self.__class__.__name__}") # button = QPushButton("Edit") # button.setFixedHeight(button.sizeHint().height()) # button.setIcon(QIcon(icon_path("pen.svg"))) @@ -424,7 +428,7 @@ def __init__( self.buttonBox, "", "send.svg", - on_clicked=lambda: self.broadcast(), + on_clicked=self.broadcast, role=QDialogButtonBox.ButtonRole.AcceptRole, ) @@ -437,10 +441,10 @@ def __init__( self.updateUi() self.reload(UpdateFilter(refresh_all=True)) self.utxo_list.update_content() - self.signals.language_switch.connect(self.updateUi) + self.signal_tracker.connect(self.signals.language_switch, self.updateUi) # after the wallet loads the transactions, then i have to reload again to # ensure that the linking warning bar appears (needs all tx loaded) - self.signals.any_wallet_updated.connect(self.reload) + self.signal_tracker.connect(self.signals.any_wallet_updated, self.reload) def updateUi(self) -> None: self.tabs_inputs_outputs.setTabText( @@ -1114,6 +1118,12 @@ def get_total_non_change_output_amount(self, tx: bdk.Transaction) -> int: total_non_change_output_amount += value return total_non_change_output_amount + def close(self): + self.signal_tracker.disconnect_all() + SignalTools.disconnect_all_signals_from(self) + self.setParent(None) + super().close() + class UITx_Creator(UITx_Base, SearchableTab): signal_create_tx: TypedPyQtSignal[TxUiInfos] = pyqtSignal(TxUiInfos) # type: ignore @@ -1137,7 +1147,7 @@ def __init__( self.widget_utxo_with_toolbar = widget_utxo_with_toolbar self.additional_outpoints: List[OutPoint] = [] - utxo_list.get_outpoints = self.get_outpoints + utxo_list.outpoints = self.get_outpoints() self.searchable_list = utxo_list self._layout = QVBoxLayout(self) @@ -1199,11 +1209,11 @@ def __init__( self.button_box.addButton(self.button_ok, QDialogButtonBox.ButtonRole.AcceptRole) if self.button_ok: self.button_ok.setDefault(True) - self.button_ok.clicked.connect(lambda: self.create_tx()) + self.button_ok.clicked.connect(self.create_tx) self.button_clear = self.button_box.addButton(QDialogButtonBox.StandardButton.Reset) if self.button_clear: - self.button_clear.clicked.connect(lambda: self.clear_ui()) + self.button_clear.clicked.connect(self.clear_ui) self._layout.addWidget(self.button_box) @@ -1218,8 +1228,8 @@ def __init__( self.mempool_data.signal_data_updated.connect(self.update_fee_rate_to_mempool) self.utxo_list.signal_selection_changed.connect(self.update_amounts_and_categories) self.recipients.signal_amount_changed.connect(self.on_signal_amount_changed) - self.recipients.signal_added_recipient.connect(self.on_recipients_changed) - self.recipients.signal_removed_recipient.connect(self.on_recipients_changed) + self.recipients.signal_added_recipient.connect(self.on_recipients_added) + self.recipients.signal_removed_recipient.connect(self.on_recipients_removed) self.category_list.signal_tag_clicked.connect(self.on_category_list_clicked) self.signals.language_switch.connect(self.updateUi) self.signals.wallet_signals[self.wallet.id].updated.connect(self.update_with_filter) @@ -1238,6 +1248,7 @@ def update_with_filter(self, update_filter: UpdateFilter) -> None: logger.debug(f"{self.__class__.__name__} update_with_filter {update_filter}") self.update_balance_label() self.update_amounts_and_categories() + self.utxo_list.set_outpoints(self.get_outpoints()) def updateUi(self) -> None: # translations @@ -1271,9 +1282,7 @@ def update_opportunistic_checkbox(self): def update_balance_label(self): balance = self.wallet.get_balance() - display_balance = ( - self.signals.wallet_signals[self.wallet.id].get_display_balance.emit().get(self.wallet.id) - ) + display_balance = self.signals.wallet_signals[self.wallet.id].get_display_balance.emit() if display_balance: balance = display_balance @@ -1297,10 +1306,17 @@ def update_amounts(self): def on_category_list_clicked(self, tag: str): self.update_amounts_and_categories() - def on_recipients_changed(self, recipient_tab_widget: RecipientTabWidget): + def on_signal_clicked_send_max_button(self, recipient_widget: RecipientWidget): + self.update_amounts() + + def on_recipients_added(self, recipient_tab_widget: RecipientTabWidget): + recipient_tab_widget.signal_clicked_send_max_button.connect(self.on_signal_clicked_send_max_button) + self.update_amounts_and_categories() + + def on_recipients_removed(self, recipient_tab_widget: RecipientTabWidget): self.update_amounts_and_categories() - def on_signal_amount_changed(self, recipient_tab_widget: RecipientTabWidget): + def on_signal_amount_changed(self, recipient_widget: Any): self.update_amounts() def update_amounts_and_categories(self): @@ -1407,34 +1423,6 @@ def update_fee_rate_to_mempool(self) -> None: def get_outpoints(self) -> List[OutPoint]: return [utxo.outpoint for utxo in self.wallet.get_all_utxos()] + self.additional_outpoints - def _get_category_python_utxo_dict(self) -> Dict[str, List[PythonUtxo]]: - category_python_utxo_dict: Dict[str, List[PythonUtxo]] = {} - for outpoint in self.get_outpoints(): - python_utxo = self.wallet.get_python_txo(str(outpoint)) - if not python_utxo: - continue - - category = self.wallet.labels.get_category(python_utxo.address) - if not category: - continue - if category not in category_python_utxo_dict: - category_python_utxo_dict[category] = [] - category_python_utxo_dict[category].append(python_utxo) - return category_python_utxo_dict - - def _get_sub_texts_for_uitx(self) -> List[str]: - category_python_utxo_dict = self._get_category_python_utxo_dict() - - return [ - self.tr("{num_inputs} Inputs: {inputs}").format( - num_inputs=len(category_python_utxo_dict.get(category, [])), - inputs=Satoshis( - python_utxo_balance(category_python_utxo_dict.get(category, [])), self.wallet.network - ).str_with_unit(), - ) - for category in self.wallet.labels.categories - ] - def create_inputs_selector(self, splitter: QSplitter) -> None: self.tabs_inputs = QTabWidget(self) @@ -1452,9 +1440,7 @@ def create_inputs_selector(self, splitter: QSplitter) -> None: # Taglist self.category_list = CategoryList( - self.categories, self.signals.wallet_signals[self.wallet.id], - self._get_sub_texts_for_uitx, immediate_release=False, ) first_entry = self.category_list.item(0) @@ -1488,10 +1474,10 @@ def create_inputs_selector(self, splitter: QSplitter) -> None: # select the first one with !=0 balance # TODO: this doesnt work however, because the wallet sync happens after this creation - category_utxo_dict = self._get_category_python_utxo_dict() + category_utxo_dict = self.wallet.get_category_python_utxo_dict() def get_idx_non_zero_category() -> Optional[int]: - for i, category in enumerate(self.category_list.categories): + for i, category in enumerate(self.wallet.labels.categories): if python_utxo_balance(category_utxo_dict.get(category, [])) > 0: return i return None @@ -1768,3 +1754,12 @@ def set_ui(self, txinfos: TxUiInfos) -> None: self.recipients.add_recipient() self.recipients.set_allow_edit(not txinfos.recipient_read_only) + + def close(self): + self.signal_tracker.disconnect_all() + SignalTools.disconnect_all_signals_from(self) + + self.category_list.close() + self.widget_utxo_with_toolbar.close() + self.setParent(None) + super().close() diff --git a/bitcoin_safe/gui/qt/unique_deque.py b/bitcoin_safe/gui/qt/unique_deque.py index eff1e31..0bde71b 100644 --- a/bitcoin_safe/gui/qt/unique_deque.py +++ b/bitcoin_safe/gui/qt/unique_deque.py @@ -33,11 +33,12 @@ _T = TypeVar("_T") -logger = logging.getLogger(__name__) from collections import deque from typing import Any +logger = logging.getLogger(__name__) + class UniqueDeque( deque, diff --git a/bitcoin_safe/gui/qt/update_notification_bar.py b/bitcoin_safe/gui/qt/update_notification_bar.py index 5bcb143..0b6acc5 100644 --- a/bitcoin_safe/gui/qt/update_notification_bar.py +++ b/bitcoin_safe/gui/qt/update_notification_bar.py @@ -81,7 +81,7 @@ def __init__( super().__init__( text="", optional_button_text="", - callback_optional_button=lambda: self.check(), + callback_optional_button=self.check, additional_widget=self.download_container, has_close_button=True, parent=parent, @@ -119,7 +119,7 @@ def refresh(self) -> None: if (layout_item := self.download_container_layout.takeAt(0)) and ( _widget := layout_item.widget() ): - _widget.deleteLater() + _widget.close() self.download_container.setVisible(bool(self.assets)) if self.assets: diff --git a/bitcoin_safe/gui/qt/usb_register_multisig.py b/bitcoin_safe/gui/qt/usb_register_multisig.py index 18bbafb..9679115 100644 --- a/bitcoin_safe/gui/qt/usb_register_multisig.py +++ b/bitcoin_safe/gui/qt/usb_register_multisig.py @@ -131,6 +131,7 @@ def on_button_click( try: address = self.usb_gui.display_address(address_descriptor) except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") Message(str(e), type=MessageType.Error) return False diff --git a/bitcoin_safe/gui/qt/util.py b/bitcoin_safe/gui/qt/util.py index 52d29ff..bbb4b06 100644 --- a/bitcoin_safe/gui/qt/util.py +++ b/bitcoin_safe/gui/qt/util.py @@ -35,7 +35,7 @@ import sys import traceback import webbrowser -from functools import lru_cache +from functools import lru_cache, partial from pathlib import Path from typing import Any, Callable, Iterable, List, Optional, Tuple, Union from urllib.parse import urlparse @@ -77,6 +77,7 @@ QWidget, ) +from bitcoin_safe.execute_config import ENABLE_TIMERS from bitcoin_safe.gui.qt.custom_edits import AnalyzerState from bitcoin_safe.gui.qt.wrappers import Menu from bitcoin_safe.i18n import translate @@ -139,20 +140,32 @@ def open_website(url: str): QDesktopServices.openUrl(QUrl(url)) -def resize(x, y, x_max, y_max): - def resize_one_side(a, b, amax): - a_new = min(amax, a) - return a_new, b * a_new / a +def proportional_fit_into_max(x, y, x_max, y_max): + """ + Scales (x, y) proportionally to fit within (x_max, y_max) while maintaining aspect ratio. + + :param x: Original width + :param y: Original height + :param x_max: Maximum allowed width + :param y_max: Maximum allowed height + :return: (new_x, new_y) scaled dimensions + """ + # Calculate scaling factors for width and height + scale_x = x_max / x + scale_y = y_max / y + + # Choose the smaller scale factor to maintain aspect ratio + scale = min(scale_x, scale_y) - # resize according to xmax - x, y = resize_one_side(x, y, x_max) - # resize according to ymax - y, x = resize_one_side(y, x, y_max) - return x, y + # Compute new dimensions + new_x = int(x * scale) + new_y = int(y * scale) + + return new_x, new_y def qresize(qsize: QSize, max_sizes: Tuple[int, int]): - x, y = resize(qsize.width(), qsize.height(), *max_sizes) + x, y = proportional_fit_into_max(qsize.width(), qsize.height(), *max_sizes) return QSize(int(x), int(y)) @@ -439,7 +452,7 @@ def custom_exception_handler(exc_type, exc_value, exc_traceback=None): f"{error_message}\n\nPlease send the error report via email, so the bug can be fixed.", ) - except: + except Exception: error_message = str([exc_type, exc_value, exc_traceback]) logger.critical(error_message) QMessageBox.critical( @@ -531,7 +544,7 @@ def __init__(self, parent: QWidget, message: str, task: Callable[[], Any]): self.message_label = QLabel(message) vbox = QVBoxLayout(self) vbox.addWidget(self.message_label) - self.finished.connect(self.deleteLater) # see #3956 + self.finished.connect(self.close) # see #3956 # show popup self.show() # refresh GUI; needed for popup to appear and for message_label to get drawn @@ -703,8 +716,10 @@ def do_copy(text: str, *, title: str | None = None) -> None: def show_tooltip_after_delay(message): timer = QTimer() + if not ENABLE_TIMERS: + return # tooltip cannot be displayed immediately when called from a menu; wait 200ms - timer.singleShot(200, lambda: QToolTip.showText(QCursor.pos(), message)) + timer.singleShot(200, partial(QToolTip.showText, QCursor.pos(), message)) def qicon_to_pil(qicon: QIcon, size=200) -> PilImage.Image: @@ -779,6 +794,9 @@ def get_host_and_port(url) -> Tuple[str | None, int | None]: def delayed_execution(f, parent, delay=10): + if not ENABLE_TIMERS: + f() + return timer = QTimer(parent) timer.setSingleShot(True) # Make sure the timer runs only once timer.timeout.connect(f) # Connect the timeout signal to the function @@ -795,7 +813,6 @@ def clear_layout(layout: QLayout) -> None: if widget: layout.removeWidget(widget) widget.setParent(None) # Remove widget from parent to fully disconnect it - widget.deleteLater() def svg_widgets_hardware_signers( diff --git a/bitcoin_safe/gui/qt/utxo_list.py b/bitcoin_safe/gui/qt/utxo_list.py index eb31440..80f8ad4 100644 --- a/bitcoin_safe/gui/qt/utxo_list.py +++ b/bitcoin_safe/gui/qt/utxo_list.py @@ -53,6 +53,7 @@ # SOFTWARE. import logging +from functools import partial from bitcoin_safe.gui.qt.wrappers import Menu @@ -147,7 +148,7 @@ def __init__( self, config: UserConfig, signals: Signals, - get_outpoints, + outpoints: List[OutPoint], hidden_columns: List[int] | None = None, txout_dict: Union[Dict[str, bdk.TxOut], Dict[str, TxOut]] | None = None, keep_outpoint_order=False, @@ -159,7 +160,7 @@ def __init__( Args: config (UserConfig): _description_ signals (Signals): _description_ - get_outpoints (_type_): _description_ + outpoints (List[OutPoint]): _description_ hidden_columns (_type_, optional): _description_. Defaults to None. txout_dict (Dict[str, bdk.TxOut], optional): Can be used to augment the list with infos, if the utxo is not from the own wallet. Defaults to None. keep_outpoint_order (bool, optional): _description_. Defaults to False. @@ -173,19 +174,27 @@ def __init__( sort_column=sort_column if sort_column is not None else UTXOList.Columns.ADDRESS, sort_order=sort_order if sort_order is not None else Qt.SortOrder.AscendingOrder, ) + self.outpoints = outpoints self.config: UserConfig = config self.keep_outpoint_order = keep_outpoint_order self.hidden_columns = hidden_columns if hidden_columns else [] self.signals = signals - self.get_outpoints = get_outpoints self.txout_dict: Union[Dict[str, bdk.TxOut], Dict[str, TxOut]] = txout_dict if txout_dict else {} self._pythonutxo_dict: Dict[str, PythonUtxo] = {} # outpoint --> txdetails self._wallet_dict: Dict[str, Wallet] = {} # outpoint --> wallet self.setTextElideMode(Qt.TextElideMode.ElideMiddle) - self._source_model = MyStandardItemModel(self, drag_key="outpoints") + self._source_model = MyStandardItemModel( + key_column=self.key_column, + parent=self, + ) self.proxy = MySortModel( - self, source_model=self._source_model, sort_role=MyItemDataRole.ROLE_SORT_ORDER + Columns=self.Columns, + drag_key="outpoints", + key_column=self.key_column, + parent=self, + source_model=self._source_model, + sort_role=MyItemDataRole.ROLE_SORT_ORDER, ) self.setModel(self.proxy) @@ -205,16 +214,26 @@ def __init__( # self.setDragDropMode(QAbstractItemView.InternalMove) # self.setDefaultDropAction(Qt.MoveAction) + def set_outpoints(self, outpoints: List[OutPoint]): + self.outpoints = outpoints + self.update_content() + def create_menu(self, position: QPoint) -> Menu: - selected: List[QModelIndex] = self.selected_in_column(self.Columns.OUTPOINT) + menu = Menu() + # is_multisig = isinstance(self.wallet, Multisig_Wallet) + selected = self.selected_in_column(self.key_column) + if not selected: + return menu + multi_select = len(selected) > 1 + + _selected_items = [self.item_from_index(item) for item in selected] + selected_items = [item for item in _selected_items if item] + if not selected: current_row = self.current_row_in_column(self.Columns.OUTPOINT) if current_row: selected = [current_row] - menu = Menu() - - multi_select = len(selected) > 1 outpoints: List[OutPoint] = [ self.model().data(item, role=MyItemDataRole.ROLE_KEY) for item in selected ] @@ -230,14 +249,14 @@ def create_menu(self, position: QPoint) -> Menu: if str(outpoints[0]) in self._wallet_dict: menu.add_action( translate("utxo_list", "Open transaction"), - lambda: self.signals.open_tx_like.emit(outpoints[0].txid), + partial(self.signals.open_tx_like.emit, outpoints[0].txid), ) txid_URL = block_explorer_URL(self.config.network_config.mempool_url, "tx", outpoints[0].txid) if txid_URL: menu.add_action( translate("utxo_list", "View on block explorer"), - lambda: webopen(txid_URL), + partial(webopen, txid_URL), icon=read_QIcon("block-explorer.svg"), ) @@ -256,8 +275,10 @@ def create_menu(self, position: QPoint) -> Menu: if wallet_ids and addresses: menu.add_action( translate("utxo_list", "Open Address Details"), - lambda: self.signals.wallet_signals[wallet_ids[0]].show_address.emit( - addresses[0], wallet_ids[0] + partial( + self.signals.wallet_signals[wallet_ids[0]].show_address.emit, + addresses[0], + wallet_ids[0], ), ) @@ -265,7 +286,10 @@ def create_menu(self, position: QPoint) -> Menu: menu.add_action( translate("utxo_list", "Copy as csv"), - lambda: self.copyRowsToClipboardAsCSV([r.row() for r in selected]), + partial( + self.copyRowsToClipboardAsCSV, + [item.data(MySortModel.role_drag_key) for item in selected_items if item], + ), icon=read_QIcon("csv-file.svg"), ) @@ -358,7 +382,7 @@ def str_format(v): self._source_model.clear() self.update_headers(self.get_headers()) - for i, outpoint in enumerate(self.get_outpoints()): + for i, outpoint in enumerate(self.outpoints): outpoint = OutPoint.from_bdk(outpoint) wallet, python_utxo, address, satoshis = self.get_wallet_address_satoshis(outpoint) @@ -508,7 +532,8 @@ def update_labels(self): number=len(selected_values), ) ) - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") self.uxto_selected_label.setText(f"") def create_toolbar_with_menu(self, title): diff --git a/bitcoin_safe/gui/qt/wallet_balance_chart.py b/bitcoin_safe/gui/qt/wallet_balance_chart.py index a8dafc8..8e8a7f0 100644 --- a/bitcoin_safe/gui/qt/wallet_balance_chart.py +++ b/bitcoin_safe/gui/qt/wallet_balance_chart.py @@ -44,6 +44,8 @@ QWidget, ) +from bitcoin_safe.execute_config import ENABLE_TIMERS +from bitcoin_safe.signal_tracker import SignalTools, SignalTracker from bitcoin_safe.signals import UpdateFilter, WalletSignals from bitcoin_safe.util import unit_str from bitcoin_safe.wallet import Wallet @@ -54,6 +56,7 @@ class BalanceChart(QWidget): def __init__(self, y_axis_text="Balance", parent: QWidget | None = None) -> None: super().__init__(parent) + self.signal_tracker = SignalTracker() self.y_axis_text = y_axis_text # Layout @@ -228,6 +231,12 @@ def update_chart(self, balance_data, project_until_now=True) -> None: # scatter_series.attachAxis(self.datetime_axis) # scatter_series.attachAxis(self.value_axis) + def close(self): + self.signal_tracker.disconnect_all() + SignalTools.disconnect_all_signals_from(self) + self.setParent(None) + super().close() + class WalletBalanceChart(BalanceChart): def __init__(self, wallet: Wallet, wallet_signals: WalletSignals, parent: QWidget | None = None) -> None: @@ -239,8 +248,8 @@ def __init__(self, wallet: Wallet, wallet_signals: WalletSignals, parent: QWidge self.updateUi() # signals - self.wallet_signals.updated.connect(self.update_balances) - self.wallet_signals.language_switch.connect(self.updateUi) + self.signal_tracker.connect(self.wallet_signals.updated, self.update_balances) + self.signal_tracker.connect(self.wallet_signals.language_switch, self.updateUi) def updateUi(self) -> None: self.y_axis_text = self.tr("Balance ({unit})").format(unit=unit_str(self.wallet.network)) @@ -296,7 +305,8 @@ def __init__(self) -> None: # QTimer to simulate incoming transactions self.timer = QTimer() self.timer.timeout.connect(self.add_transaction) - self.timer.start(3000) + if ENABLE_TIMERS: + self.timer.start(3000) # Initial chart update self.update_chart() diff --git a/bitcoin_safe/gui/qt/wallet_list.py b/bitcoin_safe/gui/qt/wallet_list.py index 9f36685..d09d0ef 100644 --- a/bitcoin_safe/gui/qt/wallet_list.py +++ b/bitcoin_safe/gui/qt/wallet_list.py @@ -28,6 +28,7 @@ import os +from functools import partial from pathlib import Path from typing import Iterable, List @@ -108,7 +109,7 @@ def handleRightClick(self, position: QPoint): menu = QMenu() openFolderAction = QAction(self.tr("Open containing folder"), self) - openFolderAction.triggered.connect(lambda: self.openContainingFolder(item.toolTip())) + openFolderAction.triggered.connect(partial(self.openContainingFolder, item.toolTip())) menu.addAction(openFolderAction) menu.exec(self.mapToGlobal(position)) diff --git a/bitcoin_safe/gui/qt/wizard.py b/bitcoin_safe/gui/qt/wizard.py index cd5f53a..c1fcd29 100644 --- a/bitcoin_safe/gui/qt/wizard.py +++ b/bitcoin_safe/gui/qt/wizard.py @@ -27,45 +27,22 @@ # SOFTWARE. +import enum import logging import xml.etree.ElementTree as ET from abc import abstractmethod - -from bitcoin_usb.address_types import AddressTypes -from bitcoin_usb.usb_gui import USBGui -from PyQt6.QtWidgets import QCheckBox - -from bitcoin_safe.gui.qt.bitcoin_quick_receive import BitcoinQuickReceive -from bitcoin_safe.gui.qt.data_tab_widget import DataTabWidget -from bitcoin_safe.gui.qt.export_data import ExportDataSimple -from bitcoin_safe.gui.qt.keystore_ui import ( - HardwareSignerInteractionWidget, - icon_for_label, -) -from bitcoin_safe.gui.qt.register_multisig import RegisterMultisigInteractionWidget -from bitcoin_safe.gui.qt.sync_tab import SyncTab -from bitcoin_safe.gui.qt.wizard_base import WizardBase -from bitcoin_safe.hardware_signers import HardwareSigners -from bitcoin_safe.html_utils import html_f, link -from bitcoin_safe.i18n import translate -from bitcoin_safe.signals import Signals, UpdateFilter, UpdateFilterReason -from bitcoin_safe.threading_manager import ThreadingManager -from bitcoin_safe.typestubs import TypedPyQtSignal -from bitcoin_safe.wallet import ProtoWallet, Wallet - -from ...signals import TypedPyQtSignalNo - -logger = logging.getLogger(__name__) - -import enum +from functools import partial from math import ceil from typing import Callable, Dict, List, Optional import bdkpython as bdk from bitcoin_qr_tools.data import Data +from bitcoin_usb.address_types import AddressTypes +from bitcoin_usb.usb_gui import USBGui from PyQt6.QtCore import QObject, QSize, Qt, pyqtSignal from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import ( + QCheckBox, QDialogButtonBox, QHBoxLayout, QInputDialog, @@ -78,17 +55,36 @@ QWidget, ) +from bitcoin_safe.execute_config import DEFAULT_LANG_CODE +from bitcoin_safe.gui.qt.bitcoin_quick_receive import BitcoinQuickReceive +from bitcoin_safe.gui.qt.data_tab_widget import DataTabWidget from bitcoin_safe.gui.qt.descriptor_ui import KeyStoreUIs from bitcoin_safe.gui.qt.dialogs import question_dialog +from bitcoin_safe.gui.qt.export_data import ExportDataSimple +from bitcoin_safe.gui.qt.keystore_ui import ( + HardwareSignerInteractionWidget, + icon_for_label, +) from bitcoin_safe.gui.qt.qt_wallet import QTWallet, QtWalletBase, SyncStatus +from bitcoin_safe.gui.qt.register_multisig import RegisterMultisigInteractionWidget +from bitcoin_safe.gui.qt.sync_tab import SyncTab from bitcoin_safe.gui.qt.tutorial_screenshots import ( ScreenshotsGenerateSeed, ScreenshotsTutorial, ScreenshotsViewSeed, ) +from bitcoin_safe.gui.qt.wizard_base import WizardBase +from bitcoin_safe.hardware_signers import HardwareSigners +from bitcoin_safe.html_utils import html_f, link +from bitcoin_safe.i18n import translate +from bitcoin_safe.signals import Signals, UpdateFilter, UpdateFilterReason +from bitcoin_safe.threading_manager import ThreadingManager +from bitcoin_safe.typestubs import TypedPyQtSignal +from bitcoin_safe.wallet import ProtoWallet, Wallet from ...pdfrecovery import TEXT_24_WORDS, make_and_open_pdf from ...pythonbdk_types import Recipient +from ...signals import TypedPyQtSignalNo from ...tx import TxUiInfos from ...util import Satoshis from .spinning_button import SpinningButton @@ -109,6 +105,8 @@ svg_widgets_hardware_signers, ) +logger = logging.getLogger(__name__) + class TutorialStep(enum.Enum): buy = enum.auto() @@ -166,7 +164,7 @@ def set_visibilities(self) -> None: self.button_yes_it_is_in_hist.setVisible( self.status in [self.TxSendStatus.finalized, self.TxSendStatus.sent] ) - self.button_create_tx_again.setVisible( + self.button_prefill_again.setVisible( self.status in [self.TxSendStatus.finalized, self.TxSendStatus.sent] ) @@ -180,15 +178,15 @@ def fill_tx(self) -> None: self._fill_tx() self.set_status(self.TxSendStatus.filled) + def _catch_tx(self, tx: bdk.Transaction) -> None: + self.set_status(self.TxSendStatus.finalized) + logger.info(f"tx {tx.txid()} is assumed to be the send test") + def create_tx(self) -> None: # before do _create_tx, setup a 1 time connection # so I can catch the tx and ensure that TxSendStatus == finalized # just in case the user clicked "go back" - def catch_tx(tx: bdk.Transaction) -> None: - self.set_status(self.TxSendStatus.finalized) - logger.info(f"tx {tx.txid()} is assumed to be the send test") - - one_time_signal_connection(self.signals.signal_broadcast_tx, catch_tx) + one_time_signal_connection(self.signals.signal_broadcast_tx, self._catch_tx) self._create_tx() self.set_status(self.TxSendStatus.finalized) @@ -201,6 +199,10 @@ def go_to_previous_index(self) -> None: self._go_to_previous_index() self.set_status(self.TxSendStatus.not_filled) + def _next_step_and_prefill(self): + self.go_to_next_index() + self.fill_tx() + def fill(self): self.setVisible(False) @@ -215,17 +217,13 @@ def fill(self): self.button_yes_it_is_in_hist = QPushButton() self.button_yes_it_is_in_hist.setVisible(False) - def next_step_and_prefill(): - self.go_to_next_index() - self.fill_tx() - - self.button_yes_it_is_in_hist.clicked.connect(next_step_and_prefill) + self.button_yes_it_is_in_hist.clicked.connect(self._next_step_and_prefill) self.addButton(self.button_yes_it_is_in_hist, QDialogButtonBox.ButtonRole.AcceptRole) - self.button_create_tx_again = QPushButton() - self.button_create_tx_again.setVisible(False) - self.button_create_tx_again.clicked.connect(self.fill_tx) - self.addButton(self.button_create_tx_again, QDialogButtonBox.ButtonRole.AcceptRole) + self.button_prefill_again = QPushButton() + self.button_prefill_again.setVisible(False) + self.button_prefill_again.clicked.connect(self.fill_tx) + self.addButton(self.button_prefill_again, QDialogButtonBox.ButtonRole.AcceptRole) self.tutorial_button_prev_step = QPushButton() self.tutorial_button_prev_step.clicked.connect(self.go_to_previous_index) @@ -237,7 +235,7 @@ def updateUi(self) -> None: self.tutorial_button_prefill.setText(self.tr("Prefill transaction fields")) self.button_create_tx.setText(self.tr("Create Transaction")) - self.button_create_tx_again.setText(self.tr("Prefill Transaction again")) + self.button_prefill_again.setText(self.tr("Retry")) self.button_yes_it_is_in_hist.setText(self.tr("Yes, I see the transaction in the history")) self.tutorial_button_prev_step.setText(self.tr("Previous Step")) @@ -251,7 +249,7 @@ def __init__( go_to_next_index: Callable, go_to_previous_index: Callable, floating_button_box: FloatingButtonBar, - signal_create_wallet, + signal_create_wallet: TypedPyQtSignal[str], max_test_fund: int, qt_wallet: QTWallet | None = None, ) -> None: @@ -348,36 +346,28 @@ def create(self) -> TutorialWidget: self.button_buy_q = QPushButton() self.button_buy_q.setIcon(QIcon(generated_hardware_signer_path("coldcard.svg"))) - self.button_buy_q.clicked.connect( - lambda: open_website("https://store.coinkite.com/promo/8BFF877000C34A86F410") - ) + self.button_buy_q.clicked.connect(self.website_open_coinkite) if HardwareSigners.q in ScreenshotsTutorial.enabled_hardware_signers: right_widget_layout.addWidget(self.button_buy_q) self.button_buy_q.setIconSize(QSize(32, 32)) # Set the icon size to 64x64 pixels self.button_buycoldcard = QPushButton() self.button_buycoldcard.setIcon(QIcon(generated_hardware_signer_path("coldcard.svg"))) - self.button_buycoldcard.clicked.connect( - lambda: open_website("https://store.coinkite.com/promo/8BFF877000C34A86F410") - ) + self.button_buycoldcard.clicked.connect(self.website_open_coinkite) if HardwareSigners.coldcard in ScreenshotsTutorial.enabled_hardware_signers: right_widget_layout.addWidget(self.button_buycoldcard) self.button_buycoldcard.setIconSize(QSize(32, 32)) # Set the icon size to 64x64 pixels self.button_buybitbox = QPushButton() self.button_buybitbox.setIcon(QIcon(generated_hardware_signer_path("bitbox02.svg"))) - self.button_buybitbox.clicked.connect( - lambda: open_website("https://shiftcrypto.ch/bitbox02/?ref=MOB4dk7gpm") - ) + self.button_buybitbox.clicked.connect(self.website_open_bitbox) self.button_buybitbox.setIconSize(QSize(45, 32)) # Set the icon size to 64x64 pixels if HardwareSigners.bitbox02 in ScreenshotsTutorial.enabled_hardware_signers: right_widget_layout.addWidget(self.button_buybitbox) self.button_buyjade = QPushButton() self.button_buyjade.setIcon(QIcon(generated_hardware_signer_path("jade.svg"))) - self.button_buyjade.clicked.connect( - lambda: open_website("https://store.blockstream.com/?code=XEocg5boS77D") - ) + self.button_buyjade.clicked.connect(self.website_open_jade) self.button_buyjade.setIconSize(QSize(45, 32)) # Set the icon size to 64x64 pixels if HardwareSigners.jade in ScreenshotsTutorial.enabled_hardware_signers: right_widget_layout.addWidget(self.button_buyjade) @@ -398,6 +388,15 @@ def create(self) -> TutorialWidget: self.updateUi() return tutorial_widget + def website_open_coinkite(self): + open_website("https://store.coinkite.com/promo/8BFF877000C34A86F410") + + def website_open_bitbox(self): + open_website("https://shiftcrypto.ch/bitbox02/?ref=MOB4dk7gpm") + + def website_open_jade(self): + open_website("https://store.blockstream.com/?code=XEocg5boS77D") + def updateUi(self) -> None: super().updateUi() self.label_buy.setText( @@ -581,6 +580,7 @@ def on_hwi_click(self, initalization_label="") -> None: try: result = self.usb_gui.get_fingerprint_and_xpub(key_origin=key_origin) except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") Message( str(e) + "\n\n" @@ -644,6 +644,27 @@ def updateUi(self) -> None: class ImportXpubs(BaseTab): + + def _callback(self, tutorial_widget: TutorialWidget) -> None: + self.refs.wallet_tabs.setCurrentWidget(self.refs.qtwalletbase.wallet_descriptor_tab) + tutorial_widget.synchronize_visiblity( + VisibilityOption(self.refs.wallet_tabs, on_focus_set_visible=bool(self.refs.qt_wallet)) + ) + + def _create_wallet(self) -> None: + if not self.keystore_uis: + return + + if not self.ask_if_can_proceed(): + return + + try: + self.keystore_uis.set_protowallet_from_keystore_ui() + self.refs.qtwalletbase.tutorial_index = self.refs.container.current_index() + 1 + self.refs.signal_create_wallet.emit(self.keystore_uis.protowallet.id) + except Exception as e: + caught_exception_message(e) + def create(self) -> TutorialWidget: widget = QWidget() @@ -665,27 +686,13 @@ def create(self) -> TutorialWidget: # this is used in TutorialStep.import_xpub self.keystore_uis = KeyStoreUIs( get_editable_protowallet=self.refs.qtwalletbase.get_editable_protowallet, - get_address_type=lambda: self.refs.qtwalletbase.get_editable_protowallet().address_type, + get_address_type=self.get_address_type, signals_min=self.refs.qtwalletbase.signals, ) self.set_current_signer(0) self.keystore_uis.setMovable(False) widget_layout.addWidget(self.keystore_uis) - def create_wallet() -> None: - if not self.keystore_uis: - return - - if not self.ask_if_can_proceed(): - return - - try: - self.keystore_uis.set_protowallet_from_keystore_ui() - self.refs.qtwalletbase.tutorial_index = self.refs.container.current_index() + 1 - self.refs.signal_create_wallet.emit() - except Exception as e: - caught_exception_message(e) - # hide the next button self.button_next.setHidden(True) # and add the prev signer button @@ -699,19 +706,13 @@ def create_wallet() -> None: # and add the create wallet button self.buttonbox_buttons.append(self.button_create_wallet) self.buttonbox.addButton(self.button_create_wallet, QDialogButtonBox.ButtonRole.AcceptRole) - self.button_create_wallet.clicked.connect(create_wallet) + self.button_create_wallet.clicked.connect(self._create_wallet) tutorial_widget = TutorialWidget( self.refs.container, widget, self.buttonbox, buttonbox_always_visible=False ) - def callback() -> None: - self.refs.wallet_tabs.setCurrentWidget(self.refs.qtwalletbase.wallet_descriptor_tab) - tutorial_widget.synchronize_visiblity( - VisibilityOption(self.refs.wallet_tabs, on_focus_set_visible=bool(self.refs.qt_wallet)) - ) - - tutorial_widget.set_callback(callback) + tutorial_widget.set_callback(partial(self._callback, tutorial_widget)) tutorial_widget.synchronize_visiblity( VisibilityOption(self.refs.wallet_tabs, on_focus_set_visible=bool(self.refs.qt_wallet)) ) @@ -722,6 +723,9 @@ def callback() -> None: self.updateUi() return tutorial_widget + def get_address_type(self): + return self.refs.qtwalletbase.get_editable_protowallet().address_type + def set_current_signer(self, value: int): if not self.keystore_uis: return @@ -798,6 +802,16 @@ def updateUi(self) -> None: class BackupSeed(BaseTab): + + def _do_pdf(self) -> None: + if not self.refs.qt_wallet: + Message(self.tr("Please complete the previous steps.")) + return + make_and_open_pdf( + self.refs.qt_wallet.wallet, + lang_code=self.refs.qtwalletbase.signals.get_current_lang_code() or DEFAULT_LANG_CODE, + ) + def create(self) -> TutorialWidget: widget = QWidget() @@ -818,16 +832,10 @@ def create(self) -> TutorialWidget: screenshots = ScreenshotsViewSeed() self.button_help = generate_help_button(screenshots, title="View seed words") - def do_pdf() -> None: - if not self.refs.qt_wallet: - Message(self.tr("Please complete the previous steps.")) - return - make_and_open_pdf(self.refs.qt_wallet.wallet, lang_code=self.refs.qtwalletbase.get_lang_code()) - buttonbox = QDialogButtonBox() self.custom_yes_button = QPushButton() self.custom_yes_button.setIcon(QIcon(icon_path("print.svg"))) - self.custom_yes_button.clicked.connect(do_pdf) + self.custom_yes_button.clicked.connect(self._do_pdf) self.custom_yes_button.clicked.connect(self.refs.go_to_next_index) buttonbox.addButton(self.custom_yes_button, QDialogButtonBox.ButtonRole.AcceptRole) self.custom_cancel_button = QPushButton() @@ -867,6 +875,31 @@ def updateUi(self) -> None: class ReceiveTest(BaseTab): + + def _on_sync_done(self, sync_status) -> None: + if not self.refs.qt_wallet: + return + utxos = self.refs.qt_wallet.wallet.get_all_utxos(include_not_mine=False) + self.check_button.setHidden(bool(utxos)) + self.next_button.setHidden(not bool(utxos)) + if utxos: + Message( + self.tr("Balance = {amount}").format( + amount=Satoshis(utxos[0].txout.value, self.refs.qt_wallet.wallet.network).str_with_unit() + ) + ) + + def _start_sync( + self, + ) -> None: + if not self.refs.qt_wallet: + Message(self.tr("No wallet setup yet"), type=MessageType.Error) + return + + self.check_button.set_enable_signal(self.refs.qtwalletbase.signal_after_sync) + one_time_signal_connection(self.refs.qtwalletbase.signal_after_sync, self._on_sync_done) + self.refs.qt_wallet.sync() + def create(self) -> TutorialWidget: widget = QWidget() @@ -914,31 +947,7 @@ def create(self) -> TutorialWidget: self.next_button.setHidden(True) - def on_sync_done(sync_status) -> None: - if not self.refs.qt_wallet: - return - utxos = self.refs.qt_wallet.wallet.get_all_utxos(include_not_mine=False) - self.check_button.setHidden(bool(utxos)) - self.next_button.setHidden(not bool(utxos)) - if utxos: - Message( - self.tr("Balance = {amount}").format( - amount=Satoshis( - utxos[0].txout.value, self.refs.qt_wallet.wallet.network - ).str_with_unit() - ) - ) - - def start_sync() -> None: - if not self.refs.qt_wallet: - Message(self.tr("No wallet setup yet"), type=MessageType.Error) - return - - self.check_button.set_enable_signal(self.refs.qtwalletbase.signal_after_sync) - one_time_signal_connection(self.refs.qtwalletbase.signal_after_sync, on_sync_done) - self.refs.qt_wallet.sync() - - self.check_button.clicked.connect(start_sync) + self.check_button.clicked.connect(self._start_sync) tutorial_widget = TutorialWidget( self.refs.container, widget, buttonbox, buttonbox_always_visible=False @@ -1091,6 +1100,13 @@ def updateUi(self) -> None: class RegisterMultisig(BaseTab): + + def _callback(self, tutorial_widget: TutorialWidget) -> None: + self.updateUi() + tutorial_widget.synchronize_visiblity( + VisibilityOption(self.refs.wallet_tabs, on_focus_set_visible=False) + ) + def create(self) -> TutorialWidget: widget = QWidget() @@ -1125,7 +1141,7 @@ def create(self) -> TutorialWidget: self.export_qr_widget.set_minimum_size_as_floating_window() # ui hardware_signer_interactions - self.hardware_signer_tabs = DataTabWidget(HardwareSignerInteractionWidget) + self.hardware_signer_tabs = DataTabWidget[HardwareSignerInteractionWidget]() widget_layout.addWidget(self.hardware_signer_tabs) for label in self.refs.qtwalletbase.get_keystore_labels(): @@ -1156,13 +1172,7 @@ def create(self) -> TutorialWidget: self.refs.container, widget, self.buttonbox, buttonbox_always_visible=False ) - def callback() -> None: - self.updateUi() - tutorial_widget.synchronize_visiblity( - VisibilityOption(self.refs.wallet_tabs, on_focus_set_visible=False) - ) - - tutorial_widget.set_callback(callback) + tutorial_widget.set_callback(partial(self._callback, tutorial_widget)) tutorial_widget.synchronize_visiblity( VisibilityOption(self.refs.wallet_tabs, on_focus_set_visible=False) ) @@ -1397,12 +1407,52 @@ def updateUi(self) -> None: class SendTest(BaseTab): - def __init__(self, test_label, test_number, tx_text, refs: TabInfo) -> None: - super().__init__(refs) + def __init__( + self, + test_label, + test_number, + tx_text, + refs: TabInfo, + threading_parent: ThreadingManager | None = None, + ) -> None: + super().__init__(refs, threading_parent=threading_parent) self.test_label = test_label self.test_number = test_number self.tx_text = tx_text + def _callback(self) -> None: + if not self.refs.qt_wallet: + return + if self.refs.qt_wallet.sync_status in [SyncStatus.unknown, SyncStatus.unsynced]: + logger.debug( + f"Skipping tutorial callback for send test, because {self.refs.qt_wallet.wallet.id} sync_status={ self.refs.qt_wallet.sync_status}" + ) + return + logger.debug(f"tutorial callback") + + # compare how many tx were already done , to the current test_number + def should_offer_skip() -> bool: + if not spend_txos: + return False + return len(spend_txos) >= self.test_number + 1 + + # offer to skip this step if it was spend from this wallet + txos = self.refs.qt_wallet.wallet.get_all_txos_dict(include_not_mine=False).values() + spend_txos = [txo for txo in txos if txo.is_spent_by_txid] + + if should_offer_skip(): + if question_dialog( + text=self.tr( + "You made {n} outgoing transactions already. Would you like to skip this spend test?" + ).format(n=len(spend_txos)), + title=self.tr("Skip spend test?"), + buttons=QMessageBox.StandardButton.No | QMessageBox.StandardButton.Yes, + ): + self.refs.go_to_next_index() + return + + self.refs.floating_button_box.fill_tx() + def create(self) -> TutorialWidget: widget = QWidget() @@ -1432,40 +1482,7 @@ def create(self) -> TutorialWidget: ) tutorial_widget.setMinimumHeight(30) - def callback() -> None: - if not self.refs.qt_wallet: - return - if self.refs.qt_wallet.sync_status in [SyncStatus.unknown, SyncStatus.unsynced]: - logger.debug( - f"Skipping tutorial callback for send test, because {self.refs.qt_wallet.wallet.id} sync_status={ self.refs.qt_wallet.sync_status}" - ) - return - logger.debug(f"tutorial callback") - - # compare how many tx were already done , to the current test_number - def should_offer_skip() -> bool: - if not spend_txos: - return False - return len(spend_txos) >= self.test_number + 1 - - # offer to skip this step if it was spend from this wallet - txos = self.refs.qt_wallet.wallet.get_all_txos_dict(include_not_mine=False).values() - spend_txos = [txo for txo in txos if txo.is_spent_by_txid] - - if should_offer_skip(): - if question_dialog( - text=self.tr( - "You made {n} outgoing transactions already. Would you like to skip this spend test?" - ).format(n=len(spend_txos)), - title=self.tr("Skip spend test?"), - buttons=QMessageBox.StandardButton.No | QMessageBox.StandardButton.Yes, - ): - self.refs.go_to_next_index() - return - - self.refs.floating_button_box.fill_tx() - - tutorial_widget.set_callback(callback) + tutorial_widget.set_callback(self._callback) tutorial_widget.synchronize_visiblity( VisibilityOption(self.refs.wallet_tabs, on_focus_set_visible=bool(self.refs.qt_wallet)) ) @@ -1499,7 +1516,7 @@ def updateUi(self) -> None: class Wizard(WizardBase): - signal_create_wallet: TypedPyQtSignalNo = pyqtSignal() # type: ignore + signal_create_wallet: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore # protowallet_id signal_step_change: TypedPyQtSignal[int] = pyqtSignal(int) # type: ignore def __init__( @@ -1514,6 +1531,7 @@ def __init__( signals_min=qtwalletbase.signals, threading_parent=qt_wallet if qt_wallet else qtwalletbase, ) # initialize with 3 steps (doesnt matter) + logger.debug(f"__init__ {self.__class__.__name__}") self.qtwalletbase = qtwalletbase self.qt_wallet = qt_wallet m, n = self.qtwalletbase.get_mn_tuple() @@ -1521,11 +1539,7 @@ def __init__( # floating_button_box self.floating_button_box = FloatingButtonBar( self.fill_tx, - ( - self.qt_wallet.uitx_creator.create_tx - if self.qt_wallet - else lambda: Message(self.tr("You must have an initilized wallet first")) - ), + (self.qt_wallet.uitx_creator.create_tx if self.qt_wallet else self.show_warning_not_initialized), self.go_to_next_index, self.go_to_previous_index, self.qtwalletbase.signals, @@ -1546,16 +1560,16 @@ def __init__( ) self.tab_generators: Dict[TutorialStep, BaseTab] = { - TutorialStep.buy: BuyHardware(refs=refs), - TutorialStep.sticker: StickerTheHardware(refs=refs), - TutorialStep.generate: GenerateSeed(refs=refs), - TutorialStep.import_xpub: ImportXpubs(refs=refs), - TutorialStep.backup_seed: BackupSeed(refs=refs), + TutorialStep.buy: BuyHardware(refs=refs, threading_parent=self), + TutorialStep.sticker: StickerTheHardware(refs=refs, threading_parent=self), + TutorialStep.generate: GenerateSeed(refs=refs, threading_parent=self), + TutorialStep.import_xpub: ImportXpubs(refs=refs, threading_parent=self), + TutorialStep.backup_seed: BackupSeed(refs=refs, threading_parent=self), } if n > 1: - self.tab_generators[TutorialStep.register] = RegisterMultisig(refs=refs) + self.tab_generators[TutorialStep.register] = RegisterMultisig(refs=refs, threading_parent=self) - self.tab_generators[TutorialStep.receive] = ReceiveTest(refs=refs) + self.tab_generators[TutorialStep.receive] = ReceiveTest(refs=refs, threading_parent=self) for test_number, tutoral_step in enumerate(self.get_send_tests_steps()): self.tab_generators[tutoral_step] = SendTest( @@ -1563,10 +1577,11 @@ def __init__( test_number=test_number, tx_text=self.tx_text(test_number), refs=refs, + threading_parent=self, ) - self.tab_generators[TutorialStep.distribute] = DistributeSeeds(refs=refs) - self.tab_generators[TutorialStep.sync] = LabelBackup(refs=refs) + self.tab_generators[TutorialStep.distribute] = DistributeSeeds(refs=refs, threading_parent=self) + self.tab_generators[TutorialStep.sync] = LabelBackup(refs=refs, threading_parent=self) self.wallet_tabs = wallet_tabs self.max_test_fund = max_test_fund @@ -1585,11 +1600,7 @@ def __init__( self.set_current_index(self.qtwalletbase.tutorial_index) # save after every step - def save(widget): - if self.qt_wallet: - self.qt_wallet.save() - - self.signal_set_current_widget.connect(save) + self.signal_set_current_widget.connect(self._save) self.signal_step_change.connect(self.qtwalletbase.set_tutorial_index) self.updateUi() @@ -1601,6 +1612,13 @@ def save(widget): self.on_utxo_update ) + def _save(self, widget): + if self.qt_wallet: + self.qt_wallet.save() + + def show_warning_not_initialized(self): + Message(self.tr("You must have an initilized wallet first"), type=MessageType.Warning) + def toggle_tutorial(self) -> None: if self.get_wallet_tutorial_index() is None: @@ -1850,3 +1868,20 @@ def updateUi(self) -> None: ) self.set_labels([labels[key] for key in self.tab_generators if key in labels]) + + def _clear_tab_generators(self): + for g in self.tab_generators.values(): + del g.refs + g.setParent(None) + self.tab_generators.clear() + + def close(self): + self.clear_widgets() + self.qtwalletbase.outer_layout.removeWidget(self.floating_button_box) + self.qtwalletbase.outer_layout.removeWidget(self) + self.floating_button_box.setParent(None) + self.floating_button_box.close() + self.widgets.clear() + self._clear_tab_generators() + self.setParent(None) + super().close() diff --git a/bitcoin_safe/gui/qt/wizard_base.py b/bitcoin_safe/gui/qt/wizard_base.py index 789b1ea..0b5a62c 100644 --- a/bitcoin_safe/gui/qt/wizard_base.py +++ b/bitcoin_safe/gui/qt/wizard_base.py @@ -28,6 +28,11 @@ import logging +from typing import List + +from bitcoin_safe.signal_tracker import SignalTools, SignalTracker +from bitcoin_safe.signals import SignalsMin +from bitcoin_safe.threading_manager import ThreadingManager logger = logging.getLogger(__name__) @@ -36,8 +41,47 @@ class WizardBase(StepProgressContainer): + def __init__( + self, + step_labels: List[str], + signals_min: SignalsMin, + current_index: int = 0, + collapsible_current_active=False, + clickable=True, + use_checkmark_icon=True, + parent=None, + sub_indices: List[int] | None = None, + use_resizing_stacked_widget=True, + threading_parent: ThreadingManager | None = None, + ) -> None: + super().__init__( + step_labels, + signals_min, + current_index, + collapsible_current_active, + clickable, + use_checkmark_icon, + parent, + sub_indices, + use_resizing_stacked_widget, + threading_parent, + ) + + self.signal_tracker = SignalTracker() + def set_visibilities(self) -> None: pass def toggle_tutorial(self) -> None: pass + + def deleterefrences(self): + pass + + def close(self): + + self.signal_tracker.disconnect_all() + SignalTools.disconnect_all_signals_from(self) + + self.setParent(None) + super().close() diff --git a/bitcoin_safe/gui/qt/wrappers.py b/bitcoin_safe/gui/qt/wrappers.py index 015ae9c..1a4dd02 100644 --- a/bitcoin_safe/gui/qt/wrappers.py +++ b/bitcoin_safe/gui/qt/wrappers.py @@ -27,6 +27,7 @@ # SOFTWARE. +from functools import partial from typing import Any, Callable, Optional from PyQt6.QtCore import pyqtBoundSignal @@ -44,7 +45,11 @@ def add_action( action = QAction(text=text, parent=self) if slot: if callable(slot): - action.triggered.connect(lambda: slot()) + action.triggered.connect( + partial( + slot, + ) + ) else: action.triggered.connect(slot) self.addAction(action) diff --git a/bitcoin_safe/hardware_signers.py b/bitcoin_safe/hardware_signers.py index d455123..76662a6 100644 --- a/bitcoin_safe/hardware_signers.py +++ b/bitcoin_safe/hardware_signers.py @@ -29,6 +29,9 @@ import logging from dataclasses import dataclass +from typing import List, Union + +from bitcoin_qr_tools.unified_encoder import QrExportType, QrExportTypes from bitcoin_safe.gui.qt.util import ( generated_hardware_signer_path, @@ -37,10 +40,6 @@ logger = logging.getLogger(__name__) -from typing import List, Union - -from bitcoin_qr_tools.unified_encoder import QrExportType, QrExportTypes - @dataclass class DescriptorExportType: diff --git a/bitcoin_safe/keystore.py b/bitcoin_safe/keystore.py index 3d99af2..dee2616 100644 --- a/bitcoin_safe/keystore.py +++ b/bitcoin_safe/keystore.py @@ -27,13 +27,10 @@ # SOFTWARE. +import copy import logging from typing import Any, Dict, List, Literal, Optional, Set -logger = logging.getLogger(__name__) - -import copy - import bdkpython as bdk from bitcoin_usb.address_types import ( AddressTypes, @@ -44,6 +41,8 @@ from .storage import BaseSaveableClass, SaveAllClass, filtered_for_init +logger = logging.getLogger(__name__) + class KeyStoreImporterType(SaveAllClass): def __init__( @@ -183,7 +182,8 @@ def is_seed_valid(cls, mnemonic: str) -> bool: try: bdk.Mnemonic.from_string(mnemonic) return True - except: + except Exception as e: + logger.debug(f"{cls.__name__}: {e}") return False @classmethod @@ -199,7 +199,8 @@ def is_xpub_valid(cls, xpub: str, network: bdk.Network) -> bool: ) return True - except: + except Exception as e: + logger.debug(f"{cls.__name__}: {e}") return False def clone(self, class_kwargs: Dict | None = None) -> "KeyStore": diff --git a/bitcoin_safe/logging_setup.py b/bitcoin_safe/logging_setup.py index 030b925..dd09524 100644 --- a/bitcoin_safe/logging_setup.py +++ b/bitcoin_safe/logging_setup.py @@ -123,6 +123,3 @@ def qt_message_handler(msg_type, context, message): def describe_os_version() -> str: return platform.platform() - - -setup_logging() diff --git a/bitcoin_safe/mempool.py b/bitcoin_safe/mempool.py index ab4cc25..847f320 100644 --- a/bitcoin_safe/mempool.py +++ b/bitcoin_safe/mempool.py @@ -27,11 +27,16 @@ # SOFTWARE. +import datetime import enum import logging from math import ceil from typing import Any, Dict, List, Optional, Tuple +import numpy as np +import requests +from PyQt6.QtCore import QObject, pyqtSignal + from bitcoin_safe.gui.qt.util import custom_exception_handler from bitcoin_safe.network_config import NetworkConfig from bitcoin_safe.signals import SignalsMin @@ -42,12 +47,6 @@ logger = logging.getLogger(__name__) -import datetime - -import numpy as np -import requests -from PyQt6.QtCore import QObject, pyqtSignal - feeLevels = [ 1, 2, @@ -196,7 +195,8 @@ def fetch_from_url(url: str, proxies: Dict | None, is_json=True) -> Optional[Any # If the request was unsuccessful, print the status code logger.error(f"Request failed with status code: {response.status_code}") return None - except: + except Exception as e: + logger.debug(str(e)) logger.error(f"fetch_json_from_url {url} failed") return None @@ -248,7 +248,7 @@ def __init__( "total_fee": 0, "fee_histogram": [], } - logger.debug(f"initialized {self}") + logger.debug(f"initialized {self.__class__.__name__}") def _empty_mempool_blocks(self) -> List[Dict[str, Any]]: return [ diff --git a/bitcoin_safe/network_config.py b/bitcoin_safe/network_config.py index c84ba11..6af18a3 100644 --- a/bitcoin_safe/network_config.py +++ b/bitcoin_safe/network_config.py @@ -29,16 +29,12 @@ import logging from dataclasses import dataclass -from urllib.parse import urlparse - -from packaging import version - -logger = logging.getLogger(__name__) - from typing import Any, Dict, Literal +from urllib.parse import urlparse import bdkpython as bdk import socks +from packaging import version from bitcoin_safe.pythonbdk_types import BlockchainType, CBFServerType from bitcoin_safe.storage import BaseSaveableClass, filtered_for_init @@ -46,6 +42,8 @@ from .html_utils import link from .i18n import translate +logger = logging.getLogger(__name__) + MIN_RELAY_FEE = 1 FEE_RATIO_HIGH_WARNING = 0.05 # warn user if fee/amount for on-chain tx is higher than this diff --git a/bitcoin_safe/pdf_statement.py b/bitcoin_safe/pdf_statement.py index a498e42..a574650 100644 --- a/bitcoin_safe/pdf_statement.py +++ b/bitcoin_safe/pdf_statement.py @@ -353,7 +353,7 @@ def open_pdf(self, filename: str) -> None: if os.path.exists(filename): xdg_open_file(Path(filename)) else: - logger.info("File not found!") + logger.info(translate("pdf", "File not found!")) def make_and_open_pdf_statement(wallet: Wallet, lang_code: str, label_sync_nsec: str | None = None) -> None: diff --git a/bitcoin_safe/pdfrecovery.py b/bitcoin_safe/pdfrecovery.py index 9d56b08..b7e5241 100644 --- a/bitcoin_safe/pdfrecovery.py +++ b/bitcoin_safe/pdfrecovery.py @@ -471,7 +471,7 @@ def open_pdf(self, filename: str) -> None: if os.path.exists(filename): xdg_open_file(Path(filename)) else: - logger.info("File not found!") + logger.info(translate("pdf", "File not found!")) def make_and_open_pdf(wallet: Wallet, lang_code: str) -> None: diff --git a/bitcoin_safe/psbt_util.py b/bitcoin_safe/psbt_util.py index 4444b7c..a0d61c6 100644 --- a/bitcoin_safe/psbt_util.py +++ b/bitcoin_safe/psbt_util.py @@ -38,12 +38,6 @@ from typing import Any, Dict, List, Tuple import bdkpython as bdk - -from .dynamic_lib_load import setup_libsecp256k1 - -setup_libsecp256k1() - - from bitcoin_usb.address_types import SimplePubKeyProvider from bitcoin_safe.util import hex_to_script, remove_duplicates_keep_order diff --git a/bitcoin_safe/pythonbdk_types.py b/bitcoin_safe/pythonbdk_types.py index bc1362e..be1354a 100644 --- a/bitcoin_safe/pythonbdk_types.py +++ b/bitcoin_safe/pythonbdk_types.py @@ -44,7 +44,8 @@ def is_address(a: str, network: bdk.Network) -> bool: try: bdk.Address(a, network=network) - except: + except Exception as e: + logger.debug(str(e)) return False return True @@ -391,7 +392,8 @@ def from_dump_migration(cls, dct: Dict[str, Any]) -> Dict[str, Any]: def robust_address_str_from_script(script_pubkey: bdk.Script, network, on_error_return_hex=True) -> str: try: return bdk.Address.from_script(script_pubkey, network).as_string() - except: + except Exception as e: + logger.debug(str(e)) if on_error_return_hex: return serialized_to_hex(script_pubkey.to_bytes()) else: diff --git a/bitcoin_safe/signal_tracker.py b/bitcoin_safe/signal_tracker.py new file mode 100644 index 0000000..36c74db --- /dev/null +++ b/bitcoin_safe/signal_tracker.py @@ -0,0 +1,164 @@ +# +# 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, List, Tuple, TypeVar + +from PyQt6.QtCore import QObject, pyqtBoundSignal + +from bitcoin_safe.signals import SignalFunction, SingularSignalFunction + +from .typestubs import TypedPyQtSignal, TypedPyQtSignalNo + +logger = logging.getLogger(__name__) + + +T0 = TypeVar("T0", bound=SingularSignalFunction) +T1 = TypeVar("T1", bound=SignalFunction) +T2 = TypeVar("T2", bound=pyqtBoundSignal) +T3 = TypeVar("T3", bound=TypedPyQtSignalNo) +T4 = TypeVar("T4", bound=TypedPyQtSignal) + + +class SignalTools: + @classmethod + def disconnect_all_signals_from(cls, object_with_bound_signals: QObject) -> None: + """Finds any qtBoundSignal attached to object_with_bound_signals + and removes all connections of them + + Args: + object_with_bound_signals (Any): _description_ + """ + + def discon_sig(signal: pyqtBoundSignal | TypedPyQtSignalNo | TypedPyQtSignal): + """ + Disconnect only breaks one connection at a time, + so loop to be safe. + """ + while True: + try: + signal.disconnect() + except TypeError: + break + return + + for signal_name in dir(object_with_bound_signals): + if signal_name in ["destroyed"]: + continue + signal = getattr(object_with_bound_signals, signal_name) + if isinstance(signal, pyqtBoundSignal): + discon_sig(signal) + + @classmethod + def connect_signal( + cls, signal: T0 | T1 | T2 | T3 | T4, f: Callable, **kwargs + ) -> Tuple[T0 | T1 | T2 | T3 | T4, Callable]: + signal.connect(f, **kwargs) + return (signal, f) + + @classmethod + def connect_signal_and_append( + cls, + connected_signals: List[ + Tuple[ + pyqtBoundSignal + | SingularSignalFunction + | SignalFunction + | TypedPyQtSignalNo + | TypedPyQtSignal, + Callable, + ] + ], + signal: ( + pyqtBoundSignal | SingularSignalFunction | SignalFunction | TypedPyQtSignalNo | TypedPyQtSignal + ), + f: Callable, + **kwargs, + ) -> None: + signal.connect(f, **kwargs) + connected_signals.append((signal, f)) + + @classmethod + def disconnect_signal( + cls, + signal: ( + pyqtBoundSignal | SingularSignalFunction | SignalFunction | TypedPyQtSignalNo | TypedPyQtSignal + ), + f: Callable, + ) -> None: + try: + signal.disconnect(f) + except: + logger.debug(f"Could not disconnect {signal=} from {f=}") + + @classmethod + def disconnect_signals( + cls, + connected_signals: List[ + Tuple[ + pyqtBoundSignal + | SingularSignalFunction + | SignalFunction + | TypedPyQtSignalNo + | TypedPyQtSignal, + Callable, + ] + ], + ) -> None: + while connected_signals: + signal, f = connected_signals.pop() + cls.disconnect_signal(signal=signal, f=f) + + +class SignalTracker: + def __init__(self) -> None: + self._connected_signals: List[ + Tuple[ + SignalFunction + | SingularSignalFunction + | pyqtBoundSignal + | TypedPyQtSignalNo + | TypedPyQtSignal, + Callable, + ] + ] = [] + + def connect( + self, + signal: ( + SignalFunction | SingularSignalFunction | pyqtBoundSignal | TypedPyQtSignalNo | TypedPyQtSignal + ), + f: Callable, + **kwargs, + ) -> None: + signal.connect(f, **kwargs) + self._connected_signals.append((signal, f)) + + def disconnect_all(self): + SignalTools.disconnect_signals(self._connected_signals) diff --git a/bitcoin_safe/signals.py b/bitcoin_safe/signals.py index 7612ae2..40c65e5 100644 --- a/bitcoin_safe/signals.py +++ b/bitcoin_safe/signals.py @@ -50,6 +50,7 @@ from bitcoin_nostr_chat.signals_min import SignalsMin as NostrSignalsMin from PyQt6.QtCore import pyqtSignal +from bitcoin_safe.category_info import CategoryInfo from bitcoin_safe.pythonbdk_types import Balance, OutPoint from .typestubs import TypedPyQtSignal, TypedPyQtSignalNo @@ -151,7 +152,8 @@ def emit(self, *args, slot_name=None, **kwargs) -> Dict[str, T]: name += f" with key={key}" if key else "" try: responses[key] = slot(*args, **kwargs) - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") logger.warning( f"{slot} with key {key} caused an exception. {slot} with key {key} could not be called, perhaps because the object doesnt exisst anymore. The slot will be deleted." ) @@ -177,6 +179,12 @@ def connect(self, slot: Callable[[], T], slot_name=None) -> None: else: raise Exception("Not allowed to add a second listener to this signal.") + def disconnect(self, slot: Callable[[], T]) -> None: + if not self.signal_f.slots: + return + else: + self.signal_f.disconnect(slot) + def emit(self, *args, **kwargs) -> T | None: responses = self.signal_f.emit(*args, **kwargs) if not responses: @@ -190,6 +198,10 @@ def __call__(self, *args, **kwargs) -> T | None: class SignalsMin(NostrSignalsMin): close_all_video_widgets: TypedPyQtSignalNo = pyqtSignal() # type: ignore + def __init__(self) -> None: + super().__init__() + self.get_current_lang_code = SingularSignalFunction[str](name="get_lang_code") + class WalletSignals(SignalsMin): updated: TypedPyQtSignal[UpdateFilter] = pyqtSignal(UpdateFilter) # type: ignore @@ -208,7 +220,8 @@ class WalletSignals(SignalsMin): def __init__(self) -> None: super().__init__() - self.get_display_balance = SignalFunction[Balance](name="get_display_balance") + self.get_display_balance = SingularSignalFunction[Balance](name="get_display_balance") + self.get_category_infos = SingularSignalFunction[list[CategoryInfo]](name="get_category_infos") class Signals(SignalsMin): @@ -241,6 +254,7 @@ class Signals(SignalsMin): open_wallet: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore # str= filepath add_qt_wallet: "TypedPyQtSignal[QTWallet, str | None, str | None]" = pyqtSignal(object, object, object) # type: ignore # object = qt_wallet, file_path, password close_qt_wallet: TypedPyQtSignal[str] = pyqtSignal(str) # type: ignore # str = wallet_id + signal_set_tab_properties: TypedPyQtSignal[str, str, str] = pyqtSignal(str, str, str) # type: ignore # wallet_id, icon: icon_name, tooltip: str | None request_manual_sync: TypedPyQtSignalNo = pyqtSignal() # type: ignore signal_broadcast_tx: TypedPyQtSignal[bdk.Transaction] = pyqtSignal(bdk.Transaction) # type: ignore diff --git a/bitcoin_safe/signature_manager.py b/bitcoin_safe/signature_manager.py index 5365e42..9f0b7e7 100644 --- a/bitcoin_safe/signature_manager.py +++ b/bitcoin_safe/signature_manager.py @@ -35,6 +35,7 @@ import shutil import subprocess from dataclasses import dataclass +from functools import partial from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union @@ -126,7 +127,7 @@ class GitHubAssetDownloader: def __init__(self, repository: str, proxies: Dict | None) -> None: self.repository = repository self.proxies = proxies - logger.debug(f"initialized {self}") + logger.debug(f"initialized {self.__class__.__name__}") def _get_assets(self, api_url) -> List[Asset]: try: @@ -222,7 +223,7 @@ def verify_manifest_hashes(manifest_file: Path) -> bool: # Compute the file's SHA-256 hash file_hash = hashlib.sha256() with open(file_path, "rb") as f: - for chunk in iter(lambda: f.read(4096), b""): + for chunk in iter(partial(f.read, 4096), b""): file_hash.update(chunk) # Compare the computed hash with the expected hash @@ -288,6 +289,7 @@ def _verify_file( # 1 single bad signature creates a False result return bool(good_signatures) and not bool(bad_signatures) except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") logger.error(f"Verification failed: {e}") return False diff --git a/bitcoin_safe/signer.py b/bitcoin_safe/signer.py index c613211..5aefb9b 100644 --- a/bitcoin_safe/signer.py +++ b/bitcoin_safe/signer.py @@ -30,20 +30,6 @@ import logging from typing import List -from bitcoin_safe.gui.qt.dialogs import question_dialog -from bitcoin_safe.gui.qt.util import Message, MessageType, caught_exception_message -from bitcoin_safe.i18n import translate -from bitcoin_safe.psbt_util import PubKeyInfo -from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo - -from .dynamic_lib_load import setup_libsecp256k1 -from .gui.qt.dialog_import import ImportDialog - -setup_libsecp256k1() - - -logger = logging.getLogger(__name__) - import bdkpython as bdk from bitcoin_qr_tools.data import Data, DataType from bitcoin_qr_tools.gui.bitcoin_video_widget import BitcoinVideoWidget @@ -52,10 +38,19 @@ from bitcoin_usb.usb_gui import USBGui from PyQt6.QtCore import QObject, pyqtSignal +from bitcoin_safe.gui.qt.dialogs import question_dialog +from bitcoin_safe.gui.qt.util import Message, MessageType, caught_exception_message +from bitcoin_safe.i18n import translate +from bitcoin_safe.psbt_util import PubKeyInfo +from bitcoin_safe.typestubs import TypedPyQtSignal, TypedPyQtSignalNo + +from .gui.qt.dialog_import import ImportDialog from .keystore import KeyStoreImporterTypes from .util import tx_of_psbt_to_hex, tx_to_hex from .wallet import Wallet +logger = logging.getLogger(__name__) + class AbstractSignatureImporter(QObject): signal_signature_added: TypedPyQtSignal[bdk.PartiallySignedTransaction] = pyqtSignal(bdk.PartiallySignedTransaction) # type: ignore @@ -357,6 +352,7 @@ def sign(self, psbt: bdk.PartiallySignedTransaction, sign_options: bdk.SignOptio if signed_psbt: self.scan_result_callback(psbt, Data.from_psbt(signed_psbt, network=self.network)) except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") if "multisig" in str(e).lower(): question_dialog( self.tr( diff --git a/bitcoin_safe/simple_mailer.py b/bitcoin_safe/simple_mailer.py index 8c73504..d9e81e5 100755 --- a/bitcoin_safe/simple_mailer.py +++ b/bitcoin_safe/simple_mailer.py @@ -49,6 +49,7 @@ def compose_email( # Attempt to use the native OS command to open the email client open_mailto_link(mailto_link) except Exception as e: + logger.debug(str(e)) logger.debug(f"Failed to open the default email client using the OS native command: {e}") logger.debug("Attempting to open using webbrowser module...") # If the native OS command fails, fall back to using the webbrowser module diff --git a/bitcoin_safe/storage.py b/bitcoin_safe/storage.py index 1392015..bb3d57b 100644 --- a/bitcoin_safe/storage.py +++ b/bitcoin_safe/storage.py @@ -234,8 +234,8 @@ def save(self, filename: Union[Path, str], password: Optional[str] = None): password=password, ) - def __str__(self) -> str: - return self.dumps() + # def __str__(self) -> str: + # return self.dumps() def dumps(self, indent=None) -> str: "Returns the json representation (recursively)" diff --git a/bitcoin_safe/threading_manager.py b/bitcoin_safe/threading_manager.py index 0002cbd..0df03a2 100644 --- a/bitcoin_safe/threading_manager.py +++ b/bitcoin_safe/threading_manager.py @@ -102,7 +102,7 @@ def thread_name(self) -> str | None: @thread_name.setter def thread_name(self, value: str | None): - logger.debug(f"setting thread_name of {self} to {value}") + logger.debug(f"setting thread_name of {self.__class__.__name__} to {value}") self._thread_name = value def __str__(self) -> str: @@ -171,7 +171,8 @@ def my_quit(self): self.quit() self.wait() self.signal_stop_threat.emit(self.thread_name if self.thread_name else "") - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") logger.error(f"An error during the shutdown of {self.thread_name}") @@ -203,7 +204,7 @@ def add_and_start( class ThreadingManager: def __init__( - self, threading_parent: "ThreadingManager" = None, threading_manager_name=None, **kwargs # type: ignore + self, threading_parent: "ThreadingManager|None" = None, threading_manager_name=None, **kwargs ) -> None: super().__init__(**kwargs) self._taskthreads: deque[TaskThread] = deque() @@ -250,7 +251,7 @@ def stop_and_wait_all(self): taskthread.stop() def end_threading_manager(self): - logger.debug(f"end_threading_manager of {self}") + logger.debug(f"end_threading_manager of {self.__class__.__name__}") self.stop_and_wait_all() if self.threading_parent and self in self.threading_parent.threading_manager_children: diff --git a/bitcoin_safe/tx.py b/bitcoin_safe/tx.py index 521fee4..12188fb 100644 --- a/bitcoin_safe/tx.py +++ b/bitcoin_safe/tx.py @@ -28,6 +28,9 @@ import logging +from typing import Any, Dict, List, Optional, Tuple + +import bdkpython as bdk from bitcoin_safe.mempool import MempoolData from bitcoin_safe.psbt_util import FeeInfo @@ -43,10 +46,6 @@ logger = logging.getLogger(__name__) -from typing import Any, Dict, List, Optional, Tuple - -import bdkpython as bdk - def short_tx_id(txid: str) -> str: return f"{txid[:4]}...{txid[-4:]}" diff --git a/bitcoin_safe/util.py b/bitcoin_safe/util.py index 8f110d6..f8277a9 100644 --- a/bitcoin_safe/util.py +++ b/bitcoin_safe/util.py @@ -110,7 +110,7 @@ def is_int(a: Any) -> bool: try: int(a) - except: + except Exception: return False return True @@ -119,7 +119,8 @@ def path_to_rel_home_path(path: Union[Path, str]) -> Path: try: return Path(path).relative_to(Path.home()) - except: + except Exception as e: + logger.debug(str(e)) return Path(path) @@ -910,3 +911,7 @@ def qbytearray_to_str(a: QByteArray) -> str: def str_to_qbytearray(s: str) -> QByteArray: return QByteArray(s.encode()) # type: ignore[call-overload] + + +def unique_elements(iterable: Iterable): + return list(dict.fromkeys(iterable)) diff --git a/bitcoin_safe/wallet.py b/bitcoin_safe/wallet.py index 506874d..2e89b9e 100644 --- a/bitcoin_safe/wallet.py +++ b/bitcoin_safe/wallet.py @@ -28,21 +28,13 @@ import functools +import json import logging import os import random -from time import time - -from bitcoin_safe.network_config import ProxyInfo, clean_electrum_url -from bitcoin_safe.psbt_util import FeeInfo - -from .signals import Signals, UpdateFilter - -logger = logging.getLogger(__name__) - -import json from collections import defaultdict from threading import Lock +from time import time from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple import bdkpython as bdk @@ -52,12 +44,16 @@ from bitcoin_usb.software_signer import derive as software_signer_derive from packaging import version +from bitcoin_safe.network_config import ProxyInfo, clean_electrum_url +from bitcoin_safe.psbt_util import FeeInfo + from .config import MIN_RELAY_FEE, UserConfig from .descriptors import AddressType, MultipathDescriptor, get_default_address_type from .i18n import translate from .keystore import KeyStore from .labels import Labels, LabelType from .pythonbdk_types import * +from .signals import Signals, UpdateFilter from .storage import BaseSaveableClass, filtered_for_init from .tx import TxBuilderInfos, TxUiInfos from .util import ( @@ -71,6 +67,8 @@ time_logger, ) +logger = logging.getLogger(__name__) + class InconsistentBDKState(Exception): pass @@ -209,6 +207,37 @@ def __init__( def get_mn_tuple(self) -> Tuple[int, int]: return self.threshold, len(self.keystores) + def get_relevant_differences(self, other_wallet: "ProtoWallet") -> Set[str]: + "Compares the relevant entries like keystores" + differences = set() + + if self.id != other_wallet.id: + differences.add("id") + if self.network != other_wallet.network: + differences.add("network") + + if len(self.keystores) != len(other_wallet.keystores): + differences.add("keystores") + + for keystore, other_keystore in zip(self.keystores, other_wallet.keystores): + if not keystore: + if not other_keystore: + continue + elif not keystore and other_keystore: + differences.add("keystores") + continue + if keystore: + if not other_keystore: + differences.add("keystores") + continue + elif not keystore.is_equal(other_keystore): + differences.add("keystores") + + return differences + + def is_essentially_equal(self, other_wallet: "ProtoWallet") -> bool: + return not self.get_relevant_differences(other_wallet) + @classmethod def from_dump(cls, dct: Dict, class_kwargs: Dict | None = None) -> "ProtoWallet": super()._from_dump(dct, class_kwargs=class_kwargs) @@ -455,7 +484,8 @@ def get_address_of_txout(self, txout: TxOut) -> Optional[str]: # this can happen if it is an input of a coinbase TX try: return bdk.Address.from_script(txout.script_pubkey, self.network()).as_string() - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") return None else: return bdk.Address.from_script(txout.script_pubkey, self.network()).as_string() @@ -887,6 +917,7 @@ def sync(self, progress: Optional[bdk.Progress] | None = None) -> None: logger.debug(f"{self.id} self.bdkwallet.sync in { time()-start_time}s") logger.info(f"Wallet balance is: { self.bdkwallet.get_balance().__dict__ }") except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") logger.error(f"{self.id} error syncing wallet {self.id}") raise e @@ -1416,7 +1447,8 @@ def get_height_no_cache(self) -> int: # update the cached height try: self._blockchain_height = self.blockchain.get_height() - except: + except Exception as e: + logger.debug(f"{self.__class__.__name__}: {e}") logger.error(f"Could not fetch self.blockchain.get_height()") return self._blockchain_height @@ -1849,6 +1881,21 @@ def get_ema_fee_rate(self, n: int = 10, default=MIN_RELAY_FEE) -> float: return calculate_ema(fee_rates, n=n, weights=weights) + def get_category_python_utxo_dict(self) -> Dict[str, List[PythonUtxo]]: + category_python_utxo_dict: Dict[str, List[PythonUtxo]] = {} + + for python_utxo in self.get_all_utxos(): + category = self.labels.get_category(python_utxo.address) + if not category: + continue + if category not in category_python_utxo_dict: + category_python_utxo_dict[category] = [] + category_python_utxo_dict[category].append(python_utxo) + return category_python_utxo_dict + + def close(self) -> None: + pass + ########### # Functions that operatate on signals.get_wallets().values() diff --git a/poetry.lock b/poetry.lock index 3279e80..e1c14f1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -85,13 +85,13 @@ requests = ">=2.31.0,<3.0.0" [[package]] name = "bitcoin-qr-tools" -version = "1.0.3" +version = "1.0.4" description = "Python bitcoin qr reader and generator" optional = false python-versions = "<3.13,>=3.9" files = [ - {file = "bitcoin_qr_tools-1.0.3-py3-none-any.whl", hash = "sha256:b87d840a677e63ef96e9ceb7bef08c8187b300a59321180aae42697c043add3d"}, - {file = "bitcoin_qr_tools-1.0.3.tar.gz", hash = "sha256:efc305beb2d6b77b2b25888d8d96e5e6a7cc960c037edb9138bdb170aa875260"}, + {file = "bitcoin_qr_tools-1.0.4-py3-none-any.whl", hash = "sha256:cbafb868b4e9abb54d56f8dd7184c07ed1dbdc365866b76b69adf5fda063f112"}, + {file = "bitcoin_qr_tools-1.0.4.tar.gz", hash = "sha256:a8db69df84811d3b8b9f976f5b02cb70553818dc99d82f4d8c3f48e10badc5e1"}, ] [package.dependencies] @@ -110,13 +110,13 @@ segno = "1.6.1" [[package]] name = "bitcoin-usb" -version = "0.7.4" +version = "0.7.6" description = "Wrapper around hwi, such that one can sign bdk PSBTs directly" optional = false python-versions = "<3.13,>=3.8.1" files = [ - {file = "bitcoin_usb-0.7.4-py3-none-any.whl", hash = "sha256:9c07bc1ddcc93c3048b76c078b124ec903514ee5fb2324c1eca665dd53bf69d5"}, - {file = "bitcoin_usb-0.7.4.tar.gz", hash = "sha256:8de44e645ed29f8149abf80cc2d97342d0696169666f4f25e12f9c68d59c64c8"}, + {file = "bitcoin_usb-0.7.6-py3-none-any.whl", hash = "sha256:a43c3c2ac73c073e659790352e853067cd0812bb75e1f74f673e2a46df64c13c"}, + {file = "bitcoin_usb-0.7.6.tar.gz", hash = "sha256:4382fad72b7191131cdfe7cde47248d87a07d8a8072753d476685065817fa994"}, ] [package.dependencies] @@ -234,13 +234,13 @@ test = ["coverage (>=7)", "hypothesis", "pytest"] [[package]] name = "certifi" -version = "2024.12.14" +version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] @@ -791,61 +791,61 @@ typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "fonttools" -version = "4.55.6" +version = "4.55.8" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.55.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:57d55fc965e5dd20c8a60d880e0f43bafb506be87af0b650bdc42591e41e0d0d"}, - {file = "fonttools-4.55.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:127999618afe3a2490fad54bab0650c5fbeab1f8109bdc0205f6ad34306deb8b"}, - {file = "fonttools-4.55.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3226d40cb92787e09dcc3730f54b3779dfe56bdfea624e263685ba17a6faac4"}, - {file = "fonttools-4.55.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e82772f70b84e17aa36e9f236feb2a4f73cb686ec1e162557a36cf759d1acd58"}, - {file = "fonttools-4.55.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a632f85bd73e002b771bcbcdc512038fa5d2e09bb18c03a22fb8d400ea492ddf"}, - {file = "fonttools-4.55.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:791e0cf862cdd3a252df395f1bb5f65e3a760f1da3c7ce184d0f7998c266614d"}, - {file = "fonttools-4.55.6-cp310-cp310-win32.whl", hash = "sha256:94f7f2c5c5f3a6422e954ecb6d37cc363e27d6f94050a7ed3f79f12157af6bb2"}, - {file = "fonttools-4.55.6-cp310-cp310-win_amd64.whl", hash = "sha256:2d15e02b93a46982a8513a208e8f89148bca8297640527365625be56151687d0"}, - {file = "fonttools-4.55.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0879f99eabbf2171dfadd9c8c75cec2b7b3aa9cd1f3955dd799c69d60a5189ef"}, - {file = "fonttools-4.55.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d77d83ca77a4c3156a2f4cbc7f09f5a8503795da658fa255b987ad433a191266"}, - {file = "fonttools-4.55.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07478132407736ee5e54f9f534e73923ae28e9bb6dba17764a35e3caf7d7fea3"}, - {file = "fonttools-4.55.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c06fbc2fd76b9bab03eddfd8aa9fb7c0981d314d780e763c80aa76be1c9982"}, - {file = "fonttools-4.55.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:09ed667c4753e1270994e5398cce8703e6423c41702a55b08f843b2907b1be65"}, - {file = "fonttools-4.55.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ee6ed68af8d57764d69da099db163aaf37d62ba246cfd42f27590e3e6724b55"}, - {file = "fonttools-4.55.6-cp311-cp311-win32.whl", hash = "sha256:9f99e7876518b2d059a9cc67c506168aebf9c71ac8d81006d75e684222f291d2"}, - {file = "fonttools-4.55.6-cp311-cp311-win_amd64.whl", hash = "sha256:3aa6c684007723895aade9b2fe76d07008c9dc90fd1ef6c310b3ca9c8566729f"}, - {file = "fonttools-4.55.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:51120695ee13001533e50abd40eec32c01b9c6f44c5567db38a7acd3eedcd19d"}, - {file = "fonttools-4.55.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:76ac5a595f86892b49ba86ba2e46185adc76328ce6eff0583b30e5c3ab02a914"}, - {file = "fonttools-4.55.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b7535a5ac386e549e2b00b34c59b53f805e2423000676723b6867df3c10df04"}, - {file = "fonttools-4.55.6-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c42009177d3690894288082d5e3dac6bdc9f5d38e25054535e341a19cf5183a4"}, - {file = "fonttools-4.55.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:88f74bc19dbab3dee6a00ca67ca54bb4793e44ff0c4dcf1fa61d68651ae3fa0a"}, - {file = "fonttools-4.55.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bc6f58976ffc19fe1630119a2736153b66151d023c6f30065f31c9e8baed1303"}, - {file = "fonttools-4.55.6-cp312-cp312-win32.whl", hash = "sha256:4259159715142c10b0f4d121ef14da3fa6eafc719289d9efa4b20c15e57fef82"}, - {file = "fonttools-4.55.6-cp312-cp312-win_amd64.whl", hash = "sha256:d91fce2e9a87cc0db9f8042281b6458f99854df810cfefab2baf6ab2acc0f4b4"}, - {file = "fonttools-4.55.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9394813cc73fa22c5413ec1c5745c0a16f68dd2b890f7c55eaba5cb40187ed55"}, - {file = "fonttools-4.55.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ac817559a7d245454231374e194b4e457dca6fefa5b52af466ab0516e9a09c6e"}, - {file = "fonttools-4.55.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34405f1314f1e88b1877a9f9e497fe45190e8c4b29a6c7cd85ed7f666a57d702"}, - {file = "fonttools-4.55.6-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af5469bbf555047efd8752d85faeb2a3510916ddc6c50dd6fb168edf1677408f"}, - {file = "fonttools-4.55.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a8004a19195eb8a8a13de69e26ec9ed60a5bc1fde336d0021b47995b368fac9"}, - {file = "fonttools-4.55.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:73a4aaf672e7b2265c6354a69cbbadf71b7f3133ecb74e98fec4c67c366698a3"}, - {file = "fonttools-4.55.6-cp313-cp313-win32.whl", hash = "sha256:73bdff9c44d36c57ea84766afc20517eda0c9bb1571b4a09876646264bd5ff3b"}, - {file = "fonttools-4.55.6-cp313-cp313-win_amd64.whl", hash = "sha256:132fa22be8a99784de8cb171b30425a581f04a40ec1c05183777fb2b1fe3bac9"}, - {file = "fonttools-4.55.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8398928acb8a57073606feb9a310682d4a7e2d7536f2c61719261f4c0974504c"}, - {file = "fonttools-4.55.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2f78ebfdef578d4db7c44bc207ac5f9a5c1f22c9db606460dcc8ad48e183338"}, - {file = "fonttools-4.55.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fb545f3a4ebada908fa717ec732277de18dd10161f03ee3b3144d34477804de"}, - {file = "fonttools-4.55.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1062daa0390b32bfd062ded2b450db9e9cf10e5a9919561c13f535e818b1952b"}, - {file = "fonttools-4.55.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:860ab9ed3f9e088d3bdb77b9074e656635f173b039e77d550b603cba052a0dca"}, - {file = "fonttools-4.55.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:03701e7de70c71eb5965cb200986b0c11dfa3cf8e843e4f517ee30a0f43f0a25"}, - {file = "fonttools-4.55.6-cp38-cp38-win32.whl", hash = "sha256:f66561fbfb75785d06513b8025a50be37bf970c3c413e87581cc6eff10bc78f1"}, - {file = "fonttools-4.55.6-cp38-cp38-win_amd64.whl", hash = "sha256:edf159a8f1e48dc4683a715b36da76dd2f82954b16bfe11a215d58e963d31cfc"}, - {file = "fonttools-4.55.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61aa1997c520bee4cde14ffabe81efc4708c500c8c81dce37831551627a2be56"}, - {file = "fonttools-4.55.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7954ea66a8d835f279c17d8474597a001ddd65a2c1ca97e223041bfbbe11f65e"}, - {file = "fonttools-4.55.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f4e88f15f5ed4d2e4bdfcc98540bb3987ae25904f9be304be9a604e7a7050a1"}, - {file = "fonttools-4.55.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d419483a6295e83cabddb56f1c7b7bfdc8169de2fcb5c68d622bd11140355f9"}, - {file = "fonttools-4.55.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:acc74884afddc2656bffc50100945ff407574538c152931c402fccddc46f0abc"}, - {file = "fonttools-4.55.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a55489c7e9d5ea69690a2afad06723c3d0c48c6d276a25391ea97cb31a16b37c"}, - {file = "fonttools-4.55.6-cp39-cp39-win32.whl", hash = "sha256:8c9de8d16d02ecc8b65e3f3d2d1e3002be2c4a3f094d580faf76d7f768bd45fe"}, - {file = "fonttools-4.55.6-cp39-cp39-win_amd64.whl", hash = "sha256:471961af7a4b8461fac0c8ee044b4986e6fe3746d4c83a1aacbdd85b4eb53f93"}, - {file = "fonttools-4.55.6-py3-none-any.whl", hash = "sha256:d20ab5a78d0536c26628eaadba661e7ae2427b1e5c748a0a510a44d914e1b155"}, - {file = "fonttools-4.55.6.tar.gz", hash = "sha256:1beb4647a0df5ceaea48015656525eb8081af226fe96554089fd3b274d239ef0"}, + {file = "fonttools-4.55.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d11600f5343092697d7434f3bf77a393c7ae74be206fe30e577b9a195fd53165"}, + {file = "fonttools-4.55.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c96f2506ce1a0beeaa9595f9a8b7446477eb133f40c0e41fc078744c28149f80"}, + {file = "fonttools-4.55.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b5f05ef72e846e9f49ccdd74b9da4309901a4248434c63c1ee9321adcb51d65"}, + {file = "fonttools-4.55.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba45b637da80a262b55b7657aec68da2ac54b8ae7891cd977a5dbe5fd26db429"}, + {file = "fonttools-4.55.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edcffaeadba9a334c1c3866e275d7dd495465e7dbd296f688901bdbd71758113"}, + {file = "fonttools-4.55.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b9f9fce3c9b2196e162182ec5db8af8eb3acd0d76c2eafe9fdba5f370044e556"}, + {file = "fonttools-4.55.8-cp310-cp310-win32.whl", hash = "sha256:f089e8da0990cfe2d67e81d9cf581ff372b48dc5acf2782701844211cd1f0eb3"}, + {file = "fonttools-4.55.8-cp310-cp310-win_amd64.whl", hash = "sha256:01ea3901b0802fc5f9e854f5aeb5bc27770dd9dd24c28df8f74ba90f8b3f5915"}, + {file = "fonttools-4.55.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:95f5a1d4432b3cea6571f5ce4f4e9b25bf36efbd61c32f4f90130a690925d6ee"}, + {file = "fonttools-4.55.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d20f152de7625a0008ba1513f126daaaa0de3b4b9030aa72dd5c27294992260"}, + {file = "fonttools-4.55.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5a3ff5bb95fd5a3962b2754f8435e6d930c84fc9e9921c51e802dddf40acd56"}, + {file = "fonttools-4.55.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b99d4fd2b6d0a00c7336c8363fccc7a11eccef4b17393af75ca6e77cf93ff413"}, + {file = "fonttools-4.55.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d637e4d33e46619c79d1a6c725f74d71b574cd15fb5bbb9b6f3eba8f28363573"}, + {file = "fonttools-4.55.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0f38bfb6b7a39c4162c3eb0820a0bdf8e3bdd125cd54e10ba242397d15e32439"}, + {file = "fonttools-4.55.8-cp311-cp311-win32.whl", hash = "sha256:acfec948de41cd5e640d5c15d0200e8b8e7c5c6bb82afe1ca095cbc4af1188ee"}, + {file = "fonttools-4.55.8-cp311-cp311-win_amd64.whl", hash = "sha256:604c805b41241b4880e2dc86cf2d4754c06777371c8299799ac88d836cb18c3b"}, + {file = "fonttools-4.55.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63403ee0f2fa4e1de28e539f8c24f2bdca1d8ecb503fa9ea2d231d9f1e729809"}, + {file = "fonttools-4.55.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:302e1003a760b222f711d5ba6d1ad7fd5f7f713eb872cd6a3eb44390bc9770af"}, + {file = "fonttools-4.55.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e72a7816ff8a759be9ca36ca46934f8ccf4383711ef597d9240306fe1878cb8d"}, + {file = "fonttools-4.55.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03c2b50b54e6e8b3564b232e57e8f58be217cf441cf0155745d9e44a76f9c30f"}, + {file = "fonttools-4.55.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7230f7590f9570d26ee903b6a4540274494e200fae978df0d9325b7b9144529"}, + {file = "fonttools-4.55.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:466a78984f0572305c3c48377f4e3f7f4e909f1209f45ef8e7041d5c8a744a56"}, + {file = "fonttools-4.55.8-cp312-cp312-win32.whl", hash = "sha256:243cbfc0b7cb1c307af40e321f8343a48d0a080bc1f9466cf2b5468f776ef108"}, + {file = "fonttools-4.55.8-cp312-cp312-win_amd64.whl", hash = "sha256:a19059aa892676822c1f05cb5a67296ecdfeb267fe7c47d4758f3e8e942c2b2a"}, + {file = "fonttools-4.55.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:332883b6280b9d90d2ba7e9e81be77cf2ace696161e60cdcf40cfcd2b3ed06fa"}, + {file = "fonttools-4.55.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6b8d7c149d47b47de7ec81763396c8266e5ebe2e0b14aa9c3ccf29e52260ab2f"}, + {file = "fonttools-4.55.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dfae7c94987149bdaa0388e6c937566aa398fa0eec973b17952350a069cff4e"}, + {file = "fonttools-4.55.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0fe12f06169af2fdc642d26a8df53e40adc3beedbd6ffedb19f1c5397b63afd"}, + {file = "fonttools-4.55.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f971aa5f50c22dc4b63a891503624ae2c77330429b34ead32f23c2260c5618cd"}, + {file = "fonttools-4.55.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708cb17b2590b7f6c6854999df0039ff1140dda9e6f56d67c3599ba6f968fab5"}, + {file = "fonttools-4.55.8-cp313-cp313-win32.whl", hash = "sha256:cfe9cf30f391a0f2875247a3e5e44d8dcb61596e5cf89b360cdffec8a80e9961"}, + {file = "fonttools-4.55.8-cp313-cp313-win_amd64.whl", hash = "sha256:1e10efc8ee10d6f1fe2931d41bccc90cd4b872f2ee4ff21f2231a2c293b2dbf8"}, + {file = "fonttools-4.55.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9b6fcff4dc755b32faff955d989ee26394ddad3a90ea7d558db17a4633c8390c"}, + {file = "fonttools-4.55.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:02c41322e5bdcb484b61b776fcea150215c83619b39c96aa0b44d4fd87bb5574"}, + {file = "fonttools-4.55.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9164f44add0acec0f12fce682824c040dc52e483bfe3838c37142897150c8364"}, + {file = "fonttools-4.55.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2248ebfbcea0d0b3cb459d76a9f67f2eadc10ec0d07e9cadab8777d3f016bf2"}, + {file = "fonttools-4.55.8-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3461347016c94cb42b36caa907e11565878c4c2c375604f3651d11dc06d1ab3e"}, + {file = "fonttools-4.55.8-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:67df1c3935838fb9e56f227d7f506c9043b149a4a3b667bef17929c7a1114d19"}, + {file = "fonttools-4.55.8-cp38-cp38-win32.whl", hash = "sha256:cb121d6dd34625cece32234a5fa0359475bb118838b6b4295ffdb13b935edb04"}, + {file = "fonttools-4.55.8-cp38-cp38-win_amd64.whl", hash = "sha256:285c1ac10c160fbdff6d05358230e66c4f98cbbf271f3ec7eb34e967771543e8"}, + {file = "fonttools-4.55.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8abd135e427d88e461a4833c03cf96cfb9028c78c15d58123291f22398e25492"}, + {file = "fonttools-4.55.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65cb8f97eed7906dcf19bc2736b70c6239e9d7e77aad7c6110ba7239ae082e81"}, + {file = "fonttools-4.55.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:450c354c04a6e12a3db968e915fe05730f79ff3d39560947ef8ee6eaa2ab2212"}, + {file = "fonttools-4.55.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2232012a1502b2b8ab4c6bc1d3524bfe90238c0c1a50ac94a0a2085aa87a58a5"}, + {file = "fonttools-4.55.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d39f0c977639be0f9f5505d4c7c478236737f960c567a35f058649c056e41434"}, + {file = "fonttools-4.55.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:de78d6d0dbe32561ce059265437021f4746e56073c4799f0f1095828ae7232bd"}, + {file = "fonttools-4.55.8-cp39-cp39-win32.whl", hash = "sha256:bf4b5b3496ddfdd4e57112e77ec51f1ab388d35ac17322c1248addb2eb0d429a"}, + {file = "fonttools-4.55.8-cp39-cp39-win_amd64.whl", hash = "sha256:ccf8ae02918f431953d338db4d0a675a395faf82bab3a76025582cf32a2f3b7b"}, + {file = "fonttools-4.55.8-py3-none-any.whl", hash = "sha256:07636dae94f7fe88561f9da7a46b13d8e3f529f87fdb221b11d85f91eabceeb7"}, + {file = "fonttools-4.55.8.tar.gz", hash = "sha256:54d481d456dcd59af25d4a9c56b2c4c3f20e9620b261b84144e5950f33e8df17"}, ] [package.extras] @@ -1534,6 +1534,20 @@ files = [ {file = "numpy-2.2.1.tar.gz", hash = "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918"}, ] +[[package]] +name = "objgraph" +version = "3.6.2" +description = "Draws Python object reference graphs with graphviz" +optional = false +python-versions = ">=3.7" +files = [ + {file = "objgraph-3.6.2-py3-none-any.whl", hash = "sha256:8114c97712291c3ba30d882406a384d0a7651b307ea9a06e0d83836ccde85e15"}, + {file = "objgraph-3.6.2.tar.gz", hash = "sha256:00b9f2f40f7422e3c7f45a61c4dafdaf81f03ff0649d6eaec866f01030e51ad8"}, +] + +[package.extras] +ipython = ["graphviz"] + [[package]] name = "opencv-python-headless" version = "4.11.0.86" @@ -2358,99 +2372,99 @@ scripts = ["Pillow (>=3.2.0)"] [[package]] name = "rapidfuzz" -version = "3.11.0" +version = "3.12.1" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.9" files = [ - {file = "rapidfuzz-3.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb8a54543d16ab1b69e2c5ed96cabbff16db044a50eddfc028000138ca9ddf33"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:231c8b2efbd7f8d2ecd1ae900363ba168b8870644bb8f2b5aa96e4a7573bde19"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54e7f442fb9cca81e9df32333fb075ef729052bcabe05b0afc0441f462299114"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:906f1f2a1b91c06599b3dd1be207449c5d4fc7bd1e1fa2f6aef161ea6223f165"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed59044aea9eb6c663112170f2399b040d5d7b162828b141f2673e822093fa8"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cb1965a28b0fa64abdee130c788a0bc0bb3cf9ef7e3a70bf055c086c14a3d7e"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b488b244931d0291412917e6e46ee9f6a14376625e150056fe7c4426ef28225"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f0ba13557fec9d5ffc0a22826754a7457cc77f1b25145be10b7bb1d143ce84c6"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3871fa7dfcef00bad3c7e8ae8d8fd58089bad6fb21f608d2bf42832267ca9663"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b2669eafee38c5884a6e7cc9769d25c19428549dcdf57de8541cf9e82822e7db"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ffa1bb0e26297b0f22881b219ffc82a33a3c84ce6174a9d69406239b14575bd5"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:45b15b8a118856ac9caac6877f70f38b8a0d310475d50bc814698659eabc1cdb"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-win32.whl", hash = "sha256:22033677982b9c4c49676f215b794b0404073f8974f98739cb7234e4a9ade9ad"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:be15496e7244361ff0efcd86e52559bacda9cd975eccf19426a0025f9547c792"}, - {file = "rapidfuzz-3.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:714a7ba31ba46b64d30fccfe95f8013ea41a2e6237ba11a805a27cdd3bce2573"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8724a978f8af7059c5323d523870bf272a097478e1471295511cf58b2642ff83"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b63cb1f2eb371ef20fb155e95efd96e060147bdd4ab9fc400c97325dfee9fe1"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82497f244aac10b20710448645f347d862364cc4f7d8b9ba14bd66b5ce4dec18"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:339607394941801e6e3f6c1ecd413a36e18454e7136ed1161388de674f47f9d9"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84819390a36d6166cec706b9d8f0941f115f700b7faecab5a7e22fc367408bc3"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eea8d9e20632d68f653455265b18c35f90965e26f30d4d92f831899d6682149b"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b659e1e2ea2784a9a397075a7fc395bfa4fe66424042161c4bcaf6e4f637b38"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1315cd2a351144572e31fe3df68340d4b83ddec0af8b2e207cd32930c6acd037"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a7743cca45b4684c54407e8638f6d07b910d8d811347b9d42ff21262c7c23245"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5bb636b0150daa6d3331b738f7c0f8b25eadc47f04a40e5c23c4bfb4c4e20ae3"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:42f4dd264ada7a9aa0805ea0da776dc063533917773cf2df5217f14eb4429eae"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51f24cb39e64256221e6952f22545b8ce21cacd59c0d3e367225da8fc4b868d8"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-win32.whl", hash = "sha256:aaf391fb6715866bc14681c76dc0308f46877f7c06f61d62cc993b79fc3c4a2a"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:ebadd5b8624d8ad503e505a99b8eb26fe3ea9f8e9c2234e805a27b269e585842"}, - {file = "rapidfuzz-3.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:d895998fec712544c13cfe833890e0226585cf0391dd3948412441d5d68a2b8c"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f382fec4a7891d66fb7163c90754454030bb9200a13f82ee7860b6359f3f2fa8"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dfaefe08af2a928e72344c800dcbaf6508e86a4ed481e28355e8d4b6a6a5230e"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92ebb7c12f682b5906ed98429f48a3dd80dd0f9721de30c97a01473d1a346576"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a1b3ebc62d4bcdfdeba110944a25ab40916d5383c5e57e7c4a8dc0b6c17211a"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c6d7fea39cb33e71de86397d38bf7ff1a6273e40367f31d05761662ffda49e4"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99aebef8268f2bc0b445b5640fd3312e080bd17efd3fbae4486b20ac00466308"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4469307f464ae3089acf3210b8fc279110d26d10f79e576f385a98f4429f7d97"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:eb97c53112b593f89a90b4f6218635a9d1eea1d7f9521a3b7d24864228bbc0aa"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef8937dae823b889c0273dfa0f0f6c46a3658ac0d851349c464d1b00e7ff4252"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d95f9e9f3777b96241d8a00d6377cc9c716981d828b5091082d0fe3a2924b43e"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:b1d67d67f89e4e013a5295e7523bc34a7a96f2dba5dd812c7c8cb65d113cbf28"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d994cf27e2f874069884d9bddf0864f9b90ad201fcc9cb2f5b82bacc17c8d5f2"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-win32.whl", hash = "sha256:ba26d87fe7fcb56c4a53b549a9e0e9143f6b0df56d35fe6ad800c902447acd5b"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1f7efdd7b7adb32102c2fa481ad6f11923e2deb191f651274be559d56fc913b"}, - {file = "rapidfuzz-3.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:ed78c8e94f57b44292c1a0350f580e18d3a3c5c0800e253f1583580c1b417ad2"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e60814edd0c9b511b5f377d48b9782b88cfe8be07a98f99973669299c8bb318a"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f28952da055dbfe75828891cd3c9abf0984edc8640573c18b48c14c68ca5e06"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e8f93bc736020351a6f8e71666e1f486bb8bd5ce8112c443a30c77bfde0eb68"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76a4a11ba8f678c9e5876a7d465ab86def047a4fcc043617578368755d63a1bc"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc0e0d41ad8a056a9886bac91ff9d9978e54a244deb61c2972cc76b66752de9c"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e8ea35f2419c7d56b3e75fbde2698766daedb374f20eea28ac9b1f668ef4f74"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd340bbd025302276b5aa221dccfe43040c7babfc32f107c36ad783f2ffd8775"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:494eef2c68305ab75139034ea25328a04a548d297712d9cf887bf27c158c388b"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5a167344c1d6db06915fb0225592afdc24d8bafaaf02de07d4788ddd37f4bc2f"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8c7af25bda96ac799378ac8aba54a8ece732835c7b74cfc201b688a87ed11152"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d2a0f7e17f33e7890257367a1662b05fecaf56625f7dbb6446227aaa2b86448b"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d0d26c7172bdb64f86ee0765c5b26ea1dc45c52389175888ec073b9b28f4305"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-win32.whl", hash = "sha256:6ad02bab756751c90fa27f3069d7b12146613061341459abf55f8190d899649f"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:b1472986fd9c5d318399a01a0881f4a0bf4950264131bb8e2deba9df6d8c362b"}, - {file = "rapidfuzz-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:c408f09649cbff8da76f8d3ad878b64ba7f7abdad1471efb293d2c075e80c822"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1bac4873f6186f5233b0084b266bfb459e997f4c21fc9f029918f44a9eccd304"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f9f12c2d0aa52b86206d2059916153876a9b1cf9dfb3cf2f344913167f1c3d4"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd501de6f7a8f83557d20613b58734d1cb5f0be78d794cde64fe43cfc63f5f2"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4416ca69af933d4a8ad30910149d3db6d084781d5c5fdedb713205389f535385"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0821b9bdf18c5b7d51722b906b233a39b17f602501a966cfbd9b285f8ab83cd"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0edecc3f90c2653298d380f6ea73b536944b767520c2179ec5d40b9145e47aa"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4513dd01cee11e354c31b75f652d4d466c9440b6859f84e600bdebfccb17735a"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d9727b85511b912571a76ce53c7640ba2c44c364e71cef6d7359b5412739c570"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ab9eab33ee3213f7751dc07a1a61b8d9a3d748ca4458fffddd9defa6f0493c16"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6b01c1ddbb054283797967ddc5433d5c108d680e8fa2684cf368be05407b07e4"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3857e335f97058c4b46fa39ca831290b70de554a5c5af0323d2f163b19c5f2a6"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d98a46cf07c0c875d27e8a7ed50f304d83063e49b9ab63f21c19c154b4c0d08d"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-win32.whl", hash = "sha256:c36539ed2c0173b053dafb221458812e178cfa3224ade0960599bec194637048"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:ec8d7d8567e14af34a7911c98f5ac74a3d4a743cd848643341fc92b12b3784ff"}, - {file = "rapidfuzz-3.11.0-cp39-cp39-win_arm64.whl", hash = "sha256:62171b270ecc4071be1c1f99960317db261d4c8c83c169e7f8ad119211fe7397"}, - {file = "rapidfuzz-3.11.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f06e3c4c0a8badfc4910b9fd15beb1ad8f3b8fafa8ea82c023e5e607b66a78e4"}, - {file = "rapidfuzz-3.11.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fe7aaf5a54821d340d21412f7f6e6272a9b17a0cbafc1d68f77f2fc11009dcd5"}, - {file = "rapidfuzz-3.11.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25398d9ac7294e99876a3027ffc52c6bebeb2d702b1895af6ae9c541ee676702"}, - {file = "rapidfuzz-3.11.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a52eea839e4bdc72c5e60a444d26004da00bb5bc6301e99b3dde18212e41465"}, - {file = "rapidfuzz-3.11.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c87319b0ab9d269ab84f6453601fd49b35d9e4a601bbaef43743f26fabf496c"}, - {file = "rapidfuzz-3.11.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3048c6ed29d693fba7d2a7caf165f5e0bb2b9743a0989012a98a47b975355cca"}, - {file = "rapidfuzz-3.11.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b04f29735bad9f06bb731c214f27253bd8bedb248ef9b8a1b4c5bde65b838454"}, - {file = "rapidfuzz-3.11.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7864e80a0d4e23eb6194254a81ee1216abdc53f9dc85b7f4d56668eced022eb8"}, - {file = "rapidfuzz-3.11.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3794df87313dfb56fafd679b962e0613c88a293fd9bd5dd5c2793d66bf06a101"}, - {file = "rapidfuzz-3.11.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d71da0012face6f45432a11bc59af19e62fac5a41f8ce489e80c0add8153c3d1"}, - {file = "rapidfuzz-3.11.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff38378346b7018f42cbc1f6d1d3778e36e16d8595f79a312b31e7c25c50bd08"}, - {file = "rapidfuzz-3.11.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6668321f90aa02a5a789d4e16058f2e4f2692c5230252425c3532a8a62bc3424"}, - {file = "rapidfuzz-3.11.0.tar.gz", hash = "sha256:a53ca4d3f52f00b393fab9b5913c5bafb9afc27d030c8a1db1283da6917a860f"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbb7ea2fd786e6d66f225ef6eef1728832314f47e82fee877cb2a793ebda9579"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ae41361de05762c1eaa3955e5355de7c4c6f30d1ef1ea23d29bf738a35809ab"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc3c39e0317e7f68ba01bac056e210dd13c7a0abf823e7b6a5fe7e451ddfc496"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69f2520296f1ae1165b724a3aad28c56fd0ac7dd2e4cff101a5d986e840f02d4"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34dcbf5a7daecebc242f72e2500665f0bde9dd11b779246c6d64d106a7d57c99"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:773ab37fccf6e0513891f8eb4393961ddd1053c6eb7e62eaa876e94668fc6d31"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ecf0e6de84c0bc2c0f48bc03ba23cef2c5f1245db7b26bc860c11c6fd7a097c"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4dc2ebad4adb29d84a661f6a42494df48ad2b72993ff43fad2b9794804f91e45"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8389d98b9f54cb4f8a95f1fa34bf0ceee639e919807bb931ca479c7a5f2930bf"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:165bcdecbfed9978962da1d3ec9c191b2ff9f1ccc2668fbaf0613a975b9aa326"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:129d536740ab0048c1a06ccff73c683f282a2347c68069affae8dbc423a37c50"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b67e390261ffe98ec86c771b89425a78b60ccb610c3b5874660216fcdbded4b"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-win32.whl", hash = "sha256:a66520180d3426b9dc2f8d312f38e19bc1fc5601f374bae5c916f53fa3534a7d"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:82260b20bc7a76556cecb0c063c87dad19246a570425d38f8107b8404ca3ac97"}, + {file = "rapidfuzz-3.12.1-cp310-cp310-win_arm64.whl", hash = "sha256:3a860d103bbb25c69c2e995fdf4fac8cb9f77fb69ec0a00469d7fd87ff148f46"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d9afad7b16d01c9e8929b6a205a18163c7e61b6cd9bcf9c81be77d5afc1067a"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb424ae7240f2d2f7d8dda66a61ebf603f74d92f109452c63b0dbf400204a437"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42149e6d13bd6d06437d2a954dae2184dadbbdec0fdb82dafe92860d99f80519"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:760ac95d788f2964b73da01e0bdffbe1bf2ad8273d0437565ce9092ae6ad1fbc"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cf27e8e4bf7bf9d92ef04f3d2b769e91c3f30ba99208c29f5b41e77271a2614"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00ceb8ff3c44ab0d6014106c71709c85dee9feedd6890eff77c814aa3798952b"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b61c558574fbc093d85940c3264c08c2b857b8916f8e8f222e7b86b0bb7d12"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:346a2d8f17224e99f9ef988606c83d809d5917d17ad00207237e0965e54f9730"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d60d1db1b7e470e71ae096b6456e20ec56b52bde6198e2dbbc5e6769fa6797dc"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2477da227e266f9c712f11393182c69a99d3c8007ea27f68c5afc3faf401cc43"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8499c7d963ddea8adb6cffac2861ee39a1053e22ca8a5ee9de1197f8dc0275a5"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:12802e5c4d8ae104fb6efeeb436098325ce0dca33b461c46e8df015c84fbef26"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-win32.whl", hash = "sha256:e1061311d07e7cdcffa92c9b50c2ab4192907e70ca01b2e8e1c0b6b4495faa37"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6e4ed63e204daa863a802eec09feea5448617981ba5d150f843ad8e3ae071a4"}, + {file = "rapidfuzz-3.12.1-cp311-cp311-win_arm64.whl", hash = "sha256:920733a28c3af47870835d59ca9879579f66238f10de91d2b4b3f809d1ebfc5b"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f6235b57ae3faa3f85cb3f90c9fee49b21bd671b76e90fc99e8ca2bdf0b5e4a3"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af4585e5812632c357fee5ab781c29f00cd06bea58f8882ff244cc4906ba6c9e"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5942dc4460e5030c5f9e1d4c9383de2f3564a2503fe25e13e89021bcbfea2f44"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b31ab59e1a0df5afc21f3109b6cfd77b34040dbf54f1bad3989f885cfae1e60"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97c885a7a480b21164f57a706418c9bbc9a496ec6da087e554424358cadde445"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d844c0587d969ce36fbf4b7cbf0860380ffeafc9ac5e17a7cbe8abf528d07bb"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93c95dce8917bf428064c64024de43ffd34ec5949dd4425780c72bd41f9d969"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:834f6113d538af358f39296604a1953e55f8eeffc20cb4caf82250edbb8bf679"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a940aa71a7f37d7f0daac186066bf6668d4d3b7e7ef464cb50bc7ba89eae1f51"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ec9eaf73501c9a7de2c6938cb3050392e2ee0c5ca3921482acf01476b85a7226"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c5ec360694ac14bfaeb6aea95737cf1a6cf805b5fe8ea7fd28814706c7fa838"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b5e176524653ac46f1802bdd273a4b44a5f8d0054ed5013a8e8a4b72f254599"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-win32.whl", hash = "sha256:6f463c6f1c42ec90e45d12a6379e18eddd5cdf74138804d8215619b6f4d31cea"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:b894fa2b30cd6498a29e5c470cb01c6ea898540b7e048a0342775a5000531334"}, + {file = "rapidfuzz-3.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:43bb17056c5d1332f517b888c4e57846c4b5f936ed304917eeb5c9ac85d940d4"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:97f824c15bc6933a31d6e3cbfa90188ba0e5043cf2b6dd342c2b90ee8b3fd47c"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a973b3f5cabf931029a3ae4a0f72e3222e53d412ea85fc37ddc49e1774f00fbf"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7880e012228722dec1be02b9ef3898ed023388b8a24d6fa8213d7581932510"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c78582f50e75e6c2bc38c791ed291cb89cf26a3148c47860c1a04d6e5379c8e"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d7d9e6a04d8344b0198c96394c28874086888d0a2b2f605f30d1b27b9377b7d"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5620001fd4d6644a2f56880388179cc8f3767670f0670160fcb97c3b46c828af"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0666ab4c52e500af7ba5cc17389f5d15c0cdad06412c80312088519fdc25686d"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:27b4d440fa50b50c515a91a01ee17e8ede719dca06eef4c0cccf1a111a4cfad3"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83dccfd5a754f2a0e8555b23dde31f0f7920601bfa807aa76829391ea81e7c67"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b572b634740e047c53743ed27a1bb3b4f93cf4abbac258cd7af377b2c4a9ba5b"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7fa7b81fb52902d5f78dac42b3d6c835a6633b01ddf9b202a3ca8443be4b2d6a"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1d4fbff980cb6baef4ee675963c081f7b5d6580a105d6a4962b20f1f880e1fb"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-win32.whl", hash = "sha256:3fe8da12ea77271097b303fa7624cfaf5afd90261002314e3b0047d36f4afd8d"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:6f7e92fc7d2a7f02e1e01fe4f539324dfab80f27cb70a30dd63a95445566946b"}, + {file = "rapidfuzz-3.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:e31be53d7f4905a6a038296d8b773a79da9ee9f0cd19af9490c5c5a22e37d2e5"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bef5c91d5db776523530073cda5b2a276283258d2f86764be4a008c83caf7acd"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:841e0c2a5fbe8fc8b9b1a56e924c871899932c0ece7fbd970aa1c32bfd12d4bf"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046fc67f3885d94693a2151dd913aaf08b10931639cbb953dfeef3151cb1027c"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4d2d39b2e76c17f92edd6d384dc21fa020871c73251cdfa017149358937a41d"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5857dda85165b986c26a474b22907db6b93932c99397c818bcdec96340a76d5"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c26cd1b9969ea70dbf0dbda3d2b54ab4b2e683d0fd0f17282169a19563efeb1"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf56ea4edd69005786e6c80a9049d95003aeb5798803e7a2906194e7a3cb6472"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fbe7580b5fb2db8ebd53819171ff671124237a55ada3f64d20fc9a149d133960"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:018506a53c3b20dcbda8c93d4484b9eb1764c93d5ea16be103cf6b0d8b11d860"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:325c9c71b737fcd32e2a4e634c430c07dd3d374cfe134eded3fe46e4c6f9bf5d"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:930756639643e3aa02d3136b6fec74e5b9370a24f8796e1065cd8a857a6a6c50"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0acbd27543b158cb915fde03877383816a9e83257832818f1e803bac9b394900"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-win32.whl", hash = "sha256:80ff9283c54d7d29b2d954181e137deee89bec62f4a54675d8b6dbb6b15d3e03"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:fd37e53f0ed239d0cec27b250cec958982a8ba252ce64aa5e6052de3a82fa8db"}, + {file = "rapidfuzz-3.12.1-cp39-cp39-win_arm64.whl", hash = "sha256:4a4422e4f73a579755ab60abccb3ff148b5c224b3c7454a13ca217dfbad54da6"}, + {file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b7cba636c32a6fc3a402d1cb2c70c6c9f8e6319380aaf15559db09d868a23e56"}, + {file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b79286738a43e8df8420c4b30a92712dec6247430b130f8e015c3a78b6d61ac2"}, + {file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dc1937198e7ff67e217e60bfa339f05da268d91bb15fec710452d11fe2fdf60"}, + {file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b85817a57cf8db32dd5d2d66ccfba656d299b09eaf86234295f89f91be1a0db2"}, + {file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04283c6f3e79f13a784f844cd5b1df4f518ad0f70c789aea733d106c26e1b4fb"}, + {file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a718f740553aad5f4daef790191511da9c6eae893ee1fc2677627e4b624ae2db"}, + {file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cbdf145c7e4ebf2e81c794ed7a582c4acad19e886d5ad6676086369bd6760753"}, + {file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0d03ad14a26a477be221fddc002954ae68a9e2402b9d85433f2d0a6af01aa2bb"}, + {file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1187aeae9c89e838d2a0a2b954b4052e4897e5f62e5794ef42527bf039d469e"}, + {file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd47dfb1bca9673a48b923b3d988b7668ee8efd0562027f58b0f2b7abf27144c"}, + {file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187cdb402e223264eebed2fe671e367e636a499a7a9c82090b8d4b75aa416c2a"}, + {file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6899b41bf6c30282179f77096c1939f1454836440a8ab05b48ebf7026a3b590"}, + {file = "rapidfuzz-3.12.1.tar.gz", hash = "sha256:6a98bbca18b4a37adddf2d8201856441c26e9c981d8895491b5bc857b5f780eb"}, ] [package.extras] @@ -2678,13 +2692,13 @@ files = [ [[package]] name = "translate-toolkit" -version = "3.14.6" +version = "3.14.7" description = "Tools and API for translation and localization engineering." optional = false python-versions = ">=3.9" files = [ - {file = "translate_toolkit-3.14.6-py3-none-any.whl", hash = "sha256:31f2247d80ad3ed12d70ec38c946873439c493be352e2e87c4fe738065efe91c"}, - {file = "translate_toolkit-3.14.6.tar.gz", hash = "sha256:d850adf03f86484bf9c5eae203c913ed3d918dbf3a8f008d9b15602a7286e79e"}, + {file = "translate_toolkit-3.14.7-py3-none-any.whl", hash = "sha256:95ef7949af9957b9f82683f777bddc366cecd6ed1d98a3079ca2f44f4834c153"}, + {file = "translate_toolkit-3.14.7.tar.gz", hash = "sha256:e71a6f37111006e32080709bd913c243f9505a8a7a1e27a9f01a5149ecb7d51d"}, ] [package.dependencies] @@ -2903,4 +2917,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "b22908aa5e308fc1caf720c70e2321e5d934f2bb0f253eebae2c3c4ac0578892" +content-hash = "1673c74b3485e6aef8688b8daa7f96dac9d806fc8798320502e09c22c3d35024" diff --git a/pyproject.toml b/pyproject.toml index 4870bd0..a28d5a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ line-length = 110 name = "bitcoin-safe" # the version here and in all other places in this toml are updated automatically # from the source: bitcoin_safe/__init__.py -version = "1.0.4" +version = "1.0.5" description = "A bitcoin savings wallet for the entire family." authors = [ "andreasgriffin ",] license = "GPL-3.0" @@ -41,9 +41,9 @@ numpy = "2.2.1" # error in wine/pyinstaller when increased # bitcoin-qr-tools = {path="../bitcoin-qr-tools"} # "^0.10.15" # bitcoin-nostr-chat = {path="../bitcoin-nostr-chat"} # "^0.2.6" # bitcoin-usb = {path="../bitcoin-usb"} # "^0.3.3" -bitcoin-qr-tools = "^1.0.3" +bitcoin-qr-tools = "^1.0.4" bitcoin-nostr-chat = "^0.6.2" # { git = "https://github.com/andreasgriffin/bitcoin-nostr-chat.git", rev = "fda38f7c4879fbced753a8d3466d50c571c06769" } -bitcoin-usb = "^0.7.4" +bitcoin-usb = "^0.7.6" pysocks = "^1.7.1" @@ -62,6 +62,7 @@ tomlkit = "^0.13.2" poetry = "^1.8.4" pyinstaller = "^6.11.0" poetry-plugin-export = "^1.8.0" +objgraph = "^3.6.2" diff --git a/tests/gui/qt/taglist/test_main.py b/tests/gui/qt/taglist/test_main.py index 4e237ca..2b45c9d 100644 --- a/tests/gui/qt/taglist/test_main.py +++ b/tests/gui/qt/taglist/test_main.py @@ -33,26 +33,26 @@ from PyQt6.QtGui import QDragEnterEvent from PyQt6.QtWidgets import QApplication -from bitcoin_safe.gui.qt.taglist.main import ( +from bitcoin_safe.category_info import CategoryInfo, SubtextType +from bitcoin_safe.gui.qt.taglist.custom_list_widget import CustomDelegate +from bitcoin_safe.gui.qt.taglist.tag_editor import ( AddressDragInfo, - CustomDelegate, CustomListWidget, CustomListWidgetItem, DeleteButton, TagEditor, clean_tag, qbytearray_to_str, - str_to_qbytearray, ) from bitcoin_safe.gui.qt.util import hash_color, rescale -from bitcoin_safe.util import hash_string +from bitcoin_safe.util import hash_string, str_to_qbytearray logger = logging.getLogger(__name__) import hashlib -from PyQt6.QtCore import QMimeData, QModelIndex, QPoint, QPointF, Qt +from PyQt6.QtCore import QMimeData, QPoint, QPointF, Qt from PyQt6.QtGui import QColor, QDragEnterEvent, QDropEvent from PyQt6.QtWidgets import QApplication @@ -110,13 +110,6 @@ def test_custom_list_widget_item(qapp: QApplication): assert stored_subtext == sub_text -def test_custom_delegate(qapp: QApplication): - parent = None - delegate = CustomDelegate(parent) - assert delegate.currentlyEditingIndex == QModelIndex() - assert isinstance(delegate.imageCache, dict) - - def test_delete_button(qapp: QApplication): button = DeleteButton() assert button.acceptDrops() @@ -135,15 +128,17 @@ def test_custom_list_widget(qapp: QApplication): assert item.text() == "TestItem" assert item.subtext == "SubText" # Test get_selected - widget.setAllSelection(True) + widget.clearSelection() selected = widget.get_selected() - assert selected == ["TestItem"] + assert not selected def test_tag_editor(qapp: QApplication): - tags = ["Tag1", "Tag2"] - sub_texts = ["Sub1", "Sub2"] - editor = TagEditor(tags=tags, sub_texts=sub_texts) + category_infos = [ + CategoryInfo("Tag1", text_click_new_address="Sub1"), + CategoryInfo("Tag2", text_click_new_address="Sub2"), + ] + editor = TagEditor(category_infos=category_infos, subtext_type=SubtextType.click_new_address) # Test that the editor initializes properly assert editor.list_widget.count() == 2 item1 = editor.list_widget.item(0) @@ -184,17 +179,20 @@ def test_list_widget_delete_item(qapp: QApplication): def test_list_widget_recreate(qapp: QApplication): - widget = CustomListWidget() - tags = ["Tag1", "Tag2", "Tag3"] - sub_texts = ["Sub1", "Sub2", "Sub3"] - widget.recreate(tags, sub_texts) + widget = CustomListWidget(subtext_type=SubtextType.click_new_address) + category_infos = [ + CategoryInfo("Tag1", text_click_new_address="Sub1"), + CategoryInfo("Tag2", text_click_new_address="Sub2"), + CategoryInfo("Tag3", text_click_new_address="Sub3"), + ] + widget.recreate(category_infos) assert widget.count() == 3 - for i, (tag, sub_text) in enumerate(zip(tags, sub_texts)): + for i, category_info in enumerate(category_infos): item = widget.item(i) assert item - assert item.text() == tag + assert item.text() == category_info.category assert isinstance(item, CustomListWidgetItem) - assert item.subtext == sub_text + assert item.subtext == category_info.text_click_new_address def test_custom_list_widget_item_mime_data(qapp: QApplication): @@ -461,19 +459,6 @@ def on_tag_renamed(old_tag, new_tag): assert editor.list_widget.count() == 0 -def test_delegate_cache_eviction(qapp: QApplication): - delegate = CustomDelegate(None) - # Set a small cache size for testing - delegate.cache_size = 5 - # Simulate adding items to the cache - for i in range(10): - key = ("index", i) - value = f"image_{i}" - delegate.add_to_cache(key, value) - # Cache size should not exceed cache_size - assert len(delegate.imageCache) <= delegate.cache_size - - def test_custom_list_widget_drag_enter_event_invalid_mime(qapp: QApplication): widget = CustomListWidget() diff --git a/tests/gui/qt/test_data_tab_widget.py b/tests/gui/qt/test_data_tab_widget.py index 1a7e0b4..fd0fd45 100644 --- a/tests/gui/qt/test_data_tab_widget.py +++ b/tests/gui/qt/test_data_tab_widget.py @@ -41,7 +41,7 @@ @pytest.fixture def data_tab_widget(qapp: QApplication) -> DataTabWidget: """Fixture to create a DataTabWidget instance with string data.""" - widget = DataTabWidget(str) + widget = DataTabWidget[str]() return widget @@ -109,8 +109,7 @@ def test_clear_tab_data(data_tab_widget: DataTabWidget): widget.clearTabData() assert len(widget._tab_data) == 0 assert widget.count() == 2 - with pytest.raises(KeyError): - widget.tabData(0) + assert widget.tabData(0) is None def test_get_current_tab_data(data_tab_widget: DataTabWidget): @@ -172,8 +171,8 @@ def test_add_tab_without_data(data_tab_widget: DataTabWidget): tab = QWidget() index = widget.addTab(tab, description="No Data Tab") assert len(widget._tab_data) == 0 - with pytest.raises(KeyError): - widget.tabData(index) + + assert widget.tabData(index) is None def test_insert_tab_without_data(data_tab_widget: DataTabWidget): @@ -182,8 +181,7 @@ def test_insert_tab_without_data(data_tab_widget: DataTabWidget): tab = QWidget() index = widget.insertTab(0, tab, data=None, description="Inserted No Data Tab") assert len(widget._tab_data) == 0 - with pytest.raises(KeyError): - widget.tabData(index) + assert widget.tabData(index) is None def test_remove_tab_updates_indices(data_tab_widget: DataTabWidget): diff --git a/tests/gui/qt/test_default_network_config.py b/tests/gui/qt/test_default_network_config.py index 84344b4..c210b30 100644 --- a/tests/gui/qt/test_default_network_config.py +++ b/tests/gui/qt/test_default_network_config.py @@ -33,20 +33,21 @@ import tempfile from datetime import datetime from pathlib import Path -from time import sleep +import pytest from PyQt6.QtTest import QTest from PyQt6.QtWidgets import QApplication from pytestqt.qtbot import QtBot from bitcoin_safe.config import UserConfig -from bitcoin_safe.logging_setup import setup_logging # type: ignore +from bitcoin_safe.gui.qt.qt_wallet import QTWallet from tests.gui.qt.test_setup_wallet import close_wallet, get_tab_with_title, save_wallet from ...test_helpers import test_config # type: ignore from ...test_helpers import test_config_main_chain # type: ignore from ...test_setup_bitcoin_core import Faucet, bitcoin_core, faucet # type: ignore from .test_helpers import ( # type: ignore + CheckedDeletionContext, Shutter, close_wallet, do_modal_click, @@ -67,6 +68,7 @@ def test_default_network_config_works( test_config_main_chain: UserConfig, bitcoin_core: Path, faucet: Faucet, + caplog: pytest.LogCaptureFixture, wallet_file: str = "bacon.wallet", amount: int = int(1e6), ) -> None: # bitcoin_core: Path, @@ -89,36 +91,42 @@ def test_default_network_config_works( qt_wallet = main_window.open_wallet(str(temp_dir)) assert qt_wallet - qt_wallet.tabs.setCurrentWidget(qt_wallet.addresses_tab) + qt_wallet.tabs.setCurrentWidget(qt_wallet.address_tab) shutter.save(main_window) # check wallet address assert qt_wallet.wallet.get_addresses()[0] == "bc1qyngkwkslw5ng4v7m42s8t9j6zldmhyvrnnn9k5" - def sync(): - with qtbot.waitSignal(qt_wallet.signal_after_sync, timeout=10000): - qt_wallet.sync() + def do_all(qt_wallet: QTWallet): + "any implicit reference to qt_wallet (including the function page_send) will create a cell refrence" - shutter.save(main_window) + def sync(): + with qtbot.waitSignal(qt_wallet.signal_after_sync, timeout=10000): + qt_wallet.sync() + + shutter.save(main_window) + + assert len(qt_wallet.wallet.sorted_delta_list_transactions()) >= 28 - assert len(qt_wallet.wallet.sorted_delta_list_transactions()) >= 28 + sync() - sync() + do_all(qt_wallet) - def do_close_wallet() -> None: + with CheckedDeletionContext( + qt_wallet=qt_wallet, qtbot=qtbot, caplog=caplog, graph_directory=shutter.used_directory() + ): + wallet_id = qt_wallet.wallet.id close_wallet( shutter=shutter, test_config=test_config_main_chain, - wallet_name=qt_wallet.wallet.id, + wallet_name=wallet_id, qtbot=qtbot, main_window=main_window, ) - + del qt_wallet shutter.save(main_window) - do_close_wallet() - def check_that_it_is_in_recent_wallets() -> None: assert any( [ @@ -133,4 +141,3 @@ def check_that_it_is_in_recent_wallets() -> None: # end shutter.save(main_window) - sleep(2) diff --git a/tests/gui/qt/test_helpers.py b/tests/gui/qt/test_helpers.py index b41a7c5..ac458bc 100644 --- a/tests/gui/qt/test_helpers.py +++ b/tests/gui/qt/test_helpers.py @@ -26,11 +26,13 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. - +import gc import inspect +import json import logging import os import platform +import re import shutil from contextlib import contextmanager from datetime import datetime @@ -39,6 +41,7 @@ from typing import Any, Callable, Generator, List, Optional, Type, TypeVar, Union from unittest.mock import patch +import objgraph import pytest from PyQt6 import QtCore from PyQt6.QtGui import QAction @@ -59,6 +62,7 @@ from bitcoin_safe.config import UserConfig from bitcoin_safe.gui.qt.dialogs import PasswordCreation from bitcoin_safe.gui.qt.main import MainWindow +from bitcoin_safe.gui.qt.qt_wallet import QTWallet logger = logging.getLogger(__name__) @@ -114,6 +118,9 @@ def directory(name: str) -> Path: screenshots_dir.mkdir(exist_ok=True, parents=True) return screenshots_dir + def used_directory(self) -> Path: + return Shutter.directory(self.name) + @staticmethod def save_screenshot(widget: QMainWindow, qtbot: QtBot, name: str) -> Path: """Saves a screenshot of the given main window using qtbot to the 'screenshots' directory with a timestamp.""" @@ -318,6 +325,117 @@ def password_creation(dialog: QMessageBox) -> None: shutter.save(dialog) dialog.button(QMessageBox.StandardButton.Yes).click() - index = main_window.tab_wallets.indexOf(main_window.qt_wallets[wallet_name].tab) + index = main_window.tab_wallets.indexOf(main_window.qt_wallets[wallet_name]) do_modal_click(lambda: main_window.close_tab(index), password_creation, qtbot, cls=QMessageBox) + + +def clean_and_shorten(input_string, max_filename_len=50): + # Remove characters problematic for filenames + cleaned_string = re.sub(r'[\/:*?"<>|]', "", input_string) + + # Truncate the string to a maximum of 30 characters + shortened_string = cleaned_string[:max_filename_len] + + return shortened_string + + +class CheckedDeletionContext: + def __init__( + self, + qt_wallet: QTWallet, + qtbot: QtBot, + caplog: pytest.LogCaptureFixture, + graph_directory: Path | None = None, + list_references=None, + ): + self.graph_directory = graph_directory + self.caplog = caplog + self.qtbot = qtbot + self.d = list_references + self.check_for_destruction: List[QtCore.QObject] = [ + qt_wallet, + # qt_wallet.address_list, + # qt_wallet.address_list_with_toolbar, + # qt_wallet.history_list, + # qt_wallet.uitx_creator, + # qt_wallet.uitx_creator.category_list, + # qt_wallet.address_tab_category_editor, + ] + + @classmethod + def serialize_referrers(cls, obj: Any): + referrers = gc.get_referrers(obj) + + # Simplify referrers to a list of strings or simple dicts + simple_referrers = [] + for ref in referrers: + if isinstance(ref, dict): + # Provide a simple representation for dictionaries + simple_referrers.append({str(k): str(v) for k, v in ref.items()}) + elif isinstance(ref, list): + # Simplify lists by providing the type of elements or simple str representation + simple_referrers.append([str(item) for item in ref]) + else: + # Use a string representation for other types + simple_referrers.append(str(ref)) + + return simple_referrers + + @classmethod + def save_single_referrers_to_json(cls, obj: Any, path: Path): + filename = str(path / f"{cls.__name__}_{clean_and_shorten(str(obj))}.json") + simplified_data = cls.serialize_referrers(obj) + with open(filename, "w") as f: + json.dump(simplified_data, f, indent=4) + + @classmethod + def save_referrers_to_json(cls, objects: List[Any], path: Path): + for o in objects: + cls.save_single_referrers_to_json(o, path=path) + + @classmethod + def show_backrefs(cls, objects: List[Any], path: Path): + for o in objects: + objgraph.show_backrefs( + [o], + shortnames=False, + refcounts=True, + max_depth=2, + too_many=30, + filename=str(path / f"{cls.__name__}_{clean_and_shorten(str(o))}.png"), + ) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + + with self.qtbot.waitSignals([q.destroyed for q in self.check_for_destruction], timeout=1000): + if self.graph_directory: + self.show_backrefs(self.check_for_destruction, self.graph_directory) + self.save_referrers_to_json(self.check_for_destruction, self.graph_directory) + + self.check_for_destruction.clear() + gc.collect() + + ##### for_debug_only + if self.d: + # with qtbot.waitSignal(d, timeout=1000): + gc.collect() + logger.warning(str(gc.get_referrers(self.d))) + del self.d + gc.collect() + # import gc + # import types + # # the function gets cell (lambda function) references + # gx.collect() + # def get_cell_referrers(obj): + # # Get all objects that refer to 'obj' + # referrers = gc.get_referrers(obj) + # # Filter to retain only cell objects + # cell_referrers = [ref for ref in referrers if isinstance(ref, types.CellType)] + # return cell_referrers + # runt eh line below separately + # len(get_cell_referrers(self)) + ##### for_debug_only diff --git a/tests/gui/qt/test_setup_wallet.py b/tests/gui/qt/test_setup_wallet.py index 6fb5199..a76bc80 100644 --- a/tests/gui/qt/test_setup_wallet.py +++ b/tests/gui/qt/test_setup_wallet.py @@ -32,7 +32,6 @@ import os from datetime import datetime from pathlib import Path -from time import sleep from unittest.mock import patch import pytest @@ -51,7 +50,7 @@ from bitcoin_safe.gui.qt.bitcoin_quick_receive import BitcoinQuickReceive from bitcoin_safe.gui.qt.dialogs import WalletIdDialog from bitcoin_safe.gui.qt.keystore_ui import SignerUI -from bitcoin_safe.gui.qt.qt_wallet import QTProtoWallet +from bitcoin_safe.gui.qt.qt_wallet import QTProtoWallet, QTWallet from bitcoin_safe.gui.qt.tx_signing_steps import HorizontalImporters from bitcoin_safe.gui.qt.ui_tx import UITx_Viewer from bitcoin_safe.gui.qt.util import MessageType @@ -68,13 +67,13 @@ TutorialStep, Wizard, ) -from bitcoin_safe.logging_setup import setup_logging # type: ignore from bitcoin_safe.util import Satoshis from ...non_gui.test_signers import test_seeds from ...test_helpers import test_config # type: ignore from ...test_setup_bitcoin_core import Faucet, bitcoin_core, faucet # type: ignore from .test_helpers import ( # type: ignore + CheckedDeletionContext, Shutter, close_wallet, do_modal_click, @@ -109,6 +108,7 @@ def test_wizard( test_config: UserConfig, bitcoin_core: Path, faucet: Faucet, + caplog: pytest.LogCaptureFixture, wallet_name="test_tutorial_wallet_setup", amount=int(1e6), ) -> None: # bitcoin_core: Path, @@ -137,10 +137,11 @@ def on_wallet_id_dialog(dialog: WalletIdDialog) -> None: do_modal_click(w, on_wallet_id_dialog, qtbot, cls=WalletIdDialog) - w = get_tab_with_title(main_window.tab_wallets, title=wallet_name) - qt_proto_wallet = main_window.tab_wallets.get_data_for_tab(w) - assert isinstance(qt_proto_wallet, QTProtoWallet) - wizard: Wizard = qt_proto_wallet.wizard + qt_protowallet = main_window.tab_wallets.get_data_for_tab( + get_tab_with_title(main_window.tab_wallets, title=wallet_name) + ) + assert isinstance(qt_protowallet, QTProtoWallet) + wizard: Wizard = qt_protowallet.wizard def page1() -> None: shutter.save(main_window) @@ -274,6 +275,7 @@ def wrong_entry_xpub_try_to_proceed(dialog: QMessageBox) -> None: shutter.save(main_window) edit.clear() type_text_in_edit(valid_text, edit) + shutter.save(main_window) # correct entry for edit in [keystore.edit_xpub, keystore.edit_key_origin, keystore.edit_fingerprint]: @@ -309,181 +311,191 @@ def wrong_entry_xpub_try_to_proceed(dialog: QMessageBox) -> None: ###################################################### # now that the qt wallet is created i have to reload the - w = get_tab_with_title(main_window.tab_wallets, title=wallet_name) - qt_wallet = main_window.get_qt_wallet(tab=w) + qt_wallet = main_window.get_qt_wallet( + tab=get_tab_with_title(main_window.tab_wallets, title=wallet_name) + ) assert qt_wallet wizard = qt_wallet.wizard - def page_backup() -> None: - shutter.save(main_window) - step: BackupSeed = wizard.tab_generators[TutorialStep.backup_seed] - with patch("bitcoin_safe.pdfrecovery.xdg_open_file") as mock_open: - assert step.custom_yes_button.isVisible() - step.custom_yes_button.click() - mock_open.assert_called_once() + def do_all(qt_wallet: QTWallet): + "any implicit reference to qt_wallet (including the function page_send) will create a cell refrence" - temp_file = os.path.join(Path.home(), f"Seed backup of {wallet_name}.pdf") - assert Path(temp_file).exists() - # remove the file again - Path(temp_file).unlink() + def page_backup() -> None: + shutter.save(main_window) + step: BackupSeed = wizard.tab_generators[TutorialStep.backup_seed] + with patch("bitcoin_safe.pdfrecovery.xdg_open_file") as mock_open: + assert step.custom_yes_button.isVisible() + step.custom_yes_button.click() + mock_open.assert_called_once() - page_backup() + temp_file = os.path.join(Path.home(), f"Seed backup of {wallet_name}.pdf") + assert Path(temp_file).exists() + # remove the file again + Path(temp_file).unlink() - def page_receive() -> None: - shutter.save(main_window) - step: ReceiveTest = wizard.tab_generators[TutorialStep.receive] - assert isinstance(step.quick_receive, BitcoinQuickReceive) - address = step.quick_receive.group_boxes[0].text_edit.input_field.toPlainText() - assert address == "bcrt1q3qt0n3z69sds3u6zxalds3fl67rez4u2wm4hes" - faucet.send(address, amount=amount) - - called_args_message_box = get_called_args_message_box( - "bitcoin_safe.gui.qt.wizard.Message", - step.check_button, - repeat_clicking_until_message_box_called=True, - ) - assert str(called_args_message_box) == str( - ( - "Balance = {amount}".format( - amount=Satoshis(amount, network=test_config.network).str_with_unit() - ), + page_backup() + + def page_receive() -> None: + shutter.save(main_window) + step: ReceiveTest = wizard.tab_generators[TutorialStep.receive] + assert isinstance(step.quick_receive, BitcoinQuickReceive) + address = step.quick_receive.group_boxes[0].text_edit.input_field.toPlainText() + assert address == "bcrt1q3qt0n3z69sds3u6zxalds3fl67rez4u2wm4hes" + faucet.send(address, amount=amount) + + called_args_message_box = get_called_args_message_box( + "bitcoin_safe.gui.qt.wizard.Message", + step.check_button, + repeat_clicking_until_message_box_called=True, ) - ) - assert not step.check_button.isVisible() - assert step.next_button.isVisible() - shutter.save(main_window) - step.next_button.click() - shutter.save(main_window) + assert str(called_args_message_box) == str( + ( + "Balance = {amount}".format( + amount=Satoshis(amount, network=test_config.network).str_with_unit() + ), + ) + ) + assert not step.check_button.isVisible() + assert step.next_button.isVisible() + shutter.save(main_window) + step.next_button.click() + shutter.save(main_window) - page_receive() + page_receive() - def page_send() -> None: - shutter.save(main_window) - step: SendTest = wizard.tab_generators[TutorialStep.send] - assert step.refs.floating_button_box.isVisible() - assert step.refs.floating_button_box.button_create_tx.isVisible() - assert not step.refs.floating_button_box.tutorial_button_prefill.isVisible() + def page_send() -> None: + shutter.save(main_window) + step: SendTest = wizard.tab_generators[TutorialStep.send] + assert step.refs.floating_button_box.isVisible() + assert step.refs.floating_button_box.button_create_tx.isVisible() + assert not step.refs.floating_button_box.tutorial_button_prefill.isVisible() - shutter.save(main_window) + shutter.save(main_window) - assert qt_wallet.tabs.currentWidget() == qt_wallet.send_tab - box = qt_wallet.uitx_creator.recipients.get_recipient_group_boxes()[0] - shutter.save(main_window) - assert [recipient.address for recipient in qt_wallet.uitx_creator.recipients.recipients] == [ - "bcrt1qz07mxz0pm3mj4jhypc6llm5mtzkcdeu3pnw042" - ] - assert box.address == "bcrt1qz07mxz0pm3mj4jhypc6llm5mtzkcdeu3pnw042" - assert ( - box.recipient_widget.address_edit.input_field.palette() - .color(QtGui.QPalette.ColorRole.Base) - .name() - == "#8af296" - ) - fee_info = qt_wallet.uitx_creator.estimate_fee_info( - qt_wallet.uitx_creator.fee_group.spin_fee_rate.value() - ) - assert qt_wallet.uitx_creator.recipients.recipients[0].amount == amount - fee_info.fee_amount - assert qt_wallet.uitx_creator.recipients.recipients[0].checked_max_amount + assert qt_wallet.tabs.currentWidget() == qt_wallet.send_tab + box = qt_wallet.uitx_creator.recipients.get_recipient_group_boxes()[0] + shutter.save(main_window) + assert [recipient.address for recipient in qt_wallet.uitx_creator.recipients.recipients] == [ + "bcrt1qz07mxz0pm3mj4jhypc6llm5mtzkcdeu3pnw042" + ] + assert box.address == "bcrt1qz07mxz0pm3mj4jhypc6llm5mtzkcdeu3pnw042" + assert ( + box.recipient_widget.address_edit.input_field.palette() + .color(QtGui.QPalette.ColorRole.Base) + .name() + == "#8af296" + ) + fee_info = qt_wallet.uitx_creator.estimate_fee_info( + qt_wallet.uitx_creator.fee_group.spin_fee_rate.value() + ) + assert qt_wallet.uitx_creator.recipients.recipients[0].amount == amount - fee_info.fee_amount + assert qt_wallet.uitx_creator.recipients.recipients[0].checked_max_amount - assert step.refs.floating_button_box.button_create_tx.isVisible() - step.refs.floating_button_box.button_create_tx.click() - shutter.save(main_window) + assert step.refs.floating_button_box.button_create_tx.isVisible() + step.refs.floating_button_box.button_create_tx.click() + shutter.save(main_window) - page_send() + page_send() - def page_sign() -> None: - shutter.save(main_window) - viewer = main_window.tab_wallets.getCurrentTabData() - assert isinstance(viewer, UITx_Viewer) - assert [recipient.address for recipient in viewer.recipients.recipients] == [ - "bcrt1qz07mxz0pm3mj4jhypc6llm5mtzkcdeu3pnw042" - ] - assert [recipient.label for recipient in viewer.recipients.recipients] == ["Send Test"] - assert [recipient.amount for recipient in viewer.recipients.recipients] == [999890] - assert viewer.fee_info - assert round(viewer.fee_info.fee_rate(), 1) == 1.3 - assert not viewer.fee_group.allow_edit - assert viewer.fee_group.spin_fee_rate.value() == 1.3 - assert viewer.fee_group.approximate_fee_label.isVisible() - - assert viewer.button_next.isVisible() - viewer.button_next.click() - shutter.save(main_window) + def page_sign() -> None: + shutter.save(main_window) + viewer = main_window.tab_wallets.getCurrentTabData() + assert isinstance(viewer, UITx_Viewer) + assert [recipient.address for recipient in viewer.recipients.recipients] == [ + "bcrt1qz07mxz0pm3mj4jhypc6llm5mtzkcdeu3pnw042" + ] + assert [recipient.label for recipient in viewer.recipients.recipients] == ["Send Test"] + assert [recipient.amount for recipient in viewer.recipients.recipients] == [999890] + assert viewer.fee_info + assert round(viewer.fee_info.fee_rate(), 1) == 1.3 + assert not viewer.fee_group.allow_edit + assert viewer.fee_group.spin_fee_rate.value() == 1.3 + assert viewer.fee_group.approximate_fee_label.isVisible() + + assert viewer.button_next.isVisible() + viewer.button_next.click() + shutter.save(main_window) - assert viewer.tx_singning_steps - importers = list(viewer.tx_singning_steps.signature_importer_dict.values())[0] - assert [importer.__class__.__name__ for importer in importers] == [ - "SignatureImporterWallet", - "SignatureImporterQR", - "SignatureImporterFile", - "SignatureImporterClipboard", - "SignatureImporterUSB", - ] - assert viewer.tx_singning_steps - widget = viewer.tx_singning_steps.stacked_widget.currentWidget() - assert isinstance(widget, HorizontalImporters) - assert isinstance(widget.group_seed.data, SignerUI) - for button in widget.group_seed.data.findChildren(QPushButton): - assert button.text() == "Sign with mnemonic seed" - assert button.isVisible() - button.click() - - # send it away now - shutter.save(main_window) + assert viewer.tx_singning_steps + importers = list(viewer.tx_singning_steps.signature_importer_dict.values())[0] + assert [importer.__class__.__name__ for importer in importers] == [ + "SignatureImporterWallet", + "SignatureImporterQR", + "SignatureImporterFile", + "SignatureImporterClipboard", + "SignatureImporterUSB", + ] + assert viewer.tx_singning_steps + widget = viewer.tx_singning_steps.stacked_widget.currentWidget() + assert isinstance(widget, HorizontalImporters) + assert isinstance(widget.group_seed.data, SignerUI) + for button in widget.group_seed.data.findChildren(QPushButton): + assert button.text() == "Sign with mnemonic seed" + assert button.isVisible() + button.click() + + # send it away now + shutter.save(main_window) - assert viewer.button_send.isVisible() + assert viewer.button_send.isVisible() - with patch("bitcoin_safe.gui.qt.wizard.Message") as mock_message: - with qtbot.waitSignal( - main_window.signals.wallet_signals[qt_wallet.wallet.id].updated, timeout=10000 - ): # Timeout after 10 seconds - viewer.button_send.click() - qtbot.wait(1000) - mock_message.assert_called_with( - main_window.tr("All Send tests done successfully."), type=MessageType.Info - ) + with patch("bitcoin_safe.gui.qt.wizard.Message") as mock_message: + with qtbot.waitSignal( + main_window.signals.wallet_signals[qt_wallet.wallet.id].updated, timeout=10000 + ): # Timeout after 10 seconds + viewer.button_send.click() + qtbot.wait(1000) + mock_message.assert_called_with( + main_window.tr("All Send tests done successfully."), type=MessageType.Info + ) - # hist list - shutter.save(main_window) + # hist list + shutter.save(main_window) - page_sign() + page_sign() - def page10() -> None: - shutter.save(main_window) + def page10() -> None: + shutter.save(main_window) - step: DistributeSeeds = wizard.tab_generators[TutorialStep.distribute] - assert step.buttonbox_buttons[0].isVisible() - step.buttonbox_buttons[0].click() + step: DistributeSeeds = wizard.tab_generators[TutorialStep.distribute] + assert step.buttonbox_buttons[0].isVisible() + step.buttonbox_buttons[0].click() - shutter.save(main_window) + shutter.save(main_window) - page10() + page10() - def page11() -> None: - shutter.save(main_window) + def page11() -> None: + shutter.save(main_window) - step: LabelBackup = wizard.tab_generators[TutorialStep.sync] - assert step.buttonbox_buttons[0].isVisible() - step.buttonbox_buttons[0].click() + step: LabelBackup = wizard.tab_generators[TutorialStep.sync] + assert step.buttonbox_buttons[0].isVisible() + step.buttonbox_buttons[0].click() - shutter.save(main_window) + shutter.save(main_window) - page11() + page11() - def do_close_wallet() -> None: + do_all(qt_wallet) + del wizard + + with CheckedDeletionContext( + qt_wallet=qt_wallet, qtbot=qtbot, caplog=caplog, graph_directory=shutter.used_directory() + ): + wallet_id = qt_wallet.wallet.id + del qt_wallet close_wallet( shutter=shutter, test_config=test_config, - wallet_name=wallet_name, + wallet_name=wallet_id, qtbot=qtbot, main_window=main_window, ) + main_window.on_close_all_tx_tabs() shutter.save(main_window) - do_close_wallet() - def check_that_it_is_in_recent_wallets() -> None: assert any( [ @@ -498,4 +510,3 @@ def check_that_it_is_in_recent_wallets() -> None: # end shutter.save(main_window) - sleep(2) diff --git a/tests/gui/qt/test_setup_wallet_custom.py b/tests/gui/qt/test_setup_wallet_custom.py index 96ddf95..d95c996 100644 --- a/tests/gui/qt/test_setup_wallet_custom.py +++ b/tests/gui/qt/test_setup_wallet_custom.py @@ -31,7 +31,6 @@ import logging from datetime import datetime from pathlib import Path -from time import sleep import bdkpython as bdk import pytest @@ -45,12 +44,12 @@ from bitcoin_safe.gui.qt.descriptor_edit import DescriptorExport from bitcoin_safe.gui.qt.dialogs import WalletIdDialog from bitcoin_safe.gui.qt.qt_wallet import QTProtoWallet, QTWallet -from bitcoin_safe.logging_setup import setup_logging # type: ignore from tests.gui.qt.test_setup_wallet import close_wallet, get_tab_with_title, save_wallet from ...test_helpers import test_config # type: ignore from ...test_setup_bitcoin_core import Faucet, bitcoin_core, faucet # type: ignore from .test_helpers import ( # type: ignore + CheckedDeletionContext, Shutter, close_wallet, do_modal_click, @@ -72,6 +71,7 @@ def test_custom_wallet_setup_custom_single_sig( test_config: UserConfig, bitcoin_core: Path, faucet: Faucet, + caplog: pytest.LogCaptureFixture, wallet_name: str = "test_custom_wallet_setup_custom_single_sig", amount: int = int(1e6), ) -> None: # bitcoin_core: Path, @@ -99,48 +99,48 @@ def on_wallet_id_dialog(dialog: WalletIdDialog) -> None: do_modal_click(button, on_wallet_id_dialog, qtbot, cls=WalletIdDialog) w = get_tab_with_title(main_window.tab_wallets, title=wallet_name) - qt_proto_wallet = main_window.tab_wallets.get_data_for_tab(w) - assert isinstance(qt_proto_wallet, QTProtoWallet) + qt_protowallet = main_window.tab_wallets.get_data_for_tab(w) + assert isinstance(qt_protowallet, QTProtoWallet) def test_block_change_signals() -> None: - with BlockChangesSignals([qt_proto_wallet.wallet_descriptor_ui.tab]): - assert qt_proto_wallet.wallet_descriptor_ui.spin_req.signalsBlocked() - with BlockChangesSignals([qt_proto_wallet.wallet_descriptor_ui.tab]): - with BlockChangesSignals([qt_proto_wallet.wallet_descriptor_ui.tab]): - assert qt_proto_wallet.wallet_descriptor_ui.spin_req.signalsBlocked() - assert qt_proto_wallet.wallet_descriptor_ui.spin_req.signalsBlocked() + with BlockChangesSignals([qt_protowallet.wallet_descriptor_ui]): + assert qt_protowallet.wallet_descriptor_ui.spin_req.signalsBlocked() + with BlockChangesSignals([qt_protowallet.wallet_descriptor_ui]): + with BlockChangesSignals([qt_protowallet.wallet_descriptor_ui]): + assert qt_protowallet.wallet_descriptor_ui.spin_req.signalsBlocked() + assert qt_protowallet.wallet_descriptor_ui.spin_req.signalsBlocked() def check_consistent() -> None: - signers = qt_proto_wallet.wallet_descriptor_ui.spin_signers.value() - qt_proto_wallet.wallet_descriptor_ui.spin_req.value() + signers = qt_protowallet.wallet_descriptor_ui.spin_signers.value() + qt_protowallet.wallet_descriptor_ui.spin_req.value() - assert signers == qt_proto_wallet.wallet_descriptor_ui.keystore_uis.count() + assert signers == qt_protowallet.wallet_descriptor_ui.keystore_uis.count() for i in range(signers): - assert qt_proto_wallet.wallet_descriptor_ui.keystore_uis.tabText( + assert qt_protowallet.wallet_descriptor_ui.keystore_uis.tabText( i - ) == qt_proto_wallet.protowallet.signer_name(i) + ) == qt_protowallet.protowallet.signer_name(i) - if qt_proto_wallet.protowallet.is_multisig(): + if qt_protowallet.protowallet.is_multisig(): assert AddressTypes.p2wsh in [ - qt_proto_wallet.wallet_descriptor_ui.comboBox_address_type.itemData(i) - for i in range(qt_proto_wallet.wallet_descriptor_ui.comboBox_address_type.count()) + qt_protowallet.wallet_descriptor_ui.comboBox_address_type.itemData(i) + for i in range(qt_protowallet.wallet_descriptor_ui.comboBox_address_type.count()) ] else: assert AddressTypes.p2pkh in [ - qt_proto_wallet.wallet_descriptor_ui.comboBox_address_type.itemData(i) - for i in range(qt_proto_wallet.wallet_descriptor_ui.comboBox_address_type.count()) + qt_protowallet.wallet_descriptor_ui.comboBox_address_type.itemData(i) + for i in range(qt_protowallet.wallet_descriptor_ui.comboBox_address_type.count()) ] def page1() -> None: shutter.save(main_window) - assert qt_proto_wallet.wallet_descriptor_ui.spin_req.value() == 3 - assert qt_proto_wallet.wallet_descriptor_ui.spin_signers.value() == 5 + assert qt_protowallet.wallet_descriptor_ui.spin_req.value() == 3 + assert qt_protowallet.wallet_descriptor_ui.spin_signers.value() == 5 assert ( - qt_proto_wallet.wallet_descriptor_ui.comboBox_address_type.currentData() == AddressTypes.p2wsh + qt_protowallet.wallet_descriptor_ui.comboBox_address_type.currentData() == AddressTypes.p2wsh ) - assert qt_proto_wallet.wallet_descriptor_ui.spin_gap.value() == 20 - assert qt_proto_wallet.wallet_descriptor_ui.keystore_uis.count() == 5 + assert qt_protowallet.wallet_descriptor_ui.spin_gap.value() == 20 + assert qt_protowallet.wallet_descriptor_ui.keystore_uis.count() == 5 shutter.save(main_window) check_consistent() @@ -149,15 +149,15 @@ def page1() -> None: page1() def change_to_single_sig() -> None: - assert qt_proto_wallet.protowallet.is_multisig() - qt_proto_wallet.wallet_descriptor_ui.spin_req.setValue(1) - assert qt_proto_wallet.wallet_descriptor_ui.spin_req.value() == 1 + assert qt_protowallet.protowallet.is_multisig() + qt_protowallet.wallet_descriptor_ui.spin_req.setValue(1) + assert qt_protowallet.wallet_descriptor_ui.spin_req.value() == 1 # change to single sig - qt_proto_wallet.wallet_descriptor_ui.spin_signers.setValue(1) - assert qt_proto_wallet.wallet_descriptor_ui.spin_signers.value() == 1 + qt_protowallet.wallet_descriptor_ui.spin_signers.setValue(1) + assert qt_protowallet.wallet_descriptor_ui.spin_signers.value() == 1 - assert not qt_proto_wallet.protowallet.is_multisig() + assert not qt_protowallet.protowallet.is_multisig() shutter.save(main_window) check_consistent() @@ -165,7 +165,7 @@ def change_to_single_sig() -> None: change_to_single_sig() def do_save_wallet() -> None: - key = list(qt_proto_wallet.wallet_descriptor_ui.keystore_uis.getAllTabData().values())[0] + key = list(qt_protowallet.wallet_descriptor_ui.keystore_uis.getAllTabData().values())[0] key.tabs_import_type.setCurrentWidget(key.tab_manual) shutter.save(main_window) @@ -186,7 +186,7 @@ def do_save_wallet() -> None: save_wallet( test_config=test_config, wallet_name=wallet_name, - save_button=qt_proto_wallet.wallet_descriptor_ui.button_box.button( + save_button=qt_protowallet.wallet_descriptor_ui.button_box.button( QDialogButtonBox.StandardButton.Apply ), ) @@ -201,52 +201,62 @@ def do_save_wallet() -> None: ) assert isinstance(qt_wallet, QTWallet) - def export_wallet_descriptor() -> None: - def on_dialog(dialog: DescriptorExport): - shutter.save(dialog) - assert dialog.isVisible() - dialog.close() + def do_all(qt_wallet: QTWallet): + "any implicit reference to qt_wallet (including the function page_send) will create a cell refrence" - do_modal_click(main_window.show_descriptor_export_window, on_dialog, qtbot, cls=DescriptorExport) + def export_wallet_descriptor() -> None: + def on_dialog(dialog: DescriptorExport): + shutter.save(dialog) + assert dialog.isVisible() + dialog.close() - shutter.save(main_window) + do_modal_click( + main_window.show_descriptor_export_window, on_dialog, qtbot, cls=DescriptorExport + ) + + shutter.save(main_window) + + export_wallet_descriptor() + + def check_that_it_is_in_recent_wallets() -> None: + assert any( + [ + (wallet_name in name) + for name in main_window.config.recently_open_wallets[main_window.config.network] + ] + ) + + shutter.save(main_window) - export_wallet_descriptor() + check_that_it_is_in_recent_wallets() - def do_close_wallet() -> None: + def switch_language() -> None: + main_window.language_chooser.switchLanguage("zh_CN") + shutter.save(main_window) + main_window.language_chooser.switchLanguage("en_US") + shutter.save(main_window) + + switch_language() + + do_all(qt_wallet) + + with CheckedDeletionContext( + qt_wallet=qt_wallet, qtbot=qtbot, caplog=caplog, graph_directory=shutter.used_directory() + ): + # if True: + wallet_id = qt_wallet.wallet.id + del qt_wallet close_wallet( shutter=shutter, test_config=test_config, - wallet_name=wallet_name, + wallet_name=wallet_id, qtbot=qtbot, main_window=main_window, ) + main_window.on_close_all_tx_tabs() shutter.save(main_window) - do_close_wallet() - - def check_that_it_is_in_recent_wallets() -> None: - assert any( - [ - (wallet_name in name) - for name in main_window.config.recently_open_wallets[main_window.config.network] - ] - ) - - shutter.save(main_window) - - check_that_it_is_in_recent_wallets() - - def switch_language() -> None: - main_window.language_chooser.switchLanguage("zh_CN") - shutter.save(main_window) - main_window.language_chooser.switchLanguage("en_US") - shutter.save(main_window) - - switch_language() - # end shutter.save(main_window) - sleep(2) diff --git a/tests/gui/qt/test_wallet_features.py b/tests/gui/qt/test_wallet_features.py index 0680c48..4246e6a 100644 --- a/tests/gui/qt/test_wallet_features.py +++ b/tests/gui/qt/test_wallet_features.py @@ -33,7 +33,6 @@ import tempfile from datetime import datetime from pathlib import Path -from time import sleep from unittest.mock import patch import bdkpython as bdk @@ -55,13 +54,13 @@ from bitcoin_safe.gui.qt.qt_wallet import QTProtoWallet, QTWallet from bitcoin_safe.gui.qt.register_multisig import RegisterMultisigInteractionWidget from bitcoin_safe.hardware_signers import DescriptorQrExportTypes -from bitcoin_safe.logging_setup import setup_logging # type: ignore from tests.gui.qt.test_setup_wallet import close_wallet, get_tab_with_title, save_wallet from ...non_gui.test_signers import test_seeds from ...test_helpers import test_config # type: ignore from ...test_setup_bitcoin_core import Faucet, bitcoin_core, faucet # type: ignore from .test_helpers import ( # type: ignore + CheckedDeletionContext, Shutter, close_wallet, do_modal_click, @@ -83,6 +82,7 @@ def test_wallet_features_multisig( test_config: UserConfig, bitcoin_core: Path, faucet: Faucet, + caplog: pytest.LogCaptureFixture, wallet_name: str = "test_custom_wallet_setup_custom_single_sig2", amount: int = int(1e6), ) -> None: # bitcoin_core: Path, @@ -110,49 +110,52 @@ def on_wallet_id_dialog(dialog: WalletIdDialog) -> None: do_modal_click(button, on_wallet_id_dialog, qtbot, cls=WalletIdDialog) + assert len(main_window.qt_protowallets) == 1 + w = get_tab_with_title(main_window.tab_wallets, title=wallet_name) - qt_proto_wallet = main_window.tab_wallets.get_data_for_tab(w) - assert isinstance(qt_proto_wallet, QTProtoWallet) + qt_protowallet = main_window.tab_wallets.get_data_for_tab(w) + assert isinstance(qt_protowallet, QTProtoWallet) + assert qt_protowallet == list(main_window.qt_protowallets.values())[0] def test_block_change_signals() -> None: - with BlockChangesSignals([qt_proto_wallet.wallet_descriptor_ui.tab]): - assert qt_proto_wallet.wallet_descriptor_ui.spin_req.signalsBlocked() - with BlockChangesSignals([qt_proto_wallet.wallet_descriptor_ui.tab]): - with BlockChangesSignals([qt_proto_wallet.wallet_descriptor_ui.tab]): - assert qt_proto_wallet.wallet_descriptor_ui.spin_req.signalsBlocked() - assert qt_proto_wallet.wallet_descriptor_ui.spin_req.signalsBlocked() + with BlockChangesSignals([qt_protowallet.wallet_descriptor_ui]): + assert qt_protowallet.wallet_descriptor_ui.spin_req.signalsBlocked() + with BlockChangesSignals([qt_protowallet.wallet_descriptor_ui]): + with BlockChangesSignals([qt_protowallet.wallet_descriptor_ui]): + assert qt_protowallet.wallet_descriptor_ui.spin_req.signalsBlocked() + assert qt_protowallet.wallet_descriptor_ui.spin_req.signalsBlocked() def check_consistent() -> None: - signers = qt_proto_wallet.wallet_descriptor_ui.spin_signers.value() - qt_proto_wallet.wallet_descriptor_ui.spin_req.value() + signers = qt_protowallet.wallet_descriptor_ui.spin_signers.value() + qt_protowallet.wallet_descriptor_ui.spin_req.value() - assert signers == qt_proto_wallet.wallet_descriptor_ui.keystore_uis.count() + assert signers == qt_protowallet.wallet_descriptor_ui.keystore_uis.count() for i in range(signers): - assert qt_proto_wallet.wallet_descriptor_ui.keystore_uis.tabText( + assert qt_protowallet.wallet_descriptor_ui.keystore_uis.tabText( i - ) == qt_proto_wallet.protowallet.signer_name(i) + ) == qt_protowallet.protowallet.signer_name(i) - if qt_proto_wallet.protowallet.is_multisig(): + if qt_protowallet.protowallet.is_multisig(): assert AddressTypes.p2wsh in [ - qt_proto_wallet.wallet_descriptor_ui.comboBox_address_type.itemData(i) - for i in range(qt_proto_wallet.wallet_descriptor_ui.comboBox_address_type.count()) + qt_protowallet.wallet_descriptor_ui.comboBox_address_type.itemData(i) + for i in range(qt_protowallet.wallet_descriptor_ui.comboBox_address_type.count()) ] else: assert AddressTypes.p2pkh in [ - qt_proto_wallet.wallet_descriptor_ui.comboBox_address_type.itemData(i) - for i in range(qt_proto_wallet.wallet_descriptor_ui.comboBox_address_type.count()) + qt_protowallet.wallet_descriptor_ui.comboBox_address_type.itemData(i) + for i in range(qt_protowallet.wallet_descriptor_ui.comboBox_address_type.count()) ] def page1() -> None: shutter.save(main_window) - assert qt_proto_wallet.wallet_descriptor_ui.spin_req.value() == 3 - assert qt_proto_wallet.wallet_descriptor_ui.spin_signers.value() == 5 + assert qt_protowallet.wallet_descriptor_ui.spin_req.value() == 3 + assert qt_protowallet.wallet_descriptor_ui.spin_signers.value() == 5 assert ( - qt_proto_wallet.wallet_descriptor_ui.comboBox_address_type.currentData() == AddressTypes.p2wsh + qt_protowallet.wallet_descriptor_ui.comboBox_address_type.currentData() == AddressTypes.p2wsh ) - assert qt_proto_wallet.wallet_descriptor_ui.spin_gap.value() == 20 - assert qt_proto_wallet.wallet_descriptor_ui.keystore_uis.count() == 5 + assert qt_protowallet.wallet_descriptor_ui.spin_gap.value() == 20 + assert qt_protowallet.wallet_descriptor_ui.keystore_uis.count() == 5 shutter.save(main_window) check_consistent() @@ -161,15 +164,15 @@ def page1() -> None: page1() def set_simple_multisig() -> None: - assert qt_proto_wallet.protowallet.is_multisig() - qt_proto_wallet.wallet_descriptor_ui.spin_req.setValue(1) - assert qt_proto_wallet.wallet_descriptor_ui.spin_req.value() == 1 + assert qt_protowallet.protowallet.is_multisig() + qt_protowallet.wallet_descriptor_ui.spin_req.setValue(1) + assert qt_protowallet.wallet_descriptor_ui.spin_req.value() == 1 # change to single sig - qt_proto_wallet.wallet_descriptor_ui.spin_signers.setValue(2) - assert qt_proto_wallet.wallet_descriptor_ui.spin_signers.value() == 2 + qt_protowallet.wallet_descriptor_ui.spin_signers.setValue(2) + assert qt_protowallet.wallet_descriptor_ui.spin_signers.value() == 2 - assert qt_proto_wallet.protowallet.is_multisig() + assert qt_protowallet.protowallet.is_multisig() shutter.save(main_window) check_consistent() @@ -177,7 +180,7 @@ def set_simple_multisig() -> None: set_simple_multisig() def set_mnemonic(index: int) -> None: - key = list(qt_proto_wallet.wallet_descriptor_ui.keystore_uis.getAllTabData().values())[index] + key = list(qt_protowallet.wallet_descriptor_ui.keystore_uis.getAllTabData().values())[index] key.tabs_import_type.setCurrentWidget(key.tab_manual) shutter.save(main_window) @@ -201,7 +204,7 @@ def do_save_wallet() -> None: wallet_file = save_wallet( test_config=test_config, wallet_name=wallet_name, - save_button=qt_proto_wallet.wallet_descriptor_ui.button_box.button( + save_button=qt_protowallet.wallet_descriptor_ui.button_box.button( QDialogButtonBox.StandardButton.Apply ), ) @@ -216,224 +219,248 @@ def do_save_wallet() -> None: get_tab_with_title(main_window.tab_wallets, title=wallet_name) ) assert isinstance(qt_wallet, QTWallet) + assert len(main_window.qt_wallets) == 1 + assert qt_wallet == list(main_window.qt_wallets.values())[0] - shutter.save(main_window) - # check wallet address - assert ( - qt_wallet.wallet.get_addresses()[0] - == "bcrt1qklm7yyvyu2av4f35ve6tm8mpn6mkr8e3dpjd3jp9vn77vu670g7qu9cznl" - ) + def do_all(qt_wallet: QTWallet): + "any implicit reference to qt_wallet (including the function page_send) will create a cell refrence" + shutter.save(main_window) + # check wallet address + assert ( + qt_wallet.wallet.get_addresses()[0] + == "bcrt1qklm7yyvyu2av4f35ve6tm8mpn6mkr8e3dpjd3jp9vn77vu670g7qu9cznl" + ) - ## from here starts testing features + ## from here starts testing features - wallet_name = wallet_name + " new" + wallet_name = qt_wallet.wallet.id + " new" - def menu_action_rename_wallet() -> None: - def callback(dialog: WalletIdDialog) -> None: - shutter.save(dialog) - dialog.name_input.setText(wallet_name) - shutter.save(dialog) + def menu_action_rename_wallet() -> None: + def callback(dialog: WalletIdDialog) -> None: + shutter.save(dialog) + dialog.name_input.setText(wallet_name) + shutter.save(dialog) - dialog.buttonbox.button(QDialogButtonBox.StandardButton.Ok).click() - shutter.save(main_window) + dialog.buttonbox.button(QDialogButtonBox.StandardButton.Ok).click() + shutter.save(main_window) - do_modal_click(main_window.menu_action_rename_wallet, callback, qtbot, cls=WalletIdDialog) + do_modal_click(main_window.menu_action_rename_wallet, callback, qtbot, cls=WalletIdDialog) - menu_action_rename_wallet() + menu_action_rename_wallet() - def menu_action_change_password() -> None: - with patch("bitcoin_safe.gui.qt.qt_wallet.Message") as mock_message: + def menu_action_change_password() -> None: + with patch("bitcoin_safe.gui.qt.qt_wallet.Message") as mock_message: - def callback(dialog: PasswordCreation) -> None: - shutter.save(dialog) - dialog.password_input1.setText("new password") - dialog.password_input2.setText("new password") + def callback(dialog: PasswordCreation) -> None: + shutter.save(dialog) + dialog.password_input1.setText("new password") + dialog.password_input2.setText("new password") + shutter.save(dialog) + + shutter.save(main_window) + dialog.submit_button.click() + + do_modal_click( + main_window.menu_action_change_password, callback, qtbot, cls=PasswordCreation + ) + + QTest.qWait(200) + + # Inspect the call arguments for each call + calls = mock_message.call_args_list + + first_call_args = calls[0][0] # args of the first call + assert first_call_args == ("Wallet saved",) + + menu_action_change_password() + + def menu_action_export_pdf() -> None: + with patch("bitcoin_safe.pdfrecovery.xdg_open_file") as mock_open: + main_window.menu_action_export_pdf.trigger() + + mock_open.assert_called_once() + + temp_file = os.path.join(Path.home(), f"Seed backup of {wallet_name}.pdf") + assert Path(temp_file).exists() + # remove the file again + Path(temp_file).unlink() + + menu_action_export_pdf() + + def menu_action_export_descriptor() -> None: + def callback(dialog: DescriptorExport) -> None: shutter.save(dialog) + dialog.close() - shutter.save(main_window) - dialog.submit_button.click() - - do_modal_click(main_window.menu_action_change_password, callback, qtbot, cls=PasswordCreation) - - QTest.qWait(200) - - # Inspect the call arguments for each call - calls = mock_message.call_args_list - - first_call_args = calls[0][0] # args of the first call - assert first_call_args == ("Wallet saved",) - - menu_action_change_password() - - def menu_action_export_pdf() -> None: - with patch("bitcoin_safe.pdfrecovery.xdg_open_file") as mock_open: - main_window.menu_action_export_pdf.trigger() - - mock_open.assert_called_once() - - temp_file = os.path.join(Path.home(), f"Seed backup of {wallet_name}.pdf") - assert Path(temp_file).exists() - # remove the file again - Path(temp_file).unlink() - - menu_action_export_pdf() - - def menu_action_export_descriptor() -> None: - def callback(dialog: DescriptorExport) -> None: - shutter.save(dialog) - dialog.close() - - do_modal_click(main_window.menu_action_export_descriptor, callback, qtbot, cls=DescriptorExport) - - menu_action_export_descriptor() - - def menu_action_register_multisig() -> None: - def callback(dialog: RegisterMultisigInteractionWidget) -> None: - shutter.save(dialog) - - with tempfile.TemporaryDirectory() as temp_dir: - # export qr gifs - for action in dialog.export_qr_widget.button_file._menu.actions(): - # export as file - filename = ( - Path(temp_dir) / f"file_{action.text()}.t" - ) # check that it also works with incomplete extensions - with patch("bitcoin_safe.gui.qt.export_data.save_file_dialog") as mock_dialog: - mock_dialog.return_value = str(filename) - action.trigger() - - mock_dialog.assert_called_once() - assert filename.exists() - - # export qr gifs - assert dialog.export_qr_widget - for i in reversed( - range(dialog.export_qr_widget.combo_qr_type.count()) - ): # reversed to it always has to set the widget to trigger signal_set_qr_images - text = dialog.export_qr_widget.combo_qr_type.itemText(i) - basename = ( - f"file_{text}.png" - if text.startswith(DescriptorQrExportTypes.text.display_name) - else f"file_{text}.gif" - ) - filename = Path(temp_dir) / basename - with patch("bitcoin_safe.gui.qt.export_data.save_file_dialog") as mock_dialog: - mock_dialog.return_value = str(filename) - # set the qr code - with qtbot.waitSignal( - dialog.export_qr_widget.signal_set_qr_images, timeout=5000 - ) as blocker: - dialog.export_qr_widget.combo_qr_type.setCurrentIndex(i) - dialog.export_qr_widget.button_save_qr.click() - - mock_dialog.assert_called_once() - assert filename.exists() - - dialog.export_qr_widget.close() - dialog.close() - - do_modal_click( - main_window.menu_action_register_multisig, - callback, - qtbot, - cls=RegisterMultisigInteractionWidget, - ) + do_modal_click( + main_window.menu_action_export_descriptor, callback, qtbot, cls=DescriptorExport + ) - menu_action_register_multisig() + menu_action_export_descriptor() - def menu_action_open_hwi_manager() -> None: - def callback(dialog: ToolGui) -> None: - shutter.save(dialog) - dialog.close() + def menu_action_register_multisig() -> None: + def callback(dialog: RegisterMultisigInteractionWidget) -> None: + shutter.save(dialog) - do_modal_click( - main_window.menu_action_open_hwi_manager, - callback, - qtbot, - cls=ToolGui, - ) + with tempfile.TemporaryDirectory() as temp_dir: + # export qr gifs + for action in dialog.export_qr_widget.button_file._menu.actions(): + # export as file + filename = ( + Path(temp_dir) / f"file_{action.text()}.t" + ) # check that it also works with incomplete extensions + with patch("bitcoin_safe.gui.qt.export_data.save_file_dialog") as mock_dialog: + mock_dialog.return_value = str(filename) + action.trigger() + + mock_dialog.assert_called_once() + assert filename.exists() + + # export qr gifs + assert dialog.export_qr_widget + for i in reversed( + range(dialog.export_qr_widget.combo_qr_type.count()) + ): # reversed to it always has to set the widget to trigger signal_set_qr_images + text = dialog.export_qr_widget.combo_qr_type.itemText(i) + basename = ( + f"file_{text}.png" + if text.startswith(DescriptorQrExportTypes.text.display_name) + else f"file_{text}.gif" + ) + filename = Path(temp_dir) / basename + with patch("bitcoin_safe.gui.qt.export_data.save_file_dialog") as mock_dialog: + mock_dialog.return_value = str(filename) + # set the qr code + with qtbot.waitSignal( + dialog.export_qr_widget.signal_set_qr_images, timeout=5000 + ) as blocker: + dialog.export_qr_widget.combo_qr_type.setCurrentIndex(i) + dialog.export_qr_widget.button_save_qr.click() + + mock_dialog.assert_called_once() + assert filename.exists() + + dialog.export_qr_widget.close() + dialog.close() + + do_modal_click( + main_window.menu_action_register_multisig, + callback, + qtbot, + cls=RegisterMultisigInteractionWidget, + ) + + menu_action_register_multisig() + + def menu_action_open_hwi_manager() -> None: + def callback(dialog: ToolGui) -> None: + shutter.save(dialog) + dialog.close() - menu_action_open_hwi_manager() + do_modal_click( + main_window.menu_action_open_hwi_manager, + callback, + qtbot, + cls=ToolGui, + ) - def menu_action_open_tx_from_str() -> None: - def callback(dialog: ImportDialog) -> None: - shutter.save(dialog) - dialog.close() + menu_action_open_hwi_manager() - do_modal_click( - main_window.menu_action_open_tx_from_str, - callback, - qtbot, - cls=ImportDialog, - ) + def menu_action_open_tx_from_str() -> None: + def callback(dialog: ImportDialog) -> None: + shutter.save(dialog) + dialog.close() - menu_action_open_tx_from_str() + do_modal_click( + main_window.menu_action_open_tx_from_str, + callback, + qtbot, + cls=ImportDialog, + ) - def menu_action_load_tx_from_qr() -> None: - def callback(dialog: BitcoinVideoWidget) -> None: - shutter.save(dialog) - dialog.close() + menu_action_open_tx_from_str() - do_modal_click( - main_window.menu_action_load_tx_from_qr, - callback, - qtbot, - cls=BitcoinVideoWidget, - ) + def menu_action_load_tx_from_qr() -> None: + def callback(dialog: BitcoinVideoWidget) -> None: + shutter.save(dialog) + dialog.close() - menu_action_load_tx_from_qr() + do_modal_click( + main_window.menu_action_load_tx_from_qr, + callback, + qtbot, + cls=BitcoinVideoWidget, + ) - def menu_action_network_settings() -> None: - def callback(dialog: NetworkSettingsUI) -> None: - shutter.save(dialog) - dialog.close() + menu_action_load_tx_from_qr() - do_modal_click( - main_window.menu_action_network_settings, - callback, - qtbot, - cls=NetworkSettingsUI, - ) + def menu_action_network_settings() -> None: + def callback(dialog: NetworkSettingsUI) -> None: + shutter.save(dialog) + dialog.close() - menu_action_network_settings() + do_modal_click( + main_window.menu_action_network_settings, + callback, + qtbot, + cls=NetworkSettingsUI, + ) - def menu_action_check_update() -> None: - main_window.menu_action_check_update.trigger() - shutter.save(main_window) - assert main_window.update_notification_bar.isVisible() + menu_action_network_settings() - with qtbot.waitSignal( - main_window.update_notification_bar.signal_on_success, timeout=10000 - ): # Timeout after 10 seconds - main_window.update_notification_bar.check() + def menu_action_check_update() -> None: + main_window.menu_action_check_update.trigger() + shutter.save(main_window) + assert main_window.update_notification_bar.isVisible() - assert main_window.update_notification_bar.assets + with qtbot.waitSignal( + main_window.update_notification_bar.signal_on_success, timeout=10000 + ): # Timeout after 10 seconds + main_window.update_notification_bar.check() - menu_action_check_update() + assert main_window.update_notification_bar.assets - def menu_action_license() -> None: - def callback(dialog: LicenseDialog) -> None: - shutter.save(dialog) - dialog.close() + menu_action_check_update() - do_modal_click( - main_window.menu_action_license, - callback, - qtbot, - cls=LicenseDialog, - ) + def menu_action_license() -> None: + def callback(dialog: LicenseDialog) -> None: + shutter.save(dialog) + dialog.close() + + do_modal_click( + main_window.menu_action_license, + callback, + qtbot, + cls=LicenseDialog, + ) - menu_action_license() + menu_action_license() - def switch_languages() -> None: - for lang in main_window.language_chooser.availableLanguages.keys(): - main_window.language_chooser.switchLanguage(lang) + def switch_languages() -> None: + for lang in main_window.language_chooser.availableLanguages.keys(): + main_window.language_chooser.switchLanguage(lang) + shutter.save(main_window) + main_window.language_chooser.switchLanguage("en_US") shutter.save(main_window) - main_window.language_chooser.switchLanguage("en_US") - shutter.save(main_window) - switch_languages() + switch_languages() + + do_all(qt_wallet) + + with CheckedDeletionContext( + qt_wallet=qt_wallet, qtbot=qtbot, caplog=caplog, graph_directory=shutter.used_directory() + ): + wallet_id = qt_wallet.wallet.id + + close_wallet( + shutter=shutter, + test_config=test_config, + wallet_name=wallet_id, + qtbot=qtbot, + main_window=main_window, + ) + del qt_wallet + shutter.save(main_window) # end shutter.save(main_window) - sleep(2) diff --git a/tests/gui/qt/test_wallet_open.py b/tests/gui/qt/test_wallet_open.py index a40522e..d9808af 100644 --- a/tests/gui/qt/test_wallet_open.py +++ b/tests/gui/qt/test_wallet_open.py @@ -33,7 +33,6 @@ import tempfile from datetime import datetime from pathlib import Path -from time import sleep import pytest from PyQt6.QtTest import QTest @@ -41,13 +40,13 @@ from pytestqt.qtbot import QtBot from bitcoin_safe.config import UserConfig -from bitcoin_safe.logging_setup import setup_logging # type: ignore from tests.gui.qt.test_setup_wallet import close_wallet, get_tab_with_title, save_wallet from ...test_helpers import test_config # type: ignore from ...test_helpers import test_config_main_chain # type: ignore from ...test_setup_bitcoin_core import Faucet, bitcoin_core, faucet # type: ignore from .test_helpers import ( # type: ignore + CheckedDeletionContext, Shutter, close_wallet, do_modal_click, @@ -62,13 +61,14 @@ @pytest.mark.marker_qt_2 -def test_open_wallet_and_address_is_consistent( +def test_open_wallet_and_address_is_consistent_and_destruction_ok( qapp: QApplication, qtbot: QtBot, test_start_time: datetime, test_config: UserConfig, bitcoin_core: Path, faucet: Faucet, + caplog: pytest.LogCaptureFixture, wallet_file: str = "0.2.0.wallet", amount: int = int(1e6), ) -> None: # bitcoin_core: Path, @@ -91,7 +91,7 @@ def test_open_wallet_and_address_is_consistent( qt_wallet = main_window.open_wallet(str(temp_dir)) assert qt_wallet - qt_wallet.tabs.setCurrentWidget(qt_wallet.addresses_tab) + qt_wallet.tabs.setCurrentWidget(qt_wallet.address_tab) shutter.save(main_window) # check wallet address @@ -100,20 +100,21 @@ def test_open_wallet_and_address_is_consistent( == "bcrt1qklm7yyvyu2av4f35ve6tm8mpn6mkr8e3dpjd3jp9vn77vu670g7qu9cznl" ) - def do_close_wallet() -> None: - + # if True: + with CheckedDeletionContext( + qt_wallet=qt_wallet, qtbot=qtbot, caplog=caplog, graph_directory=shutter.used_directory() + ): + wallet_id = qt_wallet.wallet.id + del qt_wallet close_wallet( shutter=shutter, test_config=test_config, - wallet_name=qt_wallet.wallet.id, + wallet_name=wallet_id, qtbot=qtbot, main_window=main_window, ) - shutter.save(main_window) - do_close_wallet() - def check_that_it_is_in_recent_wallets() -> None: assert any( [ @@ -128,4 +129,3 @@ def check_that_it_is_in_recent_wallets() -> None: # end shutter.save(main_window) - sleep(2) diff --git a/tests/gui/qt/test_wallet_send.py b/tests/gui/qt/test_wallet_send.py index 98725ae..757b4ee 100644 --- a/tests/gui/qt/test_wallet_send.py +++ b/tests/gui/qt/test_wallet_send.py @@ -33,7 +33,6 @@ import tempfile from datetime import datetime from pathlib import Path -from time import sleep import pytest from PyQt6.QtTest import QTest @@ -45,12 +44,12 @@ from bitcoin_safe.gui.qt.qt_wallet import QTWallet from bitcoin_safe.gui.qt.tx_signing_steps import HorizontalImporters from bitcoin_safe.gui.qt.ui_tx import UITx_Viewer -from bitcoin_safe.logging_setup import setup_logging # type: ignore from tests.gui.qt.test_setup_wallet import close_wallet, get_tab_with_title, save_wallet from ...test_helpers import test_config # type: ignore from ...test_setup_bitcoin_core import Faucet, bitcoin_core, faucet # type: ignore from .test_helpers import ( # type: ignore + CheckedDeletionContext, Shutter, close_wallet, do_modal_click, @@ -72,6 +71,7 @@ def test_wallet_send( test_config: UserConfig, bitcoin_core: Path, faucet: Faucet, + caplog: pytest.LogCaptureFixture, wallet_file: str = "send_test.wallet", amount: int = int(1e6), ) -> None: # bitcoin_core: Path, @@ -94,166 +94,173 @@ def test_wallet_send( qt_wallet = main_window.open_wallet(str(temp_dir)) assert qt_wallet - qt_wallet.tabs.setCurrentWidget(qt_wallet.addresses_tab) + def do_all(qt_wallet: QTWallet): + "any implicit reference to qt_wallet (including the function page_send) will create a cell refrence" - shutter.save(main_window) - # check wallet address - assert qt_wallet.wallet.get_addresses()[0] == "bcrt1q3y9dezdy48czsck42q5udzmlcyjlppel5eg92k" - - def fund_wallet() -> None: - # to be able to import a recipient list with amounts - # i need to fund the wallet first - faucet.send(qt_wallet.wallet.get_address().address.as_string(), amount=10000000) - counter = 0 - while qt_wallet.wallet.get_balance().total == 0: - with qtbot.waitSignal(qt_wallet.signal_after_sync, timeout=10000): - qt_wallet.sync() + qt_wallet.tabs.setCurrentWidget(qt_wallet.address_tab) + shutter.save(main_window) + # check wallet address + assert qt_wallet.wallet.get_addresses()[0] == "bcrt1q3y9dezdy48czsck42q5udzmlcyjlppel5eg92k" + + def fund_wallet() -> None: + # to be able to import a recipient list with amounts + # i need to fund the wallet first + faucet.send(qt_wallet.wallet.get_address().address.as_string(), amount=10000000) + counter = 0 + while qt_wallet.wallet.get_balance().total == 0: + with qtbot.waitSignal(qt_wallet.signal_after_sync, timeout=10000): + qt_wallet.sync() + + shutter.save(main_window) + counter += 1 + if counter > 20: + raise Exception( + f"After {counter} syncing, the wallet balance is still {qt_wallet.wallet.get_balance().total}" + ) + + fund_wallet() + + def import_recipients() -> None: + qt_wallet.tabs.setCurrentWidget(qt_wallet.send_tab) + shutter.save(main_window) + qt_wallet.uitx_creator.recipients.add_recipient_button.click() shutter.save(main_window) - counter += 1 - if counter > 20: - raise Exception( - f"After {counter} syncing, the wallet balance is still {qt_wallet.wallet.get_balance().total}" - ) - - fund_wallet() - def import_recipients() -> None: - qt_wallet.tabs.setCurrentWidget(qt_wallet.send_tab) - shutter.save(main_window) - qt_wallet.uitx_creator.recipients.add_recipient_button.click() - shutter.save(main_window) + test_file_path = "tests/data/recipients.csv" + with open(str(test_file_path), "r") as file: + test_file_content = file.read() - test_file_path = "tests/data/recipients.csv" - with open(str(test_file_path), "r") as file: - test_file_content = file.read() + qt_wallet.uitx_creator.recipients.import_csv(test_file_path) + shutter.save(main_window) - qt_wallet.uitx_creator.recipients.import_csv(test_file_path) - shutter.save(main_window) + assert len(qt_wallet.uitx_creator.recipients.recipients) == 2 + r = qt_wallet.uitx_creator.recipients.recipients[0] + assert r.address == "bcrt1q8tzpytutwlxpqjyhku3c4pyzz62sx5dv9ly67cx4qvran7stwlgqvmvhrw" + assert r.amount == 1000 + assert r.label == "1" - assert len(qt_wallet.uitx_creator.recipients.recipients) == 2 - r = qt_wallet.uitx_creator.recipients.recipients[0] - assert r.address == "bcrt1q8tzpytutwlxpqjyhku3c4pyzz62sx5dv9ly67cx4qvran7stwlgqvmvhrw" - assert r.amount == 1000 - assert r.label == "1" + r = qt_wallet.uitx_creator.recipients.recipients[1] + assert r.address == "bcrt1q6dqexpz2rp3r08nm6w8l5h3tgvqgn3c96jl6jt9vv3heylvmr8lskchhzn" + assert r.amount == 2000 + assert r.label == "2" - r = qt_wallet.uitx_creator.recipients.recipients[1] - assert r.address == "bcrt1q6dqexpz2rp3r08nm6w8l5h3tgvqgn3c96jl6jt9vv3heylvmr8lskchhzn" - assert r.amount == 2000 - assert r.label == "2" + shutter.save(main_window) - shutter.save(main_window) + with tempfile.TemporaryDirectory() as tempdir: + file_path = Path(tempdir) / "test.csv" + qt_wallet.uitx_creator.recipients.export_csv( + qt_wallet.uitx_creator.recipients.recipients, file_path=file_path + ) - with tempfile.TemporaryDirectory() as tempdir: - file_path = Path(tempdir) / "test.csv" - qt_wallet.uitx_creator.recipients.export_csv( - qt_wallet.uitx_creator.recipients.recipients, file_path=file_path - ) + assert file_path.exists() - assert file_path.exists() + with open(str(file_path), "r") as file: + output_file_content = file.read() - with open(str(file_path), "r") as file: - output_file_content = file.read() + assert test_file_content == output_file_content - assert test_file_content == output_file_content + import_recipients() - import_recipients() + def create_signed_tx() -> None: + with qtbot.waitSignal(main_window.signals.open_tx_like, timeout=10000): + qt_wallet.uitx_creator.button_ok.click() + shutter.save(main_window) - def create_signed_tx() -> None: - with qtbot.waitSignal(main_window.signals.open_tx_like, timeout=10000): - qt_wallet.uitx_creator.button_ok.click() - shutter.save(main_window) + ui_tx_viewer = main_window.tab_wallets.getCurrentTabData() + assert isinstance(ui_tx_viewer, UITx_Viewer) + assert len(ui_tx_viewer.recipients.recipients) == 3 - ui_tx_viewer = main_window.tab_wallets.getCurrentTabData() - assert isinstance(ui_tx_viewer, UITx_Viewer) - assert len(ui_tx_viewer.recipients.recipients) == 3 + sorted_recipients = sorted( + ui_tx_viewer.recipients.recipients, key=lambda recipient: recipient.address + ) - sorted_recipients = sorted( - ui_tx_viewer.recipients.recipients, key=lambda recipient: recipient.address - ) + r = sorted_recipients[1] + assert r.address == "bcrt1q8tzpytutwlxpqjyhku3c4pyzz62sx5dv9ly67cx4qvran7stwlgqvmvhrw" + assert r.amount == 1000 + assert r.label == "1" - r = sorted_recipients[1] - assert r.address == "bcrt1q8tzpytutwlxpqjyhku3c4pyzz62sx5dv9ly67cx4qvran7stwlgqvmvhrw" - assert r.amount == 1000 - assert r.label == "1" + r = sorted_recipients[0] + assert r.address == "bcrt1q6dqexpz2rp3r08nm6w8l5h3tgvqgn3c96jl6jt9vv3heylvmr8lskchhzn" + assert r.amount == 2000 + assert r.label == "2" - r = sorted_recipients[0] - assert r.address == "bcrt1q6dqexpz2rp3r08nm6w8l5h3tgvqgn3c96jl6jt9vv3heylvmr8lskchhzn" - assert r.amount == 2000 - assert r.label == "2" + r = sorted_recipients[2] + assert r.address == "bcrt1qdcn67p707adhet4a9lh6pt8m5h4yjjf2nayqlq" + assert r.address == qt_wallet.wallet.get_change_addresses()[0] + assert r.amount == 9996804 + assert r.label == "Change of: 1, 2" - r = sorted_recipients[2] - assert r.address == "bcrt1qdcn67p707adhet4a9lh6pt8m5h4yjjf2nayqlq" - assert r.address == qt_wallet.wallet.get_change_addresses()[0] - assert r.amount == 9996804 - assert r.label == "Change of: 1, 2" + ui_tx_viewer.button_next.click() - ui_tx_viewer.button_next.click() + horizontal_importer = ui_tx_viewer.tx_singning_steps.stacked_widget.widget(1) + assert isinstance(horizontal_importer, HorizontalImporters) + signer_ui = horizontal_importer.group_seed.data + assert isinstance(signer_ui, SignerUI) + assert signer_ui.buttons[0].text() == "Sign with mnemonic seed" - horizontal_importer = ui_tx_viewer.tx_singning_steps.stacked_widget.widget(1) - assert isinstance(horizontal_importer, HorizontalImporters) - signer_ui = horizontal_importer.group_seed.data - assert isinstance(signer_ui, SignerUI) - assert signer_ui.buttons[0].text() == "Sign with mnemonic seed" + with qtbot.waitSignal(signer_ui.signal_signature_added, timeout=10000): + signer_ui.buttons[0].click() - with qtbot.waitSignal(signer_ui.signal_signature_added, timeout=10000): - signer_ui.buttons[0].click() + shutter.save(main_window) - shutter.save(main_window) + create_signed_tx() - create_signed_tx() + def send_tx() -> None: + shutter.save(main_window) - def send_tx() -> None: - shutter.save(main_window) + ui_tx_viewer = main_window.tab_wallets.getCurrentTabData() + assert isinstance(ui_tx_viewer, UITx_Viewer) + assert len(ui_tx_viewer.recipients.recipients) == 3 - ui_tx_viewer = main_window.tab_wallets.getCurrentTabData() - assert isinstance(ui_tx_viewer, UITx_Viewer) - assert len(ui_tx_viewer.recipients.recipients) == 3 + sorted_recipients = sorted( + ui_tx_viewer.recipients.recipients, key=lambda recipient: recipient.address + ) - sorted_recipients = sorted( - ui_tx_viewer.recipients.recipients, key=lambda recipient: recipient.address - ) + r = sorted_recipients[1] + assert r.address == "bcrt1q8tzpytutwlxpqjyhku3c4pyzz62sx5dv9ly67cx4qvran7stwlgqvmvhrw" + assert r.amount == 1000 + assert r.label == "1" - r = sorted_recipients[1] - assert r.address == "bcrt1q8tzpytutwlxpqjyhku3c4pyzz62sx5dv9ly67cx4qvran7stwlgqvmvhrw" - assert r.amount == 1000 - assert r.label == "1" + r = sorted_recipients[0] + assert r.address == "bcrt1q6dqexpz2rp3r08nm6w8l5h3tgvqgn3c96jl6jt9vv3heylvmr8lskchhzn" + assert r.amount == 2000 + assert r.label == "2" - r = sorted_recipients[0] - assert r.address == "bcrt1q6dqexpz2rp3r08nm6w8l5h3tgvqgn3c96jl6jt9vv3heylvmr8lskchhzn" - assert r.amount == 2000 - assert r.label == "2" + r = sorted_recipients[2] + assert r.address == "bcrt1qdcn67p707adhet4a9lh6pt8m5h4yjjf2nayqlq" + assert r.address == qt_wallet.wallet.get_change_addresses()[0] + assert r.amount == 9996804 + assert r.label == "Change of: 1, 2" - r = sorted_recipients[2] - assert r.address == "bcrt1qdcn67p707adhet4a9lh6pt8m5h4yjjf2nayqlq" - assert r.address == qt_wallet.wallet.get_change_addresses()[0] - assert r.amount == 9996804 - assert r.label == "Change of: 1, 2" + with qtbot.waitSignal(qt_wallet.signal_after_sync, timeout=10000): + ui_tx_viewer.button_send.click() - with qtbot.waitSignal(qt_wallet.signal_after_sync, timeout=10000): - ui_tx_viewer.button_send.click() + shutter.save(main_window) + qt_wallet_tab = main_window.tab_wallets.getCurrentTabData() + assert isinstance(qt_wallet_tab, QTWallet) + assert qt_wallet_tab.history_list._source_model.rowCount() == 1 - shutter.save(main_window) - qt_wallet_tab = main_window.tab_wallets.getCurrentTabData() - assert isinstance(qt_wallet_tab, QTWallet) - assert qt_wallet_tab.history_list._source_model.rowCount() == 1 + send_tx() - send_tx() + do_all(qt_wallet) - def do_close_wallet() -> None: + with CheckedDeletionContext( + qt_wallet=qt_wallet, qtbot=qtbot, caplog=caplog, graph_directory=shutter.used_directory() + ): + wallet_id = qt_wallet.wallet.id + del qt_wallet close_wallet( shutter=shutter, test_config=test_config, - wallet_name=qt_wallet.wallet.id, + wallet_name=wallet_id, qtbot=qtbot, main_window=main_window, ) shutter.save(main_window) - do_close_wallet() - def check_that_it_is_in_recent_wallets() -> None: assert any( [ @@ -268,4 +275,3 @@ def check_that_it_is_in_recent_wallets() -> None: # end shutter.save(main_window) - sleep(2) diff --git a/tests/non_gui/test_mail_handler.py b/tests/non_gui/test_mail_handler.py index 7f1cbe2..d252a73 100644 --- a/tests/non_gui/test_mail_handler.py +++ b/tests/non_gui/test_mail_handler.py @@ -35,6 +35,7 @@ from bitcoin_safe.logging_setup import setup_logging # type: ignore +setup_logging() logger = logging.getLogger(__name__) diff --git a/tests/non_gui/test_wallet_coin_select.py b/tests/non_gui/test_wallet_coin_select.py index 2d12489..02275e1 100644 --- a/tests/non_gui/test_wallet_coin_select.py +++ b/tests/non_gui/test_wallet_coin_select.py @@ -39,7 +39,6 @@ from bitcoin_safe.config import UserConfig from bitcoin_safe.keystore import KeyStore -from bitcoin_safe.logging_setup import setup_logging # type: ignore from bitcoin_safe.pythonbdk_types import Recipient from bitcoin_safe.tx import TxUiInfos, transaction_to_dict from bitcoin_safe.wallet import Wallet diff --git a/tests/test_util.py b/tests/test_util.py index e2cfc44..a4bd88c 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -28,26 +28,25 @@ import logging -from typing import Callable, List - -from bitcoin_safe.gui.qt.util import one_time_signal_connection - -logger = logging.getLogger(__name__) - from pathlib import Path +from typing import Callable, List from unittest.mock import patch import bdkpython as bdk from _pytest.logging import LogCaptureFixture from PyQt6.QtCore import QObject, pyqtBoundSignal, pyqtSignal -# import the __main__ because it setsup the logging -from bitcoin_safe.logging_setup import setup_logging # type: ignore +from bitcoin_safe.gui.qt.util import one_time_signal_connection from bitcoin_safe.signals import TypedPyQtSignalNo # from bitcoin_safe.logging_setup import setup_logging from bitcoin_safe.util import path_to_rel_home_path, rel_home_path_to_abs_path +# import the __main__ because it setsup the logging + + +logger = logging.getLogger(__name__) + class MySignalclass(QObject): signal: TypedPyQtSignalNo = pyqtSignal() # type: ignore diff --git a/tools/build.py b/tools/build.py index a89d9b3..3ab10df 100644 --- a/tools/build.py +++ b/tools/build.py @@ -40,6 +40,7 @@ from translation_handler import TranslationHandler, run_local from bitcoin_safe import __version__ +from bitcoin_safe.execute_config import ENABLE_THREADING, ENABLE_TIMERS, IS_PRODUCTION from bitcoin_safe.signature_manager import ( KnownGPGKeys, SignatureSigner, @@ -51,6 +52,10 @@ logging.basicConfig(level=logging.DEBUG) +assert IS_PRODUCTION +assert ENABLE_THREADING +assert ENABLE_TIMERS + TARGET_LITERAL = Literal["windows", "mac", "appimage", "deb", "flatpak"] diff --git a/tools/release.py b/tools/release.py index 0141a7d..962586f 100644 --- a/tools/release.py +++ b/tools/release.py @@ -35,11 +35,18 @@ import os import subprocess import sys +from functools import partial from pathlib import Path from typing import Any, Dict, List, Optional, Tuple import requests +from bitcoin_safe.execute_config import ENABLE_THREADING, ENABLE_TIMERS, IS_PRODUCTION + +assert IS_PRODUCTION +assert ENABLE_THREADING +assert ENABLE_TIMERS + def get_default_description(latest_tag: str): return f""" @@ -164,7 +171,7 @@ def calculate_sha256(file_path): # Calculate the SHA-256 hash of the file sha256_hash = hashlib.sha256() with open(file_path, "rb") as f: - for byte_block in iter(lambda: f.read(4096), b""): + for byte_block in iter(partial(f.read, 4096), b""): sha256_hash.update(byte_block) return sha256_hash.hexdigest()