cosigner_pool plugin: handle multiple multisig wallets open
fixes https://github.com/spesmilo/electrum/issues/3080
This commit is contained in:
@@ -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.listener.set_keyhashes([t[1] for t in self.keys])
|
|
||||||
|
|
||||||
@hook
|
self.logger.info("starting listener")
|
||||||
def transaction_dialog(self, d: 'TxDialog'):
|
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"))
|
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, '
|
||||||
|
|||||||
Reference in New Issue
Block a user