From 7faee700f5701a2c83f639ea5b79fee4dd939d6b Mon Sep 17 00:00:00 2001 From: PiRK Date: Fri, 20 Jan 2023 14:37:15 +0100 Subject: [PATCH 1/3] Remove LabelSync plugin This has never been enabled in Electrum ABC because the server is notoriously unreliable for large wallets. It has been dropped in Electron Cash too. No one has been able to come up with a workable alternative solution. --- electrumabc_plugins/fusion/qt.py | 3 +- electrumabc_plugins/labels/__init__.py | 15 -- electrumabc_plugins/labels/labels.py | 294 ------------------------ electrumabc_plugins/labels/qt.py | 296 ------------------------- setup.py | 1 - 5 files changed, 1 insertion(+), 608 deletions(-) delete mode 100644 electrumabc_plugins/labels/__init__.py delete mode 100644 electrumabc_plugins/labels/labels.py delete mode 100644 electrumabc_plugins/labels/qt.py diff --git a/electrumabc_plugins/fusion/qt.py b/electrumabc_plugins/fusion/qt.py index 26fca2c68013..7595f8718209 100644 --- a/electrumabc_plugins/fusion/qt.py +++ b/electrumabc_plugins/fusion/qt.py @@ -781,8 +781,7 @@ def history_list_filter(self, history_list, h_item, label): def history_list_context_menu_setup(self, history_list, menu, item, tx_hash): # NB: We unconditionally create this menu if the plugin is loaded because # it's possible for any wallet, even a watching-only wallet to have - # fusion tx's with the correct labels (if the user uses labelsync or - # has imported labels). + # fusion tx's with the correct labels (if the user has imported labels). menu.addSeparator() def action_callback(): diff --git a/electrumabc_plugins/labels/__init__.py b/electrumabc_plugins/labels/__init__.py deleted file mode 100644 index 694a3ba42887..000000000000 --- a/electrumabc_plugins/labels/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from electrumabc.constants import PROJECT_NAME -from electrumabc.i18n import _ - -fullname = _("LabelSync") -description = [ - _( - "Save your wallet labels on a remote server, and synchronize " - f"them across multiple devices where you use {PROJECT_NAME}." - ), - _( - "Labels, transactions IDs and addresses are encrypted before they" - " are sent to the remote server." - ), -] -available_for = ["qt"] diff --git a/electrumabc_plugins/labels/labels.py b/electrumabc_plugins/labels/labels.py deleted file mode 100644 index 9dd104ec06d3..000000000000 --- a/electrumabc_plugins/labels/labels.py +++ /dev/null @@ -1,294 +0,0 @@ -import base64 -import hashlib -import json -import sys -import threading - -import requests - -from electrumabc.bitcoin import aes_decrypt_with_iv, aes_encrypt_with_iv -from electrumabc.plugins import BasePlugin, hook - - -class LabelsPlugin(BasePlugin): - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.target_host = "sync.imaginary.cash:8082" - self.wallets = {} - self.threads = [] - self.closing = False - - def encode(self, wallet, msg): - password, iv, wallet_id = self.wallets[wallet] - encrypted = aes_encrypt_with_iv(password, iv, msg.encode("utf8")) - return base64.b64encode(encrypted).decode() - - def decode(self, wallet, message): - password, iv, wallet_id = self.wallets[wallet] - decoded = base64.b64decode(message) - decrypted = aes_decrypt_with_iv(password, iv, decoded) - return decrypted.decode("utf8") - - def get_nonce(self, wallet): - with wallet.lock: - # nonce is the nonce to be used with the next change - nonce = wallet.storage.get("wallet_nonce") - if nonce is None: - nonce = 1 - self.set_nonce(wallet, nonce) - return nonce - - def set_nonce(self, wallet, nonce): - with wallet.lock: - self.print_error("set", wallet.basename(), "nonce to", nonce) - wallet.storage.put("wallet_nonce", nonce) - - @hook - def set_label(self, wallet, item, label): - if wallet not in self.wallets or self.closing: - return - if not item: - return - # need to hold the lock from get nonce to set nonce in order to prevent races. - with wallet.lock: - nonce = self.get_nonce(wallet) - wallet_id = self.wallets[wallet][2] - bundle = { - "walletId": wallet_id, - "walletNonce": nonce, - "externalId": self.encode(wallet, item), - "encryptedLabel": self.encode(wallet, label if label else ""), - } - t = threading.Thread( - target=self.do_request, args=["POST", "/label", False, bundle, True] - ) - t.setDaemon(True) - # Caller will write the wallet - self.set_nonce(wallet, nonce + 1) - t.start() - - def find_wallet_by_id(self, wallet_id): - for wallet, tup in self.wallets.copy().items(): - if wallet_id == tup[2]: - return wallet - return None - - def do_request(self, method, url="/labels", is_batch=False, data=None, noexc=False): - if self.closing: - return - wallet_id = data.get("walletId", None) if data else None - try: - self._curthr_push() - url = "https://" + self.target_host + url - # self.print_error("do_request",method,url,is_batch,data,"...") - kwargs = {"headers": {}} - if method == "GET" and data: - kwargs["params"] = data - elif method == "POST" and data: - kwargs["data"] = json.dumps(data) - kwargs["headers"]["Content-Type"] = "application/json" - - # will raise requests.exceptions.Timeout on timeout - response = requests.request(method, url, **kwargs, timeout=5.0) - - if response.status_code == 400: - if "serverNonce is larger then walletNonce" in response.text: - wallet = self.find_wallet_by_id(wallet_id) - if wallet: - self.on_wallet_not_synched(wallet) - return - if response.status_code != 200: - raise RuntimeError(response.status_code, response.text) - response = response.json() - if "error" in response: - raise RuntimeError(response["error"]) - return response - except Exception as e: - if noexc: - wallet = self.find_wallet_by_id(wallet_id) - if wallet: - self.on_request_exception(wallet, sys.exc_info()) - return - raise e - finally: - self._curthr_pop() - - def _curthr_push(self): # misnomer. it's not a stack. - t = threading.current_thread() - if t is not threading.main_thread(): - self.threads.append(t) - - def _curthr_pop(self): # misnomer. it's not a stack. - try: - self.threads.remove(threading.current_thread()) - except ValueError: - pass # we silently ignore unbalanced _curthr_push/pop for now... - - def push_thread(self, wallet): - if wallet not in self.wallets or self.closing: - return # still has race conditions here - try: - self._curthr_push() - # self.print_error("push_thread", wallet.basename(),"...") - wallet_id = self.wallets[wallet][2] - bundle = { - "labels": [], - "walletId": wallet_id, - "walletNonce": self.get_nonce(wallet), - } - with wallet.lock: - labels = wallet.labels.copy() - for key, value in labels.items(): - try: - encoded_key = self.encode(wallet, key) - encoded_value = self.encode(wallet, value) - except Exception: - self.print_error("cannot encode", repr(key), repr(value)) - continue - bundle["labels"].append( - {"encryptedLabel": encoded_value, "externalId": encoded_key} - ) - - self.do_request("POST", "/labels", True, bundle) - finally: - self._curthr_pop() - - def pull_thread(self, wallet, force): - if wallet not in self.wallets or self.closing: - return # still has race conditions here - try: - self._curthr_push() - # self.print_error("pull_thread", wallet.basename(),"...") - wallet_id = self.wallets[wallet][2] - nonce = 1 if force else self.get_nonce(wallet) - 1 - if nonce < 1: - nonce = 1 - self.print_error("asking for labels since nonce", nonce) - try: - response = self.do_request( - "GET", ("/labels/since/%d/for/%s" % (nonce, wallet_id)) - ) - # self.print_error(nonce, wallet_id, response) - - if not response["labels"]: - self.print_error("no new labels") - return - result = {} - for label in response["labels"]: - try: - key = self.decode(wallet, label["externalId"]) - value = self.decode(wallet, label["encryptedLabel"]) - except Exception: - continue - try: - json.dumps(key) - json.dumps(value) - except Exception: - self.print_error("error: no json", key) - continue - result[key] = value - - with wallet.lock: - for key, value in result.items(): - if force or not wallet.labels.get(key): - wallet.labels[key] = value - - self.print_error( - "received %d labels" % len(response.get("labels", 0)) - ) - # do not write to disk because we're in a daemon thread - wallet.storage.put("labels", wallet.labels) - if response.get( - "nonce", 0 - ): # only override our nonce if the response nonce makes sense. - self.set_nonce(wallet, response["nonce"] + 1) - self.on_pulled(wallet) - - except Exception as e: - # traceback.print_exc(file=sys.stderr) - self.print_error("could not retrieve labels:", str(e)) - if force: - raise e # force download means we were in "settings" mode.. notify gui of failure. - finally: - self._curthr_pop() - - def on_pulled(self, wallet): - self.print_error("Wallet", wallet.basename(), "pulled.") - - def on_wallet_not_synched(self, wallet): - pass - - def on_request_exception(self, wallet, exc_info): - pass - - def start_wallet(self, wallet): - basename = wallet.basename() - if wallet in self.wallets: - self.print_error( - "Wallet", basename, "already in wallets list, aborting early." - ) - return - if not wallet.network: - # offline mode - self.print_error("Wallet", basename, "is in offline mode, aborting early.") - return - mpk = wallet.get_fingerprint() - if not mpk: - # We base the password off the mpk so.. if no mpk we can't do anything as it's then insecure to "make up a password'. - self.print_error( - "Wallet", - basename, - "is incompatible (no master public key), aborting early.", - ) - return - nonce = self.get_nonce(wallet) - self.print_error("Wallet", basename, "nonce is", nonce) - mpk = mpk.encode("ascii") - password = hashlib.sha1(mpk).hexdigest()[:32].encode("ascii") - iv = hashlib.sha256(password).digest()[:16] - wallet_id = hashlib.sha256(mpk).hexdigest() - self.wallets[wallet] = (password, iv, wallet_id) - # If there is an auth token we can try to actually start syncing - t = threading.Thread(target=self.pull_thread, args=(wallet, False)) - t.setDaemon(True) - t.start() - self.print_error("Wallet", basename, "added.") - return True - - def stop_wallet(self, wallet): - w = self.wallets.pop(wallet, None) - if w: - self.print_error(wallet.basename(), "removed from wallets.") - return bool(w) - - def on_close(self): - # this is to minimize chance of race conditions but the way this class is - # written they can theoretically still happen. c'est la vie. - self.closing = True - ct = 0 - for w in self.wallets.copy(): - ct += int(bool(self.stop_wallet(w))) - stopped = 0 - thrds, uniq_thrds = self.threads.copy(), [] - for t in thrds: - if t in uniq_thrds: - continue - uniq_thrds.append(t) - if t.is_alive(): - # wait for it to complete - t.join() - stopped += 1 - self.print_error( - f"Plugin closed, stopped {ct} extant wallets, joined {stopped} extant" - f" threads." - ) - # due to very very unlikely race conditions this is in fact a possibility. - assert 0 == len( - self.threads - ), "Labels Plugin: Threads were left alive on close!" - - def on_init(self): - """Here for symmetry with on_close. In reality plugins get unloaded - from memory after on_close so this method is not very useful.""" - self.print_error("Initializing...") - self.closing = False diff --git a/electrumabc_plugins/labels/qt.py b/electrumabc_plugins/labels/qt.py deleted file mode 100644 index c86dac4e00f9..000000000000 --- a/electrumabc_plugins/labels/qt.py +++ /dev/null @@ -1,296 +0,0 @@ -from functools import partial - -from PyQt5 import QtWidgets -from PyQt5.QtCore import QObject, Qt, pyqtSignal - -from electrumabc.constants import PROJECT_NAME -from electrumabc.i18n import _ -from electrumabc.plugins import hook -from electrumabc.util import Weak -from electrumabc_gui.qt.main_window import ElectrumWindow -from electrumabc_gui.qt.util import ( - Buttons, - EnterButton, - OkButton, - ThreadedButton, - WaitingDialog, - WindowModalDialog, -) - -from .labels import LabelsPlugin - - -class LabelsSignalObject(QObject): - """Signals need to be members of a QObject, hence why this class exists.""" - - labels_changed_signal = pyqtSignal(object) - wallet_not_synched_signal = pyqtSignal(object) - request_exception_signal = pyqtSignal(object, object) - - -def window_parent(w): - # this is needed because WindowModalDialog overrides window.parent - if callable(w.parent): - return w.parent() - return w.parent - - -class Plugin(LabelsPlugin): - def __init__(self, *args): - LabelsPlugin.__init__(self, *args) - self.obj = LabelsSignalObject() - self.wallet_windows = {} - self.initted = False - - def requires_settings(self): - return True - - def settings_widget(self, window): - while ( - window - and window_parent(window) - and not isinstance(window_parent(window), ElectrumWindow) - ): - # MacOS fixup -- find window.parent() because we can end up with window.parent() not an ElectrumWindow - window = window_parent(window) - windowRef = Weak.ref(window) - return EnterButton(_("Settings"), partial(self.settings_dialog, windowRef)) - - def settings_dialog(self, windowRef): - window = ( - windowRef() - ) # NB: window is the internal plugins dialog and not the wallet window - if not window or not isinstance(window_parent(window), ElectrumWindow): - return - wallet = window_parent(window).wallet - d = WindowModalDialog(window.top_level_window(), _("Label Settings")) - d.ok_button = OkButton(d) - dlgRef = Weak.ref(d) - if wallet in self.wallets: - - class MySigs(QObject): - ok_button_disable_sig = pyqtSignal(bool) - - d.sigs = MySigs(d) - d.sigs.ok_button_disable_sig.connect( - d.ok_button.setDisabled - ) # disable ok button while the TaskThread runs .. - hbox = QtWidgets.QHBoxLayout() - hbox.addWidget(QtWidgets.QLabel(_("LabelSync options:"))) - upload = ThreadedButton( - _("Force upload"), - partial(Weak(self.do_force_upload), wallet, dlgRef), - partial(Weak(self.done_processing), dlgRef), - partial(Weak(self.error_processing), dlgRef), - ) - download = ThreadedButton( - _("Force download"), - partial(Weak(self.do_force_download), wallet, dlgRef), - partial(Weak(self.done_processing), dlgRef), - partial(Weak(self.error_processing), dlgRef), - ) - d.thread_buts = (upload, download) - d.finished.connect(partial(Weak(self.on_dlg_finished), dlgRef)) - vbox = QtWidgets.QVBoxLayout() - vbox.addWidget(upload) - vbox.addWidget(download) - hbox.addLayout(vbox) - vbox = QtWidgets.QVBoxLayout(d) - vbox.addLayout(hbox) - else: - vbox = QtWidgets.QVBoxLayout(d) - if wallet.network: - # has network, so the fact that the wallet isn't in the list means it's - # incompatible - label = QtWidgets.QLabel( - "" + _("LabelSync not supported for this wallet type") + "" - ) - label.setAlignment(Qt.AlignCenter) - vbox.addWidget(label) - label = QtWidgets.QLabel( - _("(Only deterministic wallets are supported)") - ) - label.setAlignment(Qt.AlignCenter) - vbox.addWidget(label) - else: - # Does not have network, so we won't speak of incompatibility, but - # instead remind user offline mode means OFFLINE! ;) - label = QtWidgets.QLabel( - _( - f"You are using {PROJECT_NAME} in offline mode;" - f" restart Electron Cash if you want to get " - f"connected" - ) - ) - label.setWordWrap(True) - vbox.addWidget(label) - vbox.addSpacing(20) - vbox.addLayout(Buttons(d.ok_button)) - return bool(d.exec_()) - - def on_dlg_finished(self, dlgRef, result_code): - """Wait for any threaded buttons that may be still extant so we don't get a crash""" - # self.print_error("Dialog finished with code", result_code) - dlg = dlgRef() - if dlg: - upload, download = dlg.thread_buts - if upload.thread and upload.thread.isRunning(): - upload.thread.stop() - upload.thread.wait() - if download.thread and download.thread.isRunning(): - download.thread.stop() - download.thread.wait() - - def do_force_upload(self, wallet, dlgRef): - # this runs in a NON-GUI thread - dlg = dlgRef() - if dlg: - # block window closing prematurely which can cause a temporary hang - # until thread completes - dlg.sigs.ok_button_disable_sig.emit(True) - self.push_thread(wallet) - - def do_force_download(self, wallet, dlgRef): - # this runs in a NON-GUI thread - dlg = dlgRef() - if dlg: - # block window closing prematurely which can cause a temporary hang - # until thread completes - dlg.sigs.ok_button_disable_sig.emit(True) - self.pull_thread(wallet, True) - - def done_processing(self, dlgRef, result): - # this runs in the GUI thread - dlg = dlgRef() - if dlg: - dlg.ok_button.setEnabled(True) - self._ok_synched(dlg) - - def _ok_synched(self, window): - if window.isVisible(): - window.show_message(_("Your labels have been synchronised.")) - - def error_processing(self, dlgRef, exc_info): - dlg = dlgRef() - if dlg: - dlg.ok_button.setEnabled(True) - self._notok_synch(dlg, exc_info) - - _warn_dlg_flg = Weak.KeyDictionary() - - def _notok_synch(self, window, exc_info): - # Runs in main thread - cls = __class__ - if window.isVisible() and not cls._warn_dlg_flg.get(window, False): - # Guard against duplicate error dialogs (without this we may get error window spam when importing labels) - cls._warn_dlg_flg[window] = True - window.show_warning( - _("LabelSync error:") + "\n\n" + str(exc_info[1]), rich_text=False - ) - cls._warn_dlg_flg.pop(window, None) - - def on_request_exception(self, wallet, exc_info): - # not main thread - self.obj.request_exception_signal.emit(wallet, exc_info) - - def request_exception_slot(self, wallet, exc_info): - # main thread - window = self.wallet_windows.get(wallet, None) - if window: - self._notok_synch(window, exc_info) - - def start_wallet(self, wallet, window=None): - ret = super().start_wallet(wallet) - if ret and window: - self.wallet_windows[wallet] = window - return ret - - def stop_wallet(self, wallet): - ret = super().stop_wallet(wallet) - self.wallet_windows.pop(wallet, None) - return ret - - def on_pulled(self, wallet): - # not main thread - super().on_pulled(wallet) # super just logs to print_error - self.obj.labels_changed_signal.emit(wallet) - - def on_labels_changed(self, wallet): - # main thread - window = self.wallet_windows.get(wallet, None) - if window: - # self.print_error("On labels changed", wallet.basename()) - window.update_labels() - - def on_wallet_not_synched(self, wallet): - # not main thread - self.obj.wallet_not_synched_signal.emit(wallet) - - def wallet_not_synched_slot(self, wallet): - # main thread - window = self.wallet_windows.get(wallet, None) - if window: - if window.question( - _( - "LabelSync detected that this wallet is not synched with the label server." - ) - + "\n\n" - + _("Synchronize now?") - ): - WaitingDialog( - window, - _("Synchronizing..."), - partial(self.pull_thread, wallet, True), - lambda *args: self._ok_synched(window), - lambda exc: self._notok_synch(window, exc), - ) - - def on_close(self): - if not self.initted: - return - try: - self.obj.labels_changed_signal.disconnect(self.on_labels_changed) - except TypeError: - pass # not connected - try: - self.obj.wallet_not_synched_signal.disconnect(self.wallet_not_synched_slot) - except TypeError: - pass # not connected - try: - self.obj.request_exception_signal.disconnect(self.request_exception_slot) - except TypeError: - pass # not connected - super().on_close() - assert 0 == len( - self.wallet_windows - ), "LabelSync still had extant wallet_windows!" - self.initted = False - - @hook - def on_new_window(self, window): - return self.start_wallet(window.wallet, window) - - @hook - def on_close_window(self, window): - return self.stop_wallet(window.wallet) - - @hook - def init_qt(self, gui): - if self.initted: - return - self.on_init() - # connect signals. this needs to happen first as below on_new_window depends on these being active - self.obj.labels_changed_signal.connect(self.on_labels_changed) - self.obj.wallet_not_synched_signal.connect(self.wallet_not_synched_slot) - self.obj.request_exception_signal.connect(self.request_exception_slot) - - ct, ct2 = 0, 0 - for window in gui.windows: - if self.on_new_window(window): - ct2 += 1 - ct += 1 - - self.initted = True - self.print_error( - "Initialized (had {} extant windows, added {}).".format(ct, ct2) - ) diff --git a/setup.py b/setup.py index 52569bdbb636..216132f6d078 100755 --- a/setup.py +++ b/setup.py @@ -215,7 +215,6 @@ def run(self): "electrumabc_plugins.email_requests", "electrumabc_plugins.hw_wallet", "electrumabc_plugins.keepkey", - "electrumabc_plugins.labels", "electrumabc_plugins.ledger", "electrumabc_plugins.trezor", "electrumabc_plugins.digitalbitbox", From 5f085607145122dc5315c62196e1c36801b87d36 Mon Sep 17 00:00:00 2001 From: PiRK Date: Fri, 20 Jan 2023 14:41:19 +0100 Subject: [PATCH 2/3] don't show cosigner_pool plugin in optional features This is a regression in commit 989e1613b820bc8cf7d42766b148edeadde35a14 --- electrumabc/plugins.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrumabc/plugins.py b/electrumabc/plugins.py index c19a3d56223f..f5f148c51f66 100644 --- a/electrumabc/plugins.py +++ b/electrumabc/plugins.py @@ -170,6 +170,9 @@ def load_internal_plugins(self): for loader, name, ispkg in pkgutil.iter_modules( [self.internal_plugins_pkgpath] ): + # we don't have a server for cosigner_pool + if name == "cosigner_pool": + continue full_name = f"electrumabc_plugins.{name}" spec = importlib.util.find_spec(full_name) # pkgutil found it but importlib can't ?! From ca7b922b35679de58e1f7f642b319a727c68a583 Mon Sep 17 00:00:00 2001 From: PiRK Date: Fri, 20 Jan 2023 15:24:15 +0100 Subject: [PATCH 3/3] Fix the Optional Features menu After commit 989e1613b820bc8cf7d42766b148edeadde35a14, `descr["__name__"]` no longer matches the short name key expected by `plugins.get_internal_plugin(name)`. As a result, all plugins in that menu dialog were greyed out, and the user could no longer disable or enable a plugin. For existing users, everything stayed the same (previously enabled plugins stayed enabled and vice-versa), so the damage is mainly limited to new users not being able to enable CashFusion. --- electrumabc_gui/qt/main_window.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrumabc_gui/qt/main_window.py b/electrumabc_gui/qt/main_window.py index 81333fd0a3d3..ce2977fbf9d8 100644 --- a/electrumabc_gui/qt/main_window.py +++ b/electrumabc_gui/qt/main_window.py @@ -5882,7 +5882,9 @@ def do_toggle(weakCb, name, i): run_hook("init_qt", gui_object) for i, descr in enumerate(plugins.internal_plugin_metadata.values()): - name = descr["__name__"] + # descr["__name__"] is the fully qualified package name + # (electrumabc_plugins.name) + name = descr["__name__"].split(".")[-1] p = plugins.get_internal_plugin(name) if descr.get("registers_keystore"): continue