qt.py (9911B)
1 #!/usr/bin/env python 2 # 3 # Electrum - lightweight Bitcoin client 4 # Copyright (C) 2014 Thomas Voegtlin 5 # 6 # Permission is hereby granted, free of charge, to any person 7 # obtaining a copy of this software and associated documentation files 8 # (the "Software"), to deal in the Software without restriction, 9 # including without limitation the rights to use, copy, modify, merge, 10 # publish, distribute, sublicense, and/or sell copies of the Software, 11 # and to permit persons to whom the Software is furnished to do so, 12 # subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be 15 # included in all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 # SOFTWARE. 25 26 import time 27 from xmlrpc.client import ServerProxy 28 from typing import TYPE_CHECKING, Union, List, Tuple 29 import ssl 30 31 from PyQt5.QtCore import QObject, pyqtSignal 32 from PyQt5.QtWidgets import QPushButton 33 import certifi 34 35 from electrum import util, keystore, ecc, crypto 36 from electrum import transaction 37 from electrum.transaction import Transaction, PartialTransaction, tx_from_any 38 from electrum.bip32 import BIP32Node 39 from electrum.plugin import BasePlugin, hook 40 from electrum.i18n import _ 41 from electrum.wallet import Multisig_Wallet, Abstract_Wallet 42 from electrum.util import bh2u, bfh 43 44 from electrum.gui.qt.transaction_dialog import show_transaction, TxDialog 45 from electrum.gui.qt.util import WaitingDialog 46 47 if TYPE_CHECKING: 48 from electrum.gui.qt import ElectrumGui 49 from electrum.gui.qt.main_window import ElectrumWindow 50 51 52 ca_path = certifi.where() 53 ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path) 54 server = ServerProxy('https://cosigner.electrum.org/', allow_none=True, context=ssl_context) 55 56 57 class Listener(util.DaemonThread): 58 59 def __init__(self, parent): 60 util.DaemonThread.__init__(self) 61 self.daemon = True 62 self.parent = parent 63 self.received = set() 64 self.keyhashes = [] 65 66 def set_keyhashes(self, keyhashes): 67 self.keyhashes = keyhashes 68 69 def clear(self, keyhash): 70 server.delete(keyhash) 71 self.received.remove(keyhash) 72 73 def run(self): 74 while self.running: 75 if not self.keyhashes: 76 time.sleep(2) 77 continue 78 for keyhash in self.keyhashes: 79 if keyhash in self.received: 80 continue 81 try: 82 message = server.get(keyhash) 83 except Exception as e: 84 self.logger.info("cannot contact cosigner pool") 85 time.sleep(30) 86 continue 87 if message: 88 self.received.add(keyhash) 89 self.logger.info(f"received message for {keyhash}") 90 self.parent.obj.cosigner_receive_signal.emit( 91 keyhash, message) 92 # poll every 30 seconds 93 time.sleep(30) 94 95 96 class QReceiveSignalObject(QObject): 97 cosigner_receive_signal = pyqtSignal(object, object) 98 99 100 class Plugin(BasePlugin): 101 102 def __init__(self, parent, config, name): 103 BasePlugin.__init__(self, parent, config, name) 104 self.listener = None 105 self.obj = QReceiveSignalObject() 106 self.obj.cosigner_receive_signal.connect(self.on_receive) 107 self.keys = [] # type: List[Tuple[str, str, ElectrumWindow]] 108 self.cosigner_list = [] # type: List[Tuple[ElectrumWindow, str, bytes, str]] 109 self._init_qt_received = False 110 111 @hook 112 def init_qt(self, gui: 'ElectrumGui'): 113 if self._init_qt_received: # only need/want the first signal 114 return 115 self._init_qt_received = True 116 for window in gui.windows: 117 self.load_wallet(window.wallet, window) 118 119 @hook 120 def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'): 121 self.update(window) 122 123 @hook 124 def on_close_window(self, window): 125 self.update(window) 126 127 def is_available(self): 128 return True 129 130 def update(self, window: 'ElectrumWindow'): 131 wallet = window.wallet 132 if type(wallet) != Multisig_Wallet: 133 return 134 assert isinstance(wallet, Multisig_Wallet) # only here for type-hints in IDE 135 if self.listener is None: 136 self.logger.info("starting listener") 137 self.listener = Listener(self) 138 self.listener.start() 139 elif self.listener: 140 self.logger.info("shutting down listener") 141 self.listener.stop() 142 self.listener = None 143 self.keys = [] 144 self.cosigner_list = [] 145 for key, keystore in wallet.keystores.items(): 146 xpub = keystore.get_master_public_key() # type: str 147 pubkey = BIP32Node.from_xkey(xpub).eckey.get_public_key_bytes(compressed=True) 148 _hash = bh2u(crypto.sha256d(pubkey)) 149 if not keystore.is_watching_only(): 150 self.keys.append((key, _hash, window)) 151 else: 152 self.cosigner_list.append((window, xpub, pubkey, _hash)) 153 if self.listener: 154 self.listener.set_keyhashes([t[1] for t in self.keys]) 155 156 @hook 157 def transaction_dialog(self, d: 'TxDialog'): 158 d.cosigner_send_button = b = QPushButton(_("Send to cosigner")) 159 b.clicked.connect(lambda: self.do_send(d.tx)) 160 d.buttons.insert(0, b) 161 b.setVisible(False) 162 163 @hook 164 def transaction_dialog_update(self, d: 'TxDialog'): 165 if not d.finalized or d.tx.is_complete() or d.wallet.can_sign(d.tx): 166 d.cosigner_send_button.setVisible(False) 167 return 168 for window, xpub, K, _hash in self.cosigner_list: 169 if window.wallet == d.wallet and self.cosigner_can_sign(d.tx, xpub): 170 d.cosigner_send_button.setVisible(True) 171 break 172 else: 173 d.cosigner_send_button.setVisible(False) 174 175 def cosigner_can_sign(self, tx: Transaction, cosigner_xpub: str) -> bool: 176 # TODO implement this properly: 177 # should return True iff cosigner (with given xpub) can sign and has not yet signed. 178 # note that tx could also be unrelated from wallet?... (not ismine inputs) 179 return True 180 181 def do_send(self, tx: Union[Transaction, PartialTransaction]): 182 def on_success(result): 183 window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' + 184 _("Open your cosigner wallet to retrieve it.")) 185 def on_failure(exc_info): 186 e = exc_info[1] 187 try: self.logger.error("on_failure", exc_info=exc_info) 188 except OSError: pass 189 window.show_error(_("Failed to send transaction to cosigning pool") + ':\n' + repr(e)) 190 191 buffer = [] 192 some_window = None 193 # construct messages 194 for window, xpub, K, _hash in self.cosigner_list: 195 if not self.cosigner_can_sign(tx, xpub): 196 continue 197 some_window = window 198 raw_tx_bytes = tx.serialize_as_bytes() 199 public_key = ecc.ECPubkey(K) 200 message = public_key.encrypt_message(raw_tx_bytes).decode('ascii') 201 buffer.append((_hash, message)) 202 if not buffer: 203 return 204 205 # send messages 206 # note: we send all messages sequentially on the same thread 207 def send_messages_task(): 208 for _hash, message in buffer: 209 server.put(_hash, message) 210 msg = _('Sending transaction to cosigning pool...') 211 WaitingDialog(some_window, msg, send_messages_task, on_success, on_failure) 212 213 def on_receive(self, keyhash, message): 214 self.logger.info(f"signal arrived for {keyhash}") 215 for key, _hash, window in self.keys: 216 if _hash == keyhash: 217 break 218 else: 219 self.logger.info("keyhash not found") 220 return 221 222 wallet = window.wallet 223 if isinstance(wallet.keystore, keystore.Hardware_KeyStore): 224 window.show_warning(_('An encrypted transaction was retrieved from cosigning pool.') + '\n' + 225 _('However, hardware wallets do not support message decryption, ' 226 'which makes them not compatible with the current design of cosigner pool.')) 227 return 228 elif wallet.has_keystore_encryption(): 229 password = window.password_dialog(_('An encrypted transaction was retrieved from cosigning pool.') + '\n' + 230 _('Please enter your password to decrypt it.')) 231 if not password: 232 return 233 else: 234 password = None 235 if not window.question(_("An encrypted transaction was retrieved from cosigning pool.") + '\n' + 236 _("Do you want to open it now?")): 237 return 238 239 xprv = wallet.keystore.get_master_private_key(password) 240 if not xprv: 241 return 242 try: 243 privkey = BIP32Node.from_xkey(xprv).eckey 244 message = privkey.decrypt_message(message) 245 except Exception as e: 246 self.logger.exception('') 247 window.show_error(_('Error decrypting message') + ':\n' + repr(e)) 248 return 249 250 self.listener.clear(keyhash) 251 tx = tx_from_any(message) 252 show_transaction(tx, parent=window, prompt_if_unsaved=True)