Browse Source

Major refactoring

- separation between Wallet and key management (Keystore)
 - simplification of wallet classes
 - remove support for multiple accounts in the same wallet
 - add support for OP_RETURN to Trezor plugin
 - split multi-accounts wallets for backward compatibility
283
ThomasV 9 years ago
parent
commit
1159f85e05
  1. 14
      gui/kivy/main_window.py
  2. 4
      gui/kivy/uix/dialogs/installwizard.py
  3. 2
      gui/kivy/uix/screens.py
  4. 4
      gui/qt/__init__.py
  5. 46
      gui/qt/address_list.py
  6. 2
      gui/qt/history_list.py
  7. 40
      gui/qt/installwizard.py
  8. 150
      gui/qt/main_window.py
  9. 2
      gui/qt/password_dialog.py
  10. 19
      gui/qt/request_list.py
  11. 8
      gui/qt/seed_dialog.py
  12. 381
      lib/account.py
  13. 217
      lib/base_wizard.py
  14. 12
      lib/commands.py
  15. 8
      lib/daemon.py
  16. 701
      lib/keystore.py
  17. 74
      lib/plugins.py
  18. 253
      lib/storage.py
  19. 2
      lib/synchronizer.py
  20. 17
      lib/transaction.py
  21. 1321
      lib/wallet.py
  22. 3
      plugins/cosigner_pool/qt.py
  23. 1
      plugins/hw_wallet/__init__.py
  24. 95
      plugins/hw_wallet/hw_wallet.py
  25. 33
      plugins/hw_wallet/plugin.py
  26. 4
      plugins/keepkey/__init__.py
  27. 6
      plugins/keepkey/keepkey.py
  28. 4
      plugins/ledger/__init__.py
  29. 8
      plugins/ledger/ledger.py
  30. 4
      plugins/trezor/__init__.py
  31. 23
      plugins/trezor/clientbase.py
  32. 116
      plugins/trezor/plugin.py
  33. 35
      plugins/trezor/qt_generic.py
  34. 7
      plugins/trezor/trezor.py
  35. 173
      plugins/trustedcoin/trustedcoin.py

14
gui/kivy/main_window.py

@ -425,7 +425,7 @@ class ElectrumWindow(App):
Logger.debug('Electrum: Wallet not found. Launching install wizard') Logger.debug('Electrum: Wallet not found. Launching install wizard')
wizard = Factory.InstallWizard(self.electrum_config, self.network, path) wizard = Factory.InstallWizard(self.electrum_config, self.network, path)
wizard.bind(on_wizard_complete=self.on_wizard_complete) wizard.bind(on_wizard_complete=self.on_wizard_complete)
action = wizard.get_action() action = wizard.storage.get_action()
wizard.run(action) wizard.run(action)
def on_stop(self): def on_stop(self):
@ -562,7 +562,7 @@ class ElectrumWindow(App):
elif server_lag > 1: elif server_lag > 1:
status = _("Server lagging (%d blocks)"%server_lag) status = _("Server lagging (%d blocks)"%server_lag)
else: else:
c, u, x = self.wallet.get_account_balance(self.current_account) c, u, x = self.wallet.get_balance(self.current_account)
text = self.format_amount(c+x+u) text = self.format_amount(c+x+u)
status = str(text.strip() + ' ' + self.base_unit) status = str(text.strip() + ' ' + self.base_unit)
else: else:
@ -749,7 +749,7 @@ class ElectrumWindow(App):
popup.open() popup.open()
def protected(self, msg, f, args): def protected(self, msg, f, args):
if self.wallet.use_encryption: if self.wallet.has_password():
self.password_dialog(msg, f, args) self.password_dialog(msg, f, args)
else: else:
apply(f, args + (None,)) apply(f, args + (None,))
@ -769,7 +769,7 @@ class ElectrumWindow(App):
wallet_path = self.get_wallet_path() wallet_path = self.get_wallet_path()
dirname = os.path.dirname(wallet_path) dirname = os.path.dirname(wallet_path)
basename = os.path.basename(wallet_path) basename = os.path.basename(wallet_path)
if self.wallet.use_encryption: if self.wallet.has_password():
try: try:
self.wallet.check_password(pw) self.wallet.check_password(pw)
except: except:
@ -787,7 +787,7 @@ class ElectrumWindow(App):
self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label,)) self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label,))
def _show_seed(self, label, password): def _show_seed(self, label, password):
if self.wallet.use_encryption and password is None: if self.wallet.has_password() and password is None:
return return
try: try:
seed = self.wallet.get_seed(password) seed = self.wallet.get_seed(password)
@ -797,13 +797,13 @@ class ElectrumWindow(App):
label.text = _('Seed') + ':\n' + seed label.text = _('Seed') + ':\n' + seed
def change_password(self, cb): def change_password(self, cb):
if self.wallet.use_encryption: if self.wallet.has_password():
self.protected(_("Changing PIN code.") + '\n' + _("Enter your current PIN:"), self._change_password, (cb,)) self.protected(_("Changing PIN code.") + '\n' + _("Enter your current PIN:"), self._change_password, (cb,))
else: else:
self._change_password(cb, None) self._change_password(cb, None)
def _change_password(self, cb, old_password): def _change_password(self, cb, old_password):
if self.wallet.use_encryption: if self.wallet.has_password():
if old_password is None: if old_password is None:
return return
try: try:

4
gui/kivy/uix/dialogs/installwizard.py

@ -742,7 +742,7 @@ class InstallWizard(BaseWizard, Widget):
def request_password(self, run_next): def request_password(self, run_next):
def callback(pin): def callback(pin):
if pin: if pin:
self.run('confirm_password', (pin, run_next)) self.run('confirm_password', pin, run_next)
else: else:
run_next(None) run_next(None)
self.password_dialog('Choose a PIN code', callback) self.password_dialog('Choose a PIN code', callback)
@ -753,7 +753,7 @@ class InstallWizard(BaseWizard, Widget):
run_next(pin) run_next(pin)
else: else:
self.show_error(_('PIN mismatch')) self.show_error(_('PIN mismatch'))
self.run('request_password', (run_next,)) self.run('request_password', run_next)
self.password_dialog('Confirm your PIN code', callback) self.password_dialog('Confirm your PIN code', callback)
def action_dialog(self, action, run_next): def action_dialog(self, action, run_next):

2
gui/kivy/uix/screens.py

@ -331,7 +331,7 @@ class ReceiveScreen(CScreen):
def get_new_address(self): def get_new_address(self):
if not self.app.wallet: if not self.app.wallet:
return False return False
addr = self.app.wallet.get_unused_address(None) addr = self.app.wallet.get_unused_address()
if addr is None: if addr is None:
return False return False
self.clear() self.clear()

4
gui/qt/__init__.py

@ -163,8 +163,8 @@ class ElectrumGui:
wallet = wizard.run_and_get_wallet() wallet = wizard.run_and_get_wallet()
if not wallet: if not wallet:
return return
if wallet.get_action(): #if wallet.get_action():
return # return
self.daemon.add_wallet(wallet) self.daemon.add_wallet(wallet)
w = self.create_window_for_wallet(wallet) w = self.create_window_for_wallet(wallet)
if uri: if uri:

46
gui/qt/address_list.py

@ -41,26 +41,14 @@ class AddressList(MyTreeWidget):
def on_update(self): def on_update(self):
self.wallet = self.parent.wallet self.wallet = self.parent.wallet
self.accounts_expanded = self.wallet.storage.get('accounts_expanded', {})
item = self.currentItem() item = self.currentItem()
current_address = item.data(0, Qt.UserRole).toString() if item else None current_address = item.data(0, Qt.UserRole).toString() if item else None
self.clear() self.clear()
accounts = self.wallet.get_accounts() receiving_addresses = self.wallet.get_receiving_addresses()
if self.parent.current_account is None: change_addresses = self.wallet.get_change_addresses()
account_items = sorted(accounts.items()) if True:
else: account_item = self
account_items = [(self.parent.current_account, accounts.get(self.parent.current_account))] sequences = [0,1] if change_addresses else [0]
for k, account in account_items:
if len(accounts) > 1:
name = self.wallet.get_account_name(k)
c, u, x = self.wallet.get_account_balance(k)
account_item = QTreeWidgetItem([ name, '', self.parent.format_amount(c + u + x), ''])
account_item.setData(0, Qt.UserRole, k)
self.addTopLevelItem(account_item)
account_item.setExpanded(self.accounts_expanded.get(k, True))
else:
account_item = self
sequences = [0,1] if account.has_change() else [0]
for is_change in sequences: for is_change in sequences:
if len(sequences) > 1: if len(sequences) > 1:
name = _("Receiving") if not is_change else _("Change") name = _("Receiving") if not is_change else _("Change")
@ -72,7 +60,7 @@ class AddressList(MyTreeWidget):
seq_item = account_item seq_item = account_item
used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] ) used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
used_flag = False used_flag = False
addr_list = account.get_addresses(is_change) addr_list = change_addresses if is_change else receiving_addresses
for address in addr_list: for address in addr_list:
num = len(self.wallet.history.get(address,[])) num = len(self.wallet.history.get(address,[]))
is_used = self.wallet.is_used(address) is_used = self.wallet.is_used(address)
@ -85,7 +73,7 @@ class AddressList(MyTreeWidget):
address_item.setData(0, Qt.UserRole+1, True) # label can be edited address_item.setData(0, Qt.UserRole+1, True) # label can be edited
if self.wallet.is_frozen(address): if self.wallet.is_frozen(address):
address_item.setBackgroundColor(0, QColor('lightblue')) address_item.setBackgroundColor(0, QColor('lightblue'))
if self.wallet.is_beyond_limit(address, account, is_change): if self.wallet.is_beyond_limit(address, is_change):
address_item.setBackgroundColor(0, QColor('red')) address_item.setBackgroundColor(0, QColor('red'))
if is_used: if is_used:
if not used_flag: if not used_flag:
@ -107,8 +95,9 @@ class AddressList(MyTreeWidget):
address_item.addChild(utxo_item) address_item.addChild(utxo_item)
def create_menu(self, position): def create_menu(self, position):
from electrum.wallet import Multisig_Wallet from electrum.wallet import Multisig_Wallet, Imported_Wallet
is_multisig = isinstance(self.wallet, Multisig_Wallet) is_multisig = isinstance(self.wallet, Multisig_Wallet)
is_imported = isinstance(self.wallet, Imported_Wallet)
selected = self.selectedItems() selected = self.selectedItems()
multi_select = len(selected) > 1 multi_select = len(selected) > 1
addrs = [unicode(item.text(0)) for item in selected] addrs = [unicode(item.text(0)) for item in selected]
@ -142,7 +131,7 @@ class AddressList(MyTreeWidget):
if not is_multisig and not self.wallet.is_watching_only(): if not is_multisig and not self.wallet.is_watching_only():
menu.addAction(_("Sign/verify message"), lambda: self.parent.sign_verify_message(addr)) menu.addAction(_("Sign/verify message"), lambda: self.parent.sign_verify_message(addr))
menu.addAction(_("Encrypt/decrypt message"), lambda: self.parent.encrypt_message(addr)) menu.addAction(_("Encrypt/decrypt message"), lambda: self.parent.encrypt_message(addr))
if self.wallet.is_imported(addr): if is_imported:
menu.addAction(_("Remove from wallet"), lambda: self.parent.delete_imported_key(addr)) menu.addAction(_("Remove from wallet"), lambda: self.parent.delete_imported_key(addr))
addr_URL = block_explorer_URL(self.config, 'addr', addr) addr_URL = block_explorer_URL(self.config, 'addr', addr)
if addr_URL: if addr_URL:
@ -161,18 +150,3 @@ class AddressList(MyTreeWidget):
run_hook('receive_menu', menu, addrs, self.wallet) run_hook('receive_menu', menu, addrs, self.wallet)
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
def create_account_menu(self, position, k, item):
menu = QMenu()
exp = item.isExpanded()
menu.addAction(_("Minimize") if exp else _("Maximize"), lambda: self.set_account_expanded(item, k, not exp))
menu.addAction(_("Rename"), lambda: self.parent.edit_account_label(k))
if self.wallet.seed_version > 4:
menu.addAction(_("View details"), lambda: self.parent.show_account_details(k))
menu.exec_(self.viewport().mapToGlobal(position))
def set_account_expanded(self, item, k, b):
item.setExpanded(b)
self.accounts_expanded[k] = b
def on_close(self):
self.wallet.storage.put('accounts_expanded', self.accounts_expanded)

2
gui/qt/history_list.py

@ -61,7 +61,7 @@ class HistoryList(MyTreeWidget):
def get_domain(self): def get_domain(self):
'''Replaced in address_dialog.py''' '''Replaced in address_dialog.py'''
return self.wallet.get_account_addresses(self.parent.current_account) return self.wallet.get_addresses()
def on_update(self): def on_update(self):
self.wallet = self.parent.wallet self.wallet = self.parent.wallet

40
gui/qt/installwizard.py

@ -1,4 +1,5 @@
import sys import sys
import os
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *
@ -156,22 +157,47 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
if self.config.get('auto_connect') is None: if self.config.get('auto_connect') is None:
self.choose_server(self.network) self.choose_server(self.network)
action = self.get_action() path = self.storage.path
if action != 'new': if self.storage.requires_split():
self.hide()
msg = _("The wallet '%s' contains multiple accounts, which are no longer supported in Electrum 2.7.\n\n"
"Do you want to split your wallet into multiple files?"%path)
if not self.question(msg):
return
file_list = '\n'.join(self.storage.split_accounts())
msg = _('Your accounts have been moved to:\n %s.\n\nDo you want to delete the old file:\n%s' % (file_list, path))
if self.question(msg):
os.remove(path)
self.show_warning(_('The file was removed'))
return
if self.storage.requires_upgrade():
self.hide()
msg = _("The format of your wallet '%s' must be upgraded for Electrum. This change will not be backward compatible"%path)
if not self.question(msg):
return
self.storage.upgrade()
self.show_warning(_('Your wallet was upgraded successfully'))
self.wallet = Wallet(self.storage)
self.terminate()
return self.wallet
action = self.storage.get_action()
if action and action != 'new':
self.hide() self.hide()
path = self.storage.path
msg = _("The file '%s' contains an incompletely created wallet.\n" msg = _("The file '%s' contains an incompletely created wallet.\n"
"Do you want to complete its creation now?") % path "Do you want to complete its creation now?") % path
if not self.question(msg): if not self.question(msg):
if self.question(_("Do you want to delete '%s'?") % path): if self.question(_("Do you want to delete '%s'?") % path):
import os
os.remove(path) os.remove(path)
self.show_warning(_('The file was removed')) self.show_warning(_('The file was removed'))
return
return return
self.show() self.show()
self.run(action) if action:
return self.wallet # self.wallet is set in run
self.run(action)
return self.wallet
def finished(self): def finished(self):
'''Ensure the dialog is closed.''' '''Ensure the dialog is closed.'''

150
gui/qt/main_window.py

