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
This commit is contained in:
ThomasV 2016-07-02 08:58:56 +02:00
parent 6373a76a4a
commit 1159f85e05
35 changed files with 1753 additions and 2068 deletions

View File

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

View File

@ -742,7 +742,7 @@ class InstallWizard(BaseWizard, Widget):
def request_password(self, run_next):
def callback(pin):
if pin:
self.run('confirm_password', (pin, run_next))
self.run('confirm_password', pin, run_next)
else:
run_next(None)
self.password_dialog('Choose a PIN code', callback)
@ -753,7 +753,7 @@ class InstallWizard(BaseWizard, Widget):
run_next(pin)
else:
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)
def action_dialog(self, action, run_next):

View File

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

View File

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

View File

@ -41,26 +41,14 @@ class AddressList(MyTreeWidget):
def on_update(self):
self.wallet = self.parent.wallet
self.accounts_expanded = self.wallet.storage.get('accounts_expanded', {})
item = self.currentItem()
current_address = item.data(0, Qt.UserRole).toString() if item else None
self.clear()
accounts = self.wallet.get_accounts()
if self.parent.current_account is None:
account_items = sorted(accounts.items())
else:
account_items = [(self.parent.current_account, accounts.get(self.parent.current_account))]
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]
receiving_addresses = self.wallet.get_receiving_addresses()
change_addresses = self.wallet.get_change_addresses()
if True:
account_item = self
sequences = [0,1] if change_addresses else [0]
for is_change in sequences:
if len(sequences) > 1:
name = _("Receiving") if not is_change else _("Change")
@ -72,7 +60,7 @@ class AddressList(MyTreeWidget):
seq_item = account_item
used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
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:
num = len(self.wallet.history.get(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
if self.wallet.is_frozen(address):
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'))
if is_used:
if not used_flag:
@ -107,8 +95,9 @@ class AddressList(MyTreeWidget):
address_item.addChild(utxo_item)
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_imported = isinstance(self.wallet, Imported_Wallet)
selected = self.selectedItems()
multi_select = len(selected) > 1
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():
menu.addAction(_("Sign/verify message"), lambda: self.parent.sign_verify_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))
addr_URL = block_explorer_URL(self.config, 'addr', addr)
if addr_URL:
@ -161,18 +150,3 @@ class AddressList(MyTreeWidget):
run_hook('receive_menu', menu, addrs, self.wallet)
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)

View File

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

View File

@ -1,4 +1,5 @@
import sys
import os
from PyQt4.QtGui import *
from PyQt4.QtCore import *
@ -156,22 +157,47 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
if self.config.get('auto_connect') is None:
self.choose_server(self.network)
action = self.get_action()
if action != 'new':
path = self.storage.path
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()
path = self.storage.path
msg = _("The file '%s' contains an incompletely created wallet.\n"
"Do you want to complete its creation now?") % path
if not self.question(msg):
if self.question(_("Do you want to delete '%s'?") % path):
import os
os.remove(path)
self.show_warning(_('The file was removed'))
return
return
self.show()
self.run(action)
return self.wallet
if action:
# self.wallet is set in run
self.run(action)
return self.wallet
def finished(self):
'''Ensure the dialog is closed.'''

View File

