You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
651 lines
25 KiB
651 lines
25 KiB
#!/usr/bin/env python |
|
# |
|
# Electrum - Lightweight Bitcoin Client |
|
# Copyright (C) 2015 Thomas Voegtlin |
|
# |
|
# Permission is hereby granted, free of charge, to any person |
|
# obtaining a copy of this software and associated documentation files |
|
# (the "Software"), to deal in the Software without restriction, |
|
# including without limitation the rights to use, copy, modify, merge, |
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
|
# and to permit persons to whom the Software is furnished to do so, |
|
# subject to the following conditions: |
|
# |
|
# The above copyright notice and this permission notice shall be |
|
# included in all copies or substantial portions of the Software. |
|
# |
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
# SOFTWARE. |
|
|
|
from functools import partial |
|
import threading |
|
import os |
|
from typing import TYPE_CHECKING |
|
|
|
from PyQt5.QtGui import QPixmap, QMovie, QColor |
|
from PyQt5.QtCore import QObject, pyqtSignal, QSize, Qt |
|
from PyQt5.QtWidgets import (QTextEdit, QVBoxLayout, QLabel, QGridLayout, QHBoxLayout, |
|
QRadioButton, QCheckBox, QLineEdit, QPushButton, QWidget) |
|
|
|
from electrum.i18n import _ |
|
from electrum.plugin import hook |
|
from electrum.util import is_valid_email |
|
from electrum.logging import Logger, get_logger |
|
from electrum.base_wizard import GoBack, UserCancelled |
|
|
|
from electrum.gui.qt.util import (read_QIcon, WindowModalDialog, WaitingDialog, OkButton, |
|
CancelButton, Buttons, icon_path, WWLabel, CloseButton, ChoicesLayout, ColorScheme) |
|
from electrum.gui.qt.qrcodewidget import QRCodeWidget |
|
from electrum.gui.qt.amountedit import AmountEdit |
|
from electrum.gui.qt.main_window import StatusBarButton |
|
from electrum.gui.qt.installwizard import InstallWizard |
|
from electrum.gui.qt.wizard.wallet import WCCreateSeed, WCConfirmSeed, WCHaveSeed, WCEnterExt, WCConfirmExt |
|
from electrum.gui.qt.wizard.wizard import WizardComponent |
|
|
|
from .qt_common import QSignalObject |
|
from .trustedcoin import TrustedCoinPlugin, server, DISCLAIMER |
|
|
|
|
|
if TYPE_CHECKING: |
|
from electrum.gui.qt.main_window import ElectrumWindow |
|
from electrum.wallet import Abstract_Wallet |
|
from electrum.gui.qt.wizard.wallet import QENewWalletWizard |
|
|
|
|
|
class TOS(QTextEdit): |
|
tos_signal = pyqtSignal() |
|
error_signal = pyqtSignal(object) |
|
|
|
|
|
class HandlerTwoFactor(QObject, Logger): |
|
|
|
def __init__(self, plugin, window): |
|
QObject.__init__(self) |
|
self.plugin = plugin |
|
self.window = window |
|
Logger.__init__(self) |
|
|
|
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure): |
|
if not isinstance(wallet, self.plugin.wallet_class): |
|
return |
|
if wallet.can_sign_without_server(): |
|
return |
|
if not wallet.keystores['x3/'].can_sign(tx, ignore_watching_only=True): |
|
self.logger.info("twofactor: xpub3 not needed") |
|
return |
|
window = self.window.top_level_window() |
|
auth_code = self.plugin.auth_dialog(window) |
|
WaitingDialog(parent=window, |
|
message=_('Waiting for TrustedCoin server to sign transaction...'), |
|
task=lambda: wallet.on_otp(tx, auth_code), |
|
on_success=lambda *args: on_success(tx), |
|
on_error=on_failure) |
|
|
|
|
|
class Plugin(TrustedCoinPlugin): |
|
|
|
def __init__(self, parent, config, name): |
|
super().__init__(parent, config, name) |
|
|
|
@hook |
|
def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'): |
|
if not isinstance(wallet, self.wallet_class): |
|
return |
|
wallet.handler_2fa = HandlerTwoFactor(self, window) |
|
if wallet.can_sign_without_server(): |
|
msg = ' '.join([ |
|
_('This wallet was restored from seed, and it contains two master private keys.'), |
|
_('Therefore, two-factor authentication is disabled.') |
|
]) |
|
action = lambda: window.show_message(msg) |
|
icon = read_QIcon("trustedcoin-status-disabled.png") |
|
else: |
|
action = partial(self.settings_dialog, window) |
|
icon = read_QIcon("trustedcoin-status.png") |
|
sb = window.statusBar() |
|
button = StatusBarButton(icon, _("TrustedCoin"), action, sb.height()) |
|
sb.addPermanentWidget(button) |
|
self.start_request_thread(window.wallet) |
|
|
|
def auth_dialog(self, window): |
|
d = WindowModalDialog(window, _("Authorization")) |
|
vbox = QVBoxLayout(d) |
|
pw = AmountEdit(None, is_int = True) |
|
msg = _('Please enter your Google Authenticator code') |
|
vbox.addWidget(QLabel(msg)) |
|
grid = QGridLayout() |
|
grid.setSpacing(8) |
|
grid.addWidget(QLabel(_('Code')), 1, 0) |
|
grid.addWidget(pw, 1, 1) |
|
vbox.addLayout(grid) |
|
msg = _('If you have lost your second factor, you need to restore your wallet from seed in order to request a new code.') |
|
label = QLabel(msg) |
|
label.setWordWrap(1) |
|
vbox.addWidget(label) |
|
vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) |
|
if not d.exec_(): |
|
return |
|
return pw.get_amount() |
|
|
|
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure): |
|
wallet.handler_2fa.prompt_user_for_otp(wallet, tx, on_success, on_failure) |
|
|
|
def waiting_dialog_for_billing_info(self, window, *, on_finished=None): |
|
def task(): |
|
return self.request_billing_info(window.wallet, suppress_connection_error=False) |
|
def on_error(exc_info): |
|
e = exc_info[1] |
|
window.show_error("{header}\n{exc}\n\n{tor}" |
|
.format(header=_('Error getting TrustedCoin account info.'), |
|
exc=repr(e), |
|
tor=_('If you keep experiencing network problems, try using a Tor proxy.'))) |
|
return WaitingDialog(parent=window, |
|
message=_('Requesting account info from TrustedCoin server...'), |
|
task=task, |
|
on_success=on_finished, |
|
on_error=on_error) |
|
|
|
@hook |
|
def abort_send(self, window): |
|
wallet = window.wallet |
|
if not isinstance(wallet, self.wallet_class): |
|
return |
|
if wallet.can_sign_without_server(): |
|
return |
|
if wallet.billing_info is None: |
|
self.waiting_dialog_for_billing_info(window) |
|
return True |
|
return False |
|
|
|
def settings_dialog(self, window): |
|
self.waiting_dialog_for_billing_info(window, |
|
on_finished=partial(self.show_settings_dialog, window)) |
|
|
|
def show_settings_dialog(self, window, success): |
|
if not success: |
|
window.show_message(_('Server not reachable.')) |
|
return |
|
|
|
wallet = window.wallet |
|
d = WindowModalDialog(window, _("TrustedCoin Information")) |
|
d.setMinimumSize(500, 200) |
|
vbox = QVBoxLayout(d) |
|
hbox = QHBoxLayout() |
|
|
|
logo = QLabel() |
|
logo.setPixmap(QPixmap(icon_path("trustedcoin-status.png"))) |
|
msg = _('This wallet is protected by TrustedCoin\'s two-factor authentication.') + '<br/>'\ |
|
+ _("For more information, visit") + " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>" |
|
label = QLabel(msg) |
|
label.setOpenExternalLinks(1) |
|
|
|
hbox.addStretch(10) |
|
hbox.addWidget(logo) |
|
hbox.addStretch(10) |
|
hbox.addWidget(label) |
|
hbox.addStretch(10) |
|
|
|
vbox.addLayout(hbox) |
|
vbox.addStretch(10) |
|
|
|
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/>' |
|
label = QLabel(msg) |
|
label.setWordWrap(1) |
|
vbox.addWidget(label) |
|
|
|
vbox.addStretch(10) |
|
grid = QGridLayout() |
|
vbox.addLayout(grid) |
|
|
|
price_per_tx = wallet.price_per_tx |
|
n_prepay = wallet.num_prepay() |
|
i = 0 |
|
for k, v in sorted(price_per_tx.items()): |
|
if k == 1: |
|
continue |
|
grid.addWidget(QLabel("Pay every %d transactions:"%k), i, 0) |
|
grid.addWidget(QLabel(window.format_amount(v/k) + ' ' + window.base_unit() + "/tx"), i, 1) |
|
b = QRadioButton() |
|
b.setChecked(k == n_prepay) |
|
def on_click(b, k): |
|
self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY = k |
|
b.clicked.connect(partial(on_click, k=k)) |
|
grid.addWidget(b, i, 2) |
|
i += 1 |
|
|
|
n = wallet.billing_info.get('tx_remaining', 0) |
|
grid.addWidget(QLabel(_("Your wallet has {} prepaid transactions.").format(n)), i, 0) |
|
vbox.addLayout(Buttons(CloseButton(d))) |
|
d.exec_() |
|
|
|
def go_online_dialog(self, wizard: InstallWizard): |
|
msg = [ |
|
_("Your wallet file is: {}.").format(os.path.abspath(wizard.path)), |
|
_("You need to be online in order to complete the creation of " |
|
"your wallet. If you generated your seed on an offline " |
|
'computer, click on "{}" to close this window, move your ' |
|
"wallet file to an online computer, and reopen it with " |
|
"Electrum.").format(_('Cancel')), |
|
_('If you are online, click on "{}" to continue.').format(_('Next')) |
|
] |
|
msg = '\n\n'.join(msg) |
|
wizard.reset_stack() |
|
try: |
|
wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('accept_terms_of_use')) |
|
except (GoBack, UserCancelled): |
|
# user clicked 'Cancel' and decided to move wallet file manually |
|
storage, db = wizard.create_storage(wizard.path) |
|
raise |
|
|
|
def accept_terms_of_use(self, window): |
|
vbox = QVBoxLayout() |
|
vbox.addWidget(QLabel(_("Terms of Service"))) |
|
|
|
tos_e = TOS() |
|
tos_e.setReadOnly(True) |
|
vbox.addWidget(tos_e) |
|
tos_received = False |
|
|
|
vbox.addWidget(QLabel(_("Please enter your e-mail address"))) |
|
email_e = QLineEdit() |
|
vbox.addWidget(email_e) |
|
|
|
next_button = window.next_button |
|
prior_button_text = next_button.text() |
|
next_button.setText(_('Accept')) |
|
|
|
def request_TOS(): |
|
try: |
|
tos = server.get_terms_of_service() |
|
except Exception as e: |
|
self.logger.exception('Could not retrieve Terms of Service') |
|
tos_e.error_signal.emit(_('Could not retrieve Terms of Service:') |
|
+ '\n' + repr(e)) |
|
return |
|
self.TOS = tos |
|
tos_e.tos_signal.emit() |
|
|
|
def on_result(): |
|
tos_e.setText(self.TOS) |
|
nonlocal tos_received |
|
tos_received = True |
|
set_enabled() |
|
|
|
def on_error(msg): |
|
window.show_error(str(msg)) |
|
window.terminate() |
|
|
|
def set_enabled(): |
|
next_button.setEnabled(tos_received and is_valid_email(email_e.text())) |
|
|
|
tos_e.tos_signal.connect(on_result) |
|
tos_e.error_signal.connect(on_error) |
|
t = threading.Thread(target=request_TOS) |
|
t.daemon = True |
|
t.start() |
|
email_e.textChanged.connect(set_enabled) |
|
email_e.setFocus(True) |
|
window.exec_layout(vbox, next_enabled=False) |
|
next_button.setText(prior_button_text) |
|
email = str(email_e.text()) |
|
self.create_remote_key(email, window) |
|
|
|
def request_otp_dialog(self, window, short_id, otp_secret, xpub3): |
|
vbox = QVBoxLayout() |
|
if otp_secret is not None: |
|
uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret) |
|
l = QLabel("Please scan the following QR code in Google Authenticator. You may as well use the following key: %s"%otp_secret) |
|
l.setWordWrap(True) |
|
vbox.addWidget(l) |
|
qrw = QRCodeWidget(uri) |
|
vbox.addWidget(qrw, 1) |
|
msg = _('Then, enter your Google Authenticator code:') |
|
else: |
|
label = QLabel( |
|
"This wallet is already registered with TrustedCoin. " |
|
"To finalize wallet creation, please enter your Google Authenticator Code. " |
|
) |
|
label.setWordWrap(1) |
|
vbox.addWidget(label) |
|
msg = _('Google Authenticator code:') |
|
hbox = QHBoxLayout() |
|
hbox.addWidget(WWLabel(msg)) |
|
pw = AmountEdit(None, is_int = True) |
|
pw.setFocus(True) |
|
pw.setMaximumWidth(50) |
|
hbox.addWidget(pw) |
|
vbox.addLayout(hbox) |
|
cb_lost = QCheckBox(_("I have lost my Google Authenticator account")) |
|
cb_lost.setToolTip(_("Check this box to request a new secret. You will need to retype your seed.")) |
|
vbox.addWidget(cb_lost) |
|
cb_lost.setVisible(otp_secret is None) |
|
def set_enabled(): |
|
b = True if cb_lost.isChecked() else len(pw.text()) == 6 |
|
window.next_button.setEnabled(b) |
|
pw.textChanged.connect(set_enabled) |
|
cb_lost.toggled.connect(set_enabled) |
|
window.exec_layout(vbox, next_enabled=False, raise_on_cancel=False) |
|
self.check_otp(window, short_id, otp_secret, xpub3, pw.get_amount(), cb_lost.isChecked()) |
|
|
|
@hook |
|
def init_wallet_wizard(self, wizard: 'QENewWalletWizard'): |
|
# FIXME: self.so is currently scoped to plugin, which is shared among wizards. This is wrong |
|
# refactor to be a member of the wizard instance |
|
self.so = QSignalObject(self, wizard, None) |
|
self.extend_wizard(wizard) |
|
self._wizard = wizard |
|
|
|
def extend_wizard(self, wizard: 'QENewWalletWizard'): |
|
super().extend_wizard(wizard) |
|
views = { |
|
'trustedcoin_start': { |
|
'gui': WCDisclaimer, |
|
'params': {'icon': icon_path('trustedcoin-wizard.png')}, |
|
}, |
|
'trustedcoin_choose_seed': { |
|
'gui': WCChooseSeed, |
|
'params': {'icon': icon_path('trustedcoin-wizard.png')}, |
|
}, |
|
'trustedcoin_create_seed': { |
|
'gui': WCCreateSeed, |
|
'params': {'icon': icon_path('trustedcoin-wizard.png')}, |
|
}, |
|
'trustedcoin_confirm_seed': { |
|
'gui': WCConfirmSeed, |
|
'params': {'icon': icon_path('trustedcoin-wizard.png')}, |
|
}, |
|
'trustedcoin_have_seed': { |
|
'gui': WCHaveSeed, |
|
'params': {'icon': icon_path('trustedcoin-wizard.png')}, |
|
}, |
|
'trustedcoin_keep_disable': { |
|
'gui': WCKeepDisable, |
|
'params': {'icon': icon_path('trustedcoin-wizard.png')}, |
|
}, |
|
'trustedcoin_tos_email': { |
|
'gui': WCTerms, |
|
'params': {'icon': icon_path('trustedcoin-wizard.png')}, |
|
}, |
|
'trustedcoin_show_confirm_otp': { |
|
'gui': WCShowConfirmOTP, |
|
'params': {'icon': icon_path('trustedcoin-wizard.png')}, |
|
} |
|
} |
|
wizard.navmap_merge(views) |
|
|
|
# modify default flow, insert seed extension entry/confirm as separate views |
|
ext = { |
|
'trustedcoin_create_seed': { |
|
'next': lambda d: 'trustedcoin_create_ext' if wizard.wants_ext(d) else 'trustedcoin_confirm_seed' |
|
}, |
|
'trustedcoin_create_ext': { |
|
'gui': WCEnterExt, |
|
'params': {'icon': icon_path('trustedcoin-wizard.png')}, |
|
'next': 'trustedcoin_confirm_seed', |
|
}, |
|
'trustedcoin_confirm_seed': { |
|
'next': lambda d: 'trustedcoin_confirm_ext' if wizard.wants_ext(d) else 'trustedcoin_tos_email' |
|
}, |
|
'trustedcoin_confirm_ext': { |
|
'gui': WCConfirmExt, |
|
'params': {'icon': icon_path('trustedcoin-wizard.png')}, |
|
'next': 'trustedcoin_tos_email', |
|
}, |
|
'trustedcoin_have_seed': { |
|
'next': lambda d: 'trustedcoin_have_ext' if wizard.wants_ext(d) else 'trustedcoin_keep_disable' |
|
}, |
|
'trustedcoin_have_ext': { |
|
'gui': WCEnterExt, |
|
'params': {'icon': icon_path('trustedcoin-wizard.png')}, |
|
'next': 'trustedcoin_keep_disable', |
|
}, |
|
} |
|
wizard.navmap_merge(ext) |
|
|
|
|
|
class WCDisclaimer(WizardComponent): |
|
def __init__(self, parent, wizard): |
|
WizardComponent.__init__(self, parent, wizard, title=_('Disclaimer')) |
|
|
|
self.layout().addWidget(WWLabel('\n\n'.join(DISCLAIMER))) |
|
self.layout().addStretch(1) |
|
|
|
self._valid = True |
|
|
|
def apply(self): |
|
pass |
|
|
|
|
|
class WCChooseSeed(WizardComponent): |
|
def __init__(self, parent, wizard): |
|
WizardComponent.__init__(self, parent, wizard, title=_('Create or restore')) |
|
message = _('Do you want to create a new seed, or restore a wallet using an existing seed?') |
|
choices = [ |
|
('createseed', _('Create a new seed')), |
|
('haveseed', _('I already have a seed')), |
|
] |
|
|
|
self.c_values = [x[0] for x in choices] |
|
c_titles = [x[1] for x in choices] |
|
self.clayout = ChoicesLayout(message, c_titles) |
|
self.layout().addLayout(self.clayout.layout()) |
|
self.layout().addStretch(1) |
|
|
|
self._valid = True |
|
|
|
def apply(self): |
|
self.wizard_data['keystore_type'] = self.c_values[self.clayout.selected_index()] |
|
|
|
|
|
class WCTerms(WizardComponent): |
|
def __init__(self, parent, wizard): |
|
WizardComponent.__init__(self, parent, wizard, title=_('Terms and conditions')) |
|
self.plugin = wizard.plugins.get_plugin('trustedcoin') |
|
self._has_tos = False |
|
|
|
def on_ready(self): |
|
self.tos_e = TOS() |
|
self.tos_e.setReadOnly(True) |
|
self.layout().addWidget(self.tos_e) |
|
|
|
self.layout().addWidget(QLabel(_("Please enter your e-mail address"))) |
|
self.email_e = QLineEdit() |
|
self.email_e.textChanged.connect(self.validate) |
|
self.layout().addWidget(self.email_e) |
|
|
|
self.fetch_terms_and_conditions() |
|
|
|
def fetch_terms_and_conditions(self): |
|
self.plugin.so.busyChanged.connect(self.on_busy_changed) |
|
self.plugin.so.termsAndConditionsRetrieved.connect(self.on_terms_retrieved) |
|
self.plugin.so.termsAndConditionsError.connect(self.on_terms_error) |
|
self.plugin.so.fetchTermsAndConditions() |
|
|
|
def on_busy_changed(self): |
|
self.busy = self.plugin.so.busy |
|
|
|
def on_terms_retrieved(self, tos: str) -> None: |
|
self._has_tos = True |
|
self.tos_e.setText(tos) |
|
self.email_e.setFocus(True) |
|
self.validate() |
|
|
|
def on_terms_error(self, error: str) -> None: |
|
self.error = error |
|
|
|
def validate(self): |
|
if self._has_tos and self.email_e.text() != '': |
|
self.valid = True |
|
else: |
|
self.valid = False |
|
|
|
def apply(self): |
|
self.wizard_data['2fa_email'] = self.email_e.text() |
|
|
|
|
|
class WCShowConfirmOTP(WizardComponent): |
|
_logger = get_logger(__name__) |
|
|
|
def __init__(self, parent, wizard): |
|
WizardComponent.__init__(self, parent, wizard, title=_('Authenticator secret')) |
|
self.plugin = wizard.plugins.get_plugin('trustedcoin') |
|
self._otp_verified = False |
|
|
|
self.new_otp = QWidget() |
|
new_otp_layout = QVBoxLayout() |
|
scanlabel = WWLabel(_('Enter or scan into authenticator app. Then authenticate below')) |
|
new_otp_layout.addWidget(scanlabel) |
|
self.qr = QRCodeWidget('') |
|
new_otp_layout.addWidget(self.qr) |
|
self.secretlabel = WWLabel() |
|
new_otp_layout.addWidget(self.secretlabel) |
|
self.new_otp.setLayout(new_otp_layout) |
|
|
|
self.exist_otp = QWidget() |
|
exist_otp_layout = QVBoxLayout() |
|
knownlabel = WWLabel(_('This wallet is already registered with TrustedCoin.')) |
|
exist_otp_layout.addWidget(knownlabel) |
|
knownsecretlabel = WWLabel(_('If you still have your OTP secret, then authenticate below to finalize wallet creation')) |
|
exist_otp_layout.addWidget(knownsecretlabel) |
|
self.exist_otp.setLayout(exist_otp_layout) |
|
|
|
self.authlabelnew = WWLabel(_('Then, enter your Google Authenticator code:')) |
|
self.authlabelexist = WWLabel(_('Google Authenticator code:')) |
|
|
|
self.spinner = QMovie(icon_path('spinner.gif')) |
|
self.spinner.setScaledSize(QSize(24, 24)) |
|
self.spinner.setBackgroundColor(QColor('black')) |
|
self.spinner_l = QLabel() |
|
self.spinner_l.setMargin(5) |
|
self.spinner_l.setVisible(False) |
|
self.spinner_l.setMovie(self.spinner) |
|
|
|
self.otp_status_l = QLabel() |
|
self.otp_status_l.setAlignment(Qt.AlignHCenter) |
|
self.otp_status_l.setVisible(False) |
|
|
|
self.resetlabel = WWLabel(_('If you have lost your OTP secret, click the button below to request a new secret from the server.')) |
|
self.button = QPushButton('Request OTP secret') |
|
self.button.clicked.connect(self.on_request_otp) |
|
|
|
hbox = QHBoxLayout() |
|
hbox.addWidget(self.authlabelnew) |
|
hbox.addWidget(self.authlabelexist) |
|
hbox.addStretch(1) |
|
hbox.addWidget(self.spinner_l) |
|
self.otp_e = AmountEdit(None, is_int=True) |
|
self.otp_e.setFocus(True) |
|
self.otp_e.setMaximumWidth(150) |
|
self.otp_e.textEdited.connect(self.on_otp_edited) |
|
hbox.addWidget(self.otp_e) |
|
|
|
self.layout().addWidget(self.new_otp) |
|
self.layout().addWidget(self.exist_otp) |
|
self.layout().addLayout(hbox) |
|
self.layout().addWidget(self.otp_status_l) |
|
self.layout().addWidget(self.resetlabel) |
|
self.layout().addWidget(self.button) |
|
self.layout().addStretch(1) |
|
|
|
def on_ready(self): |
|
self.plugin.so.busyChanged.connect(self.on_busy_changed) |
|
self.plugin.so.remoteKeyError.connect(self.on_remote_key_error) |
|
self.plugin.so.otpSuccess.connect(self.on_otp_success) |
|
self.plugin.so.otpError.connect(self.on_otp_error) |
|
self.plugin.so.remoteKeyError.connect(self.on_remote_key_error) |
|
|
|
self.plugin.so.createKeystore(self.wizard_data['2fa_email']) |
|
|
|
def update(self): |
|
is_new = bool(self.plugin.so.remoteKeyState != 'wallet_known') |
|
self.new_otp.setVisible(is_new) |
|
self.exist_otp.setVisible(not is_new) |
|
self.authlabelnew.setVisible(is_new) |
|
self.authlabelexist.setVisible(not is_new) |
|
self.resetlabel.setVisible(not is_new and not self._otp_verified) |
|
self.button.setVisible(not is_new and not self._otp_verified) |
|
|
|
if self.plugin.so.otpSecret: |
|
self.secretlabel.setText(self.plugin.so.otpSecret) |
|
uri = 'otpauth://totp/Electrum 2FA %s?secret=%s&digits=6' % ( |
|
self.wizard_data['wallet_name'], self.plugin.so.otpSecret) |
|
self.qr.setData(uri) |
|
|
|
def on_busy_changed(self): |
|
if not self.plugin.so._verifyingOtp: |
|
self.busy = self.plugin.so.busy |
|
if not self.busy: |
|
self.update() |
|
|
|
def on_remote_key_error(self, text): |
|
self._logger.error(text) |
|
self.error = text |
|
|
|
def on_request_otp(self): |
|
self.otp_status_l.setVisible(False) |
|
self.plugin.so.resetOtpSecret() |
|
self.update() |
|
|
|
def on_otp_success(self): |
|
self._otp_verified = True |
|
self.otp_status_l.setText('Valid!') |
|
self.otp_status_l.setVisible(True) |
|
self.otp_status_l.setStyleSheet(ColorScheme.GREEN.as_stylesheet(False)) |
|
self.setEnabled(True) |
|
self.spinner_l.setVisible(False) |
|
self.spinner.stop() |
|
|
|
self.valid = True |
|
|
|
def on_otp_error(self, message): |
|
self.otp_status_l.setText(message) |
|
self.otp_status_l.setVisible(True) |
|
self.otp_status_l.setStyleSheet(ColorScheme.RED.as_stylesheet(False)) |
|
self.setEnabled(True) |
|
self.spinner_l.setVisible(False) |
|
self.spinner.stop() |
|
|
|
def on_otp_edited(self): |
|
self.otp_status_l.setVisible(False) |
|
text = self.otp_e.text() |
|
if len(text) == 6: |
|
# verify otp |
|
self.plugin.so.checkOtp(self.plugin.so.shortId, int(text)) |
|
self.setEnabled(False) |
|
self.spinner_l.setVisible(True) |
|
self.spinner.start() |
|
self.otp_e.setText('') |
|
|
|
def apply(self): |
|
pass |
|
|
|
|
|
class WCKeepDisable(WizardComponent): |
|
def __init__(self, parent, wizard): |
|
WizardComponent.__init__(self, parent, wizard, title=_('Restore 2FA wallet')) |
|
message = ' '.join([ |
|
'You are going to restore a wallet protected with two-factor authentication.', |
|
'Do you want to keep using two-factor authentication with this wallet,', |
|
'or do you want to disable it, and have two master private keys in your wallet?' |
|
]) |
|
choices = [ |
|
('keep', _('Keep')), |
|
('disable', _('Disable')), |
|
] |
|
|
|
self.c_values = [x[0] for x in choices] |
|
c_titles = [x[1] for x in choices] |
|
self.clayout = ChoicesLayout(message, c_titles) |
|
self.layout().addLayout(self.clayout.layout()) |
|
self.layout().addStretch(1) |
|
|
|
self._valid = True |
|
|
|
def apply(self): |
|
self.wizard_data['trustedcoin_keepordisable'] = self.c_values[self.clayout.selected_index()]
|
|
|