electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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)