Browse Source

cosigner_pool plugin: handle multiple multisig wallets open

fixes https://github.com/spesmilo/electrum/issues/3080
patch-4
SomberNight 2 years ago
parent
commit
da3b271f5c
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 108
      electrum/plugins/cosigner_pool/qt.py

108
electrum/plugins/cosigner_pool/qt.py

@ -25,7 +25,7 @@
import time import time
from xmlrpc.client import ServerProxy from xmlrpc.client import ServerProxy
from typing import TYPE_CHECKING, Union, List, Tuple from typing import TYPE_CHECKING, Union, List, Tuple, Dict
import ssl import ssl
from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtCore import QObject, pyqtSignal
@ -40,6 +40,7 @@ from electrum.plugin import BasePlugin, hook
from electrum.i18n import _ from electrum.i18n import _
from electrum.wallet import Multisig_Wallet, Abstract_Wallet from electrum.wallet import Multisig_Wallet, Abstract_Wallet
from electrum.util import bh2u, bfh 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.transaction_dialog import show_transaction, TxDialog
from electrum.gui.qt.util import WaitingDialog 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): class Listener(util.DaemonThread):
def __init__(self, parent): def __init__(self, cw: 'CosignerWallet'):
util.DaemonThread.__init__(self) util.DaemonThread.__init__(self)
self.daemon = True self.daemon = True
self.parent = parent self.cw = cw
self.received = set() self.received = set()
self.keyhashes = [] self.keyhashes = []
@ -81,13 +82,13 @@ class Listener(util.DaemonThread):
try: try:
message = server.get(keyhash) message = server.get(keyhash)
except Exception as e: 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) time.sleep(30)
continue continue
if message: if message:
self.received.add(keyhash) self.received.add(keyhash)
self.logger.info(f"received message for {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) keyhash, message)
# poll every 30 seconds # poll every 30 seconds
time.sleep(30) time.sleep(30)
@ -101,12 +102,8 @@ class Plugin(BasePlugin):
def __init__(self, parent, config, name): def __init__(self, parent, config, name):
BasePlugin.__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._init_qt_received = False
self.cosigner_wallets = {} # type: Dict[Abstract_Wallet, CosignerWallet]
@hook @hook
def init_qt(self, gui: 'ElectrumGui'): def init_qt(self, gui: 'ElectrumGui'):
@ -118,55 +115,79 @@ class Plugin(BasePlugin):
@hook @hook
def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'): 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 @hook
def on_close_window(self, window): 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): def is_available(self):
return True return True
def update(self, window: 'ElectrumWindow'): @hook
wallet = window.wallet def transaction_dialog(self, d: 'TxDialog'):
if type(wallet) != Multisig_Wallet: if cw := self.cosigner_wallets.get(d.wallet):
return cw.hook_transaction_dialog(d)
assert isinstance(wallet, Multisig_Wallet) # only here for type-hints in IDE
if self.listener is None: @hook
self.logger.info("starting listener") def transaction_dialog_update(self, d: 'TxDialog'):
self.listener = Listener(self) if cw := self.cosigner_wallets.get(d.wallet):
self.listener.start() cw.hook_transaction_dialog_update(d)
elif self.listener:
self.logger.info("shutting down listener")
self.listener.stop() class CosignerWallet(Logger):
self.listener = None # one for each open window
self.keys = []
self.cosigner_list = [] 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(): for key, keystore in wallet.keystores.items():
xpub = keystore.get_master_public_key() # type: str xpub = keystore.get_master_public_key() # type: str
pubkey = BIP32Node.from_xkey(xpub).eckey.get_public_key_bytes(compressed=True) pubkey = BIP32Node.from_xkey(xpub).eckey.get_public_key_bytes(compressed=True)
_hash = bh2u(crypto.sha256d(pubkey)) _hash = bh2u(crypto.sha256d(pubkey))
if not keystore.is_watching_only(): if not keystore.is_watching_only():
self.keys.append((key, _hash, window)) self.keys.append((key, _hash))
else: else:
self.cosigner_list.append((window, xpub, pubkey, _hash)) self.cosigner_list.append((xpub, pubkey, _hash))
if self.listener:
self.logger.info("starting listener")
self.listener = Listener(self)
self.listener.start()
self.listener.set_keyhashes([t[1] for t in self.keys]) self.listener.set_keyhashes([t[1] for t in self.keys])
@hook def diagnostic_name(self):
def transaction_dialog(self, d: 'TxDialog'): 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")) d.cosigner_send_button = b = QPushButton(_("Send to cosigner"))
b.clicked.connect(lambda: self.do_send(d.tx)) b.clicked.connect(lambda: self.do_send(d.tx))
d.buttons.insert(0, b) d.buttons.insert(0, b)
b.setVisible(False) b.setVisible(False)
@hook def hook_transaction_dialog_update(self, d: 'TxDialog'):
def 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): if not d.finalized or d.tx.is_complete() or d.wallet.can_sign(d.tx):
d.cosigner_send_button.setVisible(False) d.cosigner_send_button.setVisible(False)
return return
for window, xpub, K, _hash in self.cosigner_list: for xpub, K, _hash in self.cosigner_list:
if window.wallet == d.wallet and self.cosigner_can_sign(d.tx, xpub): if self.cosigner_can_sign(d.tx, xpub):
d.cosigner_send_button.setVisible(True) d.cosigner_send_button.setVisible(True)
break break
else: else:
@ -180,21 +201,19 @@ class Plugin(BasePlugin):
def do_send(self, tx: Union[Transaction, PartialTransaction]): def do_send(self, tx: Union[Transaction, PartialTransaction]):
def on_success(result): 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.")) _("Open your cosigner wallet to retrieve it."))
def on_failure(exc_info): def on_failure(exc_info):
e = exc_info[1] e = exc_info[1]
try: self.logger.error("on_failure", exc_info=exc_info) try: self.logger.error("on_failure", exc_info=exc_info)
except OSError: pass 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 = [] buffer = []
some_window = None
# construct messages # 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): if not self.cosigner_can_sign(tx, xpub):
continue continue
some_window = window
raw_tx_bytes = tx.serialize_as_bytes() raw_tx_bytes = tx.serialize_as_bytes()
public_key = ecc.ECPubkey(K) public_key = ecc.ECPubkey(K)
message = public_key.encrypt_message(raw_tx_bytes).decode('ascii') message = public_key.encrypt_message(raw_tx_bytes).decode('ascii')
@ -208,18 +227,19 @@ class Plugin(BasePlugin):
for _hash, message in buffer: for _hash, message in buffer:
server.put(_hash, message) server.put(_hash, message)
msg = _('Sending transaction to cosigning pool...') 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): def on_receive(self, keyhash, message):
self.logger.info(f"signal arrived for {keyhash}") self.logger.info(f"signal arrived for {keyhash}")
for key, _hash, window in self.keys: for key, _hash in self.keys:
if _hash == keyhash: if _hash == keyhash:
break break
else: else:
self.logger.info("keyhash not found") self.logger.info("keyhash not found")
return return
wallet = window.wallet window = self.window
wallet = self.wallet
if isinstance(wallet.keystore, keystore.Hardware_KeyStore): if isinstance(wallet.keystore, keystore.Hardware_KeyStore):
window.show_warning(_('An encrypted transaction was retrieved from cosigning pool.') + '\n' + window.show_warning(_('An encrypted transaction was retrieved from cosigning pool.') + '\n' +
_('However, hardware wallets do not support message decryption, ' _('However, hardware wallets do not support message decryption, '

Loading…
Cancel
Save