qt.py (13329B)
1 #!/usr/bin/env python 2 # 3 # Electrum - Lightweight Bitcoin Client 4 # Copyright (C) 2015 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 from functools import partial 27 import threading 28 import sys 29 import os 30 from typing import TYPE_CHECKING 31 32 from PyQt5.QtGui import QPixmap 33 from PyQt5.QtCore import QObject, pyqtSignal 34 from PyQt5.QtWidgets import (QTextEdit, QVBoxLayout, QLabel, QGridLayout, QHBoxLayout, 35 QRadioButton, QCheckBox, QLineEdit) 36 37 from electrum.gui.qt.util import (read_QIcon, WindowModalDialog, WaitingDialog, OkButton, 38 CancelButton, Buttons, icon_path, WWLabel, CloseButton) 39 from electrum.gui.qt.qrcodewidget import QRCodeWidget 40 from electrum.gui.qt.amountedit import AmountEdit 41 from electrum.gui.qt.main_window import StatusBarButton 42 from electrum.gui.qt.installwizard import InstallWizard 43 from electrum.i18n import _ 44 from electrum.plugin import hook 45 from electrum.util import is_valid_email 46 from electrum.logging import Logger 47 from electrum.base_wizard import GoBack, UserCancelled 48 49 from .trustedcoin import TrustedCoinPlugin, server 50 51 if TYPE_CHECKING: 52 from electrum.gui.qt.main_window import ElectrumWindow 53 from electrum.wallet import Abstract_Wallet 54 55 56 class TOS(QTextEdit): 57 tos_signal = pyqtSignal() 58 error_signal = pyqtSignal(object) 59 60 61 class HandlerTwoFactor(QObject, Logger): 62 63 def __init__(self, plugin, window): 64 QObject.__init__(self) 65 self.plugin = plugin 66 self.window = window 67 Logger.__init__(self) 68 69 def prompt_user_for_otp(self, wallet, tx, on_success, on_failure): 70 if not isinstance(wallet, self.plugin.wallet_class): 71 return 72 if wallet.can_sign_without_server(): 73 return 74 if not wallet.keystores['x3/'].can_sign(tx, ignore_watching_only=True): 75 self.logger.info("twofactor: xpub3 not needed") 76 return 77 window = self.window.top_level_window() 78 auth_code = self.plugin.auth_dialog(window) 79 WaitingDialog(parent=window, 80 message=_('Waiting for TrustedCoin server to sign transaction...'), 81 task=lambda: wallet.on_otp(tx, auth_code), 82 on_success=lambda *args: on_success(tx), 83 on_error=on_failure) 84 85 86 class Plugin(TrustedCoinPlugin): 87 88 def __init__(self, parent, config, name): 89 super().__init__(parent, config, name) 90 91 @hook 92 def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'): 93 if not isinstance(wallet, self.wallet_class): 94 return 95 wallet.handler_2fa = HandlerTwoFactor(self, window) 96 if wallet.can_sign_without_server(): 97 msg = ' '.join([ 98 _('This wallet was restored from seed, and it contains two master private keys.'), 99 _('Therefore, two-factor authentication is disabled.') 100 ]) 101 action = lambda: window.show_message(msg) 102 else: 103 action = partial(self.settings_dialog, window) 104 button = StatusBarButton(read_QIcon("trustedcoin-status.png"), 105 _("TrustedCoin"), action) 106 window.statusBar().addPermanentWidget(button) 107 self.start_request_thread(window.wallet) 108 109 def auth_dialog(self, window): 110 d = WindowModalDialog(window, _("Authorization")) 111 vbox = QVBoxLayout(d) 112 pw = AmountEdit(None, is_int = True) 113 msg = _('Please enter your Google Authenticator code') 114 vbox.addWidget(QLabel(msg)) 115 grid = QGridLayout() 116 grid.setSpacing(8) 117 grid.addWidget(QLabel(_('Code')), 1, 0) 118 grid.addWidget(pw, 1, 1) 119 vbox.addLayout(grid) 120 msg = _('If you have lost your second factor, you need to restore your wallet from seed in order to request a new code.') 121 label = QLabel(msg) 122 label.setWordWrap(1) 123 vbox.addWidget(label) 124 vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) 125 if not d.exec_(): 126 return 127 return pw.get_amount() 128 129 def prompt_user_for_otp(self, wallet, tx, on_success, on_failure): 130 wallet.handler_2fa.prompt_user_for_otp(wallet, tx, on_success, on_failure) 131 132 def waiting_dialog_for_billing_info(self, window, *, on_finished=None): 133 def task(): 134 return self.request_billing_info(window.wallet, suppress_connection_error=False) 135 def on_error(exc_info): 136 e = exc_info[1] 137 window.show_error("{header}\n{exc}\n\n{tor}" 138 .format(header=_('Error getting TrustedCoin account info.'), 139 exc=repr(e), 140 tor=_('If you keep experiencing network problems, try using a Tor proxy.'))) 141 return WaitingDialog(parent=window, 142 message=_('Requesting account info from TrustedCoin server...'), 143 task=task, 144 on_success=on_finished, 145 on_error=on_error) 146 147 @hook 148 def abort_send(self, window): 149 wallet = window.wallet 150 if not isinstance(wallet, self.wallet_class): 151 return 152 if wallet.can_sign_without_server(): 153 return 154 if wallet.billing_info is None: 155 self.waiting_dialog_for_billing_info(window) 156 return True 157 return False 158 159 def settings_dialog(self, window): 160 self.waiting_dialog_for_billing_info(window, 161 on_finished=partial(self.show_settings_dialog, window)) 162 163 def show_settings_dialog(self, window, success): 164 if not success: 165 window.show_message(_('Server not reachable.')) 166 return 167 168 wallet = window.wallet 169 d = WindowModalDialog(window, _("TrustedCoin Information")) 170 d.setMinimumSize(500, 200) 171 vbox = QVBoxLayout(d) 172 hbox = QHBoxLayout() 173 174 logo = QLabel() 175 logo.setPixmap(QPixmap(icon_path("trustedcoin-status.png"))) 176 msg = _('This wallet is protected by TrustedCoin\'s two-factor authentication.') + '<br/>'\ 177 + _("For more information, visit") + " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>" 178 label = QLabel(msg) 179 label.setOpenExternalLinks(1) 180 181 hbox.addStretch(10) 182 hbox.addWidget(logo) 183 hbox.addStretch(10) 184 hbox.addWidget(label) 185 hbox.addStretch(10) 186 187 vbox.addLayout(hbox) 188 vbox.addStretch(10) 189 190 msg = _('TrustedCoin charges a small fee to co-sign transactions. The fee depends on how many prepaid transactions you buy. An extra output is added to your transaction every time you run out of prepaid transactions.') + '<br/>' 191 label = QLabel(msg) 192 label.setWordWrap(1) 193 vbox.addWidget(label) 194 195 vbox.addStretch(10) 196 grid = QGridLayout() 197 vbox.addLayout(grid) 198 199 price_per_tx = wallet.price_per_tx 200 n_prepay = wallet.num_prepay() 201 i = 0 202 for k, v in sorted(price_per_tx.items()): 203 if k == 1: 204 continue 205 grid.addWidget(QLabel("Pay every %d transactions:"%k), i, 0) 206 grid.addWidget(QLabel(window.format_amount(v/k) + ' ' + window.base_unit() + "/tx"), i, 1) 207 b = QRadioButton() 208 b.setChecked(k == n_prepay) 209 b.clicked.connect(lambda b, k=k: self.config.set_key('trustedcoin_prepay', k, True)) 210 grid.addWidget(b, i, 2) 211 i += 1 212 213 n = wallet.billing_info.get('tx_remaining', 0) 214 grid.addWidget(QLabel(_("Your wallet has {} prepaid transactions.").format(n)), i, 0) 215 vbox.addLayout(Buttons(CloseButton(d))) 216 d.exec_() 217 218 def go_online_dialog(self, wizard: InstallWizard): 219 msg = [ 220 _("Your wallet file is: {}.").format(os.path.abspath(wizard.path)), 221 _("You need to be online in order to complete the creation of " 222 "your wallet. If you generated your seed on an offline " 223 'computer, click on "{}" to close this window, move your ' 224 "wallet file to an online computer, and reopen it with " 225 "Electrum.").format(_('Cancel')), 226 _('If you are online, click on "{}" to continue.').format(_('Next')) 227 ] 228 msg = '\n\n'.join(msg) 229 wizard.reset_stack() 230 try: 231 wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('accept_terms_of_use')) 232 except (GoBack, UserCancelled): 233 # user clicked 'Cancel' and decided to move wallet file manually 234 storage, db = wizard.create_storage(wizard.path) 235 raise 236 237 def accept_terms_of_use(self, window): 238 vbox = QVBoxLayout() 239 vbox.addWidget(QLabel(_("Terms of Service"))) 240 241 tos_e = TOS() 242 tos_e.setReadOnly(True) 243 vbox.addWidget(tos_e) 244 tos_received = False 245 246 vbox.addWidget(QLabel(_("Please enter your e-mail address"))) 247 email_e = QLineEdit() 248 vbox.addWidget(email_e) 249 250 next_button = window.next_button 251 prior_button_text = next_button.text() 252 next_button.setText(_('Accept')) 253 254 def request_TOS(): 255 try: 256 tos = server.get_terms_of_service() 257 except Exception as e: 258 self.logger.exception('Could not retrieve Terms of Service') 259 tos_e.error_signal.emit(_('Could not retrieve Terms of Service:') 260 + '\n' + repr(e)) 261 return 262 self.TOS = tos 263 tos_e.tos_signal.emit() 264 265 def on_result(): 266 tos_e.setText(self.TOS) 267 nonlocal tos_received 268 tos_received = True 269 set_enabled() 270 271 def on_error(msg): 272 window.show_error(str(msg)) 273 window.terminate() 274 275 def set_enabled(): 276 next_button.setEnabled(tos_received and is_valid_email(email_e.text())) 277 278 tos_e.tos_signal.connect(on_result) 279 tos_e.error_signal.connect(on_error) 280 t = threading.Thread(target=request_TOS) 281 t.setDaemon(True) 282 t.start() 283 email_e.textChanged.connect(set_enabled) 284 email_e.setFocus(True) 285 window.exec_layout(vbox, next_enabled=False) 286 next_button.setText(prior_button_text) 287 email = str(email_e.text()) 288 self.create_remote_key(email, window) 289 290 def request_otp_dialog(self, window, short_id, otp_secret, xpub3): 291 vbox = QVBoxLayout() 292 if otp_secret is not None: 293 uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret) 294 l = QLabel("Please scan the following QR code in Google Authenticator. You may as well use the following key: %s"%otp_secret) 295 l.setWordWrap(True) 296 vbox.addWidget(l) 297 qrw = QRCodeWidget(uri) 298 vbox.addWidget(qrw, 1) 299 msg = _('Then, enter your Google Authenticator code:') 300 else: 301 label = QLabel( 302 "This wallet is already registered with TrustedCoin. " 303 "To finalize wallet creation, please enter your Google Authenticator Code. " 304 ) 305 label.setWordWrap(1) 306 vbox.addWidget(label) 307 msg = _('Google Authenticator code:') 308 hbox = QHBoxLayout() 309 hbox.addWidget(WWLabel(msg)) 310 pw = AmountEdit(None, is_int = True) 311 pw.setFocus(True) 312 pw.setMaximumWidth(50) 313 hbox.addWidget(pw) 314 vbox.addLayout(hbox) 315 cb_lost = QCheckBox(_("I have lost my Google Authenticator account")) 316 cb_lost.setToolTip(_("Check this box to request a new secret. You will need to retype your seed.")) 317 vbox.addWidget(cb_lost) 318 cb_lost.setVisible(otp_secret is None) 319 def set_enabled(): 320 b = True if cb_lost.isChecked() else len(pw.text()) == 6 321 window.next_button.setEnabled(b) 322 pw.textChanged.connect(set_enabled) 323 cb_lost.toggled.connect(set_enabled) 324 window.exec_layout(vbox, next_enabled=False, raise_on_cancel=False) 325 self.check_otp(window, short_id, otp_secret, xpub3, pw.get_amount(), cb_lost.isChecked())