commit 1159f85e053d14cf931f5b32444bda071860619c
parent 6373a76a4a80038936a2c4201b021e1b33c8e8bd
Author: ThomasV <thomasv@electrum.org>
Date: Sat, 2 Jul 2016 08:58:56 +0200
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
Diffstat:
35 files changed, 1737 insertions(+), 2052 deletions(-)
diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py
@@ -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:
diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py
@@ -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):
diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py
@@ -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()
diff --git a/gui/qt/__init__.py b/gui/qt/__init__.py
@@ -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:
diff --git a/gui/qt/address_list.py b/gui/qt/address_list.py
@@ -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)
diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
@@ -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
diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py
@@ -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.'''
diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
@@ -51,7 +51,7 @@ from electrum.util import (block_explorer, block_explorer_info, format_time,
from electrum import Transaction, mnemonic
from electrum import 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'))
diff --git a/gui/qt/password_dialog.py b/gui/qt/password_dialog.py
@@ -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"
diff --git a/gui/qt/request_list.py b/gui/qt/request_list.py
@@ -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)
diff --git a/gui/qt/seed_dialog.py b/gui/qt/seed_dialog.py
@@ -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()
diff --git a/lib/account.py b/lib/account.py
@@ -1,381 +0,0 @@
-#!/usr/bin/env python
-#
-# Electrum - lightweight Bitcoin client
-# Copyright (C) 2013 thomasv@gitorious
-#
-# Permission is hereby granted, free of charge, to any person
-# obtaining a copy of this software and associated documentation files
-# (the "Software"), to deal in the Software without restriction,
-# including without limitation the rights to use, copy, modify, merge,
-# publish, distribute, sublicense, and/or sell copies of the Software,
-# and to permit persons to whom the Software is furnished to do so,
-# subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
-# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-
-import bitcoin
-from bitcoin import *
-from i18n import _
-from transaction import Transaction, is_extended_pubkey
-from util import InvalidPassword
-
-
-class Account(object):
- def __init__(self, v):
- self.receiving_pubkeys = v.get('receiving', [])
- self.change_pubkeys = v.get('change', [])
- # addresses will not be stored on disk
- self.receiving_addresses = map(self.pubkeys_to_address, self.receiving_pubkeys)
- self.change_addresses = map(self.pubkeys_to_address, self.change_pubkeys)
-
- def dump(self):
- return {'receiving':self.receiving_pubkeys, 'change':self.change_pubkeys}
-
- def get_pubkey(self, for_change, n):
- pubkeys_list = self.change_pubkeys if for_change else self.receiving_pubkeys
- return pubkeys_list[n]
-
- def get_address(self, for_change, n):
- addr_list = self.change_addresses if for_change else self.receiving_addresses
- return addr_list[n]
-
- def get_pubkeys(self, for_change, n):
- return [ self.get_pubkey(for_change, n)]
-
- def get_addresses(self, for_change):
- addr_list = self.change_addresses if for_change else self.receiving_addresses
- return addr_list[:]
-
- def derive_pubkeys(self, for_change, n):
- pass
-
- def create_new_address(self, for_change):
- pubkeys_list = self.change_pubkeys if for_change else self.receiving_pubkeys
- addr_list = self.change_addresses if for_change else self.receiving_addresses
- n = len(pubkeys_list)
- pubkeys = self.derive_pubkeys(for_change, n)
- address = self.pubkeys_to_address(pubkeys)
- pubkeys_list.append(pubkeys)
- addr_list.append(address)
- return address
-
- def pubkeys_to_address(self, pubkey):
- return public_key_to_bc_address(pubkey.decode('hex'))
-
- def has_change(self):
- return True
-
- def get_name(self, k):
- return _('Main account')
-
- def redeem_script(self, for_change, n):
- return None
-
- def is_used(self, wallet):
- addresses = self.get_addresses(False)
- return any(wallet.address_is_old(a, -1) for a in addresses)
-
- def synchronize_sequence(self, wallet, for_change):
- limit = wallet.gap_limit_for_change if for_change else wallet.gap_limit
- while True:
- addresses = self.get_addresses(for_change)
- if len(addresses) < limit:
- address = self.create_new_address(for_change)
- wallet.add_address(address)
- continue
- if map( lambda a: wallet.address_is_old(a), addresses[-limit:] ) == limit*[False]:
- break
- else:
- address = self.create_new_address(for_change)
- wallet.add_address(address)
-
- def synchronize(self, wallet):
- self.synchronize_sequence(wallet, False)
- self.synchronize_sequence(wallet, True)
-
-
-class ImportedAccount(Account):
- def __init__(self, d):
- self.keypairs = d['imported']
-
- def synchronize(self, wallet):
- return
-
- def get_addresses(self, for_change):
- return [] if for_change else sorted(self.keypairs.keys())
-
- def get_pubkey(self, *sequence):
- for_change, i = sequence
- assert for_change == 0
- addr = self.get_addresses(0)[i]
- return self.keypairs[addr][0]
-
- def get_xpubkeys(self, for_change, n):
- return self.get_pubkeys(for_change, n)
-
- def get_private_key(self, sequence, wallet, password):
- from wallet import pw_decode
- for_change, i = sequence
- assert for_change == 0
- address = self.get_addresses(0)[i]
- pk = pw_decode(self.keypairs[address][1], password)
- # this checks the password
- if address != address_from_private_key(pk):
- raise InvalidPassword()
- return [pk]
-
- def has_change(self):
- return False
-
- def add(self, address, pubkey, privkey, password):
- from wallet import pw_encode
- self.keypairs[address] = [pubkey, pw_encode(privkey, password)]
-
- def remove(self, address):
- self.keypairs.pop(address)
-
- def dump(self):
- return {'imported':self.keypairs}
-
- def get_name(self, k):
- return _('Imported keys')
-
- def update_password(self, old_password, new_password):
- for k, v in self.keypairs.items():
- pubkey, a = v
- b = pw_decode(a, old_password)
- c = pw_encode(b, new_password)
- self.keypairs[k] = (pubkey, c)
-
-
-class OldAccount(Account):
- """ Privatekey(type,n) = Master_private_key + H(n|S|type) """
-
- def __init__(self, v):
- Account.__init__(self, v)
- self.mpk = v['mpk'].decode('hex')
-
- @classmethod
- def mpk_from_seed(klass, seed):
- secexp = klass.stretch_key(seed)
- master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
- master_public_key = master_private_key.get_verifying_key().to_string().encode('hex')
- return master_public_key
-
- @classmethod
- def stretch_key(self,seed):
- oldseed = seed
- for i in range(100000):
- seed = hashlib.sha256(seed + oldseed).digest()
- return string_to_number( seed )
-
- @classmethod
- def get_sequence(self, mpk, for_change, n):
- return string_to_number( Hash( "%d:%d:"%(n,for_change) + mpk ) )
-
- def get_address(self, for_change, n):
- pubkey = self.get_pubkey(for_change, n)
- address = public_key_to_bc_address( pubkey.decode('hex') )
- return address
-
- @classmethod
- def get_pubkey_from_mpk(self, mpk, for_change, n):
- z = self.get_sequence(mpk, for_change, n)
- master_public_key = ecdsa.VerifyingKey.from_string(mpk, curve = SECP256k1)
- pubkey_point = master_public_key.pubkey.point + z*SECP256k1.generator
- public_key2 = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve = SECP256k1)
- return '04' + public_key2.to_string().encode('hex')
-
- def derive_pubkeys(self, for_change, n):
- return self.get_pubkey_from_mpk(self.mpk, for_change, n)
-
- def get_private_key_from_stretched_exponent(self, for_change, n, secexp):
- order = generator_secp256k1.order()
- secexp = ( secexp + self.get_sequence(self.mpk, for_change, n) ) % order
- pk = number_to_string( secexp, generator_secp256k1.order() )
- compressed = False
- return SecretToASecret( pk, compressed )
-
-
- def get_private_key(self, sequence, wallet, password):
- seed = wallet.get_seed(password)
- self.check_seed(seed)
- for_change, n = sequence
- secexp = self.stretch_key(seed)
- pk = self.get_private_key_from_stretched_exponent(for_change, n, secexp)
- return [pk]
-
-
- def check_seed(self, seed):
- secexp = self.stretch_key(seed)
- master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
- master_public_key = master_private_key.get_verifying_key().to_string()
- if master_public_key != self.mpk:
- print_error('invalid password (mpk)', self.mpk.encode('hex'), master_public_key.encode('hex'))
- raise InvalidPassword()
- return True
-
- def get_master_pubkeys(self):
- return [self.mpk.encode('hex')]
-
- def get_type(self):
- return _('Old Electrum format')
-
- def get_xpubkeys(self, for_change, n):
- s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n)))
- mpk = self.mpk.encode('hex')
- x_pubkey = 'fe' + mpk + s
- return [ x_pubkey ]
-
- @classmethod
- def parse_xpubkey(self, x_pubkey):
- assert is_extended_pubkey(x_pubkey)
- pk = x_pubkey[2:]
- mpk = pk[0:128]
- dd = pk[128:]
- s = []
- while dd:
- n = int(bitcoin.rev_hex(dd[0:4]), 16)
- dd = dd[4:]
- s.append(n)
- assert len(s) == 2
- return mpk, s
-
-
-class BIP32_Account(Account):
-
- def __init__(self, v):
- Account.__init__(self, v)
- self.xpub = v['xpub']
- self.xpub_receive = None
- self.xpub_change = None
-
- def dump(self):
- d = Account.dump(self)
- d['xpub'] = self.xpub
- return d
-
- def first_address(self):
- pubkeys = self.derive_pubkeys(0, 0)
- addr = self.pubkeys_to_address(pubkeys)
- return addr, pubkeys
-
- def get_master_pubkeys(self):
- return [self.xpub]
-
- @classmethod
- def derive_pubkey_from_xpub(self, xpub, for_change, n):
- _, _, _, c, cK = deserialize_xkey(xpub)
- for i in [for_change, n]:
- cK, c = CKD_pub(cK, c, i)
- return cK.encode('hex')
-
- def get_pubkey_from_xpub(self, xpub, for_change, n):
- xpubs = self.get_master_pubkeys()
- i = xpubs.index(xpub)
- pubkeys = self.get_pubkeys(for_change, n)
- return pubkeys[i]
-
- def derive_pubkeys(self, for_change, n):
- xpub = self.xpub_change if for_change else self.xpub_receive
- if xpub is None:
- xpub = bip32_public_derivation(self.xpub, "", "/%d"%for_change)
- if for_change:
- self.xpub_change = xpub
- else:
- self.xpub_receive = xpub
- _, _, _, c, cK = deserialize_xkey(xpub)
- cK, c = CKD_pub(cK, c, n)
- result = cK.encode('hex')
- return result
-
-
- def get_private_key(self, sequence, wallet, password):
- out = []
- xpubs = self.get_master_pubkeys()
- roots = [k for k, v in wallet.master_public_keys.iteritems() if v in xpubs]
- for root in roots:
- xpriv = wallet.get_master_private_key(root, password)
- if not xpriv:
- continue
- _, _, _, c, k = deserialize_xkey(xpriv)
- pk = bip32_private_key( sequence, k, c )
- out.append(pk)
- return out
-
- def get_type(self):
- return _('Standard 1 of 1')
-
- def get_xpubkeys(self, for_change, n):
- # unsorted
- s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change,n)))
- xpubs = self.get_master_pubkeys()
- return map(lambda xpub: 'ff' + bitcoin.DecodeBase58Check(xpub).encode('hex') + s, xpubs)
-
- @classmethod
- def parse_xpubkey(self, pubkey):
- assert is_extended_pubkey(pubkey)
- pk = pubkey.decode('hex')
- pk = pk[1:]
- xkey = bitcoin.EncodeBase58Check(pk[0:78])
- dd = pk[78:]
- s = []
- while dd:
- n = int( bitcoin.rev_hex(dd[0:2].encode('hex')), 16)
- dd = dd[2:]
- s.append(n)
- assert len(s) == 2
- return xkey, s
-
- def get_name(self, k):
- return "Main account" if k == '0' else "Account " + k
-
-
-
-
-class Multisig_Account(BIP32_Account):
-
- def __init__(self, v):
- self.m = v.get('m', 2)
- Account.__init__(self, v)
- self.xpub_list = v['xpubs']
-
- def dump(self):
- d = Account.dump(self)
- d['xpubs'] = self.xpub_list
- d['m'] = self.m
- return d
-
- def get_pubkeys(self, for_change, n):
- return self.get_pubkey(for_change, n)
-
- def derive_pubkeys(self, for_change, n):
- return map(lambda x: self.derive_pubkey_from_xpub(x, for_change, n), self.get_master_pubkeys())
-
- def redeem_script(self, for_change, n):
- pubkeys = self.get_pubkeys(for_change, n)
- return Transaction.multisig_script(sorted(pubkeys), self.m)
-
- def pubkeys_to_address(self, pubkeys):
- redeem_script = Transaction.multisig_script(sorted(pubkeys), self.m)
- address = hash_160_to_bc_address(hash_160(redeem_script.decode('hex')), 5)
- return address
-
- def get_address(self, for_change, n):
- return self.pubkeys_to_address(self.get_pubkeys(for_change, n))
-
- def get_master_pubkeys(self):
- return self.xpub_list
-
- def get_type(self):
- return _('Multisig %d of %d'%(self.m, len(self.xpub_list)))
diff --git a/lib/base_wizard.py b/lib/base_wizard.py
@@ -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':
- choices = [
- ('create_seed', _('Create a new seed')),
- ('restore_seed', _('I already have a seed')),
- ('restore_from_key', _('Import keys')),
- ]
- elif self.wallet_type == 'twofactor':
- choices = [
- ('create_2fa', _('Create a new seed')),
- ('restore_2fa', _('I already have a seed')),
- ]
- elif self.wallet_type == 'multisig':
+ 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', _('I have a master key')),
- #('choose_hw', _('Cosign with hardware wallet')),
+ ('restore_from_key', _('Import keys or addresses')),
+ ('choose_hw', _('Use 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)
-
- def create_hardware_multisig(self):
- self.storage.put('wallet_type', self.multisig_type)
- self.wallet = Multisig_Wallet(self.storage)
- # todo: get the xpub from the plugin
- self.run('create_wallet', xpub, None)
-
- def create_hardware_wallet(self):
- self.storage.put('wallet_type', self.hw_type)
- self.wallet = Wallet(self.storage)
- self.wallet.plugin.on_create_wallet(self.wallet, self)
- self.terminate()
-
- def restore_hardware_wallet(self):
- self.storage.put('wallet_type', self.wallet_type)
- self.wallet = Wallet(self.storage)
- self.wallet.plugin.on_restore_wallet(self.wallet, self)
- self.terminate()
+ 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_wallet(self, text, password):
+ def on_hardware_device(self):
+ from keystore import load_keystore
+ keystore = load_keystore(self.storage, None)
+ keystore.plugin.on_create_wallet(keystore, self)
+ self.create_wallet(keystore, None)
+
+ def on_hardware_seed(self):
+ from keystore import load_keystore
+ self.storage.put('key_type', 'hw_seed')
+ keystore = load_keystore(self.storage, None)
+ self.plugin = keystore #fixme .plugin
+ keystore.on_restore_wallet(self)
+ self.wallet = Standard_Wallet(self.storage)
+ self.run('create_addresses')
+
+ def create_wallet(self, k, password):
if self.wallet_type == 'standard':
- 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.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('add_cosigners', password, 1))
- 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 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 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_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 = self.wallet.get_missing_cosigner()
+ 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)
diff --git a/lib/commands.py b/lib/commands.py
@@ -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"),
diff --git a/lib/daemon.py b/lib/daemon.py
@@ -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
diff --git a/lib/keystore.py b/lib/keystore.py
@@ -0,0 +1,701 @@
+#!/usr/bin/env python2
+# -*- mode: python -*-
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2016 The Electrum developers
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation files
+# (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge,
+# publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+
+from unicodedata import normalize
+
+from version import *
+import bitcoin
+from bitcoin import pw_encode, pw_decode, bip32_root, bip32_private_derivation, bip32_public_derivation, bip32_private_key, deserialize_xkey
+from bitcoin import public_key_from_private_key, public_key_to_bc_address
+from bitcoin import *
+
+from bitcoin import is_old_seed, is_new_seed
+from util import PrintError, InvalidPassword
+from mnemonic import Mnemonic
+
+
+class KeyStore(PrintError):
+
+ def has_seed(self):
+ return False
+
+ def has_password(self):
+ return False
+
+ def is_watching_only(self):
+ return False
+
+ def can_import(self):
+ return False
+
+
+class Software_KeyStore(KeyStore):
+
+ def __init__(self):
+ KeyStore.__init__(self)
+ self.use_encryption = False
+
+ def has_password(self):
+ return self.use_encryption
+
+
+class Imported_KeyStore(Software_KeyStore):
+ # keystore for imported private keys
+
+ def __init__(self):
+ Software_KeyStore.__init__(self)
+ self.keypairs = {}
+
+ def is_deterministic(self):
+ return False
+
+ def can_change_password(self):
+ return True
+
+ def get_master_public_key(self):
+ return None
+
+ def load(self, storage, name):
+ self.keypairs = storage.get('keypairs', {})
+ self.use_encryption = storage.get('use_encryption', False)
+ self.receiving_pubkeys = self.keypairs.keys()
+ self.change_pubkeys = []
+
+ def save(self, storage, root_name):
+ storage.put('key_type', 'imported')
+ storage.put('keypairs', self.keypairs)
+ storage.put('use_encryption', self.use_encryption)
+
+ def can_import(self):
+ return True
+
+ def check_password(self, password):
+ self.get_private_key((0,0), password)
+
+ def import_key(self, sec, password):
+ if not self.can_import():
+ raise BaseException('This wallet cannot import private keys')
+ try:
+ pubkey = public_key_from_private_key(sec)
+ except Exception:
+ raise Exception('Invalid private key')
+ self.keypairs[pubkey] = sec
+ return pubkey
+
+ def delete_imported_key(self, key):
+ self.keypairs.pop(key)
+
+ def get_private_key(self, sequence, password):
+ for_change, i = sequence
+ assert for_change == 0
+ pubkey = (self.change_pubkeys if for_change else self.receiving_pubkeys)[i]
+ pk = pw_decode(self.keypairs[pubkey], password)
+ # this checks the password
+ if pubkey != public_key_from_private_key(pk):
+ raise InvalidPassword()
+ return pk
+
+ def update_password(self, old_password, new_password):
+ if old_password is not None:
+ self.check_password(old_password)
+ if new_password == '':
+ new_password = None
+ for k, v in self.keypairs.items():
+ b = pw_decode(v, old_password)
+ c = pw_encode(b, new_password)
+ self.keypairs[k] = b
+ self.use_encryption = (new_password is not None)
+
+
+class Deterministic_KeyStore(Software_KeyStore):
+
+ def __init__(self):
+ Software_KeyStore.__init__(self)
+ self.seed = ''
+
+ def is_deterministic(self):
+ return True
+
+ def load(self, storage, name):
+ self.seed = storage.get('seed', '')
+ self.use_encryption = storage.get('use_encryption', False)
+
+ def save(self, storage, name):
+ storage.put('seed', self.seed)
+ storage.put('use_encryption', self.use_encryption)
+
+ def has_seed(self):
+ return self.seed != ''
+
+ def can_change_password(self):
+ return not self.is_watching_only()
+
+ def add_seed(self, seed, password):
+ if self.seed:
+ raise Exception("a seed exists")
+ self.seed_version, self.seed = self.format_seed(seed)
+ if password:
+ self.seed = pw_encode(self.seed, password)
+ self.use_encryption = (password is not None)
+
+ def get_seed(self, password):
+ return pw_decode(self.seed, password).encode('utf8')
+
+
+class Xpub:
+
+ def __init__(self):
+ self.xpub = None
+ self.xpub_receive = None
+ self.xpub_change = None
+
+ def add_master_public_key(self, xpub):
+ self.xpub = xpub
+
+ def get_master_public_key(self):
+ return self.xpub
+
+ def derive_pubkey(self, for_change, n):
+ xpub = self.xpub_change if for_change else self.xpub_receive
+ if xpub is None:
+ xpub = bip32_public_derivation(self.xpub, "", "/%d"%for_change)
+ if for_change:
+ self.xpub_change = xpub
+ else:
+ self.xpub_receive = xpub
+ _, _, _, c, cK = deserialize_xkey(xpub)
+ cK, c = CKD_pub(cK, c, n)
+ result = cK.encode('hex')
+ return result
+
+ def get_xpubkey(self, c, i):
+ s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (c, i)))
+ return 'ff' + bitcoin.DecodeBase58Check(self.xpub).encode('hex') + s
+
+
+class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
+ root_derivation = "m/"
+
+ def __init__(self):
+ Xpub.__init__(self)
+ Deterministic_KeyStore.__init__(self)
+ self.xprv = None
+
+ def format_seed(self, seed):
+ return NEW_SEED_VERSION, ' '.join(seed.split())
+
+ def load(self, storage, name):
+ Deterministic_KeyStore.load(self, storage, name)
+ self.xpub = storage.get('master_public_keys', {}).get(name)
+ self.xprv = storage.get('master_private_keys', {}).get(name)
+
+ def save(self, storage, name):
+ Deterministic_KeyStore.save(self, storage, name)
+ d = storage.get('master_public_keys', {})
+ d[name] = self.xpub
+ storage.put('master_public_keys', d)
+ d = storage.get('master_private_keys', {})
+ d[name] = self.xprv
+ storage.put('master_private_keys', d)
+
+ def add_master_private_key(self, xprv, password):
+ self.xprv = pw_encode(xprv, password)
+
+ def get_master_private_key(self, password):
+ return pw_decode(self.xprv, password)
+
+ def check_password(self, password):
+ xprv = pw_decode(self.xprv, password)
+ if deserialize_xkey(xprv)[3] != deserialize_xkey(self.xpub)[3]:
+ raise InvalidPassword()
+
+ def update_password(self, old_password, new_password):
+ if old_password is not None:
+ self.check_password(old_password)
+ if new_password == '':
+ new_password = None
+ if self.has_seed():
+ decoded = self.get_seed(old_password)
+ self.seed = pw_encode( decoded, new_password)
+ if self.xprv is not None:
+ b = pw_decode(self.xprv, old_password)
+ self.xprv = pw_encode(b, new_password)
+ self.use_encryption = (new_password is not None)
+
+ def is_watching_only(self):
+ return self.xprv is None
+
+ def get_keypairs_for_sig(self, tx, password):
+ keypairs = {}
+ for txin in tx.inputs():
+ num_sig = txin.get('num_sig')
+ if num_sig is None:
+ continue
+ x_signatures = txin['signatures']
+ signatures = filter(None, x_signatures)
+ if len(signatures) == num_sig:
+ # input is complete
+ continue
+ for k, x_pubkey in enumerate(txin['x_pubkeys']):
+ if x_signatures[k] is not None:
+ # this pubkey already signed
+ continue
+ derivation = txin['derivation']
+ sec = self.get_private_key(derivation, password)
+ if sec:
+ keypairs[x_pubkey] = sec
+
+ return keypairs
+
+ def sign_transaction(self, tx, password):
+ # Raise if password is not correct.
+ self.check_password(password)
+ # Add private keys
+ keypairs = self.get_keypairs_for_sig(tx, password)
+ # Sign
+ if keypairs:
+ tx.sign(keypairs)
+
+ def derive_xkeys(self, root, derivation, password):
+ x = self.master_private_keys[root]
+ root_xprv = pw_decode(x, password)
+ xprv, xpub = bip32_private_derivation(root_xprv, root, derivation)
+ return xpub, xprv
+
+ def get_mnemonic(self, password):
+ return self.get_seed(password)
+
+ def mnemonic_to_seed(self, seed, password):
+ return Mnemonic.mnemonic_to_seed(seed, password)
+
+ @classmethod
+ def make_seed(self, lang=None):
+ return Mnemonic(lang).make_seed()
+
+ @classmethod
+ def address_derivation(self, account_id, change, address_index):
+ account_derivation = self.account_derivation(account_id)
+ return "%s/%d/%d" % (account_derivation, change, address_index)
+
+ def address_id(self, address):
+ acc_id, (change, address_index) = self.get_address_index(address)
+ return self.address_derivation(acc_id, change, address_index)
+
+ def add_seed_and_xprv(self, seed, password, passphrase=''):
+ xprv, xpub = bip32_root(self.mnemonic_to_seed(seed, passphrase))
+ xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
+ self.add_seed(seed, password)
+ self.add_master_private_key(xprv, password)
+ self.add_master_public_key(xpub)
+
+ def add_xprv(self, xprv, password):
+ xpub = bitcoin.xpub_from_xprv(xprv)
+ self.add_master_private_key(xprv, password)
+ self.add_master_public_key(xpub)
+
+ def can_sign(self, xpub):
+ return xpub == self.xpub and self.xprv is not None
+
+ def get_private_key(self, sequence, password):
+ xprv = self.get_master_private_key(password)
+ _, _, _, c, k = deserialize_xkey(xprv)
+ pk = bip32_private_key(sequence, k, c)
+ return pk
+
+
+class Old_KeyStore(Deterministic_KeyStore):
+
+ def __init__(self):
+ Deterministic_KeyStore.__init__(self)
+ self.mpk = None
+
+ def load(self, storage, name):
+ Deterministic_KeyStore.load(self, storage, name)
+ self.mpk = storage.get('master_public_key').decode('hex')
+
+ def save(self, storage, name):
+ Deterministic_KeyStore.save(self, storage, name)
+ storage.put('wallet_type', 'old')
+ storage.put('master_public_key', self.mpk.encode('hex'))
+
+ def add_seed(self, seed, password):
+ Deterministic_KeyStore.add_seed(self, seed, password)
+ self.mpk = self.mpk_from_seed(self.get_seed(password))
+
+ def add_master_public_key(self, mpk):
+ self.mpk = mpk.decode('hex')
+
+ def format_seed(self, seed):
+ import old_mnemonic
+ # see if seed was entered as hex
+ seed = seed.strip()
+ if seed:
+ try:
+ seed.decode('hex')
+ return OLD_SEED_VERSION, str(seed)
+ except Exception:
+ pass
+ words = seed.split()
+ seed = old_mnemonic.mn_decode(words)
+ if not seed:
+ raise Exception("Invalid seed")
+ return OLD_SEED_VERSION, seed
+
+ def get_mnemonic(self, password):
+ import old_mnemonic
+ s = self.get_seed(password)
+ return ' '.join(old_mnemonic.mn_encode(s))
+
+ @classmethod
+ def mpk_from_seed(klass, seed):
+ secexp = klass.stretch_key(seed)
+ master_private_key = ecdsa.SigningKey.from_secret_exponent(secexp, curve = SECP256k1)
+ master_public_key = master_private_key.get_verifying_key().to_string()
+ return master_public_key
+
+ @classmethod
+ def stretch_key(self, seed):
+ x = seed
+ for i in range(100000):
+ x = hashlib.sha256(x + seed).digest()
+ return string_to_number(x)
+
+ @classmethod
+ def get_sequence(self, mpk, for_change, n):
+ return string_to_number(Hash("%d:%d:"%(n, for_change) + mpk))
+
+ def get_address(self, for_change, n):
+ pubkey = self.get_pubkey(for_change, n)
+ address = public_key_to_bc_address(pubkey.decode('hex'))
+ return address
+
+ @classmethod
+ def get_pubkey_from_mpk(self, mpk, for_change, n):
+ z = self.get_sequence(mpk, for_change, n)
+ master_public_key = ecdsa.VerifyingKey.from_string(mpk, curve = SECP256k1)
+ pubkey_point = master_public_key.pubkey.point + z*SECP256k1.generator
+ public_key2 = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve = SECP256k1)
+ return '04' + public_key2.to_string().encode('hex')
+
+ def derive_pubkey(self, for_change, n):
+ return self.get_pubkey_from_mpk(self.mpk, for_change, n)
+
+ def get_private_key_from_stretched_exponent(self, for_change, n, secexp):
+ order = generator_secp256k1.order()
+ secexp = (secexp + self.get_sequence(self.mpk, for_change, n)) % order
+ pk = number_to_string(secexp, generator_secp256k1.order())
+ compressed = False
+ return SecretToASecret(pk, compressed)
+
+ def get_private_key(self, sequence, password):
+ seed = self.get_seed(password)
+ self.check_seed(seed)
+ for_change, n = sequence
+ secexp = self.stretch_key(seed)
+ pk = self.get_private_key_from_stretched_exponent(for_change, n, secexp)
+ return pk
+
+ def check_seed(self, seed):
+ secexp = self.stretch_key(seed)
+ master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
+ master_public_key = master_private_key.get_verifying_key().to_string()
+ if master_public_key != self.mpk:
+ print_error('invalid password (mpk)', self.mpk.encode('hex'), master_public_key.encode('hex'))
+ raise InvalidPassword()
+
+ def check_password(self, password):
+ seed = self.get_seed(password)
+ self.check_seed(seed)
+
+ def get_master_public_key(self):
+ return self.mpk.encode('hex')
+
+ def get_xpubkeys(self, for_change, n):
+ s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n)))
+ mpk = self.mpk.encode('hex')
+ x_pubkey = 'fe' + mpk + s
+ return [ x_pubkey ]
+
+ @classmethod
+ def parse_xpubkey(self, x_pubkey):
+ assert is_extended_pubkey(x_pubkey)
+ pk = x_pubkey[2:]
+ mpk = pk[0:128]
+ dd = pk[128:]
+ s = []
+ while dd:
+ n = int(bitcoin.rev_hex(dd[0:4]), 16)
+ dd = dd[4:]
+ s.append(n)
+ assert len(s) == 2
+ return mpk, s
+
+ def update_password(self, old_password, new_password):
+ if old_password is not None:
+ self.check_password(old_password)
+ if new_password == '':
+ new_password = None
+ if self.has_seed():
+ decoded = self.get_seed(old_password)
+ self.seed = pw_encode(decoded, new_password)
+ self.use_encryption = (new_password is not None)
+
+
+class Hardware_KeyStore(KeyStore, Xpub):
+ # Derived classes must set:
+ # - device
+ # - DEVICE_IDS
+ # - wallet_type
+
+ #restore_wallet_class = BIP32_RD_Wallet
+ max_change_outputs = 1
+
+ def __init__(self):
+ Xpub.__init__(self)
+ KeyStore.__init__(self)
+ # Errors and other user interaction is done through the wallet's
+ # handler. The handler is per-window and preserved across
+ # device reconnects
+ self.handler = None
+
+ def is_deterministic(self):
+ return True
+
+ def load(self, storage, name):
+ self.xpub = storage.get('master_public_keys', {}).get(name)
+
+ def save(self, storage, name):
+ d = storage.get('master_public_keys', {})
+ d[name] = self.xpub
+ storage.put('master_public_keys', d)
+
+ def unpaired(self):
+ '''A device paired with the wallet was diconnected. This can be
+ called in any thread context.'''
+ self.print_error("unpaired")
+
+ def paired(self):
+ '''A device paired with the wallet was (re-)connected. This can be
+ called in any thread context.'''
+ self.print_error("paired")
+
+ def can_export(self):
+ return False
+
+ def is_watching_only(self):
+ '''The wallet is not watching-only; the user will be prompted for
+ pin and passphrase as appropriate when needed.'''
+ assert not self.has_seed()
+ return False
+
+ def can_change_password(self):
+ return False
+
+ def derive_xkeys(self, root, derivation, password):
+ if self.master_public_keys.get(self.root_name):
+ return BIP44_wallet.derive_xkeys(self, root, derivation, password)
+ # When creating a wallet we need to ask the device for the
+ # master public key
+ xpub = self.get_public_key(derivation)
+ return xpub, None
+
+
+class BIP44_KeyStore(BIP32_KeyStore):
+ root_derivation = "m/44'/0'/0'"
+
+ def normalize_passphrase(self, passphrase):
+ return normalize('NFKD', unicode(passphrase or ''))
+
+ def is_valid_seed(self, seed):
+ return True
+
+ def mnemonic_to_seed(self, mnemonic, passphrase):
+ # See BIP39
+ import pbkdf2, hashlib, hmac
+ PBKDF2_ROUNDS = 2048
+ mnemonic = normalize('NFKD', ' '.join(mnemonic.split()))
+ passphrase = self.normalize_passphrase(passphrase)
+ return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase,
+ iterations = PBKDF2_ROUNDS, macmodule = hmac,
+ digestmodule = hashlib.sha512).read(64)
+
+ def on_restore_wallet(self, wizard):
+ #assert isinstance(keystore, self.keystore_class)
+ #msg = _("Enter the seed for your %s wallet:" % self.device)
+ #title=_('Restore hardware wallet'),
+ f = lambda seed: wizard.run('on_restore_seed', seed)
+ wizard.restore_seed_dialog(run_next=f, is_valid=self.is_valid_seed)
+
+ def on_restore_seed(self, wizard, seed):
+ f = lambda passphrase: wizard.run('on_restore_passphrase', seed, passphrase)
+ self.device = ''
+ wizard.request_passphrase(self.device, run_next=f)
+
+ def on_restore_passphrase(self, wizard, seed, passphrase):
+ f = lambda pw: wizard.run('on_restore_password', seed, passphrase, pw)
+ wizard.request_password(run_next=f)
+
+ def on_restore_password(self, wizard, seed, passphrase, password):
+ self.add_seed_and_xprv(seed, password, passphrase)
+ self.save(wizard.storage, 'x/')
+
+
+
+keystores = []
+
+def load_keystore(storage, name):
+ w = storage.get('wallet_type')
+ t = storage.get('key_type', 'seed')
+ seed_version = storage.get_seed_version()
+ if seed_version == OLD_SEED_VERSION or w == 'old':
+ k = Old_KeyStore()
+ elif t == 'imported':
+ k = Imported_KeyStore()
+ elif name and name not in [ 'x/', 'x1/' ]:
+ k = BIP32_KeyStore()
+ elif t == 'seed':
+ k = BIP32_KeyStore()
+ elif t == 'hardware':
+ hw_type = storage.get('hardware_type')
+ for cat, _type, constructor in keystores:
+ if cat == 'hardware' and _type == hw_type:
+ k = constructor()
+ break
+ else:
+ raise BaseException('unknown hardware type')
+ elif t == 'hw_seed':
+ k = BIP44_KeyStore()
+ else:
+ raise BaseException('unknown wallet type', t)
+ k.load(storage, name)
+ return k
+
+
+def register_keystore(category, type, constructor):
+ keystores.append((category, type, constructor))
+
+
+def is_old_mpk(mpk):
+ try:
+ int(mpk, 16)
+ except:
+ return False
+ return len(mpk) == 128
+
+def is_xpub(text):
+ if text[0:4] != 'xpub':
+ return False
+ try:
+ deserialize_xkey(text)
+ return True
+ except:
+ return False
+
+def is_xprv(text):
+ if text[0:4] != 'xprv':
+ return False
+ try:
+ deserialize_xkey(text)
+ return True
+ except:
+ return False
+
+def is_address_list(text):
+ parts = text.split()
+ return bool(parts) and all(bitcoin.is_address(x) for x in parts)
+
+def is_private_key_list(text):
+ parts = text.split()
+ return bool(parts) and all(bitcoin.is_private_key(x) for x in parts)
+
+is_seed = lambda x: is_old_seed(x) or is_new_seed(x)
+is_mpk = lambda x: is_old_mpk(x) or is_xpub(x)
+is_private = lambda x: is_seed(x) or is_xprv(x) or is_private_key_list(x)
+is_any_key = lambda x: is_old_mpk(x) or is_xprv(x) or is_xpub(x) or is_address_list(x) or is_private_key_list(x)
+is_private_key = lambda x: is_xprv(x) or is_private_key_list(x)
+is_bip32_key = lambda x: is_xprv(x) or is_xpub(x)
+
+
+def from_seed(seed, password):
+ if is_old_seed(seed):
+ keystore = Old_KeyStore()
+ keystore.add_seed(seed, password)
+ elif is_new_seed(seed):
+ keystore = BIP32_KeyStore()
+ keystore.add_seed_and_xprv(seed, password)
+ return keystore
+
+def from_private_key_list(text, password):
+ keystore = Imported_KeyStore()
+ for x in text.split():
+ keystore.import_key(x, None)
+ keystore.update_password(None, password)
+ return keystore
+
+def from_old_mpk(mpk):
+ keystore = Old_KeyStore()
+ keystore.add_master_public_key(mpk)
+ return keystore
+
+def from_xpub(xpub):
+ keystore = BIP32_KeyStore()
+ keystore.add_master_public_key(xpub)
+ return keystore
+
+def from_xprv(xprv, password):
+ xpub = bitcoin.xpub_from_xprv(xprv)
+ keystore = BIP32_KeyStore()
+ keystore.add_master_private_key(xprv, password)
+ keystore.add_master_public_key(xpub)
+ return keystore
+
+def xprv_from_seed(seed, password):
+ # do not store the seed, only the master xprv
+ xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed, ''))
+ #xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
+ return from_xprv(xprv, password)
+
+def xpub_from_seed(seed):
+ # store only master xpub
+ xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed,''))
+ #xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
+ return from_xpub(xpub)
+
+def from_text(text, password):
+ if is_xprv(text):
+ k = from_xprv(text, password)
+ elif is_old_mpk(text):
+ k = from_old_mpk(text)
+ elif is_xpub(text):
+ k = from_xpub(text)
+ elif is_private_key_list(text):
+ k = from_private_key_list(text, password)
+ elif is_seed(text):
+ k = from_seed(text, password)
+ else:
+ raise BaseException('Invalid seedphrase or key')
+ return k
diff --git a/lib/plugins.py b/lib/plugins.py
@@ -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
-
- def dynamic_constructor(storage):
- return self.wallet_plugin_loader(name).wallet_class(storage)
-
+ 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 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,
diff --git a/lib/storage.py b/lib/storage.py
@@ -0,0 +1,253 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2015 Thomas Voegtlin
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation files
+# (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge,
+# publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import os
+import ast
+import threading
+import random
+import time
+import json
+import copy
+import re
+import stat
+
+from i18n import _
+from util import NotEnoughFunds, PrintError, profiler
+from plugins import run_hook, plugin_loaders
+
+class WalletStorage(PrintError):
+
+ def __init__(self, path):
+ self.lock = threading.RLock()
+ self.data = {}
+ self.path = path
+ self.file_exists = False
+ self.modified = False
+ self.print_error("wallet path", self.path)
+ if self.path:
+ self.read(self.path)
+
+ # check here if I need to load a plugin
+ t = self.get('wallet_type')
+ l = plugin_loaders.get(t)
+ if l: l()
+
+
+ def read(self, path):
+ """Read the contents of the wallet file."""
+ try:
+ with open(self.path, "r") as f:
+ data = f.read()
+ except IOError:
+ return
+ if not data:
+ return
+ try:
+ self.data = json.loads(data)
+ except:
+ try:
+ d = ast.literal_eval(data) #parse raw data from reading wallet file
+ labels = d.get('labels', {})
+ except Exception as e:
+ raise IOError("Cannot read wallet file '%s'" % self.path)
+ self.data = {}
+ # In old versions of Electrum labels were latin1 encoded, this fixes breakage.
+ for i, label in labels.items():
+ try:
+ unicode(label)
+ except UnicodeDecodeError:
+ d['labels'][i] = unicode(label.decode('latin1'))
+ for key, value in d.items():
+ try:
+ json.dumps(key)
+ json.dumps(value)
+ except:
+ self.print_error('Failed to convert label to json format', key)
+ continue
+ self.data[key] = value
+ self.file_exists = True
+
+ def get(self, key, default=None):
+ with self.lock:
+ v = self.data.get(key)
+ if v is None:
+ v = default
+ else:
+ v = copy.deepcopy(v)
+ return v
+
+ def put(self, key, value):
+ try:
+ json.dumps(key)
+ json.dumps(value)
+ except:
+ self.print_error("json error: cannot save", key)
+ return
+ with self.lock:
+ if value is not None:
+ if self.data.get(key) != value:
+ self.modified = True
+ self.data[key] = copy.deepcopy(value)
+ elif key in self.data:
+ self.modified = True
+ self.data.pop(key)
+
+ def write(self):
+ with self.lock:
+ self._write()
+ self.file_exists = True
+
+ def _write(self):
+ if threading.currentThread().isDaemon():
+ self.print_error('warning: daemon thread cannot write wallet')
+ return
+ if not self.modified:
+ return
+ s = json.dumps(self.data, indent=4, sort_keys=True)
+ temp_path = "%s.tmp.%s" % (self.path, os.getpid())
+ with open(temp_path, "w") as f:
+ f.write(s)
+ f.flush()
+ os.fsync(f.fileno())
+
+ mode = os.stat(self.path).st_mode if os.path.exists(self.path) else stat.S_IREAD | stat.S_IWRITE
+ # perform atomic write on POSIX systems
+ try:
+ os.rename(temp_path, self.path)
+ except:
+ os.remove(self.path)
+ os.rename(temp_path, self.path)
+ os.chmod(self.path, mode)
+ self.print_error("saved", self.path)
+ self.modified = False
+
+ def requires_split(self):
+ d = self.get('accounts', {})
+ return len(d) > 1
+
+ def split_accounts(storage):
+ result = []
+ # backward compatibility with old wallets
+ d = storage.get('accounts', {})
+ if len(d) < 2:
+ return
+ wallet_type = storage.get('wallet_type')
+ if wallet_type == 'old':
+ assert len(d) == 2
+ storage1 = WalletStorage(storage.path + '.deterministic')
+ storage1.data = copy.deepcopy(storage.data)
+ storage1.put('accounts', {'0': d['0']})
+ storage1.write()
+ storage2 = WalletStorage(storage.path + '.imported')
+ storage2.data = copy.deepcopy(storage.data)
+ storage2.put('accounts', {'/x': d['/x']})
+ storage2.put('seed', None)
+ storage2.put('seed_version', None)
+ storage2.put('master_public_key', None)
+ storage2.put('wallet_type', 'imported')
+ storage2.write()
+ storage2.upgrade()
+ result = [storage1.path, storage2.path]
+ elif wallet_type in ['bip44', 'trezor']:
+ mpk = storage.get('master_public_keys')
+ for k in d.keys():
+ i = int(k)
+ x = d[k]
+ if x.get("pending"):
+ continue
+ xpub = mpk["x/%d'"%i]
+ new_path = storage.path + '.' + k
+ storage2 = WalletStorage(new_path)
+ storage2.data = copy.deepcopy(storage.data)
+ storage2.put('wallet_type', 'standard')
+ if wallet_type in ['trezor', 'keepkey']:
+ storage2.put('key_type', 'hardware')
+ storage2.put('hardware_type', wallet_type)
+ storage2.put('accounts', {'0': x})
+ # need to save derivation and xpub too
+ storage2.put('master_public_keys', {'x/': xpub})
+ storage2.put('account_id', k)
+ storage2.write()
+ result.append(new_path)
+ else:
+ raise BaseException("This wallet has multiple accounts and must be split")
+ return result
+
+ def requires_upgrade(storage):
+ # '/x' is the internal ID for imported accounts
+ return bool(storage.get('accounts', {}).get('/x', {}).get('imported',{}))
+
+ def upgrade(storage):
+ d = storage.get('accounts', {}).get('/x', {}).get('imported',{})
+ addresses = []
+ keypairs = {}
+ for addr, v in d.items():
+ pubkey, privkey = v
+ if privkey:
+ keypairs[pubkey] = privkey
+ else:
+ addresses.append(addr)
+ if addresses and keypairs:
+ raise BaseException('mixed addresses and privkeys')
+ elif addresses:
+ storage.put('addresses', addresses)
+ storage.put('accounts', None)
+ elif keypairs:
+ storage.put('wallet_type', 'standard')
+ storage.put('key_type', 'imported')
+ storage.put('keypairs', keypairs)
+ storage.put('accounts', None)
+ else:
+ raise BaseException('no addresses or privkeys')
+ storage.write()
+
+ def get_action(self):
+ action = run_hook('get_action', self)
+ if action:
+ return action
+ if not self.file_exists:
+ return 'new'
+
+ def get_seed_version(self):
+ from version import OLD_SEED_VERSION, NEW_SEED_VERSION
+ seed_version = self.get('seed_version')
+ if not seed_version:
+ seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION
+ if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]:
+ msg = "Your wallet has an unsupported seed version."
+ msg += '\n\nWallet file: %s' % os.path.abspath(self.path)
+ if seed_version in [5, 7, 8, 9, 10]:
+ msg += "\n\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version
+ if seed_version == 6:
+ # version 1.9.8 created v6 wallets when an incorrect seed was entered in the restore dialog
+ msg += '\n\nThis file was created because of a bug in version 1.9.8.'
+ if self.get('master_public_keys') is None and self.get('master_private_keys') is None and self.get('imported_keys') is None:
+ # pbkdf2 was not included with the binaries, and wallet creation aborted.
+ msg += "\nIt does not contain any keys, and can safely be removed."
+ else:
+ # creation was complete if electrum was run from source
+ msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet."
+ raise BaseException(msg)
+ return seed_version
diff --git a/lib/synchronizer.py b/lib/synchronizer.py
@@ -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.'''
diff --git a/lib/transaction.py b/lib/transaction.py
@@ -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()):
diff --git a/lib/wallet.py b/lib/wallet.py
@@ -23,6 +23,14 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
+"""
+Wallet classes:
+ - Imported_Wallet: imported address, no keystore
+ - Standard_Wallet: one keystore, P2PKH
+ - Multisig_Wallet: several keystores, P2SH
+
+"""
+
import os
import hashlib
import ast
@@ -34,15 +42,14 @@ import copy
import re
import stat
from functools import partial
-from unicodedata import normalize
from collections import namedtuple, defaultdict
from i18n import _
from util import NotEnoughFunds, PrintError, profiler
from bitcoin import *
-from account import *
from version import *
+from keystore import load_keystore
from transaction import Transaction
from plugins import run_hook
@@ -54,9 +61,7 @@ from mnemonic import Mnemonic
import paymentrequest
-# internal ID for imported account
-IMPORTED_ACCOUNT = '/x'
-
+from storage import WalletStorage
TX_STATUS = [
_('Replaceable'),
@@ -67,104 +72,6 @@ TX_STATUS = [
]
-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)
-
- 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()
-
- 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
-
class Abstract_Wallet(PrintError):
"""
@@ -184,20 +91,15 @@ class Abstract_Wallet(PrintError):
self.gap_limit_for_change = 6 # constant
# saved fields
- self.seed_version = storage.get('seed_version', NEW_SEED_VERSION)
self.use_change = storage.get('use_change', True)
self.multiple_change = storage.get('multiple_change', False)
- self.use_encryption = storage.get('use_encryption', False)
- self.seed = storage.get('seed', '') # encrypted
self.labels = storage.get('labels', {})
self.frozen_addresses = set(storage.get('frozen_addresses',[]))
self.stored_height = storage.get('stored_height', 0) # last known height (for offline mode)
self.history = storage.get('addr_history',{}) # address -> list(txid, height)
- # imported_keys is deprecated. The GUI should call convert_imported_keys
- self.imported_keys = self.storage.get('imported_keys',{})
-
- self.load_accounts()
+ self.load_keystore()
+ self.load_addresses()
self.load_transactions()
self.build_reverse_history()
@@ -209,7 +111,7 @@ class Abstract_Wallet(PrintError):
self.unverified_tx = defaultdict(int)
# Verified transactions. Each value is a (height, timestamp, block_pos) tuple. Access with self.lock.
- self.verified_tx = storage.get('verified_tx3',{})
+ self.verified_tx = storage.get('verified_tx3', {})
# there is a difference between wallet.up_to_date and interface.is_up_to_date()
# interface.is_up_to_date() returns true when all requests have been answered and processed
@@ -230,12 +132,8 @@ class Abstract_Wallet(PrintError):
def __str__(self):
return self.basename()
- def set_use_encryption(self, use_encryption):
- self.use_encryption = use_encryption
- self.storage.put('use_encryption', use_encryption)
-
def get_master_public_key(self):
- pass
+ raise NotImplementedError
@profiler
def load_transactions(self):
@@ -306,58 +204,23 @@ class Abstract_Wallet(PrintError):
if save:
self.save_transactions()
- # wizard action
- def get_action(self):
- pass
-
def basename(self):
return os.path.basename(self.storage.path)
- def convert_imported_keys(self, password):
- for k, v in self.imported_keys.items():
- sec = pw_decode(v, password)
- pubkey = public_key_from_private_key(sec)
- address = public_key_to_bc_address(pubkey.decode('hex'))
- if address != k:
- raise InvalidPassword()
- self.import_key(sec, password)
- self.imported_keys.pop(k)
- self.storage.put('imported_keys', self.imported_keys)
-
- def load_accounts(self):
- self.accounts = {}
- d = self.storage.get('accounts', {})
- removed = False
- for k, v in d.items():
- if self.wallet_type == 'old' and k in [0, '0']:
- v['mpk'] = self.storage.get('master_public_key')
- self.accounts['0'] = OldAccount(v)
- elif v.get('imported'):
- self.accounts[k] = ImportedAccount(v)
- elif v.get('xpub'):
- self.accounts[k] = BIP32_Account(v)
- elif v.get('pending'):
- removed = True
- else:
- self.print_error("cannot load account", v)
- if removed:
- self.save_accounts()
+ def save_pubkeys(self):
+ # this name is inherited from old multi-account wallets
+ self.storage.put('accounts', {'0': {'receiving':self.receiving_pubkeys, 'change':self.change_pubkeys}})
- def create_main_account(self):
- pass
+ def load_addresses(self):
+ d = self.storage.get('accounts', {}).get('0', {})
+ self.receiving_pubkeys = d.get('receiving', [])
+ self.change_pubkeys = d.get('change', [])
+ self.receiving_addresses = map(self.pubkeys_to_address, self.receiving_pubkeys)
+ self.change_addresses = map(self.pubkeys_to_address, self.change_pubkeys)
def synchronize(self):
pass
- def can_create_accounts(self):
- return False
-
- def needs_next_account(self):
- return self.can_create_accounts() and self.accounts_all_used()
-
- def permit_account_naming(self):
- return self.can_create_accounts()
-
def set_up_to_date(self, up_to_date):
with self.lock:
self.up_to_date = up_to_date
@@ -367,49 +230,6 @@ class Abstract_Wallet(PrintError):
def is_up_to_date(self):
with self.lock: return self.up_to_date
- def is_imported(self, addr):
- account = self.accounts.get(IMPORTED_ACCOUNT)
- if account:
- return addr in account.get_addresses(0)
- else:
- return False
-
- def has_imported_keys(self):
- account = self.accounts.get(IMPORTED_ACCOUNT)
- return account is not None
-
- 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)
- address = public_key_to_bc_address(pubkey.decode('hex'))
- except Exception:
- raise Exception('Invalid private key')
-
- if self.is_mine(address):
- raise Exception('Address already in wallet')
-
- if self.accounts.get(IMPORTED_ACCOUNT) is None:
- self.accounts[IMPORTED_ACCOUNT] = ImportedAccount({'imported':{}})
- self.accounts[IMPORTED_ACCOUNT].add(address, pubkey, sec, password)
- self.save_accounts()
-
- # force resynchronization, because we need to re-run add_transaction
- if address in self.history:
- self.history.pop(address)
-
- if self.synchronizer:
- self.synchronizer.add(address)
- return address
-
- def delete_imported_key(self, addr):
- account = self.accounts[IMPORTED_ACCOUNT]
- account.remove(addr)
- if not account.get_addresses(0):
- self.accounts.pop(IMPORTED_ACCOUNT)
- self.save_accounts()
-
def set_label(self, name, text = None):
changed = False
old_text = self.labels.get(name)
@@ -428,35 +248,33 @@ class Abstract_Wallet(PrintError):
return changed
- def addresses(self, include_change = True):
- return list(addr for acc in self.accounts for addr in self.get_account_addresses(acc, include_change))
-
def is_mine(self, address):
- return address in self.addresses(True)
+ return address in self.get_addresses()
def is_change(self, address):
- if not self.is_mine(address): return False
- acct, s = self.get_address_index(address)
- if s is None: return False
+ if not self.is_mine(address):
+ return False
+ s = self.get_address_index(address)
+ if s is None:
+ return False
return s[0] == 1
def get_address_index(self, address):
- for acc_id in self.accounts:
- for for_change in [0,1]:
- addresses = self.accounts[acc_id].get_addresses(for_change)
- if address in addresses:
- return acc_id, (for_change, addresses.index(address))
+ if address in self.receiving_addresses:
+ return False, self.receiving_addresses.index(address)
+ if address in self.change_addresses:
+ return True, self.change_addresses.index(address)
raise Exception("Address not found", address)
def get_private_key(self, address, password):
if self.is_watching_only():
return []
- account_id, sequence = self.get_address_index(address)
- return self.accounts[account_id].get_private_key(sequence, self, password)
+ sequence = self.get_address_index(address)
+ return [ self.keystore.get_private_key(sequence, password) ]
def get_public_keys(self, address):
- account_id, sequence = self.get_address_index(address)
- return self.accounts[account_id].get_pubkeys(*sequence)
+ sequence = self.get_address_index(address)
+ return self.get_pubkeys(*sequence)
def sign_message(self, address, message, password):
keys = self.get_private_key(address, password)
@@ -556,7 +374,7 @@ class Abstract_Wallet(PrintError):
def get_wallet_delta(self, tx):
""" effect of tx on wallet """
- addresses = self.addresses(True)
+ addresses = self.get_addresses()
is_relevant = False
is_mine = False
is_pruned = False
@@ -711,11 +529,10 @@ class Abstract_Wallet(PrintError):
u -= v
return c, u, x
-
def get_spendable_coins(self, domain = None, exclude_frozen = True):
coins = []
if domain is None:
- domain = self.addresses(True)
+ domain = self.get_addresses()
if exclude_frozen:
domain = set(domain) - self.frozen_addresses
for addr in domain:
@@ -728,7 +545,7 @@ class Abstract_Wallet(PrintError):
return coins
def dummy_address(self):
- return self.addresses(False)[0]
+ return self.get_receiving_addresses()[0]
def get_max_amount(self, config, inputs, recipient, fee):
sendable = sum(map(lambda x:x['value'], inputs))
@@ -742,34 +559,18 @@ class Abstract_Wallet(PrintError):
amount = max(0, sendable - fee)
return amount, fee
- def get_account_addresses(self, acc_id, include_change=True):
- '''acc_id of None means all user-visible accounts'''
- addr_list = []
- acc_ids = self.accounts_to_show() if acc_id is None else [acc_id]
- for acc_id in acc_ids:
- if acc_id in self.accounts:
- acc = self.accounts[acc_id]
- addr_list += acc.get_addresses(0)
- if include_change:
- addr_list += acc.get_addresses(1)
- return addr_list
-
- def get_account_from_address(self, addr):
- "Returns the account that contains this address, or None"
- for acc_id in self.accounts: # similar to get_address_index but simpler
- if addr in self.get_account_addresses(acc_id):
- return acc_id
- return None
-
- def get_account_balance(self, account):
- return self.get_balance(self.get_account_addresses(account))
+ def get_addresses(self):
+ out = []
+ out += self.get_receiving_addresses()
+ out += self.get_change_addresses()
+ return out
def get_frozen_balance(self):
return self.get_balance(self.frozen_addresses)
def get_balance(self, domain=None):
if domain is None:
- domain = self.addresses(True)
+ domain = self.get_addresses()
cc = uu = xx = 0
for addr in domain:
c, u, x = self.get_addr_balance(addr)
@@ -904,8 +705,7 @@ class Abstract_Wallet(PrintError):
def get_history(self, domain=None):
# get domain
if domain is None:
- domain = self.get_account_addresses(None)
-
+ domain = self.get_addresses()
# 1. Get the history of each address in the domain, maintain the
# delta of a tx as the sum of its deltas on domain addresses
tx_deltas = defaultdict(int)
@@ -1027,14 +827,11 @@ class Abstract_Wallet(PrintError):
if change_addr:
change_addrs = [change_addr]
else:
- # send change to one of the accounts involved in the tx
- address = coins[0].get('address')
- account, _ = self.get_address_index(address)
- if self.use_change and self.accounts[account].has_change():
+ addrs = self.get_change_addresses()[-self.gap_limit_for_change:]
+ if self.use_change and addrs:
# New change addresses are created only after a few
# confirmations. Select the unused addresses within the
# gap limit; if none take one at random
- addrs = self.accounts[account].get_addresses(1)[-self.gap_limit_for_change:]
change_addrs = [addr for addr in addrs if
self.get_num_tx(addr) == 0]
if not change_addrs:
@@ -1073,71 +870,6 @@ class Abstract_Wallet(PrintError):
self.sign_transaction(tx, password)
return tx
- def add_input_info(self, txin):
- address = txin['address']
- account_id, sequence = self.get_address_index(address)
- account = self.accounts[account_id]
- redeemScript = account.redeem_script(*sequence)
- pubkeys = account.get_pubkeys(*sequence)
- x_pubkeys = account.get_xpubkeys(*sequence)
- # sort pubkeys and x_pubkeys, using the order of pubkeys
- pubkeys, x_pubkeys = zip( *sorted(zip(pubkeys, x_pubkeys)))
- txin['pubkeys'] = list(pubkeys)
- txin['x_pubkeys'] = list(x_pubkeys)
- txin['signatures'] = [None] * len(pubkeys)
- if redeemScript:
- txin['redeemScript'] = redeemScript
- txin['num_sig'] = account.m
- else:
- txin['redeemPubkey'] = account.get_pubkey(*sequence)
- txin['num_sig'] = 1
-
- def sign_transaction(self, tx, password):
- if self.is_watching_only():
- return
- # Raise if password is not correct.
- self.check_password(password)
- # Add derivation for utxo in wallets
- for i, addr in self.utxo_can_sign(tx):
- txin = tx.inputs()[i]
- txin['address'] = addr
- self.add_input_info(txin)
- # Add private keys
- keypairs = {}
- for x in self.xkeys_can_sign(tx):
- sec = self.get_private_key_from_xpubkey(x, password)
- if sec:
- keypairs[x] = sec
- # Sign
- if keypairs:
- tx.sign(keypairs)
-
- 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.storage.put('seed', self.seed)
-
- imported_account = self.accounts.get(IMPORTED_ACCOUNT)
- if imported_account:
- imported_account.update_password(old_password, new_password)
- self.save_accounts()
-
- if hasattr(self, 'master_private_keys'):
- for k, v in self.master_private_keys.items():
- b = pw_decode(v, old_password)
- c = pw_encode(b, new_password)
- self.master_private_keys[k] = c
- self.storage.put('master_private_keys', self.master_private_keys)
-
- self.set_use_encryption(new_password is not None)
-
def is_frozen(self, addr):
return addr in self.frozen_addresses
@@ -1214,33 +946,6 @@ class Abstract_Wallet(PrintError):
else:
self.synchronize()
- def accounts_to_show(self):
- return self.accounts.keys()
-
- def get_accounts(self):
- return {a_id: a for a_id, a in self.accounts.items()
- if a_id in self.accounts_to_show()}
-
- def get_account_name(self, k):
- return self.labels.get(k, self.accounts[k].get_name(k))
-
- def get_account_names(self):
- ids = self.accounts_to_show()
- return dict(zip(ids, map(self.get_account_name, ids)))
-
- def add_account(self, account_id, account):
- self.accounts[account_id] = account
- self.save_accounts()
-
- def save_accounts(self):
- d = {}
- for k, v in self.accounts.items():
- d[k] = v.dump()
- self.storage.put('accounts', d)
-
- def can_import(self):
- return not self.is_watching_only()
-
def can_export(self):
return not self.is_watching_only()
@@ -1282,96 +987,71 @@ class Abstract_Wallet(PrintError):
new_tx = Transaction.from_io(inputs, outputs)
return new_tx
- def can_sign(self, tx):
- if self.is_watching_only():
- return False
- if tx.is_complete():
- return False
- if self.xkeys_can_sign(tx):
- return True
- if self.utxo_can_sign(tx):
- return True
- return False
-
- def utxo_can_sign(self, tx):
- out = set()
+ def add_input_info(self, txin):
+ # Add address for utxo that are in wallet
coins = self.get_spendable_coins()
- for i in tx.inputs_without_script():
- txin = tx.inputs()[i]
+ if txin.get('scriptSig') == '':
for item in coins:
if txin.get('prevout_hash') == item.get('prevout_hash') and txin.get('prevout_n') == item.get('prevout_n'):
- out.add((i, item.get('address')))
- return out
-
- def xkeys_can_sign(self, tx):
- out = set()
- for x in tx.inputs_to_sign():
- if self.can_sign_xpubkey(x):
- out.add(x)
- return out
-
- def get_private_key_from_xpubkey(self, x_pubkey, password):
- if x_pubkey[0:2] in ['02','03','04']:
- addr = bitcoin.public_key_to_bc_address(x_pubkey.decode('hex'))
- if self.is_mine(addr):
- return self.get_private_key(addr, password)[0]
- elif x_pubkey[0:2] == 'ff':
- xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey)
- for k, v in self.master_public_keys.items():
- if v == xpub:
- xprv = self.get_master_private_key(k, password)
- if xprv:
- _, _, _, c, k = deserialize_xkey(xprv)
- return bip32_private_key(sequence, k, c)
- elif x_pubkey[0:2] == 'fe':
- xpub, sequence = OldAccount.parse_xpubkey(x_pubkey)
- for k, account in self.accounts.items():
- if xpub in account.get_master_pubkeys():
- pk = account.get_private_key(sequence, self, password)
- return pk[0]
- elif x_pubkey[0:2] == 'fd':
- addrtype = ord(x_pubkey[2:4].decode('hex'))
- addr = hash_160_to_bc_address(x_pubkey[4:].decode('hex'), addrtype)
- if self.is_mine(addr):
- return self.get_private_key(addr, password)[0]
- else:
- raise BaseException("z")
-
-
- def can_sign_xpubkey(self, x_pubkey):
- if x_pubkey[0:2] in ['02','03','04']:
- addr = bitcoin.public_key_to_bc_address(x_pubkey.decode('hex'))
- return self.is_mine(addr)
- elif x_pubkey[0:2] == 'ff':
- if not isinstance(self, BIP32_Wallet): return False
- xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey)
- return xpub in [ self.master_public_keys[k] for k in self.master_private_keys.keys() ]
- elif x_pubkey[0:2] == 'fe':
- if not isinstance(self, OldWallet): return False
- xpub, sequence = OldAccount.parse_xpubkey(x_pubkey)
- return xpub == self.get_master_public_key()
- elif x_pubkey[0:2] == 'fd':
- addrtype = ord(x_pubkey[2:4].decode('hex'))
- addr = hash_160_to_bc_address(x_pubkey[4:].decode('hex'), addrtype)
- return self.is_mine(addr)
+ txin['address'] = item.get('address')
+ address = txin['address']
+ if self.is_mine(address):
+ self.add_input_sig_info(txin, address)
else:
- raise BaseException("z")
+ txin['can_sign'] = False
+ def can_sign(self, tx):
+ if self.is_watching_only():
+ return False
+ if tx.is_complete():
+ return False
+ # add input info. (should be done already)
+ for txin in tx.inputs():
+ self.add_input_info(txin)
+ can_sign = any([txin['can_sign'] for txin in tx.inputs()])
+ return can_sign
+
+ 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 is_watching_only(self):
- False
-
- def can_change_password(self):
- return not self.is_watching_only()
+ def sign_transaction(self, tx, password):
+ if self.is_watching_only():
+ return
- def get_unused_addresses(self, account):
+ # add previous tx for hw wallets
+ for txin in tx.inputs():
+ tx_hash = txin['prevout_hash']
+ txin['prev_tx'] = self.get_input_tx(tx_hash)
+ # I should add the address index if it's an address of mine
+
+ # add output info for hw wallets
+ tx.output_info = []
+ for i, txout in enumerate(tx.outputs()):
+ _type, addr, amount = txout
+ change, address_index = self.get_address_index(addr) if self.is_change(addr) else None, None
+ tx.output_info.append((change, address_index))
+
+ # sign
+ for keystore in self.get_keystores():
+ if not keystore.is_watching_only():
+ keystore.sign_transaction(tx, password)
+
+ def get_unused_addresses(self):
# fixme: use slots from expired requests
- domain = self.get_account_addresses(account, include_change=False)
+ domain = self.get_receiving_addresses()
return [addr for addr in domain if not self.history.get(addr)
and addr not in self.receive_requests.keys()]
- def get_unused_address(self, account):
- addrs = self.get_unused_addresses(account)
+ def get_unused_address(self):
+ addrs = self.get_unused_addresses()
if addrs:
return addrs[0]
@@ -1489,19 +1169,32 @@ class Abstract_Wallet(PrintError):
raise NotImplementedError()
+
class Imported_Wallet(Abstract_Wallet):
+ # wallet made of imported addresses
+
wallet_type = 'imported'
def __init__(self, storage):
Abstract_Wallet.__init__(self, storage)
- a = self.accounts.get(IMPORTED_ACCOUNT)
- if not a:
- self.accounts[IMPORTED_ACCOUNT] = ImportedAccount({'imported':{}})
+
+ def load_keystore(self):
+ pass
+
+ def load_addresses(self):
+ self.addresses = self.storage.get('addresses', [])
+
+ def has_password(self):
+ return False
+
+ def can_change_password(self):
+ return False
+
+ def can_import(self):
+ return True
def is_watching_only(self):
- acc = self.accounts[IMPORTED_ACCOUNT]
- n = acc.keypairs.values()
- return len(n) > 0 and n == [[None, None]] * len(n)
+ return True
def has_seed(self):
return False
@@ -1509,51 +1202,93 @@ class Imported_Wallet(Abstract_Wallet):
def is_deterministic(self):
return False
- def check_password(self, password):
- self.accounts[IMPORTED_ACCOUNT].get_private_key((0,0), self, password)
-
def is_used(self, address):
return False
def get_master_public_keys(self):
return {}
- def is_beyond_limit(self, address, account, is_change):
+ def is_beyond_limit(self, address, is_change):
return False
def get_fingerprint(self):
return ''
+ def get_addresses(self, include_change=False):
+ return self.addresses
+
+ def add_address(self, address):
+ if address in self.addresses:
+ return
+ self.addresses.append(address)
+ self.storage.put('addresses', self.addresses)
+ self.storage.write()
+
+ # force resynchronization, because we need to re-run add_transaction
+ if address in self.history:
+ self.history.pop(address)
+ if self.synchronizer:
+ self.synchronizer.add(address)
+ return address
+
+ def get_receiving_addresses(self):
+ return self.addresses[:]
+
+ def get_change_addresses(self):
+ return []
+
+
+
+class P2PK_Wallet(Abstract_Wallet):
+
+ def pubkeys_to_address(self, pubkey):
+ return public_key_to_bc_address(pubkey.decode('hex'))
+
+ def load_keystore(self):
+ self.keystore = load_keystore(self.storage, self.root_name)
+
+ def get_pubkey(self, c, i):
+ pubkey_list = self.change_pubkeys if c else self.receiving_pubkeys
+ return pubkey_list[i]
+
+ def add_input_sig_info(self, txin, address):
+ txin['derivation'] = derivation = self.get_address_index(address)
+ x_pubkey = self.keystore.get_xpubkey(*derivation)
+ pubkey = self.get_pubkey(*derivation)
+ txin['x_pubkeys'] = [x_pubkey]
+ txin['pubkeys'] = [pubkey]
+ txin['signatures'] = [None]
+ txin['redeemPubkey'] = pubkey
+ txin['num_sig'] = 1
+ txin['can_sign'] = any([x is None for x in txin['signatures']])
+
+
class Deterministic_Wallet(Abstract_Wallet):
def __init__(self, storage):
Abstract_Wallet.__init__(self, storage)
+ self.gap_limit = storage.get('gap_limit', 20)
def has_seed(self):
- return self.seed != ''
+ return self.keystore.has_seed()
def is_deterministic(self):
- return True
-
- def is_watching_only(self):
- return not self.has_seed()
+ return self.keystore.is_deterministic()
- def add_seed(self, seed, password):
- if self.seed:
- raise Exception("a seed exists")
+ def get_receiving_addresses(self):
+ return self.receiving_addresses
- self.seed_version, self.seed = self.format_seed(seed)
- if password:
- self.seed = pw_encode(self.seed, password)
- self.storage.put('seed', self.seed)
- self.storage.put('seed_version', self.seed_version)
- self.set_use_encryption(password is not None)
+ def get_change_addresses(self):
+ return self.change_addresses
def get_seed(self, password):
- return pw_decode(self.seed, password)
+ return self.keystore.get_seed(password)
+
+ def add_seed(self, seed, pw):
+ self.keystore.add_seed(seed, pw)
def get_mnemonic(self, password):
- return self.get_seed(password)
+ return self.keystore.get_mnemonic(password)
def change_gap_limit(self, value):
'''This method is not called in the code, it is kept for console use'''
@@ -1561,17 +1296,15 @@ class Deterministic_Wallet(Abstract_Wallet):
self.gap_limit = value
self.storage.put('gap_limit', self.gap_limit)
return True
-
elif value >= self.min_acceptable_gap():
- for key, account in self.accounts.items():
- addresses = account.get_addresses(False)
- k = self.num_unused_trailing_addresses(addresses)
- n = len(addresses) - k + value
- account.receiving_pubkeys = account.receiving_pubkeys[0:n]
- account.receiving_addresses = account.receiving_addresses[0:n]
+ addresses = self.get_receiving_addresses()
+ k = self.num_unused_trailing_addresses(addresses)
+ n = len(addresses) - k + value
+ self.receiving_pubkeys = self.receiving_pubkeys[0:n]
+ self.receiving_addresses = self.receiving_addresses[0:n]
self.gap_limit = value
self.storage.put('gap_limit', self.gap_limit)
- self.save_accounts()
+ self.save_pubkeys()
return True
else:
return False
@@ -1587,44 +1320,61 @@ class Deterministic_Wallet(Abstract_Wallet):
# fixme: this assumes wallet is synchronized
n = 0
nmax = 0
-
- for account in self.accounts.values():
- addresses = account.get_addresses(0)
- k = self.num_unused_trailing_addresses(addresses)
- for a in addresses[0:-k]:
- if self.history.get(a):
- n = 0
- else:
- n += 1
- if n > nmax: nmax = n
+ addresses = self.account.get_receiving_addresses()
+ k = self.num_unused_trailing_addresses(addresses)
+ for a in addresses[0:-k]:
+ if self.history.get(a):
+ n = 0
+ else:
+ n += 1
+ if n > nmax: nmax = n
return nmax + 1
- def default_account(self):
- return self.accounts['0']
-
- def create_new_address(self, account=None, for_change=0):
- if account is None:
- account = self.default_account()
- address = account.create_new_address(for_change)
- self.add_address(address)
- return address
-
def add_address(self, address):
if address not in self.history:
self.history[address] = []
if self.synchronizer:
self.synchronizer.add(address)
- self.save_accounts()
+
+ def create_new_address(self, for_change):
+ pubkey_list = self.change_pubkeys if for_change else self.receiving_pubkeys
+ n = len(pubkey_list)
+ x = self.new_pubkeys(for_change, n)
+ pubkey_list.append(x)
+ self.save_pubkeys()
+ address = self.pubkeys_to_address(x)
+ addr_list = self.change_addresses if for_change else self.receiving_addresses
+ addr_list.append(address)
+ self.add_address(address)
+ return address
+
+ def synchronize_sequence(self, for_change):
+ limit = self.gap_limit_for_change if for_change else self.gap_limit
+ while True:
+ addresses = self.get_change_addresses() if for_change else self.get_receiving_addresses()
+ if len(addresses) < limit:
+ self.create_new_address(for_change)
+ continue
+ if map(lambda a: self.address_is_old(a), addresses[-limit:] ) == limit*[False]:
+ break
+ else:
+ self.create_new_address(for_change)
def synchronize(self):
with self.lock:
- for account in self.accounts.values():
- account.synchronize(self)
-
- def is_beyond_limit(self, address, account, is_change):
- if type(account) == ImportedAccount:
- return False
- addr_list = account.get_addresses(is_change)
+ if self.is_deterministic():
+ self.synchronize_sequence(False)
+ self.synchronize_sequence(True)
+ else:
+ if len(self.receiving_pubkeys) != len(self.keystore.keypairs):
+ self.receiving_pubkeys = self.keystore.keypairs.keys()
+ self.save_pubkeys()
+ self.receiving_addresses = map(self.pubkeys_to_address, self.receiving_pubkeys)
+ for addr in self.receiving_addresses:
+ self.add_address(addr)
+
+ def is_beyond_limit(self, address, is_change):
+ addr_list = self.get_change_addresses() if is_change else self.get_receiving_addresses()
i = addr_list.index(address)
prev_addresses = addr_list[:max(0, i)]
limit = self.gap_limit_for_change if is_change else self.gap_limit
@@ -1636,393 +1386,146 @@ class Deterministic_Wallet(Abstract_Wallet):
return False
return True
- def get_action(self):
- if not self.get_master_public_key():
- return 'create_seed'
- if not self.accounts:
- return 'create_main_account'
-
def get_master_public_keys(self):
- out = {}
- for k, account in self.accounts.items():
- if type(account) == ImportedAccount:
- continue
- name = self.get_account_name(k)
- mpk_text = '\n\n'.join(account.get_master_pubkeys())
- out[name] = mpk_text
- return out
+ return {'x':self.get_master_public_key()}
def get_fingerprint(self):
return self.get_master_public_key()
-class BIP32_Wallet(Deterministic_Wallet):
- # abstract class, bip32 logic
+
+
+class Standard_Wallet(Deterministic_Wallet, P2PK_Wallet):
root_name = 'x/'
+ wallet_type = 'standard'
def __init__(self, storage):
Deterministic_Wallet.__init__(self, storage)
- self.master_public_keys = storage.get('master_public_keys', {})
- self.master_private_keys = storage.get('master_private_keys', {})
- self.gap_limit = storage.get('gap_limit', 20)
- def is_watching_only(self):
- return not bool(self.master_private_keys)
+ def get_master_public_key(self):
+ return self.keystore.get_master_public_key()
- def can_import(self):
- return False
+ def new_pubkeys(self, c, i):
+ return self.keystore.derive_pubkey(c, i)
- def get_master_public_key(self):
- return self.master_public_keys.get(self.root_name)
-
- def get_master_private_key(self, account, password):
- k = self.master_private_keys.get(account)
- if not k: return
- xprv = pw_decode(k, password)
- try:
- deserialize_xkey(xprv)
- except:
- raise InvalidPassword()
- return xprv
+ def get_keystore(self):
+ return self.keystore
- def check_password(self, password):
- xpriv = self.get_master_private_key(self.root_name, password)
- xpub = self.master_public_keys[self.root_name]
- if deserialize_xkey(xpriv)[3] != deserialize_xkey(xpub)[3]:
- raise InvalidPassword()
-
- def add_master_public_key(self, name, xpub):
- if xpub in self.master_public_keys.values():
- raise BaseException('Duplicate master public key')
- self.master_public_keys[name] = xpub
- self.storage.put('master_public_keys', self.master_public_keys)
-
- def add_master_private_key(self, name, xpriv, password):
- self.master_private_keys[name] = pw_encode(xpriv, password)
- self.storage.put('master_private_keys', self.master_private_keys)
-
- 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 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()
-
- def format_seed(self, seed):
- return NEW_SEED_VERSION, ' '.join(seed.split())
-
-
-class BIP32_Simple_Wallet(BIP32_Wallet):
- # Wallet with a single BIP32 account, no seed
- # gap limit 20
- wallet_type = 'xpub'
-
- def create_xprv_wallet(self, xprv, password):
- xpub = bitcoin.xpub_from_xprv(xprv)
- account = BIP32_Account({'xpub':xpub})
- self.storage.put('seed_version', self.seed_version)
- self.add_master_private_key(self.root_name, xprv, password)
- self.add_master_public_key(self.root_name, xpub)
- self.add_account('0', account)
- self.set_use_encryption(password is not None)
-
- def create_xpub_wallet(self, xpub):
- account = BIP32_Account({'xpub':xpub})
- self.storage.put('seed_version', self.seed_version)
- self.add_master_public_key(self.root_name, xpub)
- self.add_account('0', account)
-
-class BIP32_RD_Wallet(BIP32_Wallet):
- # Abstract base class for a BIP32 wallet with a self.root_derivation
-
- @classmethod
- def account_derivation(self, account_id):
- return self.root_derivation + account_id
-
- @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_xprv_from_seed(self, seed, name, password, passphrase=''):
- # we don't store the seed, only the master xpriv
- xprv, xpub = bip32_root(self.mnemonic_to_seed(seed, passphrase))
- xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
- self.add_master_public_key(name, xpub)
- self.add_master_private_key(name, xprv, password)
-
- def add_xpub_from_seed(self, seed, name):
- # store only master xpub
- xprv, xpub = bip32_root(self.mnemonic_to_seed(seed,''))
- xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
- self.add_master_public_key(name, xpub)
-
- def create_master_keys(self, password):
- seed = self.get_seed(password)
- self.add_xprv_from_seed(seed, self.root_name, password)
-
-
-class BIP32_HD_Wallet(BIP32_RD_Wallet):
- # Abstract base class for a BIP32 wallet that admits account creation
+ def get_keystores(self):
+ return [self.keystore]
- def __init__(self, storage):
- BIP32_Wallet.__init__(self, storage)
- # Backwards-compatibility. Remove legacy "next_account2" and
- # drop unused master public key to avoid duplicate errors
- acc2 = storage.get('next_account2', None)
- if acc2:
- self.master_public_keys.pop(self.root_name + acc2[0] + "'", None)
- storage.put('next_account2', None)
- storage.put('master_public_keys', self.master_public_keys)
-
- def next_account_number(self):
- assert (set(self.accounts.keys()) ==
- set(['%d' % n for n in range(len(self.accounts))]))
- return len(self.accounts)
-
- def show_account(self, account_id):
- return self.account_is_used(account_id) or account_id in self.labels
-
- def last_account_id(self):
- return '%d' % (self.next_account_number() - 1)
-
- def accounts_to_show(self):
- # The last account is shown only if named or used
- result = list(self.accounts.keys())
- last_id = self.last_account_id()
- if not self.show_account(last_id):
- result.remove(last_id)
- return result
-
- def can_create_accounts(self):
- return self.root_name in self.master_private_keys.keys()
-
- def permit_account_naming(self):
- return (self.can_create_accounts() and
- not self.show_account(self.last_account_id()))
-
- def create_hd_account(self, password):
- # First check the password is valid (this raises if it isn't).
- if self.can_change_password():
- self.check_password(password)
- assert self.next_account_number() == 0
- self.create_next_account(password, _('Main account'))
- self.create_next_account(password)
-
- def create_next_account(self, password, label=None):
- account_id = '%d' % self.next_account_number()
- derivation = self.account_derivation(account_id)
- root_name = self.root_derivation.split('/')[0] # NOT self.root_name!
- xpub, xprv = self.derive_xkeys(root_name, derivation, password)
- wallet_key = self.root_name + account_id + "'"
- self.add_master_public_key(wallet_key, xpub)
- if xprv:
- self.add_master_private_key(wallet_key, xprv, password)
- account = BIP32_Account({'xpub':xpub})
- self.add_account(account_id, account)
- if label:
- self.set_label(account_id, label)
- self.save_accounts()
-
- def account_is_used(self, account_id):
- return self.accounts[account_id].is_used(self)
-
- def accounts_all_used(self):
- return all(self.account_is_used(acc_id) for acc_id in self.accounts)
-
-
-class BIP44_Wallet(BIP32_HD_Wallet):
- root_derivation = "m/44'/0'/"
- wallet_type = 'bip44'
-
- @classmethod
- def account_derivation(self, account_id):
- return self.root_derivation + account_id + "'"
-
- def can_sign_xpubkey(self, x_pubkey):
- xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey)
- return xpub in self.master_public_keys.values()
-
- def can_create_accounts(self):
- return not self.is_watching_only()
+ def is_watching_only(self):
+ return self.keystore.is_watching_only()
- @staticmethod
- def normalize_passphrase(passphrase):
- return normalize('NFKD', unicode(passphrase or ''))
+ def can_change_password(self):
+ return self.keystore.can_change_password()
- @staticmethod
- def mnemonic_to_seed(mnemonic, passphrase):
- # See BIP39
- import pbkdf2, hashlib, hmac
- PBKDF2_ROUNDS = 2048
- mnemonic = normalize('NFKD', ' '.join(mnemonic.split()))
- passphrase = BIP44_Wallet.normalize_passphrase(passphrase)
- return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase,
- iterations = PBKDF2_ROUNDS, macmodule = hmac,
- digestmodule = hashlib.sha512).read(64)
-
- def derive_xkeys(self, root, derivation, password):
- root = self.root_name
- derivation = derivation.replace(self.root_derivation, root)
- x = self.master_private_keys.get(root)
- if x:
- root_xprv = pw_decode(x, password)
- xprv, xpub = bip32_private_derivation(root_xprv, root, derivation)
- return xpub, xprv
- else:
- root_xpub = self.master_public_keys.get(root)
- xpub = bip32_public_derivation(root_xpub, root, derivation)
- return xpub, None
+ def has_password(self):
+ return self.keystore.has_password()
+ def check_password(self, password):
+ self.keystore.check_password(password)
-class NewWallet(BIP32_RD_Wallet, Mnemonic):
- # Standard wallet
- root_derivation = "m/"
- wallet_type = 'standard'
+ def update_password(self, old_pw, new_pw):
+ self.keystore.update_password(old_pw, new_pw)
+ self.keystore.save(self.storage, self.root_name)
+
+ def can_import(self):
+ return self.keystore.can_import()
- def create_main_account(self):
- xpub = self.master_public_keys.get("x/")
- account = BIP32_Account({'xpub':xpub})
- self.add_account('0', account)
+ def import_key(self, pk, pw):
+ pubkey = self.keystore.import_key(pk, pw)
+ self.receiving_pubkeys.append(pubkey)
+ self.save_pubkeys()
+ addr = self.pubkeys_to_address(pubkey)
+ self.receiving_addresses.append(addr)
+ self.add_address(addr)
+ return addr
-class Multisig_Wallet(BIP32_RD_Wallet, Mnemonic):
+class Multisig_Wallet(Deterministic_Wallet):
# generic m of n
root_name = "x1/"
- root_derivation = "m/"
+ gap_limit = 20
def __init__(self, storage):
- BIP32_Wallet.__init__(self, storage)
self.wallet_type = storage.get('wallet_type')
self.m, self.n = Wallet.multisig_type(self.wallet_type)
+ Deterministic_Wallet.__init__(self, storage)
- def load_accounts(self):
- self.accounts = {}
- d = self.storage.get('accounts', {})
- v = d.get('0')
- if v:
- if v.get('xpub3'):
- v['xpubs'] = [v['xpub'], v['xpub2'], v['xpub3']]
- elif v.get('xpub2'):
- v['xpubs'] = [v['xpub'], v['xpub2']]
- self.accounts = {'0': Multisig_Account(v)}
-
- def create_main_account(self):
- account = Multisig_Account({'xpubs': self.master_public_keys.values(), 'm': self.m})
- self.add_account('0', account)
+ def get_pubkeys(self, c, i):
+ pubkey_list = self.change_pubkeys if c else self.receiving_pubkeys
+ return pubkey_list[i]
- def get_master_public_keys(self):
- return self.master_public_keys
+ def redeem_script(self, c, i):
+ pubkeys = self.get_pubkeys(c, i)
+ 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_missing_cosigner(self):
+ def new_pubkeys(self, c, i):
+ return [k.derive_pubkey(c, i) for k in self.keystores.values()]
+
+ def load_keystore(self):
+ self.keystores = {}
for i in range(self.n):
- if self.master_public_keys.get("x%d/"%(i+1)) is None:
- return i+1
-
- def add_cosigner(self, name, text, password):
- if Wallet.is_xprv(text):
- xpub = bitcoin.xpub_from_xprv(text)
- self.add_master_public_key(name, xpub)
- self.add_master_private_key(name, text, password)
- elif Wallet.is_xpub(text):
- self.add_master_public_key(name, text)
- if Wallet.is_seed(text):
- if name == 'x1/':
- self.add_seed(text, password)
- self.create_master_keys(password)
- else:
- self.add_xprv_from_seed(text, name, password)
+ name = 'x%d/'%(i+1)
+ self.keystores[name] = load_keystore(self.storage, name)
+ self.keystore = self.keystores[self.root_name]
- def get_action(self):
- i = self.get_missing_cosigner()
- if i is not None:
- return 'create_seed' if i == 1 else 'show_xpub_and_add_cosigners'
- if not self.accounts:
- return 'create_main_account'
+ def get_keystore(self):
+ return self.keystores.get(self.root_name)
- def get_fingerprint(self):
- return ''.join(sorted(self.get_master_public_keys().values()))
+ def get_keystores(self):
+ return self.keystores.values()
+
+ def update_password(self, old_pw, new_pw):
+ for name, keystore in self.keystores.items():
+ keystore.update_password(old_pw, new_pw)
+ keystore.save(self.storage, name)
+ def has_seed(self):
+ return self.keystore.has_seed()
-class OldWallet(Deterministic_Wallet):
- wallet_type = 'old'
+ def can_change_password(self):
+ return self.keystore.can_change_password()
- def __init__(self, storage):
- Deterministic_Wallet.__init__(self, storage)
- self.gap_limit = storage.get('gap_limit', 5)
-
- def make_seed(self):
- import old_mnemonic
- seed = random_seed(128)
- return ' '.join(old_mnemonic.mn_encode(seed))
-
- 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 create_master_keys(self, password):
- seed = self.get_seed(password)
- mpk = OldAccount.mpk_from_seed(seed)
- self.storage.put('master_public_key', mpk)
+ def has_password(self):
+ return self.keystore.has_password()
+
+ def is_watching_only(self):
+ return not any([not k.is_watching_only() for k in self.get_keystores()])
def get_master_public_key(self):
- return self.storage.get("master_public_key")
+ return self.keystore.get_master_public_key()
def get_master_public_keys(self):
- return {'Main Account':self.get_master_public_key()}
+ return dict(map(lambda x: (x[0], x[1].get_master_public_key()), self.keystores.items()))
- def create_main_account(self):
- mpk = self.storage.get("master_public_key")
- self.create_account(mpk)
-
- def create_account(self, mpk):
- self.accounts['0'] = OldAccount({'mpk':mpk, 0:[], 1:[]})
- self.save_accounts()
+ def get_fingerprint(self):
+ return ''.join(sorted(self.get_master_public_keys()))
- def create_watching_only_wallet(self, mpk):
- self.seed_version = OLD_SEED_VERSION
- self.storage.put('seed_version', self.seed_version)
- self.storage.put('master_public_key', mpk)
- self.create_account(mpk)
+ def add_input_sig_info(self, txin, address):
+ txin['derivation'] = derivation = self.get_address_index(address)
+ pubkeys = self.get_pubkeys(*derivation)
+ x_pubkeys = self.get_xpubkeys(*derivation)
+ # sort pubkeys and x_pubkeys, using the order of pubkeys
+ pubkeys, x_pubkeys = zip( *sorted(zip(pubkeys, x_pubkeys)))
+ txin['pubkeys'] = list(pubkeys)
+ txin['x_pubkeys'] = list(x_pubkeys)
+ txin['signatures'] = [None] * len(pubkeys)
+ txin['redeemScript'] = self.redeem_script(*derivation)
+ txin['num_sig'] = self.m
- def get_seed(self, password):
- seed = pw_decode(self.seed, password).encode('utf8')
- return seed
- def check_password(self, password):
- seed = self.get_seed(password)
- self.accounts['0'].check_seed(seed)
-
- def get_mnemonic(self, password):
- import old_mnemonic
- s = self.get_seed(password)
- return ' '.join(old_mnemonic.mn_encode(s))
WalletType = namedtuple("WalletType", "category type constructor")
+
# former WalletFactory
class Wallet(object):
"""The main wallet "entry point".
@@ -2030,40 +1533,18 @@ class Wallet(object):
type when passed a WalletStorage instance."""
wallets = [ # category type constructor
- WalletType('standard', 'old', OldWallet),
- WalletType('standard', 'xpub', BIP32_Simple_Wallet),
- WalletType('standard', 'standard', NewWallet),
+ WalletType('standard', 'old', Standard_Wallet),
+ WalletType('standard', 'xpub', Standard_Wallet),
+ WalletType('standard', 'standard', Standard_Wallet),
WalletType('standard', 'imported', Imported_Wallet),
WalletType('multisig', '2of2', Multisig_Wallet),
WalletType('multisig', '2of3', Multisig_Wallet),
- WalletType('bip44', 'bip44', BIP44_Wallet),
]
def __new__(self, storage):
- seed_version = storage.get('seed_version')
- if not seed_version:
- seed_version = OLD_SEED_VERSION if len(storage.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(storage.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 storage.get('master_public_keys') is None and storage.get('master_private_keys') is None and storage.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)
-
wallet_type = storage.get('wallet_type')
- WalletClass = Wallet.wallet_class(wallet_type, seed_version)
+ WalletClass = Wallet.wallet_class(wallet_type)
wallet = WalletClass(storage)
-
# Convert hardware wallets restored with older versions of
# Electrum to BIP44 wallets. A hardware wallet does not have
# a seed and plugins do not need to handle having one.
@@ -2072,7 +1553,6 @@ class Wallet(object):
storage.print_error("converting wallet type to " + rwc.wallet_type)
storage.put('wallet_type', rwc.wallet_type)
wallet = rwc(storage)
-
return wallet
@staticmethod
@@ -2080,79 +1560,17 @@ class Wallet(object):
return [wallet.category for wallet in Wallet.wallets]
@staticmethod
- def register_plugin_wallet(category, type, constructor):
+ def register_constructor(category, type, constructor):
Wallet.wallets.append(WalletType(category, type, constructor))
@staticmethod
- def wallet_class(wallet_type, seed_version):
- if wallet_type:
- if Wallet.multisig_type(wallet_type):
- return Multisig_Wallet
-
- for wallet in Wallet.wallets:
- if wallet.type == wallet_type:
- return wallet.constructor
-
- raise RuntimeError("Unknown wallet type: " + wallet_type)
-
- return OldWallet if seed_version == OLD_SEED_VERSION else NewWallet
-
- @staticmethod
- def is_seed(seed):
- return is_old_seed(seed) or is_new_seed(seed)
-
- @staticmethod
- def is_mpk(text):
- return Wallet.is_old_mpk(text) or Wallet.is_xpub(text)
-
- @staticmethod
- def is_old_mpk(mpk):
- try:
- int(mpk, 16)
- except:
- return False
- return len(mpk) == 128
-
- @staticmethod
- def is_xpub(text):
- if text[0:4] != 'xpub':
- return False
- try:
- deserialize_xkey(text)
- return True
- except:
- return False
-
- @staticmethod
- def is_xprv(text):
- if text[0:4] != 'xprv':
- return False
- try:
- deserialize_xkey(text)
- return True
- except:
- return False
-
- @staticmethod
- def is_address(text):
- parts = text.split()
- return bool(parts) and all(bitcoin.is_address(x) for x in parts)
-
- @staticmethod
- def is_private_key(text):
- parts = text.split()
- return bool(parts) and all(bitcoin.is_private_key(x) for x in parts)
-
- @staticmethod
- def is_any(text):
- return (Wallet.is_seed(text) or Wallet.is_old_mpk(text)
- or Wallet.is_xprv(text) or Wallet.is_xpub(text)
- or Wallet.is_address(text) or Wallet.is_private_key(text))
-
- @staticmethod
- def should_encrypt(text):
- return (Wallet.is_seed(text) or Wallet.is_xprv(text)
- or Wallet.is_private_key(text))
+ def wallet_class(wallet_type):
+ if Wallet.multisig_type(wallet_type):
+ return Multisig_Wallet
+ for wallet in Wallet.wallets:
+ if wallet.type == wallet_type:
+ return wallet.constructor
+ raise RuntimeError("Unknown wallet type: " + wallet_type)
@staticmethod
def multisig_type(wallet_type):
@@ -2163,66 +1581,3 @@ class Wallet(object):
match = [int(x) for x in match.group(1, 2)]
return match
- @staticmethod
- def from_seed(seed, password, storage):
- if is_old_seed(seed):
- klass = OldWallet
- elif is_new_seed(seed):
- klass = NewWallet
- w = klass(storage)
- w.add_seed(seed, password)
- w.create_master_keys(password)
- return w
-
- @staticmethod
- def from_address(text, storage):
- w = Imported_Wallet(storage)
- for x in text.split():
- w.accounts[IMPORTED_ACCOUNT].add(x, None, None, None)
- w.save_accounts()
- return w
-
- @staticmethod
- def from_private_key(text, password, storage):
- w = Imported_Wallet(storage)
- w.update_password(None, password)
- for x in text.split():
- w.import_key(x, password)
- return w
-
- @staticmethod
- def from_old_mpk(mpk, storage):
- w = OldWallet(storage)
- w.seed = ''
- w.create_watching_only_wallet(mpk)
- return w
-
- @staticmethod
- def from_xpub(xpub, storage):
- w = BIP32_Simple_Wallet(storage)
- w.create_xpub_wallet(xpub)
- return w
-
- @staticmethod
- def from_xprv(xprv, password, storage):
- w = BIP32_Simple_Wallet(storage)
- w.create_xprv_wallet(xprv, password)
- return w
-
- @staticmethod
- def from_text(text, password, storage):
- if Wallet.is_xprv(text):
- wallet = Wallet.from_xprv(text, password, storage)
- elif Wallet.is_old_mpk(text):
- wallet = Wallet.from_old_mpk(text, storage)
- elif Wallet.is_xpub(text):
- wallet = Wallet.from_xpub(text, storage)
- elif Wallet.is_address(text):
- wallet = Wallet.from_address(text, storage)
- elif Wallet.is_private_key(text):
- wallet = Wallet.from_private_key(text, password, storage)
- elif Wallet.is_seed(text):
- wallet = Wallet.from_seed(text, password, storage)
- else:
- raise BaseException('Invalid seedphrase or key')
- return wallet
diff --git a/plugins/cosigner_pool/qt.py b/plugins/cosigner_pool/qt.py
@@ -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):
diff --git a/plugins/hw_wallet/__init__.py b/plugins/hw_wallet/__init__.py
@@ -1,2 +1 @@
-from hw_wallet import BIP44_HW_Wallet
from plugin import HW_PluginBase
diff --git a/plugins/hw_wallet/hw_wallet.py b/plugins/hw_wallet/hw_wallet.py
@@ -1,95 +0,0 @@
-#!/usr/bin/env python2
-# -*- mode: python -*-
-#
-# Electrum - lightweight Bitcoin client
-# Copyright (C) 2016 The Electrum developers
-#
-# Permission is hereby granted, free of charge, to any person
-# obtaining a copy of this software and associated documentation files
-# (the "Software"), to deal in the Software without restriction,
-# including without limitation the rights to use, copy, modify, merge,
-# publish, distribute, sublicense, and/or sell copies of the Software,
-# and to permit persons to whom the Software is furnished to do so,
-# subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
-# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-
-from struct import pack
-
-from electrum.wallet import BIP44_Wallet
-
-class BIP44_HW_Wallet(BIP44_Wallet):
- '''A BIP44 hardware wallet base class.'''
- # Derived classes must set:
- # - device
- # - DEVICE_IDS
- # - wallet_type
-
- restore_wallet_class = BIP44_Wallet
- max_change_outputs = 1
-
- def __init__(self, storage):
- BIP44_Wallet.__init__(self, storage)
- # Errors and other user interaction is done through the wallet's
- # handler. The handler is per-window and preserved across
- # device reconnects
- self.handler = None
-
- def unpaired(self):
- '''A device paired with the wallet was diconnected. This can be
- called in any thread context.'''
- self.print_error("unpaired")
-
- def paired(self):
- '''A device paired with the wallet was (re-)connected. This can be
- called in any thread context.'''
- self.print_error("paired")
-
- def get_action(self):
- pass
-
- def can_create_accounts(self):
- return True
-
- def can_export(self):
- return False
-
- def is_watching_only(self):
- '''The wallet is not watching-only; the user will be prompted for
- pin and passphrase as appropriate when needed.'''
- assert not self.has_seed()
- return False
-
- def can_change_password(self):
- return False
-
- def get_client(self, force_pair=True):
- return self.plugin.get_client(self, force_pair)
-
- def first_address(self):
- '''Used to check a hardware wallet matches a software wallet'''
- account = self.accounts.get('0')
- derivation = self.address_derivation('0', 0, 0)
- return (account.first_address()[0] if account else None, derivation)
-
- def derive_xkeys(self, root, derivation, password):
- if self.master_public_keys.get(self.root_name):
- return BIP44_wallet.derive_xkeys(self, root, derivation, password)
-
- # When creating a wallet we need to ask the device for the
- # master public key
- xpub = self.get_public_key(derivation)
- return xpub, None
-
- def i4b(self, x):
- return pack('>I', x)
diff --git a/plugins/hw_wallet/plugin.py b/plugins/hw_wallet/plugin.py
@@ -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
diff --git a/plugins/keepkey/__init__.py b/plugins/keepkey/__init__.py
@@ -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']
diff --git a/plugins/keepkey/keepkey.py b/plugins/keepkey/keepkey.py
@@ -1,7 +1,7 @@
-from ..trezor.plugin import TrezorCompatiblePlugin, TrezorCompatibleWallet
+from ..trezor.plugin import TrezorCompatiblePlugin, TrezorCompatibleKeyStore
-class KeepKeyWallet(TrezorCompatibleWallet):
+class KeepKey_KeyStore(TrezorCompatibleKeyStore):
wallet_type = 'keepkey'
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
diff --git a/plugins/ledger/__init__.py b/plugins/ledger/__init__.py
@@ -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']
diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py
@@ -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'
diff --git a/plugins/trezor/__init__.py b/plugins/trezor/__init__.py
@@ -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']
diff --git a/plugins/trezor/clientbase.py b/plugins/trezor/clientbase.py
@@ -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:
diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py
@@ -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_derivation(self):
+ return self.root + "/%d'"%self.account_id
- def get_public_key(self, bip32_path):
+ def get_client(self, force_pair=True):
+ return self.plugin.get_client(self, force_pair)
+
+ def init_xpub(self):
client = self.get_client()
- 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)
diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py
@@ -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)
diff --git a/plugins/trezor/trezor.py b/plugins/trezor/trezor.py
@@ -1,16 +1,15 @@
-from .plugin import TrezorCompatiblePlugin, TrezorCompatibleWallet
+from .plugin import TrezorCompatiblePlugin, TrezorCompatibleKeyStore
-class TrezorWallet(TrezorCompatibleWallet):
+class TrezorKeyStore(TrezorCompatibleKeyStore):
wallet_type = 'trezor'
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
diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py
@@ -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)
+
+ @hook
+ def do_clear(self, window):
+ window.wallet.is_billing = False
- def show_disclaimer(self, wallet, wizard):
- self.set_enabled(wallet, True)
+ 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 choose_seed(self, wizard):
+ title = _('Create or restore')
+ message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
+ choices = [
+ ('create_seed', _('Create a new seed')),
+ ('restore_wallet', _('I already have a seed')),
+ ]
+ wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run)
+
+ def create_seed(self, wizard):
+ seed = self.make_seed()
+ wizard.show_seed_dialog(run_next=wizard.confirm_seed, seed_text=seed)
- def create_wallet(self, wallet, wizard, seed, password):
- wallet.storage.put('seed_version', wallet.seed_version)
- wallet.storage.put('use_encryption', password is not None)
+ 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'