From 3c9c6a286c11254cc6a78bc1a19b13683888597c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 12 Jun 2025 13:57:04 +0200 Subject: [PATCH] imports, whitespace, type hints, copyright headers --- electrum/hw_wallet/cmdline.py | 26 +++++++++- electrum/hw_wallet/plugin.py | 5 +- electrum/hw_wallet/qt.py | 45 ++++++++++------- electrum/hw_wallet/trezor_qt_pinmatrix.py | 10 +--- electrum/plugins/labels/__init__.py | 25 ++++++++++ electrum/plugins/labels/cmdline.py | 25 ++++++++++ electrum/plugins/labels/labels.py | 26 +++++++++- electrum/plugins/labels/manifest.json | 2 +- electrum/plugins/labels/qml.py | 24 +++++++++ electrum/plugins/labels/qt.py | 24 +++++++++ electrum/plugins/nwc/__init__.py | 29 ++++++++++- electrum/plugins/nwc/cmdline.py | 25 ++++++++++ electrum/plugins/nwc/nwcserver.py | 30 ++++++++++-- electrum/plugins/nwc/qt.py | 30 ++++++++++-- electrum/plugins/psbt_nostr/manifest.json | 2 +- electrum/plugins/swapserver/cmdline.py | 1 + electrum/plugins/swapserver/server.py | 25 +++++++++- electrum/plugins/swapserver/swapserver.py | 3 -- electrum/util.py | 60 ++++++++++++++++------- 19 files changed, 354 insertions(+), 63 deletions(-) diff --git a/electrum/hw_wallet/cmdline.py b/electrum/hw_wallet/cmdline.py index 6072b0a02..367af7255 100644 --- a/electrum/hw_wallet/cmdline.py +++ b/electrum/hw_wallet/cmdline.py @@ -1,3 +1,27 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2025 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# 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. from electrum.util import print_stderr, raw_input from electrum.logging import get_logger @@ -15,7 +39,7 @@ class CmdLineHandler(HardwareHandlerBase): return getpass.getpass('') def get_pin(self, msg, *, show_strength=True): - t = {'a':'7', 'b':'8', 'c':'9', 'd':'4', 'e':'5', 'f':'6', 'g':'1', 'h':'2', 'i':'3'} + t = {'a': '7', 'b': '8', 'c': '9', 'd': '4', 'e': '5', 'f': '6', 'g': '1', 'h': '2', 'i': '3'} t.update({str(i): str(i) for i in range(1, 10)}) # sneakily also support numpad-conversion print_stderr(msg) print_stderr("a b c\nd e f\ng h i\n-----") diff --git a/electrum/hw_wallet/plugin.py b/electrum/hw_wallet/plugin.py index 8225ce194..c8c9e96b6 100644 --- a/electrum/hw_wallet/plugin.py +++ b/electrum/hw_wallet/plugin.py @@ -1,8 +1,7 @@ -#!/usr/bin/env python2 -# -*- mode: python -*- +#!/usr/bin/env python # # Electrum - lightweight Bitcoin client -# Copyright (C) 2016 The Electrum developers +# Copyright (C) 2025 The Electrum Developers # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files diff --git a/electrum/hw_wallet/qt.py b/electrum/hw_wallet/qt.py index d920fb985..5cef3e175 100644 --- a/electrum/hw_wallet/qt.py +++ b/electrum/hw_wallet/qt.py @@ -26,24 +26,24 @@ import threading from functools import partial -from typing import TYPE_CHECKING, Union, Optional, Sequence, Tuple +from typing import TYPE_CHECKING, Union, Optional, Sequence from PyQt6.QtCore import QObject, pyqtSignal, Qt from PyQt6.QtWidgets import QVBoxLayout, QLineEdit, QHBoxLayout, QLabel, QMenu -from electrum.gui.common_qt.util import TaskThread -from electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE -from electrum.gui.qt.util import (read_QIcon, WWLabel, OkButton, WindowModalDialog, - Buttons, CancelButton, char_width_in_lineedit, - PasswordLineEdit) -from electrum.gui.qt.main_window import StatusBarButton -from electrum.gui.qt.util import read_QIcon_from_bytes - from electrum.i18n import _ from electrum.logging import Logger from electrum.util import UserCancelled, UserFacingException, ChoiceItem -from electrum.plugin import hook, DeviceUnpairableError -from electrum.wallet import Standard_Wallet +from electrum.plugin import hook + +from electrum.gui.common_qt.util import TaskThread +from electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE +from electrum.gui.qt.util import ( + read_QIcon, WWLabel, OkButton, WindowModalDialog, Buttons, CancelButton, char_width_in_lineedit, PasswordLineEdit, + read_QIcon_from_bytes +) +from electrum.gui.qt.main_window import StatusBarButton + from .plugin import OutdatedHwFirmwareException, HW_PluginBase, HardwareHandlerBase @@ -57,8 +57,8 @@ if TYPE_CHECKING: # The trickiest thing about this handler was getting windows properly # parented on macOS. class QtHandlerBase(HardwareHandlerBase, QObject, Logger): - '''An interface between the GUI (here, QT) and the device handling - logic for handling I/O.''' + """An interface between the GUI (here, QT) and the device handling + logic for handling I/O.""" passphrase_signal = pyqtSignal(object, object) message_signal = pyqtSignal(object, object) @@ -257,10 +257,12 @@ class QtPluginBase(object): self.set_ignore_outdated_fw() # will need to re-pair devmgr = self.device_manager() + def re_pair_device(): device_id = self.choose_device(window, keystore) devmgr.unpair_id(device_id) self.get_client(keystore) + keystore.thread.add(re_pair_device) return else: @@ -268,8 +270,8 @@ class QtPluginBase(object): def choose_device(self: Union['QtPluginBase', HW_PluginBase], window: 'ElectrumWindow', keystore: 'Hardware_KeyStore') -> Optional[str]: - '''This dialog box should be usable even if the user has - forgotten their PIN or it is in bootloader mode.''' + """This dialog box should be usable even if the user has + forgotten their PIN or it is in bootloader mode.""" assert window.gui_thread != threading.current_thread(), 'must not be called from GUI thread' device_id = self.device_manager().id_by_pairing_code(keystore.pairing_code()) if not device_id: @@ -284,17 +286,22 @@ class QtPluginBase(object): # default implementation (if no dialog): just try to connect to device def connect(): device_id = self.choose_device(window, keystore) + keystore.thread.add(connect) - def add_show_address_on_hw_device_button_for_receive_addr(self, wallet: 'Abstract_Wallet', - keystore: 'Hardware_KeyStore', - main_window: 'ElectrumWindow'): + def add_show_address_on_hw_device_button_for_receive_addr( + self, + wallet: 'Abstract_Wallet', + keystore: 'Hardware_KeyStore', + main_window: 'ElectrumWindow' + ): plugin = keystore.plugin receive_tab = main_window.receive_tab def show_address(): addr = str(receive_tab.addr) keystore.thread.add(partial(plugin.show_address, wallet, addr, keystore)) + dev_name = f"{plugin.device} ({keystore.label})" receive_tab.toolbar_menu.addAction(read_QIcon("eye1.png"), _("Show address on {}").format(dev_name), show_address) @@ -306,7 +313,9 @@ class QtPluginBase(object): return for keystore in wallet.get_keystores(): if type(keystore) == self.keystore_class: + def show_address(keystore=keystore): keystore.thread.add(partial(self.show_address, wallet, address, keystore=keystore)) + device_name = "{} ({})".format(self.device, keystore.label) menu.addAction(read_QIcon("eye1.png"), _("Show address on {}").format(device_name), show_address) diff --git a/electrum/hw_wallet/trezor_qt_pinmatrix.py b/electrum/hw_wallet/trezor_qt_pinmatrix.py index 3458c38f3..5eb9126dc 100644 --- a/electrum/hw_wallet/trezor_qt_pinmatrix.py +++ b/electrum/hw_wallet/trezor_qt_pinmatrix.py @@ -22,18 +22,10 @@ from typing import Any from PyQt6.QtCore import QRegularExpression, Qt from PyQt6.QtGui import QRegularExpressionValidator from PyQt6.QtWidgets import ( - QGridLayout, - QHBoxLayout, - QLabel, - QLineEdit, - QPushButton, - QSizePolicy, - QVBoxLayout, - QWidget, + QGridLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy, QVBoxLayout, QWidget ) - class PinButton(QPushButton): def __init__(self, password: QLineEdit, encoded_value: int) -> None: super(PinButton, self).__init__("?") diff --git a/electrum/plugins/labels/__init__.py b/electrum/plugins/labels/__init__.py index b68127df8..436faedd1 100644 --- a/electrum/plugins/labels/__init__.py +++ b/electrum/plugins/labels/__init__.py @@ -1,3 +1,27 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2025 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# 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. from electrum.commands import plugin_command from typing import TYPE_CHECKING @@ -7,6 +31,7 @@ if TYPE_CHECKING: plugin_name = "labels" + @plugin_command('w', plugin_name) async def push(self: 'Commands', plugin: 'LabelsPlugin' = None, wallet=None) -> int: """ push labels to server """ diff --git a/electrum/plugins/labels/cmdline.py b/electrum/plugins/labels/cmdline.py index 50a1ab46b..2aeb3d507 100644 --- a/electrum/plugins/labels/cmdline.py +++ b/electrum/plugins/labels/cmdline.py @@ -1,6 +1,31 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2025 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# 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. from .labels import LabelsPlugin from electrum.plugin import hook + class Plugin(LabelsPlugin): @hook diff --git a/electrum/plugins/labels/labels.py b/electrum/plugins/labels/labels.py index 2d0ba2686..0b7dd75f4 100644 --- a/electrum/plugins/labels/labels.py +++ b/electrum/plugins/labels/labels.py @@ -1,8 +1,30 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2025 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# 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 asyncio import hashlib import json -import sys -import traceback from typing import Union, TYPE_CHECKING import base64 diff --git a/electrum/plugins/labels/manifest.json b/electrum/plugins/labels/manifest.json index 8883fd17b..0be106c0e 100644 --- a/electrum/plugins/labels/manifest.json +++ b/electrum/plugins/labels/manifest.json @@ -3,6 +3,6 @@ "fullname": "LabelSync", "author": "The Electrum Developers", "description": "Save your wallet labels on a remote server, and synchronize them across multiple devices where you use Electrum. Labels, transactions IDs and addresses are encrypted before they are sent to the remote server.", - "icon":"labelsync.png", + "icon": "labelsync.png", "available_for": ["qt", "qml", "cmdline"] } diff --git a/electrum/plugins/labels/qml.py b/electrum/plugins/labels/qml.py index fd20c5b9e..9e43370f4 100644 --- a/electrum/plugins/labels/qml.py +++ b/electrum/plugins/labels/qml.py @@ -1,3 +1,27 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2025 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# 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 threading from PyQt6.QtCore import pyqtSignal, pyqtSlot diff --git a/electrum/plugins/labels/qt.py b/electrum/plugins/labels/qt.py index 2eec4b874..c0d6b20df 100644 --- a/electrum/plugins/labels/qt.py +++ b/electrum/plugins/labels/qt.py @@ -1,3 +1,27 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2025 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# 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. from functools import partial from typing import TYPE_CHECKING diff --git a/electrum/plugins/nwc/__init__.py b/electrum/plugins/nwc/__init__.py index e146114b1..19ce2763e 100644 --- a/electrum/plugins/nwc/__init__.py +++ b/electrum/plugins/nwc/__init__.py @@ -1,3 +1,27 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2025 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# 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. from typing import TYPE_CHECKING from electrum.commands import plugin_command @@ -25,7 +49,8 @@ async def add_connection( name: str, daily_limit_sat=None, valid_for_sec=None, - plugin: 'NWCServerPlugin' = None) -> str: + plugin: 'NWCServerPlugin' = None +) -> str: """ Create a new NWC connection string. @@ -36,6 +61,7 @@ async def add_connection( connection_string: str = plugin.create_connection(name, daily_limit_sat, valid_for_sec) return connection_string + @plugin_command('', plugin_name) async def remove_connection(self: 'Commands', name: str, plugin=None) -> str: """ @@ -45,6 +71,7 @@ async def remove_connection(self: 'Commands', name: str, plugin=None) -> str: plugin.remove_connection(name) return f"removed connection {name}" + @plugin_command('', plugin_name) async def list_connections(self: 'Commands', plugin=None) -> dict: """ diff --git a/electrum/plugins/nwc/cmdline.py b/electrum/plugins/nwc/cmdline.py index f268361c8..a3c6c23a8 100644 --- a/electrum/plugins/nwc/cmdline.py +++ b/electrum/plugins/nwc/cmdline.py @@ -1,3 +1,27 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2025 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# 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. from typing import TYPE_CHECKING from electrum.plugin import hook @@ -8,6 +32,7 @@ if TYPE_CHECKING: from electrum.daemon import Daemon from electrum.wallet import Abstract_Wallet + class Plugin(NWCServerPlugin): def __init__(self, *args): diff --git a/electrum/plugins/nwc/nwcserver.py b/electrum/plugins/nwc/nwcserver.py index 13257e36c..60896b496 100644 --- a/electrum/plugins/nwc/nwcserver.py +++ b/electrum/plugins/nwc/nwcserver.py @@ -1,3 +1,27 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2025 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# 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 asyncio import json import time @@ -32,8 +56,8 @@ class NWCServerPlugin(BasePlugin): def __init__(self, parent, config: 'SimpleConfig', name): BasePlugin.__init__(self, parent, config, name) self.config = config - self.connections = None # type: Optional[dict[str, dict]] # pubkey_hex -> connection data - self.nwc_server = None # type: Optional[NWCServer] + self.connections = None # type: Optional[dict[str, dict]] # pubkey_hex -> connection data + self.nwc_server = None # type: Optional[NWCServer] self.taskgroup = OldTaskGroup() self.initialized = False if not self.config.NWC_RELAY: # type: ignore # defined in __init__ @@ -156,7 +180,7 @@ class NWCServer(Logger, EventListener): NOTIFICATION_EVENT_KIND: int = 23196 SUPPORTED_SPENDING_METHODS: set[str] = {'pay_invoice', 'multi_pay_invoice'} SUPPORTED_METHODS: set[str] = {'make_invoice', 'lookup_invoice', 'get_balance', 'get_info', - 'list_transactions', 'notifications'}.union(SUPPORTED_SPENDING_METHODS) + 'list_transactions', 'notifications'}.union(SUPPORTED_SPENDING_METHODS) SUPPORTED_NOTIFICATIONS: list[str] = ["payment_sent", "payment_received"] def __init__( diff --git a/electrum/plugins/nwc/qt.py b/electrum/plugins/nwc/qt.py index 1891a0393..f58f46584 100644 --- a/electrum/plugins/nwc/qt.py +++ b/electrum/plugins/nwc/qt.py @@ -1,3 +1,28 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2025 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# 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. +from typing import TYPE_CHECKING, Optional from functools import partial from datetime import datetime @@ -8,17 +33,16 @@ from PyQt6.QtWidgets import ( from PyQt6.QtGui import QPixmap, QImage from PyQt6.QtCore import Qt +from electrum.i18n import _ +from electrum.plugin import hook from electrum.gui.qt.util import ( WindowModalDialog, Buttons, OkButton, CancelButton, CloseButton, read_QIcon_from_bytes, read_QPixmap_from_bytes, ) from electrum.gui.common_qt.util import paintQR -from electrum.i18n import _ -from electrum.plugin import hook from .nwcserver import NWCServerPlugin -from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from electrum.wallet import Abstract_Wallet from electrum.gui.qt.main_window import ElectrumWindow diff --git a/electrum/plugins/psbt_nostr/manifest.json b/electrum/plugins/psbt_nostr/manifest.json index 0319b9124..ff1e39d9b 100644 --- a/electrum/plugins/psbt_nostr/manifest.json +++ b/electrum/plugins/psbt_nostr/manifest.json @@ -3,7 +3,7 @@ "fullname": "Nostr Cosigner", "description": "This plugin facilitates the use of multi-signatures wallets. It sends and receives partially signed transactions from/to your cosigner wallet. PSBTs are sent and retrieved from Nostr relays.", "author": "The Electrum Developers", - "icon":"nostr_multisig.png", + "icon": "nostr_multisig.png", "available_for": ["qt", "qml"], "version": "0.0.1" } diff --git a/electrum/plugins/swapserver/cmdline.py b/electrum/plugins/swapserver/cmdline.py index 7add7a386..d9cae780c 100644 --- a/electrum/plugins/swapserver/cmdline.py +++ b/electrum/plugins/swapserver/cmdline.py @@ -26,6 +26,7 @@ from .swapserver import SwapServerPlugin + class Plugin(SwapServerPlugin): pass diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py index 14dc7533e..b2163736e 100644 --- a/electrum/plugins/swapserver/server.py +++ b/electrum/plugins/swapserver/server.py @@ -1,3 +1,27 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2025 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# 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 os import asyncio from collections import defaultdict @@ -8,7 +32,6 @@ from aiohttp import web from electrum.util import log_exceptions, ignore_exceptions from electrum.logging import Logger from electrum.util import EventListener -from electrum.lnaddr import lndecode if TYPE_CHECKING: from electrum.simple_config import SimpleConfig diff --git a/electrum/plugins/swapserver/swapserver.py b/electrum/plugins/swapserver/swapserver.py index c24c4e13e..e733f4bc6 100644 --- a/electrum/plugins/swapserver/swapserver.py +++ b/electrum/plugins/swapserver/swapserver.py @@ -22,9 +22,6 @@ # 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 asyncio from typing import TYPE_CHECKING from electrum.plugin import BasePlugin, hook diff --git a/electrum/util.py b/electrum/util.py index 4a8fc32bb..a1d7aee3e 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -20,24 +20,25 @@ # 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 binascii import concurrent.futures from dataclasses import dataclass import logging -import os, sys, re +import os +import sys +import re from collections import defaultdict, OrderedDict from concurrent.futures.process import ProcessPoolExecutor -from typing import (NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any, - Sequence, Dict, Generic, TypeVar, List, Iterable, Set, Awaitable) +from typing import ( + NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any, Sequence, Dict, Generic, TypeVar, List, Iterable, + Set, Awaitable +) from datetime import datetime, timezone, timedelta import decimal from decimal import Decimal -from urllib.parse import urlparse import threading import hmac import hashlib import stat -import locale import asyncio import builtins import json @@ -50,12 +51,10 @@ import secrets import functools from functools import partial from abc import abstractmethod, ABC -import socket import enum from contextlib import nullcontext import traceback -import attr import aiohttp from aiohttp_socks import ProxyConnector, ProxyType import aiorpcx @@ -69,7 +68,6 @@ if TYPE_CHECKING: from .network import Network, ProxySettings from .interface import Interface from .simple_config import SimpleConfig - from .paymentrequest import PaymentRequest _logger = get_logger(__name__) @@ -441,12 +439,14 @@ def print_stderr(*args): sys.stderr.write(" ".join(args) + "\n") sys.stderr.flush() + def print_msg(*args): # Stringify args args = [str(item) for item in args] sys.stdout.write(" ".join(args) + "\n") sys.stdout.flush() + def json_encode(obj): try: s = json.dumps(obj, sort_keys = True, indent = 4, cls=MyEncoder) @@ -454,12 +454,14 @@ def json_encode(obj): s = repr(obj) return s + def json_decode(x): try: return json.loads(x, parse_float=Decimal) except Exception: return x + def json_normalize(x): # note: The return value of commands, when going through the JSON-RPC interface, # is json-encoded. The encoder used there cannot handle some types, e.g. electrum.util.Satoshis. @@ -475,6 +477,8 @@ def constant_time_compare(val1, val2): _profiler_logger = _logger.getChild('profiler') + + def profiler(func=None, *, min_threshold: Union[int, float, None] = None): """Function decorator that logs execution time. @@ -483,13 +487,16 @@ def profiler(func=None, *, min_threshold: Union[int, float, None] = None): if func is None: # to make "@profiler(...)" work. (in addition to bare "@profiler") return partial(profiler, min_threshold=min_threshold) t0 = None # type: Optional[float] + def timer_start(): nonlocal t0 t0 = time.time() + def timer_done(): t = time.time() - t0 if min_threshold is None or t > min_threshold: _profiler_logger.debug(f"{func.__qualname__} {t:,.4f} sec") + if asyncio.iscoroutinefunction(func): async def do_profile(*args, **kw_args): timer_start() @@ -538,6 +545,7 @@ def android_ext_dir(): from android.storage import primary_external_storage_path return primary_external_storage_path() + def android_backup_dir(): pkgname = get_android_package_name() d = os.path.join(android_ext_dir(), pkgname) @@ -545,11 +553,13 @@ def android_backup_dir(): os.mkdir(d) return d + def android_data_dir(): import jnius PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity') return PythonActivity.mActivity.getFilesDir().getPath() + '/data' + def ensure_sparse_file(filename): # On modern Linux, no need to do anything. # On Windows, need to explicitly mark file. @@ -708,6 +718,7 @@ def is_valid_email(s): regexp = r"[^@]+@[^@]+\.[^@]+" return re.match(regexp, s) is not None + def is_valid_websocket_url(url: str) -> bool: """ uses this django url validation regex: @@ -730,6 +741,7 @@ def is_valid_websocket_url(url: str) -> bool: except Exception: return False + def is_hash256_str(text: Any) -> bool: if not isinstance(text, str): return False if len(text) != 64: return False @@ -941,6 +953,7 @@ def delta_time_str(distance_in_time: timedelta, *, include_seconds: bool = False else: return _("over {} years").format(round(distance_in_minutes / 525600)) + mainnet_block_explorers = { '3xpl.com': ('https://3xpl.com/bitcoin/', {'tx': 'transaction/', 'addr': 'address/'}), @@ -1072,9 +1085,6 @@ def block_explorer_URL(config: 'SimpleConfig', kind: str, item: str) -> Optional return ''.join(url_parts) - - - # Python bug (http://bugs.python.org/issue1927) causes raw_input # to be redirected improperly between stdin/stderr on Unix systems #TODO: py3 @@ -1083,6 +1093,7 @@ def raw_input(prompt=None): sys.stdout.write(prompt) return builtin_raw_input() + builtin_raw_input = builtins.input builtins.input = raw_input @@ -1090,7 +1101,7 @@ builtins.input = raw_input def parse_json(message): # TODO: check \r\n pattern n = message.find(b'\n') - if n==-1: + if n == -1: return None, message try: j = json.loads(message[0:n].decode('utf8')) @@ -1199,6 +1210,7 @@ def is_subpath(long_path: str, short_path: str) -> bool: def log_exceptions(func): """Decorator to log AND re-raise exceptions.""" assert asyncio.iscoroutinefunction(func), 'func needs to be a coroutine' + @functools.wraps(func) async def wrapper(*args, **kwargs): self = args[0] if len(args) > 0 else None @@ -1219,6 +1231,7 @@ def log_exceptions(func): def ignore_exceptions(func): """Decorator to silently swallow all exceptions.""" assert asyncio.iscoroutinefunction(func), 'func needs to be a coroutine' + @functools.wraps(func) async def wrapper(*args, **kwargs): try: @@ -1391,6 +1404,7 @@ class OldTaskGroup(aiorpcx.TaskGroup): if self.completed: self.completed.result() + # We monkey-patch aiorpcx TimeoutAfter (used by timeout_after and ignore_after API), # to fix a timing issue present in asyncio as a whole re timing out tasks. # To see the issue we are trying to fix, consider example: @@ -1412,10 +1426,12 @@ def _aiorpcx_monkeypatched_set_new_deadline(task, deadline): def timeout_task(): task._orig_cancel() task._timed_out = None if getattr(task, "_externally_cancelled", False) else deadline + def mycancel(*args, **kwargs): task._orig_cancel(*args, **kwargs) task._externally_cancelled = True task._timed_out = None + if not hasattr(task, "_orig_cancel"): task._orig_cancel = task.cancel task.cancel = mycancel @@ -1505,6 +1521,7 @@ class NetworkJobOnDefaultServer(Logger, ABC): self.interface = interface taskgroup = self.taskgroup + async def run_tasks_wrapper(): self.logger.debug(f"starting taskgroup ({hex(id(taskgroup))}).") try: @@ -1566,6 +1583,7 @@ async def detect_tor_socks_proxy() -> Optional[Tuple[str, int]]: ] proxy_addr = None + async def test_net_addr(net_addr): is_tor = await is_tor_socks_port(*net_addr) # set result, and cancel remaining probes @@ -1607,6 +1625,8 @@ async def is_tor_socks_port(host: str, port: int) -> bool: AS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP = False # used by unit tests _asyncio_event_loop = None # type: Optional[asyncio.AbstractEventLoop] + + def get_asyncio_loop() -> asyncio.AbstractEventLoop: """Returns the global asyncio event loop we use.""" if loop := _asyncio_event_loop: @@ -1682,6 +1702,8 @@ def create_and_start_event_loop() -> Tuple[asyncio.AbstractEventLoop, _running_asyncio_tasks = set() # type: Set[asyncio.Future] + + def _set_custom_task_factory(loop: asyncio.AbstractEventLoop): """Wrap task creation to track pending and running tasks. When tasks are created, asyncio only maintains a weak reference to them. @@ -1739,6 +1761,7 @@ def run_sync_function_on_asyncio_thread(func: Callable, *, block: bool) -> None: # add explicit logging of exceptions, otherwise they might get lost tb1 = traceback.format_stack()[:-1] tb1_str = "".join(tb1) + def on_done(fut_: concurrent.futures.Future): assert fut_.done() if fut_.cancelled(): @@ -1815,8 +1838,8 @@ class OrderedDictWithIndex(OrderedDict): def multisig_type(wallet_type): - '''If wallet_type is mofn multi-sig, return [m, n], - otherwise return None.''' + """If wallet_type is mofn multi-sig, return [m, n], + otherwise return None.""" if not wallet_type: return None match = re.match(r'(\d+)of(\d+)', wallet_type) @@ -1929,6 +1952,7 @@ class CallbackManager(Logger): for callback in callbacks: if asyncio.iscoroutinefunction(callback): # async cb fut = asyncio.run_coroutine_threadsafe(callback(*args), loop) + def on_done(fut_: concurrent.futures.Future): assert fut_.done() if fut_.cancelled(): @@ -1987,6 +2011,7 @@ _NetAddrType = TypeVar("_NetAddrType") # requirements for _NetAddrType: # - reasonable __hash__() implementation (e.g. based on host/port of remote endpoint) + class NetworkRetryManager(Generic[_NetAddrType]): """Truncated Exponential Backoff for network connections.""" @@ -2135,6 +2160,7 @@ class JsonRPCClient: T = TypeVar('T') + def random_shuffled_copy(x: Iterable[T]) -> List[T]: """Returns a shuffled copy of the input.""" x_copy = list(x) # copy @@ -2287,7 +2313,7 @@ class OnchainHistoryItem(NamedTuple): balance_sat: int tx_mined_status: TxMinedInfo group_id: Optional[str] - label: str + label: Optional[str] monotonic_timestamp: int group_id: Optional[str] def to_dict(self): @@ -2318,7 +2344,7 @@ class LightningHistoryItem(NamedTuple): type: str group_id: Optional[str] timestamp: int - label: str + label: Optional[str] direction: Optional[int] def to_dict(self): return {