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