@ -51,7 +51,7 @@ from electrum.util import (block_explorer, block_explorer_info, format_time,
from electrum import Transaction, mnemonic
from electrum import util, bitcoin, commands, coinchooser
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 network_dialog import NetworkDialog
@ -248,21 +248,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
t.setDaemon(True)
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):
if self.wallet:
self.print_error('close_wallet', self.wallet.storage.path)
self.address_list.on_close()
run_hook('close_wallet', self.wallet)
def load_wallet(self, wallet):
@ -270,13 +258,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.wallet = wallet
self.update_recently_visited(wallet.storage.path)
# 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.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
self.notify_transactions()
# update menus
self.update_new_account_menu()
self.seed_menu.setEnabled(self.wallet.has_seed())
self.mpk_menu.setEnabled(self.wallet.is_deterministic())
self.update_lock_icon()
@ -391,8 +377,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
wallet_menu = menubar.addMenu(_("&Wallet"))
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()
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)
icon = QIcon(":icons/status_lagging.png")
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))
if u:
text += " [%s unconfirmed]"%(self.format_amount(u, True).strip())
@ -593,8 +577,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.update_status()
if self.wallet.up_to_date or not self.network or not self.network.is_connected():
self.update_tabs()
if self.wallet.up_to_date:
self.check_next_account()
def update_tabs(self):
self.history_list.update()
@ -788,7 +770,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.saved = True
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:
from electrum.wallet import Imported_Wallet
if isinstance(self.wallet, Imported_Wallet):
@ -796,7 +778,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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?")):
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.expires_label.hide()
self.expires_combo.show()
@ -809,7 +791,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.receive_amount_e.setAmount(None)
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_message_e.setText('')
self.receive_amount_e.setAmount(None)
@ -1102,7 +1084,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def request_password(self, *args, **kwargs):
parent = self.top_level_window()
password = None
while self.wallet.use_encryption:
while self.wallet.has_password():
password = self.password_dialog(parent=parent)
try:
if password:
@ -1208,7 +1190,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if tx.get_fee() >= self.config.get('confirm_fee', 100000):
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(_("Enter your password to proceed"))
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
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
return
@ -1438,7 +1420,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.pay_from:
return self.pay_from
else:
domain = self.wallet.get_account_addresses(self.current_account)
domain = self.wallet.get_addresses()
return self.wallet.get_spendable_coins(domain)
@ -1561,18 +1543,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
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):
@ -1583,11 +1553,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.balance_label = QLabel("")
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.textChanged.connect(self.do_search)
self.search_box.hide()
@ -1606,7 +1571,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.setStatusBar(sb)
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)
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 '
'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'))
d = PasswordDialog(self, self.wallet, msg, PW_CHANGE)
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())):
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):
dialog = WindowModalDialog(self, "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:
def label(key):
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")]
return "%s (%s)" % (key, mine_text[is_mine])
return key
@ -1759,19 +1682,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
@protected
def show_seed_dialog(self, password):
if self.wallet.use_encryption and password is None:
return # User cancelled password input
if self.wallet.has_password() and password is None:
# User cancelled password input
return
if not self.wallet.has_seed():
self.show_message(_('This wallet has no seed'))
return
try:
mnemonic = self.wallet.get_mnemonic(password)
except BaseException as e:
self.show_error(str(e))
return
from seed_dialog import SeedDialog
d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
d = SeedDialog(self, mnemonic)
d.exec_()
@ -1795,9 +1718,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
d.setMinimumSize(600, 200)
vbox = QVBoxLayout()
vbox.addWidget( QLabel(_("Address") + ': ' + address))
if isinstance(self.wallet, BIP32_RD_Wallet):
derivation = self.wallet.address_id(address)
vbox.addWidget(QLabel(_("Derivation") + ': ' + derivation))
#if isinstance(self.wallet, BIP32_RD_Wallet):
# derivation = self.wallet.address_id(address)
# vbox.addWidget(QLabel(_("Derivation") + ': ' + derivation))
vbox.addWidget(QLabel(_("Public key") + ':'))
keys_e = ShowQRTextEdit(text='\n'.join(pubkey_list))
keys_e.addCopyButton(self.app)
@ -2045,7 +1968,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.wallet.is_watching_only():
self.show_message(_("This is a watching-only wallet"))
return
try:
self.wallet.check_password(password)
except Exception as e:
@ -2235,7 +2157,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
keys_e.setTabChangesFocus(True)
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)
vbox.addLayout(h)
@ -2271,19 +2193,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
@protected
def do_import_privkey(self, password):
if not self.wallet.has_imported_keys():
if not self.question('<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
+ _('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
if not self.wallet.keystore.can_import():
return
text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
if not text: return
if not text:
return
text = str(text).split()
badkeys = []
addrlist = []
for key in text:
addr = self.wallet.import_key(key, password)
try:
addr = self.wallet.import_key(key, password)
except Exception as e:
@ -2673,25 +2592,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
vbox.addLayout(Buttons(CloseButton(d)))
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):
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
d = WindowModalDialog(self, _('Bump Fee'))

View File

@ -94,7 +94,7 @@ class PasswordLayout(object):
m1 = _('New Password:') if kind == PW_NEW else _('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(self.pw, 0, 1)
lockfile = ":icons/lock.png"

View File

@ -36,20 +36,19 @@ from util import MyTreeWidget, pr_tooltips, pr_icons
class RequestList(MyTreeWidget):
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.itemClicked.connect(self.item_changed)
self.setSortingEnabled(True)
self.setColumnWidth(0, 180)
self.hideColumn(1)
self.hideColumn(2)
def item_changed(self, item):
if item is None:
return
if not self.isItemSelected(item):
return
addr = str(item.text(2))
addr = str(item.text(1))
req = self.wallet.receive_requests[addr]
expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never')
amount = req['amount']
@ -72,13 +71,10 @@ class RequestList(MyTreeWidget):
self.parent.expires_label.hide()
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
current_address = self.parent.receive_address_e.text()
domain = self.wallet.get_account_addresses(self.parent.current_account, include_change=False)
addr = self.wallet.get_unused_address(self.parent.current_account)
domain = self.wallet.get_receiving_addresses()
addr = self.wallet.get_unused_address()
if not current_address in domain and addr:
self.parent.set_receive_address(addr)
self.parent.new_request_button.setEnabled(addr != current_address)
@ -98,11 +94,10 @@ class RequestList(MyTreeWidget):
signature = req.get('sig')
requestor = req.get('name', '')
amount_str = self.parent.format_amount(amount) if amount else ""
account = ''
item = QTreeWidgetItem([date, account, address, '', message, amount_str, pr_tooltips.get(status,'')])
item = QTreeWidgetItem([date, address, '', message, amount_str, pr_tooltips.get(status,'')])
if signature is not None:
item.setIcon(3, QIcon(":icons/seal.png"))
item.setToolTip(3, 'signed by '+ requestor)
item.setIcon(2, QIcon(":icons/seal.png"))
item.setToolTip(2, 'signed by '+ requestor)
if status is not PR_UNKNOWN:
item.setIcon(6, QIcon(pr_icons.get(status)))
self.addTopLevelItem(item)

View File

@ -39,19 +39,13 @@ def icon_filename(sid):
return ":icons/seed.png"
class SeedDialog(WindowModalDialog):
def __init__(self, parent, seed, imported_keys):
def __init__(self, parent, seed):
WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed')))
self.setMinimumWidth(400)
vbox = QVBoxLayout(self)
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)))
class SeedLayoutBase(object):
def _seed_layout(self, seed=None, title=None, sid=None):
logo = QLabel()

View File

@ -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)))

View File

@ -24,24 +24,21 @@
# SOFTWARE.
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 _
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)
from plugins import run_hook
class BaseWizard(object):
def __init__(self, config, network, path):
super(BaseWizard, self).__init__()
self.config = config
self.config = config
self.network = network
self.storage = WalletStorage(path)
self.wallet = None
self.stack = []
self.plugin = None
def run(self, *args):
action = args[0]
@ -49,27 +46,17 @@ class BaseWizard(object):
self.stack.append((action, args))
if not action:
return
if hasattr(self.wallet, 'plugin') and hasattr(self.wallet.plugin, action):
f = getattr(self.wallet.plugin, action)
apply(f, (self.wallet, self) + args)
if type(action) is tuple:
self.plugin, action = action
if self.plugin and hasattr(self.plugin, action):
f = getattr(self.plugin, action)
apply(f, (self,) + args)
elif hasattr(self, action):
f = getattr(self, action)
apply(f, args)
else:
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):
return len(self.stack)>1
@ -91,11 +78,10 @@ class BaseWizard(object):
('standard', _("Standard wallet")),
('twofactor', _("Wallet with two-factor authentication")),
('multisig', _("Multi-signature wallet")),
('hardware', _("Hardware wallet")),
]
registered_kinds = Wallet.categories()
choices = [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)
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)
def on_wallet_type(self, choice):
self.wallet_type = choice
@ -103,66 +89,58 @@ class BaseWizard(object):
action = 'choose_seed'
elif choice == 'multisig':
action = 'choose_multisig'
elif choice == 'hardware':
action = 'choose_hw'
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)
def choose_multisig(self):
def on_multisig(m, n):
self.multisig_type = "%dof%d"%(m, n)
self.n = n
self.run('choose_seed')
self.multisig_dialog(run_next=on_multisig)
def choose_seed(self):
title = _('Choose Seed')
message = _("Do you want to create a new seed, or to restore a wallet using an existing seed?")
if self.wallet_type == 'standard':
title = _('Seed and Private Keys')
message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
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')),
('restore_from_key', _('Import keys or addresses')),
('choose_hw', _('Use hardware wallet')),
]
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 = [
('create_seed', _('Create a new seed')),
('restore_seed', _('I already have a seed')),
('restore_from_key', _('I have a master key')),
#('choose_hw', _('Cosign with hardware wallet')),
]
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')
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
def restore_seed(self):
# 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):
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)
else:
self.create_wallet(text, None)
self.create_keystore(text, None)
def restore_from_key(self):
if self.wallet_type == 'standard':
v = is_any_key
v = keystore.is_any_key
title = _("Import keys")
message = ' '.join([
_("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.")
])
else:
v = is_bip32_key
v = keystore.is_bip32_key
title = _("Master public or private key")
message = ' '.join([
_("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)
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):
self.storage.put('key_type', 'hardware')
hw_wallet_types, choices = self.plugins.hardware_wallets('create')
choices = zip(hw_wallet_types, choices)
title = _('Hardware wallet')
@ -189,84 +163,87 @@ class BaseWizard(object):
self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_hardware)
def on_hardware(self, hw_type):
self.hw_type = hw_type
if self.wallet_type == 'multisig':
self.create_hardware_multisig()
else:
title = _('Hardware wallet') + ' [%s]' % hw_type
message = _('Do you have a device, or do you want to restore a wallet using an existing seed?')
choices = [
('create_hardware_wallet', _('I have a device')),
('restore_hardware_wallet', _('Use hardware wallet seed')),
]
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
self.storage.put('hardware_type', hw_type)
title = _('Hardware wallet') + ' [%s]' % hw_type
message = _('Do you have a device, or do you want to restore a wallet using an existing seed?')
choices = [
('on_hardware_device', _('I have a %s device')%hw_type),
('on_hardware_seed', _('I have a %s seed')%hw_type),
]
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 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 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 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 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 create_wallet(self, k, password):
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')
elif self.wallet_type == 'multisig':
self.storage.put('wallet_type', self.multisig_type)
self.wallet = Multisig_Wallet(self.storage)
self.wallet.add_cosigner('x1/', text, password)
self.add_cosigner(k, 0)
xpub = k.get_master_public_key()
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.wallet.master_public_keys.get('x1/')
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('add_cosigners', password))
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 add_cosigners(self, password):
i = self.wallet.get_missing_cosigner()
self.add_cosigner_dialog(run_next=lambda x: self.on_cosigner(x, password), index=(i-1), is_valid=Wallet.is_xpub)
def add_cosigner(self, keystore, i):
d = self.storage.get('master_public_keys', {})
if keystore.xpub in d.values():
raise BaseException('duplicate key')
keystore.save(self.storage, 'x%d/'%(i+1))
def on_cosigner(self, text, password):
i = self.wallet.get_missing_cosigner()
def add_cosigners(self, password, i):
self.add_cosigner_dialog(run_next=lambda x: self.on_cosigner(x, password, i), index=i, is_valid=keystore.is_xpub)
def on_cosigner(self, text, password, i):
k = keystore.from_text(text, password)
try:
self.wallet.add_cosigner('x%d/'%i, text, password)
self.add_cosigner(k, i)
except BaseException as e:
print "error:" + str(e)
i = self.wallet.get_missing_cosigner()
if i:
self.run('add_cosigners', password)
self.show_message("error:" + str(e))
return
if i < self.n - 1:
self.run('add_cosigners', password, i+1)
else:
self.wallet = Multisig_Wallet(self.storage)
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):
from electrum.wallet import BIP32_Wallet
seed = BIP32_Wallet.make_seed()
from electrum.mnemonic import Mnemonic
seed = Mnemonic('en').make_seed()
self.show_seed_dialog(run_next=self.confirm_seed, seed_text=seed)
def confirm_seed(self, seed):
self.confirm_seed_dialog(run_next=self.add_password, is_valid=lambda x: x==seed)
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)
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)

