electrum-bitcoinprivate/gui/qt/installwizard.py

380 lines
14 KiB
Python
Raw Normal View History

from sys import stdout
2015-06-26 05:29:26 -07:00
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import PyQt4.QtCore as QtCore
import electrum
2013-09-11 04:55:49 -07:00
from electrum.i18n import _
2013-08-29 07:07:55 -07:00
2016-01-13 02:20:58 -08:00
from seed_dialog import SeedDisplayLayout, SeedWarningLayout, SeedInputLayout
from network_dialog import NetworkChoiceLayout
2013-09-24 07:57:12 -07:00
from util import *
2016-01-12 06:32:13 -08:00
from password_dialog import PasswordLayout, PW_NEW, PW_PASSPHRASE
2013-08-29 07:07:55 -07:00
from electrum.wallet import Wallet
from electrum.mnemonic import prepare_seed
2016-01-17 05:03:57 -08:00
from electrum.util import SilentException
from electrum.wizard import (WizardBase, UserCancelled,
MSG_ENTER_PASSWORD, MSG_RESTORE_PASSPHRASE,
MSG_COSIGNER, MSG_ENTER_SEED_OR_MPK,
MSG_SHOW_MPK, MSG_VERIFY_SEED,
MSG_GENERATING_WAIT)
2014-04-29 12:19:42 -07:00
def clean_text(seed_e):
text = unicode(seed_e.toPlainText()).strip()
text = ' '.join(text.split())
return text
2015-06-26 05:29:26 -07:00
class CosignWidget(QWidget):
size = 120
def __init__(self, m, n):
QWidget.__init__(self)
self.R = QRect(0, 0, self.size, self.size)
self.setGeometry(self.R)
2016-01-13 02:20:58 -08:00
self.setMinimumHeight(self.size)
self.setMaximumHeight(self.size)
2015-06-26 05:29:26 -07:00
self.m = m
self.n = n
def set_n(self, n):
self.n = n
self.update()
def set_m(self, m):
self.m = m
self.update()
def paintEvent(self, event):
import math
bgcolor = self.palette().color(QPalette.Background)
pen = QPen(bgcolor, 7, QtCore.Qt.SolidLine)
qp = QPainter()
qp.begin(self)
qp.setPen(pen)
qp.setRenderHint(QPainter.Antialiasing)
qp.setBrush(Qt.gray)
for i in range(self.n):
alpha = int(16* 360 * i/self.n)
alpha2 = int(16* 360 * 1/self.n)
qp.setBrush(Qt.green if i<self.m else Qt.gray)
qp.drawPie(self.R, alpha, alpha2)
qp.end()
2016-01-02 18:18:20 -08:00
# WindowModalDialog must come first as it overrides show_error
class InstallWizard(WindowModalDialog, WizardBase):
def __init__(self, config, app, plugins):
title = 'Electrum - ' + _('Install Wizard')
WindowModalDialog.__init__(self, None, title=title)
self.app = app
self.config = config
# Set for base base class
self.plugins = plugins
self.language_for_seed = config.get('language')
self.setMinimumSize(530, 370)
self.setMaximumSize(530, 370)
2013-09-12 10:42:00 -07:00
self.connect(self, QtCore.SIGNAL('accept'), self.accept)
2016-01-13 02:20:58 -08:00
self.title = WWLabel()
self.main_widget = QWidget()
self.cancel_button = QPushButton(_("Cancel"), self)
self.next_button = QPushButton(_("Next"), self)
self.next_button.setDefault(True)
self.logo = QLabel()
self.please_wait = QLabel(_("Please wait..."))
self.please_wait.setAlignment(Qt.AlignCenter)
self.icon_filename = None
self.loop = QEventLoop()
2016-01-22 04:36:10 -08:00
self.rejected.connect(lambda: self.loop.exit(False))
self.cancel_button.clicked.connect(lambda: self.loop.exit(False))
self.next_button.clicked.connect(lambda: self.loop.exit(True))
outer_vbox = QVBoxLayout(self)
inner_vbox = QVBoxLayout()
inner_vbox = QVBoxLayout()
inner_vbox.addWidget(self.title)
inner_vbox.addWidget(self.main_widget)
inner_vbox.addStretch(1)
inner_vbox.addWidget(self.please_wait)
inner_vbox.addStretch(1)
icon_vbox = QVBoxLayout()
icon_vbox.addWidget(self.logo)
icon_vbox.addStretch(1)
hbox = QHBoxLayout()
hbox.addLayout(icon_vbox)
2016-01-13 02:20:58 -08:00
hbox.addSpacing(5)
hbox.addLayout(inner_vbox)
hbox.setStretchFactor(inner_vbox, 1)
outer_vbox.addLayout(hbox)
outer_vbox.addLayout(Buttons(self.cancel_button, self.next_button))
self.set_icon(':icons/electrum.png')
self.show()
self.raise_()
self.refresh_gui() # Need for QT on MacOSX. Lame.
def finished(self):
'''Ensure the dialog is closed.'''
self.accept()
self.refresh_gui()
2016-01-17 05:03:57 -08:00
def on_error(self, exc_info):
if not isinstance(exc_info[1], SilentException):
traceback.print_exception(*exc_info)
self.show_error(str(exc_info[1]))
def set_icon(self, filename):
prior_filename, self.icon_filename = self.icon_filename, filename
self.logo.setPixmap(QPixmap(filename).scaledToWidth(60))
return prior_filename
def set_main_layout(self, layout, title=None, raise_on_cancel=True,
next_enabled=True):
2016-01-12 06:32:13 -08:00
self.title.setText(title or "")
self.title.setVisible(bool(title))
# Get rid of any prior layout by assigning it to a temporary widget
prior_layout = self.main_widget.layout()
if prior_layout:
QWidget().setLayout(prior_layout)
self.main_widget.setLayout(layout)
self.cancel_button.setEnabled(True)
self.next_button.setEnabled(next_enabled)
self.main_widget.setVisible(True)
self.please_wait.setVisible(False)
result = self.loop.exec_()
if not result and raise_on_cancel:
raise UserCancelled
2016-01-12 06:32:13 -08:00
self.title.setVisible(False)
self.cancel_button.setEnabled(False)
self.next_button.setEnabled(False)
self.main_widget.setVisible(False)
self.please_wait.setVisible(True)
2016-01-12 06:36:25 -08:00
self.refresh_gui()
return result
2016-01-12 06:36:25 -08:00
def refresh_gui(self):
# For some reason, to refresh the GUI this needs to be called twice
self.app.processEvents()
self.app.processEvents()
2013-11-03 02:03:45 -08:00
def run(self, *args):
'''Wrap the base wizard implementation with try/except blocks
to give a sensible error message to the user.'''
wallet = None
try:
wallet = WizardBase.run(self, *args)
except UserCancelled:
self.print_error("wallet creation cancelled by user")
self.accept() # For when called from menu
return wallet
def remove_from_recently_open(self, filename):
self.config.remove_from_recently_open(filename)
def request_seed(self, title, is_valid=None):
is_valid = is_valid or Wallet.is_any
2016-01-13 02:20:58 -08:00
slayout = SeedInputLayout()
def sanitized_seed():
return clean_text(slayout.seed_edit())
def set_enabled():
self.next_button.setEnabled(is_valid(sanitized_seed()))
slayout.seed_edit().textChanged.connect(set_enabled)
self.set_main_layout(slayout.layout(), title, next_enabled=False)
return sanitized_seed()
def show_seed(self, seed):
2016-01-13 02:20:58 -08:00
slayout = SeedWarningLayout(seed)
self.set_main_layout(slayout.layout())
def verify_seed(self, seed, is_valid=None):
while True:
r = self.request_seed(MSG_VERIFY_SEED, is_valid)
if prepare_seed(r) == prepare_seed(seed):
return
self.show_error(_('Incorrect seed'))
def show_and_verify_seed(self, seed, is_valid=None):
"""Show the user their seed. Ask them to re-enter it. Return
True on success."""
self.show_seed(seed)
self.app.clipboard().clear()
self.verify_seed(seed, is_valid)
2016-01-12 06:32:13 -08:00
def pw_layout(self, msg, kind):
playout = PasswordLayout(None, msg, kind, self.next_button)
2016-01-13 02:20:58 -08:00
self.set_main_layout(playout.layout())
2016-01-12 06:32:13 -08:00
return playout.new_password()
def request_passphrase(self, device_text, restore=True):
"""Request a passphrase for a wallet from the given device and
confirm it. restore is True if restoring a wallet. Should return
a unicode string."""
if restore:
msg = MSG_RESTORE_PASSPHRASE % device_text
2016-01-12 06:32:13 -08:00
return unicode(self.pw_layout(msg, PW_PASSPHRASE) or '')
def request_password(self, msg=None):
"""Request the user enter a new password and confirm it. Return
the password or None for no password."""
2016-01-12 06:32:13 -08:00
return self.pw_layout(msg or MSG_ENTER_PASSWORD, PW_NEW)
2016-01-09 01:35:10 -08:00
def show_restore(self, wallet, network):
# FIXME: these messages are shown after the install wizard is
# finished and the window closed. On MacOSX they appear parented
# with a re-appeared ghost install wizard window...
2016-01-09 01:35:10 -08:00
if network:
def task():
wallet.wait_until_synchronized()
if wallet.is_found():
msg = _("Recovery successful")
else:
2016-01-09 01:35:10 -08:00
msg = _("No transactions found for this seed")
self.emit(QtCore.SIGNAL('synchronized'), msg)
self.connect(self, QtCore.SIGNAL('synchronized'), self.show_message)
t = threading.Thread(target = task)
t.start()
else:
msg = _("This wallet was restored offline. It may "
"contain more addresses than displayed.")
self.show_message(msg)
2016-01-06 01:31:55 -08:00
def create_addresses(self, wallet):
def task():
wallet.synchronize()
self.emit(QtCore.SIGNAL('accept'))
t = threading.Thread(target = task)
t.start()
2016-01-12 06:36:25 -08:00
self.please_wait.setText(MSG_GENERATING_WAIT)
self.refresh_gui()
def query_create_or_restore(self, wallet_kinds):
"""Ask the user what they want to do, and which wallet kind.
wallet_kinds is an array of translated wallet descriptions.
Return a a tuple (action, kind_index). Action is 'create' or
'restore', and kind the index of the wallet kind chosen."""
2015-08-26 09:35:21 -07:00
2016-01-22 04:07:41 -08:00
hw_wallet_help = _(
"NOTE regarding hardware wallets: If you want to set up a new "
"or wiped device, or if you want Electrum to manage a device "
"you have already set up, select \"create\". If you wish to "
"recover a wallet from a device seed and no longer use the "
"device, select \"restore\"."
)
actions = [_("Create a new wallet"),
_("Restore a wallet or import keys")]
title = _("Electrum could not find an existing wallet.")
actions_clayout = ChoicesLayout(_("What do you want to do?"), actions)
wallet_clayout = ChoicesLayout(_("Wallet kind:"), wallet_kinds)
2014-05-09 04:12:07 -07:00
vbox = QVBoxLayout()
vbox.addLayout(actions_clayout.layout())
vbox.addLayout(wallet_clayout.layout())
2016-01-22 04:07:41 -08:00
vbox.addSpacing(10)
vbox.addWidget(WWLabel(hw_wallet_help))
self.set_main_layout(vbox, title)
2016-01-12 06:32:13 -08:00
action = ['create', 'restore'][actions_clayout.selected_index()]
return action, wallet_clayout.selected_index()
def request_many(self, n, xpub_hot=None):
2014-05-09 07:27:12 -07:00
vbox = QVBoxLayout()
2015-06-27 15:47:25 -07:00
scroll = QScrollArea()
scroll.setWidgetResizable(True)
2016-01-13 02:20:58 -08:00
scroll.setFrameShape(QFrame.NoFrame)
2015-06-27 15:47:25 -07:00
vbox.addWidget(scroll)
w = QWidget()
2016-01-13 02:20:58 -08:00
innerVbox = QVBoxLayout(w)
2015-06-27 15:47:25 -07:00
scroll.setWidget(w)
2014-05-12 01:53:04 -07:00
entries = []
2016-01-13 02:20:58 -08:00
if xpub_hot:
2016-01-13 02:20:58 -08:00
layout = SeedDisplayLayout(xpub_hot, title=MSG_SHOW_MPK, sid='hot')
else:
2016-01-13 02:20:58 -08:00
layout = SeedInputLayout(title=MSG_ENTER_SEED_OR_MPK, sid='hot')
entries.append(layout.seed_edit())
2016-01-13 02:20:58 -08:00
innerVbox.addLayout(layout.layout())
2014-05-12 01:53:04 -07:00
for i in range(n):
2016-01-13 02:20:58 -08:00
msg = MSG_COSIGNER % (i + 1) if xpub_hot else MSG_ENTER_SEED_OR_MPK
layout = SeedInputLayout(title=msg, sid='cold')
innerVbox.addLayout(layout.layout())
entries.append(layout.seed_edit())
2015-06-27 15:47:25 -07:00
def get_texts():
return [clean_text(entry) for entry in entries]
def set_enabled():
texts = get_texts()
is_valid = Wallet.is_xpub if xpub_hot else Wallet.is_any
all_valid = all(is_valid(text) for text in texts)
if xpub_hot:
texts.append(xpub_hot)
has_dups = len(set(texts)) < len(texts)
2016-01-13 02:20:58 -08:00
self.next_button.setEnabled(all_valid and not has_dups)
2014-05-12 01:53:04 -07:00
for e in entries:
e.textChanged.connect(set_enabled)
self.set_main_layout(vbox, next_enabled=False)
return get_texts()
2013-09-12 10:42:00 -07:00
def choose_server(self, network):
title = _("Electrum communicates with remote servers to get "
"information about your transactions and addresses. The "
"servers all fulfil the same purpose only differing in "
"hardware. In most cases you simply want to let Electrum "
"pick one at random. However if you prefer feel free to "
"select a server manually.")
choices = [_("Auto connect"), _("Select server manually")]
choices_title = _("How do you want to connect to a server? ")
clayout = ChoicesLayout(choices_title, choices)
self.set_main_layout(clayout.layout(), title)
auto_connect = True
if clayout.selected_index() == 1:
nlayout = NetworkChoiceLayout(network, self.config, wizard=True)
if self.set_main_layout(nlayout.layout(), raise_on_cancel=False):
nlayout.accept()
auto_connect = False
self.config.set_key('auto_connect', auto_connect, True)
network.auto_connect = auto_connect
2013-09-03 05:32:56 -07:00
def query_choice(self, msg, choices):
clayout = ChoicesLayout(msg, choices)
self.set_main_layout(clayout.layout(), next_enabled=bool(choices))
return clayout.selected_index()
2014-09-05 07:14:40 -07:00
def query_multisig(self, action):
2015-06-26 05:29:26 -07:00
cw = CosignWidget(2, 2)
m_edit = QSpinBox()
n_edit = QSpinBox()
m_edit.setValue(2)
n_edit.setValue(2)
n_edit.setMinimum(2)
n_edit.setMaximum(15)
m_edit.setMinimum(1)
m_edit.setMaximum(2)
n_edit.valueChanged.connect(m_edit.setMaximum)
n_edit.valueChanged.connect(cw.set_n)
m_edit.valueChanged.connect(cw.set_m)
hbox = QHBoxLayout()
hbox.addWidget(QLabel(_('Require')))
hbox.addWidget(m_edit)
hbox.addWidget(QLabel(_('of')))
hbox.addWidget(n_edit)
hbox.addWidget(QLabel(_('signatures')))
hbox.addStretch(1)
2016-01-13 02:20:58 -08:00
vbox = QVBoxLayout()
vbox.addWidget(cw)
vbox.addWidget(WWLabel(_("Choose the number of signatures needed "
"to unlock funds in your wallet:")))
2015-06-26 05:29:26 -07:00
vbox.addLayout(hbox)
2016-01-13 02:20:58 -08:00
self.set_main_layout(vbox, _("Multi-Signature Wallet"))
2015-06-26 05:29:26 -07:00
m = int(m_edit.value())
n = int(n_edit.value())
wallet_type = '%dof%d'%(m,n)
return wallet_type