You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
395 lines
15 KiB
395 lines
15 KiB
import sys |
|
|
|
from PyQt4.QtGui import * |
|
from PyQt4.QtCore import * |
|
import PyQt4.QtCore as QtCore |
|
|
|
import electrum |
|
from electrum.i18n import _ |
|
|
|
from seed_dialog import SeedDisplayLayout, SeedWarningLayout, SeedInputLayout |
|
from network_dialog import NetworkChoiceLayout |
|
from util import * |
|
from password_dialog import PasswordLayout, PW_NEW, PW_PASSPHRASE |
|
|
|
from electrum.wallet import Wallet |
|
from electrum.mnemonic import prepare_seed |
|
from electrum.util import UserCancelled |
|
from electrum.wizard import (WizardBase, |
|
MSG_ENTER_PASSWORD, MSG_RESTORE_PASSPHRASE, |
|
MSG_COSIGNER, MSG_ENTER_SEED_OR_MPK, |
|
MSG_SHOW_MPK, MSG_VERIFY_SEED, |
|
MSG_GENERATING_WAIT) |
|
|
|
def clean_text(seed_e): |
|
text = unicode(seed_e.toPlainText()).strip() |
|
text = ' '.join(text.split()) |
|
return text |
|
|
|
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) |
|
self.setMinimumHeight(self.size) |
|
self.setMaximumHeight(self.size) |
|
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() |
|
|
|
|
|
# WindowModalDialog must come first as it overrides show_error |
|
class InstallWizard(QDialog, MessageBoxMixin, WizardBase): |
|
|
|
def __init__(self, config, app, plugins): |
|
QDialog.__init__(self, None) |
|
self.setWindowTitle('Electrum - ' + _('Install Wizard')) |
|
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) |
|
self.connect(self, QtCore.SIGNAL('accept'), self.accept) |
|
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() |
|
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) |
|
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() |
|
|
|
def on_error(self, exc_info): |
|
if not isinstance(exc_info[1], UserCancelled): |
|
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): |
|
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 |
|
self.title.setVisible(False) |
|
self.cancel_button.setEnabled(False) |
|
self.next_button.setEnabled(False) |
|
self.main_widget.setVisible(False) |
|
self.please_wait.setVisible(True) |
|
self.refresh_gui() |
|
return result |
|
|
|
def refresh_gui(self): |
|
# For some reason, to refresh the GUI this needs to be called twice |
|
self.app.processEvents() |
|
self.app.processEvents() |
|
|
|
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 |
|
except BaseException as e: |
|
self.on_error(sys.exc_info()) |
|
raise |
|
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 |
|
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): |
|
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) |
|
|
|
def pw_layout(self, msg, kind): |
|
playout = PasswordLayout(None, msg, kind, self.next_button) |
|
self.set_main_layout(playout.layout()) |
|
return playout.new_password() |
|
|
|
def request_passphrase(self, device_text): |
|
"""When restoring a wallet, request the passphrase that was used for |
|
the wallet on the given device and confirm it. Should return |
|
a unicode string.""" |
|
phrase = self.pw_layout(MSG_RESTORE_PASSPHRASE % device_text, |
|
PW_PASSPHRASE) |
|
if phrase is None: |
|
raise UserCancelled |
|
return phrase |
|
|
|
def request_password(self, msg=None): |
|
"""Request the user enter a new password and confirm it. Return |
|
the password or None for no password.""" |
|
return self.pw_layout(msg or MSG_ENTER_PASSWORD, PW_NEW) |
|
|
|
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... |
|
if network: |
|
def task(): |
|
wallet.wait_until_synchronized() |
|
if wallet.is_found(): |
|
msg = _("Recovery successful") |
|
else: |
|
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.daemon = True |
|
t.start() |
|
else: |
|
msg = _("This wallet was restored offline. It may " |
|
"contain more addresses than displayed.") |
|
self.show_message(msg) |
|
|
|
def create_addresses(self, wallet): |
|
def task(): |
|
wallet.synchronize() |
|
self.emit(QtCore.SIGNAL('accept')) |
|
t = threading.Thread(target = task) |
|
t.start() |
|
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.""" |
|
|
|
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) |
|
|
|
vbox = QVBoxLayout() |
|
vbox.addLayout(actions_clayout.layout()) |
|
vbox.addLayout(wallet_clayout.layout()) |
|
self.set_main_layout(vbox, title) |
|
|
|
action = ['create', 'restore'][actions_clayout.selected_index()] |
|
return action, wallet_clayout.selected_index() |
|
|
|
def query_hw_wallet_choice(self, msg, action, choices): |
|
actions = [_("Initialize a new or wiped device"), |
|
_("Use a device you have already set up"), |
|
_("Restore Electrum wallet from device seed words")] |
|
default_action = 1 if action == 'create' else 2 |
|
actions_clayout = ChoicesLayout(_("What do you want to do?"), actions, |
|
checked_index=default_action) |
|
wallet_clayout = ChoicesLayout(msg, choices) |
|
|
|
vbox = QVBoxLayout() |
|
vbox.addLayout(actions_clayout.layout()) |
|
vbox.addLayout(wallet_clayout.layout()) |
|
self.set_main_layout(vbox, next_enabled=len(choices) != 0) |
|
|
|
if actions_clayout.selected_index() == 2: |
|
action = 'restore' |
|
else: |
|
action = 'create' |
|
return action, wallet_clayout.selected_index() |
|
|
|
def request_many(self, n, xpub_hot=None): |
|
vbox = QVBoxLayout() |
|
scroll = QScrollArea() |
|
scroll.setWidgetResizable(True) |
|
scroll.setFrameShape(QFrame.NoFrame) |
|
vbox.addWidget(scroll) |
|
|
|
w = QWidget() |
|
innerVbox = QVBoxLayout(w) |
|
scroll.setWidget(w) |
|
|
|
entries = [] |
|
|
|
if xpub_hot: |
|
layout = SeedDisplayLayout(xpub_hot, title=MSG_SHOW_MPK, sid='hot') |
|
else: |
|
layout = SeedInputLayout(title=MSG_ENTER_SEED_OR_MPK, sid='hot') |
|
entries.append(layout.seed_edit()) |
|
innerVbox.addLayout(layout.layout()) |
|
|
|
for i in range(n): |
|
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()) |
|
|
|
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) |
|
self.next_button.setEnabled(all_valid and not has_dups) |
|
for e in entries: |
|
e.textChanged.connect(set_enabled) |
|
self.set_main_layout(vbox, next_enabled=False) |
|
return get_texts() |
|
|
|
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 |
|
|
|
def query_choice(self, msg, choices): |
|
clayout = ChoicesLayout(msg, choices) |
|
self.set_main_layout(clayout.layout(), next_enabled=bool(choices)) |
|
return clayout.selected_index() |
|
|
|
def query_multisig(self, action): |
|
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) |
|
|
|
vbox = QVBoxLayout() |
|
vbox.addWidget(cw) |
|
vbox.addWidget(WWLabel(_("Choose the number of signatures needed " |
|
"to unlock funds in your wallet:"))) |
|
vbox.addLayout(hbox) |
|
self.set_main_layout(vbox, _("Multi-Signature Wallet")) |
|
m = int(m_edit.value()) |
|
n = int(n_edit.value()) |
|
wallet_type = '%dof%d'%(m,n) |
|
return wallet_type
|
|
|