electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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:
Mgui/kivy/main_window.py | 14+++++++-------
Mgui/kivy/uix/dialogs/installwizard.py | 4++--
Mgui/kivy/uix/screens.py | 2+-
Mgui/qt/__init__.py | 4++--
Mgui/qt/address_list.py | 46++++++++++------------------------------------
Mgui/qt/history_list.py | 2+-
Mgui/qt/installwizard.py | 40+++++++++++++++++++++++++++++++++-------
Mgui/qt/main_window.py | 150++++++++++++++------------------------------------------------------------------
Mgui/qt/password_dialog.py | 2+-
Mgui/qt/request_list.py | 19+++++++------------
Mgui/qt/seed_dialog.py | 8+-------
Dlib/account.py | 381-------------------------------------------------------------------------------
Mlib/base_wizard.py | 217+++++++++++++++++++++++++++++++++++--------------------------------------------
Mlib/commands.py | 12++++--------
Mlib/daemon.py | 8++++----
Alib/keystore.py | 701+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/plugins.py | 74+++++++++++++++++++++++++++++++++++++++++---------------------------------
Alib/storage.py | 253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/synchronizer.py | 2+-
Mlib/transaction.py | 17-----------------
Mlib/wallet.py | 1321++++++++++++++++++++-----------------------------------------------------------
Mplugins/cosigner_pool/qt.py | 3++-
Mplugins/hw_wallet/__init__.py | 1-
Dplugins/hw_wallet/hw_wallet.py | 95-------------------------------------------------------------------------------
Mplugins/hw_wallet/plugin.py | 33+++------------------------------
Mplugins/keepkey/__init__.py | 4++--
Mplugins/keepkey/keepkey.py | 6+++---
Mplugins/ledger/__init__.py | 4++--
Mplugins/ledger/ledger.py | 8++++----
Mplugins/trezor/__init__.py | 4++--
Mplugins/trezor/clientbase.py | 23+++++++++++++++++------
Mplugins/trezor/plugin.py | 116++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mplugins/trezor/qt_generic.py | 35++++++++++++++++++++---------------
Mplugins/trezor/trezor.py | 7+++----
Mplugins/trustedcoin/trustedcoin.py | 173+++++++++++++++++++++++++++++++++++++++++++------------------------------------
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'