View File

@ -300,12 +300,9 @@ class Commands:
return self.wallet.get_public_keys(address)
@command('w')
def getbalance(self, account=None):
def getbalance(self):
"""Return the balance of your wallet. """
if account is None:
c, u, x = self.wallet.get_balance()
else:
c, u, x = self.wallet.get_account_balance(account)
c, u, x = self.wallet.get_balance()
out = {"confirmed": str(Decimal(c)/COIN)}
if u:
out["unconfirmed"] = str(Decimal(u)/COIN)
@ -357,7 +354,7 @@ class Commands:
@command('wp')
def getmasterprivate(self):
"""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')
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):
"""List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results."""
out = []
for addr in self.wallet.addresses(True):
for addr in self.wallet.get_addresses():
if frozen and not self.wallet.is_frozen(addr):
continue
if receiving and self.wallet.is_change(addr):
@ -681,7 +678,6 @@ command_options = {
'unsigned': ("-u", "--unsigned", "Do not sign transaction"),
'rbf': (None, "--rbf", "Replace-by-fee transaction"),
'domain': ("-D", "--domain", "List of addresses"),
'account': (None, "--account", "Account"),
'memo': ("-m", "--memo", "Description of the request"),
'expiration': (None, "--expiration", "Time in seconds"),
'timeout': (None, "--timeout", "Timeout in seconds"),

View File

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

701
lib/keystore.py Normal file
View File

@ -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

View File

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

253
lib/storage.py Normal file
View File

@ -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

View File

@ -180,7 +180,7 @@ class Synchronizer(ThreadJob):
if 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):
'''Called from the network proxy thread main loop.'''

View File

@ -761,23 +761,6 @@ class Transaction:
out.add(i)
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):
for i, txin in enumerate(self.inputs()):

File diff suppressed because it is too large Load Diff

View File

@ -126,7 +126,8 @@ class Plugin(BasePlugin):
self.listener = None
self.keys = []
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')
_hash = bitcoin.Hash(K).encode('hex')
if wallet.master_private_keys.get(key):

View File

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

View File

@ -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)

View File

@ -37,8 +37,8 @@ class HW_PluginBase(BasePlugin):
def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name)
self.device = self.wallet_class.device
self.wallet_class.plugin = self
self.device = self.keystore_class.device
self.keystore_class.plugin = self
def is_enabled(self):
return self.libraries_available
@ -48,33 +48,6 @@ class HW_PluginBase(BasePlugin):
@hook
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)
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

View File

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

View File

@ -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'
device = 'KeepKey'
@ -10,7 +10,7 @@ class KeepKeyPlugin(TrezorCompatiblePlugin):
firmware_URL = 'https://www.keepkey.com'
libraries_URL = 'https://github.com/keepkey/python-keepkey'
minimum_firmware = (1, 0, 0)
wallet_class = KeepKeyWallet
keystore_class = KeepKey_KeyStore
try:
from .client import KeepKeyClient as client_class
import keepkeylib.ckd_public as ckd_public

View File

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

View File

@ -7,7 +7,7 @@ import electrum
from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, TYPE_ADDRESS
from electrum.i18n import _
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 electrum.util import format_satoshis_plain, print_error
@ -26,12 +26,12 @@ except ImportError:
BTCHIP = False
class BTChipWallet(BIP44_HW_Wallet):
class BTChipWallet(BIP32_HW_Wallet):
wallet_type = 'btchip'
device = 'Ledger'
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
# handler. The handler is per-window and preserved across
# device reconnects
@ -53,7 +53,7 @@ class BTChipWallet(BIP44_HW_Wallet):
def address_id(self, address):
# 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):
# bip32_path is of the form 44'/0'/1'

View File

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

View File

@ -1,8 +1,10 @@
import time
from struct import pack
from electrum.i18n import _
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):
@ -63,7 +65,7 @@ class GuiMixin(object):
passphrase = self.handler.get_passphrase(msg, self.creating_wallet)
if passphrase is None:
return self.proto.Cancel()
passphrase = BIP44_Wallet.normalize_passphrase(passphrase)
passphrase = BIP44_KeyStore.normalize_passphrase(passphrase)
return self.proto.PassphraseAck(passphrase=passphrase)
def callback_WordRequest(self, msg):
@ -142,11 +144,20 @@ class TrezorClientBase(GuiMixin, PrintError):
'''Provided here as in keepkeylib but not trezorlib.'''
self.transport.write(self.proto.Cancel())
def first_address(self, derivation):
return self.address_from_derivation(derivation)
def i4b(self, x):
return pack('>I', x)
def address_from_derivation(self, derivation):
return self.get_address('Bitcoin', self.expand_path(derivation))
def get_xpub(self, bip32_path):
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):
if self.features.passphrase_protection:

View File

@ -8,28 +8,32 @@ from functools import partial
from electrum.account import BIP32_Account
from electrum.bitcoin import (bc_address_to_hash_160, xpub_from_pubkey,
public_key_to_bc_address, EncodeBase58Check,
TYPE_ADDRESS)
TYPE_ADDRESS, TYPE_SCRIPT)
from electrum.i18n import _
from electrum.plugins import BasePlugin, hook
from electrum.transaction import (deserialize, is_extended_pubkey,
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
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_public_key(self, bip32_path):
def get_derivation(self):
return self.root + "/%d'"%self.account_id
def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair)
def init_xpub(self):
client = self.get_client()
address_n = client.expand_path(bip32_path)
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)
self.xpub = client.get_xpub(self.get_derivation())
def decrypt_message(self, pubkey, message, password):
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)
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):
if tx.is_complete():
return
@ -69,15 +62,13 @@ class TrezorCompatibleWallet(BIP44_HW_Wallet):
xpub_path = {}
for txin in tx.inputs():
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']:
if not is_extended_pubkey(x_pubkey):
continue
xpub = x_to_xpub(x_pubkey)
for k, v in self.master_public_keys.items():
if v == xpub:
acc_id = re.match("x/(\d+)'", k).group(1)
xpub_path[xpub] = self.account_derivation(acc_id)
if xpub == self.get_master_public_key():
xpub_path[xpub] = self.get_derivation()
self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
@ -149,18 +140,16 @@ class TrezorCompatiblePlugin(HW_PluginBase):
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
assert self.main_thread != threading.current_thread()
devmgr = self.device_manager()
client = devmgr.client_for_wallet(self, wallet, force_pair)
client = devmgr.client_for_keystore(self, keystore, force_pair)
if client:
client.used()
return client
def initialize_device(self, wallet):
def initialize_device(self, keystore):
# Initialization method
msg = _("Choose how you want to initialize your %s.\n\n"
"The first two methods are secure as no secret information "
@ -179,13 +168,13 @@ class TrezorCompatiblePlugin(HW_PluginBase):
_("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) \
= wallet.handler.request_trezor_init_settings(method, self.device)
if method == TIM_RECOVER and self.device == 'TREZOR':
# Warn user about firmware lameness
wallet.handler.show_error(_(
keystore.handler.show_error(_(
"You will be asked to enter 24 words regardless of your "
"seed's actual length. If you enter a word incorrectly or "
"misspell it, you cannot change it or go back - you will need "
@ -195,7 +184,7 @@ class TrezorCompatiblePlugin(HW_PluginBase):
language = 'english'
def initialize_method():
client = self.get_client(wallet)
client = self.get_client(keystore)
if method == TIM_NEW:
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,
label, language)
# After successful initialization create accounts
wallet.create_hd_account(None)
keystore.init_xpub()
#wallet.create_main_account()
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
the device is uninitialized, go through the intialization
process. Then create the wallet accounts.'''
devmgr = self.device_manager()
device_info = devmgr.select_device(wallet, self)
devmgr.pair_wallet(wallet, device_info.device.id_)
device_info = devmgr.select_device(keystore, self)
devmgr.pair_wallet(keystore, device_info.device.id_)
if device_info.initialized:
task = partial(wallet.create_hd_account, None)
task = keystore.init_xpub
else:
task = self.initialize_device(wallet)
wallet.thread.add(task, on_done=on_done, on_error=on_error)
task = self.initialize_device(keystore)
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.xpub_path = xpub_path
client = self.get_client(wallet)
client = self.get_client(keystore)
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]
raw = signed_tx.encode('hex')
tx.update_signatures(raw)
def show_address(self, wallet, address):
client = self.get_client(wallet)
client = self.get_client(wallet.keystore)
if not client.atleast_version(1, 3):
wallet.handler.show_error(_("Your device firmware is too old"))
return
@ -313,23 +303,29 @@ class TrezorCompatiblePlugin(HW_PluginBase):
return inputs
def tx_outputs(self, wallet, tx):
def tx_outputs(self, derivation, tx):
outputs = []
for type, address, amount in tx.outputs():
assert type == TYPE_ADDRESS
for i, (_type, address, amount) in enumerate(tx.outputs()):
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
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
change, index = tx.output_info[i]
if _type == TYPE_SCRIPT:
txoutputtype.script_type = self.types.PAYTOOPRETURN
txoutputtype.op_return_data = address[2:]
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:
raise BaseException('addrtype')
outputs.append(txoutputtype)

View File

@ -12,7 +12,7 @@ from ..hw_wallet.qt import QtHandlerBase
from electrum.i18n import _
from electrum.plugins import hook, DeviceMgr
from electrum.util import PrintError, UserCancelled
from electrum.wallet import Wallet, BIP44_Wallet
from electrum.wallet import Wallet
PASSPHRASE_HELP_SHORT =_(
"Passphrases allow you to access new wallets, each "
@ -273,23 +273,25 @@ def qt_plugin_class(base_plugin_class):
@hook
def load_wallet(self, wallet, window):
if type(wallet) != self.wallet_class:
keystore = wallet.get_keystore()
if type(keystore) != self.keystore_class:
return
window.tzb = StatusBarButton(QIcon(self.icon_file), self.device,
partial(self.settings_dialog, window))
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
wallet.thread.add(partial(self.get_client, wallet))
keystore.thread.add(partial(self.get_client, keystore))
def on_create_wallet(self, wallet, wizard):
assert type(wallet) == self.wallet_class
wallet.handler = self.create_handler(wizard)
wallet.thread = TaskThread(wizard, wizard.on_error)
def on_create_wallet(self, keystore, wizard):
#assert type(keystore) == self.keystore_class
keystore.handler = self.create_handler(wizard)
keystore.thread = TaskThread(wizard, wizard.on_error)
# Setup device and create accounts in separate thread; wait until done
loop = QEventLoop()
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))
loop.exec_()
# If an exception was thrown, show to user and exit install wizard
@ -299,9 +301,10 @@ def qt_plugin_class(base_plugin_class):
@hook
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():
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)
def settings_dialog(self, window):
@ -312,9 +315,10 @@ def qt_plugin_class(base_plugin_class):
def choose_device(self, window):
'''This dialog box should be usable even if the user has
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:
info = self.device_manager().select_device(window.wallet, self)
info = self.device_manager().select_device(keystore, self)
device_id = info.device.id_
return device_id
@ -345,8 +349,9 @@ class SettingsDialog(WindowModalDialog):
devmgr = plugin.device_manager()
config = devmgr.config
handler = window.wallet.handler
thread = window.wallet.thread
keystore = window.wallet.get_keystore()
handler = keystore.handler
thread = keystore.thread
# wallet can be None, needn't be window.wallet
wallet = devmgr.wallet_by_id(device_id)
hs_rows, hs_cols = (64, 128)

View File

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

View File

@ -34,10 +34,11 @@ from urllib import quote
import electrum
from electrum import bitcoin
from electrum import keystore
from electrum.bitcoin import *
from electrum.mnemonic import Mnemonic
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.plugins import BasePlugin, run_hook, hook
from electrum.util import NotEnoughFunds
@ -187,29 +188,16 @@ server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VER
class Wallet_2fa(Multisig_Wallet):
def __init__(self, storage):
BIP32_Wallet.__init__(self, storage)
self.wallet_type = '2fa'
self.m = 2
self.n = 3
self.m, self.n = 2, 3
Deterministic_Wallet.__init__(self, storage)
self.is_billing = False
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):
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):
from electrum.transaction import Transaction
@ -244,7 +232,7 @@ class Wallet_2fa(Multisig_Wallet):
def make_unsigned_transaction(self, coins, outputs, config,
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)
fee = self.extra_fee()
if fee:
@ -264,7 +252,7 @@ class Wallet_2fa(Multisig_Wallet):
return tx
def sign_transaction(self, tx, password):
BIP32_Wallet.sign_transaction(self, tx, password)
Multisig_Wallet.sign_transaction(self, tx, password)
if tx.is_complete():
return
if not self.auth_code:
@ -279,27 +267,25 @@ class Wallet_2fa(Multisig_Wallet):
tx.update(raw_tx)
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
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):
_, _, _, c, cK = deserialize_xkey(xpub)
cK2, c2 = bitcoin._CKD_pub(cK, c, s)
xpub2 = ("0488B21E" + "00" + "00000000" + "00000000").decode("hex") + c2 + cK2
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):
long_id, short_id = wallet.get_user_id()
@ -324,9 +310,6 @@ class TrustedCoinPlugin(BasePlugin):
def is_available(self):
return True
def set_enabled(self, wallet, enabled):
wallet.storage.put('use_' + self.name, enabled)
def is_enabled(self):
return True
@ -345,28 +328,42 @@ class TrustedCoinPlugin(BasePlugin):
wallet.price_per_tx = dict(billing_info['price_per_tx'])
return True
def create_extended_seed(self, wallet, wizard):
self.wallet = wallet
self.wizard = wizard
seed = wallet.make_seed()
self.wizard.show_seed_dialog(run_next=wizard.confirm_seed, seed_text=seed)
def make_seed(self):
return Mnemonic('english').make_seed(num_bits=256, prefix=SEED_PREFIX)
def show_disclaimer(self, wallet, wizard):
self.set_enabled(wallet, True)
@hook
def do_clear(self, window):
window.wallet.is_billing = False
def show_disclaimer(self, wizard):
wizard.set_icon(':icons/trustedcoin.png')
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 create_wallet(self, wallet, wizard, seed, password):
wallet.storage.put('seed_version', wallet.seed_version)
wallet.storage.put('use_encryption', password is not None)
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_keystore(self, wizard, seed, password):
# this overloads the wizard's method
words = seed.split()
n = len(words)/2
wallet.add_xprv_from_seed(' '.join(words[0:n]), 'x1/', password)
wallet.add_xpub_from_seed(' '.join(words[n:]), 'x2/')
wallet.storage.write()
keystore1 = keystore.xprv_from_seed(' '.join(words[0:n]), password)
keystore2 = keystore.xpub_from_seed(' '.join(words[n:]))
keystore1.save(wizard.storage, 'x1/')
keystore2.save(wizard.storage, 'x2/')
wizard.storage.write()
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 "
"your wallet. If you generated your seed on an offline "
'computer, click on "%s" to close this window, move your '
@ -378,41 +375,45 @@ class TrustedCoinPlugin(BasePlugin):
wizard.stack = []
wizard.confirm_dialog(msg, run_next = lambda x: wizard.run('create_remote_key'))
@hook
def do_clear(self, window):
window.wallet.is_billing = False
def on_restore_wallet(self, wallet, wizard):
assert isinstance(wallet, self.wallet_class)
def restore_wallet(self, wizard):
title = _("Restore two-factor Wallet")
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):
f = lambda x: wizard.run('on_restore_pw', seed, x)
def on_restore_seed(self, wizard, seed):
f = lambda pw: wizard.run('on_restore_pw', seed, pw)
wizard.request_password(run_next=f)
def on_restore_pw(self, wallet, wizard, seed, password):
wallet.add_seed(seed, password)
def on_restore_pw(self, wizard, seed, password):
# FIXME
# wallet.add_seed(seed, password)
storage = wizard.storage
words = seed.split()
n = len(words)/2
wallet.add_xprv_from_seed(' '.join(words[0:n]), 'x1/', password)
wallet.add_xprv_from_seed(' '.join(words[n:]), 'x2/', password)
restore_third_key(wallet)
keystore1 = keystore.xprv_from_seed(' '.join(words[0:n]), password)
keystore2 = keystore.xprv_from_seed(' '.join(words[n:]), password)
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()
def create_remote_key(self, wallet, window):
email = self.accept_terms_of_use(window)
xpub_hot = wallet.master_public_keys["x1/"]
xpub_cold = wallet.master_public_keys["x2/"]
def create_remote_key(self, wizard):
email = self.accept_terms_of_use(wizard)
mpk = wizard.storage.get('master_public_keys')
xpub1 = mpk["x1/"]
xpub2 = mpk["x2/"]
# 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)
# secret must be sent by the server
try:
r = server.create(xpub_hot, xpub_cold, email)
r = server.create(xpub1, xpub2, email)
except socket.error:
window.show_message('Server not reachable, aborting')
wizard.show_message('Server not reachable, aborting')
return
except TrustedCoinException as e:
if e.status_code == 409:
@ -424,7 +425,7 @@ class TrustedCoinPlugin(BasePlugin):
else:
otp_secret = r.get('otp_secret')
if not otp_secret:
window.show_message(_('Error'))
wizard.show_message(_('Error'))
return
_xpub3 = r['xpubkey_cosigner']
_id = r['id']
@ -432,10 +433,24 @@ class TrustedCoinPlugin(BasePlugin):
assert _id == short_id, ("user id error", _id, short_id)
assert xpub3 == _xpub3, ("xpub3 error", xpub3, _xpub3)
except Exception as e:
window.show_message(str(e))
wizard.show_message(str(e))
return
if not self.setup_google_auth(window, short_id, otp_secret):
window.show_message("otp error")
if not self.setup_google_auth(wizard, short_id, otp_secret):
wizard.show_message("otp error")
return
wallet.add_master_public_key('x3/', xpub3)
window.run('create_addresses')
keystore3 = keystore.from_xpub(xpub3)
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'