@ -51,7 +51,7 @@ from electrum.util import (block_explorer, block_explorer_info, format_time,
from electrum import Transaction, mnemonic from electrum import Transaction, mnemonic
from electrum import util, bitcoin, commands, coinchooser from electrum import util, bitcoin, commands, coinchooser
from electrum import SimpleConfig, paymentrequest from electrum import SimpleConfig, paymentrequest
from electrum.wallet import Wallet, BIP32_RD_Wallet, Multisig_Wallet from electrum.wallet import Wallet, Multisig_Wallet
from amountedit import BTCAmountEdit, MyLineEdit, BTCkBEdit from amountedit import BTCAmountEdit, MyLineEdit, BTCkBEdit
from network_dialog import NetworkDialog from network_dialog import NetworkDialog
@ -248,21 +248,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
t.setDaemon(True) t.setDaemon(True)
t.start() t.start()
def update_account_selector(self):
# account selector
accounts = self.wallet.get_account_names()
self.account_selector.clear()
if len(accounts) > 1:
self.account_selector.addItems([_("All accounts")] + accounts.values())
self.account_selector.setCurrentIndex(0)
self.account_selector.show()
else:
self.account_selector.hide()
def close_wallet(self): def close_wallet(self):
if self.wallet: if self.wallet:
self.print_error('close_wallet', self.wallet.storage.path) self.print_error('close_wallet', self.wallet.storage.path)
self.address_list.on_close()
run_hook('close_wallet', self.wallet) run_hook('close_wallet', self.wallet)
def load_wallet(self, wallet): def load_wallet(self, wallet):
@ -270,13 +258,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.wallet = wallet self.wallet = wallet
self.update_recently_visited(wallet.storage.path) self.update_recently_visited(wallet.storage.path)
# address used to create a dummy transaction and estimate transaction fee # address used to create a dummy transaction and estimate transaction fee
self.current_account = self.wallet.storage.get("current_account", None)
self.history_list.update() self.history_list.update()
self.need_update.set() self.need_update.set()
# Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
self.notify_transactions() self.notify_transactions()
# update menus # update menus
self.update_new_account_menu()
self.seed_menu.setEnabled(self.wallet.has_seed()) self.seed_menu.setEnabled(self.wallet.has_seed())
self.mpk_menu.setEnabled(self.wallet.is_deterministic()) self.mpk_menu.setEnabled(self.wallet.is_deterministic())
self.update_lock_icon() self.update_lock_icon()
@ -391,8 +377,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
wallet_menu = menubar.addMenu(_("&Wallet")) wallet_menu = menubar.addMenu(_("&Wallet"))
wallet_menu.addAction(_("&New contact"), self.new_contact_dialog) wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
wallet_menu.addSeparator() wallet_menu.addSeparator()
self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog) self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
@ -569,7 +553,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
text = _("Server is lagging (%d blocks)"%server_lag) text = _("Server is lagging (%d blocks)"%server_lag)
icon = QIcon(":icons/status_lagging.png") icon = QIcon(":icons/status_lagging.png")
else: else:
c, u, x = self.wallet.get_account_balance(self.current_account) c, u, x = self.wallet.get_balance()
text = _("Balance" ) + ": %s "%(self.format_amount_and_units(c)) text = _("Balance" ) + ": %s "%(self.format_amount_and_units(c))
if u: if u:
text += " [%s unconfirmed]"%(self.format_amount(u, True).strip()) text += " [%s unconfirmed]"%(self.format_amount(u, True).strip())
@ -593,8 +577,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.update_status() self.update_status()
if self.wallet.up_to_date or not self.network or not self.network.is_connected(): if self.wallet.up_to_date or not self.network or not self.network.is_connected():
self.update_tabs() self.update_tabs()
if self.wallet.up_to_date:
self.check_next_account()
def update_tabs(self): def update_tabs(self):
self.history_list.update() self.history_list.update()
@ -788,7 +770,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.saved = True self.saved = True
def new_payment_request(self): def new_payment_request(self):
addr = self.wallet.get_unused_address(self.current_account) addr = self.wallet.get_unused_address(None)
if addr is None: if addr is None:
from electrum.wallet import Imported_Wallet from electrum.wallet import Imported_Wallet
if isinstance(self.wallet, Imported_Wallet): if isinstance(self.wallet, Imported_Wallet):
@ -796,7 +778,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
return return
if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")): if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
return return
addr = self.wallet.create_new_address(self.current_account, False) addr = self.wallet.create_new_address(None, False)
self.set_receive_address(addr) self.set_receive_address(addr)
self.expires_label.hide() self.expires_label.hide()
self.expires_combo.show() self.expires_combo.show()
@ -809,7 +791,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.receive_amount_e.setAmount(None) self.receive_amount_e.setAmount(None)
def clear_receive_tab(self): def clear_receive_tab(self):
addr = self.wallet.get_unused_address(self.current_account) addr = self.wallet.get_unused_address()
self.receive_address_e.setText(addr if addr else '') self.receive_address_e.setText(addr if addr else '')
self.receive_message_e.setText('') self.receive_message_e.setText('')
self.receive_amount_e.setAmount(None) self.receive_amount_e.setAmount(None)
@ -1102,7 +1084,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def request_password(self, *args, **kwargs): def request_password(self, *args, **kwargs):
parent = self.top_level_window() parent = self.top_level_window()
password = None password = None
while self.wallet.use_encryption: while self.wallet.has_password():
password = self.password_dialog(parent=parent) password = self.password_dialog(parent=parent)
try: try:
if password: if password:
@ -1208,7 +1190,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if tx.get_fee() >= self.config.get('confirm_fee', 100000): if tx.get_fee() >= self.config.get('confirm_fee', 100000):
msg.append(_('Warning')+ ': ' + _("The fee for this transaction seems unusually high.")) msg.append(_('Warning')+ ': ' + _("The fee for this transaction seems unusually high."))
if self.wallet.use_encryption: if self.wallet.has_password():
msg.append("") msg.append("")
msg.append(_("Enter your password to proceed")) msg.append(_("Enter your password to proceed"))
password = self.password_dialog('\n'.join(msg)) password = self.password_dialog('\n'.join(msg))
@ -1237,7 +1219,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
'''Sign the transaction in a separate thread. When done, calls '''Sign the transaction in a separate thread. When done, calls
the callback with a success code of True or False. the callback with a success code of True or False.
''' '''
if self.wallet.use_encryption and not password: if self.wallet.has_password() and not password:
callback(False) # User cancelled password input callback(False) # User cancelled password input
return return
@ -1438,7 +1420,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.pay_from: if self.pay_from:
return self.pay_from return self.pay_from
else: else:
domain = self.wallet.get_account_addresses(self.current_account) domain = self.wallet.get_addresses()
return self.wallet.get_spendable_coins(domain) return self.wallet.get_spendable_coins(domain)
@ -1561,18 +1543,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
console.updateNamespace(methods) console.updateNamespace(methods)
def change_account(self,s):
if s == _("All accounts"):
self.current_account = None
else:
accounts = self.wallet.get_account_names()
for k, v in accounts.items():
if v == s:
self.current_account = k
self.history_list.update()
self.update_status()
self.address_list.update()
self.request_list.update()
def create_status_bar(self): def create_status_bar(self):
@ -1583,11 +1553,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.balance_label = QLabel("") self.balance_label = QLabel("")
sb.addWidget(self.balance_label) sb.addWidget(self.balance_label)
self.account_selector = QComboBox()
self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
self.connect(self.account_selector, SIGNAL("activated(QString)"), self.change_account)
sb.addPermanentWidget(self.account_selector)
self.search_box = QLineEdit() self.search_box = QLineEdit()
self.search_box.textChanged.connect(self.do_search) self.search_box.textChanged.connect(self.do_search)
self.search_box.hide() self.search_box.hide()
@ -1606,7 +1571,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.setStatusBar(sb) self.setStatusBar(sb)
def update_lock_icon(self): def update_lock_icon(self):
icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png") icon = QIcon(":icons/lock.png") if self.wallet.has_password() else QIcon(":icons/unlock.png")
self.password_button.setIcon(icon) self.password_button.setIcon(icon)
def update_buttons_on_seed(self): def update_buttons_on_seed(self):
@ -1619,7 +1584,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
msg = (_('Your wallet is encrypted. Use this dialog to change your ' msg = (_('Your wallet is encrypted. Use this dialog to change your '
'password. To disable wallet encryption, enter an empty new ' 'password. To disable wallet encryption, enter an empty new '
'password.') if self.wallet.use_encryption 'password.') if self.wallet.has_password()
else _('Your wallet keys are not encrypted')) else _('Your wallet keys are not encrypted'))
d = PasswordDialog(self, self.wallet, msg, PW_CHANGE) d = PasswordDialog(self, self.wallet, msg, PW_CHANGE)
ok, password, new_password = d.run() ok, password, new_password = d.run()
@ -1684,48 +1649,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.set_contact(unicode(line2.text()), str(line1.text())): if self.set_contact(unicode(line2.text()), str(line1.text())):
self.tabs.setCurrentIndex(4) self.tabs.setCurrentIndex(4)
def update_new_account_menu(self):
self.new_account_menu.setVisible(self.wallet.can_create_accounts())
self.new_account_menu.setEnabled(self.wallet.permit_account_naming())
self.update_account_selector()
def new_account_dialog(self):
dialog = WindowModalDialog(self, _("New Account Name"))
vbox = QVBoxLayout()
msg = _("Enter a name to give the account. You will not be "
"permitted to create further accounts until the new account "
"receives at least one transaction.") + "\n"
label = QLabel(msg)
label.setWordWrap(True)
vbox.addWidget(label)
e = QLineEdit()
vbox.addWidget(e)
vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
dialog.setLayout(vbox)
if dialog.exec_():
self.wallet.set_label(self.wallet.last_account_id(), str(e.text()))
self.address_list.update()
self.tabs.setCurrentIndex(3)
self.update_new_account_menu()
def check_next_account(self):
if self.wallet.needs_next_account() and not self.checking_accounts:
self.checking_accounts = True
msg = _("All the accounts in your wallet have received "
"transactions. Electrum must check whether more "
"accounts exist; one will only be shown if "
"it has been used or you give it a name.")
self.show_message(msg, title=_("Check Accounts"))
self.create_next_account()
@protected
def create_next_account(self, password):
def on_done():
self.checking_accounts = False
self.update_new_account_menu()
task = partial(self.wallet.create_next_account, password)
self.wallet.thread.add(task, on_done=on_done)
def show_master_public_keys(self): def show_master_public_keys(self):
dialog = WindowModalDialog(self, "Master Public Keys") dialog = WindowModalDialog(self, "Master Public Keys")
mpk_dict = self.wallet.get_master_public_keys() mpk_dict = self.wallet.get_master_public_keys()
@ -1741,7 +1664,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if len(mpk_dict) > 1: if len(mpk_dict) > 1:
def label(key): def label(key):
if isinstance(self.wallet, Multisig_Wallet): if isinstance(self.wallet, Multisig_Wallet):
is_mine = self.wallet.master_private_keys.has_key(key) is_mine = False#self.wallet.master_private_keys.has_key(key)
mine_text = [_("cosigner"), _("self")] mine_text = [_("cosigner"), _("self")]
return "%s (%s)" % (key, mine_text[is_mine]) return "%s (%s)" % (key, mine_text[is_mine])
return key return key
@ -1759,19 +1682,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
@protected @protected
def show_seed_dialog(self, password): def show_seed_dialog(self, password):
if self.wallet.use_encryption and password is None: if self.wallet.has_password() and password is None:
return # User cancelled password input # User cancelled password input
return
if not self.wallet.has_seed(): if not self.wallet.has_seed():
self.show_message(_('This wallet has no seed')) self.show_message(_('This wallet has no seed'))
return return
try: try:
mnemonic = self.wallet.get_mnemonic(password) mnemonic = self.wallet.get_mnemonic(password)
except BaseException as e: except BaseException as e:
self.show_error(str(e)) self.show_error(str(e))
return return
from seed_dialog import SeedDialog from seed_dialog import SeedDialog
d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys()) d = SeedDialog(self, mnemonic)
d.exec_() d.exec_()
@ -1795,9 +1718,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
d.setMinimumSize(600, 200) d.setMinimumSize(600, 200)
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.addWidget( QLabel(_("Address") + ': ' + address)) vbox.addWidget( QLabel(_("Address") + ': ' + address))
if isinstance(self.wallet, BIP32_RD_Wallet): #if isinstance(self.wallet, BIP32_RD_Wallet):
derivation = self.wallet.address_id(address) # derivation = self.wallet.address_id(address)
vbox.addWidget(QLabel(_("Derivation") + ': ' + derivation)) # vbox.addWidget(QLabel(_("Derivation") + ': ' + derivation))
vbox.addWidget(QLabel(_("Public key") + ':')) vbox.addWidget(QLabel(_("Public key") + ':'))
keys_e = ShowQRTextEdit(text='\n'.join(pubkey_list)) keys_e = ShowQRTextEdit(text='\n'.join(pubkey_list))
keys_e.addCopyButton(self.app) keys_e.addCopyButton(self.app)
@ -2045,7 +1968,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.wallet.is_watching_only(): if self.wallet.is_watching_only():
self.show_message(_("This is a watching-only wallet")) self.show_message(_("This is a watching-only wallet"))
return return
try: try:
self.wallet.check_password(password) self.wallet.check_password(password)
except Exception as e: except Exception as e:
@ -2235,7 +2157,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
keys_e.setTabChangesFocus(True) keys_e.setTabChangesFocus(True)
vbox.addWidget(keys_e) vbox.addWidget(keys_e)
addresses = self.wallet.get_unused_addresses(self.current_account) addresses = self.wallet.get_unused_addresses(None)
h, address_e = address_field(addresses) h, address_e = address_field(addresses)
vbox.addLayout(h) vbox.addLayout(h)
@ -2271,19 +2193,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
@protected @protected
def do_import_privkey(self, password): def do_import_privkey(self, password):
if not self.wallet.has_imported_keys(): if not self.wallet.keystore.can_import():
if not self.question('<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \ return
+ _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
+ _('Are you sure you understand what you are doing?'), title=_('Warning')):
return
text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import")) text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
if not text: return if not text:
return
text = str(text).split() text = str(text).split()
badkeys = [] badkeys = []
addrlist = [] addrlist = []
for key in text: for key in text:
addr = self.wallet.import_key(key, password)
try: try:
addr = self.wallet.import_key(key, password) addr = self.wallet.import_key(key, password)
except Exception as e: except Exception as e:
@ -2673,25 +2592,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
vbox.addLayout(Buttons(CloseButton(d))) vbox.addLayout(Buttons(CloseButton(d)))
d.exec_() d.exec_()
def show_account_details(self, k):
account = self.wallet.accounts[k]
d = WindowModalDialog(self, _('Account Details'))
vbox = QVBoxLayout(d)
name = self.wallet.get_account_name(k)
label = QLabel('Name: ' + name)
vbox.addWidget(label)
vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
vbox.addWidget(QLabel(_('Master Public Key:')))
text = QTextEdit()
text.setReadOnly(True)
text.setMaximumHeight(170)
vbox.addWidget(text)
mpk_text = '\n'.join(account.get_master_pubkeys())
text.setText(mpk_text)
vbox.addLayout(Buttons(CloseButton(d)))
d.exec_()
def bump_fee_dialog(self, tx): def bump_fee_dialog(self, tx):
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
d = WindowModalDialog(self, _('Bump Fee')) d = WindowModalDialog(self, _('Bump Fee'))

2
gui/qt/password_dialog.py

@ -94,7 +94,7 @@ class PasswordLayout(object):
m1 = _('New Password:') if kind == PW_NEW else _('Password:') m1 = _('New Password:') if kind == PW_NEW else _('Password:')
msgs = [m1, _('Confirm Password:')] msgs = [m1, _('Confirm Password:')]
if wallet and wallet.use_encryption: if wallet and wallet.has_password():
grid.addWidget(QLabel(_('Current Password:')), 0, 0) grid.addWidget(QLabel(_('Current Password:')), 0, 0)
grid.addWidget(self.pw, 0, 1) grid.addWidget(self.pw, 0, 1)
lockfile = ":icons/lock.png" lockfile = ":icons/lock.png"

19
gui/qt/request_list.py

@ -36,20 +36,19 @@ from util import MyTreeWidget, pr_tooltips, pr_icons
class RequestList(MyTreeWidget): class RequestList(MyTreeWidget):
def __init__(self, parent): def __init__(self, parent):
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Account'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 4) MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 3)
self.currentItemChanged.connect(self.item_changed) self.currentItemChanged.connect(self.item_changed)
self.itemClicked.connect(self.item_changed) self.itemClicked.connect(self.item_changed)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.setColumnWidth(0, 180) self.setColumnWidth(0, 180)
self.hideColumn(1) self.hideColumn(1)
self.hideColumn(2)
def item_changed(self, item): def item_changed(self, item):
if item is None: if item is None:
return return
if not self.isItemSelected(item): if not self.isItemSelected(item):
return return
addr = str(item.text(2)) addr = str(item.text(1))
req = self.wallet.receive_requests[addr] req = self.wallet.receive_requests[addr]
expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never') expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never')
amount = req['amount'] amount = req['amount']
@ -72,13 +71,10 @@ class RequestList(MyTreeWidget):
self.parent.expires_label.hide() self.parent.expires_label.hide()
self.parent.expires_combo.show() self.parent.expires_combo.show()
# check if it is necessary to show the account
self.setColumnHidden(1, len(self.wallet.get_accounts()) == 1)
# update the receive address if necessary # update the receive address if necessary
current_address = self.parent.receive_address_e.text() current_address = self.parent.receive_address_e.text()
domain = self.wallet.get_account_addresses(self.parent.current_account, include_change=False) domain = self.wallet.get_receiving_addresses()
addr = self.wallet.get_unused_address(self.parent.current_account) addr = self.wallet.get_unused_address()
if not current_address in domain and addr: if not current_address in domain and addr:
self.parent.set_receive_address(addr) self.parent.set_receive_address(addr)
self.parent.new_request_button.setEnabled(addr != current_address) self.parent.new_request_button.setEnabled(addr != current_address)
@ -98,11 +94,10 @@ class RequestList(MyTreeWidget):
signature = req.get('sig') signature = req.get('sig')
requestor = req.get('name', '') requestor = req.get('name', '')
amount_str = self.parent.format_amount(amount) if amount else "" amount_str = self.parent.format_amount(amount) if amount else ""
account = '' item = QTreeWidgetItem([date, address, '', message, amount_str, pr_tooltips.get(status,'')])
item = QTreeWidgetItem([date, account, address, '', message, amount_str, pr_tooltips.get(status,'')])
if signature is not None: if signature is not None:
item.setIcon(3, QIcon(":icons/seal.png")) item.setIcon(2, QIcon(":icons/seal.png"))
item.setToolTip(3, 'signed by '+ requestor) item.setToolTip(2, 'signed by '+ requestor)
if status is not PR_UNKNOWN: if status is not PR_UNKNOWN:
item.setIcon(6, QIcon(pr_icons.get(status))) item.setIcon(6, QIcon(pr_icons.get(status)))
self.addTopLevelItem(item) self.addTopLevelItem(item)

8
gui/qt/seed_dialog.py

