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 from .qeapp import ElectrumQmlApplication
class UncaughtException(Exception):
pass
class ElectrumGui(Logger): class ElectrumGui(Logger):
@profiler @profiler

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

@ -13,142 +13,28 @@ Wizard {
signal walletCreated signal walletCreated
property alias path: walletdb.path property string path
// State transition functions. These functions are called when the 'Next' enter: null // disable transition
// 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 {}
}
}
property QtObject wiz: Daemon.newWalletWizard
Component.onCompleted: { Component.onCompleted: {
_setWizardData({}) var view = wiz.start_wizard()
var start = _loadNextComponent(components.walletname) _loadNextComponent(view)
start.next.connect(function() {walletnameDone()})
} }
onAccepted: { onAccepted: {
console.log('Finished new wallet wizard') 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 { Connections {
id: walletdb target: wiz
onCreateSuccess: walletwizard.walletCreated() 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: '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: 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: Daemon.currentWallet.txinType }
Label { text: 'is deterministic'; color: Material.accentColor } Label { text: 'is deterministic'; color: Material.accentColor }
@ -148,7 +151,7 @@ Pane {
Label { text: Daemon.currentWallet.isLightning } Label { text: Daemon.currentWallet.isLightning }
Label { text: 'has Seed'; color: Material.accentColor } 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 } 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: { onAccept: {
wizard_data['seed'] = seedtext.text wizard_data['seed'] = seedtext.text
wizard_data['seed_type'] = 'segwit'
wizard_data['seed_extend'] = extendcb.checked wizard_data['seed_extend'] = extendcb.checked
wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : '' wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : ''
} }
@ -73,11 +72,16 @@ WizardComponent {
} }
Component.onCompleted : { Component.onCompleted : {
setWarningText(12) setWarningText(12)
bitcoin.generate_seed()
} }
} }
} }
onReadyChanged: {
if (!ready)
return
bitcoin.generate_seed(wizard_data['seed_type'])
}
Bitcoin { Bitcoin {
id: bitcoin id: bitcoin
onGeneratedSeedChanged: { onGeneratedSeedChanged: {

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

@ -12,6 +12,8 @@ WizardComponent {
id: root id: root
valid: false valid: false
property bool is2fa: false
onAccept: { onAccept: {
wizard_data['seed'] = seedtext.text wizard_data['seed'] = seedtext.text
wizard_data['seed_type'] = bitcoin.seed_type wizard_data['seed_type'] = bitcoin.seed_type
@ -43,7 +45,7 @@ WizardComponent {
} }
function checkValid() { 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 { Flickable {
@ -58,11 +60,13 @@ WizardComponent {
columns: 2 columns: 2
Label { Label {
visible: !is2fa
text: qsTr('Seed Type') text: qsTr('Seed Type')
Layout.fillWidth: true Layout.fillWidth: true
} }
ComboBox { ComboBox {
id: seed_type id: seed_type
visible: !is2fa
model: ['Electrum', 'BIP39'/*, 'SLIP39'*/] model: ['Electrum', 'BIP39'/*, 'SLIP39'*/]
onActivated: { onActivated: {
setSeedTypeHelpText() setSeedTypeHelpText()
@ -91,7 +95,7 @@ WizardComponent {
Rectangle { Rectangle {
anchors.fill: contentText anchors.fill: contentText
color: 'green' color: root.valid ? 'green' : 'red'
border.color: Material.accentColor border.color: Material.accentColor
radius: 2 radius: 2
} }
@ -148,4 +152,12 @@ WizardComponent {
Component.onCompleted: { Component.onCompleted: {
setSeedTypeHelpText() 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: { onAccept: {
wizard_data['wallet_type'] = wallettypegroup.checkedButton.wallettype 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 { ButtonGroup {
@ -22,7 +27,6 @@ WizardComponent {
text: qsTr('Standard Wallet') text: qsTr('Standard Wallet')
} }
RadioButton { RadioButton {
enabled: false
ButtonGroup.group: wallettypegroup ButtonGroup.group: wallettypegroup
property string wallettype: '2fa' property string wallettype: '2fa'
text: qsTr('Wallet with two-factor authentication') 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 // Here we do some manual binding of page.valid -> pages.pagevalid and
// page.last -> pages.lastpage to propagate the state without the binding // page.last -> pages.lastpage to propagate the state without the binding
// going stale. // going stale.
function _loadNextComponent(comp, wdata={}) { function _loadNextComponent(view, wdata={}) {
// remove any existing pages after current page // remove any existing pages after current page
while (pages.contentChildren[pages.currentIndex+1]) { while (pages.contentChildren[pages.currentIndex+1]) {
pages.takeItem(pages.currentIndex+1).destroy() 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) var page = comp.createObject(pages)
page.validChanged.connect(function() { page.validChanged.connect(function() {
pages.pagevalid = page.valid pages.pagevalid = page.valid
@ -37,6 +44,21 @@ Dialog {
page.lastChanged.connect(function() { page.lastChanged.connect(function() {
pages.lastpage = page.last 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 Object.assign(page.wizard_data, wdata) // deep copy
page.ready = true // signal page it can access wizard_data page.ready = true // signal page it can access wizard_data
pages.pagevalid = page.valid pages.pagevalid = page.valid
@ -58,10 +80,12 @@ Dialog {
clip:true clip:true
function prev() { function prev() {
currentItem.prev()
currentIndex = currentIndex - 1 currentIndex = currentIndex - 1
_setWizardData(pages.contentChildren[currentIndex].wizard_data) _setWizardData(pages.contentChildren[currentIndex].wizard_data)
pages.pagevalid = pages.contentChildren[currentIndex].valid pages.pagevalid = pages.contentChildren[currentIndex].valid
pages.lastpage = pages.contentChildren[currentIndex].last pages.lastpage = pages.contentChildren[currentIndex].last
} }
function next() { function next() {

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

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

4
electrum/gui/qml/qeapp.py

@ -29,6 +29,7 @@ from .qechannelopener import QEChannelOpener
from .qelnpaymentdetails import QELnPaymentDetails from .qelnpaymentdetails import QELnPaymentDetails
from .qechanneldetails import QEChannelDetails from .qechanneldetails import QEChannelDetails
from .qeswaphelper import QESwapHelper from .qeswaphelper import QESwapHelper
from .qewizard import QENewWalletWizard
notification = None notification = None
@ -217,6 +218,7 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QERequestDetails, 'org.electrum', 1, 0, 'RequestDetails') qmlRegisterType(QERequestDetails, 'org.electrum', 1, 0, 'RequestDetails')
qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property') 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) self.engine = QQmlApplicationEngine(parent=self)
@ -254,6 +256,8 @@ class ElectrumQmlApplication(QGuiApplication):
'protocol_version': version.PROTOCOL_VERSION 'protocol_version': version.PROTOCOL_VERSION
}) })
self.plugins.load_plugin('trustedcoin')
qInstallMessageHandler(self.message_handler) qInstallMessageHandler(self.message_handler)
# get notified whether root QML document loads or not # 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.slip39 import decode_mnemonic, Slip39Error
from electrum.util import parse_URI, create_bip21_uri, InvalidBitcoinURI, get_asyncio_loop from electrum.util import parse_URI, create_bip21_uri, InvalidBitcoinURI, get_asyncio_loop
from electrum.transaction import tx_from_any from electrum.transaction import tx_from_any
from electrum.mnemonic import is_any_2fa_seed_type
from .qetypes import QEAmount from .qetypes import QEAmount
@ -69,7 +70,8 @@ class QEBitcoin(QObject):
@pyqtSlot(str) @pyqtSlot(str)
@pyqtSlot(str,bool,bool) @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'): def verify_seed(self, seed, bip39=False, slip39=False, wallet_type='standard', language='en'):
self._logger.debug('bip39 ' + str(bip39)) self._logger.debug('bip39 ' + str(bip39))
self._logger.debug('slip39 ' + str(slip39)) self._logger.debug('slip39 ' + str(slip39))
@ -100,9 +102,10 @@ class QEBitcoin(QObject):
self.validationMessage = 'SLIP39: %s' % str(e) self.validationMessage = 'SLIP39: %s' % str(e)
seed_valid = False # for now seed_valid = False # for now
# cosigning seed # check if seed matches wallet type
if wallet_type != 'standard' and seed_type not in ['standard', 'segwit']: if wallet_type == '2fa' and not is_any_2fa_seed_type(seed_type):
seed_type = '' seed_valid = False
elif wallet_type == 'standard' and seed_type not in ['old', 'standard', 'segwit']:
seed_valid = False seed_valid = False
self.seedType = seed_type 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 .qefx import QEFX
from .qewallet import QEWallet from .qewallet import QEWallet
from .qewalletdb import QEWalletDB from .qewalletdb import QEWalletDB
from .qewizard import QENewWalletWizard
# wallet list model. supports both wallet basenames (wallet file basenames) # wallet list model. supports both wallet basenames (wallet file basenames)
# and whole Wallet instances (loaded wallets) # and whole Wallet instances (loaded wallets)
@ -121,16 +122,19 @@ class QEDaemon(AuthMixin, QObject):
_loaded_wallets = QEWalletListModel() _loaded_wallets = QEWalletListModel()
_available_wallets = None _available_wallets = None
_current_wallet = None _current_wallet = None
_new_wallet_wizard = None
_path = None _path = None
_use_single_password = False _use_single_password = False
_password = None _password = None
walletLoaded = pyqtSignal()
walletRequiresPassword = pyqtSignal()
activeWalletsChanged = pyqtSignal() activeWalletsChanged = pyqtSignal()
availableWalletsChanged = pyqtSignal() availableWalletsChanged = pyqtSignal()
walletOpenError = pyqtSignal([str], arguments=["error"])
fxChanged = pyqtSignal() fxChanged = pyqtSignal()
newWalletWizardChanged = pyqtSignal()
walletLoaded = pyqtSignal()
walletRequiresPassword = pyqtSignal()
walletOpenError = pyqtSignal([str], arguments=["error"])
walletDeleteError = pyqtSignal([str,str], arguments=['code', 'message']) walletDeleteError = pyqtSignal([str,str], arguments=['code', 'message'])
@pyqtSlot() @pyqtSlot()
@ -283,3 +287,9 @@ class QEDaemon(AuthMixin, QObject):
self.daemon.update_password_for_directory(old_password=self._password, new_password=password) self.daemon.update_password_for_directory(old_password=self._password, new_password=password)
self._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): def canHaveLightning(self):
return self.wallet.can_have_lightning() return self.wallet.can_have_lightning()
@pyqtProperty(str, notify=dataChanged)
def walletType(self):
return self.wallet.wallet_type
@pyqtProperty(bool, notify=dataChanged) @pyqtProperty(bool, notify=dataChanged)
def hasSeed(self): def hasSeed(self):
return self.wallet.has_seed() return self.wallet.has_seed()

70
electrum/gui/qml/qewalletdb.py

@ -29,10 +29,8 @@ class QEWalletDB(QObject):
requiresSplitChanged = pyqtSignal() requiresSplitChanged = pyqtSignal()
splitFinished = pyqtSignal() splitFinished = pyqtSignal()
readyChanged = pyqtSignal() readyChanged = pyqtSignal()
createError = pyqtSignal([str], arguments=["error"])
createSuccess = pyqtSignal()
invalidPassword = pyqtSignal() invalidPassword = pyqtSignal()
def reset(self): def reset(self):
self._path = None self._path = None
self._needsPassword = False self._needsPassword = False
@ -172,69 +170,3 @@ class QEWalletDB(QObject):
self._ready = True self._ready = True
self.readyChanged.emit() 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'] requires_wallet_type = ['2fa']
registers_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" return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU"
DISCLAIMER = [ DESKTOP_DISCLAIMER = [
_("Two-factor authentication is a service provided by TrustedCoin. " _("Two-factor authentication is a service provided by TrustedCoin. "
"It uses a multi-signature wallet, where you own 2 of 3 keys. " "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 " "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 " "To be safe from malware, you may want to do this on an offline "
"computer, and move your wallet later to an online computer."), "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. " _("Two-factor authentication is a service provided by TrustedCoin. "
"To use it, you must have a separate device with Google Authenticator."), "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. " _("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 " "your funds at any time and at no cost, without the remote server, by "
"using the 'restore wallet' option with your wallet seed."), "using the 'restore wallet' option with your wallet seed."),
] ]
KIVY_DISCLAIMER = MOBILE_DISCLAIMER
RESTORE_MSG = _("Enter the seed for your 2-factor wallet:") RESTORE_MSG = _("Enter the seed for your 2-factor wallet:")
class TrustedCoinException(Exception): class TrustedCoinException(Exception):

Loading…
Cancel
Save