Browse Source

qml: use new wizard approach in qml and also implement 2FA/trustedcoin

master
Sander van Grieken 3 years ago
parent
commit
43bac2edff
  1. 4
      electrum/gui/qml/__init__.py
  2. 138
      electrum/gui/qml/components/NewWalletWizard.qml
  3. 7
      electrum/gui/qml/components/Wallets.qml
  4. 8
      electrum/gui/qml/components/wizard/WCCreateSeed.qml
  5. 16
      electrum/gui/qml/components/wizard/WCHaveSeed.qml
  6. 6
      electrum/gui/qml/components/wizard/WCWalletType.qml
  7. 26
      electrum/gui/qml/components/wizard/Wizard.qml
  8. 1
      electrum/gui/qml/components/wizard/WizardComponent.qml
  9. 4
      electrum/gui/qml/qeapp.py
  10. 11
      electrum/gui/qml/qebitcoin.py
  11. 16
      electrum/gui/qml/qedaemon.py
  12. 4
      electrum/gui/qml/qewallet.py
  13. 70
      electrum/gui/qml/qewalletdb.py
  14. 160
      electrum/gui/qml/qewizard.py
  15. 2
      electrum/plugins/trustedcoin/__init__.py
  16. 332
      electrum/plugins/trustedcoin/qml.py
  17. 38
      electrum/plugins/trustedcoin/qml/ChooseSeed.qml
  18. 27
      electrum/plugins/trustedcoin/qml/Disclaimer.qml
  19. 46
      electrum/plugins/trustedcoin/qml/Settings.qml
  20. 101
      electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml
  21. 67
      electrum/plugins/trustedcoin/qml/Terms.qml
  22. 7
      electrum/plugins/trustedcoin/trustedcoin.py

4
electrum/gui/qml/__init__.py

@ -31,10 +31,6 @@ if TYPE_CHECKING:
from .qeapp import ElectrumQmlApplication
class UncaughtException(Exception):
pass
class ElectrumGui(Logger):
@profiler

138
electrum/gui/qml/components/NewWalletWizard.qml