@ -39,19 +39,13 @@ def icon_filename(sid):
return ":icons/seed.png" return ":icons/seed.png"
class SeedDialog(WindowModalDialog): class SeedDialog(WindowModalDialog):
def __init__(self, parent, seed, imported_keys): def __init__(self, parent, seed):
WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed'))) WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed')))
self.setMinimumWidth(400) self.setMinimumWidth(400)
vbox = QVBoxLayout(self) vbox = QVBoxLayout(self)
vbox.addLayout(SeedWarningLayout(seed).layout()) vbox.addLayout(SeedWarningLayout(seed).layout())
if imported_keys:
warning = ("<b>" + _("WARNING") + ":</b> " +
_("Your wallet contains imported keys. These keys "
"cannot be recovered from your seed.") + "</b><p>")
vbox.addWidget(WWLabel(warning))
vbox.addLayout(Buttons(CloseButton(self))) vbox.addLayout(Buttons(CloseButton(self)))
class SeedLayoutBase(object): class SeedLayoutBase(object):
def _seed_layout(self, seed=None, title=None, sid=None): def _seed_layout(self, seed=None, title=None, sid=None):
logo = QLabel() logo = QLabel()

381
lib/account.py

@ -1,381 +0,0 @@
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2013 thomasv@gitorious
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import bitcoin
from bitcoin import *
from i18n import _
from transaction import Transaction, is_extended_pubkey
from util import InvalidPassword
class Account(object):
def __init__(self, v):
self.receiving_pubkeys = v.get('receiving', [])
self.change_pubkeys = v.get('change', [])
# addresses will not be stored on disk
self.receiving_addresses = map(self.pubkeys_to_address, self.receiving_pubkeys)
self.change_addresses = map(self.pubkeys_to_address, self.change_pubkeys)
def dump(self):
return {'receiving':self.receiving_pubkeys, 'change':self.change_pubkeys}
def get_pubkey(self, for_change, n):
pubkeys_list = self.change_pubkeys if for_change else self.receiving_pubkeys
return pubkeys_list[n]
def get_address(self, for_change, n):
addr_list = self.change_addresses if for_change else self.receiving_addresses
return addr_list[n]
def get_pubkeys(self, for_change, n):
return [ self.get_pubkey(for_change, n)]
def get_addresses(self, for_change):
addr_list = self.change_addresses if for_change else self.receiving_addresses
return addr_list[:]
def derive_pubkeys(self, for_change, n):
pass
def create_new_address(self, for_change):
pubkeys_list = self.change_pubkeys if for_change else self.receiving_pubkeys
addr_list = self.change_addresses if for_change else self.receiving_addresses
n = len(pubkeys_list)
pubkeys = self.derive_pubkeys(for_change, n)
address = self.pubkeys_to_address(pubkeys)
pubkeys_list.append(pubkeys)
addr_list.append(address)
return address
def pubkeys_to_address(self, pubkey):
return public_key_to_bc_address(pubkey.decode('hex'))
def has_change(self):
return True
def get_name(self, k):
return _('Main account')
def redeem_script(self, for_change, n):
return None
def is_used(self, wallet):
addresses = self.get_addresses(False)
return any(wallet.address_is_old(a, -1) for a in addresses)
def synchronize_sequence(self, wallet, for_change):
limit = wallet.gap_limit_for_change if for_change else wallet.gap_limit
while True:
addresses = self.get_addresses(for_change)
if len(addresses) < limit:
address = self.create_new_address(for_change)
wallet.add_address(address)
continue
if map( lambda a: wallet.address_is_old(a), addresses[-limit:] ) == limit*[False]:
break
else:
address = self.create_new_address(for_change)
wallet.add_address(address)
def synchronize(self, wallet):
self.synchronize_sequence(wallet, False)
self.synchronize_sequence(wallet, True)
class ImportedAccount(Account):
def __init__(self, d):
self.keypairs = d['imported']
def synchronize(self, wallet):
return
def get_addresses(self, for_change):
return [] if for_change else sorted(self.keypairs.keys())
def get_pubkey(self, *sequence):
for_change, i = sequence
assert for_change == 0
addr = self.get_addresses(0)[i]
return self.keypairs[addr][0]
def get_xpubkeys(self, for_change, n):
return self.get_pubkeys(for_change, n)
def get_private_key(self, sequence, wallet, password):
from wallet import pw_decode
for_change, i = sequence
assert for_change == 0
address = self.get_addresses(0)[i]
pk = pw_decode(self.keypairs[address][1], password)
# this checks the password
if address != address_from_private_key(pk):
raise InvalidPassword()
return [pk]
def has_change(self):
return False
def add(self, address, pubkey, privkey, password):
from wallet import pw_encode
self.keypairs[address] = [pubkey, pw_encode(privkey, password)]
def remove(self, address):
self.keypairs.pop(address)
def dump(self):
return {'imported':self.keypairs}
def get_name(self, k):
return _('Imported keys')
def update_password(self, old_password, new_password):
for k, v in self.keypairs.items():
pubkey, a = v
b = pw_decode(a, old_password)
c = pw_encode(b, new_password)
self.keypairs[k] = (pubkey, c)
class OldAccount(Account):
""" Privatekey(type,n) = Master_private_key + H(n|S|type) """
def __init__(self, v):
Account.__init__(self, v)
self.mpk = v['mpk'].decode('hex')
@classmethod
def mpk_from_seed(klass, seed):
secexp = klass.stretch_key(seed)
master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
master_public_key = master_private_key.get_verifying_key().to_string().encode('hex')
return master_public_key
@classmethod
def stretch_key(self,seed):
oldseed = seed
for i in range(100000):
seed = hashlib.sha256(seed + oldseed).digest()
return string_to_number( seed )
@classmethod
def get_sequence(self, mpk, for_change, n):
return string_to_number( Hash( "%d:%d:"%(n,for_change) + mpk ) )
def get_address(self, for_change, n):
pubkey = self.get_pubkey(for_change, n)
address = public_key_to_bc_address( pubkey.decode('hex') )
return address
@classmethod
def get_pubkey_from_mpk(self, mpk, for_change, n):
z = self.get_sequence(mpk, for_change, n)
master_public_key = ecdsa.VerifyingKey.from_string(mpk, curve = SECP256k1)
pubkey_point = master_public_key.pubkey.point + z*SECP256k1.generator
public_key2 = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve = SECP256k1)
return '04' + public_key2.to_string().encode('hex')
def derive_pubkeys(self, for_change, n):
return self.get_pubkey_from_mpk(self.mpk, for_change, n)
def get_private_key_from_stretched_exponent(self, for_change, n, secexp):
order = generator_secp256k1.order()
secexp = ( secexp + self.get_sequence(self.mpk, for_change, n) ) % order
pk = number_to_string( secexp, generator_secp256k1.order() )
compressed = False
return SecretToASecret( pk, compressed )
def get_private_key(self, sequence, wallet, password):
seed = wallet.get_seed(password)
self.check_seed(seed)
for_change, n = sequence
secexp = self.stretch_key(seed)
pk = self.get_private_key_from_stretched_exponent(for_change, n, secexp)
return [pk]
def check_seed(self, seed):
secexp = self.stretch_key(seed)
master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
master_public_key = master_private_key.get_verifying_key().to_string()
if master_public_key != self.mpk:
print_error('invalid password (mpk)', self.mpk.encode('hex'), master_public_key.encode('hex'))
raise InvalidPassword()
return True
def get_master_pubkeys(self):
return [self.mpk.encode('hex')]
def get_type(self):
return _('Old Electrum format')
def get_xpubkeys(self, for_change, n):
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n)))
mpk = self.mpk.encode('hex')
x_pubkey = 'fe' + mpk + s
return [ x_pubkey ]
@classmethod
def parse_xpubkey(self, x_pubkey):
assert is_extended_pubkey(x_pubkey)
pk = x_pubkey[2:]
mpk = pk[0:128]
dd = pk[128:]
s = []
while dd:
n = int(bitcoin.rev_hex(dd[0:4]), 16)
dd = dd[4:]
s.append(n)
assert len(s) == 2
return mpk, s
class BIP32_Account(Account):
def __init__(self, v):
Account.__init__(self, v)
self.xpub = v['xpub']
self.xpub_receive = None
self.xpub_change = None
def dump(self):
d = Account.dump(self)
d['xpub'] = self.xpub
return d
def first_address(self):
pubkeys = self.derive_pubkeys(0, 0)
addr = self.pubkeys_to_address(pubkeys)
return addr, pubkeys
def get_master_pubkeys(self):
return [self.xpub]
@classmethod
def derive_pubkey_from_xpub(self, xpub, for_change, n):
_, _, _, c, cK = deserialize_xkey(xpub)
for i in [for_change, n]:
cK, c = CKD_pub(cK, c, i)
return cK.encode('hex')
def get_pubkey_from_xpub(self, xpub, for_change, n):
xpubs = self.get_master_pubkeys()
i = xpubs.index(xpub)
pubkeys = self.get_pubkeys(for_change, n)
return pubkeys[i]
def derive_pubkeys(self, for_change, n):
xpub = self.xpub_change if for_change else self.xpub_receive
if xpub is None:
xpub = bip32_public_derivation(self.xpub, "", "/%d"%for_change)
if for_change:
self.xpub_change = xpub
else:
self.xpub_receive = xpub
_, _, _, c, cK = deserialize_xkey(xpub)
cK, c = CKD_pub(cK, c, n)
result = cK.encode('hex')
return result
def get_private_key(self, sequence, wallet, password):
out = []
xpubs = self.get_master_pubkeys()
roots = [k for k, v in wallet.master_public_keys.iteritems() if v in xpubs]
for root in roots:
xpriv = wallet.get_master_private_key(root, password)
if not xpriv:
continue
_, _, _, c, k = deserialize_xkey(xpriv)
pk = bip32_private_key( sequence, k, c )
out.append(pk)
return out
def get_type(self):
return _('Standard 1 of 1')
def get_xpubkeys(self, for_change, n):
# unsorted
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change,n)))
xpubs = self.get_master_pubkeys()
return map(lambda xpub: 'ff' + bitcoin.DecodeBase58Check(xpub).encode('hex') + s, xpubs)
@classmethod
def parse_xpubkey(self, pubkey):
assert is_extended_pubkey(pubkey)
pk = pubkey.decode('hex')
pk = pk[1:]
xkey = bitcoin.EncodeBase58Check(pk[0:78])
dd = pk[78:]
s = []
while dd:
n = int( bitcoin.rev_hex(dd[0:2].encode('hex')), 16)
dd = dd[2:]
s.append(n)
assert len(s) == 2
return xkey, s
def get_name(self, k):
return "Main account" if k == '0' else "Account " + k
class Multisig_Account(BIP32_Account):
def __init__(self, v):
self.m = v.get('m', 2)
Account.__init__(self, v)
self.xpub_list = v['xpubs']
def dump(self):
d = Account.dump(self)
d['xpubs'] = self.xpub_list
d['m'] = self.m
return d
def get_pubkeys(self, for_change, n):
return self.get_pubkey(for_change, n)
def derive_pubkeys(self, for_change, n):
return map(lambda x: self.derive_pubkey_from_xpub(x, for_change, n), self.get_master_pubkeys())
def redeem_script(self, for_change, n):
pubkeys = self.get_pubkeys(for_change, n)
return Transaction.multisig_script(sorted(pubkeys), self.m)
def pubkeys_to_address(self, pubkeys):
redeem_script = Transaction.multisig_script(sorted(pubkeys), self.m)
address = hash_160_to_bc_address(hash_160(redeem_script.decode('hex')), 5)
return address
def get_address(self, for_change, n):
return self.pubkeys_to_address(self.get_pubkeys(for_change, n))
def get_master_pubkeys(self):
return self.xpub_list
def get_type(self):
return _('Multisig %d of %d'%(self.m, len(self.xpub_list)))

217
lib/base_wizard.py

