ThomasV
2 years ago
committed by
GitHub
38 changed files with 1583 additions and 309 deletions
@ -0,0 +1,92 @@ |
|||
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" |
|||
|
|||
ElDialog { |
|||
id: dialog |
|||
|
|||
title: qsTr('Trustedcoin') |
|||
iconSource: '../../../icons/trustedcoin-status.png' |
|||
|
|||
property string otpauth |
|||
|
|||
property bool _waiting: false |
|||
property string _otpError |
|||
|
|||
standardButtons: Dialog.Cancel |
|||
|
|||
modal: true |
|||
parent: Overlay.overlay |
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
|
|||
focus: true |
|||
|
|||
ColumnLayout { |
|||
width: parent.width |
|||
|
|||
Label { |
|||
text: qsTr('Enter Authenticator code') |
|||
font.pixelSize: constants.fontSizeLarge |
|||
Layout.alignment: Qt.AlignHCenter |
|||
} |
|||
|
|||
TextField { |
|||
id: otpEdit |
|||
Layout.preferredWidth: fontMetrics.advanceWidth(passwordCharacter) * 6 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
font.pixelSize: constants.fontSizeXXLarge |
|||
maximumLength: 6 |
|||
inputMethodHints: Qt.ImhSensitiveData | Qt.ImhDigitsOnly |
|||
echoMode: TextInput.Password |
|||
focus: true |
|||
onTextChanged: { |
|||
if (activeFocus) |
|||
_otpError = '' |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
opacity: _otpError ? 1 : 0 |
|||
text: _otpError |
|||
color: constants.colorError |
|||
Layout.alignment: Qt.AlignHCenter |
|||
} |
|||
|
|||
Button { |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
text: qsTr('Submit') |
|||
enabled: !_waiting |
|||
onClicked: { |
|||
_waiting = true |
|||
Daemon.currentWallet.submitOtp(otpEdit.text) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon.currentWallet |
|||
function onOtpSuccess() { |
|||
_waiting = false |
|||
otpauth = otpEdit.text |
|||
dialog.accept() |
|||
} |
|||
function onOtpFailed(code, message) { |
|||
_waiting = false |
|||
_otpError = message |
|||
otpEdit.text = '' |
|||
} |
|||
} |
|||
|
|||
FontMetrics { |
|||
id: fontMetrics |
|||
font: otpEdit.font |
|||
} |
|||
} |
@ -0,0 +1,102 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "../controls" |
|||
|
|||
WizardComponent { |
|||
id: root |
|||
|
|||
valid: false |
|||
|
|||
function apply() { |
|||
if (bitcoin.isAddressList(import_ta.text)) { |
|||
wizard_data['address_list'] = import_ta.text |
|||
} else if (bitcoin.isPrivateKeyList(import_ta.text)) { |
|||
wizard_data['private_key_list'] = import_ta.text |
|||
} |
|||
} |
|||
|
|||
function verify(text) { |
|||
return bitcoin.isAddressList(text) || bitcoin.isPrivateKeyList(text) |
|||
} |
|||
|
|||
ColumnLayout { |
|||
width: parent.width |
|||
|
|||
Label { text: qsTr('Import Bitcoin Addresses') } |
|||
|
|||
InfoTextArea { |
|||
text: qsTr('Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.') |
|||
} |
|||
|
|||
RowLayout { |
|||
TextArea { |
|||
id: import_ta |
|||
Layout.fillWidth: true |
|||
Layout.minimumHeight: 80 |
|||
focus: true |
|||
wrapMode: TextEdit.WrapAnywhere |
|||
onTextChanged: valid = verify(text) |
|||
} |
|||
ColumnLayout { |
|||
Layout.alignment: Qt.AlignTop |
|||
ToolButton { |
|||
icon.source: '../../../icons/paste.png' |
|||
icon.height: constants.iconSizeMedium |
|||
icon.width: constants.iconSizeMedium |
|||
onClicked: { |
|||
if (verify(AppController.clipboardToText())) { |
|||
if (import_ta.text != '') |
|||
import_ta.text = import_ta.text + '\n' |
|||
import_ta.text = import_ta.text + AppController.clipboardToText() |
|||
} |
|||
} |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../../icons/qrcode.png' |
|||
icon.height: constants.iconSizeMedium |
|||
icon.width: constants.iconSizeMedium |
|||
scale: 1.2 |
|||
onClicked: { |
|||
var scan = qrscan.createObject(root) |
|||
scan.onFound.connect(function() { |
|||
if (verify(scan.scanData)) { |
|||
if (import_ta.text != '') |
|||
import_ta.text = import_ta.text + ',\n' |
|||
import_ta.text = import_ta.text + scan.scanData |
|||
} |
|||
scan.destroy() |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Component { |
|||
id: qrscan |
|||
QRScan { |
|||
width: root.width |
|||
height: root.height |
|||
|
|||
ToolButton { |
|||
icon.source: '../../../icons/closebutton.png' |
|||
icon.height: constants.iconSizeMedium |
|||
icon.width: constants.iconSizeMedium |
|||
anchors.right: parent.right |
|||
anchors.top: parent.top |
|||
onClicked: { |
|||
parent.destroy() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Bitcoin { |
|||
id: bitcoin |
|||
} |
|||
|
|||
} |
@ -0,0 +1,115 @@ |
|||
import os |
|||
|
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject |
|||
from PyQt5.QtQml import QQmlApplicationEngine |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.wizard import NewWalletWizard, ServerConnectWizard |
|||
|
|||
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' }, |
|||
'imported': { 'gui': 'WCImport' }, |
|||
'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, *args): |
|||
return self._daemon.singlePasswordEnabled |
|||
|
|||
@pyqtSlot('QJSValue', bool, str) |
|||
def createStorage(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)) |
|||
|
|||
if single_password_enabled and single_password: |
|||
data['encrypt'] = True |
|||
data['password'] = single_password |
|||
|
|||
path = os.path.join(os.path.dirname(self._daemon.daemon.config.get_wallet_path()), data['wallet_name']) |
|||
|
|||
try: |
|||
self.create_storage(path, data) |
|||
|
|||
# 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)) |
|||
|
|||
class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard): |
|||
|
|||
def __init__(self, daemon, parent = None): |
|||
ServerConnectWizard.__init__(self, daemon) |
|||
QEAbstractWizard.__init__(self, parent) |
|||
self._daemon = daemon |
|||
|
|||
# attach view names |
|||
self.navmap_merge({ |
|||
'autoconnect': { 'gui': 'WCAutoConnect' }, |
|||
'proxy_config': { 'gui': 'WCProxyConfig' }, |
|||
'server_config': { 'gui': 'WCServerConfig' }, |
|||
}) |
@ -1,5 +0,0 @@ |
|||
from electrum.i18n import _ |
|||
|
|||
fullname = 'QML Plugin Test' |
|||
description = '%s\n%s' % (_("Plugin to test QML integration from plugins."), _("Note: Used for development")) |
|||
available_for = ['qml'] |
@ -1,15 +0,0 @@ |
|||
from typing import TYPE_CHECKING |
|||
from PyQt5.QtQml import QQmlApplicationEngine |
|||
from electrum.plugin import hook, BasePlugin |
|||
from electrum.logging import get_logger |
|||
|
|||
if TYPE_CHECKING: |
|||
from electrum.gui.qml import ElectrumGui |
|||
|
|||
class Plugin(BasePlugin): |
|||
def __init__(self, parent, config, name): |
|||
BasePlugin.__init__(self, parent, config, name) |
|||
|
|||
@hook |
|||
def init_qml(self, gui: 'ElectrumGui'): |
|||
self.logger.debug('init_qml hook called') |
@ -0,0 +1,395 @@ |
|||
import threading |
|||
import socket |
|||
import base64 |
|||
from typing import TYPE_CHECKING |
|||
|
|||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot |
|||
|
|||
from electrum.i18n import _ |
|||
from electrum.plugin import hook |
|||
from electrum.bip32 import xpub_type, BIP32Node |
|||
from electrum.util import UserFacingException |
|||
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) |
|||
|
|||
if TYPE_CHECKING: |
|||
from electrum.gui.qml import ElectrumGui |
|||
from electrum.wallet import Abstract_Wallet |
|||
|
|||
class Plugin(TrustedCoinPlugin): |
|||
|
|||
class QSignalObject(PluginQObject): |
|||
canSignWithoutServerChanged = pyqtSignal() |
|||
_canSignWithoutServer = False |
|||
termsAndConditionsChanged = pyqtSignal() |
|||
_termsAndConditions = '' |
|||
termsAndConditionsErrorChanged = pyqtSignal() |
|||
_termsAndConditionsError = '' |
|||
otpError = pyqtSignal([str], arguments=['message']) |
|||
otpSuccess = pyqtSignal() |
|||
disclaimerChanged = pyqtSignal() |
|||
keystoreChanged = pyqtSignal() |
|||
otpSecretChanged = pyqtSignal() |
|||
_otpSecret = '' |
|||
shortIdChanged = pyqtSignal() |
|||
_shortId = '' |
|||
|
|||
_remoteKeyState = '' |
|||
remoteKeyStateChanged = pyqtSignal() |
|||
remoteKeyError = pyqtSignal([str], arguments=['message']) |
|||
|
|||
requestOtp = pyqtSignal() |
|||
|
|||
def __init__(self, plugin, parent): |
|||
super().__init__(plugin, parent) |
|||
|
|||
@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=remoteKeyStateChanged) |
|||
def remoteKeyState(self): |
|||
return self._remoteKeyState |
|||
|
|||
@remoteKeyState.setter |
|||
def remoteKeyState(self, new_state): |
|||
if self._remoteKeyState != new_state: |
|||
self._remoteKeyState = new_state |
|||
self.remoteKeyStateChanged.emit() |
|||
|
|||
@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): |
|||
self.remoteKeyState = '' |
|||
xprv1, xpub1, xprv2, 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) as e: |
|||
self.remoteKeyState = 'error' |
|||
self.remoteKeyError.emit(f'Network error: {str(e)}') |
|||
except TrustedCoinException as e: |
|||
if e.status_code == 409: |
|||
self.remoteKeyState = 'wallet_known' |
|||
self._shortId = short_id |
|||
self.shortIdChanged.emit() |
|||
else: |
|||
self.remoteKeyState = 'error' |
|||
self.logger.warning(str(e)) |
|||
self.remoteKeyError.emit(f'Service error: {str(e)}') |
|||
except (KeyError,TypeError) as e: # catch any assumptions |
|||
self.remoteKeyState = 'error' |
|||
self.remoteKeyError.emit(f'Error: {str(e)}') |
|||
self.logger.error(str(e)) |
|||
else: |
|||
if short_id != _id: |
|||
self.remoteKeyState = 'error' |
|||
self.logger.error("unexpected trustedcoin short_id: expected {}, received {}".format(short_id, _id)) |
|||
self.remoteKeyError.emit('Unexpected short_id') |
|||
return |
|||
if xpub3 != _xpub3: |
|||
self.remoteKeyState = 'error' |
|||
self.logger.error("unexpected trustedcoin xpub3: expected {}, received {}".format(xpub3, _xpub3)) |
|||
self.remoteKeyError.emit('Unexpected trustedcoin xpub3') |
|||
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() |
|||
def resetOtpSecret(self): |
|||
self.remoteKeyState = '' |
|||
xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys() |
|||
def reset_otp_task(): |
|||
try: |
|||
self.plugin.logger.debug('reset_otp') |
|||
r = server.get_challenge(short_id) |
|||
challenge = r.get('challenge') |
|||
message = 'TRUSTEDCOIN CHALLENGE: ' + challenge |
|||
def f(xprv): |
|||
rootnode = BIP32Node.from_xkey(xprv) |
|||
key = rootnode.subkey_at_private_derivation((0, 0)).eckey |
|||
sig = key.sign_message(message, True) |
|||
return base64.b64encode(sig).decode() |
|||
|
|||
signatures = [f(x) for x in [xprv1, xprv2]] |
|||
r = server.reset_auth(short_id, challenge, signatures) |
|||
otp_secret = r.get('otp_secret') |
|||
except (socket.error, ErrorConnectingServer) as e: |
|||
self.remoteKeyState = 'error' |
|||
self.remoteKeyError.emit(f'Network error: {str(e)}') |
|||
except Exception as e: |
|||
self.remoteKeyState = 'error' |
|||
self.remoteKeyError.emit(f'Error: {str(e)}') |
|||
else: |
|||
self._otpSecret = otp_secret |
|||
self.otpSecretChanged.emit() |
|||
finally: |
|||
self._busy = False |
|||
self.busyChanged.emit() |
|||
|
|||
self._busy = True |
|||
self.busyChanged.emit() |
|||
|
|||
t = threading.Thread(target=reset_otp_task, 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(_('Invalid one-time password.')) |
|||
else: |
|||
self.plugin.logger.error(str(e)) |
|||
self.otpError.emit(f'Service error: {str(e)}') |
|||
except Exception as e: |
|||
self.plugin.logger.error(str(e)) |
|||
self.otpError.emit(f'Error: {str(e)}') |
|||
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, 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.') |
|||
]) |
|||
self.logger.info(msg) |
|||
#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': lambda d: 'trustedcoin_create_seed' if d['keystore_type'] == 'createseed' |
|||
else 'trustedcoin_have_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_keep_disable' |
|||
}, |
|||
'trustedcoin_keep_disable': { |
|||
'gui': '../../../../plugins/trustedcoin/qml/KeepDisable', |
|||
'next': lambda d: 'trustedcoin_tos_email' if d['trustedcoin_keepordisable'] != 'disable' |
|||
else 'wallet_password', |
|||
'accept': self.recovery_disable, |
|||
'last': lambda v,d: wizard.last_if_single_password() and d['trustedcoin_keepordisable'] == 'disable' |
|||
}, |
|||
'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) |
|||
|
|||
|
|||
# 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,xprv2,xpub2,xpub3,short_id) |
|||
|
|||
def on_accept_otp_secret(self, wizard_data): |
|||
self.logger.debug('OTP secret accepted, creating keystores') |
|||
xprv1,xpub1,xprv2,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() |
|||
|
|||
def recovery_disable(self, wizard_data): |
|||
if wizard_data['trustedcoin_keepordisable'] != 'disable': |
|||
return |
|||
|
|||
self.logger.debug('2fa disabled, creating keystores') |
|||
xprv1,xpub1,xprv2,xpub2,xpub3,short_id = self.create_keys() |
|||
k1 = keystore.from_xprv(xprv1) |
|||
k2 = keystore.from_xprv(xprv2) |
|||
k3 = keystore.from_xpub(xpub3) |
|||
|
|||
wizard_data['x1/'] = k1.dump() |
|||
wizard_data['x2/'] = k2.dump() |
|||
wizard_data['x3/'] = k3.dump() |
|||
|
|||
|
|||
# regular wallet prompt functions |
|||
|
|||
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 if on_failure else lambda x: self.logger.error(x) |
|||
self.wallet = wallet |
|||
self.tx = tx |
|||
qewallet = QEWallet.getInstanceFor(wallet) |
|||
qewallet.request_otp(self.on_otp) |
|||
|
|||
def on_otp(self, otp): |
|||
self.logger.debug(f'on_otp {otp} for tx {repr(self.tx)}') |
|||
try: |
|||
self.wallet.on_otp(self.tx, otp) |
|||
except UserFacingException as e: |
|||
self.on_failure(_('Invalid one-time password.')) |
|||
except TrustedCoinException as e: |
|||
if e.status_code == 400: # invalid OTP |
|||
self.on_failure(_('Invalid one-time password.')) |
|||
else: |
|||
self.on_failure(_('Error') + ':\n' + str(e)) |
|||
except Exception as e: |
|||
self.on_failure(_('Error') + ':\n' + str(e)) |
|||
else: |
|||
self.on_success(self.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,35 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
import "../../../gui/qml/components/wizard" |
|||
|
|||
WizardComponent { |
|||
valid: keepordisablegroup.checkedButton |
|||
|
|||
function apply() { |
|||
wizard_data['trustedcoin_keepordisable'] = keepordisablegroup.checkedButton.keepordisable |
|||
} |
|||
|
|||
ButtonGroup { |
|||
id: keepordisablegroup |
|||
onCheckedButtonChanged: checkIsLast() |
|||
} |
|||
|
|||
ColumnLayout { |
|||
Label { |
|||
text: qsTr('Restore 2FA wallet') |
|||
} |
|||
RadioButton { |
|||
ButtonGroup.group: keepordisablegroup |
|||
property string keepordisable: 'keep' |
|||
checked: true |
|||
text: qsTr('Keep') |
|||
} |
|||
RadioButton { |
|||
ButtonGroup.group: keepordisablegroup |
|||
property string keepordisable: 'disable' |
|||
text: qsTr('Disable') |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,137 @@ |
|||
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 |
|||
|
|||
function apply() { |
|||
wizard_data['trustedcoin_new_otp_secret'] = requestNewSecret.checked |
|||
} |
|||
|
|||
ColumnLayout { |
|||
width: parent.width |
|||
|
|||
Label { |
|||
text: qsTr('Authenticator secret') |
|||
} |
|||
|
|||
InfoTextArea { |
|||
id: errorBox |
|||
iconStyle: InfoTextArea.IconStyle.Error |
|||
visible: !otpVerified && plugin.remoteKeyState == 'error' |
|||
} |
|||
|
|||
InfoTextArea { |
|||
iconStyle: InfoTextArea.IconStyle.Warn |
|||
visible: plugin.remoteKeyState == 'wallet_known' |
|||
text: qsTr('This wallet is already registered with TrustedCoin. ') |
|||
+ qsTr('To finalize wallet creation, please enter your Google Authenticator Code. ') |
|||
} |
|||
|
|||
QRImage { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
visible: plugin.remoteKeyState == '' |
|||
qrdata: encodeURI('otpauth://totp/Electrum 2FA ' + wizard_data['wallet_name'] |
|||
+ '?secret=' + plugin.otpSecret + '&digits=6') |
|||
render: plugin.otpSecret |
|||
} |
|||
|
|||
TextHighlightPane { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
visible: plugin.otpSecret |
|||
Label { |
|||
text: plugin.otpSecret |
|||
font.family: FixedFont |
|||
font.bold: true |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
visible: !otpVerified && plugin.otpSecret |
|||
Layout.preferredWidth: parent.width |
|||
wrapMode: Text.Wrap |
|||
text: qsTr('Enter or scan into authenticator app. Then authenticate below') |
|||
} |
|||
|
|||
Label { |
|||
visible: !otpVerified && plugin.remoteKeyState == 'wallet_known' |
|||
Layout.preferredWidth: parent.width |
|||
wrapMode: Text.Wrap |
|||
text: qsTr('If you still have your OTP secret, then authenticate below') |
|||
} |
|||
|
|||
TextField { |
|||
id: otp_auth |
|||
visible: !otpVerified && (plugin.otpSecret || plugin.remoteKeyState == 'wallet_known') |
|||
Layout.alignment: Qt.AlignHCenter |
|||
focus: true |
|||
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 = '' |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
visible: !otpVerified && plugin.remoteKeyState == 'wallet_known' |
|||
Layout.preferredWidth: parent.width |
|||
wrapMode: Text.Wrap |
|||
text: qsTr('Otherwise, you can request your OTP secret from the server, by pressing the button below') |
|||
} |
|||
|
|||
Button { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
visible: plugin.remoteKeyState == 'wallet_known' && !otpVerified |
|||
text: qsTr('Request OTP secret') |
|||
onClicked: plugin.resetOtpSecret() |
|||
} |
|||
|
|||
Image { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
source: '../../../gui/icons/confirmed.png' |
|||
visible: otpVerified |
|||
Layout.preferredWidth: constants.iconSizeXLarge |
|||
Layout.preferredHeight: constants.iconSizeXLarge |
|||
} |
|||
} |
|||
|
|||
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(message) { |
|||
console.log('OTP verify error') |
|||
errorBox.text = message |
|||
} |
|||
function onOtpSuccess() { |
|||
console.log('OTP verify success') |
|||
otpVerified = true |
|||
} |
|||
function onRemoteKeyError(message) { |
|||
errorBox.text = message |
|||
} |
|||
} |
|||
} |
|||
|
@ -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() |
|||
} |
|||
} |
@ -0,0 +1,319 @@ |
|||
import copy |
|||
import os |
|||
|
|||
from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any, Dict, Optional, Union |
|||
|
|||
from electrum.logging import get_logger |
|||
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 |
|||
from electrum import bitcoin |
|||
|
|||
class WizardViewState(NamedTuple): |
|||
view: str |
|||
wizard_data: Dict[str, Any] |
|||
params: Dict[str, Any] |
|||
|
|||
class AbstractWizard: |
|||
# serve as a base for all UIs, so no qt |
|||
# encapsulate wizard state |
|||
# encapsulate navigation decisions, UI agnostic |
|||
# encapsulate stack, go backwards |
|||
# allow extend/override flow in subclasses e.g. |
|||
# - override: replace 'next' value to own fn |
|||
# - extend: add new keys to navmap, wire up flow by override |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
navmap = {} |
|||
|
|||
_current = WizardViewState(None, {}, {}) |
|||
_stack = [] # type: List[WizardViewState] |
|||
|
|||
def navmap_merge(self, additional_navmap): |
|||
# NOTE: only merges one level deep. Deeper dict levels will overwrite |
|||
for k,v in additional_navmap.items(): |
|||
if k in self.navmap: |
|||
self.navmap[k].update(v) |
|||
else: |
|||
self.navmap[k] = v |
|||
|
|||
# from current view and wizard_data, resolve the new view |
|||
# returns WizardViewState tuple (view name, wizard_data, view params) |
|||
# view name is the string id of the view in the nav map |
|||
# wizard data is the (stacked) wizard data dict containing user input and choices |
|||
# view params are transient, meant for extra configuration of a view (e.g. info |
|||
# msg in a generic choice dialog) |
|||
# exception: stay on this view |
|||
def resolve_next(self, view, wizard_data): |
|||
assert view |
|||
self._logger.debug(f'view={view}') |
|||
assert view in self.navmap |
|||
|
|||
nav = self.navmap[view] |
|||
|
|||
if 'accept' in nav: |
|||
# allow python scope to append to wizard_data before |
|||
# adding to stack or finishing |
|||
if callable(nav['accept']): |
|||
nav['accept'](wizard_data) |
|||
else: |
|||
self._logger.error(f'accept handler for view {view} not callable') |
|||
|
|||
if not 'next' in nav: |
|||
# finished |
|||
self.finished(wizard_data) |
|||
return (None, wizard_data, {}) |
|||
|
|||
nexteval = nav['next'] |
|||
# simple string based next view |
|||
if isinstance(nexteval, str): |
|||
new_view = WizardViewState(nexteval, wizard_data, {}) |
|||
else: |
|||
# handler fn based next view |
|||
nv = nexteval(wizard_data) |
|||
self._logger.debug(repr(nv)) |
|||
|
|||
# append wizard_data and params if not returned |
|||
if isinstance(nv, str): |
|||
new_view = WizardViewState(nv, wizard_data, {}) |
|||
elif len(nv) == 1: |
|||
new_view = WizardViewState(nv[0], wizard_data, {}) |
|||
elif len(nv) == 2: |
|||
new_view = WizardViewState(nv[0], nv[1], {}) |
|||
else: |
|||
new_view = nv |
|||
|
|||
self._stack.append(copy.deepcopy(self._current)) |
|||
self._current = new_view |
|||
|
|||
self._logger.debug(f'resolve_next view is {self._current.view}') |
|||
self._logger.debug('stack:' + repr(self._stack)) |
|||
|
|||
return new_view |
|||
|
|||
def resolve_prev(self): |
|||
prev_view = self._stack.pop() |
|||
self._logger.debug(f'resolve_prev view is {prev_view}') |
|||
self._logger.debug('stack:' + repr(self._stack)) |
|||
self._current = prev_view |
|||
return prev_view |
|||
|
|||
# check if this view is the final view |
|||
def is_last_view(self, view, wizard_data): |
|||
assert view |
|||
assert view in self.navmap |
|||
|
|||
nav = self.navmap[view] |
|||
|
|||
if not 'last' in nav: |
|||
return False |
|||
|
|||
lastnav = nav['last'] |
|||
# bool literal |
|||
if isinstance(lastnav, bool): |
|||
return lastnav |
|||
elif callable(lastnav): |
|||
# handler fn based |
|||
l = lastnav(view, wizard_data) |
|||
self._logger.debug(f'view "{view}" last: {l}') |
|||
return l |
|||
else: |
|||
raise Exception('last handler for view {view} is not callable nor a bool literal') |
|||
|
|||
def finished(self, wizard_data): |
|||
self._logger.debug('finished.') |
|||
|
|||
def reset(self): |
|||
self.stack = [] |
|||
self._current = WizardViewState(None, {}, {}) |
|||
|
|||
class NewWalletWizard(AbstractWizard): |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
def __init__(self, daemon): |
|||
self.navmap = { |
|||
'wallet_name': { |
|||
'next': 'wallet_type' |
|||
}, |
|||
'wallet_type': { |
|||
'next': self.on_wallet_type |
|||
}, |
|||
'keystore_type': { |
|||
'next': self.on_keystore_type |
|||
}, |
|||
'create_seed': { |
|||
'next': 'confirm_seed' |
|||
}, |
|||
'confirm_seed': { |
|||
'next': 'wallet_password', |
|||
'last': self.last_if_single_password |
|||
}, |
|||
'have_seed': { |
|||
'next': self.on_have_seed, |
|||
'last': self.last_if_single_password_and_not_bip39 |
|||
}, |
|||
'bip39_refine': { |
|||
'next': 'wallet_password', |
|||
'last': self.last_if_single_password |
|||
}, |
|||
'have_master_key': { |
|||
'next': 'wallet_password', |
|||
'last': self.last_if_single_password |
|||
}, |
|||
'imported': { |
|||
'next': 'wallet_password', |
|||
'last': self.last_if_single_password |
|||
}, |
|||
'wallet_password': { |
|||
'last': True |
|||
} |
|||
} |
|||
self._daemon = daemon |
|||
|
|||
def start(self, initial_data = {}): |
|||
self.reset() |
|||
self._current = WizardViewState('wallet_name', initial_data, {}) |
|||
return self._current |
|||
|
|||
def last_if_single_password(self, view, wizard_data): |
|||
raise NotImplementedError() |
|||
|
|||
def last_if_single_password_and_not_bip39(self, view, wizard_data): |
|||
return self.last_if_single_password(view, wizard_data) and not wizard_data['seed_variant'] == 'bip39' |
|||
|
|||
def on_wallet_type(self, wizard_data): |
|||
t = wizard_data['wallet_type'] |
|||
return { |
|||
'standard': 'keystore_type', |
|||
'2fa': 'trustedcoin_start', |
|||
'imported': 'imported' |
|||
}.get(t) |
|||
|
|||
def on_keystore_type(self, wizard_data): |
|||
t = wizard_data['keystore_type'] |
|||
return { |
|||
'createseed': 'create_seed', |
|||
'haveseed': 'have_seed', |
|||
'masterkey': 'have_master_key' |
|||
}.get(t) |
|||
|
|||
def on_have_seed(self, wizard_data): |
|||
if (wizard_data['seed_type'] == 'bip39'): |
|||
return 'bip39_refine' |
|||
else: |
|||
return 'wallet_password' |
|||
|
|||
def finished(self, wizard_data): |
|||
self._logger.debug('finished') |
|||
# override |
|||
|
|||
def create_storage(self, path, data): |
|||
# only standard and 2fa wallets for now |
|||
assert data['wallet_type'] in ['standard', '2fa', 'imported'] |
|||
|
|||
if os.path.exists(path): |
|||
raise Exception('file already exists at path') |
|||
storage = WalletStorage(path) |
|||
|
|||
k = None |
|||
if not 'keystore_type' in data: |
|||
assert data['wallet_type'] == 'imported' |
|||
addresses = {} |
|||
if 'private_key_list' in data: |
|||
k = keystore.Imported_KeyStore({}) |
|||
keys = keystore.get_private_keys(data['private_key_list']) |
|||
for pk in keys: |
|||
assert bitcoin.is_private_key(pk) |
|||
txin_type, pubkey = k.import_privkey(pk, None) |
|||
addr = bitcoin.pubkey_to_address(txin_type, pubkey) |
|||
addresses[addr] = {'type': txin_type, 'pubkey': pubkey} |
|||
elif 'address_list' in data: |
|||
for addr in data['address_list'].split(): |
|||
addresses[addr] = {} |
|||
elif data['keystore_type'] in ['createseed', 'haveseed']: |
|||
if data['seed_type'] in ['old', 'standard', '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 '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 and 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()) |
|||
if data['trustedcoin_keepordisable'] == 'disable': |
|||
k2 = keystore.from_xprv(data['x2/']['xprv']) |
|||
if data['encrypt'] and k2.may_have_password(): |
|||
k2.update_password(None, data['password']) |
|||
db.put('x2/', k2.dump()) |
|||
else: |
|||
db.put('x2/', data['x2/']) |
|||
db.put('x3/', data['x3/']) |
|||
db.put('use_trustedcoin', True) |
|||
elif data['wallet_type'] == 'imported': |
|||
if k: |
|||
db.put('keystore', k.dump()) |
|||
db.put('addresses', addresses) |
|||
|
|||
if k and 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) |
|||
|
|||
class ServerConnectWizard(AbstractWizard): |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
def __init__(self, daemon): |
|||
self.navmap = { |
|||
'autoconnect': { |
|||
'next': 'proxy_config', |
|||
'last': lambda v,d: d['autoconnect'] |
|||
}, |
|||
'proxy_config': { |
|||
'next': 'server_config' |
|||
}, |
|||
'server_config': { |
|||
'last': True |
|||
} |
|||
} |
|||
self._daemon = daemon |
|||
|
|||
def start(self, initial_data = {}): |
|||
self.reset() |
|||
self._current = WizardViewState('autoconnect', initial_data, {}) |
|||
return self._current |
Loading…
Reference in new issue