@ -13,142 +13,28 @@ Wizard {
signal walletCreated
property alias path: walletdb.path
property string path
// State transition functions. These functions are called when the 'Next'
// button is pressed. Depending on the data create the next page
// in the conversation.
function walletnameDone(d) {
console.log('wallet name done')
var page = _loadNextComponent(components.wallettype, wizard_data)
page.next.connect(function() {wallettypeDone()})
}
function wallettypeDone(d) {
console.log('wallet type done')
var page = _loadNextComponent(components.keystore, wizard_data)
page.next.connect(function() {keystoretypeDone()})
}
function keystoretypeDone(d) {
console.log('keystore type done')
var page
switch(wizard_data['keystore_type']) {
case 'createseed':
page = _loadNextComponent(components.createseed, wizard_data)
page.next.connect(function() {createseedDone()})
break
case 'haveseed':
page = _loadNextComponent(components.haveseed, wizard_data)
page.next.connect(function() {haveseedDone()})
if (wizard_data['seed_type'] != 'bip39' && Daemon.singlePasswordEnabled)
page.last = true
break
case 'masterkey':
page = _loadNextComponent(components.havemasterkey, wizard_data)
page.next.connect(function() {havemasterkeyDone()})
if (Daemon.singlePasswordEnabled)
page.last = true
break
}
}
function createseedDone(d) {
console.log('create seed done')
var page = _loadNextComponent(components.confirmseed, wizard_data)
if (Daemon.singlePasswordEnabled)
page.last = true
else
page.next.connect(function() {confirmseedDone()})
}
function confirmseedDone(d) {
console.log('confirm seed done')
var page = _loadNextComponent(components.walletpassword, wizard_data)
page.last = true
}
function haveseedDone(d) {
console.log('have seed done')
if (wizard_data['seed_type'] == 'bip39') {
var page = _loadNextComponent(components.bip39refine, wizard_data)
if (Daemon.singlePasswordEnabled)
page.last = true
else
page.next.connect(function() {bip39refineDone()})
} else {
var page = _loadNextComponent(components.walletpassword, wizard_data)
page.last = true
}
}
function bip39refineDone(d) {
console.log('bip39 refine done')
var page = _loadNextComponent(components.walletpassword, wizard_data)
page.last = true
}
function havemasterkeyDone(d) {
console.log('have master key done')
var page = _loadNextComponent(components.walletpassword, wizard_data)
page.last = true
}
Item {
id: components
property Component walletname: Component {
WCWalletName {}
}
property Component wallettype: Component {
WCWalletType {}
}
property Component keystore: Component {
WCKeystoreType {}
}
property Component createseed: Component {
WCCreateSeed {}
}
property Component haveseed: Component {
WCHaveSeed {}
}
property Component confirmseed: Component {
WCConfirmSeed {}
}
property Component bip39refine: Component {
WCBIP39Refine {}
}
property Component havemasterkey: Component {
WCHaveMasterKey {}
}
property Component walletpassword: Component {
WCWalletPassword {}
}
}
enter: null // disable transition
property QtObject wiz: Daemon.newWalletWizard
Component.onCompleted: {
_setWizardData({})
var start = _loadNextComponent(components.walletname)
start.next.connect(function() {walletnameDone()})
var view = wiz.start_wizard()
_loadNextComponent(view)
}
onAccepted: {
console.log('Finished new wallet wizard')
walletdb.create_storage(wizard_data, Daemon.singlePasswordEnabled, Daemon.singlePassword)
wiz.create_storage(wizard_data, Daemon.singlePasswordEnabled, Daemon.singlePassword)
}
WalletDB {
id: walletdb
onCreateSuccess: walletwizard.walletCreated()
Connections {
target: wiz
function onCreateSuccess() {
walletwizard.path = wiz.path
walletwizard.walletCreated()
}
}
}

7
electrum/gui/qml/components/Wallets.qml

@ -129,7 +129,10 @@ Pane {
Label { text: 'derivation prefix (BIP32)'; visible: Daemon.currentWallet.isDeterministic; color: Material.accentColor; Layout.columnSpan: 2 }
Label { text: Daemon.currentWallet.derivationPrefix; visible: Daemon.currentWallet.isDeterministic; Layout.columnSpan: 2 }
Label { text: 'txinType'; color: Material.accentColor }
Label { text: 'wallet type'; color: Material.accentColor }
Label { text: Daemon.currentWallet.walletType }
Label { text: 'txin Type'; color: Material.accentColor }
Label { text: Daemon.currentWallet.txinType }
Label { text: 'is deterministic'; color: Material.accentColor }
@ -148,7 +151,7 @@ Pane {
Label { text: Daemon.currentWallet.isLightning }
Label { text: 'has Seed'; color: Material.accentColor }
Label { text: Daemon.currentWallet.hasSeed; Layout.columnSpan: 3 }
Label { text: Daemon.currentWallet.hasSeed }
Label { Layout.columnSpan:4; text: qsTr('Master Public Key'); color: Material.accentColor }

8
electrum/gui/qml/components/wizard/WCCreateSeed.qml

@ -12,7 +12,6 @@ WizardComponent {
onAccept: {
wizard_data['seed'] = seedtext.text
wizard_data['seed_type'] = 'segwit'
wizard_data['seed_extend'] = extendcb.checked
wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : ''
}
@ -73,11 +72,16 @@ WizardComponent {
}
Component.onCompleted : {
setWarningText(12)
bitcoin.generate_seed()
}
}
}
onReadyChanged: {
if (!ready)
return
bitcoin.generate_seed(wizard_data['seed_type'])
}
Bitcoin {
id: bitcoin
onGeneratedSeedChanged: {

16
electrum/gui/qml/components/wizard/WCHaveSeed.qml

@ -12,6 +12,8 @@ WizardComponent {
id: root
valid: false
property bool is2fa: false
onAccept: {
wizard_data['seed'] = seedtext.text
wizard_data['seed_type'] = bitcoin.seed_type
@ -43,7 +45,7 @@ WizardComponent {
}
function checkValid() {
bitcoin.verify_seed(seedtext.text, seed_type.getTypeCode() == 'BIP39', seed_type.getTypeCode() == 'SLIP39')
bitcoin.verify_seed(seedtext.text, seed_type.getTypeCode() == 'BIP39', seed_type.getTypeCode() == 'SLIP39', wizard_data['wallet_type'])
}
Flickable {
@ -58,11 +60,13 @@ WizardComponent {
columns: 2
Label {
visible: !is2fa
text: qsTr('Seed Type')
Layout.fillWidth: true
}
ComboBox {
id: seed_type
visible: !is2fa
model: ['Electrum', 'BIP39'/*, 'SLIP39'*/]
onActivated: {
setSeedTypeHelpText()
@ -91,7 +95,7 @@ WizardComponent {
Rectangle {
anchors.fill: contentText
color: 'green'
color: root.valid ? 'green' : 'red'
border.color: Material.accentColor
radius: 2
}
@ -148,4 +152,12 @@ WizardComponent {
Component.onCompleted: {
setSeedTypeHelpText()
}
onReadyChanged: {
if (!ready)
return
if (wizard_data['wallet_type'] == '2fa')
root.is2fa = true
}
}

6
electrum/gui/qml/components/wizard/WCWalletType.qml

@ -6,6 +6,11 @@ WizardComponent {
onAccept: {
wizard_data['wallet_type'] = wallettypegroup.checkedButton.wallettype
if (wizard_data['wallet_type'] == 'standard')
wizard_data['seed_type'] = 'segwit'
else if (wizard_data['wallet_type'] == '2fa')
wizard_data['seed_type'] = '2fa_segwit'
// TODO: multisig
}
ButtonGroup {
@ -22,7 +27,6 @@ WizardComponent {
text: qsTr('Standard Wallet')
}
RadioButton {
enabled: false
ButtonGroup.group: wallettypegroup
property string wallettype: '2fa'
text: qsTr('Wallet with two-factor authentication')

26
electrum/gui/qml/components/wizard/Wizard.qml

@ -24,12 +24,19 @@ Dialog {
// Here we do some manual binding of page.valid -> pages.pagevalid and
// page.last -> pages.lastpage to propagate the state without the binding
// going stale.
function _loadNextComponent(comp, wdata={}) {
function _loadNextComponent(view, wdata={}) {
// remove any existing pages after current page
while (pages.contentChildren[pages.currentIndex+1]) {
pages.takeItem(pages.currentIndex+1).destroy()
}
var url = Qt.resolvedUrl(wiz.viewToComponent(view))
console.log(url)
var comp = Qt.createComponent(url)
if (comp.status == Component.Error) {
console.log(comp.errorString())
return null
}
var page = comp.createObject(pages)
page.validChanged.connect(function() {
pages.pagevalid = page.valid
@ -37,6 +44,21 @@ Dialog {
page.lastChanged.connect(function() {
pages.lastpage = page.last
} )
page.next.connect(function() {
var newview = wiz.submit(page.wizard_data)
if (newview.view) {
console.log('next view: ' + newview.view)
var newpage = _loadNextComponent(newview.view, newview.wizard_data)
newpage.last = wiz.isLast(newview.wizard_data)
} else {
console.log('END')
}
})
page.prev.connect(function() {
var wdata = wiz.prev()
console.log('prev view data: ' + JSON.stringify(wdata))
page.last = wiz.isLast(wdata)
})
Object.assign(page.wizard_data, wdata) // deep copy
page.ready = true // signal page it can access wizard_data
pages.pagevalid = page.valid
@ -58,10 +80,12 @@ Dialog {
clip:true
function prev() {
currentItem.prev()
currentIndex = currentIndex - 1
_setWizardData(pages.contentChildren[currentIndex].wizard_data)
pages.pagevalid = pages.contentChildren[currentIndex].valid
pages.lastpage = pages.contentChildren[currentIndex].last
}
function next() {

1
electrum/gui/qml/components/wizard/WizardComponent.qml

@ -2,6 +2,7 @@ import QtQuick 2.0
Item {
signal next
signal prev
signal accept
property var wizard_data : ({})
property bool valid

4
electrum/gui/qml/qeapp.py

@ -29,6 +29,7 @@ from .qechannelopener import QEChannelOpener
from .qelnpaymentdetails import QELnPaymentDetails
from .qechanneldetails import QEChannelDetails
from .qeswaphelper import QESwapHelper
from .qewizard import QENewWalletWizard
notification = None
@ -217,6 +218,7 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QERequestDetails, 'org.electrum', 1, 0, 'RequestDetails')
qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property')
qmlRegisterUncreatableType(QENewWalletWizard, 'org.electrum', 1, 0, 'NewWalletWizard', 'NewWalletWizard can only be used as property')
self.engine = QQmlApplicationEngine(parent=self)
@ -254,6 +256,8 @@ class ElectrumQmlApplication(QGuiApplication):
'protocol_version': version.PROTOCOL_VERSION
})
self.plugins.load_plugin('trustedcoin')
qInstallMessageHandler(self.message_handler)
# get notified whether root QML document loads or not

11
electrum/gui/qml/qebitcoin.py

@ -10,6 +10,7 @@ from electrum.logging import get_logger
from electrum.slip39 import decode_mnemonic, Slip39Error
from electrum.util import parse_URI, create_bip21_uri, InvalidBitcoinURI, get_asyncio_loop
from electrum.transaction import tx_from_any
from electrum.mnemonic import is_any_2fa_seed_type
from .qetypes import QEAmount
@ -69,7 +70,8 @@ class QEBitcoin(QObject):
@pyqtSlot(str)
@pyqtSlot(str,bool,bool)
@pyqtSlot(str,bool,bool,str,str,str)
@pyqtSlot(str,bool,bool,str)
@pyqtSlot(str,bool,bool,str,str)
def verify_seed(self, seed, bip39=False, slip39=False, wallet_type='standard', language='en'):
self._logger.debug('bip39 ' + str(bip39))
self._logger.debug('slip39 ' + str(slip39))
@ -100,9 +102,10 @@ class QEBitcoin(QObject):
self.validationMessage = 'SLIP39: %s' % str(e)
seed_valid = False # for now
# cosigning seed
if wallet_type != 'standard' and seed_type not in ['standard', 'segwit']:
seed_type = ''
# check if seed matches wallet type
if wallet_type == '2fa' and not is_any_2fa_seed_type(seed_type):
seed_valid = False
elif wallet_type == 'standard' and seed_type not in ['old', 'standard', 'segwit']:
seed_valid = False
self.seedType = seed_type

16
electrum/gui/qml/qedaemon.py

@ -14,6 +14,7 @@ from .auth import AuthMixin, auth_protect
from .qefx import QEFX
from .qewallet import QEWallet
from .qewalletdb import QEWalletDB
from .qewizard import QENewWalletWizard
# wallet list model. supports both wallet basenames (wallet file basenames)
# and whole Wallet instances (loaded wallets)
@ -121,16 +122,19 @@ class QEDaemon(AuthMixin, QObject):
_loaded_wallets = QEWalletListModel()
_available_wallets = None
_current_wallet = None
_new_wallet_wizard = None
_path = None
_use_single_password = False
_password = None
walletLoaded = pyqtSignal()
walletRequiresPassword = pyqtSignal()
activeWalletsChanged = pyqtSignal()
availableWalletsChanged = pyqtSignal()
walletOpenError = pyqtSignal([str], arguments=["error"])
fxChanged = pyqtSignal()
newWalletWizardChanged = pyqtSignal()
walletLoaded = pyqtSignal()
walletRequiresPassword = pyqtSignal()
walletOpenError = pyqtSignal([str], arguments=["error"])
walletDeleteError = pyqtSignal([str,str], arguments=['code', 'message'])
@pyqtSlot()
@ -283,3 +287,9 @@ class QEDaemon(AuthMixin, QObject):
self.daemon.update_password_for_directory(old_password=self._password, new_password=password)
self._password = password
@pyqtProperty(QENewWalletWizard, notify=newWalletWizardChanged)
def newWalletWizard(self):
if not self._new_wallet_wizard:
self._new_wallet_wizard = QENewWalletWizard(self)
return self._new_wallet_wizard

4
electrum/gui/qml/qewallet.py

@ -304,6 +304,10 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
def canHaveLightning(self):
return self.wallet.can_have_lightning()
@pyqtProperty(str, notify=dataChanged)
def walletType(self):
return self.wallet.wallet_type
@pyqtProperty(bool, notify=dataChanged)
def hasSeed(self):
return self.wallet.has_seed()

70
electrum/gui/qml/qewalletdb.py

@ -29,10 +29,8 @@ class QEWalletDB(QObject):
requiresSplitChanged = pyqtSignal()
splitFinished = pyqtSignal()
readyChanged = pyqtSignal()
createError = pyqtSignal([str], arguments=["error"])
createSuccess = pyqtSignal()
invalidPassword = pyqtSignal()
def reset(self):
self._path = None
self._needsPassword = False
@ -172,69 +170,3 @@ class QEWalletDB(QObject):
self._ready = True
self.readyChanged.emit()
@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))
assert data['wallet_type'] == 'standard' # only standard wallets for now
if single_password_enabled and single_password:
data['encrypt'] = True
data['password'] = single_password
try:
path = os.path.join(os.path.dirname(self.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)
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'])
db.put('keystore', k.dump())
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))

160
electrum/gui/qml/qewizard.py

@ -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))

2
electrum/plugins/trustedcoin/__init__.py

@ -8,4 +8,4 @@ description = ''.join([
])
requires_wallet_type = ['2fa']
registers_wallet_type = '2fa'
available_for = ['qt', 'cmdline', 'kivy']
available_for = ['qt', 'cmdline', 'kivy', 'qml']

332
electrum/plugins/trustedcoin/qml.py

@ -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)

38
electrum/plugins/trustedcoin/qml/ChooseSeed.qml

@ -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')
}
}
}

27
electrum/plugins/trustedcoin/qml/Disclaimer.qml

@ -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')
}
}

46
electrum/plugins/trustedcoin/qml/Settings.qml

@ -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')
}
}
}

101
electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml

@ -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
}
}
}

67
electrum/plugins/trustedcoin/qml/Terms.qml

@ -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()
}
}

7
electrum/plugins/trustedcoin/trustedcoin.py

@ -69,7 +69,7 @@ def get_billing_xpub():
return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU"
DISCLAIMER = [
DESKTOP_DISCLAIMER = [
_("Two-factor authentication is a service provided by TrustedCoin. "
"It uses a multi-signature wallet, where you own 2 of 3 keys. "
"The third key is stored on a remote server that signs transactions on "
@ -86,8 +86,9 @@ DISCLAIMER = [
"To be safe from malware, you may want to do this on an offline "
"computer, and move your wallet later to an online computer."),
]
DISCLAIMER = DESKTOP_DISCLAIMER
KIVY_DISCLAIMER = [
MOBILE_DISCLAIMER = [
_("Two-factor authentication is a service provided by TrustedCoin. "
"To use it, you must have a separate device with Google Authenticator."),
_("This service uses a multi-signature wallet, where you own 2 of 3 keys. "
@ -98,6 +99,8 @@ KIVY_DISCLAIMER = [
"your funds at any time and at no cost, without the remote server, by "
"using the 'restore wallet' option with your wallet seed."),
]
KIVY_DISCLAIMER = MOBILE_DISCLAIMER
RESTORE_MSG = _("Enter the seed for your 2-factor wallet:")
class TrustedCoinException(Exception):

Loading…
Cancel
Save