@ -24,24 +24,21 @@
# SOFTWARE. # SOFTWARE.
import os import os
from electrum.wallet import Wallet, Multisig_Wallet, WalletStorage import keystore
from wallet import Wallet, Imported_Wallet, Standard_Wallet, Multisig_Wallet, WalletStorage
from i18n import _ from i18n import _
from plugins import run_hook
is_any_key = lambda x: Wallet.is_old_mpk(x) or Wallet.is_xprv(x) or Wallet.is_xpub(x) or Wallet.is_address(x) or Wallet.is_private_key(x)
is_private_key = lambda x: Wallet.is_xprv(x) or Wallet.is_private_key(x)
is_bip32_key = lambda x: Wallet.is_xprv(x) or Wallet.is_xpub(x)
class BaseWizard(object): class BaseWizard(object):
def __init__(self, config, network, path): def __init__(self, config, network, path):
super(BaseWizard, self).__init__() super(BaseWizard, self).__init__()
self.config = config self.config = config
self.network = network self.network = network
self.storage = WalletStorage(path) self.storage = WalletStorage(path)
self.wallet = None self.wallet = None
self.stack = [] self.stack = []
self.plugin = None
def run(self, *args): def run(self, *args):
action = args[0] action = args[0]
@ -49,27 +46,17 @@ class BaseWizard(object):
self.stack.append((action, args)) self.stack.append((action, args))
if not action: if not action:
return return
if hasattr(self.wallet, 'plugin') and hasattr(self.wallet.plugin, action): if type(action) is tuple:
f = getattr(self.wallet.plugin, action) self.plugin, action = action
apply(f, (self.wallet, self) + args) if self.plugin and hasattr(self.plugin, action):
f = getattr(self.plugin, action)
apply(f, (self,) + args)
elif hasattr(self, action): elif hasattr(self, action):
f = getattr(self, action) f = getattr(self, action)
apply(f, args) apply(f, args)
else: else:
raise BaseException("unknown action", action) raise BaseException("unknown action", action)
def get_action(self):
if self.storage.file_exists:
self.wallet = Wallet(self.storage)
action = self.wallet.get_action()
else:
action = 'new'
return action
def get_wallet(self):
if self.wallet and self.wallet.get_action() is None:
return self.wallet
def can_go_back(self): def can_go_back(self):
return len(self.stack)>1 return len(self.stack)>1
@ -91,11 +78,10 @@ class BaseWizard(object):
('standard', _("Standard wallet")), ('standard', _("Standard wallet")),
('twofactor', _("Wallet with two-factor authentication")), ('twofactor', _("Wallet with two-factor authentication")),
('multisig', _("Multi-signature wallet")), ('multisig', _("Multi-signature wallet")),
('hardware', _("Hardware wallet")),
] ]
registered_kinds = Wallet.categories() registered_kinds = Wallet.categories()
choices = [pair for pair in wallet_kinds if pair[0] in registered_kinds] choices = wallet_kinds#[pair for pair in wallet_kinds if pair[0] in registered_kinds]
self.choice_dialog(title = title, message=message, choices=choices, run_next=self.on_wallet_type) self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type)
def on_wallet_type(self, choice): def on_wallet_type(self, choice):
self.wallet_type = choice self.wallet_type = choice
@ -103,66 +89,58 @@ class BaseWizard(object):
action = 'choose_seed' action = 'choose_seed'
elif choice == 'multisig': elif choice == 'multisig':
action = 'choose_multisig' action = 'choose_multisig'
elif choice == 'hardware':
action = 'choose_hw'
elif choice == 'twofactor': elif choice == 'twofactor':
action = 'choose_seed' self.storage.put('wallet_type', '2fa')
self.storage.put('use_trustedcoin', True)
self.plugin = self.plugins.load_plugin('trustedcoin')
action = self.storage.get_action()
self.run(action) self.run(action)
def choose_multisig(self): def choose_multisig(self):
def on_multisig(m, n): def on_multisig(m, n):
self.multisig_type = "%dof%d"%(m, n) self.multisig_type = "%dof%d"%(m, n)
self.n = n
self.run('choose_seed') self.run('choose_seed')
self.multisig_dialog(run_next=on_multisig) self.multisig_dialog(run_next=on_multisig)
def choose_seed(self): def choose_seed(self):
title = _('Choose Seed') title = _('Seed and Private Keys')
message = _("Do you want to create a new seed, or to restore a wallet using an existing seed?") message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
if self.wallet_type == 'standard': if self.wallet_type in ['standard', 'multisig']:
choices = [
('create_seed', _('Create a new seed')),
('restore_seed', _('I already have a seed')),
('restore_from_key', _('Import keys')),
]
elif self.wallet_type == 'twofactor':
choices = [
('create_2fa', _('Create a new seed')),
('restore_2fa', _('I already have a seed')),
]
elif self.wallet_type == 'multisig':
choices = [ choices = [
('create_seed', _('Create a new seed')), ('create_seed', _('Create a new seed')),
('restore_seed', _('I already have a seed')), ('restore_seed', _('I already have a seed')),
('restore_from_key', _('I have a master key')), ('restore_from_key', _('Import keys or addresses')),
#('choose_hw', _('Cosign with hardware wallet')), ('choose_hw', _('Use hardware wallet')),
] ]
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run) self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
def create_2fa(self):
self.storage.put('wallet_type', '2fa')
self.wallet = Wallet(self.storage)
self.run('show_disclaimer')
def restore_seed(self): def restore_seed(self):
# TODO: return derivation password too # TODO: return derivation password too
self.restore_seed_dialog(run_next=self.add_password, is_valid=Wallet.is_seed) self.restore_seed_dialog(run_next=self.add_password, is_valid=keystore.is_seed)
def on_restore(self, text): def on_restore(self, text):
if is_private_key(text): if keystore.is_address_list(text):
self.wallet = Imported_Wallet(self.storage)
for x in text.split():
self.wallet.add_address(x)
self.terminate()
elif keystore.is_private(text):
self.add_password(text) self.add_password(text)
else: else:
self.create_wallet(text, None) self.create_keystore(text, None)
def restore_from_key(self): def restore_from_key(self):
if self.wallet_type == 'standard': if self.wallet_type == 'standard':
v = is_any_key v = keystore.is_any_key
title = _("Import keys") title = _("Import keys")
message = ' '.join([ message = ' '.join([
_("To create a watching-only wallet, please enter your master public key (xpub), or a list of Bitcoin addresses."), _("To create a watching-only wallet, please enter your master public key (xpub), or a list of Bitcoin addresses."),
_("To create a spending wallet, please enter a master private key (xprv), or a list of Bitcoin private keys.") _("To create a spending wallet, please enter a master private key (xprv), or a list of Bitcoin private keys.")
]) ])
else: else:
v = is_bip32_key v = keystore.is_bip32_key
title = _("Master public or private key") title = _("Master public or private key")
message = ' '.join([ message = ' '.join([
_("To create a watching-only wallet, please enter your master public key (xpub)."), _("To create a watching-only wallet, please enter your master public key (xpub)."),
@ -170,12 +148,8 @@ class BaseWizard(object):
]) ])
self.restore_keys_dialog(title=title, message=message, run_next=self.on_restore, is_valid=v) self.restore_keys_dialog(title=title, message=message, run_next=self.on_restore, is_valid=v)
def restore_2fa(self):
self.storage.put('wallet_type', '2fa')
self.wallet = Wallet(self.storage)
self.wallet.plugin.on_restore_wallet(self.wallet, self)
def choose_hw(self): def choose_hw(self):
self.storage.put('key_type', 'hardware')
hw_wallet_types, choices = self.plugins.hardware_wallets('create') hw_wallet_types, choices = self.plugins.hardware_wallets('create')
choices = zip(hw_wallet_types, choices) choices = zip(hw_wallet_types, choices)
title = _('Hardware wallet') title = _('Hardware wallet')
@ -189,84 +163,87 @@ class BaseWizard(object):
self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_hardware) self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_hardware)
def on_hardware(self, hw_type): def on_hardware(self, hw_type):
self.hw_type = hw_type self.storage.put('hardware_type', hw_type)
if self.wallet_type == 'multisig': title = _('Hardware wallet') + ' [%s]' % hw_type
self.create_hardware_multisig() message = _('Do you have a device, or do you want to restore a wallet using an existing seed?')
else: choices = [
title = _('Hardware wallet') + ' [%s]' % hw_type ('on_hardware_device', _('I have a %s device')%hw_type),
message = _('Do you have a device, or do you want to restore a wallet using an existing seed?') ('on_hardware_seed', _('I have a %s seed')%hw_type),
choices = [ ]
('create_hardware_wallet', _('I have a device')), self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
('restore_hardware_wallet', _('Use hardware wallet seed')),
]
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
def create_hardware_multisig(self):
self.storage.put('wallet_type', self.multisig_type)
self.wallet = Multisig_Wallet(self.storage)
# todo: get the xpub from the plugin
self.run('create_wallet', xpub, None)
def create_hardware_wallet(self):
self.storage.put('wallet_type', self.hw_type)
self.wallet = Wallet(self.storage)
self.wallet.plugin.on_create_wallet(self.wallet, self)
self.terminate()
def restore_hardware_wallet(self):
self.storage.put('wallet_type', self.wallet_type)
self.wallet = Wallet(self.storage)
self.wallet.plugin.on_restore_wallet(self.wallet, self)
self.terminate()
def create_wallet(self, text, password): def on_hardware_device(self):
from keystore import load_keystore
keystore = load_keystore(self.storage, None)
keystore.plugin.on_create_wallet(keystore, self)
self.create_wallet(keystore, None)
def on_hardware_seed(self):
from keystore import load_keystore
self.storage.put('key_type', 'hw_seed')
keystore = load_keystore(self.storage, None)
self.plugin = keystore #fixme .plugin
keystore.on_restore_wallet(self)
self.wallet = Standard_Wallet(self.storage)
self.run('create_addresses')
def create_wallet(self, k, password):
if self.wallet_type == 'standard': if self.wallet_type == 'standard':
self.wallet = Wallet.from_text(text, password, self.storage) k.save(self.storage, 'x/')
self.wallet = Standard_Wallet(self.storage)
self.run('create_addresses') self.run('create_addresses')
elif self.wallet_type == 'multisig': elif self.wallet_type == 'multisig':
self.storage.put('wallet_type', self.multisig_type) self.storage.put('wallet_type', self.multisig_type)
self.wallet = Multisig_Wallet(self.storage) self.add_cosigner(k, 0)
self.wallet.add_cosigner('x1/', text, password) xpub = k.get_master_public_key()
self.stack = [] self.stack = []
self.run('show_xpub_and_add_cosigners', (password,)) self.run('show_xpub_and_add_cosigners', password, xpub)
def show_xpub_and_add_cosigners(self, password, xpub):
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('add_cosigners', password, 1))
def show_xpub_and_add_cosigners(self, password): def add_cosigner(self, keystore, i):
xpub = self.wallet.master_public_keys.get('x1/') d = self.storage.get('master_public_keys', {})
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('add_cosigners', password)) if keystore.xpub in d.values():
raise BaseException('duplicate key')
keystore.save(self.storage, 'x%d/'%(i+1))
def add_cosigners(self, password): def add_cosigners(self, password, i):
i = self.wallet.get_missing_cosigner() self.add_cosigner_dialog(run_next=lambda x: self.on_cosigner(x, password, i), index=i, is_valid=keystore.is_xpub)
self.add_cosigner_dialog(run_next=lambda x: self.on_cosigner(x, password), index=(i-1), is_valid=Wallet.is_xpub)
def on_cosigner(self, text, password): def on_cosigner(self, text, password, i):
i = self.wallet.get_missing_cosigner() k = keystore.from_text(text, password)
try: try:
self.wallet.add_cosigner('x%d/'%i, text, password) self.add_cosigner(k, i)
except BaseException as e: except BaseException as e:
print "error:" + str(e) self.show_message("error:" + str(e))
i = self.wallet.get_missing_cosigner() return
if i: if i < self.n - 1:
self.run('add_cosigners', password) self.run('add_cosigners', password, i+1)
else: else:
self.wallet = Multisig_Wallet(self.storage)
self.create_addresses() self.create_addresses()
def create_addresses(self):
def task():
self.wallet.create_main_account()
self.wallet.synchronize()
self.wallet.storage.write()
self.terminate()
msg = _("Electrum is generating your addresses, please wait.")
self.waiting_dialog(task, msg)
def create_seed(self): def create_seed(self):
from electrum.wallet import BIP32_Wallet from electrum.mnemonic import Mnemonic
seed = BIP32_Wallet.make_seed() seed = Mnemonic('en').make_seed()
self.show_seed_dialog(run_next=self.confirm_seed, seed_text=seed) self.show_seed_dialog(run_next=self.confirm_seed, seed_text=seed)
def confirm_seed(self, seed): def confirm_seed(self, seed):
self.confirm_seed_dialog(run_next=self.add_password, is_valid=lambda x: x==seed) self.confirm_seed_dialog(run_next=self.add_password, is_valid=lambda x: x==seed)
def add_password(self, text): def add_password(self, text):
f = lambda pw: self.run('create_wallet', text, pw) f = lambda pw: self.run('create_keystore', text, pw)
self.request_password(run_next=f) self.request_password(run_next=f)
def create_keystore(self, text, password):
k = keystore.from_text(text, password)
self.create_wallet(k, password)
def create_addresses(self):
def task():
self.wallet.synchronize()
self.wallet.storage.write()
self.terminate()
msg = _("Electrum is generating your addresses, please wait.")
self.waiting_dialog(task, msg)

12
lib/commands.py

