qml: use new wizard approach in qml and also implement 2FA/trustedcoin
This commit is contained in:
@@ -31,10 +31,6 @@ if TYPE_CHECKING:
|
||||
|
||||
from .qeapp import ElectrumQmlApplication
|
||||
|
||||
class UncaughtException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ElectrumGui(Logger):
|
||||
|
||||
@profiler
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,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')
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick 2.0
|
||||
|
||||
Item {
|
||||
signal next
|
||||
signal prev
|
||||
signal accept
|
||||
property var wizard_data : ({})
|
||||
property bool valid
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
Normal file
160
electrum/gui/qml/qewizard.py
Normal file
@@ -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))
|
||||
@@ -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
Normal file
332
electrum/plugins/trustedcoin/qml.py
Normal file
@@ -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
Normal file
38
electrum/plugins/trustedcoin/qml/ChooseSeed.qml
Normal file
@@ -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
Normal file
27
electrum/plugins/trustedcoin/qml/Disclaimer.qml
Normal file
@@ -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
Normal file
46
electrum/plugins/trustedcoin/qml/Settings.qml
Normal file
@@ -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
Normal file
101
electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml
Normal file
@@ -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
Normal file
67
electrum/plugins/trustedcoin/qml/Terms.qml
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user