URI: 
       tqt.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tqt.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)