@ -300,12 +300,9 @@ class Commands:
return self.wallet.get_public_keys(address) return self.wallet.get_public_keys(address)
@command('w') @command('w')
def getbalance(self, account=None): def getbalance(self):
"""Return the balance of your wallet. """ """Return the balance of your wallet. """
if account is None: c, u, x = self.wallet.get_balance()
c, u, x = self.wallet.get_balance()
else:
c, u, x = self.wallet.get_account_balance(account)
out = {"confirmed": str(Decimal(c)/COIN)} out = {"confirmed": str(Decimal(c)/COIN)}
if u: if u:
out["unconfirmed"] = str(Decimal(u)/COIN) out["unconfirmed"] = str(Decimal(u)/COIN)
@ -357,7 +354,7 @@ class Commands:
@command('wp') @command('wp')
def getmasterprivate(self): def getmasterprivate(self):
"""Get master private key. Return your wallet\'s master private key""" """Get master private key. Return your wallet\'s master private key"""
return str(self.wallet.get_master_private_key(self.wallet.root_name, self._password)) return str(self.wallet.keystore.get_master_private_key(self._password))
@command('wp') @command('wp')
def getseed(self): def getseed(self):
@ -499,7 +496,7 @@ class Commands:
def listaddresses(self, receiving=False, change=False, show_labels=False, frozen=False, unused=False, funded=False, show_balance=False): def listaddresses(self, receiving=False, change=False, show_labels=False, frozen=False, unused=False, funded=False, show_balance=False):
"""List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results.""" """List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results."""
out = [] out = []
for addr in self.wallet.addresses(True): for addr in self.wallet.get_addresses():
if frozen and not self.wallet.is_frozen(addr): if frozen and not self.wallet.is_frozen(addr):
continue continue
if receiving and self.wallet.is_change(addr): if receiving and self.wallet.is_change(addr):
@ -681,7 +678,6 @@ command_options = {
'unsigned': ("-u", "--unsigned", "Do not sign transaction"), 'unsigned': ("-u", "--unsigned", "Do not sign transaction"),
'rbf': (None, "--rbf", "Replace-by-fee transaction"), 'rbf': (None, "--rbf", "Replace-by-fee transaction"),
'domain': ("-D", "--domain", "List of addresses"), 'domain': ("-D", "--domain", "List of addresses"),
'account': (None, "--account", "Account"),
'memo': ("-m", "--memo", "Description of the request"), 'memo': ("-m", "--memo", "Description of the request"),
'expiration': (None, "--expiration", "Time in seconds"), 'expiration': (None, "--expiration", "Time in seconds"),
'timeout': (None, "--timeout", "Timeout in seconds"), 'timeout': (None, "--timeout", "Timeout in seconds"),

8
lib/daemon.py

@ -37,7 +37,7 @@ from util import print_msg, print_error, print_stderr
from wallet import WalletStorage, Wallet from wallet import WalletStorage, Wallet
from commands import known_commands, Commands from commands import known_commands, Commands
from simple_config import SimpleConfig from simple_config import SimpleConfig
from plugins import run_hook
def get_lockfile(config): def get_lockfile(config):
return os.path.join(config.path, 'daemon') return os.path.join(config.path, 'daemon')
@ -171,16 +171,16 @@ class Daemon(DaemonThread):
return response return response
def load_wallet(self, path): def load_wallet(self, path):
# wizard will be launched if we return
if path in self.wallets: if path in self.wallets:
wallet = self.wallets[path] wallet = self.wallets[path]
return wallet return wallet
storage = WalletStorage(path) storage = WalletStorage(path)
if not storage.file_exists: if not storage.file_exists:
return return
wallet = Wallet(storage) if storage.requires_split() or storage.requires_upgrade() or storage.get_action():
action = wallet.get_action()
if action:
return return
wallet = Wallet(storage)
wallet.start_threads(self.network) wallet.start_threads(self.network)
self.wallets[path] = wallet self.wallets[path] = wallet
return wallet return wallet

701
lib/keystore.py

@ -0,0 +1,701 @@
#!/usr/bin/env python2
# -*- mode: python -*-
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2016 The Electrum developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from unicodedata import normalize
from version import *
import bitcoin
from bitcoin import pw_encode, pw_decode, bip32_root, bip32_private_derivation, bip32_public_derivation, bip32_private_key, deserialize_xkey
from bitcoin import public_key_from_private_key, public_key_to_bc_address
from bitcoin import *
from bitcoin import is_old_seed, is_new_seed
from util import PrintError, InvalidPassword
from mnemonic import Mnemonic
class KeyStore(PrintError):
def has_seed(self):
return False
def has_password(self):
return False
def is_watching_only(self):
return False
def can_import(self):
return False
class Software_KeyStore(KeyStore):
def __init__(self):
KeyStore.__init__(self)
self.use_encryption = False
def has_password(self):
return self.use_encryption
class Imported_KeyStore(Software_KeyStore):
# keystore for imported private keys
def __init__(self):
Software_KeyStore.__init__(self)
self.keypairs = {}
def is_deterministic(self):
return False
def can_change_password(self):
return True
def get_master_public_key(self):
return None
def load(self, storage, name):
self.keypairs = storage.get('keypairs', {})
self.use_encryption = storage.get('use_encryption', False)
self.receiving_pubkeys = self.keypairs.keys()
self.change_pubkeys = []
def save(self, storage, root_name):
storage.put('key_type', 'imported')
storage.put('keypairs', self.keypairs)
storage.put('use_encryption', self.use_encryption)
def can_import(self):
return True
def check_password(self, password):
self.get_private_key((0,0), password)
def import_key(self, sec, password):
if not self.can_import():
raise BaseException('This wallet cannot import private keys')
try:
pubkey = public_key_from_private_key(sec)
except Exception:
raise Exception('Invalid private key')
self.keypairs[pubkey] = sec
return pubkey
def delete_imported_key(self, key):
self.keypairs.pop(key)
def get_private_key(self, sequence, password):
for_change, i = sequence
assert for_change == 0
pubkey = (self.change_pubkeys if for_change else self.receiving_pubkeys)[i]
pk = pw_decode(self.keypairs[pubkey], password)
# this checks the password
if pubkey != public_key_from_private_key(pk):
raise InvalidPassword()
return pk
def update_password(self, old_password, new_password):
if old_password is not None:
self.check_password(old_password)
if new_password == '':
new_password = None
for k, v in self.keypairs.items():
b = pw_decode(v, old_password)
c = pw_encode(b, new_password)
self.keypairs[k] = b
self.use_encryption = (new_password is not None)
class Deterministic_KeyStore(Software_KeyStore):
def __init__(self):
Software_KeyStore.__init__(self)
self.seed = ''
def is_deterministic(self):
return True
def load(self, storage, name):
self.seed = storage.get('seed', '')
self.use_encryption = storage.get('use_encryption', False)
def save(self, storage, name):
storage.put('seed', self.seed)
storage.put('use_encryption', self.use_encryption)
def has_seed(self):
return self.seed != ''
def can_change_password(self):
return not self.is_watching_only()
def add_seed(self, seed, password):
if self.seed:
raise Exception("a seed exists")
self.seed_version, self.seed = self.format_seed(seed)
if password:
self.seed = pw_encode(self.seed, password)
self.use_encryption = (password is not None)
def get_seed(self, password):
return pw_decode(self.seed, password).encode('utf8')
class Xpub:
def __init__(self):
self.xpub = None
self.xpub_receive = None
self.xpub_change = None
def add_master_public_key(self, xpub):
self.xpub = xpub
def get_master_public_key(self):
return self.xpub
def derive_pubkey(self, for_change, n):
xpub = self.xpub_change if for_change else self.xpub_receive
if xpub is None:
xpub = bip32_public_derivation(self.xpub, "", "/%d"%for_change)
if for_change:
self.xpub_change = xpub
else:
self.xpub_receive = xpub
_, _, _, c, cK = deserialize_xkey(xpub)
cK, c = CKD_pub(cK, c, n)
result = cK.encode('hex')
return result
def get_xpubkey(self, c, i):
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (c, i)))
return 'ff' + bitcoin.DecodeBase58Check(self.xpub).encode('hex') + s
class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
root_derivation = "m/"
def __init__(self):
Xpub.__init__(self)
Deterministic_KeyStore.__init__(self)
self.xprv = None
def format_seed(self, seed):
return NEW_SEED_VERSION, ' '.join(seed.split())
def load(self, storage, name):
Deterministic_KeyStore.load(self, storage, name)
self.xpub = storage.get('master_public_keys', {}).get(name)
self.xprv = storage.get('master_private_keys', {}).get(name)
def save(self, storage, name):
Deterministic_KeyStore.save(self, storage, name)
d = storage.get('master_public_keys', {})
d[name] = self.xpub
storage.put('master_public_keys', d)
d = storage.get('master_private_keys', {})
d[name] = self.xprv
storage.put('master_private_keys', d)
def add_master_private_key(self, xprv, password):
self.xprv = pw_encode(xprv, password)
def get_master_private_key(self, password):
return pw_decode(self.xprv, password)
def check_password(self, password):
xprv = pw_decode(self.xprv, password)
if deserialize_xkey(xprv)[3] != deserialize_xkey(self.xpub)[3]:
raise InvalidPassword()
def update_password(self, old_password, new_password):
if old_password is not None:
self.check_password(old_password)
if new_password == '':
new_password = None
if self.has_seed():
decoded = self.get_seed(old_password)
self.seed = pw_encode( decoded, new_password)
if self.xprv is not None:
b = pw_decode(self.xprv, old_password)
self.xprv = pw_encode(b, new_password)
self.use_encryption = (new_password is not None)
def is_watching_only(self):
return self.xprv is None
def get_keypairs_for_sig(self, tx, password):
keypairs = {}
for txin in tx.inputs():
num_sig = txin.get('num_sig')
if num_sig is None:
continue
x_signatures = txin['signatures']
signatures = filter(None, x_signatures)
if len(signatures) == num_sig:
# input is complete
continue
for k, x_pubkey in enumerate(txin['x_pubkeys']):
if x_signatures[k] is not None:
# this pubkey already signed
continue
derivation = txin['derivation']
sec = self.get_private_key(derivation, password)
if sec:
keypairs[x_pubkey] = sec
return keypairs
def sign_transaction(self, tx, password):
# Raise if password is not correct.
self.check_password(password)
# Add private keys
keypairs = self.get_keypairs_for_sig(tx, password)
# Sign
if keypairs:
tx.sign(keypairs)
def derive_xkeys(self, root, derivation, password):
x = self.master_private_keys[root]
root_xprv = pw_decode(x, password)
xprv, xpub = bip32_private_derivation(root_xprv, root, derivation)
return xpub, xprv
def get_mnemonic(self, password):
return self.get_seed(password)
def mnemonic_to_seed(self, seed, password):
return Mnemonic.mnemonic_to_seed(seed, password)
@classmethod
def make_seed(self, lang=None):
return Mnemonic(lang).make_seed()
@classmethod
def address_derivation(self, account_id, change, address_index):
account_derivation = self.account_derivation(account_id)
return "%s/%d/%d" % (account_derivation, change, address_index)
def address_id(self, address):
acc_id, (change, address_index) = self.get_address_index(address)
return self.address_derivation(acc_id, change, address_index)
def add_seed_and_xprv(self, seed, password, passphrase=''):
xprv, xpub = bip32_root(self.mnemonic_to_seed(seed, passphrase))
xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
self.add_seed(seed, password)
self.add_master_private_key(xprv, password)
self.add_master_public_key(xpub)
def add_xprv(self, xprv, password):
xpub = bitcoin.xpub_from_xprv(xprv)
self.add_master_private_key(xprv, password)
self.add_master_public_key(xpub)
def can_sign(self, xpub):
return xpub == self.xpub and self.xprv is not None
def get_private_key(self, sequence, password):
xprv = self.get_master_private_key(password)
_, _, _, c, k = deserialize_xkey(xprv)
pk = bip32_private_key(sequence, k, c)
return pk
class Old_KeyStore(Deterministic_KeyStore):
def __init__(self):
Deterministic_KeyStore.__init__(self)
self.mpk = None
def load(self, storage, name):
Deterministic_KeyStore.load(self, storage, name)
self.mpk = storage.get('master_public_key').decode('hex')
def save(self, storage, name):
Deterministic_KeyStore.save(self, storage, name)
storage.put('wallet_type', 'old')
storage.put('master_public_key', self.mpk.encode('hex'))
def add_seed(self, seed, password):
Deterministic_KeyStore.add_seed(self, seed, password)
self.mpk = self.mpk_from_seed(self.get_seed(password))
def add_master_public_key(self, mpk):
self.mpk = mpk.decode('hex')
def format_seed(self, seed):
import old_mnemonic
# see if seed was entered as hex
seed = seed.strip()
if seed:
try:
seed.decode('hex')
return OLD_SEED_VERSION, str(seed)
except Exception:
pass
words = seed.split()
seed = old_mnemonic.mn_decode(words)
if not seed:
raise Exception("Invalid seed")
return OLD_SEED_VERSION, seed
def get_mnemonic(self, password):
import old_mnemonic
s = self.get_seed(password)
return ' '.join(old_mnemonic.mn_encode(s))
@classmethod
def mpk_from_seed(klass, seed):
secexp = klass.stretch_key(seed)
master_private_key = ecdsa.SigningKey.from_secret_exponent(secexp, curve = SECP256k1)
master_public_key = master_private_key.get_verifying_key().to_string()
return master_public_key
@classmethod
def stretch_key(self, seed):
x = seed
for i in range(100000):
x = hashlib.sha256(x + seed).digest()
return string_to_number(x)
@classmethod
def get_sequence(self, mpk, for_change, n):
return string_to_number(Hash("%d:%d:"%(n, for_change) + mpk))
def get_address(self, for_change, n):
pubkey = self.get_pubkey(for_change, n)
address = public_key_to_bc_address(pubkey.decode('hex'))
return address
@classmethod
def get_pubkey_from_mpk(self, mpk, for_change, n):
z = self.get_sequence(mpk, for_change, n)
master_public_key = ecdsa.VerifyingKey.from_string(mpk, curve = SECP256k1)
pubkey_point = master_public_key.pubkey.point + z*SECP256k1.generator
public_key2 = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve = SECP256k1)
return '04' + public_key2.to_string().encode('hex')
def derive_pubkey(self, for_change, n):
return self.get_pubkey_from_mpk(self.mpk, for_change, n)
def get_private_key_from_stretched_exponent(self, for_change, n, secexp):
order = generator_secp256k1.order()
secexp = (secexp + self.get_sequence(self.mpk, for_change, n)) % order
pk = number_to_string(secexp, generator_secp256k1.order())
compressed = False
return SecretToASecret(pk, compressed)
def get_private_key(self, sequence, password):
seed = self.get_seed(password)
self.check_seed(seed)
for_change, n = sequence
secexp = self.stretch_key(seed)
pk = self.get_private_key_from_stretched_exponent(for_change, n, secexp)
return pk
def check_seed(self, seed):
secexp = self.stretch_key(seed)
master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
master_public_key = master_private_key.get_verifying_key().to_string()
if master_public_key != self.mpk:
print_error('invalid password (mpk)', self.mpk.encode('hex'), master_public_key.encode('hex'))
raise InvalidPassword()
def check_password(self, password):
seed = self.get_seed(password)
self.check_seed(seed)
def get_master_public_key(self):
return self.mpk.encode('hex')
def get_xpubkeys(self, for_change, n):
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n)))
mpk = self.mpk.encode('hex')
x_pubkey = 'fe' + mpk + s
return [ x_pubkey ]
@classmethod
def parse_xpubkey(self, x_pubkey):
assert is_extended_pubkey(x_pubkey)
pk = x_pubkey[2:]
mpk = pk[0:128]
dd = pk[128:]
s = []
while dd:
n = int(bitcoin.rev_hex(dd[0:4]), 16)
dd = dd[4:]
s.append(n)
assert len(s) == 2
return mpk, s
def update_password(self, old_password, new_password):
if old_password is not None:
self.check_password(old_password)
if new_password == '':
new_password = None
if self.has_seed():
decoded = self.get_seed(old_password)
self.seed = pw_encode(decoded, new_password)
self.use_encryption = (new_password is not None)
class Hardware_KeyStore(KeyStore, Xpub):
# Derived classes must set:
# - device
# - DEVICE_IDS
# - wallet_type
#restore_wallet_class = BIP32_RD_Wallet
max_change_outputs = 1
def __init__(self):
Xpub.__init__(self)
KeyStore.__init__(self)
# Errors and other user interaction is done through the wallet's
# handler. The handler is per-window and preserved across
# device reconnects
self.handler = None
def is_deterministic(self):
return True
def load(self, storage, name):
self.xpub = storage.get('master_public_keys', {}).get(name)
def save(self, storage, name):
d = storage.get('master_public_keys', {})
d[name] = self.xpub
storage.put('master_public_keys', d)
def unpaired(self):
'''A device paired with the wallet was diconnected. This can be
called in any thread context.'''
self.print_error("unpaired")
def paired(self):
'''A device paired with the wallet was (re-)connected. This can be
called in any thread context.'''
self.print_error("paired")
def can_export(self):
return False
def is_watching_only(self):
'''The wallet is not watching-only; the user will be prompted for
pin and passphrase as appropriate when needed.'''
assert not self.has_seed()
return False
def can_change_password(self):
return False
def derive_xkeys(self, root, derivation, password):
if self.master_public_keys.get(self.root_name):
return BIP44_wallet.derive_xkeys(self, root, derivation, password)
# When creating a wallet we need to ask the device for the
# master public key
xpub = self.get_public_key(derivation)
return xpub, None
class BIP44_KeyStore(BIP32_KeyStore):
root_derivation = "m/44'/0'/0'"
def normalize_passphrase(self, passphrase):
return normalize('NFKD', unicode(passphrase or ''))
def is_valid_seed(self, seed):
return True
def mnemonic_to_seed(self, mnemonic, passphrase):
# See BIP39
import pbkdf2, hashlib, hmac
PBKDF2_ROUNDS = 2048
mnemonic = normalize('NFKD', ' '.join(mnemonic.split()))
passphrase = self.normalize_passphrase(passphrase)
return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase,
iterations = PBKDF2_ROUNDS, macmodule = hmac,
digestmodule = hashlib.sha512).read(64)
def on_restore_wallet(self, wizard):
#assert isinstance(keystore, self.keystore_class)
#msg = _("Enter the seed for your %s wallet:" % self.device)
#title=_('Restore hardware wallet'),
f = lambda seed: wizard.run('on_restore_seed', seed)
wizard.restore_seed_dialog(run_next=f, is_valid=self.is_valid_seed)
def on_restore_seed(self, wizard, seed):
f = lambda passphrase: wizard.run('on_restore_passphrase', seed, passphrase)
self.device = ''
wizard.request_passphrase(self.device, run_next=f)
def on_restore_passphrase(self, wizard, seed, passphrase):
f = lambda pw: wizard.run('on_restore_password', seed, passphrase, pw)
wizard.request_password(run_next=f)
def on_restore_password(self, wizard, seed, passphrase, password):
self.add_seed_and_xprv(seed, password, passphrase)
self.save(wizard.storage, 'x/')
keystores = []
def load_keystore(storage, name):
w = storage.get('wallet_type')
t = storage.get('key_type', 'seed')
seed_version = storage.get_seed_version()
if seed_version == OLD_SEED_VERSION or w == 'old':
k = Old_KeyStore()
elif t == 'imported':
k = Imported_KeyStore()
elif name and name not in [ 'x/', 'x1/' ]:
k = BIP32_KeyStore()
elif t == 'seed':
k = BIP32_KeyStore()
elif t == 'hardware':
hw_type = storage.get('hardware_type')
for cat, _type, constructor in keystores:
if cat == 'hardware' and _type == hw_type:
k = constructor()
break
else:
raise BaseException('unknown hardware type')
elif t == 'hw_seed':
k = BIP44_KeyStore()
else:
raise BaseException('unknown wallet type', t)
k.load(storage, name)
return k
def register_keystore(category, type, constructor):
keystores.append((category, type, constructor))
def is_old_mpk(mpk):
try:
int(mpk, 16)
except:
return False
return len(mpk) == 128
def is_xpub(text):
if text[0:4] != 'xpub':
return False
try:
deserialize_xkey(text)
return True
except:
return False
def is_xprv(text):
if text[0:4] != 'xprv':
return False
try:
deserialize_xkey(text)
return True
except:
return False
def is_address_list(text):
parts = text.split()
return bool(parts) and all(bitcoin.is_address(x) for x in parts)
def is_private_key_list(text):
parts = text.split()
return bool(parts) and all(bitcoin.is_private_key(x) for x in parts)
is_seed = lambda x: is_old_seed(x) or is_new_seed(x)
is_mpk = lambda x: is_old_mpk(x) or is_xpub(x)
is_private = lambda x: is_seed(x) or is_xprv(x) or is_private_key_list(x)
is_any_key = lambda x: is_old_mpk(x) or is_xprv(x) or is_xpub(x) or is_address_list(x) or is_private_key_list(x)
is_private_key = lambda x: is_xprv(x) or is_private_key_list(x)
is_bip32_key = lambda x: is_xprv(x) or is_xpub(x)
def from_seed(seed, password):
if is_old_seed(seed):
keystore = Old_KeyStore()
keystore.add_seed(seed, password)
elif is_new_seed(seed):
keystore = BIP32_KeyStore()
keystore.add_seed_and_xprv(seed, password)
return keystore
def from_private_key_list(text, password):
keystore = Imported_KeyStore()
for x in text.split():
keystore.import_key(x, None)
keystore.update_password(None, password)
return keystore
def from_old_mpk(mpk):
keystore = Old_KeyStore()
keystore.add_master_public_key(mpk)
return keystore
def from_xpub(xpub):
keystore = BIP32_KeyStore()
keystore.add_master_public_key(xpub)
return keystore
def from_xprv(xprv, password):
xpub = bitcoin.xpub_from_xprv(xprv)
keystore = BIP32_KeyStore()
keystore.add_master_private_key(xprv, password)
keystore.add_master_public_key(xpub)
return keystore
def xprv_from_seed(seed, password):
# do not store the seed, only the master xprv
xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed, ''))
#xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
return from_xprv(xprv, password)
def xpub_from_seed(seed):
# store only master xpub
xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed,''))
#xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
return from_xpub(xpub)
def from_text(text, password):
if is_xprv(text):
k = from_xprv(text, password)
elif is_old_mpk(text):
k = from_old_mpk(text)
elif is_xpub(text):
k = from_xpub(text)
elif is_private_key_list(text):
k = from_private_key_list(text, password)
elif is_seed(text):
k = from_seed(text, password)
else:
raise BaseException('Invalid seedphrase or key')
return k

74
lib/plugins.py

@ -35,6 +35,10 @@ from util import *
from i18n import _ from i18n import _
from util import profiler, PrintError, DaemonThread, UserCancelled from util import profiler, PrintError, DaemonThread, UserCancelled
plugin_loaders = {}
hook_names = set()
hooks = {}
class Plugins(DaemonThread): class Plugins(DaemonThread):
@ -66,15 +70,17 @@ class Plugins(DaemonThread):
continue continue
details = d.get('registers_wallet_type') details = d.get('registers_wallet_type')
if details: if details:
self.register_plugin_wallet(name, gui_good, details) self.register_wallet_type(name, gui_good, details)
details = d.get('registers_keystore')
if details:
self.register_keystore(name, gui_good, details)
self.descriptions[name] = d self.descriptions[name] = d
if not d.get('requires_wallet_type') and self.config.get('use_' + name): if not d.get('requires_wallet_type') and self.config.get('use_' + name):
try: try:
self.load_plugin(name) self.load_plugin(name)
except BaseException as e: except BaseException as e:
traceback.print_exc(file=sys.stdout) traceback.print_exc(file=sys.stdout)
self.print_error("cannot initialize plugin %s:" % name, self.print_error("cannot initialize plugin %s:" % name, str(e))
str(e))
def get(self, name): def get(self, name):
return self.plugins.get(name) return self.plugins.get(name)
@ -83,6 +89,8 @@ class Plugins(DaemonThread):
return len(self.plugins) return len(self.plugins)
def load_plugin(self, name): def load_plugin(self, name):
if name in self.plugins:
return
full_name = 'electrum_plugins.' + name + '.' + self.gui_name full_name = 'electrum_plugins.' + name + '.' + self.gui_name
loader = pkgutil.find_loader(full_name) loader = pkgutil.find_loader(full_name)
if not loader: if not loader:
@ -145,17 +153,23 @@ class Plugins(DaemonThread):
self.print_error("cannot load plugin for:", name) self.print_error("cannot load plugin for:", name)
return wallet_types, descs return wallet_types, descs
def register_plugin_wallet(self, name, gui_good, details): def register_wallet_type(self, name, gui_good, details):
from wallet import Wallet from wallet import Wallet
global plugin_loaders
def dynamic_constructor(storage): def loader():
return self.wallet_plugin_loader(name).wallet_class(storage) plugin = self.wallet_plugin_loader(name)
Wallet.register_constructor(details[0], details[1], plugin.wallet_class)
self.print_error("registering wallet type %s: %s" %(name, details))
plugin_loaders[details[1]] = loader
def register_keystore(self, name, gui_good, details):
from keystore import register_keystore
def dynamic_constructor():
return self.wallet_plugin_loader(name).keystore_class()
if details[0] == 'hardware': if details[0] == 'hardware':
self.hw_wallets[name] = (gui_good, details) self.hw_wallets[name] = (gui_good, details)
self.print_error("registering wallet %s: %s" %(name, details)) self.print_error("registering keystore %s: %s" %(name, details))
Wallet.register_plugin_wallet(details[0], details[1], register_keystore(details[0], details[1], dynamic_constructor)
dynamic_constructor)
def wallet_plugin_loader(self, name): def wallet_plugin_loader(self, name):
if not name in self.plugins: if not name in self.plugins:
@ -169,9 +183,6 @@ class Plugins(DaemonThread):
self.on_stop() self.on_stop()
hook_names = set()
hooks = {}
def hook(func): def hook(func):
hook_names.add(func.func_name) hook_names.add(func.func_name)
return func return func
@ -375,48 +386,45 @@ class DeviceMgr(ThreadJob, PrintError):
self.scan_devices(handler) self.scan_devices(handler)
return self.client_lookup(id_) return self.client_lookup(id_)
def client_for_wallet(self, plugin, wallet, force_pair): def client_for_keystore(self, plugin, keystore, force_pair):
assert wallet.handler assert keystore.handler
devices = self.scan_devices(keystore.handler)
devices = self.scan_devices(wallet.handler) wallet_id = self.wallet_id(keystore)
wallet_id = self.wallet_id(wallet)
client = self.client_lookup(wallet_id) client = self.client_lookup(wallet_id)
if client: if client:
# An unpaired client might have another wallet's handler # An unpaired client might have another wallet's handler
# from a prior scan. Replace to fix dialog parenting. # from a prior scan. Replace to fix dialog parenting.
client.handler = wallet.handler client.handler = keystore.handler
return client return client
for device in devices: for device in devices:
if device.id_ == wallet_id: if device.id_ == wallet_id:
return self.create_client(device, wallet.handler, plugin) return self.create_client(device, keystore.handler, plugin)
if force_pair: if force_pair:
return self.force_pair_wallet(plugin, wallet, devices) return self.force_pair_wallet(plugin, keystore, devices)
return None return None
def force_pair_wallet(self, plugin, wallet, devices): def force_pair_wallet(self, plugin, keystore, devices):
first_address, derivation = wallet.first_address() xpub = keystore.get_master_public_key()
assert first_address derivation = keystore.get_derivation()
# The wallet has not been previously paired, so let the user # The wallet has not been previously paired, so let the user
# choose an unpaired device and compare its first address. # choose an unpaired device and compare its first address.
info = self.select_device(wallet, plugin, devices) info = self.select_device(keystore, plugin, devices)
client = self.client_lookup(info.device.id_) client = self.client_lookup(info.device.id_)
if client and client.is_pairable(): if client and client.is_pairable():
# See comment above for same code # See comment above for same code
client.handler = wallet.handler client.handler = keystore.handler
# This will trigger a PIN/passphrase entry request # This will trigger a PIN/passphrase entry request
try: try:
client_first_address = client.first_address(derivation) client_xpub = client.get_xpub(derivation)
except (UserCancelled, RuntimeError): except (UserCancelled, RuntimeError):
# Bad / cancelled PIN / passphrase # Bad / cancelled PIN / passphrase
client_first_address = None client_xpub = None
if client_first_address == first_address: if client_xpub == xpub:
self.pair_wallet(wallet, info.device.id_) self.pair_wallet(keystore, info.device.id_)
return client return client
# The user input has wrong PIN or passphrase, or cancelled input, # The user input has wrong PIN or passphrase, or cancelled input,

