Major refactoring
- separation between Wallet and key management (Keystore) - simplification of wallet classes - remove support for multiple accounts in the same wallet - add support for OP_RETURN to Trezor plugin - split multi-accounts wallets for backward compatibility
This commit is contained in:
parent
6373a76a4a
commit
1159f85e05
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.'''
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
381
lib/account.py
381
lib/account.py
|
@ -1,381 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Electrum - lightweight Bitcoin client
|
||||
# Copyright (C) 2013 thomasv@gitorious
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import bitcoin
|
||||
from bitcoin import *
|
||||
from i18n import _
|
||||
from transaction import Transaction, is_extended_pubkey
|
||||
from util import InvalidPassword
|
||||
|
||||
|
||||
class Account(object):
|
||||
def __init__(self, v):
|
||||
self.receiving_pubkeys = v.get('receiving', [])
|
||||
self.change_pubkeys = v.get('change', [])
|
||||
# addresses will not be stored on disk
|
||||
self.receiving_addresses = map(self.pubkeys_to_address, self.receiving_pubkeys)
|
||||
self.change_addresses = map(self.pubkeys_to_address, self.change_pubkeys)
|
||||
|
||||
def dump(self):
|
||||
return {'receiving':self.receiving_pubkeys, 'change':self.change_pubkeys}
|
||||
|
||||
def get_pubkey(self, for_change, n):
|
||||
pubkeys_list = self.change_pubkeys if for_change else self.receiving_pubkeys
|
||||
return pubkeys_list[n]
|
||||
|
||||
def get_address(self, for_change, n):
|
||||
addr_list = self.change_addresses if for_change else self.receiving_addresses
|
||||
return addr_list[n]
|
||||
|
||||
def get_pubkeys(self, for_change, n):
|
||||
return [ self.get_pubkey(for_change, n)]
|
||||
|
||||
def get_addresses(self, for_change):
|
||||
addr_list = self.change_addresses if for_change else self.receiving_addresses
|
||||
return addr_list[:]
|
||||
|
||||
def derive_pubkeys(self, for_change, n):
|
||||
pass
|
||||
|
||||
def create_new_address(self, for_change):
|
||||
pubkeys_list = self.change_pubkeys if for_change else self.receiving_pubkeys
|
||||
addr_list = self.change_addresses if for_change else self.receiving_addresses
|
||||
n = len(pubkeys_list)
|
||||
pubkeys = self.derive_pubkeys(for_change, n)
|
||||
address = self.pubkeys_to_address(pubkeys)
|
||||
pubkeys_list.append(pubkeys)
|
||||
addr_list.append(address)
|
||||
return address
|
||||
|
||||
def pubkeys_to_address(self, pubkey):
|
||||
return public_key_to_bc_address(pubkey.decode('hex'))
|
||||
|
||||
def has_change(self):
|
||||
return True
|
||||
|
||||
def get_name(self, k):
|
||||
return _('Main account')
|
||||
|
||||
def redeem_script(self, for_change, n):
|
||||
return None
|
||||
|
||||
def is_used(self, wallet):
|
||||
addresses = self.get_addresses(False)
|
||||
return any(wallet.address_is_old(a, -1) for a in addresses)
|
||||
|
||||
def synchronize_sequence(self, wallet, for_change):
|
||||
limit = wallet.gap_limit_for_change if for_change else wallet.gap_limit
|
||||
while True:
|
||||
addresses = self.get_addresses(for_change)
|
||||
if len(addresses) < limit:
|
||||
address = self.create_new_address(for_change)
|
||||
wallet.add_address(address)
|
||||
continue
|
||||
if map( lambda a: wallet.address_is_old(a), addresses[-limit:] ) == limit*[False]:
|
||||
break
|
||||
else:
|
||||
address = self.create_new_address(for_change)
|
||||
wallet.add_address(address)
|
||||
|
||||
def synchronize(self, wallet):
|
||||
self.synchronize_sequence(wallet, False)
|
||||
self.synchronize_sequence(wallet, True)
|
||||
|
||||
|
||||
class ImportedAccount(Account):
|
||||
def __init__(self, d):
|
||||
self.keypairs = d['imported']
|
||||
|
||||
def synchronize(self, wallet):
|
||||
return
|
||||
|
||||
def get_addresses(self, for_change):
|
||||
return [] if for_change else sorted(self.keypairs.keys())
|
||||
|
||||
def get_pubkey(self, *sequence):
|
||||
for_change, i = sequence
|
||||
assert for_change == 0
|
||||
addr = self.get_addresses(0)[i]
|
||||
return self.keypairs[addr][0]
|
||||
|
||||
def get_xpubkeys(self, for_change, n):
|
||||
return self.get_pubkeys(for_change, n)
|
||||
|
||||
def get_private_key(self, sequence, wallet, password):
|
||||
from wallet import pw_decode
|
||||
for_change, i = sequence
|
||||
assert for_change == 0
|
||||
address = self.get_addresses(0)[i]
|
||||
pk = pw_decode(self.keypairs[address][1], password)
|
||||
# this checks the password
|
||||
if address != address_from_private_key(pk):
|
||||
raise InvalidPassword()
|
||||
return [pk]
|
||||
|
||||
def has_change(self):
|
||||
return False
|
||||
|
||||
def add(self, address, pubkey, privkey, password):
|
||||
from wallet import pw_encode
|
||||
self.keypairs[address] = [pubkey, pw_encode(privkey, password)]
|
||||
|
||||
def remove(self, address):
|
||||
self.keypairs.pop(address)
|
||||
|
||||
def dump(self):
|
||||
return {'imported':self.keypairs}
|
||||
|
||||
def get_name(self, k):
|
||||
return _('Imported keys')
|
||||
|
||||
def update_password(self, old_password, new_password):
|
||||
for k, v in self.keypairs.items():
|
||||
pubkey, a = v
|
||||
b = pw_decode(a, old_password)
|
||||
c = pw_encode(b, new_password)
|
||||
self.keypairs[k] = (pubkey, c)
|
||||
|
||||
|
||||
class OldAccount(Account):
|
||||
""" Privatekey(type,n) = Master_private_key + H(n|S|type) """
|
||||
|
||||
def __init__(self, v):
|
||||
Account.__init__(self, v)
|
||||
self.mpk = v['mpk'].decode('hex')
|
||||
|
||||
@classmethod
|
||||
def mpk_from_seed(klass, seed):
|
||||
secexp = klass.stretch_key(seed)
|
||||
master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
|
||||
master_public_key = master_private_key.get_verifying_key().to_string().encode('hex')
|
||||
return master_public_key
|
||||
|
||||
@classmethod
|
||||
def stretch_key(self,seed):
|
||||
oldseed = seed
|
||||
for i in range(100000):
|
||||
seed = hashlib.sha256(seed + oldseed).digest()
|
||||
return string_to_number( seed )
|
||||
|
||||
@classmethod
|
||||
def get_sequence(self, mpk, for_change, n):
|
||||
return string_to_number( Hash( "%d:%d:"%(n,for_change) + mpk ) )
|
||||
|
||||
def get_address(self, for_change, n):
|
||||
pubkey = self.get_pubkey(for_change, n)
|
||||
address = public_key_to_bc_address( pubkey.decode('hex') )
|
||||
return address
|
||||
|
||||
@classmethod
|
||||
def get_pubkey_from_mpk(self, mpk, for_change, n):
|
||||
z = self.get_sequence(mpk, for_change, n)
|
||||
master_public_key = ecdsa.VerifyingKey.from_string(mpk, curve = SECP256k1)
|
||||
pubkey_point = master_public_key.pubkey.point + z*SECP256k1.generator
|
||||
public_key2 = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve = SECP256k1)
|
||||
return '04' + public_key2.to_string().encode('hex')
|
||||
|
||||
def derive_pubkeys(self, for_change, n):
|
||||
return self.get_pubkey_from_mpk(self.mpk, for_change, n)
|
||||
|
||||
def get_private_key_from_stretched_exponent(self, for_change, n, secexp):
|
||||
order = generator_secp256k1.order()
|
||||
secexp = ( secexp + self.get_sequence(self.mpk, for_change, n) ) % order
|
||||
pk = number_to_string( secexp, generator_secp256k1.order() )
|
||||
compressed = False
|
||||
return SecretToASecret( pk, compressed )
|
||||
|
||||
|
||||
def get_private_key(self, sequence, wallet, password):
|
||||
seed = wallet.get_seed(password)
|
||||
self.check_seed(seed)
|
||||
for_change, n = sequence
|
||||
secexp = self.stretch_key(seed)
|
||||
pk = self.get_private_key_from_stretched_exponent(for_change, n, secexp)
|
||||
return [pk]
|
||||
|
||||
|
||||
def check_seed(self, seed):
|
||||
secexp = self.stretch_key(seed)
|
||||
master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
|
||||
master_public_key = master_private_key.get_verifying_key().to_string()
|
||||
if master_public_key != self.mpk:
|
||||
print_error('invalid password (mpk)', self.mpk.encode('hex'), master_public_key.encode('hex'))
|
||||
raise InvalidPassword()
|
||||
return True
|
||||
|
||||
def get_master_pubkeys(self):
|
||||
return [self.mpk.encode('hex')]
|
||||
|
||||
def get_type(self):
|
||||
return _('Old Electrum format')
|
||||
|
||||
def get_xpubkeys(self, for_change, n):
|
||||
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n)))
|
||||
mpk = self.mpk.encode('hex')
|
||||
x_pubkey = 'fe' + mpk + s
|
||||
return [ x_pubkey ]
|
||||
|
||||
@classmethod
|
||||
def parse_xpubkey(self, x_pubkey):
|
||||
assert is_extended_pubkey(x_pubkey)
|
||||
pk = x_pubkey[2:]
|
||||
mpk = pk[0:128]
|
||||
dd = pk[128:]
|
||||
s = []
|
||||
while dd:
|
||||
n = int(bitcoin.rev_hex(dd[0:4]), 16)
|
||||
dd = dd[4:]
|
||||
s.append(n)
|
||||
assert len(s) == 2
|
||||
return mpk, s
|
||||
|
||||
|
||||
class BIP32_Account(Account):
|
||||
|
||||
def __init__(self, v):
|
||||
Account.__init__(self, v)
|
||||
self.xpub = v['xpub']
|
||||
self.xpub_receive = None
|
||||
self.xpub_change = None
|
||||
|
||||
def dump(self):
|
||||
d = Account.dump(self)
|
||||
d['xpub'] = self.xpub
|
||||
return d
|
||||
|
||||
def first_address(self):
|
||||
pubkeys = self.derive_pubkeys(0, 0)
|
||||
addr = self.pubkeys_to_address(pubkeys)
|
||||
return addr, pubkeys
|
||||
|
||||
def get_master_pubkeys(self):
|
||||
return [self.xpub]
|
||||
|
||||
@classmethod
|
||||
def derive_pubkey_from_xpub(self, xpub, for_change, n):
|
||||
_, _, _, c, cK = deserialize_xkey(xpub)
|
||||
for i in [for_change, n]:
|
||||
cK, c = CKD_pub(cK, c, i)
|
||||
return cK.encode('hex')
|
||||
|
||||
def get_pubkey_from_xpub(self, xpub, for_change, n):
|
||||
xpubs = self.get_master_pubkeys()
|
||||
i = xpubs.index(xpub)
|
||||
pubkeys = self.get_pubkeys(for_change, n)
|
||||
return pubkeys[i]
|
||||
|
||||
def derive_pubkeys(self, for_change, n):
|
||||
xpub = self.xpub_change if for_change else self.xpub_receive
|
||||
if xpub is None:
|
||||
xpub = bip32_public_derivation(self.xpub, "", "/%d"%for_change)
|
||||
if for_change:
|
||||
self.xpub_change = xpub
|
||||
else:
|
||||
self.xpub_receive = xpub
|
||||
_, _, _, c, cK = deserialize_xkey(xpub)
|
||||
cK, c = CKD_pub(cK, c, n)
|
||||
result = cK.encode('hex')
|
||||
return result
|
||||
|
||||
|
||||
def get_private_key(self, sequence, wallet, password):
|
||||
out = []
|
||||
xpubs = self.get_master_pubkeys()
|
||||
roots = [k for k, v in wallet.master_public_keys.iteritems() if v in xpubs]
|
||||
for root in roots:
|
||||
xpriv = wallet.get_master_private_key(root, password)
|
||||
if not xpriv:
|
||||
continue
|
||||
_, _, _, c, k = deserialize_xkey(xpriv)
|
||||
pk = bip32_private_key( sequence, k, c )
|
||||
out.append(pk)
|
||||
return out
|
||||
|
||||
def get_type(self):
|
||||
return _('Standard 1 of 1')
|
||||
|
||||
def get_xpubkeys(self, for_change, n):
|
||||
# unsorted
|
||||
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change,n)))
|
||||
xpubs = self.get_master_pubkeys()
|
||||
return map(lambda xpub: 'ff' + bitcoin.DecodeBase58Check(xpub).encode('hex') + s, xpubs)
|
||||
|
||||
@classmethod
|
||||
def parse_xpubkey(self, pubkey):
|
||||
assert is_extended_pubkey(pubkey)
|
||||
pk = pubkey.decode('hex')
|
||||
pk = pk[1:]
|
||||
xkey = bitcoin.EncodeBase58Check(pk[0:78])
|
||||
dd = pk[78:]
|
||||
s = []
|
||||
while dd:
|
||||
n = int( bitcoin.rev_hex(dd[0:2].encode('hex')), 16)
|
||||
dd = dd[2:]
|
||||
s.append(n)
|
||||
assert len(s) == 2
|
||||
return xkey, s
|
||||
|
||||
def get_name(self, k):
|
||||
return "Main account" if k == '0' else "Account " + k
|
||||
|
||||
|
||||
|
||||
|
||||
class Multisig_Account(BIP32_Account):
|
||||
|
||||
def __init__(self, v):
|
||||
self.m = v.get('m', 2)
|
||||
Account.__init__(self, v)
|
||||
self.xpub_list = v['xpubs']
|
||||
|
||||
def dump(self):
|
||||
d = Account.dump(self)
|
||||
d['xpubs'] = self.xpub_list
|
||||
d['m'] = self.m
|
||||
return d
|
||||
|
||||
def get_pubkeys(self, for_change, n):
|
||||
return self.get_pubkey(for_change, n)
|
||||
|
||||
def derive_pubkeys(self, for_change, n):
|
||||
return map(lambda x: self.derive_pubkey_from_xpub(x, for_change, n), self.get_master_pubkeys())
|
||||
|
||||
def redeem_script(self, for_change, n):
|
||||
pubkeys = self.get_pubkeys(for_change, n)
|
||||
return Transaction.multisig_script(sorted(pubkeys), self.m)
|
||||
|
||||
def pubkeys_to_address(self, pubkeys):
|
||||
redeem_script = Transaction.multisig_script(sorted(pubkeys), self.m)
|
||||
address = hash_160_to_bc_address(hash_160(redeem_script.decode('hex')), 5)
|
||||
return address
|
||||
|
||||
def get_address(self, for_change, n):
|
||||
return self.pubkeys_to_address(self.get_pubkeys(for_change, n))
|
||||
|
||||
def get_master_pubkeys(self):
|
||||
return self.xpub_list
|
||||
|
||||
def get_type(self):
|
||||
return _('Multisig %d of %d'%(self.m, len(self.xpub_list)))
|
|
@ -24,24 +24,21 @@
|
|||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
from electrum.wallet import Wallet, Multisig_Wallet, WalletStorage
|
||||
import keystore
|
||||
from wallet import Wallet, Imported_Wallet, Standard_Wallet, Multisig_Wallet, WalletStorage
|
||||
from i18n import _
|
||||
|
||||
|
||||
is_any_key = lambda x: Wallet.is_old_mpk(x) or Wallet.is_xprv(x) or Wallet.is_xpub(x) or Wallet.is_address(x) or Wallet.is_private_key(x)
|
||||
is_private_key = lambda x: Wallet.is_xprv(x) or Wallet.is_private_key(x)
|
||||
is_bip32_key = lambda x: Wallet.is_xprv(x) or Wallet.is_xpub(x)
|
||||
|
||||
from plugins import run_hook
|
||||
|
||||
class BaseWizard(object):
|
||||
|
||||
def __init__(self, config, network, path):
|
||||
super(BaseWizard, self).__init__()
|
||||
self.config = config
|
||||
self.config = config
|
||||
self.network = network
|
||||
self.storage = WalletStorage(path)
|
||||
self.wallet = None
|
||||
self.stack = []
|
||||
self.plugin = None
|
||||
|
||||
def run(self, *args):
|
||||
action = args[0]
|
||||
|
@ -49,27 +46,17 @@ class BaseWizard(object):
|
|||
self.stack.append((action, args))
|
||||
if not action:
|
||||
return
|
||||
if hasattr(self.wallet, 'plugin') and hasattr(self.wallet.plugin, action):
|
||||
f = getattr(self.wallet.plugin, action)
|
||||
apply(f, (self.wallet, self) + args)
|
||||
if type(action) is tuple:
|
||||
self.plugin, action = action
|
||||
if self.plugin and hasattr(self.plugin, action):
|
||||
f = getattr(self.plugin, action)
|
||||
apply(f, (self,) + args)
|
||||
elif hasattr(self, action):
|
||||
f = getattr(self, action)
|
||||
apply(f, args)
|
||||
else:
|
||||
raise BaseException("unknown action", action)
|
||||
|
||||
def get_action(self):
|
||||
if self.storage.file_exists:
|
||||
self.wallet = Wallet(self.storage)
|
||||
action = self.wallet.get_action()
|
||||
else:
|
||||
action = 'new'
|
||||
return action
|
||||
|
||||
def get_wallet(self):
|
||||
if self.wallet and self.wallet.get_action() is None:
|
||||
return self.wallet
|
||||
|
||||
def can_go_back(self):
|
||||
return len(self.stack)>1
|
||||
|
||||
|
@ -91,11 +78,10 @@ class BaseWizard(object):
|
|||
('standard', _("Standard wallet")),
|
||||
('twofactor', _("Wallet with two-factor authentication")),
|
||||
('multisig', _("Multi-signature wallet")),
|
||||
('hardware', _("Hardware wallet")),
|
||||
]
|
||||
registered_kinds = Wallet.categories()
|
||||
choices = [pair for pair in wallet_kinds if pair[0] in registered_kinds]
|
||||
self.choice_dialog(title = title, message=message, choices=choices, run_next=self.on_wallet_type)
|
||||
choices = wallet_kinds#[pair for pair in wallet_kinds if pair[0] in registered_kinds]
|
||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type)
|
||||
|
||||
def on_wallet_type(self, choice):
|
||||
self.wallet_type = choice
|
||||
|
@ -103,66 +89,58 @@ class BaseWizard(object):
|
|||
action = 'choose_seed'
|
||||
elif choice == 'multisig':
|
||||
action = 'choose_multisig'
|
||||
elif choice == 'hardware':
|
||||
action = 'choose_hw'
|
||||
elif choice == 'twofactor':
|
||||
action = 'choose_seed'
|
||||
self.storage.put('wallet_type', '2fa')
|
||||
self.storage.put('use_trustedcoin', True)
|
||||
self.plugin = self.plugins.load_plugin('trustedcoin')
|
||||
action = self.storage.get_action()
|
||||
|
||||
self.run(action)
|
||||
|
||||
def choose_multisig(self):
|
||||
def on_multisig(m, n):
|
||||
self.multisig_type = "%dof%d"%(m, n)
|
||||
self.n = n
|
||||
self.run('choose_seed')
|
||||
self.multisig_dialog(run_next=on_multisig)
|
||||
|
||||
def choose_seed(self):
|
||||
title = _('Choose Seed')
|
||||
message = _("Do you want to create a new seed, or to restore a wallet using an existing seed?")
|
||||
if self.wallet_type == 'standard':
|
||||
title = _('Seed and Private Keys')
|
||||
message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
|
||||
if self.wallet_type in ['standard', 'multisig']:
|
||||
choices = [
|
||||
('create_seed', _('Create a new seed')),
|
||||
('restore_seed', _('I already have a seed')),
|
||||
('restore_from_key', _('Import keys')),
|
||||
('restore_from_key', _('Import keys or addresses')),
|
||||
('choose_hw', _('Use hardware wallet')),
|
||||
]
|
||||
elif self.wallet_type == 'twofactor':
|
||||
choices = [
|
||||
('create_2fa', _('Create a new seed')),
|
||||
('restore_2fa', _('I already have a seed')),
|
||||
]
|
||||
elif self.wallet_type == 'multisig':
|
||||
choices = [
|
||||
('create_seed', _('Create a new seed')),
|
||||
('restore_seed', _('I already have a seed')),
|
||||
('restore_from_key', _('I have a master key')),
|
||||
#('choose_hw', _('Cosign with hardware wallet')),
|
||||
]
|
||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
|
||||
|
||||
def create_2fa(self):
|
||||
self.storage.put('wallet_type', '2fa')
|
||||
self.wallet = Wallet(self.storage)
|
||||
self.run('show_disclaimer')
|
||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
|
||||
|
||||
def restore_seed(self):
|
||||
# TODO: return derivation password too
|
||||
self.restore_seed_dialog(run_next=self.add_password, is_valid=Wallet.is_seed)
|
||||
self.restore_seed_dialog(run_next=self.add_password, is_valid=keystore.is_seed)
|
||||
|
||||
def on_restore(self, text):
|
||||
if is_private_key(text):
|
||||
if keystore.is_address_list(text):
|
||||
self.wallet = Imported_Wallet(self.storage)
|
||||
for x in text.split():
|
||||
self.wallet.add_address(x)
|
||||
self.terminate()
|
||||
elif keystore.is_private(text):
|
||||
self.add_password(text)
|
||||
else:
|
||||
self.create_wallet(text, None)
|
||||
self.create_keystore(text, None)
|
||||
|
||||
def restore_from_key(self):
|
||||
if self.wallet_type == 'standard':
|
||||
v = is_any_key
|
||||
v = keystore.is_any_key
|
||||
title = _("Import keys")
|
||||
message = ' '.join([
|
||||
_("To create a watching-only wallet, please enter your master public key (xpub), or a list of Bitcoin addresses."),
|
||||
_("To create a spending wallet, please enter a master private key (xprv), or a list of Bitcoin private keys.")
|
||||
])
|
||||
else:
|
||||
v = is_bip32_key
|
||||
v = keystore.is_bip32_key
|
||||
title = _("Master public or private key")
|
||||
message = ' '.join([
|
||||
_("To create a watching-only wallet, please enter your master public key (xpub)."),
|
||||
|
@ -170,12 +148,8 @@ class BaseWizard(object):
|
|||
])
|
||||
self.restore_keys_dialog(title=title, message=message, run_next=self.on_restore, is_valid=v)
|
||||
|
||||
def restore_2fa(self):
|
||||
self.storage.put('wallet_type', '2fa')
|
||||
self.wallet = Wallet(self.storage)
|
||||
self.wallet.plugin.on_restore_wallet(self.wallet, self)
|
||||
|
||||
def choose_hw(self):
|
||||
self.storage.put('key_type', 'hardware')
|
||||
hw_wallet_types, choices = self.plugins.hardware_wallets('create')
|
||||
choices = zip(hw_wallet_types, choices)
|
||||
title = _('Hardware wallet')
|
||||
|
@ -189,84 +163,87 @@ class BaseWizard(object):
|
|||
self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_hardware)
|
||||
|
||||
def on_hardware(self, hw_type):
|
||||
self.hw_type = hw_type
|
||||
if self.wallet_type == 'multisig':
|
||||
self.create_hardware_multisig()
|
||||
else:
|
||||
title = _('Hardware wallet') + ' [%s]' % hw_type
|
||||
message = _('Do you have a device, or do you want to restore a wallet using an existing seed?')
|
||||
choices = [
|
||||
('create_hardware_wallet', _('I have a device')),
|
||||
('restore_hardware_wallet', _('Use hardware wallet seed')),
|
||||
]
|
||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
|
||||
self.storage.put('hardware_type', hw_type)
|
||||
title = _('Hardware wallet') + ' [%s]' % hw_type
|
||||
message = _('Do you have a device, or do you want to restore a wallet using an existing seed?')
|
||||
choices = [
|
||||
('on_hardware_device', _('I have a %s device')%hw_type),
|
||||
('on_hardware_seed', _('I have a %s seed')%hw_type),
|
||||
]
|
||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
|
||||
|
||||
def create_hardware_multisig(self):
|
||||
self.storage.put('wallet_type', self.multisig_type)
|
||||
self.wallet = Multisig_Wallet(self.storage)
|
||||
# todo: get the xpub from the plugin
|
||||
self.run('create_wallet', xpub, None)
|
||||
def on_hardware_device(self):
|
||||
from keystore import load_keystore
|
||||
keystore = load_keystore(self.storage, None)
|
||||
keystore.plugin.on_create_wallet(keystore, self)
|
||||
self.create_wallet(keystore, None)
|
||||
|
||||
def create_hardware_wallet(self):
|
||||
self.storage.put('wallet_type', self.hw_type)
|
||||
self.wallet = Wallet(self.storage)
|
||||
self.wallet.plugin.on_create_wallet(self.wallet, self)
|
||||
self.terminate()
|
||||
def on_hardware_seed(self):
|
||||
from keystore import load_keystore
|
||||
self.storage.put('key_type', 'hw_seed')
|
||||
keystore = load_keystore(self.storage, None)
|
||||
self.plugin = keystore #fixme .plugin
|
||||
keystore.on_restore_wallet(self)
|
||||
self.wallet = Standard_Wallet(self.storage)
|
||||
self.run('create_addresses')
|
||||
|
||||
def restore_hardware_wallet(self):
|
||||
self.storage.put('wallet_type', self.wallet_type)
|
||||
self.wallet = Wallet(self.storage)
|
||||
self.wallet.plugin.on_restore_wallet(self.wallet, self)
|
||||
self.terminate()
|
||||
|
||||
def create_wallet(self, text, password):
|
||||
def create_wallet(self, k, password):
|
||||
if self.wallet_type == 'standard':
|
||||
self.wallet = Wallet.from_text(text, password, self.storage)
|
||||
k.save(self.storage, 'x/')
|
||||
self.wallet = Standard_Wallet(self.storage)
|
||||
self.run('create_addresses')
|
||||
elif self.wallet_type == 'multisig':
|
||||
self.storage.put('wallet_type', self.multisig_type)
|
||||
self.wallet = Multisig_Wallet(self.storage)
|
||||
self.wallet.add_cosigner('x1/', text, password)
|
||||
self.add_cosigner(k, 0)
|
||||
xpub = k.get_master_public_key()
|
||||
self.stack = []
|
||||
self.run('show_xpub_and_add_cosigners', (password,))
|
||||
self.run('show_xpub_and_add_cosigners', password, xpub)
|
||||
|
||||
def show_xpub_and_add_cosigners(self, password):
|
||||
xpub = self.wallet.master_public_keys.get('x1/')
|
||||
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('add_cosigners', password))
|
||||
def show_xpub_and_add_cosigners(self, password, xpub):
|
||||
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('add_cosigners', password, 1))
|
||||
|
||||
def add_cosigners(self, password):
|
||||
i = self.wallet.get_missing_cosigner()
|
||||
self.add_cosigner_dialog(run_next=lambda x: self.on_cosigner(x, password), index=(i-1), is_valid=Wallet.is_xpub)
|
||||
def add_cosigner(self, keystore, i):
|
||||
d = self.storage.get('master_public_keys', {})
|
||||
if keystore.xpub in d.values():
|
||||
raise BaseException('duplicate key')
|
||||
keystore.save(self.storage, 'x%d/'%(i+1))
|
||||
|
||||
def on_cosigner(self, text, password):
|
||||
i = self.wallet.get_missing_cosigner()
|
||||
def add_cosigners(self, password, i):
|
||||
self.add_cosigner_dialog(run_next=lambda x: self.on_cosigner(x, password, i), index=i, is_valid=keystore.is_xpub)
|
||||
|
||||
def on_cosigner(self, text, password, i):
|
||||
k = keystore.from_text(text, password)
|
||||
try:
|
||||
self.wallet.add_cosigner('x%d/'%i, text, password)
|
||||
self.add_cosigner(k, i)
|
||||
except BaseException as e:
|
||||
print "error:" + str(e)
|
||||
i = self.wallet.get_missing_cosigner()
|
||||
if i:
|
||||
self.run('add_cosigners', password)
|
||||
self.show_message("error:" + str(e))
|
||||
return
|
||||
if i < self.n - 1:
|
||||
self.run('add_cosigners', password, i+1)
|
||||
else:
|
||||
self.wallet = Multisig_Wallet(self.storage)
|
||||
self.create_addresses()
|
||||
|
||||
def create_addresses(self):
|
||||
def task():
|
||||
self.wallet.create_main_account()
|
||||
self.wallet.synchronize()
|
||||
self.wallet.storage.write()
|
||||
self.terminate()
|
||||
msg = _("Electrum is generating your addresses, please wait.")
|
||||
self.waiting_dialog(task, msg)
|
||||
|
||||
def create_seed(self):
|
||||
from electrum.wallet import BIP32_Wallet
|
||||
seed = BIP32_Wallet.make_seed()
|
||||
from electrum.mnemonic import Mnemonic
|
||||
seed = Mnemonic('en').make_seed()
|
||||
self.show_seed_dialog(run_next=self.confirm_seed, seed_text=seed)
|
||||
|
||||
def confirm_seed(self, seed):
|
||||
self.confirm_seed_dialog(run_next=self.add_password, is_valid=lambda x: x==seed)
|
||||
|
||||
def add_password(self, text):
|
||||
f = lambda pw: self.run('create_wallet', text, pw)
|
||||
f = lambda pw: self.run('create_keystore', text, pw)
|
||||
self.request_password(run_next=f)
|
||||
|
||||
def create_keystore(self, text, password):
|
||||
k = keystore.from_text(text, password)
|
||||
self.create_wallet(k, password)
|
||||
|
||||
def create_addresses(self):
|
||||
def task():
|
||||
self.wallet.synchronize()
|
||||
self.wallet.storage.write()
|
||||
self.terminate()
|
||||
msg = _("Electrum is generating your addresses, please wait.")
|
||||
self.waiting_dialog(task, msg)
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -35,6 +35,10 @@ from util import *
|
|||
from i18n import _
|
||||
from util import profiler, PrintError, DaemonThread, UserCancelled
|
||||
|
||||
plugin_loaders = {}
|
||||
hook_names = set()
|
||||
hooks = {}
|
||||
|
||||
|
||||
class Plugins(DaemonThread):
|
||||
|
||||
|
@ -66,15 +70,17 @@ class Plugins(DaemonThread):
|
|||
continue
|
||||
details = d.get('registers_wallet_type')
|
||||
if details:
|
||||
self.register_plugin_wallet(name, gui_good, details)
|
||||
self.register_wallet_type(name, gui_good, details)
|
||||
details = d.get('registers_keystore')
|
||||
if details:
|
||||
self.register_keystore(name, gui_good, details)
|
||||
self.descriptions[name] = d
|
||||
if not d.get('requires_wallet_type') and self.config.get('use_' + name):
|
||||
try:
|
||||
self.load_plugin(name)
|
||||
except BaseException as e:
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
self.print_error("cannot initialize plugin %s:" % name,
|
||||
str(e))
|
||||
self.print_error("cannot initialize plugin %s:" % name, str(e))
|
||||
|
||||
def get(self, name):
|
||||
return self.plugins.get(name)
|
||||
|
@ -83,6 +89,8 @@ class Plugins(DaemonThread):
|
|||
return len(self.plugins)
|
||||
|
||||
def load_plugin(self, name):
|
||||
if name in self.plugins:
|
||||
return
|
||||
full_name = 'electrum_plugins.' + name + '.' + self.gui_name
|
||||
loader = pkgutil.find_loader(full_name)
|
||||
if not loader:
|
||||
|
@ -145,17 +153,23 @@ class Plugins(DaemonThread):
|
|||
self.print_error("cannot load plugin for:", name)
|
||||
return wallet_types, descs
|
||||
|
||||
def register_plugin_wallet(self, name, gui_good, details):
|
||||
def register_wallet_type(self, name, gui_good, details):
|
||||
from wallet import Wallet
|
||||
global plugin_loaders
|
||||
def loader():
|
||||
plugin = self.wallet_plugin_loader(name)
|
||||
Wallet.register_constructor(details[0], details[1], plugin.wallet_class)
|
||||
self.print_error("registering wallet type %s: %s" %(name, details))
|
||||
plugin_loaders[details[1]] = loader
|
||||
|
||||
def dynamic_constructor(storage):
|
||||
return self.wallet_plugin_loader(name).wallet_class(storage)
|
||||
|
||||
def register_keystore(self, name, gui_good, details):
|
||||
from keystore import register_keystore
|
||||
def dynamic_constructor():
|
||||
return self.wallet_plugin_loader(name).keystore_class()
|
||||
if details[0] == 'hardware':
|
||||
self.hw_wallets[name] = (gui_good, details)
|
||||
self.print_error("registering wallet %s: %s" %(name, details))
|
||||
Wallet.register_plugin_wallet(details[0], details[1],
|
||||
dynamic_constructor)
|
||||
self.print_error("registering keystore %s: %s" %(name, details))
|
||||
register_keystore(details[0], details[1], dynamic_constructor)
|
||||
|
||||
def wallet_plugin_loader(self, name):
|
||||
if not name in self.plugins:
|
||||
|
@ -169,9 +183,6 @@ class Plugins(DaemonThread):
|
|||
self.on_stop()
|
||||
|
||||
|
||||
hook_names = set()
|
||||
hooks = {}
|
||||
|
||||
def hook(func):
|
||||
hook_names.add(func.func_name)
|
||||
return func
|
||||
|
@ -375,48 +386,45 @@ class DeviceMgr(ThreadJob, PrintError):
|
|||
self.scan_devices(handler)
|
||||
return self.client_lookup(id_)
|
||||
|
||||
def client_for_wallet(self, plugin, wallet, force_pair):
|
||||
assert wallet.handler
|
||||
|
||||
devices = self.scan_devices(wallet.handler)
|
||||
wallet_id = self.wallet_id(wallet)
|
||||
|
||||
def client_for_keystore(self, plugin, keystore, force_pair):
|
||||
assert keystore.handler
|
||||
devices = self.scan_devices(keystore.handler)
|
||||
wallet_id = self.wallet_id(keystore)
|
||||
client = self.client_lookup(wallet_id)
|
||||
if client:
|
||||
# An unpaired client might have another wallet's handler
|
||||
# from a prior scan. Replace to fix dialog parenting.
|
||||
client.handler = wallet.handler
|
||||
client.handler = keystore.handler
|
||||
return client
|
||||
|
||||
for device in devices:
|
||||
if device.id_ == wallet_id:
|
||||
return self.create_client(device, wallet.handler, plugin)
|
||||
return self.create_client(device, keystore.handler, plugin)
|
||||
|
||||
if force_pair:
|
||||
return self.force_pair_wallet(plugin, wallet, devices)
|
||||
return self.force_pair_wallet(plugin, keystore, devices)
|
||||
|
||||
return None
|
||||
|
||||
def force_pair_wallet(self, plugin, wallet, devices):
|
||||
first_address, derivation = wallet.first_address()
|
||||
assert first_address
|
||||
def force_pair_wallet(self, plugin, keystore, devices):
|
||||
xpub = keystore.get_master_public_key()
|
||||
derivation = keystore.get_derivation()
|
||||
|
||||
# The wallet has not been previously paired, so let the user
|
||||
# choose an unpaired device and compare its first address.
|
||||
info = self.select_device(wallet, plugin, devices)
|
||||
|
||||
info = self.select_device(keystore, plugin, devices)
|
||||
client = self.client_lookup(info.device.id_)
|
||||
if client and client.is_pairable():
|
||||
# See comment above for same code
|
||||
client.handler = wallet.handler
|
||||
client.handler = keystore.handler
|
||||
# This will trigger a PIN/passphrase entry request
|
||||
try:
|
||||
client_first_address = client.first_address(derivation)
|
||||
client_xpub = client.get_xpub(derivation)
|
||||
except (UserCancelled, RuntimeError):
|
||||
# Bad / cancelled PIN / passphrase
|
||||
client_first_address = None
|
||||
if client_first_address == first_address:
|
||||
self.pair_wallet(wallet, info.device.id_)
|
||||
client_xpub = None
|
||||
if client_xpub == xpub:
|
||||
self.pair_wallet(keystore, info.device.id_)
|
||||
return client
|
||||
|
||||
# The user input has wrong PIN or passphrase, or cancelled input,
|
||||
|
|
|
@ -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
|
|
@ -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.'''
|
||||
|
|
|
@ -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()):
|
||||
|
|
1359
lib/wallet.py
1359
lib/wallet.py
File diff suppressed because it is too large
Load Diff
|
@ -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):
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
from hw_wallet import BIP44_HW_Wallet
|
||||
from plugin import HW_PluginBase
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -8,28 +8,32 @@ from functools import partial
|
|||
from electrum.account import BIP32_Account
|
||||
from electrum.bitcoin import (bc_address_to_hash_160, xpub_from_pubkey,
|
||||
public_key_to_bc_address, EncodeBase58Check,
|
||||
TYPE_ADDRESS)
|
||||
TYPE_ADDRESS, TYPE_SCRIPT)
|
||||
from electrum.i18n import _
|
||||
from electrum.plugins import BasePlugin, hook
|
||||
from electrum.transaction import (deserialize, is_extended_pubkey,
|
||||
Transaction, x_to_xpub)
|
||||
from ..hw_wallet import BIP44_HW_Wallet, HW_PluginBase
|
||||
from electrum.keystore import Hardware_KeyStore
|
||||
|
||||
from ..hw_wallet import HW_PluginBase
|
||||
|
||||
|
||||
# TREZOR initialization methods
|
||||
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
|
||||
|
||||
class TrezorCompatibleWallet(BIP44_HW_Wallet):
|
||||
class TrezorCompatibleKeyStore(Hardware_KeyStore):
|
||||
root = "m/44'/0'"
|
||||
account_id = 0
|
||||
|
||||
def get_public_key(self, bip32_path):
|
||||
def get_derivation(self):
|
||||
return self.root + "/%d'"%self.account_id
|
||||
|
||||
def get_client(self, force_pair=True):
|
||||
return self.plugin.get_client(self, force_pair)
|
||||
|
||||
def init_xpub(self):
|
||||
client = self.get_client()
|
||||
address_n = client.expand_path(bip32_path)
|
||||
creating = self.next_account_number() == 0
|
||||
node = client.get_public_node(address_n, creating).node
|
||||
xpub = ("0488B21E".decode('hex') + chr(node.depth)
|
||||
+ self.i4b(node.fingerprint) + self.i4b(node.child_num)
|
||||
+ node.chain_code + node.public_key)
|
||||
return EncodeBase58Check(xpub)
|
||||
self.xpub = client.get_xpub(self.get_derivation())
|
||||
|
||||
def decrypt_message(self, pubkey, message, password):
|
||||
raise RuntimeError(_('Electrum and %s encryption and decryption are currently incompatible') % self.device)
|
||||
|
@ -49,17 +53,6 @@ class TrezorCompatibleWallet(BIP44_HW_Wallet):
|
|||
msg_sig = client.sign_message('Bitcoin', address_n, message)
|
||||
return msg_sig.signature
|
||||
|
||||
def get_input_tx(self, tx_hash):
|
||||
# First look up an input transaction in the wallet where it
|
||||
# will likely be. If co-signing a transaction it may not have
|
||||
# all the input txs, in which case we ask the network.
|
||||
tx = self.transactions.get(tx_hash)
|
||||
if not tx:
|
||||
request = ('blockchain.transaction.get', [tx_hash])
|
||||
# FIXME: what if offline?
|
||||
tx = Transaction(self.network.synchronous_get(request))
|
||||
return tx
|
||||
|
||||
def sign_transaction(self, tx, password):
|
||||
if tx.is_complete():
|
||||
return
|
||||
|
@ -69,15 +62,13 @@ class TrezorCompatibleWallet(BIP44_HW_Wallet):
|
|||
xpub_path = {}
|
||||
for txin in tx.inputs():
|
||||
tx_hash = txin['prevout_hash']
|
||||
prev_tx[tx_hash] = self.get_input_tx(tx_hash)
|
||||
prev_tx[tx_hash] = txin['prev_tx']
|
||||
for x_pubkey in txin['x_pubkeys']:
|
||||
if not is_extended_pubkey(x_pubkey):
|
||||
continue
|
||||
xpub = x_to_xpub(x_pubkey)
|
||||
for k, v in self.master_public_keys.items():
|
||||
if v == xpub:
|
||||
acc_id = re.match("x/(\d+)'", k).group(1)
|
||||
xpub_path[xpub] = self.account_derivation(acc_id)
|
||||
if xpub == self.get_master_public_key():
|
||||
xpub_path[xpub] = self.get_derivation()
|
||||
|
||||
self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
|
||||
|
||||
|
@ -149,18 +140,16 @@ class TrezorCompatiblePlugin(HW_PluginBase):
|
|||
|
||||
return client
|
||||
|
||||
def get_client(self, wallet, force_pair=True):
|
||||
def get_client(self, keystore, force_pair=True):
|
||||
# All client interaction should not be in the main GUI thread
|
||||
assert self.main_thread != threading.current_thread()
|
||||
|
||||
devmgr = self.device_manager()
|
||||
client = devmgr.client_for_wallet(self, wallet, force_pair)
|
||||
client = devmgr.client_for_keystore(self, keystore, force_pair)
|
||||
if client:
|
||||
client.used()
|
||||
|
||||
return client
|
||||
|
||||
def initialize_device(self, wallet):
|
||||
def initialize_device(self, keystore):
|
||||
# Initialization method
|
||||
msg = _("Choose how you want to initialize your %s.\n\n"
|
||||
"The first two methods are secure as no secret information "
|
||||
|
@ -179,13 +168,13 @@ class TrezorCompatiblePlugin(HW_PluginBase):
|
|||
_("Upload a master private key")
|
||||
]
|
||||
|
||||
method = wallet.handler.query_choice(msg, methods)
|
||||
method = keystore.handler.query_choice(msg, methods)
|
||||
(item, label, pin_protection, passphrase_protection) \
|
||||
= wallet.handler.request_trezor_init_settings(method, self.device)
|
||||
|
||||
if method == TIM_RECOVER and self.device == 'TREZOR':
|
||||
# Warn user about firmware lameness
|
||||
wallet.handler.show_error(_(
|
||||
keystore.handler.show_error(_(
|
||||
"You will be asked to enter 24 words regardless of your "
|
||||
"seed's actual length. If you enter a word incorrectly or "
|
||||
"misspell it, you cannot change it or go back - you will need "
|
||||
|
@ -195,7 +184,7 @@ class TrezorCompatiblePlugin(HW_PluginBase):
|
|||
language = 'english'
|
||||
|
||||
def initialize_method():
|
||||
client = self.get_client(wallet)
|
||||
client = self.get_client(keystore)
|
||||
|
||||
if method == TIM_NEW:
|
||||
strength = 64 * (item + 2) # 128, 192 or 256
|
||||
|
@ -216,35 +205,36 @@ class TrezorCompatiblePlugin(HW_PluginBase):
|
|||
client.load_device_by_xprv(item, pin, passphrase_protection,
|
||||
label, language)
|
||||
# After successful initialization create accounts
|
||||
wallet.create_hd_account(None)
|
||||
keystore.init_xpub()
|
||||
#wallet.create_main_account()
|
||||
|
||||
return initialize_method
|
||||
|
||||
def setup_device(self, wallet, on_done, on_error):
|
||||
def setup_device(self, keystore, on_done, on_error):
|
||||
'''Called when creating a new wallet. Select the device to use. If
|
||||
the device is uninitialized, go through the intialization
|
||||
process. Then create the wallet accounts.'''
|
||||
devmgr = self.device_manager()
|
||||
device_info = devmgr.select_device(wallet, self)
|
||||
devmgr.pair_wallet(wallet, device_info.device.id_)
|
||||
device_info = devmgr.select_device(keystore, self)
|
||||
devmgr.pair_wallet(keystore, device_info.device.id_)
|
||||
if device_info.initialized:
|
||||
task = partial(wallet.create_hd_account, None)
|
||||
task = keystore.init_xpub
|
||||
else:
|
||||
task = self.initialize_device(wallet)
|
||||
wallet.thread.add(task, on_done=on_done, on_error=on_error)
|
||||
task = self.initialize_device(keystore)
|
||||
keystore.thread.add(task, on_done=on_done, on_error=on_error)
|
||||
|
||||
def sign_transaction(self, wallet, tx, prev_tx, xpub_path):
|
||||
def sign_transaction(self, keystore, tx, prev_tx, xpub_path):
|
||||
self.prev_tx = prev_tx
|
||||
self.xpub_path = xpub_path
|
||||
client = self.get_client(wallet)
|
||||
client = self.get_client(keystore)
|
||||
inputs = self.tx_inputs(tx, True)
|
||||
outputs = self.tx_outputs(wallet, tx)
|
||||
outputs = self.tx_outputs(keystore.get_derivation(), tx)
|
||||
signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
|
||||
raw = signed_tx.encode('hex')
|
||||
tx.update_signatures(raw)
|
||||
|
||||
def show_address(self, wallet, address):
|
||||
client = self.get_client(wallet)
|
||||
client = self.get_client(wallet.keystore)
|
||||
if not client.atleast_version(1, 3):
|
||||
wallet.handler.show_error(_("Your device firmware is too old"))
|
||||
return
|
||||
|
@ -313,23 +303,29 @@ class TrezorCompatiblePlugin(HW_PluginBase):
|
|||
|
||||
return inputs
|
||||
|
||||
def tx_outputs(self, wallet, tx):
|
||||
def tx_outputs(self, derivation, tx):
|
||||
outputs = []
|
||||
for type, address, amount in tx.outputs():
|
||||
assert type == TYPE_ADDRESS
|
||||
for i, (_type, address, amount) in enumerate(tx.outputs()):
|
||||
txoutputtype = self.types.TxOutputType()
|
||||
if wallet.is_change(address):
|
||||
address_path = wallet.address_id(address)
|
||||
address_n = self.client_class.expand_path(address_path)
|
||||
txoutputtype.address_n.extend(address_n)
|
||||
else:
|
||||
txoutputtype.address = address
|
||||
txoutputtype.amount = amount
|
||||
addrtype, hash_160 = bc_address_to_hash_160(address)
|
||||
if addrtype == 0:
|
||||
txoutputtype.script_type = self.types.PAYTOADDRESS
|
||||
elif addrtype == 5:
|
||||
txoutputtype.script_type = self.types.PAYTOSCRIPTHASH
|
||||
change, index = tx.output_info[i]
|
||||
if _type == TYPE_SCRIPT:
|
||||
txoutputtype.script_type = self.types.PAYTOOPRETURN
|
||||
txoutputtype.op_return_data = address[2:]
|
||||
elif _type == TYPE_ADDRESS:
|
||||
if change is not None:
|
||||
address_path = "%s/%d/%d/"%(derivation, change, index)
|
||||
address_n = self.client_class.expand_path(address_path)
|
||||
txoutputtype.address_n.extend(address_n)
|
||||
else:
|
||||
txoutputtype.address = address
|
||||
addrtype, hash_160 = bc_address_to_hash_160(address)
|
||||
if addrtype == 0:
|
||||
txoutputtype.script_type = self.types.PAYTOADDRESS
|
||||
elif addrtype == 5:
|
||||
txoutputtype.script_type = self.types.PAYTOSCRIPTHASH
|
||||
else:
|
||||
raise BaseException('addrtype')
|
||||
else:
|
||||
raise BaseException('addrtype')
|
||||
outputs.append(txoutputtype)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -34,10 +34,11 @@ from urllib import quote
|
|||
|
||||
import electrum
|
||||
from electrum import bitcoin
|
||||
from electrum import keystore
|
||||
from electrum.bitcoin import *
|
||||
from electrum.mnemonic import Mnemonic
|
||||
from electrum import version
|
||||
from electrum.wallet import Multisig_Wallet, BIP32_Wallet
|
||||
from electrum.wallet import Multisig_Wallet, Deterministic_Wallet, Wallet
|
||||
from electrum.i18n import _
|
||||
from electrum.plugins import BasePlugin, run_hook, hook
|
||||
from electrum.util import NotEnoughFunds
|
||||
|
@ -187,29 +188,16 @@ server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VER
|
|||
class Wallet_2fa(Multisig_Wallet):
|
||||
|
||||
def __init__(self, storage):
|
||||
BIP32_Wallet.__init__(self, storage)
|
||||
self.wallet_type = '2fa'
|
||||
self.m = 2
|
||||
self.n = 3
|
||||
self.m, self.n = 2, 3
|
||||
Deterministic_Wallet.__init__(self, storage)
|
||||
self.is_billing = False
|
||||
self.billing_info = None
|
||||
|
||||
def get_action(self):
|
||||
xpub1 = self.master_public_keys.get("x1/")
|
||||
xpub2 = self.master_public_keys.get("x2/")
|
||||
xpub3 = self.master_public_keys.get("x3/")
|
||||
if xpub2 is None and not self.storage.get('use_trustedcoin'):
|
||||
return 'show_disclaimer'
|
||||
if xpub2 is None:
|
||||
return 'create_extended_seed'
|
||||
if xpub3 is None:
|
||||
return 'create_remote_key'
|
||||
|
||||
def make_seed(self):
|
||||
return Mnemonic('english').make_seed(num_bits=256, prefix=SEED_PREFIX)
|
||||
|
||||
def can_sign_without_server(self):
|
||||
return self.master_private_keys.get('x2/') is not None
|
||||
return not self.keystores.get('x2/').is_watching_only()
|
||||
|
||||
def get_user_id(self):
|
||||
return get_user_id(self.storage)
|
||||
|
||||
def get_max_amount(self, config, inputs, recipient, fee):
|
||||
from electrum.transaction import Transaction
|
||||
|
@ -244,7 +232,7 @@ class Wallet_2fa(Multisig_Wallet):
|
|||
|
||||
def make_unsigned_transaction(self, coins, outputs, config,
|
||||
fixed_fee=None, change_addr=None):
|
||||
mk_tx = lambda o: BIP32_Wallet.make_unsigned_transaction(
|
||||
mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction(
|
||||
self, coins, o, config, fixed_fee, change_addr)
|
||||
fee = self.extra_fee()
|
||||
if fee:
|
||||
|
@ -264,7 +252,7 @@ class Wallet_2fa(Multisig_Wallet):
|
|||
return tx
|
||||
|
||||
def sign_transaction(self, tx, password):
|
||||
BIP32_Wallet.sign_transaction(self, tx, password)
|
||||
Multisig_Wallet.sign_transaction(self, tx, password)
|
||||
if tx.is_complete():
|
||||
return
|
||||
if not self.auth_code:
|
||||
|
@ -279,27 +267,25 @@ class Wallet_2fa(Multisig_Wallet):
|
|||
tx.update(raw_tx)
|
||||
self.print_error("twofactor: is complete", tx.is_complete())
|
||||
|
||||
def get_user_id(self):
|
||||
def make_long_id(xpub_hot, xpub_cold):
|
||||
return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold])))
|
||||
xpub_hot = self.master_public_keys["x1/"]
|
||||
xpub_cold = self.master_public_keys["x2/"]
|
||||
long_id = make_long_id(xpub_hot, xpub_cold)
|
||||
short_id = hashlib.sha256(long_id).hexdigest()
|
||||
return long_id, short_id
|
||||
|
||||
# Utility functions
|
||||
|
||||
def get_user_id(storage):
|
||||
def make_long_id(xpub_hot, xpub_cold):
|
||||
return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold])))
|
||||
mpk = storage.get('master_public_keys')
|
||||
xpub1 = mpk["x1/"]
|
||||
xpub2 = mpk["x2/"]
|
||||
long_id = make_long_id(xpub1, xpub2)
|
||||
short_id = hashlib.sha256(long_id).hexdigest()
|
||||
return long_id, short_id
|
||||
|
||||
def make_xpub(xpub, s):
|
||||
_, _, _, c, cK = deserialize_xkey(xpub)
|
||||
cK2, c2 = bitcoin._CKD_pub(cK, c, s)
|
||||
xpub2 = ("0488B21E" + "00" + "00000000" + "00000000").decode("hex") + c2 + cK2
|
||||
return EncodeBase58Check(xpub2)
|
||||
|
||||
def restore_third_key(wallet):
|
||||
long_user_id, short_id = wallet.get_user_id()
|
||||
xpub3 = make_xpub(signing_xpub, long_user_id)
|
||||
wallet.add_master_public_key('x3/', xpub3)
|
||||
|
||||
def make_billing_address(wallet, num):
|
||||
long_id, short_id = wallet.get_user_id()
|
||||
|
@ -324,9 +310,6 @@ class TrustedCoinPlugin(BasePlugin):
|
|||
def is_available(self):
|
||||
return True
|
||||
|
||||
def set_enabled(self, wallet, enabled):
|
||||
wallet.storage.put('use_' + self.name, enabled)
|
||||
|
||||
def is_enabled(self):
|
||||
return True
|
||||
|
||||
|
@ -345,28 +328,42 @@ class TrustedCoinPlugin(BasePlugin):
|
|||
wallet.price_per_tx = dict(billing_info['price_per_tx'])
|
||||
return True
|
||||
|
||||
def create_extended_seed(self, wallet, wizard):
|
||||
self.wallet = wallet
|
||||
self.wizard = wizard
|
||||
seed = wallet.make_seed()
|
||||
self.wizard.show_seed_dialog(run_next=wizard.confirm_seed, seed_text=seed)
|
||||
def make_seed(self):
|
||||
return Mnemonic('english').make_seed(num_bits=256, prefix=SEED_PREFIX)
|
||||
|
||||
def show_disclaimer(self, wallet, wizard):
|
||||
self.set_enabled(wallet, True)
|
||||
@hook
|
||||
def do_clear(self, window):
|
||||
window.wallet.is_billing = False
|
||||
|
||||
def show_disclaimer(self, wizard):
|
||||
wizard.set_icon(':icons/trustedcoin.png')
|
||||
wizard.stack = []
|
||||
wizard.confirm_dialog('\n\n'.join(DISCLAIMER), run_next = lambda x: wizard.run('create_extended_seed'))
|
||||
wizard.confirm_dialog('\n\n'.join(DISCLAIMER), run_next = lambda x: wizard.run('choose_seed'))
|
||||
|
||||
def create_wallet(self, wallet, wizard, seed, password):
|
||||
wallet.storage.put('seed_version', wallet.seed_version)
|
||||
wallet.storage.put('use_encryption', password is not None)
|
||||
def choose_seed(self, wizard):
|
||||
title = _('Create or restore')
|
||||
message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
|
||||
choices = [
|
||||
('create_seed', _('Create a new seed')),
|
||||
('restore_wallet', _('I already have a seed')),
|
||||
]
|
||||
wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run)
|
||||
|
||||
def create_seed(self, wizard):
|
||||
seed = self.make_seed()
|
||||
wizard.show_seed_dialog(run_next=wizard.confirm_seed, seed_text=seed)
|
||||
|
||||
def create_keystore(self, wizard, seed, password):
|
||||
# this overloads the wizard's method
|
||||
words = seed.split()
|
||||
n = len(words)/2
|
||||
wallet.add_xprv_from_seed(' '.join(words[0:n]), 'x1/', password)
|
||||
wallet.add_xpub_from_seed(' '.join(words[n:]), 'x2/')
|
||||
wallet.storage.write()
|
||||
keystore1 = keystore.xprv_from_seed(' '.join(words[0:n]), password)
|
||||
keystore2 = keystore.xpub_from_seed(' '.join(words[n:]))
|
||||
keystore1.save(wizard.storage, 'x1/')
|
||||
keystore2.save(wizard.storage, 'x2/')
|
||||
wizard.storage.write()
|
||||
msg = [
|
||||
_("Your wallet file is: %s.")%os.path.abspath(wallet.storage.path),
|
||||
_("Your wallet file is: %s.")%os.path.abspath(wizard.storage.path),
|
||||
_("You need to be online in order to complete the creation of "
|
||||
"your wallet. If you generated your seed on an offline "
|
||||
'computer, click on "%s" to close this window, move your '
|
||||
|
@ -378,41 +375,45 @@ class TrustedCoinPlugin(BasePlugin):
|
|||
wizard.stack = []
|
||||
wizard.confirm_dialog(msg, run_next = lambda x: wizard.run('create_remote_key'))
|
||||
|
||||
@hook
|
||||
def do_clear(self, window):
|
||||
window.wallet.is_billing = False
|
||||
|
||||
def on_restore_wallet(self, wallet, wizard):
|
||||
assert isinstance(wallet, self.wallet_class)
|
||||
def restore_wallet(self, wizard):
|
||||
title = _("Restore two-factor Wallet")
|
||||
f = lambda x: wizard.run('on_restore_seed', x)
|
||||
wizard.enter_seed_dialog(run_next=f, title=title, message=RESTORE_MSG, is_valid=self.is_valid_seed)
|
||||
wizard.restore_seed_dialog(run_next=f, is_valid=self.is_valid_seed)
|
||||
|
||||
def on_restore_seed(self, wallet, wizard, seed):
|
||||
f = lambda x: wizard.run('on_restore_pw', seed, x)
|
||||
def on_restore_seed(self, wizard, seed):
|
||||
f = lambda pw: wizard.run('on_restore_pw', seed, pw)
|
||||
wizard.request_password(run_next=f)
|
||||
|
||||
def on_restore_pw(self, wallet, wizard, seed, password):
|
||||
wallet.add_seed(seed, password)
|
||||
def on_restore_pw(self, wizard, seed, password):
|
||||
# FIXME
|
||||
# wallet.add_seed(seed, password)
|
||||
storage = wizard.storage
|
||||
words = seed.split()
|
||||
n = len(words)/2
|
||||
wallet.add_xprv_from_seed(' '.join(words[0:n]), 'x1/', password)
|
||||
wallet.add_xprv_from_seed(' '.join(words[n:]), 'x2/', password)
|
||||
restore_third_key(wallet)
|
||||
keystore1 = keystore.xprv_from_seed(' '.join(words[0:n]), password)
|
||||
keystore2 = keystore.xprv_from_seed(' '.join(words[n:]), password)
|
||||
keystore1.save(storage, 'x1/')
|
||||
keystore2.save(storage, 'x2/')
|
||||
long_user_id, short_id = get_user_id(storage)
|
||||
xpub3 = make_xpub(signing_xpub, long_user_id)
|
||||
keystore3 = keystore.from_xpub(xpub3)
|
||||
keystore3.save(storage, 'x3/')
|
||||
wizard.wallet = Wallet(storage)
|
||||
wizard.create_addresses()
|
||||
|
||||
def create_remote_key(self, wallet, window):
|
||||
email = self.accept_terms_of_use(window)
|
||||
xpub_hot = wallet.master_public_keys["x1/"]
|
||||
xpub_cold = wallet.master_public_keys["x2/"]
|
||||
def create_remote_key(self, wizard):
|
||||
email = self.accept_terms_of_use(wizard)
|
||||
mpk = wizard.storage.get('master_public_keys')
|
||||
xpub1 = mpk["x1/"]
|
||||
xpub2 = mpk["x2/"]
|
||||
# Generate third key deterministically.
|
||||
long_user_id, short_id = wallet.get_user_id()
|
||||
long_user_id, short_id = get_user_id(wizard.storage)
|
||||
xpub3 = make_xpub(signing_xpub, long_user_id)
|
||||
# secret must be sent by the server
|
||||
try:
|
||||
r = server.create(xpub_hot, xpub_cold, email)
|
||||
r = server.create(xpub1, xpub2, email)
|
||||
except socket.error:
|
||||
window.show_message('Server not reachable, aborting')
|
||||
wizard.show_message('Server not reachable, aborting')
|
||||
return
|
||||
except TrustedCoinException as e:
|
||||
if e.status_code == 409:
|
||||
|
@ -424,7 +425,7 @@ class TrustedCoinPlugin(BasePlugin):
|
|||
else:
|
||||
otp_secret = r.get('otp_secret')
|
||||
if not otp_secret:
|
||||
window.show_message(_('Error'))
|
||||
wizard.show_message(_('Error'))
|
||||
return
|
||||
_xpub3 = r['xpubkey_cosigner']
|
||||
_id = r['id']
|
||||
|
@ -432,10 +433,24 @@ class TrustedCoinPlugin(BasePlugin):
|
|||
assert _id == short_id, ("user id error", _id, short_id)
|
||||
assert xpub3 == _xpub3, ("xpub3 error", xpub3, _xpub3)
|
||||
except Exception as e:
|
||||
window.show_message(str(e))
|
||||
wizard.show_message(str(e))
|
||||
return
|
||||
if not self.setup_google_auth(window, short_id, otp_secret):
|
||||
window.show_message("otp error")
|
||||
if not self.setup_google_auth(wizard, short_id, otp_secret):
|
||||
wizard.show_message("otp error")
|
||||
return
|
||||
wallet.add_master_public_key('x3/', xpub3)
|
||||
window.run('create_addresses')
|
||||
keystore3 = keystore.from_xpub(xpub3)
|
||||
keystore3.save(wizard.storage, 'x3/')
|
||||
wizard.storage.put('use_trustedcoin', True)
|
||||
wizard.storage.write()
|
||||
wizard.wallet = Wallet(wizard.storage)
|
||||
wizard.run('create_addresses')
|
||||
|
||||
@hook
|
||||
def get_action(self, storage):
|
||||
mpk = storage.get('master_public_keys', {})
|
||||
if not mpk.get('x1/'):
|
||||
return self, 'show_disclaimer'
|
||||
if not mpk.get('x2/'):
|
||||
return self, 'show_disclaimer'
|
||||
if not mpk.get('x3/'):
|
||||
return self, 'create_remote_key'
|
||||
|
|
Loading…
Reference in New Issue