22 changed files with 874 additions and 217 deletions
@ -0,0 +1,160 @@ |
|||||||
|
import os |
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject |
||||||
|
from PyQt5.QtQml import QQmlApplicationEngine |
||||||
|
|
||||||
|
from electrum.logging import get_logger |
||||||
|
from electrum.gui.wizard import NewWalletWizard |
||||||
|
|
||||||
|
from electrum.storage import WalletStorage, StorageEncryptionVersion |
||||||
|
from electrum.wallet_db import WalletDB |
||||||
|
from electrum.bip32 import normalize_bip32_derivation, xpub_type |
||||||
|
from electrum import keystore |
||||||
|
|
||||||
|
class QEAbstractWizard(QObject): |
||||||
|
_logger = get_logger(__name__) |
||||||
|
|
||||||
|
def __init__(self, parent = None): |
||||||
|
QObject.__init__(self, parent) |
||||||
|
|
||||||
|
@pyqtSlot(result=str) |
||||||
|
def start_wizard(self): |
||||||
|
self.start() |
||||||
|
return self._current.view |
||||||
|
|
||||||
|
@pyqtSlot(str, result=str) |
||||||
|
def viewToComponent(self, view): |
||||||
|
return self.navmap[view]['gui'] + '.qml' |
||||||
|
|
||||||
|
@pyqtSlot('QJSValue', result='QVariant') |
||||||
|
def submit(self, wizard_data): |
||||||
|
wdata = wizard_data.toVariant() |
||||||
|
self._logger.debug(str(wdata)) |
||||||
|
view = self.resolve_next(self._current.view, wdata) |
||||||
|
return { 'view': view.view, 'wizard_data': view.wizard_data } |
||||||
|
|
||||||
|
@pyqtSlot(result='QVariant') |
||||||
|
def prev(self): |
||||||
|
viewstate = self.resolve_prev() |
||||||
|
return viewstate.wizard_data |
||||||
|
|
||||||
|
@pyqtSlot('QJSValue', result=bool) |
||||||
|
def isLast(self, wizard_data): |
||||||
|
wdata = wizard_data.toVariant() |
||||||
|
return self.is_last_view(self._current.view, wdata) |
||||||
|
|
||||||
|
|
||||||
|
class QENewWalletWizard(NewWalletWizard, QEAbstractWizard): |
||||||
|
|
||||||
|
createError = pyqtSignal([str], arguments=["error"]) |
||||||
|
createSuccess = pyqtSignal() |
||||||
|
|
||||||
|
def __init__(self, daemon, parent = None): |
||||||
|
NewWalletWizard.__init__(self, daemon) |
||||||
|
QEAbstractWizard.__init__(self, parent) |
||||||
|
self._daemon = daemon |
||||||
|
|
||||||
|
# attach view names |
||||||
|
self.navmap_merge({ |
||||||
|
'wallet_name': { 'gui': 'WCWalletName' }, |
||||||
|
'wallet_type': { 'gui': 'WCWalletType' }, |
||||||
|
'keystore_type': { 'gui': 'WCKeystoreType' }, |
||||||
|
'create_seed': { 'gui': 'WCCreateSeed' }, |
||||||
|
'confirm_seed': { 'gui': 'WCConfirmSeed' }, |
||||||
|
'have_seed': { 'gui': 'WCHaveSeed' }, |
||||||
|
'bip39_refine': { 'gui': 'WCBIP39Refine' }, |
||||||
|
'have_master_key': { 'gui': 'WCHaveMasterKey' }, |
||||||
|
'wallet_password': { 'gui': 'WCWalletPassword' } |
||||||
|
}) |
||||||
|
|
||||||
|
pathChanged = pyqtSignal() |
||||||
|
@pyqtProperty(str, notify=pathChanged) |
||||||
|
def path(self): |
||||||
|
return self._path |
||||||
|
|
||||||
|
@path.setter |
||||||
|
def path(self, path): |
||||||
|
self._path = path |
||||||
|
self.pathChanged.emit() |
||||||
|
|
||||||
|
def last_if_single_password(self, view, wizard_data): |
||||||
|
return self._daemon.singlePasswordEnabled |
||||||
|
|
||||||
|
@pyqtSlot('QJSValue',bool,str) |
||||||
|
def create_storage(self, js_data, single_password_enabled, single_password): |
||||||
|
self._logger.info('Creating wallet from wizard data') |
||||||
|
data = js_data.toVariant() |
||||||
|
self._logger.debug(str(data)) |
||||||
|
|
||||||
|
# only standard and 2fa wallets for now |
||||||
|
assert data['wallet_type'] in ['standard', '2fa'] |
||||||
|
|
||||||
|
if single_password_enabled and single_password: |
||||||
|
data['encrypt'] = True |
||||||
|
data['password'] = single_password |
||||||
|
|
||||||
|
try: |
||||||
|
path = os.path.join(os.path.dirname(self._daemon.daemon.config.get_wallet_path()), data['wallet_name']) |
||||||
|
if os.path.exists(path): |
||||||
|
raise Exception('file already exists at path') |
||||||
|
storage = WalletStorage(path) |
||||||
|
|
||||||
|
if data['keystore_type'] in ['createseed', 'haveseed']: |
||||||
|
if data['seed_type'] in ['old', 'standard', 'segwit']: #2fa, 2fa-segwit |
||||||
|
self._logger.debug('creating keystore from electrum seed') |
||||||
|
k = keystore.from_seed(data['seed'], data['seed_extra_words'], data['wallet_type'] == 'multisig') |
||||||
|
elif data['seed_type'] == 'bip39': |
||||||
|
self._logger.debug('creating keystore from bip39 seed') |
||||||
|
root_seed = keystore.bip39_to_seed(data['seed'], data['seed_extra_words']) |
||||||
|
derivation = normalize_bip32_derivation(data['derivation_path']) |
||||||
|
script = data['script_type'] if data['script_type'] != 'p2pkh' else 'standard' |
||||||
|
k = keystore.from_bip43_rootseed(root_seed, derivation, xtype=script) |
||||||
|
elif data['seed_type'] == '2fa_segwit': # TODO: legacy 2fa |
||||||
|
self._logger.debug('creating keystore from 2fa seed') |
||||||
|
k = keystore.from_xprv(data['x1/']['xprv']) |
||||||
|
else: |
||||||
|
raise Exception('unsupported/unknown seed_type %s' % data['seed_type']) |
||||||
|
elif data['keystore_type'] == 'masterkey': |
||||||
|
k = keystore.from_master_key(data['master_key']) |
||||||
|
has_xpub = isinstance(k, keystore.Xpub) |
||||||
|
assert has_xpub |
||||||
|
t1 = xpub_type(k.xpub) |
||||||
|
if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: |
||||||
|
raise Exception('wrong key type %s' % t1) |
||||||
|
else: |
||||||
|
raise Exception('unsupported/unknown keystore_type %s' % data['keystore_type']) |
||||||
|
|
||||||
|
if data['encrypt']: |
||||||
|
if k.may_have_password(): |
||||||
|
k.update_password(None, data['password']) |
||||||
|
storage.set_password(data['password'], enc_version=StorageEncryptionVersion.USER_PASSWORD) |
||||||
|
|
||||||
|
db = WalletDB('', manual_upgrades=False) |
||||||
|
db.set_keystore_encryption(bool(data['password']) and data['encrypt']) |
||||||
|
|
||||||
|
db.put('wallet_type', data['wallet_type']) |
||||||
|
if 'seed_type' in data: |
||||||
|
db.put('seed_type', data['seed_type']) |
||||||
|
|
||||||
|
if data['wallet_type'] == 'standard': |
||||||
|
db.put('keystore', k.dump()) |
||||||
|
elif data['wallet_type'] == '2fa': |
||||||
|
db.put('x1/', k.dump()) |
||||||
|
db.put('x2/', data['x2/']) |
||||||
|
db.put('x3/', data['x3/']) |
||||||
|
db.put('use_trustedcoin', True) |
||||||
|
|
||||||
|
if k.can_have_deterministic_lightning_xprv(): |
||||||
|
db.put('lightning_xprv', k.get_lightning_xprv(data['password'] if data['encrypt'] else None)) |
||||||
|
|
||||||
|
db.load_plugins() |
||||||
|
db.write(storage) |
||||||
|
|
||||||
|
# minimally populate self after create |
||||||
|
self._password = data['password'] |
||||||
|
self.path = path |
||||||
|
|
||||||
|
self.createSuccess.emit() |
||||||
|
except Exception as e: |
||||||
|
self._logger.error(repr(e)) |
||||||
|
self.createError.emit(str(e)) |
||||||
@ -0,0 +1,332 @@ |
|||||||
|
import threading |
||||||
|
import socket |
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot |
||||||
|
|
||||||
|
from electrum.i18n import _ |
||||||
|
from electrum.plugin import hook |
||||||
|
from electrum.bip32 import xpub_type |
||||||
|
from electrum import keystore |
||||||
|
|
||||||
|
from electrum.gui.qml.qewallet import QEWallet |
||||||
|
from electrum.gui.qml.plugins import PluginQObject |
||||||
|
|
||||||
|
from .trustedcoin import (TrustedCoinPlugin, server, ErrorConnectingServer, |
||||||
|
MOBILE_DISCLAIMER, get_user_id, get_signing_xpub, |
||||||
|
TrustedCoinException, make_xpub) |
||||||
|
|
||||||
|
class Plugin(TrustedCoinPlugin): |
||||||
|
|
||||||
|
class QSignalObject(PluginQObject): |
||||||
|
requestView = pyqtSignal([str], arguments=['component']) |
||||||
|
|
||||||
|
canSignWithoutServerChanged = pyqtSignal() |
||||||
|
_canSignWithoutServer = False |
||||||
|
termsAndConditionsChanged = pyqtSignal() |
||||||
|
_termsAndConditions = '' |
||||||
|
termsAndConditionsErrorChanged = pyqtSignal() |
||||||
|
_termsAndConditionsError = '' |
||||||
|
createRemoteKeyErrorChanged = pyqtSignal() |
||||||
|
_createRemoteKeyError = '' |
||||||
|
otpError = pyqtSignal() |
||||||
|
otpSuccess = pyqtSignal() |
||||||
|
disclaimerChanged = pyqtSignal() |
||||||
|
keystoreChanged = pyqtSignal() |
||||||
|
otpSecretChanged = pyqtSignal() |
||||||
|
_otpSecret = '' |
||||||
|
shortIdChanged = pyqtSignal() |
||||||
|
_shortId = '' |
||||||
|
|
||||||
|
def __init__(self, plugin, parent): |
||||||
|
super().__init__(plugin, parent) |
||||||
|
|
||||||
|
@pyqtSlot(result=str) |
||||||
|
def settingsComponent(self): return '../../../plugins/trustedcoin/qml/Settings.qml' |
||||||
|
|
||||||
|
@pyqtProperty(str, notify=disclaimerChanged) |
||||||
|
def disclaimer(self): |
||||||
|
return '\n\n'.join(MOBILE_DISCLAIMER) |
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=canSignWithoutServerChanged) |
||||||
|
def canSignWithoutServer(self): |
||||||
|
return self._canSignWithoutServer |
||||||
|
|
||||||
|
@pyqtProperty('QVariantMap', notify=keystoreChanged) |
||||||
|
def keystore(self): |
||||||
|
return self._keystore |
||||||
|
|
||||||
|
@pyqtProperty(str, notify=otpSecretChanged) |
||||||
|
def otpSecret(self): |
||||||
|
return self._otpSecret |
||||||
|
|
||||||
|
@pyqtProperty(str, notify=shortIdChanged) |
||||||
|
def shortId(self): |
||||||
|
return self._shortId |
||||||
|
|
||||||
|
@pyqtSlot(str) |
||||||
|
def otpSubmit(self, otp): |
||||||
|
self._plugin.on_otp(otp) |
||||||
|
|
||||||
|
@pyqtProperty(str, notify=termsAndConditionsChanged) |
||||||
|
def termsAndConditions(self): |
||||||
|
return self._termsAndConditions |
||||||
|
|
||||||
|
@pyqtProperty(str, notify=termsAndConditionsErrorChanged) |
||||||
|
def termsAndConditionsError(self): |
||||||
|
return self._termsAndConditionsError |
||||||
|
|
||||||
|
@pyqtProperty(str, notify=createRemoteKeyErrorChanged) |
||||||
|
def createRemoteKeyError(self): |
||||||
|
return self._createRemoteKeyError |
||||||
|
|
||||||
|
@pyqtSlot() |
||||||
|
def fetchTermsAndConditions(self): |
||||||
|
def fetch_task(): |
||||||
|
try: |
||||||
|
self.plugin.logger.debug('TOS') |
||||||
|
tos = server.get_terms_of_service() |
||||||
|
except ErrorConnectingServer as e: |
||||||
|
self._termsAndConditionsError = _('Error connecting to server') |
||||||
|
self.termsAndConditionsErrorChanged.emit() |
||||||
|
except Exception as e: |
||||||
|
self._termsAndConditionsError = '%s: %s' % (_('Error'), repr(e)) |
||||||
|
self.termsAndConditionsErrorChanged.emit() |
||||||
|
else: |
||||||
|
self._termsAndConditions = tos |
||||||
|
self.termsAndConditionsChanged.emit() |
||||||
|
finally: |
||||||
|
self._busy = False |
||||||
|
self.busyChanged.emit() |
||||||
|
|
||||||
|
self._busy = True |
||||||
|
self.busyChanged.emit() |
||||||
|
t = threading.Thread(target=fetch_task) |
||||||
|
t.daemon = True |
||||||
|
t.start() |
||||||
|
|
||||||
|
@pyqtSlot(str) |
||||||
|
def createKeystore(self, email): |
||||||
|
xprv1, xpub1, xpub2, xpub3, short_id = self.plugin.create_keys() |
||||||
|
def create_remote_key_task(): |
||||||
|
try: |
||||||
|
self.plugin.logger.debug('create remote key') |
||||||
|
r = server.create(xpub1, xpub2, email) |
||||||
|
|
||||||
|
otp_secret = r['otp_secret'] |
||||||
|
_xpub3 = r['xpubkey_cosigner'] |
||||||
|
_id = r['id'] |
||||||
|
except (socket.error, ErrorConnectingServer): |
||||||
|
self._createRemoteKeyError = _('Error creating key') |
||||||
|
self.createRemoteKeyErrorChanged.emit() |
||||||
|
except TrustedCoinException as e: |
||||||
|
# if e.status_code == 409: TODO ? |
||||||
|
# r = None |
||||||
|
self._createRemoteKeyError = str(e) |
||||||
|
self.createRemoteKeyErrorChanged.emit() |
||||||
|
except (KeyError,TypeError) as e: # catch any assumptions |
||||||
|
self._createRemoteKeyError = str(e) |
||||||
|
self.createRemoteKeyErrorChanged.emit() |
||||||
|
else: |
||||||
|
if short_id != _id: |
||||||
|
self._createRemoteKeyError = "unexpected trustedcoin short_id: expected {}, received {}".format(short_id, _id) |
||||||
|
self.createRemoteKeyErrorChanged.emit() |
||||||
|
return |
||||||
|
if xpub3 != _xpub3: |
||||||
|
self._createRemoteKeyError = "unexpected trustedcoin xpub3: expected {}, received {}".format(xpub3, _xpub3) |
||||||
|
self.createRemoteKeyErrorChanged.emit() |
||||||
|
return |
||||||
|
self._otpSecret = otp_secret |
||||||
|
self.otpSecretChanged.emit() |
||||||
|
self._shortId = short_id |
||||||
|
self.shortIdChanged.emit() |
||||||
|
finally: |
||||||
|
self._busy = False |
||||||
|
self.busyChanged.emit() |
||||||
|
|
||||||
|
self._busy = True |
||||||
|
self.busyChanged.emit() |
||||||
|
t = threading.Thread(target=create_remote_key_task) |
||||||
|
t.daemon = True |
||||||
|
t.start() |
||||||
|
|
||||||
|
@pyqtSlot(str, int) |
||||||
|
def checkOtp(self, short_id, otp): |
||||||
|
def check_otp_task(): |
||||||
|
try: |
||||||
|
self.plugin.logger.debug(f'check OTP, shortId={short_id}, otp={otp}') |
||||||
|
server.auth(short_id, otp) |
||||||
|
except TrustedCoinException as e: |
||||||
|
if e.status_code == 400: # invalid OTP |
||||||
|
self.plugin.logger.debug('Invalid one-time password.') |
||||||
|
self.otpError.emit() |
||||||
|
else: |
||||||
|
self.plugin.logger.error(str(e)) |
||||||
|
self._createRemoteKeyError = str(e) |
||||||
|
self.createRemoteKeyErrorChanged.emit() |
||||||
|
except Exception as e: |
||||||
|
self.plugin.logger.error(str(e)) |
||||||
|
self._createRemoteKeyError = str(e) |
||||||
|
self.createRemoteKeyErrorChanged.emit() |
||||||
|
else: |
||||||
|
self.plugin.logger.debug('OTP verify success') |
||||||
|
self.otpSuccess.emit() |
||||||
|
finally: |
||||||
|
self._busy = False |
||||||
|
self.busyChanged.emit() |
||||||
|
|
||||||
|
self._busy = True |
||||||
|
self.busyChanged.emit() |
||||||
|
t = threading.Thread(target=check_otp_task) |
||||||
|
t.daemon = True |
||||||
|
t.start() |
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, *args): |
||||||
|
super().__init__(*args) |
||||||
|
|
||||||
|
@hook |
||||||
|
def load_wallet(self, wallet: 'Abstract_Wallet'): |
||||||
|
if not isinstance(wallet, self.wallet_class): |
||||||
|
return |
||||||
|
self.logger.debug(f'plugin enabled for wallet "{str(wallet)}"') |
||||||
|
#wallet.handler_2fa = HandlerTwoFactor(self, window) |
||||||
|
if wallet.can_sign_without_server(): |
||||||
|
self.so._canSignWithoutServer = True |
||||||
|
self.so.canSignWithoutServerChanged.emit() |
||||||
|
|
||||||
|
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) |
||||||
|
#else: |
||||||
|
#action = partial(self.settings_dialog, window) |
||||||
|
#button = StatusBarButton(read_QIcon("trustedcoin-status.png"), |
||||||
|
#_("TrustedCoin"), action) |
||||||
|
#window.statusBar().addPermanentWidget(button) |
||||||
|
self.start_request_thread(wallet) |
||||||
|
|
||||||
|
@hook |
||||||
|
def init_qml(self, gui: 'ElectrumGui'): |
||||||
|
self.logger.debug(f'init_qml hook called, gui={str(type(gui))}') |
||||||
|
self._app = gui.app |
||||||
|
# important: QSignalObject needs to be parented, as keeping a ref |
||||||
|
# in the plugin is not enough to avoid gc |
||||||
|
self.so = Plugin.QSignalObject(self, self._app) |
||||||
|
|
||||||
|
# extend wizard |
||||||
|
self.extend_wizard() |
||||||
|
|
||||||
|
def extend_wizard(self): |
||||||
|
wizard = self._app.daemon.newWalletWizard |
||||||
|
self.logger.debug(repr(wizard)) |
||||||
|
views = { |
||||||
|
'trustedcoin_start': { |
||||||
|
'gui': '../../../../plugins/trustedcoin/qml/Disclaimer', |
||||||
|
'next': 'trustedcoin_choose_seed' |
||||||
|
}, |
||||||
|
'trustedcoin_choose_seed': { |
||||||
|
'gui': '../../../../plugins/trustedcoin/qml/ChooseSeed', |
||||||
|
'next': self.on_choose_seed |
||||||
|
}, |
||||||
|
'trustedcoin_create_seed': { |
||||||
|
'gui': 'WCCreateSeed', |
||||||
|
'next': 'trustedcoin_confirm_seed' |
||||||
|
}, |
||||||
|
'trustedcoin_confirm_seed': { |
||||||
|
'gui': 'WCConfirmSeed', |
||||||
|
'next': 'trustedcoin_tos_email' |
||||||
|
}, |
||||||
|
'trustedcoin_have_seed': { |
||||||
|
'gui': 'WCHaveSeed', |
||||||
|
'next': 'trustedcoin_tos_email' |
||||||
|
}, |
||||||
|
'trustedcoin_tos_email': { |
||||||
|
'gui': '../../../../plugins/trustedcoin/qml/Terms', |
||||||
|
'next': 'trustedcoin_show_confirm_otp' |
||||||
|
}, |
||||||
|
'trustedcoin_show_confirm_otp': { |
||||||
|
'gui': '../../../../plugins/trustedcoin/qml/ShowConfirmOTP', |
||||||
|
'accept': self.on_accept_otp_secret, |
||||||
|
'next': 'wallet_password', |
||||||
|
'last': wizard.last_if_single_password |
||||||
|
} |
||||||
|
} |
||||||
|
wizard.navmap_merge(views) |
||||||
|
|
||||||
|
def on_choose_seed(self, wizard_data): |
||||||
|
self.logger.debug('on_choose_seed') |
||||||
|
if wizard_data['keystore_type'] == 'createseed': |
||||||
|
return 'trustedcoin_create_seed' |
||||||
|
else: |
||||||
|
return 'trustedcoin_have_seed' |
||||||
|
|
||||||
|
# combined create_keystore and create_remote_key pre |
||||||
|
def create_keys(self): |
||||||
|
wizard = self._app.daemon.newWalletWizard |
||||||
|
wizard_data = wizard._current.wizard_data |
||||||
|
|
||||||
|
xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(wizard_data['seed'], wizard_data['seed_extra_words']) |
||||||
|
|
||||||
|
# NOTE: at this point, old style wizard creates a wallet file (w. password if set) and |
||||||
|
# stores the keystores and wizard state, in order to separate offline seed creation |
||||||
|
# and online retrieval of the OTP secret. For mobile, we don't do this, but |
||||||
|
# for desktop the wizard should support this usecase. |
||||||
|
|
||||||
|
data = {'x1/': {'xpub': xpub1}, 'x2/': {'xpub': xpub2}} |
||||||
|
|
||||||
|
# Generate third key deterministically. |
||||||
|
long_user_id, short_id = get_user_id(data) |
||||||
|
xtype = xpub_type(xpub1) |
||||||
|
xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id) |
||||||
|
|
||||||
|
return (xprv1,xpub1,xpub2,xpub3,short_id) |
||||||
|
|
||||||
|
def on_accept_otp_secret(self, wizard_data): |
||||||
|
self.logger.debug('on accept otp: ' + repr(wizard_data)) |
||||||
|
|
||||||
|
xprv1,xpub1,xpub2,xpub3,short_id = self.create_keys() |
||||||
|
|
||||||
|
k1 = keystore.from_xprv(xprv1) |
||||||
|
k2 = keystore.from_xpub(xpub2) |
||||||
|
k3 = keystore.from_xpub(xpub3) |
||||||
|
|
||||||
|
wizard_data['x1/'] = k1.dump() |
||||||
|
wizard_data['x2/'] = k2.dump() |
||||||
|
wizard_data['x3/'] = k3.dump() |
||||||
|
# wizard_data['use_trustedcoin'] = True |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# wizard |
||||||
|
def request_otp_dialog(self, wizard, short_id, otp_secret, xpub3): |
||||||
|
f = lambda otp, reset: self.check_otp(wizard, short_id, otp_secret, xpub3, otp, reset) |
||||||
|
wizard.otp_dialog(otp_secret=otp_secret, run_next=f) |
||||||
|
|
||||||
|
# regular wallet prompt function |
||||||
|
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure): |
||||||
|
self.logger.debug('prompt_user_for_otp') |
||||||
|
self.on_success = on_success |
||||||
|
self.on_failure = on_failure |
||||||
|
self.wallet = wallet |
||||||
|
self.tx = tx |
||||||
|
self.so.requestView.emit('../../../../plugins/trustedcoin/qml/OTP.qml') |
||||||
|
|
||||||
|
def on_otp(self, otp): |
||||||
|
try: |
||||||
|
self.wallet.on_otp(self.tx, otp) |
||||||
|
except TrustedCoinException as e: |
||||||
|
if e.status_code == 400: # invalid OTP |
||||||
|
# Clock.schedule_once(lambda dt: |
||||||
|
self.on_failure(_('Invalid one-time password.')) |
||||||
|
# ) |
||||||
|
else: |
||||||
|
# Clock.schedule_once(lambda dt, bound_e=e: |
||||||
|
self.on_failure(_('Error') + ':\n' + str(bound_e)) |
||||||
|
# ) |
||||||
|
except Exception as e: |
||||||
|
# Clock.schedule_once(lambda dt, bound_e=e: |
||||||
|
self.on_failure(_('Error') + ':\n' + str(bound_e)) |
||||||
|
# ) |
||||||
|
else: |
||||||
|
self.on_success(tx) |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
import QtQuick 2.6 |
||||||
|
import QtQuick.Layouts 1.0 |
||||||
|
import QtQuick.Controls 2.1 |
||||||
|
|
||||||
|
import "../../../gui/qml/components/wizard" |
||||||
|
|
||||||
|
WizardComponent { |
||||||
|
valid: keystoregroup.checkedButton !== null |
||||||
|
|
||||||
|
onAccept: { |
||||||
|
wizard_data['keystore_type'] = keystoregroup.checkedButton.keystoretype |
||||||
|
} |
||||||
|
|
||||||
|
ButtonGroup { |
||||||
|
id: keystoregroup |
||||||
|
} |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
width: parent.width |
||||||
|
Label { |
||||||
|
text: qsTr('Do you want to create a new seed, or restore a wallet using an existing seed?') |
||||||
|
Layout.preferredWidth: parent.width |
||||||
|
wrapMode: Text.Wrap |
||||||
|
} |
||||||
|
RadioButton { |
||||||
|
ButtonGroup.group: keystoregroup |
||||||
|
property string keystoretype: 'createseed' |
||||||
|
checked: true |
||||||
|
text: qsTr('Create a new seed') |
||||||
|
} |
||||||
|
RadioButton { |
||||||
|
ButtonGroup.group: keystoregroup |
||||||
|
property string keystoretype: 'haveseed' |
||||||
|
text: qsTr('I already have a seed') |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,27 @@ |
|||||||
|
import QtQuick 2.6 |
||||||
|
import QtQuick.Layouts 1.0 |
||||||
|
import QtQuick.Controls 2.1 |
||||||
|
|
||||||
|
import org.electrum 1.0 |
||||||
|
|
||||||
|
import "../../../gui/qml/components/wizard" |
||||||
|
|
||||||
|
WizardComponent { |
||||||
|
valid: true |
||||||
|
|
||||||
|
property QtObject plugin |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
width: parent.width |
||||||
|
|
||||||
|
Label { |
||||||
|
Layout.preferredWidth: parent.width |
||||||
|
text: plugin ? plugin.disclaimer : '' |
||||||
|
wrapMode: Text.Wrap |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Component.onCompleted: { |
||||||
|
plugin = AppController.plugin('trustedcoin') |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,46 @@ |
|||||||
|
import QtQuick 2.6 |
||||||
|
import QtQuick.Layouts 1.0 |
||||||
|
import QtQuick.Controls 2.14 |
||||||
|
import QtQuick.Controls.Material 2.0 |
||||||
|
|
||||||
|
import org.electrum 1.0 |
||||||
|
|
||||||
|
//import "controls" |
||||||
|
|
||||||
|
Item { |
||||||
|
width: parent.width |
||||||
|
height: rootLayout.height |
||||||
|
|
||||||
|
property QtObject plugin |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
id: rootLayout |
||||||
|
Button { |
||||||
|
text: 'Force upload' |
||||||
|
enabled: !plugin.busy |
||||||
|
onClicked: plugin.upload() |
||||||
|
} |
||||||
|
Button { |
||||||
|
text: 'Force download' |
||||||
|
enabled: !plugin.busy |
||||||
|
onClicked: plugin.download() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Connections { |
||||||
|
target: plugin |
||||||
|
function onUploadSuccess() { |
||||||
|
console.log('upload success') |
||||||
|
} |
||||||
|
function onUploadFailed() { |
||||||
|
console.log('upload failed') |
||||||
|
} |
||||||
|
function onDownloadSuccess() { |
||||||
|
console.log('download success') |
||||||
|
} |
||||||
|
function onDownloadFailed() { |
||||||
|
console.log('download failed') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,101 @@ |
|||||||
|
import QtQuick 2.6 |
||||||
|
import QtQuick.Layouts 1.0 |
||||||
|
import QtQuick.Controls 2.1 |
||||||
|
|
||||||
|
import "../../../gui/qml/components/wizard" |
||||||
|
import "../../../gui/qml/components/controls" |
||||||
|
|
||||||
|
WizardComponent { |
||||||
|
valid: otpVerified |
||||||
|
|
||||||
|
property QtObject plugin |
||||||
|
|
||||||
|
property bool otpVerified: false |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
width: parent.width |
||||||
|
|
||||||
|
Label { |
||||||
|
text: qsTr('Authenticator secret') |
||||||
|
} |
||||||
|
|
||||||
|
InfoTextArea { |
||||||
|
iconStyle: InfoTextArea.IconStyle.Error |
||||||
|
visible: plugin ? plugin.createRemoteKeyError : false |
||||||
|
text: plugin ? plugin.createRemoteKeyError : '' |
||||||
|
} |
||||||
|
|
||||||
|
QRImage { |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
qrdata: encodeURI('otpauth://totp/Electrum 2FA ' + wizard_data['wallet_name'] |
||||||
|
+ '?secret=' + plugin.otpSecret + '&digits=6') |
||||||
|
render: plugin ? plugin.otpSecret : false |
||||||
|
} |
||||||
|
|
||||||
|
TextHighlightPane { |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
visible: plugin.otpSecret |
||||||
|
Label { |
||||||
|
text: plugin.otpSecret |
||||||
|
font.family: FixedFont |
||||||
|
font.bold: true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Label { |
||||||
|
Layout.preferredWidth: parent.width |
||||||
|
wrapMode: Text.Wrap |
||||||
|
text: qsTr('Enter or scan into authenticator app. Then authenticate below') |
||||||
|
visible: plugin.otpSecret && !otpVerified |
||||||
|
} |
||||||
|
|
||||||
|
TextField { |
||||||
|
id: otp_auth |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
focus: true |
||||||
|
visible: plugin.otpSecret && !otpVerified |
||||||
|
inputMethodHints: Qt.ImhSensitiveData | Qt.ImhDigitsOnly |
||||||
|
font.family: FixedFont |
||||||
|
font.pixelSize: constants.fontSizeLarge |
||||||
|
onTextChanged: { |
||||||
|
if (text.length >= 6) { |
||||||
|
plugin.checkOtp(plugin.shortId, otp_auth.text) |
||||||
|
text = '' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Image { |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
source: '../../../gui/icons/confirmed.png' |
||||||
|
visible: otpVerified |
||||||
|
Layout.preferredWidth: constants.iconSizeLarge |
||||||
|
Layout.preferredHeight: constants.iconSizeLarge |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
BusyIndicator { |
||||||
|
anchors.centerIn: parent |
||||||
|
visible: plugin ? plugin.busy : false |
||||||
|
running: visible |
||||||
|
} |
||||||
|
|
||||||
|
Component.onCompleted: { |
||||||
|
plugin = AppController.plugin('trustedcoin') |
||||||
|
plugin.createKeystore(wizard_data['2fa_email']) |
||||||
|
otp_auth.forceActiveFocus() |
||||||
|
} |
||||||
|
|
||||||
|
Connections { |
||||||
|
target: plugin |
||||||
|
function onOtpError() { |
||||||
|
console.log('OTP verify error') |
||||||
|
// TODO: show error in UI |
||||||
|
} |
||||||
|
function onOtpSuccess() { |
||||||
|
console.log('OTP verify success') |
||||||
|
otpVerified = true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,67 @@ |
|||||||
|
import QtQuick 2.6 |
||||||
|
import QtQuick.Layouts 1.0 |
||||||
|
import QtQuick.Controls 2.1 |
||||||
|
|
||||||
|
import org.electrum 1.0 |
||||||
|
|
||||||
|
import "../../../gui/qml/components/wizard" |
||||||
|
import "../../../gui/qml/components/controls" |
||||||
|
|
||||||
|
WizardComponent { |
||||||
|
valid: !plugin ? false |
||||||
|
: email.text.length > 0 // TODO: validate email address |
||||||
|
&& plugin.termsAndConditions |
||||||
|
|
||||||
|
property QtObject plugin |
||||||
|
|
||||||
|
onAccept: { |
||||||
|
wizard_data['2fa_email'] = email.text |
||||||
|
} |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
anchors.fill: parent |
||||||
|
|
||||||
|
Label { text: qsTr('Terms and conditions') } |
||||||
|
|
||||||
|
TextHighlightPane { |
||||||
|
Layout.fillWidth: true |
||||||
|
Layout.fillHeight: true |
||||||
|
rightPadding: 0 |
||||||
|
|
||||||
|
Flickable { |
||||||
|
anchors.fill: parent |
||||||
|
contentHeight: termsText.height |
||||||
|
clip: true |
||||||
|
boundsBehavior: Flickable.StopAtBounds |
||||||
|
|
||||||
|
Label { |
||||||
|
id: termsText |
||||||
|
width: parent.width |
||||||
|
rightPadding: constants.paddingSmall |
||||||
|
wrapMode: Text.Wrap |
||||||
|
text: plugin ? plugin.termsAndConditions : '' |
||||||
|
} |
||||||
|
ScrollIndicator.vertical: ScrollIndicator { } |
||||||
|
} |
||||||
|
|
||||||
|
BusyIndicator { |
||||||
|
anchors.centerIn: parent |
||||||
|
visible: plugin ? plugin.busy : false |
||||||
|
running: visible |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Label { text: qsTr('Email') } |
||||||
|
|
||||||
|
TextField { |
||||||
|
id: email |
||||||
|
Layout.fillWidth: true |
||||||
|
placeholderText: qsTr('Enter your email address') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Component.onCompleted: { |
||||||
|
plugin = AppController.plugin('trustedcoin') |
||||||
|
plugin.fetchTermsAndConditions() |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue