qt: introduce electrum/gui/qt_common, implement remaining trustedcoin views,
unify most qml and qt wizard code for trustedcoin, separate non-GUI trustedcoin wizard definition to trustedcoin.py
This commit is contained in:
@@ -3,12 +3,12 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QSize
|
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QSize
|
||||||
from PyQt5.QtGui import QPixmap
|
from PyQt5.QtGui import QPixmap
|
||||||
from PyQt5.QtWidgets import (QDialog, QApplication, QPushButton, QWidget, QLabel, QVBoxLayout, QScrollArea,
|
from PyQt5.QtWidgets import (QDialog, QPushButton, QWidget, QLabel, QVBoxLayout, QScrollArea,
|
||||||
QHBoxLayout, QLayout, QStackedWidget)
|
QHBoxLayout, QLayout, QStackedWidget)
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from ..util import Buttons, icon_path
|
|
||||||
from electrum.logging import get_logger
|
from electrum.logging import get_logger
|
||||||
|
from electrum.gui.qt.util import Buttons, icon_path
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from electrum.simple_config import SimpleConfig
|
from electrum.simple_config import SimpleConfig
|
||||||
@@ -165,7 +165,9 @@ class QEAbstractWizard(QDialog):
|
|||||||
def on_back_button_clicked(self):
|
def on_back_button_clicked(self):
|
||||||
if self.can_go_back():
|
if self.can_go_back():
|
||||||
self.prev()
|
self.prev()
|
||||||
self.main_widget.removeWidget(self.main_widget.currentWidget())
|
widget = self.main_widget.currentWidget()
|
||||||
|
self.main_widget.removeWidget(widget)
|
||||||
|
widget.deleteLater()
|
||||||
self.update()
|
self.update()
|
||||||
else:
|
else:
|
||||||
self.close()
|
self.close()
|
||||||
@@ -212,7 +214,7 @@ class WizardComponent(QWidget):
|
|||||||
self.wizard_data = {}
|
self.wizard_data = {}
|
||||||
self.title = title if title is not None else 'No title'
|
self.title = title if title is not None else 'No title'
|
||||||
self.wizard = wizard
|
self.wizard = wizard
|
||||||
self.error = ''
|
self._error = ''
|
||||||
self._valid = False
|
self._valid = False
|
||||||
self._busy = False
|
self._busy = False
|
||||||
|
|
||||||
@@ -236,6 +238,16 @@ class WizardComponent(QWidget):
|
|||||||
self._busy = is_busy
|
self._busy = is_busy
|
||||||
self.on_updated()
|
self.on_updated()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def error(self):
|
||||||
|
return self._error
|
||||||
|
|
||||||
|
@error.setter
|
||||||
|
def error(self, error):
|
||||||
|
if self._error != error:
|
||||||
|
self._error = error
|
||||||
|
self.on_updated()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def apply(self):
|
def apply(self):
|
||||||
# called to apply UI component values to wizard_data
|
# called to apply UI component values to wizard_data
|
||||||
|
|||||||
0
electrum/gui/qt_common/__init__.py
Normal file
0
electrum/gui/qt_common/__init__.py
Normal file
@@ -1,8 +1,8 @@
|
|||||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, QObject
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject
|
||||||
|
|
||||||
from electrum.i18n import _
|
|
||||||
from electrum.logging import get_logger
|
from electrum.logging import get_logger
|
||||||
|
|
||||||
|
|
||||||
class PluginQObject(QObject):
|
class PluginQObject(QObject):
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -24,6 +24,8 @@ class PluginQObject(QObject):
|
|||||||
@pyqtProperty(bool, notify=busyChanged)
|
@pyqtProperty(bool, notify=busyChanged)
|
||||||
def busy(self): return self._busy
|
def busy(self): return self._busy
|
||||||
|
|
||||||
|
# below only used for QML, not compatible yet with Qt
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=pluginEnabledChanged)
|
@pyqtProperty(bool, notify=pluginEnabledChanged)
|
||||||
def pluginEnabled(self): return self.plugin.is_enabled()
|
def pluginEnabled(self): return self.plugin.is_enabled()
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ from electrum.i18n import _
|
|||||||
from electrum.plugin import hook
|
from electrum.plugin import hook
|
||||||
|
|
||||||
from electrum.gui.qml.qewallet import QEWallet
|
from electrum.gui.qml.qewallet import QEWallet
|
||||||
from electrum.gui.qml.plugins import PluginQObject
|
from electrum.gui.qt_common.plugins import PluginQObject
|
||||||
|
|
||||||
from .labels import LabelsPlugin
|
from .labels import LabelsPlugin
|
||||||
|
|
||||||
|
|||||||
@@ -1,261 +1,22 @@
|
|||||||
import threading
|
|
||||||
import socket
|
|
||||||
import base64
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.plugin import hook
|
from electrum.plugin import hook
|
||||||
from electrum.bip32 import xpub_type, BIP32Node
|
|
||||||
from electrum.util import UserFacingException
|
from electrum.util import UserFacingException
|
||||||
from electrum import keystore
|
|
||||||
|
|
||||||
from electrum.gui.qml.qewallet import QEWallet
|
from electrum.gui.qml.qewallet import QEWallet
|
||||||
from electrum.gui.qml.plugins import PluginQObject
|
from .qt_common import QSignalObject
|
||||||
|
|
||||||
from .trustedcoin import (TrustedCoinPlugin, server, ErrorConnectingServer,
|
from .trustedcoin import TrustedCoinPlugin, TrustedCoinException
|
||||||
MOBILE_DISCLAIMER, get_user_id, get_signing_xpub,
|
|
||||||
TrustedCoinException, make_xpub)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from electrum.gui.qml import ElectrumQmlApplication
|
from electrum.gui.qml import ElectrumQmlApplication
|
||||||
from electrum.wallet import Abstract_Wallet
|
from electrum.wallet import Abstract_Wallet
|
||||||
|
from electrum.wizard import NewWalletWizard
|
||||||
|
|
||||||
|
|
||||||
class Plugin(TrustedCoinPlugin):
|
class Plugin(TrustedCoinPlugin):
|
||||||
|
|
||||||
class QSignalObject(PluginQObject):
|
|
||||||
canSignWithoutServerChanged = pyqtSignal()
|
|
||||||
_canSignWithoutServer = False
|
|
||||||
termsAndConditionsRetrieved = pyqtSignal([str], arguments=['message'])
|
|
||||||
termsAndConditionsError = pyqtSignal([str], arguments=['message'])
|
|
||||||
otpError = pyqtSignal([str], arguments=['message'])
|
|
||||||
otpSuccess = pyqtSignal()
|
|
||||||
disclaimerChanged = pyqtSignal()
|
|
||||||
keystoreChanged = pyqtSignal()
|
|
||||||
otpSecretChanged = pyqtSignal()
|
|
||||||
_otpSecret = ''
|
|
||||||
shortIdChanged = pyqtSignal()
|
|
||||||
_shortId = ''
|
|
||||||
billingModelChanged = pyqtSignal()
|
|
||||||
_billingModel = []
|
|
||||||
|
|
||||||
_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=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()
|
|
||||||
|
|
||||||
@pyqtProperty('QVariantList', notify=billingModelChanged)
|
|
||||||
def billingModel(self):
|
|
||||||
return self._billingModel
|
|
||||||
|
|
||||||
def updateBillingInfo(self, wallet):
|
|
||||||
billingModel = []
|
|
||||||
|
|
||||||
price_per_tx = wallet.price_per_tx
|
|
||||||
for k, v in sorted(price_per_tx.items()):
|
|
||||||
if k == 1:
|
|
||||||
continue
|
|
||||||
item = {
|
|
||||||
'text': 'Pay every %d transactions' % k,
|
|
||||||
'value': k,
|
|
||||||
'sats_per_tx': v/k
|
|
||||||
}
|
|
||||||
billingModel.append(item)
|
|
||||||
|
|
||||||
self._billingModel = billingModel
|
|
||||||
self.billingModelChanged.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.emit(_('Error connecting to server'))
|
|
||||||
except Exception as e:
|
|
||||||
self.termsAndConditionsError.emit('%s: %s' % (_('Error'), repr(e)))
|
|
||||||
else:
|
|
||||||
self.termsAndConditionsRetrieved.emit(tos)
|
|
||||||
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 = ''
|
|
||||||
self._otpSecret = ''
|
|
||||||
self.otpSecretChanged.emit()
|
|
||||||
|
|
||||||
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.remoteKeyState = 'new'
|
|
||||||
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:
|
|
||||||
# TODO: move reset request to UI agnostic plugin section
|
|
||||||
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.remoteKeyState = 'reset'
|
|
||||||
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):
|
def __init__(self, *args):
|
||||||
super().__init__(*args)
|
super().__init__(*args)
|
||||||
|
|
||||||
@@ -279,108 +40,45 @@ class Plugin(TrustedCoinPlugin):
|
|||||||
def init_qml(self, app: 'ElectrumQmlApplication'):
|
def init_qml(self, app: 'ElectrumQmlApplication'):
|
||||||
self.logger.debug(f'init_qml hook called, gui={str(type(app))}')
|
self.logger.debug(f'init_qml hook called, gui={str(type(app))}')
|
||||||
self._app = app
|
self._app = app
|
||||||
|
wizard = self._app.daemon.newWalletWizard
|
||||||
# important: QSignalObject needs to be parented, as keeping a ref
|
# important: QSignalObject needs to be parented, as keeping a ref
|
||||||
# in the plugin is not enough to avoid gc
|
# in the plugin is not enough to avoid gc
|
||||||
self.so = Plugin.QSignalObject(self, self._app)
|
self.so = QSignalObject(self, wizard, self._app)
|
||||||
|
|
||||||
# extend wizard
|
# extend wizard
|
||||||
self.extend_wizard()
|
self.extend_wizard(wizard)
|
||||||
|
|
||||||
# wizard support functions
|
# wizard support functions
|
||||||
|
|
||||||
def extend_wizard(self):
|
def extend_wizard(self, wizard: 'NewWalletWizard'):
|
||||||
wizard = self._app.daemon.newWalletWizard
|
super().extend_wizard(wizard)
|
||||||
self.logger.debug(repr(wizard))
|
|
||||||
views = {
|
views = {
|
||||||
'trustedcoin_start': {
|
'trustedcoin_start': {
|
||||||
'gui': '../../../../plugins/trustedcoin/qml/Disclaimer',
|
'gui': '../../../../plugins/trustedcoin/qml/Disclaimer',
|
||||||
'next': 'trustedcoin_choose_seed'
|
|
||||||
},
|
},
|
||||||
'trustedcoin_choose_seed': {
|
'trustedcoin_choose_seed': {
|
||||||
'gui': '../../../../plugins/trustedcoin/qml/ChooseSeed',
|
'gui': '../../../../plugins/trustedcoin/qml/ChooseSeed',
|
||||||
'next': lambda d: 'trustedcoin_create_seed' if d['keystore_type'] == 'createseed'
|
|
||||||
else 'trustedcoin_have_seed'
|
|
||||||
},
|
},
|
||||||
'trustedcoin_create_seed': {
|
'trustedcoin_create_seed': {
|
||||||
'gui': 'WCCreateSeed',
|
'gui': 'WCCreateSeed',
|
||||||
'next': 'trustedcoin_confirm_seed'
|
|
||||||
},
|
},
|
||||||
'trustedcoin_confirm_seed': {
|
'trustedcoin_confirm_seed': {
|
||||||
'gui': 'WCConfirmSeed',
|
'gui': 'WCConfirmSeed',
|
||||||
'next': 'trustedcoin_tos_email'
|
|
||||||
},
|
},
|
||||||
'trustedcoin_have_seed': {
|
'trustedcoin_have_seed': {
|
||||||
'gui': 'WCHaveSeed',
|
'gui': 'WCHaveSeed',
|
||||||
'next': 'trustedcoin_keep_disable'
|
|
||||||
},
|
},
|
||||||
'trustedcoin_keep_disable': {
|
'trustedcoin_keep_disable': {
|
||||||
'gui': '../../../../plugins/trustedcoin/qml/KeepDisable',
|
'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 d: wizard.is_single_password() and d['trustedcoin_keepordisable'] == 'disable'
|
|
||||||
},
|
},
|
||||||
'trustedcoin_tos_email': {
|
'trustedcoin_tos_email': {
|
||||||
'gui': '../../../../plugins/trustedcoin/qml/Terms',
|
'gui': '../../../../plugins/trustedcoin/qml/Terms',
|
||||||
'next': 'trustedcoin_show_confirm_otp'
|
|
||||||
},
|
},
|
||||||
'trustedcoin_show_confirm_otp': {
|
'trustedcoin_show_confirm_otp': {
|
||||||
'gui': '../../../../plugins/trustedcoin/qml/ShowConfirmOTP',
|
'gui': '../../../../plugins/trustedcoin/qml/ShowConfirmOTP',
|
||||||
'accept': self.on_accept_otp_secret,
|
|
||||||
'next': 'wallet_password',
|
|
||||||
'last': lambda d: wizard.is_single_password()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wizard.navmap_merge(views)
|
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()
|
|
||||||
|
|
||||||
|
|
||||||
# running wallet functions
|
# running wallet functions
|
||||||
|
|
||||||
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):
|
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):
|
||||||
@@ -418,4 +116,3 @@ class Plugin(TrustedCoinPlugin):
|
|||||||
qewallet = QEWallet.getInstanceFor(wallet)
|
qewallet = QEWallet.getInstanceFor(wallet)
|
||||||
qewallet.billingInfoChanged.emit()
|
qewallet.billingInfoChanged.emit()
|
||||||
self.so.updateBillingInfo(wallet)
|
self.so.updateBillingInfo(wallet)
|
||||||
|
|
||||||
|
|||||||
@@ -30,29 +30,38 @@ import os
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from PyQt5.QtGui import QPixmap
|
from PyQt5.QtGui import QPixmap
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal
|
from PyQt5.QtCore import QObject, pyqtSignal, QTimer
|
||||||
from PyQt5.QtWidgets import (QTextEdit, QVBoxLayout, QLabel, QGridLayout, QHBoxLayout,
|
from PyQt5.QtWidgets import (QTextEdit, QVBoxLayout, QLabel, QGridLayout, QHBoxLayout,
|
||||||
QRadioButton, QCheckBox, QLineEdit)
|
QRadioButton, QCheckBox, QLineEdit, QPushButton, QWidget)
|
||||||
|
|
||||||
|
from electrum.i18n import _
|
||||||
|
from electrum import keystore
|
||||||
|
from electrum.bip32 import xpub_type
|
||||||
|
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 .qt_common import QSignalObject
|
||||||
from electrum.gui.qt.util import (read_QIcon, WindowModalDialog, WaitingDialog, OkButton,
|
from electrum.gui.qt.util import (read_QIcon, WindowModalDialog, WaitingDialog, OkButton,
|
||||||
CancelButton, Buttons, icon_path, WWLabel, CloseButton, ChoicesLayout)
|
CancelButton, Buttons, icon_path, WWLabel, CloseButton, ChoicesLayout)
|
||||||
from electrum.gui.qt.qrcodewidget import QRCodeWidget
|
from electrum.gui.qt.qrcodewidget import QRCodeWidget
|
||||||
from electrum.gui.qt.amountedit import AmountEdit
|
from electrum.gui.qt.amountedit import AmountEdit
|
||||||
from electrum.gui.qt.main_window import StatusBarButton
|
from electrum.gui.qt.main_window import StatusBarButton
|
||||||
from electrum.gui.qt.installwizard import InstallWizard
|
from electrum.gui.qt.installwizard import InstallWizard
|
||||||
from electrum.i18n import _
|
from electrum.gui.qt.wizard.wallet import WCCreateSeed, WCConfirmSeed, WCHaveSeed, WCEnterExt, WCConfirmExt
|
||||||
from electrum.plugin import hook
|
from electrum.gui.qt.wizard.wizard import WizardComponent
|
||||||
from electrum.util import is_valid_email
|
|
||||||
from electrum.logging import Logger
|
# from .trustedcoin import TrustedCoinPlugin, server, DISCLAIMER, make_xpub, get_signing_xpub, get_user_id
|
||||||
from electrum.base_wizard import GoBack, UserCancelled
|
from .trustedcoin import (TrustedCoinPlugin, server, ErrorConnectingServer,
|
||||||
|
DISCLAIMER, get_user_id, get_signing_xpub,
|
||||||
|
TrustedCoinException, make_xpub)
|
||||||
|
|
||||||
from .trustedcoin import TrustedCoinPlugin, server, DISCLAIMER
|
|
||||||
from ...gui.qt.wizard.wallet import WCCreateSeed, WCConfirmSeed, WCHaveSeed, WCEnterExt, WCConfirmExt
|
|
||||||
from ...gui.qt.wizard.wizard import WizardComponent
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from electrum.gui.qt.main_window import ElectrumWindow
|
from electrum.gui.qt.main_window import ElectrumWindow
|
||||||
from electrum.wallet import Abstract_Wallet
|
from electrum.wallet import Abstract_Wallet
|
||||||
|
from electrum.wizard import NewWalletWizard
|
||||||
|
|
||||||
|
|
||||||
class TOS(QTextEdit):
|
class TOS(QTextEdit):
|
||||||
@@ -336,56 +345,47 @@ class Plugin(TrustedCoinPlugin):
|
|||||||
|
|
||||||
@hook
|
@hook
|
||||||
def init_wallet_wizard(self, wizard: 'QEWalletWizard'):
|
def init_wallet_wizard(self, wizard: 'QEWalletWizard'):
|
||||||
|
# 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.extend_wizard(wizard)
|
||||||
|
self._wizard = wizard
|
||||||
|
|
||||||
def extend_wizard(self, wizard):
|
def extend_wizard(self, wizard: 'NewWalletWizard'):
|
||||||
# wizard = self._app.daemon.newWalletWizard
|
super().extend_wizard(wizard)
|
||||||
# self.logger.debug(repr(wizard))
|
|
||||||
# TODO: move non-gui parts to base plugin
|
|
||||||
views = {
|
views = {
|
||||||
'trustedcoin_start': {
|
'trustedcoin_start': {
|
||||||
'gui': WCDisclaimer,
|
'gui': WCDisclaimer,
|
||||||
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
||||||
'next': 'trustedcoin_choose_seed'
|
|
||||||
},
|
},
|
||||||
'trustedcoin_choose_seed': {
|
'trustedcoin_choose_seed': {
|
||||||
'gui': WCChooseSeed,
|
'gui': WCChooseSeed,
|
||||||
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
||||||
'next': lambda d: 'trustedcoin_create_seed' if d['keystore_type'] == 'createseed'
|
|
||||||
else 'trustedcoin_have_seed'
|
|
||||||
},
|
},
|
||||||
'trustedcoin_create_seed': {
|
'trustedcoin_create_seed': {
|
||||||
'gui': WCCreateSeed,
|
'gui': WCCreateSeed,
|
||||||
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
||||||
'next': 'trustedcoin_confirm_seed'
|
|
||||||
},
|
},
|
||||||
'trustedcoin_confirm_seed': {
|
'trustedcoin_confirm_seed': {
|
||||||
'gui': WCConfirmSeed,
|
'gui': WCConfirmSeed,
|
||||||
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
||||||
'next': 'trustedcoin_tos_email'
|
|
||||||
},
|
},
|
||||||
'trustedcoin_have_seed': {
|
'trustedcoin_have_seed': {
|
||||||
'gui': WCHaveSeed,
|
'gui': WCHaveSeed,
|
||||||
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
||||||
'next': 'trustedcoin_keep_disable'
|
|
||||||
},
|
},
|
||||||
# 'trustedcoin_keep_disable': {
|
'trustedcoin_keep_disable': {
|
||||||
# 'gui': '../../../../plugins/trustedcoin/qml/KeepDisable',
|
'gui': WCKeepDisable,
|
||||||
# 'next': lambda d: 'trustedcoin_tos_email' if d['trustedcoin_keepordisable'] != 'disable'
|
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
||||||
# else 'wallet_password',
|
},
|
||||||
# 'accept': self.recovery_disable,
|
'trustedcoin_tos_email': {
|
||||||
# 'last': lambda d: wizard.is_single_password() and d['trustedcoin_keepordisable'] == 'disable'
|
'gui': WCTerms,
|
||||||
# },
|
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
||||||
# 'trustedcoin_tos_email': {
|
},
|
||||||
# 'gui': '../../../../plugins/trustedcoin/qml/Terms',
|
'trustedcoin_show_confirm_otp': {
|
||||||
# 'next': 'trustedcoin_show_confirm_otp'
|
'gui': WCShowConfirmOTP,
|
||||||
# },
|
'params': {'icon': icon_path('trustedcoin-wizard.png')},
|
||||||
# 'trustedcoin_show_confirm_otp': {
|
}
|
||||||
# 'gui': '../../../../plugins/trustedcoin/qml/ShowConfirmOTP',
|
|
||||||
# 'accept': self.on_accept_otp_secret,
|
|
||||||
# 'next': 'wallet_password',
|
|
||||||
# 'last': lambda d: wizard.is_single_password()
|
|
||||||
# }
|
|
||||||
}
|
}
|
||||||
wizard.navmap_merge(views)
|
wizard.navmap_merge(views)
|
||||||
|
|
||||||
@@ -451,3 +451,159 @@ class WCChooseSeed(WizardComponent):
|
|||||||
|
|
||||||
def apply(self):
|
def apply(self):
|
||||||
self.wizard_data['keystore_type'] = self.c_values[self.clayout.selected_index()]
|
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.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.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)
|
||||||
|
pw = AmountEdit(None, is_int = True)
|
||||||
|
pw.setFocus(True)
|
||||||
|
pw.setMaximumWidth(150)
|
||||||
|
hbox.addWidget(pw)
|
||||||
|
# hbox.addStretch(1)
|
||||||
|
|
||||||
|
self.layout().addWidget(self.new_otp)
|
||||||
|
self.layout().addWidget(self.exist_otp)
|
||||||
|
self.layout().addLayout(hbox)
|
||||||
|
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.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)
|
||||||
|
self.button.setVisible(not is_new)
|
||||||
|
|
||||||
|
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):
|
||||||
|
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.plugin.so.resetOtpSecret()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
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()]
|
||||||
|
|||||||
248
electrum/plugins/trustedcoin/qt_common.py
Normal file
248
electrum/plugins/trustedcoin/qt_common.py
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import threading
|
||||||
|
import socket
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot
|
||||||
|
|
||||||
|
from electrum.i18n import _
|
||||||
|
from electrum.bip32 import BIP32Node
|
||||||
|
|
||||||
|
from .trustedcoin import (server, ErrorConnectingServer, MOBILE_DISCLAIMER, TrustedCoinException)
|
||||||
|
from electrum.gui.qt_common.plugins import PluginQObject
|
||||||
|
|
||||||
|
|
||||||
|
class QSignalObject(PluginQObject):
|
||||||
|
canSignWithoutServerChanged = pyqtSignal()
|
||||||
|
_canSignWithoutServer = False
|
||||||
|
termsAndConditionsRetrieved = pyqtSignal([str], arguments=['message'])
|
||||||
|
termsAndConditionsError = pyqtSignal([str], arguments=['message'])
|
||||||
|
otpError = pyqtSignal([str], arguments=['message'])
|
||||||
|
otpSuccess = pyqtSignal()
|
||||||
|
disclaimerChanged = pyqtSignal()
|
||||||
|
keystoreChanged = pyqtSignal()
|
||||||
|
otpSecretChanged = pyqtSignal()
|
||||||
|
_otpSecret = ''
|
||||||
|
shortIdChanged = pyqtSignal()
|
||||||
|
_shortId = ''
|
||||||
|
billingModelChanged = pyqtSignal()
|
||||||
|
_billingModel = []
|
||||||
|
|
||||||
|
_remoteKeyState = ''
|
||||||
|
remoteKeyStateChanged = pyqtSignal()
|
||||||
|
remoteKeyError = pyqtSignal([str], arguments=['message'])
|
||||||
|
|
||||||
|
requestOtp = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, plugin, wizard, parent):
|
||||||
|
super().__init__(plugin, parent)
|
||||||
|
self.wizard = wizard
|
||||||
|
|
||||||
|
@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=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()
|
||||||
|
|
||||||
|
@pyqtProperty('QVariantList', notify=billingModelChanged)
|
||||||
|
def billingModel(self):
|
||||||
|
return self._billingModel
|
||||||
|
|
||||||
|
def updateBillingInfo(self, wallet):
|
||||||
|
billingModel = []
|
||||||
|
|
||||||
|
price_per_tx = wallet.price_per_tx
|
||||||
|
for k, v in sorted(price_per_tx.items()):
|
||||||
|
if k == 1:
|
||||||
|
continue
|
||||||
|
item = {
|
||||||
|
'text': 'Pay every %d transactions' % k,
|
||||||
|
'value': k,
|
||||||
|
'sats_per_tx': v / k
|
||||||
|
}
|
||||||
|
billingModel.append(item)
|
||||||
|
|
||||||
|
self._billingModel = billingModel
|
||||||
|
self.billingModelChanged.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.emit(_('Error connecting to server'))
|
||||||
|
except Exception as e:
|
||||||
|
self.termsAndConditionsError.emit('%s: %s' % (_('Error'), repr(e)))
|
||||||
|
else:
|
||||||
|
self.termsAndConditionsRetrieved.emit(tos)
|
||||||
|
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 = ''
|
||||||
|
self._otpSecret = ''
|
||||||
|
self.otpSecretChanged.emit()
|
||||||
|
|
||||||
|
wizard_data = self.wizard._current.wizard_data
|
||||||
|
|
||||||
|
xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys(wizard_data)
|
||||||
|
|
||||||
|
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.remoteKeyState = 'new'
|
||||||
|
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 = ''
|
||||||
|
|
||||||
|
wizard_data = self.wizard._current.wizard_data
|
||||||
|
|
||||||
|
xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys(wizard_data)
|
||||||
|
|
||||||
|
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.remoteKeyState = 'reset'
|
||||||
|
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()
|
||||||
@@ -783,3 +783,91 @@ class TrustedCoinPlugin(BasePlugin):
|
|||||||
return self, 'show_disclaimer'
|
return self, 'show_disclaimer'
|
||||||
if not db.get('x3/'):
|
if not db.get('x3/'):
|
||||||
return self, 'accept_terms_of_use'
|
return self, 'accept_terms_of_use'
|
||||||
|
|
||||||
|
# new wizard
|
||||||
|
|
||||||
|
# insert trustedcoin pages in new wallet wizard
|
||||||
|
def extend_wizard(self, wizard: 'NewWalletWizard'):
|
||||||
|
# wizard = self._app.daemon.newWalletWizard
|
||||||
|
# self.logger.debug(repr(wizard))
|
||||||
|
views = {
|
||||||
|
'trustedcoin_start': {
|
||||||
|
'next': 'trustedcoin_choose_seed',
|
||||||
|
},
|
||||||
|
'trustedcoin_choose_seed': {
|
||||||
|
'next': lambda d: 'trustedcoin_create_seed' if d['keystore_type'] == 'createseed'
|
||||||
|
else 'trustedcoin_have_seed'
|
||||||
|
},
|
||||||
|
'trustedcoin_create_seed': {
|
||||||
|
'next': 'trustedcoin_confirm_seed'
|
||||||
|
},
|
||||||
|
'trustedcoin_confirm_seed': {
|
||||||
|
'next': 'trustedcoin_tos_email'
|
||||||
|
},
|
||||||
|
'trustedcoin_have_seed': {
|
||||||
|
'next': 'trustedcoin_keep_disable'
|
||||||
|
},
|
||||||
|
'trustedcoin_keep_disable': {
|
||||||
|
'next': lambda d: 'trustedcoin_tos_email' if d['trustedcoin_keepordisable'] != 'disable'
|
||||||
|
else 'wallet_password',
|
||||||
|
'accept': self.recovery_disable,
|
||||||
|
'last': lambda d: wizard.is_single_password() and d['trustedcoin_keepordisable'] == 'disable'
|
||||||
|
},
|
||||||
|
'trustedcoin_tos_email': {
|
||||||
|
'next': 'trustedcoin_show_confirm_otp'
|
||||||
|
},
|
||||||
|
'trustedcoin_show_confirm_otp': {
|
||||||
|
'accept': self.on_accept_otp_secret,
|
||||||
|
'next': 'wallet_password',
|
||||||
|
'last': lambda d: wizard.is_single_password()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wizard.navmap_merge(views)
|
||||||
|
|
||||||
|
# combined create_keystore and create_remote_key pre
|
||||||
|
def create_keys(self, wizard_data):
|
||||||
|
# wizard = self._app.daemon.newWalletWizard
|
||||||
|
# wizard = self._wizard
|
||||||
|
# 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(wizard_data)
|
||||||
|
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(wizard_data)
|
||||||
|
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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user