253
lib/storage.py

@ -0,0 +1,253 @@
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2015 Thomas Voegtlin
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import ast
import threading
import random
import time
import json
import copy
import re
import stat
from i18n import _
from util import NotEnoughFunds, PrintError, profiler
from plugins import run_hook, plugin_loaders
class WalletStorage(PrintError):
def __init__(self, path):
self.lock = threading.RLock()
self.data = {}
self.path = path
self.file_exists = False
self.modified = False
self.print_error("wallet path", self.path)
if self.path:
self.read(self.path)
# check here if I need to load a plugin
t = self.get('wallet_type')
l = plugin_loaders.get(t)
if l: l()
def read(self, path):
"""Read the contents of the wallet file."""
try:
with open(self.path, "r") as f:
data = f.read()
except IOError:
return
if not data:
return
try:
self.data = json.loads(data)
except:
try:
d = ast.literal_eval(data) #parse raw data from reading wallet file
labels = d.get('labels', {})
except Exception as e:
raise IOError("Cannot read wallet file '%s'" % self.path)
self.data = {}
# In old versions of Electrum labels were latin1 encoded, this fixes breakage.
for i, label in labels.items():
try:
unicode(label)
except UnicodeDecodeError:
d['labels'][i] = unicode(label.decode('latin1'))
for key, value in d.items():
try:
json.dumps(key)
json.dumps(value)
except:
self.print_error('Failed to convert label to json format', key)
continue
self.data[key] = value
self.file_exists = True
def get(self, key, default=None):
with self.lock:
v = self.data.get(key)
if v is None:
v = default
else:
v = copy.deepcopy(v)
return v
def put(self, key, value):
try:
json.dumps(key)
json.dumps(value)
except:
self.print_error("json error: cannot save", key)
return
with self.lock:
if value is not None:
if self.data.get(key) != value:
self.modified = True
self.data[key] = copy.deepcopy(value)
elif key in self.data:
self.modified = True
self.data.pop(key)
def write(self):
with self.lock:
self._write()
self.file_exists = True
def _write(self):
if threading.currentThread().isDaemon():
self.print_error('warning: daemon thread cannot write wallet')
return
if not self.modified:
return
s = json.dumps(self.data, indent=4, sort_keys=True)
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
with open(temp_path, "w") as f:
f.write(s)
f.flush()
os.fsync(f.fileno())
mode = os.stat(self.path).st_mode if os.path.exists(self.path) else stat.S_IREAD | stat.S_IWRITE
# perform atomic write on POSIX systems
try:
os.rename(temp_path, self.path)
except:
os.remove(self.path)
os.rename(temp_path, self.path)
os.chmod(self.path, mode)
self.print_error("saved", self.path)
self.modified = False
def requires_split(self):
d = self.get('accounts', {})
return len(d) > 1
def split_accounts(storage):
result = []
# backward compatibility with old wallets
d = storage.get('accounts', {})
if len(d) < 2:
return
wallet_type = storage.get('wallet_type')
if wallet_type == 'old':
assert len(d) == 2
storage1 = WalletStorage(storage.path + '.deterministic')
storage1.data = copy.deepcopy(storage.data)
storage1.put('accounts', {'0': d['0']})
storage1.write()
storage2 = WalletStorage(storage.path + '.imported')
storage2.data = copy.deepcopy(storage.data)
storage2.put('accounts', {'/x': d['/x']})
storage2.put('seed', None)
storage2.put('seed_version', None)
storage2.put('master_public_key', None)
storage2.put('wallet_type', 'imported')
storage2.write()
storage2.upgrade()
result = [storage1.path, storage2.path]
elif wallet_type in ['bip44', 'trezor']:
mpk = storage.get('master_public_keys')
for k in d.keys():
i = int(k)
x = d[k]
if x.get("pending"):
continue
xpub = mpk["x/%d'"%i]
new_path = storage.path + '.' + k
storage2 = WalletStorage(new_path)
storage2.data = copy.deepcopy(storage.data)
storage2.put('wallet_type', 'standard')
if wallet_type in ['trezor', 'keepkey']:
storage2.put('key_type', 'hardware')
storage2.put('hardware_type', wallet_type)
storage2.put('accounts', {'0': x})
# need to save derivation and xpub too
storage2.put('master_public_keys', {'x/': xpub})
storage2.put('account_id', k)
storage2.write()
result.append(new_path)
else:
raise BaseException("This wallet has multiple accounts and must be split")
return result
def requires_upgrade(storage):
# '/x' is the internal ID for imported accounts
return bool(storage.get('accounts', {}).get('/x', {}).get('imported',{}))
def upgrade(storage):
d = storage.get('accounts', {}).get('/x', {}).get('imported',{})
addresses = []
keypairs = {}
for addr, v in d.items():
pubkey, privkey = v
if privkey:
keypairs[pubkey] = privkey
else:
addresses.append(addr)
if addresses and keypairs:
raise BaseException('mixed addresses and privkeys')
elif addresses:
storage.put('addresses', addresses)
storage.put('accounts', None)
elif keypairs:
storage.put('wallet_type', 'standard')
storage.put('key_type', 'imported')
storage.put('keypairs', keypairs)
storage.put('accounts', None)
else:
raise BaseException('no addresses or privkeys')
storage.write()
def get_action(self):
action = run_hook('get_action', self)
if action:
return action
if not self.file_exists:
return 'new'
def get_seed_version(self):
from version import OLD_SEED_VERSION, NEW_SEED_VERSION
seed_version = self.get('seed_version')
if not seed_version:
seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION
if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]:
msg = "Your wallet has an unsupported seed version."
msg += '\n\nWallet file: %s' % os.path.abspath(self.path)
if seed_version in [5, 7, 8, 9, 10]:
msg += "\n\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version
if seed_version == 6:
# version 1.9.8 created v6 wallets when an incorrect seed was entered in the restore dialog
msg += '\n\nThis file was created because of a bug in version 1.9.8.'
if self.get('master_public_keys') is None and self.get('master_private_keys') is None and self.get('imported_keys') is None:
# pbkdf2 was not included with the binaries, and wallet creation aborted.
msg += "\nIt does not contain any keys, and can safely be removed."
else:
# creation was complete if electrum was run from source
msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet."
raise BaseException(msg)
return seed_version

2
lib/synchronizer.py

@ -180,7 +180,7 @@ class Synchronizer(ThreadJob):
if self.requested_tx: if self.requested_tx:
self.print_error("missing tx", self.requested_tx) self.print_error("missing tx", self.requested_tx)
self.subscribe_to_addresses(set(self.wallet.addresses(True))) self.subscribe_to_addresses(set(self.wallet.get_addresses()))
def run(self): def run(self):
'''Called from the network proxy thread main loop.''' '''Called from the network proxy thread main loop.'''

17
lib/transaction.py

@ -761,23 +761,6 @@ class Transaction:
out.add(i) out.add(i)
return out return out
def inputs_to_sign(self):
out = set()
for txin in self.inputs():
num_sig = txin.get('num_sig')
if num_sig is None:
continue
x_signatures = txin['signatures']
signatures = filter(None, x_signatures)
if len(signatures) == num_sig:
# input is complete
continue
for k, x_pubkey in enumerate(txin['x_pubkeys']):
if x_signatures[k] is not None:
# this pubkey already signed
continue
out.add(x_pubkey)
return out
def sign(self, keypairs): def sign(self, keypairs):
for i, txin in enumerate(self.inputs()): for i, txin in enumerate(self.inputs()):

1321
lib/wallet.py

File diff suppressed because it is too large

3
plugins/cosigner_pool/qt.py

@ -126,7 +126,8 @@ class Plugin(BasePlugin):
self.listener = None self.listener = None
self.keys = [] self.keys = []
self.cosigner_list = [] self.cosigner_list = []
for key, xpub in wallet.master_public_keys.items(): for key, keystore in wallet.keystores.items():
xpub = keystore.get_master_public_key()
K = bitcoin.deserialize_xkey(xpub)[-1].encode('hex') K = bitcoin.deserialize_xkey(xpub)[-1].encode('hex')
_hash = bitcoin.Hash(K).encode('hex') _hash = bitcoin.Hash(K).encode('hex')
if wallet.master_private_keys.get(key): if wallet.master_private_keys.get(key):

1
plugins/hw_wallet/__init__.py

@ -1,2 +1 @@
from hw_wallet import BIP44_HW_Wallet
from plugin import HW_PluginBase from plugin import HW_PluginBase

95
plugins/hw_wallet/hw_wallet.py

@ -1,95 +0,0 @@
#!/usr/bin/env python2
# -*- mode: python -*-
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2016 The Electrum developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from struct import pack
from electrum.wallet import BIP44_Wallet
class BIP44_HW_Wallet(BIP44_Wallet):
'''A BIP44 hardware wallet base class.'''
# Derived classes must set:
# - device
# - DEVICE_IDS
# - wallet_type
restore_wallet_class = BIP44_Wallet
max_change_outputs = 1
def __init__(self, storage):
BIP44_Wallet.__init__(self, storage)
# Errors and other user interaction is done through the wallet's
# handler. The handler is per-window and preserved across
# device reconnects
self.handler = None
def unpaired(self):
'''A device paired with the wallet was diconnected. This can be
called in any thread context.'''
self.print_error("unpaired")
def paired(self):
'''A device paired with the wallet was (re-)connected. This can be
called in any thread context.'''
self.print_error("paired")
def get_action(self):
pass
def can_create_accounts(self):
return True
def can_export(self):
return False
def is_watching_only(self):
'''The wallet is not watching-only; the user will be prompted for
pin and passphrase as appropriate when needed.'''
assert not self.has_seed()
return False
def can_change_password(self):
return False
def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair)
def first_address(self):
'''Used to check a hardware wallet matches a software wallet'''
account = self.accounts.get('0')
derivation = self.address_derivation('0', 0, 0)
return (account.first_address()[0] if account else None, derivation)
def derive_xkeys(self, root, derivation, password):
if self.master_public_keys.get(self.root_name):
return BIP44_wallet.derive_xkeys(self, root, derivation, password)
# When creating a wallet we need to ask the device for the
# master public key
xpub = self.get_public_key(derivation)
return xpub, None
def i4b(self, x):
return pack('>I', x)

33
plugins/hw_wallet/plugin.py

@ -37,8 +37,8 @@ class HW_PluginBase(BasePlugin):
def __init__(self, parent, config, name): def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name) BasePlugin.__init__(self, parent, config, name)
self.device = self.wallet_class.device self.device = self.keystore_class.device
self.wallet_class.plugin = self self.keystore_class.plugin = self
def is_enabled(self): def is_enabled(self):
return self.libraries_available return self.libraries_available
@ -48,33 +48,6 @@ class HW_PluginBase(BasePlugin):
@hook @hook
def close_wallet(self, wallet): def close_wallet(self, wallet):
if isinstance(wallet, self.wallet_class): if isinstance(wallet.get_keystore(), self.keystore_class):
self.device_manager().unpair_wallet(wallet) self.device_manager().unpair_wallet(wallet)
def on_restore_wallet(self, wallet, wizard):
assert isinstance(wallet, self.wallet_class)
msg = _("Enter the seed for your %s wallet:" % self.device)
f = lambda x: wizard.run('on_restore_seed', x)
wizard.enter_seed_dialog(run_next=f, title=_('Restore hardware wallet'), message=msg, is_valid=self.is_valid_seed)
def on_restore_seed(self, wallet, wizard, seed):
f = lambda x: wizard.run('on_restore_passphrase', seed, x)
wizard.request_passphrase(self.device, run_next=f)
def on_restore_passphrase(self, wallet, wizard, seed, passphrase):
f = lambda x: wizard.run('on_restore_password', seed, passphrase, x)
wizard.request_password(run_next=f)
def on_restore_password(self, wallet, wizard, seed, passphrase, password):
# Restored wallets are not hardware wallets
wallet_class = self.wallet_class.restore_wallet_class
wallet.storage.put('wallet_type', wallet_class.wallet_type)
wallet = wallet_class(wallet.storage)
wallet.add_seed(seed, password)
wallet.add_xprv_from_seed(seed, 'x/', password, passphrase)
wallet.create_hd_account(password)
wizard.create_addresses()
@staticmethod
def is_valid_seed(seed):
return True

4
plugins/keepkey/__init__.py

@ -3,6 +3,6 @@ from electrum.i18n import _
fullname = 'KeepKey' fullname = 'KeepKey'
description = _('Provides support for KeepKey hardware wallet') description = _('Provides support for KeepKey hardware wallet')
requires = [('keepkeylib','github.com/keepkey/python-keepkey')] requires = [('keepkeylib','github.com/keepkey/python-keepkey')]
requires_wallet_type = ['keepkey'] #requires_wallet_type = ['keepkey']
registers_wallet_type = ('hardware', 'keepkey', _("KeepKey wallet")) registers_keystore = ('hardware', 'keepkey', _("KeepKey wallet"))
available_for = ['qt', 'cmdline'] available_for = ['qt', 'cmdline']

6
plugins/keepkey/keepkey.py

@ -1,7 +1,7 @@
from ..trezor.plugin import TrezorCompatiblePlugin, TrezorCompatibleWallet from ..trezor.plugin import TrezorCompatiblePlugin, TrezorCompatibleKeyStore
class KeepKeyWallet(TrezorCompatibleWallet): class KeepKey_KeyStore(TrezorCompatibleKeyStore):
wallet_type = 'keepkey' wallet_type = 'keepkey'
device = 'KeepKey' device = 'KeepKey'
@ -10,7 +10,7 @@ class KeepKeyPlugin(TrezorCompatiblePlugin):
firmware_URL = 'https://www.keepkey.com' firmware_URL = 'https://www.keepkey.com'
libraries_URL = 'https://github.com/keepkey/python-keepkey' libraries_URL = 'https://github.com/keepkey/python-keepkey'
minimum_firmware = (1, 0, 0) minimum_firmware = (1, 0, 0)
wallet_class = KeepKeyWallet keystore_class = KeepKey_KeyStore
try: try:
from .client import KeepKeyClient as client_class from .client import KeepKeyClient as client_class
import keepkeylib.ckd_public as ckd_public import keepkeylib.ckd_public as ckd_public

4
plugins/ledger/__init__.py

