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 d5d084f..7d9ceb0 100644
Binary files a/bitcoin_safe/gui/icons/distribute-multisigsig-export.svgz and b/bitcoin_safe/gui/icons/distribute-multisigsig-export.svgz differ
diff --git a/bitcoin_safe/gui/locales/app_ar_AE.qm b/bitcoin_safe/gui/locales/app_ar_AE.qm
index 517d37f..bea802e 100644
Binary files a/bitcoin_safe/gui/locales/app_ar_AE.qm and b/bitcoin_safe/gui/locales/app_ar_AE.qm differ
diff --git a/bitcoin_safe/gui/locales/app_ar_AE.ts b/bitcoin_safe/gui/locales/app_ar_AE.ts
index daa2bf3..64e84e0 100644
--- a/bitcoin_safe/gui/locales/app_ar_AE.ts
+++ b/bitcoin_safe/gui/locales/app_ar_AE.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}
- الملف المحدد: {مسار_الملف}
+ 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 75f7b5d..21744fa 100644
Binary files a/bitcoin_safe/gui/locales/app_de_DE.qm and b/bitcoin_safe/gui/locales/app_de_DE.qm differ
diff --git a/bitcoin_safe/gui/locales/app_de_DE.ts b/bitcoin_safe/gui/locales/app_de_DE.ts
index 6b0c105..492636b 100644
--- a/bitcoin_safe/gui/locales/app_de_DE.ts
+++ b/bitcoin_safe/gui/locales/app_de_DE.ts
@@ -430,6 +430,10 @@ shown on your BitBox02.
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 7e8e909..3463589 100644
Binary files a/bitcoin_safe/gui/locales/app_es_ES.qm and b/bitcoin_safe/gui/locales/app_es_ES.qm differ
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 4aaf410..c75168e 100644
Binary files a/bitcoin_safe/gui/locales/app_fr_FR.qm and b/bitcoin_safe/gui/locales/app_fr_FR.qm differ
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 8c26a34..a7dcabd 100644
Binary files a/bitcoin_safe/gui/locales/app_hi_IN.qm and b/bitcoin_safe/gui/locales/app_hi_IN.qm differ
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 7ac65fd..946e831 100644
Binary files a/bitcoin_safe/gui/locales/app_it_IT.qm and b/bitcoin_safe/gui/locales/app_it_IT.qm differ
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 62ae1bc..af078b4 100644
Binary files a/bitcoin_safe/gui/locales/app_ja_JP.qm and b/bitcoin_safe/gui/locales/app_ja_JP.qm differ
diff --git a/bitcoin_safe/gui/locales/app_ja_JP.ts b/bitcoin_safe/gui/locales/app_ja_JP.ts
index 2aa7937..7b5ffe1 100644
--- a/bitcoin_safe/gui/locales/app_ja_JP.ts
+++ b/bitcoin_safe/gui/locales/app_ja_JP.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: .....
選択されたファイル:{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 75f7c22..0f4f5d0 100644
Binary files a/bitcoin_safe/gui/locales/app_pt_PT.qm and b/bitcoin_safe/gui/locales/app_pt_PT.qm differ
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 2a8bfc8..cdb5d3e 100644
Binary files a/bitcoin_safe/gui/locales/app_ru_RU.qm and b/bitcoin_safe/gui/locales/app_ru_RU.qm differ
diff --git a/bitcoin_safe/gui/locales/app_ru_RU.ts b/bitcoin_safe/gui/locales/app_ru_RU.ts
index 4a13b0e..ca1e903 100644
--- a/bitcoin_safe/gui/locales/app_ru_RU.ts
+++ b/bitcoin_safe/gui/locales/app_ru_RU.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.
Кошелек с идентификатором {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 5a5e3ab..f2f2a8e 100644
Binary files a/bitcoin_safe/gui/locales/app_zh_CN.qm and b/bitcoin_safe/gui/locales/app_zh_CN.qm differ
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()