From da3b271f5cfaf5b489013bc17460531718f582f9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 6 Nov 2022 04:36:56 +0000 Subject: [PATCH] cosigner_pool plugin: handle multiple multisig wallets open fixes https://github.com/spesmilo/electrum/issues/3080 --- electrum/plugins/cosigner_pool/qt.py | 110 ++++++++++++++++----------- 1 file changed, 65 insertions(+), 45 deletions(-) diff --git a/electrum/plugins/cosigner_pool/qt.py b/electrum/plugins/cosigner_pool/qt.py index bdf15c0ab..d47dd4da3 100644 --- a/electrum/plugins/cosigner_pool/qt.py +++ b/electrum/plugins/cosigner_pool/qt.py @@ -25,7 +25,7 @@ import time from xmlrpc.client import ServerProxy -from typing import TYPE_CHECKING, Union, List, Tuple +from typing import TYPE_CHECKING, Union, List, Tuple, Dict import ssl from PyQt5.QtCore import QObject, pyqtSignal @@ -40,6 +40,7 @@ from electrum.plugin import BasePlugin, hook from electrum.i18n import _ from electrum.wallet import Multisig_Wallet, Abstract_Wallet from electrum.util import bh2u, bfh +from electrum.logging import Logger from electrum.gui.qt.transaction_dialog import show_transaction, TxDialog from electrum.gui.qt.util import WaitingDialog @@ -56,10 +57,10 @@ server = ServerProxy('https://cosigner.electrum.org/', allow_none=True, context= class Listener(util.DaemonThread): - def __init__(self, parent): + def __init__(self, cw: 'CosignerWallet'): util.DaemonThread.__init__(self) self.daemon = True - self.parent = parent + self.cw = cw self.received = set() self.keyhashes = [] @@ -81,13 +82,13 @@ class Listener(util.DaemonThread): try: message = server.get(keyhash) except Exception as e: - self.logger.info("cannot contact cosigner pool") + self.logger.info(f"cannot contact cosigner pool. exc: {e!r}") time.sleep(30) continue if message: self.received.add(keyhash) self.logger.info(f"received message for {keyhash}") - self.parent.obj.cosigner_receive_signal.emit( + self.cw.obj.cosigner_receive_signal.emit( keyhash, message) # poll every 30 seconds time.sleep(30) @@ -101,12 +102,8 @@ class Plugin(BasePlugin): def __init__(self, parent, config, name): BasePlugin.__init__(self, parent, config, name) - self.listener = None - self.obj = QReceiveSignalObject() - self.obj.cosigner_receive_signal.connect(self.on_receive) - self.keys = [] # type: List[Tuple[str, str, ElectrumWindow]] - self.cosigner_list = [] # type: List[Tuple[ElectrumWindow, str, bytes, str]] self._init_qt_received = False + self.cosigner_wallets = {} # type: Dict[Abstract_Wallet, CosignerWallet] @hook def init_qt(self, gui: 'ElectrumGui'): @@ -118,55 +115,79 @@ class Plugin(BasePlugin): @hook def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'): - self.update(window) + if type(wallet) != Multisig_Wallet: + return + self.cosigner_wallets[wallet] = CosignerWallet(wallet, window) @hook def on_close_window(self, window): - self.update(window) + wallet = window.wallet + if cw := self.cosigner_wallets.get(wallet): + cw.close() + self.cosigner_wallets.pop(wallet) def is_available(self): return True - def update(self, window: 'ElectrumWindow'): - wallet = window.wallet - if type(wallet) != Multisig_Wallet: - return - assert isinstance(wallet, Multisig_Wallet) # only here for type-hints in IDE - if self.listener is None: - self.logger.info("starting listener") - self.listener = Listener(self) - self.listener.start() - elif self.listener: - self.logger.info("shutting down listener") - self.listener.stop() - self.listener = None - self.keys = [] - self.cosigner_list = [] + @hook + def transaction_dialog(self, d: 'TxDialog'): + if cw := self.cosigner_wallets.get(d.wallet): + cw.hook_transaction_dialog(d) + + @hook + def transaction_dialog_update(self, d: 'TxDialog'): + if cw := self.cosigner_wallets.get(d.wallet): + cw.hook_transaction_dialog_update(d) + + +class CosignerWallet(Logger): + # one for each open window + + def __init__(self, wallet: 'Multisig_Wallet', window: 'ElectrumWindow'): + assert isinstance(wallet, Multisig_Wallet) + self.wallet = wallet + self.window = window + Logger.__init__(self) + self.obj = QReceiveSignalObject() + self.obj.cosigner_receive_signal.connect(self.on_receive) + + self.keys = [] # type: List[Tuple[str, str]] + self.cosigner_list = [] # type: List[Tuple[str, bytes, str]] for key, keystore in wallet.keystores.items(): xpub = keystore.get_master_public_key() # type: str pubkey = BIP32Node.from_xkey(xpub).eckey.get_public_key_bytes(compressed=True) _hash = bh2u(crypto.sha256d(pubkey)) if not keystore.is_watching_only(): - self.keys.append((key, _hash, window)) + self.keys.append((key, _hash)) else: - self.cosigner_list.append((window, xpub, pubkey, _hash)) - if self.listener: - self.listener.set_keyhashes([t[1] for t in self.keys]) + self.cosigner_list.append((xpub, pubkey, _hash)) - @hook - def transaction_dialog(self, d: 'TxDialog'): + self.logger.info("starting listener") + self.listener = Listener(self) + self.listener.start() + self.listener.set_keyhashes([t[1] for t in self.keys]) + + def diagnostic_name(self): + return self.wallet.diagnostic_name() + + def close(self): + self.logger.info("shutting down listener") + self.listener.stop() + self.listener = None + + def hook_transaction_dialog(self, d: 'TxDialog'): d.cosigner_send_button = b = QPushButton(_("Send to cosigner")) b.clicked.connect(lambda: self.do_send(d.tx)) d.buttons.insert(0, b) b.setVisible(False) - @hook - def transaction_dialog_update(self, d: 'TxDialog'): + def hook_transaction_dialog_update(self, d: 'TxDialog'): + assert self.wallet == d.wallet if not d.finalized or d.tx.is_complete() or d.wallet.can_sign(d.tx): d.cosigner_send_button.setVisible(False) return - for window, xpub, K, _hash in self.cosigner_list: - if window.wallet == d.wallet and self.cosigner_can_sign(d.tx, xpub): + for xpub, K, _hash in self.cosigner_list: + if self.cosigner_can_sign(d.tx, xpub): d.cosigner_send_button.setVisible(True) break else: @@ -180,21 +201,19 @@ class Plugin(BasePlugin): def do_send(self, tx: Union[Transaction, PartialTransaction]): def on_success(result): - window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' + + self.window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' + _("Open your cosigner wallet to retrieve it.")) def on_failure(exc_info): e = exc_info[1] try: self.logger.error("on_failure", exc_info=exc_info) except OSError: pass - window.show_error(_("Failed to send transaction to cosigning pool") + ':\n' + repr(e)) + self.window.show_error(_("Failed to send transaction to cosigning pool") + ':\n' + repr(e)) buffer = [] - some_window = None # construct messages - for window, xpub, K, _hash in self.cosigner_list: + for xpub, K, _hash in self.cosigner_list: if not self.cosigner_can_sign(tx, xpub): continue - some_window = window raw_tx_bytes = tx.serialize_as_bytes() public_key = ecc.ECPubkey(K) message = public_key.encrypt_message(raw_tx_bytes).decode('ascii') @@ -208,18 +227,19 @@ class Plugin(BasePlugin): for _hash, message in buffer: server.put(_hash, message) msg = _('Sending transaction to cosigning pool...') - WaitingDialog(some_window, msg, send_messages_task, on_success, on_failure) + WaitingDialog(self.window, msg, send_messages_task, on_success, on_failure) def on_receive(self, keyhash, message): self.logger.info(f"signal arrived for {keyhash}") - for key, _hash, window in self.keys: + for key, _hash in self.keys: if _hash == keyhash: break else: self.logger.info("keyhash not found") return - wallet = window.wallet + window = self.window + wallet = self.wallet if isinstance(wallet.keystore, keystore.Hardware_KeyStore): window.show_warning(_('An encrypted transaction was retrieved from cosigning pool.') + '\n' + _('However, hardware wallets do not support message decryption, '