@ -3,6 +3,6 @@ from electrum.i18n import _
fullname = 'Ledger Wallet' fullname = 'Ledger Wallet'
description = 'Provides support for Ledger hardware wallet' description = 'Provides support for Ledger hardware wallet'
requires = [('btchip', 'github.com/ledgerhq/btchip-python')] requires = [('btchip', 'github.com/ledgerhq/btchip-python')]
requires_wallet_type = ['btchip'] #requires_wallet_type = ['btchip']
registers_wallet_type = ('hardware', 'btchip', _("Ledger wallet")) registers_keystore = ('hardware', 'btchip', _("Ledger wallet"))
available_for = ['qt', 'cmdline'] available_for = ['qt', 'cmdline']

8
plugins/ledger/ledger.py

@ -7,7 +7,7 @@ import electrum
from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, TYPE_ADDRESS from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, TYPE_ADDRESS
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugins import BasePlugin, hook from electrum.plugins import BasePlugin, hook
from ..hw_wallet import BIP44_HW_Wallet from ..hw_wallet import BIP32_HW_Wallet
from ..hw_wallet import HW_PluginBase from ..hw_wallet import HW_PluginBase
from electrum.util import format_satoshis_plain, print_error from electrum.util import format_satoshis_plain, print_error
@ -26,12 +26,12 @@ except ImportError:
BTCHIP = False BTCHIP = False
class BTChipWallet(BIP44_HW_Wallet): class BTChipWallet(BIP32_HW_Wallet):
wallet_type = 'btchip' wallet_type = 'btchip'
device = 'Ledger' device = 'Ledger'
def __init__(self, storage): def __init__(self, storage):
BIP44_HW_Wallet.__init__(self, storage) BIP32_HW_Wallet.__init__(self, storage)
# Errors and other user interaction is done through the wallet's # Errors and other user interaction is done through the wallet's
# handler. The handler is per-window and preserved across # handler. The handler is per-window and preserved across
# device reconnects # device reconnects
@ -53,7 +53,7 @@ class BTChipWallet(BIP44_HW_Wallet):
def address_id(self, address): def address_id(self, address):
# Strip the leading "m/" # Strip the leading "m/"
return BIP44_HW_Wallet.address_id(self, address)[2:] return BIP32_HW_Wallet.address_id(self, address)[2:]
def get_public_key(self, bip32_path): def get_public_key(self, bip32_path):
# bip32_path is of the form 44'/0'/1' # bip32_path is of the form 44'/0'/1'

4
plugins/trezor/__init__.py

@ -3,7 +3,7 @@ from electrum.i18n import _
fullname = 'TREZOR Wallet' fullname = 'TREZOR Wallet'
description = _('Provides support for TREZOR hardware wallet') description = _('Provides support for TREZOR hardware wallet')
requires = [('trezorlib','github.com/trezor/python-trezor')] requires = [('trezorlib','github.com/trezor/python-trezor')]
requires_wallet_type = ['trezor'] #requires_wallet_type = ['trezor']
registers_wallet_type = ('hardware', 'trezor', _("TREZOR wallet")) registers_keystore = ('hardware', 'trezor', _("TREZOR wallet"))
available_for = ['qt', 'cmdline'] available_for = ['qt', 'cmdline']

23
plugins/trezor/clientbase.py

@ -1,8 +1,10 @@
import time import time
from struct import pack
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import PrintError, UserCancelled from electrum.util import PrintError, UserCancelled
from electrum.wallet import BIP44_Wallet from electrum.keystore import BIP44_KeyStore
from electrum.bitcoin import EncodeBase58Check
class GuiMixin(object): class GuiMixin(object):
@ -63,7 +65,7 @@ class GuiMixin(object):
passphrase = self.handler.get_passphrase(msg, self.creating_wallet) passphrase = self.handler.get_passphrase(msg, self.creating_wallet)
if passphrase is None: if passphrase is None:
return self.proto.Cancel() return self.proto.Cancel()
passphrase = BIP44_Wallet.normalize_passphrase(passphrase) passphrase = BIP44_KeyStore.normalize_passphrase(passphrase)
return self.proto.PassphraseAck(passphrase=passphrase) return self.proto.PassphraseAck(passphrase=passphrase)
def callback_WordRequest(self, msg): def callback_WordRequest(self, msg):
@ -142,11 +144,20 @@ class TrezorClientBase(GuiMixin, PrintError):
'''Provided here as in keepkeylib but not trezorlib.''' '''Provided here as in keepkeylib but not trezorlib.'''
self.transport.write(self.proto.Cancel()) self.transport.write(self.proto.Cancel())
def first_address(self, derivation): def i4b(self, x):
return self.address_from_derivation(derivation) return pack('>I', x)
def address_from_derivation(self, derivation): def get_xpub(self, bip32_path):
return self.get_address('Bitcoin', self.expand_path(derivation)) address_n = self.expand_path(bip32_path)
creating = False #self.next_account_number() == 0
node = self.get_public_node(address_n, creating).node
xpub = ("0488B21E".decode('hex') + chr(node.depth)
+ self.i4b(node.fingerprint) + self.i4b(node.child_num)
+ node.chain_code + node.public_key)
return EncodeBase58Check(xpub)
#def address_from_derivation(self, derivation):
# return self.get_address('Bitcoin', self.expand_path(derivation))
def toggle_passphrase(self): def toggle_passphrase(self):
if self.features.passphrase_protection: if self.features.passphrase_protection:

116
plugins/trezor/plugin.py

