tqt.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
tqt.py (21197B)
---
1 from functools import partial
2 import threading
3
4 from PyQt5.QtCore import Qt, pyqtSignal, QRegExp
5 from PyQt5.QtGui import QRegExpValidator
6 from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,
7 QHBoxLayout, QButtonGroup, QGroupBox,
8 QTextEdit, QLineEdit, QRadioButton, QCheckBox, QWidget,
9 QMessageBox, QFileDialog, QSlider, QTabWidget)
10
11 from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
12 OkButton, CloseButton, getOpenFileName)
13 from electrum.i18n import _
14 from electrum.plugin import hook
15 from electrum.util import bh2u
16
17 from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
18 from ..hw_wallet.plugin import only_hook_if_libraries_available
19 from .safe_t import SafeTPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC
20
21
22 PASSPHRASE_HELP_SHORT =_(
23 "Passphrases allow you to access new wallets, each "
24 "hidden behind a particular case-sensitive passphrase.")
25 PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + " " + _(
26 "You need to create a separate Electrum wallet for each passphrase "
27 "you use as they each generate different addresses. Changing "
28 "your passphrase does not lose other wallets, each is still "
29 "accessible behind its own passphrase.")
30 RECOMMEND_PIN = _(
31 "You should enable PIN protection. Your PIN is the only protection "
32 "for your bitcoins if your device is lost or stolen.")
33 PASSPHRASE_NOT_PIN = _(
34 "If you forget a passphrase you will be unable to access any "
35 "bitcoins in the wallet behind it. A passphrase is not a PIN. "
36 "Only change this if you are sure you understand it.")
37
38
39 class QtHandler(QtHandlerBase):
40
41 pin_signal = pyqtSignal(object, object)
42
43 def __init__(self, win, pin_matrix_widget_class, device):
44 super(QtHandler, self).__init__(win, device)
45 self.pin_signal.connect(self.pin_dialog)
46 self.pin_matrix_widget_class = pin_matrix_widget_class
47
48 def get_pin(self, msg, *, show_strength=True):
49 self.done.clear()
50 self.pin_signal.emit(msg, show_strength)
51 self.done.wait()
52 return self.response
53
54 def pin_dialog(self, msg, show_strength):
55 # Needed e.g. when resetting a device
56 self.clear_dialog()
57 dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN"))
58 matrix = self.pin_matrix_widget_class(show_strength)
59 vbox = QVBoxLayout()
60 vbox.addWidget(QLabel(msg))
61 vbox.addWidget(matrix)
62 vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
63 dialog.setLayout(vbox)
64 dialog.exec_()
65 self.response = str(matrix.get_value())
66 self.done.set()
67
68
69 class QtPlugin(QtPluginBase):
70 # Derived classes must provide the following class-static variables:
71 # icon_file
72 # pin_matrix_widget_class
73
74 @only_hook_if_libraries_available
75 @hook
76 def receive_menu(self, menu, addrs, wallet):
77 if len(addrs) != 1:
78 return
79 for keystore in wallet.get_keystores():
80 if type(keystore) == self.keystore_class:
81 def show_address(keystore=keystore):
82 keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore))
83 device_name = "{} ({})".format(self.device, keystore.label)
84 menu.addAction(_("Show on {}").format(device_name), show_address)
85
86 def show_settings_dialog(self, window, keystore):
87 def connect():
88 device_id = self.choose_device(window, keystore)
89 return device_id
90 def show_dialog(device_id):
91 if device_id:
92 SettingsDialog(window, self, keystore, device_id).exec_()
93 keystore.thread.add(connect, on_success=show_dialog)
94
95 def request_safe_t_init_settings(self, wizard, method, device):
96 vbox = QVBoxLayout()
97 next_enabled = True
98 label = QLabel(_("Enter a label to name your device:"))
99 name = QLineEdit()
100 hl = QHBoxLayout()
101 hl.addWidget(label)
102 hl.addWidget(name)
103 hl.addStretch(1)
104 vbox.addLayout(hl)
105
106 def clean_text(widget):
107 text = widget.toPlainText().strip()
108 return ' '.join(text.split())
109
110 if method in [TIM_NEW, TIM_RECOVER]:
111 gb = QGroupBox()
112 hbox1 = QHBoxLayout()
113 gb.setLayout(hbox1)
114 vbox.addWidget(gb)
115 gb.setTitle(_("Select your seed length:"))
116 bg = QButtonGroup()
117 for i, count in enumerate([12, 18, 24]):
118 rb = QRadioButton(gb)
119 rb.setText(_("{:d} words").format(count))
120 bg.addButton(rb)
121 bg.setId(rb, i)
122 hbox1.addWidget(rb)
123 rb.setChecked(True)
124 cb_pin = QCheckBox(_('Enable PIN protection'))
125 cb_pin.setChecked(True)
126 else:
127 text = QTextEdit()
128 text.setMaximumHeight(60)
129 if method == TIM_MNEMONIC:
130 msg = _("Enter your BIP39 mnemonic:")
131 else:
132 msg = _("Enter the master private key beginning with xprv:")
133 def set_enabled():
134 from electrum.bip32 import is_xprv
135 wizard.next_button.setEnabled(is_xprv(clean_text(text)))
136 text.textChanged.connect(set_enabled)
137 next_enabled = False
138
139 vbox.addWidget(QLabel(msg))
140 vbox.addWidget(text)
141 pin = QLineEdit()
142 pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}')))
143 pin.setMaximumWidth(100)
144 hbox_pin = QHBoxLayout()
145 hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):")))
146 hbox_pin.addWidget(pin)
147 hbox_pin.addStretch(1)
148
149 if method in [TIM_NEW, TIM_RECOVER]:
150 vbox.addWidget(WWLabel(RECOMMEND_PIN))
151 vbox.addWidget(cb_pin)
152 else:
153 vbox.addLayout(hbox_pin)
154
155 passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT)
156 passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
157 passphrase_warning.setStyleSheet("color: red")
158 cb_phrase = QCheckBox(_('Enable passphrases'))
159 cb_phrase.setChecked(False)
160 vbox.addWidget(passphrase_msg)
161 vbox.addWidget(passphrase_warning)
162 vbox.addWidget(cb_phrase)
163
164 wizard.exec_layout(vbox, next_enabled=next_enabled)
165
166 if method in [TIM_NEW, TIM_RECOVER]:
167 item = bg.checkedId()
168 pin = cb_pin.isChecked()
169 else:
170 item = ' '.join(str(clean_text(text)).split())
171 pin = str(pin.text())
172
173 return (item, name.text(), pin, cb_phrase.isChecked())
174
175
176 class Plugin(SafeTPlugin, QtPlugin):
177 icon_unpaired = "safe-t_unpaired.png"
178 icon_paired = "safe-t.png"
179
180 def create_handler(self, window):
181 return QtHandler(window, self.pin_matrix_widget_class(), self.device)
182
183 @classmethod
184 def pin_matrix_widget_class(self):
185 from safetlib.qt.pinmatrix import PinMatrixWidget
186 return PinMatrixWidget
187
188
189 class SettingsDialog(WindowModalDialog):
190 '''This dialog doesn't require a device be paired with a wallet.
191 We want users to be able to wipe a device even if they've forgotten
192 their PIN.'''
193
194 def __init__(self, window, plugin, keystore, device_id):
195 title = _("{} Settings").format(plugin.device)
196 super(SettingsDialog, self).__init__(window, title)
197 self.setMaximumWidth(540)
198
199 devmgr = plugin.device_manager()
200 config = devmgr.config
201 handler = keystore.handler
202 thread = keystore.thread
203 hs_cols, hs_rows = (128, 64)
204
205 def invoke_client(method, *args, **kw_args):
206 unpair_after = kw_args.pop('unpair_after', False)
207
208 def task():
209 client = devmgr.client_by_id(device_id)
210 if not client:
211 raise RuntimeError("Device not connected")
212 if method:
213 getattr(client, method)(*args, **kw_args)
214 if unpair_after:
215 devmgr.unpair_id(device_id)
216 return client.features
217
218 thread.add(task, on_success=update)
219
220 def update(features):
221 self.features = features
222 set_label_enabled()
223 if features.bootloader_hash:
224 bl_hash = bh2u(features.bootloader_hash)
225 bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
226 else:
227 bl_hash = "N/A"
228 noyes = [_("No"), _("Yes")]
229 endis = [_("Enable Passphrases"), _("Disable Passphrases")]
230 disen = [_("Disabled"), _("Enabled")]
231 setchange = [_("Set a PIN"), _("Change PIN")]
232
233 version = "%d.%d.%d" % (features.major_version,
234 features.minor_version,
235 features.patch_version)
236
237 device_label.setText(features.label)
238 pin_set_label.setText(noyes[features.pin_protection])
239 passphrases_label.setText(disen[features.passphrase_protection])
240 bl_hash_label.setText(bl_hash)
241 label_edit.setText(features.label)
242 device_id_label.setText(features.device_id)
243 initialized_label.setText(noyes[features.initialized])
244 version_label.setText(version)
245 clear_pin_button.setVisible(features.pin_protection)
246 clear_pin_warning.setVisible(features.pin_protection)
247 pin_button.setText(setchange[features.pin_protection])
248 pin_msg.setVisible(not features.pin_protection)
249 passphrase_button.setText(endis[features.passphrase_protection])
250 language_label.setText(features.language)
251
252 def set_label_enabled():
253 label_apply.setEnabled(label_edit.text() != self.features.label)
254
255 def rename():
256 invoke_client('change_label', label_edit.text())
257
258 def toggle_passphrase():
259 title = _("Confirm Toggle Passphrase Protection")
260 currently_enabled = self.features.passphrase_protection
261 if currently_enabled:
262 msg = _("After disabling passphrases, you can only pair this "
263 "Electrum wallet if it had an empty passphrase. "
264 "If its passphrase was not empty, you will need to "
265 "create a new wallet with the install wizard. You "
266 "can use this wallet again at any time by re-enabling "
267 "passphrases and entering its passphrase.")
268 else:
269 msg = _("Your current Electrum wallet can only be used with "
270 "an empty passphrase. You must create a separate "
271 "wallet with the install wizard for other passphrases "
272 "as each one generates a new set of addresses.")
273 msg += "\n\n" + _("Are you sure you want to proceed?")
274 if not self.question(msg, title=title):
275 return
276 invoke_client('toggle_passphrase', unpair_after=currently_enabled)
277
278 def change_homescreen():
279 filename = getOpenFileName(
280 parent=self,
281 title=_("Choose Homescreen"),
282 config=config,
283 )
284 if not filename:
285 return # user cancelled
286
287 if filename.endswith('.toif'):
288 img = open(filename, 'rb').read()
289 if img[:8] != b'TOIf\x90\x00\x90\x00':
290 handler.show_error('File is not a TOIF file with size of 144x144')
291 return
292 else:
293 from PIL import Image # FIXME
294 im = Image.open(filename)
295 if im.size != (128, 64):
296 handler.show_error('Image must be 128 x 64 pixels')
297 return
298 im = im.convert('1')
299 pix = im.load()
300 img = bytearray(1024)
301 for j in range(64):
302 for i in range(128):
303 if pix[i, j]:
304 o = (i + j * 128)
305 img[o // 8] |= (1 << (7 - o % 8))
306 img = bytes(img)
307 invoke_client('change_homescreen', img)
308
309 def clear_homescreen():
310 invoke_client('change_homescreen', b'\x00')
311
312 def set_pin():
313 invoke_client('set_pin', remove=False)
314
315 def clear_pin():
316 invoke_client('set_pin', remove=True)
317
318 def wipe_device():
319 wallet = window.wallet
320 if wallet and sum(wallet.get_balance()):
321 title = _("Confirm Device Wipe")
322 msg = _("Are you SURE you want to wipe the device?\n"
323 "Your wallet still has bitcoins in it!")
324 if not self.question(msg, title=title,
325 icon=QMessageBox.Critical):
326 return
327 invoke_client('wipe_device', unpair_after=True)
328
329 def slider_moved():
330 mins = timeout_slider.sliderPosition()
331 timeout_minutes.setText(_("{:2d} minutes").format(mins))
332
333 def slider_released():
334 config.set_session_timeout(timeout_slider.sliderPosition() * 60)
335
336 # Information tab
337 info_tab = QWidget()
338 info_layout = QVBoxLayout(info_tab)
339 info_glayout = QGridLayout()
340 info_glayout.setColumnStretch(2, 1)
341 device_label = QLabel()
342 pin_set_label = QLabel()
343 passphrases_label = QLabel()
344 version_label = QLabel()
345 device_id_label = QLabel()
346 bl_hash_label = QLabel()
347 bl_hash_label.setWordWrap(True)
348 language_label = QLabel()
349 initialized_label = QLabel()
350 rows = [
351 (_("Device Label"), device_label),
352 (_("PIN set"), pin_set_label),
353 (_("Passphrases"), passphrases_label),
354 (_("Firmware Version"), version_label),
355 (_("Device ID"), device_id_label),
356 (_("Bootloader Hash"), bl_hash_label),
357 (_("Language"), language_label),
358 (_("Initialized"), initialized_label),
359 ]
360 for row_num, (label, widget) in enumerate(rows):
361 info_glayout.addWidget(QLabel(label), row_num, 0)
362 info_glayout.addWidget(widget, row_num, 1)
363 info_layout.addLayout(info_glayout)
364
365 # Settings tab
366 settings_tab = QWidget()
367 settings_layout = QVBoxLayout(settings_tab)
368 settings_glayout = QGridLayout()
369
370 # Settings tab - Label
371 label_msg = QLabel(_("Name this {}. If you have multiple devices "
372 "their labels help distinguish them.")
373 .format(plugin.device))
374 label_msg.setWordWrap(True)
375 label_label = QLabel(_("Device Label"))
376 label_edit = QLineEdit()
377 label_edit.setMinimumWidth(150)
378 label_edit.setMaxLength(plugin.MAX_LABEL_LEN)
379 label_apply = QPushButton(_("Apply"))
380 label_apply.clicked.connect(rename)
381 label_edit.textChanged.connect(set_label_enabled)
382 settings_glayout.addWidget(label_label, 0, 0)
383 settings_glayout.addWidget(label_edit, 0, 1, 1, 2)
384 settings_glayout.addWidget(label_apply, 0, 3)
385 settings_glayout.addWidget(label_msg, 1, 1, 1, -1)
386
387 # Settings tab - PIN
388 pin_label = QLabel(_("PIN Protection"))
389 pin_button = QPushButton()
390 pin_button.clicked.connect(set_pin)
391 settings_glayout.addWidget(pin_label, 2, 0)
392 settings_glayout.addWidget(pin_button, 2, 1)
393 pin_msg = QLabel(_("PIN protection is strongly recommended. "
394 "A PIN is your only protection against someone "
395 "stealing your bitcoins if they obtain physical "
396 "access to your {}.").format(plugin.device))
397 pin_msg.setWordWrap(True)
398 pin_msg.setStyleSheet("color: red")
399 settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
400
401 # Settings tab - Homescreen
402 homescreen_label = QLabel(_("Homescreen"))
403 homescreen_change_button = QPushButton(_("Change..."))
404 homescreen_clear_button = QPushButton(_("Reset"))
405 homescreen_change_button.clicked.connect(change_homescreen)
406 try:
407 import PIL
408 except ImportError:
409 homescreen_change_button.setDisabled(True)
410 homescreen_change_button.setToolTip(
411 _("Required package 'PIL' is not available - Please install it.")
412 )
413 homescreen_clear_button.clicked.connect(clear_homescreen)
414 homescreen_msg = QLabel(_("You can set the homescreen on your "
415 "device to personalize it. You must "
416 "choose a {} x {} monochrome black and "
417 "white image.").format(hs_cols, hs_rows))
418 homescreen_msg.setWordWrap(True)
419 settings_glayout.addWidget(homescreen_label, 4, 0)
420 settings_glayout.addWidget(homescreen_change_button, 4, 1)
421 settings_glayout.addWidget(homescreen_clear_button, 4, 2)
422 settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1)
423
424 # Settings tab - Session Timeout
425 timeout_label = QLabel(_("Session Timeout"))
426 timeout_minutes = QLabel()
427 timeout_slider = QSlider(Qt.Horizontal)
428 timeout_slider.setRange(1, 60)
429 timeout_slider.setSingleStep(1)
430 timeout_slider.setTickInterval(5)
431 timeout_slider.setTickPosition(QSlider.TicksBelow)
432 timeout_slider.setTracking(True)
433 timeout_msg = QLabel(
434 _("Clear the session after the specified period "
435 "of inactivity. Once a session has timed out, "
436 "your PIN and passphrase (if enabled) must be "
437 "re-entered to use the device."))
438 timeout_msg.setWordWrap(True)
439 timeout_slider.setSliderPosition(config.get_session_timeout() // 60)
440 slider_moved()
441 timeout_slider.valueChanged.connect(slider_moved)
442 timeout_slider.sliderReleased.connect(slider_released)
443 settings_glayout.addWidget(timeout_label, 6, 0)
444 settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)
445 settings_glayout.addWidget(timeout_minutes, 6, 4)
446 settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)
447 settings_layout.addLayout(settings_glayout)
448 settings_layout.addStretch(1)
449
450 # Advanced tab
451 advanced_tab = QWidget()
452 advanced_layout = QVBoxLayout(advanced_tab)
453 advanced_glayout = QGridLayout()
454
455 # Advanced tab - clear PIN
456 clear_pin_button = QPushButton(_("Disable PIN"))
457 clear_pin_button.clicked.connect(clear_pin)
458 clear_pin_warning = QLabel(
459 _("If you disable your PIN, anyone with physical access to your "
460 "{} device can spend your bitcoins.").format(plugin.device))
461 clear_pin_warning.setWordWrap(True)
462 clear_pin_warning.setStyleSheet("color: red")
463 advanced_glayout.addWidget(clear_pin_button, 0, 2)
464 advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5)
465
466 # Advanced tab - toggle passphrase protection
467 passphrase_button = QPushButton()
468 passphrase_button.clicked.connect(toggle_passphrase)
469 passphrase_msg = WWLabel(PASSPHRASE_HELP)
470 passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
471 passphrase_warning.setStyleSheet("color: red")
472 advanced_glayout.addWidget(passphrase_button, 3, 2)
473 advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5)
474 advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5)
475
476 # Advanced tab - wipe device
477 wipe_device_button = QPushButton(_("Wipe Device"))
478 wipe_device_button.clicked.connect(wipe_device)
479 wipe_device_msg = QLabel(
480 _("Wipe the device, removing all data from it. The firmware "
481 "is left unchanged."))
482 wipe_device_msg.setWordWrap(True)
483 wipe_device_warning = QLabel(
484 _("Only wipe a device if you have the recovery seed written down "
485 "and the device wallet(s) are empty, otherwise the bitcoins "
486 "will be lost forever."))
487 wipe_device_warning.setWordWrap(True)
488 wipe_device_warning.setStyleSheet("color: red")
489 advanced_glayout.addWidget(wipe_device_button, 6, 2)
490 advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5)
491 advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5)
492 advanced_layout.addLayout(advanced_glayout)
493 advanced_layout.addStretch(1)
494
495 tabs = QTabWidget(self)
496 tabs.addTab(info_tab, _("Information"))
497 tabs.addTab(settings_tab, _("Settings"))
498 tabs.addTab(advanced_tab, _("Advanced"))
499 dialog_vbox = QVBoxLayout(self)
500 dialog_vbox.addWidget(tabs)
501 dialog_vbox.addLayout(Buttons(CloseButton(self)))
502
503 # Update information
504 invoke_client(None)