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)