@ -8,28 +8,32 @@ from functools import partial
from electrum.account import BIP32_Account from electrum.account import BIP32_Account
from electrum.bitcoin import (bc_address_to_hash_160, xpub_from_pubkey, from electrum.bitcoin import (bc_address_to_hash_160, xpub_from_pubkey,
public_key_to_bc_address, EncodeBase58Check, public_key_to_bc_address, EncodeBase58Check,
TYPE_ADDRESS) TYPE_ADDRESS, TYPE_SCRIPT)
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugins import BasePlugin, hook from electrum.plugins import BasePlugin, hook
from electrum.transaction import (deserialize, is_extended_pubkey, from electrum.transaction import (deserialize, is_extended_pubkey,
Transaction, x_to_xpub) Transaction, x_to_xpub)
from ..hw_wallet import BIP44_HW_Wallet, HW_PluginBase from electrum.keystore import Hardware_KeyStore
from ..hw_wallet import HW_PluginBase
# TREZOR initialization methods # TREZOR initialization methods
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
class TrezorCompatibleWallet(BIP44_HW_Wallet): class TrezorCompatibleKeyStore(Hardware_KeyStore):
root = "m/44'/0'"
account_id = 0
def get_derivation(self):
return self.root + "/%d'"%self.account_id
def get_public_key(self, bip32_path): def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair)
def init_xpub(self):
client = self.get_client() client = self.get_client()
address_n = client.expand_path(bip32_path) self.xpub = client.get_xpub(self.get_derivation())
creating = self.next_account_number() == 0
node = client.get_public_node(address_n, creating).node
xpub = ("0488B21E".decode('hex') + chr(node.depth)
+ self.i4b(node.fingerprint) + self.i4b(node.child_num)
+ node.chain_code + node.public_key)
return EncodeBase58Check(xpub)
def decrypt_message(self, pubkey, message, password): def decrypt_message(self, pubkey, message, password):
raise RuntimeError(_('Electrum and %s encryption and decryption are currently incompatible') % self.device) raise RuntimeError(_('Electrum and %s encryption and decryption are currently incompatible') % self.device)
@ -49,17 +53,6 @@ class TrezorCompatibleWallet(BIP44_HW_Wallet):
msg_sig = client.sign_message('Bitcoin', address_n, message) msg_sig = client.sign_message('Bitcoin', address_n, message)
return msg_sig.signature return msg_sig.signature
def get_input_tx(self, tx_hash):
# First look up an input transaction in the wallet where it
# will likely be. If co-signing a transaction it may not have
# all the input txs, in which case we ask the network.
tx = self.transactions.get(tx_hash)
if not tx:
request = ('blockchain.transaction.get', [tx_hash])
# FIXME: what if offline?
tx = Transaction(self.network.synchronous_get(request))
return tx
def sign_transaction(self, tx, password): def sign_transaction(self, tx, password):
if tx.is_complete(): if tx.is_complete():
return return
@ -69,15 +62,13 @@ class TrezorCompatibleWallet(BIP44_HW_Wallet):
xpub_path = {} xpub_path = {}
for txin in tx.inputs(): for txin in tx.inputs():
tx_hash = txin['prevout_hash'] tx_hash = txin['prevout_hash']
prev_tx[tx_hash] = self.get_input_tx(tx_hash) prev_tx[tx_hash] = txin['prev_tx']
for x_pubkey in txin['x_pubkeys']: for x_pubkey in txin['x_pubkeys']:
if not is_extended_pubkey(x_pubkey): if not is_extended_pubkey(x_pubkey):
continue continue
xpub = x_to_xpub(x_pubkey) xpub = x_to_xpub(x_pubkey)
for k, v in self.master_public_keys.items(): if xpub == self.get_master_public_key():
if v == xpub: xpub_path[xpub] = self.get_derivation()
acc_id = re.match("x/(\d+)'", k).group(1)
xpub_path[xpub] = self.account_derivation(acc_id)
self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
@ -149,18 +140,16 @@ class TrezorCompatiblePlugin(HW_PluginBase):
return client return client
def get_client(self, wallet, force_pair=True): def get_client(self, keystore, force_pair=True):
# All client interaction should not be in the main GUI thread # All client interaction should not be in the main GUI thread
assert self.main_thread != threading.current_thread() assert self.main_thread != threading.current_thread()
devmgr = self.device_manager() devmgr = self.device_manager()
client = devmgr.client_for_wallet(self, wallet, force_pair) client = devmgr.client_for_keystore(self, keystore, force_pair)
if client: if client:
client.used() client.used()
return client return client
def initialize_device(self, wallet): def initialize_device(self, keystore):
# Initialization method # Initialization method
msg = _("Choose how you want to initialize your %s.\n\n" msg = _("Choose how you want to initialize your %s.\n\n"
"The first two methods are secure as no secret information " "The first two methods are secure as no secret information "
@ -179,13 +168,13 @@ class TrezorCompatiblePlugin(HW_PluginBase):
_("Upload a master private key") _("Upload a master private key")
] ]
method = wallet.handler.query_choice(msg, methods) method = keystore.handler.query_choice(msg, methods)
(item, label, pin_protection, passphrase_protection) \ (item, label, pin_protection, passphrase_protection) \
= wallet.handler.request_trezor_init_settings(method, self.device) = wallet.handler.request_trezor_init_settings(method, self.device)
if method == TIM_RECOVER and self.device == 'TREZOR': if method == TIM_RECOVER and self.device == 'TREZOR':
# Warn user about firmware lameness # Warn user about firmware lameness
wallet.handler.show_error(_( keystore.handler.show_error(_(
"You will be asked to enter 24 words regardless of your " "You will be asked to enter 24 words regardless of your "
"seed's actual length. If you enter a word incorrectly or " "seed's actual length. If you enter a word incorrectly or "
"misspell it, you cannot change it or go back - you will need " "misspell it, you cannot change it or go back - you will need "
@ -195,7 +184,7 @@ class TrezorCompatiblePlugin(HW_PluginBase):
language = 'english' language = 'english'
def initialize_method(): def initialize_method():
client = self.get_client(wallet) client = self.get_client(keystore)
if method == TIM_NEW: if method == TIM_NEW:
strength = 64 * (item + 2) # 128, 192 or 256 strength = 64 * (item + 2) # 128, 192 or 256
@ -216,35 +205,36 @@ class TrezorCompatiblePlugin(HW_PluginBase):
client.load_device_by_xprv(item, pin, passphrase_protection, client.load_device_by_xprv(item, pin, passphrase_protection,
label, language) label, language)
# After successful initialization create accounts # After successful initialization create accounts
wallet.create_hd_account(None) keystore.init_xpub()
#wallet.create_main_account()
return initialize_method return initialize_method
def setup_device(self, wallet, on_done, on_error): def setup_device(self, keystore, on_done, on_error):
'''Called when creating a new wallet. Select the device to use. If '''Called when creating a new wallet. Select the device to use. If
the device is uninitialized, go through the intialization the device is uninitialized, go through the intialization
process. Then create the wallet accounts.''' process. Then create the wallet accounts.'''
devmgr = self.device_manager() devmgr = self.device_manager()
device_info = devmgr.select_device(wallet, self) device_info = devmgr.select_device(keystore, self)
devmgr.pair_wallet(wallet, device_info.device.id_) devmgr.pair_wallet(keystore, device_info.device.id_)
if device_info.initialized: if device_info.initialized:
task = partial(wallet.create_hd_account, None) task = keystore.init_xpub
else: else:
task = self.initialize_device(wallet) task = self.initialize_device(keystore)
wallet.thread.add(task, on_done=on_done, on_error=on_error) keystore.thread.add(task, on_done=on_done, on_error=on_error)
def sign_transaction(self, wallet, tx, prev_tx, xpub_path): def sign_transaction(self, keystore, tx, prev_tx, xpub_path):
self.prev_tx = prev_tx self.prev_tx = prev_tx
self.xpub_path = xpub_path self.xpub_path = xpub_path
client = self.get_client(wallet) client = self.get_client(keystore)
inputs = self.tx_inputs(tx, True) inputs = self.tx_inputs(tx, True)
outputs = self.tx_outputs(wallet, tx) outputs = self.tx_outputs(keystore.get_derivation(), tx)
signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
raw = signed_tx.encode('hex') raw = signed_tx.encode('hex')
tx.update_signatures(raw) tx.update_signatures(raw)
def show_address(self, wallet, address): def show_address(self, wallet, address):
client = self.get_client(wallet) client = self.get_client(wallet.keystore)
if not client.atleast_version(1, 3): if not client.atleast_version(1, 3):
wallet.handler.show_error(_("Your device firmware is too old")) wallet.handler.show_error(_("Your device firmware is too old"))
return return
@ -313,23 +303,29 @@ class TrezorCompatiblePlugin(HW_PluginBase):
return inputs return inputs
def tx_outputs(self, wallet, tx): def tx_outputs(self, derivation, tx):
outputs = [] outputs = []
for type, address, amount in tx.outputs(): for i, (_type, address, amount) in enumerate(tx.outputs()):
assert type == TYPE_ADDRESS
txoutputtype = self.types.TxOutputType() txoutputtype = self.types.TxOutputType()
if wallet.is_change(address):
address_path = wallet.address_id(address)
address_n = self.client_class.expand_path(address_path)
txoutputtype.address_n.extend(address_n)
else:
txoutputtype.address = address
txoutputtype.amount = amount txoutputtype.amount = amount
addrtype, hash_160 = bc_address_to_hash_160(address) change, index = tx.output_info[i]
if addrtype == 0: if _type == TYPE_SCRIPT:
txoutputtype.script_type = self.types.PAYTOADDRESS txoutputtype.script_type = self.types.PAYTOOPRETURN
elif addrtype == 5: txoutputtype.op_return_data = address[2:]
txoutputtype.script_type = self.types.PAYTOSCRIPTHASH elif _type == TYPE_ADDRESS:
if change is not None:
address_path = "%s/%d/%d/"%(derivation, change, index)
address_n = self.client_class.expand_path(address_path)
txoutputtype.address_n.extend(address_n)
else:
txoutputtype.address = address
addrtype, hash_160 = bc_address_to_hash_160(address)
if addrtype == 0:
txoutputtype.script_type = self.types.PAYTOADDRESS
elif addrtype == 5:
txoutputtype.script_type = self.types.PAYTOSCRIPTHASH
else:
raise BaseException('addrtype')
else: else:
raise BaseException('addrtype') raise BaseException('addrtype')
outputs.append(txoutputtype) outputs.append(txoutputtype)

35
plugins/trezor/qt_generic.py

@ -12,7 +12,7 @@ from ..hw_wallet.qt import QtHandlerBase
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugins import hook, DeviceMgr from electrum.plugins import hook, DeviceMgr
from electrum.util import PrintError, UserCancelled from electrum.util import PrintError, UserCancelled
from electrum.wallet import Wallet, BIP44_Wallet from electrum.wallet import Wallet
PASSPHRASE_HELP_SHORT =_( PASSPHRASE_HELP_SHORT =_(
"Passphrases allow you to access new wallets, each " "Passphrases allow you to access new wallets, each "
@ -273,23 +273,25 @@ def qt_plugin_class(base_plugin_class):
@hook @hook
def load_wallet(self, wallet, window): def load_wallet(self, wallet, window):
if type(wallet) != self.wallet_class: keystore = wallet.get_keystore()
if type(keystore) != self.keystore_class:
return return
window.tzb = StatusBarButton(QIcon(self.icon_file), self.device, window.tzb = StatusBarButton(QIcon(self.icon_file), self.device,
partial(self.settings_dialog, window)) partial(self.settings_dialog, window))
window.statusBar().addPermanentWidget(window.tzb) window.statusBar().addPermanentWidget(window.tzb)
wallet.handler = self.create_handler(window) keystore.handler = self.create_handler(window)
keystore.thread = TaskThread(window, window.on_error)
# Trigger a pairing # Trigger a pairing
wallet.thread.add(partial(self.get_client, wallet)) keystore.thread.add(partial(self.get_client, keystore))
def on_create_wallet(self, wallet, wizard): def on_create_wallet(self, keystore, wizard):
assert type(wallet) == self.wallet_class #assert type(keystore) == self.keystore_class
wallet.handler = self.create_handler(wizard) keystore.handler = self.create_handler(wizard)
wallet.thread = TaskThread(wizard, wizard.on_error) keystore.thread = TaskThread(wizard, wizard.on_error)
# Setup device and create accounts in separate thread; wait until done # Setup device and create accounts in separate thread; wait until done
loop = QEventLoop() loop = QEventLoop()
exc_info = [] exc_info = []
self.setup_device(wallet, on_done=loop.quit, self.setup_device(keystore, on_done=loop.quit,
on_error=lambda info: exc_info.extend(info)) on_error=lambda info: exc_info.extend(info))
loop.exec_() loop.exec_()
# If an exception was thrown, show to user and exit install wizard # If an exception was thrown, show to user and exit install wizard
@ -299,9 +301,10 @@ def qt_plugin_class(base_plugin_class):
@hook @hook
def receive_menu(self, menu, addrs, wallet): def receive_menu(self, menu, addrs, wallet):
if type(wallet) == self.wallet_class and len(addrs) == 1: keystore = wallet.get_keystore()
if type(keystore) == self.keystore_class and len(addrs) == 1:
def show_address(): def show_address():
wallet.thread.add(partial(self.show_address, wallet, addrs[0])) keystore.thread.add(partial(self.show_address, wallet, addrs[0]))
menu.addAction(_("Show on %s") % self.device, show_address) menu.addAction(_("Show on %s") % self.device, show_address)
def settings_dialog(self, window): def settings_dialog(self, window):
@ -312,9 +315,10 @@ def qt_plugin_class(base_plugin_class):
def choose_device(self, window): def choose_device(self, window):
'''This dialog box should be usable even if the user has '''This dialog box should be usable even if the user has
forgotten their PIN or it is in bootloader mode.''' forgotten their PIN or it is in bootloader mode.'''
device_id = self.device_manager().wallet_id(window.wallet) keystore = window.wallet.get_keystore()
device_id = self.device_manager().wallet_id(keystore)
if not device_id: if not device_id:
info = self.device_manager().select_device(window.wallet, self) info = self.device_manager().select_device(keystore, self)
device_id = info.device.id_ device_id = info.device.id_
return device_id return device_id
@ -345,8 +349,9 @@ class SettingsDialog(WindowModalDialog):
devmgr = plugin.device_manager() devmgr = plugin.device_manager()
config = devmgr.config config = devmgr.config
handler = window.wallet.handler keystore = window.wallet.get_keystore()
thread = window.wallet.thread handler = keystore.handler
thread = keystore.thread
# wallet can be None, needn't be window.wallet # wallet can be None, needn't be window.wallet
wallet = devmgr.wallet_by_id(device_id) wallet = devmgr.wallet_by_id(device_id)
hs_rows, hs_cols = (64, 128) hs_rows, hs_cols = (64, 128)

7
plugins/trezor/trezor.py

@ -1,16 +1,15 @@
from .plugin import TrezorCompatiblePlugin, TrezorCompatibleWallet from .plugin import TrezorCompatiblePlugin, TrezorCompatibleKeyStore
class TrezorWallet(TrezorCompatibleWallet): class TrezorKeyStore(TrezorCompatibleKeyStore):
wallet_type = 'trezor' wallet_type = 'trezor'
device = 'TREZOR' device = 'TREZOR'
class TrezorPlugin(TrezorCompatiblePlugin): class TrezorPlugin(TrezorCompatiblePlugin):
firmware_URL = 'https://www.mytrezor.com' firmware_URL = 'https://www.mytrezor.com'
libraries_URL = 'https://github.com/trezor/python-trezor' libraries_URL = 'https://github.com/trezor/python-trezor'
minimum_firmware = (1, 3, 3) minimum_firmware = (1, 3, 3)
wallet_class = TrezorWallet keystore_class = TrezorKeyStore
try: try:
from .client import TrezorClient as client_class from .client import TrezorClient as client_class
import trezorlib.ckd_public as ckd_public import trezorlib.ckd_public as ckd_public

173
plugins/trustedcoin/trustedcoin.py

@ -34,10 +34,11 @@ from urllib import quote
import electrum import electrum
from electrum import bitcoin from electrum import bitcoin
from electrum import keystore
from electrum.bitcoin import * from electrum.bitcoin import *
from electrum.mnemonic import Mnemonic from electrum.mnemonic import Mnemonic
from electrum import version from electrum import version
from electrum.wallet import Multisig_Wallet, BIP32_Wallet from electrum.wallet import Multisig_Wallet, Deterministic_Wallet, Wallet
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugins import BasePlugin, run_hook, hook from electrum.plugins import BasePlugin, run_hook, hook
from electrum.util import NotEnoughFunds from electrum.util import NotEnoughFunds
@ -187,29 +188,16 @@ server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VER
class Wallet_2fa(Multisig_Wallet): class Wallet_2fa(Multisig_Wallet):
def __init__(self, storage): def __init__(self, storage):
BIP32_Wallet.__init__(self, storage) self.m, self.n = 2, 3
self.wallet_type = '2fa' Deterministic_Wallet.__init__(self, storage)
self.m = 2
self.n = 3
self.is_billing = False self.is_billing = False
self.billing_info = None self.billing_info = None
def get_action(self):
xpub1 = self.master_public_keys.get("x1/")
xpub2 = self.master_public_keys.get("x2/")
xpub3 = self.master_public_keys.get("x3/")
if xpub2 is None and not self.storage.get('use_trustedcoin'):
return 'show_disclaimer'
if xpub2 is None:
return 'create_extended_seed'
if xpub3 is None:
return 'create_remote_key'
def make_seed(self):
return Mnemonic('english').make_seed(num_bits=256, prefix=SEED_PREFIX)
def can_sign_without_server(self): def can_sign_without_server(self):
return self.master_private_keys.get('x2/') is not None return not self.keystores.get('x2/').is_watching_only()
def get_user_id(self):
return get_user_id(self.storage)
def get_max_amount(self, config, inputs, recipient, fee): def get_max_amount(self, config, inputs, recipient, fee):
from electrum.transaction import Transaction from electrum.transaction import Transaction
@ -244,7 +232,7 @@ class Wallet_2fa(Multisig_Wallet):
def make_unsigned_transaction(self, coins, outputs, config, def make_unsigned_transaction(self, coins, outputs, config,
fixed_fee=None, change_addr=None): fixed_fee=None, change_addr=None):
mk_tx = lambda o: BIP32_Wallet.make_unsigned_transaction( mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction(
self, coins, o, config, fixed_fee, change_addr) self, coins, o, config, fixed_fee, change_addr)
fee = self.extra_fee() fee = self.extra_fee()
if fee: if fee:
@ -264,7 +252,7 @@ class Wallet_2fa(Multisig_Wallet):
return tx return tx
def sign_transaction(self, tx, password): def sign_transaction(self, tx, password):
BIP32_Wallet.sign_transaction(self, tx, password) Multisig_Wallet.sign_transaction(self, tx, password)
if tx.is_complete(): if tx.is_complete():
return return
if not self.auth_code: if not self.auth_code:
@ -279,27 +267,25 @@ class Wallet_2fa(Multisig_Wallet):
tx.update(raw_tx) tx.update(raw_tx)
self.print_error("twofactor: is complete", tx.is_complete()) self.print_error("twofactor: is complete", tx.is_complete())
def get_user_id(self):
def make_long_id(xpub_hot, xpub_cold):
return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold])))
xpub_hot = self.master_public_keys["x1/"]
xpub_cold = self.master_public_keys["x2/"]
long_id = make_long_id(xpub_hot, xpub_cold)
short_id = hashlib.sha256(long_id).hexdigest()
return long_id, short_id
# Utility functions # Utility functions
def get_user_id(storage):
def make_long_id(xpub_hot, xpub_cold):
return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold])))
mpk = storage.get('master_public_keys')
xpub1 = mpk["x1/"]
xpub2 = mpk["x2/"]
long_id = make_long_id(xpub1, xpub2)
short_id = hashlib.sha256(long_id).hexdigest()
return long_id, short_id
def make_xpub(xpub, s): def make_xpub(xpub, s):
_, _, _, c, cK = deserialize_xkey(xpub) _, _, _, c, cK = deserialize_xkey(xpub)
cK2, c2 = bitcoin._CKD_pub(cK, c, s) cK2, c2 = bitcoin._CKD_pub(cK, c, s)
xpub2 = ("0488B21E" + "00" + "00000000" + "00000000").decode("hex") + c2 + cK2 xpub2 = ("0488B21E" + "00" + "00000000" + "00000000").decode("hex") + c2 + cK2
return EncodeBase58Check(xpub2) return EncodeBase58Check(xpub2)
def restore_third_key(wallet):
long_user_id, short_id = wallet.get_user_id()
xpub3 = make_xpub(signing_xpub, long_user_id)
wallet.add_master_public_key('x3/', xpub3)
def make_billing_address(wallet, num): def make_billing_address(wallet, num):
long_id, short_id = wallet.get_user_id() long_id, short_id = wallet.get_user_id()
@ -324,9 +310,6 @@ class TrustedCoinPlugin(BasePlugin):
def is_available(self): def is_available(self):
return True return True
def set_enabled(self, wallet, enabled):
wallet.storage.put('use_' + self.name, enabled)
def is_enabled(self): def is_enabled(self):
return True return True
@ -345,28 +328,42 @@ class TrustedCoinPlugin(BasePlugin):
wallet.price_per_tx = dict(billing_info['price_per_tx']) wallet.price_per_tx = dict(billing_info['price_per_tx'])
return True return True
def create_extended_seed(self, wallet, wizard): def make_seed(self):
self.wallet = wallet return Mnemonic('english').make_seed(num_bits=256, prefix=SEED_PREFIX)
self.wizard = wizard
seed = wallet.make_seed() @hook
self.wizard.show_seed_dialog(run_next=wizard.confirm_seed, seed_text=seed) def do_clear(self, window):
window.wallet.is_billing = False
def show_disclaimer(self, wallet, wizard): def show_disclaimer(self, wizard):
self.set_enabled(wallet, True)
wizard.set_icon(':icons/trustedcoin.png') wizard.set_icon(':icons/trustedcoin.png')
wizard.stack = [] wizard.stack = []
wizard.confirm_dialog('\n\n'.join(DISCLAIMER), run_next = lambda x: wizard.run('create_extended_seed')) wizard.confirm_dialog('\n\n'.join(DISCLAIMER), run_next = lambda x: wizard.run('choose_seed'))
def choose_seed(self, wizard):
title = _('Create or restore')
message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
choices = [
('create_seed', _('Create a new seed')),
('restore_wallet', _('I already have a seed')),
]
wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run)
def create_seed(self, wizard):
seed = self.make_seed()
wizard.show_seed_dialog(run_next=wizard.confirm_seed, seed_text=seed)
def create_wallet(self, wallet, wizard, seed, password): def create_keystore(self, wizard, seed, password):
wallet.storage.put('seed_version', wallet.seed_version) # this overloads the wizard's method
wallet.storage.put('use_encryption', password is not None)
words = seed.split() words = seed.split()
n = len(words)/2 n = len(words)/2
wallet.add_xprv_from_seed(' '.join(words[0:n]), 'x1/', password) keystore1 = keystore.xprv_from_seed(' '.join(words[0:n]), password)
wallet.add_xpub_from_seed(' '.join(words[n:]), 'x2/') keystore2 = keystore.xpub_from_seed(' '.join(words[n:]))
wallet.storage.write() keystore1.save(wizard.storage, 'x1/')
keystore2.save(wizard.storage, 'x2/')
wizard.storage.write()
msg = [ msg = [
_("Your wallet file is: %s.")%os.path.abspath(wallet.storage.path), _("Your wallet file is: %s.")%os.path.abspath(wizard.storage.path),
_("You need to be online in order to complete the creation of " _("You need to be online in order to complete the creation of "
"your wallet. If you generated your seed on an offline " "your wallet. If you generated your seed on an offline "
'computer, click on "%s" to close this window, move your ' 'computer, click on "%s" to close this window, move your '
@ -378,41 +375,45 @@ class TrustedCoinPlugin(BasePlugin):
wizard.stack = [] wizard.stack = []
wizard.confirm_dialog(msg, run_next = lambda x: wizard.run('create_remote_key')) wizard.confirm_dialog(msg, run_next = lambda x: wizard.run('create_remote_key'))
@hook def restore_wallet(self, wizard):
def do_clear(self, window):
window.wallet.is_billing = False
def on_restore_wallet(self, wallet, wizard):
assert isinstance(wallet, self.wallet_class)
title = _("Restore two-factor Wallet") title = _("Restore two-factor Wallet")
f = lambda x: wizard.run('on_restore_seed', x) f = lambda x: wizard.run('on_restore_seed', x)
wizard.enter_seed_dialog(run_next=f, title=title, message=RESTORE_MSG, is_valid=self.is_valid_seed) wizard.restore_seed_dialog(run_next=f, is_valid=self.is_valid_seed)
def on_restore_seed(self, wallet, wizard, seed): def on_restore_seed(self, wizard, seed):
f = lambda x: wizard.run('on_restore_pw', seed, x) f = lambda pw: wizard.run('on_restore_pw', seed, pw)
wizard.request_password(run_next=f) wizard.request_password(run_next=f)
def on_restore_pw(self, wallet, wizard, seed, password): def on_restore_pw(self, wizard, seed, password):
wallet.add_seed(seed, password) # FIXME
# wallet.add_seed(seed, password)
storage = wizard.storage
words = seed.split() words = seed.split()
n = len(words)/2 n = len(words)/2
wallet.add_xprv_from_seed(' '.join(words[0:n]), 'x1/', password) keystore1 = keystore.xprv_from_seed(' '.join(words[0:n]), password)
wallet.add_xprv_from_seed(' '.join(words[n:]), 'x2/', password) keystore2 = keystore.xprv_from_seed(' '.join(words[n:]), password)
restore_third_key(wallet) keystore1.save(storage, 'x1/')
keystore2.save(storage, 'x2/')
long_user_id, short_id = get_user_id(storage)
xpub3 = make_xpub(signing_xpub, long_user_id)
keystore3 = keystore.from_xpub(xpub3)
keystore3.save(storage, 'x3/')
wizard.wallet = Wallet(storage)
wizard.create_addresses() wizard.create_addresses()
def create_remote_key(self, wallet, window): def create_remote_key(self, wizard):
email = self.accept_terms_of_use(window) email = self.accept_terms_of_use(wizard)
xpub_hot = wallet.master_public_keys["x1/"] mpk = wizard.storage.get('master_public_keys')
xpub_cold = wallet.master_public_keys["x2/"] xpub1 = mpk["x1/"]
xpub2 = mpk["x2/"]
# Generate third key deterministically. # Generate third key deterministically.
long_user_id, short_id = wallet.get_user_id() long_user_id, short_id = get_user_id(wizard.storage)
xpub3 = make_xpub(signing_xpub, long_user_id) xpub3 = make_xpub(signing_xpub, long_user_id)
# secret must be sent by the server # secret must be sent by the server
try: try:
r = server.create(xpub_hot, xpub_cold, email) r = server.create(xpub1, xpub2, email)
except socket.error: except socket.error:
window.show_message('Server not reachable, aborting') wizard.show_message('Server not reachable, aborting')
return return
except TrustedCoinException as e: except TrustedCoinException as e:
if e.status_code == 409: if e.status_code == 409:
@ -424,7 +425,7 @@ class TrustedCoinPlugin(BasePlugin):
else: else:
otp_secret = r.get('otp_secret') otp_secret = r.get('otp_secret')
if not otp_secret: if not otp_secret:
window.show_message(_('Error')) wizard.show_message(_('Error'))
return return
_xpub3 = r['xpubkey_cosigner'] _xpub3 = r['xpubkey_cosigner']
_id = r['id'] _id = r['id']
@ -432,10 +433,24 @@ class TrustedCoinPlugin(BasePlugin):
assert _id == short_id, ("user id error", _id, short_id) assert _id == short_id, ("user id error", _id, short_id)
assert xpub3 == _xpub3, ("xpub3 error", xpub3, _xpub3) assert xpub3 == _xpub3, ("xpub3 error", xpub3, _xpub3)
except Exception as e: except Exception as e:
window.show_message(str(e)) wizard.show_message(str(e))
return return
if not self.setup_google_auth(window, short_id, otp_secret): if not self.setup_google_auth(wizard, short_id, otp_secret):
window.show_message("otp error") wizard.show_message("otp error")
return return
wallet.add_master_public_key('x3/', xpub3) keystore3 = keystore.from_xpub(xpub3)
window.run('create_addresses') keystore3.save(wizard.storage, 'x3/')
wizard.storage.put('use_trustedcoin', True)
wizard.storage.write()
wizard.wallet = Wallet(wizard.storage)
wizard.run('create_addresses')
@hook
def get_action(self, storage):
mpk = storage.get('master_public_keys', {})
if not mpk.get('x1/'):
return self, 'show_disclaimer'
if not mpk.get('x2/'):
return self, 'show_disclaimer'
if not mpk.get('x3/'):
return self, 'create_remote_key'

Loading…
Cancel
Save